TL;DR
在单节点 8× H200(143 GB/卡)上跑了一次 terminal-rl GRPO outcome-only(dense pass-rate reward,无 PRM、无 process reward)训练 10 小时,模型为 Qwen3-4B,数据集为 seta_env(1376 个 Linux 终端任务)。
- W&B run:https://wandb.ai/hansbug/openclaw-terminal-rl/runs/lpurziy1
- 训练步数:~270 step(对应 ~135 个 rollout round,每个 rollout batch=16 × n_samples=8 = 128 样本)
- 结果:
terminal/accuracy 从 baseline 0.19 爬到 peak 0.385(25 步桶均值)/ 0.60(单 batch 最高),随后在 0.32–0.38 plateau;后期 KL 漂移到 0.54 时主动停下
- 7 处 OpenClaw-RL 本仓库源码修改 + 1 处
mbridge site-packages 补丁,见下文"源码修改清单",每处都附 diff、必要性、不打补丁的后果、潜在副作用
1 期望训练结果 —— 我查过的所有官方 / 半官方来源
这一段是想说清楚"官方到底有没有给出 terminal-rl 的期望指标",答案是 没有,所以我们这次跑的结果没有直接可比的目标值。下面是我为了确认这一点翻过的所有位置。
1.1 Tech Report PDF
-
arxiv 链接:https://arxiv.org/abs/2603.10165
-
Section 5.4 General Agents: Unified RL Across Terminal, GUI, SWE, and Tool-Call(page 12)——只提到 terminal-rl 的基础设施描述("128 parallel environments for terminal agents"),没有训练曲线或精度数字
-
Table 4 给出 outcome-only vs integrated(outcome+PRM) 的对比:
| Setting |
Integrated reward |
Outcome only |
| Tool-call |
0.30 |
0.17 (train 250 steps) |
| GUI |
0.33 |
0.31 (train 120 steps) |
terminal 这一行是空的——paper 没给数字。
-
Table 3(Personal-Agent 作业场景实验):
| Method |
Updated 8 steps |
Updated 16 steps |
| Binary RL |
0.25 |
0.23 |
| OPD |
0.25 |
0.72 |
| Combined |
0.76 |
0.81 |
这一张是 personal-agent 的,不是 terminal-rl;而且 paper 里 personal-agent 的 "Binary RL" 是真 0/1 reward,跟本工作 fraction-of-pass 的 dense reward 形态不一样。但作者自己这里就说明了 Binary RL 在 16 步就 plateau 甚至 0.25→0.23 略降,同属 outcome-only / 无 critic / 无 process reward 一族,plateau pattern 有参考意义。
1.2 Blog 系列
1.3 伴生论文 RLAnything
1.4 GitHub issues(main repo 全文搜 "terminal",10 条)
我读了相关的几条(Gen-Verse#59、Gen-Verse#62、Gen-Verse#68、Gen-Verse#69、Gen-Verse#87、Gen-Verse#88),作者都没给出训练曲线数字;Gen-Verse#62 有个用户问 retool_qwen3_4b_rl(和我们同等级的 4B agent RL)的预期训练时间,至今 OPEN、没有权威回复。
1.5 HuggingFace Papers 讨论区
https://huggingface.co/papers/2603.10165 —— 只有一条和 baseline 无关的排版疑问,没有训练数字讨论。
1.6 仓库 assets
assets/openclawrl1performance.png —— 是 personal-agent 的 Figure 2(学生/老师 OpenClaw 对话模拟),不是 terminal-rl 曲线。
1.7 结论
terminal-rl 没有官方公开的"预期训练到多少"数字。 唯一有指导性的是 Table 4 中 tool-call outcome-only 基线 0.17 @ 250 步、GUI outcome-only 0.31 @ 120 步。把我们的 terminal accuracy 对照这两条:
- 我们 peak 0.385(25 步桶均值),高于 tool-call outcome-only 基线(0.17)
- 介于 GUI outcome-only(0.31)和 integrated(0.33)之间
- 但这两个 setting 都不是 terminal,只能说明我们的量级合理,不能说"达标"或"不达标"
2 启动顺序和命令
完整可运行的启动脚本在 run_terminal_rl.sh (attachment);该脚本基于仓库里官方提供的 terminal-rl/terminal-rl_qwen3-8b.sh 改的,结构完全同构(cleanup_prev → check_gpus → detect_nvlink → start_ray_head → submit_job),只做了针对 4B + 单机的最小改动,下面列清楚。
2.1 前置依赖
# 在 /nfs/terminal-rl-workspace/ 下
conda env 'tbench-rl' (Python 3.12)
torch 2.9.1+cu128
sglang 0.5.10.post1
ray 2.55.1
mbridge 0.15.1
transformer-engine 2.13.0 (prebuilt cu12 wheel)
flash-attn 2.8.3 (prebuilt cu12torch2.9 wheel)
+ slime -e . (editable install)
.env 文件(在 /nfs/terminal-rl-workspace/.env):
WANDB_API_KEY=<key>
WANDB_PROJECT=openclaw-terminal-rl
WANDB_ORG=hansbug
WANDB_MODE=online
checkpoint 准备:
# HF 权重下载(用 xet 会在 NFS 上 perm denied)
HF_HUB_DISABLE_XET=1 hf download Qwen/Qwen3-4B --local-dir /nfs/models/Qwen3-4B
# HF → Megatron torch_dist(用 TE impl 做,不要加 --transformer-impl local)
cd slime && source scripts/models/qwen3-4B.sh
python tools/convert_hf_to_torch_dist.py \
"${MODEL_ARGS[@]}" \
--hf-checkpoint /nfs/models/Qwen3-4B \
--rotary-base 1000000 \
--no-gradient-accumulation-fusion \
--save /nfs/models/Qwen3-4B_torch_dist
2.2 Pool server(和训练分离、独立 Docker 容器)
docker run -d --name openclaw_pool_server --restart unless-stopped \
-p 18081:18081 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /nfs/terminal-rl-workspace/OpenClaw-RL:/nfs/terminal-rl-workspace/OpenClaw-RL \
-e DATASET_DIR=/nfs/terminal-rl-workspace/OpenClaw-RL/terminal-rl/dataset \
-e TBENCH_OUTPUT_ROOT=/nfs/terminal-rl-workspace/OpenClaw-RL/terminal-rl/build_outputs \
-e TBENCH_DOCKER_IMAGE_SOURCE=build \
-e ENV_SERVER_PORT=18081 \
openclaw_pool_server:v1 \
bash -c "cd /nfs/terminal-rl-workspace/OpenClaw-RL && \
python -m terminal-rl.remote.pool_server \
--host 0.0.0.0 --port 18081 \
--max-tasks 8 --max-runs-per-task 4 \
--output-root /nfs/terminal-rl-workspace/OpenClaw-RL/terminal-rl/build_outputs"
# 健康检查:curl http://127.0.0.1:18081/healthz → {"ok":true}
2.3 启动训练
cd /nfs/terminal-rl-workspace/OpenClaw-RL
source /home/ubuntu/miniconda3/etc/profile.d/conda.sh && conda activate tbench-rl
: > terminal-rl/logs/training.log
nohup bash terminal-rl/run_terminal_rl.sh > terminal-rl/logs/training.log 2>&1 &
脚本内部流程:
set +u 保护 conda activate tbench-rl(conda activate 脚本引用 unset 变量)
source ../.env 注入 wandb 凭证
- 拼接
LD_LIBRARY_PATH 指向 $CONDA_PREFIX/lib/python3.12/site-packages/nvidia/{cudnn,nvtx,cusparse,nccl,...}/lib(TE 运行时需要)
source slime/scripts/models/qwen3-4B.sh 加载 MODEL_ARGS
cleanup_prev(pkill sglang/ray/python)
ray start --head --num-gpus 8 ...
ray job submit --runtime-env-json=... 把 train_async.py + 全部 args 提交
完整参数 list 见 attachment 里的脚本。
2.4 ckpt 保留守护(单独进程)
slime 原生没有 --save-max-to-keep,每个 iter_* 是 ~50 GB(4B 模型 dist_checkpoint),几小时下来 NFS 就能吃掉 2 TB。我写了个简单的 polling daemon:
nohup bash terminal-rl/ckpt_retention.sh \
> terminal-rl/logs/ckpt_retention.log 2>&1 &
脚本逻辑:每 5 分钟扫一次 iter_* 目录,只保留最新 KEEP_N=2 个,且只动 mtime > 3 分钟前的目录(避免和 in-flight save 打架)。
3 和官方 terminal-rl_qwen3-8b.sh 的配置差异
| Arg |
官方 8B |
本次 4B |
为什么改 |
| 模型脚本 |
qwen3-8B.sh |
qwen3-4B.sh |
用 4B 模型 |
ACTOR_GPUS / TP |
4 / 4 |
2 / 2 |
4B + TP=2 足够,省卡给 rollout |
ROLLOUT_GPUS |
4 |
6 |
actor 少了 2 卡,给 rollout |
--rollout-num-gpus-per-engine |
2 |
2 |
3 engines × 2 GPUs |
--no-gradient-accumulation-fusion |
❌(官方假设装了 Apex) |
✅ |
我们没装 Apex,原配置会跑到 Megatron 内部 gradient_accumulation_fusion=True 然后崩:ColumnParallelLinear was called with gradient_accumulation_fusion set to True but the custom CUDA extension fused_weight_gradient_mlp_cuda module is not found |
ENV_SERVER_URL |
经 router_server.py 在 :18080 上 proxy WORKER_URLS |
直接 http://127.0.0.1:18081 |
单机 + 单 pool_server,不需要多 worker 的 proxy |
--prm-enable |
❌(默认 8B 脚本本来就没有,PRM 在另一个 _prm_2nodes.sh) |
❌ |
保持 outcome-only(无 process reward) |
--use-slime-router |
❌(默认 sglang_router) |
❌ |
不踩 --use-slime-router 自带的 h11 Content-Length bug |
4 源码修改清单(共 7 处 repo 内 + 1 处 site-packages)
下面每一项都给出 diff + 为什么需要 + 不打补丁的后果 + 潜在副作用 / 风险。这些修改都是运行环境和仓库上游假设不一致造成的,不是"我觉得代码写得不好所以改了"。
git diff --stat 汇总:
Megatron-LM/megatron/core/utils.py | 7 +++++--
slime/slime/backends/megatron_utils/initialize.py | 4 +++-
slime/slime/backends/megatron_utils/megatron_to_hf/qwen2.py | 11 +++++++++++
slime/slime/backends/megatron_utils/update_weight/update_weight_from_distributed.py | 22 ++++++++++++++++++----
slime/slime/backends/sglang_utils/sglang_engine.py | 6 ++++++
terminal-rl/remote/docker_compose_utils.py | 5 ++++-
terminal-rl/remote/terminal_env.py | 5 ++++-
7 files changed, 51 insertions(+), 9 deletions(-)
4.1 Megatron-LM/megatron/core/utils.py — TE 未装时 is_te_min_version 的安全回退
@@ -346,9 +346,12 @@ def is_te_min_version(version, check_equality=True):
"packaging is not installed. Please install it with `pip install packaging`."
)
+ te_version = get_te_version()
+ if te_version is None:
+ return False # TE not installed
if check_equality:
- return get_te_version() >= PkgVersion(version)
- return get_te_version() > PkgVersion(version)
+ return te_version >= PkgVersion(version)
+ return te_version > PkgVersion(version)
- 为什么需要:我们最早没装 transformer_engine(走的是
--transformer-impl local 路线),get_te_version() 返回 None,原代码 None >= PkgVersion(...) 直接 TypeError 崩。
- 不打的后果:Megatron 模块加载期 TypeError。
- 当前状态是否还需要:不严格需要——我们后来装了 TE 2.13,这条
if te_version is None 在当前跑法里永远不进入。但保留不影响正常路径,且让没 TE 的 dry-run 也能 import Megatron,所以留着。
- 副作用:零。原路径(TE 已装)走
te_version = get_te_version(); if check_equality: ... 和原来一字不差。
4.2 slime/slime/backends/megatron_utils/initialize.py — numpy 2.x 硬 assert 降为 warning
@@ -63,7 +63,9 @@ def init(args):
_initialize_distributed(args)
# https://github.com/NVIDIA/Megatron-LM/issues/1563
- assert np.__version__.startswith("1."), "Megatron does not support numpy 2.x"
+ if not np.__version__.startswith("1."):
+ import warnings
+ warnings.warn(f"Megatron was designed for numpy 1.x, got {np.__version__}. Continuing anyway.")
- 为什么需要:sglang ≥ 0.5 的依赖树把 numpy 拉到 2.3.5(pandas/ml_dtypes/...)。这个 assert 必炸,训练进程刚初始化就挂。
- 不打的后果:
AssertionError: Megatron does not support numpy 2.x 训练进程 0 步就死。
- 副作用 / 风险:
- 上游那条 assert 是防御性的,numpy 1.x → 2.x API 少数地方不兼容(最常见
np.float_ 被删、np.infty 被删、np.asfarray 被删等)。Megatron 用到的 numpy API 在 2.x 里大部分仍然是稳定的;这次 270 步训练没触发。但如果未来 Megatron 改动走到某条 numpy 2.x 缺的 API,会在那个调用点报错,错误信息不像 assert 那样好看。
- 理想方案应该是 pin numpy<2 到上游;这里只是"凑合能跑"的权宜。
4.3 slime/slime/backends/megatron_utils/megatron_to_hf/qwen2.py — 补 local-impl 命名
@@ -68,4 +68,15 @@ def convert_qwen2_to_hf(args, name, param):
elif rest == "self_attention.k_layernorm.weight":
return [(f"model.layers.{layer_idx}.self_attn.k_norm.weight", param)]
+ # local (non-TE) impl: standalone layernorm modules
+ elif rest == "input_layernorm.weight":
+ return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)]
+ elif rest in ("pre_mlp_layernorm.weight", "post_attention_layernorm.weight"):
+ return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
+ elif rest == "mlp.router.weight":
+ return [(f"model.layers.{layer_idx}.mlp.gate.weight", param)]
+ elif rest == "self_attention.core_attention.rotary_emb.inv_freq":
+ # rotary embeddings are not saved in HF format
+ return []
+
raise ValueError(f"Unknown parameter name: {name}")
- 为什么需要:当使用
--transformer-impl local(TE 未装时的退路)时,Megatron 模型里出现的是 input_layernorm.weight / pre_mlp_layernorm.weight 这类"独立 layernorm 模块"的名字,而不是 TE fused 的 self_attention.linear_qkv.layer_norm_weight。原 convert_qwen2_to_hf 只认 TE 名字,遇到 local 名字直接 raise ValueError(Unknown parameter name: ...) 把 Megatron→HF 的 save_checkpoint 打断。
- 不打的后果:如果走
--transformer-impl local 路径(例如机器上装不了 TE),Megatron 保存 HF 格式 ckpt 时崩。
- 当前是否需要:不严格需要——我们最终用 TE impl 训,导出时走不到这几条 elif。但保留对 "没 TE 的环境" 是一层兼容。
- 副作用:纯新增的 elif 分支,不改动现有分支逻辑;零副作用。
4.4 slime/slime/backends/megatron_utils/update_weight/update_weight_from_distributed.py — NCCL 重复 GPU 错误的软失败
handles = []
- for _, param in converted_named_tensors:
- handles.append(dist.broadcast(param.data, 0, group=group, async_op=True))
- for handle in handles:
- handle.wait()
+ try:
+ for _, param in converted_named_tensors:
+ handles.append(dist.broadcast(param.data, 0, group=group, async_op=True))
+ for handle in handles:
+ handle.wait()
+ except Exception as e:
+ if "Duplicate GPU" in str(e) or "ncclInvalidUsage" in str(e):
+ import logging
+ logging.getLogger(__name__).warning(
+ "NCCL weight broadcast skipped (single-GPU demo, duplicate GPU): %s", str(e)[:200]
+ )
+ for h in handles:
+ try: h.wait()
+ except Exception: pass
+ else:
+ raise
- 为什么需要:用于1-GPU hack(
CUDA_VISIBLE_DEVICES=0 + Ray --num-gpus 3 模拟 3 卡,全落在 GPU 0)的场景。NCCL 在同一张物理卡上建 broadcast group 会 Duplicate GPU detected 报错。这个 try/except 让 1-GPU demo 能跑完(权重不会在 rollout 间同步,但至少训练步能走)。
- 不打的后果:1-GPU demo 模式训练进程第一次 weight sync 就 crash。
- 当前是否需要:当前 8-GPU 跑法不触发。但保留给未来想用 1-GPU debug 的人。
- 副作用 / 风险:有真风险——如果 NCCL 真的在多卡下出了 Duplicate GPU / ncclInvalidUsage 这类错(硬件故障、错误配置),这个 catch 会把它变成 warning 吞掉,训练继续但权重根本没同步。生产环境应该移掉或加一个 env var 开关来决定是否吞。
4.5 slime/slime/backends/sglang_utils/sglang_engine.py — SGLang HTTP 端同样的软失败
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
+ if "Duplicate GPU detected" in response.text or "ncclInvalidUsage" in response.text:
+ logger.warning(
+ "Weight sync skipped (NCCL duplicate GPU on single-GPU setup): %s",
+ response.text[:200],
+ )
+ return {"success": True, "skipped": True}
e.add_note(f"{response.text=}")
raise
- 为什么需要:同 4.4,但这里是 actor 通过 HTTP 调 SGLang 的
/update_weights_from_distributed 端点,NCCL 错从 HTTP 响应 text 里传回来。1-GPU hack 用。
- 不打的后果:同 4.4,1-GPU demo 挂。
- 当前是否需要:当前 8-GPU 跑法不触发。
- 副作用 / 风险:和 4.4 完全对称。生产环境应该移掉。
4.6 terminal-rl/remote/docker_compose_utils.py — DockerComposeManager.build(timeout=…) 兼容
- compose_manager.build(timeout=timeout)
+ try:
+ compose_manager.build(timeout=timeout)
+ except TypeError:
+ compose_manager.build()
-
为什么需要:terminal-bench 本次我们装的版本 0.2.18,DockerComposeManager.build() 方法不接受 timeout 关键字参数。上游 docker_compose_utils.py 里写死了 build(timeout=…)。
/nfs/terminal-rl-workspace/OpenClaw-RL/terminal-rl/remote/README.md 或 terminal-rl/README.md 都没固定 terminal-bench 版本。用 pip install git+https://github.com/laude-institute/terminal-bench.git 默认装 main 分支 → 我装到的 0.2.18 里已经没这个 kwarg。
-
不打的后果:pool_server 第一个 /reset 就 TypeError: build() got an unexpected keyword argument 'timeout',500 返回给 RolloutManager,样本全标 FAILED。
-
副作用:fallback 到无 timeout 的 build(),意味着build 本身可能挂死而不是 timeout 触发。实际运行下 build 都能几秒内完成,没观察到挂死。
-
理想方案:上游 pin 一个 terminal-bench 版本(或检测 signature 自适应),我这里只是凑合跑。
4.7 terminal-rl/remote/terminal_env.py — Terminal.start(timeout=…) 兼容
else:
- self._terminal.start(timeout=self._timeouts.reset_session)
+ try:
+ self._terminal.start(timeout=self._timeouts.reset_session)
+ except TypeError:
+ self._terminal.start()
try:
from .docker_compose_utils import (
_DEFAULT_CONTAINER_MEMORY_LIMIT,
- 为什么需要:同 4.6,terminal-bench 0.2.18 的
Terminal.start() 不接受 timeout kwarg。
- 不打的后果:pool server 一律 500,agent 一个都跑不起来,和我们最初 9 小时白训(agent 全失败、grad=0)的根因之一。
- 副作用:fallback 不传 timeout,依赖 terminal-bench 内部默认。实际运行无观察到问题。
- 理想方案:同 4.6,pin 版本或动态自适应。
4.8 mbridge/models/qwen2.py(site-packages,非 git 追踪)
不在仓库里,但是 HF → torch_dist 转换时必需的外部依赖补丁。上游 mbridge==0.15.1 的 Qwen2Bridge 只认 TE fused 的 self_attention.linear_qkv.layer_norm_weight 类命名。走 --transformer-impl local 转换时,Megatron 会产生 input_layernorm.weight / pre_mlp_layernorm.weight 这种 local-impl 专属的参数名,mbridge 找不到映射直接 NotImplementedError: Unsupported parameter name: decoder.layers.0.input_layernorm.weight 转换崩。
修改内容(加到 _MLP_MAPPING 和新建的 _OTHER_MAPPING):
_MLP_MAPPING = {
# ... 原有映射 ...
# local (non-TE) impl: standalone pre_mlp_layernorm goes here because its
# name contains "mlp" so routing sends it to _MLP_MAPPING.
"pre_mlp_layernorm.weight": [
"model.layers.{layer_number}.post_attention_layernorm.weight"
],
}
# local (non-TE) transformer_impl: standalone layernorm modules whose names
# do not contain "self_attention" or "mlp", so they land in _OTHER_MAPPING.
_OTHER_MAPPING = {
"input_layernorm.weight": [
"model.layers.{layer_number}.input_layernorm.weight"
],
"pre_mlp_layernorm.weight": [
"model.layers.{layer_number}.post_attention_layernorm.weight"
],
"post_attention_layernorm.weight": [
"model.layers.{layer_number}.post_attention_layernorm.weight"
],
}
- 为什么需要:只有在用
--transformer-impl local 做 HF → torch_dist 转换时才会踩。
- 不打的后果:
python tools/convert_hf_to_torch_dist.py ... --transformer-impl local 直接 NotImplementedError。
- 当前是否需要:不严格需要——我们最终重做了转换,用 TE impl(不传
--transformer-impl local)。但装补丁对 TE 路径 0 影响(新增的 key 永远不匹配)。
- 副作用:纯新增键;对 TE 路径 0 影响;对 local 路径是 bug fix。
- 最佳处理:写到 mbridge 上游。
4.9 小结:必要性分类
| 修改 |
当前 8-GPU + TE 跑法严格必需 |
其他路径有用 |
| 4.1 utils.py(TE None safe) |
❌ |
✅ 无 TE 场景 |
| 4.2 initialize.py(numpy 2.x warning) |
✅ |
— |
| 4.3 slime qwen2.py mapping |
❌ |
✅ local-impl 导出 |
| 4.4 update_weight NCCL dup catch |
❌ |
✅ 1-GPU hack |
| 4.5 sglang_engine NCCL dup catch |
❌ |
✅ 1-GPU hack |
| 4.6 docker_compose build(timeout) |
✅ |
— |
| 4.7 terminal_env start(timeout) |
✅ |
— |
| 4.8 mbridge qwen2 mapping |
❌ |
✅ local-impl 转换 |
当前跑法下严格必需的只有 #4.2、#4.6、#4.7 三处。 其他五处是沿着外部 setup guide 一起打的兼容补丁,当前 TE + 8-GPU 的正路不触发,但也不会有反作用,所以没删。
5 关键指标曲线

原图 + 单指标图(均附 MA(11) 平滑):
| 指标 |
图 |
terminal/accuracy(pytest 通过率) |
 |
terminal/reward_mean(= 2·accuracy − 1) |
 |
rollout/raw_reward(训练 batch 均值) |
 |
train/grad_norm |
 |
train/kl_loss |
 |
train/entropy_loss |
 |
terminal/non_trainable_ratio |
 |
rollout/response_len/mean(agent 输出 token 数) |
 |
6 训练阶段小结(25 步桶均值)
accuracy raw_reward kl_loss
steps 0-24 (cold) 0.211 -0.412 0.007
steps 25-49 0.303 -0.225 0.049
steps 50-74 0.344 -0.117 0.162
steps 60-79 (PEAK) 0.385 -0.058 0.182
steps 100-124 0.337 -0.224 0.067
steps 125-149 (spike) 噪声 噪声 0.296 ← grad_norm max=909 @ step 266,GRPO clip 把策略拉回
steps 150-174 (rec) 0.349 -0.176 0.108
steps 200-224 (plat) 0.334 -0.115 0.139
steps 275-297 (drift) — — 0.540 ← 我在这里停了
完整 JSON 在 summary_stats.json (attachment)。
观察:
- 冷启动顺利:grad 0→1.5,accuracy 0.19→0.28 in 20 步
- 爬坡:到 step 60–79 达到峰 0.385,和 paper Table 3 personal-agent Binary RL "16 步 plateau 0.25" 的规律同族(reward 形态不同,但都是 outcome-only 无 critic) —— 爬升阶段短
- Plateau:0.32–0.38 震荡约 150 步
- Outlier:step 266
grad_norm=909, kl=5,下一步立即被 GRPO ratio clip + KL 压回 grad=0.2, kl=0.24。不是发散
- 晚期 KL 漂移:step 275–297 桶 kl mean 从 0.14 慢慢爬到 0.54,accuracy 没跟着涨 —— 这是 policy 在无收益地远离 ref,典型过度训练信号,所以主动停
7 Attachments(均走 gh attach session-token 路径,挂在 refs/uploads/issues/1 下,不产生 commit / branch)
8 给上游的建议(按优先级)
- 文档里给出 terminal-rl 的一条参考 wandb 曲线或一个 Table(即使是一张作者内部训的截图),省掉"我这是否在 work"的排查时间
- pin
terminal-bench 版本,或者在 terminal-rl/remote/docker_compose_utils.py 和 terminal-rl/remote/terminal_env.py 里用 inspect.signature(...) 动态判断 kwarg 支持情况
- slime 加
--save-max-to-keep N;每 50 GB 的 ckpt 不清理几小时就把盘打爆
--kl-loss-coef 0.01 配合 k3 对晚期 KL 漂移保护不足,建议 README 标注"建议 step > 200 观察 KL,必要时加大 coef 或早停"
- 单机部署可跳过
router_server.py,让 ENV_SERVER_URL 直连 pool_server;可以作为"single-node"模式写到 README
TL;DR
在单节点 8× H200(143 GB/卡)上跑了一次
terminal-rlGRPO outcome-only(dense pass-rate reward,无 PRM、无 process reward)训练 10 小时,模型为 Qwen3-4B,数据集为seta_env(1376 个 Linux 终端任务)。terminal/accuracy从 baseline 0.19 爬到 peak 0.385(25 步桶均值)/ 0.60(单 batch 最高),随后在 0.32–0.38 plateau;后期 KL 漂移到 0.54 时主动停下mbridgesite-packages 补丁,见下文"源码修改清单",每处都附 diff、必要性、不打补丁的后果、潜在副作用1 期望训练结果 —— 我查过的所有官方 / 半官方来源
1.1 Tech Report PDF
arxiv 链接:https://arxiv.org/abs/2603.10165
Section 5.4 General Agents: Unified RL Across Terminal, GUI, SWE, and Tool-Call(page 12)——只提到 terminal-rl 的基础设施描述("128 parallel environments for terminal agents"),没有训练曲线或精度数字
Table 4 给出 outcome-only vs integrated(outcome+PRM) 的对比:
terminal 这一行是空的——paper 没给数字。
Table 3(Personal-Agent 作业场景实验):
这一张是 personal-agent 的,不是 terminal-rl;而且 paper 里 personal-agent 的 "Binary RL" 是真 0/1 reward,跟本工作 fraction-of-pass 的 dense reward 形态不一样。但作者自己这里就说明了 Binary RL 在 16 步就 plateau 甚至 0.25→0.23 略降,同属 outcome-only / 无 critic / 无 process reward 一族,plateau pattern 有参考意义。
1.2 Blog 系列
1.3 伴生论文 RLAnything
1.4 GitHub issues(main repo 全文搜 "terminal",10 条)
我读了相关的几条(Gen-Verse#59、Gen-Verse#62、Gen-Verse#68、Gen-Verse#69、Gen-Verse#87、Gen-Verse#88),作者都没给出训练曲线数字;Gen-Verse#62 有个用户问 retool_qwen3_4b_rl(和我们同等级的 4B agent RL)的预期训练时间,至今 OPEN、没有权威回复。
1.5 HuggingFace Papers 讨论区
https://huggingface.co/papers/2603.10165 —— 只有一条和 baseline 无关的排版疑问,没有训练数字讨论。
1.6 仓库 assets
assets/openclawrl1performance.png—— 是 personal-agent 的 Figure 2(学生/老师 OpenClaw 对话模拟),不是 terminal-rl 曲线。1.7 结论
terminal-rl 没有官方公开的"预期训练到多少"数字。 唯一有指导性的是 Table 4 中 tool-call outcome-only 基线 0.17 @ 250 步、GUI outcome-only 0.31 @ 120 步。把我们的 terminal accuracy 对照这两条:
2 启动顺序和命令
完整可运行的启动脚本在
run_terminal_rl.sh(attachment);该脚本基于仓库里官方提供的terminal-rl/terminal-rl_qwen3-8b.sh改的,结构完全同构(cleanup_prev → check_gpus → detect_nvlink → start_ray_head → submit_job),只做了针对 4B + 单机的最小改动,下面列清楚。2.1 前置依赖
checkpoint 准备:
2.2 Pool server(和训练分离、独立 Docker 容器)
docker run -d --name openclaw_pool_server --restart unless-stopped \ -p 18081:18081 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /nfs/terminal-rl-workspace/OpenClaw-RL:/nfs/terminal-rl-workspace/OpenClaw-RL \ -e DATASET_DIR=/nfs/terminal-rl-workspace/OpenClaw-RL/terminal-rl/dataset \ -e TBENCH_OUTPUT_ROOT=/nfs/terminal-rl-workspace/OpenClaw-RL/terminal-rl/build_outputs \ -e TBENCH_DOCKER_IMAGE_SOURCE=build \ -e ENV_SERVER_PORT=18081 \ openclaw_pool_server:v1 \ bash -c "cd /nfs/terminal-rl-workspace/OpenClaw-RL && \ python -m terminal-rl.remote.pool_server \ --host 0.0.0.0 --port 18081 \ --max-tasks 8 --max-runs-per-task 4 \ --output-root /nfs/terminal-rl-workspace/OpenClaw-RL/terminal-rl/build_outputs" # 健康检查:curl http://127.0.0.1:18081/healthz → {"ok":true}2.3 启动训练
脚本内部流程:
set +u保护conda activate tbench-rl(conda activate 脚本引用 unset 变量)source ../.env注入 wandb 凭证LD_LIBRARY_PATH指向$CONDA_PREFIX/lib/python3.12/site-packages/nvidia/{cudnn,nvtx,cusparse,nccl,...}/lib(TE 运行时需要)source slime/scripts/models/qwen3-4B.sh加载MODEL_ARGScleanup_prev(pkill sglang/ray/python)ray start --head --num-gpus 8 ...ray job submit --runtime-env-json=...把train_async.py + 全部 args提交完整参数 list 见 attachment 里的脚本。
2.4 ckpt 保留守护(单独进程)
slime 原生没有
--save-max-to-keep,每个iter_*是 ~50 GB(4B 模型 dist_checkpoint),几小时下来 NFS 就能吃掉 2 TB。我写了个简单的 polling daemon:nohup bash terminal-rl/ckpt_retention.sh \ > terminal-rl/logs/ckpt_retention.log 2>&1 &脚本逻辑:每 5 分钟扫一次
iter_*目录,只保留最新KEEP_N=2个,且只动 mtime > 3 分钟前的目录(避免和 in-flight save 打架)。3 和官方
terminal-rl_qwen3-8b.sh的配置差异qwen3-8B.shqwen3-4B.shACTOR_GPUS/ TPROLLOUT_GPUS--rollout-num-gpus-per-engine--no-gradient-accumulation-fusiongradient_accumulation_fusion=True然后崩:ColumnParallelLinear was called with gradient_accumulation_fusion set to True but the custom CUDA extension fused_weight_gradient_mlp_cuda module is not foundENV_SERVER_URLrouter_server.py在 :18080 上 proxyWORKER_URLShttp://127.0.0.1:18081--prm-enable_prm_2nodes.sh)--use-slime-routersglang_router)--use-slime-router自带的 h11 Content-Length bug4 源码修改清单(共 7 处 repo 内 + 1 处 site-packages)
4.1
Megatron-LM/megatron/core/utils.py— TE 未装时is_te_min_version的安全回退--transformer-impl local路线),get_te_version()返回None,原代码None >= PkgVersion(...)直接 TypeError 崩。if te_version is None在当前跑法里永远不进入。但保留不影响正常路径,且让没 TE 的 dry-run 也能 import Megatron,所以留着。te_version = get_te_version(); if check_equality: ...和原来一字不差。4.2
slime/slime/backends/megatron_utils/initialize.py— numpy 2.x 硬 assert 降为 warningAssertionError: Megatron does not support numpy 2.x训练进程 0 步就死。np.float_被删、np.infty被删、np.asfarray被删等)。Megatron 用到的 numpy API 在 2.x 里大部分仍然是稳定的;这次 270 步训练没触发。但如果未来 Megatron 改动走到某条 numpy 2.x 缺的 API,会在那个调用点报错,错误信息不像 assert 那样好看。4.3
slime/slime/backends/megatron_utils/megatron_to_hf/qwen2.py— 补 local-impl 命名--transformer-impl local(TE 未装时的退路)时,Megatron 模型里出现的是input_layernorm.weight/pre_mlp_layernorm.weight这类"独立 layernorm 模块"的名字,而不是 TE fused 的self_attention.linear_qkv.layer_norm_weight。原convert_qwen2_to_hf只认 TE 名字,遇到 local 名字直接raise ValueError(Unknown parameter name: ...)把 Megatron→HF 的 save_checkpoint 打断。--transformer-impl local路径(例如机器上装不了 TE),Megatron 保存 HF 格式 ckpt 时崩。4.4
slime/slime/backends/megatron_utils/update_weight/update_weight_from_distributed.py— NCCL 重复 GPU 错误的软失败CUDA_VISIBLE_DEVICES=0+ Ray--num-gpus 3模拟 3 卡,全落在 GPU 0)的场景。NCCL 在同一张物理卡上建 broadcast group 会Duplicate GPU detected报错。这个 try/except 让 1-GPU demo 能跑完(权重不会在 rollout 间同步,但至少训练步能走)。4.5
slime/slime/backends/sglang_utils/sglang_engine.py— SGLang HTTP 端同样的软失败try: response.raise_for_status() except requests.exceptions.HTTPError as e: + if "Duplicate GPU detected" in response.text or "ncclInvalidUsage" in response.text: + logger.warning( + "Weight sync skipped (NCCL duplicate GPU on single-GPU setup): %s", + response.text[:200], + ) + return {"success": True, "skipped": True} e.add_note(f"{response.text=}") raise/update_weights_from_distributed端点,NCCL 错从 HTTP 响应 text 里传回来。1-GPU hack 用。4.6
terminal-rl/remote/docker_compose_utils.py—DockerComposeManager.build(timeout=…)兼容为什么需要:terminal-bench 本次我们装的版本 0.2.18,
DockerComposeManager.build()方法不接受timeout关键字参数。上游docker_compose_utils.py里写死了build(timeout=…)。/nfs/terminal-rl-workspace/OpenClaw-RL/terminal-rl/remote/README.md或terminal-rl/README.md都没固定 terminal-bench 版本。用pip install git+https://github.com/laude-institute/terminal-bench.git默认装 main 分支 → 我装到的 0.2.18 里已经没这个 kwarg。不打的后果:pool_server 第一个
/reset就TypeError: build() got an unexpected keyword argument 'timeout',500 返回给 RolloutManager,样本全标 FAILED。副作用:fallback 到无 timeout 的
build(),意味着build 本身可能挂死而不是 timeout 触发。实际运行下 build 都能几秒内完成,没观察到挂死。理想方案:上游 pin 一个 terminal-bench 版本(或检测 signature 自适应),我这里只是凑合跑。
4.7
terminal-rl/remote/terminal_env.py—Terminal.start(timeout=…)兼容Terminal.start()不接受timeoutkwarg。4.8
mbridge/models/qwen2.py(site-packages,非 git 追踪)不在仓库里,但是 HF → torch_dist 转换时必需的外部依赖补丁。上游
mbridge==0.15.1的Qwen2Bridge只认 TE fused 的self_attention.linear_qkv.layer_norm_weight类命名。走--transformer-impl local转换时,Megatron 会产生input_layernorm.weight/pre_mlp_layernorm.weight这种 local-impl 专属的参数名,mbridge 找不到映射直接NotImplementedError: Unsupported parameter name: decoder.layers.0.input_layernorm.weight转换崩。修改内容(加到
_MLP_MAPPING和新建的_OTHER_MAPPING):--transformer-impl local做 HF → torch_dist 转换时才会踩。python tools/convert_hf_to_torch_dist.py ... --transformer-impl local直接NotImplementedError。--transformer-impl local)。但装补丁对 TE 路径 0 影响(新增的 key 永远不匹配)。4.9 小结:必要性分类
当前跑法下严格必需的只有 #4.2、#4.6、#4.7 三处。 其他五处是沿着外部 setup guide 一起打的兼容补丁,当前 TE + 8-GPU 的正路不触发,但也不会有反作用,所以没删。
5 关键指标曲线
原图 + 单指标图(均附 MA(11) 平滑):
terminal/accuracy(pytest 通过率)terminal/reward_mean(= 2·accuracy − 1)rollout/raw_reward(训练 batch 均值)train/grad_normtrain/kl_losstrain/entropy_lossterminal/non_trainable_ratiorollout/response_len/mean(agent 输出 token 数)6 训练阶段小结(25 步桶均值)
完整 JSON 在
summary_stats.json(attachment)。观察:
grad_norm=909, kl=5,下一步立即被 GRPO ratio clip + KL 压回grad=0.2, kl=0.24。不是发散7 Attachments(均走
gh attachsession-token 路径,挂在refs/uploads/issues/1下,不产生 commit / branch)00_dashboard.png— 6 面板 summary01_accuracy.png…08_response_len.png— 单指标 tracesummary_stats.json— 指标 min/max/mean + 25 步桶run_terminal_rl.sh— 完整启动脚本8 给上游的建议(按优先级)
terminal-bench版本,或者在terminal-rl/remote/docker_compose_utils.py和terminal-rl/remote/terminal_env.py里用inspect.signature(...)动态判断 kwarg 支持情况--save-max-to-keep N;每 50 GB 的 ckpt 不清理几小时就把盘打爆--kl-loss-coef 0.01配合 k3 对晚期 KL 漂移保护不足,建议 README 标注"建议 step > 200 观察 KL,必要时加大 coef 或早停"router_server.py,让ENV_SERVER_URL直连 pool_server;可以作为"single-node"模式写到 README