# 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 完全一致（仅目录与命名变化），避免引入不必要的不兼容。
