# MVP 代码结构重构方案(按功能模块划分) 背景:当前 `src/mvp/` 下以 `v1.1/`、`v2.0/` 版本目录来组织代码。实际上 **v2.0 是在 v1.1 的 Ray Jobs SDK 提交链路基础上扩展了服务层**,并且为了让 v2.0 工作又对 v1.1 的 `docker-compose.yaml`、`dev.yaml` 做了修改(挂载 v2、开放 8080、增加 `v2:` 配置段)。因此“按版本分目录”会让依赖关系变得不清晰(谁是库、谁是应用、哪些配置是共享的)。 本方案目标:把 `src/mvp/` 重构为“按功能模块”划分(ray 提交核心库 / service 服务层 / cli 工具 / TaskSpecs / configs / scripts),并给出迁移后的验证与执行方案。 > 本文仅给出设计与迁移/验证方案,不直接改代码(待确认后再实施)。 --- ## 1. 现状梳理(问题点) ### 1.1 代码重复与耦合 - `src/mvp/v2.0/py/mvp_v11/` 是从 `src/mvp/v1.1/py/mvp_v11/` 复制而来用于复用,但这导致: - **库代码重复**(修 bug 要改两份) - 谁是“权威实现”不明确 - v2 API(`mvp_v2`)通过引用复制的 `mvp_v11.RayJobTool` 来提交 Ray Job,本质上依赖 v1.1 提交链路作为“库”。 ### 1.2 配置与部署目录不稳定 - v2.0 复用了 v1.1 config 文件并新增 `v2:` section,这是合理的“向后兼容扩展”,但它把: - “Ray submit 基础配置” - “API 服务配置” - “部署路径约定(/workspace/mvp/v1.1 vs /workspace/mvp/v2)” 混在一个文件里,不利于长期维护。 ### 1.3 命名歧义:jobspec 与 Ray job - v1.1/v2.0 都使用 `jobspec.yaml` 指代“训练语义参数”(PPO/GRPO/SFT 的训练字段)。 - 但 Ray 也有 “Ray Job” 概念(submission_id、entrypoint、runtime_env 等),易造成沟通误解。 - 需要把训练侧 specs 改名为 **TaskSpecs**(代表平台级任务规范),与 Ray Job 区分。 --- ## 2. 重构目标(What good looks like) ### 2.1 目录与职责清晰 - “提交 Ray Job 的 SDK 封装”是一个可复用模块(库)。 - “服务层(API + scheduler + SQLite)”是一个独立模块(应用/服务)。 - “训练语义参数(TaskSpecs)”与 “Ray Job 提交参数(RayConfig)”分层清楚。 ### 2.2 单一真源(Single Source of Truth) - 只能有一份“Ray submitter core”实现(不能复制一份到另一个版本目录)。 - API 与 CLI/脚本都复用同一份 core。 ### 2.3 兼容现有运行方式(渐进迁移) - 保留现有的脚本式启动/准备流程(Ray 起集群、准备模型/数据仍用 scripts)。 - 允许在迁移期提供薄 wrapper 兼容旧路径(减少一次性 break)。 --- ## 3. 目标结构(按功能模块划分) 建议把 `src/mvp/` 重构为下面的“功能分层”: ``` src/mvp/ py/ argus/ # 顶层包(避免与 Ray 的 `ray` 包冲突) __init__.py core/ # 通用:yaml/模型定义/工具函数(纯库) __init__.py yaml_io.py ids.py # task_id / attempt_id 生成规则 ray/ # Ray Job 提交“核心库”(由现成 mvp_v11 迁移而来) __init__.py models.py # RayConfig, TaskSpec(解析), Attempt, enums builders.py # build_training_argv (ppo/grpo/sft) driver_entrypoint.py # 仍然作为 Ray job entrypoint(worker 上执行) ray_job_tool.py # Ray Jobs SDK 封装(submit/status/stop/logs) runtime_env.py # 统一 PYTHONPATH/runtime_env 组装逻辑 service/ # 服务层:FastAPI + scheduler + sqlite(应用) __init__.py app.py scheduler.py db.py config.py # service 相关配置读取(从 configs 读取) ray_resources.py cli/ # 命令行/SDK 提交入口(由现成 v1.1 run.py 迁移而来) __init__.py run.py # submit/status/logs/stop 等 action server.py # uvicorn 入口(导入 argus.service.*) configs/ dev.yaml # RayConfig + ServiceConfig(按层次组织、可扩展) prod.yaml # (可选)生产配置模板 taskspecs/ # 原 jobspecs/,改名 TaskSpecs(训练语义规范) ppo.yaml grpo.yaml sft.yaml README.md # TaskSpec 字段解释、示例 scripts/ # 宿主机脚本(docker exec/compose 编排) lib.sh 00_prereq_check.sh 01_up.sh / 02_down.sh 20_start_head.sh / 21_start_workers.sh 30_prepare_data_and_model.sh 40_submit_cli.sh # 通过 cli/run.py 提交 TaskSpec 60_start_api.sh # 启动 API(service) 61_stop_api.sh 62_status_api.sh docker-compose.yaml # dev 环境 compose(从 v1.1 迁移到这里,路径稳定) README.md # 总入口文档(运行方式、目录约定) ``` ### 3.1 关键点:库 vs 应用边界 - `argus.ray` 是唯一的 Ray submitter 库(替代当前 v1.1/v2.0 的 `mvp_v11` 双份拷贝)。 - `argus.service` 依赖 `argus.ray`,不反向依赖。 - `argus.cli` 依赖 `argus.ray`,用于脚本化提交/调试。 ### 3.2 TaskSpecs vs RayConfig - `taskspecs/*.yaml`:描述训练任务语义参数(workload、nnodes、n_gpus_per_node、数据/模型路径、训练步数等)。 - `configs/*.yaml`:描述 Ray 提交环境(address、entrypoint_resources、runtime_env 以及 service 配置)。 --- ## 4. 配置策略(重构后如何组织 configs) ### 4.1 建议的 config 分层 把当前 `dev.yaml` 的内容明确分为两段(按模块名分段): 1) `ray:`(RayConfig) - job server address - shared_root(`/private`) - entrypoint resources(强制 driver 落 worker) - runtime_env env_vars(HF cache、PYTHONPATH 注入策略) 2) `service:`(ServiceConfig) - api host/port - auth token_env - sqlite db_path - scheduler tick/max_running/retry_interval 示例(结构示意): ```yaml ray: address: "http://127.0.0.1:8265" shared_root: "/private" entrypoint_num_cpus: 1 entrypoint_resources: worker_node: 1 runtime_env: env_vars: HF_ENDPOINT: "https://hf-mirror.com" PYTHONUNBUFFERED: "1" user_code_path: "/private/user/code" service: api: host: "0.0.0.0" port: 8080 auth: token_env: "MVP_INTERNAL_TOKEN" sqlite: db_path: "/private/common/db/mvp.sqlite3" scheduler: tick_s: 5 retry_interval_s: 60 max_running_tasks: 1 ``` > 迁移期可以支持“旧格式”(v1.1 顶层字段 + v2: 段)与“新格式”(ray:/service: 两段)并存:解析时兼容读取,降低一次性改动风险;但最终以新格式为准。 --- ## 5. 迁移路径(推荐分两阶段实施) ### 阶段 A:先拷贝/迁移现成文件,再做最小调整(保持行为不变) 目标:不改功能、不改 API 行为。优先通过“拷贝/迁移现成文件 + 修正包引用/路径”完成重构,避免重头重写逻辑(降低出错概率)。 建议步骤: 1) 抽出 `src/mvp/py/argus/ray/`(由现成代码迁移) - 将 `src/mvp/v1.1/py/mvp_v11/` 迁移到 `src/mvp/py/argus/ray/`,并把它作为 submitter core 的唯一真源(不再保留一份复制品在其它目录)。 - 只做机械化调整:修正 import、修正默认路径常量(例如 tool code path / working dir)、修正 scripts 的调用路径。 2) 抽出 `src/mvp/py/argus/service/`(由现成代码迁移) - 将 `src/mvp/v2.0/py/mvp_v2/` 迁移到 `src/mvp/py/argus/service/`。 - service 侧对 submitter 的依赖统一改为 `src/mvp/py/argus/ray/`(不再引用 `src/mvp/v2.0/py/mvp_v11/` 的复制品)。 3) CLI 统一入口:`src/mvp/py/argus/cli/run.py`(由现成代码迁移) - 将 `src/mvp/v1.1/py/run.py` 迁移到 `src/mvp/py/argus/cli/run.py`,保留 action 语义(submit/status/logs/stop)。 - 仅调整 import 与默认路径,使其指向新目录(configs/taskspecs/py)。 4) scripts 合并(以 v1.1 为基、合入 v2 API) - 将 `src/mvp/v1.1/scripts/` 迁移到 `src/mvp/scripts/`(Ray 集群编排最成熟)。 - 将 `src/mvp/v2.0/scripts/` 的 API 启停脚本合入 `src/mvp/scripts/`,并统一命名与默认路径。 5) docker-compose / mounts 稳定化(你已确认要迁移) - 将 `src/mvp/v1.1/docker-compose.yaml` 迁移为 `src/mvp/docker-compose.yaml`。 - 容器内挂载统一:宿主机 `.../src/mvp/` → 容器 `/workspace/mvp/`(包含 `py/ configs/ taskspecs/ scripts/`)。 - runtime_env 的 `PYTHONPATH` 注入统一指向 `/workspace/mvp/py`(不再出现 `/workspace/mvp/v1.1/py`、`/workspace/mvp/v2/...`)。 阶段 A 完成标准: - 原来 v1.1 的 CLI 提交方式仍可用(提交 PPO/GRPO/SFT)。 - v2 API 仍可用(队列、取消、日志)。 - 不再存在 `mvp_v11` 的重复目录。 ### 阶段 B:配置格式升级(按模块两段)+ TaskSpecs 更名落地 目标:把 jobspec 真正改名为 TaskSpec,并把 config 升级为按模块两段(`ray:`/`service:`)清晰分层。 建议步骤: - `jobspecs/` → `taskspecs/`,并更新 README/脚本引用。 - `dev.yaml` 从“顶层字段 + v2:”迁移为“ray:/service:”两段。 - 保留一段时间的兼容解析逻辑(读取旧格式时发出 warning)。 - 完成验证后删除旧版本目录:`src/mvp/v1.1/`、`src/mvp/v2.0/`(以及远端对应目录),确保新结构成为唯一入口。 阶段 B 完成标准: - docs 里不再出现 “jobspec” 词汇,统一称 “TaskSpec”。 - `configs/dev.yaml` 分层清晰(`ray:`/`service:` 两段按模块名),服务与 Ray 的配置互不干扰。 --- ## 6. 重构后的验证与执行方案(验收/回归) ### 6.1 本地(仓库内)静态验证 1) import / 入口检查(在容器环境) - `python3 -c "from argus.ray.ray_job_tool import RayJobTool"` - `python3 -c "from argus.service.app import create_app"` 2) 目录结构检查 - 确保 `src/mvp/py` 是唯一 python 代码根 - 确保 `taskspecs/`、`configs/`、`scripts/` 都在 `src/mvp/` 下 ### 6.2 dev 环境(argus@h1)端到端验证 前置: - 远端目录:`argus@h1:/home2/argus/infra/mvp/`(维持不变) - 共享目录:`/home2/argus/infra/mvp/shared` 挂载到容器 `/private` 验证步骤(推荐顺序): 1) 重新拉起容器 + 启动 Ray - `scripts/01_up.sh` - `scripts/20_start_head.sh`(head `--num-cpus=0 --num-gpus=0`) - `scripts/21_start_workers.sh`(workers 带 `worker_node` 资源) - `ray status` / `ray list nodes` 确认 head 无 GPU、workers 各 4 GPU 2) CLI 提交回归(等价 v1.1) - 提交 `taskspecs/sft.yaml`,确认成功 - 提交 `taskspecs/grpo.yaml`,确认成功 - 提交 `taskspecs/ppo.yaml`,确认成功 3) API 服务回归(等价 v2.0) - `scripts/60_start_api.sh` - `POST /api/v2/tasks`(raw YAML TaskSpec) - `GET /api/v2/tasks/{task_id}`:确认返回 `created_at/updated_at` - `POST /api/v2/tasks/{task_id}:cancel`:确认任务 `state=CANCELED` 且 attempt `ray_status=STOPPED`(服务侧语义一致) 4) 队列行为回归 - 在 `service.scheduler.max_running_tasks=1` 下: - 连续提交两个 8-GPU 任务:第二个应保持 `QUEUED/PENDING_RESOURCES`,直到第一个结束后自动提交。 验收标准: - 三种 workload(PPO/GRPO/SFT)都能通过 CLI 跑通(或至少能正确提交到 Ray 并进入 RUNNING)。 - API 提交/查询/取消/日志正常。 - “cancel 后 state=CANCELED 但 attempt 仍 RUNNING”的不一致问题不再出现。 --- ## 7. 风险与注意事项 1) PYTHONPATH 注入路径变化 - 当前 runtime_env 里有 `MVP_TOOL_CODE_PATH=/workspace/mvp/v1.1/py` 的历史路径假设; - 重构后需统一为 `/workspace/mvp/py`,并确保所有容器都挂载到相同路径。 2) SQLite WAL 在 NFS 上的稳定性 - 目前 db 使用 WAL(生成 `-wal/-shm`),NFS 下可能有一致性风险; - 可作为后续优化:检测 NFS 时退回 `journal_mode=DELETE` 或换成单机本地盘。 3) 渐进迁移的兼容期 - 迁移期可以短暂保留旧路径(例如 `src/mvp/v1.1`、`src/mvp/v2.0` 仅保留 README 指向新路径)以减少一次性断裂;但你已确认最终会删除这两个目录,因此需要在 scripts/文档同步更新后尽快清理。 --- ## 8. 已确认约束(将按此实施) 你已明确: 1) `docker-compose.yaml` 必须迁移到 `src/mvp/` 根;重构完成后 `src/mvp/v1.1`、`src/mvp/v2.0` 都会删除。 2) `configs/dev.yaml` 升级为两段,按模块名分段:`ray:` 与 `service:`;并保持纯 YAML 风格(不混用 JSON inline map)。 3) TaskSpec 字段先保持与现有 v1.1 YAML 完全一致(仅目录与命名变化),避免引入不必要的不兼容。