# MVP v2.0 API 设计(最小可用) v2.0 的 API 目标是:把 v1.1 的“脚本提交”变成“服务化提交”,并在服务侧实现队列/重试/状态聚合。 约束: - 内部 token 鉴权(简单即可)。 - Ray Job 提交必须使用 **Ray Python SDK**(`JobSubmissionClient`),不使用 `requests` 手写 HTTP。 - 输出与状态必须落盘到 NFS(容器内 `/private`)。 --- ## 1. 鉴权 - Header:`Authorization: Bearer ` - v2.0 不做用户体系与权限隔离;token 只是“防误用”。 - 配置建议:复用 `src/mvp/v1.1/py/configs/dev.yaml` 并在 `v2.auth.token_env` 指定 token 环境变量名。 ## 1.1 运行位置(dev 示例) - 服务进程运行在 **Ray head 容器**(便于访问 Ray Job server)。 - 宿主机侧用脚本控制(`docker exec`): - `src/mvp/v2.0/scripts/20_start_api.sh` - `src/mvp/v2.0/scripts/21_stop_api.sh` - `src/mvp/v2.0/scripts/22_status_api.sh` - 远程机目录约定(示例):`argus@h1:/home2/argus/infra/mvp/v2/`,容器内挂载到 `/workspace/mvp/v2/`。 --- ## 2. 资源与 ID 约定 ### 2.1 task_id(服务层主 ID) - 格式建议:`mvp2----` - 示例:`mvp2-ppo-20251223-143201-7f3a` ### 2.2 ray_submission_id(attempt 级 ID) - 由 service 派生:`--a` - 示例:`mvp2-ppo-20251223-143201-7f3a--a01` 好处: - Ray 的 submission id 自带 task_id,可直接从 Ray dashboard 反查到服务侧任务。 - `/private/jobs//...` 目录天然隔离且可读。 --- ## 3. JobSpec(请求体) v2.0 **要求 JobSpec 使用 v1.1 同款 YAML**(字段与语义保持一致),服务端接收 YAML 文本并解析后入库(同时原样保存 `jobspec_yaml` 便于审计/复现)。 最小字段(示例 YAML): ```yaml workload: "ppo" submission_id: "" # v2.0 服务端会忽略/覆盖(由 task_id 派生 ray_submission_id) code_path: "/private/common/code/verl/verl_repo" model_id: "Qwen/Qwen2.5-0.5B-Instruct" train_file: "/private/datasets/gsm8k/train.parquet" val_file: "/private/datasets/gsm8k/test.parquet" nnodes: 2 n_gpus_per_node: 4 total_epochs: 1 total_training_steps: 10 save_freq: 10 test_freq: -1 trainer_device: null # 仅 sft 使用(通常 "cpu") ``` 说明: - `trainer_device` 仅对 `sft` 生效(通常为 `cpu`,避免 driver 无 GPU)。 - `val_file` 可为 `null`(例如 SFT)。 --- ## 4. API 端点 ### 4.1 提交任务 `POST /api/v2/tasks` Request body: - **raw JobSpec YAML**(与 v1.1 jobspec YAML 结构一致) Headers: - `Content-Type: application/yaml`(或 `text/yaml`) Response: ```json { "task_id": "mvp2-ppo-20251223-143201-7f3a", "state": "QUEUED" } ``` ### 4.2 查询任务(聚合状态) `GET /api/v2/tasks/{task_id}` Response(示例): ```json { "task_id": "mvp2-ppo-20251223-143201-7f3a", "workload": "ppo", "state": "RUNNING", "desired_resources": {"nnodes": 2, "n_gpus_per_node": 4, "total_gpus": 8}, "latest_attempt": { "attempt_no": 1, "ray_submission_id": "mvp2-ppo-20251223-143201-7f3a--a01", "ray_status": "RUNNING", "start_time": "2025-12-23T14:32:10+08:00" }, "error_summary": null } ``` ### 4.3 列出 attempts `GET /api/v2/tasks/{task_id}/attempts` Response: ```json { "task_id": "mvp2-ppo-20251223-143201-7f3a", "attempts": [ { "attempt_no": 1, "ray_submission_id": "mvp2-ppo-20251223-143201-7f3a--a01", "ray_status": "FAILED", "failure_kind": "INSUFFICIENT_RESOURCES", "message": "Total available GPUs 0 is less than total desired GPUs 8", "start_time": "...", "end_time": "..." } ] } ``` ### 4.4 取消任务 `POST /api/v2/tasks/{task_id}:cancel` 行为: - 若 task 处于 `SUBMITTED/RUNNING`:调用 Ray Jobs SDK `stop_job(ray_submission_id)` 并标记 `CANCELED` - 若处于 `QUEUED/PENDING_RESOURCES`:直接标记 `CANCELED`(不提交) Response: ```json {"task_id":"...","state":"CANCELED"} ``` ### 4.5 获取日志 `GET /api/v2/tasks/{task_id}/logs?attempt=latest&tail=2000` 返回: - `text/plain`(直接透传 Ray Job logs tail) 说明: - v2.0 先用 Ray SDK `get_job_logs()`。 - 若需要更稳定的归档,可在 scheduler 定期抓取并落盘(v2.1+)。 ### 4.6 列出队列(运维/调试) `GET /api/v2/queue` Response: ```json { "pending": [{"task_id":"...","state":"PENDING_RESOURCES","next_run_at":"..."}], "running": [{"task_id":"...","ray_submission_id":"..."}] } ``` --- ## 5. 错误码(最小) - `400`:jobspec 缺字段/非法 - `401`:token 不正确 - `404`:task 不存在 - `409`:状态冲突(例如已终态又 cancel) - `500`:服务内部错误 --- ## 6. SQLite 持久化(API 可见性) v2.0 服务端使用 SQLite 持久化保存: - tasks(`task_id`、`state`、`jobspec_yaml`、`next_run_at`、`latest_attempt_no` 等) - attempts(`ray_submission_id`、`ray_status`、失败原因等) 因此: - `GET /api/v2/tasks/{task_id}` 的数据来自 SQLite(再叠加 Ray 状态同步的结果)。 - 进程重启后,队列可恢复,`PENDING_RESOURCES` 的任务会在 `next_run_at` 到期后继续尝试提交。