07. Rollout、推理后端与权重同步
本章基于源码快照
d33ddd7140f44d392e0e10b48a8902651a1340f4的 V1 主路径讲解。这里的“当前”很重要:verl 的 rollout 架构已经从旧的同步 SPMD 接口迁移到原生异步 server 模式。读旧博客时如果看到sharding_manager、同步generate_sequences()或把hf当成正式 rollout backend,请先对照本章的“版本边界”。
本章目标
学完后,你应该能回答以下问题:
- actor、rollout engine、server、replica、client 和 load balancer 分别是什么?
- 一条 prompt 如何变成 token,再如何进入 PPO/GRPO 的训练 batch?
- vLLM、SGLang、TensorRT-LLM 和 Hugging Face Transformers 在当前代码中的真实边界是什么?
- 为什么同一个模型要同时有“训练副本”和“推理副本”,更新后又如何同步权重?
rollout_log_probs、old_log_probs和ref_log_prob为什么不是同一个东西?- hybrid 模式为什么要让推理引擎睡眠、释放 KV cache,再切回训练?
1. 先建立一个最重要的心智模型
在 RL post-training 中,同一个策略模型承担两种工作:
- rollout / 采样:给定 prompt,让模型高速自回归生成 token,必要时调用工具,形成 trajectory。
- actor training / 策略训练:对完整 trajectory 做 forward/backward,用 reward、advantage 和 PPO/GRPO loss 更新参数。
这两种工作对系统的要求不同:
| 工作 | 最关心什么 | 典型引擎 |
|---|---|---|
| rollout | token/s、连续批处理、Paged KV cache、prefix cache | vLLM、SGLang、TensorRT-LLM |
| actor training | 梯度、optimizer state、参数分片、反向传播 | FSDP、Megatron、VeOmni、TorchTitan 等训练 engine |
所以 verl 通常不是让一个对象同时做 model.generate() 和 backward(),而是维护两个逻辑副本:
训练 actor:保存参数分片、梯度、optimizer state;负责更新 θ
推理 rollout:保存适合 decoding 的权重布局和 KV cache;负责用 θ_rollout 生成 token
actor 到达配置的参数同步点后,需要把新参数从 θ 同步到 θ_rollout。同步 trainer 通常每个外层 step 做一次;separate_async 则可以先在一个外层 step 中完成 parameter_sync_step 次 _step_once(),再于 on_step_end() 单次同步。这就是 checkpoint engine / weight sync 子系统存在的原因。
源码入口:
- rollout 抽象与正式 backend registry:
verl/workers/rollout/base.py:L29-L109 - replica 与部署模式:
verl/workers/rollout/replica.py:L39-L141 - server manager、client 和负载均衡:
GlobalRequestLoadBalancer与 client、LLMServerManager - actor 与 rollout 的组合 worker:
verl/workers/engine_workers.py:L446-L470 - checkpoint engine 抽象:
verl/checkpoint_engine/base.py:L49-L205
2. 必要前置知识
2.1 自回归生成、logits 和 log probability
假设当前已有 token x_0, ..., x_t。语言模型输出下一个 token 的 logits:
公式含义: 模型读完当前位置及之前的 token 后,为“下一个 token 可能是什么”给词表中的每个候选项打一个原始分数;这些分数还不是概率。
符号说明:
- 是模型在第 个位置输出的整组 logits;下标 表示“当前位置的下一位”。
- 表示语言模型这个计算函数, 表示模型当前的全部参数(也就是训练会更新的权重)。
- 表示从第 0 个位置到第 个位置的已有 token; 是 token,数字下标是位置, 表示省略中间连续的项。
- 等号表示右边的模型计算结果就是左边的 logits。
经过 temperature、top-k、top-p 等处理后得到采样分布 π_rollout,从中采出 x_{t+1}。被选中 token 的 log probability 是:
公式含义: 在已经看到前面所有 token 的条件下,rollout 实际采样分布给刚选中的下一个 token 分配了多大概率,再对这个概率取对数。结果通常不大于 0;越接近 0,表示该 token 在这个分布下越可能被选中。
符号说明:
- 表示推理后端实际用来采样的概率分布;希腊字母 常用来表示策略,直立的下标
rollout说明这里特指 rollout 时的策略。 - 表示自然对数;它把概率转换为 log probability。
- 是这一步实际选中的下一个 token,下标含义与上一式相同。
- 表示位置不超过 的全部 token,也就是 ; 表示“小于或等于”。
- 竖线 表示“在……条件下”:右边是已经知道的上下文,左边是要计算概率的 token。
生成一个长度为 R 的 response,就有 R 个被采样 token,也应有 R 个逐 token log probability。verl 的 backend 返回的核心结构正是:
TokenOutput(
token_ids=[21, 22, 23],
log_probs=[-0.31, -0.74, -0.08],
stop_reason="completed",
extra_fields={"global_steps": 42},
)
真实定义见 TokenOutput。这里的 global_steps 表示产生这些 token 时 rollout 权重的版本。
2.2 KV cache 是什么
自回归生成第 t+1 个 token 时,不需要重新计算前 t 个 token 的全部 attention key/value;推理引擎会把它们保存在 KV cache 中。它提高生成速度,但会占用大量显存。
需要区分两种 cache:
- 一次请求内部的 KV cache:让后续 token 复用前缀计算。
- prefix/radix cache:让多个请求或多轮对话复用相同 prompt 前缀。
模型权重更新后,旧权重产生的 KV cache 不再可信。因此权重同步流程必须释放或清空相关 cache,而不能只覆盖权重后继续使用旧 cache。
2.3 TP、DP、PP 与 replica
一个 rollout replica 是一个可以独立接收请求的推理服务实例。普通情况下,一个 replica 占用的 worker/GPU 数为:
公式含义: 一个普通 rollout replica 需要的 worker/GPU 数,等于张量并行、数据并行和流水线并行规模的乘积。例如任一并行规模翻倍,单个 replica 的资源占用也会相应翻倍。
符号说明:
- 是单个 replica 的总 worker/GPU 数;
world_size使用直立字体表示一个完整变量名,下标replica说明统计范围是“一个 replica”。 - 是 tensor parallel size(张量并行规模),表示一份模型的张量计算拆到多少个 worker/GPU 上。
- 是 data parallel size(数据并行规模),表示有多少份并行处理不同请求或数据的模型副本。
- 是 pipeline parallel size(流水线并行规模),表示模型按层被切成多少个流水线阶段。
- 是乘号,表示三个并行维度的资源数量要相乘,而不是相加。
计算发生在 RolloutReplica.__init__。server manager 再用总 GPU 数除以每个 replica 的 footprint,得到 replica 数量,见 LLMServerManager._initialize_llm_servers。
例如有 8 张 GPU,TP=2, DP=1, PP=1:
每个 replica 占 2 张 GPU
8 / 2 = 4 个 replica
负载均衡器可以把不同 trajectory 分发到 4 个 replica
当前内置 rollout backend 虽然保留了 pipeline_model_parallel_size 字段,但 vLLM、SGLang、TRT-LLM 都会拒绝 PP > 1,见 RolloutConfig.__post_init__。所以当前常见公式实际上是 TP × DP。
如果启用 prefill/decode disaggregation,server manager 会先用下式做通用的 footprint bookkeeping:
公式含义: prefill 和 decode 分离后,要先分别算出两个阶段占用的资源数并相加,再乘上数据并行和流水线并行规模。括号里是一个 rollout replica 内部的 prefill/decode 基础资源总和,整个公式才是该 rollout replica 的 footprint。
符号说明:
- 是每个 prefill 子副本的张量并行规模, 是这类子副本的数量;直立下标
prefill表示“读取并处理完整 prompt”的阶段。 - 是每个 decode 子副本的张量并行规模, 是这类子副本的数量;直立下标
decode表示“逐个生成后续 token”的阶段。这里的两类“子副本”都位于一个外层 rollout replica 内部。 - 和 分别是两个阶段的 worker/GPU 数; 把两部分资源相加。
- 圆括号表示先计算括号内的 prefill 与 decode 资源总和,再做外层乘法。
- 、 和 与上一式含义相同:分别是数据并行规模、流水线并行规模和乘法。
当前只有 vLLM 和 SGLang 支持该路径,见 get_rollout_replica_class 与配置校验 rollout.py:L339-L342。但在本章固定的源码快照中,上式不代表这些维度已经可任意组合:两个 backend 都要求 、、,而且整个 PD replica 必须放进单个节点。因此当前真正可执行的 footprint 是 。具体限制见 vllm_pd_replica.py:L62-L99 和 sglang_pd_replica.py:L62-L87;vLLM 在此快照还只接受 nixl 或 mooncake transfer backend。
3. 六个容易混淆的对象
3.1 BaseRollout:训练侧控制推理引擎的最小接口
BaseRollout 只有三个关键异步控制动作:
await rollout.resume(tags=["weights"])
await rollout.update_weights(named_tensor_generator)
await rollout.release()
它不等于“真正执行 token decoding 的模型”。当前实现通常是一个 ServerAdapter:它负责找到 backend server,并向它发送恢复内存、释放内存、更新权重等控制请求。
generate_sequences() 仍留在基类中作为旧同步接口,但当前正式 server adapter 不使用它。
3.2 server:实际托管推理 backend 的 Ray actor
vLLM、SGLang、TRT-LLM 都会启动 Ray server actor,并在其内部启动对应推理引擎。多节点 replica 通常每个节点有一个 server actor,node-rank 0 暴露主地址和 token-in/token-out handle。
- vLLM server 启动:
vllm_async_server.py:L1102-L1198 - SGLang server 启动:
async_sglang_server.py:L744-L856 - TRT-LLM 的 replica 到 server 是 1:1:
trtllm_async_server.py:L570-L631
3.3 replica:一组共同完成一次推理的 workers + servers
RolloutReplica 负责:
- 选择并占用 GPU workers;
- 在这些 workers 所在节点启动 server;
- 对整组 server 执行
sleep、wake_up、abort_all_requests、clear_kv_cache; - 对外提供主 server handle/address。
公共控制方法见 replica.py:L228-L291。
3.4 LLMServerManager:创建很多 replica
LLMServerManager 计算每个 replica 的 GPU footprint、创建全部 replica、启动全局 load balancer,并提供 client,见 llm_server.py:L453-L608。
3.5 GlobalRequestLoadBalancer:选哪一个 replica
负载均衡规则不是简单 round-robin,而是:
- 同一个高层
request_id优先粘到之前的 server,便于多轮对话复用 prefix cache。 - 新 request 选择当前 in-flight 请求最少的 server。
- server 动态移除后,旧 sticky mapping 会失效并重新选择。
实现见 GlobalRequestLoadBalancer。注意 client 用高层 request_id 做 sticky routing,但发送给 backend 的每一轮实际请求会换成新的 UUID,见 LLMServerClient.generate。
3.6 LLMServerClient:AgentLoop 看到的统一 token API
AgentLoop 不直接 import vLLM/SGLang API。它调用统一接口:
output = await client.generate(
request_id=trajectory_id,
prompt_ids=[...],
sampling_params={...},
)
client 选择 server,再通过 Ray actor RPC 调用 server.generate,最后返回统一 TokenOutput。这使 ToolAgentLoop 不需要知道底层是 vLLM 还是 SGLang。
FullyAsyncLLMServerClient 还支持 partial rollout:如果训练切换导致请求被 abort,它会把已经生成的 token 追加到 prompt,再继续生成,并缩减剩余 token budget。整个 trajectory 可能跨越多个权重版本,因此同时记录 min_global_steps 和 max_global_steps,见 llm_server.py:L281-L450。
4. 当前真实可用的 rollout backend
不要仅相信 YAML 注释;运行时 registry 才是 source of truth。当前 _ROLLOUT_REGISTRY 只有:
rollout.name | 当前正式模式 | 主要特点 |
|---|---|---|
vllm | async | 高吞吐 decoding、prefix cache、vLLM sleep mode、可做 PD disaggregation |
sglang | async | radix/prefix cache、真实的分标签显存释放、可做 PD disaggregation、支持 delta_sharded 应用路径 |
trtllm | async | TensorRT-LLM backend、独立的 sampler/logprob 映射、当前无 PD 路径 |
replica registry 也只注册这三个名字,见 replica.py:L320-L408。
4.1 mode=sync 已删除
RolloutConfig.mode 的默认值虽然仍是字符串 async,但设置为 sync 会直接抛错,见 rollout.py:L276-L290。vLLM ServerAdapter 的 generate_sequences() 也明确说明旧 SPMD 同步生成已经退役,见 vllm_rollout.py:L252-L267。
这里要区分:
rollout.mode=async:推理 API 采用异步 server 模式。trainer.v1.trainer_mode=sync:PPO 训练步骤之间是否同步推进。
两者不是一回事。默认配置正是“异步 rollout server + 同步 PPO trainer”,见 ppo_trainer.yaml:L221-L231。
4.2 Transformers 到底还算不算 rollout backend
当前答案是:Hugging Face Transformers 仍是模型、tokenizer、chat template 和部分训练实现的重要基础,但 hf 不是当前 V1 registry 中的一等 rollout backend。
仓库仍导出 HFRollout,它的旧实现直接调用 module.generate(),仓库也仍保留相应测试文件;但:
- 它不在
_ROLLOUT_REGISTRY中,不能走当前get_rollout_class(name, mode)正式路径。 - 它没有实现
BaseRollout要求的resume()、update_weights()和release()三个抽象异步接口,而且旧构造函数也没有向当前基类传入必需参数,见base.py:L29-L73和hf_rollout.py:L39-L51。所以按当前类层次直接实例化也会失败,不能把现存旧测试当作可用性证据。 - 文件自己标记了待重构,并说明 FSDP HybridShard 下可能 hang,见
hf_rollout.py:L14-L18。 - 它采用旧的 dense
DataProto+ 同步generate_sequences()接口,见hf_rollout.py:L39-L51。
NaiveRollout 也类似:源码存在,但不在当前正式 registry,也没有实现三个抽象控制接口,见 naive_rollout.py:L36-L51。阅读旧教程时,应把这些类当作未与当前抽象对齐的 legacy/reference code,而不是可直接实例化的 V1 生产路径。
配置文件
rollout.yaml:L4的注释仍列出了hf。这是注释与当前 registry 不一致的例子,判断真实支持边界应以 Python registry 和配置校验为准。
5. 三种部署含义,以及两个名字相似的 “colocate”
RolloutMode 定义了 replica 的资源布局:
RolloutMode | 训练与推理的关系 | 是否共享 GPU | 典型用途 |
|---|---|---|---|
HYBRID | training engine 与 rollout engine 融合在同一组 worker/process 资源中,按阶段切换 | 是 | 常见 on-policy RL |
COLOCATED | 不同进程,但放在同一个 Ray placement group/GPU 上 | 是 | 例如 LLM-as-judge 等角色共置 |
STANDALONE | rollout 独占单独 GPU resource pool | 否 | 异步/解耦训练 |
初始化分别在 init_hybrid、init_colocated 和 init_standalone。
一个很容易踩的坑是:trainer_mode=colocate_async 并不等于 RolloutMode.COLOCATED。V1 base trainer 把 actor worker group 传给 LLMServerManager,后者对 vLLM/SGLang 调用 init_hybrid,见 trainer_base.py:L350-L353 和 llm_server.py:L547-L558。colocate_async 的意思是“trainer 与 rollout 共用 GPU,但允许 partial rollout”,其 replica 仍是 hybrid 资源布局。
6. 一条 prompt 的完整 token 生命周期
6.1 Dataset 不再提前 tokenize rollout prompt
当前 RL dataset 的 __getitem__ 不再提前返回 tokenized rollout prompt;chat template 被移到 AgentLoop 中。它从完整 row_dict 出发,新增 raw_prompt message list 和一个兼容旧 DataProto 的临时 dummy_tensor,同时保留 data source、reward、extra_info 等原始行元数据,并新增 index、tools_kwargs 和 interaction_kwargs,见 rl_dataset.py:L386-L411。
V1 trainer 为每个 prompt 分配唯一 uid,把 prompt 状态注册进 TransferQueue,然后 fire-and-forget 地交给 AgentLoop,见 trainer_base.py:L1315-L1361。
6.2 n 不是 backend 一次返回 n 个 completion
在当前 TQ AgentLoop 中,训练 prompt 默认读取 rollout.n,验证 prompt 默认读取 rollout.val_kwargs.n;如果样本带有 __rollout_n__,该值还会覆盖相应默认值。最终得到的 n 会为同一 prompt 启动 n 个独立 session/task:
for session_id in range(n):
create_task(run_agent_loop(..., session_id=session_id))
具体实现在 AgentLoopWorkerTQ._run_prompt。所以训练时 GRPO 的多条回答在系统里是多条独立 trajectory,而不是依赖 backend 的 num_return_sequences=n。
6.3 sampling 参数如何一路传到底层
TQ AgentLoop 创建的核心请求参数是:
sampling_params = {
"temperature": config.temperature,
"top_p": config.top_p,
"top_k": config.top_k,
"repetition_penalty": 1.0,
"logprobs": config.calculate_log_probs,
}
源码见 agent_loop_tq.py:L59-L76。几点细节:
- 当前这个路径固定传
repetition_penalty=1.0;它不是从RolloutConfig.repetition_penalty读取的。 - 对训练样本,per-sample
__do_sample__=False会被编码成temperature=0, top_p=1, top_k=-1,而不是把布尔do_sample传给 backend;精确参数见agent_loop_tq.py:L41-L44,应用分支见L107-L126。 - validation 不会根据这个 per-sample flag 切换 greedy,而是用
val_kwargs覆盖 temperature/top-p/top-k。 - 每个 backend 再把统一字典翻译为自己的 API。
backend 翻译边界:
| backend | token budget | 请求 logprob 的方式 | 返回 |
|---|---|---|---|
| vLLM | max_tokens,同时受剩余 context 与 response_length 限制 | 布尔值转为 logprobs=0,只要 sampled-token logprob | 对每个生成 token 取其 logprob |
| SGLang | max_new_tokens,同样限制 context | return_logprob=True | 校验 output ids 与 logprob 长度一致 |
| TRT-LLM | max_tokens | TorchSampler 用 0,TRTLLMSampler 用 1 | 每个位置取被采 token 的 logprob |
对应源码:
- vLLM:
vllm_async_server.py:L511-L587、L624-L696 - SGLang:
async_sglang_server.py:L526-L628、L634-L702 - TRT-LLM:
trtllm_async_server.py:L321-L390
6.4 ToolAgentLoop 如何把工具观察插进 token stream
AgentLoopOutput.response_ids 不只包含模型生成 token,还包含工具 response/observation token。response_mask 用于区分来源:
response_ids: [模型A, 模型B, 工具X, 工具Y, 模型C]
response_mask: [ 1, 1, 0, 0, 1 ]
rollout_log_probs: [lpA, lpB, 0.0, 0.0, lpC ]
在普通的逐段拼接路径中,模型 token 被追加 mask 1 和 backend logprob,见 tool_agent_loop.py:L262-L280;工具 response token 被追加 mask 0 和 logprob 0.0,见 tool_agent_loop.py:L433-L449。启用 Continuous Token 时,两类 token 会先经过 merge,然后由 align_response_metadata() 按同样语义对齐:assistant token 为 1 并保留采样 logprob,non-assistant 与边界 token 为 0/0.0,见 agent_loop.py:L322-L368 和 continuous_token.py:L300-L350。结束时再按 response_mask 的长度把初始 prompt 与完整 response 切开,见 tool_agent_loop.py:L176-L204。
这里的 response_mask 后续就是 loss mask 的基础:工具输出提供上下文,但不应该被当作 actor 自己采取的 action 来优化。
7. 数据 shape、padding 与 jagged tensor
令 batch size 为 B,第 i 条样本的 prompt/response 真正长度为 P_i 和 R_i。
7.1 当前 V1 TQ 主路径:每条 trajectory 先保持无 padding
AgentLoop postprocess 为每条 trajectory 产生:
| 字段 | 单样本 shape | 含义 |
|---|---|---|
prompts | [P_i] | 初始 prompt token |
responses | [R_i] | 模型 token + 工具观察 token |
input_ids | [P_i + R_i] | 两者拼接 |
position_ids | [P_i + R_i],VLM 可有额外前缀维 | token 位置 |
response_mask | [R_i] | 模型 token 为 1,工具 token 为 0 |
loss_mask | [R_i] | 当前直接复制 response_mask |
rollout_log_probs | [R_i],可选 | backend 采样时的逐 token logprob |
实现见 agent_loop_tq.py:L150-L227。代码会临时构造全 1 的 attention mask 来算 position ids,但普通文本 trajectory 不需要保存全局 padded attention mask。
同一 batch 中长度不同的 tensor 会由 list_of_dict_to_tensordict 自动变成 jagged nested tensor;长度相同才 torch.stack,见 tensordict_utils.py:L918-L950。因此 batch 逻辑 shape 是:
input_ids: NestedTensor[B, j_i],其中 j_i = P_i + R_i
response_mask: NestedTensor[B, R_i]
而不是先强行补成 [B, P_max + R_max]。
7.2 一个具体例子
假设:
prompt_ids = [11, 12, 13]
response_ids = [21, 22, 31, 32, 23]
其中 21,22,23 是模型生成,31,32 是工具观察。存进 TQ 的字段为:
prompts [11, 12, 13] shape [3]
responses [21, 22, 31, 32, 23] shape [5]
input_ids [11, 12, 13, 21, 22, 31, 32, 23] shape [8]
response_mask [ 1, 1, 0, 0, 1] shape [5]
loss_mask [ 1, 1, 0, 0, 1] shape [5]
rollout_log_probs [a, b, 0., 0., c] shape [5]
注意:工具 token 仍在 input_ids 里,因为后续模型生成要以工具结果为条件;只是 loss 被 mask 掉。
7.3 actor forward 时如何处理 variable length
FSDP Transformer 实现默认读取 nested input_ids:
use_remove_padding=True:取input_ids.values(),把全 batch 的有效 token 打包成[1, total_nnz];必要时再为 sequence parallel 做少量尾部 padding。use_remove_padding=False:临时转成[B, max_i(P_i+R_i)]的右 padding tensor,并构造 attention mask。
源码见 transformer_impl.py:L1128-L1243 和 L1245-L1280。
7.4 为什么 response logprob 的切片要左移一位
模型在位置 t 的 logits 预测位置 t+1 的 token。因此 full-sequence forward 得到的 logprob 逻辑长度是 [P_i+R_i],但第一枚 response token 的概率位于最后一枚 prompt token 的输出位置。
verl 对第 i 条序列使用近似如下的切片:
full_seq_log_probs[seq_end - R_i - 1 : seq_end - 1]
得到 shape [R_i]。实现见 response_from_nested。这不是 off-by-one bug,而是 causal LM 的 next-token shift。
7.5 旧 dense 路径长什么样
旧的非 TQ/dense postprocess 仍有助于理解传统 shape:
prompts: [B, P] 左 padding
responses: [B, R] 右 padding
input_ids: [B, P+R]
attention_mask: [B, P+R]
response_mask: [B, R]
position_ids: [B, P+R]
定义和多轮 mask 示例见 agent_loop.py:L581-L600,具体左右 padding 见 agent_loop.py:L741-L795。HFRollout 也是这种 dense 风格,见 hf_rollout.py:L96-L170。
7.6 batch size 不可整除时的“样本 padding”
除了 token padding,还有另一种完全不同的 padding:为了让 trajectory 数能被 DP size 和 mini-batch size 整除,V1 trainer 可能追加 synthetic no-op sample。
这个样本只有:
prompt = [EOS]
response = [EOS]
input_ids shape = [2]
response_mask = [0]
loss_mask = [0]
rm_scores = [0.0]
rollout_log_probs = [0.0]
tag.is_padding = true
实现见 padding_utils.py:L70-L124 与 upsample_batch_to_divisible_size。这里显式初始化为 0 的 logprob 字段是 rollout_log_probs;后续 actor/ref forward 仍可能算出非零的 old/ref/current logprob,但它们会被全 0 的 response_mask / loss_mask 屏蔽。这种 padding 解决的是“样本数整除”,不是“token 长度对齐”;不要把两者混为一谈。
8. 三种 log probability 的生命周期
8.1 rollout_log_probs
它来自 vLLM/SGLang/TRT-LLM 实际采样时的分布,shape 为每条 trajectory 的 [R_i]。工具观察位置补 0.0,并由 response_mask=0 屏蔽。
若 calculate_log_probs=False,backend 不必返回它。当前默认 YAML 为 True,见 rollout.yaml:L231-L233;dataclass 自身的默认值则是 False,见 rollout.py:L208-L220,最终以 Hydra 合成后的运行配置为准。
8.2 old_log_probs
PPO 的 proximal anchor 通常不是直接信任 backend 返回值,而是让训练 actor 对同一条 input_ids 再 forward 一次,重新计算 logprob。当前 trainer 流程:
- actor engine 生成 full-sequence
log_probs; response_from_nested切出[R_i];- 写回
old_log_probs; - 若有
rollout_log_probs,计算二者差异用于 debug/rollout correction。
源码见 trainer_base.py:L1479-L1538。只有 rollout-correction 的 bypass mode 会直接做:
old_log_probs = rollout_log_probs
8.3 ref_log_prob
这是固定 reference policy 对相同 action token 的 logprob,用于 KL penalty。它同样通过训练/ref engine forward,再切成 [R_i],见 trainer_base.py:L1540-L1564。
三者可以概括为:
| 字段 | 哪个模型算 | 什么时候算 | 主要用途 |
|---|---|---|---|
rollout_log_probs | 推理 backend 中的 π_rollout | token 被采样时 | 记录 behavior policy、校正与 debug |
old_log_probs | 训练 actor 中的 π_old | PPO update 前 | importance ratio 的分母/proximal anchor |
ref_log_prob | 固定 reference policy | PPO update 前 | KL regularization |
即使权重名义上相同,也不要假设 rollout_log_probs == old_log_probs。推理 backend 和训练 engine 可能使用不同 kernel、dtype、logits 处理与并行布局。vLLM 的 logprobs_mode 默认还是 processed_logprobs,见 RolloutConfig。
reward 的位置取决于部署方式:启用异步 reward worker 时,AgentLoop 会先算 reward、把完整 trajectory 写入 TQ/replay buffer,trainer 随后再 sample,见 agent_loop_tq.py:L150-L227 与 agent_loop.py:L961-L1023。只有 colocated reward 分支是在 sample 后补算 reward。因此两条顺序分别是:
异步 reward:generation → reward → TQ/replay buffer → sample → balance → old/ref logprob → value → advantage → critic/actor update
colocated reward:generation → TQ/replay buffer → sample → reward → balance → old/ref logprob → value → advantage → critic/actor update
sample 之后的条件分支见 trainer_base.py:L536-L586。
9. 当前代码中的“sharding manager”去了哪里
旧资料经常把训练/推理之间的上下文切换统称为 sharding manager。当前 V1 主路径并没有一个统一的 sharding_manager 类;原来的职责被拆成三层:
- 训练
BaseEngine:维护 FSDP/Megatron 等训练分片,并导出参数流。 CheckpointEngine/CheckpointEngineManager:建立传输拓扑,发送/接收权重。- backend
ServerAdapter:把收到的 tensor 按 backend 能理解的方式加载,并管理 weight/KV cache 显存。
训练 engine 的标准导出接口是:
generator, peft_config = engine.get_per_tensor_param()
# generator yields: (parameter_name, tensor)
此外还提供 local shard 和 delta shard 导出接口,见 BaseEngine。
10. 权重同步:从 actor shard 到 inference layout
10.1 CheckpointEngine 的抽象
CheckpointEngine 把同步拆成:
prepare
-> build_topology
-> init_process_group
-> actor.send_weights(...) || rollout.receive_weights(...)
-> finalize
默认 wire format 是一串完整 (name, tensor),名为 named_tensors。delta_sharded 使用特殊 delta_flush wire format;当前只有 SGLang adapter 能消费它,限制写在 CheckpointEngineWorker。
10.2 同 GPU 的 naive 路径
naive 不是“低质量算法”,而是“actor 与 rollout 共置时,不需要跨节点传输 backend”的意思。ColocatedCheckpointEngine 只是暂存 generator 再交给 receiver,见 base.py:L225-L280。
actor worker 的实际顺序见 engine_workers.py:L719-L805:
V1 base trainer 会把 hybrid checkpoint manager 强制设置为 naive,见 trainer_base.py:L355-L362。
backend 仍需完成最后一跳:
- vLLM adapter 把 tensor 分 bucket,经 CUDA IPC 发送;不支持 IPC 时退回 shared memory,并在更新后清 cache/版本,见
vllm_rollout.py:L208-L246。 - SGLang adapter 分 bucket、跨 TP gather/序列化,再调用 SGLang weight update;更新后 flush cache,见
sglang_rollout.py:L291-L370。 - TRT-LLM 有自己的 ServerAdapter 和 CUDA IPC weight loader,见
trtllm_rollout.py:L268-L544。
10.3 独立 GPU 的 checkpoint-engine 路径
当 rollout 是 STANDALONE,actor 和 rollout 不共享进程/GPU,不能直接把 generator 交给同 GPU adapter。CheckpointEngineManager.update_weights() 的真实状态机是:
- abort 未完成请求;已经产出的 partial tokens 由 FullyAsync client 保留,稍后用于重试;
- 汇总所有 rollout
CheckpointEngineWorker; - 只释放 KV cache,保留现有 weight buffer;
- 为 actor 与 rollout workers 建通信拓扑;
- actor
send_weights与 rolloutreceive_weights + ServerAdapter.update_weights并发执行; - finalize,释放传输 bucket/process group;
- 恢复 KV cache;
- 继续 partial rollout。
实现见 CheckpointEngineManager.update_weights。拓扑中训练侧 checkpoint engine 与 rollout 侧 checkpoint worker 的关系,可直接看源码内图示 base.py:L361-L382。
10.4 当前注册的 checkpoint engine 名称
这些 backend 是条件 import 的;环境没有相应依赖时不一定可用,见 checkpoint_engine/__init__.py:L33-L73。当前代码实际注册键包括:
| registry key | 实现文件 | 说明 |
|---|---|---|
naive | base.py:L225 | 同 GPU/共置直接同步 |
nccl | nccl_checkpoint_engine.py:L103 | CUDA/NCCL 传输 |
nixl | nixl_checkpoint_engine.py:L238 | NIXL 传输 |
mooncake | mooncake_checkpoint_engine.py:L38 | Mooncake backend |
kimi_ckpt_engine | kimi_checkpoint_engine.py:L222 | Kimi checkpoint engine |
delta_sharded | delta_checkpoint_engine.py:L218 | shard-local diff;当前仅 SGLang apply |
Ascend 的 HCCL 实现当前也注册到键 nccl,见 hccl_checkpoint_engine.py:L106。因此不要根据 YAML 中“hccl backend”注释自行假设 registry key;应在目标环境检查实际 registry。
11. sleep、wake、abort 与 cache:四个动作不要混用
| 动作 | 影响新请求 | 处理进行中的请求 | 权重显存 | KV cache |
|---|---|---|---|---|
abort_all_requests | backend 暂停/阻止继续生成 | 中止并让 FullyAsync client 可重试 | 保留 | backend 相关 |
sleep / release | 推理阶段暂停 | 通常先 drain/abort | 可释放 | 可释放 |
release_kv_cache | 用于 weight sync 临界区 | 应先 abort | 保留 | 释放 |
clear_kv_cache | 不一定暂停 | 不负责保存 partial request | 保留 | 失效/清空 prefix cache |
公共控制面见 RolloutReplica。各 backend 的实现并不完全对称:
- SGLang 能按
weights/kv_cachetags 真实 release/resume;LoRA adapter 模式只释放 KV cache,保留 base weights,见async_sglang_server.py:L461-L524。 - TRT-LLM 实现了独立
release_kv_cache/resume_kv_cache,见trtllm_async_server.py:L411-L450。 - vLLM 的 hybrid sleep 会根据普通 full weights、LoRA/MTP/NPU 选择 level 2 或 level 1,见
vllm_async_server.py:L1071-L1099。但当前独立的release_kv_cache/resume_kv_cache方法仍是 TODO/no-op,见vllm_async_server.py:L813-L824。
所以 CheckpointEngineManager 的统一状态机表达的是协议意图,但实际显存行为必须再看所选 backend 的实现。
12. actor training 与 inference engine 如何切换
12.1 trainer_mode=sync:同一批 GPU 分时复用
默认 V1 mode 是 sync。关键 hooks 见 trainer_sync.py:L24-L42:
初始化/step 结束:把 actor 新权重同步到 rollout,并恢复推理所需内存
rollout 采样结束:sleep rollout,释放 weights/KV cache
随后:actor 计算 old/ref logprob,做 forward/backward/update
下一 step:再同步新权重,切回 rollout
可以把同一张 GPU 想成只有一间工作室:采样时摆推理设备,训练时收起 KV cache/推理权重,摆梯度和 optimizer state。
12.2 trainer_mode=colocate_async:同 GPU,但保留 partial rollout
该模式仍共享 GPU,不过采样时可以有后台 trajectory。训练前:
abort unfinished requests -> sleep replicas -> train
step 结束后:
update weights -> resume generation
源码见 trainer_colocate_async.py:L25-L59。FullyAsyncLLMServerClient 会让 AgentLoop 感觉 abort/retry 是透明的。
12.3 trainer_mode=separate_async:rollout 有独立 GPU
该模式要求 rollout.nnodes > 0、rollout.n_gpus_per_node > 0 且 checkpoint backend 不能是 naive。它还强制要求 data.train_batch_size == trainer.v1.separate_async.parameter_sync_step * actor_rollout_ref.actor.ppo_mini_batch_size;如果启用 reward model,则必须使用独立的 reward resource pool,即 reward.reward_model.enable_resource_pool=True。这些硬性校验见 trainer_separate_async.py:L39-L67。它会创建 standalone rollout server 和独立 checkpoint manager,见 L81-L101。
standalone rollout 可以在 actor 训练时继续生成;step 结束后通过 NCCL/NIXL/Mooncake 等同步新权重。代码还提供把 hybrid actor GPU 临时加入/移出 load balancer 的切换框架,见 L180-L203,但当前 should_switch_to_rollout() 固定返回 False,动态闲置 GPU spillover 策略仍未实现,见 L205-L207。
13. 一个最小配置阅读例子
下面不是完整可运行命令,而是学习 rollout 子系统时最值得先看的配置切片:
trainer:
use_v1: true
v1:
trainer_mode: sync # sync | colocate_async | separate_async
actor_rollout_ref:
rollout:
name: vllm # vllm | sglang | trtllm
mode: async # 当前只有 async
prompt_length: 2048
response_length: 1024
temperature: 1.0
top_p: 1.0
top_k: -1
n: 4 # 每个训练 prompt 默认启动 4 条;验证读取 val_kwargs.n,样本仍可覆盖
calculate_log_probs: true
tensor_model_parallel_size: 2
data_parallel_size: 1
pipeline_model_parallel_size: 1
max_num_batched_tokens: 8192
max_num_seqs: 256
gpu_memory_utilization: 0.5
enable_prefix_caching: true
free_cache_engine: true
checkpoint_engine:
backend: naive # hybrid sync 的常见路径
update_weights_bucket_megabytes: 2048
主要 rollout 字段定义在 RolloutConfig;上面嵌套的 checkpoint_engine.backend 和 update_weights_bucket_megabytes 定义在 CheckpointEngineConfig。默认 YAML 及说明见 rollout.yaml:L1-L270 与 rollout.yaml:L271-L300。
如果切换为 separate_async,还需为 standalone rollout 配置 nnodes / n_gpus_per_node,把 checkpoint engine 改为目标环境实际可用的非 naive backend,并配置 trainer.v1.separate_async.parameter_sync_step 使上述 batch-size 等式成立。它在默认配置中是 4,见 ppo_trainer.yaml:L239-L246。
14. 推荐的源码追踪顺序
第一次读源码时,不建议从数千行 backend server 实现开始。按一条 trajectory 的调用链追:
RLHFDataset.__getitem__:确认 rollout prompt 尚未 tokenize,batch 保留原始行元数据并新增raw_prompt/dummy_tensor等字段。PPOTrainer._submit_batch_to_rollout:确认 prompt 怎样进入 TQ/AgentLoop。AgentLoopWorkerTQ.generate_sequences:确认 sampling 和n。ToolAgentLoop模型 token 追加与工具 token 追加:确认两类 token 如何拼接。LLMServerClient.generate:确认 routing 与统一TokenOutput。- 选择一个 backend 的
generate:vLLM、SGLang 或 TRT-LLM。 AgentLoopWorkerTQ._agent_loop_postprocess:确认 TQ 中的 shape。PPOTrainer._compute_old_log_prob:确认 rollout 概率如何转成训练 anchor。ActorRolloutRefWorker.update_weights:确认 actor 参数如何进入 adapter。CheckpointEngineManager.update_weights:最后再看跨 GPU/跨节点权重同步。
15. 自检题
- 为什么
trainer_mode=sync仍然要求rollout.mode=async? rollout.n=8为什么不等于 backendnum_return_sequences=8?- 工具观察 token 为什么必须在
input_ids中,却必须在response_mask中为 0? - 为什么 causal LM 的 response logprob 切片起点是
seq_end - R - 1? - actor 和 rollout 权重相同,为什么仍然建议重新计算
old_log_probs? - standalone rollout 为什么不能使用
naivecheckpoint engine? - 在 vLLM 当前实现中,统一的
release_kv_cache协议与 backend 实际行为有什么差异?
如果你能沿源码回答这七题,就已经掌握了 verl rollout 子系统从 high-level 到 low-level 的主干。