1. 为什么 Agentic RL 训练总卡在“等”上
如果你最近在跑 Agentic 场景的强化学习后训练,大概率遇到过这种画面:8 张卡里 6 张在等 Rollout 生成,Trainer 干瞪眼;一条超长样本把整个 step 拖到超时;好不容易跑起来,一个 Rollout 实例 OOM,整个任务从头再来。这不是你配置写错了,而是传统 RL 训练框架的结构性问题——Rollout 和 Train 绑在同一组 GPU 上串行执行,谁慢谁说了算。
小红书 AI 平台团队开源的 Relax,就是冲着这个结构性问题来的。它是一个为全模态数据、Agentic 工作流和大规模异步训练协同设计的现代 RL 训练引擎,核心思路很直接:把 Rollout 和 Train 拆成两个独立服务,用数据总线连接,让推理生成和梯度更新真正并行跑起来。官方实测全异步 Off-Policy 模式相比共卡 On-Policy 吞吐提升 76%,相比 veRL 的全异步实现提升 20%。
这篇文章面向的是已经在跑或准备跑 Agentic RL 训练的工程师,我会交付一套可复制的 Relax 启动配置骨架、TaoToken 统一 Key 接入 settings.json 的示例,以及吞吐对比验证的具体动作。你可以在自己的 H800 或 A800 集群上按步骤复现,不需要从头理解整个架构。
先说清楚 Relax 解决的三层问题,这样你配置的时候知道每个参数在干什么。第一层是数据异构:多模态的图片、视频、音频原始数据体积大,CPU 预处理开销高,编码后 token 爆炸,传统并行策略很难高效协同。第二层是系统脆弱:上千卡长时训练,OOM 和 NCCL 超时是常态,传统方案缺乏分钟级故障恢复和单角色弹性伸缩。第三层是角色耦合:Colocate 方案下各角色共享 GPU 只能串行,Trainer 等最慢的 Rollout;现有全异步方案虽然拆了组,但缺少细粒度流水线调度。Relax 用一套协同设计把这三个问题一起解决,而不是分别打补丁。
2. TaoToken 前置:统一 Key 接入 settings.json
在开始配 Relax 之前,先把模型调用的 Key 管理理顺。Agentic RL 训练里,Rollout 阶段要调模型生成、Reward 阶段可能要用 LLM-as-Judge 做打分、工具调用可能涉及外部模型服务,如果每个环节各配一套 Key,调试和轮换都是灾难。我的做法是用 TaoToken 做统一入口,一个 Key 覆盖对话、编码、Agent 多种调用场景。
TaoToken 的官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数,配置的时候别把跟踪参数写进去。你需要先去控制台创建 API Key,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,创建完在 API Keys 页面复制,页面地址 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
拿到 Key 之后,在项目根目录建一个 settings.json,把模型调用统一收口到这里。下面是我实际用的骨架,你可以直接复制改:
{ "llm": { "provider": "taotoken", "base_url": "https://taotoken.net/api", "api_key": "sk-你的TaoToken密钥", "default_model": "claude-sonnet-4-20250514", "timeout_seconds": 120, "max_retries": 3 }, "rollout": { "engine": "sglang", "model_path": "/data/models/Qwen3-4B", "tp_size": 2, "max_new_tokens": 4096 }, "reward": { "judge_model": "claude-sonnet-4-20250514", "use_llm_judge": true } }这里有几个点要注意。base_url 必须写 https://taotoken.net/api ,不要加任何查询参数,否则部分 SDK 会解析异常。api_key 建议用环境变量注入,settings.json 里只留占位符,避免提交到仓库。default_model 按你实际要调的模型填,Agentic 场景下多轮推理建议选上下文窗口大的。timeout_seconds 给到 120 是因为 Agentic 多轮交互单次可能跑很久,太短会频繁触发重试。
如果你用的是 Claude Code 做辅助编码,TaoToken 也支持 Anthropic 兼容接入,配置文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,ClaudeCodeAnthropic 的接入说明在 https://taotoken.net/doc/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite 。这样你在写 Relax 配置脚本的时候,可以让编码助手直接读你的 settings.json 结构,减少手写 YAML 的出错率。
3. 可复制的 Relax 启动配置骨架
Relax 的架构是 Ray Serve + Megatron + SGLang 的组合,Rollout 服务用 SGLang 做推理生成,Train 服务用 Megatron 做梯度更新,中间通过 TransferQueue 数据总线连接。配置的核心是把每个角色拆成独立的 Ray Serve 服务,各自有独立的资源配额和故障域。
先装依赖。Relax 基于 Ray 生态,建议用 Python 3.10 以上:
pip install "ray[serve]" sglang megatron-core transferqueue pip install relax-rl如果你的集群有 InfiniBand,确认 NCCL 走 IB 而不是 TCP:
export NCCL_IB_DISABLE=0 export NCCL_IB_GID_INDEX=3 export NCCL_SOCKET_IFNAME=ib0 export NCCL_DEBUG=WARN接下来是 Relax 的启动配置。我把它拆成三个部分:集群资源声明、Rollout 服务配置、Train 服务配置。先看集群资源声明,这是 Ray 的初始化部分:
import ray from ray import serve ray.init( address="auto", runtime_env={ "env_vars": { "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "${TAOTOKEN_API_KEY}", "NCCL_IB_DISABLE": "0", "NCCL_SOCKET_IFNAME": "ib0", } } ) serve.start(http_options={"host": "0.0.0.0", "port": 8000})Rollout 服务用 SGLang 引擎,配置重点是 tp_size 和 micro batch 的切分粒度。Relax 把全局 batch 切成 micro batch 做流水线,256 条的全局 batch 切成 32 条一组,每组生成完立即写入 TransferQueue:
from relax import RolloutService, RolloutConfig rollout_config = RolloutConfig( engine="sglang", model_path="/data/models/Qwen3-4B", tp_size=2, dp_size=4, micro_batch_size=32, global_batch_size=256, max_new_tokens=4096, temperature=0.7, top_p=0.95, partial_rollout=True, partial_rollout_timeout=300, enable_processor_pool=True, processor_workers=8, ) rollout_service = RolloutService.options( num_replicas=4, ray_actor_options={"num_gpus": 2} ).bind(rollout_config)Train 服务用 Megatron 后端,关键是和 Rollout 服务的资源隔离。Train 服务不直接引用 Rollout 的实例,全部通过 TransferQueue 通信:
from relax import TrainService, TrainConfig train_config = TrainConfig( backend="megatron", model_path="/data/models/Qwen3-4B", tp_size=2, pp_size=1, dp_size=4, micro_batch_size=8, global_batch_size=256, learning_rate=1e-6, kl_coef=0.01, clip_range=0.2, advantage_estimator="grpo", use_r3=True, r3_replay_mode="async", ) train_service = TrainService.options( num_replicas=1, ray_actor_options={"num_gpus": 8} ).bind(train_config)TransferQueue 数据总线是连接两个服务的核心,配置里要指定存储路径和异步读写参数:
from relax import TransferQueueConfig tq_config = TransferQueueConfig( storage_path="/dev/shm/relax_tq", max_size_gb=64, async_put=True, async_get=True, prefetch_batches=4, enable_multimodal=True, )把三个服务串起来启动:
from relax import RelaxPipeline pipeline = RelaxPipeline( rollout=rollout_service, train=train_service, transfer_queue=tq_config, checkpoint_dir="/data/checkpoints/relax_qwen3_4b", dcs_enabled=True, dcs_topology_aware=True, elastic_rollout=True, min_rollout_replicas=2, max_rollout_replicas=8, ) pipeline.run()这套配置的核心逻辑是:Rollout 用 4 个副本各 2 张卡做推理,Train 用 8 张卡做梯度更新,两边通过 TransferQueue 异步读写。micro_batch_size=32 是 Rollout 侧的切分粒度,Train 侧 micro_batch_size=8 是训练切分粒度,两者不需要一致,TransferQueue 会做缓冲。partial_rollout=True 开启部分回收,超时样本已生成的部分直接复用,不白等也不白扔。
4. 验证请求与吞吐对比动作
配置写完,先做一次最小验证,确认 Rollout 和 Train 能正常通信。用一个简单的数学推理任务跑通链路:
from relax import RelaxClient client = RelaxClient("http://localhost:8000") # 提交一个验证任务 task = client.submit( prompt="计算 23 * 47 的结果,并解释计算过程。", reward_fn="math_verify", max_turns=3, ) result = client.wait(task.id, timeout=600) print(f"生成结果: {result.output}") print(f"Reward: {result.reward}") print(f"Rollout 耗时: {result.rollout_time_ms}ms") print(f"Train 耗时: {result.train_time_ms}ms")如果链路通了,你会看到 Rollout 和 Train 的耗时是重叠的,而不是串行相加。接下来做吞吐对比验证,这是复现 76% 提升的关键动作。
对比实验分三组:Colocate On-Policy、Async On-Policy、Async Off-Policy。用同一份 DAPO-MATH-17k 数据集,Qwen3-4B 模型,16 张 H800。Colocate 模式把 Rollout 和 Train 绑在同一组卡上:
# Colocate 模式启动 python -m relax.launch \ --mode colocate \ --model /data/models/Qwen3-4B \ --dataset /data/datasets/DAPO-MATH-17k \ --gpus 16 \ --global_batch_size 256 \ --micro_batch_size 32 \ --steps 100Async Off-Policy 模式用上面的分离配置,Rollout 和 Train 各占独立 GPU:
# Async Off-Policy 模式启动 python -m relax.launch \ --mode async_off_policy \ --model /data/models/Qwen3-4B \ --dataset /data/datasets/DAPO-MATH-17k \ --rollout_gpus 8 \ --train_gpus 8 \ --global_batch_size 256 \ --micro_batch_size 32 \ --partial_rollout \ --steps 100跑完之后对比 steps/hour 指标。官方数据是 Async Off-Policy 达到 28.7 steps/hour,Colocate 是 16.3 steps/hour,提升 76%;veRL 全异步是 23.9 steps/hour,Relax 比它快 20%。你在自己环境跑的时候,重点看三个指标:steps/hour、单 step 的 wall-clock 时间、以及长尾样本对整体 step 的拖累程度。
验证收敛质量同样重要。按 wall-clock time 看,三种模式最终收敛到相同 reward 水平,但 Async Off-Policy 达到同等 reward 的 wall-clock 时间比 Colocate 缩短 43%。你可以在训练日志里记录每 10 个 step 的平均 reward,画一条 reward vs wall-clock 的曲线,确认没有因为异步导致质量下降。
如果你在验证过程中需要快速调模型做对比测试,可以用 TaoToken 的模型对话入口 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite ,直接对比不同模型在相同 prompt 下的输出差异,不用改训练代码。
5. 本篇常见错排查
配置 Relax 的过程中,有几个报错出现频率很高,我按实际踩坑顺序列出来。
第一个是 TransferQueue 连接超时。报错长这样:TransferQueueTimeoutError: Failed to connect to storage at /dev/shm/relax_tq after 30s。原因通常是 /dev/shm 空间不够,或者多个 Ray actor 挂载的路径不一致。检查df -h /dev/shm,如果小于 64GB,把 storage_path 改到实际数据盘,比如/data/relax_tq。另外确认所有节点的 Ray runtime_env 里路径一致,跨节点训练时 /dev/shm 是各自独立的,必须用共享存储。
第二个是 NCCL 通信卡住。现象是 Rollout 和 Train 都启动了,但 step 一直不推进,日志停在NCCL INFO Bootstrap。这通常是 IB 网卡配置问题。先确认ibstat能看到活动端口,然后检查 NCCL_SOCKET_IFNAME 是否写对,用ip addr看实际网卡名。如果集群没有 IB,把 NCCL_IB_DISABLE=1,走 TCP 也能跑,但吞吐会降。还有一个隐蔽原因:不同角色的 CUDA_VISIBLE_DEVICES 有重叠,导致 NCCL 初始化时 rank 冲突,检查 Ray actor 的 num_gpus 分配是否和物理卡一一对应。
第三个是 R3 路由回放导致 log probs mismatch。报错是RoutingMismatchError: expert routing mismatch between rollout and training。Relax 的 R3 机制在 Rollout 时记录路由决策,Training 时原样回放,但如果你的模型是 MoE 且用了自定义路由,需要确认 r3_replay_mode 设为 async。同步模式下 R3 开销会加到关键路径上,官方数据是 veRL 的 R3 开销 +32%,Relax 异步模式只有 +1.9%。检查配置里use_r3=True和r3_replay_mode="async"同时生效。
第四个是 partial rollout 回收的数据格式错误。报错PartialRolloutFormatError: incomplete sequence missing finish_reason。这是因为超时样本的已生成部分没有正确的结束标记,下游 Advantage 计算时解析失败。解决办法是在 RolloutConfig 里设partial_rollout_pad_token="<|endoftext|>",让回收的部分自动补结束符。另外确认 partial_rollout_timeout 不要设太短,300 秒是 Agentic 多轮场景的合理值,太短会导致大量样本被截断。
第五个是 DCS 权重同步失败。报错DCSSyncError: rank mapping mismatch for TP=2 PP=1。DCS 做拓扑感知的权重传输,如果 Rollout 和 Train 的 TP/PP 配置不一致,rank 映射会对不上。检查两边 tp_size 和 pp_size 是否匹配,如果不匹配,DCS 需要显式配置 rank_mapping。弹性扩缩场景下,新加入的 Rollout 实例会经过 PENDING→CREATING→HEALTH_CHECKING→WEIGHT_SYNCING→READY→ACTIVE 六个阶段,任何一步超时都会回滚,看日志确认卡在哪一步。
如果你在接入 TaoToken 做 Reward 打分时遇到 401 或 429,先确认 API Key 有没有过期,然后看 settings.json 里的 base_url 是不是写成了带 UTM 的地址。正确的 API 端点是 https://taotoken.net/api ,不带任何查询参数。429 通常是并发太高,Reward 服务里加个信号量限流,或者把 max_retries 调到 5。
6. 长期编码与 Agent 场景的接入建议
Relax 的服务化架构对 Agentic 场景是天然适配的。自定义 Rollout 可以写成可插拔服务,Agent 要调工具、查数据库、跑沙箱,挂上去就行;多轮状态管理在服务化架构下就是服务间的消息传递;Reward 可以按需组合 Rule-based、LLM-as-Judge、自定义函数。
如果你打算长期跑 Agentic RL 训练,建议把模型调用统一收口到 TaoToken,用 Coding Plan 管理编码和 Agent 场景的调用配额,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。这样 Rollout 生成、Reward 打分、工具调用三条链路共用一个 Key,轮换和监控都省事。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各语言的 SDK 示例和错误码说明。
最后说一个实际经验:Relax 的 micro batch 流水线粒度不要设得太细。我试过把 global_batch_size=256 切成 8 条一组的 micro batch,结果 TransferQueue 的元数据开销反而拖慢了整体吞吐。32 条一组是官方验证过的平衡点,长尾样本的影响被限制在单个 micro batch 内,同时流水线调度开销可控。如果你的 Agentic 任务单条样本特别长,可以适当放大到 64,但不要超过 global_batch_size 的四分之一。