cleaning code

This commit is contained in:
yuyr 2026-01-13 10:46:43 +08:00
parent ac9c80ed8c
commit 4a5afb7ed7
91 changed files with 1 additions and 13294 deletions

1
.gitignore vendored
View File

@ -6,4 +6,3 @@ __pycache__/
.pytest_cache/ .pytest_cache/
.coverage .coverage
htmlcov/ htmlcov/
AGENTS.md

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 5.2 MiB

After

Width:  |  Height:  |  Size: 5.2 MiB

View File

Before

Width:  |  Height:  |  Size: 5.2 MiB

After

Width:  |  Height:  |  Size: 5.2 MiB

View File

Before

Width:  |  Height:  |  Size: 5.6 MiB

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

Before

Width:  |  Height:  |  Size: 5.6 MiB

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

Before

Width:  |  Height:  |  Size: 5.1 MiB

After

Width:  |  Height:  |  Size: 5.1 MiB

View File

Before

Width:  |  Height:  |  Size: 5.8 MiB

After

Width:  |  Height:  |  Size: 5.8 MiB

View File

Before

Width:  |  Height:  |  Size: 5.6 MiB

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

@ -1,279 +0,0 @@
# AI Infra 训练平台建设方案
## 1. 愿景与目标
### 1.1 愿景
构建一套**端到端的智能化AI训练平台**,将分散的训练框架、资源调度、监控运维、数据管理能力整合为统一的标准化流水线,让大模型训练算法团队**专注于模型创新而非基础设施运维**,同时与现有运维智能体深度协同,实现训练任务的智能化运维闭环。
### 1.2 核心目标
| 目标维度 | 描述 |
|---------|------|
| **效率提升** | 训练任务从准备到启动时间缩短 70%,故障恢复时间缩短 50% |
| **标准化** | 建立统一的训练流程规范,消除"人人一套环境"的混乱局面 |
| **可观测性** | 全链路监控覆盖,训练状态、资源利用、异常事件一目了然 |
| **智能运维** | 与运维智能体对接,实现断训自动分析、故障智能诊断 |
---
## 2. 整体架构概览
```
┌─────────────────────────────────────────────────────────────────────┐
│ 用户交互层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Web 前端 │ │ CLI 工具 │ │ API 接口 │ │
│ │ (任务提交) │ │ (高级用户) │ │ (自动化集成) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 平台服务层 │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ 任务调度 │ │ 数据管理 │ │ 模型管理 │ │
│ │ (SkyPilot) │ │(schema/dataset)│ │ (版本/产物) │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ 镜像管理 │ │ 指标追踪 │ │ 日志中心 │ │
│ │(Local Registry)│ │ (W&B) │ │ (集中采集) │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 智能运维层 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 运维智能体对接 │ │
│ │ • 断训自动分析 • 故障根因定位 • 资源利用率优化建议 │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 基础设施层 │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Kubernetes │ │ GPU 集群 │ │ 分布式存储 │ │
│ │ (容器编排) │ │ H20/A6000/H100│ │ (JuiceFS) │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 3. 用户故事
### 3.1 算法工程师视角
> **作为**一名算法工程师,
> **我希望**通过简单的界面配置就能提交一个多节点 RLHF 训练任务,
> **以便于**我可以专注于模型和数据本身,而不是花大量时间在环境配置和资源协调上。
**验收标准:**
- [ ] 在 Web 界面上选择数据集、模型、训练配置
- [ ] 一键提交后,系统自动完成资源分配、镜像拉取、任务启动
- [ ] 实时查看训练进度曲线和关键指标
---
> **作为**一名算法工程师,
> **我希望**训练中断时能快速定位问题原因,
> **以便于**减少排查时间,尽快恢复训练。
**验收标准:**
- [ ] 系统自动检测训练中断事件
- [ ] 智能体自动分析中断原因OOM、网络故障、硬件异常等
- [ ] 提供可操作的恢复建议
---
> **作为**一名算法工程师,
> **我希望**启动一个 Notebook 环境调试代码.
> **以便于**小规模试跑训练,测试训练数据集、调整模型参数
**验收标准:**
- [ ] Notebook容器启动速度
- [ ] 开发容器内置依赖包完善度,按照新包
---
### 3.2 团队负责人视角
> **作为**团队负责人,
> **我希望**能够看到所有训练任务的整体资源利用情况,
> **以便于**合理规划算力资源,识别资源浪费。
**验收标准:**
- [ ] 仪表盘展示各集群 GPU 利用率趋势
- [ ] 任务队列可视化,等待/运行/完成状态一目了然
- [ ] 资源使用报表按项目/用户统计
---
### 3.3 运维工程师视角
> **作为**运维工程师,
> **我希望**训练任务的监控数据能自动接入现有运维系统,
> **以便于**统一管理,减少割裂的监控工具。
**验收标准:**
- [ ] 训练任务指标自动推送到运维智能体
- [ ] 异常告警自动触发智能体分析流程
- [ ] 与现有运维系统数据互通
---
## 4. 里程碑规划
### 里程碑总览
```
M1 M2 M3 M4
│ │ │ │
────●──────────────────●──────────────────●──────────────────●────────▶
│ │ │ │
基础设施就绪 训练流水线上线 监控运维闭环 智能化运维
```
---
### M1: 基础设施就绪
**目标:** 完成底层平台搭建,具备运行训练任务的基础能力
| 交付物 | 说明 |
|-------|------|
| K8S 集群 | H20 集群上部署 Kubernetes支持 GPU 调度 |
| 本地 Registry | 内网镜像仓库,解决镜像拉取问题 |
| JuiceFS/MinIO | 分布式存储,数据集和模型 checkpoint 持久化 |
| 基础镜像 | veRL 训练镜像,预置常用依赖 |
**关键验证点:**
- 能够手动在 K8S 上启动单节点训练任务
- 数据从 JuiceFS 正常读写
- 镜像从本地 Registry 正常拉取
- 引入 Volcano 或 Kueue并配置 Gang Scheduling 策略实现All-or-Nothing 的资源分配
- 确认JuiceFS 的本地 SSD 缓存策略在其中一台机器部署MiniIO单节点另外两台机器上部署JuiceFS client
- 网络通信支持 RoCE/InfiniBand
- Notebook 交互式开发环境
---
### M2: 训练流水线上线
**目标:** 用户可通过前端提交和管理训练任务
| 交付物 | 说明 |
|-------|------|
| SkyPilot 集成 | 任务调度与资源编排 |
| W&B 本地服务 | 训练指标追踪与可视化 |
| 任务管理前端 | 数据上传、任务提交、进度查看、日志查看 |
| 数据管理模块 | 支持从 HuggingFace 链接或 FTP 导入数据集 |
**关键验证点:**
- 端到端完成一次多节点 SFT 训练
- 通过前端提交任务,查看 W&B 训练曲线
- 训练日志完整保存并可查询
- 多租户、项目制配额管理功能
---
### M3: 监控运维闭环
**目标:** 实现任务全生命周期监控,与运维智能体初步对接
| 交付物 | 说明 |
|-------|------|
| 资源监控 | GPU 利用率、显存、网络带宽实时采集 |
| 日志采集 | 训练日志集中存储,支持检索 |
| 智能体对接 | 断训事件自动推送,触发智能体分析 |
| 告警机制 | 异常状态OOM、任务卡死等自动告警 |
**关键验证点:**
- 训练任务异常时5 分钟内收到告警
- 断训事件自动生成分析报告
- Grafana 仪表盘展示集群整体健康状态
- sidecar方式部署 DCGM Exporter 来获取细粒度指标自动采集到Prometheus
- 断训时“保留现场”机制,供人工/智能体排查介入
---
### M4: 智能化运维
**目标:** 深度整合运维智能体,实现智能调度与自愈
| 交付物 | 说明 |
|-------|------|
| 故障自愈 | 常见故障自动处理(如重新调度到健康节点) |
| 智能调度 | 基于历史数据优化任务资源分配 |
| 根因分析 | 复杂故障场景的深度分析能力 |
| 容量预测 | 基于任务趋势预测算力需求 |
---
## 5. 约束与风险
### 5.1 已知约束
| 约束项 | 影响 | 应对策略 |
|-------|------|---------|
| **内网环境** | 无法直接访问HF/Dockerhub/Github资源模型、数据集 | 本地 Registry + 数据导入工具 |
| **算力平台限制** | 现有平台调度能力有限 | 引入 SkyPilot 作为上层调度 |
| **数据持久化** | 需要可靠的分布式存储 | JuiceFS + MinIO 方案 |
### 5.2 潜在风险
| 风险 | 可能性 | 影响 | 缓解措施 |
|-----|-------|------|---------|
| K8S 与现有系统集成复杂 | 中 | 高 | 先在 H20 集群小范围验证 |
| 智能体接口适配工作量大 | 中 | 中 | 早期明确接口规范,持续对齐 |
| 用户习惯迁移阻力 | 低 | 中 | 渐进式推广,保留手动模式 |
---
## 6. 资源与依赖
### 6.1 硬件资源
| 集群 | 配置 | 用途 |
|-----|------|------|
| H20 集群 | 2 节点 × 8 卡 = 16 卡 | 主力训练集群,首期部署目标 |
| A6000 集群 | 2 节点 × 4 卡 = 8 卡 | 开发测试、小规模实验 |
| H100 集群 | 多节点 | 目前仅提供容器方式不确定能否提供KubeConfig接入大规模训练 |
### 6.2 外部依赖
| 依赖项 | 状态 | 负责方 |
|-------|------|-------|
| yd运维智能体接口 | 已有基础 | |
| argus运维系统 | 已有 | 运维团队 |
---
## 7. 成功标准
### 阶段一完成标准M1 + M2
- [ ] 算法工程师可通过 Web 界面完成 SFT/RLHF 训练全流程
- [ ] 任务提交到开始训练时间 < 10 分钟
- [ ] 训练指标实时可视化,延迟 < 1 分钟
- [ ] 至少完成 3 个实际项目的验证使用
### 阶段二完成标准M3 + M4
- [ ] 断训事件 100% 自动检测并推送智能体
- [ ] 常见故障OOM、节点失联自动生成分析报告
- [ ] GPU 整体利用率提升 20%(通过更好的调度)
- [ ] 平均故障恢复时间MTTR缩短 50%
---
## 附录:关键技术选型
| 领域 | 选型 | 选型理由 |
|-----|------|---------|
| 容器编排 | Kubernetes | 业界标准,生态成熟 |
| 任务调度 | SkyPilot | 专为 ML 场景设计,支持多集群 |
| 分布式存储 | JuiceFS + MinIO | 兼容 POSIX适合训练场景 |
| 实验追踪 | W&B (自部署) | 功能完善,团队已有使用经验 |
| 镜像仓库 | Harbor / Registry | 内网环境必需 |
| 训练框架 | veRL / Megatron | 支持 RLHF与现有工作对齐 |

View File

@ -1,18 +0,0 @@
目标设计一套AI任务调度和管理系统
环境在K8S集群商业化封装的算力平台上运行若干个GPU容器和CPU容器
约束CPU容器可以暴露端口供desktop访问CPU容器可以通过ssh访问GPU容器容器的生命周期在单独的算力平台控制台上管理所有容器
有共享的NFS文件系统
目标在CPU容器上部署一个web server 向desktop提供服务后端运行一个类似skypilot的程序能够注册和管理GPU容器实例进来。
- 用户在desktop上通过网站提交执行模型训练的任务任务管理程序会将任务加入调度队列等资源池有空闲资源的时候就会调度任务到对应的GPU节点上执行任务。任务有单节点任务和多节点任务两类预先的节点也提前配置好了多节点任务的容器之间是专门
走的IB网络。
任务完成技术选型主要是普通的skypilot不支持现成的ssh节点管理。你调研是否有节点满足需求的任务调度管理方案。
补充:运行的任务一般是 verl / skyRL 强化学习训练框架以及llamafactory 框架等。GPU节点容器假设是已经预先安装好了对应的环境依赖任务管理器只需要调度和查看任务执行情况还有监控任务是否健康有没有任务中断。
未来规划:
- 统一数据集管理,包括模型库、数据集库
- 统一日志采集&规范
- 统一容器GPU/CPU指标采集
- 统一的 weight & bias 指标服务器
结构图:见附件

View File

@ -1,315 +0,0 @@
# 关于MinIO + JuiceFS 在H20部署
这是一个架构设计的关键决策点。在 **H20 节点运行 K3s** 的前提下,我建议采取 **“存储组件独立部署Outside客户端插件化接入Inside”** 的混合策略。
简单来说:**MinIO 和 Redis 跑在 K3s 外面(用 DockerJuiceFS Client 通过 CSI 跑在 K3s 里面。**
以下是详细的架构拆解和理由:
### 1\. 架构图解
```mermaid
graph TD
subgraph "物理层 / Host OS (H20 Node)"
NVMe[NVMe SSD (物理盘)]
end
subgraph "K3s 集群 (容器层)"
Pod[训练任务 Pod]
CSI[JuiceFS CSI Driver (DaemonSet)]
Pod -- "PVC 挂载" --> CSI
end
subgraph "独立 Docker 容器 (非 K3s)"
Redis[Redis 容器]
MinIO[MinIO 容器]
end
%% 关键数据流
CSI -- "1. 读写缓存" --> NVMe
CSI -- "2. 网络 IO" --> MinIO
CSI -- "3. 元数据 IO" --> Redis
%% 避免环路
MinIO -. "数据持久化" .- NVMe
```
-----
### 2\. 组件部署位置详解
#### A. MinIO & Redis建议 **独立于 K3s 部署 (Outside)**
**方式**直接在宿主机Host OS上使用 `docker run``docker-compose` 启动,或者使用 `systemd` 管理。
**理由**
1. **爆炸半径隔离(最重要)**AI 实验初期K3s 集群可能会因为 CNI 网络插件配置错误、Device Plugin 崩溃、或者各种误操作导致集群不可用Crash。如果存储后端MinIO跑在 K3s 里K3s 一挂,你的数据就“锁”在里面了,连日志都拷不出来。
2. **资源竞争保护**K8s 的调度器虽然有 QoS但在只有 2 个节点的情况下H20 的高负载很容易导致控制面不稳。把 MinIO 独立出来,保证存储服务的 CPU/内存资源不受 K3s 内部 Pod 挤压。
3. **网络直通**:宿主机直接部署 MinIO使用的是 `host` 网络,少了一层 K8s Service/Ingress 转发,对于大带宽吞吐稍微有一点点性能优势。
#### B. JuiceFS Client建议 **集成在 K3s 内部 (Inside)**
**方式**:使用 **JuiceFS CSI Driver**
**理由**
1. **符合“平台化”愿景**:你的目标是做一个 "AI Infra 平台"。对于上层用户(算法工程师),他们不应该关心宿主机挂载了什么目录。他们提交的 YAML 只需要写:
```yaml
volumes:
- name: data
persistentVolumeClaim:
claimName: pvc-juicefs
```
这是标准的云原生体验。
2. **权限与隔离**CSI Driver 可以更好地处理容器内的挂载点权限问题。如果手动在宿主机 `mount` 然后用 `hostPath` 映射进容器,经常会遇到 `Permission Denied` 或者 `root` 用户归属问题,非常头疼。
3. **自动缓存管理**CSI Driver 可以通过配置 `StorageClass`,自动帮你处理缓存路径的挂载和清理。
-----
### 3\. 实操配置指南 (关键点)
既然你只有两台 H20 并且要上 K3s以下是落地的关键配置
#### 第一步:在宿主机启动 MinIO 和 Redis (后端)
不要用 Helm直接写一个 `docker-compose.yml` 放在 `/opt/ai-infra/` 下运行:
```yaml
version: '3'
services:
minio:
image: minio/minio
network_mode: "host" # 直接用宿主机网络,性能最好
volumes:
- /mnt/data1/minio_data:/data # 挂载到大容量HDD或SATA SSD
command: server /data --console-address ":9001"
redis:
image: redis:7
network_mode: "host"
volumes:
- /mnt/data1/redis_data:/data
command: redis-server --appendonly yes
```
#### 第二步:在 K3s 中安装 JuiceFS CSI Driver (前端)
1. **安装 CSI Driver**
```bash
helm repo add juicefs https://juicedata.github.io/charts/
helm install juicefs-csi-driver juicefs/juicefs-csi-driver -n kube-system
```
2. **创建 Secret (连接信息)**
```yaml
apiVersion: v1
kind: Secret
metadata:
name: juicefs-sc-secret
namespace: default
type: Opaque
stringData:
name: "myjfs"
metaurl: "redis://<宿主机IP>:6379/1" # 注意这里填宿主机内网IP
storage: "minio"
bucket: "http://<宿主机IP>:9000/mybucket"
access-key: "minioadmin"
secret-key: "minioadmin"
```
3. **创建 StorageClass (核心中的核心)**
这里要配置**本地缓存**,让 JuiceFS 能够利用 H20 的 NVMe。
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: juicefs-sc
provisioner: csi.juicedata.com
parameters:
csi.storage.k8s.io/node-publish-secret-name: juicefs-sc-secret
csi.storage.k8s.io/node-publish-secret-namespace: default
csi.storage.k8s.io/provisioner-secret-name: juicefs-sc-secret
csi.storage.k8s.io/provisioner-secret-namespace: default
# === 关键性能参数 ===
cache-dir: /mnt/nvme0/juicefs-cache # H20 宿主机上的 NVMe 路径
cache-size: "2048000" # 2TB 缓存上限
buffer-size: "512" # 写缓冲
prefetch: "2" # 预读并发度
```
### 4\. 备选方案:如果 CSI 搞不定怎么办?
如果 M1 阶段你发现 CSI Driver 调试太麻烦(有时候 K3s 的路径和标准 K8s 不一样,导致 CSI 挂载失败),可以退回到 **“Host Mount + HostPath”** 模式:
1. **Outside**: 在宿主机直接运行 `juicefs mount ... /mnt/ai-data`
2. **Inside**: 在 Pod YAML 里直接映射:
```yaml
volumes:
- name: data
hostPath:
path: /mnt/ai-data
type: Directory
```
**结论:**
优先尝试 **CSI Driver** 方案这才是正规军的做法能为你的“平台化”愿景打下基础。MinIO/Redis 请务必保持独立,不要让 K3s 的波动影响到底层数据安全。
----------------
# 关于Minio扩容
这是一个非常关键的运维问题。
首先要泼一盆冷水(也是为了避坑):**MinIO 不支持从“单机模式Standalone”无缝平滑扩容到“分布式模式Distributed”**。
* **单机模式**下MinIO 是直接把文件存成普通文件。
* **分布式模式**下MinIO 会启用**纠删码Erasure Coding**,把文件切片打散存储。
底层数据结构完全不同,因此你**不能**简单地把新节点加入旧集群。你必须通过\*\*“建立新集群 -\> 数据迁移 -\> 切换流量”\*\*的方式来完成。
鉴于你使用了 JuiceFS这个迁移过程其实比纯 MinIO 迁移要灵活得多。以下是针对你 H20/K3s 环境的最佳实战方案:
-----
### 方案概览:蓝绿部署迁移法
我们不修改旧的 MinIO而是搭建一个新的 3 节点集群,同步数据,然后改一下 K8s Secret 指向新集群。
#### 前置条件
假设你现有的环境是:
* **Old MinIO**: 跑在 Node A (192.168.1.10)
* **New Target**: 准备在 Node A, Node B, Node C 上跑 3 节点分布式 MinIO。
-----
### 第一步:搭建全新的 3 节点 MinIO 集群
由于旧的 MinIO 还在通过 `host` 网络运行(占用 9000 端口),新集群如果不关掉旧的,需要用**不同端口**(比如 9100或者部署在不同机器上。
假设你新增了机器,或者错开端口。以下是 3 节点分布式 MinIO 的 `docker-compose.yml` 示例(需要在三台机器上都运行):
```yaml
version: '3'
services:
minio-distributed:
image: minio/minio
network_mode: "host"
hostname: "node1" # 另外两台改为 node2, node3
# 关键:分布式启动命令,必须列出所有节点
command: server http://192.168.1.10:9100/data http://192.168.1.11:9100/data http://192.168.1.12:9100/data --console-address ":9101"
volumes:
- /mnt/data_new:/data # 挂载新的数据盘(或者旧盘的新目录)
environment:
MINIO_ROOT_USER: "admin"
MINIO_ROOT_PASSWORD: "strongpassword"
```
*注意3 节点 MinIO 允许挂掉 1 台机器而不丢失数据。*
-----
### 第二步:数据迁移 (两种路径)
鉴于你用的是 JuiceFS这里有**两条路**可选:
#### 路径 A底座迁移推荐速度快原汁原味
直接搬运 MinIO 里的对象块Block。因为 JuiceFS 把数据切成了固定的 Block 存在 MinIO 里,我们只需要把这些 Block 从旧 MinIO 搬到新 MinIO**不需要经过 JuiceFS 客户端**。
1. **安装 `mc` (MinIO Client)** 命令行工具。
2. **配置别名**
```bash
mc alias set oldm http://192.168.1.10:9000 minioadmin minioadmin
mc alias set newm http://192.168.1.10:9100 admin strongpassword
```
3. **全量镜像 (Mirror)**
```bash
# 创建新桶
mc mb newm/mybucket
# 开始同步数据 (将旧桶数据镜像到新桶)
# --watch 参数可以持续监听增量数据,适合不停机迁移
mc mirror --watch oldm/mybucket newm/mybucket
```
#### 路径 BJuiceFS 层面迁移(适合要换云厂商/存储类型)
如果你想顺便整理数据碎片,或者从 MinIO 迁移到 阿里云 OSS可以用这个。但在你的场景下路径 A 更快。
* 命令:`juicefs sync minio://... minio://...` (不推荐,因为需要解密再加密,消耗 CPU)。
-----
### 第三步:停机切换 (Cutover)
为了保证 100% 数据一致性建议申请一个短时间的维护窗口10-20分钟
1. **停止训练任务**Scale down 所有的 Training Job。
2. **停止旧 MinIO 写入**
* 确保 `mc mirror` 已经追平了数据(没有 pending
* 你可以把旧 MinIO 设为只读,或者直接停止旧容器。
3. **最后一次 Sync**
* 运行 `mc mirror --overwrite` 确保最后一点差异也同步了。
4. **修改 K8s Secret**
* 这是 JuiceFS 的魔力所在。你**不需要**重新格式化 JuiceFS也**不需要**修改元数据引擎Redis。你只需要告诉 JuiceFS“底下的对象存储换地方了”。
* 编辑 K8s 里的 Secret
<!-- end list -->
```bash
kubectl edit secret juicefs-sc-secret
```
* 修改 `bucket``access-key/secret-key`
<!-- end list -->
```yaml
stringData:
# Redis 配置保持不变!元数据还在那里
metaurl: "redis://192.168.1.10:6379/1"
# 修改 bucket 地址为新集群地址
# 注意MinIO 分布式集群通常前面会加一个 Nginx 做负载均衡
# 或者你可以填任意一个节点的 IP (MinIO 会自动转发)
bucket: "http://192.168.1.10:9100/mybucket"
access-key: "admin"
secret-key: "strongpassword"
```
5. **重启 CSI Driver**
* 删除 JuiceFS CSI 的 Pod让它们重建并加载新的 Secret。
* 删除使用该 PVC 的 Pod。
-----
### 第四步:验证与回收
1. **验证**
* 启动一个测试 Pod挂载 PVC。
* `ls` 一下目录,确认文件都在。
* `cat` 一个文件确认能读JuiceFS 会去新 MinIO 拿数据块)。
2. **回收**
* 确认一切正常运行几天后。
* 下线旧的单节点 MinIO 容器。
* 释放旧的磁盘空间。
### 专家建议 (Pro Tips)
1. **负载均衡 (Load Balancer)**
* 到了 3 节点分布式阶段,建议在 3 台 MinIO 前面架设一个 **Nginx** 或者 **HAProxy**
* 这样 JuiceFS Client 连接的是 `http://nginx:9000`Nginx 轮询转发给后端 3 台 MinIO。
* **好处**:如果某台 MinIO 挂了Nginx 自动剔除,训练任务完全无感。如果直连某台 IP那台挂了训练就断了。
2. **元数据备份**
* 在做这一切之前,**务必备份 Redis 的 dump.rdb**。
* 只要 Redis 在JuiceFS 的文件结构就在。对象存储里的数据块丢了还能通过文件名找回来(虽然内容坏了),但 Redis 丢了,对象存储里那一堆 `chunk-xxx` 的文件就是一堆毫无意义的二进制垃圾,神仙难救。
3. **拓扑限制**
* MinIO 扩容通常是“倍增”或者“对等扩容”。比如 4 节点扩容,通常是再加 4 节点(变成 2 个 Server Pool
* 所以,规划 3 节点时,最好磁盘大小一致,网络环境一致。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

View File

@ -1,34 +0,0 @@
# milestones
通过以下几个里程碑来梳理和分析确认可行性最终目标是产出一套基于Native Ray集群无k8s底座的verl 训练平台支持多用户运行各类verl任务提高整体集群的资源利用效率并且能够通过监测系统进行观察和资源统计监控报警。未来形成运维SOP后接入运维智能体执行自动化运维。
- Workload
- ppo on ray
- grpo on ray
- sft on ray 可行性
- model serving on ray
- customize code 自定义代码任意verl example 提交代码
- 自定义reward function
- 同时多verl版本支持同时跑不同的ray任务但是使用不同版本的verl甚至是用户魔改版本
- Ray Job管理
- 通过python api提交而不是通过ray cli提交
- 任务排队机制。无优先级多个pending job谁先满足资源就谁先执行。
- 【确认支持】gang scheduling (all or nothing), 指定好trainer.nnodes和trainer.n_gpus_per_node参数不满足就pending。
- 无配额管理、公平调度等特性。
- Ray本身不支持任务超时参数需要单独job监控发现超时才停止。
- Pipeline管理【高级, 暂不实现】
- 提供对Ray Job进一步封装串联多个Ray Job自动完成训练模型合并等job串联
- 可观测性 Observability
- 测试本地部署 weight and bias server 可行性如何集成现有job流程
- 测试部署 prometheus & grafana对ray节点进行监测
- job监控哪些job使用了多少资源跑了多长时间资源利用率是否充分是否空占着GPU
- 数据、模型存储管理
- shared dataset管理所有用户共享的hf数据集
- hf 模型管理所有用户共享的hf 基座模型库
- user dataset 管理: 用户独自的数据集管理
- user 模型管理:用户独自的模型管理,保存训练好的模型
- job 作业数据管理,作业产出物,临时目录数据
- user management用户可以通过统一界面来管理自己是user dataset/model space和自己运行的job的临时目录从而灵活组织任务流水线提供灵活的文件查看方式
- 网络
- 确认是否支持IB(H100环境)以及RoCEv2H20环境需要怎样配置

View File

@ -1,348 +0,0 @@
# MVP RoadmapV1 → V2 → … → 训练平台)
本文档在 `specs/mvp/milestones.md` 的草稿基础上做**扩展与细化**把目标拆成可迭代的版本MVP v1/v2/…),保证每个版本都能**独立运行、可验证验收**,并且在上一版本基础上演进。
> 总目标North Star产出一套**基于 Native Ray 集群(无 K8s 底座)**的训练平台,面向多用户,支持 `verl` 各类训练/评测/Serving 工作负载,提升集群利用率,并通过可观测系统实现资源统计、监控告警,最终形成运维 SOP 并可接入运维智能体做自动化运维。
---
## 0. 关键原则(贯穿所有版本)
1) **版本可独立运行**:每个版本都能从“空环境”按文档跑起来(不依赖未来能力)。
2) **验收可客观验证**:每个里程碑必须有明确的 DoDDefinition of Done与可复现步骤。
3) **强制产物落盘**:模型/数据/日志/ckpt 必须可追踪、可复用、可审计(基于共享存储/NFS
4) **Head 不参与计算**Head 只承担控制面GCS/Dashboard/Job server避免训练抢占控制面资源。
5) **按 submission id 组织作业**:作业输出目录与 Ray submission id 绑定,方便检索、回收、归档。
6) **“先把 RL 跑稳”,再扩 workload**:先 PPO已验证再 GRPO/SFT/Serving。
---
## 0.1 里程碑总览(建议交付顺序)
| 版本 | 定位 | 关键交付 | 核心验收点 |
|---|---|---|---|
| v1 | 可复现实验闭环 | Ray 集群 + PPO 跑通 + 持久化 | driver 不在 head产物落盘 |
| v1.1 | 实验工程化 | JobSpec 模板 + 新增 1 个 workload | 可回归、可定位、可扩展 |
| v2.0 | 服务化入口 | API + Ray Jobs SDK | API 提交/查询/停止可用 |
| v2.1 | 节点纳管 | SSH 注入 + 资源池/标签 | 节点上线/下线、gang 约束 |
| v3.0 | 平台雏形 | 队列 + 超时 + 最小多用户 | pending→running 自动调度 |
| v3.1 | 可扩展平台 | 自定义代码/reward + 多版本 | 多版本并存、插件可用 |
| v4.0 | 可运营平台 | Prom/Grafana + W&B | 资源核算/告警/归档 |
| v4.1 | 可交接平台 | SOP + 自动化运维接口 | 非开发可按 SOP 运维 |
| v5.0 | 长期形态 | Serving + Pipeline | 训练→发布推理闭环 |
## 1. 当前基线MVP v1已完成/已验证)
### 1.1 目标
在单机(或同一宿主机)用 3 个容器跑通:
- Ray head无 GPUCPU=0/GPU=0
- 2 个 Ray worker每个 4 GPU
- 通过 **head 上的 `ray job submit`** 提交 `verl` PPO`total_epochs=1`
- 通过 **entrypoint 自定义资源**强制 driver 在 worker 上
- 数据/模型/日志/ckpt 全部持久化
### 1.2 交付物repo 中已存在)
- 脚本与 compose`src/mvp/v1/`
- 行动与验收文档:`specs/mvp/v1/v1_action.md`
- 共享目录约定:`shared/datasets``shared/hf``shared/jobs` 等(与 NFS 对齐)
### 1.3 验收口径(摘要)
- `ray job list``driver_info.node_ip_address` ∈ worker IP且 ≠ head IP
- 训练输出落在 `/mnt/shared/jobs/<submission_id>/...`
- checkpoint 按 `save_freq` 产生(避免爆磁盘)
---
## 2. MVP v1.1Hardening + 多 workload 可行性验证)
> 目标:把 v1 从“实验脚本”升级成“可长期回归的最小系统”,并验证更多 workload 的可行性边界。
### 2.1 主要能力
- Workload 扩展(可选顺序):
- PPO回归金标
- GRPO on Ray可运行验证
- SFT on Ray可运行验证`llamafactory``verl` 相关 SFT 路径)
- 作业模板化(最小实现):
- 统一 JobSpecYAML/JSON描述workload 类型、资源nnodes/n_gpus_per_node、数据、模型、输出目录、超时
- 仍然用 `ray job submit`,但把 entrypoint 组装逻辑标准化
- checkpoint 策略与磁盘保护:
- 默认 `save_freq` ≥ 10或按训练总 steps 的比例)
- 明确保留策略(至少提供“保留最后 N 个 ckpt”的配置建议/脚本)
- “失败可定位”:
- 统一收敛日志入口Ray job logs + hydra 日志目录 + 关键参数快照)
- 失败时能定位:是资源不足 / NCCL / 数据 / 模型 / 配置错误
### 2.2 验收DoD
- 同一套脚本在同一台机器能连续跑 3 次 PPO 回归,产物目录不互相覆盖
- 至少新增 1 个 workloadGRPO 或 SFT可以跑通 “启动→训练→落盘” 闭环
- 作业目录内包含:
- `config/submit_cmd.txt`(或 job spec 快照)
- `logs/`(可追踪)
- `checkpoints/`(按策略生成)
---
## 3. MVP v2.0Control Plane 服务化API + Ray Jobs SDK
> 目标:从“人跑脚本”升级为“服务提交任务”。依然是 Native Ray 集群,但引入一个最小控制平面服务。
### 3.1 系统形态
- Control Plane建议部署在 head/CPU 机器):
- FastAPI 服务REST
- Job 管理:用 Ray Jobs **Python SDK** 提交/查询/停止(不再依赖 CLI 文本解析)
- 节点视图:读取 Ray statenodes, actors, placement groups
- Data Plane
- 仍然是预先启动的 worker 节点加入集群(先不做 SSH 动态纳管也可)
### 3.2 APIMVP 级别)
- `POST /v1/jobs`:提交 JobSpecppo/grpo/sft
- `GET /v1/jobs`:列表(含状态、资源、开始/结束时间)
- `GET /v1/jobs/{id}`详情含输出目录、driver node
- `POST /v1/jobs/{id}:stop`:停止作业
### 3.3 验收DoD
- API 提交 PPO返回 submission id输出目录为 `/mnt/shared/jobs/<submission_id>/...`
- API 查询 job 状态与 driver node必须是 worker
- 停止 job 后,资源释放、状态可见
---
## 4. MVP v2.1SSH 纳管 + 资源池 + Gang 约束)
> 目标对齐你草稿里“SSH 纳管”的约束与需求:控制面能纳管 GPU 节点,形成可运营的资源池。
### 4.1 节点纳管SSH Provisioner
- 控制面保存 NodeSpecip/user/port/labels/gpu_count
- 通过 SSH 执行:
- `ray start --address=<head>:6379 --resources=...`
- `ray stop`drain/下线)
- 维护节点状态机:`pending → online → draining → offline`
### 4.2 资源池与 gangAll-or-nothing
- 资源池最小模型:
- pool 标签(如 `pool_a``h20``ib_domain_1`
- 提交 job 时指定 pool 约束
- Gang 约束MVP 实现方式):
- job spec 明确 `trainer.nnodes` + `trainer.n_gpus_per_node`
- 提交前检查 Ray 可用资源是否满足,不满足则进入 pending 队列(见 v3.0
### 4.3 验收DoD
- 通过 API 注册 2 个 workerSSH 注入 ray start`ray status` 可见节点上线
- 通过 API 下线节点,节点被标记不可调度且不再分配新 job
- gang 不满足时 job 不提交(或提交后一直 pending满足后可运行
---
## 5. MVP v3.0(调度与多用户:队列 + 超时 + 最小权限)
> 目标:平台开始“像个平台”:多用户、队列、超时、审计。仍然不做复杂配额/公平调度。
### 5.1 作业队列(简单但可用)
- FIFO 队列:无优先级
- “资源满足就调度”:谁先满足谁先跑(可接受非严格 FIFO
- job 超时Ray 原生不支持统一 timeout草稿已指出因此控制面需
- 记录 start_time
- 定期扫描超时 job → `stop`
### 5.2 多用户最小闭环
- 认证MVPtoken 或 basic auth先不做复杂 RBAC
- 归属与隔离(文件层):
- `/mnt/shared/users/<user>/datasets/`
- `/mnt/shared/users/<user>/models/`
- `/mnt/shared/jobs/<submission_id>/` 记录 user/metadata
### 5.3 验收DoD
- 2 个用户可各自提交 job能看到自己的 job 列表与输出目录
- 超时策略可触发(模拟短 timeoutjob 被停止且状态标记为 timeout
- 队列在资源不足时保持 pending资源释放后自动运行
---
## 6. MVP v3.1(可扩展性:自定义代码/Reward、多版本 VERL
> 目标:把“平台内置 workload”升级成“用户可提交自定义代码与 reward”并支持多版本并存。
### 6.1 自定义代码提交(最小实现)
两种方式二选一(建议先做 A
- A`working_dir` 指向 NFS 上的代码快照目录(用户自己准备/上传)
- B上传 zip控制面落到 NFS 并解压为 code snapshot
### 6.2 多版本 VERL 并存
约束前提:**基础镜像保持同一个**(生产环境容器由算力平台创建时已固定镜像标签)。
目标:在同一 Ray 集群内,不同 job 可以使用不同版本的 `verl`(例如不同分支/commit 或用户魔改版)。
已确认优先方案A**必须通过 Ray Job 的 `runtime_env.env_vars` 透传 `PYTHONPATH`**,让 job 粒度优先 import 指定代码快照。
建议方案(以 NFS 为中心,最小可行实现):
- 在共享存储上以“不可变快照”的方式存放代码版本(推荐 commit hash 命名):
- `${SHARED_ROOT}/common/code/verl/<commit>/...`
- `${SHARED_ROOT}/users/<user>/code/verl/<commit>/...`(用户魔改版)
- JobSpec 增加 `code_path`(指向上述目录),控制面在提交 job 时注入(必须走 runtime_env
- `runtime_env.env_vars.PYTHONPATH = "<code_path>:$PYTHONPATH"`(把 code_path 放最前面,确保 import 优先级)
示例(概念性,实际以 `${SHARED_ROOT}` 为准):
```bash
CODE_PATH="${SHARED_ROOT}/common/code/verl/<commit>"
ray job submit \
--address="http://127.0.0.1:8265" \
--submission-id="<submission_id>" \
--runtime-env-json='{"env_vars": {"PYTHONPATH": "'"${CODE_PATH}"':$PYTHONPATH"}}' \
-- \
python3 -m verl.trainer.main_ppo ...
```
需要验证的关键点(作为 v3.1 的 DoD 之一):
- 同时运行两个 job
- jobA 使用 `<commitA>`jobB 使用 `<commitB>`
- 互不影响,且各自训练/日志/ckpt 正常
- job 粒度是否能做到“依赖隔离”(至少做到 `verl` 版本隔离;第三方依赖冲突可先假设镜像内一致)
> 备注:当前 v1 的做法是容器内全局 `pip install -e /workspace/verl`,这会让所有 job 默认使用同一份 `verl`。要实现多版本并存,必须让 job 的 import 优先使用 `code_path`(或为每个 job 单独创建 venv/安装 wheel后者更重建议后置
### 6.3 自定义 reward function
- JobSpec 支持 `reward_fn_path`Python 模块路径)
- `reward_fn_path` 可指向共享存储中用户自定义代码目录(例如 `${SHARED_ROOT}/users/<user>/code/...`
- 约束:代码必须在 job runtime 中可 import`working_dir`/`PYTHONPATH` 或 runtime_env 保障)
- 控制面校验模块可导入basic lint/安全白名单可后置)
### 6.4 验收DoD
- 同时运行两个 job使用不同的 `verl` 代码版本(或用户魔改版本),互不影响
- 用户可在 JobSpec 中替换 reward function 并跑通一个最小训练闭环
---
## 7. MVP v4.0可观测性Prometheus/Grafana + W&B 集成)
> 目标:平台可运营:能回答“谁在用多少资源、跑了多久、利用率如何、是否空占 GPU”。
### 7.1 指标与监控
- Ray 指标接入 Prometheus节点/任务/actor
- GPU 指标nvidia exporter 或 DCGM exporter
- DashboardGrafana至少 3 张核心面板)
- 集群总 GPU/CPU 使用率、空闲率
- 每 job 的 GPU 时间、峰值显存、运行时长
- 节点健康(心跳/掉线)与告警
### 7.2 W&B或等价集成验证
- 最小可行:单机 self-host W&B server 可用性验证
- JobSpec 支持启用/关闭 W&B并传入 project/run name
### 7.3 验收DoD
- Grafana 上能看到集群与 job 资源视图
- 某个 job GPU 利用率异常(模拟)能触发告警规则(邮件/IM/日志即可)
- W&B 指标能按 job 维度归档(至少 PPO 能上报)
---
## 8. MVP v4.1运维化SOP + 自动化运维接口)
> 目标:把平台变成“可交接”的系统:运维动作标准化,并为智能体留出接口。
### 8.1 SOP 与自动化入口
- SOP 文档:
- 节点上线/下线
- 故障定位Ray session、Ray job、NCCL、OOM
- 资源回收(停止 job、清理 ckpt
- 自动化接口(最小):
- `/v1/ops/drain_node`
- `/v1/ops/restart_ray_head`(谨慎:需要保护与权限)
- `/v1/ops/cleanup_job_artifacts`
### 8.2 验收DoD
- 按 SOP非开发人员可完成一次“节点上线→跑任务→下线→清理”
- 自动化接口至少能完成 1 个高频动作(如清理/停止/下线)
---
## 9. MVP v5.0Serving 与 Pipeline偏长期
> 目标:训练-部署一体化:支持 model serving并在平台内串联训练→评测→发布。
### 9.1 Serving
- Ray Serve或等价部署模型推理服务
- Serving 与训练共用模型库与权限(按 user/project
### 9.2 Pipeline草稿里标为高级
- Pipeline 是对多个 job 的封装训练→merge→eval→publish
- 可先实现最小 DAG两步串联作为验证
### 9.3 验收DoD
- 训练产物一键发布为一个可访问的推理 endpoint
- Pipeline 能自动串联并产出最终 artifact可回滚/可追踪)
---
## 10. 并行技术验证(建议尽早做)
这些属于“跨版本”风险项,建议在 v1.1 ~ v2.0 期间尽早做:
### 10.1 网络IB / RoCEv2
- 确认环境是否支持 IBH100或 RoCEv2H20
- 跑最小 NCCL 通信验证all-reduce / bandwidth
- 将必要的 NCCL 环境变量注入到 job runtime_env
### 10.2 Ray + 多节点容器约束
- 多容器同宿主机时的 Ray node_ip/临时目录冲突规律(已踩坑,需固化规范)
- 端口范围与防火墙策略Ray worker 端口、dashboard、metrics
---
## 11. 已确认的约束与假设(来自讨论结论)
这些会直接影响 v2.1SSH 纳管)与后续多用户/存储设计:
1) **最终形态仍以“每节点容器”运行**(不是裸机 systemd
- H20 开发环境:我们可在宿主机用 `docker compose` 自建容器,并通过 SSH 进入容器调试/纳管。
- H100 生产环境:容器由算力平台创建/回收;平台侧控制面只能 **SSH 进入这些容器** 做纳管(执行 `ray start/stop`、注入 env 等)。
2) **认证**:内部 token 即可MVP 阶段不对接 SSO
3) **存储**:只考虑 NFS。
- 开发环境NFS/共享目录可通过宿主机 bind mount 提供给容器。
- 生产环境:所有容器挂载相同 NFS容器内共享根路径为 `/private/`(需要在实现时把“共享根路径”做成可配置项,而不是写死 `/mnt/shared`)。
4) **网络拓扑约束**:暂不做按 IB 域/机架/拓扑的强约束调度(第 10.1 仍需验证 IB/RoCE 是否可用与配置方式,但调度不引入拓扑维度)。
5) **共享目录分层**:在 `users/<user>/...` 之外增加一个可读写的 `common/` 目录用于共享数据/模型/代码:
- `${SHARED_ROOT}/common/datasets/`
- `${SHARED_ROOT}/common/models/`
- `${SHARED_ROOT}/common/code/`
- 权限MVP先默认“所有内部 token 用户可读写”,后续再细化只读/受控写。
---
## 12. 仍需你确认/讨论的问题(剩余不确定项)
1) `runtime_env.env_vars` 注入对“子进程/训练框架内部启动进程”的覆盖范围是否足够?
- 需要确认 `verl`/`sglang` 等子进程是否继承 driver 的环境变量(通常会继承,但建议在 v3.1 验收时明确验证)。

View File

@ -1,133 +0,0 @@
这一版的设计采用了 **Overlay 架构 + GPFS 核心存储 + 无状态Stateless节点池** 的模式,逻辑非常自洽且具备极高的云原生弹性。
---
### **项目代号AI Infra Overlay Platform (Stateless Ray + GPFS)**
#### **阶段一:内核构建与验证 (Kernel & Verification)**
*目标:验证核心计算逻辑,跑通“提交-执行”的最小闭环。*
* **v1.1: 原型验证 (Verl Task Spec & Ray Job)**
* **核心功能**:实现基础的任务定义与提交。
* **组件**
* `Ray Job Tool (Ray Client)`:客户端工具。
* `VerlTaskSpec YAML`:定义多代码路径 (Multi-Verl Code Path) 和任务参数。
* **基础设施**Handmade Ray Cluster手工搭建的集群用于验证核心代码。
* **v2.0: 任务管理层 (Task Management)**
* **核心功能**:引入服务端,管理任务生命周期。
* **新增组件**
* `API Server`:统一接口层。
* `Task Management`:实现任务的队列 (Queue)、映射 (Map) 和重试 (Resubmit) 机制。
* **基础设施**:仍运行在手工集群上,但控制面开始服务化。
---
### **阶段二:架构质变 - 无状态节点池 (The Stateless Shift)**
*目标:通过 GPFS 实现控制反转 (IoC),彻底解耦平台层与计算节点层。这是本架构最关键的转折点。*
* **v2.5: 用户管理 & 无状态 Ray 节点池 (User Mgmt & Stateless Ray Node Pool)** * **核心机制:基于 GPFS 的服务发现 (Service Discovery)**
* **Ray Head (有状态)**:由 `Node Management` 启动(通常通过 SSH 或 K8s StatefulSet。启动后将自身的 IP 地址写入 GPFS 中的 `Head IP File`
* **Ray Worker (无状态)**
* **Stateless**Worker 容器启动时不依赖平台指令。
* **Auto Connect**:启动脚本读取 GPFS 中的 `Head IP File`,获得 Head 地址并自动加入集群。
* **Watchdog**Worker 内部运行看门狗进程,监控 Head IP 变化。如果 Head 变动Worker 自动重启或重连,实现自愈。
* **新增组件**
* `User Management`:多用户隔离。
* `GPFS`:取代了之前的 JuiceFS作为唯一的共享存储和元数据交换媒介。
---
### **阶段三:产品化与高级能力 (Productization & Advanced Features)**
*目标:发布首个正式版本,并支持大模型训练所需的复杂网络与推理能力。*
* **v3.0: 正式发布版 (Release v1.0)** * **里程碑****1st Version to Release!!**
* **核心功能**:闭环用户数据流。
* **新增组件**
* `WebUI`:可视化操作界面。
* `Data Management (SFTPGo)`:用户上传数据/代码 -> SFTPGo -> 写入 GPFS -> Ray Worker 可见。
* **基础设施**:全量切换到 `Ray Worker Node` (Stateless) + `GPFS` 的架构。
* **v3.5: 高级定制与训推一体 (Advanced Task & Serving)** * **核心功能**:支持复杂的科研需求。
* **新增组件**
* `Model Serving`:支持模型推理服务。
* `Advanced VerlTaskSpec`:支持自定义 Reward Function、自定义代码、Checkpoint 断点续训 (Resubmit from last checkpoint)。
* **网络增强**
* **IB Network Supporting**:支持 InfiniBand 网络,确保多机训练的高性能互联。
---
### **阶段四:全链路可观测性 (Full-Stack Observability)**
*目标:打开黑盒,监控基础设施与业务指标。*
* **v4.0: 系统级可观测性 (System Observability)** * **核心功能**:监控集群“活着”且“健康”。
* **新增组件**
* `Prometheus` + `Grafana` + `ELK`:指标与日志平台。
* `Exporter`:部署在 Ray Worker Node 中的监控探针(采集 GPU/CPU/GPFS IO 指标)。
* **v4.5: 算法级可观测性 (ML Observability)** * **核心功能**:监控模型“练得好不好”。
* **新增组件**
* `Weights & Bias (WanB)`:集成实验追踪工具,记录 Loss 曲线和训练参数。
---
### **阶段五:智能化运维 (AIOps)**
*目标:迈向自动化与自治。*
* **v5.0: 智能运维闭环 (Operability)** * **核心功能**:降低运维成本,提升稳定性。
* **新增组件**
* `Statistics`:集群资源利用率统计报表。
* `SOP Tools`:标准运维工具(如自动清理 GPFS 垃圾文件、僵尸节点检测)。
* `Agent`:智能运维助手(基于 LLM 的日志分析与故障诊断)。
---
### **新架构核心亮点总结**
1. **极简的节点管理**
* 利用 v2.5 的 **Head IP File + Watchdog** 机制,平台层不再需要维护复杂的 Worker IP 列表和 SSH 连接池。
* **扩缩容极其简单**只需在底层K8s/Docker增加 Worker 副本数,它们就会自动通过 GPFS 找到 Head 并加入战斗。
2. **统一的数据平面 (GPFS)**
* 从 v2.5 开始GPFS 承担了 **数据存储** (Code/Data)、**状态同步** (Head IP) 和 **检查点存储** (Checkpoints) 三大职责,架构非常收敛。
3. **高弹性 (Resilience)**
* Worker 的 **Watchdog** 机制确保了当 Head 重启或网络抖动时,集群具备自我修复能力,无需人工干预。

View File

@ -1,301 +0,0 @@
# 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 entrypointworker 上执行)
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 # 启动 APIservice
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_varsHF 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`,直到第一个结束后自动提交。
验收标准:
- 三种 workloadPPO/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 完全一致(仅目录与命名变化),避免引入不必要的不兼容。

View File

@ -1,27 +0,0 @@
# v3.6
wandb 映射目录是/vol 固定问题:
查过官方文档/公开资料后结论是wandb/localW&B local server 容器)没有提供“把服务端持
久化根目录从 /vol 改成别的路径”的官方环境变量/启动参数。官方用法一直是假设你把持久化卷
挂到容器内的固定路径 /vol例如 -v <something>:/vol。(github.com (https://github.com/
wandb/server))
需要注意区分两类“目录”:
- 服务端wandb/local 容器):持久化目录是容器内固定 /vol用于保存实例元数据、账号/初
始化信息等license 也可以用 env 配,但数据目录仍是 /vol。(github.com (https://
github.com/wandb/server))
- 训练侧wandb Python SDK / VERL 任务WANDB_DIR、WANDB_DATA_DIR 等环境变量只影响“客
户端本地生成文件/缓存”,不改变服务端容器的数据落盘路径。(docs.wandb.ai (https://
docs.wandb.ai/platform/hosting/env-vars))
所以如果你现在的约束是“只能挂 ../../shared:/private不能再额外挂 ../../shared/common/
wandb:/vol”要把 W&B 服务端数据落到 shared 下面,现实可行的路子是:
- 自定义 W&B 容器 entrypoint或 wrapper在启动前做一次 ln -s /private/common/wandb /
vol或 bind-mount 到 /vol让服务仍然写 /vol但实际落到 /private/common/wandb。
这属于“容器层改造”,不是 W&B 官方参数。
如果你允许 compose 再加一条 volume那最简单仍是保留 ../../shared:/private再额外
加 ../../shared/common/wandb:/vol服务端就无需任何改造

View File

@ -1,169 +0,0 @@
# MVP v1.1 计划Hardening + 多 Workload 可行性验证)
本目录是 `specs/mvp/v1/` 的下一步迭代:在 v1 已经跑通Ray head + 2 workerPPO on Ray持久化落盘的基础上把它升级为**可长期回归**的最小系统,并扩展至少一个新 workload 的可行性闭环。
> v1.1 的目标不是做平台服务化API/队列/多用户)——那是 v2/v3 的工作v1.1 聚焦“工程化 + 可行性边界验证 + 可观测/可排障基础”。
---
## 1. v1 基线回顾(已完成)
- 拓扑1 head无 GPUCPU/GPU=0+ 2 worker各 4 GPU
- 提交方式:必须用 head 上的 `ray job submit`
- driver 调度:通过 `worker_node` 自定义资源 + `--entrypoint-resources` 强制 driver 在 worker
- 输出:按 `submission_id` 组织到共享目录NFS
相关实现参考:
- 脚本:`src/mvp/v1/`
- 验收动作:`specs/mvp/v1/v1_action.md`
- Roadmap`specs/mvp/mvp_roadmap.md`
---
## 2. v1.1 目标(必须达成)
### 2.1 工程化Hardening
1) **JobSpec 标准化(最小)**
- 把“提交 job 需要的参数”收敛成结构化文件:
- Ray 基础配置YAMLcluster 地址、entrypoint 资源约束、runtime_env 等
- 训练 JobSpecYAMLworkload 语义与训练参数
- 至少覆盖:`submission_id`、workload 类型、资源需求、共享根路径、模型/数据路径、输出目录、超时、环境变量注入。
- v1.1 实现落点(已在 repo 里提供SDK 方式):
- RayConfig 示例:`src/mvp/v1.1/py/configs/dev.yaml`
- JobSpec 示例:`src/mvp/v1.1/py/jobspecs/{ppo,grpo,sft}.yaml`
- 提交入口:`src/mvp/v1.1/py/run.py`(在 head 容器内执行,使用 Ray Python SDK 提交)
- 设计文档:`specs/mvp/v1.1/sdk_submit_refactor.md`
2) **共享根路径抽象dev/prod 一致)**
- 引入 `SHARED_ROOT` 作为唯一共享根路径:
- dev建议也用 `/private`docker compose 把宿主机 shared 挂到容器内 `/private`,模拟生产)
- prod固定 `/private`(算力平台容器内 NFS
- 任何代码/脚本不得写死 `/mnt/shared`(允许兼容旧路径但不得作为主路径)。
3) **共享目录分层(新增 `common/``user/`**
- 在 `datasets/hf/jobs/outputs` 之外,新增一个所有用户可读写的共享区:
- `${SHARED_ROOT}/common/`:共享模型/数据/代码快照(多版本 verl / 公共数据)
- `${SHARED_ROOT}/user/`:用户自定义代码(例如 `reward_fn_path` 指向这里)
- v1.1 默认策略:先假设“所有用户可写”(后续 v3 再做权限与隔离)。
4) **可排障基础**
- 每个 job 目录必须有:
- `config/`提交命令、JobSpec 快照、关键 env_vars
- `logs/`Ray job logs + hydra logs如有
- `checkpoints/`:按 `save_freq` 控制频率(默认每 10 step
- 提供“失败快照”能力:收集 `ray status` / `ray job list` / `ray list nodes` / `ray list actors`(最少其中 2 项)写入 job 目录。
- v1.1 submitter 默认落盘:
- `${SHARED_ROOT}/jobs/<id>/config/job_spec.json`
- `${SHARED_ROOT}/jobs/<id>/config/runtime_env.json`
- `${SHARED_ROOT}/jobs/<id>/config/submit_cmd.txt`
- `${SHARED_ROOT}/jobs/<id>/logs/ray_job_submit.out`
- `${SHARED_ROOT}/jobs/<id>/debug/ray_status_{pre,post}.txt`
- `${SHARED_ROOT}/jobs/<id>/debug/ray_job_list_post.txt`
### 2.2 Workload 扩展(至少新增 1 个)
v1.1 需要新增并验收通过两个 workload都要跑通闭环
- **GRPO on Ray**(推荐优先,复用 PPO 入口,通过算法配置切换)
- 基于 `python -m verl.trainer.main_ppo`
- 通过配置覆盖:`algorithm.adv_estimator=grpo`(以及必要的 rollout 参数)
- **SFT on RayRay-native**
- 入口:`python -m verl.trainer.sft_trainer_ray`
- 参考实现:`verl/verl/trainer/sft_trainer_ray.py`(内部会 `ray.init()`
- 需要确保 `ray.init()` 连接已有集群:
- 优先:`runtime_env.env_vars.RAY_ADDRESS=auto`(配合 `ray job submit`
- 兜底:在 v1.1 的 launcher 脚本里显式 `ray.init(address="auto")` 再调用 trainer避免依赖 Ray 的 env var 行为差异)
- 重要细节Ray Job 的 entrypointdriver默认不分配 GPU因此 SFT driver 侧不要强依赖 CUDA
- 推荐:`trainer.device=cpu`driver 只做 orchestration训练由 Ray workers 占 GPU
---
## 3. v1.1 关键设计点
### 3.1 多版本代码与自定义逻辑(为 v3.1 铺路,但 v1.1 先做最小验证)
已确定优先方案A通过 **Ray Job 的 `runtime_env.env_vars`** 注入 `PYTHONPATH`
- `code_path`(例如 `${SHARED_ROOT}/common/code/verl/<commit>`
- 提交 job 时设置:
- `runtime_env.env_vars.PYTHONPATH = "<code_path>:$PYTHONPATH"`
并约定:
- `reward_fn_path` 可指向 `${SHARED_ROOT}/user/code/...` 下用户自定义代码
- 与 `code_path` 一样,必须通过 `runtime_env.env_vars` 确保该路径可被 import例如把 `${SHARED_ROOT}/user/code` 也加入 `PYTHONPATH`
v1.1 中至少做一次“代码覆盖验证”:
- 在 code_path 下放一个可识别的 `verl` 版本标识(例如 `verl.__version__` 打印差异)
- 提交 job 并在日志中确认 import 的是 code_path 的版本(而不是镜像内默认安装)
v1.1 的最小落地方式(已实现):
- 提供代码快照脚本:`src/mvp/v1.1/scripts/31_snapshot_verl_code.sh`
- 会把 `/workspace/verl`(挂载的 repo复制到 `${SHARED_ROOT}/common/code/verl/<code_id>/`
- 并写入 `${code_path}/mvp_marker.py`,用于在 Ray job logs 中验证“选用的是哪份 code_path”
- submitter 会在 entrypoint 前运行 preflight
- 打印 `verl.__file__``mvp_marker.MARKER`
- 由此确认 job 粒度的 PYTHONPATH 生效,且不同 job 可指向不同 `code_path`(多版本共存)
### 3.2 Checkpoint 策略(磁盘保护)
- 默认:`save_freq=10`(每 10 step 保存一次)
- 对于 step 数已知的短任务(例如 29 steps可以通过配置把 `save_freq` 调整为 10/15/29按需求权衡
- 作业目录按 `submission_id` 隔离,方便清理与归档
---
## 4. v1.1 交付物清单(代码 + 文档)
### 4.1 代码(建议落点)
`src/mvp/` 下新增 v1.1 级别的提交器与模板(或在 `src/mvp/v1` 原地演进但要保持 v1 可回归):
- `src/mvp/v1.1/`
- `docker-compose.yaml`(与 v1 互不干扰的容器名/网络名)
- `scripts/`Ray 启动/prepare 保留 bashsubmit 通过 SDK 工具执行)
- `py/`工程化提交层YAML + Ray Python SDK
- `py/configs/`Ray 基础配置)
- `py/jobspecs/`(训练 JobSpec
- `py/run.py`(入口)
此外,为了对齐 dev 环境约束(远程机固定目录):
- 远程机目录必须新增:`argus@h1:/home2/argus/infra/mvp/v1.1/`
- 该目录内需包含 v1.1 的全部内容compose + scripts + README可由本 repo 的 `src/mvp/v1.1/` 同步过去
### 4.2 文档
- `specs/mvp/v1.1/v1.1_action.md`:开发、部署、测试、验收流程(可复现)
- 更新 `specs/mvp/mvp_roadmap.md`:保持路线图与落地一致(按需)
---
## 5. v1.1 验收标准DoD
### 5.1 Hardening DoD
- [ ] 所有提交均由 head 执行 `ray job submit`,且显式 `--submission-id=<id>`
- [ ] 共享根路径由 `SHARED_ROOT` 控制dev/prod 可切换),脚本无硬编码
- [ ] 每个 job 的输出目录为:`${SHARED_ROOT}/jobs/<submission_id>/`
- [ ] checkpoint 不会“每 step 保存”导致爆盘:默认 `save_freq=10`
- [ ] job 失败时,`${SHARED_ROOT}/jobs/<id>/config/` 中有足够信息定位命令、env、ray 状态快照)
- [ ] v1.1 测试前会清理 v1 的遗留容器/进程避免端口、容器名、Ray session 干扰)
### 5.2 Workload DoDGRPO + SFT 都必须)
GRPO必须
- [ ] `algorithm.adv_estimator=grpo` 的 job 可提交并进入 RUNNING
- [ ] job 能跑完最小训练步数(可设 `total_epochs=1``total_training_steps`
- [ ] 输出目录内有日志与至少 1 次 checkpoint或明确不保存并说明原因
SFT必须
- [ ] `sft_trainer_ray` 可连接集群并跑到至少 1 个 step推荐最小训练步数/epoch
- [ ] 输出目录与 checkpoint 策略同 v1.1 规范(落盘到 `${SHARED_ROOT}/jobs/<id>/...`

View File

@ -1,148 +0,0 @@
# MVP v1.1 工程化重构方案Ray Python SDK 提交层YAML Config + YAML JobSpec
本文档把 v1.1 的“代码工程化”目标落到一个明确的设计:**保留现有 scripts**Ray 集群构建、数据准备、模型准备、代码快照),将“任务提交机制”重构为 **Ray Python SDK**`ray.job_submission.JobSubmissionClient`)驱动的 Python 工具层。
> 约束(已确认)
> 1) 基础配置用 YAMLJobSpec 也用 YAML。
> 2) 工具必须在 **head 容器**执行(从 head 发起提交,满足“在 head 提交”的要求)。
> 3) 训练参数组织保持与现在一致:仍然使用 **Hydra overrides** 方式构造 entrypoint。
> 4) 不使用 `requests` 直连 HTTP API只用 Ray SDK
---
## 1. 当前 Ray SDK 能力验证(关键前提)
在 head 容器(`mvp11-ray-head`)中验证:
- Ray 版本:`2.51.1`
- `JobSubmissionClient.submit_job` 支持以下关键字段:
- `submission_id`
- `runtime_env`
- `entrypoint_num_cpus`
- `entrypoint_num_gpus`
- `entrypoint_resources`(用于强制 driver 落 worker
因此 v1.1 可以“纯 SDK”完成提交不需要 `requests` fallback。
---
## 2. 系统分层(不动 scripts只重构提交层
### 2.1 scripts保留
`src/mvp/v1.1/scripts/` 继续负责:
- 容器生命周期:`01_up.sh` / `02_down.sh`
- Ray 启动:`20_start_head.sh` / `21_start_workers.sh`
- 数据/模型准备:`30_prepare_data_and_model.sh`
- 代码快照:`31_snapshot_verl_code.sh`(生成 `${SHARED_ROOT}/common/code/verl/<code_id>/`
scripts 可以新增一个“薄封装”脚本,负责 `docker exec` 进 head 容器并运行 Python 提交器,但 scripts 不再拼 `ray job submit ...` CLI 字符串。
### 2.2 Python 工具层(新增)
`src/mvp/v1.1/py/` 新增提交工具层:
- 读取 Ray 基础配置YAML
- 读取训练 JobSpecYAML
- 用 Ray Python SDK 提交/查询/停止/拉日志
- 将 job 级别产物落盘到:`${SHARED_ROOT}/jobs/<submission_id>/...`
---
## 3. 输入定义:两份 YAML
### 3.1 Ray 基础配置RayConfig YAML
这份配置是“稳定可复用”的,描述 cluster 与 driver placement 等通用信息。
字段建议:
- `address`: `http://127.0.0.1:8265`(从 head 容器内部视角)
- `shared_root`: `/private`
- `entrypoint_num_cpus`: `1`
- `entrypoint_resources`: `{"worker_node": 1}`(强制 driver 使用 worker 才有的资源)
- `runtime_env.env_vars`: HF cache / endpoint 等通用环境变量
- `user_code_path`: `${shared_root}/user/code`(可选,默认值也可)
### 3.2 训练 JobSpecJobSpec YAML
这份配置是“一次训练”语义,描述 workload + 训练参数 + code_path 多版本等。
字段建议:
- `workload`: `ppo|grpo|sft`
- `submission_id`: 可选(不填则生成;但最终必须显式传给 SDK
- `code_path`: `${shared_root}/common/code/verl/<code_id>`(多版本关键字段)
- `model_id`
- 数据路径:`train_file` / `val_file`(按 workload
- 训练参数:`nnodes` / `n_gpus_per_node` / `total_training_steps` / `save_freq` / `test_freq`
注意SFT 的 driver 设备选择):
- Ray job 的 entrypointdriver默认不分配 GPU我们通常不设置 `entrypoint_num_gpus`)。
- `sft_trainer_ray.py` 的 driver 会用 `trainer.device` 做张量统计;若设置为 `cuda` 且 driver 无 GPU会报
- `RuntimeError: No CUDA GPUs are available`
- 因此 v1.1 的 SFT JobSpec 默认应设置:`trainer.device=cpu`(训练 workers 仍会占用 GPU
---
## 4. Python 提交器的职责tool class
建议实现 `RayJobTool`(或类似命名),能力:
### 4.1 submit核心
输入:`RayConfig + JobSpec`
输出:`submission_id`
实现要点:
- `client = JobSubmissionClient(address)`
- 生成/确定 `submission_id`
- `runtime_env` 合并逻辑:
- 合并 config 与 jobspec 的 `env_vars`
- 强制注入多版本:
- `PYTHONPATH = "<code_path>:<user_code_path>:$PYTHONPATH"`
- 构造 entrypoint保持 hydra overrides 风格):
- PPO/GRPO`python3 -m verl.trainer.main_ppo ...`
- SFT`python3 -m verl.trainer.sft_trainer_ray ...`
- 强制 driver 落 worker
- `entrypoint_resources=config.entrypoint_resources`
- `entrypoint_num_cpus=config.entrypoint_num_cpus`
- 落盘产物:
- `${shared_root}/jobs/<id>/config/{ray_config.yaml,jobspec.yaml,submit_payload.json}`
- `${shared_root}/jobs/<id>/logs/submit.out`
- `${shared_root}/jobs/<id>/debug/{ray_status_pre,ray_job_list_post}.txt`(可用 SDK 或 `ray status` 采集)
### 4.2 status / stop / logs / list
- `status(submission_id)`
- `stop(submission_id)`
- `logs(submission_id)`(可支持 tail
- `list()`
---
## 5. `run.py` 入口(必须在 head 容器执行)
建议入口:
- `python3 /workspace/mvp/v1.1/py/run.py --config <ray_config.yaml> --jobspec <jobspec.yaml> --action submit`
- `--action` 支持:`submit|status|stop|logs|list`
host 侧执行方式(由 scripts 薄封装):
- `docker exec mvp11-ray-head python3 /workspace/mvp/v1.1/py/run.py ...`
---
## 6. 验收口径(工程化部分)
1) **SDK 提交**:不使用 `ray job submit` CLI改用 `JobSubmissionClient.submit_job`
2) **driver 仍强制在 worker**SDK 提交时 `entrypoint_resources={"worker_node":1}` 生效。
3) **多版本共存验证**
- 通过 `31_snapshot_verl_code.sh` 生成 `codeA/codeB` 两份 code_path
- 通过两份 JobSpec 分别指向不同 `code_path`
- 在 job logs 中看到不同的 marker例如 `mvp_marker.MARKER`

View File

@ -1,333 +0,0 @@
# MVP v1.1 行动文档(实施方案 / 部署测试 / 验收口径)
本文档面向“把 v1 跑通的实验脚本,升级为可长期回归的 v1.1 最小系统”,并给出**开发改造 → 部署测试 → 验收**的可复现流程。
> v1.1 的核心约束(来自讨论结论)
> - 仍然必须通过 **head 节点执行 `ray job submit`** 提交任务。
> - 训练/driver **必须落在 worker**head 不跑训练)。
> - 多版本 `verl` 共存:同一镜像不变,必须通过 **Ray Job `runtime_env.env_vars` 注入 `PYTHONPATH`** 让 job 粒度选择代码版本。
> - 存储只考虑 NFSdev 环境我们自己 mount生产环境容器内统一看到 `/private/`
---
## 1. 目标与非目标
### 1.1 目标v1.1 必须做到)
1) **可回归**:同一环境连续跑多次 PPO 回归,不互相覆盖,输出按 submission id 归档。
2) **可扩展**:新增并验收通过 2 个 workload**GRPO + SFT**)并跑通闭环。
3) **可排障**:每个 job 目录包含完整的提交快照、关键 env、Ray 状态快照与日志入口。
4) **可多版本共存**:同一 Ray 集群内,不同 job 通过 `PYTHONPATH` 选择不同 `verl` 代码快照。
### 1.2 非目标v1.1 不做)
- 不做平台 API/队列/多租户/RBAC这是 v2/v3
- 不做复杂调度拓扑、IB 域、NUMA、Gang 等自动化策略)。
---
## 2. 运行环境约定dev / prod 一致抽象)
### 2.1 拓扑(单机 3 容器)
- `mvp-ray-head`:无 GPU`ray start --head --num-cpus=0 --num-gpus=0`(控制面 only
- `mvp-ray-worker-0`4 GPU
- `mvp-ray-worker-1`4 GPU
### 2.2 “head 不跑训练”的硬约束实现(必须)
1) **head CPU=0**:从资源层面阻断默认 task/driver 落到 head。
2) **worker 自定义资源标签**worker 启动时带 `--resources='{"worker_node": 100}'`
3) **ray job submit 强制 entrypoint 落 worker**:提交时必须带:
- `--entrypoint-resources='{"worker_node": 1}'`
- `--entrypoint-num-cpus=1`(显式声明 driver 需要的 CPU
> 验证口径:`ray job list``driver_info.node_ip_address` 必须是 worker 的 IP而不是 head IP。
### 2.3 共享存储NFS与路径关键
- 生产环境:容器内共享根路径固定为 `/private/`(算力平台统一挂载 NFS
- 开发环境docker compose 也应把宿主机共享目录挂载到容器内的 `/private/`,从而做到 dev/prod 一致。
统一约定(容器内视角):
- `SHARED_ROOT=/private`
- Job 输出:`${SHARED_ROOT}/jobs/<submission_id>/`
建议的共享目录结构v1.1 新增 `common/``user/`
- `${SHARED_ROOT}/datasets/`:通用数据(例如 gsm8k parquet
- `${SHARED_ROOT}/hf/`HuggingFace cache模型/分词器/权重)
- `${SHARED_ROOT}/jobs/`:按 submission id 归档的作业目录(强制)
- `${SHARED_ROOT}/outputs/`:临时/非强约束输出(不建议长期依赖)
- `${SHARED_ROOT}/ray/`Ray 调试痕迹(可选,通常 Ray 默认写 `/tmp/ray`
- `${SHARED_ROOT}/common/`:所有用户可读写共享区(模型/数据/代码快照)
- `${SHARED_ROOT}/common/models/`:可复用基础模型(可用软链指向 hf cache 或 snapshot
- `${SHARED_ROOT}/common/datasets/`:共享数据(或与 `datasets/` 统一规划)
- `${SHARED_ROOT}/common/code/`:代码快照(多版本 `verl` / 自定义 reward
- `${SHARED_ROOT}/user/`:用户自定义内容(默认所有用户可写)
- `${SHARED_ROOT}/user/code/`reward_fn 等自定义 Python 代码
---
## 3. 开发实施方案(代码改造清单)
> v1.1 建议新增 `src/mvp/v1.1/`(保持 v1 可回归不被破坏)。
### 3.1 JobSpec最小标准化
v1.1 的工程化目标是把“提交机制”迁移到 Ray Python SDK因此输入拆为两份 YAML
1) Ray 基础配置YAMLaddress / entrypoint resources / runtime_env 等
2) 训练 JobSpecYAMLworkload 语义与训练参数(仍由 Hydra overrides 组织)
训练 JobSpecYAML至少包含
- `submission_id`:可空;为空时由 submitter 生成(但最终必须显式传给 `ray job submit --submission-id`
- `workload``ppo` / `grpo` / `sft`v1.1 必须 `ppo` + `grpo` + `sft`
- `shared_root`:默认 `/private`(容器内路径)
- `code_path``verl` 代码快照目录(用于多版本共存)
- `reward_fn_path`(可选):指向 `${shared_root}/user/code/...` 下的 Python 文件或模块入口
- `model` / `dataset`:必须指向共享存储的持久化路径(避免每次下载/生成)
- `ray``address=http://127.0.0.1:8265`(从 head 容器内部视角)
- `resources`
- `entrypoint_resources={"worker_node":1}`
- `entrypoint_num_cpus=1`
- `trainer_overrides`训练参数覆盖v1.1 默认 `total_epochs=1``save_freq=10`
- `env_vars`:会被透传到 `runtime_env.env_vars`(必须包含 `PYTHONPATH` 注入)
交付物v1.1 SDK 方式):
- `src/mvp/v1.1/py/configs/dev.yaml`Ray 基础配置示例)
- `src/mvp/v1.1/py/jobspecs/{ppo,grpo,sft}.yaml`(训练 JobSpec 示例)
- `src/mvp/v1.1/py/run.py`(入口:使用 Ray Python SDK 提交/查询/停止/拉日志)
- 设计文档:`specs/mvp/v1.1/sdk_submit_refactor.md`
### 3.2 多版本 `verl` 共存(必须)
原则:**镜像固定不变**job 粒度通过 `PYTHONPATH` 选择 `verl` 代码快照。
提交时必须注入runtime_env
- `PYTHONPATH="<CODE_PATH>:$PYTHONPATH"``CODE_PATH` 放最前面)
并要求 job 在日志中打印一行确认 import 来源,例如:
- `python -c "import verl,inspect; print(verl.__file__)"`(或训练入口启动时打印)
v1.1 具体实现(可复现):
- 先用 `src/mvp/v1.1/scripts/31_snapshot_verl_code.sh` 生成代码快照目录 `${SHARED_ROOT}/common/code/verl/<code_id>/`
- 该目录里会包含一个 `mvp_marker.py``MARKER=<code_id>`
- 提交 job 时让 `code_path` 指向该快照目录submitter 会在 entrypoint 前打印:
- `MVP_PRECHECK_VERL_FILE`(验证 import 来源)
- `MVP_PRECHECK_MARKER`(验证选择的 code_path
### 3.3 `submit_job` 工具(组装 ray job submit
新增一个提交器(建议 Python避免复杂 bash quoting
- 输入JobSpec JSON
- 产物:
- 生成/确定 `submission_id`
- 创建 `${SHARED_ROOT}/jobs/<id>/config/``logs/``checkpoints/`
- 写入 `config/job_spec.json`(原样快照)
- 写入 `config/runtime_env.json`(最终用于 submit 的 JSON
- 写入 `config/submit_cmd.txt`(最终命令行)
- 执行:在 **head 容器内**运行 `ray job submit ...`
### 3.4 可排障debug bundle强制落盘
在 job 生命周期的关键节点收集并落盘(至少 2 类):
- `ray status`
- `ray job list`
- `ray list nodes`
- `ray list actors`
建议落盘到:
- `${SHARED_ROOT}/jobs/<id>/debug/`(每次收集带时间戳文件名)
### 3.5 Workload 扩展GRPOv1.1 新增闭环)
优先用与 PPO 相同入口 `python -m verl.trainer.main_ppo`,仅通过配置切换算法:
- `algorithm.adv_estimator=grpo`
- 其余保持最小可跑:`total_epochs=1``save_freq=10`
### 3.6 Workload 扩展SFT on Rayv1.1 必须新增闭环)
#### 3.6.1 入口与参考实现
- 入口:`python -m verl.trainer.sft_trainer_ray`
- 参考代码:`verl/verl/trainer/sft_trainer.py`(非 Ray 版本)与 `verl/verl/trainer/sft_trainer_ray.py`Ray 版本)
> v1.1 要验收的是 “SFT on Ray”因此默认使用 `sft_trainer_ray.py`
#### 3.6.2 连接已有 Ray 集群(必须)
`sft_trainer_ray.py` 内部直接调用 `ray.init()`,为了确保它连接到**已有集群**head+workersv1.1 约定:
- 提交 job 时通过 `runtime_env.env_vars` 注入:`RAY_ADDRESS=auto`
如果发现 `ray.init()` 未按预期读取 `RAY_ADDRESS`Ray 版本差异风险v1.1 需要提供一个 launcher 兜底:
- 由 launcher 先显式 `ray.init(address="auto")`,再调用 SFT trainer 逻辑
#### 3.6.3 SFT 数据格式parquet schema
`sft_trainer_ray` 默认使用 `MultiTurnSFTDataset`parquet 中至少需要:
- `messages`list[dict]dict 至少含 `role`/`content`
v1.1 的 `prepare` 阶段需要生成并持久化 SFT 数据,例如:
- `${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet`
- `${SHARED_ROOT}/datasets/gsm8k_sft/val.parquet`(可选)
单条样本的 `messages` 形态示例:
- `[{ "role": "user", "content": "<question>" }, { "role": "assistant", "content": "<answer>" }]`
> 注意SFT parquet 不能直接复用 PPO/RL 的 parquetschema 不同)。
#### 3.6.4 重要细节SFT Ray Driver 不应依赖 GPU
`ray job submit` 模式下,我们的 entrypointdriver默认 **不会分配 GPU**(我们只指定了 `--entrypoint-num-cpus=1`,没有指定 `--entrypoint-num-gpus`)。
`verl/verl/trainer/sft_trainer_ray.py` 的 driver 逻辑里会用 `trainer.device` 来创建 `torch.tensor(..., device=...)` 做统计,如果设置为 `cuda` 且 driver 没有 GPU会触发
- `RuntimeError: No CUDA GPUs are available`
因此 v1.1 的 SFT on Ray 验收默认要求:
- `trainer.device=cpu`driver 只做 orchestration真正训练仍由 Ray 的 TrainingWorker/资源池占用 GPU
### 3.7 v1.1 脚本化交付(必须独立完整)
`src/mvp/v1.1/` 需要像 v1 一样提供一套完整脚本,确保 v1.1 可独立运行、可回归:
- `src/mvp/v1.1/docker-compose.yaml`(容器名建议与 v1 区分,避免冲突)
- `src/mvp/v1.1/scripts/00_prereq_check.sh`(含 GPU/目录/NFS/verl 代码检查)
- `src/mvp/v1.1/scripts/01_up.sh` / `02_down.sh`(起停)
- `src/mvp/v1.1/scripts/20_start_head.sh` / `21_start_workers.sh`
- `src/mvp/v1.1/scripts/30_prepare_data_and_model.sh`(包含 PPO 数据 + SFT 数据)
- `src/mvp/v1.1/scripts/40_submit_ppo_epoch1.sh`
- `src/mvp/v1.1/scripts/41_submit_grpo_epoch1.sh`
- `src/mvp/v1.1/scripts/42_submit_sft_minimal.sh`
- `src/mvp/v1.1/scripts/50_status.sh`
- `src/mvp/v1.1/scripts/31_snapshot_verl_code.sh`(多版本 code snapshot
- `src/mvp/v1.1/scripts/43_submit_jobspec.sh`(通过 JobSpec 提交)
- `src/mvp/v1.1/scripts/12_install_py_deps.sh`(安装 PyYAML 等依赖)
- `src/mvp/v1.1/scripts/44_submit_sdk.sh`(通过 Ray Python SDK + YAML 提交)
---
## 4. 部署与测试流程dev 环境)
> dev 环境以远程机目录为例:`argus@h1:/home2/argus/infra/mvp`。v1.1 的所有内容要求放在:
>
> - `argus@h1:/home2/argus/infra/mvp/v1.1/`
>
> 并在该目录中通过脚本使用 `docker exec` 协调容器。
### 4.0 清理 v1 环境(必须先做)
v1 已在 `argus@h1` 部署过容器与 Ray。为保证 v1.1 的可重复测试,开始 v1.1 前必须清理 v1
1) 停止并删除 v1 容器(推荐用 v1 的 down 脚本)
2) 确认 `docker ps` 中不再有 v1 的 `mvp-ray-head/mvp-ray-worker-*`
v1.1 的脚本里也提供了一个 best-effort 清理脚本:`src/mvp/v1.1/scripts/03_cleanup_v1_legacy.sh`(远程目录中同名脚本)。
### 4.1 环境准备(一次性 / 幂等)
1) 目录检查(远程机):
- `${WORKDIR}/shared/` 存在并具备上述子目录(含 `common/``user/`
2) `verl` 代码目录检查:
- `${WORKDIR}/verl` 不存在则执行 `git clone https://github.com/volcengine/verl.git`
3) GPU 可用性检查:
- 设备存在(例如 0-7 可见),并按 worker 容器分配(每个 worker 4 GPU
4) 模型与数据持久化路径:
- 模型与数据必须落在 `${SHARED_ROOT}` 下;若已存在则跳过下载/生成
- SFT parquet 同样必须落在 `${SHARED_ROOT}` 下;若已存在则跳过生成
### 4.2 启动 Ray 集群(每次测试)
1) `docker compose up -d`
2) head`ray start --head --num-cpus=0 --num-gpus=0 ...`
3) workers`ray start --address=<head>:6379 --resources='{"worker_node":100}' ...`
4) 验证:`ray status` 显示 1 head + 2 worker且 head `CPU:0 GPU:0`
### 4.3 提交 PPO 回归(必须跑 2 次)
1) 生成 JobSpec可用模板 + 覆盖项)
2) 在 head 容器内执行 submitter或直接 `ray job submit`
3) 验证要点:
- `ray job list`driver node 是 worker
- `${SHARED_ROOT}/jobs/<id>/` 下存在 `config/``logs/``checkpoints/`
- checkpoint 每 10 step 产生(例如 `global_step_10`
### 4.4 提交 GRPO新增 workload 验收)
同 PPO但覆盖 `algorithm.adv_estimator=grpo`,确保能进入 RUNNING 并完成最小步数。
### 4.5 提交 SFT on Ray新增 workload 验收,必须)
1) 确认 `${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet` 已存在(由 v1.1 prepare 生成)。
2) 通过 head 容器执行 `ray job submit` 提交 `python -m verl.trainer.sft_trainer_ray`
3) 关键约束:
- `runtime_env.env_vars.RAY_ADDRESS=auto`(连接已有集群)
- `--entrypoint-resources='{"worker_node": 1}'`driver 落 worker
- `PYTHONPATH=<code_path>:$PYTHONPATH`(多版本 verl
4) 最小化训练配置建议(避免 OOM/耗时过长):
- `trainer.total_epochs=1`
- `trainer.total_training_steps=10~30`
- `trainer.save_freq=10`
- `trainer.nnodes=2``trainer.n_gpus_per_node=4`(用满 8 卡做一次最小分布式验证)
- `data.train_files=${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet`
- `trainer.default_local_dir=${SHARED_ROOT}/jobs/<id>/checkpoints`
### 4.6 工程化验证JobSpec + 多版本共存v1.1 必须)
1) 生成两个 code snapshot不同 `CODE_ID`
- `CODE_ID=codeA ./scripts/31_snapshot_verl_code.sh`
- `CODE_ID=codeB ./scripts/31_snapshot_verl_code.sh`
2) 分别修改/复制 JobSpec 模板,使 `code_path` 指向不同 snapshot
- `${SHARED_ROOT}/common/code/verl/codeA`
- `${SHARED_ROOT}/common/code/verl/codeB`
3) 用 JobSpec 提交(必须从 head
- `./scripts/43_submit_jobspec.sh /workspace/mvp/v1.1/templates/ppo.json`(示例)
4) 在 Ray job logs 中验证:
- `MVP_PRECHECK_MARKER` 打印为对应的 `codeA`/`codeB`
- `MVP_PRECHECK_VERL_FILE` 指向 `${SHARED_ROOT}/common/code/verl/...` 而不是镜像内 site-packages
---
## 5. 验收标准Definition of Done
### 5.1 Hardening DoD全部必选
- [ ] 提交必须来自 head能在 head 容器内看到 `ray job submit ...` 的提交记录
- [ ] driver 不在 head`ray job list``driver_info.node_ip_address` ∈ worker IP且 ≠ head IP
- [ ] 输出目录按 submission id 隔离:`${SHARED_ROOT}/jobs/<submission_id>/` 不复用、不覆盖
- [ ] 数据/模型持久化:再次提交时不重复下载/生成(有 “skip if exists” 的日志)
- [ ] checkpoint 策略有效:默认 `save_freq=10`,不会每 step 保存爆盘
- [ ] debug bundle 落盘:`${SHARED_ROOT}/jobs/<id>/debug/` 至少包含 2 类 Ray 状态快照
- [ ] 多版本共存验证通过:日志中能确认 `verl` import 来源来自 JobSpec 指定的 `code_path`
### 5.2 Workload DoDGRPO + SFT 都必须)
- [ ] GRPO job 能提交、RUNNING、完成最小训练步数
- [ ] GRPO job 产物目录满足与 PPO 相同的目录规范与 debug 规范
- [ ] SFT job 能提交、连接已有集群并跑到至少 1 个 step建议最小步数/epoch
- [ ] SFT job 产物目录满足与 PPO 相同的目录规范与 debug 规范
---
## 6. 生产环境部署注意事项v1.1 需要考虑但不强制在 dev 全量模拟)
- 容器由算力平台创建:我们只负责 SSH 进去纳管(启动 ray / 提交 job / 收集产物)。
- 容器内共享路径为 `/private`:所有脚本必须以 `SHARED_ROOT=/private` 工作,不得写死 `/mnt/shared`
- 认证仅内部 token在 submitter 中把 token 作为 env var 透传(不写入日志明文)。

View File

@ -1,120 +0,0 @@
# MVP 计划V1
本文档目标:把当前“口述/实验记录”整理成**可复现、可验收**的 MVP 计划,并明确下一步最小闭环。
## 1. 背景与目标
我们要验证的最小闭环是:
1) 在“HeadCPU 容器)+ WorkerGPU 容器)”的 Ray Cluster 上,能够跑通一次 `verl` 的 PPO 训练。
2) 训练所需的 **数据集 / 模型缓存 / 训练产物checkpoint/ 日志** 不落在容器临时文件系统里,而是落在**共享存储NFS**,容器重启后可继续使用。
3) 所有步骤能写成一套**清晰命令/脚本**,新人可照着复现。
## 2. 环境与假设
- 机器H20 机器(具体规格由算力平台提供)
- 访问方式:通过 `ssh h1a` 远程登录(进入算力平台/宿主访问入口)
- 容器:算力平台可申请 CPU 容器(对外暴露端口)与若干 GPU 容器(可 SSH 互通)
- 共享存储:所有容器可挂载同一套 NFS`specs/hl_design_v2.md` 中假设为 `/mnt/shared`
## 3. 已验证现状(现有实验)
目录 `ray_in_docker/` 已经做过一次可运行的实验(偏“本地/示例级别”):
- 用 `docker-compose` 起了 2 个 `verl` 镜像容器:
- `verl-head`:作为 Ray HeadDashboard 端口 `8265`
- `verl-worker`:作为 Ray Worker
- 在容器中执行:
- 下载 GSM8K 数据集(`examples/data_preprocess/gsm8k.py`
- 拉取 HuggingFace 模型(示例:`Qwen/Qwen2.5-0.5B-Instruct`
- `ray start --head` + `ray start --address=...`
- 通过 `ray job submit ... python -m verl.trainer.main_ppo ...` 提交 PPO 训练任务(见 `ray_in_docker/ray_example/ppo_train.sh`
结论:**训练脚本可以跑通**。
## 4. 当前主要问题(从实验到平台化 MVP 的差距)
1) **数据 / 模型 / 输出落在容器内**:容器重启/替换后不可复用;也不利于多人共享与审计。
2) **缓存路径不规范**HuggingFace cache、Ray 临时目录、Hydra 输出目录等可能分散在容器默认路径。
3) **可复现不足**:缺少明确的目录规范、统一的启动/提交流程、验收口径。
4) Ray 节点**打标签/亲和性调度**的方法未固化:需要明确是否统一用 `ray start --resources`,以及命名规范如何设计。
## 5. MVP V1最小闭环定义
`specs/hl_design_v2.md` 的方向为准,但 V1 **只做最小可运行原型**,暂不做完整 Web/调度系统。
### 5.1 目录规范(统一落到 NFS
约定所有容器统一挂载 NFS 到 `/mnt/shared`,并在其中固定目录结构:
- `/mnt/shared/code/`:代码(可选:按版本/分支隔离)
- `/mnt/shared/datasets/`:数据集(如 `gsm8k/`
- `/mnt/shared/hf/`HuggingFace 缓存(设置 `HF_HOME=/mnt/shared/hf`
- `/mnt/shared/ray/`Ray 运行期临时目录(可选:设置 `RAY_TMPDIR=/mnt/shared/ray/tmp`
- `/mnt/shared/outputs/`训练输出根目录Hydra/日志/ckpt 统一落这里)
- `/mnt/shared/outputs/logs/<job_id>/`
- `/mnt/shared/outputs/checkpoints/<job_id>/`
### 5.2 最小集群形态
- 1 个 HeadCPU 容器)
- 跑 `ray start --head --dashboard-host=0.0.0.0`
- 暴露 `8265` 给 Desktop/用户查看 Job 状态
- 1~2 个 WorkerGPU 容器)
- 跑 `ray start --address=<head_ip>:6379` 加入集群
- (可选)通过 `--resources='{\"gpu_pool_a\": 1}'` 给节点打标签
### 5.3 最小训练任务
- 目标任务:跑通一次 `verl.trainer.main_ppo`(以 GSM8K 为例)
- 要求:
- `data.train_files` / `data.val_files` 指向 `/mnt/shared/datasets/...`
- HuggingFace 模型下载缓存落到 `/mnt/shared/hf`
- 训练输出Hydra outputs、checkpoint、stdout/stderr落到 `/mnt/shared/outputs/...`
建议在提交命令里显式覆盖 Hydra 输出目录(示例,具体目录名按需调整):
- `hydra.run.dir=/mnt/shared/outputs/logs/${JOB_TAG}`
## 6. 实施步骤Checklist
### 6.1 一次性准备
- [ ] 确认所有容器已挂载 NFS 到同一路径:`/mnt/shared`
- [ ] 在 `/mnt/shared/` 下创建目录:`datasets/ hf/ outputs/ ray/`
- [ ] 在所有容器中设置/注入环境变量(推荐写入统一脚本):
- `HF_HOME=/mnt/shared/hf`
- `HF_ENDPOINT=https://hf-mirror.com`(如需)
- `RAY_TMPDIR=/mnt/shared/ray/tmp`(可选)
### 6.2 启动集群Head + Worker
- [ ] 在 Head 容器启动 Ray Head并记录 `head_ip:6379`
- [ ] 在每个 Worker 容器执行 `ray start --address=...` 加入集群
- [ ] 在 Head 上通过 `ray status` / Dashboard 验证节点已注册
### 6.3 准备数据与模型
- [ ] 数据集下载到:`/mnt/shared/datasets/gsm8k/`
- [ ] 模型缓存落到:`/mnt/shared/hf/`(拉取一次即可多任务复用)
### 6.4 提交训练任务
- [ ] 用 `ray job submit --address=http://<head>:8265 ...` 提交 PPO 训练
- [ ] 训练日志与 checkpoint 在 `/mnt/shared/outputs/` 可见
## 7. 验收标准V1
- [ ] Ray Head/Worker 能稳定加入同一集群Dashboard 可见)
- [ ] PPO 训练任务可提交并跑通(至少完成若干 step/epoch
- [ ] 数据集、HF 缓存、训练输出均在 `/mnt/shared/` 下可复用(容器重启后仍在)
- [ ] 有一份“从零到跑通”的命令清单(或脚本)可复现
## 8. 未决问题(记录待补齐)
- [ ] Ray 节点标签/亲和性调度:是否统一用 `ray start --resources`,以及命名规范如何设计
- [ ] RL workload 的 Driver 放置策略:先按 `verl` 默认即可,后续再按 `specs/hl_design_v2.md` 收敛到“Driver-on-Head / Placement Group”等模式
## 9. 下一步(进入 V2
当 V1 达到“可复现 + 产物可落盘”的验收标准后,下一阶段工作见:`specs/mvp_plan_v2.md`

View File

@ -1,111 +0,0 @@
# MVP V1 远程实验行动文档(待确认后执行)
## 1. 任务复述(我理解的需求)
你希望我在远程机器 `argus@h1` 上,进入目录 `/home2/argus/infra/mvp`,把 MVP V1 的“原本流程”**手动完整跑一遍并验证**。要求:
1) 在宿主机上编写脚本,脚本通过 `docker exec` 在容器内执行命令,负责协调启动顺序(先 head、后 worker
2) 集群拓扑改为:
- 1 个 Ray Head**没有 GPU**,并且 Head 的 Ray 资源 `CPU=0`(防止 Ray 把训练任务调度到 head
- 2 个 Ray Worker各自 **4 GPU**(总 8 GPU
3) PPO 训练需要“轻量化”,把 `total_epochs` 改为 `1`
4) 先在本地仓库 `src/mvp/v1/` 写好脚本与 compose 文件;再拷贝到远程目录执行与验证。
5) 在你确认这份行动文档没问题之前,我**不执行**远程操作。
## 2. 本地已准备的文件(在本仓库内)
- `src/mvp/v1/docker-compose.yaml`3 容器head + 2 workerhead 不使用 nvidia runtimeworker0/1 各限制 4 GPU。
- `src/mvp/v1/scripts/`:宿主机脚本(内部全部用 `docker exec`
- `01_up.sh`:起容器
- `20_start_head.sh`:启动 Ray head`--num-cpus=0 --num-gpus=0`
- `21_start_workers.sh`:启动 Ray worker 加入集群
- `30_prepare_data_and_model.sh`:准备 GSM8K 数据与预下载模型
- `40_submit_ppo_epoch1.sh`:提交 PPO`trainer.total_epochs=1`,并设置 `nnodes=2, n_gpus_per_node=4`
- `run_all.sh`:按顺序一键执行
## 3. 远程环境前置条件(需要你确认/保证)
`argus@h1` 上:
- Docker 可用,且有 `docker compose` 插件Compose v2
- NVIDIA runtime 可用worker 容器需要 `runtime: nvidia`),宿主机有至少 8 张 GPU。
- 不强制要求提前准备 `./verl`:脚本会在宿主机侧检查 `${PWD}/verl`,如果不存在会自动执行:
- `git clone https://github.com/volcengine/verl.git`
此外本实验默认写入持久化目录:`/home2/argus/infra/mvp/shared`(会自动创建)。
## 4. 拷贝到远程(我执行前会再次征求你确认)
从本地(本机)同步到远程:
1) 同步脚本与 compose
- `rsync -av ./src/mvp/v1/ argus@h1:/home2/argus/infra/mvp/src/mvp/v1/`
- `rsync -av ./specs/mvp/v1_action.md argus@h1:/home2/argus/infra/mvp/specs/mvp/v1_action.md`
2) `verl/` 默认不需要同步(远程会 clone。如果你更希望固定版本/避免网络波动,也可以手动同步:
- `rsync -av --delete ./verl/ argus@h1:/home2/argus/infra/mvp/verl/`
## 5. 远程执行步骤(在宿主机上)
在远程机器执行:
1) 进入目录:
- `cd /home2/argus/infra/mvp`
2) 确保脚本可执行(首次同步后需要做一次):
- `chmod +x ./src/mvp/v1/scripts/*.sh`
3) 启动容器:
- `./src/mvp/v1/scripts/01_up.sh`
4) 安装 editable 版 `verl`(保证 `python -m verl...` 可用):
- `./src/mvp/v1/scripts/10_install_verl_editable.sh`
5) 启动 Ray Head禁止调度到 head
- `./src/mvp/v1/scripts/20_start_head.sh`
6) 启动两个 Ray Worker 加入集群:
- `./src/mvp/v1/scripts/21_start_workers.sh`
7) 准备数据 + 预下载模型(落到 `./shared`
- `./src/mvp/v1/scripts/30_prepare_data_and_model.sh`
8) 提交 PPO`total_epochs=1`,必须用 `ray job submit` 在 head 提交;通过 `--entrypoint-resources` 强制 driver 调度到 worker
- `./src/mvp/v1/scripts/40_submit_ppo_epoch1.sh`
9) 观察状态:
- `./src/mvp/v1/scripts/50_status.sh`
- 打开 Ray Dashboard`http://<h1宿主机IP>:8265`
也可以一键跑:
- `./src/mvp/v1/scripts/run_all.sh`
## 6. 验收与验证点(执行时我会逐项检查)
1) Head 节点无 GPU在 head 容器内 `nvidia-smi` 应不可用或无设备worker 内可见 4 张)。
2) Head 的 Ray 逻辑资源为 `CPU=0, GPU=0`head 不应承载训练任务调度资源(通过 `ray start --num-cpus=0 --num-gpus=0`)。
3) 集群节点数量正确:`ray status` 中应看到 1 head + 2 worker。
4) PPO driver 不在 head`ray job list` 里该 `submission_id``driver_info.node_ip_address` 应该是 worker 的 IP`172.19.0.3/172.19.0.4`),不能是 head`172.19.0.2`)。
5) PPO 训练只跑 1 个 epoch提交参数包含 `trainer.total_epochs=1`
6) checkpoint 落盘:`/mnt/shared/jobs/<job_id>/checkpoints/` 有产物(脚本通过 `trainer.default_local_dir` 强制指向该目录;不设置 `trainer.default_hdfs_dir`)。
7) 数据与缓存落盘:`/home2/argus/infra/mvp/shared/` 下出现 datasets/hf/jobs 等目录。
补充(磁盘保护):
- checkpoint 不要每步保存(会非常占空间);当前脚本默认 `trainer.save_freq=10`(每 10 step 保存一次)。
## 10. 目录命名约定submission id
- 脚本默认会显式指定 `ray job submit --submission-id=$SUBMISSION_ID`,并使用同一个值作为输出目录名:
- 输出目录:`/mnt/shared/jobs/$SUBMISSION_ID/`
- 你可以在提交时自定义 ID推荐这样便于检索
- `SUBMISSION_ID=my_run_20251219_001 ./src/mvp/v1/scripts/40_submit_ppo_epoch1.sh`
## 7. 风险点与兜底
- 如果 `runtime: nvidia` 在该环境不生效:需要改成 compose 的 `gpus:` 写法(我会按远程 docker 版本调整)。
- 如果 Ray Jobs 的 driver 必须在 head 启动Ray 机制如此):这不影响“训练任务不调度到 head”但 head 仍会有一个 job driver 进程。
- 如果 `verl` 在镜像内已安装但版本不匹配:脚本会优先 `pip install -e /workspace/verl` 以保证行为一致。
## 8. 你需要确认的 3 个问题(你已确认,我按此执行)
1) `verl/`:脚本会在远程自动 `git clone https://github.com/volcengine/verl.git`(如你希望固定版本,可改成同步或 checkout tag/commit
2) GPU`0-7` 可用worker0 用 `0-3`worker1 用 `4-7`)。
3) PPO用满 8 GPU`nnodes=2, n_gpus_per_node=4`)。
## 9. 你新增的关键要求(我已纳入脚本)
- 数据与模型必须落在 `/mnt/shared`(由宿主机 `./shared` bind mount 提供),并且具备**幂等**
- 数据:如果 `train.parquet/test.parquet` 已存在则跳过下载。
- 模型:优先检测本地 cache`HF_HOME=/mnt/shared/hf`);存在则跳过,否则才下载。
- 提交 job 时显式注入 `HF_HOME/HUGGINGFACE_HUB_CACHE/TRANSFORMERS_CACHE`,确保训练使用持久化缓存与数据路径。

View File

@ -1,194 +0,0 @@
# MVP v2.0 API 设计(最小可用)
v2.0 的 API 目标是:把 v1.1 的“脚本提交”变成“服务化提交”,并在服务侧实现队列/重试/状态聚合。
约束:
- 内部 token 鉴权(简单即可)。
- Ray Job 提交必须使用 **Ray Python SDK**`JobSubmissionClient`),不使用 `requests` 手写 HTTP。
- 输出与状态必须落盘到 NFS容器内 `/private`)。
---
## 1. 鉴权
- Header`Authorization: Bearer <INTERNAL_TOKEN>`
- 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-<workload>-<YYYYMMDD>-<HHMMSS>-<suffix>`
- 示例:`mvp2-ppo-20251223-143201-7f3a`
### 2.2 ray_submission_idattempt 级 ID
- 由 service 派生:`<task_id>--a<NN>`
- 示例:`mvp2-ppo-20251223-143201-7f3a--a01`
好处:
- Ray 的 submission id 自带 task_id可直接从 Ray dashboard 反查到服务侧任务。
- `/private/jobs/<ray_submission_id>/...` 目录天然隔离且可读。
---
## 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` 到期后继续尝试提交。

View File

@ -1,306 +0,0 @@
# MVP v2.0 开发计划(服务化入口 + 队列调度 + Ray Jobs SDK
目标:在 v1.1(脚本 + Ray Jobs SDK已验收通过的基础上交付一个**可独立运行的最小“服务层”**
- 用户通过 **HTTP API** 提交训练任务PPO/GRPO/SFT
- 服务层分配一个**人类易读的任务 ID**`task_id`),并把任务放入队列。
- 后台调度器在资源满足时再向 Ray 集群提交 Ray Job并持续追踪 Ray Job 状态。
- 针对 `verl`**fail-fast 资源预检查**(资源不足直接 `ValueError` 失败)做“服务级重试/排队”,避免用户反复手工提交。
> 约束继承 v1.1head 不跑训练driver 必须落到 worker共享存储只考虑 NFS容器内 `/private`)。
---
## 1. 背景:为什么 v2.0 需要“服务层调度”
在 v1.1 中我们通过 Ray Job 提交 `verl` 训练任务。`verl` PPO/GRPO 在初始化 worker 时会创建资源池,并做一次 fail-fast 的资源检查:
- 触发点:`ResourcePoolManager.create_resource_pool()` 末尾调用 `_check_resource_available()`
- `_check_resource_available()` 使用 `ray._private.state.available_resources_per_node()` 统计“可用 GPU/NPU”如果不足则直接抛异常
- `ValueError: Total available GPUs 0 is less than total desired GPUs 8`
这是一种合理的选择(避免 Ray 层面无限 pending/卡死),但会带来一个平台侧问题:
- 当集群暂时没有足够资源时,用户提交会“立刻失败”,需要手动重试。
因此 v2.0 的服务层要提供:
- **队列 + gang 约束**:资源不满足则任务在服务层 pending不提交到 Ray
- **状态追踪**:一旦提交到 Ray持续获取 Ray Job 状态并回传给用户。
- **资源不足的“自动重试”**:即使发生 race提交时资源够、启动时被抢走也能识别该类失败并延迟重试。
---
## 2. v2.0 交付范围Scope
### 2.1 必做MVP v2.0
1) **HTTP API**(内部 token
- 提交任务、查询任务、取消任务、拉取日志(最小可用)。
2) **任务队列与调度器**
- FIFO先到先服务无配额/公平性(留给 v3+)。
- gang`nnodes` + `n_gpus_per_node` 的固定资源需求“全有才提交”。
3) **Ray Jobs SDK 集成**(不使用 `requests` 自己拼 HTTP
- 通过 `ray.job_submission.JobSubmissionClient` submit/status/stop/logs。
4) **可观测/可排障最小集**
- 每个 task/attempt 落盘配置、提交载荷、Ray 返回的 `submission_id`、关键日志。
5) **失败策略**
- 识别 “资源不足 fail-fast” 类失败 → 转为 `PENDING_RESOURCES` 并延迟重试。
- 其他失败保持 `FAILED`(不自动重试,避免掩盖错误)。
### 2.2 不做v2.0 不实现)
- 多租户/配额/优先级/公平性调度v3
- Pipeline多 job 串联v3+)。
- 完整 UIv3+v2.0 可只提供 OpenAPI/Swagger
- K8s 编排(明确不做,仍是 Native Ray
---
## 2.3 工程原则(开闭原则 / 复用 v1.1
v2.0 研发遵循开闭原则Open/Closed Principle
- **对扩展开放**新增“服务层API + scheduler + SQLite”能力以支持排队、重试、状态聚合。
- **对修改关闭**:尽量不改动 v1.1 已经稳定可用的 Ray Jobs SDK 提交链路代码。
落地方式:
- 将 `src/mvp/v1.1/py/mvp_v11/` 作为“成熟可用提交层”,原样拷贝到 `src/mvp/v2.0/py/mvp_v11/` 供 v2.0 复用。
- v2.0 的新增功能全部在新模块实现(例如 `src/mvp/v2.0/py/mvp_v2/`),通过组合/封装来调用 `mvp_v11`,避免在旧代码中掺杂平台逻辑。
---
## 3. 总体架构v2.0
### 3.1 组件
- **mvp-api**HTTP Server
- 接收 JobSpec结构化字段保持与 v1.1 一致的语义)
- 生成 `task_id` 并写入持久化
- 提供 query/cancel/logs
- **mvp-scheduler**(后台调度器,可与 api 同进程也可拆进程)
- 轮询队列:对 `PENDING_RESOURCES` 的任务做资源判断
- 资源满足 → 调用 Ray Jobs SDK 提交 → 记录 `ray_submission_id`
- 对 `SUBMITTED/RUNNING` 的任务持续同步 Ray Job 状态
- 如果 Ray Job 失败且命中资源不足模式 → 延迟重试
> 部署建议v2.0 先在 **head 容器**内运行该服务dev/prod 行为一致;生产环境只能 ssh 进入容器纳管)。
### 3.4 dev 环境目录约定(示例)
以当前远程开发机为例(`argus@h1`
- 宿主机目录:`/home2/argus/infra/mvp/v2/`
- 容器内挂载:`/workspace/mvp/v2/`
- 共享 NFS容器内统一为 `/private/`(与 v1.1 保持一致)
> 注意:服务脚本(`v2/scripts/*.sh`)应在**宿主机**执行,通过 `docker exec` 控制 head 容器;训练 driver 仍通过 Ray entrypoint_resources 强制落到 worker。
### 3.2 与 Ray/容器的关系
- 服务进程运行在 head或等价能访问 head 的 Job server 地址)。
- 提交时仍使用 v1.1 的强约束:
- head`--num-cpus=0 --num-gpus=0`
- worker`--resources='{\"worker_node\": 100}'`
- job entrypoint`entrypoint_resources={\"worker_node\": 1}` 强制 driver 落 worker
---
## 3.3 配置约定(复用 v1.1 dev.yaml 并扩展)
v2.0 的服务层API + scheduler建议复用 v1.1 已存在的 RayConfig 文件:
- `src/mvp/v1.1/py/configs/dev.yaml`
原因:
- 其中已包含 v1.1 运行所需的 Ray 基础配置Ray Job server address、entrypoint_resources、runtime_env 等v2.0 也需要同样的信息来提交 Ray Jobs。
扩展方式:
- 在该 YAML 中新增一个顶层 `v2:` section存放 v2 服务专属配置API 监听、SQLite 路径、scheduler 间隔等)。
- v1.1 submitter 只读取 `address/shared_root/entrypoint_* /runtime_env/user_code_path`,会忽略 `v2:` 之类的额外字段;因此不会破坏 v1.1。
最小新增项建议(示例):
- `v2.api.host` / `v2.api.port`
- `v2.auth.token_env`(内部 token 环境变量名)
- `v2.sqlite.db_path`(建议 `/private/common/db/mvp_v2.sqlite3`
- `v2.scheduler.tick_s` / `v2.scheduler.retry_interval_s` / `v2.scheduler.max_running_tasks`
---
## 4. 核心数据模型Task / Attempt
### 4.1 Task用户视角的任务
- `task_id`**人类易读**且唯一,例如:
- `mvp2-ppo-20251223-143201-7f3a`
- `workload``ppo|grpo|sft`
- `jobspec`:提交参数(**保持 v1.1 的 jobspec YAML 字段与语义**;服务端解析 YAML 后入库)
- `state`:见第 5 节状态机
- `created_at` / `updated_at`
- `latest_attempt`:指向当前 attempt
- `attempts[]`:历史尝试列表
- `error_summary`:面向用户的简短错误(最后一次失败原因)
### 4.2 Attempt一次真实的 Ray Job 提交)
- `attempt_no`:从 1 开始递增
- `ray_submission_id`:建议派生自 task_id
- `ray_submission_id = <task_id>--a01`
- 好处Ray 侧输出目录天然可读、可追溯
- `status`Ray Job 状态PENDING/RUNNING/SUCCEEDED/FAILED/STOPPED
- `start_time` / `end_time`
- `exit_code`(如可取)
- `failure_kind`(枚举):
- `INSUFFICIENT_RESOURCES`(匹配 “Total available GPUs … less than total desired …”)
- `USER_ERROR`(配置/数据路径错误等)
- `RUNTIME_ERROR`(代码异常)
- `UNKNOWN`
---
## 5. 状态机(服务侧)
建议最小状态集:
- `QUEUED`:已入队,尚未进行资源判断
- `PENDING_RESOURCES`:资源不足,等待(服务侧 pending不提交 Ray
- `SUBMITTING`:正在向 Ray 提交 attempt
- `SUBMITTED`Ray 已接受 submission拿到 `ray_submission_id`
- `RUNNING`Ray Job RUNNING
- `SUCCEEDED`:任务成功(终态)
- `FAILED`:任务失败(终态,除非命中“资源不足重试策略”)
- `CANCELED`:用户取消(终态)
关键转换:
- `QUEUED -> PENDING_RESOURCES`:资源不足
- `QUEUED/PENDING_RESOURCES -> SUBMITTING`:资源满足
- `SUBMITTING -> SUBMITTED`:提交成功
- `SUBMITTED -> RUNNING`Ray 状态推进
- `SUBMITTED/RUNNING -> SUCCEEDED|FAILED`Ray 终态
- `FAILED (INSUFFICIENT_RESOURCES) -> PENDING_RESOURCES`进入延迟重试attempt_no+1
---
## 6. 调度策略v2.0
### 6.1 资源计算(对齐 verl 的“可用资源”口径)
由于 verl 使用 `ray._private.state.available_resources_per_node()` 做“可用资源”统计,
v2.0 的 scheduler 应该尽量使用相同口径,避免:
- 我们认为够了 → 实际 verl 认为不够(仍 fail-fast
- 我们认为不够 → 实际够了(浪费)
策略(建议):
1) scheduler 周期性获取 per-node 可用 GPU
2) 计算 total_available_gpus = sum(node_gpu_available)
3) 任务需求 total_required_gpus = nnodes * n_gpus_per_node
4) 如果 `total_available_gpus < total_required_gpus``PENDING_RESOURCES`
注意v2.0 先只做总量判断;节点级分配(保证每个 node 恰好 n_gpus_per_node可作为 v2.1+(资源池/标签/节点纳管)增强点。
### 6.2 排队与并发
- 默认 FIFO。
- 并发度:允许同时跑多个任务,但必须保证资源足够。
- 简化实现:如果任务默认都吃满 8 卡,则 scheduler 实际上一次只能跑一个。
- 若未来支持小任务1*1、1*4可以自然并发。
### 6.3 重试策略(资源不足)
当出现下面模式时判定为 `INSUFFICIENT_RESOURCES`
- Ray Job `status=FAILED`
- `JobDetails.message``job logs` 中匹配:
- `Total available GPUs``less than total desired`
处理:
- 将 task 置为 `PENDING_RESOURCES`
- `next_run_at = now + 60s`固定间隔v2.1 可改指数退避)
- attempt_no++ 后重提(新 submission id
---
## 7. SQLite 持久化(队列/状态/attempt
v2.0 引入一个**最小但可恢复的持久化层**:使用 SQLite 保存任务队列与状态,确保:
- api/scheduler 进程重启后,队列不丢;
- task/attempt 历史可追溯;
- 能实现“服务侧 pending + 延迟重试”的确定性行为。
### 7.1 存放位置
建议路径(容器内):
- `DB_PATH=/private/common/db/mvp_v2.sqlite3`
说明:
- v2.0 默认单实例服务(单 writerSQLite 足够。
- 生产环境若 NFS 上的 SQLite 有锁/性能风险v2.1+ 再演进到 Postgres/Redisv2.0 先以“可回放/可恢复”为第一目标。
### 7.2 表设计(建议最小集合)
- `tasks`
- `task_id` (PK)
- `workload`
- `state`(服务侧状态机)
- `jobspec_yaml`(原始 YAML 文本,原样落盘便于审计/复现)
- `created_at`, `updated_at`
- `next_run_at`(用于 `PENDING_RESOURCES` 的延迟重试)
- `error_summary`
- `latest_attempt_no`
- `attempts`
- `task_id` (FK)
- `attempt_no`
- `ray_submission_id`
- `ray_status`
- `failure_kind`
- `message`(截断后的关键信息)
- `start_time`, `end_time`
- `events`(可选,但非常利于排障)
- `id` (PK)
- `task_id`
- `ts`
- `event_type`STATE_TRANSITION / SUBMIT / RAY_STATUS_SYNC / RETRY_SCHEDULED 等)
- `payload_json`
### 7.3 调度循环(与 SQLite 的交互)
scheduler 每个 tick 做三件事:
1) **挑选可运行任务**FIFO + next_run_at
- `state IN ('QUEUED','PENDING_RESOURCES') AND next_run_at <= now`
2) **资源判断**(对齐 verl 的可用资源口径):
- 不满足:更新 `state='PENDING_RESOURCES'`,并写入 `next_run_at=now+60s`
3) **提交 Ray Job 并追踪**
- 提交成功:写入 `attempts` 并更新 `tasks.latest_attempt_no``state='SUBMITTED'`
- 周期性同步 Ray 状态:`SUBMITTED/RUNNING -> SUCCEEDED/FAILED`
- 若失败命中资源不足模式:`FAILED -> PENDING_RESOURCES` + 计划下次重试
---
## 8. 接口与验收DoD
### 8.1 API 能力(最小集合)
详见 `specs/mvp/v2.0/v2_api.md`
### 8.2 验收口径DoD
1) API 提交 PPO/GRPO/SFT返回 `task_id`,并在 NFS 上创建任务目录。
2) 当集群忙GPU 不足)时:
- task 状态为 `PENDING_RESOURCES`(不是 FAILED
- 一旦资源释放,任务自动变为 `SUBMITTED/RUNNING`
3) 当 race 导致触发 verl fail-fast
- attempt 标记为 `INSUFFICIENT_RESOURCES`
- task 回到 `PENDING_RESOURCES`,并在 60s 后自动重试
4) 通过 API 查询 task 能看到:
- 当前 state
- 最新 attempt 的 `ray_submission_id`
- attempt 历史(至少包含开始/结束/失败原因)
5) Cancel 能停止正在运行的 Ray Job调用 Ray Jobs SDK stop
---
## 9. v2.0 交付物建议(目录)
`specs/mvp/v2.0/`(本目录):
- `v2_plan.md`:总体设计与开发计划(本文件)
- `v2_api.md`API 详细定义(请求/响应/字段/错误码)
代码建议位置(后续实现时):
- `src/mvp/v2.0/`
- `py/`API server + scheduler
- `scripts/`:启动/停止/查看状态(仍沿用 v1.1 的 compose/cluster 逻辑)

View File

@ -1,15 +0,0 @@
# MVP v2.5Design— User Management & Stateless Ray Node Pool
本目录基于 `specs/mvp/mvp_roadmap_v2.md``specs/mvp/image/roadmap_v2.5.png` 的 v2.5 规划,
给出一份**可落地、可验证、可迭代实现**的详细方案设计文档集合。
v2.5 的核心变化:
- 在 v2.0 的任务队列/调度/重试基础上,引入 **User Management**多用户隔离、目录隔离、token
- 引入 **Stateless Ray Node Pool**worker 节点/容器不再需要平台显式下发 head 地址通过共享存储GPFS/NFS完成服务发现与自愈连接watchdog
文档:
- `specs/mvp/v2.5/v2.5_design.md`总体架构、关键机制head IP file / watchdog / 用户隔离 / 任务流)。
- `specs/mvp/v2.5/v2.5_api.md`API 设计(用户、任务、队列、日志)与鉴权约定。
- `specs/mvp/v2.5/v2.5_acceptance.md`:开发/部署/验收流程与可验证标准。
- `specs/mvp/v2.5/v2.5_summary.md`v2.5 已实现内容总结(本次迭代做了什么、验收结果、已知限制)。
- `specs/mvp/v2.5/v2.5_container_design.md`:将 stateless pool 固化到单镜像head/worker 复用 + supervisor 守护)的设计与验证流程。

View File

@ -1,3 +0,0 @@
# 记录问题
1. task 、 submission id 里加上 user name
2. 补全端到端测试用例,各种正常和异常用例,边界情况测试

View File

@ -1,67 +0,0 @@
# MVP v2.5 开发/部署/验收标准
本文件定义 v2.5 的“可验证闭环”,确保每个里程碑可验收。
---
## 1. 开发交付物Deliverables
### 1.1 代码交付(建议)
- API Server 增强user management + task 关联 user_id + 鉴权隔离
- SQLite schema 迁移:新增 users/tokenstasks 增加 user_id
- Ray Head service discoveryhead.json 写入与心跳刷新
- Worker bootstrap + watchdog
- dev以脚本方式提供docker compose 场景)
- prod以容器 command/entrypoint 方式可注入
### 1.2 文档交付
- 目录结构与 GPFS 路径约定
- API 文档(含用户与多租户隔离)
- 运维 SOPhead 重启、worker 自愈、如何排障 head.json
---
## 2. 部署流程Dev 环境可验证)
### 2.1 启动顺序(推荐)
1) 启动 head包含 API server + Ray head
2) head 写入 `/private/ray/discovery/<cluster_name>/head.json`
3) 启动若干 worker无须指定 head 地址)
4) worker 自动读取 head.json 并加入集群
5) 通过 API 创建用户并获取 token
6) 使用 user token 提交 PPO/GRPO/SFT
---
## 3. 验收标准Acceptance Criteria
### 3.1 Stateless Ray Node Pool
- A1在 worker 启动时不传 head 地址worker 能在 `T<=60s` 内加入集群ray status 可见)
- A2head 容器重启IP 变化或 Ray 重启)后:
- head.json 更新
- worker watchdog 在 `T<=60s` 内自动重连
- A3head 设置 `--num-gpus=0 --num-cpus=0`,训练 driver 不会跑到 head可通过 Ray dashboard/日志验证)
### 3.2 User Management
- U1admin 可创建用户并签发 tokentoken 仅返回一次)
- U2用户 A 提交的 task用户 B 无法查询/取消/获取日志API 返回 404 或 403按设计约定
- U3仅隔离 jobs 输出:任务输出落在 `/private/users/<user_id>/jobs/<ray_submission_id>/...`,不同用户互不覆盖
- U4训练输入verl 代码、HF cache、datasets统一使用 `/private/common/...`v2.5 不做输入隔离)
### 3.3 Task Flow继承 v2.0
- T1PPO/GRPO/SFT 三种 workload 都能成功提交并跑通dev 规模可用 epoch=1/steps=10
- T2资源不足时任务不会“直接失败不可恢复”而是进入 `PENDING_RESOURCES` 并按间隔重试(与 v2.0 同逻辑)
---
## 4. 回归用例(最小集合)
1) 创建用户 alice/bob分别提交 sft验证隔离与输出目录
2) 启动 head + 2 workers提交 ppo/grpo验证 driver 落 worker
3) 重启 head或修改 head.json 指向新 IP验证 worker watchdog 自动重连

View File

@ -1,109 +0,0 @@
# MVP v2.5 API 设计User + Task + Queue
v2.5 在 v2.0 API 基础上,新增 **User Management** 与多租户隔离。
约束:
- 仍使用内部 tokenAPI key
- 不引入外部 IAM
- TaskSpec 仍为 YAML沿用现有结构化字段
---
## 1. Auth
Header
- `Authorization: Bearer <api_token>`
服务端行为:
- 将 `api_token` 映射到 `user_id`
- 之后的 task 操作默认仅作用于该 `user_id`
Admin token可选
- 支持额外配置 `MVP_ADMIN_TOKEN`(或 user.role=admin
- admin 可跨用户查询/取消(用于运维)。
---
## 2. User Management
### 2.1 创建用户admin
`POST /api/v2/users`
RequestJSON
```json
{"user_id":"alice","display_name":"Alice"}
```
Response
```json
{"user_id":"alice","state":"ACTIVE"}
```
### 2.2 为用户签发 tokenadmin
`POST /api/v2/users/{user_id}/tokens`
Response只返回一次明文 token
```json
{"user_id":"alice","token":"mvp_u_..."}
```
### 2.3 禁用用户admin
`POST /api/v2/users/{user_id}:disable`
---
## 3. Task Management多租户
### 3.1 提交任务
`POST /api/v2/tasks`
Body
- `Content-Type: application/yaml`
- raw TaskSpec YAML训练语义字段不含 user_id
Response
```json
{"task_id":"mvp25-ppo-20251225-170001-2a3f","state":"QUEUED"}
```
服务端 side effects
- 记录 tasks.user_id由 token 得到)
- 计算输出目录:`/private/users/<uid>/jobs/<ray_submission_id>/...`
### 3.2 查询任务(仅本人)
`GET /api/v2/tasks/{task_id}`
若 task 不属于当前 user
- 返回 `404`(避免泄露存在性)
### 3.3 取消任务(仅本人)
`POST /api/v2/tasks/{task_id}:cancel`
---
## 4. Queue/Debug
### 4.1 查看队列(本人视角)
`GET /api/v2/queue`
返回该 user 的 pending/running 列表。
### 4.2 管理员查看全局队列admin
`GET /api/v2/admin/queue`
---
## 5. Logs
`GET /api/v2/tasks/{task_id}/logs?attempt=latest&tail=2000`
行为与 v2.0 一致:透传 Ray Job logs tail。

View File

@ -1,202 +0,0 @@
# MVP v2.5 — Stateless Ray Node Pool 容器固化设计
目标:把 v2.5 的 **stateless poolhead discovery + worker watchdog** 能力固化到一个可复用镜像中,避免依赖宿主机脚本在容器内 `docker exec` 启动/守护进程。**同一个镜像同时供 head/worker 复用**,通过环境变量区分角色。
约束:**API server 代码与镜像解耦**,短期仍按现状“宿主机代码挂载到 head 容器,在 head 容器内启动 API”不把 API 代码打进本镜像。
---
## 1. 背景(现状与痛点)
当前 `src/mvp/docker-compose.yaml` 里 head/worker 都基于 `verlai/verl:sgl055.latest`,容器启动后 `command: sleep infinity`,再由宿主机脚本完成:
- head`ray start --head ...` + `head_publisher`(写 `head.json`
- worker`worker_watchdog`(读取 `head.json`,自动加入/重连 ray 集群)
现状问题:
- 启动流程依赖宿主脚本 `docker exec`,易受权限/路径/人为操作影响;
- “守护”目前是 bash while-loop出现异常时排障成本高
- 未来生产环境容器可能由算力平台拉起,我们只能 SSH 纳管,更需要把“自启动 + 自愈”放到容器内部。
---
## 2. v2.5 容器固化目标与非目标
### 2.1 目标
- **一个镜像复用**head/worker 统一镜像,通过 `ARGUS_ROLE=head|worker` 区分。
- **supervisor 守护**:无论 head/worker都使用 `supervisord` 守护关键进程:
- watchdog 崩溃 → supervisor 自动重启 watchdog
- ray 节点崩溃 → watchdog/或 supervisor 触发自动恢复(见 3.2 进程模型)
- **与共享存储对齐**:容器内统一挂载根路径 `/private`discovery 文件写到共享存储。
- **最小内置代码**:镜像只内置 stateless pool 相关 python 脚本discovery/publisher/watchdog/entrypoint不把 API 服务代码打进镜像。
- **远端构建**:镜像构建必须在开发/运行机器(例如 `argus@h1`)上完成,本机不要求具备 `verlai/verl:*` 基础镜像。
### 2.2 非目标(本迭代不做)
- 不把 API server 打包进本镜像(后续可做单独 `argus-api` 镜像)。
- 不改变 v2.5 TaskSpec 约束(仍使用 `/private/common/...` 公共资源;用户隔离只隔离 jobs
- 不在本迭代引入 K8s/operator/autoscaler只固化容器自启动/自愈。
---
## 3. 设计方案
### 3.1 单镜像架构概览
新增一个镜像(示例名):
- `argus/argus-ray-node:v2.5`
该镜像:
- `FROM verlai/verl:sgl055.latest`(通过 build-arg 可切换 base
- 内置:
- `argus_raypool`(或复用现有 `argus.ray.*` 子集)脚本:
- `discovery.py`head record 读写head.json
- `head_publisher.py`head 写入 head.json带 TTL/刷新)
- `worker_watchdog.py`worker 读取 head.json自动加入/重连
- (可选)`head_watchdog.py`:把 “ray head + publisher” 组装成一个可恢复的 watchdog
- `/usr/local/bin/argus-entrypoint.sh`:根据 role 生成 supervisor 配置并启动 supervisor
- supervisor 配置模板(或运行时生成)
### 3.2 进程模型确保“ray 崩/ watchdog 崩都能恢复”)
用户新增要求head/worker 均要 supervisor 守护 watchdogray 节点崩溃或 watchdog 崩溃都要自动恢复。
推荐进程组织(避免 “ray start” 后台化导致 supervisor 无法感知):
#### A) Head 容器ARGUS_ROLE=head
由 supervisor 启动 **两个 program**
1) `argus_head_watchdog`(推荐实现为 python 或 bash内部用 `ray start --head --block` 前台运行)
- 关键点:`ray start --head --block` 让 Ray 进程前台阻塞watchdog 作为父进程能感知退出码
- ray 崩 → `ray start --block` 返回 → watchdog 退出非 0 → supervisor 重启 watchdog → ray 自动重启
2) `argus_head_publisher`
- 定期刷新 `head.json`TTL/refresh
- publisher 崩 → supervisor 自动重启
> 备选:把 publisher 逻辑合并进 `argus_head_watchdog`(一个进程同时跑 ray + publisher 线程),减少 supervisor program 数量;但拆分更易观测与定位问题。
#### B) Worker 容器ARGUS_ROLE=worker
由 supervisor 启动 **一个 program**
1) `argus_worker_watchdog`
- 轮询读取 `head.json`,并以 `ray start --address=<head>:6379 --block` 方式加入集群
- 只要 ray 进程退出ray 崩/被 stop`--block` 结束watchdog 进入下一轮重连/重启
- watchdog 自己异常退出 → supervisor 自动重启 watchdog
> 注意:当前仓库里的 `worker_watchdog.py` 是 “`ray start` 非 block + 仅在 head addr 变化时重启”。容器固化建议升级为 “`--block` + 监测 ray 退出” 模式,否则 supervisor 很难准确感知 ray 的生命周期。
### 3.3 配置与环境变量Role 驱动)
镜像入口只依赖环境变量,不依赖宿主脚本参数。
建议环境变量清单(含默认值):
- `ARGUS_ROLE``head` / `worker`(必填)
- `ARGUS_SHARED_ROOT`:默认 `/private`
- `ARGUS_CLUSTER_NAME`:默认 `argus-ray`
- `ARGUS_HEAD_IP_FILE`:默认 `${ARGUS_SHARED_ROOT}/ray/discovery/${ARGUS_CLUSTER_NAME}/head.json`
- `ARGUS_RAY_PORT`:默认 `6379`
- `ARGUS_DASHBOARD_PORT`:默认 `8265`head
- `ARGUS_TTL_S`:默认 `60`head publisher
- `ARGUS_REFRESH_S`:默认 `10`head publisher
- `ARGUS_POLL_S`:默认 `5`worker watchdog
- `ARGUS_NODE_IP`:默认空;若空则 entrypoint 自动探测容器 IP
- `ARGUS_WORKER_RESOURCES_KV`:默认 `worker_node=100`(用于 driver 强制落 worker 的自定义资源)
- `ARGUS_RAY_EXTRA_ARGS`:可选,传递额外 `ray start` 参数
- `ARGUS_LOG_DIR`:默认 `${ARGUS_SHARED_ROOT}/common/logs`(落到共享目录便于排障)
### 3.4 Dockerfile / entrypoint / supervisor 设计
#### Dockerfile建议路径
在仓库新增(后续实现时):
- `src/mvp/images/argus-ray-node/Dockerfile`
- `src/mvp/images/argus-ray-node/entrypoint.sh`
- `src/mvp/images/argus-ray-node/supervisord.conf.tmpl`(可选)
- `src/mvp/images/argus-ray-node/py/argus_raypool/*.py`(仅 stateless pool 子集)
Dockerfile 关键动作:
- `FROM verlai/verl:sgl055.latest`(可 `ARG BASE_IMAGE=...`
- 安装 supervisor
- Debian/Ubuntu 基底:`apt-get update && apt-get install -y supervisor`
- 设定 `CMD ["supervisord","-n","-c","/etc/supervisor/supervisord.conf"]`
- 拷贝 python 脚本到 `/opt/argus/raypool` 并设置 `PYTHONPATH=/opt/argus`
- 拷贝 entrypoint 到 `/usr/local/bin/argus-entrypoint.sh`
- `ENTRYPOINT ["/usr/local/bin/argus-entrypoint.sh"]`
entrypoint.sh 逻辑:
- 探测容器 IP`hostname -i``ip route get 1.1.1.1`
- 根据 `ARGUS_ROLE` 生成 supervisor 配置:
- head启动 `head_watchdog` + `head_publisher`
- worker启动 `worker_watchdog`
- 配置 supervisor
- `autorestart=true`
- `startretries` 合理配置
- stdout/stderr 指向 `${ARGUS_LOG_DIR}/...` 或直接 stdout便于 `docker logs`
### 3.5 与 API server 的关系(保持解耦)
API server 仍按现状(短期方案):
- **代码存放在宿主机**,通过 volume mount 挂载到 head 容器(例如 `/workspace/mvp`)。
- **在 head 容器内启动 API**(例如用脚本 `docker exec argus-ray-head ... python3 /workspace/mvp/py/server.py`)。
- 关键点:即使 API 进程跑在 head 容器里,也仍视作“独立于 ray node 镜像的业务代码”,后续可独立演进为单独的 `argus-api` 镜像。
- 只要 API 能访问 Ray job server通常 `http://127.0.0.1:8265` 在 head 容器视角)即可。
未来(非本迭代)可将 API server 单独做 `argus-api` 镜像,按相同 `/private` 共享目录运行。
---
## 4. docker-compose 调整建议(后续实现)
当前 compose 的变化点(概念上):
- `image: verlai/verl:sgl055.latest``image: argus/argus-ray-node:v2.5`
- `command: sleep infinity` 移除(镜像自带 entrypoint
- head service 增加:
- `ARGUS_ROLE=head`
- 暴露 dashboard 端口保持 `8265:8265`
- worker service 增加:
- `ARGUS_ROLE=worker`
- `ARGUS_WORKER_RESOURCES_KV=worker_node=100`
- volumes 仍需要:
- `../../shared:/private`(共享存储)
- `../../verl:/workspace/verl`verl 代码/依赖按现状)
---
## 5. 验证与回归流程(落地后怎么验收)
### 5.1 构建镜像
1) **在远端 `argus@h1` 构建**(本机不要求具备基础镜像):
- `cd /home2/argus/infra/mvp/src/mvp`
- `docker build -t argus/argus-ray-node:v2.5 -f images/argus-ray-node/Dockerfile .`
2) 也可以使用 compose build推荐和实际运行一致
- `docker compose -f docker-compose.yaml build --no-cache`
### 5.2 基础连通性stateless pool 验证)
1) `docker compose up -d`
2) 验证 head 写入:
- 共享目录存在 `head.json``${ARGUS_SHARED_ROOT}/ray/discovery/${ARGUS_CLUSTER_NAME}/head.json`
3) 验证 worker 自动加入:
- 在 head 容器内 `ray status` 能看到 worker 节点加入
- Dashboard Nodes 页面能看到 head + worker
### 5.3 故障注入supervisor 自愈验证)
1) watchdog 崩溃:
- `pkill -f worker_watchdog`(或 kill 对应 PID
- 期望supervisor 自动拉起 watchdogworker 最终重新加入集群
2) ray 节点崩溃worker
- `ray stop --force` 或 kill raylet
- 期望watchdog 重新执行 `ray start ... --block`worker 恢复
3) ray 节点崩溃head
- kill head ray 前台进程(由 watchdog 启动)
- 期望supervisor 重启 head_watchdoghead 恢复并重写 head.jsonworkers 自动重连
### 5.4 端到端任务回归(与 v2.5 API 协作)
沿用现有 v2.5 E2E
- `src/mvp/scripts/run_all_v25_api.sh`
- `src/mvp/scripts/run_e2e_v25_cases.sh`
验收标准:
- PPO/GRPO/SFT 均能在 worker 上运行head 不跑训练
- API 的 task_id / submission_id 正常携带用户名
- 资源不足可转 `PENDING_RESOURCES` 并按周期重试
---
## 6. 风险点与对策
- **ray start 后台化**如果继续后台启动supervisor 不易感知 ray 崩溃。对策:使用 `ray start --block`(推荐)。
- **IP 探测不稳定**不同环境compose/平台)容器 IP 获取方式不同。对策entrypoint 做多策略探测并允许 `ARGUS_NODE_IP` 显式覆盖。
- **日志可观测性**:建议同时支持写到 `/private/common/logs`(共享)以及 stdout`docker logs`)。

View File

@ -1,255 +0,0 @@
# MVP v2.5 详细设计方案User Management + Stateless Ray Node Pool
本文目标:把 `mvp_roadmap_v2.md` 中 v2.5 的思路落到**可工程化实现**的设计层,包括:
- API Server 内新增 user management
- Ray node pool 变为无状态worker 自发现 head、自动加入、watchdog 自愈);
- 仍保持 v2.0 的“任务管理层”语义Task/Attempt、队列、资源判断、Ray Job 提交与状态同步;
- 所有共享数据/状态统一落在 GPFSdev 环境可先用 NFS容器内路径统一为 `/private/`
> 术语说明文中“GPFS”代表生产共享存储dev 环境可用 NFS但容器内仍以 `/private/` 访问。
---
## 1. 目标与非目标
### 1.1 v2.5 目标Must
1) **User Management最小多租户**
- 支持创建/禁用用户;
- 为每个用户签发内部 tokenAPI key用于认证与隔离
- 用户隔离v2.5 先做最小闭环,仅隔离 **jobs 输出** 与 API 可见性):
- 用户只能看到/操作自己的 Task
- 训练输出job root、checkpoints、日志归档等按 user 目录落盘;
- 训练输入verl 代码、HF cache、datasets统一使用 `common/`v2.5 不支持用户自定义代码/模型/数据集隔离)。
2) **Stateless Ray Worker Node Pool**
- worker 容器启动时无需被平台告知 head 地址;
- worker 通过 GPFS 读取 **Head IP File** 自动连接 Ray head
- worker 内部 watchdog 监控 head 地址变化,发生变化时自动 `ray stop` + `ray start` 重连;
- worker 尽量不依赖本地持久化状态(宕机/替换后可无感重建)。
3) **保持 v2.0 的 Task 管理行为**
- Task/Attempt 模型不变(或向后兼容扩展);
- 对齐 verl 的 fail-fast 行为:资源不足时服务侧 pending + 重试;
- Ray Job 提交仍通过 Ray Python SDKJobSubmissionClient
### 1.2 v2.5 非目标Not Now
- 完整 WebUI留到 v3.0)。
- 公平调度/配额/优先级(留到 v3.x
- 完整生产级 IAM留到 v4+v2.5 仅内部 token。
- K8s 原生编排(本阶段不要求,但设计需能适配“算力平台拉起容器,只能 ssh 进去纳管”的模式)。
---
## 2. 总体架构(对应 roadmap v2.5
### 2.1 组件划分
**控制面Control Plane**
- **API Server**
- user management
- task management队列/调度/重试/状态聚合)
- Ray Job ToolRay Client
- VerlTaskSpecTaskSpec YAML沿用 v2.0/v2.1 格式)
- 与 Ray head 在同一台/同一容器是推荐形态(便于访问 dashboard / job server
- **Ray Head有状态**
- 启动后把 head 地址写入 GPFS 的 Head IP File用于 worker 服务发现
**数据面Data Plane**
- **Ray Workers无状态节点池**
- stateless bootstrap从 GPFS 读取 head 地址自动加入集群
- watchdog持续 watch head 地址文件变化并自愈重连
**共享存储GPFS**
- 统一数据路径:数据、模型 cache、代码、任务输出、以及 head 服务发现文件。
### 2.2 v2.5 的控制反转IoC
与 v2.0/手工集群的关键差异:
- v2.0:平台脚本/运维显式启动 worker 并指定 `--address=<head>`
- v2.5worker 自己从 GPFS 读取 `head_ip_file`,无需平台维持 worker 列表与 SSH 连接池。
---
## 3. GPFS 目录结构(容器内 `/private`
建议在 v2.5 固化以下目录(与现有 v2.0 兼容扩展):
```
/private/
ray/
discovery/
<cluster_name>/
head.json # Head IP File服务发现
head.json.lock # 可选写入锁v2.5 可先不实现)
users/
<user_id>/
jobs/ # /private/users/<uid>/jobs/<ray_submission_id>/*
outputs/ # 训练输出聚合(按需要)
common/
code/ # 平台/公共代码快照verl code snapshot 等)
datasets/ # 公共数据集
hf/ # 公共 HF cachedev 复用)
db/ # sqlite
logs/ # API 日志、平台日志
```
说明:
- `common/`平台默认目录v2.5 先默认所有用户可写;后续再加 ACL/只读)。
- `users/<user_id>/...`:用户隔离主边界(最小多租户的关键)。
---
## 4. Head IP File服务发现设计
### 4.1 文件路径
- `head_ip_file = /private/ray/discovery/<cluster_name>/head.json`
- `<cluster_name>`:由配置指定(例如 `argus-ray`),允许同一 GPFS 上存在多个环境/集群。
### 4.2 文件内容JSON
建议采用 JSON易扩展
```json
{
"cluster_name": "argus-ray",
"head_ip": "10.0.0.12",
"gcs_port": 6379,
"dashboard_port": 8265,
"job_server_url": "http://10.0.0.12:8265",
"updated_at": "2025-12-25T17:00:00Z",
"expires_at": "2025-12-25T17:01:00Z"
}
```
关键点:
- `updated_at`:便于排障与可观测;
- `expires_at`:避免 worker 读取到“陈旧 head 地址”后无限重连;
- `job_server_url`:对外可直接用于 Ray Job Tool 配置(便于无脑接入)。
### 4.3 写入策略(原子更新)
Head 写入时必须保证 worker 读取不会读到半文件:
- 写临时文件 `head.json.tmp`
- `fsync`(可选);
- `rename(head.json.tmp -> head.json)`(原子替换)。
### 4.4 心跳与 TTL
Head 进程需周期性刷新 `head.json`
- 建议 `ttl_s=60`,刷新周期 `refresh_s=10`
- 若 head 进程异常退出worker 读取到过期文件可进入“等待模式”而非无限重连。
---
## 5. Stateless Worker Bootstrap + Watchdog
### 5.1 启动序列worker 容器内)
1) 启动脚本读取 `head.json`
- 若文件不存在sleep + 重试(直到存在)
- 若存在但 `expires_at` 已过期sleep + 重试(直到变为新鲜)
2) 解析 `head_ip:gcs_port` 并执行:
- `ray stop --force || true`
- `ray start --address=<head_ip>:<gcs_port> --resources='{"worker_node": 100, ...}' ...`
3) 启动 watchdog 进程(同容器):
- 轮询/监听 `head.json` 的内容变化
- 一旦 `head_ip``gcs_port` 改变,触发 `ray stop` + `ray start` 重连
### 5.2 Watchdog 策略(最小可用)
v2.5 推荐“简单且稳”的实现:
- polling 间隔 `watch_s=5`
- 对比 `head.json``updated_at` 或 hash
- 若发现变更:执行重连;
- 若连续多次重连失败指数退避v2.5 可先固定退避v2.6 再增强)。
### 5.3 资源标签driver 强制落 worker
继续沿用 v2.0 的思路:
- worker 启动时 `--resources='{"worker_node": 100}'`
- head 不包含 `worker_node` 资源
- Ray job submit 时设置 entrypoint_resources`{"worker_node": 1}`
### 5.4 GPU/CPU 的“无状态”约束
- worker 是否有 GPU 由底层算力平台决定(生产上平台会为容器挂载 GPU
- worker 启动脚本不应硬编码 GPU 编号,只依赖 `NVIDIA_VISIBLE_DEVICES`/平台注入;
- head 推荐 `--num-gpus=0 --num-cpus=0`,避免训练调度到 head。
---
## 6. User Management 设计(最小多租户)
### 6.1 数据模型SQLite
新增两张表(示意):
- `users`
- `user_id`PK
- `display_name`
- `state`ACTIVE/DISABLED
- `created_at`
- `api_tokens`
- `token_hash`PK
- `user_id`FK
- `created_at`
- `last_used_at`
并在 `tasks` 表增加:
- `user_id`FK
### 6.2 鉴权策略
内部 token 模式:
- `Authorization: Bearer <token>`
- 服务端将 token 映射到 `user_id`
- 后续所有 task 查询/取消/日志默认 scope 到该 `user_id`
管理员能力v2.5 最小实现):
- 额外配置一个 admin token或把特定 user 标记为 admin
- admin 可 list all users/tasks用于运维排障
### 6.3 用户目录隔离(路径约束)
核心原则v2.5 版):
- **输出**:必须落在 `/private/users/<uid>/jobs/...`(服务端统一计算,不允许用户任意指定输出根)
- **输入**:统一使用 `/private/common/...`v2.5 不支持用户自定义 verl 代码、也不做 hf/datasets 的用户隔离)
服务端处理策略(最小可用):
- 解析 TaskSpec 后,对输入路径字段做白名单前缀校验(必须是 `/private/common/...`;拒绝 `../` 与越界路径);
- 输出目录统一由服务端计算:`job_root = /private/users/<uid>/jobs/<ray_submission_id>/`
---
## 7. TaskSpecVerlTaskSpec YAML在 v2.5 的扩展点
v2.5 **不扩展 TaskSpec**:保持与 v2.0/v2.1 的 YAML 结构化字段与语义一致。
v2.5 的“用户语义”仅体现在服务端的补齐/约束:
- user_id 由 token 推导(用户不需要在 YAML 里写 user_id
- 服务端派生 `ray_submission_id`(由 task_id/attempt 派生);
- 服务端统一计算输出目录 `job_root=/private/users/<uid>/jobs/<ray_submission_id>/...`
- v2.5 不支持用户自定义 verl 代码路径(因此 runtime_env 不需要注入用户 code 目录)。
---
## 8. 迁移与兼容性
v2.5 设计需满足:
- 现有 v2.0 的“手工启动 worker”仍可运行作为 dev fallback
- 在不改镜像的前提下worker watchdog 可以以“容器启动命令/entrypoint”方式注入dev 用 scripts生产由算力平台指定 command
---
## 9. 风险与对策v2.5
1) **GPFS 上 head.json 一致性/延迟**
- 对策:原子 rename + TTLwatchdog polling。
2) **Ray head 重启后 job server URL 变化**
- 对策head.json 内写入 `job_server_url`Ray Job Tool 可读取该文件更新 addressv2.6 可做动态 reload
3) **Worker 重连期间任务波动**
- 对策:服务侧调度器对齐 verl 的资源 fail-fast任务失败可归因并排队重试。

View File

@ -1,229 +0,0 @@
# MVP v2.5 开发计划TDD 驱动)
本文是 v2.5 的**工程化开发计划**强调“先写测试再写实现”TDD并将每个里程碑拆成**可独立验收**的小闭环。
输入依据:
- 路线图:`specs/mvp/mvp_roadmap_v2.md`
- v2.5 设计:`specs/mvp/v2.5/v2.5_design.md`
- v2.5 API 草案:`specs/mvp/v2.5/v2.5_api.md`
- v2.5 验收:`specs/mvp/v2.5/v2.5_acceptance.md`
v2.5 约束(已确认):
- **不扩展 TaskSpec**:沿用 v2.0/v2.1 的 YAML 结构化字段与语义。
- **不支持自定义 reward function / 不支持用户自定义 verl 代码**
- 训练输入verl 代码、HF cache、datasets统一使用 `/private/common/...`
- 多用户隔离 v2.5 **先只隔离 jobs 输出目录**`/private/users/<uid>/jobs/<ray_submission_id>/...`
---
## 0. TDD 规范(所有功能都遵循)
### 0.1 测试分层
1) **单元测试fast**
- 纯 Python 逻辑DB、鉴权、ID、路径派生、head.json 解析/TTL、watchdog 决策逻辑。
- 目标:不依赖真实 Ray、不依赖 docker、不依赖网络。
2) **组件测试(中等)**
- FastAPI 路由:使用 `fastapi.testclient.TestClient`(现有 v2.0 已采用)。
- 目标:验证 auth/权限隔离、API 行为、状态机。
3) **端到端(慢/手工或脚本)**
- 在 `argus@h1` 上通过 scripts/compose 跑一次“head publish → worker auto-connect → API submit”闭环。
- 目标:验证无状态 worker + watchdog 的真实行为。
### 0.2 测试约定
- 测试目录:`src/mvp/py/tests/`
- 新增功能必须先补齐测试用例,并让其在未实现时失败(红)。
- 实现最小改动让测试变绿(绿)。
- 重构/去重复(重构)。
> 注:现有测试通过 `src/mvp/py/tests/conftest.py` 注入 ray stub确保单测不依赖真实 ray 包v2.5 新增模块也应复用此模式。
---
## 1. 里程碑拆分v2.5 = 4 个可验证闭环)
### M1User 表/Token 表 + 基础鉴权(不影响现有内部 token 兼容)
**目标**
- 引入 user/token 的持久化与鉴权映射token → user_id
- 兼容现有 `Authorization: Bearer <MVP_INTERNAL_TOKEN>` 的“单租户模式”,避免一次性破坏 v2.0 用法:
- v2.5 可以先支持两种 token 模式:
- legacy环境变量 `MVP_INTERNAL_TOKEN`(全局单租户);
- user tokenDB 内签发 token多用户
- admin 能创建用户、签发 token、禁用用户。
**TDD 用例(先写测试)**
单测:
- `test_user_db_create_disable()`
- 创建用户 ACTIVE禁用后状态变为 DISABLED重复创建返回冲突或幂等按最终约定
- `test_token_hashing()`
- 签发 token 时 DB 中只保存 hash不保存明文。
API 测试TestClient
- `test_admin_create_user_and_issue_token()`
- admin token 可创建用户并签发 token明文 token 只返回一次)。
- `test_disabled_user_token_rejected()`
- 用户被禁用后,使用旧 token 调用 API 返回 401/403。
**实现落点(建议模块)**
- `argus.service.auth`token 校验与 user_id 解析(兼容 legacy 模式)
- `argus.service.db`:新增 `users``api_tokens` 表与 CRUD
- `argus.service.app`:新增 user 管理 endpointsadmin scope
- `configs/dev.yaml`:补充 admin token/env 相关配置(保持 YAML 风格)
**验收点**
- `v2.5_acceptance.md`U1 可通过自动化 API 测试覆盖。
---
### M2Task 绑定 user_id + API 可见性隔离(仍不改 TaskSpec
**目标**
- 提交 task 时由 token 推导 `user_id`,写入 `tasks.user_id`
- task 查询/取消/日志默认只允许 owner他人访问返回 404避免泄露存在性
- queue 默认只返回当前用户队列admin 可查询全局队列(可选)。
**TDD 用例(先写测试)**
单测:
- `test_tasks_table_has_user_id()`:创建任务必须落 `user_id`,且 `list_queue(user_id=...)` 只返回该用户任务。
API 测试:
- `test_task_visibility_isolated()`
- user A 创建 taskuser B 查询 `/api/v2/tasks/{id}` 返回 404
- user B cancel/logs 也返回 404。
- `test_queue_isolated()`
- A/B 各自创建 task`GET /api/v2/queue` 只看到自己的。
**实现落点**
- `argus.service.app`:为 task endpoints 增加 user scope
- `argus.service.db`tasks 表增加 user_id 字段、索引、按 user 过滤的查询方法
- `argus.service.scheduler`pick_next_runnable_task 等仍按“全局 FIFO”或“按 user FIFO”
- v2.5 先保持“全局 FIFO”最简单但 API queue 视角是按 user 过滤)。
**验收点**
- `v2.5_acceptance.md`U2 可通过 API 测试覆盖。
---
### M3Jobs 输出目录按 user 隔离(只改输出,不改输入)
**目标**
- Ray Job 的 job_root 目录由服务端统一计算到:
- `/private/users/<uid>/jobs/<ray_submission_id>/...`
- TaskSpec 内与输入相关的路径字段必须是 `/private/common/...`v2.5 输入统一 common
- 任何用户无法通过 TaskSpec 指定输出写到非 user jobs 目录(避免越权写)。
**TDD 用例(先写测试)**
单测:
- `test_job_root_derivation_per_user()`
- 给定 user_id 与 ray_submission_id派生 job_root 固定且正确。
- `test_reject_non_common_inputs()`
- TaskSpec 中 train_file / val_file / code_path / hf 路径等若不以 `/private/common/` 开头则拒绝HTTP 400
API 测试:
- `test_job_dir_written_under_user_jobs()`
- 提交 task 后,在 DB 或 submit payload 中能看到 job_root 在 user jobs 下(可通过 mock RayJobTool.submit 捕获 spec
**实现落点(建议最小侵入)**
- 在 service 层派生 `job_root` 并注入到 RayJobTool/builders而不是让用户从 TaskSpec 指定)。
- RayJobTool `_job_dir()` 改为接收“job_root 生成器”或直接接收 `job_root` 参数(由服务层提供)。
- 目标:保持 RayJobTool 的职责清晰:提交 Ray job路径策略由 service 决定。
**验收点**
- `v2.5_acceptance.md`U3/U4 可通过 API/单测覆盖。
---
### M4Stateless Ray Node Poolhead.json + worker watchdog+ 端到端脚本验证
**目标**
- head 启动后持续写入 `/private/ray/discovery/<cluster_name>/head.json`(包含 TTL
- worker 容器内运行 watchdog或启动脚本 + watchdog无需平台显式传 head 地址:
- 读取 head.json存在且未过期`ray start --address=<head_ip>:<gcs_port>`
- head.json 变化 → `ray stop` + `ray start` 重连
- 在 dev 环境docker compose提供一键脚本复现e2e
**TDD 用例(先写测试)**
单测(不跑真实 ray
- `test_head_json_read_validate_ttl()`
- 文件不存在/过期 → 返回“不可用”
- 未过期 → 返回 head 地址
- `test_watchdog_decision_on_change()`
- head_ip 变化 → 触发重连动作
- only updated_at 变化(地址不变)→ 不重连(或按策略重连,需确定)
组件/脚本级测试(可选):
- 如果 watchdog 用 Python 实现,可对“执行命令”层做 stub不真正跑 `ray start`),只验证会调用什么命令。
端到端脚本(手工/慢):
- 提供脚本 `scripts/run_all_v25_stateless.sh`(命名示例):
1) 起 headRay head + API
2) 启动 head publisher写 head.json
3) 起 2 个 worker每个 4 GPUworker 只跑 watchdog不传 head 地址
4) `ray status` 显示 1 head + 2 worker 且 GPU=8
5) 通过 API 创建用户/签发 token提交 PPO/GRPO/SFT
6) 重启 head或更新 head.json 指向新地址)验证 worker 自动重连
**实现落点(建议实现策略)**
为了可测试性TDD推荐把“读 head.json/判定 TTL/生成 ray start 命令”做成 Python 模块:
- `argus.ray.discovery`read/write head.json原子写、TTL
- `argus.ray.worker_watchdog`watch looppolling + change detection执行命令可注入便于单测 stub
脚本层保持薄:
- `scripts/` 负责 docker exec / compose 编排与进程守护;
- watchdog 进程由容器内 python 模块运行(更可测、更易移植到生产平台的 entrypoint/command
**验收点**
- `v2.5_acceptance.md`A1/A2/A3 主要通过 e2e 脚本 + dashboard/日志验证。
---
## 2. 回归策略(确保 v2.0 不被破坏)
在 v2.5 过程中保留并持续回归以下用例(至少单测覆盖):
- 旧的内部 token 模式仍可访问 `GET /api/v2/queue` 与提交 task若决定保留兼容
- scheduler 的“资源不足 → PENDING_RESOURCES → 延迟重试”行为不变(现有 `test_scheduler.py` 覆盖)。
- `ray entrypoint_resources` 强制 driver 落 worker继续使用 `worker_node` 自定义资源)。
---
## 3. 交付清单(代码/脚本/文档)
### 3.1 代码
- user/tokensDB schema + auth + API endpoints
- tasks绑定 user_id + 权限隔离
- job_root按 user jobs 输出目录派生(输入仍 common
- discovery/watchdoghead.json + worker 自愈
### 3.2 scriptsdev e2e
- head启动 Ray head + head publisher
- workers以无状态方式启动不传 head addr+ watchdog
- `run_all`:一键跑通(含 API submit + 查询 + cancel + 观察队列)
### 3.3 文档
- 更新 `specs/mvp/v2.5/*`(设计/API/验收/开发计划)
- 补充 `src/mvp/README.md` 的 v2.5 使用方式(如需要)
---
## 4. 关键待确认点(开始实现前必须定稿)
1) **legacy token 是否继续兼容**
- 方案 A保留 `MVP_INTERNAL_TOKEN`(单租户)+ 新增 user token多租户
- 方案 Bv2.5 直接切换到 user token破坏兼容但更清晰
2) **调度公平性**
- v2.5 先全局 FIFO简单后续 v3 再引入 per-user 公平调度/配额。
3) **head.json 的生产写入者**
- 方案 A与 API 同进程线程(最少组件)
- 方案 B独立进程更独立、易运维

View File

@ -1,132 +0,0 @@
# MVP v2.5 端到端测试用例(正常/异常/边界)
本用例集目标:覆盖 v2.5 的关键能力与边界条件User + jobs 隔离 + stateless node pool + API 队列调度)。
约束v2.5 已确认):
- TaskSpec 不扩展;不支持 reward_fn不支持用户自定义 verl 代码。
- 输入统一 `/private/common/...`;用户隔离先只隔离 `/private/users/<uid>/jobs/...` 输出。
---
## 0. 环境前置
远端目录示例:
- `argus@h1:/home2/argus/infra/mvp/src/mvp/`
共享目录(宿主机):
- `/home2/argus/infra/mvp/shared/`
容器内路径约定:
- `/private` 为共享存储挂载点
需要:
- GPU 0-7 可用
- 3 容器head无 GPU+ 2 worker各 4 GPU
---
## 1. 正常用例Happy Path
### HP-1v2.5 全链路PPO → GRPO → SFT串行
步骤:
1) `cd /home2/argus/infra/mvp/src/mvp/scripts`
2) `MVP_INTERNAL_TOKEN=<admin_token> RESET_DB=1 ./run_all_v25_api.sh`
期望:
- Ray dashboard 显示 3 nodeshead+2 workersGPU 总数 8。
- 3 个 task 最终为 `SUCCEEDED`
- 输出目录存在且按用户隔离:
- `/private/users/<uid>/jobs/<ray_submission_id>/{config,logs,checkpoints,debug}`
### HP-2Driver 不在 head 跑
验证点(任选一种):
- Ray job 的 driver node IP 不等于 head 容器 IP
- 或日志/调度信息显示 entrypoint_resources 生效driver 在 worker
---
## 2. 异常用例Error Cases
### E-Auth-1缺 token
请求:
- `GET /api/v2/queue` 不带 `Authorization`
期望:
- 返回 401missing bearer token
### E-Auth-2无效 token
请求:
- `Authorization: Bearer <random>`
期望:
- 返回 401invalid token
### E-Auth-3用户禁用后拒绝访问
步骤:
1) admin 创建用户 `bob` 并签发 token
2) admin 禁用 `bob`
3) 用 bob token 请求 `/api/v2/queue`
期望:
- 返回 403user disabled
### E-Isolation-1跨用户访问 task 资源(不泄露存在性)
步骤:
1) alice 提交 task 得到 `task_id`
2) bob 查询 `/api/v2/tasks/{task_id}`
期望:
- 返回 404task not found
### E-Input-1输入路径不在 /private/commonv2.5 约束)
请求:
- 提交 taskspec 但 `train_file``code_path` 不以 `/private/common/` 开头
期望:
- 返回 400并给出具体字段错误例如 `train_file must start with /private/common/`)。
---
## 3. 边界用例Boundary
### B-Queue-1资源不足时不提交 RayPENDING_RESOURCES
步骤:
1) 构造任务需求 `nnodes=3``n_gpus_per_node=4`total 12 GPU
2) 提交后轮询状态
期望:
- task 进入 `PENDING_RESOURCES`(服务侧 pending不向 Ray submit
- 具备 `next_run_at`
### B-Cancel-1任务取消QUEUED/RUNNING
步骤:
1) 提交一个较长 steps 的任务(确保有机会 RUNNING
2) 调用 `POST /api/v2/tasks/{task_id}:cancel`
期望:
- task state 为 `CANCELED`
- attempt 中 `ray_status` 最终为 `STOPPED`(或 Ray 侧停止)
---
## 4. 可执行回归脚本
见:
- `src/mvp/scripts/run_e2e_v25_cases.sh`
脚本覆盖:
- HP-1
- E-Auth-1/E-Auth-2/E-Input-1
- E-Isolation-1
- B-Queue-1
- B-Cancel-1best-effort

View File

@ -1,92 +0,0 @@
# MVP v2.5 迭代总结(已落地)
本文档总结 v2.5 在 v2.0/v2.1/v2.2…基础上完成的能力、实现点、验收方式与已知限制,便于回顾与后续版本迭代对齐。
## 目标与边界
v2.5 的核心目标:
- 引入 **User Management用户管理**:基于 token 的鉴权与任务级隔离(“只隔离 jobs”
- 引入 **Stateless Ray Node Pool无状态 Ray worker 池)**worker 不依赖平台下发 head 地址,自动发现并连接/自愈。
- 保持 **TaskSpecv1.1 同款 YAML 格式)不扩展**:本迭代不支持 reward function、自定义 verl 代码等。
明确不做v2.5 约束):
- 不支持 TaskSpec 扩展(例如 `reward_fn_path` 等)。
- 不支持用户自定义 verl/hf/dataset 的隔离或自定义路径:**统一使用 `/private/common/...`** 的公共资源。
- 用户隔离仅覆盖 **任务与产物目录**jobs不覆盖 HF cache、datasets 等公共缓存。
## 关键能力(对外表现)
### 1) 多用户鉴权与任务隔离
- API 仍使用内部 `Authorization: Bearer <token>` 方式:
- 管理员 token 来自环境变量 `MVP_INTERNAL_TOKEN`admin
- 业务用户 token 由管理员通过 API 下发并持久化到 SQLite。
- 用户隔离策略:
- 非管理员用户只能查询/取消/拉取日志 **自己的 task**;跨用户访问返回 404不泄露存在性
- 训练产物落盘隔离Ray job 目录统一写入 `/private/users/<user_id>/jobs/<ray_submission_id>/...`
### 2) task_id / submission_id 带用户名
- 新任务 ID 规则:`mvp2-<user>-<workload>-<YYYYMMDD-HHMMSS>-<suffix>`
- Ray submission idattempt规则`<task_id>--aNN`,因此自然包含用户名。
- 作用Dashboard/日志/落盘目录可读性更强,便于按用户追踪和审计。
### 3) “无状态 worker 池”与 head 地址发现
- Head 在共享存储写入 **head 地址文件**(例如 `head.json`worker 通过 watchdog
- 轮询发现 head 地址
- 自动 `ray start --address ...` 加入集群
- 掉线后自动重连watchdog 自愈)
- 达成效果:在生产环境中,即使 worker 容器由算力平台创建(只提供 SSH 纳管),也能通过共享存储实现连接与自愈。
### 4) 任务调度:队列 + Ray Job 提交 + 状态回传
- API 提交任务后进入 SQLite 队列,由后台 scheduler 逐个提交到 Ray默认 `max_running_tasks=1`)。
- Scheduler 持续轮询 Ray job 状态并回写任务状态RUNNING/SUCCEEDED/FAILED/CANCELED
- 资源不足的“可重试失败”处理:
- 针对 VERL 的 fail-fast`Total available GPUs ... is less than total desired GPUs ...`)或集群资源不足,
任务进入 `PENDING_RESOURCES` 并设置 `next_run_at`,按 `retry_interval_s` 周期重试。
## 关键实现点(工程化落地)
### 存储与目录约定(容器内视角)
- 共享根路径统一为 `/private`(对齐生产挂载)。
- v2.5 强约束TaskSpec 的以下字段必须以 `/private/common/` 开头:
- `code_path` / `train_file` / `val_file`
- 公共目录(示例):
- `/private/common/hf`HF 缓存
- `/private/common/datasets`:训练数据(必要时通过 symlink 指向已有缓存目录复用下载)
- `/private/common/db/mvp.sqlite3`队列与用户信息SQLite
- `/private/common/logs`API / watchdog 日志
- `/private/users/<uid>/jobs/...`:用户作业产物(隔离)
### Ray 拓扑与“head 不跑训练”
- Head 启动为管理节点CPU/GPU=0避免训练任务落到 head。
- Worker 节点具备 GPU示例2 个 worker * 每个 4 GPU
- driver 通过 `entrypoint_resources`(例如 `worker_node: 1`)强制落 worker。
### 部署脚本与可重复执行
提供完整脚本链路,覆盖:
- 清理 legacy 环境、起停容器、启动 Ray head
- head discovery publisher、worker watchdog 启动与状态检查
- 数据/模型/代码准备(幂等、可复用已有下载)
- 启动 API server并支持 RESET_DB
- API 方式连续提交 PPO/GRPO/SFT 并等待完成
代表性脚本:
- `src/mvp/scripts/run_all_v25_api.sh`v2.5 happy-path 端到端(含重建集群、准备资源、起 API、提交 3 类任务)
- `src/mvp/scripts/run_e2e_v25_cases.sh`:在 happy-path 基础上增加鉴权/隔离/输入校验/资源不足/取消等用例
## 验收与测试(已通过)
### 单元测试(本机 venv
- `.venv/bin/python -m pytest`
- 覆盖率阈值:>= 90%
### 远端端到端h1
- 在 `argus@h1:/home2/argus/infra/mvp/src/mvp/scripts` 执行:
- `MVP_INTERNAL_TOKEN=mvp-dev-token RESET_DB=1 ./run_e2e_v25_cases.sh`
- 结果happy-pathPPO/GRPO/SFT完成且异常/边界用例验证通过(鉴权、跨用户隔离、输入校验、资源不足转 PENDING_RESOURCES、取消任务等
## 已知问题与后续建议
- `max_running_tasks=1` 会让队列中的任务在前序 RUNNING 时保持 QUEUED这在“资源不足”边界测试里需要显式清空/取消前序任务,或接受该行为作为设计的一部分。
- 当前仍是 SQLite 单点;后续若要 HA/水平扩展,可在 v2.6+ 引入更强的持久化与多副本(例如 Postgres/etcd
- API server / watchdog 目前以脚本方式守护;后续可进一步统一为 systemd/supervisor或平台侧守护并补齐健康检查与告警。

View File

@ -1,15 +0,0 @@
# MVP v3.0Design— WebUI + 用户数据上传/下载SFTPGo→ 首个可发布版本
本目录基于:
- `specs/mvp/mvp_roadmap_v2.md`(总体路线图)
- `specs/mvp/image/roadmap_v3.0.png`v3.0 迭代图)
- 当前已落地的 v2.5User Mgmt + Stateless Ray Node Pool
目标是在 v2.5 的基础上补齐 **用户数据闭环**(上传→训练可见→产物下载)以及最小可用的 **WebUI**,形成“可发布”的 v3.0 版本。
文档:
- `specs/mvp/v3.0/v3.0_design.md`总体架构与关键机制WebUI、SFTPGo、数据/权限模型、任务流)。
- `specs/mvp/v3.0/v3.0_api.md`v3.0 API 扩展设计UI、数据、SFTPGo 管理、权限约束)。
- `specs/mvp/v3.0/v3.0_acceptance.md`:部署/升级/验收流程与可验证标准(含故障注入与回归清单)。
- `specs/mvp/v3.0/v3.0_dev_plan.md`TDD 驱动的工程化开发计划里程碑拆分、测试分层、E2E 验收)。
- `specs/mvp/v3.0/v3.0_progress.md`:实施进展记录(每个里程碑完成后追加记录)。

View File

@ -1,55 +0,0 @@
# MVP v3.0 — 部署与验收流程(草案)
## 0) 环境前提
- Ray 集群:延续 v2.5 的 head + stateless worker自动 join
- 共享存储:容器内挂载 `/private`dev/prod 对齐)
- API server宿主机代码挂载到 head 容器,在 head 容器内启动
- 新增SFTPGo 服务(建议容器化部署)
## 1) 部署步骤(高层)
1) 部署/升级 Ray 节点镜像(沿用 v2.5 的 `argus/argus-ray-node:v2.5` 或更高版本)
2) 启动 Ray 集群compose 或平台创建容器)
3) 启动/配置 SFTPGo挂载 `/private`
4) 启动 API serverhead 容器内)
5) 启动 WebUI由 API server 托管)
## 2) 验收用例(必须通过)
### A. 用户与凭据
1) admin 创建用户 `alice`,签发 API token
2) 系统联动在 SFTPGo 创建 `alice`home=/private/users/alice
3) `alice` 使用 token 登录 WebUI或调用 `/api/v2/me` 成功)
### B. 上传数据闭环(核心)
1) `alice` 通过 SFTP 上传数据集到 `/private/users/alice/datasets/...`
2) `alice` 通过 WebUI/API 提交任务TaskSpec 引用该路径
3) Ray worker 读取该数据,任务 RUNNING 并最终 SUCCEEDED
### C. 下载产物闭环
1) 训练完成后,产物落到 `/private/users/alice/jobs/<submission_id>/...`
2) `alice` 通过 SFTP 下载 checkpoints/logs 成功
3) (新增)`alice` 将需要长期保留的权重从 `jobs/<submission_id>/...` 移动到 `models/`,确认移动后可长期存在
### C2. Jobs 回收站与自动清理3 天移入回收站7 天后永久删除)
1) 将 `jobs_trash_after_days`/`jobs_purge_after_days` 配置为较小值(例如分钟级,用于验证)
2) 训练完成进入 terminal 状态
3) 等待 API server 内置 janitor 扫描周期后,确认对应 `jobs/<submission_id>` 被移动到 `trash/jobs/<submission_id>`
4) 在回收站窗口内,把某个文件从 `trash/jobs/<submission_id>` 移动到 `models/`,确认移动成功
5) 等待超过 `jobs_purge_after_days` 后,确认 `trash/jobs/<submission_id>` 被永久删除
6) 确认已移动到 `models/` 的文件不被删除
### D. 安全隔离(必须)
1) `bob` 不能通过 API 查询 `alice` 的 task404
2) `bob` 不能提交引用 `/private/users/alice/...` 的 TaskSpec400/403
3) `bob` 通过 SFTP 无法访问 `/private/users/alice/...`chroot 生效)
## 3) 故障注入(推荐通过)
1) kill worker watchdog 或 raylet → worker 自动恢复并重新加入集群
2) 重启 head 容器 → head 重新写 `head.json`worker 自动重连
3) SFTPGo 重启 → 不影响 Ray 集群;用户可重新连接上传/下载
## 4) 回归清单(与 v2.5 一致)
- 任务队列、重试INSUFFICIENT_RESOURCES → PENDING_RESOURCES → retry
- PPO/GRPO/SFT 三种 workload 均可跑通
- head 不跑训练driver 强制落 worker

View File

@ -1,109 +0,0 @@
# MVP v3.0 — API 扩展设计(基于 v2.5
v3.0 的原则是:**尽量复用 v2.5 API**,只增量增加 “数据闭环” 与 “WebUI 支持” 所需的最小接口。
## 1) 认证与权限
沿用 v2.5
- Header`Authorization: Bearer <token>`
- admin token来自 `MVP_INTERNAL_TOKEN`
- 普通用户 token由 admin 颁发并持久化在 SQLite
权限规则:
- 非 admin只能访问自己的 task、自己的数据空间`/private/users/<user_id>/...`)。
- 跨用户访问返回 404不泄露存在性
## 2) 用户与 SFTPGo 联动(管理员接口)
### 2.1 创建用户(复用 v2.5
`POST /api/v2/users`
- v3.0 行为:成功后,**可选**联动创建 SFTPGo 用户
- v3.0 默认启用联动:创建 SFTPGo 用户 + 生成一次性密码password 认证)
- v3.0 仅保留该方案(方案 A不做外部认证/SSO 集成(留到更后续版本)
- `data.sftpgo.admin_api_base` 推荐形如:`http://argus-sftpgo:8080/api/v2`(包含 `/api/v2` 前缀)
### 2.2 下发 token复用 v2.5
`POST /api/v2/users/{user_id}/tokens`
### 2.3 禁用用户(复用 v2.5
`POST /api/v2/users/{user_id}:disable`
- v3.0 行为:联动禁用 SFTPGo 用户(可选)
### 2.4 SFTP 凭据管理(新增,管理员或用户自助)
(具体由你确认 v3.0 需要“用户自助”还是“管理员操作”)
#### 重置 SFTP 密码(管理员)
`POST /api/v2/users/{user_id}/sftp:reset_password`
- 返回:一次性密码(只返回一次,服务端不保存明文)
> v3.0 先只做 password 方案SSH public key 作为后续版本可选增强(不在 v3.0 范围)。
## 3) 用户自助信息(新增)
### 3.1 获取当前用户信息
`GET /api/v2/me`
- 返回示例:
```json
{
"user_id": "alice",
"is_admin": false,
"paths": {
"home": "/private/users/alice",
"datasets": "/private/users/alice/datasets",
"models": "/private/users/alice/models",
"code": "/private/users/alice/code",
"jobs": "/private/users/alice/jobs",
"trash_jobs": "/private/users/alice/trash/jobs"
},
"retention": {
"jobs_trash_after_days": 3,
"jobs_purge_after_days": 7
},
"sftp": {
"host": "h1.example.internal",
"port": 2022,
"username": "alice"
}
}
```
## 3.2 Jobs Retention 提示(新增)
为了支撑 WebUI 展示与用户预期管理,可在 `/api/v2/me` 或单独接口返回:
- `jobs_trash_after_days`:默认 3
- `jobs_purge_after_days`:默认 7
- `jobs_root``/private/users/<me>/jobs`
- `trash_jobs_root``/private/users/<me>/trash/jobs`
- `recommendations`:提示用户把需要长期保存的产物移动到 `models/``datasets/`
## 4) 数据浏览/下载可选v3.0 最小化)
说明:上传/下载主通道仍是 SFTP。
WebUI 如果要提供“快速浏览/查看”,可实现只读接口(避免实现大文件上传/断点等复杂逻辑)。
### 4.1 列目录
`GET /api/v2/files?path=/private/users/alice`
- 权限path 必须在 `/private/common/``/private/users/<me>/`
- 返回文件列表name/type/size/mtime
### 4.2 下载文件(小文件为主)
`GET /api/v2/files:download?path=/private/users/alice/jobs/.../logs/...`
- 返回:流式下载
- 大文件仍建议走 SFTP
## 5) TaskSpec 路径校验升级v3.0 关键)
v2.5:仅允许 `/private/common/...`
v3.0:允许 `/private/common/...``/private/users/<me>/...`
应用字段(至少):
- `train_file` / `val_file`
- `code_path`:仍仅允许 `/private/common/...`v3.0 不支持执行用户 code
- 本地模型路径字段(如果引入):允许 `/private/users/<me>/models/...`
## 6) WebUI 路由(新增)
由 API server 托管:
- `GET /ui`:主页面
- `GET /ui/login`token 登录页
- 静态资源:`/ui/static/...`
WebUI 的所有操作均调用同源 API不额外开 CORS

View File

@ -1,358 +0,0 @@
# MVP v3.0 详细设计方案(基于 v2.5
## 0. 结论摘要v3.0 要交付什么)
v3.0 = v2.5 + **WebUI** + **用户数据上传/下载SFTPGo**,形成第一个可对外发布的版本:
- 用户可以通过 **SFTP** 上传数据/模型/代码(至少数据),落到 GPFS容器内 `/private`)并对 Ray worker 可见。
- 用户可以通过 API/WebUI 提交训练任务,任务读取自己上传的数据。
- 用户可以下载训练产物checkpoints/logs 等),最小闭环跑通。
## 1. 范围与原则
### 1.1 继承 v2.5 的前提(不回退)
- **Stateless Ray Node Pool**head 写 `head.json`worker watchdog 自动 join/自愈。
- **User Management**token 鉴权、任务可见性隔离(跨用户 404 不泄漏)。
- **作业产物隔离**Ray job 目录落到 `/private/users/<user_id>/jobs/<ray_submission_id>/...`
- **API server 短期运行方式**:代码在宿主机,挂载到 head 容器,在 head 容器内启动(保持现状)。
### 1.2 v3.0 新增目标
1) **Data ManagementSFTPGo**
- 提供用户上传/下载入口SFTP 为主)。
- 数据落到 GPFSdev 环境 NFS/GPFS生产环境 GPFS训练 job 在 worker 容器内可直接读取。
2) **WebUI**
- 用户可视化创建任务、查看队列/状态/日志、查看“数据路径约定”和自己的 SFTP 信息。
- 目标是 “可用而非豪华”,支持核心工作流。
3) **权限闭环**
- 用户只能使用自己目录下的数据(`/private/users/<user_id>/...`)或公共目录(`/private/common/...`)。
- 防止用户提交任务读取其他用户的文件路径。
### 1.3 v3.0 明确不做(留给 v3.5
- 不做 “自定义 reward function / 自定义 verl 代码 / 多版本 verl 共存”(路线图 v3.5)。
- 不做复杂 Serving/训推一体(路线图 v3.5)。
- 不做 IB 网络/拓扑优化(路线图 v3.5)。
- 不做系统级可观测性平台(路线图 v4.0)。
## 2. 架构概览
参考 `roadmap_v3.0.png`v3.0 的控制面与数据面:
### 2.1 控制面Control Plane
- **API ServerFastAPI**
- v2.5 的任务队列/调度/重试 + 用户管理能力继续复用
- 新增:数据管理能力(与 SFTPGo 对接) + WebUI
- **WebUI**
- 通过 API 使用 token 登录
- 提供任务/日志/数据入口(不直接运行训练)
- **Ray Head状态节点**
- 仍在 head 容器内(或单独节点)
- job server/dashbaord 提供 job submit/status/logs 能力
### 2.2 数据面Data Plane
- **GPFS容器内挂载 `/private`**
- 存放 common 与 users 两大根目录
- **Ray Worker Node无状态**
- 自动连接 head执行训练
- 读取 `/private/users/<user>/...` 的数据
### 2.3 新增组件SFTPGoData Management
- 作为独立服务运行(容器化优先),后端存储使用 **filesystem**GPFS 挂载路径)。
- 用户的 home directory 指向 `/private/users/<user_id>`(或其子目录)。
## 3. 存储与目录规范v3.0 统一约定)
### 3.1 目录层级
统一以容器内 `/private` 作为根路径dev/prod 对齐):
- `/private/common/`:公共资源
- `hf/`HF cache
- `datasets/`:公共数据集(可选)
- `code/`:公共代码(例如公共 verl repo snapshot
- `db/`SQLite队列、用户、token
- `logs/`API/supervisor/watchdog 日志
- `/private/users/<user_id>/`用户空间v3.0 重点)
- `datasets/`:用户上传的数据集(推荐)
- `models/`:用户保存/上传的本地模型(允许;也用于“把 job 产物移动到长期保存目录”)
- `code/`用户上传的代码v3.0 **不支持执行**;仅存放/下载)
- `jobs/`:训练任务产物(已在 v2.5 落地)
- `tmp/`:临时文件(可选)
### 3.2 Jobs Retention两段式3 天移入回收站7 天后永久删除)
v3.0 引入 **jobs 目录两段式保留策略**
- 第 1 阶段soft-deletejob 结束后 **3 天**,将该 job 目录从 `jobs/` **移动到用户回收目录**
- 第 2 阶段hard-delete进入回收目录后再过 **7 天**,从回收目录 **永久删除**
目录约定(建议):
- jobs 根目录:`/private/users/<user_id>/jobs/<ray_submission_id>/...`
- 回收目录:`/private/users/<user_id>/trash/jobs/<ray_submission_id>/...`
计时规则:
- 以 job 进入 terminal 状态SUCCEEDED/FAILED/CANCELED的结束时间为起点
- “3 天”用于从 `jobs/` 移入 `trash/jobs/`
- “7 天”用于从 `trash/jobs/` 永久删除(即总共最多 10 天窗口)。
用户保留关键产物的方式(无需 keep 标记):
- 在 “3 天窗口”内把需要长期保存的文件从 `jobs/<submission_id>/...` **移动/复制**到 `models/`(例如权重)或 `datasets/`(例如评估输出数据);
- 即便已被移动到回收目录,用户仍可在 “7 天窗口”内从 `trash/jobs/<submission_id>/...` 把需要的文件移到 `models/` / `datasets/`
- janitor 只管理 `jobs/``trash/jobs/`,不会触碰 `models/``datasets/`
这里的“清理程序”我们称为 **janitor**
- 定义:一个后台清理执行器,按固定周期扫描“已结束且已过期”的 job 目录并删除
- v3.0 目标实现“3 天移入回收站 + 7 天后删除”这一条产品规则(不提供 keep/延长保留标记)
实现建议(按你的偏好):
- **janitor 作为 API server 内置后台线程**运行:
- 优点:天然可访问 SQLite任务状态、结束时间、user_id、ray_submission_id并能把清理结果写回 events 表用于审计
- 部署更简单:不额外引入 cronjob/独立服务
- 删除/移动动作建议 **直接在 GPFS/NFS 文件系统上操作**API server 运行在 head 容器,已挂载 `/private`
- 第 1 阶段:`os.rename`(同文件系统原子移动)把 `jobs/<sid>` 移到 `trash/jobs/<sid>`
- 若跨文件系统(理论上不应发生),则降级为 copy+delete
- 移动前做严格路径前缀校验(必须在 `.../users/<u>/jobs/` 下)。
- 第 2 阶段:对 `trash/jobs/<sid>` 执行递归删除(例如 `shutil.rmtree`),同样做路径前缀校验(必须在 `.../users/<u>/trash/jobs/` 下)。
- 为什么不依赖 SFTPGo APISFTPGo 只是用户访问协议层SFTP/Web目录物理就在同一份文件系统文件系统直连更简单、也不依赖 SFTPGo 在线。
- 如果你强烈希望“通过 SFTPGo API 删除”:
- 可以作为可选实现/补充(例如用于统一审计或未来接入配额/策略但不建议作为唯一手段SFTPGo 停机不应阻塞清理)。
### 3.3 用户在 SFTPGo 内移动/整理文件(确认点)
支持用户在 SFTPGo 中进行“移动/重命名/整理”(例如把权重从 `jobs/` 移动到 `models/`
- 前提SFTPGo 用户权限允许对其 home 目录进行 `rename/mkdir/remove` 等操作v3.0 默认可写)。
- 行为:用户可以把 `jobs/` 下某些文件移动到 `models/``datasets/`,用于长期保存权重/评估产物等。
- 与 retention 的关系:只要文件被移动出 `jobs/`,就不会被 jobs 清理逻辑删除。
### 3.4 路径权限规则API 侧校验)
v2.5 约束是 “只允许 `/private/common/...`”。
v3.0 需要升级为:
- 允许:
- `/private/common/...`
- `/private/users/<current_user_id>/...`
- 禁止:
- 任何其他绝对路径(例如 `/private/users/other/...``/etc/...`
并把该规则应用到 TaskSpec 的相关字段(至少):
- `train_file` / `val_file`
- `code_path`:仍仅允许 `/private/common/...`v3.0 不支持执行用户 code
- 本地模型路径字段:允许 `/private/users/<me>/models/...`确认v3.0 允许)
## 4. SFTPGo 方案设计Data Management
### 4.1 运行形态
推荐用容器运行 SFTPGo与 Ray/API 解耦),挂载同一份 `/private`
- `sftpgo` 容器挂载 `../../shared:/private`
- 对外暴露:
- SFTP 端口(建议 2022
- WebAdmin/API 端口(建议 8081仅内网或管理员访问
#### 4.1.1 镜像来源(现成 Docker 镜像)
SFTPGo 有现成可用的 Docker 镜像(无需自建):
- v3.0 推荐优先使用官方/上游发布的 `sftpgo` 镜像作为运行基座
- 我们在 v3.0 里不需要定制 SFTPGo 代码,只需要:
- 正确挂载 GPFS/NFS容器内 `/private`
- 配置管理员账号(用于 API server 联动创建/禁用用户、重置密码)
- 配置每用户 home/chroot
> 注意:具体镜像名/tag 在不同环境可能有差异(官方/镜像仓库策略会变动)。落地时建议在 `argus@h1``docker search sftpgo` 或由你们内部镜像仓库提供固定版本v3.0 设计只要求“使用现成镜像”,不强依赖某个 tag。
#### 4.1.2 docker-compose 服务草案(示意)
下面给出一个**示意**(最终以实际镜像名/tag 与你们端口规划为准):
```yaml
services:
sftpgo:
image: sftpgo/sftpgo:latest # 示例:使用现成镜像
container_name: argus-sftpgo
ports:
- "2022:2022" # SFTP
- "8081:8080" # WebAdmin/API建议仅内网/管理员)
volumes:
- ../../shared:/private
- ../../shared/common/sftpgo:/var/lib/sftpgo # 持久化 SFTPGo 元数据(可选/建议)
environment:
# 管理员账号/密码(示意,具体变量名以镜像文档为准)
SFTPGO_ADMIN_USERNAME: "admin"
SFTPGO_ADMIN_PASSWORD: "${SFTPGO_ADMIN_PASSWORD}"
```
与 v3.0 的配合点:
- API server 使用 `data.sftpgo.admin_api_base` + admin 凭据联动创建用户
- 用户 home/chroot 统一指向 `/private/users/<user_id>`
### 4.2 用户隔离
每个用户在 SFTPGo 中的 home dir 绑定到:
- `/private/users/<user_id>`chroot用户只能读写自己的目录。
### 4.3 用户创建与凭据管理(两种实现,建议先做 A
**方案 Av3.0 推荐API Server 负责“联动创建 SFTPGo 用户”**
- 在 v2.5 的 `POST /api/v2/users` 成功后:
- API server 调用 SFTPGo 管理 API 创建同名用户
- 设置 home dir = `/private/users/<user_id>`
- 设置权限(默认可写;是否只读可配置)
- 认证方式:
- v3.0 最小可用:用户名+密码确认v3.0 先 passwordAPI 生成一次性密码,用户首次登录后要求改密)
- 或SSH public keyWebUI 允许上传 public keyAPI 写入 SFTPGo
**方案 B更强但复杂SFTPGo 外部认证**
- SFTPGo 把认证委托给 API servertoken/SSOSFTP 也走内部 token。
- 复杂度高,建议 v3.0 不做,放到 v3.5 或更后。
### 4.4 用户上传/下载体验
用户通过 SFTP 上传:
- `datasets/...`(训练数据)
- `models/...`(本地模型,可选)
下载:
- `jobs/<ray_submission_id>/...`checkpoints/logs
WebUI/文档提供 “路径如何写进 TaskSpec” 的指引。
## 5. WebUI 方案设计(最小可用)
### 5.1 目标页面
v3.0 WebUI 采用“**多子页面 + 侧边导航栏**”而不是把所有功能挤到单页:
- 原因信息密度更可控后续可扩展v3.5+)且不会把一个页面做成“巨型表单/巨型列表”。
- 实现仍保持轻量:服务端渲染(或静态 HTML + 少量 JS不引入复杂前端工程。
信息架构IA建议如下
1) **登录页**`/ui/login`
- 用户粘贴 token管理员发放浏览器保存localStorage/sessionStorage
- 提供“退出登录/清空 token”
2) **任务列表页**`/ui/tasks`
- 默认列表:最近 N 条任务(按 created_at 倒序)
- 支持过滤workload、stateQUEUED/RUNNING/SUCCEEDED/FAILED/CANCELED、时间范围
- 支持快捷操作:进入详情、取消任务
3) **新建任务页**`/ui/tasks/new`
- 两种模式(二选一,均可实现):
- **YAML 直接提交**:上传/粘贴 TaskSpec YAML最省开发
- **表单生成 YAML**:选择 workload填写核心字段train/val/model/nnodes/gpus生成 YAML 预览后提交
- 提交后跳转到任务详情页
4) **任务详情页**`/ui/tasks/{task_id}`
- 顶部task_id、workload、state、created_at、updated_at、error_summary
- Attempt 卡片latest attempt_no、ray_submission_id、ray_status、start/end
- 操作区:取消任务(若非 terminal、刷新状态、复制路径/ID
- 链接到日志页与产物提示SFTP 路径)
5) **任务日志页**`/ui/tasks/{task_id}/logs`
- 默认 tail=2000可选 200/1000/5000
- 提供“自动刷新(每 3~5 秒)”开关(简单轮询即可)
6) **数据页**`/ui/data`
- 显示 SFTP 连接信息host/port/username
- 显示用户目录约定:
- home`/private/users/<user_id>`
- datasets`/private/users/<user_id>/datasets`
- models`/private/users/<user_id>/models`
- jobs`/private/users/<user_id>/jobs`
- trash/jobs`/private/users/<user_id>/trash/jobs`
- 明确 retentionjobs 结束后 3 天移入回收站,回收站 7 天后删除;重要文件请移到 `models/``datasets/`
7) **(仅管理员可见)用户管理页**`/ui/admin/users`,可选但很有价值)
- 创建用户、禁用用户、签发 token、重置 SFTP 密码(方案 A
### 5.2 页面组织与导航(建议)
侧边栏导航(普通用户):
- Tasks列表
- New Task新建
- DataSFTP/目录说明)
管理员侧边栏额外增加:
- Admin / Users
### 5.3 大致示意图wireframe
下面是一个粗略示意(非最终 UI仅表达信息结构与布局
```
┌──────────────────────────────────────────────────────────────────────┐
│ Argus MVP v3.0 [user: alice] │
├───────────────┬──────────────────────────────────────────────────────┤
│ Side Nav │ /ui/tasks │
│ │ │
│ • Tasks │ [Filter] workload=all state=all [Search task_id] │
│ • New Task │ │
│ • Data │ Task List │
│ • Admin(*) │ ┌────────────────────────────────────────────────┐ │
│ │ │ task_id workload state ... │ │
│ │ │ mvp2-alice-ppo-... ppo RUNNING ... │ │
│ │ │ mvp2-alice-sft-... sft SUCCEEDED... │ │
│ │ └────────────────────────────────────────────────┘ │
│ │ [View] [Cancel] │
└───────────────┴──────────────────────────────────────────────────────┘
```
任务详情页(示意):
```
┌──────────────────────────────────────────────────────────────────────┐
│ /ui/tasks/{task_id} │
├──────────────────────────────────────────────────────────────────────┤
│ task_id: mvp2-alice-ppo-... state: RUNNING workload: ppo │
│ created_at: ... updated_at: ... │
│ error_summary: (empty) │
│ │
│ latest_attempt: a01 ray_submission_id: ...--a01 ray_status: RUNNING │
│ [Open Logs] [Cancel Task] [Refresh] │
│ │
│ Artifacts (SFTP paths): │
│ jobs/: /private/users/alice/jobs/<ray_submission_id>/ │
│ trash/: /private/users/alice/trash/jobs/<ray_submission_id>/ │
│ tip: move important files to /private/users/alice/models/ │
└──────────────────────────────────────────────────────────────────────┘
```
### 5.2 技术取舍(建议:不引入 Node 构建)
为了降低部署复杂度,建议 v3.0 WebUI 以 “服务端渲染 + 少量 JS/HTMX” 或 “纯静态 HTML+fetch” 实现:
- 由 API server 提供静态资源FastAPI StaticFiles
- 页面调用同源 API避免跨域与复杂前端构建链
## 6. API 扩展设计(概览)
v3.0 可以保持 `/api/v2/...` 不变,增量加:
- SFTPGo 集成管理端点(管理员):
- 创建/禁用用户时联动 SFTPGo
- 重置 SFTP 密码 / 更新 SSH key
- 用户数据端点(可选,最小化):
- `/api/v2/me`:返回 user_id、SFTP 信息host/port/home
- `/api/v2/files`:仅用于浏览/下载(上传仍走 SFTP
详细见 `specs/mvp/v3.0/v3.0_api.md`
## 7. 配置与部署v3.0 新增项)
`configs/dev.yaml` 基础上扩展一组 `data` 配置(示意):
```yaml
data:
shared_root: "/private" # 通常与 ray.shared_root 一致
user_root: "/private/users" # 用户空间根目录
allow_common_prefix: "/private/common/"
allow_user_prefix_template: "/private/users/{user_id}/"
sftpgo:
enabled: true
host: "127.0.0.1"
sftp_port: 2022
admin_api_base: "http://127.0.0.1:8081/api/v2"
admin_user: "admin"
admin_password_env: "SFTPGO_ADMIN_PASSWORD" # 仅 head 容器内可读
retention:
jobs_trash_after_days: 3
jobs_purge_after_days: 7
trash_root_template: "/private/users/{user_id}/trash/jobs"
janitor_interval_s: 3600 # 每小时扫一次(可配置)
```
## 8. 风险点与对策
1) **路径逃逸/越权读取**
- 必须在 API 提交任务时校验路径前缀
- SFTPGo 必须 chroot 到用户 home
2) **大文件上传稳定性**
- 优先用 SFTP断点续传/可靠性更好)
3) **用户 token 与 SFTP 凭据的生命周期**
- token 走 v2.5 SQLite
- SFTP 凭据建议独立(密码/SSH key并提供 reset 流程
4) **GPFS/NFS 权限**
- 确保 `/private/users/<user>` 目录权限可被 SFTPGo 写入且 worker 可读
## 9. 已确认结论(来自你的反馈)
1) 允许用户上传并在训练时使用自定义数据集:允许(`/private/users/<u>/datasets/...`)。
2) 允许用户上传并在训练时使用本地模型路径:允许(`/private/users/<u>/models/...`)。
3) v3.0 不允许执行用户自定义代码(不注入 `PYTHONPATH` 作为可执行 code path
4) SFTPGo 认证方式v3.0 先 password。
5) WebUI按“简单最小必要功能”做token 粘贴登录优先)。
## 10. 待确认问题(需要你给结论)
已确认jobs 清理执行主体v3.0 采用 **API server 内置 janitor 后台线程**

View File

@ -1,232 +0,0 @@
# MVP v3.0 开发计划TDD 驱动)
本文是 v3.0 的**工程化开发计划**强调“先写测试再写实现”TDD并将每个里程碑拆成**可独立验收**的小闭环。
输入依据:
- 路线图:`specs/mvp/mvp_roadmap_v2.md`
- v3.0 设计:`specs/mvp/v3.0/v3.0_design.md`
- v3.0 API`specs/mvp/v3.0/v3.0_api.md`
- v3.0 验收:`specs/mvp/v3.0/v3.0_acceptance.md`
- 现状基线v2.5Task queue + User mgmt + Stateless ray pool + 单镜像节点守护)
v3.0 已确认约束:
- 允许用户数据集路径:`/private/users/<me>/datasets/...`
- 允许用户本地模型路径:`/private/users/<me>/models/...`
- **不允许执行用户自定义代码**(不注入 user code 到 PYTHONPATH`code_path` 仍只允许 `/private/common/...`
- SFTPGo 先用 **password** 方案(方案 AAPI 联动创建/管理 SFTPGo 用户)
- jobs retention**3 天移入回收站trash/jobs再 7 天永久删除**;不提供 keep/延长保留标记
- janitor**API server 内置后台线程**;删除/移动采用**文件系统直接操作**(不依赖 SFTPGo API
---
## 0. TDD 规范(所有功能都遵循)
### 0.1 测试分层
1) **单元测试fast**
- 纯 Python 逻辑路径策略、SFTPGo client、retention 计算、文件移动/删除策略(用临时目录)。
- 不依赖真实 Ray、不依赖 docker、不依赖网络。
2) **组件测试(中等)**
- FastAPI 路由(含 WebUI 路由):`fastapi.testclient.TestClient`
- mock/stub SFTPGo client 与 ray client
3) **端到端(慢)**
- 在 `argus@h1` 通过 docker compose + scripts
- Ray 集群自动起来head+2 worker
- SFTPGo 服务可用
- 上传数据 → 提交训练 → 下载产物 → jobs 回收站/清理
### 0.2 代码与测试约定
- 测试目录:`src/mvp/py/tests/`
- 新功能必须先补齐测试用例,并让其在未实现时失败(红)
- 最小实现让测试变绿(绿)
- 再做重构(重构)
- 覆盖率:继续沿用当前阈值(>= 90%
---
## 1. 里程碑拆分v3.0 = 5 个可验证闭环)
### M1TaskSpec 路径策略升级(允许 user datasets/modelscode_path 仍仅 common
**目标**
- API submit 时的路径校验从 v2.5 的 “仅 `/private/common/`” 升级为:
- `train_file` / `val_file`:允许 `/private/common/...``/private/users/<me>/...`
- 本地模型路径:允许 `/private/users/<me>/models/...`(不改变 YAML 结构,见实现建议)
- `code_path`:仍仅允许 `/private/common/...`
- 阻止越权路径(`/private/users/other/...`)与非 `/private/...` 路径。
**实现建议(不扩展 TaskSpec**
- `model_id` 字段保持不变:
- 若 `model_id``/private/` 开头 → 视作本地模型路径
- 否则视作 HuggingFace repo id`Qwen/...`
**TDD 用例(先写测试)**
- 单测:
- `test_paths_allow_common_and_own_user_prefix()`
- `test_paths_reject_other_user_prefix()`
- `test_model_id_local_path_allowed_only_under_users_models()`
- `test_code_path_still_common_only()`
- API 测试:
- `test_submit_accepts_user_datasets_paths()`
- `test_submit_rejects_cross_user_paths_404_or_400()`(按约定返回 400/403
**验收点**
- `v3.0_acceptance.md` 的 D 类安全隔离用例可由 API 测试覆盖。
---
### M2SFTPGo 集成(方案 A用户联动创建 + password
**目标**
- 引入 `data management (SFTPGo)`
- admin 创建用户时联动创建 SFTPGo 用户home=/private/users/<user_id>chroot
- password 模式生成一次性密码reset/create并返回给 admin明文只返回一次
- 提供用户自助信息:
- `GET /api/v2/me` 返回 SFTP 连接信息、目录约定、retention 提示。
**实现建议**
- 新增 `SFTPGoAdminClient`(同步调用):
- 通过 `urllib``httpx`(建议 `urllib`,减少依赖;禁止 hard-code requests 使用)
- 支持create user / disable user / reset password最小集合
- API server 启动时校验配置enabled 时必须具备 admin 密码 env
- 同步创建用户目录结构(文件系统):
- `/private/users/<u>/{datasets,models,code,jobs,trash/jobs}`(最小必需)
**TDD 用例(先写测试)**
- 单测:
- `test_sftpgo_client_builds_correct_requests()`不发真实网络mock urlopen
- `test_user_dirs_created_on_user_create()`tmp dir 断言目录存在)
- API 测试:
- `test_create_user_calls_sftpgo_client()`stub client断言调用参数
- `test_me_returns_sftp_info_and_paths()`(含 trash/jobs 与 TTL 字段)
**验收点**
- `v3.0_acceptance.md` 的 A 类(用户/凭据)与 B 类(上传闭环前置)覆盖。
---
### M3WebUI最小可用多页面 + 侧边栏)
**目标**
- WebUI 由 API server 托管(同源,无额外 CORS
- `/ui/login`token 粘贴登录localStorage
- `/ui/tasks`:任务列表 + 过滤(最小)
- `/ui/tasks/new`YAML 提交(优先)+(可选)表单生成 YAML
- `/ui/tasks/{task_id}`:详情页
- `/ui/tasks/{task_id}/logs`:日志 tail + 可选自动刷新
- `/ui/data`SFTP 信息 + 目录/retention 提示
- (可选)`/ui/admin/users`:管理员用户管理(若时间允许,强烈建议)
**实现建议**
- 先不引入 Node 构建:
- HTML 模板可用最简单的字符串拼接或 Jinja2若引入 jinja2则补齐依赖与测试
- 页面通过 fetch 调用 `/api/v2/...`,并复用 token header
**TDD 用例(先写测试)**
- 组件测试TestClient
- `test_ui_routes_render_200()`
- `test_ui_contains_sidebar_links()`(简单断言文本包含导航链接)
- `test_ui_tasks_detail_shows_ids()`(包含 task_id、state、ray_submission_id
**验收点**
- WebUI 能完成:登录→创建任务→查看任务→查看日志→看到 data 页提示。
---
### M4Jobs Retention janitor3 天移入 trash7 天后 purge
**目标**
- API server 内置 janitor 后台线程:
- 周期性扫描 DB 中 terminal tasks
- 到期后执行:
- move`/private/users/<u>/jobs/<sid>``/private/users/<u>/trash/jobs/<sid>`
- purge递归删除 `/private/users/<u>/trash/jobs/<sid>`
- 全程严格 path 校验,禁止越界删除
- 清理操作记录到 DB events审计
**实现建议(数据与状态)**
- 需要稳定的时间锚点与幂等:
- 使用 attempts.end_time 作为 job 结束时间latest attempt
- 在 tasks 表新增字段(或新表)记录:
- `trashed_at`(首次成功 move 时间)
- `purged_at`(成功删除时间)
- `trash_path`(可选)
- 幂等:重复运行不会报错(目录不存在视为已处理)
**TDD 用例(先写测试)**
- 单测(用 tmpdir 构造 jobs/trash 目录):
- `test_janitor_moves_job_to_trash_after_threshold()`
- `test_janitor_purges_trash_after_threshold()`
- `test_janitor_never_touches_models_or_datasets()`
- `test_janitor_path_escape_rejected()`(恶意 path 不可删)
- API/组件测试:
- `test_me_includes_retention_fields()`jobs_trash_after_days/jobs_purge_after_days
**验收点**
- `v3.0_acceptance.md` 的 C2 用例可按“把阈值调小到分钟级”完成验证。
---
### M5端到端h1— SFTP 上传→训练→产物下载→回收站/清理
**目标**
- 在 `argus@h1` 落一个一键脚本(或手册)跑通:
1) `docker compose up -d` 拉起 Rayhead+2 worker+ SFTPGo
2) admin 创建用户 alice联动创建 SFTPGo 用户 + password
3) alice 通过 SFTP 上传:
- 数据集到 `/private/users/alice/datasets/...`
- (可选)本地模型到 `/private/users/alice/models/...`
4) alice 通过 API/WebUI 提交任务引用上述路径
5) 任务成功后:
- 从 `jobs/<sid>` 下载 logs/checkpoints
- 把权重移动到 `models/`,验证不会被清理
6) 把 retention 配置调小,验证 jobs→trash→purge
**交付建议**
- 新增脚本(命名示例):
- `scripts/run_all_v30_api.sh`
- `scripts/run_e2e_v30_cases.sh`
- 新增 `docker-compose.yaml` 中的 `sftpgo` service`docker-compose.v30.yaml` 叠加文件)
**验收点**
- `v3.0_acceptance.md` 全部 MUST 用例通过。
---
## 2. 风险与测试关注点
1) **权限与路径逃逸**
- path policy 必须覆盖train/val/model_id(local)/output dirsjobs/trash
- 所有删除/移动必须做 prefix 校验
2) **并发与竞态**
- janitor 只处理 terminal tasks避免清理正在写入的目录
- move 使用同文件系统 `os.replace`(原子)
3) **SFTPGo 可用性**
- SFTPGo 不在线不应影响训练与 API 核心功能(除了用户创建联动)
- janitor 不依赖 SFTPGo文件系统直连
---
## 3. 交付清单(代码/配置/脚本/文档)
### 3.1 代码
- Path policyv3.0
- SFTPGoAdminClient + user create/disable/reset password 联动
- `/api/v2/me` 扩展SFTP/目录/retention
- WebUI 路由与静态资源
- janitortrash+purge后台线程 + DB 记录
### 3.2 配置
- `configs/dev.yaml` 增加 `data.sftpgo``data.retention` 段(详见设计文档)
### 3.3 scripts / compose
- compose 增加 `sftpgo`(或新增 overlay compose 文件)
- v3.0 e2e 脚本(上传/下载/清理验证)
### 3.4 文档
- 更新 `specs/mvp/v3.0/*``src/mvp/README.md`运行方式、路径约定、SFTP 操作、retention 解释)

View File

@ -1,154 +0,0 @@
# MVP v3.0 进展记录milestone log
本文档用于记录 v3.0 按 `specs/mvp/v3.0/v3.0_dev_plan.md` 实施过程中的里程碑完成情况。
约定:每完成一个里程碑,追加一条记录,包含**日期**、**完成内容**、**涉及文件**、**验证方式/结果**、**待办/风险**。
---
## M1Path policy + tests已完成
- 日期2025-12-30
- 范围:按 v3.0 路径策略升级 API submit 的路径校验(不扩展 TaskSpec YAML 结构)。
- 完成内容:
- `code_path`:仍只允许 `/private/common/...`v3.0 不执行 user code
- `train_file`/`val_file`:允许 `/private/common/datasets/...``/private/users/<me>/datasets/...`
- `model_id`:若以 `/private/` 开头则视为本地路径,仅允许:
- `/private/common/models/...`
- `/private/users/<me>/models/...`
否则仍按 HuggingFace repo id`Qwen/...`)处理。
- 拒绝跨用户路径(例如 `bob` 提交 `/private/users/alice/datasets/...`)。
- 拒绝本地模型路径不在 `models/`(例如指向 `jobs/`)。
- 涉及文件:
- `src/mvp/py/argus/service/app.py`
- `src/mvp/py/tests/test_users.py`
- 验证方式与结果:
- 本地单测:`.venv/bin/python -m pytest -q`
- 结果:全部通过(`54 passed`),覆盖率阈值保持 `>= 90%`
- 待办/风险:
- `model_id=/private/...` 的“本地模型路径语义”需要在用户文档/WebUI 中明确提示(避免误用)。
- 后续 M2/M3 需要把该路径策略同步到 UI 表单/提示文本(避免用户填错路径)。
---
## M2SFTPGo 集成(方案 A用户联动创建 + password已完成
- 日期2025-12-30
- 范围SFTPGoData Management最小集成 + 用户自助信息 `/api/v2/me` + 用户目录结构落盘。
- 完成内容:
- 新增 `data` 配置段:
- `data.user_root`:用户数据根目录(默认 `/private/users`
- `data.sftpgo`SFTPGo 可选联动enabled/host/sftp_port/admin_api_base/admin_user/admin_password_env
- `data.retention`jobs 过期策略配置3 天移入 trash7 天 purgejanitor 在 M4 实现)
- 新增 `SFTPGoAdminClient``urllib` 实现,不使用 `requests`
- `create_user` / `disable_user` / `reset_password`(最小集合)
- API server 增强:
- `POST /api/v2/users`:创建 DB user + 同步创建目录结构(`datasets/models/code/jobs/trash/jobs`
- 当 `data.sftpgo.enabled=true` 时,创建用户会联动调用 SFTPGo admin API并返回一次性密码明文仅返回一次服务端不保存
- `POST /api/v2/users/{user_id}:disable`禁用用户SFTPGo 禁用 best-effort
- `POST /api/v2/users/{user_id}/sftp:reset_password`管理员重置一次性密码SFTPGo enabled 才允许)
- `GET /api/v2/me`返回当前用户的目录约定、retention 提示以及可选SFTP 连接信息
- 同步更新 `src/mvp/configs/dev.yaml`:补齐 v3.0 相关 `data.*` 配置(默认关闭 sftpgo
- 涉及文件:
- `src/mvp/py/argus/service/config.py`
- `src/mvp/py/argus/service/sftpgo.py`
- `src/mvp/py/argus/service/app.py`
- `src/mvp/py/tests/test_sftpgo.py`
- `src/mvp/py/tests/test_users.py`
- `src/mvp/py/tests/test_app.py`
- `src/mvp/py/tests/test_service_config.py`
- `src/mvp/configs/dev.yaml`
- `specs/mvp/v3.0/v3.0_api.md`
- 验证方式与结果:
- 本地单测:`.venv/bin/python -m pytest -q`
- 结果:全部通过(`62 passed`),覆盖率 `90.11%`(阈值 `>= 90%`)。
- 待办/风险:
- M2 仅做了“API 侧联动 + 单测”,未在真实 SFTPGo 容器上端到端验证(按计划在 M5 完成)。
- 目录创建依赖文件系统权限:生产部署时需确保 API/head 容器对 `/private/users` 可写。
---
## M3WebUI最小可用多页面 + 侧边栏)(已完成)
- 日期2025-12-30
- 范围API server 托管最小 WebUI同源不引入 Node 构建),用于登录/提交/查看任务与日志、查看 data 信息。
- 完成内容:
- 新增 UI 路由HTML+少量 JS
- `/ui`(重定向到 tasks
- `/ui/login`token 粘贴并写入浏览器 localStoragekey=`mvp_token`
- `/ui/tasks`:任务队列列表(调用 `/api/v2/queue`
- `/ui/tasks/new`:提交 TaskSpec YAMLPOST `/api/v2/tasks`
- `/ui/tasks/{task_id}`任务详情GET `/api/v2/tasks/{task_id}`,支持 cancel
- `/ui/tasks/{task_id}/logs`日志查看GET `/api/v2/tasks/{task_id}/logs`,可选自动刷新)
- `/ui/data`:展示 `/api/v2/me` 返回的路径/SFTP/retention 信息
- 统一侧边栏导航Tasks / New Task / Data / Login。
- UI 不做服务端 session所有 API 调用均由浏览器带 `Authorization: Bearer <token>`localStorage 注入)。
- 涉及文件:
- `src/mvp/py/argus/service/ui.py`
- `src/mvp/py/argus/service/app.py`
- `src/mvp/py/tests/test_ui.py`
- 验证方式与结果:
- 本地单测:`.venv/bin/python -m pytest -q`
- 结果:全部通过(`65 passed`),覆盖率 `90.53%`(阈值 `>= 90%`)。
- 待办/风险:
- WebUI 当前为“骨架+API 驱动”,不做复杂交互与大文件下载;上传/下载仍以 SFTP 为主(按设计)。
- Starlette TestClient 的 `allow_redirects` 有弃用告警(不影响功能,可在后续清理)。
---
## M4Jobs Retention janitor3 天移入 trash7 天后 purge已完成
- 日期2025-12-30
- 范围API server 内置后台线程,对“已结束 attempt”的 job 目录执行保留策略(文件系统直连,不依赖 SFTPGo
- 完成内容:
- 新增 `JobsJanitor`
- 以 `attempts.end_time` 为基准计算 TTL从 job 结束开始算)
- `>= 3 天 && < 7 天`:把目录从 `.../jobs/<ray_submission_id>` 移动到 `.../trash/jobs/<ray_submission_id>`
- `>= 7 天`:确保目录进入 trash 后删除(`shutil.rmtree`
- 对缺失目录、异常移动/删除为 best-effort不影响服务主流程
- DB 增强:新增查询 `list_ended_attempts_before()`,用于 janitor 扫描候选 attempt。
- API server 启动时启动 janitor 线程(可通过 `data.retention.janitor_interval_s` 控制;<=0 视为关闭)。
- 涉及文件:
- `src/mvp/py/argus/service/janitor.py`
- `src/mvp/py/argus/service/db.py`
- `src/mvp/py/argus/service/app.py`
- `src/mvp/py/tests/test_janitor.py`
- 验证方式与结果:
- 本地单测:`.venv/bin/python -m pytest -q`
- 结果:全部通过(`75 passed`),覆盖率 `90.72%`(阈值 `>= 90%`)。
- 待办/风险:
- M4 只做“逻辑 + 单测”,实际 `/private/users/...` 的权限与在 `argus@h1` 的行为验证放到 M5端到端
---
## M5端到端h1— SFTPGo compose + v3.0 E2E 脚本(已完成:交付脚本/配置)
- 日期2025-12-30
- 范围:补齐 h1 端到端所需的 compose/service、配置与一键脚本实际运行/验收由你在 `argus@h1` 执行)。
- 完成内容:
- SFTPGo 集成到 `docker compose`
- 新增 `argus-sftpgo` serviceSFTP 2022Admin API/UI 8080→host 8081避免与 MVP API 8080 冲突)
- 同挂载 `../../shared:/private`,并持久化元数据到 `../../shared/common/sftpgo`
- SFTPGoAdminClient 实装(对齐 upstream OpenAPI
- `GET /api/v2/token`BasicAuth获取 admin token
- `POST /api/v2/users` 创建用户(含 `permissions: {"/":["*"]}`
- `PUT /api/v2/users/{username}` 禁用/重置密码
- 新增 v3.0 dev 配置:`configs/dev_v30.yaml`(启用 `data.sftpgo` 并配置 `admin_api_base=http://argus-sftpgo:8080/api/v2`
- 新增 v3.0 一键脚本:
- `scripts/run_all_v30_api.sh`:起 Ray+SFTPGo、启动 API、创建用户并提交 PPO/GRPO/SFT引用 user dataset 路径)
- `scripts/run_e2e_v30_cases.sh`:最小 E2E runnerHP-1
- API 启动脚本增强:`scripts/60_start_api.sh` 支持透传 `SFTPGO_ADMIN_PASSWORD` 到 head 容器内的 API 进程。
- 涉及文件:
- `src/mvp/docker-compose.yaml`
- `src/mvp/configs/dev_v30.yaml`
- `src/mvp/scripts/run_all_v30_api.sh`
- `src/mvp/scripts/run_e2e_v30_cases.sh`
- `src/mvp/scripts/60_start_api.sh`
- `src/mvp/py/argus/service/sftpgo.py`
- `src/mvp/py/tests/test_sftpgo.py`
- `src/mvp/README.md`
- `specs/mvp/v3.0/v3.0_api.md`
- 验证方式与结果:
- 本地单测:`.venv/bin/python -m pytest -q`
- 结果:全部通过(`75 passed`),覆盖率 `90.35%`(阈值 `>= 90%`)。
- 待办/风险:
- 需要你在 `argus@h1` 实跑 `scripts/run_all_v30_api.sh` 完成真正的 SFTP 上传/下载与 retention 验收(按 `v3.0_acceptance.md`)。

View File

@ -1,166 +0,0 @@
# MVP v3.0 迭代总结Ray + SFTPGo + API + WebUI
本文总结 v3.0 迭代最终落地的功能、架构、运行方式、验收点与已知限制,便于后续评审、交接与继续迭代。
相关更详细文档:
- `specs/mvp/v3.0/v3.0_design.md`
- `specs/mvp/v3.0/v3.0_api.md`
- `specs/mvp/v3.0/v3.0_dev_plan.md`
- `specs/mvp/v3.0/v3.0_acceptance.md`
- `specs/mvp/v3.0/v3.0_progress.md`
---
## 1. 目标与范围
v3.0 作为“第一版可发布”的最小闭环,主要新增:
- **WebUI**:最小可用的人机界面(登录、任务提交与查看、数据入口、管理员入口)。
- **用户管理**:基于内部 token 的用户体系admin 与普通用户),支持创建用户与签发 token。
- **数据管理入口SFTPGo**:用户通过 SFTP/WebClient 上传下载自己的数据;同时暴露只读的共享数据/缓存目录common用于复用。
- **保持训练闭环**:仍通过 Ray Job 提交到集群执行PPO/GRPO/SFT 三类 workload 都验证)。
明确不做(本迭代保持最小):
- 不支持用户自定义训练代码TaskSpec 的 `code_path` 固定走 common 下的 verl snapshot 策略)。
- 不做复杂资源排队优化/多集群/多租隔离策略(目前隔离粒度主要在用户 jobs 目录层)。
---
## 2. 系统架构(最终形态)
核心组件:
- **Ray 集群(容器)**
- `argus-ray-head`head 节点(无 GPU/不跑训练),提供 Ray Dashboard 与 Job Server。
- `argus-ray-worker-0/1`worker 节点(有 GPU承载训练任务。
- worker 以 “stateless + watchdog 自动连接 head” 的方式加入集群。
- **API Server运行在 head 容器内)**
- 读取 YAML 配置dev/prod维护任务队列sqlite并周期性调度将任务提交到 Ray。
- 同时承载 WebUI`/ui`)。
- **SFTPGo容器**
- 提供 SFTP端口 `2022`)与 Web Client/Admin端口 `8081` 映射到容器 8080
- 用户 home 为 `/private/users/<user>`,默认可读写。
- 额外提供 `/common/*` 共享只读入口(见第 4 节)。
- **共享存储NFS/GPFS 等挂载到容器内 `/private`**
- `/private/common`共享缓存hf、datasets、models、db、logs 等)。
- `/private/users/<user>`用户隔离目录jobs/datasets/models/code/trash 等)。
---
## 3. 任务与调度Task / Ray Job
### 3.1 Task平台概念
- 用户向 API 提交 TaskSpecYAML平台分配 `task_id`(可读、包含用户名)。
- `task_id` 对应内部状态机与重试逻辑;底层每次提交 Ray Job 会产生 attempt 与 `ray_submission_id`
### 3.2 Ray JobRay 概念)
- 真正执行训练的 driver 通过 Ray Job 运行在集群 worker 上(避免 head 承载训练)。
- head 节点通过 `--num-cpus=0` / 自定义资源等策略避免调度到 head。
### 3.3 VERL 资源预检查的处理
- VERL 在创建资源池时会做 fail-fast 资源预检查(如“可用 GPU 不足”直接报错退出)。
- v3.0 延续 v2.x 的策略:服务端识别失败原因并按策略重试/回退(具体见 scheduler 实现与 v2.5/3.0 文档)。
---
## 4. 数据管理SFTPGo与 common 只读目录
### 4.1 用户目录(读写)
- 用户通过 SFTP/WebClient 访问自己的 home`/private/users/<user>`
- 目录结构(至少):`datasets/ models/ code/ jobs/ trash/ common/`
### 4.2 common 只读(方案 AVirtual Folder
本迭代采用 SFTPGo 的 Virtual Folder + 路径权限覆盖,实现用户可读共享目录但不可写。
最终对外暴露为:
- `/common/datasets`(只读)
- **mapped_path 指向真实目录 `/private/datasets`**(避免 `/private/common/datasets` 中大量 symlink 导致的 WebClient “权限不足/越界”问题)
- `/common/hf`(只读)
- mapped_path 指向 `/private/hf`
备注:
- `/private/common/datasets` 内部存在 symlink`gsm8k -> /private/datasets/gsm8k`),如果虚拟目录映射到 symlink 根目录SFTPGo 会把 symlink 跳转视为“逃逸 root”导致点击进入时报权限不足因此选择直接映射到真实目录根。
---
## 5. WebUI最小可用
入口:
- `/ui/login`:粘贴 token存 browser `localStorage`
- `/ui/tasks`任务列表Running/Pending/CompletedCompleted 支持分页
- `/ui/tasks/new`提交任务PPO/GRPO/SFT 三套样例可一键填充)
- `/ui/data`:展示当前用户名、支持重置 SFTPGo 密码并复制;提供跳转到 SFTPGo WebClient提示 FileZilla 等客户端用法
- `/ui/admin`:管理员入口(创建用户、签发 token、用户列表
- 导航栏提供 Ray Dashboard 快捷跳转(当前 IP 的 `:8265`
关于 admin 页面权限:
- admin 页面本身可访问,但其数据请求必须携带 admin token否则会在页面内显示 401/403/错误信息(满足“需要先提供 admin token 才能看到内容”)。
---
## 6. APIv3.0 新增/强化点)
核心接口(节选):
- 认证:
- Bearer token`MVP_INTERNAL_TOKEN`admin或用户 token由 admin 签发)
- 用户管理admin
- `POST /api/v2/users` 创建用户(并初始化用户目录)
- `GET /api/v2/users` 获取用户列表(包含最新 token、创建/更新时间等)
- `POST /api/v2/users/{user_id}/tokens` 签发用户 token
- 任务:
- `POST /api/v2/tasks` 提交 TaskSpecYAML
- `GET /api/v2/tasks` 任务列表(支持 states/limit/offset用于 Completed 分页)
- `GET /api/v2/tasks/{task_id}``POST /api/v2/tasks/{task_id}:cancel``GET /api/v2/tasks/{task_id}/logs`
- `GET /api/v2/queue`(运行中/待调度概览)
- 数据/SFTP
- `GET /api/v2/me` 返回用户路径信息、SFTP 连接信息,并 best-effort 对齐 SFTPGo 用户配置
- `POST /api/v2/me/sftp:reset_password` 用户自助重置 SFTPGo 密码(一次性返回明文)
安全取舍说明(当前为内网/开发优先):
- 为了 Admin WebUI “可查看并复制 token”数据库持久化存储了 `token_plain`(明文 token
- 这在生产场景通常不建议;未来可改为只展示“重置/重新签发”而不回显明文,或只回显一次。
---
## 7. 持久化与清理
- 任务队列sqliteWAL 模式)
- SFTPGo自带 sqlite db容器挂载持久化目录
- Jobs 目录清理策略(服务端 janitor
- job 结束后 3 天移动到回收目录trash
- 回收目录再保留 7 天后删除
---
## 8. 运行方式与脚本
开发/验收脚本:
- `src/mvp/scripts/run_all_v30_api.sh`端到端拉起Ray + SFTPGo + API并通过 API 提交 PPO/GRPO/SFT等待完成并验收
- 其他脚本用于启动/停止 API、准备数据与模型、探测服务就绪等详见 scripts 目录与 README
典型端到端(示例参数):
- `MVP_INTERNAL_TOKEN=my-dev-token`
- `SFTPGO_ADMIN_PASSWORD=my-dev-sftpgo-admin`
- 支持 `RESET_DB/RESET_SFTPGO` 用于测试环境重置
---
## 9. 验证结果(已跑通)
`argus@h1` 环境中已完成端到端验证:
- Ray 集群可用head + 2 worker
- API server + WebUI 可用
- SFTPGoadmin + 普通用户)可用
- 通过 API 连续提交 PPO/GRPO/SFT 三种任务均能完成SUCCEEDED
- 用户可以登录 SFTPGo WebClient/SFTP访问自己的目录并访问 `/common/datasets``/common/hf` 的只读内容
同时本地单测通过:
- pytest 全绿
- 覆盖率阈值 >= 90%
---
## 10. 已知限制 & 后续可改进
- WebUI 当前为最小版,交互与权限提示仍偏“工程化”而非产品化(后续可增强错误提示、搜索筛选、任务详情聚合等)。
- token 明文持久化仅适合内网/开发场景;生产建议改为一次性展示或支持撤销/轮换策略。
- SFTPGo 虚拟目录目前保留了历史遗留映射(例如 `/common/models` 可能残留),后续可在升级脚本中做一次性清理与迁移。

View File

@ -1,7 +0,0 @@
# MVP v3.5
本目录包含 v3.5 的需求与设计(精简版):
- `requirement.md`:需求补充说明(来源于讨论)
- `roadmap_v3.5.png`架构草图Advanced Task + Resume + IB + Serving
- `v3.5_design.md`:详细设计方案(基于 v3.0;当前迭代仅聚焦 Advanced TaskSpec + Custom RewardServing/IB/Resume/多版本 verl 暂缓)

View File

@ -1,3 +0,0 @@
1. node managementv3.5 引入的接口骨架:通过 SSH/平台能力管理 head/worker 节点生命周期;先做最小可用 --- 这个是干嘛的?
2.

View File

@ -1,40 +0,0 @@
v3.5 版本是在v3.0的基础上进行功能扩展:
1. 支持自定义命令不走固定的TaskSpec模板用户直接提供调用verl 的python命令如下这个灵活度更高需要用户自己把握文件路径用户使用 $HOME服务层替换为用户自己的/private/users/<user>/路径,使用$COMMON 则替换为/private/
```
PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \
data.train_files=$HOME/data/gsm8k/train.parquet \
data.val_files=$HOME/data/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=console \
trainer.val_before_train=False \
trainer.n_gpus_per_node=1 \
trainer.nnodes=1 \
trainer.save_freq=10 \
trainer.test_freq=10 \
trainer.total_epochs=15
```
2. 支持自定义的奖励函数方法,你参考 verl 项目 [text](../../../verl) 里的示例,设计方案
3. 支持codepath指定用户上传到自己user路径下的 verl版本代码
4. 断点续训支持某个已经complete成功或者fail或者stopped的任务task从最后一个保存的checkpoint 继续训练参数应该保持不变你确认一下是不是对应一个新的ray job或者分析一下verl 是否已经有类似的功能支持。
5. 支持训练走NCCL使用RoCEv2和Infiband网络调研一些verl怎样支持需要哪些配置。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@ -1,67 +0,0 @@
# MVP v3.5 功能变更总结(相对 v3.0
> v3.5 本轮按已确认的精简 scope**只聚焦 Advanced TaskSpec + Custom Reward方式 A用户在 command 里写 overrides**。Serving/IB/断点续训/多版本 verl 等能力本轮仍不做。
## 1. TaskSpec / 任务语义
### 1.1 新增 Advanced TaskSpec自定义 command
- 新增 `kind: advanced` 的 TaskSpec 类型:
- 用户可提交任意 bash `command`,不再局限于平台内置 PPO/GRPO/SFT 模板。
- `workload` 不再要求用户填写,也不做 infer平台内部统一按 `"advanced"` 做任务分类与 task_id 命名(避免未来训练类型扩展带来的限制)。
- 支持 `$HOME` 宏替换(服务端提交前展开):
- `$HOME``/private/users/<user_id>`
- `$HOME/common/datasets``/private/datasets`
- `$HOME/common/hf``/private/hf`
- `command` 校验best-effort面向内部可信用户
- 要求包含 `python3 -m verl.`(允许 `verl.trainer.*` / `verl.model_merger` 等)。
- 不做强沙箱;主要防止明显误用导致的不可预期行为。
### 1.2 Custom Reward方式 A
- 平台不新增 reward 专用字段、不扩展 TaskSpec schema。
- 用户通过在 `command` 里写 VERL 原生 overrides 来注入 reward
- `custom_reward_function.path=...`
- `custom_reward_function.name=...`
- `custom_reward_function.reward_kwargs=...`(如需)
- 平台侧仅做:
- 基础路径/宏展开($HOME
- best-effort 的字符串校验(不做深度 AST 解析)
## 2. WebUINew Task 体验增强,仍兼容 YAML
- `New Task` 页面新增 **YAML 模式 / 表单模式**切换:
- 表单模式只覆盖 **5 个模板**PPO / GRPO / SFT / Advanced / Model Merge。
- 表单模式实时生成 YAML 预览Submit 时提交生成 YAML可一键切回 YAML 模式继续手工编辑。
- `Advanced example`
- 示例命令改为多行、可读性更好。
- 补齐 PPO 常见 fail-fast 所需的关键 overrides例如 actor micro batch避免用户“照抄即失败”。
- 新增 `Model merge example`Advanced command 形式):
- 使用 `python3 -m verl.model_merger merge ...`
- 支持用 `$HOME/jobs/<RAY_SUBMISSION_ID>/...` 访问训练产物目录。
## 3. SFTPGo / common 目录可读性(配合 v3.5 的 $HOME/common 语义)
> 这些变更主要用于保证 v3.5 所定义的 `$HOME/common/{datasets,hf}` 语义在 SFTPGo WebClient/客户端下可用。
- `/common/datasets``/common/hf` 作为 SFTPGo virtual folders 暴露为只读共享目录:
- 允许 list + download用于浏览与下载/查看内容;仍不允许 upload/rename/delete
- 权限规则覆盖到子路径(避免“能进目录但文件不可读”的情况)。
- API 调用 SFTPGo admin API 的连通性增强:
- dev 环境下避免依赖容器内 DNS部分 head 容器环境存在临时解析失败),改为通过 docker bridge 网关 + 映射端口访问 admin API。
- API 启动脚本确保注入 `SFTPGO_ADMIN_PASSWORD`(与 compose 默认值保持一致),避免 Reset Password 走到 401。
## 4. 兼容性与行为变化
- **完全兼容 v3.0 的 PPO/GRPO/SFT TaskSpec YAML**(原有字段与提交方式不变)。
- 新增能力不会影响 ray/node management仍按 v3.0head 发布 discovery、worker watchdog join/self-heal
- Advanced 任务不会进入 PPO/GRPO/SFT 的语义约束;平台仅负责:
- 资源字段(`nnodes` / `n_gpus_per_node`)用于队列调度与提交 gate
- 将 `command` 作为 Ray job entrypoint 执行
## 5. 已知限制v3.5 不做)
- 不提供“可视化” reward 配置面板(仅方式 A用户自己写 command
- 不支持 per-job 自定义 verl 代码快照/多版本共存(本轮不做 code_path 选择)。
- 不支持断点续训一键 resubmit / IB(RDMA) / model serving按 roadmap 后续版本推进)。

View File

@ -1,366 +0,0 @@
# MVP v3.5 详细设计方案(进一步精简版,基于 v3.0
> 背景v3.0 已具备 WebUI + API server + 用户/任务隔离 + SFTPGo 数据管理 + Stateless Ray clusterhead + 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/<user_id>`(容器内路径)。
---
## 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 TaskSpecppo/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/<user_id>`
用户如果要用共享数据/缓存:
- 共享数据:`$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/<user_id>"`
这样用户写的 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/<me>/datasets/...`
- 允许(共享目录):`/private/datasets/...`
- 对 `custom_reward_function.path=...`(若出现)做 allowlist
- 允许:`/private/users/<me>/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/...`
- 用户 codereward`$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 commandtrain/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", "<expanded_command>"]`
- 原因:用户 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/<user>/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/<user>`
- 校验使用 “展开后的 command”
3) reward 注入检查(仅方式 A
- 若发现 `custom_reward_function.path=...`
- 校验展开后的 path 前缀必须是 `/private/users/<me>/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` 字段(可选增量迁移)。

View File

@ -1,200 +0,0 @@
# MVP v3.5精简版开发计划TDD
> 目标:在 v3.0 已有能力基础上,仅新增两项能力:
> 1) **Advanced TaskSpec自定义 command**
> 2) **Custom Reward方式 A用户自己在 command 里写 `custom_reward_function.*`**
>
> 设计依据:`specs/mvp/v3.5/v3.5_design.md`(本计划不再扩展 scope
---
## 0. 范围与约束
### 0.1 In scope
- 新增 `kind: advanced` 的 TaskSpec用户提供 `command`,平台做 `$HOME` 宏替换与 best-effort 校验,再提交 Ray Job。
- Custom Reward平台仅做 **reward path 校验**(方式 A不新增结构化字段。
- `$HOME/common/*` 路径语义支持(关键):用户在 SFTPGo/WebClient 看到的路径能被训练进程正确读取。
### 0.2 Out of scope本轮不做
- 自定义 verl 版本/代码路径(多版本共存)
- 断点续训resume from checkpoint
- IB/RoCEv2/NCCL 专项支持
- Model Serving
- Node management 改造v3.0 的 stateless head/worker/watchdog/supervisor 机制保持不变)
### 0.3 关键路径映射(必须保持一致)
> 说明SFTPGo 的 `$HOME/common/...`**virtual folder**,训练进程看不到该虚拟路径。
提交 Advanced command 前必须展开/映射:
- `$HOME/common/datasets``/private/datasets`(只读语义)
- `$HOME/common/hf``/private/hf`(只读语义)
- 其余 `$HOME``/private/users/<user_id>`
并且为兼容历史用法v3.0
- Basic TaskSpec 仍接受 `/private/common/datasets/...``/private/common/hf/...`(不强制迁移)。
---
## 1. 测试策略TDD
### 1.1 单元测试优先级
1) **解析与兼容**`kind: advanced` 能解析;无 `kind` 仍按 Basic 解析,旧用法不破坏。
2) **宏替换正确性**`$HOME` / `$HOME/common/*` 映射严格按约定展开。
3) **best-effort 校验**:拒绝明显危险/跨用户路径;对 reward path 做 allowlist。
4) **提交链路**Scheduler 能识别 Advanced spec 并调用对应的提交方法,确保 submission_id/目录规范不变。
5) **WebUI/API**New Task 模板与 `/spec` 展示完整 resolved spec包含展开后的 command
### 1.2 本地运行方式
- 复用已有 `.venv`,执行:`.venv/bin/python -m pytest`
- 若环境没有 pip使用 uv 的方式参考 v3.0 约定(不在本计划重复)。
---
## 2. 里程碑划分(每个里程碑可独立验证)
> 约定:每个里程碑先写测试(失败),再实现代码使测试通过;里程碑结束跑一遍 `pytest`
### M1 — TaskSpec 模型与解析(兼容优先)
**目标**
- 引入 AdvancedTaskSpec 数据结构与 union parser同时保证 v3.0 Basic 行为不变。
**新增/修改(建议位置)**
- `src/mvp/py/argus/ray/models.py`
- 新增 `AdvancedTaskSpec`
- 新增 `parse_taskspec(obj: dict) -> JobSpec | AdvancedTaskSpec`
- 兼容策略:缺省 `kind` → 走 `JobSpec.from_dict`
**测试(先写)**
- `src/mvp/py/tests/test_models.py`
- `test_parse_taskspec_basic_no_kind_compat()`
- `test_parse_taskspec_advanced_smoke()`
- `test_parse_taskspec_advanced_requires_command_nnodes_gpus()`
**验收**
- `pytest -q` 通过;旧测试不修改或仅做最小必要更新。
---
### M2 — Advanced command 展开与校验(核心能力)
**目标**
- 实现 command 展开(含 `$HOME/common/*` 映射)与 best-effort 强约束校验。
**实现点(建议新增模块)**
- `src/mvp/py/argus/service/command_expand.py`(或放在 `argus/service/validation.py`
- `expand_advanced_command(user_id: str, command: str) -> str`
- `validate_advanced_command(user_id: str, expanded_command: str) -> None`(失败抛 `ValueError`
**强约束(与设计文档一致)**
- 必须包含 `python3` 且包含 `-m verl.trainer.`(否则 400
- 禁止出现 `/private/users/<other>/...`(跨用户路径)
- 若检测到 `data.train_files=`/`data.val_files=`
- 只允许 `/private/users/<me>/datasets/...``/private/datasets/...`
- (兼容)允许 `/private/common/datasets/...`(旧路径)
- 若检测到 `custom_reward_function.path=`
- 只允许 `/private/users/<me>/code/...`(展开后校验)
**测试(先写)**
- 新增:`src/mvp/py/tests/test_advanced_command.py`
- `test_expand_maps_home_common_datasets_to_private_datasets()`
- `test_expand_maps_home_common_hf_to_private_hf()`
- `test_expand_maps_home_to_private_users()`
- `test_validate_rejects_cross_user_paths()`
- `test_validate_requires_verl_trainer_entry()`
- `test_validate_allows_reward_path_under_user_code()`
- `test_validate_rejects_reward_path_outside_user_code()`
**验收**
- 单测覆盖映射/校验的正反例;错误信息可读(用于 API 400 detail
---
### M3 — Ray 提交链路支持 AdvancedBuilder/Tool/Scheduler
**目标**
- Advanced spec 能进入 scheduler 队列并提交为 Ray jobdriver 仍落 worker
**代码改动点(建议)**
- `src/mvp/py/argus/ray/builders.py`
- 新增 `build_advanced_argv(command: str)`:返回 `["bash","-lc", expanded_command]`
- `src/mvp/py/argus/ray/ray_job_tool.py`
- 新增 `submit_advanced(...)`(或统一成内部 submit plan
- runtime_env继续注入公共 verl code path本轮不支持用户自定义 verl 代码)
- 可选:把 `/private/users/<user>/code` 加入 `PYTHONPATH`,提升 reward 代码 `import` 体验
- `src/mvp/py/argus/service/scheduler.py`
- 使用 `parse_taskspec` 分流 Basic/Advanced
- Advanced 调用 `tool.submit_advanced(...)`
**测试(先写)**
- `src/mvp/py/tests/test_builders.py`
- `test_build_advanced_argv_uses_bash_lc()`
- `src/mvp/py/tests/test_scheduler.py`
- 新增一个 `kind: advanced` 的任务,断言 scheduler 调用了 `submit_advanced`
- 断言 job_dir/submission_id 规则不变(仍按 `/private/users/<user>/jobs/<sid>`
- `src/mvp/py/tests/test_ray_job_tool.py`
- 断言 advanced 提交时 entrypoint 是 driver_entrypoint + `bash -lc ...`
**验收**
- 单测跑通Scheduler tick 能完成 Advanced 任务从 QUEUED → SUBMITTEDmock Ray
---
### M4 — API & WebUI最小功能闭环
**目标**
- WebUI/HTTP API 能提交 Advanced Task并在详情页看到 resolved spec含完整 command
**API 改动点**
- `src/mvp/py/argus/service/app.py`
- `POST /api/v2/tasks`:支持 `kind: advanced`
- 保存 raw YAML保持与 Basic 一致)
- 对 Advanced展开 command + 校验(失败返回 400
- `GET /api/v2/tasks/{task_id}/spec`
- 返回 resolved spec建议同时返回 raw + expanded或 YAML 中直接给 expanded
**WebUI 改动点**
- `src/mvp/py/argus/service/ui.py`
- New Task 页面新增 Advanced 模板(含中文注释)
- 文案强调共享目录:`$HOME/common/datasets``$HOME/common/hf`
**测试(先写)**
- `src/mvp/py/tests/test_app.py`
- `test_create_task_advanced_ok()`(最小 valid command
- `test_create_task_advanced_rejects_invalid_command()`
- `test_task_spec_endpoint_includes_expanded_command()`
- `src/mvp/py/tests/test_ui.py`
- 断言页面包含 Advanced 示例块
**验收**
- `pytest` 通过;浏览器可提交 Advanced YAML 并看到 expanded command。
---
### M5 — 端到端验证(远端 argus@h1
**目标**
- 在真实 Ray cluster + VERL 环境下验证 Advanced 与 Custom Reward方式 A
**步骤(手工验收脚本化可选)**
1) 启动 v3.0/v3.5 统一的 compose + API沿用现有 `run_all` 脚本体系)
2) 用户(如 `alice`)通过 SFTP 上传 reward 代码到:
- `$HOME/code/reward.py`(真实路径 `/private/users/alice/code/reward.py`
3) 通过 WebUI 或 curl 提交 Advanced task
- `command` 中包含:
- `custom_reward_function.path=$HOME/code/reward.py`
- `custom_reward_function.name=compute_score`
- `data.train_files=$HOME/common/datasets/gsm8k/train.parquet`
- `data.val_files=$HOME/common/datasets/gsm8k/test.parquet`
4) 检查:
- 任务状态从 QUEUED → RUNNING → SUCCEEDED/FAILED有日志
- driver 不在 head 上跑dashboard 验证)
- 日志出现 “custom reward” 生效的提示(按 VERL 实际日志关键字确认)
5) 回归:提交 Basic ppo/grpo/sft 任务仍可运行(确保兼容性)
**验收**
- Advanced task 能跑至少若干 step且 reward 注入生效。
- Basic 任务兼容不回退。
---
## 3. 风险点与边界(明确写进 PR/变更说明)
- Advanced command 只做 best-effort 校验,不做完整 shell AST 解析;复杂命令可能存在漏检/误判(后续可扩展)。
- `$HOME/common/*` 是“用户侧语义”,服务层必须映射到真实路径,否则训练必然 FileNotFound。
- 校验策略(强约束)如果后续要允许非 VERL 命令,需要调整规则并补测试(本轮默认拒绝)。

View File

@ -1,9 +0,0 @@
# MVP v3.6
本目录包含 v3.6 的需求与设计:
- `Snipaste_2026-01-05_10-56-34.png`v3.6 架构草图(在 v3.5 基础上增加 Weights & Biases其余模块保持不变
- `requirements.md`需求要点W&B + Evaluation 模板)
- `wandb.md`W&B local server 的前期调研与资料license、部署方式、VERL 配置要点等)
- `v3.6_design.md`v3.6 详细设计方案(基于 v3.5
- `v3.6_progress.md`v3.6 里程碑进度记录

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View File

@ -1,3 +0,0 @@
1. 增加wandb功能
2. 增加evaluation 模板

View File

@ -1,280 +0,0 @@
# MVP v3.6 详细设计方案(基于 v3.5
> 设计基线:当前线上已具备 v3.5 能力PPO/GRPO/SFT + Advanced TaskSpec + SFTPGo 数据管理 + WebUI
> v3.6 的架构草图见:`specs/mvp/v3.6/Snipaste_2026-01-05_10-56-34.png`
## 0. 目标与范围
### 0.1 v3.6 目标
1) **Weights & BiasesW&B集成**
- 训练/任务运行时自动打点到 W&B local server。
- 采用“共享 W&B 账号license 只支持 1 user + 为每个 MVP 用户创建独立 project”的方式隔离可视化与检索体验。
- 平台提供 W&B 的跳转链接、以及每个 task 对应 run 的可定位信息(最小闭环)。
2) **New Task 增加 Evaluation 模板**
- 在 New Task 页面提供一个最小可用的 “Evaluation” 任务模板(用于离线评估/打分),并能把结果落到 job 输出与可选W&B。
### 0.2 非目标v3.6 不做)
- 不引入新的 node management 机制;保持 v3.5 的 head discovery + worker watchdog stateless pool。
- 不做 Serving/IB/RDMA/断点续训/多版本 verl code_path 等(仍按 v3.5 的范围)。
- 不做多租户安全隔离W&B API Key 注入属于内部可信环境)。
---
## 1. W&B local server部署与连通
> 资料参考:`specs/mvp/v3.6/wandb.md`。**文档中 license/token 属敏感信息v3.6 设计不在代码/文档里写死。**
### 1.1 部署方式dev/h1
建议在 `src/mvp/docker-compose.yaml` 新增 `wandb` 服务(与现有 ray_head / sftpgo 同一 compose network
- 镜像:`wandb/local:latest`
- 容器端口:`8080`
- 宿主机端口:建议 `8090:8080`(避免和 MVP API `8080`、SFTPGo `8081` 冲突)
- 持久化:挂载到 NFS/共享目录(例如 `/private/common/wandb`)以持久化 runs/artifacts
- 首次启动后由管理员在 W&B System Admin 页面粘贴 license`wandb.md`
### 1.1.1 持久化策略(必须)
v3.6 约定 **W&B server 的元数据必须持久化**(否则会丢 license/账号/API key、历史 runs/project 索引等):
- W&B server 数据目录(`/vol`挂载到共享目录NFS例如 `/private/common/wandb:/vol`
- 这部分数据由平台/管理员长期保留(不跟随单个 job 清理)
与之相对,**每个 Ray job 对应的本地 W&B run 文件**放在 job 目录下(见 §2.3 的 `WANDB_DIR`),并由现有 janitor 随 job 一起清理:
- `WANDB_DIR=/private/users/<user_id>/jobs/<ray_submission_id>/wandb`
- janitor 对 job 的策略是“结束后 3 天移入回收站、7 天后删除”,因此该目录会被一并处理
> 说明W&B 的最终“事实数据”在 server 侧持久化runs/metrics/可视化job 目录下的 `WANDB_DIR` 更像运行期缓存/临时文件与调试材料。
### 1.2 容器内访问 W&B 的 base_url
在 v3.5 经验中Ray head 容器对 docker 内部 DNS 名称解析不稳定(`Temporary failure in name resolution`)。
为避免再次踩坑v3.6 统一采用 **“docker bridge 网关 + host 映射端口”** 的方式让容器访问 W&B
- `WANDB_BASE_URL=http://172.22.0.1:8090`(其中 `172.22.0.1``mvp_argus-ray-net` 网关)
> 注意:如果未来 dev 环境 network 网段变化,需要把网关地址做成配置项(见 §2
---
## 2. 平台侧 W&B 集成API/Scheduler/Ray Job runtime_env
### 2.1 配置设计YAML
`configs/dev.yaml`(以及生产配置)中新增一段 W&B 配置,建议结构:
```yaml
tracking:
wandb:
enabled: true
base_url: "http://172.22.0.1:8090"
api_key_env: "WANDB_API_KEY"
entity: "" # 可选verL 通过 env WANDB_ENTITY 读取
project_suffix: "_project" # 例如 alice_project
# 可选代理verL 支持 trainer.wandb_proxy
proxy: null
```
平台读取 `api_key_env` 对应的环境变量,并在 job 维度注入 `WANDB_API_KEY`
**不在配置文件中存明文 API KEY**(避免泄露)。
### 2.2 项目/命名规范(共享账号 + user project
由于 W&B local license 限制 “最多 1 user”v3.6 采用:
- **同一个 W&B 账号**(同一个 `WANDB_API_KEY`
- 每个 MVP user 使用不同 project
- `project_name = <user_id> + project_suffix`
- 示例:`alice_project`
- 每个 Ray job attempt 对应一个 run
- `experiment_name = <ray_submission_id>`(保证 attempt 唯一;也便于从 dashboard 反查)
verL 内部 `wandb.init(project=project_name, name=experiment_name, entity=$WANDB_ENTITY)`(见 `verl/utils/tracking.py`)。
### 2.3 Ray Job 注入runtime_env env_vars
`tracking.wandb.enabled=true` 时,在 scheduler 提交 ray job 时,统一注入:
- `WANDB_BASE_URL`(来自配置)
- `WANDB_API_KEY`(来自 env `tracking.wandb.api_key_env`
- `WANDB_ENTITY`(可选)
- `WANDB_MODE=online`(默认在线;可在 dev/离线时切换)
- `WANDB_DIR=/private/users/<user_id>/jobs/<ray_submission_id>/wandb`(保证每个 job 的本地 run 文件落到对应 job 目录;便于随 job 一起被 janitor 清理)
> 对 Advanced TaskSpec平台无法替用户改 command但仍可注入上述 env让用户在 command 中自行启用 wandb。
### 2.4 verL 训练任务自动开启 wandb logger
对平台内置 workloadPPO/GRPO/SFT平台将 hydra overrides 改为:
- `trainer.logger=[console,wandb]`(替代 v3.5 的 `trainer.logger=console`
- `trainer.project_name=<user_id>_project`
- `trainer.experiment_name=<ray_submission_id>`
- 可选:如果配置了 `tracking.wandb.proxy`,注入 `trainer.wandb_proxy=...`
兼容性说明:
- `trainer.logger` 在 verL 的 ppo/sft config 中本身是 list示例为 `["console","wandb"]`),因此不会破坏 verL 配置解析。
- 现有 v3.5 的 checkpoint/log dir 仍按 job_dir 注入,不依赖 `trainer.default_local_dir`
### 2.5 数据库存储与 API 输出(最小闭环)
v3.6 需要让用户能在 WebUI/Task 详情中 “点一下跳转到 W&B”
建议在 DBsqlite里新增 attempt 级字段(或 metadata JSON
- `wandb_project`:如 `alice_project`
- `wandb_run_name`:如 `<ray_submission_id>`
- `wandb_base_url`:如 `http://<host>:8090`
- `wandb_url`:拼出来的最终链接(若 W&B local 的 URL 结构不稳定,可只存前 3 项,由前端拼接)
API 侧在:
- `GET /api/v2/tasks/<task_id>``latest_attempt` 增加 `wandb` 字段(或顶层增加 `tracking` 字段)
- `GET /api/v2/me` 增加 `wandb` 信息base_url、project_name便于页面展示 “Open W&B”
> W&B local 的 URL 路径结构需要在接入时确认;若不确定,先只提供 base_url + project/run 名称,用户可在 W&B UI 搜索。
---
## 3. WebUI 变更v3.6
### 3.1 Data / Login 页
- 在 Data 或 Login 页新增:
- “Open W&B” 链接(跳转到 `tracking.wandb.base_url` 的 Web UI
- 展示当前用户的 project 名称(例如 `alice_project`),并提供 Copy 按钮
### 3.2 Tasks / Task Detail
- Task list 无需大改Task detail 页面增加:
- W&B run 信息project / run name / link
- 若任务失败W&B 仍可用于查看已上报的中间日志(对训练类任务有价值)
### 3.3 New Task 增加 Evaluation 模板
New Task 页面新增一个 “Evaluation example”
#### 方案 A推荐最小改动作为 Advanced TaskSpec 模板
- 直接提供 `kind: advanced` 的模板command 运行 verL 自带评估入口:
- `python3 -m verl.trainer.main_eval data.path=... custom_reward_function.path=... +ray_kwargs.ray_init.address=auto`
- 优点:不需要扩展 TaskSpec schema / scheduler 逻辑
- 缺点Evaluation 在平台侧不可结构化(仍属于 advanced
#### 方案 B更产品化新增内置 workload: evaluation后续可选
若希望在任务分类/队列中把 evaluation 作为一类“内置任务”,可新增:
```yaml
workload: evaluation
code_path: /private/common/code/verl/verl_repo
nnodes: 1
n_gpus_per_node: 0
data_path: $HOME/common/datasets/<...>.parquet
response_key: responses
data_source_key: data_source
reward_model_key: reward_model
custom_reward_path: $HOME/code/reward.py # 可选
custom_reward_name: compute_score # 可选
```
平台把它编译成 `verl.trainer.main_eval` 的 hydra overrides + ray init address并可选择把结果解析后上报 W&B。
v3.6 建议先落地 **方案 A**(模板即可),方案 B 作为 v3.7+ 的演进。
---
## 4. 验收标准v3.6
### 4.1 W&B训练任务
- 提交 PPO/GRPO/SFT 任一任务:
- W&B local server 能看到对应 project`alice_project`
- 能看到 run 名称为该任务 attempt 的 `ray_submission_id`
- run 内能持续刷新 metrics至少包含 loss/step 等 verL 默认上报)
- WebUI
- 能打开 W&B UI 链接
- Task detail 能展示 project/run或至少可检索信息
### 4.2 Evaluation 模板
- New Task 中出现 “Evaluation example”
- 复制模板提交后:
- 任务能在 Ray 上运行CPU 即可,`+ray_kwargs.ray_init.address=auto` 连接集群)
- 输出 metrics`main_eval.py` 默认 print dict
- (可选)若用户在 command 里启用 wandb则能在对应 project 下看到评估 run
---
## 5. 兼容性影响评估
- 对现有 v3.5 功能:
- 默认行为改变PPO/GRPO/SFT 会默认多一个 loggerwandb并对外发起 HTTP 请求到 W&B server。
- 若 W&B server 不可用/配置缺失:
- 建议行为:平台自动降级为 `trainer.logger=[console]` 并在 task 状态中给出 warning避免训练直接失败
- 初版也可选择 fail-fast缺少 `WANDB_API_KEY` 时拒绝开启 wandb由配置开关控制
- 对资源/存储:
- W&B server 自身会写入一定量数据license 限制 10GB需配合 retention 策略做清理v3.6 先手动,后续可自动)。
- job 目录下的 `WANDB_DIR` 会随 jobs retention 被清理;不会无限增长。
---
## 6. 已确认的落地约定
- W&B server 对外端口:`8090`
- Project 命名:`<user_id>_project`(不使用 `WANDB_ENTITY`
- Evaluation先只提供 **Advanced 模板**New Task 页面提供示例即可)
---
## 7. 运维与验收流程dev/h1
### 7.1 启动服务docker compose
1) 启动/重启 composeRay head/worker + SFTPGo + W&B
```bash
cd /home2/argus/infra/mvp/src/mvp
docker compose up -d
```
2) 访问 UI
- MVP WebUI`http://<HOST>:8080/ui`
- Ray Dashboard`http://<HOST>:8265`
- SFTPGo Web`http://<HOST>:8081/web/`
- W&B Web`http://<HOST>:8090`
> W&B 容器数据目录已挂载到共享盘:`/private/common/wandb`(容器内 `/vol`)。
### 7.2 初始化 W&B管理员一次性操作
1) 打开 `http://<HOST>:8090/system-admin` 粘贴 license详见 `specs/mvp/v3.6/wandb.md`)。
2) 进入 W&B UI 创建/登录到共享账号license 只支持 1 user
3) 在 W&B UI 的用户设置页面生成 API Key通常位于 “User Settings / API Keys”
### 7.3 启动 API server需要注入 WANDB_API_KEY
平台不会把 `WANDB_API_KEY` 写入配置文件;必须在启动 API server 时通过环境变量提供。
示例(在宿主机):
```bash
export MVP_INTERNAL_TOKEN="my-dev-token"
export WANDB_API_KEY="...从 W&B UI 复制..."
cd /home2/argus/infra/mvp/src/mvp/scripts
./60_start_api.sh
```
> 说明:`./60_start_api.sh` 会把 `WANDB_API_KEY` 透传给 head 容器内运行的 API server。
### 7.4 验收(最小闭环)
1) 在 WebUI Login 页面能看到 W&B 区块Open W&B + project copy
2) 提交一个 PPO/GRPO/SFT 任务(任意一个即可):
- W&B project 为 `<user_id>_project`(如 `alice_project`
- run name 为该 attempt 的 `ray_submission_id`
3) 提交 Evaluation 模板Advanced能在 Ray 上运行并输出评估结果stdout / logs

View File

@ -1,194 +0,0 @@
# MVP v3.6(基于 v3.5开发计划TDD
> 设计依据:`specs/mvp/v3.6/v3.6_design.md`
> 本计划默认已确认:
> 1) W&B host 端口:`8090`2) project`<user_id>_project`3) evaluation 先做 Advanced 模板4) 不使用 `WANDB_ENTITY`
## 总体原则
- **TDD**:每个里程碑先补齐单测(或契约测试)再实现功能;保证覆盖率门槛不回退。
- **最小闭环**:先做到“可用+可验证”,再做体验优化;不做超出 v3.6 scope 的扩展。
- **配置不落盘敏感信息**`WANDB_API_KEY` 只能来自运行环境变量,不写入仓库配置文件。
---
## Milestones
### M1配置层tracking.wandb与解析
**目标**
- 在服务配置中新增 `tracking.wandb`支持开关、base_url、api_key_env。
- 不引入 `WANDB_ENTITY`(保持为空即可)。
**开发任务**
- 在 `src/mvp/py/argus/service/config.py`
- 新增 `TrackingConfig/WandbConfig` dataclass或等价结构
- `V2Config.from_root_dict()` 解析 `tracking.wandb.*`(缺省为 disabled
- 校验:`enabled=true``base_url` 不能为空;`api_key_env` 默认 `WANDB_API_KEY`
**测试(先写)**
- `test_config_parses_wandb_defaults`:没有 tracking 字段时,默认 disabled。
- `test_config_parses_wandb_enabled`enabled=true 能读到 base_url/api_key_env。
- `test_config_rejects_empty_base_url_when_enabled`enabled=true 且 base_url 空时报错(或记录 warning取决于实现选择
**验收**
- 仅通过 config 即能决定是否启用 W&B且不会破坏 v3.5 现有配置解析。
---
### M2Ray Job runtime_env 注入WANDB_* env
**目标**
- 当 `tracking.wandb.enabled=true` 时:平台在 job 粒度注入 `WANDB_BASE_URL/WANDB_API_KEY/WANDB_MODE/WANDB_DIR` 等 env。
- `WANDB_API_KEY` 从 API server 进程环境变量中读取:`os.environ[api_key_env]`
**开发任务**
- 在 scheduler / ray job builder`src/mvp/py/argus/service/scheduler.py``src/mvp/py/argus/ray/builders.py`
- 构造 job runtime_env.env_vars 的 merge 逻辑:
- 现有 `ray.runtime_env.env_vars` 为基础;
- 追加 W&B env不覆盖用户显式指定的同名变量或按“平台优先”策略二选一并写清楚
- 注入:
- `WANDB_BASE_URL=<cfg.tracking.wandb.base_url>`
- `WANDB_API_KEY=<os.environ[cfg.tracking.wandb.api_key_env]>`
- `WANDB_MODE=online`
- `WANDB_DIR=/private/users/<user_id>/jobs/<ray_submission_id>/wandb`(本地 run 文件随 job 一起由 janitor 清理)
**测试(先写)**
- `test_scheduler_injects_wandb_env_when_enabled`
- mock env 中存在 `WANDB_API_KEY`
- 提交一个内置任务ppo/sft 任意),断言构造出来的 runtime_env 含以上 env_vars。
- `test_scheduler_sets_wandb_dir_under_job_dir`
- 断言 `WANDB_DIR` 位于该 attempt 的 job 目录下(而不是 common 目录),避免无法跟随 job retention 清理。
- `test_scheduler_does_not_inject_wandb_env_when_disabled`
- `test_scheduler_wandb_missing_api_key_behaviour`
- enabled=true 但缺少 env 时的行为:
- 方案 A推荐**自动降级**为 console不注入 wandb并在 task/attempt message 记录 warning
- 或方案 Bfail-fast返回 500/400
- 需在实现前确认采用哪种策略;建议 v3.6 选 A 提升可用性。
**验收**
- 任意内置任务提交后,在 Ray job runtime_env 中能看到 `WANDB_*`
---
### M3内置训练任务自动开启 wandb loggerPPO/GRPO/SFT
**目标**
- 当 W&B enabled 时,平台默认把内置训练任务改成 `trainer.logger=["console","wandb"]`,并设置 project/run 命名。
**开发任务**
- 在 job 构建PPO/GRPO/SFT 的 overrides 生成处):
- 将 `trainer.logger``console` 改为 list`[console,wandb]`hydra 语法按现有实现方式拼接)。
- `trainer.project_name=<user_id>_project`
- `trainer.experiment_name=<ray_submission_id>`
- 保持 v3.5 的 job_dir/checkpoint/log dir 注入方式不变。
**测试(先写)**
- `test_job_overrides_include_wandb_logger_when_enabled`
- 断言 entrypoint/overrides 包含 `trainer.logger=[console,wandb]`(或等价写法)。
- 断言包含 `trainer.project_name=<user>_project``trainer.experiment_name=<submission_id>`
- `test_job_overrides_keep_console_only_when_wandb_disabled_or_missing_key`
**验收**
- 训练任务 run 会自动出现在 W&B 对应 project 下E2E 验证在 M6
---
### M4API 输出与 WebUI 链接(最小闭环)
**目标**
- 用户可以在 UI 里“知道去哪看 W&B”以及知道本 task 对应哪个 project/run。
**开发任务**
- API
- `GET /api/v2/me` 增加 `wandb` 信息(仅当 enabled 时返回):
- `base_url`
- `project_name``<user_id>_project`
- `GET /api/v2/tasks/{task_id}`(或 attempt 结构)增加 `wandb_project` / `wandb_run_name`run_name=ray_submission_id
- WebUI
- Login/Data 页增加 “Open W&B” 链接(跳 `base_url`),展示 project_name + Copy。
- Task detail 增加 wandb 字段展示project/run/可点击链接或可复制文本)。
**测试(先写)**
- `test_me_includes_wandb_when_enabled`mock config + env
- `test_task_detail_contains_wandb_fields_when_enabled`mock task/attempt
- `test_ui_contains_wandb_link`(渲染 HTML 断言包含 base_url/project_name 字样)。
**验收**
- WebUI 能一键跳转 W&BTask detail 能定位到 run。
---
### M5New Task 增加 Evaluation 模板Advanced
**目标**
- New Task 页面增加一个 Evaluation 模板按钮/示例(先按 Advanced TaskSpec 提供)。
**开发任务**
- 在 `src/mvp/py/argus/service/ui.py`
- YAML 模式增加 “Evaluation example”。
- 表单模式本轮可选(不要求):
- 如果要支持:把 evaluation 作为 advanced 模板的一种预填 command`kind: advanced`)。
- 模板建议使用 verL 自带入口:
- `python3 -m verl.trainer.main_eval ... +ray_kwargs.ray_init.address=auto`
- `data.path=$HOME/common/datasets/...`(按 v3.5 的宏规则)
**测试(先写)**
- `test_ui_new_task_contains_evaluation_example`:断言页面包含 `main_eval` 与关键字段。
**验收**
- 用户复制 evaluation 模板可提交成功并在 Ray 上运行E2E 在 M6
---
### M6端到端dev/h1部署与验收流程
> 里程碑 M6 以脚本/手工步骤为主;不强制写 e2e 自动化测试。
**部署任务**
- compose 增加 `wandb` 服务:
- `wandb/local:latest`
- host 端口 `8090:8080`
- 数据挂载到 `/private/common/wandb:/vol` 持久化W&B 元数据/账号/API key/历史 runs
- API 启动方式:
- 在宿主机 export`WANDB_API_KEY=<从 W&B UI 生成的 key>`
- 启动 API确保 env 透传到容器内)
- 首次初始化:
- 打开 `http://<h1_ip>:8090/system-admin` 粘贴 license管理员操作
**验收用例**
1) **训练类任务自动打点**
- 用 alice 提交一个 SFT或 PPO任务内置 workload
- 在 W&B UI 中看到 project`alice_project`
- run name 为 `ray_submission_id`metrics 可见
2) **Advanced task 可手动打点**
- 提交一个 Advanced用户自己写 command并在 command 中启用 `trainer.logger=["console","wandb"]`(如需)
- 确认 env 注入生效W&B 记录出现)
3) **Evaluation 模板**
- 用 New Task 的 Evaluation example 提交
- 任务成功运行并输出 metricsstdout/logs
- (可选)如果 evaluation 也启用 wandb则能出现在对应 project 下
**回归**
- v3.5 的三类任务ppo/grpo/sft在 W&B disabled 或缺少 key 时仍可跑通(至少 console 输出不受影响)。
**Retention 联动检查**
- 提交一个短任务生成 `WANDB_DIR`,结束后确认:
- `WANDB_DIR` 位于 `/private/users/<user>/jobs/<ray_submission_id>/wandb`
- janitor 运行后该目录会随 job 一起进入 trash / 被 purge与 jobs retention 一致)
---
## 交付物清单v3.6
- 文档:
- `specs/mvp/v3.6/v3.6_design.md`(已存在,必要时补充操作流程)
- `specs/mvp/v3.6/v3.6_dev_plan.md`(本文)
- 代码(预期变更点):
- `src/mvp/py/argus/service/config.py`
- `src/mvp/py/argus/service/scheduler.py` / `src/mvp/py/argus/ray/builders.py` / `src/mvp/py/argus/ray/ray_job_tool.py`
- `src/mvp/py/argus/service/app.py`/me 与 task detail 输出)
- `src/mvp/py/argus/service/ui.py`Open W&B + Evaluation template
- `src/mvp/docker-compose.yaml`wandb service
- `src/mvp/configs/dev.yaml`tracking.wandb 配置)
- `src/mvp/scripts/*`API 启动时 env 透传,必要时补充)

View File

@ -1,42 +0,0 @@
# MVP v3.6 进度记录
> 基线v3.5 已完成Advanced TaskSpec + Custom reward方式A+ WebUI + SFTPGo + stateless ray node pool
> 本文件用于记录 v3.6 每个 milestone 的完成情况与关键改动点。
## M1完成
- 新增 `tracking.wandb` 配置解析与校验enabled/base_url/api_key_env
## M2完成
- Ray job 维度注入 `WANDB_*` env`WANDB_BASE_URL/WANDB_API_KEY/WANDB_MODE/WANDB_DIR`),缺少 key 时降级并记录 warning。
## M3完成
- PPO/GRPO/SFT 内置训练任务在 wandb 可用时自动追加 overrides
- `trainer.logger=[console,wandb]`
- `trainer.project_name=<user_id>_project`
- `trainer.experiment_name=<ray_submission_id>`
## M4完成
- API 输出增加 W&B 定位信息:
- `/api/v2/me` 返回 `wandb.{enabled,base_url,project_name}`
- `/api/v2/tasks/{task_id}``latest_attempt.wandb` 返回 `{base_url,project_name,run_name}`
- WebUI
- Login 页面增加 W&B 区块(跳转 8090、copy project
- Task detail 页面增加 W&B 区块copy run
## M5完成
- WebUI New Task 增加 Evaluation 模板Advanced
- 使用 `python3 -m verl.trainer.main_eval ... +ray_kwargs.ray_init.address=auto`
- 以占位符路径示例(用户替换 `<RAY_SUBMISSION_ID>/<EVAL_PARQUET>`
## M6完成
- `docker-compose.yaml` 集成 W&B local server
- host 端口 `8090`
- 持久化目录 `/private/common/wandb`(容器内 `/vol`
- dev 配置新增 `tracking.wandb` 默认开启(缺 key 自动降级并记录 warning
- API 启动脚本支持把 `WANDB_API_KEY` 从宿主机透传到 head 容器中的 API server。

View File

@ -1,126 +0,0 @@
# MVP v3.6 迭代研发总结(基于 v3.5
> 时间基线2026-01H20 dev 环境:`argus@h1:/home2/argus/infra/mvp`
> v3.6 架构草图:`specs/mvp/v3.6/Snipaste_2026-01-05_10-56-34.png`
## 1. 迭代目标回顾
v3.6 在 v3.5WebUI + API server + Ray stateless node pool + SFTPGo + Advanced TaskSpec基础上新增两块能力
1) **Weights & BiasesW&Blocal server 集成**
- 训练任务PPO/GRPO/SFT默认可写入 W&B。
- 采用“共享 W&B 账号 + 按用户拆分 project`<user_id>_project`)”的隔离策略。
2) **New Task 增加 Evaluation 示例**
- New Task 页面新增一个最小可用的 evaluation 模板(以 Advanced command 方式运行 `verl.trainer.main_eval`)。
## 2. 交付内容(代码/配置/脚本)
### 2.1 部署形态docker compose
v3.6 在 `src/mvp/docker-compose.yaml` 新增 W&B 服务:
- 服务名:`wandb`(容器名:`argus-wandb`
- 宿主机端口:`8090:8080`
- 持久化:`../../shared/common/wandb:/vol`
- 同 network`argus-ray-net`(便于 Ray 容器内访问)
### 2.2 平台配置YAML
`src/mvp/configs/dev.yaml` 增加/启用 W&B 配置:
```yaml
tracking:
wandb:
enabled: true
base_url: "http://172.22.0.1:8090"
api_key_env: "WANDB_API_KEY"
project_suffix: "_project"
```
说明:
- `base_url` 采用 docker bridge 网关 + 宿主机映射端口的方式,规避容器内 DNS 偶发解析失败问题。
- 不在 config 中写明文 key统一通过启动 API server 时注入 `WANDB_API_KEY`
### 2.3 Ray Job runtime_env 注入(核心)
v3.6 在**每个 Ray job attempt**提交时注入两类环境变量:
1) **始终注入(无论是否启用 W&B**:便于 Advanced command 在不改模板的情况下能运行
- `MVP_TRAINER_LOGGER``console``[console,wandb]`
- `MVP_WANDB_PROJECT``<user_id>_project`(例如 `alice_project`
- `MVP_WANDB_RUN``<ray_submission_id>`(每次 attempt 唯一)
2) **当 W&B 有效启用时注入**
- `WANDB_BASE_URL`
- `WANDB_API_KEY`
- `WANDB_MODE=online`
- `WANDB_DIR=<job_dir>/wandb`(例如 `/private/users/alice/jobs/<ray_sid>/wandb`
降级策略:
- 当 `tracking.wandb.enabled=true` 但缺少 `WANDB_API_KEY` 时,平台会**降级为 console**(并在 attempt.message 中记录 warning避免训练失败。
### 2.4 WebUI 变更
1) **Login 页面**
- 增加 “Open W&B” 跳转(指向 `tracking.wandb.base_url`
2) **New Task 页面**
- 新增 **Evaluation example**
- Advanced example 更新为 v3.6
- `command: |` 内不再包含任何注释(避免 YAML/命令解析报错)
- W&B 参数不再让用户手填,改为引用平台注入的 `${MVP_*}` env
- `trainer.logger=${MVP_TRAINER_LOGGER}`
- `trainer.project_name=${MVP_WANDB_PROJECT}`
- `trainer.experiment_name=${MVP_WANDB_RUN}`
> 备注driver 日志里会打印 `MVP_DRIVER_EXEC: bash -lc '...'`,此处看到 `${MVP_*}` 仍是“未替换”的字符串是正常现象;变量替换发生在 `bash` 执行阶段,而不是打印 argv 阶段。
### 2.5 启动脚本
`src/mvp/scripts/60_start_api.sh` 支持将宿主机的 `WANDB_API_KEY` 透传进 head 容器内启动的 API server
- 宿主机设置:`export WANDB_API_KEY=...`
- 启动 API脚本会 `docker exec -e WANDB_API_KEY=...` 进入 head 容器启动 `python3 /workspace/mvp/py/server.py`
## 3. 用户侧操作流程v3.6
### 3.1 一次性初始化(只在首次启用/清空 /vol 时需要)
1) 打开 W&B UI`http://<h1机器IP>:8090`
2) 在 System Admin 页面粘贴 license 完成初始化
3) 生成并记录 `WANDB_API_KEY`local key
4) 以后启动 API server 时注入该 key`WANDB_API_KEY=...`
只要保留 `shared/common/wandb`(即 `/vol` 持久化目录),重建容器无需再次进入 8090 配置。
### 3.2 日常使用
1) WebUI 登录:`http://<h1机器IP>:8080/ui/login`(输入 user token
2) New Task 提交任务:`http://<h1机器IP>:8080/ui/tasks/new`
3) 到 Tasks 查看状态/日志:`/ui/tasks` 与 task detail
4) 打开 W&B`http://<h1机器IP>:8090`,在 `<user_id>_project` 下查看 runs/metrics
## 4. 验收结果(本迭代应达成)
1) PPO/GRPO/SFT 任一任务运行后:
- W&B local server 可见对应 project`alice_project`
- run name 与 `ray_submission_id` 对齐(便于追踪每次 attempt
2) Evaluation 示例:
- 可作为 Advanced 任务提交并在 Ray 上执行 `verl.trainer.main_eval`
- 支持用户在 command 内自行加入 reward overrides平台不做封装
## 5. 已知限制与后续建议
1) **W&B 初始化自动化**
- 当前:首次仍需在 8090 页面粘贴 license、生成 key更稳、侵入最小
- 若需要“从零部署也完全免页面操作”,可进一步调研 W&B local 的可用管理 API/启动参数(自动注入 license + 自动创建 key
2) **Advanced command 的自由度**
- 平台只负责:
- `$HOME` 宏替换
- runtime_env env_vars 注入
- 任务队列与 Ray job 提交
- command 的语义正确性仍由用户负责(例如 PPO 必需的 micro batch 等参数)。

View File

@ -1,70 +0,0 @@
# License
License:
eyJhbGciOiJSUzI1NiIsImtpZCI6InUzaHgyQjQyQWhEUXM1M0xQY09yNnZhaTdoSlduYnF1bTRZTlZWd1VwSWM9In0.eyJjb25jdXJyZW50QWdlbnRzIjoxLCJ0cmlhbCI6ZmFsc2UsIm1heFN0b3JhZ2VHYiI6MTAsIm1heFRlYW1zIjowLCJtYXhVc2VycyI6MSwibWF4Vmlld09ubHlVc2VycyI6MCwibWF4UmVnaXN0ZXJlZE1vZGVscyI6MiwiZXhwaXJlc0F0IjoiMjAyNy0wMS0wNVQwMjoxMjo1MC4zMjRaIiwiZGVwbG95bWVudElkIjoiYzNmN2Y5N2ItMzAxOS00Nzk2LTkxYTgtZDUyMjc1NDBiMTI1IiwiZmxhZ3MiOltdLCJjb250cmFjdFN0YXJ0RGF0ZSI6IjIwMjYtMDEtMDVUMDI6MTI6NTAuMzI0WiIsImFjY2Vzc0tleSI6IjYxMGM5NjliLTk4ZWEtNGRhNS1iYzU1LWM2MzVlZWNhNzc0OCIsInNlYXRzIjoxLCJ2aWV3T25seVNlYXRzIjowLCJ0ZWFtcyI6MCwicmVnaXN0ZXJlZE1vZGVscyI6Miwic3RvcmFnZUdpZ3MiOjEwLCJleHAiOjE3OTkxMTUxNzAsIndlYXZlTGltaXRzIjp7IndlYXZlTGltaXRCeXRlcyI6bnVsbCwid2VhdmVPdmVyYWdlQ29zdENlbnRzIjowLCJ3ZWF2ZU92ZXJhZ2VVbml0IjoiTUIifX0.VADnc0PExWhGDAxMIbu0vlmPN423B398of4HFl6BMJ1vqGA9H1ESElOZfk0VQ0YnYgwZc_CZF9k0HRyfCBgRhtRKyB1PpGnaKT_kKNVQryykWRpNhnpDqhmTa-wfTUBXNxhu1ktNPKBFNaEbaYuPsLN_aXPGW0dDwp6coGnGEXEqdRmuvekE6ytu7t6IA6flYs35WqCojvvjAmfBdovo2zPTfmlqKeaz7GPrApMo9JBpmb1a6bZEjCoRhhUx_k-v2rbvE3hd9ix9_UMZ6siJ5IKtNuXy_cprcCXXIFVUMcfTnt78RRXY0jCRMQqWkNq9ZGF0Mgcjsh3ts9xSxPgWnw
# License 限制
Add License to your Local Instance
Create up to 0 teams
Create up to 1 users
Store up to 10 GB of data
Create up to 2 Registered Models
Quickstart
On a machine with Docker and Python installed, run:
1 pip install wandb --upgrade
2 wandb server start
Generate a free license from the Deployer.
Add it to your W&B Server's localhost's settings.
Paste the license in the /system-admin page on your localhost
# docker 部署
deployment:
version: "3.8"
services:
wandb:
image: wandb/local:latest
container_name: wandb-local
ports:
- "8080:8080"
volumes:
- wandb_data:/vol
restart: unless-stopped
volumes:
wandb_data:
# 连接方式:
方式 B环境变量适合容器/批处理/CI
通过 ray job的runtime_env来设置环境变量
export WANDB_BASE_URL=http://<服务器IP或域名>:8080
export WANDB_API_KEY=<你的API_KEY>
官方文档说明可以用 WANDB_BASE_URL + WANDB_API_KEY 代替 wandb login --host ..
# verl配置
在 verl 里打开 wandb你只需要配 trainer
verl 的配置里最关键是这三个字段trainer.logger、trainer.project_name、trainer.experiment_name。文档里也写了 logger 用于 console + trackingtracking 会初始化 wandb
veRL Documentation
+1
推荐写法(新版本):
trainer:
logger: ["console", "wandb"]
project_name: my_project # 用argus的用户名_project ,譬如 alice_project
experiment_name: exp_001 # 用 task id 作为实验名

View File

@ -1,215 +0,0 @@
# MVP v3.7 设计方案:切换 `verlai/verl:vllm011.latest` + 默认 rollout=vllm
## 0. 背景与目标
当前 dev/h1 环境的 Ray 节点镜像基于 `verlai/verl:sgl055.latest`,并且平台内置 PPO/GRPO 的默认参数中写死了:
- `actor_rollout_ref.rollout.name=sglang`
v3.7 的目标是:
1. **Ray 节点镜像切换到 vLLM 版本**
- 基础镜像改为 `verlai/verl:vllm011.latest`
- 构建并打标:`argus/argus-ray-node:vllm011.latest`
- 构建在远端 `argus@h1` 上完成(本地没有 verlai 基础镜像)
2. **端到端跑通 v3.0 API 流程**
- 通过 `src/mvp/scripts/run_all_v30_api.sh` 完整 E2E
3. **内置训练任务默认使用 vLLM rollout**
- 提交 VERL 训练任务时将 `actor_rollout_ref.rollout.name``sglang` 改为 `vllm`
> 备注:本迭代是“替换默认 backend”而非“新增能力”尽量保持对 v3.6 功能兼容W&B、SFTPGo、Advanced TaskSpec、stateless pool 等不改协议)。
---
## 1. 现状梳理(源码定位)
### 1.1 Ray 节点镜像与 compose
- Dockerfile`src/mvp/images/argus-ray-node/Dockerfile`
- 当前 `ARG BASE_IMAGE=verlai/verl:sgl055.latest`
- Compose`src/mvp/docker-compose.yaml`
- `ray_head.build.args.BASE_IMAGE: verlai/verl:sgl055.latest`
- `ray_head.image / worker.image: argus/argus-ray-node:v2.5`
### 1.2 默认 rollout.name=sglang 的位置
平台内置 PPO/GRPO 参数由 Ray job 入口构建器生成:
- `src/mvp/py/argus/ray/builders.py`
- `build_training_argv()` 中写死了:
- `actor_rollout_ref.rollout.name=sglang`
WebUI 的 Advanced 示例也包含 rollout.name用于指导用户
- `src/mvp/py/argus/service/ui.py`
- Advanced example 中当前为 `actor_rollout_ref.rollout.name=sglang`(需要同步改成 vllm避免用户 copy/paste 走错)
### 1.3 `run_all_v30_api.sh` 依赖默认参数
`src/mvp/scripts/run_all_v30_api.sh` 提交 PPO/GRPO/SFT 的 TaskSpecYAML**不会显式携带 rollout.name**,因此是否能切到 vllm依赖平台默认值builders是否变更。
---
## 2. 方案设计
### 2.0 已确认决策(来自评审)
1) **compose 移除 build**:允许移除 `ray_head.build`,强制使用远端已构建镜像。
2) **全量切换 vllm**:不保留 sglang 作为可选项v3.7 默认全部切到 vllm
3) **backend 名称**:确认 VERL backend 名为 `vllm`(即 `actor_rollout_ref.rollout.name=vllm`)。
### 2.1 镜像策略vllm011
#### 2.1.1 Dockerfile 修改
目标:
- 默认基础镜像改为 `verlai/verl:vllm011.latest`
改动点:
- `src/mvp/images/argus-ray-node/Dockerfile`
- `ARG BASE_IMAGE=verlai/verl:vllm011.latest`
说明:
- 仍保留 `BASE_IMAGE` build arg便于未来热切换不同基础镜像而不是把镜像写死在 compose
#### 2.1.2 镜像 tag
构建产物镜像:
- `argus/argus-ray-node:vllm011.latest`
> 注意:该 tag 用于表达“运行时依赖的 vllm 版本线”,而不是 MVP 功能版本v3.7)。
#### 2.1.3 compose 复用新镜像(避免每次重建)
目标E2E 时尽量避免每次 `docker compose up` 都 build。
建议修改 `src/mvp/docker-compose.yaml`
- `ray_head.image: argus/argus-ray-node:vllm011.latest`
- `ray_worker_0.image: argus/argus-ray-node:vllm011.latest`
- `ray_worker_1.image: argus/argus-ray-node:vllm011.latest`
并采用:**移除 `ray_head.build`**(强制使用已构建镜像),避免每次 `docker compose up` 触发 build。
---
### 2.2 训练默认参数切换到 vllm
目标:平台内置 PPO/GRPO 的默认 rollout backend 从 sglang 切到 vllm。
改动点:
- `src/mvp/py/argus/ray/builders.py`
- 将 `actor_rollout_ref.rollout.name=sglang` 替换为 `actor_rollout_ref.rollout.name=vllm`
影响范围:
- PPO、GRPO两者都走 `verl.trainer.main_ppo`
- 对 SFT 不影响SFT 走 `verl.trainer.sft_trainer_ray`
兼容性评估:
- `run_all_v30_api.sh` 会受益:无需修改 TaskSpec即可自动切换。
- 若未来仍需支持 sglang可考虑在 v3.7 之后引入“配置驱动”的默认值(见 §2.4 可选增强)。
---
### 2.3 WebUI/模板同步(避免误导用户)
目标New Task 页面的 Advanced example 也应默认 vllm避免用户 copy 后手工改参数。
改动点:
- `src/mvp/py/argus/service/ui.py`
- Advanced example 中 `actor_rollout_ref.rollout.name=vllm`
> 注意:该模板仅用于 UX 指导;实际生效仍由用户提交的 command 决定。
---
### 2.4 可选增强(不强制,供评审)
为避免后续再硬编码切换,可引入“平台训练默认值”配置(可选):
- 在 `configs/dev.yaml` 增加:
```yaml
verl_defaults:
rollout_backend: "vllm" # 或 "sglang"
```
- `builders.py` 从配置读取默认值,而非写死。
本次 v3.7 的最低交付可以先不做该增强,只做硬替换;若你希望后续支持 A/B 切换,再纳入。
---
## 3. 远端部署/迁移步骤argus@h1
> 本节是“计划步骤”,评审通过后再执行。
### 3.1 同步代码到远端目录
远端目录约定:
- `argus@h1:/home2/argus/infra/mvp/src/mvp`compose 与 scripts
将本地变更 rsync 到远端后再进行构建/拉起。
### 3.2 在远端构建镜像(只在 h1
`argus@h1` 执行(示例命令):
```bash
cd /home2/argus/infra/mvp/src/mvp
docker build \
-f images/argus-ray-node/Dockerfile \
--build-arg BASE_IMAGE=verlai/verl:vllm011.latest \
-t argus/argus-ray-node:vllm011.latest \
.
```
### 3.3 清理旧环境并用新镜像拉起
```bash
cd /home2/argus/infra/mvp/src/mvp
docker compose down
docker compose up -d
```
验证:
- `docker ps``argus-ray-head/worker` 的 image 为 `argus/argus-ray-node:vllm011.latest`
- Ray dashboard 可访问:`http://<h1IP>:8265`
### 3.4 E2E`run_all_v30_api.sh`
```bash
cd /home2/argus/infra/mvp/src/mvp
MVP_INTERNAL_TOKEN=my-dev-token \
WANDB_API_KEY=... \
./scripts/run_all_v30_api.sh
```
验收关键点:
- PPO/GRPO/SFT 全部成功(或至少 PPO/GRPO 不卡在 rollout backend 初始化阶段)
- 任一 PPO/GRPO 的 driver logs / hydra overrides 中能看到:
- `actor_rollout_ref.rollout.name=vllm`
---
## 4. 风险与排查要点
### 4.1 vLLM backend 在 VERL 的参数兼容性
平台默认传入的这些参数当前是为 sglang 写的:
- `actor_rollout_ref.rollout.tensor_model_parallel_size=1`
- `actor_rollout_ref.rollout.gpu_memory_utilization=0.4`
vLLM rollout 是否接受/需要额外参数(例如 tokenizer、engine 配置),需要在 E2E 中观察:
- 如果 vLLM rollout 初始化报错,可能需要补充 vllm 特定 overrides属于 v3.7 的后续修复项)。
### 4.2 镜像依赖差异
更换 base image 可能带来:
- Python/Ray/依赖版本差异
- CUDA/NCCL 依赖差异
建议:
- 在 v3.7 评审通过后,优先跑最小 PPOepochs=1、steps=10验证 vllm backend 能启动并完成。
---
## 5. 待确认问题(请你评审时确认)
已完成评审确认(见 §2.0),无额外待确认项。

View File

@ -1,122 +0,0 @@
# MVP v3.7 开发计划TDD
> 目标:切换 Ray 节点基础镜像到 `verlai/verl:vllm011.latest`,并将平台内置 PPO/GRPO 默认 rollout backend 全量切到 `vllm`,最后在远端 `argus@h1` 通过 `run_all_v30_api.sh` 跑通端到端。
## M0 - 基线确认(不改行为)
**目的**:确认当前 v3.6 baseline 可跑(避免把历史问题混入 v3.7)。
- [ ] 本地单测全绿:`.venv/bin/python -m pytest`
- [ ] 远端 h1 当前环境可跑(可选):`./scripts/run_all_v30_api.sh`(或至少能启动 Ray+API
**验收**
- 单测通过coverage ≥ 90%(现有门槛)
---
## M1 - 训练默认参数切换到 vllmTDD
**目的**:在不碰镜像/compose 的前提下,先把“默认 rollout=sglang”替换为 vllm并用单测锁定行为。
### 1.1 新增/更新单测(先写测试)
- [ ] `src/mvp/py/tests/test_builders.py`
- 新增断言PPO/GRPO 的 argv 中包含 `actor_rollout_ref.rollout.name=vllm`
- 且不再包含 `actor_rollout_ref.rollout.name=sglang`
- [ ] `src/mvp/py/tests/test_ui.py`
- New Task Advanced example 模板包含 `actor_rollout_ref.rollout.name=vllm`(避免用户 copy/paste 走错默认)
> 这两条测试先写出来预期先失败red
### 1.2 实现改动(让测试变绿)
- [ ] `src/mvp/py/argus/ray/builders.py`
- 将 `actor_rollout_ref.rollout.name=sglang` 改为 `...=vllm`
- [ ] `src/mvp/py/argus/service/ui.py`
- Advanced example 中同样改为 `...=vllm`
### 1.3 回归测试
- [ ] `.venv/bin/python -m pytest`
**验收**
- 单测全绿coverage ≥ 90%
- 平台内置 PPO/GRPO 构建出的 command/overrides 默认 rollout backend 为 vllm
---
## M2 - 镜像与 compose 切换(远端构建为主)
**目的**:完成镜像切换与环境拉起,确保 Ray stateless pool 正常工作。
### 2.1 Dockerfile 默认 base image 切换
- [ ] `src/mvp/images/argus-ray-node/Dockerfile`
- `ARG BASE_IMAGE=verlai/verl:vllm011.latest`
### 2.2 docker-compose 强制使用新镜像(移除 build
- [ ] `src/mvp/docker-compose.yaml`
- 移除 `ray_head.build` 段(强制走 `image:`
- `ray_head.image / ray_worker_0.image / ray_worker_1.image` 统一改为:
- `argus/argus-ray-node:vllm011.latest`
### 2.3 远端构建镜像h1
`argus@h1:/home2/argus/infra/mvp/src/mvp`
- [ ] `docker build -f images/argus-ray-node/Dockerfile -t argus/argus-ray-node:vllm011.latest .`
### 2.4 清理旧 compose 并拉起
- [ ] `docker compose down`
- [ ] `docker compose up -d`
- [ ] 验证:
- `docker ps` 看到 `argus-ray-head/worker` 正常运行
- Ray dashboard`http://<h1IP>:8265` 可访问,节点数 1 head + 2 worker
**验收**
- h1 环境成功使用新镜像拉起 Ray 集群head 无 GPU、worker 各 4 GPU 的配置仍保持)
---
## M3 - 端到端验证run_all_v30_api.sh
**目的**:验证在新镜像 + 默认 vllm rollout 下API 提交的训练任务能跑通闭环。
### 3.1 同步代码到远端
- [ ] rsync `src/mvp``argus@h1:/home2/argus/infra/mvp/src/mvp`
### 3.2 执行 E2E
在 h1
- [ ] `./scripts/run_all_v30_api.sh`(确保环境变量按脚本要求设置:`MVP_INTERNAL_TOKEN`、可选 `WANDB_API_KEY` 等)
### 3.3 核心检查点
- [ ] PPO/GRPO/SFT 任务整体流程可执行(至少 PPO/GRPO 不因 rollout backend 初始化失败)
- [ ] 任一 PPO/GRPO 的 Ray job logs / submit payload / hydra overrides 中可确认:
- `actor_rollout_ref.rollout.name=vllm`
**验收**
- `run_all_v30_api.sh` 端到端成功(或若 PPO/GRPO 因 vllm 参数差异失败,需在本 milestone 内补齐必要 overrides 并重新跑通)
---
## 风险与回滚策略
### 风险
- vLLM rollout 可能对部分参数(如 batch/并发/显存利用率)有不同约束,导致训练启动失败。
- base image 切换导致 ray/依赖版本差异。
### 回滚
回滚到 v3.6 / sglang 的最小动作:
- `docker-compose.yaml` 恢复旧镜像 tag
- `builders.py` 恢复 rollout.name=sglang

View File

@ -1,121 +0,0 @@
# MVP v3.7 迭代总结:切换 vLLM rollout + `verlai/verl:vllm011.latest`
> 基线版本v3.6W&B + SFTPGo + WebUI/API + Ray stateless pool + Advanced TaskSpec
> 验证环境:`argus@h1:/home2/argus/infra/mvp`
## 1. 目标与结果
### 1.1 本次目标
1) Ray 节点镜像切换到 vLLM 版本:
- base image`verlai/verl:vllm011.latest`
- 构建镜像 tag`argus/argus-ray-node:vllm011.latest`
2) 平台内置 PPO/GRPO 默认 rollout backend 全量切换:
- `actor_rollout_ref.rollout.name=sglang``actor_rollout_ref.rollout.name=vllm`
3) 端到端验证:
- 使用 `src/mvp/scripts/run_all_v30_api.sh` 在 h1 上跑通 E2E通过 API 提交 PPO/GRPO/SFT
### 1.2 实际结果(验收)
- h1 上已成功构建并使用新镜像拉起head + 2 worker
- `docker ps` 显示 `argus-ray-head/worker-*` 使用 `argus/argus-ray-node:vllm011.latest`
- `run_all_v30_api.sh` 端到端跑通:
- PPO/GRPO/SFT 任务均 `SUCCEEDED`
- 在 job submit payload 中验证关键点:
- `actor_rollout_ref.rollout.name=vllm`
- `HF_HUB_OFFLINE=1`(见 §3.2
---
## 2. 代码与配置改动点
### 2.1 训练默认参数sglang → vllm
- `src/mvp/py/argus/ray/builders.py`
- 将 PPO/GRPO 默认参数中的 `actor_rollout_ref.rollout.name` 固定为 `vllm`
- `src/mvp/py/argus/service/ui.py`
- New Task → Advanced example 同步改为 `actor_rollout_ref.rollout.name=vllm`(避免用户 copy/paste 走错)
并用单测锁定行为TDD
- `src/mvp/py/tests/test_builders.py`
- `src/mvp/py/tests/test_ui.py`
### 2.2 镜像与 compose强制用预构建镜像
- `src/mvp/images/argus-ray-node/Dockerfile`
- 默认 `ARG BASE_IMAGE=verlai/verl:vllm011.latest`
- `src/mvp/docker-compose.yaml`
- 移除 `ray_head.build`(避免每次 `docker compose up` 触发 build
- head/worker 统一使用 `image: argus/argus-ray-node:vllm011.latest`
---
## 3. E2E 遇到的问题与修复
### 3.1 问题vLLM 初始化触发 HF mirror 429
在切换到 vLLM rollout 后PPO/GRPO 任务启动阶段出现:
- `huggingface_hub.errors.HfHubHTTPError: 429 Too Many Requests`
- 请求来源:`https://hf-mirror.com/api/models/<repo>/tree/main?...`
原因要点:
- 传入模型为 repo id`Qwen/Qwen2.5-0.5B-Instruct`vLLM 会调用 HF API 获取 repo tree/file list
- 多进程/多 replica 并发会瞬间放大请求,导致 mirror 限流;
- 即便本地 cache 已存在repo id 路径仍可能触发远端检查。
### 3.2 修复:禁用 HF Hub 联网 + 使用本地 snapshot path
1) 在 Ray job runtime_env 注入离线开关:
- `src/mvp/configs/dev.yaml`
- `src/mvp/configs/dev_v30.yaml`
新增:
```yaml
HF_HUB_OFFLINE: "1"
```
2) E2E 脚本提交任务时,`model_id` 改为本地 snapshot 目录,避免 repo id
- `src/mvp/scripts/run_all_v30_api.sh`
- 在 head 容器内用 `snapshot_download(..., local_files_only=True)` 解析本地路径
- 用该路径作为 `model_id:` 提交 PPO/GRPO/SFT
> 结果E2E 任务不再触发 HF mirror 429PPO/GRPO/SFT 全部跑通。
---
## 4. 远端部署/操作记录h1
### 4.1 构建镜像h1 上执行)
`argus@h1:/home2/argus/infra/mvp/src/mvp`
```bash
docker build -f images/argus-ray-node/Dockerfile \
--build-arg BASE_IMAGE=verlai/verl:vllm011.latest \
-t argus/argus-ray-node:vllm011.latest .
```
### 4.2 拉起环境compose
```bash
docker compose down
docker compose up -d
```
### 4.3 E2E
```bash
export MVP_INTERNAL_TOKEN=my-dev-token
export SFTPGO_ADMIN_PASSWORD=my-dev-sftpgo-admin
./scripts/run_all_v30_api.sh
```
---
## 5. 已知影响与注意事项
1) **vLLM rollout 更敏感于模型加载路径与联网行为**:建议默认离线(`HF_HUB_OFFLINE=1`)并优先使用本地 snapshot path。
2) **镜像切换可能带来依赖差异**:后续若遇到 rollout 相关参数兼容问题,应以 vLLM 的配置要求为准逐项调整(保持小步快跑)。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 KiB

View File

@ -1,314 +0,0 @@
API参考资料
https://docs.ray.io/en/latest/serve/api/doc/ray.serve.llm.LLMConfig.html
ray.serve.llm.LLMConfig
pydantic model ray.serve.llm.LLMConfig[source]
The configuration for starting an LLM deployment.
PublicAPI (alpha): This API is in alpha and may change before becoming stable.
field accelerator_type: str | None = None
The type of accelerator runs the model on. Only the following values are supported: [V100, P100, T4, P4, K80, A10G, L4, L40S, A100, H100, H200, H20, B200, Intel-GPU-Max-1550, Intel-GPU-Max-1100, Intel-GAUDI, AMD-Instinct-MI100, AMD-Instinct-MI250X, AMD-Instinct-MI250X-MI250, AMD-Instinct-MI210, AMD-Instinct-MI300A, AMD-Instinct-MI300X-OAM, AMD-Instinct-MI300X-HF, AMD-Instinct-MI308X, AMD-Instinct-MI325X-OAM, AMD-Instinct-MI350X-OAM, AMD-Instinct-MI355X-OAM, AMD-Radeon-R9-200-HD-7900, AMD-Radeon-HD-7900, aws-neuron-core, TPU-V2, TPU-V3, TPU-V4, TPU-V5P, TPU-V5LITEPOD, TPU-V6E, Ascend910B, Ascend910B4, MXC500, MXC550, A100-40G, A100-80G]
field callback_config: CallbackConfig [Optional]
Callback configuration to use for model initialization. Can be a string path to a class or a Callback subclass.
field deployment_config: Dict[str, Any] [Optional]
The Ray @server.deployment options. Supported fields are: name, num_replicas, ray_actor_options, max_ongoing_requests, autoscaling_config, max_queued_requests, user_config, health_check_period_s, health_check_timeout_s, graceful_shutdown_wait_loop_s, graceful_shutdown_timeout_s, logging_config, request_router_config. For more details, see the Ray Serve Documentation.
field engine_kwargs: Dict[str, Any] = {}
Additional keyword arguments for the engine. In case of vLLM, this will include all the configuration knobs they provide out of the box, except for tensor-parallelism which is set automatically from Ray Serve configs.
field experimental_configs: Dict[str, Any] [Optional]
Experimental configurations for Ray Serve LLM. This is a dictionary of key-value pairs. Current supported keys are: - stream_batching_interval_ms: Ray Serve LLM batches streaming requests together. This config decides how long to wait for the batch before processing the requests. Defaults to 50.0. - num_ingress_replicas: The number of replicas for the router. Ray Serve will take the max amount all the replicas. Default would be 2 router replicas per model replica.
field llm_engine: str = 'vLLM'
The LLMEngine that should be used to run the model. Only the following values are supported: [vLLM]
field log_engine_metrics: bool | None = True
Enable additional engine metrics via Ray Prometheus port.
field lora_config: Dict[str, Any] | LoraConfig | None = None
Settings for LoRA adapter. Validated against LoraConfig.
field model_loading_config: Dict[str, Any] | ModelLoadingConfig [Required]
The settings for how to download and expose the model. Validated against ModelLoadingConfig.
field placement_group_config: Dict[str, Any] | None = None
Ray placement group configuration for scheduling vLLM engine workers. Defines resource bundles and placement strategy for multi-node deployments. Should contain bundles (list of resource dicts) and optionally strategy (defaults to PACK). Example: {bundles: [{GPU: 1, CPU: 2}], strategy: PACK}
field runtime_env: Dict[str, Any] | None = None
The runtime_env to use for the model deployment replica and the engine workers.
apply_checkpoint_info(model_id_or_path: str, trust_remote_code: bool = False) → None[source]
Apply the checkpoint info to the model config.
classmethod from_file(path: str, **kwargs) → ModelT
Load a model from a YAML file path.
get_engine_config() → None | VLLMEngineConfig[source]
Returns the engine config for the given LLM config.
LLMConfig not only has engine config but also deployment config, etc.
get_or_create_callback() → CallbackBase | None[source]
Get or create the callback instance for this process.
This ensures one callback instance per process (singleton pattern). The instance is cached so the same object is used across all hooks.
Returns
:
Instance of class that implements Callback
multiplex_config() → ServeMultiplexConfig[source]
classmethod parse_yaml(file, **kwargs) → ModelT
setup_engine_backend()[source]
update_engine_kwargs(**kwargs: Any) → None[source]
Update the engine_kwargs and the engine_config engine_kwargs.
This is typically called during engine starts, when certain engine_kwargs (e.g., data_parallel_rank) become available.
validator validate_accelerator_type » accelerator_type[source]
validator validate_deployment_config » deployment_config[source]
Validates the deployment config dictionary.
validator validate_experimental_configs » experimental_configs[source]
Validates the experimental configs dictionary.
validator validate_llm_engine » llm_engine[source]
Validates the llm_engine string value.
validator validate_lora_config » lora_config[source]
Validates the lora config dictionary.
validator validate_model_loading_config » model_loading_config[source]
Validates the model loading config dictionary.
property input_modality: str
Returns the input modality of the model. There could be more types in the future. Right now assumes if the model doesnt support version, itll be text.
property max_request_context_length: int | None
property model_architecture: str
property model_id: str
property supports_vision: bool
# Python API
ray serve api
https://docs.ray.io/en/latest/serve/api/index.html#serve-api
Python API
Writing Applications
serve.Deployment
Class (or function) decorated with the @serve.deployment decorator.
serve.Application
One or more deployments bound with arguments that can be deployed together.
Deployment Decorators
serve.deployment
Decorator that converts a Python class to a Deployment.
serve.ingress
Wrap a deployment class with an ASGI application for HTTP request parsing.
serve.batch
Converts a function to asynchronously handle batches.
serve.multiplexed
Wrap a callable or method used to load multiplexed models in a replica.
Deployment Handles
Note
The deprecated RayServeHandle and RayServeSyncHandle APIs have been fully removed as of Ray 2.10. See the model composition guide for how to update code to use the DeploymentHandle API instead.
serve.handle.DeploymentHandle
A handle used to make requests to a deployment at runtime.
serve.handle.DeploymentResponse
A future-like object wrapping the result of a unary deployment handle call.
serve.handle.DeploymentResponseGenerator
A future-like object wrapping the result of a streaming deployment handle call.
Running Applications
serve.start
Start Serve on the cluster.
serve.run
Run an application and return a handle to its ingress deployment.
serve.delete
Delete an application by its name.
serve.status
Get the status of Serve on the cluster.
serve.shutdown
Completely shut down Serve on the cluster.
serve.shutdown_async
Completely shut down Serve on the cluster asynchronously.
Configurations
serve.config.ProxyLocation
Config for where to run proxies to receive ingress traffic to the cluster.
serve.config.gRPCOptions
gRPC options for the proxies.
serve.config.HTTPOptions
HTTP options for the proxies.
serve.config.AutoscalingConfig
Config for the Serve Autoscaler.
serve.config.AutoscalingPolicy
PublicAPI (alpha): This API is in alpha and may change before becoming stable.
serve.config.AutoscalingContext
Rich context provided to custom autoscaling policies.
serve.config.AggregationFunction
An enumeration.
serve.config.RequestRouterConfig
Config for the Serve request router.
Schemas
serve.schema.ServeActorDetails
Detailed info about a Ray Serve actor.
serve.schema.ProxyDetails
Detailed info about a Ray Serve ProxyActor.
serve.schema.ApplicationStatusOverview
Describes the status of an application and all its deployments.
serve.schema.ServeStatus
Describes the status of Serve.
serve.schema.DeploymentStatusOverview
Describes the status of a deployment.
serve.schema.EncodingType
Encoding type for the serve logs.
serve.schema.AutoscalingMetricsHealth
An enumeration.
serve.schema.AutoscalingStatus
An enumeration.
serve.schema.ScalingDecision
One autoscaling decision with minimal provenance.
serve.schema.DeploymentAutoscalingDetail
Deployment-level autoscaler observability.
serve.schema.ReplicaRank
Replica rank model.
Request Router
serve.request_router.ReplicaID
A unique identifier for a replica.
serve.request_router.PendingRequest
A request that is pending execution by a replica.
serve.request_router.RunningReplica
Contains info on a running replica.
serve.request_router.FIFOMixin
Mixin for FIFO routing.
serve.request_router.LocalityMixin
Mixin for locality routing.
serve.request_router.MultiplexMixin
Mixin for multiplex routing.
serve.request_router.RequestRouter
Abstract interface for a request router (how the router calls it).
Advanced APIs
serve.get_replica_context
Returns the deployment and replica tag from within a replica at runtime.
serve.context.ReplicaContext
Stores runtime context info for replicas.
serve.get_multiplexed_model_id
Get the multiplexed model ID for the current request.
serve.get_app_handle
Get a handle to the application's ingress deployment by name.
serve.get_deployment_handle
Get a handle to a deployment by name.
serve.grpc_util.RayServegRPCContext
Context manager to set and get gRPC context.
serve.exceptions.BackPressureError
Raised when max_queued_requests is exceeded on a DeploymentHandle.
serve.exceptions.RayServeException
serve.exceptions.RequestCancelledError
Raise when a Serve request is cancelled.
serve.exceptions.DeploymentUnavailableError
Raised when a Serve deployment is unavailable to receive requests.

View File

@ -1,87 +0,0 @@
基于提供的来源,以下是使用 **Builder Pattern构建器模式** 结合 Ray Serve 和 vllm 动态部署**中型大语言模型Medium-sized LLM**的原理与操作方案。
### 一、 核心原理
1. **中型 LLM 定义**:中型模型(如 Llama-3.1-70B通常具有约 70B 参数。它们通常运行在**单个节点**上,利用 **4 到 8 个 GPU**
2. **Builder Pattern 机制**:该模式通过 `build_openai_app` 函数提供高度抽象。开发者只需定义一个 `LLMConfig` 对象,即可自动构建并链接底层的 `LLMServer``OpenAiIngress` 组件。
3. **高性能后端 (vLLM)**Ray Serve LLM 使用 vLLM 作为推理引擎,支持高性能推理和显存管理。
4. **动态扩缩容与资源调度**
* **张量并行 (Tensor Parallelism)**:通过 `tensor_parallel_size` 将模型权重均匀分布在单节点的所有 GPU 上。
* **副本缩放 (Autoscaling)**:通过 `autoscaling_config` 动态调整 `min_replicas``max_replicas`,使服务能根据实时流量增减推理副本。
---
### 二、 操作方案
#### 1. 环境准备
确保已安装必要的依赖包并配置 Hugging Face 访问令牌(针对 Llama-3.1 等受限模型)。
```bash
pip install "ray[serve,llm]"
export HF_TOKEN=<YOUR_HUGGINGFACE_TOKEN>
```
#### 2. 编写部署脚本 (`serve_medium_llm.py`)
使用 **Builder Pattern** 定义配置并构建应用。以下示例配置了一个典型的 70B 模型部署:
```python
# serve_medium_llm.py
from ray.serve.llm import LLMConfig, build_openai_app
import os
llm_config = LLMConfig(
model_loading_config=dict(
model_id="my-llama-3.1-70b",
model_source="meta-llama/Llama-3.1-70B-Instruct",
),
accelerator_type="A100-40G", # 或 L40S
deployment_config=dict(
autoscaling_config=dict(
min_replicas=1, # 最小副本数
max_replicas=4, # 最大副本数,实现动态扩展
)
),
runtime_env=dict(env_vars={"HF_TOKEN": os.environ.get("HF_TOKEN")}),
engine_kwargs=dict(
max_model_len=32768, # 上下文长度
tensor_parallel_size=8, # 在单节点的 8 个 GPU 间拆分权重
),
)
# 使用 Builder Pattern 构建应用
app = build_openai_app({"llm_configs": [llm_config]})
```
#### 3. 启动部署
在终端运行以下命令启动服务:
```bash
serve run serve_medium_llm:app
```
部署过程通常需要几分钟,包括配置集群、启动 vLLM 服务器以及下载模型权重。
#### 4. 发送请求测试
服务启动后,可以通过符合 OpenAI 标准的接口进行访问。
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="FAKE_KEY")
response = client.chat.completions.create(
model="my-llama-3.1-70b",
messages=[{"role": "user", "content": "解释一下什么是量子纠缠?"}],
stream=True
)
for chunk in response:
if chunk.choices.delta.content:
print(chunk.choices.delta.content, end="", flush=True)
```
---
### 三、 性能与并发优化建议
* **提高并发量**:可以通过降低 `max_model_len` 来减少 KV 缓存所需的显存,从而显著提升每个副本支持的最大并发请求数。
* **监控指标**:通过 Ray Serve LLM 仪表盘监控 **TTFT首字延迟**、**TPOT单字延迟** 和 **Token 吞吐量** 来评估服务性能。
* **精度折衷**:对于资源受限的场景,可以使用**量化模型**(如 FP8来减少模型内存占用为 KV 缓存留出更多空间,进而提高并发能力。
**比喻理解**
部署**中型 LLM** 就像是在一个大型车间里组装一台复杂的精密机器(模型权重)。**Builder Pattern** 是你的“全自动组装线”你只需设定好机器的参数Config生产线就会自动帮你把零件固定好并接通电源。而 **vLLM 和张量并行** 就像是让 8 个熟练工人GPU共同抬起这台沉重的机器每个人只负责自己那一部分的力气从而让机器能够平稳地运转。

View File

@ -1,8 +0,0 @@
1. 通过ray serve后端vllm来动态拉起llm支持多模型application部署
2. 默认一个模型只有一个replica用户配置可以多个
3. 用户可以删除(下线)模型
4. 可以指定模型用几张卡
5. 通过WebUI来进行配置查看当前部署的模型列表以及可以查看详情
6. 模型路径可以使用common也可以用户自己指定user路径
7.

View File

@ -1,224 +0,0 @@
# MVP v3.8 API ReferenceServing
> 说明:本节为 v3.8 新增的 **Model Serving** APIRay Serve LLM / vLLM
> 认证Serving 管理 API 复用现有 MVP API 的认证方式(`Authorization: Bearer <user_token>`)。
> 推理:对外 OpenAI endpoint **不做鉴权**v3.8 约定)。
## 0. 基本信息
### 0.1 Base URLs
- MVP API server`http://<host>:8080`
- Ray Serve OpenAI ingress固定端口 8000`http://<host>:8000/v1`
### 0.2 认证
所有 `/api/v2/serve/*` 接口要求:
```
Authorization: Bearer <user_token>
```
其中 `user_token` 由管理员通过 `/api/v2/users/<user_id>/tokens` 颁发(沿用现有机制)。
### 0.3 命名规则:`model_id = user_id-YYYYMMDDHHMM-<suffix>`
- 用户提交时填写 `model_id`(语义为 suffix例如 `qwen-0.5b`
- 平台生成前缀:
- `prefix = "<user_id>-<YYYYMMDDHHMM>"`
- 平台实际对外暴露的 OpenAI model 名称为:
- `model_id = "<prefix>-<suffix>"`
- 示例:`alice-202601061235-qwen-0.5b`
## 1. 数据结构
### 1.1 ServingSpecYAML
请求体建议使用 YAML与 TaskSpec 一致),示例:
```yaml
model_id: qwen-0.5b # 必填suffix平台自动加 user_id- 前缀)
model_source: $HOME/common/hf/.../<sha> # 必填:本地路径或 repo id平台做 $HOME 宏替换与路径校验
num_replicas: 1 # 可选,默认 1
gpus_per_replica: 1 # 可选,默认 1
# engine_kwargs: # 可选vLLM 参数透传(白名单/黑名单由实现决定)
# max_model_len: 8192
# gpu_memory_utilization: 0.9
```
说明:
- `accelerator_type` 不在 ServingSpec 中暴露;由平台配置(`dev.yaml``serving.llm.accelerator_type`)统一注入到 Ray Serve LLM 的 `LLMConfig.accelerator_type`dev/h1: `H20`)。
#### 宏替换
- `$HOME``/private/users/<user_id>`
- `$HOME/common/hf``/private/hf`
- `$HOME/common/datasets``/private/datasets`serving 不强依赖,但保留一致语义)
#### 路径校验v3.8 约定)
`model_source` 允许:
- `/private/hf/...`common
- `/private/users/<user_id>/...`user
拒绝:
- 其它用户目录
- 非 `/private` 下路径
- 空路径或包含 `..` 的可疑路径
### 1.2 ServingModel响应体JSON
```json
{
"model_key": "svc-alice-20260106-123000-abcd",
"user_id": "alice",
"model_id": "alice-202601061235-qwen-0.5b",
"model_id_suffix": "qwen-0.5b",
"model_id_prefix": "alice-202601061235",
"model_source": "/private/hf/hub/models--.../snapshots/<sha>",
"num_replicas": 1,
"gpus_per_replica": 1,
"total_gpus": 1,
"state": "RUNNING",
"endpoint": {
"openai_base_url": "http://<host>:8000/v1",
"model": "alice-202601061235-qwen-0.5b"
},
"error_summary": null,
"created_at": "2026-01-06T12:30:00Z",
"updated_at": "2026-01-06T12:31:02Z"
}
```
## 2. 管理 APIMVP API server
### 2.1 Create / Upsert model
`POST /api/v2/serve/models`
#### Request
- Header: `Content-Type: application/yaml`
- Body: ServingSpecYAML
#### Response (202)
```json
{
"model_key": "svc-alice-20260106-123000-abcd",
"state": "QUEUED"
}
```
语义:
- 创建新模型(若 suffix 不存在)
- 或更新已有模型(若同一用户同一 suffix 已存在):更新 replicas/gpu 等配置,进入 `QUEUED` 等待 reconciler apply
### 2.2 List models (current user)
`GET /api/v2/serve/models`
#### Response (200)
```json
{
"items": [ ... ServingModel ... ],
"openai_base_url": "http://<host>:8000/v1"
}
```
### 2.3 Get model detail
`GET /api/v2/serve/models/{model_key}`
#### Response (200)
```json
{
"model": { ... ServingModel ... },
"resolved_spec_yaml": "model_id: ...\nmodel_source: ...\n",
"events": [
{ "event_type": "DEPLOY_REQUESTED", "created_at": "...", "payload": {...} }
],
"serve_status": {
"app_name": "argus_llm_app",
"app_status": "RUNNING"
}
}
```
### 2.4 Scale replicas (PATCH)
`PATCH /api/v2/serve/models/{model_key}`
#### Request (JSON)
```json
{ "num_replicas": 2 }
```
#### Response (200)
```json
{ "model_key": "...", "state": "QUEUED" }
```
> v3.8 只支持修改 `num_replicas`(以及可选 engine_kwargs`gpus_per_replica` 若修改,可能触发重新部署。
### 2.5 Delete / Undeploy model
`DELETE /api/v2/serve/models/{model_key}`
#### Response (200)
```json
{ "model_key": "...", "state": "DELETING" }
```
语义从“声明式配置”中删除该模型reconciler 会在下一轮 tick 触发 `serve.run(...)` 更新 app 配置并最终使其不可见。
### 2.6 Admin: Serve cluster status可选
`GET /api/v2/serve/status`
#### Response (200)
返回 `serve.status()` 摘要(集群级 + app 级)。
> 仅 admin token 可访问(沿用 v3.x admin gate
## 3. 推理 APIRay Serve OpenAI ingress
> v3.8 不做鉴权:无需 `Authorization`
### 3.1 List models
`GET http://<host>:8000/v1/models`
返回可用 model 列表(包含 `alice-qwen-0.5b` 这类带前缀名称)。
### 3.2 Chat completions
`POST http://<host>:8000/v1/chat/completions`
```json
{
"model": "alice-202601061235-qwen-0.5b",
"messages": [{"role":"user","content":"Hello"}],
"stream": false
}
```
### 3.3 Completions / Embeddings
按 Ray Serve LLM OpenAI ingress 支持范围提供v3.8 验收至少覆盖 chat
## 4. 错误码约定MVP API server
- `400 invalid yaml/spec`YAML 解析失败、字段缺失、值不合法
- `403 forbidden`路径越权model_source 访问其他用户目录)
- `409 conflict`model_id_suffix 冲突(同一用户重复创建且不允许覆盖时;若选择 upsert 则不返回该错误)
- `422 unprocessable`资源参数非法replica/gpu <=0
- `500 internal`reconciler/serve 调用异常(详情记录到 `serve_events`,并写入 `error_summary`

View File

@ -1,371 +0,0 @@
# MVP v3.8 详细设计方案Ray ServevLLM模型动态部署与管理
> 基线:当前已具备 v3.7 能力(训练平台 + W&B + SFTPGo + WebUI/API + Ray stateless pool训练侧默认 rollout=vllm
> v3.8 目标:在同一套 Ray 集群上,引入 **Ray Serve LLM后端 vLLM** 的模型推理服务能力,并通过 WebUI/API 动态管理模型生命周期。
## 0. 需求范围(来自 requirements.md
1) 通过 Ray Serve后端 vLLM动态拉起 LLM支持**多模型 application** 部署
2) 默认一个模型 1 个 replica用户可配置多个
3) 用户可删除(下线)模型
4) 用户可指定模型使用几张 GPU
5) WebUI 可配置、查看模型列表、查看详情
6) 模型路径可用 common也可用 user 路径(本地路径)
## 1. 总体架构
### 1.1 组件关系
v3.8 在现有“训练平台”之上新增一个 **Serving 子系统**
- **API server现有**
- 新增 Serving API模型部署/删除/扩缩容/状态)
- 新增 Serving 后台线程reconciler周期性对齐 DB 与 Ray Serve 实际状态
- **SQLite现有**
- 新增 `serve_models``serve_events` 等表,保存声明式配置与状态
- **Ray 集群(现有 stateless pool**
- 复用现有 head/worker 容器
- 在集群内启动 Ray Servecontroller + proxy + deployments
- **Ray Serve LLM新增**
- 通过 `ray.serve.llm.build_openai_app` 构建一个 OpenAI-compatible app
- app 内包含多个 `LLMConfig`(每个对应一个模型)
### 1.2 为什么选择“单个 multi-model application”
Ray Serve 支持 multi-app但在 dev/docker 场景下多个 app 的 route_prefix 管理更复杂;同时 requirements 要求“多模型 application 部署”,因此 v3.8 采用:
- 一个固定的 app`argus_llm_app`(名字可配置)
- route_prefix 固定为 `/`(对外暴露 `/v1/...` OpenAI 接口)
- 每个模型对应一个 `LLMConfig`,通过 `model_id` 区分(即 OpenAI API 里的 `model` 字段)
这样对用户而言最直观:
- base_url 固定:`http://<host>:8000/v1`
- `model=` 选择不同模型(`/v1/models` 自动列出)
## 2. Ray Serve 部署策略dev/h1 约束)
### 2.1 HTTP 入口端口与 docker compose
Ray Serve 默认 HTTP 端口是 `8000`。v3.8 约定:
- 在 **head 容器** 映射 `8000:8000`
- API server 仍在 `8080`
- Ray Dashboard 在 `8265`
原因:在单机多容器 docker 环境里,如果让 proxy “每个节点都起”,会出现多个容器同时想绑定同一个 host 端口的问题(不可行)。因此 v3.8 推荐:
- Serve proxy 位置设为 **HeadOnly**(只在 head 上提供 HTTP 入口)
- GPU replica 仍运行在 worker 上proxy 只转发,不跑推理)
> 需要注意:
> - Serve 的 HTTP 配置host/port/proxy_location**Ray 集群全局配置**,启动后无法动态修改,因此应当在平台启动时一次性设定并持久化。
> - proxy Actor 需要 CPU 资源head 节点的 `num-cpus=0` 策略可能需要在 v3.8 做小幅调整(例如给 head 保留少量 CPU但仍通过 `entrypoint_resources` 确保训练 driver 不会被调度到 head。
#### 2.1.1 compose 预期改动v3.8 实现时落地)
- `src/mvp/docker-compose.yaml`ray_head新增
- `ports: - "8000:8000"`
> worker 容器不暴露 8000避免 host 端口冲突),由 head proxy 统一对外提供入口。
### 2.2 启动/配置方式Python SDK 优先)
v3.8 采用 Ray Serve Python SDK
- `ray.init(address="auto")`
- `serve.start(proxy_location="HeadOnly", http_options={"host":"0.0.0.0","port":8000})`(一次性全局配置)
- `serve.run(app, name=<app_name>, route_prefix="/")`
- `serve.delete(name=<app_name>)`(必要时)
- `serve.status()` 查询集群/应用状态
理由:
- 避免在平台内部引入额外 REST client 依赖(并减少跨版本 REST schema 不稳定风险)
- API server 本身运行在 head 容器内,可直接 `ray.init(address="auto")` 连接现有集群
> 另Ray Dashboard 暴露 Serve REST API`PUT /api/serve/applications/` 等)可作为备选方案,但 v3.8 先不以它为主通路。
### 2.3 依赖与镜像假设
v3.8 依赖:
- `ray[serve]`Serve Controller/Proxy
- `ray[llm]`Ray Serve LLM 的 `ray.serve.llm` 模块)
- vLLM推理引擎
由于 v3.7 已切换到 `verlai/verl:vllm011.latest`,预期镜像内包含 vLLM`ray.serve.llm` 是否开箱即用需要在实现阶段确认。
若缺失v3.8 将在 `argus-ray-node` 镜像构建阶段补充 `pip install "ray[serve,llm]"`(或按官方建议的最小依赖)并做版本锁定。
### 2.4 Serving 配置dev.yaml
v3.8 新增一段 serving 配置,至少包含:
```yaml
serving:
serve:
http_port: 8000 # 固定 8000
proxy_location: HeadOnly # dev/docker 下推荐
llm:
accelerator_type: H20 # dev 环境填写 H20对应 ray.serve.llm.LLMConfig.accelerator_type
```
说明:
- `accelerator_type` 是 Ray Serve LLM 的 `LLMConfig.accelerator_type` 字段,用于表达“该模型运行在哪类加速卡上”。在 dev/h1 环境我们固定为 `H20`
- v3.8 不把 `accelerator_type` 暴露给普通用户编辑(避免误配);由部署环境配置统一决定。
## 3. 模型配置与资源映射
### 3.1 关键配置对象:`ray.serve.llm.LLMConfig`
每个模型部署由一个 `LLMConfig` 描述关键字段v3.8 用到的子集):
- `model_loading_config`
- `model_id`: 对外展示/请求时用的模型名(唯一 key
- `model_source`: HF repo id / S3 / **local path**
- `accelerator_type`
- 从 `dev.yaml``serving.llm.accelerator_type` 读取dev/h1: `H20`
- `deployment_config`
- `num_replicas``autoscaling_config`v3.8 先用固定 `num_replicas`
- `ray_actor_options`CPU/资源约束)
- `engine_kwargs`
- vLLM 相关参数(`max_model_len``gpu_memory_utilization` 等)
- `placement_group_config`
- 控制 vLLM engine workers 使用的资源 bundle用于多 GPU / 跨节点)
- `runtime_env`
- 注入 HF cache、离线开关等环境变量
### 3.2 GPU 张数gpus_per_replica如何落到 LLMConfig
v3.8 把用户输入的:
- `gpus_per_replica = N`
映射为:
- `engine_kwargs.tensor_parallel_size = N`(单机/跨机张量并行Ray Serve LLM 官方示例写法)
- `placement_group_config = {"bundles": [{"GPU": 1, "CPU": <cpu_per_gpu>}] * N, "strategy": "PACK"}`
并在 `engine_kwargs` 中保留 vLLM 其他参数(`max_model_len``gpu_memory_utilization` 等)。
> 兼容性说明Ray Serve LLM/Serve LLM 仍处于快速演进阶段v3.8 会以我们线上实际 Ray 版本为准做最小适配与回归测试。
### 3.2.1 跨节点场景N > 单机 GPU
Ray Serve LLM 默认使用 `PACK` 策略,优先把 GPU worker 放在尽量少的节点上;如果单机放不下,会自动 spill 到其它节点从而支持跨节点张量并行TP部署。
### 3.3 replica 数num_replicas
v3.8 默认:
- `num_replicas = 1`
允许用户在 UI 中设置为 `>=1`
多 replica 会线性消耗 GPU`num_replicas * gpus_per_replica`),需要做资源预检查。
### 3.4 模型路径与宏替换common / user
v3.8 支持两类模型来源:
1) **common**
- 典型为 `/private/hf/...`(共享 HF cache / snapshot
2) **user**
- `/private/users/<user_id>/models/...`
- 以及用户训练输出(例如 `jobs/<sid>/checkpoints/.../huggingface`
为保证 UI 易用,沿用平台已有的宏语义:
- `$HOME``/private/users/<user_id>`
- `$HOME/common/hf``/private/hf`
并进行路径校验:
- 允许前缀:`/private/hf``/private/users/<user_id>/`
- 拒绝:越权访问其他用户目录、或访问系统敏感路径
### 3.5 离线模式(避免 HF mirror 429
v3.7 训练侧已验证 `HF_HUB_OFFLINE=1` 的必要性。v3.8 Serving 侧同样默认注入:
- `HF_HOME=/private/hf`
- `HUGGINGFACE_HUB_CACHE=/private/hf/hub`
- `TRANSFORMERS_CACHE=/private/hf/transformers`
- `HF_HUB_OFFLINE=1`
- `HF_ENDPOINT=https://hf-mirror.com`(可保留,但离线模式下不应触发网络)
并建议用户在 ServingSpec 中尽量填写 **local path** 作为 `model_source`,而不是直接 repo id。
## 4. 平台数据模型SQLite
新增两张主表:
### 4.1 `serve_models`
每一行代表一个“声明式模型部署”:
- `model_key`(平台内部唯一 ID便于重命名/去重)
- `user_id`
- `model_id`(对外 OpenAI model 名称,要求 per-app 唯一)
- `model_source`(本地路径或 repo id存 resolved 后的结果)
- `num_replicas`
- `gpus_per_replica`
- `engine_kwargs_json`(可选)
- `state``QUEUED | DEPLOYING | RUNNING | FAILED | DELETING | DELETED`
- `serve_app_name`(默认 `argus_llm_app`
- `created_at / updated_at`
- `error_summary`
### 4.2 `serve_events`
记录关键事件与排障信息(类似 task_events
- `id`
- `model_key`
- `event_type`DEPLOY_REQUESTED/DEPLOY_APPLIED/STATUS_SYNC/DELETE_REQUESTED/...
- `payload_json`
- `created_at`
## 5. API 设计(新增)
在现有 `Authorization: Bearer <user_token>` 的认证体系下,新增 Serving API路径仅示意具体在实现时与现有 `api/v2` 对齐)。
### 5.1 用户接口
- `POST /api/v2/serve/models`
- body: YAML 或 JSONv3.8 先用 YAML 与现有 TaskSpec 一致)
- 创建/更新upsert一个模型配置进入 `QUEUED`
- `GET /api/v2/serve/models`
- 列出当前用户的模型列表(含 state、资源、endpoint
- `GET /api/v2/serve/models/{model_key}`
- 详情:完整 spec + 最近事件 + Serve status 摘要
- `PATCH /api/v2/serve/models/{model_key}`
- 修改 `num_replicas`、或 engine_kwargs可选
- `DELETE /api/v2/serve/models/{model_key}`
- 下线模型(进入 `DELETING`
### 5.2 系统接口admin
- `GET /api/v2/serve/status`admin
- 返回 `serve.status()` 的摘要(集群级 / app 级)
### 5.3 对外推理 endpoint
固定输出到 UI/接口中:
- `openai_base_url = http://<host>:8000/v1`
- 支持:
- `/v1/chat/completions`
- `/v1/completions`
- `/v1/embeddings`
- `/v1/models`
> v3.8 不做额外网关与鉴权(保持与现有 dev 环境一致);若后续需要,可在 v3.9+ 引入 token 校验/反向代理。
### 5.4 `model_id` 前缀策略user_id-
为避免多用户冲突并保持可读性:
v3.8 采用“**user_id + 日期小时分钟**”作为稳定前缀,以降低冲突并便于快速定位创建时间:
- 用户在 UI/API 中仅填写 `model_id_suffix`(或仍用字段名 `model_id`,但语义为 suffix
- 平台计算实际对外 `model_id`
- `prefix = f"{user_id}-{YYYYMMDDHHMM}"`
- `model_id = f"{prefix}-{model_id_suffix}"`
- 在列表/详情中同时展示:
- `model_id_suffix`(用户输入)
- `model_id_prefix`(平台生成,例如 `alice-202601061235`
- `model_id`(对外 OpenAI 名称)
## 6. 后台执行模型Serving Reconciler
v3.8 参考任务 scheduler 的模式,引入一个轻量的 reconciler
- tick 周期(例如 5s
- 每次 tick
1) 拉取 DB 中 `QUEUED/DEPLOYING/RUNNING/DELETING` 的模型
2) 调用 `serve.status()` 读取当前 app 及 deployments 状态
3) 若存在 `QUEUED` 或需要变更的模型:构建新的 multi-model app包含全部 `RUNNING/DEPLOYING/QUEUED` 的模型配置)并 `serve.run(...)`
4) 若存在 `DELETING`:从 app 配置中移除对应模型,并 `serve.run(...)` 应用变更
5) 更新每个模型的 state依据 Serve status
重要行为说明multi-model app 的代价):
- 每次“新增/删除/改 replicas”都会触发对同一个 app 的一次 `serve.run(...)` 更新;
- Ray Serve 会尽量做增量更新,但在某些版本/配置下可能导致 ingress/router 短暂重启;
- v3.8 先接受该代价(满足需求闭环优先);若后续需要“删除某模型不影响其它模型”,可演进为“每模型一个 app + 单独 route_prefix”的方案。
资源预检查:
- 在 apply 前使用 `ray.available_resources()` 做粗粒度 GPU 预检查:
- 需要 GPU 总量 = `sum(num_replicas * gpus_per_replica)`(仅对“新增/扩容的差量”更精确)
- 若不足:
- 模型保持 `QUEUED`,记录事件 `PENDING_RESOURCES`
- 用户 UI 显示“资源不足,等待释放”
> v3.8 不引入更复杂的抢占/优先级。Serving 与 Training 会竞争 GPU用户需要自行规划资源或后续版本引入统一调度
## 7. WebUI 设计(新增 Serving 页面)
新增侧边栏入口:**Serving**
### 7.1 Serving 列表页
- 展示字段:
- model_id
- user_id仅 admin 可见)
- replicas / gpus_per_replica / total_gpus
- stateRUNNING/DEPLOYING/QUEUED/FAILED
- 操作Scale修改 replicas、Delete
### 7.2 Serving 创建/编辑页
两种模式(与 New Task 类似,先做 YAML 模式即可):
示例 YAMLv3.8
```yaml
model_id: qwen-0.5b
model_source: $HOME/common/hf/hub/models--Qwen--Qwen2.5-0.5B-Instruct/snapshots/<sha>
num_replicas: 1
gpus_per_replica: 1
# engine_kwargs:
# max_model_len: 8192
# gpu_memory_utilization: 0.9
```
### 7.3 Serving 详情页
- 完整配置resolved spec
- Serve status 摘要deployments 状态、replica 健康)
- OpenAI 调用示例python openai client
## 8. 验收标准v3.8
1) 部署:
- 一键部署一个模型1 replica、1 GPU成功状态变为 RUNNING
- `/v1/models` 可列出该模型
2) 扩缩容:
- 修改 `num_replicas` 生效Serve status 看到副本数变化)
3) 多模型:
- 同一个 app 内能同时部署 2 个模型(不同 model_id
- 通过 OpenAI 接口用不同 `model=` 请求可得到响应
4) 下线:
- 删除某模型后 `/v1/models` 不再出现
5) 模型路径:
- 支持 `/private/hf/...`common`/private/users/<user>/...`user两类本地路径
6) 资源不足可解释:
- 当 GPU 不足时,模型进入 `QUEUED` 并在 UI/详情中提示“资源不足”
## 9. 待确认点(请你评审时确认)
已确认(来自评审):
1) 推理端口固定使用 `8000`Ray Serve 默认端口)。
2) 对外暴露的 OpenAI 接口 **不与现有 token 体系绑定**v3.8 不做推理侧鉴权)。
3) `model_id` 命名规则:平台统一加 `user_id + 日期小时分钟` 前缀,用户在 UI 里只填写后缀部分。
> 说明:这样可以避免跨用户 model_id 冲突,同时在 OpenAI API 的 `model=` 字段上自然可读。

View File

@ -1,266 +0,0 @@
# MVP v3.8 开发计划TDD细化版
> 目标:在 v3.7 基础上引入 Ray ServevLLM模型动态部署与管理多模型单 app并提供 WebUI + API 管理闭环。
> 约束(已确认):
> - 推理端口固定 `8000`Serve HTTP
> - 推理侧不接入现有 token 鉴权(对外 OpenAI endpoint 无鉴权)。
> - 对外 `model_id` 统一加前缀:`<user_id>-<YYYYMMDDHHMM>-<suffix>`(用户只填 suffix
> - `LLMConfig.accelerator_type``dev.yaml` 读取dev/h1: `H20`)。
本计划按“测试先行 → 实现 → 回归”的节奏拆分到可验证粒度;每个 milestone 都能单独验收。
---
## M0 - 基线与依赖探测(不改行为)
**目的**:确认 v3.7 baseline 稳定,并明确 Ray Serve LLM 依赖是否已具备(否则后续会卡在镜像/依赖)。
### M0.1 本地回归
- [ ] `.venv/bin/python -m pytest` 通过coverage ≥ 90%
### M0.2 远端回归h1
- [ ] `src/mvp/scripts/run_all_v30_api.sh` 可跑通(确认训练闭环未回退)
### M0.3 head 容器内依赖探测(记录结论)
- [ ] `python3 -c "import ray; import ray.serve; print(ray.__version__)"`
- [ ] `python3 -c "from ray.serve.llm import LLMConfig, build_openai_app; print('serve_llm_ok')"`
- [ ] 若失败(例如缺 `gymnasium`):记录缺失项,并在 M6 通过补齐 `ray[llm]` 解决
### M0.4 配置探测
- [ ] `configs/dev.yaml` 中存在:
- `serving.llm.accelerator_type: H20`
- `serving.serve.http_port: 8000`
- `serving.serve.proxy_location: HeadOnly`
**验收**
- baseline 无回退;依赖探测结论明确(可用/不可用)
---
## M1 - ServingSpec解析/校验/宏替换/路径校验)(单测驱动)
**目的**先把“输入”这层彻底固化API/UI 复用),避免后期反复改 schema。
### M1.1 新增/扩展数据模型
- [ ] `ServingSpec`(输入)
- `model_id`suffix
- `model_source`(支持 `$HOME` 宏)
- `num_replicas`default=1
- `gpus_per_replica`default=1
- `engine_kwargs`(可选 dict先原样存 DB实现阶段再做白名单/黑名单)
- [ ] `ResolvedServingSpec`(内部)
- `model_id_suffix`
- `model_id_prefix`(由平台生成:`user_id-YYYYMMDDHHMM`
- `model_id`(对外:`<prefix>-<suffix>`
- `model_source`resolved path
### M1.2 规则(写成纯函数,便于测)
- [ ] `validate_model_id_suffix(suffix)`:长度/字符集限制(建议:`[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}`
- [ ] `$HOME` 宏替换:`$HOME``$HOME/common/hf``$HOME/common/datasets`
- [ ] 路径校验(强制本地路径):
- 允许:`/private/hf/...``/private/users/<user_id>/...`
- 拒绝:`..`、空、其它用户路径、非 `/private` 路径
- [ ] `make_model_id_prefix(user_id, now_utc)``YYYYMMDDHHMM`UTC+ user_id
### M1.3 单测(先写失败用例,再补实现)
- [ ] `test_serving_spec_validation.py`
- suffix 合法/非法
- replicas/gpus 边界0、负数、小数、超大值按实现决定是否限制上限
- [ ] `test_serving_spec_paths.py`
- `$HOME` 替换正确
- 越权路径返回 403/ValueError按接口层映射
- `/private/hf``/private/users/<user>` 均可
- [ ] `test_serving_model_id_prefix.py`
- 固定时间输入 → prefix 输出一致(避免时区/格式问题)
**验收**
- 输入 spec 规则稳定;核心校验/替换均有单测覆盖
---
## M2 - SQLite 表结构与 Db 接口(单测驱动)
**目的**Serving 的声明式状态必须持久化,可审计、可恢复。
### M2.1 DB schema
- [ ] `serve_models`
- 主键:`model_key`(平台生成)
- unique`(user_id, model_id_suffix)`(实现 upsert
- 存储resolved spec包含 prefix/full model_id、resolved model_source
- 状态:`QUEUED/DEPLOYING/RUNNING/FAILED/DELETING/DELETED`
- `error_summary`
- [ ] `serve_events`append-only
### M2.2 Db 方法
- [ ] `upsert_serve_model(user_id, spec_yaml, now)` → (model_key, state)
- [ ] `list_serve_models(user_id, include_deleted=False, limit/offset?)`
- [ ] `get_serve_model(model_key)`
- [ ] `set_serve_model_state(model_key, state, error_summary=None)`
- [ ] `append_serve_event(model_key, event_type, payload_json=None)`
- [ ] `pick_next_runnable_serve_change()`(给 reconciler 用)
### M2.3 单测
- [ ] `test_db_serving.py`
- upsert 行为(同 suffix 更新不产生新 model_key 或产生新版本——此处需在实现前明确策略)
- state 流转 + 事件记录
- list 的过滤与排序(按 updated_at
**验收**
- DB 行为可预测upsert/unique 语义确定并测试覆盖
---
## M3 - Serving 管理 APIFastAPI单测驱动
**目的**:先把管理 API 跑通Ray Serve 先不接真实reconciler 之后再接)。
### M3.1 API 路由(用户)
- [ ] `POST /api/v2/serve/models`Content-Type: application/yaml
- 入参ServingSpec YAML
- 出参:`{model_key,state}`202
- [ ] `GET /api/v2/serve/models`
- 返回 items + `openai_base_url=http://<host>:8000/v1`
- [ ] `GET /api/v2/serve/models/{model_key}`
- 返回 model + resolved_spec_yaml + events分页可后置+ serve_status先空/占位)
- [ ] `PATCH /api/v2/serve/models/{model_key}`JSON
- 支持 `num_replicas`(最小闭环)
- [ ] `DELETE /api/v2/serve/models/{model_key}`
### M3.2 API 路由admin可选
- [ ] `GET /api/v2/serve/status`(仅 admin token
### M3.3 错误映射(必须测试)
- [ ] YAML 解析失败400
- [ ] spec 校验失败422
- [ ] 越权路径403
- [ ] 不存在 model_key404
### M3.4 单测
- [ ] `test_app_serving_api.py`
- happy pathcreate → list → get → patch → delete
- 多用户隔离:用户只能看到自己的 model
- 错误码覆盖400/403/404/422
**验收**
- API reference (`v3.8_api.md`) 中所有管理接口可返回预期结构Serve 未接入也能工作)
---
## M4 - ServeClient 抽象 + LLMConfig builder单测驱动
**目的**:将“如何从 ResolvedServingSpec 构造 LLMConfig”固化并把 Ray Serve 的依赖隔离到 client 里,便于 mock。
### M4.1 `ServeClient` 接口(可 mock
- [ ] `ensure_started(http_port=8000, proxy_location="HeadOnly")`
- [ ] `apply_app(app_name, llm_configs)`multi-model
- [ ] `get_status()`serve.status 摘要)
### M4.2 `build_llm_config(resolved_spec, accelerator_type, runtime_env_defaults)` 纯函数
- [ ] 写入 `LLMConfig.accelerator_type`(来自 dev.yamlH20
- [ ] `deployment_config.num_replicas`
- [ ] `engine_kwargs.tensor_parallel_size = gpus_per_replica`
- [ ] `placement_group_config` bundles 按 GPU 张数生成
- [ ] `runtime_env.env_vars` 注入(至少包含 HF cache + `HF_HUB_OFFLINE=1`
### M4.3 单测
- [ ] `test_llm_config_builder.py`
- gpus_per_replica=1/2/4 → tensor_parallel_size 与 bundles 数量正确
- accelerator_type 注入正确
- runtime_env 含 HF_HUB_OFFLINE 等关键 env
**验收**
- 从平台 spec 到 Ray Serve LLMConfig 的映射规则稳定,有单测锁定
---
## M5 - Serving Reconciler状态机 + 资源预检查)(单测驱动)
**目的**实现声明式对齐DB → Serve同时提供可解释的 QUEUED/FAILED 状态。
### M5.1 状态机(最小闭环)
- [ ] `QUEUED`:等待 apply
- [ ] `DEPLOYING`:已触发 apply等待 Serve running/healthy
- [ ] `RUNNING`Serve status running
- [ ] `FAILED`apply 或 status 失败(写 error_summary + event
- [ ] `DELETING`:等待从 app 中移除
- [ ] `DELETED`:完成删除(可选保留记录)
### M5.2 资源预检查
- [ ] `needed_total_gpus = sum(num_replicas*gpus_per_replica)`(最小可用预检查)
- [ ] `ray.available_resources()["GPU"]`(或更稳健的 per-node 统计)不足时:
- 保持 `QUEUED`
- 记录 `PENDING_RESOURCES` event
### M5.3 reconcile 策略multi-model app
- [ ] tick 读取 active models构建全量 `llm_configs`
- [ ] 处理 deleting从 configs 中移除对应 model再 apply
### M5.4 单测mock ServeClient + mock ray resources
- [ ] `test_serving_reconciler.py`
- 新增模型apply_app 被调用state 进入 DEPLOYING
- 删除模型apply_app configs 不包含该模型
- GPU 不足:不 applystate 仍 QUEUEDevent 写入
- apply 抛异常state FAILEDerror_summary 写入
**验收**
- reconciler 行为在纯单测环境可验证;失败可解释
---
## M6 - 真实集成h1Ray Serve 启动 + 推理闭环E2E
**目的**:在 dev/h1 环境真正跑通:部署模型 → `/v1/models` 可见 → `chat/completions` 成功 → 删除后消失。
### M6.1 compose/端口
- [ ] `src/mvp/docker-compose.yaml``ray_head` 增加 `8000:8000`
### M6.2 镜像依赖(若 M0 发现缺失)
- [ ] 在 `argus-ray-node` 镜像中补齐 `ray[serve,llm]`(版本与现有 Ray 对齐,避免升级 Ray 导致不兼容)
- 推荐优先补齐 `ray[llm]`(包含 `ray.serve.llm` 依赖闭包,如 `gymnasium`),再按需补 `ray[serve]`
- 验证点:`python3 -c "from ray.serve.llm import LLMConfig, build_openai_app; print('serve_llm_ok')"`
### M6.3 E2E 脚本(幂等)
- [ ] 新增 `scripts/run_all_v38_serving.sh`
- 起 compose确保 Serve 端口可用)
- 起 API
- 创建 user + token
- `POST /api/v2/serve/models` 创建 1GPU 模型
- 轮询模型 state 到 RUNNING
- `curl http://127.0.0.1:8000/v1/models` 验证包含 `<prefix>-<suffix>`
- `curl http://127.0.0.1:8000/v1/chat/completions` 进行最小推理
- `DELETE /api/v2/serve/models/{model_key}` 下线
- 再轮询 `/v1/models` 不包含
**验收**
- E2E 可重复跑通(至少两次连续跑不需要人工清理)
---
## M7 - WebUIServing 页面)(单测驱动)
**目的**:给用户可视化的模型管理页面(最小必要功能)。
### M7.1 页面
- [ ] Sidebar 增加 Serving
- [ ] `/ui/serving`:列表 + 状态 + 操作delete/scale
- [ ] `/ui/serving/new`YAML 输入 + submit
- [ ] `/ui/serving/{model_key}`详情resolved spec、events、OpenAI 调用示例)
### M7.2 单测
- [ ] `test_ui_serving.py`:路由 200、关键链接存在、包含 openai_base_url=8000
**验收**
- WebUI 覆盖 create/list/detail/scale/delete 的主链路
---
## M8 - 文档与验收用例(交付)
**目的**:给用户/运维一套可复用的运行方式与排障路径。
- [ ] 更新 `specs/mvp/v3.8/v3.8_progress.md`(按 milestone 记录)
- [ ] 补充 README可选端口说明、推理 API 无鉴权警示、模型路径约定
- [ ] 验收清单checklist
- 单测通过
- h1 E2E 通过
- UI 主链路可操作

View File

@ -1,189 +0,0 @@
# v3.8 方案补充:每个模型一个 Ray Serve App隔离增删影响
## 背景与问题复现
当前 v3.8 的实现采用 **单 application + 多模型** 的方式:
- 服务层每次 reconcile 都会构造“全量 llm_configs”并调用一次 `serve.run(app, name="argus_llm_app", route_prefix="/")`
- **新增/删除一个模型**会触发对同一个 app 的“整体更新”
- Ray Serve 在 app 更新时会对该 app 内的 deployments/replicas 做滚动更新与重新调度
因此你在 Ray Dashboard 中观察到:
- 添加/删除一个模型时,其他模型的 Serve deployment 也进入更新状态
- 内存/显存占用重新变化,甚至出现 GPU 卡位变化replica 重新调度到不同 node/GPU
这与“其他未变更 model 不受影响”的期望不一致。
---
## 目标
将 serving 架构调整为:
- **每个模型一个 Serve App独立 app name**
- 每个模型一个独立 `route_prefix`
- 新增/删除/缩放某个模型只更新该模型对应的 app不影响其他模型 app
约束保持不变:
- 推理端口固定 `8000`
- 推理侧不接入现有 token 鉴权OpenAI endpoint 无鉴权)
- `model_id` 前缀规则:`<user_id>-<YYYYMMDDHHMM>-<suffix>`
- `LLMConfig.accelerator_type``configs/dev.yaml` 配置dev/h1: `H20`
---
## 总体设计
### 1) 命名与路由
为每个 model 生成:
- `app_name`:建议直接使用 `model_key`(天然唯一且 URL-safe例如
- `app_name = "mvp2-alice-serve-20260106-060203-aad8"`
- `route_prefix`:建议使用 model_key避免 model_id 中的 `.``_` 等带来的 URL/编码歧义:
- `route_prefix = f"/serve/{model_key}"`
于是该模型的 OpenAI base url 为:
- `openai_base_url = http://<host>:8000/serve/<model_key>/v1`
说明:
- 仍然是 **OpenAI-compatible**,只是 base_url 不再是根路径 `/v1`,而是每个模型一个前缀。
- 这样可以做到“每个模型的 OpenAI endpoint 互不影响”,也更容易做按模型的观测/下线。
### 2) 运行方式Ray Serve
单模型 app 的创建/更新:
- `app = build_openai_app({"llm_configs":[LLMConfig(...)]})`
- `serve.run(app, name=app_name, route_prefix=route_prefix)`
单模型 app 的删除:
- `serve.delete(app_name)`
关键点:
- **更新/删除只作用于对应 app_name**;其它 app 不会被 serve.run “整体重建”触发滚动更新。
### 3) 服务层Scheduler/Reconciler改造点高层
现状:`ServingReconciler.tick()` 每次对“全量模型集合” apply 一次 app。
目标:改成按 model_key 的“局部 reconcile”
- 对于状态 `QUEUED` 的 model
- 只构建该 model 的 `LLMConfig`
- `serve.run(app, name=model_key, route_prefix="/serve/<model_key>")`
- 状态:`DEPLOYING`probe 成功)→ `RUNNING`
- 对于状态 `DELETING` 的 model
- `serve.delete(model_key)`
- 状态:`DELETED`
资源预检查:
- 只需要预检查“本次变更模型”需要的 GPU`num_replicas * gpus_per_replica`
- 不需要把其他模型资源都算入 needed_total_gpus因为不再重建全量 app
### 4) API/UI 返回的 endpoint 结构
现状 API 返回:
- `endpoint.openai_base_url = http://<host>:8000/v1`
- `endpoint.model = <model_id>`
建议改为(字段不变,值变化):
- `endpoint.openai_base_url = http://<host>:8000/serve/<model_key>/v1`
- `endpoint.model = <model_id>`(保持)
UI 的示例 curl 也应使用上面的 base_url。
---
## 行为变化与兼容性影响
### 1) `/v1/models` 聚合能力变化(重要)
采用“每模型一个 route_prefix”后
- `http://<host>:8000/v1/models` **不再是“所有模型的总览”**(除非我们再提供一个聚合层)
- 每个模型的 models list 在它自己的前缀下:
- `http://<host>:8000/serve/<model_key>/v1/models`
如果仍然希望保留一个统一入口(可选增强,非本方案必做):
- 额外引入一个“稳定不重建”的 **OpenAI Router**(可以是:
- FastAPI(8080) 侧做反向代理;或
- 一个单独 Ray Serve app 只负责路由,不随模型变更重建)
- Router 读取 SQLite/内存缓存的 model 映射:
- `model_id -> route_prefix`
- 将 `/v1/chat/completions` 转发到对应 model 的 prefix
这可以作为 v3.9+ 的增强项v3.8 的核心目标是“变更隔离”,优先保证稳定性。
### 2) 资源与调度稳定性
改为 per-app 后:
- 新增模型 B 不再引起模型 A 的 replica 重新调度 → **A 的 GPU/内存占用更稳定**
- 删除模型 B 也不会触发 A 的滚动更新
但仍需注意:
- 如果 Ray 集群发生节点故障/资源回收Serve 本身仍可能重启个别 replica这是系统层行为
---
## 验证与验收流程(建议)
### A. 功能验收API/UI
1. 通过 UI/或 API 创建模型 A等待 RUNNING
2. 记录 A 的:
- `model_key_A`
- `endpoint.openai_base_url_A`
3. 再创建模型 B等待 RUNNING
4. 确认:
- A 的 endpoint 仍可用(对 A 的 base_url 发 chat completion
- B 的 endpoint 可用
5. 删除模型 B确认
- B endpoint 404/不可用
- A endpoint 仍可用
### B. “不影响其它模型”的强验证Ray actor 级别)
在 Ray 上抓取 A 对应 `LLMServer` replica 的 actor_id/node_id
- 创建 B 前:`actor_id_A_before`
- 创建 B 后:`actor_id_A_after`
- 删除 B 后:`actor_id_A_after_delete`
预期:
- `actor_id_A_before == actor_id_A_after == actor_id_A_after_delete`
(允许 `LLMRouter` 变化,但 **LLMServer for A 不应变化**
---
## 需要修改的代码点(清单级)
> 这里只列“改哪里”,不展开具体实现(实现时按 TDD 补单测)。
- `argus.service.serving_reconciler`
- 从“全量 apply 单 app”改为“按 model_key 局部 apply/delete 单 app”
- GPU 预检查改为 per-model
- `argus.service.serve_client`
- 增加 `delete_app(app_name)`(封装 `serve.delete(app_name)`
- `apply_app` 传入 `app_name/route_prefix`(已存在,但将不再传固定 app_name
- `argus.service.app`Serving API 输出):
- `_serve_model_public().endpoint.openai_base_url` 改为 per-model 前缀
- `/api/v2/serve/models` list/get 的 openai_base_url 语义调整(可返回“该模型的 base_url”列表里每条都有
- `argus.service.ui`Serving 页面):
- “OpenAI /v1/models” 需要调整为“选择某个模型后打开该模型的 /v1/models”
- 详情页 curl 示例使用 per-model base_url

View File

@ -1,174 +0,0 @@
# MVP v3.8变更开发计划Per-Model Serve AppTDD
> 目标:按 `specs/mvp/v3.8/v3.8_per_model_app.md` 将 v3.8 从“单 app 多模型(全量重建)”改为“**每个模型一个 Ray Serve app + 独立 route_prefix**”,实现增删/缩放某个模型不触发其它模型重启与重调度。
## 约束与结论
- 推理端口固定:`8000`
- 推理 endpoint **不做鉴权**
- `model_id` 前缀规则:`<user_id>-<YYYYMMDDHHMM>-<suffix>`
- `LLMConfig.accelerator_type``configs/dev.yaml` 决定dev/h1: `H20`
- 路由方案(本迭代固定):
- `app_name = model_key`
- `route_prefix = /serve/<model_key>`
- `openai_base_url = http://<host>:8000/serve/<model_key>/v1`
## 非目标(明确不做)
- 不提供统一 `/v1` 的“跨模型聚合路由”(如要,需要额外 router 层;可在后续迭代做)
- 不改 ServingSpec 语义(输入仍为 `model_id/model_source/num_replicas/gpus_per_replica/engine_kwargs`
---
## M0 - 基线回归与分支保护
**目的**:确保切换架构前训练/现有功能不回退。
### 测试
- [ ] 本地:`.venv/bin/python -m pytest` 全绿coverage ≥ 90%
### 验收
- [ ] 基线可用;进入 M1
---
## M1 - API 输出与 endpoint 语义调整(单测驱动)
**目的**API/DB/前端都统一 per-model 的 `openai_base_url` 语义;避免 UI/脚本继续使用 `/v1` 根路径。
### 变更点
- `GET /api/v2/serve/models`
- 保持返回 `items[]`,但每个 item 的 `endpoint.openai_base_url` 必须是 per-model base url
- `openai_base_url`(列表层级字段)处理策略二选一:
- A推荐移除该字段breaking需同步 UI/脚本)
- B兼容保留但改为 `null` 或提示字符串(不再保证可用)
- `GET /api/v2/serve/models/{model_key}`
- `model.endpoint.openai_base_url` 改为 per-model base url
### 单测(先写)
- [ ] 更新/新增 `src/mvp/py/tests/test_app_serving_api.py`
- 断言 `endpoint.openai_base_url` 包含 `/serve/<model_key>/v1`
- 断言多条 models 的 base_url 不相同(随 model_key 变化)
### 实现
- [ ] 更新 `src/mvp/py/argus/service/app.py`
- `_serve_model_public()``endpoint.openai_base_url` 拼接 per-model prefix
- 如选择移除/调整 list 层的 `openai_base_url`,同步实现
### 验收
- [ ] API 单测通过;返回结构可被 UI/脚本消费
---
## M2 - ServeClient 扩展delete_app+ Reconciler 改造成 per-model单测驱动
**目的**:核心行为变更:每次 tick 只部署/删除一个模型对应的 app不重建全量 app。
### 变更点(行为)
- `QUEUED`
- 对该 `model_key` 执行 `serve.run(app, name=model_key, route_prefix=/serve/<model_key>)`
- 状态:`DEPLOYING → RUNNING`
- `DELETING`
- 对该 `model_key` 执行 `serve.delete(model_key)`
- 状态:`DELETED`
- 资源预检查从“全量 needed_total_gpus”改为“本次变更模型所需 GPU”
### 单测(先写)
- [ ] 更新 `src/mvp/py/tests/test_serving_reconciler.py`
- `create A`reconciler 只 `apply_app(app_name=A.model_key, route_prefix=/serve/A)`
- `create B`reconciler 只 `apply_app(app_name=B.model_key, route_prefix=/serve/B)`(不再对 A 触发 apply
- `delete B`reconciler 只 `delete_app(B.model_key)`(不触发 A
- GPU 不足时:保持 `QUEUED` 且 event=SERVE_PENDING_RESOURCES
### 实现
- [ ] `src/mvp/py/argus/service/serve_client.py`
- 增加 `delete_app(app_name: str)`(封装 `serve.delete`
- [ ] `src/mvp/py/argus/service/serving_reconciler.py`
- 移除“全量 app apply”逻辑
- 每个 model_key 独立部署:`app_name=model_key``route_prefix=/serve/<model_key>`
- 删除路径走 `delete_app`
### 验收
- [ ] reconciler 单测全绿逻辑可解释events/state 正确)
---
## M3 - WebUI Serving 页面适配 per-model base_url单测驱动
**目的**:用户从 UI 复制的示例命令必须可用;不再指向根 `/v1`
### 变更点
- `/ui/serving` 列表:
- “OpenAI /v1/models”按钮改为
- A移除因为没有聚合 `/v1/models`
- B保留但文案改为“OpenAI base 需进入详情页”
- `/ui/serving/{model_key}` 详情页:
- `curl` 示例使用 per-model `openai_base_url`
- 增加一键打开:`/serve/<model_key>/v1/models`
### 单测(先写)
- [ ] 更新/新增 `src/mvp/py/tests/test_ui_serving.py`
- 断言页面包含 `/serve/` 前缀
- 断言详情页示例里包含 `/serve/<model_key>/v1/chat/completions`
### 实现
- [ ] `src/mvp/py/argus/service/ui.py` 更新 Serving UI
### 验收
- [ ] UI 单测全绿;页面内容与 API 语义一致
---
## M4 - E2E 脚本更新v3.8 serving
**目的**:在 dev/h1 一键验证 per-model appA/B 增删不互相影响,且推理可用。
### 变更点
- 更新 `src/mvp/scripts/run_all_v38_serving.sh`
- `/v1/models``chat/completions` 均改用 per-model base_url`/serve/<model_key>/v1`
- 增加“隔离验证”步骤:
- 部署 A → 记录 A 的 serve replica actor_id/node_id
- 部署 B → 再次记录 A 的 actor_id/node_id必须一致
- 删除 B → 再次记录 A 的 actor_id/node_id必须一致
- 最后删除 A
### 验收
- [ ] E2E 脚本能跑通且输出明确断言(一致/不一致)
---
## M5 - h1 端到端验证与回归
**目的**:确认实际 Ray Serve 行为满足“其它模型不滚动更新”的核心目标。
### 操作
- [ ] 同步代码到:`argus@h1:/home2/argus/infra/mvp/src/mvp`
- [ ] 重启 API`scripts/61_stop_api.sh && scripts/60_start_api.sh`
- [ ] 执行:`MVP_INTERNAL_TOKEN=... scripts/run_all_v38_serving.sh`
### 验收标准(必须满足)
- [ ] 新增/删除 B 时A 的 `LLMServer` replica actor_id/node_id 不变
- [ ] A/B 的 OpenAI endpoint 均可完成 `chat/completions`
- [ ] 删除 B 后 A 仍可推理
---
## M6 - 文档与迁移说明
**目的**:明确“路由语义变化”和“如何使用”。
- [ ] 更新 `src/mvp/README.md`
- 新增 per-model base_url 说明(`/serve/<model_key>/v1`
- 提示不再提供聚合 `/v1/models`
- [ ] 更新 `specs/mvp/v3.8/v3.8_progress.md`
- 记录 per-model app 变更与验收结论
---
## 风险与缓解
- **风险:旧 `argus_llm_app` 残留**
- 缓解:在 E2E/迁移步骤里增加一次 best-effort `serve.delete("argus_llm_app")`(可选)
- **风险:用户仍按旧方式访问 `/v1`**
- 缓解UI/文档/脚本统一切换到 per-model base_url并在列表页给出明显提示

View File

@ -1,48 +0,0 @@
# MVP v3.8 进展记录
## 2026-01-06
- 完成 v3.8 设计文档:`specs/mvp/v3.8/v3.8_design.md`
- 完成 v3.8 Serving API reference`specs/mvp/v3.8/v3.8_api.md`
- 完成 v3.8 TDD 开发计划:`specs/mvp/v3.8/v3.8_dev_plan.md`
- 完成 M0`configs/dev.yaml` 增加 `serving` 配置http_port=8000, proxy_location=HeadOnly, accelerator_type=H20
- 完成 M1ServingSpec 解析/宏替换/路径校验 + 单测(`src/mvp/py/argus/service/serving_spec.py`
- 完成 M2SQLite 新增 `serve_models`/`serve_events` + Db API + 单测(`src/mvp/py/argus/service/db.py`
- 完成 M3FastAPI Serving 管理 API + 单测(`src/mvp/py/argus/service/app.py`
- 完成 M4ServeClient 抽象 + LLMConfig builderdict 形态)+ 单测(`src/mvp/py/argus/service/serve_client.py``src/mvp/py/argus/service/serve_llm_config.py`
- 完成 M5Serving reconciler状态机 + 资源预检查 + mock 单测)(`src/mvp/py/argus/service/serving_reconciler.py`
### M6h1 真实集成)
- `argus-ray-node` 镜像补齐依赖:`ray[serve,llm]` + `gymnasium` + `dm-tree`(避免 `ray.serve.llm` 导入失败)
- 修复 Ray 2.49.2 兼容性问题:
- `LLMConfig` 不支持 `placement_group_config`,改为使用 `resources_per_bundle``src/mvp/py/argus/service/serve_llm_config.py`
- 远端 E2E
- `scripts/run_all_v38_serving.sh` 可跑通create → RUNNING → `/v1/models``chat/completions` → delete → DELETED
- 修复脚本中 `/v1/models` 解析的 bash heredoc 引号错误(`src/mvp/scripts/run_all_v38_serving.sh`
### M7WebUI - Serving
- WebUI 增加 Serving 页面:
- 列表:`/ui/serving`
- 创建:`/ui/serving/new`
- 详情/事件/缩放/删除:`/ui/serving/{model_key}`
- 单测覆盖:
- `src/mvp/py/tests/test_ui_serving.py`
### M8文档/验收)
- `src/mvp/README.md` 补充 v3.8 serving 端口与 E2E 脚本说明
### 环境探测h1 / head 容器)
> 目的:确认 Ray Serve LLM 依赖是否开箱即用,避免后续集成阶段才暴雷。
- `ray`:可用,版本 `2.49.2`
- `ray.serve`:可 importServe 基础可用)
- `ray.serve.llm`:当前不可 import
- 报错:`ModuleNotFoundError: No module named 'gymnasium'`
- 原因:`ray.serve.llm` 的导入链路会触发 `ray.rllib`,而 rllib 依赖 `gymnasium`
结论:
- v3.8 在实现阶段需要在 `argus-ray-node` 镜像中补齐 `ray[llm]`(推荐)或至少补齐 `gymnasium` 等必要依赖,确保 `from ray.serve.llm import ...` 可用。

View File

@ -1,108 +0,0 @@
# v3.9 UI 重构方案(保持功能不变)
## 背景与问题
当前 `src/mvp/py/argus/service/ui.py` 单文件约 1400+ 行,包含:
- 全局 CSS/JS长字符串
- 布局渲染nav/page 拼接)
- 11 个页面的 HTML + 大段内嵌 JS包含 TaskSpec 模板与表单逻辑)
导致:变更难定位、合并冲突多、缺少模块边界、复用困难、测试覆盖薄弱。
## 目标(功能不变)
- **路由与页面行为完全不变**URL、返回内容、按钮/表单行为、localStorage key`mvp_token`/`mvp_sftp_password`、API 调用路径保持不变。
- **不引入前端构建链/新依赖**(仍然用纯字符串/轻量模板函数)。
- 将 UI 拆分为可维护的多个文件(放到 `src/mvp/py/argus/ui/`)。
- 增加最小的单测(确保路由可访问、关键 DOM 标识存在)。
## 非目标
- 不重做 UI 样式/交互;不引入 React/Vue不改后端 API。
- 不新增鉴权逻辑(仍然是浏览器 localStorage + Bearer token
## 拆分后的目录结构(建议)
新增包:`src/mvp/py/argus/ui/`
```
argus/ui/
__init__.py # register_ui_routes(app) 统一入口
assets/
base_css.py # BASE_CSS 常量
base_js.py # BASE_JS 常量apiFetch/apiJson 等通用函数)
layout/
nav.py # nav(active) + 链接配置
page.py # page(title, active, body, script, extra_head=...)
pages/
login.py # /ui/login
tasks.py # /ui/tasks
task_new.py # /ui/tasks/new模板常量 + 表单 JS
task_detail.py # /ui/tasks/{task_id}
task_logs.py # /ui/tasks/{task_id}/logs
serving.py # /ui/serving, /ui/serving/new, /ui/serving/{model_key}
data.py # /ui/data
admin.py # /ui/admin
routes.py # 将各 pages.register(app) 聚合注册
```
兼容层(可选但推荐):保留 `src/mvp/py/argus/service/ui.py` 仅做转发:
```py
from argus.ui import register_ui_routes
```
这样可以避免一次性改动 `service/app.py` 的 import 路径,减少风险。
## 页面拆分原则
每个 page 模块提供两个函数:
- `render(...) -> HTMLResponse`:只负责拼接 body/script不直接碰 FastAPI app
- `register(app: FastAPI) -> None`:只负责挂载路由(`@app.get(...)`)。
通用能力下沉:
- `_BASE_CSS`/`_BASE_JS` 移到 `assets/`
- `_nav()``_page()` 移到 `layout/`
- 大块常量TaskSpec 模板、UI 文案)放在页面模块同文件顶部,避免散落在函数内部。
## 资源交付方式(两种可选)
### 方案 A最稳继续内联 CSS/JS但拆到不同 Python 文件
- `page()` 内继续 `<style>{BASE_CSS}</style>``<script>{BASE_JS}</script>`
- 只改变代码组织,不改变浏览器加载方式,风险最低。
### 方案 B推荐中期新增静态端点分发资源
新增:
- `GET /ui/assets/base.css`
- `GET /ui/assets/base.js`
页面改为 `<link rel="stylesheet" href="/ui/assets/base.css">` + `<script src="/ui/assets/base.js"></script>`
优点:减少 HTML 体积、浏览器缓存更好;缺点:需要确认反向代理/中间件不拦截这些路由。
建议 v3.9 先落地方案 A稳定后再做方案 B。
## 迁移步骤(建议分 3 次 PR
1) **抽公共层**:引入 `argus/ui/assets/*``argus/ui/layout/*`,保持 UI 输出完全一致;`service/ui.py` 仍在但内部改为调用新 layout或先不动
2) **按页面迁移**:逐个把 routes 迁移到 `argus/ui/pages/*`每迁一个页面就加一个最小测试用例200 + 关键文本存在)。
3) **清理与稳定**`service/ui.py` 变为兼容转发;可选引入 `/ui/assets/*` 静态端点(方案 B
## 测试策略(最小但有效)
新增 `src/mvp/py/tests/test_ui_pages.py`
- 创建 FastAPI app复用现有测试的 app 初始化方式)
- 请求下列页面,断言 `status_code == 200`
- `/ui/login`, `/ui/tasks`, `/ui/tasks/new`, `/ui/serving`, `/ui/data`, `/ui/admin`
- 断言响应包含稳定锚点文本(例如 `Argus MVP`, `New Task`, `Tasks`),避免脆弱的全量快照。
## 验收标准Definition of Done
- 11 个 `/ui/*` 路由行为与输出不变(人工 smoke + 自动化最小测试)。
- `src/mvp/py/argus/service/ui.py` 不再包含大段 HTML/JS仅兼容转发或极薄封装
- 新增/修改 UI 页面不需要触碰 1000+ 行单文件;每页的改动范围限定在对应模块。

View File

@ -1,64 +0,0 @@
ray:
# Ray Job server address (head 容器内视角)
address: "http://127.0.0.1:8265"
# 共享根路径(容器内统一 /private对齐生产
shared_root: "/private"
# 强制 driver 落 workerhead 不跑训练)
entrypoint_num_cpus: 1
entrypoint_resources:
worker_node: 1
# 所有 job 通用 runtime env
runtime_env:
env_vars:
HF_ENDPOINT: "https://hf-mirror.com"
PYTHONUNBUFFERED: "1"
# v3.7: forbid HuggingFace Hub network access from Ray jobs (use cached snapshots).
HF_HUB_OFFLINE: "1"
# Unify cache dirs so `from_pretrained("org/name")` resolves from the same on-disk cache in offline mode.
HF_HOME: "/private/hf"
HUGGINGFACE_HUB_CACHE: "/private/hf/hub"
TRANSFORMERS_CACHE: "/private/hf/hub"
# v3.0 先不支持 user code 执行
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
tracking:
wandb:
enabled: true
# For dev compose, recommend docker bridge gateway + host published port for stability.
base_url: "http://172.22.0.1:8090"
api_key_env: "WANDB_API_KEY"
project_suffix: "_project"
data:
user_root: "/private/users"
sftpgo:
enabled: true
# Returned to users by GET /api/v2/me. For h1 E2E, usually connect to the host IP.
host: "127.0.0.1"
sftp_port: 2022
# Admin API base should include /api/v2 (SFTPGo OpenAPI server base).
# From head container, access SFTPGo by service name on the compose network.
admin_api_base: "http://argus-sftpgo:8080/api/v2"
admin_user: "admin"
admin_password_env: "SFTPGO_ADMIN_PASSWORD"
retention:
jobs_trash_after_days: 3
jobs_purge_after_days: 7
janitor_interval_s: 3600

View File

@ -1,118 +0,0 @@
# MVP V1Ray + VERL PPO实验脚本
本目录用于在“宿主机 + Docker 容器”环境下,**用宿主机脚本(`docker exec`**协调启动 Ray 集群,并通过 **`ray job submit`(在 head 提交)**跑通一次 `verl` 的 PPO 训练闭环(`total_epochs=1`),且数据/模型/日志/ckpt 都持久化到宿主机目录。
## 1. 运行环境与拓扑
### 1.1 依赖
- 宿主机Linux
- 必需工具:`docker``docker compose`Compose v2 插件)、`git`
- GPU至少 8 张可用 GPU索引 `0-7`Docker 的 NVIDIA runtime 可用
### 1.2 集群拓扑3 个容器)
- `mvp-ray-head`Ray Head
- **不挂 GPU**(容器内 `nvidia-smi` 不可用)
- `ray start --head --num-cpus=0 --num-gpus=0`head 只做控制面,不参与计算调度
- 暴露 dashboard宿主机端口 `8265`
- `mvp-ray-worker-0`Ray Worker
- 4 GPU`0,1,2,3`
- `ray start ... --resources='{"worker_node": 100}'`
- `mvp-ray-worker-1`Ray Worker
- 4 GPU`4,5,6,7`
- `ray start ... --resources='{"worker_node": 100}'`
**关键点driver 不在 head**
- 作业通过 head 提交:`ray job submit ...`
- 通过 `--entrypoint-resources='{"worker_node": 1}'` 强制 entrypoint/driver 只能调度到 workerhead 没有该资源)
## 2. 持久化目录(宿主机 <-> 容器)
在宿主机项目根目录(运行脚本时的 `${PWD}`)下使用 `./shared` 做持久化根目录,并 bind mount 到容器内 `/mnt/shared`
- 宿主机:`./shared`
- 容器:`/mnt/shared`
主要内容:
- 数据集:`/mnt/shared/datasets/gsm8k/`
- HF 缓存:`/mnt/shared/hf/`(脚本会设置 `HF_HOME`,并尽量幂等跳过重复下载)
- 每个 Ray Job 的输出(按 submission id 分目录):
- `/mnt/shared/jobs/<submission_id>/logs/`
- `/mnt/shared/jobs/<submission_id>/checkpoints/`
## 3. 整体流程(代码逻辑)
脚本都在 `src/mvp/v1/scripts/`,整体顺序如下:
1) `00_prereq_check.sh`
- 检查 `docker/docker compose/git`
2) `05_ensure_verl_repo.sh`
- 若项目根目录下没有 `./verl`,自动 `git clone https://github.com/volcengine/verl.git`
3) `01_up.sh`
- 创建持久化目录(`./shared/...`
- `docker compose up -d` 启动 3 个容器
4) `10_install_verl_editable.sh`
- 在 3 个容器内执行 `pip install -e /workspace/verl`(确保 `python -m verl...` 可用且代码与 `./verl` 同步)
5) `20_start_head.sh`
- 在 `mvp-ray-head` 内启动 Ray headCPU=0、GPU=0
6) `21_start_workers.sh`
- 在两个 worker 内启动 Ray worker 加入集群
- 同时给 worker 打 `worker_node` 自定义资源标签
7) `30_prepare_data_and_model.sh`
- 数据集:若 `train.parquet/test.parquet` 已存在则跳过,否则生成
- 模型:使用 HF cache`HF_HOME=/mnt/shared/hf`),存在则跳过,不存在才下载
8) `40_submit_ppo_epoch1.sh`
- 在 head 容器里执行 `ray job submit`
- 显式指定 `--submission-id=$SUBMISSION_ID`
- 通过 `--entrypoint-resources='{"worker_node": 1}'` 强制 driver 在 worker
- 训练参数:
- `trainer.total_epochs=1`
- `trainer.total_training_steps=29`GSM8K 该配置下对应 29 steps
- `trainer.save_freq=10`(每 10 step 保存一次 checkpoint避免磁盘爆炸
- `trainer.default_local_dir=/mnt/shared/jobs/$SUBMISSION_ID/checkpoints`
- `hydra.run.dir=/mnt/shared/jobs/$SUBMISSION_ID/logs/hydra`
9) `50_status.sh`
- 打印 `ray status` / `ray job list` / `ray job status` / `ray job logs | tail`
## 4. 运行方法
### 4.1 一键执行
在项目根目录执行:
- `./src/mvp/v1/scripts/run_all.sh`
### 4.2 分步执行(推荐)
按顺序执行:
- `./src/mvp/v1/scripts/01_up.sh`
- `./src/mvp/v1/scripts/10_install_verl_editable.sh`
- `./src/mvp/v1/scripts/20_start_head.sh`
- `./src/mvp/v1/scripts/21_start_workers.sh`
- `./src/mvp/v1/scripts/30_prepare_data_and_model.sh`
- `SUBMISSION_ID=ppo_h20_8g_$(date +%Y%m%d_%H%M%S) ./src/mvp/v1/scripts/40_submit_ppo_epoch1.sh`
- `./src/mvp/v1/scripts/50_status.sh`
### 4.3 查看与停止
- Dashboard`http://<宿主机IP>:8265`
- 列出作业(在 head 容器内):
- `docker exec mvp-ray-head bash -lc "ray job list --address=http://127.0.0.1:8265"`
- 停止某个 submission id
- `docker exec mvp-ray-head bash -lc "ray job stop --address=http://127.0.0.1:8265 <submission_id>"`
### 4.4 清理
- 停止并删除容器:`./src/mvp/v1/scripts/02_down.sh`
- 清理输出(谨慎,数据量可能很大):删除 `./shared/jobs/<submission_id>/`
## 5. 常见坑
- **不传 `--submission-id` 会导致“输出目录难以等于 submission id”**:因为 hydra/ckpt 目录需要在提交前确定。当前脚本会显式传 `--submission-id=$SUBMISSION_ID`,并使用同名目录。
- **checkpoint 太大**PPO 的 checkpoint 非常占空间。当前脚本默认 `save_freq=10`,如仍过大,可调大 `save_freq` 或减少保存内容/频率。
更多分步操作与验收标准见:`specs/mvp/v1_action.md`

File diff suppressed because it is too large Load Diff

View File

@ -1,86 +0,0 @@
version: "3.8"
services:
ray_head:
image: verlai/verl:sgl055.latest
container_name: mvp-ray-head
command: sleep infinity
ports:
- "8265:8265"
volumes:
- ./verl:/workspace/verl
- ./shared:/mnt/shared
shm_size: "10g"
ulimits:
nofile:
soft: 65536
hard: 65536
cap_add:
- SYS_ADMIN
- SYS_PTRACE
networks:
- mvp-ray-net
environment:
HF_HOME: "/mnt/shared/hf"
HUGGINGFACE_HUB_CACHE: "/mnt/shared/hf/hub"
TRANSFORMERS_CACHE: "/mnt/shared/hf/transformers"
HF_ENDPOINT: "https://hf-mirror.com"
PYTHONUNBUFFERED: "1"
ray_worker_0:
image: verlai/verl:sgl055.latest
container_name: mvp-ray-worker-0
command: sleep infinity
volumes:
- ./verl:/workspace/verl
- ./shared:/mnt/shared
shm_size: "10g"
ulimits:
nofile:
soft: 65536
hard: 65536
cap_add:
- SYS_ADMIN
- SYS_PTRACE
networks:
- mvp-ray-net
runtime: nvidia
environment:
NVIDIA_VISIBLE_DEVICES: "0,1,2,3"
NVIDIA_DRIVER_CAPABILITIES: "all"
HF_HOME: "/mnt/shared/hf"
HUGGINGFACE_HUB_CACHE: "/mnt/shared/hf/hub"
TRANSFORMERS_CACHE: "/mnt/shared/hf/transformers"
HF_ENDPOINT: "https://hf-mirror.com"
PYTHONUNBUFFERED: "1"
ray_worker_1:
image: verlai/verl:sgl055.latest
container_name: mvp-ray-worker-1
command: sleep infinity
volumes:
- ./verl:/workspace/verl
- ./shared:/mnt/shared
shm_size: "10g"
ulimits:
nofile:
soft: 65536
hard: 65536
cap_add:
- SYS_ADMIN
- SYS_PTRACE
networks:
- mvp-ray-net
runtime: nvidia
environment:
NVIDIA_VISIBLE_DEVICES: "4,5,6,7"
NVIDIA_DRIVER_CAPABILITIES: "all"
HF_HOME: "/mnt/shared/hf"
HUGGINGFACE_HUB_CACHE: "/mnt/shared/hf/hub"
TRANSFORMERS_CACHE: "/mnt/shared/hf/transformers"
HF_ENDPOINT: "https://hf-mirror.com"
PYTHONUNBUFFERED: "1"
networks:
mvp-ray-net:
driver: bridge

View File

@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
require_cmd docker
require_cmd git
if ! docker compose version >/dev/null 2>&1; then
echo "docker compose plugin not available; please install docker compose v2" >&2
exit 1
fi
echo "OK: docker + docker compose + git"

View File

@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
"${SCRIPT_DIR}/00_prereq_check.sh"
"${SCRIPT_DIR}/05_ensure_verl_repo.sh"
mkdir -p \
"${ROOT_DIR}/shared/hf" \
"${ROOT_DIR}/shared/datasets" \
"${ROOT_DIR}/shared/jobs" \
"${ROOT_DIR}/shared/outputs" \
"${ROOT_DIR}/shared/ray"
dc up -d
dc ps

View File

@ -1,9 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
dc down

View File

@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
VERL_DIR="${ROOT_DIR}/verl"
if [[ -d "${VERL_DIR}/.git" ]]; then
echo "OK: verl repo exists: ${VERL_DIR}"
exit 0
fi
echo "verl repo not found at ${VERL_DIR}; cloning..."
rm -rf "${VERL_DIR}"
git clone https://github.com/volcengine/verl.git "${VERL_DIR}"
echo "OK: cloned verl -> ${VERL_DIR}"

View File

@ -1,22 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
install_one() {
local name="$1"
echo "[${name}] ensure verl importable"
if ! dexec "${name}" python3 -c "import verl; print(verl.__file__)" >/dev/null 2>&1; then
echo "[${name}] verl not importable; installing editable from /workspace/verl"
dexec "${name}" bash -lc "pip install -e /workspace/verl"
else
echo "[${name}] verl import OK"
fi
}
install_one "${HEAD_CONTAINER}"
install_one "${WORKER0_CONTAINER}"
install_one "${WORKER1_CONTAINER}"

View File

@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
echo "[head] ray stop (ignore errors)"
dexec "${HEAD_CONTAINER}" bash -lc "ray stop --force || true"
echo "[head] ray start (CPU=0, GPU=0 to prevent scheduling on head)"
HEAD_IP="$(container_ip "${HEAD_CONTAINER}")"
echo "[head] container ip: ${HEAD_IP}"
dexec "${HEAD_CONTAINER}" bash -lc "ray start --head --node-ip-address=${HEAD_IP} --dashboard-host=0.0.0.0 --dashboard-port=8265 --port=6379 --num-cpus=0 --num-gpus=0"
echo "[head] ray status"
dexec "${HEAD_CONTAINER}" bash -lc "ray status || true"

View File

@ -1,33 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
start_worker() {
local name="$1"
local node_ip
node_ip="$(container_ip "${name}")"
echo "[${name}] ray stop (ignore errors)"
dexec "${name}" bash -lc "ray stop --force || true"
local head_ip
head_ip="$(container_ip "${HEAD_CONTAINER}")"
echo "[${name}] container ip: ${node_ip}"
echo "[${name}] ray start -> join ${head_ip}:6379 (num_gpus=4, resources worker_node=100)"
dexec "${name}" bash -lc "ray start --node-ip-address=${node_ip} --address=${head_ip}:6379 --num-gpus=4 --resources='{\"worker_node\": 100}'"
}
start_worker "${WORKER0_CONTAINER}"
start_worker "${WORKER1_CONTAINER}"
echo "[head] waiting for workers to register"
for _ in $(seq 1 30); do
if dexec "${HEAD_CONTAINER}" bash -lc "ray status" | grep -q "Active:"; then
break
fi
sleep 2
done
dexec "${HEAD_CONTAINER}" bash -lc "ray status || true"

View File

@ -1,37 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
DATA_DIR="/mnt/shared/datasets/gsm8k"
MODEL_ID="Qwen/Qwen2.5-0.5B-Instruct"
echo "[head] prepare dataset (idempotent) -> ${DATA_DIR}"
dexec "${HEAD_CONTAINER}" bash -lc "mkdir -p ${DATA_DIR} && if [[ -f ${DATA_DIR}/train.parquet && -f ${DATA_DIR}/test.parquet ]]; then echo 'dataset_exists: skip'; else python3 /workspace/verl/examples/data_preprocess/gsm8k.py --local_save_dir ${DATA_DIR}; fi"
echo "[head] ensure model cached to persistent HF_HOME (idempotent) -> ${MODEL_ID}"
PY_CODE="$(cat <<'PY'
import os
model_id = os.environ["MODEL_ID"]
hf_home = os.environ.get("HF_HOME", "/mnt/shared/hf")
os.environ.setdefault("HF_HOME", hf_home)
os.environ.setdefault("HUGGINGFACE_HUB_CACHE", os.path.join(hf_home, "hub"))
os.environ.setdefault("TRANSFORMERS_CACHE", os.path.join(hf_home, "transformers"))
from huggingface_hub import snapshot_download
try:
snapshot_download(repo_id=model_id, local_files_only=True)
print("model_cache_exists: skip", model_id)
except Exception:
print("model_cache_missing: downloading", model_id)
snapshot_download(repo_id=model_id)
print("model_cached_ok:", model_id)
PY
)"
printf "%s\n" "${PY_CODE}" | dexec "${HEAD_CONTAINER}" bash -lc "MODEL_ID='${MODEL_ID}' python3 -"

View File

@ -1,70 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
SUBMISSION_ID="${SUBMISSION_ID:-mvp_ppo_$(timestamp)_$RANDOM}"
# 为了让“输出目录 = submission id”默认把 JOB_TAG 也设成 SUBMISSION_ID可手动覆盖
JOB_TAG="${JOB_TAG:-${SUBMISSION_ID}}"
JOB_DIR="/mnt/shared/jobs/${SUBMISSION_ID}"
MODEL_ID="Qwen/Qwen2.5-0.5B-Instruct"
TRAIN_FILE="/mnt/shared/datasets/gsm8k/train.parquet"
VAL_FILE="/mnt/shared/datasets/gsm8k/test.parquet"
echo "[head] create job dir: ${JOB_DIR}"
dexec "${HEAD_CONTAINER}" bash -lc "mkdir -p ${JOB_DIR}/logs ${JOB_DIR}/checkpoints ${JOB_DIR}/config"
SUBMIT_CMD="python3 -m verl.trainer.main_ppo \
data.train_files=${TRAIN_FILE} \
data.val_files=${VAL_FILE} \
data.train_batch_size=256 \
data.max_prompt_length=512 \
data.max_response_length=512 \
actor_rollout_ref.model.path=${MODEL_ID} \
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=sglang \
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=${MODEL_ID} \
critic.ppo_micro_batch_size_per_gpu=4 \
algorithm.kl_ctrl.kl_coef=0.001 \
trainer.logger=console \
trainer.val_before_train=False \
trainer.n_gpus_per_node=4 \
trainer.nnodes=2 \
trainer.save_freq=10 \
trainer.test_freq=29 \
trainer.total_epochs=1 \
trainer.total_training_steps=29 \
trainer.resume_mode=disable \
trainer.default_local_dir=${JOB_DIR}/checkpoints \
+ray_kwargs.ray_init.address=auto \
hydra.run.dir=${JOB_DIR}/logs/hydra"
printf "%s\n" "${SUBMIT_CMD}" | dexec "${HEAD_CONTAINER}" bash -lc "cat > ${JOB_DIR}/config/submit_cmd.txt"
echo "[head] submit PPO via ray job submit (force driver on worker via entrypoint resources)"
SUBMIT_OUT="$(dexec "${HEAD_CONTAINER}" bash -lc "export HF_HOME=/mnt/shared/hf HUGGINGFACE_HUB_CACHE=/mnt/shared/hf/hub TRANSFORMERS_CACHE=/mnt/shared/hf/transformers HF_ENDPOINT=https://hf-mirror.com PYTHONUNBUFFERED=1; ray job submit --address=http://127.0.0.1:8265 --submission-id='${SUBMISSION_ID}' --entrypoint-num-cpus=1 --entrypoint-resources='{\"worker_node\": 1}' --runtime-env-json='{\"env_vars\":{\"HF_HOME\":\"/mnt/shared/hf\",\"HUGGINGFACE_HUB_CACHE\":\"/mnt/shared/hf/hub\",\"TRANSFORMERS_CACHE\":\"/mnt/shared/hf/transformers\",\"HF_ENDPOINT\":\"https://hf-mirror.com\",\"PYTHONUNBUFFERED\":\"1\"}}' --no-wait -- ${SUBMIT_CMD}")"
printf "%s\n" "${SUBMIT_OUT}"
printf "%s\n" "${SUBMIT_OUT}" | dexec "${HEAD_CONTAINER}" bash -lc "cat > ${JOB_DIR}/logs/ray_job_submit.out"
PARSED_SUBMISSION_ID="$(printf "%s\n" "${SUBMIT_OUT}" | sed -r 's/\x1b\\[[0-9;]*m//g' | grep -Eo "raysubmit_[A-Za-z0-9_-]+" | head -n 1 || true)"
if [[ -n "${PARSED_SUBMISSION_ID}" && "${PARSED_SUBMISSION_ID}" != "${SUBMISSION_ID}" ]]; then
echo "WARN: submission id mismatch: expected=${SUBMISSION_ID} parsed=${PARSED_SUBMISSION_ID}" >&2
fi
echo "${SUBMISSION_ID}" | dexec "${HEAD_CONTAINER}" bash -lc "cat > ${JOB_DIR}/config/ray_submission_id.txt"
echo "ray submission id: ${SUBMISSION_ID}"
echo "submitted. track via Ray dashboard: http://<host>:8265 (driver should be scheduled on a worker due to entrypoint resources)"
echo "job dir: ${JOB_DIR}"

View File

@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
echo "[head] ray status"
dexec "${HEAD_CONTAINER}" bash -lc "ray status || true"
echo "[head] ray jobs list (optional)"
dexec "${HEAD_CONTAINER}" bash -lc "ray job list --address=http://127.0.0.1:8265 || true"
LATEST_JOB_DIR="$(dexec "${HEAD_CONTAINER}" bash -lc "ls -1dt /mnt/shared/jobs/* 2>/dev/null | head -n 1 || true")"
if [[ -n "${LATEST_JOB_DIR}" ]]; then
echo "[host] latest job dir: ${LATEST_JOB_DIR}"
echo "[host] ray submission id (if exists):"
SUB_ID="$(dexec "${HEAD_CONTAINER}" bash -lc "cat ${LATEST_JOB_DIR}/config/ray_submission_id.txt 2>/dev/null || true")"
echo "${SUB_ID}"
if [[ -n "${SUB_ID}" ]]; then
echo "[head] ray job status:"
dexec "${HEAD_CONTAINER}" bash -lc "ray job status --address=http://127.0.0.1:8265 ${SUB_ID} || true"
echo "[head] ray job logs (tail):"
dexec "${HEAD_CONTAINER}" bash -lc "ray job logs --address=http://127.0.0.1:8265 ${SUB_ID} 2>/dev/null | tail -n 60 || true"
fi
fi

View File

@ -1,48 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../../../../" && pwd)"
COMPOSE_FILE="${ROOT_DIR}/src/mvp/v1/docker-compose.yaml"
HEAD_CONTAINER="mvp-ray-head"
WORKER0_CONTAINER="mvp-ray-worker-0"
WORKER1_CONTAINER="mvp-ray-worker-1"
dc() {
docker compose --project-directory "${ROOT_DIR}" -f "${COMPOSE_FILE}" "$@"
}
require_cmd() {
local cmd="$1"
command -v "${cmd}" >/dev/null 2>&1 || {
echo "missing required command: ${cmd}" >&2
exit 1
}
}
ensure_container_running() {
local name="$1"
if ! docker ps --format '{{.Names}}' | grep -qx "${name}"; then
echo "container not running: ${name}" >&2
exit 1
fi
}
dexec() {
local name="$1"
shift
ensure_container_running "${name}"
docker exec -i "${name}" "$@"
}
container_ip() {
local name="$1"
ensure_container_running "${name}"
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "${name}"
}
timestamp() {
date +"%Y%m%d_%H%M%S"
}

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"${SCRIPT_DIR}/01_up.sh"
"${SCRIPT_DIR}/10_install_verl_editable.sh"
"${SCRIPT_DIR}/20_start_head.sh"
"${SCRIPT_DIR}/21_start_workers.sh"
"${SCRIPT_DIR}/30_prepare_data_and_model.sh"
"${SCRIPT_DIR}/40_submit_ppo_epoch1.sh"
"${SCRIPT_DIR}/50_status.sh"