539 lines
25 KiB
Python
539 lines
25 KiB
Python
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/<user>/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/<user>/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/<user>/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/<user>
|
||
# - $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: "<user_id>_project"
|
||
# - MVP_WANDB_RUN: "<ray_submission_id>"
|
||
#
|
||
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 格式目录。
|
||
#
|
||
# 你需要把 <RAY_SUBMISSION_ID> 和 <GLOBAL_STEP> 替换成真实值:
|
||
# - 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/<RAY_SUBMISSION_ID>/checkpoints/<GLOBAL_STEP>/actor \
|
||
--target_dir $HOME/jobs/<RAY_SUBMISSION_ID>/checkpoints/<GLOBAL_STEP>/actor/huggingface
|
||
""".strip()
|
||
evaluation = """# Evaluation (YAML) - v3.6 (Advanced command)
|
||
# 用途:对“已生成的结果 parquet”做离线评估(基于 custom reward function),示例使用 VERL 内置 main_eval。
|
||
#
|
||
# 说明:
|
||
# - 你需要准备一个包含生成 responses 的 parquet,并替换 <EVAL_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/<RAY_SUBMISSION_ID>/outputs/<EVAL_PARQUET>.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"""
|
||
<h1>New Task</h1>
|
||
<div class="card">
|
||
<div class="muted">
|
||
Paste TaskSpec YAML and submit to API server.
|
||
Basic tasks require <code>code_path</code>; Advanced tasks use <code>kind: advanced</code> with a custom <code>command</code>.
|
||
</div>
|
||
<div style="height:10px"></div>
|
||
<div class="row">
|
||
<button class="btn" id="mode-yaml">YAML 模式</button>
|
||
<button class="btn" id="mode-form">表单模式</button>
|
||
</div>
|
||
<div style="height:10px"></div>
|
||
|
||
<div id="panel-yaml">
|
||
<div class="row">
|
||
<button class="btn" id="tpl-ppo">PPO example</button>
|
||
<button class="btn" id="tpl-grpo">GRPO example</button>
|
||
<button class="btn" id="tpl-sft">SFT example</button>
|
||
<button class="btn" id="tpl-adv">Advanced example</button>
|
||
<button class="btn" id="tpl-eval">Evaluation example</button>
|
||
<button class="btn" id="tpl-merge">Model merge example</button>
|
||
</div>
|
||
<div style="height:10px"></div>
|
||
<textarea id="yaml" rows="16">{html.escape(ppo)}</textarea>
|
||
</div>
|
||
|
||
<div id="panel-form" style="display:none">
|
||
<div class="row">
|
||
<div style="min-width:240px">
|
||
<div class="muted">模板</div>
|
||
<div style="height:6px"></div>
|
||
<select id="f-tpl" style="width:240px; padding:10px 12px; border-radius: 12px; background: rgba(255,255,255,0.06); color: var(--fg); border: 1px solid rgba(255,255,255,0.12);">
|
||
<option value="ppo">PPO</option>
|
||
<option value="grpo">GRPO</option>
|
||
<option value="sft">SFT</option>
|
||
<option value="advanced">Advanced</option>
|
||
<option value="merge">Model Merge</option>
|
||
</select>
|
||
</div>
|
||
<div class="muted" style="flex:1; min-width:260px">
|
||
表单模式会生成 TaskSpec YAML 并用同一个 API 提交;可随时切回 YAML 模式查看/编辑生成结果。
|
||
</div>
|
||
</div>
|
||
|
||
<div style="height:14px"></div>
|
||
|
||
<div id="f-basic">
|
||
<div class="row">
|
||
<div style="flex:1; min-width:260px">
|
||
<div class="muted">code_path</div>
|
||
<div style="height:6px"></div>
|
||
<input id="f-code-path" />
|
||
</div>
|
||
<div style="flex:1; min-width:260px">
|
||
<div class="muted">model_id</div>
|
||
<div style="height:6px"></div>
|
||
<input id="f-model-id" />
|
||
</div>
|
||
</div>
|
||
<div style="height:10px"></div>
|
||
<div class="row">
|
||
<div style="flex:1; min-width:260px">
|
||
<div class="muted">train_file</div>
|
||
<div style="height:6px"></div>
|
||
<input id="f-train-file" />
|
||
</div>
|
||
<div style="flex:1; min-width:260px">
|
||
<div class="muted">val_file</div>
|
||
<div style="height:6px"></div>
|
||
<input id="f-val-file" />
|
||
</div>
|
||
</div>
|
||
<div style="height:10px"></div>
|
||
<div class="row">
|
||
<div style="min-width:220px">
|
||
<div class="muted">nnodes</div>
|
||
<div style="height:6px"></div>
|
||
<div class="row" style="gap:8px">
|
||
<button class="btn" id="f-nnodes-dec">-</button>
|
||
<input id="f-nnodes" type="number" min="1" step="1" style="width:120px" />
|
||
<button class="btn" id="f-nnodes-inc">+</button>
|
||
</div>
|
||
</div>
|
||
<div style="min-width:240px">
|
||
<div class="muted">n_gpus_per_node</div>
|
||
<div style="height:6px"></div>
|
||
<div class="row" style="gap:8px">
|
||
<button class="btn" id="f-gpn-dec">-</button>
|
||
<input id="f-gpn" type="number" min="0" step="1" style="width:120px" />
|
||
<button class="btn" id="f-gpn-inc">+</button>
|
||
</div>
|
||
</div>
|
||
<div style="min-width:220px">
|
||
<div class="muted">total_epochs</div>
|
||
<div style="height:6px"></div>
|
||
<div class="row" style="gap:8px">
|
||
<button class="btn" id="f-epochs-dec">-</button>
|
||
<input id="f-epochs" type="number" min="1" step="1" style="width:120px" />
|
||
<button class="btn" id="f-epochs-inc">+</button>
|
||
</div>
|
||
</div>
|
||
<div style="min-width:260px">
|
||
<div class="muted">total_training_steps(可选)</div>
|
||
<div style="height:6px"></div>
|
||
<input id="f-steps" type="number" min="1" step="1" placeholder="留空表示不传(让 VERL 自动推导)" />
|
||
</div>
|
||
</div>
|
||
<div style="height:10px"></div>
|
||
<div class="row">
|
||
<div style="min-width:220px">
|
||
<div class="muted">save_freq</div>
|
||
<div style="height:6px"></div>
|
||
<input id="f-save-freq" type="number" min="1" step="1" style="width:160px" />
|
||
</div>
|
||
<div style="min-width:260px">
|
||
<div class="muted">test_freq(可选)</div>
|
||
<div style="height:6px"></div>
|
||
<input id="f-test-freq" type="number" min="1" step="1" placeholder="留空表示不传(训练端会当成 -1=不验证)" />
|
||
</div>
|
||
<div id="f-sft-device-wrap" style="min-width:260px; display:none">
|
||
<div class="muted">trainer_device(仅 SFT)</div>
|
||
<div style="height:6px"></div>
|
||
<select id="f-trainer-device" style="width:200px; padding:10px 12px; border-radius: 12px; background: rgba(255,255,255,0.06); color: var(--fg); border: 1px solid rgba(255,255,255,0.12);">
|
||
<option value="cpu">cpu</option>
|
||
<option value="cuda">cuda</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="f-adv" style="display:none">
|
||
<div class="row">
|
||
<div style="min-width:220px">
|
||
<div class="muted">nnodes</div>
|
||
<div style="height:6px"></div>
|
||
<div class="row" style="gap:8px">
|
||
<button class="btn" id="f-adv-nnodes-dec">-</button>
|
||
<input id="f-adv-nnodes" type="number" min="1" step="1" style="width:120px" />
|
||
<button class="btn" id="f-adv-nnodes-inc">+</button>
|
||
</div>
|
||
</div>
|
||
<div style="min-width:240px">
|
||
<div class="muted">n_gpus_per_node</div>
|
||
<div style="height:6px"></div>
|
||
<div class="row" style="gap:8px">
|
||
<button class="btn" id="f-adv-gpn-dec">-</button>
|
||
<input id="f-adv-gpn" type="number" min="0" step="1" style="width:120px" />
|
||
<button class="btn" id="f-adv-gpn-inc">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="height:10px"></div>
|
||
<div class="muted">command</div>
|
||
<div style="height:6px"></div>
|
||
<textarea id="f-command" rows="10"></textarea>
|
||
<div style="height:10px"></div>
|
||
<div class="muted">
|
||
Tips:
|
||
Advanced 模式只允许执行 <code>python3 -m verl.*</code>。
|
||
如果你需要自定义 reward,可把 <code>reward.py</code> 上传到 <code>$HOME/code/</code> 并在 command 里引用。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="height:10px"></div>
|
||
<div class="row">
|
||
<button class="btn" id="submit">Submit</button>
|
||
<a class="btn" href="/ui/tasks" style="display:inline-block">Back</a>
|
||
</div>
|
||
<div style="height:10px"></div>
|
||
<pre id="msg" class="muted"></pre>
|
||
</div>
|
||
""".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))
|
||
|