- 文档
- 教程
- 人工智能
- 大模型
- RLHF
【免费下载链接】Awesome-ML-SYS-Tutorial
My learning notes for ML SYS.
本篇技术指南围绕 slime 社区推出的「VLM/LLM 多轮强化学习(Agentic Multi-Turn RL)统一范式」展开,讲解如何仅通过编写一套自定义
rollout函数与交互环境(Environment),即可让视觉语言模型(VLM)像 LLM 一样进行多轮 Agent 交互训练。读完本文,你将掌握 slime 中--rollout-function-path与--rollout-interaction-env-path的接入方式、多轮「生成 → 环境交互 → 观测回传 → 迭代推理」的循环设计与终止条件、环境接口约定,以及 Observation 增量编码(dummy messages + delta tokens)和多模态训练张量缓冲合并等工程细节,并了解其在一套开源仓库(Awesome-ML-SYS-Tutorial)中的源码级佐证与实验结果。
背景:Agentic VLM 为什么需要多轮 RL
与传统的单轮推理(Single-turn Inference)不同,Agentic VLM 的本质是连续交互:模型不再是端到端地吐出一个"最终答案",而是作为决策核心,在执行动作(Action)与感知环境观测(Observation)的往复循环中不断演进。每一次模型输出都是对环境的一次试探,环境每一轮的反馈又为模型下一步行动提供更多信息。这种与环境的多轮交互,被认为是 VLM 进化为真正智能体的必经之路。
典型场景包括 Computer Use Agent 与具身智能:模型不是孤立的对话机器人(chatbot),而是深嵌在环境链路中的思维引擎(thinking machine)。它需要具备"审时度势"的能力——输出 Action 引起环境状态变更(UI 状态、物理位移等),实时捕获环境以图片等丰富形式返回的 Observation,并在持续滚动的长上下文中完成复杂推理。
这正是 slime 协同 Miles 社区在 VLM Agentic Training 中攻克的核心场景。得益于其在 LLM Multi-Turn Training 阶段就完成的解耦设计,用户仅需通过--rollout-function-path参数传入为 VLM Agent 设计的交互逻辑,即可无缝衔接「自主生成 → 环境交互 → 多模态观测回传 → 迭代推理」的完整链路。其设计哲学保持一贯的极致解耦:Rollout 逻辑不与任何特定数据集格式或交互协议强绑定,"环境如何解析 Action、如何执行工具、如何反馈 Observation"完全由用户自由决定。
该方案与 SGLang RL 团队近期在 RL 训练稳定性、效率与适用场景方面的系列工作一脉相承,包括 INT4 QAT 全流程训练(见 rlhf/slime/int4/readme-en.md)、FP8 全流程训练与采样、投机采样(见 rlhf/slime/spec/readme-en.md)以及 Rollout Router Replay 机制等。
核心设计:从第一性原理出发,只需定义采样与交互逻辑
正如文档反复强调的,从第一性原理出发,任何 multi-turn 训练本质上只需要定义采样与交互逻辑。
以 slime 的 LLM Multi-Turn Training(例如 Search-R1 式的自定义采样)为例:模型在每一轮根据当前上下文生成动作指令,实时捕获环境观测并将其增量注入上下文,直至模型决定返回结论或超出上下文限制。得到完整的 trajectory 后,再通过正确的loss mask区分模型输出的动作指令(loss_mask=1)与环境反馈信息(loss_mask=0)。
VLM 与 LLM 的多轮采样并无本质区别,只需在每一轮交互中额外维护并拼接多模态上下文信息。slime 将 environment 与 rollout 明确解耦——"环境如何解析 Action"等设计完全独立于采样与训练之外,从而提升了可复用性与可扩展性。
多轮交互迭代逻辑
整个多轮循环由以下 5 个步骤组成:
- 初始化任务:从
Sample提取prompt与多模态输入,完成首轮编码,初始化sample.tokens、image_data、multimodal_train_inputs_buffer等,为多轮循环提供初始上下文。 - 模型生成:模型产生本回合执行的动作,追加到上下文,并将对应位置的 loss mask 设置为 1(这些是模型需要被训练学习的动作指令)。
- 环境接受动作:把模型输出传递给 env,env 返回 observation(可能包含多模态内容)。
- 追加 Observation 到上下文:将 observation 编码为下一回合的输入:
- 获取干净的
prompt_ids(详见下文"工程附录"),追加到上下文,并将对应位置的loss_mask设置为 0; - VLM 场景下 observation 可能携带新的多模态内容,因此需要同时维护两条链路的拼接:
- rollout 侧的
image_data:每轮把新图片 encode 后 append; - 训练侧的
multimodal_train_inputs:每轮 processor 产生的张量需要合并。
- rollout 侧的
- 获取干净的
- 终止条件:由以下限制条件共同决定:
- max_turns:最多执行
max_turns轮交互,达到上限后无论任务是否完成都将被迫停止; - token budget:为避免采样长度过长,维护一个可用 token 预算,每次模型生成或追加 observation 都会消耗预算,一旦耗尽就提前停止并标记为截断(TRUNCATED),确保不超出最大上下文或最大生成限制;
- env done:环境在
env.step()中返回done=True,表示任务已完成或无法继续(如已得到最终判定、进入终止状态),rollout 立即停止。
- max_turns:最多执行
上图展示了该设计的完整闭环:左侧为 Rollout 多轮交互采样分支(Prepare model inputs → SGLang generate → Concate assistant tokens(loss mask = 1)→ Tool Call/Environment Interaction → Encode and Concate Observation(loss mask = 0)→ 终止判断 → Finalize Sample 拼接多模态 tensor),右侧为 Training 分支(Megatron/FSDP),两者通过Sample与Update Weights形成 RLHF 迭代闭环。
文档给出了自定义多轮rollout.generate的伪代码骨架,它完整地对应上述 5 步逻辑:
# Pseudocode: custom multi-turn rollout.generate async def generate(args, sample, sampling_params): # 0) Init: load custom variable like envrionment path and max_turn env = load_env_module(args.rollout_interaction_env_path).build_env(sample=sample, args=args) max_turns = args.max_turns # injected via --custom-config-path (YAML) # 1) Encode initial prompt and multimodal inputs sample.tokens, image_data, mm_train_buffer = init_from_prompt(sample, state) # 2) Turn loop: actor -> env -> append observation -> repeat for _ in range(max_turns): # (a) Actor generation (assistant tokens) response_text, new_tokens, new_logprobs, finish_reason = sglang_generate( url=url, input_ids=sample.tokens, sampling_params=sampling_params, image_data=image_data ) append(sample, new_tokens, new_logprobs, loss_mask_val=1) # (b) Env step (returns next observation; may include multimodal payload) observation, done, _ = env.step(response_text) if done: break # (c) Process and append observation tokens user_msg = env.format_observation(observation) obs_ids, obs_image_data, obs_mm_inputs, obs_mm_train = encode_observation_delta( user_msg, tokenizer=state.tokenizer, processor=state.processor, tools=sample.metadata.get("tools") ) append(sample, obs_ids, [0.0] * len(obs_ids), loss_mask_val=0) # (d) Multimodal state update image_data += obs_image_data # inference-side image_data if obs_mm_train: mm_train_buffer.append(obs_mm_train) # training-side image_data return sample要点解读:
rollout_interaction_env_path指定交互环境的模块路径,通过load_env_module加载后调用build_env实例化环境;max_turns通过--custom-config-path(YAML)注入,与采样逻辑解耦;- 每个回合中,模型生成的动作 token 以
loss_mask_val=1追加(参与训练),环境观测 token 以loss_mask_val=0追加(不参与损失计算,仅作为上下文); - 多模态状态沿两条链路同步更新:
image_data服务于推理侧 SGLang 生成,mm_train_buffer服务于训练侧。
环境接口(BaseInteractionEnv)
为了便于用户自定义环境,slime 为环境基类BaseInteractionEnv定义了如下公共接口:
reset():清空环境内部状态;step(response_text: str) -> (observation: dict, done: bool, info: dict):接收模型输出,返回观测、是否结束以及额外信息;format_observation(observation: dict) -> dict:把 observation 转成下一回合要追加的 chat message;如果 observation 携带multi_modal_data,会把图片放进 message content。
这组接口设计得非常克制:环境只负责"接收动作、产出观测、报告终止",其余一切(工具如何执行、观测如何产生)完全由用户实现,从而保证了 rollout 主循环的稳定与可复用。
工程附录:两个关键工程细节
Observation Tokens 编码:dummy messages + delta tokens
在 multi-turn rollout 中,每一轮环境都会返回 observation,需要将其编码成prompt_ids追加到sample.tokens,让下一轮生成能"看到"环境反馈。直觉做法是直接对 observation 调用tokenizer.apply_chat_template([message], tools=...),但这会引入一个实际问题:chat template 往往会自动插入 system prompt 以及 tool 的使用说明(若tools非空),例如:
<|im start]>system You are a helpful assistant that can use tools to get information for the user. # Tools You may call one or more functions to assist with the user query.You are provided with function signatures within <tools></tools> XMLtags: <tools> ...如果每轮都对 observation 直接做上述操作,这些文本会被重复追加到上下文,导致两个问题:
- 上下文被重复内容快速撑大,浪费 token budget;
- 即便这些 observation tokens 在训练中被
loss_mask=0屏蔽,它们仍占据上下文位置,可能影响行为分布与稳定性。
解决方案是采用一个通用技巧:用固定的DUMMY_MESSAGES作为模板基座,计算其对应的 token 数,只取 observation 带来的增量 tokens(delta tokens)。核心思路分三步:
- 先对
DUMMY_MESSAGES单独 apply chat template,得到dummy_prompt(包含 system/tool preamble,但不包含本轮 observation); - 再对
DUMMY_MESSAGES + [message]apply chat template,得到formatted_prompt(包含相同的 system/tool preamble + 本轮 observation); - 用
trim_length = len(encode(dummy_prompt))得到需要裁剪的前缀长度,对formatted_prompt编码后直接切片:prompt_ids = prompt_ids[trim_length:],从而确保追加到上下文中的只是干净的 observation tokens,不会把 system/tool preamble 每轮重复塞入。
伪代码概括如下:
dummy = apply_chat_template(DUMMY_MESSAGES, tools=tools, add_generation_prompt=False) full = apply_chat_template(DUMMY_MESSAGES + [obs_msg], tools=tools, add_generation_prompt=True) trim = len(encode(dummy)) obs_ids = encode(full)[trim:] # delta tokens only这一技巧既节省了上下文空间,又保持了 chat template 语义的完整性(system/tool preamble 依然只出现一次)。
多轮multimodal_train_inputs的缓冲合并
多轮 rollout 中,每一轮把 observation 编码进上下文时,若使用 VLM 的 processor,会产出一份供训练侧使用的multimodal_train_inputs(一个 dict,value 往往是torch.Tensor,例如图片相关的特征信息)。关键问题是:这些张量是按轮产生的碎片化 tensor,而训练最终希望得到"拼起来的一整块 tensor"。
slime 的策略是:先 buffer,最后按 key 只做一次torch.cat。实现分两步:
- 逐轮收集到 buffer:每次
_encode_observation_for_generation(...)产出obs_multimodal_train_inputs时,不立即拼接,而是执行multimodal_train_inputs_buffer.append(obs_multimodal_train_inputs); - 结束时统一 merge:在
_finalize_sample(...)中调用_merge_multimodal_train_inputs(multimodal_train_inputs_buffer):- 先把每一轮的 dict 按 key 聚合成
values_by_key[key] = [t0, t1, ...]; - 对每个 key只做一次
torch.cat(values, dim=0)得到最终 tensor。
- 先把每一轮的 dict 按 key 聚合成
这样每个 key 对应的 tensor 只发生一次大张量分配 + 一次线性拷贝。相比每轮都 concat 一次,其好处是:
- 避免反复大块显存分配与拷贝(
torch.cat每次都会新分配输出张量并复制旧内容); - 降低碎片化风险,避免每轮
cat时短暂同时持有old + new带来的峰值显存抖动; - 整体把拷贝/分配开销从O(n²) 降到 O(n),peak memory 更稳定、更不易 OOM。
结合仓库源码:自定义 rollout 的加载链路与数据流
在 Awesome-ML-SYS-Tutorial 仓库中,rlhf/slime/code-walk-through/readme.md 对 slime 的 rollout 系统进行了源码级走读,可以与本篇文档的设计相互印证。
Rollout 函数的动态加载:在RolloutController初始化中(对应 slime 源码slime/ray/buffer.py),框架通过load_function动态加载用户指定的 rollout 函数与 eval 函数:
self.generate_rollout = load_function(self.args.rollout_function_path) self.eval_generate_rollout = load_function(self.args.eval_function_path)这正是--rollout-function-path参数的核心机制:用户传入自定义函数路径后,框架在运行时加载并调用,无需改动训练主循环。从源码结构看,多轮 VLM 的generate函数即是通过该机制注册进 rollout 主流程的(对应RolloutController.generate()中的self.generate_rollout(self.args, rollout_id, self.data_source, evaluation=False)调用链)。
自定义 rollout 的函数签名约定(见 rlhf/slime/code-walk-through/original/part-4.md):
def generate_rollout(args, rollout_id, data_source, evaluation=False) -> list[list[Sample]]: """ Args: args: 全局参数 rollout_id: rollout标识 data_source: 数据源 evaluation: 是否为评估模式 Returns: list[list[Sample]]: 生成的样本组 """ return samplesSample 与 loss_mask 的数据结构:Sample对象承载tokens、response、response_length、reward、loss_mask等字段,并带有Status枚举(PENDING/COMPLETED/TRUNCATED/ABORTED)。多轮循环中每轮追加的 token 与 loss mask 都会写入sample,最终由_convert_samples_to_train_data统一转换为训练数据,其中loss_masks会校验长度必须与response_length一致,确保"动作指令算损失、环境反馈不算损失"的语义在训练端严格生效。
关键配置参数(整理自 rlhf/slime/code-walk-through/original/part-4.md 的参数表):
| 参数 | 说明 | 默认值 |
|---|---|---|
rollout_function_path | 自定义 rollout 函数路径(VLM 多轮即在此实现) | slime.rollout.sglang_rollout.generate_rollout |
eval_function_path | 评估函数路径 | - |
rollout_interaction_env_path | 交互环境模块路径(多轮 VLM 新增) | - |
max_turns | 最大交互轮数,经--custom-config-path(YAML)注入 | - |
rollout_max_response_len | 最大响应长度(文中实验从 4096 上调到 32000) | 4096(默认脚本) |
rollout_num_gpus_per_engine | 每个 rollout 引擎使用的 GPU 数量 | 0.2 |
实验验证:Qwen3-VL-2B 上的 Agentic Multi-Turn GRPO
基于上述设计,团队使用 geo3k 多模态数据集对Qwen3-VL-2B-Instruct进行了 Agentic Multi-Turn GRPO 训练,以 Megatron-LM 作为训练后端(训练脚本对应 slime 上游仓库examples/geo3k_vlm_multi_turn/run_geo3k_vlm_multi_turn.py)。
短轮次/短上下文实验
默认设置(--rollout-max-response-len=4096、max_turns=3)下的训练曲线如下:
图中 6 个子图分别刻画了 raw reward、平均/最大响应长度、重复率(repetition fraction)、log probs 与 advantages 随rollout/step的变化趋势。从结果看:
- raw reward 持续上升并收敛,说明 actor model 实现了有效学习;
- repetition fraction 很快下降,未出现无效语言重复问题;
- 模型的平均响应长度显著缩短,模型逐步学会更高效的推理方式。
长轮次/长上下文压力测试
为了进一步测试性能与稳定性,将--rollout-max-response-len从默认的 4096 逐渐增加到 32000,并将max_turns从 3 调大到 20,得到如下结果:
可以看出,raw reward 仍稳定上升并收敛,其他指标的变化趋势几乎与短上下文、小轮数时无异,说明该设计在更激进的长轮次设置下依然稳定。
性能方面,与短上下文、小轮数相比,训练时间与采样时间均有上升,且采样时间与训练时间的比值明显增大(多轮采样耗时占比更高,符合预期)。需要提醒的是:如果上下文长度或轮数设置过大,可能出现 OOM,需要根据自身硬件条件和场景合理设置参数(如上下文长度、max_turns、token budget 等)。
未来工作方向
随着 multimodal agentic AI 训练需求快速增长,VLM multi-turn RL 需要在可扩展性与可诊断性上继续加强,文档明确了以下四个方向:
- 更稳健的回合控制与重试机制:当前 turn loop 采用
for turn_idx in range(max_turns),在环境稳定、交互逻辑简单时可用;但接入更复杂的交互环境(如 OS 执行)时,env 可能因超时、动作解析失败、偶发服务错误而失败。未来考虑引入 retry 功能:将回合推进改为while循环,仅在"成功完成一次有效交互"后才递增 turn,并指定失败预算(如max_env_retries_per_turn),在可恢复场景下通过env.reset()重试当前 turn,同时避免无限循环与不可控的运行时开销。 - 在多轮采样中使用各类 async 训练方法提升性能:多轮采样中不同样本的实际轮数与长度差异较大,少数超长样本造成的长尾效应可能成为整体吞吐的主要瓶颈。slime 已支持 partial rollout(相关代码走读见 rlhf/slime/batch-GAE/ppo-gae-chunk.md),但尚未与 VLM 多轮采样充分适配与测试,这将是未来工作方向之一。
- 支持 LLM-as-judge 等更复杂的交互反馈:Geo3K 示例环境是 rule-based 的最小实现,便于验证链路;而很多 multi-turn 场景会引入一个 LLM 提供每轮的反馈、批改或评价。由于 rollout 与 env 已解耦,引入 LLM judge 本身并不要求修改 rollout 主循环——用户只需实现自己的 env 并通过
rollout_interaction_env_path替换即可。不过随着交互逻辑变复杂,当前BaseInteractionEnv基类可能偏简单,未来可考虑为环境接口补充更强能力。 - 更完善的 logging 与 turn-level 指标体系:未来需补充更细粒度的指标,如实际执行的轮数分布、截断原因分布、env retry 次数与类型等;当前日志多为整条轨迹粒度,而调参与排障往往需要轮粒度(per-turn)的 debug 信息,因此计划支持每轮 logging。
结语
本文围绕 slime 的 VLM/LLM 多轮 Agentic RL 统一设计,完整覆盖了从第一性原理的采样逻辑定义、多轮交互循环与终止条件、环境接口约定,到 dummy messages + delta tokens 的观测增量编码、multimodal_train_inputs的缓冲合并等工程细节,并以 Qwen3-VL-2B 在 geo3k 数据集上的短/长轮次 GRPO 实验验证了方案的稳定性。其核心启示在于:通过极致解耦(rollout 与环境分离、训练与采样分离),一套自定义rollout函数即可同时承载 LLM 与 VLM 的 Agentic 多轮训练,而所有多模态上下文管理、loss mask 语义与张量合并等复杂工程问题,都被收敛在采样层内部解决,为后续接入更复杂环境(OS 执行、LLM-as-judge 等)保留了充分的扩展空间。
Acknowledgements
Xiaole Guo, Nan Jiang, Zilin Zhu, Jin Pan, Jiajun Li, Yuzhe Zhou, Chengxing Xie, Yueming Yuan, Chenyang Zhao
- 文档
- 教程
- 人工智能
- 大模型
- RLHF
【免费下载链接】Awesome-ML-SYS-Tutorial
My learning notes for ML SYS.
相关推荐
slime 中的 SGLang PD Disaggregation:为 Agentic RL 与长上下文 Rollout 拆分 Prefill/Decode 服务拓扑
slime 中的 SGLang PD Disaggregation:为 Agentic RL 与长上下文 Rollout 拆分 Prefill/Decode 服
人工智能大模型强化学习RLHF分布式训练slime × Tau-Bench 实战:Agentic 多轮工具调用环境下的 RL 训练指南
slime × Tau Bench 实战:Agentic 多轮工具调用环境下的 RL 训练指南 本文围绕 slime 开源仓库中 examples/tau be
人工智能大模型强化学习RLHF分布式训练slime Agentic RL 接入技术路线:从多轮工具调用、Agent Runtime Adapters 到 test-based reward 的完整实战指南
slime Agentic RL 接入技术路线:从多轮工具调用、Agent Runtime Adapters 到 test based reward 的完整实战
人工智能大模型强化学习RLHF分布式训练
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考