在华为 Ascend 910B(Atlas A2)集群上跑 OpenClaw-RL 的 Hybrid Top-K Select 训练 (GRPO 在线 RL + On-Policy Distillation,对齐论文 arXiv:2603.10165), 以 GSM8K 个人助手多轮对话为场景,student/TA/teacher 三角色由模拟器驱动。
本仓库是从生产集群工作目录 /home/openclaw-ascend-work 整理出来的可复现版本:
剔除了模型权重、checkpoint、Node.js 运行时、镜像 tar 包和调试残留,保留全部代码与脚本。
📘 完整运行指导书见 docs/RUNBOOK.md:从裸机到训练出 loss 的 逐步操作手册,含每步验证方法、启动时序、参数手册和故障排查。本 README 只做概览。
┌─ 训练节点 (MASTER_ADDR, 8×910B) ──────────────────────────────┐
│ docker: openclaw-slime-ascend:a2-cann85-home │
│ ├─ Ray head (:6379) │
│ ├─ actor 训练 4 NPU Megatron-LM (TP2×DP2) │
│ ├─ rollout 推理 2 NPU SGLang → :30000 (OpenAI 兼容 API) │
│ └─ teacher 参考 2 NPU (--prm-teacher-load, 冻结) │
│ │
│ docker: openclaw-feeder(同一镜像) │
│ └─ sim_joint.py → openclaw agent --local → :30000 │
│ (student/TA/teacher 三角色,外部模拟器扮演用户) │
└───────────────────────────────────────────────────────────────┘
┌─ PRM 节点 (PRM_NODE, 8×910B) ─────────────────────────────────┐
│ docker: 同一镜像 → run_ray_prm_worker_inner.sh │
│ └─ ray worker 加入 MASTER:6379,注册自定义资源 prm_gpu×8 │
│ PRM SGLang 引擎(打分 + 生成 hint)被调度到这里 │
└───────────────────────────────────────────────────────────────┘
数据流:feeder 通过 openclaw CLI 向 :30000 发多轮对话 → 训练侧
openclaw_combine_api_server 抓取为 rollout 样本 → PRM 打分并生成 hint →
openclaw_topk_select_loss 计算 GRPO + Top-K OPD 混合 loss → Megatron 训一步 →
SGLang 同步新权重 → 下一轮 rollout(严格 on-policy)。
关键铁律:各节点文件系统完全独立,没有共享存储。 本仓库要在
每个参与节点 部署一份到相同路径 /home/openclaw-ascend-work。
改训练代码必须改「训练容器所在节点」的那份——容器挂载的是本机目录。
├── Dockerfile.slime-ascend-a2-openclaw # 镜像构建(见 §4)
├── run_qwen3_4b_openclaw_combine_topk_ascend_2nodes.sh # ★4B 2节点外层启动
├── run_qwen3_32b_openclaw_combine_topk_ascend_8nodes.sh # 32B 8节点外层启动
├── convert_qwen3_4b_hf_to_torch_dist_ascend.sh # HF→torch_dist 转换
├── run_openclaw_container.sh # 手动进容器调试用
├── check_openclaw_env.sh # NPU 环境自检
├── scripts/ # 容器内执行的 inner 脚本
│ ├── run_qwen3_4b_openclaw_combine_topk_ascend_2nodes_inner.sh # ★训练参数都在这
│ ├── run_ray_prm_worker_inner.sh # PRM 节点入口
│ └── ...(32B/1节点等其他变体)
├── OpenClaw-RL/ # 算法代码(上游 fork,只保留用到的部分)
│ ├── openclaw-combine/ # ★Top-K Select Hybrid:rollout/loss/API server
│ ├── openclaw-opd/ # OPD 蒸馏:opd api server(rep_penalty 注入点)
│ ├── openclaw-rl/ # 基础 RL:api server / rollout / oel eval
│ ├── Megatron-LM/ # vendor 的 Megatron(已含 slime NPU 补丁 + NaN-skip 修复)
│ └── slime/ # 上游自带 slime(镜像构建时 pip install,运行时不用)
├── slime-ascend-openclaw-full/ # ★SLIME_ROOT:训练框架(train_async.py 入口)
│ ├── slime/backends/megatron_utils/ # Megatron 后端适配层(LSE 修复在 loss.py)
│ ├── slime/backends/sglang_utils/ # SGLang 引擎适配层
│ └── docker/npu_patch/ # Ascend 补丁集(构建镜像时 git am)
├── patches/ # MindSpeed 运行时兼容补丁
├── sim_joint.py # ★GSM8K joint feeder(三角色模拟)
├── feed_forever_joint_sim.sh # feeder 常驻循环
├── make_gsm8k.py / GSM8K.json # 数据集(7473 题 train split)
└── .openclaw/ # openclaw CLI 配置
├── openclaw.json # agent 指向 :30000
└── extensions/rl-training-headers/ # 注入 X-Session-Id/X-Turn-Type 的插件
三个「Megatron」的关系(容易混淆):
| 位置 | 是什么 | 运行时是否生效 |
|---|---|---|
slime-ascend-openclaw-full/slime/backends/megatron_utils/ |
slime 的后端胶水层,不是 Megatron 本体 | ✅ |
OpenClaw-RL/Megatron-LM/ |
vendor 的完整 Megatron-LM,已含静态 NPU 补丁 | ✅ PYTHONPATH 优先,实际 import 的是它 |
镜像内 /workspace/Megatron-LM |
构建时 pip install -e 的一份 | ❌ 被 PYTHONPATH 遮蔽,仅兜底 |
真正的 Ascend 适配主力是 MindSpeed 运行时猴子补丁:
megatron_utils 入口处 import mindspeed.megatron_adaptor 会把当前 import 到的
megatron 包的 CUDA 实现动态替换为 NPU 实现。
- 2 台(4B)或 8 台(32B)Atlas A2 节点,每台 8×910B,aarch64
- 宿主机已装 Ascend 驱动/固件(
/usr/local/Ascend/driver、npu-smi可用) - Docker,节点间内网互通,launch 机到各节点 root SSH 免密(外层脚本靠 ssh 编排)
- 每节点磁盘预留:镜像 ~15G + 模型 ~10G + ckpt(4B 每份 ~50G,注意 save-interval)
镜像 openclaw-slime-ascend:a2-cann85-home,基于华为云
swr.cn-south-1.myhuaweicloud.com/ascendhub/cann:8.5.0-910b-ubuntu22.04-py3.11。
# build context 需包含 OpenClaw-RL/(Dockerfile 里 COPY 它)
cd /home/openclaw-ascend-work
docker build -f Dockerfile.slime-ascend-a2-openclaw \
-t openclaw-slime-ascend:a2-cann85-home \
--build-arg HTTP_PROXY=... --build-arg HTTPS_PROXY=... . # 无代理可不传构建内容(都钉了版本,不要随意升级):
torch 2.8.0 + torch_npu 2.8.0.post2 → SGLang v0.5.10(NPU 版)→
sgl-kernel-npu 预编译 wheel → mbridge/Megatron-Bridge(dev_rl)/Megatron-LM(3714d81d)/
MindSpeed(fc63de5c) → slime → 对五个仓库 git am 应用
slime-ascend-openclaw-full/docker/npu_patch/v0.2.4/ 补丁。
分发到其余节点(构建一次,到处 load):
docker save openclaw-slime-ascend:a2-cann85-home | zstd -T0 > openclaw-slime-ascend-a2-cann85-home.tar.zst
scp *.tar.zst <node>:/home/openclaw-ascend-work/
ssh <node> 'zstd -dc /home/openclaw-ascend-work/*.tar.zst | docker load'在每个参与节点上:
# 1. 代码放到固定路径(外层/inner 脚本都默认这个 WORK_ROOT)
git clone <本仓库> /home/openclaw-ascend-work
# 2. 下载模型(训练节点 + PRM 节点都要,路径必须一致)
# 4B: Qwen/Qwen3-4B-Thinking-2507 32B: Qwen/Qwen3-32B
huggingface-cli download Qwen/Qwen3-4B-Thinking-2507 \
--local-dir /home/openclaw-ascend-work/models/Qwen3-4B-Thinking-2507
# 3. HF → Megatron torch_dist 转换(只需在训练节点做一次)
HF_CKPT=/home/openclaw-ascend-work/models/Qwen3-4B-Thinking-2507 \
bash /home/openclaw-ascend-work/convert_qwen3_4b_hf_to_torch_dist_ascend.sh
# 产出 models/Qwen3-4B-Thinking-2507_torch_dist(inner 脚本的 REF_LOAD 默认值)
# 4. 环境自检:期望 npu_available True / npu_count 8
bash /home/openclaw-ascend-work/check_openclaw_env.sh在 launch 机(能 ssh 到两个节点即可,不占 NPU)执行:
cd /home/openclaw-ascend-work
CONTEXT_LENGTH=65536 ROLLOUT_MAX_CONTEXT_LEN=65536 \
ROLLOUT_TEMPERATURE=1.0 \
MASTER_ADDR=<训练节点IP> PRM_NODE=<PRM节点IP> \
HF_CKPT=/home/openclaw-ascend-work/models/Qwen3-4B-Thinking-2507 \
nohup bash run_qwen3_4b_openclaw_combine_topk_ascend_2nodes.sh \
> /tmp/openclaw_4b_topk_2nodes.log 2>&1 &外层脚本做的事:docker rm -f 清两节点旧容器 → PRM 节点起 ray worker 容器 →
训练节点起 master 容器跑 inner 脚本(ray head + 提交训练 job)。
启动后约 15–20 分钟 :30000 才可用(SGLang 加载 + NPU 图捕获 + Megatron 读 ckpt)。
关键参数(外层 -e 透传,inner 脚本给默认值):
| 参数 | 默认 | 说明 |
|---|---|---|
OPENCLAW_TOPK_K |
4 | Top-K 蒸馏候选数(论文 k) |
OPENCLAW_TOPK_MAX_CAND / PRM_M |
3 | hint 候选数(论文 M) |
OPENCLAW_TOPK_ADV_DIFF_CLIP |
1.0 | OPD 优势差裁剪 C |
OPENCLAW_TOPK_W_RL / W_OPD |
1.0 / 1.0 | 混合权重 |
OPENCLAW_TOPK_HINT_SELECTION |
sequence_optimal | hint 选择策略 |
OPENCLAW_ROLLOUT_REP_PENALTY |
1.1 | rollout 重复惩罚(掐断 thinking 死循环,必开) |
kl-loss-coef |
0(inner 硬编码) | 当前 KL 关闭;要开改 inner 脚本第 158 行 |
ROLLOUT_BATCH_SIZE |
16 | 每步训练的 turn 数 |
NUM_ROLLOUT |
— | 总步数 |
注意:rollout-max-response-len、rollout-batch-size 在外层脚本有 -e 覆盖,
改 inner 默认值不够,外层也要改。
当前 inner 脚本没有 --load:重启即 fresh(从 REF_LOAD 基座重新开始),不自动续训。
要续训需在 inner 脚本 CKPT_ARGS 加 --load "${SAVE_CKPT}",Megatron 会从
ckpt/qwen3-4b-openclaw-topk-select-ascend/ 的 latest iteration 恢复(详见 RUNBOOK §8.3)。
# 1. 用容器名删,不要 pkill -f 脚本名(模式会匹配到自己的 shell → 自杀 exit 255)
ssh <训练节点> 'docker rm -f openclaw-rl-qwen3-4b-topk-2nodes'
ssh <PRM节点> 'docker rm -f openclaw-rl-qwen3-4b-topk-2nodes'
# 2. 检查 NPU 孤儿进程(docker rm 杀容器但进程可能泄漏,会占死 NPU)
ssh <节点> 'npu-smi info | grep python' # 应为空,残留则 kill -9
ssh <节点> 'pgrep -f train_async' # 应为空feeder 在训练节点上跑一个同镜像的常驻容器,内部用 openclaw CLI 造多轮会话。
cd /home/openclaw-ascend-work
# Node.js ≥ 20 解压到 node/(aarch64 版),feeder 脚本用 $WORK/node/bin
wget https://npmmirror.com/mirrors/node/v22.11.0/node-v22.11.0-linux-arm64.tar.xz
tar xf node-*.tar.xz && mv node-v22.11.0-linux-arm64 node
# openclaw CLI 必须 2026.3.31 版(依赖 globalThis.fetch 行为与 --json 输出格式,勿 npm update)
# 国内环境用 npmmirror 源
./node/bin/npm config set registry https://registry.npmmirror.com
./node/bin/npm install -g openclaw@2026.3.31
# .openclaw/ 已在仓库里:openclaw.json 定义 rl-agent + provider(127.0.0.1:30000),
# extensions/rl-training-headers 插件负责注入 X-Session-Id / X-Turn-Type。
# 启动 feeder 容器时直接挂载为 /root/.openclaw(见下),无需手动拷贝。sim_joint.py 用外部大模型(火山方舟,glm-5.1)扮演学生/助教/老师三个 persona,
key 从环境变量读:
export ARK_API_KEY=<你的火山方舟 key>curl -s http://127.0.0.1:30000/v1/models && \
docker run -d --name openclaw-feeder --network host \
-v /home/openclaw-ascend-work:/home/openclaw-ascend-work \
-v /home/openclaw-ascend-work/.openclaw:/root/.openclaw \
-e ARK_API_KEY -e HOME=/root \
openclaw-slime-ascend:a2-cann85-home \
bash /home/openclaw-ascend-work/feed_forever_joint_sim.shfeeder 设计要点(详见 sim_joint.py 头部注释):
- 每个任务串行链 student → TA → teacher(TA 拿 student 真实答案,teacher 拿 TA 真实反馈),4 任务并发
- 每 role-session 用独立
OPENCLAW_STATE_DIR=/tmp/ocfeed/<id>隔离, 并 symlink extensions 进去(否则插件丢失 → X-Session-Id 不注入 → 样本全被丢弃) - 模拟器 看不到正确答案(不泄露 ground truth),persona 满意时输出 DONE 哨兵终止会话
- SGLang 权重同步期间返回 503:feeder 自带指数退避重试(15s 起,×6 次),属正常现象
# 训练指标(reward / loss / grad_norm 等)
grep -a 'model.py:681\|rollout.py:1227\|data.py:236' /tmp/openclaw_4b_topk_2nodes.log | tail -20
# feeder
docker logs -f openclaw-feeder健康值参考:
| 指标 | 健康区间 | 异常含义 |
|---|---|---|
train/grad_norm |
~2–5 | 飙到数十/数百 = 发散 |
train/train_rollout_logprob_abs_diff |
0.04–0.09 | 持续走高 = 策略漂移 |
OPD abs_mean |
< 1 | ≡1.0 = 优势差恒被 clip,饱和(见 §9) |
sel_overlap(teacher/student top-k 重叠) |
稳定不单调下降 | 单调崩 = OPD 方向错 |
| response_len 刷满 8192 占比 | ~0 | 大量刷满 = thinking 死循环,查 rep_penalty |
perf/step_time |
~2–4 min/step(4B) |
以下修复已包含在本仓库代码里,列出来是为了防止有人"顺手还原":
- LSE bug(发散的压底根因):
slime-ascend-openclaw-full/slime/backends/megatron_utils/loss.py_vocab_parallel_log_probs_at_indices计算 teacher logprob 曾漏减 all-reduce 的 max 项,导致ℓ_T系统性偏高 ~+30–42 nat → OPD 优势差恒 +1 饱和 → 熵崩发散。 已修(return gathered − log_sum_exp − local_max)。 - NaN 梯度跳过:
OpenClaw-RL/Megatron-LM/megatron/core/distributed/param_and_grad_buffer.pycheck_grads遇非有限梯度时清零该 bucket 并 continue(跳过坏样本), 而不是走 Megatron rerun_state_machine 直接 abort 整个 job。 - rep_penalty 注入:
OpenClaw-RL/openclaw-opd/openclaw_opd_api_server.py对 rollout 请求setdefaultrepetition_penalty(envOPENCLAW_ROLLOUT_REP_PENALTY)。 Thinking 模型在长 agentic 上下文里会在<think>内循环刷满 max tokens,必须开。 - sim_joint 成功判据:openclaw 3.31
agent --local --json输出无顶层status字段,按meta.stopReason+ 非空文本判成功(曾因判status=="ok"导致每 turn 假失败重试 6 次、整条链路断裂)。 - flush_cache 重试:
slime/backends/sglang_utils/sglang_engine.py等待循环range(300)且非 200 响应时 sleep 在 try 块内(否则 400 空转打爆)。 - KL 锚:inner 脚本
KL_LOSS_COEF默认 0.01(原 0.0,无锚易发散)。
已知未解问题:OPD 的 teacher(prm_teacher)从参考 ckpt 加载后冻结,
不随 actor 更新。论文要求 ℓ_T = π_old + hint(on-policy teacher)。直接把 gather
换到 actor 上会碰 dp_size 断言(prm_teacher DP=1 vs actor DP=2,数据分片不对齐)。
真修需要 per-rank indices 管线或每步向 teacher 广播权重,均为非平凡框架改动。
长训发散时可先降 OPENCLAW_TOPK_W_OPD 缓解。
流程同 4B,用 run_qwen3_32b_openclaw_combine_topk_ascend_8nodes.sh +
scripts/run_qwen3_32b_openclaw_combine_topk_ascend_8nodes_inner.sh,
模型 Qwen3-32B(转换脚本参考 4B 的,换 scripts/models/qwen3-32B.sh)。
节点角色:1 master + 3 actor worker + 2 rollout + 1 PRM + 1 teacher,
所有节点都要部署本仓库 + 镜像 + 模型到相同路径。