from __future__ import annotations import html from fastapi import FastAPI from fastapi.responses import HTMLResponse from ..layout.page import page def register(app: FastAPI) -> None: @app.get("/ui/tasks/new") async def ui_new_task() -> HTMLResponse: ppo = """# PPO TaskSpec (YAML) workload: ppo # 任务类型(必填):ppo|grpo|sft code_path: /private/common/code/verl/verl_repo # 代码路径(必填):v3.0 固定使用 common 下的 verl 快照(不支持用户自定义代码) model_id: Qwen/Qwen2.5-0.5B-Instruct # 基础模型(必填):HuggingFace 模型 ID 或 /private/... 本地模型路径 train_file: /private/common/datasets/gsm8k/train.parquet # 训练数据(必填):parquet 文件路径(支持 /private/common/datasets 或 /private/users//datasets) val_file: /private/common/datasets/gsm8k/test.parquet # 验证数据(必填):parquet 文件路径(VERL 侧会用来构建 val dataset,不能为 null) # nnodes: 2 # 训练节点数(可选,默认:2) # n_gpus_per_node: 4 # 每节点 GPU 数(可选,默认:4) # total_epochs: 1 # 总训练 epoch(可选,默认:1) # total_training_steps: null # 总训练 step(可选,默认:null;不传则让 VERL 按 epochs 和数据长度自动推导) # save_freq: 10 # checkpoint 保存频率(step)(可选,默认:10) # test_freq: null # 验证频率(step)(可选,默认:null;训练端会当成 -1=不验证) # submission_id: "" # Ray submission_id(可选,默认空;通常由服务自动生成,无需填写) """.strip() grpo = """# GRPO TaskSpec (YAML) workload: grpo # 任务类型(必填):ppo|grpo|sft(grpo 会自动启用对应的算法配置) code_path: /private/common/code/verl/verl_repo # 代码路径(必填):v3.0 固定使用 common 下的 verl 快照(不支持用户自定义代码) model_id: Qwen/Qwen2.5-0.5B-Instruct # 基础模型(必填):HuggingFace 模型 ID 或 /private/... 本地模型路径 train_file: /private/common/datasets/gsm8k/train.parquet # 训练数据(必填):parquet 文件路径(支持 /private/common/datasets 或 /private/users//datasets) val_file: /private/common/datasets/gsm8k/test.parquet # 验证数据(必填):parquet 文件路径(VERL 侧会用来构建 val dataset,不能为 null) # nnodes: 2 # 训练节点数(可选,默认:2) # n_gpus_per_node: 4 # 每节点 GPU 数(可选,默认:4) # total_epochs: 1 # 总训练 epoch(可选,默认:1) # total_training_steps: null # 总训练 step(可选,默认:null;不传则让 VERL 按 epochs 和数据长度自动推导) # save_freq: 10 # checkpoint 保存频率(step)(可选,默认:10) # test_freq: null # 验证频率(step)(可选,默认:null;训练端会当成 -1=不验证) # submission_id: "" # Ray submission_id(可选,默认空;通常由服务自动生成,无需填写) """.strip() sft = """# SFT TaskSpec (YAML) workload: sft # 任务类型(必填):ppo|grpo|sft code_path: /private/common/code/verl/verl_repo # 代码路径(必填):v3.0 固定使用 common 下的 verl 快照(不支持用户自定义代码) model_id: Qwen/Qwen2.5-0.5B-Instruct # 基础模型(必填):HuggingFace 模型 ID 或 /private/... 本地模型路径 train_file: /private/common/datasets/gsm8k_sft/train.parquet # 训练数据(必填):parquet 文件路径(支持 /private/common/datasets 或 /private/users//datasets) val_file: /private/common/datasets/gsm8k_sft/test.parquet # 验证数据(必填):parquet 文件路径(VERL 侧会用来构建 val dataset,不能为 null) # nnodes: 2 # 训练节点数(可选,默认:2;单机可设 1) # n_gpus_per_node: 4 # 每节点 GPU 数(可选,默认:4;单卡可设 1) # total_epochs: 1 # 总训练 epoch(可选,默认:1) # total_training_steps: null # 总训练 step(可选,默认:null;不传则让 VERL 按 epochs 和数据长度自动推导) # save_freq: 10 # checkpoint 保存频率(step)(可选,默认:10) # test_freq: null # 验证频率(step)(可选,默认:null;训练端会当成 -1=不验证) # trainer_device: cpu # 仅 SFT 生效:driver 侧 device(可选,默认:cpu) # submission_id: "" # Ray submission_id(可选,默认空;通常由服务自动生成,无需填写) """.strip() adv = """# Advanced TaskSpec (YAML) - v3.6 kind: advanced # 任务类型(必填):advanced(自定义 command) # 说明:平台统一按 "advanced" 做任务分类与 task_id 命名(不按 ppo/grpo/sft 细分)。 # # 自定义训练命令:平台会做 $HOME 宏替换: # - $HOME -> /private/users/ # - $HOME/common/datasets -> /private/datasets(共享只读数据) # - $HOME/common/hf -> /private/hf(共享只读 HF cache) # # W&B(v3.6): # - 平台会在 runtime_env 注入 WANDB_BASE_URL/WANDB_API_KEY/WANDB_DIR # - 以及注入以下 env,供 Advanced command 使用(无需用户手动修改 project): # - MVP_TRAINER_LOGGER: "console" 或 "[console,wandb]" # - MVP_WANDB_PROJECT: "_project" # - MVP_WANDB_RUN: "" # nnodes: 2 # 训练节点数(必填):用于平台队列调度与资源预检查 n_gpus_per_node: 4 # 每节点 GPU 数(必填):用于平台队列调度与资源预检查 command: | PYTHONUNBUFFERED=1 \ python3 -m verl.trainer.main_ppo \ data.train_files=$HOME/common/datasets/gsm8k/train.parquet \ data.val_files=$HOME/common/datasets/gsm8k/test.parquet \ data.train_batch_size=256 \ data.max_prompt_length=512 \ data.max_response_length=512 \ actor_rollout_ref.model.path=Qwen/Qwen2.5-0.5B-Instruct \ actor_rollout_ref.actor.optim.lr=1e-6 \ actor_rollout_ref.actor.ppo_mini_batch_size=64 \ actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=4 \ actor_rollout_ref.rollout.name=vllm \ actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \ actor_rollout_ref.rollout.tensor_model_parallel_size=1 \ actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \ actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=4 \ critic.optim.lr=1e-5 \ critic.model.path=Qwen/Qwen2.5-0.5B-Instruct \ critic.ppo_micro_batch_size_per_gpu=4 \ algorithm.kl_ctrl.kl_coef=0.001 \ trainer.logger=${MVP_TRAINER_LOGGER} \ trainer.project_name=${MVP_WANDB_PROJECT} \ trainer.experiment_name=${MVP_WANDB_RUN} \ trainer.val_before_train=False \ trainer.nnodes=2 \ trainer.n_gpus_per_node=4 \ trainer.total_epochs=1 \ trainer.total_training_steps=10 \ trainer.save_freq=10 \ trainer.test_freq=-1 \ trainer.resume_mode=disable \ trainer.default_local_dir=checkpoints \ +ray_kwargs.ray_init.address=auto \ hydra.run.dir=logs/hydra # 可选:自定义 reward(方式 A:直接写在 command 里) # command 里增加如下 overrides: # custom_reward_function.path=$HOME/code/reward.py # custom_reward_function.name=compute_score """.strip() merge = """# Model Merge (YAML) - v3.5 (Advanced command) # 用途:将 VERL 训练产生的 FSDP 分片 checkpoint 合并为 HuggingFace 格式目录。 # # 你需要把 和 替换成真实值: # - submission id:在 Tasks 详情页里看到的 `ray_submission_id`(如 mvp2-...--a01) # - global_step:对应 checkpoints 下的 global_step_xxx 目录(如 global_step_10) # # 注意:这里不需要 GPU,建议把 n_gpus_per_node 设为 0,这样不受训练任务占用 GPU 的影响。 kind: advanced nnodes: 1 n_gpus_per_node: 0 command: | python3 -m verl.model_merger merge \ --backend fsdp \ --local_dir $HOME/jobs//checkpoints//actor \ --target_dir $HOME/jobs//checkpoints//actor/huggingface """.strip() evaluation = """# Evaluation (YAML) - v3.6 (Advanced command) # 用途:对“已生成的结果 parquet”做离线评估(基于 custom reward function),示例使用 VERL 内置 main_eval。 # # 说明: # - 你需要准备一个包含生成 responses 的 parquet,并替换 (示例放在某个 job 目录下)。 # - main_eval 会在 Ray 上并发计算,因此这里也用 +ray_kwargs 连接已有 Ray 集群。 # kind: advanced nnodes: 1 n_gpus_per_node: 0 command: | PYTHONUNBUFFERED=1 \ python3 -m verl.trainer.main_eval \ data.path=$HOME/jobs//outputs/.parquet \ +ray_kwargs.ray_init.address=auto # 可选:指定 parquet schema(按你的 parquet 列名调整) # data.response_key=responses # data.data_source_key=data_source # data.reward_model_key=reward_model # # 可选:自定义 reward(方式 A:用户自行提供 reward.py) # custom_reward_function.path=$HOME/code/reward.py # custom_reward_function.name=compute_score """.strip() body = f"""

New Task

Paste TaskSpec YAML and submit to API server. Basic tasks require code_path; Advanced tasks use kind: advanced with a custom command.
Back

""".strip() tpl_ppo = ppo.replace("\n", "\\n").replace('"', '\\"') tpl_grpo = grpo.replace("\n", "\\n").replace('"', '\\"') tpl_sft = sft.replace("\n", "\\n").replace('"', '\\"') tpl_adv = adv.replace("\n", "\\n").replace('"', '\\"') tpl_eval = evaluation.replace("\n", "\\n").replace('"', '\\"') tpl_merge = merge.replace("\n", "\\n").replace('"', '\\"') script = ( """ document.getElementById("nav-ray-dashboard").href = curOriginWithPort(8265); const yamlEl = document.getElementById("yaml"); const msg = document.getElementById("msg"); const TPL_PPO = "__TPL_PPO__"; const TPL_GRPO = "__TPL_GRPO__"; const TPL_SFT = "__TPL_SFT__"; const TPL_ADV = "__TPL_ADV__"; const TPL_EVAL = "__TPL_EVAL__"; const TPL_MERGE = "__TPL_MERGE__"; function showMode(mode) { const a = document.getElementById("mode-yaml"); const b = document.getElementById("mode-form"); const py = document.getElementById("panel-yaml"); const pf = document.getElementById("panel-form"); if (mode === "yaml") { a.classList.add("active"); b.classList.remove("active"); py.style.display = ""; pf.style.display = "none"; } else { b.classList.add("active"); a.classList.remove("active"); pf.style.display = ""; py.style.display = "none"; } } function yamlQuote(v) { v = String(v || ""); if (!v) return '""'; if (/^[A-Za-z0-9_./:-]+$/.test(v) && !v.includes("#")) return v; return '"' + v.replaceAll("\\\\","\\\\\\\\").replaceAll('"','\\\\"') + '"'; } function updateFormVisibility() { const tpl = document.getElementById("f-tpl").value; const basic = document.getElementById("f-basic"); const adv = document.getElementById("f-adv"); const sftDev = document.getElementById("f-sft-device-wrap"); if (tpl === "advanced" || tpl === "merge") { basic.style.display = "none"; adv.style.display = ""; sftDev.style.display = "none"; } else { basic.style.display = ""; adv.style.display = "none"; sftDev.style.display = (tpl === "sft") ? "" : "none"; } } function updatePreview() { const tpl = document.getElementById("f-tpl").value; if (tpl === "advanced" || tpl === "merge") { const nnodes = Number(document.getElementById("f-adv-nnodes").value || "1") || 1; const gpn = Number(document.getElementById("f-adv-gpn").value || "0") || 0; const cmd = (document.getElementById("f-command").value || "").trimEnd(); const out = `kind: advanced\\n` + `nnodes: ${nnodes}\\n` + `n_gpus_per_node: ${gpn}\\n\\n` + `command: |\\n` + cmd.split("\\n").map(line => " " + line).join("\\n") + "\\n"; return out; } const workload = tpl; const codePath = document.getElementById("f-code-path").value || ""; const modelId = document.getElementById("f-model-id").value || ""; const trainFile = document.getElementById("f-train-file").value || ""; const valFile = document.getElementById("f-val-file").value || ""; const nnodes = Number(document.getElementById("f-nnodes").value || "2") || 2; const gpn = Number(document.getElementById("f-gpn").value || "4") || 4; const epochs = Number(document.getElementById("f-epochs").value || "1") || 1; const steps = (document.getElementById("f-steps").value || "").trim(); const saveFreq = Number(document.getElementById("f-save-freq").value || "10") || 10; const testFreq = (document.getElementById("f-test-freq").value || "").trim(); const trainerDevice = document.getElementById("f-trainer-device").value || "cpu"; let y = ""; y += `workload: ${workload}\\n`; y += `code_path: ${yamlQuote(codePath)}\\n`; y += `model_id: ${yamlQuote(modelId)}\\n`; y += `train_file: ${yamlQuote(trainFile)}\\n`; y += `val_file: ${yamlQuote(valFile)}\\n`; y += `nnodes: ${nnodes}\\n`; y += `n_gpus_per_node: ${gpn}\\n`; y += `total_epochs: ${epochs}\\n`; if (steps) y += `total_training_steps: ${Number(steps)}\\n`; y += `save_freq: ${saveFreq}\\n`; if (testFreq) y += `test_freq: ${Number(testFreq)}\\n`; if (workload === "sft") y += `trainer_device: ${trainerDevice}\\n`; return y; } function bindStepper(inputId, decId, incId, min) { const inp = document.getElementById(inputId); document.getElementById(decId).onclick = () => { inp.value = String(Math.max(min, (Number(inp.value || "0") || 0) - 1)); inp.dispatchEvent(new Event("input")); }; document.getElementById(incId).onclick = () => { inp.value = String((Number(inp.value || "0") || 0) + 1); inp.dispatchEvent(new Event("input")); }; } document.getElementById("tpl-ppo").onclick = () => { yamlEl.value = TPL_PPO; }; document.getElementById("tpl-grpo").onclick = () => { yamlEl.value = TPL_GRPO; }; document.getElementById("tpl-sft").onclick = () => { yamlEl.value = TPL_SFT; }; document.getElementById("tpl-adv").onclick = () => { yamlEl.value = TPL_ADV; }; document.getElementById("tpl-eval").onclick = () => { yamlEl.value = TPL_EVAL; }; document.getElementById("tpl-merge").onclick = () => { yamlEl.value = TPL_MERGE; }; document.getElementById("f-code-path").value = "/private/common/code/verl/verl_repo"; document.getElementById("f-model-id").value = "Qwen/Qwen2.5-0.5B-Instruct"; document.getElementById("f-train-file").value = "/private/common/datasets/gsm8k/train.parquet"; document.getElementById("f-val-file").value = "/private/common/datasets/gsm8k/test.parquet"; document.getElementById("f-nnodes").value = "2"; document.getElementById("f-gpn").value = "4"; document.getElementById("f-epochs").value = "1"; document.getElementById("f-save-freq").value = "10"; document.getElementById("f-trainer-device").value = "cpu"; document.getElementById("f-adv-nnodes").value = "2"; document.getElementById("f-adv-gpn").value = "4"; document.getElementById("f-command").value = `PYTHONUNBUFFERED=1 \\\\\\npython3 -m verl.trainer.main_ppo \\\\\\n data.train_files=$HOME/common/datasets/gsm8k/train.parquet \\\\\\n data.val_files=$HOME/common/datasets/gsm8k/test.parquet \\\\\\n +ray_kwargs.ray_init.address=auto`; bindStepper("f-nnodes", "f-nnodes-dec", "f-nnodes-inc", 1); bindStepper("f-gpn", "f-gpn-dec", "f-gpn-inc", 0); bindStepper("f-epochs", "f-epochs-dec", "f-epochs-inc", 1); bindStepper("f-adv-nnodes", "f-adv-nnodes-dec", "f-adv-nnodes-inc", 1); bindStepper("f-adv-gpn", "f-adv-gpn-dec", "f-adv-gpn-inc", 0); document.getElementById("f-tpl").onchange = () => { const tpl = document.getElementById("f-tpl").value; if (tpl === "ppo") { document.getElementById("f-train-file").value = "/private/common/datasets/gsm8k/train.parquet"; document.getElementById("f-val-file").value = "/private/common/datasets/gsm8k/test.parquet"; } else if (tpl === "grpo") { document.getElementById("f-train-file").value = "/private/common/datasets/gsm8k/train.parquet"; document.getElementById("f-val-file").value = "/private/common/datasets/gsm8k/test.parquet"; } else if (tpl === "sft") { document.getElementById("f-train-file").value = "/private/common/datasets/gsm8k_sft/train.parquet"; document.getElementById("f-val-file").value = "/private/common/datasets/gsm8k_sft/test.parquet"; } else if (tpl === "advanced") { document.getElementById("f-adv-nnodes").value = "2"; document.getElementById("f-adv-gpn").value = "4"; document.getElementById("f-command").value = TPL_ADV.split("command: |")[1]?.trimStart() || document.getElementById("f-command").value; } else if (tpl === "merge") { document.getElementById("f-adv-nnodes").value = "1"; document.getElementById("f-adv-gpn").value = "0"; document.getElementById("f-command").value = TPL_MERGE.split("command: |")[1]?.trimStart() || document.getElementById("f-command").value; } updateFormVisibility(); updatePreview(); }; for (const id of ["f-code-path","f-model-id","f-train-file","f-val-file","f-nnodes","f-gpn","f-epochs","f-steps","f-save-freq","f-test-freq","f-trainer-device","f-adv-nnodes","f-adv-gpn","f-command"]) { const el = document.getElementById(id); if (el) el.oninput = () => updatePreview(); } document.getElementById("mode-yaml").onclick = () => { // When switching to YAML, sync from form if form is visible. const formVisible = document.getElementById("panel-form").style.display !== "none"; if (formVisible) yamlEl.value = updatePreview(); showMode("yaml"); }; document.getElementById("mode-form").onclick = () => { showMode("form"); }; showMode("yaml"); document.getElementById("submit").onclick = async () => { msg.textContent = "Submitting..."; const body = (document.getElementById("panel-form").style.display !== "none") ? updatePreview() : yamlEl.value; const resp = await apiFetch("/api/v2/tasks", { method: "POST", headers: {"Content-Type":"text/plain"}, body }); const text = await resp.text(); if (!resp.ok) { msg.textContent = "Error: " + resp.status + "\\n" + text; return; } const obj = JSON.parse(text); msg.textContent = "OK: " + fmtJson(obj); if (obj.task_id) window.location.href = "/ui/tasks/" + obj.task_id; }; """.strip() .replace("__TPL_PPO__", tpl_ppo) .replace("__TPL_GRPO__", tpl_grpo) .replace("__TPL_SFT__", tpl_sft) .replace("__TPL_ADV__", tpl_adv) .replace("__TPL_EVAL__", tpl_eval) .replace("__TPL_MERGE__", tpl_merge) ) return HTMLResponse(content=page("New Task", "new", body, script))