# 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`）。
