# MVP v3.5 详细设计方案(进一步精简版,基于 v3.0) > 背景:v3.0 已具备 WebUI + API server + 用户/任务隔离 + SFTPGo 数据管理 + Stateless Ray cluster(head + worker node pool)。 > > v3.5 本轮 **只做 2 件事**: > 1) Advanced Task:支持用户提交自定义训练命令(command) > 2) Custom Reward:支持用户通过 VERL 原生 `custom_reward_function.*` 方式注入 reward(仅方式 A:用户自己写命令) > > 明确不做(从上一版设计中移除):(3) 自定义 verl 版本/代码路径、(4) 断点续训、(5) IB/RoCEv2 网络支持、(6) Model Serving。 --- ## 0. 继承 v3.0 的不变点(重要约束) 1) **Node management 不变** - v3.5 不新增/不修改 node management 机制;仍按 v3.0 现状运行(head 写 discovery、worker watchdog 自动 join、自愈)。 2) **Head 不跑训练** - 所有训练/Serving driver 通过 Ray entrypoint placement 强制落在 worker(例如 `entrypoint_resources={"worker_node": 1}`)。 3) **SFTPGo 的 “common” 目录约定变更** - 不再使用 `$COMMON` 宏。 - 在 SFTPGo 中,把共享只读资源映射到用户 home 下的固定目录(用户在 SFTP/WebClient 看到的是 `$HOME/common/...`): - `$HOME/common/datasets` → 容器内真实路径 `/private/datasets`(只读) - `$HOME/common/hf` → 容器内真实路径 `/private/hf`(只读) > 这里的 `$HOME` 指:`/private/users/`(容器内路径)。 --- ## 1. v3.5 需求范围(精简后) ### 1.1 In scope **A. Advanced TaskSpec(自定义命令)** - 用户提交 `command`(多行 shell 或单行) - 平台做 `$HOME` 宏替换 - 平台做 best-effort 安全检查(路径/关键参数),然后提交为 Ray job **B. Custom Reward(仅方式 A)** - 用户在 `command` 里显式写 hydra overrides: - `custom_reward_function.path=...` - `custom_reward_function.name=...` - `custom_reward_function.reward_kwargs.*=...`(可选) - 平台不提供结构化 reward 字段(不做方式 B),只做检查(校验 path 合法) ### 1.2 Out of scope(本轮不做) - 自定义 verl 版本/代码路径(仍使用平台内置/公共 verl 代码快照) - 断点续训(resume from checkpoint) - IB/RoCEv2 网络专门支持(NCCL/RDMA env 先不引入平台) - Model Serving(暂缓,后续单独设计迭代) --- ## 2. Advanced TaskSpec 设计 ### 2.1 为什么需要 Advanced Task v3.0 的 Basic TaskSpec(ppo/grpo/sft)通过平台模板生成固定 overrides,适合“快速跑通”。 但科研/调参场景需要更高自由度:用户希望直接写 `python3 -m verl.trainer.main_ppo ...` 并自行控制每个 override。 ### 2.2 Advanced TaskSpec(建议 schema) 建议新增一种 TaskSpec 类型,通过 `kind: advanced` 区分: ```yaml kind: advanced # 资源(平台调度与预检查用;仍需要) nnodes: 2 n_gpus_per_node: 4 # 自定义命令(用户负责写对 VERL 的参数/路径) # 平台会对 $HOME 做宏替换;其余保持原样 command: | PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \ data.train_files=$HOME/datasets/gsm8k/train.parquet \ data.val_files=$HOME/datasets/gsm8k/test.parquet \ actor_rollout_ref.model.path=Qwen/Qwen2.5-0.5B-Instruct \ trainer.nnodes=2 \ trainer.n_gpus_per_node=4 \ trainer.total_epochs=1 \ trainer.save_freq=10 \ +ray_kwargs.ray_init.address=auto ``` ### 2.3 `$HOME` 宏替换规则 仅支持 `$HOME`(v3.5 移除 `$COMMON`): - `$HOME` → `/private/users/` 用户如果要用共享数据/缓存: - 共享数据:`$HOME/common/datasets/...` - 共享 HF 缓存:`$HOME/common/hf/...`(通常不需要写进 command,但可用于 debug) #### 2.3.1 重要说明:SFTPGo “virtual folder” 与训练进程看到的“真实路径” 在 SFTPGo 中,`$HOME/common/datasets` / `$HOME/common/hf` 是 **SFTP 虚拟目录映射**(virtual folder),它们映射到容器内真实路径: - `$HOME/common/datasets` ↔ `/private/datasets` - `$HOME/common/hf` ↔ `/private/hf` 训练进程(Ray worker 上的 python 进程)看到的是 **容器内真实文件系统**,它并不会理解 SFTPGo 的 virtual folder。 因此,为了让用户能沿用 WebClient 里看到的路径语义(写 `$HOME/common/...`),服务层在提交 Advanced command 前需要做 **路径宏映射**: - `"$HOME/common/datasets"` → `"/private/datasets"` - `"$HOME/common/hf"` → `"/private/hf"` - 其余 `"$HOME"` → `"/private/users/"` 这样用户写的 command 能在训练进程里正确读到文件。 ### 2.4 服务层检查(best-effort,强约束 + 弱约束) > 目标:在不“解析完整 shell”的前提下,尽可能避免跨用户读文件与明显错误的任务。 **强约束(必须通过,否则 400)** 1) `nnodes`、`n_gpus_per_node` 必须存在(用于队列/资源预检查/placement) 2) `command` 必须包含一个明确的 python entry: - 建议最低要求:包含 `python3` 且包含 `-m verl.trainer.`(防止随意执行系统命令) 3) 路径隔离校验(字符串/正则级别): - 展开 `$HOME`(含 `$HOME/common/*` 映射到 `/private/*`)后: - 禁止出现 `/private/users/` 下 “非当前用户”的路径(例如 `/private/users/bob/...`) - 对 `data.train_files=...`、`data.val_files=...`(若出现)做 allowlist: - 允许(用户目录):`/private/users//datasets/...` - 允许(共享目录):`/private/datasets/...` - 对 `custom_reward_function.path=...`(若出现)做 allowlist: - 允许:`/private/users//code/...`(用户自行上传) **弱约束(warning,不阻塞)** - 未检测到 `data.train_files=`/`data.val_files=`(可能是用户写成了别的 key 或使用了 config file) - 未检测到 `+ray_kwargs.ray_init.address=auto`(v3.0/v3.5 推荐加,但用户可自行负责) > 说明:Advanced command 本质上属于“内部可信用户”能力,v3.5 不做强沙箱;安全检查以 best-effort 为主。 --- ## 3. Custom Reward(仅方式 A:用户自己写) ### 3.1 VERL 原生机制(本仓库 `verl/` 已调研) VERL PPO trainer 配置里支持: - `custom_reward_function.path` - `custom_reward_function.name` - `custom_reward_function.reward_kwargs` 对应实现位置: - 配置模板:`verl/verl/trainer/config/ppo_trainer.yaml` - 加载逻辑:`verl/verl/trainer/ppo/reward.py:get_custom_reward_fn` - 典型 reward manager:`verl/verl/workers/reward_manager/naive.py` 会调用 `compute_score(...)` ### 3.2 用户写法(示例) 用户上传 `$HOME/code/reward.py`,在 command 里加: ```bash custom_reward_function.path=$HOME/code/reward.py \ custom_reward_function.name=compute_score ``` 函数签名建议(与 `naive` reward manager 参数对齐): ```python def compute_score(*, data_source: str, solution_str: str, ground_truth: str, extra_info=None, **kwargs): ... ``` ### 3.3 平台侧只做检查(不做字段扩展) v3.5 限定 reward 注入方式为 “用户写 command”,平台只做: - 展开 `$HOME` - 若检测到 `custom_reward_function.path=`,校验 path 在 `$HOME/code/` 下 - 不尝试解析/合并 reward_kwargs(用户自己写) --- ## 4. 服务层与 SFTPGo 的映射修改(你提出的关键点) v3.0 时代平台允许用户引用: - `/private/common/datasets/...` - `/private/common/hf/...` 但现在 common 以 **SFTPGo virtual folder** 的形式呈现给用户(用户看到 `$HOME/common/...`,真实路径是 `/private/...`),因此 v3.5 的服务层需要做两件事: 1) **用户侧语义(写 TaskSpec/command)** - 共享 datasets(只读):`$HOME/common/datasets/...` - 共享 hf cache(只读):`$HOME/common/hf/...` 2) **运行时真实路径(提交到 Ray 前展开)** - `$HOME/common/datasets/...` → `/private/datasets/...` - `$HOME/common/hf/...` → `/private/hf/...` 同时保留用户自有目录: - 用户 datasets:`$HOME/datasets/...` - 用户 models:`$HOME/models/...` - 用户 code(reward):`$HOME/code/...` > 这部分主要影响: > - Advanced command 检查(allowlist) > - WebUI/Data 页面文案(告诉用户共享数据在哪里) > 兼容性建议:为了不影响 v3.0 期间已经习惯使用 `/private/common/datasets/...` 的用户/历史任务, > v3.5 实现阶段建议 **同时接受**: > - `/private/common/datasets/...`(旧路径语义,仍可读) > - `/private/datasets/...`(真实路径语义,推荐) > - Advanced command 里写的 `$HOME/common/datasets/...` 会先映射到 `/private/datasets/...` --- ## 5. 验收标准(精简版) ### 5.1 Advanced command - 提交一个 Advanced PPO command(train/val 使用 `$HOME/common/datasets/...` 或 `$HOME/datasets/...`) - 确认: - 任务从 QUEUED → SUBMITTED/RUNNING - driver 在 worker 上(head 不跑训练) - 训练能正常跑至少若干 step ### 5.2 Custom reward(方式 A) - 用户上传 `$HOME/code/reward.py` - 在 command 中设置 `custom_reward_function.path=$HOME/code/reward.py` - 确认训练日志出现 `using customized reward function ...` --- ## 6. 待确认问题(需要你拍板/补充) 1) Advanced command 的“强约束”是否需要更严格? - 目前建议要求包含 `python3 -m verl.trainer.`,否则拒绝。 - 你是否允许用户跑非 verl 的命令(例如自定义评估脚本)? 2) `$HOME/common/datasets` 与 `$HOME/common/hf` 两个映射目录在平台侧是否需要“强制只读”语义? - 例如:TaskSpec 校验允许读取但禁止写入(目前设计是 best-effort 字符串级校验)。 --- ## 7. 基于现有源码的改动点分析(实现清单) 本节按当前 v3.0 已上线的源码结构(`src/mvp/py/argus/...`)逐文件列出 v3.5 需要的具体改动点,并评估对现有能力的影响面。 ### 7.1 TaskSpec/模型层(解析与兼容) **现状** - Basic TaskSpec 由 `argus.ray.models.JobSpec.from_dict()` 解析:`src/mvp/py/argus/ray/models.py` - API `/api/v2/tasks` 直接 `JobSpec.from_dict(obj)`,并基于字段做路径校验:`src/mvp/py/argus/service/app.py` - Scheduler 同样假定 jobspec_yaml 能解析为 `JobSpec`:`src/mvp/py/argus/service/scheduler.py` **v3.5 需要新增** 1) 新增 `AdvancedTaskSpec` 数据结构(建议放在 `src/mvp/py/argus/ray/models.py`): - 必填:`kind: advanced`、`workload`(建议仍要求 ppo/grpo/sft,用于 task_id 命名与 UI 分类)、`nnodes`、`n_gpus_per_node`、`command` - 可选:`submission_id`(由服务层 override) 2) 新增 “union 解析”: - 新增 `parse_taskspec(obj: dict) -> Basic(JobSpec) | Advanced(AdvancedTaskSpec)` - 兼容策略:如果没有 `kind` 字段,则 **默认按 v3.0 Basic JobSpec 解析**(保证老客户端无感)。 ### 7.2 Builder 层(把 TaskSpec 转为可执行 argv) **现状** - `src/mvp/py/argus/ray/builders.py:build_training_argv(spec: JobSpec, ...)` 只支持模板化 PPO/GRPO/SFT。 **v3.5 需要新增** 1) 新增 `build_advanced_argv(command: str) -> list[str]` - 推荐实现:返回 `["bash", "-lc", ""]` - 原因:用户 command 允许 `ENV=... python3 ... \` 多行以及 shell 语法,`bash -lc` 兼容性最好。 2) Driver entrypoint 复用: - 仍通过 `argus.ray.driver_entrypoint` 执行(统一 job_dir、日志与退出码)。 ### 7.3 RayJobTool 层(runtime_env 与提交) **现状** - `src/mvp/py/argus/ray/ray_job_tool.py:RayJobTool.submit(spec: JobSpec, ...)`: - runtime_env 的 `PYTHONPATH` 由 `spec.code_path` 决定 - entrypoint 固定为 driver_entrypoint + builder 生成 argv **v3.5 需要新增** 1) 扩展 submit 支持 AdvancedTaskSpec: - 方案 A(最小侵入):新增 `submit_advanced(...)` 方法,参数为 `command` + `job_dir` + `submission_id` + `nnodes/n_gpus...` - 方案 B(统一接口):新增内部抽象 `SubmitPlan`(包含 `runtime_env` + `entrypoint` + `artifacts`),Basic/Advanced 都生成 plan,再走同一 submit 逻辑。 2) runtime_env 的 code path: - 因 v3.5 本轮不做“自定义 verl code_path”,建议仍固定使用公共快照(例如 `/private/common/code/verl/verl_repo`)。 - 为减少散落常量,建议在 config 增加 `ray.verl_code_path`(或 `service.verl_code_path`),RayJobTool 统一读取。 3) runtime_env 的用户代码目录(可选增强): - VERL 的自定义 reward 函数是通过 `custom_reward_function.path` 以“文件路径”动态 import 的,理论上不依赖 `PYTHONPATH`。 - 但用户的 `reward.py` 可能会 `import` 自己目录下的其他模块;为了提升易用性,可将 `/private/users//code` 追加到 job 的 `PYTHONPATH`。 - 这需要 RayJobTool.submit/submit_advanced 能感知 `user_id`(由 Scheduler 传入),属于小改动但要注意兼容性。 ### 7.4 API Server(提交校验、宏替换、spec 展示) **现状** - `POST /api/v2/tasks`:只支持 Basic JobSpec 且强校验 `code_path/train_file/val_file/model_id` 前缀:`src/mvp/py/argus/service/app.py` - `/api/v2/tasks/{task_id}/spec`:返回 resolved 的 Basic JobSpec(补默认值/补 submission_id):`src/mvp/py/argus/service/app.py` **v3.5 需要新增/修改** 1) `POST /api/v2/tasks` 分流: - `kind != advanced`:走原 Basic 流程(兼容 v3.0) - `kind == advanced`:走 Advanced 解析 + 校验 2) Advanced command 宏替换与映射(核心): - 实现 `expand_command(user_id, command)`: - 先把 `$HOME/common/datasets` → `/private/datasets` - 再把 `$HOME/common/hf` → `/private/hf` - 再把其余 `$HOME` → `/private/users/` - 校验使用 “展开后的 command” 3) reward 注入检查(仅方式 A): - 若发现 `custom_reward_function.path=...`: - 校验展开后的 path 前缀必须是 `/private/users//code/` 4) `/api/v2/tasks/{task_id}/spec`: - 需要支持返回 AdvancedTaskSpec 的 resolved 版本: - 展示时可选择“原始 command”(含 `$HOME`)或“展开后的 command”(建议都展示:raw + expanded) ### 7.5 Scheduler(队列与提交) **现状** - `src/mvp/py/argus/service/scheduler.py` 假定 jobspec_yaml 一定是 Basic JobSpec,并调用 `tool.submit(spec2, ...)`。 **v3.5 需要新增** 1) Scheduler 的 `_parse_jobspec` 替换为 `parse_taskspec`(支持 Basic/Advanced)。 2) `_submit_one` 根据 spec 类型调用: - Basic:保持现状 `tool.submit(JobSpec, ...)` - Advanced:调用 `tool.submit_advanced(...)`(或统一 SubmitPlan) ### 7.6 WebUI(最小改动) **现状** - `src/mvp/py/argus/service/ui.py` 的 New Task 页面只提供 Basic YAML 模板。 **v3.5 需要新增** - 增加 “Advanced Task” 模板按钮: - `kind: advanced` - `workload: ppo|grpo|sft`(用于 UI 分类与 task_id) - `nnodes/n_gpus_per_node` - `command: | ...`(带中文注释) - Data 页面文案更新: - 明确共享目录在 `$HOME/common/datasets`、`$HOME/common/hf`(并解释会映射到 `/private/datasets`、`/private/hf`) --- ## 8. 对现有功能的兼容性影响评估 ### 8.1 API/TaskSpec 兼容 - 兼容策略:**没有 `kind` 字段的 YAML 一律按 v3.0 Basic JobSpec 解析**。 - 现有脚本/客户端(提交 ppo/grpo/sft 的 YAML)无需修改。 - AdvancedTaskSpec 是新增能力,不影响既有任务状态机/DB。 ### 8.2 路径策略变更的影响 风险点:v3.0 的 Basic 任务/模板大量使用 `/private/common/datasets/...`。 建议: - v3.5 实现阶段先保持 “双栈兼容”: - Basic 继续接受 `/private/common/datasets/...`(旧) - 同时接受 `/private/datasets/...`(新/真实路径) - Advanced command 允许用户写 `$HOME/common/datasets/...`,服务层展开为 `/private/datasets/...`(避免虚拟目录不可见问题)。 ### 8.3 任务执行/调度兼容 - Scheduler 队列/并发控制(`max_running_tasks`)保持不变。 - 资源预检查仍只依赖 `nnodes/n_gpus_per_node`,AdvancedTaskSpec 不改变资源模型。 ### 8.4 安全边界变化 - Advanced command 引入后,平台从“结构化参数”变成“执行用户命令”,安全边界变宽。 - 缓解措施(best-effort): - 强约束要求命令包含 `python3 -m verl.trainer.` - 基础路径隔离校验(禁止跨用户路径) - reward 文件路径限制在 `$HOME/code` ### 8.5 数据库兼容 - DB schema 不强制变更:仍复用 `tasks.jobspec_yaml` 存储原始 YAML。 - 若后续需要更强查询/过滤,再考虑增加 `tasks.kind` 字段(可选增量迁移)。