203 lines
10 KiB
Markdown
203 lines
10 KiB
Markdown
# 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`)。
|