☰
slime 统一 VLM 与 LLM 多轮 Agentic RL:从自定义 rollout 到多模态上下文管理的完整实践
2026/9/29 2:43:16 网站建设 项目流程
  • 文档
  • 教程
  • 人工智能
  • 大模型
  • RLHF

【免费下载链接】Awesome-ML-SYS-Tutorial

My learning notes for ML SYS.

项目地址:https://gitcode.com/gh_mirrors/aw/Awesome-ML-SYS-Tutorial
点击查看免费下载

本篇技术指南围绕 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 个步骤组成:

  1. 初始化任务:从Sample提取prompt与多模态输入,完成首轮编码,初始化sample.tokens、image_data、multimodal_train_inputs_buffer等,为多轮循环提供初始上下文。
  2. 模型生成:模型产生本回合执行的动作,追加到上下文,并将对应位置的 loss mask 设置为 1(这些是模型需要被训练学习的动作指令)。
  3. 环境接受动作:把模型输出传递给 env,env 返回 observation(可能包含多模态内容)。
  4. 追加 Observation 到上下文:将 observation 编码为下一回合的输入:
    • 获取干净的prompt_ids(详见下文"工程附录"),追加到上下文,并将对应位置的loss_mask设置为 0;
    • VLM 场景下 observation 可能携带新的多模态内容,因此需要同时维护两条链路的拼接:
      • rollout 侧的image_data:每轮把新图片 encode 后 append;
      • 训练侧的multimodal_train_inputs:每轮 processor 产生的张量需要合并。
  5. 终止条件:由以下限制条件共同决定:
    • max_turns:最多执行max_turns轮交互,达到上限后无论任务是否完成都将被迫停止;
    • token budget:为避免采样长度过长,维护一个可用 token 预算,每次模型生成或追加 observation 都会消耗预算,一旦耗尽就提前停止并标记为截断(TRUNCATED),确保不超出最大上下文或最大生成限制;
    • env done:环境在env.step()中返回done=True,表示任务已完成或无法继续(如已得到最终判定、进入终止状态),rollout 立即停止。

上图展示了该设计的完整闭环:左侧为 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)。核心思路分三步:

  1. 先对DUMMY_MESSAGES单独 apply chat template,得到dummy_prompt(包含 system/tool preamble,但不包含本轮 observation);
  2. 再对DUMMY_MESSAGES + [message]apply chat template,得到formatted_prompt(包含相同的 system/tool preamble + 本轮 observation);
  3. 用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。实现分两步:

  1. 逐轮收集到 buffer:每次_encode_observation_for_generation(...)产出obs_multimodal_train_inputs时,不立即拼接,而是执行multimodal_train_inputs_buffer.append(obs_multimodal_train_inputs);
  2. 结束时统一 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。

这样每个 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 samples

Sample 与 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.

项目地址:https://gitcode.com/gh_mirrors/aw/Awesome-ML-SYS-Tutorial
点击查看免费下载

相关推荐

上一篇:gh_mirrors/sh1/sh的内存分配分析:使用trace工具追踪内存分配
下一篇:Mayan EDMS完整功能解析:为什么它是最先进的文档管理系统

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询