# 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=: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`)。