cleaning code
3
.gitignore
vendored
@ -5,5 +5,4 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.coverage
|
.coverage
|
||||||
htmlcov/
|
htmlcov/
|
||||||
AGENTS.md
|
|
||||||
2133
doc/arch.excalidraw
|
Before Width: | Height: | Size: 5.2 MiB After Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 5.2 MiB After Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 5.6 MiB After Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 5.6 MiB After Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 5.1 MiB After Width: | Height: | Size: 5.1 MiB |
|
Before Width: | Height: | Size: 5.8 MiB After Width: | Height: | Size: 5.8 MiB |
|
Before Width: | Height: | Size: 5.6 MiB After Width: | Height: | Size: 5.6 MiB |
279
doc/hl_design.md
@ -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,与现有工作对齐 |
|
|
||||||
@ -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 指标服务器
|
|
||||||
结构图:见附件
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,315 +0,0 @@
|
|||||||
# 关于MinIO + JuiceFS 在H20部署
|
|
||||||
|
|
||||||
这是一个架构设计的关键决策点。在 **H20 节点运行 K3s** 的前提下,我建议采取 **“存储组件独立部署(Outside),客户端插件化接入(Inside)”** 的混合策略。
|
|
||||||
|
|
||||||
简单来说:**MinIO 和 Redis 跑在 K3s 外面(用 Docker),JuiceFS 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
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 路径 B:JuiceFS 层面迁移(适合要换云厂商/存储类型)
|
|
||||||
|
|
||||||
如果你想顺便整理数据碎片,或者从 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 节点时,最好磁盘大小一致,网络环境一致。
|
|
||||||
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 65 KiB |
@ -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环境),以及RoCEv2(H20环境),需要怎样配置
|
|
||||||
|
|
||||||
@ -1,348 +0,0 @@
|
|||||||
# MVP Roadmap(V1 → V2 → … → 训练平台)
|
|
||||||
|
|
||||||
本文档在 `specs/mvp/milestones.md` 的草稿基础上做**扩展与细化**:把目标拆成可迭代的版本(MVP v1/v2/…),保证每个版本都能**独立运行、可验证验收**,并且在上一版本基础上演进。
|
|
||||||
|
|
||||||
> 总目标(North Star):产出一套**基于 Native Ray 集群(无 K8s 底座)**的训练平台,面向多用户,支持 `verl` 各类训练/评测/Serving 工作负载,提升集群利用率,并通过可观测系统实现资源统计、监控告警,最终形成运维 SOP 并可接入运维智能体做自动化运维。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. 关键原则(贯穿所有版本)
|
|
||||||
|
|
||||||
1) **版本可独立运行**:每个版本都能从“空环境”按文档跑起来(不依赖未来能力)。
|
|
||||||
2) **验收可客观验证**:每个里程碑必须有明确的 DoD(Definition 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(无 GPU,CPU=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.1(Hardening + 多 workload 可行性验证)
|
|
||||||
|
|
||||||
> 目标:把 v1 从“实验脚本”升级成“可长期回归的最小系统”,并验证更多 workload 的可行性边界。
|
|
||||||
|
|
||||||
### 2.1 主要能力
|
|
||||||
|
|
||||||
- Workload 扩展(可选顺序):
|
|
||||||
- PPO(回归金标)
|
|
||||||
- GRPO on Ray(可运行验证)
|
|
||||||
- SFT on Ray(可运行验证:`llamafactory` 或 `verl` 相关 SFT 路径)
|
|
||||||
- 作业模板化(最小实现):
|
|
||||||
- 统一 JobSpec(YAML/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 个 workload(GRPO 或 SFT)可以跑通 “启动→训练→落盘” 闭环
|
|
||||||
- 作业目录内包含:
|
|
||||||
- `config/submit_cmd.txt`(或 job spec 快照)
|
|
||||||
- `logs/`(可追踪)
|
|
||||||
- `checkpoints/`(按策略生成)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. MVP v2.0(Control Plane 服务化:API + Ray Jobs SDK)
|
|
||||||
|
|
||||||
> 目标:从“人跑脚本”升级为“服务提交任务”。依然是 Native Ray 集群,但引入一个最小控制平面服务。
|
|
||||||
|
|
||||||
### 3.1 系统形态
|
|
||||||
|
|
||||||
- Control Plane(建议部署在 head/CPU 机器):
|
|
||||||
- FastAPI 服务(REST)
|
|
||||||
- Job 管理:用 Ray Jobs **Python SDK** 提交/查询/停止(不再依赖 CLI 文本解析)
|
|
||||||
- 节点视图:读取 Ray state(nodes, actors, placement groups)
|
|
||||||
- Data Plane:
|
|
||||||
- 仍然是预先启动的 worker 节点加入集群(先不做 SSH 动态纳管也可)
|
|
||||||
|
|
||||||
### 3.2 API(MVP 级别)
|
|
||||||
|
|
||||||
- `POST /v1/jobs`:提交 JobSpec(ppo/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.1(SSH 纳管 + 资源池 + Gang 约束)
|
|
||||||
|
|
||||||
> 目标:对齐你草稿里“SSH 纳管”的约束与需求:控制面能纳管 GPU 节点,形成可运营的资源池。
|
|
||||||
|
|
||||||
### 4.1 节点纳管(SSH Provisioner)
|
|
||||||
|
|
||||||
- 控制面保存 NodeSpec(ip/user/port/labels/gpu_count)
|
|
||||||
- 通过 SSH 执行:
|
|
||||||
- `ray start --address=<head>:6379 --resources=...`
|
|
||||||
- `ray stop`(drain/下线)
|
|
||||||
- 维护节点状态机:`pending → online → draining → offline`
|
|
||||||
|
|
||||||
### 4.2 资源池与 gang(All-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 个 worker(SSH 注入 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 多用户最小闭环
|
|
||||||
|
|
||||||
- 认证(MVP):token 或 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 列表与输出目录
|
|
||||||
- 超时策略可触发(模拟短 timeout),job 被停止且状态标记为 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
|
|
||||||
- Dashboard:Grafana(至少 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.0(Serving 与 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)
|
|
||||||
|
|
||||||
- 确认环境是否支持 IB(H100)或 RoCEv2(H20)
|
|
||||||
- 跑最小 NCCL 通信验证(all-reduce / bandwidth)
|
|
||||||
- 将必要的 NCCL 环境变量注入到 job runtime_env
|
|
||||||
|
|
||||||
### 10.2 Ray + 多节点容器约束
|
|
||||||
|
|
||||||
- 多容器同宿主机时的 Ray node_ip/临时目录冲突规律(已踩坑,需固化规范)
|
|
||||||
- 端口范围与防火墙策略(Ray worker 端口、dashboard、metrics)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. 已确认的约束与假设(来自讨论结论)
|
|
||||||
|
|
||||||
这些会直接影响 v2.1(SSH 纳管)与后续多用户/存储设计:
|
|
||||||
|
|
||||||
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 验收时明确验证)。
|
|
||||||
@ -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 重启或网络抖动时,集群具备自我修复能力,无需人工干预。
|
|
||||||
@ -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 entrypoint(worker 上执行)
|
|
||||||
ray_job_tool.py # Ray Jobs SDK 封装(submit/status/stop/logs)
|
|
||||||
runtime_env.py # 统一 PYTHONPATH/runtime_env 组装逻辑
|
|
||||||
|
|
||||||
service/ # 服务层:FastAPI + scheduler + sqlite(应用)
|
|
||||||
__init__.py
|
|
||||||
app.py
|
|
||||||
scheduler.py
|
|
||||||
db.py
|
|
||||||
config.py # service 相关配置读取(从 configs 读取)
|
|
||||||
ray_resources.py
|
|
||||||
|
|
||||||
cli/ # 命令行/SDK 提交入口(由现成 v1.1 run.py 迁移而来)
|
|
||||||
__init__.py
|
|
||||||
run.py # submit/status/logs/stop 等 action
|
|
||||||
|
|
||||||
server.py # uvicorn 入口(导入 argus.service.*)
|
|
||||||
|
|
||||||
configs/
|
|
||||||
dev.yaml # RayConfig + ServiceConfig(按层次组织、可扩展)
|
|
||||||
prod.yaml # (可选)生产配置模板
|
|
||||||
|
|
||||||
taskspecs/ # 原 jobspecs/,改名 TaskSpecs(训练语义规范)
|
|
||||||
ppo.yaml
|
|
||||||
grpo.yaml
|
|
||||||
sft.yaml
|
|
||||||
README.md # TaskSpec 字段解释、示例
|
|
||||||
|
|
||||||
scripts/ # 宿主机脚本(docker exec/compose 编排)
|
|
||||||
lib.sh
|
|
||||||
00_prereq_check.sh
|
|
||||||
01_up.sh / 02_down.sh
|
|
||||||
20_start_head.sh / 21_start_workers.sh
|
|
||||||
30_prepare_data_and_model.sh
|
|
||||||
40_submit_cli.sh # 通过 cli/run.py 提交 TaskSpec
|
|
||||||
60_start_api.sh # 启动 API(service)
|
|
||||||
61_stop_api.sh
|
|
||||||
62_status_api.sh
|
|
||||||
|
|
||||||
docker-compose.yaml # dev 环境 compose(从 v1.1 迁移到这里,路径稳定)
|
|
||||||
README.md # 总入口文档(运行方式、目录约定)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.1 关键点:库 vs 应用边界
|
|
||||||
|
|
||||||
- `argus.ray` 是唯一的 Ray submitter 库(替代当前 v1.1/v2.0 的 `mvp_v11` 双份拷贝)。
|
|
||||||
- `argus.service` 依赖 `argus.ray`,不反向依赖。
|
|
||||||
- `argus.cli` 依赖 `argus.ray`,用于脚本化提交/调试。
|
|
||||||
|
|
||||||
### 3.2 TaskSpecs vs RayConfig
|
|
||||||
|
|
||||||
- `taskspecs/*.yaml`:描述训练任务语义参数(workload、nnodes、n_gpus_per_node、数据/模型路径、训练步数等)。
|
|
||||||
- `configs/*.yaml`:描述 Ray 提交环境(address、entrypoint_resources、runtime_env 以及 service 配置)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 配置策略(重构后如何组织 configs)
|
|
||||||
|
|
||||||
### 4.1 建议的 config 分层
|
|
||||||
|
|
||||||
把当前 `dev.yaml` 的内容明确分为两段(按模块名分段):
|
|
||||||
|
|
||||||
1) `ray:`(RayConfig)
|
|
||||||
- job server address
|
|
||||||
- shared_root(`/private`)
|
|
||||||
- entrypoint resources(强制 driver 落 worker)
|
|
||||||
- runtime_env env_vars(HF cache、PYTHONPATH 注入策略)
|
|
||||||
|
|
||||||
2) `service:`(ServiceConfig)
|
|
||||||
- api host/port
|
|
||||||
- auth token_env
|
|
||||||
- sqlite db_path
|
|
||||||
- scheduler tick/max_running/retry_interval
|
|
||||||
|
|
||||||
示例(结构示意):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
ray:
|
|
||||||
address: "http://127.0.0.1:8265"
|
|
||||||
shared_root: "/private"
|
|
||||||
entrypoint_num_cpus: 1
|
|
||||||
entrypoint_resources:
|
|
||||||
worker_node: 1
|
|
||||||
runtime_env:
|
|
||||||
env_vars:
|
|
||||||
HF_ENDPOINT: "https://hf-mirror.com"
|
|
||||||
PYTHONUNBUFFERED: "1"
|
|
||||||
user_code_path: "/private/user/code"
|
|
||||||
|
|
||||||
service:
|
|
||||||
api:
|
|
||||||
host: "0.0.0.0"
|
|
||||||
port: 8080
|
|
||||||
auth:
|
|
||||||
token_env: "MVP_INTERNAL_TOKEN"
|
|
||||||
sqlite:
|
|
||||||
db_path: "/private/common/db/mvp.sqlite3"
|
|
||||||
scheduler:
|
|
||||||
tick_s: 5
|
|
||||||
retry_interval_s: 60
|
|
||||||
max_running_tasks: 1
|
|
||||||
```
|
|
||||||
|
|
||||||
> 迁移期可以支持“旧格式”(v1.1 顶层字段 + v2: 段)与“新格式”(ray:/service: 两段)并存:解析时兼容读取,降低一次性改动风险;但最终以新格式为准。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 迁移路径(推荐分两阶段实施)
|
|
||||||
|
|
||||||
### 阶段 A:先拷贝/迁移现成文件,再做最小调整(保持行为不变)
|
|
||||||
|
|
||||||
目标:不改功能、不改 API 行为。优先通过“拷贝/迁移现成文件 + 修正包引用/路径”完成重构,避免重头重写逻辑(降低出错概率)。
|
|
||||||
|
|
||||||
建议步骤:
|
|
||||||
|
|
||||||
1) 抽出 `src/mvp/py/argus/ray/`(由现成代码迁移)
|
|
||||||
- 将 `src/mvp/v1.1/py/mvp_v11/` 迁移到 `src/mvp/py/argus/ray/`,并把它作为 submitter core 的唯一真源(不再保留一份复制品在其它目录)。
|
|
||||||
- 只做机械化调整:修正 import、修正默认路径常量(例如 tool code path / working dir)、修正 scripts 的调用路径。
|
|
||||||
|
|
||||||
2) 抽出 `src/mvp/py/argus/service/`(由现成代码迁移)
|
|
||||||
- 将 `src/mvp/v2.0/py/mvp_v2/` 迁移到 `src/mvp/py/argus/service/`。
|
|
||||||
- service 侧对 submitter 的依赖统一改为 `src/mvp/py/argus/ray/`(不再引用 `src/mvp/v2.0/py/mvp_v11/` 的复制品)。
|
|
||||||
|
|
||||||
3) CLI 统一入口:`src/mvp/py/argus/cli/run.py`(由现成代码迁移)
|
|
||||||
- 将 `src/mvp/v1.1/py/run.py` 迁移到 `src/mvp/py/argus/cli/run.py`,保留 action 语义(submit/status/logs/stop)。
|
|
||||||
- 仅调整 import 与默认路径,使其指向新目录(configs/taskspecs/py)。
|
|
||||||
|
|
||||||
4) scripts 合并(以 v1.1 为基、合入 v2 API)
|
|
||||||
- 将 `src/mvp/v1.1/scripts/` 迁移到 `src/mvp/scripts/`(Ray 集群编排最成熟)。
|
|
||||||
- 将 `src/mvp/v2.0/scripts/` 的 API 启停脚本合入 `src/mvp/scripts/`,并统一命名与默认路径。
|
|
||||||
|
|
||||||
5) docker-compose / mounts 稳定化(你已确认要迁移)
|
|
||||||
- 将 `src/mvp/v1.1/docker-compose.yaml` 迁移为 `src/mvp/docker-compose.yaml`。
|
|
||||||
- 容器内挂载统一:宿主机 `.../src/mvp/` → 容器 `/workspace/mvp/`(包含 `py/ configs/ taskspecs/ scripts/`)。
|
|
||||||
- runtime_env 的 `PYTHONPATH` 注入统一指向 `/workspace/mvp/py`(不再出现 `/workspace/mvp/v1.1/py`、`/workspace/mvp/v2/...`)。
|
|
||||||
|
|
||||||
阶段 A 完成标准:
|
|
||||||
- 原来 v1.1 的 CLI 提交方式仍可用(提交 PPO/GRPO/SFT)。
|
|
||||||
- v2 API 仍可用(队列、取消、日志)。
|
|
||||||
- 不再存在 `mvp_v11` 的重复目录。
|
|
||||||
|
|
||||||
### 阶段 B:配置格式升级(按模块两段)+ TaskSpecs 更名落地
|
|
||||||
|
|
||||||
目标:把 jobspec 真正改名为 TaskSpec,并把 config 升级为按模块两段(`ray:`/`service:`)清晰分层。
|
|
||||||
|
|
||||||
建议步骤:
|
|
||||||
- `jobspecs/` → `taskspecs/`,并更新 README/脚本引用。
|
|
||||||
- `dev.yaml` 从“顶层字段 + v2:”迁移为“ray:/service:”两段。
|
|
||||||
- 保留一段时间的兼容解析逻辑(读取旧格式时发出 warning)。
|
|
||||||
- 完成验证后删除旧版本目录:`src/mvp/v1.1/`、`src/mvp/v2.0/`(以及远端对应目录),确保新结构成为唯一入口。
|
|
||||||
|
|
||||||
阶段 B 完成标准:
|
|
||||||
- docs 里不再出现 “jobspec” 词汇,统一称 “TaskSpec”。
|
|
||||||
- `configs/dev.yaml` 分层清晰(`ray:`/`service:` 两段按模块名),服务与 Ray 的配置互不干扰。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 重构后的验证与执行方案(验收/回归)
|
|
||||||
|
|
||||||
### 6.1 本地(仓库内)静态验证
|
|
||||||
|
|
||||||
1) import / 入口检查(在容器环境)
|
|
||||||
- `python3 -c "from argus.ray.ray_job_tool import RayJobTool"`
|
|
||||||
- `python3 -c "from argus.service.app import create_app"`
|
|
||||||
|
|
||||||
2) 目录结构检查
|
|
||||||
- 确保 `src/mvp/py` 是唯一 python 代码根
|
|
||||||
- 确保 `taskspecs/`、`configs/`、`scripts/` 都在 `src/mvp/` 下
|
|
||||||
|
|
||||||
### 6.2 dev 环境(argus@h1)端到端验证
|
|
||||||
|
|
||||||
前置:
|
|
||||||
- 远端目录:`argus@h1:/home2/argus/infra/mvp/`(维持不变)
|
|
||||||
- 共享目录:`/home2/argus/infra/mvp/shared` 挂载到容器 `/private`
|
|
||||||
|
|
||||||
验证步骤(推荐顺序):
|
|
||||||
|
|
||||||
1) 重新拉起容器 + 启动 Ray
|
|
||||||
- `scripts/01_up.sh`
|
|
||||||
- `scripts/20_start_head.sh`(head `--num-cpus=0 --num-gpus=0`)
|
|
||||||
- `scripts/21_start_workers.sh`(workers 带 `worker_node` 资源)
|
|
||||||
- `ray status` / `ray list nodes` 确认 head 无 GPU、workers 各 4 GPU
|
|
||||||
|
|
||||||
2) CLI 提交回归(等价 v1.1)
|
|
||||||
- 提交 `taskspecs/sft.yaml`,确认成功
|
|
||||||
- 提交 `taskspecs/grpo.yaml`,确认成功
|
|
||||||
- 提交 `taskspecs/ppo.yaml`,确认成功
|
|
||||||
|
|
||||||
3) API 服务回归(等价 v2.0)
|
|
||||||
- `scripts/60_start_api.sh`
|
|
||||||
- `POST /api/v2/tasks`(raw YAML TaskSpec)
|
|
||||||
- `GET /api/v2/tasks/{task_id}`:确认返回 `created_at/updated_at`
|
|
||||||
- `POST /api/v2/tasks/{task_id}:cancel`:确认任务 `state=CANCELED` 且 attempt `ray_status=STOPPED`(服务侧语义一致)
|
|
||||||
|
|
||||||
4) 队列行为回归
|
|
||||||
- 在 `service.scheduler.max_running_tasks=1` 下:
|
|
||||||
- 连续提交两个 8-GPU 任务:第二个应保持 `QUEUED/PENDING_RESOURCES`,直到第一个结束后自动提交。
|
|
||||||
|
|
||||||
验收标准:
|
|
||||||
- 三种 workload(PPO/GRPO/SFT)都能通过 CLI 跑通(或至少能正确提交到 Ray 并进入 RUNNING)。
|
|
||||||
- API 提交/查询/取消/日志正常。
|
|
||||||
- “cancel 后 state=CANCELED 但 attempt 仍 RUNNING”的不一致问题不再出现。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 风险与注意事项
|
|
||||||
|
|
||||||
1) PYTHONPATH 注入路径变化
|
|
||||||
- 当前 runtime_env 里有 `MVP_TOOL_CODE_PATH=/workspace/mvp/v1.1/py` 的历史路径假设;
|
|
||||||
- 重构后需统一为 `/workspace/mvp/py`,并确保所有容器都挂载到相同路径。
|
|
||||||
|
|
||||||
2) SQLite WAL 在 NFS 上的稳定性
|
|
||||||
- 目前 db 使用 WAL(生成 `-wal/-shm`),NFS 下可能有一致性风险;
|
|
||||||
- 可作为后续优化:检测 NFS 时退回 `journal_mode=DELETE` 或换成单机本地盘。
|
|
||||||
|
|
||||||
3) 渐进迁移的兼容期
|
|
||||||
- 迁移期可以短暂保留旧路径(例如 `src/mvp/v1.1`、`src/mvp/v2.0` 仅保留 README 指向新路径)以减少一次性断裂;但你已确认最终会删除这两个目录,因此需要在 scripts/文档同步更新后尽快清理。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 已确认约束(将按此实施)
|
|
||||||
|
|
||||||
你已明确:
|
|
||||||
|
|
||||||
1) `docker-compose.yaml` 必须迁移到 `src/mvp/` 根;重构完成后 `src/mvp/v1.1`、`src/mvp/v2.0` 都会删除。
|
|
||||||
2) `configs/dev.yaml` 升级为两段,按模块名分段:`ray:` 与 `service:`;并保持纯 YAML 风格(不混用 JSON inline map)。
|
|
||||||
3) TaskSpec 字段先保持与现有 v1.1 YAML 完全一致(仅目录与命名变化),避免引入不必要的不兼容。
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
|
|
||||||
# v3.6
|
|
||||||
wandb 映射目录是/vol 固定问题:
|
|
||||||
查过官方文档/公开资料后结论是:wandb/local(W&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(服务端就无需任何改造)。
|
|
||||||
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
# MVP v1.1 计划(Hardening + 多 Workload 可行性验证)
|
|
||||||
|
|
||||||
本目录是 `specs/mvp/v1/` 的下一步迭代:在 v1 已经跑通(Ray head + 2 worker,PPO on Ray,持久化落盘)的基础上,把它升级为**可长期回归**的最小系统,并扩展至少一个新 workload 的可行性闭环。
|
|
||||||
|
|
||||||
> v1.1 的目标不是做平台服务化(API/队列/多用户)——那是 v2/v3 的工作;v1.1 聚焦“工程化 + 可行性边界验证 + 可观测/可排障基础”。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. v1 基线回顾(已完成)
|
|
||||||
|
|
||||||
- 拓扑:1 head(无 GPU,CPU/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 基础配置(YAML):cluster 地址、entrypoint 资源约束、runtime_env 等
|
|
||||||
- 训练 JobSpec(YAML):workload 语义与训练参数
|
|
||||||
- 至少覆盖:`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 Ray(Ray-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 的 entrypoint(driver)默认不分配 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 保留 bash;submit 通过 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 DoD(GRPO + 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>/...`)
|
|
||||||
@ -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) 基础配置用 YAML,JobSpec 也用 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)
|
|
||||||
- 读取训练 JobSpec(YAML)
|
|
||||||
- 用 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 训练 JobSpec(JobSpec 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 的 entrypoint(driver)默认不分配 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`)
|
|
||||||
|
|
||||||
@ -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 粒度选择代码版本。
|
|
||||||
> - 存储只考虑 NFS:dev 环境我们自己 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 基础配置(YAML):address / entrypoint resources / runtime_env 等
|
|
||||||
2) 训练 JobSpec(YAML):workload 语义与训练参数(仍由 Hydra overrides 组织)
|
|
||||||
|
|
||||||
训练 JobSpec(YAML)至少包含:
|
|
||||||
|
|
||||||
- `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 扩展:GRPO(v1.1 新增闭环)
|
|
||||||
|
|
||||||
优先用与 PPO 相同入口 `python -m verl.trainer.main_ppo`,仅通过配置切换算法:
|
|
||||||
|
|
||||||
- `algorithm.adv_estimator=grpo`
|
|
||||||
- 其余保持最小可跑:`total_epochs=1`、`save_freq=10`
|
|
||||||
|
|
||||||
### 3.6 Workload 扩展:SFT on Ray(v1.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+workers),v1.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 的 parquet(schema 不同)。
|
|
||||||
|
|
||||||
#### 3.6.4 重要细节:SFT Ray Driver 不应依赖 GPU
|
|
||||||
|
|
||||||
在 `ray job submit` 模式下,我们的 entrypoint(driver)默认 **不会分配 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 DoD(GRPO + 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 透传(不写入日志明文)。
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
# MVP 计划(V1)
|
|
||||||
|
|
||||||
本文档目标:把当前“口述/实验记录”整理成**可复现、可验收**的 MVP 计划,并明确下一步最小闭环。
|
|
||||||
|
|
||||||
## 1. 背景与目标
|
|
||||||
|
|
||||||
我们要验证的最小闭环是:
|
|
||||||
|
|
||||||
1) 在“Head(CPU 容器)+ Worker(GPU 容器)”的 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 Head(Dashboard 端口 `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 个 Head(CPU 容器)
|
|
||||||
- 跑 `ray start --head --dashboard-host=0.0.0.0`
|
|
||||||
- 暴露 `8265` 给 Desktop/用户查看 Job 状态
|
|
||||||
- 1~2 个 Worker(GPU 容器)
|
|
||||||
- 跑 `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`。
|
|
||||||
@ -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 worker),head 不使用 nvidia runtime;worker0/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`,确保训练使用持久化缓存与数据路径。
|
|
||||||
@ -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_id(attempt 级 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` 到期后继续尝试提交。
|
|
||||||
@ -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.1:head 不跑训练;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+)。
|
|
||||||
- 完整 UI(v3+,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 默认单实例服务(单 writer),SQLite 足够。
|
|
||||||
- 生产环境若 NFS 上的 SQLite 有锁/性能风险,v2.1+ 再演进到 Postgres/Redis;v2.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 逻辑)
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
# MVP v2.5(Design)— 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 守护)的设计与验证流程。
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# 记录问题
|
|
||||||
1. task 、 submission id 里加上 user name
|
|
||||||
2. 补全端到端测试用例,各种正常和异常用例,边界情况测试
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
# MVP v2.5 开发/部署/验收标准
|
|
||||||
|
|
||||||
本文件定义 v2.5 的“可验证闭环”,确保每个里程碑可验收。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 开发交付物(Deliverables)
|
|
||||||
|
|
||||||
### 1.1 代码交付(建议)
|
|
||||||
|
|
||||||
- API Server 增强:user management + task 关联 user_id + 鉴权隔离
|
|
||||||
- SQLite schema 迁移:新增 users/tokens,tasks 增加 user_id
|
|
||||||
- Ray Head service discovery:head.json 写入与心跳刷新
|
|
||||||
- Worker bootstrap + watchdog:
|
|
||||||
- dev:以脚本方式提供(docker compose 场景)
|
|
||||||
- prod:以容器 command/entrypoint 方式可注入
|
|
||||||
|
|
||||||
### 1.2 文档交付
|
|
||||||
|
|
||||||
- 目录结构与 GPFS 路径约定
|
|
||||||
- API 文档(含用户与多租户隔离)
|
|
||||||
- 运维 SOP:head 重启、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 可见)
|
|
||||||
- A2:head 容器重启(IP 变化或 Ray 重启)后:
|
|
||||||
- head.json 更新
|
|
||||||
- worker watchdog 在 `T<=60s` 内自动重连
|
|
||||||
- A3:head 设置 `--num-gpus=0 --num-cpus=0`,训练 driver 不会跑到 head(可通过 Ray dashboard/日志验证)
|
|
||||||
|
|
||||||
### 3.2 User Management
|
|
||||||
|
|
||||||
- U1:admin 可创建用户并签发 token(token 仅返回一次)
|
|
||||||
- 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)
|
|
||||||
|
|
||||||
- T1:PPO/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 自动重连
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
# MVP v2.5 API 设计(User + Task + Queue)
|
|
||||||
|
|
||||||
v2.5 在 v2.0 API 基础上,新增 **User Management** 与多租户隔离。
|
|
||||||
|
|
||||||
约束:
|
|
||||||
- 仍使用内部 token(API 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`
|
|
||||||
|
|
||||||
Request(JSON):
|
|
||||||
```json
|
|
||||||
{"user_id":"alice","display_name":"Alice"}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
```json
|
|
||||||
{"user_id":"alice","state":"ACTIVE"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 为用户签发 token(admin)
|
|
||||||
|
|
||||||
`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。
|
|
||||||
|
|
||||||
@ -1,202 +0,0 @@
|
|||||||
# MVP v2.5 — Stateless Ray Node Pool 容器固化设计
|
|
||||||
|
|
||||||
目标:把 v2.5 的 **stateless pool(head 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 守护 watchdog;ray 节点崩溃或 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 自动拉起 watchdog;worker 最终重新加入集群
|
|
||||||
2) ray 节点崩溃(worker):
|
|
||||||
- `ray stop --force` 或 kill raylet
|
|
||||||
- 期望:watchdog 重新执行 `ray start ... --block`,worker 恢复
|
|
||||||
3) ray 节点崩溃(head):
|
|
||||||
- kill head ray 前台进程(由 watchdog 启动)
|
|
||||||
- 期望:supervisor 重启 head_watchdog;head 恢复并重写 head.json;workers 自动重连
|
|
||||||
|
|
||||||
### 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`)。
|
|
||||||
@ -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 提交与状态同步;
|
|
||||||
- 所有共享数据/状态统一落在 GPFS(dev 环境可先用 NFS),容器内路径统一为 `/private/`。
|
|
||||||
|
|
||||||
> 术语说明:文中“GPFS”代表生产共享存储;dev 环境可用 NFS,但容器内仍以 `/private/` 访问。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 目标与非目标
|
|
||||||
|
|
||||||
### 1.1 v2.5 目标(Must)
|
|
||||||
|
|
||||||
1) **User Management(最小多租户)**
|
|
||||||
- 支持创建/禁用用户;
|
|
||||||
- 为每个用户签发内部 token(API 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 SDK(JobSubmissionClient)。
|
|
||||||
|
|
||||||
### 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 Tool(Ray Client)
|
|
||||||
- VerlTaskSpec(TaskSpec 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.5:worker 自己从 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 cache(dev 复用)
|
|
||||||
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. TaskSpec(VerlTaskSpec 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 + TTL;watchdog polling。
|
|
||||||
|
|
||||||
2) **Ray head 重启后 job server URL 变化**
|
|
||||||
- 对策:head.json 内写入 `job_server_url`,Ray Job Tool 可读取该文件更新 address(v2.6 可做动态 reload)。
|
|
||||||
|
|
||||||
3) **Worker 重连期间任务波动**
|
|
||||||
- 对策:服务侧调度器对齐 verl 的资源 fail-fast;任务失败可归因并排队重试。
|
|
||||||
@ -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 个可验证闭环)
|
|
||||||
|
|
||||||
### M1:User 表/Token 表 + 基础鉴权(不影响现有内部 token 兼容)
|
|
||||||
|
|
||||||
**目标**
|
|
||||||
- 引入 user/token 的持久化与鉴权映射(token → user_id)。
|
|
||||||
- 兼容现有 `Authorization: Bearer <MVP_INTERNAL_TOKEN>` 的“单租户模式”,避免一次性破坏 v2.0 用法:
|
|
||||||
- v2.5 可以先支持两种 token 模式:
|
|
||||||
- legacy:环境变量 `MVP_INTERNAL_TOKEN`(全局单租户);
|
|
||||||
- user token:DB 内签发 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 管理 endpoints(admin scope)
|
|
||||||
- `configs/dev.yaml`:补充 admin token/env 相关配置(保持 YAML 风格)
|
|
||||||
|
|
||||||
**验收点**
|
|
||||||
- `v2.5_acceptance.md`:U1 可通过自动化 API 测试覆盖。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M2:Task 绑定 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 创建 task;user 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 测试覆盖。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M3:Jobs 输出目录按 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/单测覆盖。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M4:Stateless Ray Node Pool(head.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) 起 head(Ray head + API)
|
|
||||||
2) 启动 head publisher(写 head.json)
|
|
||||||
3) 起 2 个 worker(每个 4 GPU),worker 只跑 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 loop(polling + 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/tokens:DB schema + auth + API endpoints
|
|
||||||
- tasks:绑定 user_id + 权限隔离
|
|
||||||
- job_root:按 user jobs 输出目录派生(输入仍 common)
|
|
||||||
- discovery/watchdog:head.json + worker 自愈
|
|
||||||
|
|
||||||
### 3.2 scripts(dev 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(多租户)
|
|
||||||
- 方案 B:v2.5 直接切换到 user token(破坏兼容,但更清晰)
|
|
||||||
|
|
||||||
2) **调度公平性**
|
|
||||||
- v2.5 先全局 FIFO(简单);后续 v3 再引入 per-user 公平调度/配额。
|
|
||||||
|
|
||||||
3) **head.json 的生产写入者**
|
|
||||||
- 方案 A:与 API 同进程线程(最少组件)
|
|
||||||
- 方案 B:独立进程(更独立、易运维)
|
|
||||||
|
|
||||||
@ -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-1:v2.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 nodes(head+2 workers),GPU 总数 8。
|
|
||||||
- 3 个 task 最终为 `SUCCEEDED`。
|
|
||||||
- 输出目录存在且按用户隔离:
|
|
||||||
- `/private/users/<uid>/jobs/<ray_submission_id>/{config,logs,checkpoints,debug}`
|
|
||||||
|
|
||||||
### HP-2:Driver 不在 head 跑
|
|
||||||
|
|
||||||
验证点(任选一种):
|
|
||||||
- Ray job 的 driver node IP 不等于 head 容器 IP;
|
|
||||||
- 或日志/调度信息显示 entrypoint_resources 生效(driver 在 worker)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 异常用例(Error Cases)
|
|
||||||
|
|
||||||
### E-Auth-1:缺 token
|
|
||||||
|
|
||||||
请求:
|
|
||||||
- `GET /api/v2/queue` 不带 `Authorization` 头
|
|
||||||
|
|
||||||
期望:
|
|
||||||
- 返回 401(missing bearer token)
|
|
||||||
|
|
||||||
### E-Auth-2:无效 token
|
|
||||||
|
|
||||||
请求:
|
|
||||||
- `Authorization: Bearer <random>`
|
|
||||||
|
|
||||||
期望:
|
|
||||||
- 返回 401(invalid token)
|
|
||||||
|
|
||||||
### E-Auth-3:用户禁用后拒绝访问
|
|
||||||
|
|
||||||
步骤:
|
|
||||||
1) admin 创建用户 `bob` 并签发 token
|
|
||||||
2) admin 禁用 `bob`
|
|
||||||
3) 用 bob token 请求 `/api/v2/queue`
|
|
||||||
|
|
||||||
期望:
|
|
||||||
- 返回 403(user disabled)
|
|
||||||
|
|
||||||
### E-Isolation-1:跨用户访问 task 资源(不泄露存在性)
|
|
||||||
|
|
||||||
步骤:
|
|
||||||
1) alice 提交 task 得到 `task_id`
|
|
||||||
2) bob 查询 `/api/v2/tasks/{task_id}`
|
|
||||||
|
|
||||||
期望:
|
|
||||||
- 返回 404(task not found)
|
|
||||||
|
|
||||||
### E-Input-1:输入路径不在 /private/common(v2.5 约束)
|
|
||||||
|
|
||||||
请求:
|
|
||||||
- 提交 taskspec 但 `train_file` 或 `code_path` 不以 `/private/common/` 开头
|
|
||||||
|
|
||||||
期望:
|
|
||||||
- 返回 400,并给出具体字段错误(例如 `train_file must start with /private/common/`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 边界用例(Boundary)
|
|
||||||
|
|
||||||
### B-Queue-1:资源不足时不提交 Ray(PENDING_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-1(best-effort)
|
|
||||||
|
|
||||||
@ -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 地址,自动发现并连接/自愈。
|
|
||||||
- 保持 **TaskSpec(v1.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 id(attempt)规则:`<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-path(PPO/GRPO/SFT)完成,且异常/边界用例验证通过(鉴权、跨用户隔离、输入校验、资源不足转 PENDING_RESOURCES、取消任务等)。
|
|
||||||
|
|
||||||
## 已知问题与后续建议
|
|
||||||
|
|
||||||
- `max_running_tasks=1` 会让队列中的任务在前序 RUNNING 时保持 QUEUED,这在“资源不足”边界测试里需要显式清空/取消前序任务,或接受该行为作为设计的一部分。
|
|
||||||
- 当前仍是 SQLite 单点;后续若要 HA/水平扩展,可在 v2.6+ 引入更强的持久化与多副本(例如 Postgres/etcd)。
|
|
||||||
- API server / watchdog 目前以脚本方式守护;后续可进一步统一为 systemd/supervisor(或平台侧守护)并补齐健康检查与告警。
|
|
||||||
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
# MVP v3.0(Design)— WebUI + 用户数据上传/下载(SFTPGo)→ 首个可发布版本
|
|
||||||
|
|
||||||
本目录基于:
|
|
||||||
- `specs/mvp/mvp_roadmap_v2.md`(总体路线图)
|
|
||||||
- `specs/mvp/image/roadmap_v3.0.png`(v3.0 迭代图)
|
|
||||||
- 当前已落地的 v2.5(User 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`:实施进展记录(每个里程碑完成后追加记录)。
|
|
||||||
@ -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 server(head 容器内)
|
|
||||||
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` 的 task(404)
|
|
||||||
2) `bob` 不能提交引用 `/private/users/alice/...` 的 TaskSpec(400/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)
|
|
||||||
@ -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)。
|
|
||||||
@ -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 Management(SFTPGo)**
|
|
||||||
- 提供用户上传/下载入口(SFTP 为主)。
|
|
||||||
- 数据落到 GPFS(dev 环境 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 Server(FastAPI)**
|
|
||||||
- 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 新增组件:SFTPGo(Data 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-delete):job 结束后 **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 API:SFTPGo 只是用户访问协议层(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)
|
|
||||||
|
|
||||||
**方案 A(v3.0 推荐):API Server 负责“联动创建 SFTPGo 用户”**
|
|
||||||
- 在 v2.5 的 `POST /api/v2/users` 成功后:
|
|
||||||
- API server 调用 SFTPGo 管理 API 创建同名用户
|
|
||||||
- 设置 home dir = `/private/users/<user_id>`
|
|
||||||
- 设置权限(默认可写;是否只读可配置)
|
|
||||||
- 认证方式:
|
|
||||||
- v3.0 最小可用:用户名+密码(确认:v3.0 先 password;API 生成一次性密码,用户首次登录后要求改密)
|
|
||||||
- 或:SSH public key(WebUI 允许上传 public key,API 写入 SFTPGo)
|
|
||||||
|
|
||||||
**方案 B(更强但复杂):SFTPGo 外部认证**
|
|
||||||
- SFTPGo 把认证委托给 API server(token/SSO),SFTP 也走内部 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、state(QUEUED/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`
|
|
||||||
- 明确 retention:jobs 结束后 3 天移入回收站,回收站 7 天后删除;重要文件请移到 `models/` 或 `datasets/`
|
|
||||||
7) **(仅管理员可见)用户管理页**(`/ui/admin/users`,可选但很有价值)
|
|
||||||
- 创建用户、禁用用户、签发 token、重置 SFTP 密码(方案 A)
|
|
||||||
|
|
||||||
### 5.2 页面组织与导航(建议)
|
|
||||||
侧边栏导航(普通用户):
|
|
||||||
- Tasks(列表)
|
|
||||||
- New Task(新建)
|
|
||||||
- Data(SFTP/目录说明)
|
|
||||||
|
|
||||||
管理员侧边栏额外增加:
|
|
||||||
- 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 后台线程**。
|
|
||||||
@ -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.5(Task 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** 方案(方案 A:API 联动创建/管理 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 个可验证闭环)
|
|
||||||
|
|
||||||
### M1:TaskSpec 路径策略升级(允许 user datasets/models;code_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 测试覆盖。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M2:SFTPGo 集成(方案 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 类(上传闭环前置)覆盖。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M3:WebUI(最小可用,多页面 + 侧边栏)
|
|
||||||
|
|
||||||
**目标**
|
|
||||||
- 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 页提示。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M4:Jobs Retention janitor(3 天移入 trash,7 天后 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` 拉起 Ray(head+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 dirs(jobs/trash)
|
|
||||||
- 所有删除/移动必须做 prefix 校验
|
|
||||||
|
|
||||||
2) **并发与竞态**
|
|
||||||
- janitor 只处理 terminal tasks,避免清理正在写入的目录
|
|
||||||
- move 使用同文件系统 `os.replace`(原子)
|
|
||||||
|
|
||||||
3) **SFTPGo 可用性**
|
|
||||||
- SFTPGo 不在线不应影响训练与 API 核心功能(除了用户创建联动)
|
|
||||||
- janitor 不依赖 SFTPGo(文件系统直连)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 交付清单(代码/配置/脚本/文档)
|
|
||||||
|
|
||||||
### 3.1 代码
|
|
||||||
- Path policy(v3.0)
|
|
||||||
- SFTPGoAdminClient + user create/disable/reset password 联动
|
|
||||||
- `/api/v2/me` 扩展(SFTP/目录/retention)
|
|
||||||
- WebUI 路由与静态资源
|
|
||||||
- janitor(trash+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 解释)
|
|
||||||
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
# MVP v3.0 进展记录(milestone log)
|
|
||||||
|
|
||||||
本文档用于记录 v3.0 按 `specs/mvp/v3.0/v3.0_dev_plan.md` 实施过程中的里程碑完成情况。
|
|
||||||
约定:每完成一个里程碑,追加一条记录,包含**日期**、**完成内容**、**涉及文件**、**验证方式/结果**、**待办/风险**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## M1:Path 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 表单/提示文本(避免用户填错路径)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## M2:SFTPGo 集成(方案 A:用户联动创建 + password)(已完成)
|
|
||||||
|
|
||||||
- 日期:2025-12-30
|
|
||||||
- 范围:SFTPGo(Data 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 天移入 trash,7 天 purge;janitor 在 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` 可写。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## M3:WebUI(最小可用,多页面 + 侧边栏)(已完成)
|
|
||||||
|
|
||||||
- 日期:2025-12-30
|
|
||||||
- 范围:API server 托管最小 WebUI(同源,不引入 Node 构建),用于登录/提交/查看任务与日志、查看 data 信息。
|
|
||||||
- 完成内容:
|
|
||||||
- 新增 UI 路由(HTML+少量 JS):
|
|
||||||
- `/ui`(重定向到 tasks)
|
|
||||||
- `/ui/login`:token 粘贴并写入浏览器 localStorage(key=`mvp_token`)
|
|
||||||
- `/ui/tasks`:任务队列列表(调用 `/api/v2/queue`)
|
|
||||||
- `/ui/tasks/new`:提交 TaskSpec YAML(POST `/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` 有弃用告警(不影响功能,可在后续清理)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## M4:Jobs Retention janitor(3 天移入 trash,7 天后 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` service(SFTP 2022;Admin 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 runner(HP-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`)。
|
|
||||||
@ -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 提交 TaskSpec(YAML),平台分配 `task_id`(可读、包含用户名)。
|
|
||||||
- `task_id` 对应内部状态机与重试逻辑;底层每次提交 Ray Job 会产生 attempt 与 `ray_submission_id`。
|
|
||||||
|
|
||||||
### 3.2 Ray Job(Ray 概念)
|
|
||||||
- 真正执行训练的 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 只读(方案 A:Virtual 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/Completed),Completed 支持分页
|
|
||||||
- `/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. API(v3.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` 提交 TaskSpec(YAML)
|
|
||||||
- `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. 持久化与清理
|
|
||||||
|
|
||||||
- 任务队列:sqlite(WAL 模式)
|
|
||||||
- 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 可用
|
|
||||||
- SFTPGo(admin + 普通用户)可用
|
|
||||||
- 通过 API 连续提交 PPO/GRPO/SFT 三种任务均能完成(SUCCEEDED)
|
|
||||||
- 用户可以登录 SFTPGo WebClient/SFTP,访问自己的目录,并访问 `/common/datasets`、`/common/hf` 的只读内容
|
|
||||||
|
|
||||||
同时本地单测通过:
|
|
||||||
- pytest 全绿
|
|
||||||
- 覆盖率阈值 >= 90%
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 已知限制 & 后续可改进
|
|
||||||
|
|
||||||
- WebUI 当前为最小版,交互与权限提示仍偏“工程化”而非产品化(后续可增强错误提示、搜索筛选、任务详情聚合等)。
|
|
||||||
- token 明文持久化仅适合内网/开发场景;生产建议改为一次性展示或支持撤销/轮换策略。
|
|
||||||
- SFTPGo 虚拟目录目前保留了历史遗留映射(例如 `/common/models` 可能残留),后续可在升级脚本中做一次性清理与迁移。
|
|
||||||
|
|
||||||
@ -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 Reward,Serving/IB/Resume/多版本 verl 暂缓)
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
1. node management(v3.5 引入的接口骨架:通过 SSH/平台能力管理 head/worker 节点生命周期;先做最小可用 --- 这个是干嘛的?
|
|
||||||
2.
|
|
||||||
@ -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怎样支持,需要哪些配置。
|
|
||||||
|
Before Width: | Height: | Size: 98 KiB |
@ -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. WebUI(New 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.0:head 发布 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 后续版本推进)。
|
|
||||||
|
|
||||||
@ -1,366 +0,0 @@
|
|||||||
# MVP v3.5 详细设计方案(进一步精简版,基于 v3.0)
|
|
||||||
|
|
||||||
> 背景:v3.0 已具备 WebUI + API server + 用户/任务隔离 + SFTPGo 数据管理 + Stateless Ray cluster(head + worker node pool)。
|
|
||||||
>
|
|
||||||
> v3.5 本轮 **只做 2 件事**:
|
|
||||||
> 1) Advanced Task:支持用户提交自定义训练命令(command)
|
|
||||||
> 2) Custom Reward:支持用户通过 VERL 原生 `custom_reward_function.*` 方式注入 reward(仅方式 A:用户自己写命令)
|
|
||||||
>
|
|
||||||
> 明确不做(从上一版设计中移除):(3) 自定义 verl 版本/代码路径、(4) 断点续训、(5) IB/RoCEv2 网络支持、(6) Model Serving。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. 继承 v3.0 的不变点(重要约束)
|
|
||||||
|
|
||||||
1) **Node management 不变**
|
|
||||||
- v3.5 不新增/不修改 node management 机制;仍按 v3.0 现状运行(head 写 discovery、worker watchdog 自动 join、自愈)。
|
|
||||||
|
|
||||||
2) **Head 不跑训练**
|
|
||||||
- 所有训练/Serving driver 通过 Ray entrypoint placement 强制落在 worker(例如 `entrypoint_resources={"worker_node": 1}`)。
|
|
||||||
|
|
||||||
3) **SFTPGo 的 “common” 目录约定变更**
|
|
||||||
- 不再使用 `$COMMON` 宏。
|
|
||||||
- 在 SFTPGo 中,把共享只读资源映射到用户 home 下的固定目录(用户在 SFTP/WebClient 看到的是 `$HOME/common/...`):
|
|
||||||
- `$HOME/common/datasets` → 容器内真实路径 `/private/datasets`(只读)
|
|
||||||
- `$HOME/common/hf` → 容器内真实路径 `/private/hf`(只读)
|
|
||||||
|
|
||||||
> 这里的 `$HOME` 指:`/private/users/<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 TaskSpec(ppo/grpo/sft)通过平台模板生成固定 overrides,适合“快速跑通”。
|
|
||||||
但科研/调参场景需要更高自由度:用户希望直接写 `python3 -m verl.trainer.main_ppo ...` 并自行控制每个 override。
|
|
||||||
|
|
||||||
### 2.2 Advanced TaskSpec(建议 schema)
|
|
||||||
|
|
||||||
建议新增一种 TaskSpec 类型,通过 `kind: advanced` 区分:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
kind: advanced
|
|
||||||
|
|
||||||
# 资源(平台调度与预检查用;仍需要)
|
|
||||||
nnodes: 2
|
|
||||||
n_gpus_per_node: 4
|
|
||||||
|
|
||||||
# 自定义命令(用户负责写对 VERL 的参数/路径)
|
|
||||||
# 平台会对 $HOME 做宏替换;其余保持原样
|
|
||||||
command: |
|
|
||||||
PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \
|
|
||||||
data.train_files=$HOME/datasets/gsm8k/train.parquet \
|
|
||||||
data.val_files=$HOME/datasets/gsm8k/test.parquet \
|
|
||||||
actor_rollout_ref.model.path=Qwen/Qwen2.5-0.5B-Instruct \
|
|
||||||
trainer.nnodes=2 \
|
|
||||||
trainer.n_gpus_per_node=4 \
|
|
||||||
trainer.total_epochs=1 \
|
|
||||||
trainer.save_freq=10 \
|
|
||||||
+ray_kwargs.ray_init.address=auto
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 `$HOME` 宏替换规则
|
|
||||||
|
|
||||||
仅支持 `$HOME`(v3.5 移除 `$COMMON`):
|
|
||||||
- `$HOME` → `/private/users/<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/...`
|
|
||||||
- 用户 code(reward):`$HOME/code/...`
|
|
||||||
|
|
||||||
> 这部分主要影响:
|
|
||||||
> - Advanced command 检查(allowlist)
|
|
||||||
> - WebUI/Data 页面文案(告诉用户共享数据在哪里)
|
|
||||||
|
|
||||||
> 兼容性建议:为了不影响 v3.0 期间已经习惯使用 `/private/common/datasets/...` 的用户/历史任务,
|
|
||||||
> v3.5 实现阶段建议 **同时接受**:
|
|
||||||
> - `/private/common/datasets/...`(旧路径语义,仍可读)
|
|
||||||
> - `/private/datasets/...`(真实路径语义,推荐)
|
|
||||||
> - Advanced command 里写的 `$HOME/common/datasets/...` 会先映射到 `/private/datasets/...`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 验收标准(精简版)
|
|
||||||
|
|
||||||
### 5.1 Advanced command
|
|
||||||
- 提交一个 Advanced PPO command(train/val 使用 `$HOME/common/datasets/...` 或 `$HOME/datasets/...`)
|
|
||||||
- 确认:
|
|
||||||
- 任务从 QUEUED → SUBMITTED/RUNNING
|
|
||||||
- driver 在 worker 上(head 不跑训练)
|
|
||||||
- 训练能正常跑至少若干 step
|
|
||||||
|
|
||||||
### 5.2 Custom reward(方式 A)
|
|
||||||
- 用户上传 `$HOME/code/reward.py`
|
|
||||||
- 在 command 中设置 `custom_reward_function.path=$HOME/code/reward.py`
|
|
||||||
- 确认训练日志出现 `using customized reward function ...`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 待确认问题(需要你拍板/补充)
|
|
||||||
|
|
||||||
1) Advanced command 的“强约束”是否需要更严格?
|
|
||||||
- 目前建议要求包含 `python3 -m verl.trainer.`,否则拒绝。
|
|
||||||
- 你是否允许用户跑非 verl 的命令(例如自定义评估脚本)?
|
|
||||||
|
|
||||||
2) `$HOME/common/datasets` 与 `$HOME/common/hf` 两个映射目录在平台侧是否需要“强制只读”语义?
|
|
||||||
- 例如:TaskSpec 校验允许读取但禁止写入(目前设计是 best-effort 字符串级校验)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 基于现有源码的改动点分析(实现清单)
|
|
||||||
|
|
||||||
本节按当前 v3.0 已上线的源码结构(`src/mvp/py/argus/...`)逐文件列出 v3.5 需要的具体改动点,并评估对现有能力的影响面。
|
|
||||||
|
|
||||||
### 7.1 TaskSpec/模型层(解析与兼容)
|
|
||||||
|
|
||||||
**现状**
|
|
||||||
- Basic TaskSpec 由 `argus.ray.models.JobSpec.from_dict()` 解析:`src/mvp/py/argus/ray/models.py`
|
|
||||||
- API `/api/v2/tasks` 直接 `JobSpec.from_dict(obj)`,并基于字段做路径校验:`src/mvp/py/argus/service/app.py`
|
|
||||||
- Scheduler 同样假定 jobspec_yaml 能解析为 `JobSpec`:`src/mvp/py/argus/service/scheduler.py`
|
|
||||||
|
|
||||||
**v3.5 需要新增**
|
|
||||||
1) 新增 `AdvancedTaskSpec` 数据结构(建议放在 `src/mvp/py/argus/ray/models.py`):
|
|
||||||
- 必填:`kind: advanced`、`workload`(建议仍要求 ppo/grpo/sft,用于 task_id 命名与 UI 分类)、`nnodes`、`n_gpus_per_node`、`command`
|
|
||||||
- 可选:`submission_id`(由服务层 override)
|
|
||||||
2) 新增 “union 解析”:
|
|
||||||
- 新增 `parse_taskspec(obj: dict) -> Basic(JobSpec) | Advanced(AdvancedTaskSpec)`
|
|
||||||
- 兼容策略:如果没有 `kind` 字段,则 **默认按 v3.0 Basic JobSpec 解析**(保证老客户端无感)。
|
|
||||||
|
|
||||||
### 7.2 Builder 层(把 TaskSpec 转为可执行 argv)
|
|
||||||
|
|
||||||
**现状**
|
|
||||||
- `src/mvp/py/argus/ray/builders.py:build_training_argv(spec: JobSpec, ...)` 只支持模板化 PPO/GRPO/SFT。
|
|
||||||
|
|
||||||
**v3.5 需要新增**
|
|
||||||
1) 新增 `build_advanced_argv(command: str) -> list[str]`
|
|
||||||
- 推荐实现:返回 `["bash", "-lc", "<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` 字段(可选增量迁移)。
|
|
||||||
@ -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 提交链路支持 Advanced(Builder/Tool/Scheduler)
|
|
||||||
**目标**
|
|
||||||
- Advanced spec 能进入 scheduler 队列并提交为 Ray job(driver 仍落 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 → SUBMITTED(mock 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 命令,需要调整规则并补测试(本轮默认拒绝)。
|
|
||||||
|
|
||||||
@ -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 里程碑进度记录
|
|
||||||
|
Before Width: | Height: | Size: 70 KiB |
@ -1,3 +0,0 @@
|
|||||||
1. 增加wandb功能
|
|
||||||
2. 增加evaluation 模板
|
|
||||||
|
|
||||||
@ -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 & Biases(W&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
|
|
||||||
|
|
||||||
对平台内置 workload(PPO/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”:
|
|
||||||
|
|
||||||
建议在 DB(sqlite)里新增 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 会默认多一个 logger(wandb),并对外发起 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) 启动/重启 compose(Ray 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)。
|
|
||||||
@ -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 现有配置解析。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M2:Ray 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;
|
|
||||||
- 或方案 B:fail-fast(返回 500/400)。
|
|
||||||
- 需在实现前确认采用哪种策略;建议 v3.6 选 A 提升可用性。
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
- 任意内置任务提交后,在 Ray job runtime_env 中能看到 `WANDB_*`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M3:内置训练任务自动开启 wandb logger(PPO/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)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M4:API 输出与 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&B;Task detail 能定位到 run。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### M5:New 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 提交
|
|
||||||
- 任务成功运行并输出 metrics(stdout/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 透传,必要时补充)
|
|
||||||
@ -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。
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
# MVP v3.6 迭代研发总结(基于 v3.5)
|
|
||||||
|
|
||||||
> 时间基线:2026-01(H20 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.5(WebUI + API server + Ray stateless node pool + SFTPGo + Advanced TaskSpec)基础上,新增两块能力:
|
|
||||||
|
|
||||||
1) **Weights & Biases(W&B)local 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 等参数)。
|
|
||||||
|
|
||||||
@ -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 + tracking(tracking 会初始化 wandb)。
|
|
||||||
veRL Documentation
|
|
||||||
+1
|
|
||||||
|
|
||||||
推荐写法(新版本):
|
|
||||||
|
|
||||||
trainer:
|
|
||||||
logger: ["console", "wandb"]
|
|
||||||
project_name: my_project # 用argus的用户名_project ,譬如 alice_project
|
|
||||||
experiment_name: exp_001 # 用 task id 作为实验名
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -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 的 TaskSpec(YAML)时 **不会显式携带 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 评审通过后,优先跑最小 PPO(epochs=1、steps=10)验证 vllm backend 能启动并完成。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 待确认问题(请你评审时确认)
|
|
||||||
已完成评审确认(见 §2.0),无额外待确认项。
|
|
||||||
@ -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 - 训练默认参数切换到 vllm(TDD)
|
|
||||||
|
|
||||||
**目的**:在不碰镜像/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
|
|
||||||
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
# MVP v3.7 迭代总结:切换 vLLM rollout + `verlai/verl:vllm011.latest`
|
|
||||||
|
|
||||||
> 基线版本:v3.6(W&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 429,PPO/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 的配置要求为准逐项调整(保持小步快跑)。
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 403 KiB |
@ -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 doesn’t support version, it’ll 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.
|
|
||||||
@ -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)共同抬起这台沉重的机器,每个人只负责自己那一部分的力气,从而让机器能够平稳地运转。
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
|
|
||||||
1. 通过ray serve(后端vllm)来动态拉起llm,支持多模型application部署,
|
|
||||||
2. 默认一个模型只有一个replica,用户配置可以多个
|
|
||||||
3. 用户可以删除(下线)模型
|
|
||||||
4. 可以指定模型用几张卡
|
|
||||||
5. 通过WebUI来进行配置,查看当前部署的模型列表,以及可以查看详情
|
|
||||||
6. 模型路径可以使用common,也可以用户自己指定user路径
|
|
||||||
7.
|
|
||||||
@ -1,224 +0,0 @@
|
|||||||
# MVP v3.8 API Reference(Serving)
|
|
||||||
|
|
||||||
> 说明:本节为 v3.8 新增的 **Model Serving** API(Ray 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 ServingSpec(YAML)
|
|
||||||
|
|
||||||
请求体建议使用 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. 管理 API(MVP API server)
|
|
||||||
|
|
||||||
### 2.1 Create / Upsert model
|
|
||||||
|
|
||||||
`POST /api/v2/serve/models`
|
|
||||||
|
|
||||||
#### Request
|
|
||||||
|
|
||||||
- Header: `Content-Type: application/yaml`
|
|
||||||
- Body: ServingSpec(YAML)
|
|
||||||
|
|
||||||
#### 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. 推理 API(Ray 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`)
|
|
||||||
@ -1,371 +0,0 @@
|
|||||||
# MVP v3.8 详细设计方案:Ray Serve(vLLM)模型动态部署与管理
|
|
||||||
|
|
||||||
> 基线:当前已具备 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 Serve(controller + 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 或 JSON(v3.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
|
|
||||||
- state(RUNNING/DEPLOYING/QUEUED/FAILED)
|
|
||||||
- 操作:Scale(修改 replicas)、Delete
|
|
||||||
|
|
||||||
### 7.2 Serving 创建/编辑页
|
|
||||||
|
|
||||||
两种模式(与 New Task 类似,先做 YAML 模式即可):
|
|
||||||
|
|
||||||
示例 YAML(v3.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=` 字段上自然可读。
|
|
||||||
@ -1,266 +0,0 @@
|
|||||||
# MVP v3.8 开发计划(TDD,细化版)
|
|
||||||
|
|
||||||
> 目标:在 v3.7 基础上引入 Ray Serve(vLLM)模型动态部署与管理(多模型单 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 管理 API(FastAPI)(单测驱动)
|
|
||||||
|
|
||||||
**目的**:先把管理 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_key:404
|
|
||||||
|
|
||||||
### M3.4 单测
|
|
||||||
- [ ] `test_app_serving_api.py`
|
|
||||||
- happy path:create → 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.yaml:H20)
|
|
||||||
- [ ] `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 不足:不 apply;state 仍 QUEUED;event 写入
|
|
||||||
- apply 抛异常:state FAILED;error_summary 写入
|
|
||||||
|
|
||||||
**验收**:
|
|
||||||
- reconciler 行为在纯单测环境可验证;失败可解释
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## M6 - 真实集成(h1):Ray 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 - WebUI(Serving 页面)(单测驱动)
|
|
||||||
|
|
||||||
**目的**:给用户可视化的模型管理页面(最小必要功能)。
|
|
||||||
|
|
||||||
### 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 主链路可操作
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
# MVP v3.8(变更)开发计划:Per-Model Serve App(TDD)
|
|
||||||
|
|
||||||
> 目标:按 `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 app:A/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,并在列表页给出明显提示
|
|
||||||
|
|
||||||
@ -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)
|
|
||||||
- 完成 M1:ServingSpec 解析/宏替换/路径校验 + 单测(`src/mvp/py/argus/service/serving_spec.py`)
|
|
||||||
- 完成 M2:SQLite 新增 `serve_models`/`serve_events` + Db API + 单测(`src/mvp/py/argus/service/db.py`)
|
|
||||||
- 完成 M3:FastAPI Serving 管理 API + 单测(`src/mvp/py/argus/service/app.py`)
|
|
||||||
- 完成 M4:ServeClient 抽象 + LLMConfig builder(dict 形态)+ 单测(`src/mvp/py/argus/service/serve_client.py`、`src/mvp/py/argus/service/serve_llm_config.py`)
|
|
||||||
- 完成 M5:Serving reconciler(状态机 + 资源预检查 + mock 单测)(`src/mvp/py/argus/service/serving_reconciler.py`)
|
|
||||||
|
|
||||||
### M6(h1 真实集成)
|
|
||||||
|
|
||||||
- `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`)
|
|
||||||
|
|
||||||
### M7(WebUI - 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`:可 import(Serve 基础可用)
|
|
||||||
- `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 ...` 可用。
|
|
||||||
@ -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+ 行单文件;每页的改动范围限定在对应模块。
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
ray:
|
|
||||||
# Ray Job server address (head 容器内视角)
|
|
||||||
address: "http://127.0.0.1:8265"
|
|
||||||
|
|
||||||
# 共享根路径(容器内统一 /private,对齐生产)
|
|
||||||
shared_root: "/private"
|
|
||||||
|
|
||||||
# 强制 driver 落 worker(head 不跑训练)
|
|
||||||
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
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
# MVP V1(Ray + 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 只能调度到 worker(head 没有该资源)
|
|
||||||
|
|
||||||
## 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 head(CPU=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`
|
|
||||||
@ -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
|
|
||||||
@ -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"
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
@ -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}"
|
|
||||||
|
|
||||||
@ -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}"
|
|
||||||
|
|
||||||
@ -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"
|
|
||||||
@ -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"
|
|
||||||
@ -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 -"
|
|
||||||
@ -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}"
|
|
||||||
@ -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
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
@ -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"
|
|
||||||
|
|
||||||