本文是《RL 后训练基础设施:rollout 与训练如何共享一组 GPU》系列的第 1 篇(共一篇)。

一个 8B 的策略模型做 GRPO:512 个 prompt、每个采 16 条回答、回答平均 8K token。这一步要生成 6700 万个 token,参考模型与旧策略要再各过一遍 7100 万个 token 的前向,训练要对同样多的 token 做前向加反向。合计 6.8×10¹⁸ FLOP,其中训练占一半、生成只占六分之一。但在 64 张 H100 上,生成要 600 秒、训练 135 秒——按时间生成占四分之三。整步下来,64 张卡的算力只用到了 13%。

这几个数字是本系列后面七篇的全部起点。系统形态(共置 / 分离 / 异步)要解决的是”生成的 600 秒里训练器在做什么、训练的 135 秒里推理引擎在做什么”;显存切换要解决的是”同一张卡上训练状态与 KV cache 怎么轮流占”;权重同步要解决的是”每步 16 GB 新权重怎么进推理引擎”;异步要解决的是”600 秒里有 196 秒是等最长的几条回答,能不能不等”;Agent 篇要解决的是”当 rollout 还要等几千个容器跑测试时,这张账再放大多少”。不先把这些数字算出来,后面每一个决定都只能靠试。

算这些数字不需要 GPU,需要的是一套一致的记账方法:一步 RL 是三个形态不同的作业加两次同步——生成是 serving 作业、打分是一次或几次前向(或一个外部服务)、训练是常规的分布式训练步;两次同步是样本从推理引擎进训练器、新权重从训练器回推理引擎。每个作业有明确的 FLOPs 公式、显存公式与时间模型;把它们与硬件的三个数字(峰值算力、HBM 容量、HBM 带宽)放在一起,就得到三段各花多少秒、整步的 GPU 利用率上限是多少这两个答案。

这一篇不讨论任何系统形态,也不进入任何框架的源码。它只做一件事:把这套记账方法建立起来,给出符号、公式,算清四个场景(8B 对话、8B 推理、32B 推理、DeepSeek-V3 规格的 MoE 推理)的完整数字,并解释为什么 RL 后训练的系统问题与预训练、推理服务都不同。

本篇的核心问题:

一个 8B 策略模型,\(B = 512\)、\(G = 16\)、\(\bar L = 8\text{K}\),在 64 张 H100 上做 GRPO。一步生成多少 token、多少 KV、要分几波?三段各要多少 GPU·秒?GPU 平均利用率的理论上限是多少?——这一步之后每一篇要解决的,就是实际值与这个上限之间的差。

文中所有硬件峰值都是标称值(H100 SXM bf16 dense 989 TFLOPS、HBM3 3.35 TB/s、80 GB),字节数用十进制单位(1 GB = 10⁹ 字节),KV 按惯例用 KiB / GiB 注明;所有时间都是在明确写出的 MFU 或带宽利用率假设下的估算,不是实测;论文数字在首次出现处注明出处。

一、总览

1. 先说答案:三个作业、两次同步

在线 RL 后训练(PPO、GRPO 及其一族)的一步,展开是这样的:

 ┌──────────────── 生成 rollout ────────────────┐   ┌──── 打分 ────┐   ┌────── 训练 ──────┐
 │ 推理引擎:B 个 prompt × G 条回答,decode 为主   │   │ 规则 / 验证器 │   │ 参考前向  log π_ref │
 │ 形态:serving —— 请求、批、KV 池、连续批处理     │──►│ 或奖励模型    │──►│ 旧策略前向 log π_old│
 │ memory-bound,长尾由最长的回答决定              │   │ 或代码沙箱    │   │ 策略前向 + 反向     │
 └──────────────────────────────────────────────┘   └──────────────┘   │ (PPO:价值模型)    │
            ▲                                                          └─────────┬──────────┘
            │                    同步 ②:新权重 → 推理引擎                          │
            └──────────────────────────────────────────────────────────────────────┘
                              同步 ①:样本(token、logprob、reward)→ 训练器  在打分与训练之间

三个作业对 GPU 的用法相反:生成是一个 token 一个 token 的 decode,每步读一遍权重与 KV、算得很少,受 HBM 带宽限制;训练是整条序列的大矩阵乘,受算力限制;打分要么是 prefill 形态的前向(也受算力限制),要么根本不在 GPU 上。它们对显存的要求也互斥:生成要尽可能大的 KV 池,训练要放下参数、梯度、优化器状态(每参数 16 字节)与激活。两次同步是两道墙:训练要等全部 rollout 完成(严格 on-policy 下),下一步生成要等新权重到位。

预训练里只有第三个作业;SFT 与 DPO 也只有第三个。在线 RL 是唯一把一个 serving 作业放进训练循环的负载,本系列全部的系统问题都从这里来。

2. 三本账:FLOP、字节、秒

三个作业各有一本 FLOP 账和一本字节账,加起来再算一本时间账:

FLOP 的账    生成 2N/token · 前向 2N/token · 训练 6N/token(PPO 价值模型再加 8N)
             合计 ≈ 12N × 一步的 token 数(GRPO)
字节的账     训练状态 16N + 参考 2N + 推理权重副本 2N + KV cache(token 数 × 每 token KV)+ 激活
             ——加起来远超集群显存,所以 rollout 要分波、共置要切换
秒的账       生成 = decode 步数 × 每步读的字节 ÷ 带宽 + 长尾
             前向 / 训练 = FLOP ÷ (卡数 × 峰值 × MFU)
             同步 = 字节 ÷ 链路带宽
             全步 MFU = 总 FLOP ÷ (卡数 × 峰值 × 一步墙钟)

FLOP 账在算法侧已经算过(《后训练》第三篇第六章),本篇复述并补上 prompt、前缀缓存与”旧策略要不要重算”三个细节。字节账与秒的账是本篇的主体:前者决定什么放得下、什么必须让路,后者决定三段的时间比例——而这个比例就是第二篇选系统形态、定 GPU 配比的依据。

3. 记账符号

全系列使用同一套符号,后续各篇在总览里复述用到的部分:

N  N_a           总参数量、每 token 激活参数量(dense 模型两者相等;MoE 只有 N_a 进 FLOP 账)
B  G             一步的 prompt 数、每个 prompt 的回答数;序列数 S = B·G
P  L̄  L_max      prompt 平均长度、回答平均长度、回答最大长度(token)
T_resp T_all     一步的回答 token 数 S·L̄、前向要过的全部 token 数 S·(P + L̄)
k_kv             每 token 的 KV 字节数(Llama-3-8B:128 KiB;MLA:≈ 70 KB)
n  t             GPU 总数、推理引擎一个实例占的卡数(TP 度)
c                一个推理实例同时在飞的序列数(并发)
F_peak BW M      单卡峰值 FLOPS、HBM 带宽、HBM 容量
η_train η_pre    训练与 prefill 形态前向的 MFU 假设(本篇 0.40、0.50)
η_bw             decode 时 HBM 带宽的有效利用率假设(本篇 0.60)

四个场景的模型(KV 字节数按 bf16、\(2 \times\) 层数 \(\times\) KV 头数 \(\times\) 头维 \(\times\) 2 字节算;DeepSeek-V3 的 MLA 每层只缓存 \(d_c = 512\) 加 \(d_h^R = 64\),见《Transformer 与 LLM》第三篇):

                 N        N_a     层   KV 头 × 头维    k_kv           推理侧权重
Llama-3-8B       8.0 B    8.0 B    32   8 × 128        128 KiB        bf16
Qwen3-32B       32.8 B   32.8 B    64   8 × 128        256 KiB        bf16
DeepSeek-V3    671 B      37 B    61   MLA 512 + 64    ≈ 70 KB        FP8(官方部署格式)

4. 本文的章节安排

内容 回答的问题
一步的时间线 三个作业与两次同步各在什么时候发生,谁等谁
FLOP 账 12N 怎么来的,prompt、前缀缓存、旧策略重算各占多少
字节账 四份显存各多大,KV 为什么放不下,分几波
秒的账 decode 的带宽模型、长尾、三段时间比例、全步 MFU
三个作业的形态 为什么它们对 GPU 的用法互斥,两次同步搬什么
放大器 长思维链、Agent 环境、MoE 各把哪本账放大
公开配方 DeepSeekMath、R1、Kimi k1.5 的配置代进账本
动手 rl_ledger.py:四个场景的完整输出
小结 要点、速查表、下一篇

二、一步的时间线

1. GRPO 的一步

以 GRPO 为例(PPO 只多一个价值模型,第 3 节说),严格 on-policy 的一步按时间展开:

 t0  推理引擎持有第 t 步的权重 θ_t
     ├─ prefill:B 个 prompt 各一次(前缀缓存:同一 prompt 的 G 条回答共享)
     ├─ decode:S = B·G 条序列并发生成,连续批处理,完成一条补一条
     │           ……直到最后一条回答结束(长尾)
 t1  样本(token id、推理引擎算的 log π_old、结束原因)→ 打分
     ├─ 规则 / 验证器:CPU,几乎不占时间;代码沙箱:几十秒到几分钟(第六篇)
     ├─ 奖励模型:一次 prefill 形态的前向(若有)
 t2  样本 + reward → 训练器
     ├─ 参考模型前向:log π_ref(KL 项)
     ├─ 旧策略前向:重算 log π_old(多数框架默认重算,第三章第 4 节)
     ├─ 组内归一化得优势 A,按 mini-batch 做 μ 轮前向 + 反向 + 优化器步
 t3  θ_{t+1} → 推理引擎(同步 ②);共置形态下还要先把训练状态让出、KV 池建回来
 t0' 下一步

两道墙在 t1 和 t3:t1 之前训练器无事可做(严格 on-policy 下它没有样本),t3 之后推理引擎无事可做(它没有新权重)。共置形态把两类作业放在同一组卡上轮流跑,墙的代价是显存切换;分离形态把它们放在不同的卡上,墙的代价是一方空转;异步形态拆掉 t1 这道墙,代价是样本过期。这是第二篇的全部内容,本篇只算每一段的长度。

2. 每段需要 GPU 的什么

谁在跑 计算形态 瓶颈 显存里要有什么
prefill 推理引擎 整条 prompt 一次前向 算力 推理权重副本 + 这批的 KV
decode 推理引擎 每步每序列一个 token HBM 带宽 推理权重副本 + 尽可能大的 KV 池
打分 CPU / 沙箱 / RM — / 容器 / prefill 前向 环境延迟 / 算力 RM 权重(若有)
参考、旧策略前向 训练器 整条序列一次前向 算力 参考权重 2N + 激活(不留)
训练 训练器 前向 + 反向 + 优化器 算力 16N 训练状态 + 激活
同步 ② 两者 字节搬运 链路带宽 一个 bucket 的临时缓冲

decode 是唯一受带宽限制的段,也是唯一按”序列数”而不是”token 数”扩展的段——这两点决定了它的时间模型与其他段完全不同(第五章)。

3. PPO 多了什么

PPO 有一个与策略同规模的价值模型:rollout 后要对全部 token 做一次价值前向(\(2N\)),训练时它也要前向 + 反向(\(6N\));它的训练状态又是 \(16N\) 字节。所以 PPO 一步的 FLOP 从约 \(12N\) 涨到约 \(20N\),显存里多一整套训练状态。GRPO 用组内平均代替价值网络,省掉的正是这一块——它在 2025 年成为默认,一半是算法原因,一半是这本账。

4. μ 轮更新

PPO / GRPO 允许对同一批样本做 \(\mu\) 个 epoch 的更新(clip 就是为此存在的)。\(\mu > 1\) 时训练的 FLOP 乘 \(\mu\),其余不变;DeepSeek 一族的配方多用 \(\mu = 1\),本篇按 1 算。

三、FLOP 账:12N 从哪来

1. 每 token 的系数

一个参数量为 \(N_a\)(激活参数)的 Transformer,每个 token 的前向约 \(2N_a\) FLOP、反向约 \(4N_a\),前向 + 反向 \(6N_a\)(《Transformer 与 LLM》第二篇;注意力的 \(s^2\) 项在这里忽略,8K 序列上它约占 5–10%)。生成一个 token 是一次前向,也是 \(2N_a\)。于是 GRPO 一步:

部分 FLOP / token 过多少 token 说明
生成(decode) \(2N_a\) \(T_{resp} = S \cdot \bar L\) 只有回答 token 是生成的
prompt prefill \(2N_a\) \(B \cdot P\) 前缀缓存下每个 prompt 只 prefill 一次;没有前缀缓存是 \(S \cdot P\)
参考前向 \(2N_a\) \(T_{all} = S(P + \bar L)\) prompt 也要过前向(它是上下文),只是不进 loss
旧策略前向 \(2N_a\) 或 0 \(T_{all}\) 见第 4 节
奖励模型 \(2N_{RM}\) 或 0 \(T_{all}\) 规则 / 验证器奖励为 0
策略训练 \(6N_a\) \(T_{all}\) 反向要过全部位置的激活,prompt 位置的梯度为零但算力照付

合计(重算旧策略、规则奖励):\(2N_a T_{resp} + 2N_a BP + (2 + 2 + 6) N_a T_{all} \approx 12 N_a T_{all}\)——当 \(\bar L \gg P\) 时 \(T_{resp} \approx T_{all}\)。训练占一半,三个前向各六分之一,生成只占六分之一。

2. 8B 推理场景的数字

\(B = 512\)、\(G = 16\)、\(P = 500\)、\(\bar L = 8192\):\(S = 8192\) 条序列,\(T_{resp} = 6.71 \times 10^7\),\(T_{all} = 7.12 \times 10^7\)。

生成            2 × 8.03e9 × 6.71e7  =  1.08 EFLOP    15.9%
prompt prefill  2 × 8.03e9 × 2.56e5  =  4.1  PFLOP     0.1%
参考前向        2 × 8.03e9 × 7.12e7  =  1.14 EFLOP    16.8%
旧策略前向      同上                  =  1.14 EFLOP    16.8%
策略训练        6 × 8.03e9 × 7.12e7  =  3.43 EFLOP    50.5%
合计                                     6.80 EFLOP  = 11.9 N_a 每 token

64 张 H100 的峰值是 \(64 \times 989 \times 10^{12} = 6.33 \times 10^{16}\) FLOP/s,这 6.8 EFLOP 全部按峰值算要 107 秒。第五章会算出实际的墙钟是它的 7 到 8 倍——差距几乎全部来自生成。

3. 前缀缓存的账

没有前缀缓存时 \(G\) 条回答各 prefill 一次 prompt,FLOP 是 \(2N_a SP\);有前缀缓存只 prefill \(B\) 次。\(G = 16\)、\(P = 500\)、\(\bar L = 8192\) 时两者差 \(15 \times 2N_a BP \approx 6 \times 10^{16}\),不到总量的 1%——对单轮 RL 前缀缓存省的是 KV 显存,不是 FLOP:同一 prompt 的 \(G\) 条序列共享 prompt 部分的 KV(\(P \cdot k_{kv}\) 每条),推理引擎的 prefix caching 让它自动发生。到 Agent 训练(几十轮、每轮追加)它才成为 FLOP 上的决定因素(第七章)。

4. 旧策略要不要重算

PPO 的重要性比需要 \(\log \pi_{old}\)——采样时策略的对数概率。推理引擎在生成时顺手就能算出它(采样本来就要算 logits),原则上不必再过一遍前向。但多数框架默认在训练器上重算(verl 的 recompute_log_prob),原因是训推不一致:推理引擎(可能是 FP8 权重、不同的 attention kernel、不同的 reduce 顺序)算出的概率与训练器(bf16、FSDP / Megatron)算出的不完全相同,直接混用会让重要性比在 \(\rho = 1\) 附近有系统偏差。重算的代价是 \(2N_a T_{all}\)——一步 FLOP 的六分之一,时间上约 8%(第五章)。第五篇会讨论这个偏差本身,以及用推理引擎的 \(\log \pi\) 做修正而不重算的做法(TIS 一类)。

5. 与 SFT、预训练的量级对比

一步 6.8 EFLOP、几百到一千步,一次推理模型的 RL 训练约 \(10^{21}\)–\(10^{22}\);DeepSeek-R1 级别(更大的模型、更长的回答、几千步)到 \(10^{22}\)–\(10^{23}\)。8B 模型在 15T token 上的预训练是 \(6 \times 8 \times 10^9 \times 1.5 \times 10^{13} = 7 \times 10^{23}\)。RL 后训练的 FLOP 比预训练低一到两个数量级、比 SFT 高一个数量级,它贵的地方从来不在 FLOP——在需要一个推理引擎、几个模型同时在显存、以及每步之间的两道墙。

四、字节账:四份显存与放不下的 KV

1. 四份显存

一步里显存中要出现过的东西:

                            字节                      8B 推理场景(64 卡)           每卡(均分)
① 训练状态                   16 N(bf16 参数 + 梯度 + fp32 主参数 + Adam 两个矩)
                                                      128.5 GB                     2.0 GB
② 参考模型(bf16,只前向)    2 N                       16.1 GB                      0.25 GB
③ 推理引擎的权重副本          2 N(FP8 则 N)           16.1 GB / 实例               16.1 GB(TP = 1)
④ KV cache                  T_all × k_kv             7.12e7 × 128 KiB = 9.33 TB   146 GB —— 放不下
   激活(训练时)             按 micro-batch 的 token 数,可重计算;与预训练同一公式

①的 16 字节来自《大规模训练工程》第一篇;FSDP / ZeRO-3 把它均分到全部训练卡上,64 卡时每卡只有 2 GB——训练状态在这个规模下不是问题,问题是 ③ 和 ④。

③ 是推理引擎自己持有的一份权重,与训练器的分片是两份独立的拷贝(布局不同、可能精度不同),同步 ② 就是刷新它。TP = 1 时每张卡上完整一份 16 GB;32B 模型 TP = 2 每卡 33 GB;DeepSeek-V3 FP8 671 GB 分到 32 张卡每卡 21 GB。

④ 是大头。每 token 128 KiB 是 Llama-3-8B 的 GQA 配置(32 层 × 8 个 KV 头 × 128 维 × K 和 V × 2 字节 = 131072 字节);一条 \(P + \bar L = 8.7\text{K}\) 的序列 1.1 GiB;8192 条序列 9.3 TB——是 64 张卡全部显存(5.1 TB)的近两倍,还没算权重。任何形态下 rollout 都不可能一次装下全部序列。

2. 分波:一个实例装得下多少序列

连续批处理下,一个推理实例(\(t\) 张卡)同时在飞的序列不断有完成、有补入,在飞序列的平均长度约为 \(P + \bar L / 2\)。可用于 KV 的显存是容量减去权重副本、再减去 CUDA context / NCCL / 碎片(本篇按 10%):

\[c = \left\lfloor \frac{t \cdot M (1 - 10\%) - \text{权重副本}}{(P + \bar L/2) \cdot k_{kv}} \right\rfloor\]

8B 推理场景:\((72 - 16.1) \text{ GB} / (4596 \times 128 \text{ KiB}) = 55.9 / 0.60 \approx 92\) 条。64 个实例共 5900 条并发,8192 条序列分 1.4 波。32B(TP = 2)每实例 65 条、32 个实例,分 3.9 波;对话场景(\(\bar L = 1\text{K}\))4096 条序列 16 卡一波装完。

“波”不是字面上的批:连续批处理让它是流水的,但总 decode 步数由 \(T_{resp} / (\text{实例数} \times c)\) 决定,这是第五章时间模型的输入。并发 \(c\) 越大,每个 decode 步摊到每个 token 的权重读取越少——但 KV 读取按并发线性增长,第五章会看到当 KV 填满显存时两者之和是个常数。

3. 共置形态下的显存冲突

把 ① ② ③ ④ 放在同一组卡上:训练时需要 ① + ② + 激活,生成时需要 ③ + ④。8B 场景每卡训练侧只要几 GB 加激活,推理侧要 16 GB 权重加尽量大的 KV 池——两者加起来其实放得下,但 KV 池要”尽量大”,训练的激活(一个 8K 序列的 micro-batch,FlashAttention 下每层约 \(34 \cdot s \cdot h\) 字节,32 层 36 GB,重计算后几 GB)也要空间;32B 以上或 PPO(两套 16N)就真的放不下。于是共置形态在每段之间切换显存归属:生成前把训练状态 offload 到 CPU、释放激活、把 KV 池建满;训练前反过来。切换搬的字节数是每卡的 ① + ②,8B 场景 2.3 GB、PCIe 往返不到 1 秒;32B 场景 9 GB,几秒;DeepSeek-V3 规格 47 GB,接近 4 秒——与步时间相比都小,但它还带着 CUDA graph 重捕获、KV 池重分配、prefix cache 失效这些隐性代价,第三篇专门讨论。

4. 分离形态下的显存

rollout 与训练在不同的卡上,各自的显存只放自己的东西:推理池全部给 ③ + ④,训练池全部给 ① + ② + 激活。代价是两个池的大小要事先定(第二篇),以及同步 ② 要跨机器搬 \(2N\) 字节(第四篇)。

五、秒的账:为什么生成占四分之三

1. decode 的时间模型

decode 每一步对每条在飞的序列生成一个 token,这一步要把全部权重读一遍(\(2N_a\) 字节,MoE 是被路由到的专家)、把每条序列的 KV 读一遍(\(c \times (P + \bar L/2) \times k_{kv}\)),而算的只有 \(2N_a \times c\) FLOP。以 8B、\(c = 92\) 为例:读 16 GB 权重加 \(92 \times 4596 \times 128 \text{ KiB} = 55.4\) GB 的 KV,共 71.5 GB;算 \(2 \times 8 \times 10^9 \times 92 = 1.5 \times 10^{12}\) FLOP。H100 读 71.5 GB 要 21 毫秒(3.35 TB/s),算 1.5 TFLOP 要 1.5 毫秒——相差 14 倍,decode 是纯 memory-bound,与推理服务里的结论相同(《大模型推理系统揭秘》第一篇)。

于是一个 decode 步的时间:

\[t_{step} = \frac{\text{权重副本} + c \cdot (P + \bar L/2) \cdot k_{kv}}{t \cdot BW \cdot \eta_{bw}}\]

注意分子:当 KV 池按第四章第 2 节填满显存时,权重 + 在飞 KV ≈ 可用显存 ≈ \(0.9 M\)。一个 decode 步就是把整块 HBM 读一遍:\(0.9 \times 80 \text{ GB} / (3.35 \text{ TB/s} \times 0.6) \approx 36\) 毫秒,与模型大小、序列长度都无关——它们只影响这 36 毫秒里有多少条序列、也就是每卡的 token/s。8B 推理场景每卡 \(92 / 0.036 = 2600\) token/s;对话场景(\(\bar L = 1\text{K}\),KV 小、并发 256 就把序列用完,HBM 没填满)每卡 11900 token/s;32B(TP = 2)每卡 900 token/s。这些数字与 vLLM 在长上下文下的实测吞吐是同一量级。

换算成 MFU:8B 推理场景 \(2 \times 8 \times 10^9 \times 2600 / 989 \times 10^{12} = 4\%\);对话场景 9%;带宽利用率 \(\eta_{bw}\) 取 0.6 是保守值,实测更好的引擎能到 0.7–0.8,MFU 相应上浮但仍是个位数到百分之十几《后训练》第三篇给的 10–25% 对应短回答、大并发的对话场景;长思维链下 KV 读取占了带宽的大半,MFU 只有它的一半到四分之一。

2. 吞吐时间与长尾

生成的总时间由两部分组成。吞吐部分:\(T_{resp} / (\text{实例数} \times c)\) 个 decode 步,每步 \(t_{step}\);8B 推理场景 \(6.71 \times 10^7 / (64 \times 92) = 11400\) 步 × 35.6 毫秒 = 406 秒。长尾部分:最后一波里最长的回答要一直生成到 \(L_{max}\),此时同批的其他序列早已结束,并发很低,每步只读权重(8 毫秒);多走的 \(L_{max} - \bar L = 24576\) 步是 196 秒——长尾占了生成时间的三分之一,而这段时间里 64 张卡只在为几条序列服务。

回答长度的分布在推理任务上极宽(简单题几百 token、难题几万),\(L_{max} / \bar L = 4\) 是保守假设;实际训练里 \(L_{max}\) 常设为 32K 而 \(\bar L\) 几 K,比值 8–10。长尾是同步形态的固有代价——训练要等全部 rollout 完成——第五篇的部分 rollout 与异步就是为它而生。

3. 前向与训练

三个前向与训练是算力受限的常规负载,时间 = FLOP ÷(卡数 × 峰值 × MFU)。prefill 形态的前向 MFU 取 0.5(整条序列一次算、大 GEMM);训练取 0.4(《大规模训练工程》第四篇的水平;RL 的训练 batch 是变长序列打包,MFU 比预训练略低)。8B 推理场景:参考 + 旧策略前向 \(2.28 \text{ EFLOP} / (6.33 \times 10^{16} \times 0.5) = 72\) 秒;训练 \(3.43 \text{ EFLOP} / (6.33 \times 10^{16} \times 0.4) = 136\) 秒。

4. 两次同步

共置形态的显存切换:每卡的训练状态 + 参考权重经 PCIe(25 GB/s)出去再回来,8B 场景 \(2 \times 2.3 \text{ GB} / 25 \text{ GB/s} = 0.2\) 秒。权重同步:\(2N = 16\) GB 广播到全部推理实例,跨机按一条 400 Gb/s 链路的有效带宽 50 GB/s 算 0.3 秒(NVLink 内更快;DeepSeek-V3 规格 1.34 TB 是 27 秒——第四篇的主题)。在同步形态下这两项都不是主要开销;异步形态下同步频率与方式会变,第五篇再算。

5. 一步的墙钟与全步 MFU

把五段加起来(8B 推理场景,64 张 H100):

生成(吞吐 406 s + 长尾 196 s)    602 s    74%
prompt prefill                    0.1 s     0%
参考 + 旧策略前向                    72 s     9%
训练                               136 s    17%
显存切换                            0.2 s     0%
权重同步                            0.3 s     0%
一步墙钟                            810 s

全步 MFU \(= 6.80 \times 10^{18} / (64 \times 989 \times 10^{12} \times 810) = 13\%\)。这就是本篇核心问题的答案:在这个配置下,GPU 平均利用率的上限是 13%,其中生成阶段 4%、训练阶段 40%,按时间加权。它是上限而不是估计,因为每一段都按各自形态的理想值算,没有算任何等待、数据搬运、Python 开销。

四个场景放在一起:

                            一步墙钟    生成占比    全步 MFU    每卡 decode token/s
8B 对话  B=512 G=8  L̄=1K    111 s      42%        29%        11900
8B 推理  B=512 G=16 L̄=8K    810 s      74%        13%         2600
32B 推理  (TP=2)            2407 s      65%        18%          900
DeepSeek-V3 推理 (256 卡)     654 s      59%        19%         2100

规律:回答越长、生成占比越高、全步 MFU 越低——RL 后训练的系统效率由 rollout 决定。HybridFlow(verl)与 OpenRLHF 的论文报告 rollout 占 60–80% 的墙钟,与这张账一致;它们的系统优化几乎全部落在生成一侧,也是这个原因。

6. 三个杠杆

从时间模型能直接读出三个能压缩生成时间的杠杆,它们分别对应后面的三篇:

  • 让训练器在生成时不闲着——生成的 602 秒里训练器没有工作;分离 / 异步形态把它填上(第二、五篇);
  • 砍掉长尾——196 秒在等几条序列;部分 rollout、异步(第五篇);
  • 提高 decode 的带宽效率——更大的并发(更多显存给 KV:FP8 权重、MLA、KV 量化)、更高的 \(\eta_{bw}\)(更好的 kernel);这是推理引擎自己的事(《大模型推理系统揭秘》),本系列只用它的结果。

FLOP 一侧几乎没有杠杆:12N 里能省的只有旧策略重算的 2N。

六、三个作业的形态

1. 为什么它们互斥

把三个作业的特征放在一张表里,就能看到它们为什么不能简单地”放在一起跑”:

  生成 前向(参考 / 旧策略 / RM) 训练
系统形态 serving:请求、批、KV 池 批处理 分布式训练步
扩展单位 序列(并发数) token token
瓶颈 HBM 带宽 算力 算力
MFU 个位数到 10% 出头 40–50% 40% 上下
显存主体 KV 池(越大越好) 权重 + 一份激活 16N 状态 + 激活
并行配置 TP(NVLink 内)、EP、DP 多实例 与训练相同 TP / PP / DP / CP / EP
权重 独立副本,可能 FP8 训练器的分片 训练器的分片
时间由什么决定 最长的序列 token 总数 token 总数
引擎 vLLM / SGLang FSDP / Megatron FSDP / Megatron

三行最关键:瓶颈不同(一个吃带宽一个吃算力——同一张卡同一时刻只能被一种用法用满)、显存主体不同(KV 池要尽量大 vs 16N 要放下——两者争同一块显存)、并行配置不同(推理引擎的 TP / EP 度按延迟与显存定,训练器的按 16N 与通信定——两边的权重分片布局不一样,所以同步 ② 不是简单的拷贝)。

2. 两次同步搬什么

同步 ①(样本进训练器):每条序列的 token id(8K token × 4 字节 = 32 KB)、推理引擎的 \(\log \pi_{old}\)(若不重算,8K × 4 字节)、reward、长度与结束原因。8192 条序列共几百 MB——小到可以忽略,但它是控制流的同步点:严格 on-policy 下训练器要等最后一条。

同步 ②(权重回推理引擎):\(2N\) 字节(FP8 推理则 \(N\)),要从训练器的分片布局映射到推理引擎的分片布局、可能要量化、要在不打断生成的前提下加载。它的字节数不大,但机制复杂——第四篇整篇讨论。

3. RL 与预训练、推理服务的分工

用架构视图说:预训练只有右半(训练器),推理服务只有左半(推理引擎),两条主线在《大规模训练工程》《大模型推理系统揭秘》里各自讲完。RL 后训练是第一个同时需要两半、并且两半之间有每步一次的数据与权重交换的负载。本系列不重讲任何一半的内部,只讲它们之间的那条线——以及为了这条线,两半各自要暴露什么接口(推理引擎的批量生成、让渡显存、加载权重;训练器的按并行配置跑一步、导出分片权重)。

七、放大器:长思维链、Agent 与 MoE

1. 长思维链

从对话(\(\bar L = 1\text{K}\))到推理模型(\(\bar L = 8\text{K}\)–\(32\text{K}\)),账上的变化不是线性的:

  • \(T_{resp}\) 涨 8–32 倍,FLOP 同比;
  • KV 每条涨 8–32 倍,单实例并发 \(c\) 反比下降,decode 步数(\(T_{resp} / c\))涨 64–1000 倍——这是墙钟从两分钟到十几分钟的来源;
  • 长尾 \(L_{max} - \bar L\) 从 3K 到 24K,占比从 25 秒 / 47 秒到 196 秒 / 602 秒;
  • 全步 MFU 从 29% 掉到 13%。

四个场景表里 8B 对话 → 8B 推理这一行就是这个放大器。

2. Agent 环境

多轮工具调用的 RL(《后训练》第六篇)改变的是账的组成

  • 一条轨迹几万 token,其中约八成来自环境(工具返回、文件内容、测试输出),只有两成是模型生成的;生成 FLOP 按两成算,但参考前向、旧策略前向、训练要过全部 token——每个有效训练 token 的代价是单轮的约 5 倍;
  • 前缀缓存从”省显存”变成”省 FLOP”:几十轮里每轮把新增部分 prefill 进已有的 KV,累计 prefill 从 \(O(\text{轮数}^2)\) 降到线性,没有它 prefill 会超过 decode;
  • 长尾的来源从”回答长度的方差”变成”环境耗时的方差”:一次测试几十秒到几分钟、几十轮,一条轨迹的墙钟以十分钟计,而且不能像部分 rollout 那样”暂停一半下次继续”(沙箱状态要保持);
  • 账上多了一列:环境的 CPU·小时。500 个任务 × \(G = 8\) × 20 轮 = 8 万次容器执行,每次几十秒——几百 CPU·小时,与模型侧的几个 GPU·小时按价格算同量级、按墙钟算取决于沙箱并发数。

这些让异步从”优化”变成”必需”,第六篇专门讨论。rl_ledger.pyagent 场景只算模型侧,环境侧的账在第六篇加进去。

3. MoE

DeepSeek-V3 规格(671B 总参数、37B 激活)改变的是FLOP 与字节的比例

  • FLOP 按 \(N_a = 37\text{B}\) 算,一步 31 EFLOP,只比 32B dense 多 13%;
  • 权重字节按 \(N = 671\text{B}\) 算:训练状态 10.7 TB(256 卡每卡 42 GB——训练侧的 FP8 与 EP 是必需,《大规模训练工程》第二篇),推理副本 FP8 671 GB 要 32 张卡一个实例;
  • decode 每步读的权重是被路由到的专家——并发几千条时几乎全部专家都被命中,等于读 671 GB / 32 卡 = 21 GB 每卡,与 dense 模型”读全部权重”相同;
  • MLA 把 \(k_{kv}\) 压到约 70 KB,KV 总量 5 TB,是 GQA 8B 模型的一半——这是它能把 8192 条序列放进一波的原因;
  • 权重同步 1.34 TB(bf16 训练侧)或 671 GB(量化后),跨机几十秒——第四篇的增量同步与量化传输就是为这个量级设计的;
  • 推理侧的 EP 与训练侧的 EP 度不同,专家的布局映射是同步 ② 最复杂的部分。

四个场景表里 DeepSeek-V3 一行的全步 MFU(19%)反而比 8B 推理高,来自两点:MLA 让并发大(1024 条 / 实例)、FP8 让权重读取少;MoE 的 decode 效率问题在别处——专家负载不均、all-to-all 通信——不在这张单卡带宽模型里,第八篇的可观测会讨论。

八、公开配方的账

把几个公开配方的配置代进账本(都是从论文能读出的参数,缺的项按本篇的默认值补):

配方 模型 \(B\) \(G\) \(L_{max}\) 一步 token(量级) 说明
DeepSeekMath GRPO(Shao 等 2024) 7B 1024 64 1024 \(6.5 \times 10^7\) 对话长度、超大 \(G\):一步 6.5 万条序列,KV 是对话规格但序列数是推理场景的 8 倍,分波与长尾都由 \(G\) 撑起
DeepSeek-R1-Zero(DeepSeek-AI 2025) 671B / 37B MoE 16 32768 \(10^8\) 量级 长思维链 + MoE:本篇第四个场景的原型;论文报告回答长度在训练中从几百涨到上万 token——同一个任务的账在训练过程中变化一个数量级,配比不能一次定死
Kimi k1.5(Kimi Team 2025) 明确报告了部分 rollout:给每步的生成设 token 上限、未完成的序列暂停到下一步继续,就是为了第五章第 2 节的长尾
HybridFlow / verl(Sheng 等 2024) 7B–70B 系统论文;报告生成占一步时间的大半、以及共置形态下切换与同步的开销,本篇的账是它的解析版

R1 那一行值得单独说:账不是静态的。RL 训练的目标之一就是让回答变长(更长的思维链),于是 \(\bar L\)、KV、并发、长尾都随训练推进变化——第一步适用的 GPU 配比到第五百步可能已经让训练器空转一半。第二篇讨论配比时会回到这一点;第八篇的可观测要监控的第一个量就是回答长度的分布。

九、动手:rl_ledger.py

本篇的账本是一个纯标准库的 Python 脚本 rl_ledger.py(第一版):输入模型规格、\(B / G / P / \bar L / L_{max}\)、GPU 数与 TP 度、算法(GRPO / PPO)、是否用奖励模型、是否重算旧策略,输出三本账。它是全系列的贯穿工具,后面每一篇给它加一个维度(系统形态与配比、切换、同步方式、异步、环境)。

核心是 ledger() 函数,把前面五章的公式按顺序算一遍:

def ledger(m: ModelSpec, c: RLConfig, hw: Hardware):
    seqs = c.B * c.G
    resp_tokens = seqs * c.L
    prompt_tokens_unique = c.B * c.P            # 前缀缓存:一个 prompt 的 prefill 只做一次
    all_tokens = seqs * (c.P + c.L)             # 前向 / 训练要过的 token(prompt 也过前向)
    Na, Nt = m.n_active, m.n_total

    # --- FLOPs:生成 2N/token,前向 2N,训练 6N;critic 8N ---
    f_gen = 2 * Na * resp_tokens
    f_prefill = 2 * Na * prompt_tokens_unique
    f_ref = 2 * Na * all_tokens
    f_old = 2 * Na * all_tokens if c.recompute_old_logprob else 0.0
    f_rm = 2 * Na * all_tokens if c.reward_model else 0.0
    f_train = 6 * Na * all_tokens
    f_critic = 8 * Na * all_tokens if c.ppo else 0.0
    f_total = f_gen + f_prefill + f_ref + f_old + f_rm + f_train + f_critic

    # --- 显存(bf16 权重 2 字节/参数;训练状态 16 字节/参数,FSDP 均分到全部卡)---
    w_bytes = 2 * Nt
    train_state = 16 * Nt
    per_gpu_train = train_state / c.gpus
    per_gpu_ref = w_bytes / c.gpus
    per_gpu_rollout_w = m.rollout_dtype_bytes * Nt / c.tp
    kv_total = all_tokens * m.kv_bytes_per_token

    # --- 生成:decode 是 memory-bound,按每步读的字节数估时间 ---
    instances = c.gpus // c.tp
    usable = hw.hbm_bytes * c.tp * (1 - c.overhead_frac) - per_gpu_rollout_w * c.tp
    # 连续批处理下在飞的序列平均只生成到一半:按 P + L/2 估每条的 KV,得到一个实例的稳态并发
    kv_per_seq_avg = (c.P + c.L / 2) * m.kv_bytes_per_token
    concurrent = max(1, int(usable // kv_per_seq_avg))
    concurrent = min(concurrent, math.ceil(seqs / instances))    # 序列不够多时并发由序列数决定
    kv_inflight = concurrent * kv_per_seq_avg
    step_bytes = per_gpu_rollout_w * c.tp + kv_inflight
    t_decode_step = step_bytes / (hw.hbm_bw * c.tp * c.bw_util_decode)
    decode_steps_total = resp_tokens / (instances * concurrent)   # 吞吐意义下的 decode 步数
    t_gen_throughput = decode_steps_total * t_decode_step
    # 长尾:最后一波里最长的序列要多走 (L_max - L) 步,此时并发很低、每步只读权重
    t_tail = (c.L_max - c.L) * (per_gpu_rollout_w * c.tp) / (hw.hbm_bw * c.tp * c.bw_util_decode)
    t_gen = t_gen_throughput + t_tail

    cluster = c.gpus * hw.peak_flops
    t_prefill = f_prefill / (cluster * c.mfu_prefill)
    t_fwd = (f_ref + f_old + f_rm) / (cluster * c.mfu_prefill)
    t_train = (f_train + f_critic) / (cluster * c.mfu_train)

    # --- 两次同步(共置形态):显存切换 + 权重同步 ---
    t_switch = 2 * (per_gpu_train + per_gpu_ref) / hw.pcie_bw    # 训练状态出去再回来
    t_sync = w_bytes / hw.sync_bw                                 # 新权重广播到推理实例
    t_step = t_gen + t_prefill + t_fwd + t_train + t_switch + t_sync
    step_mfu = f_total / (t_step * cluster)
    ...

不带参数运行打印四个内置场景,本篇第五章的表就是从这里来的。8B 推理场景的完整输出:

$ python rl_ledger.py --model 8b --profile reasoning
== Llama-3-8B  GRPO  B=512 G=16 P=500 L=8192 L_max=32768  64xH100 SXM tp=1
  序列 8,192   回答 token  67.11 M   前向要过的 token  71.20 M
  -- FLOPs
     生成 2N               1.08 EFLOP    15.9%
     prompt prefill 2N   4.11 PFLOP     0.1%
     参考前向 2N             1.14 EFLOP    16.8%
     旧策略前向 2N            1.14 EFLOP    16.8%
     策略训练 6N             3.43 EFLOP    50.5%
     合计                  6.80 EFLOP   = 11.9 N 每 token
  -- 显存
     bf16 权重  16.06 GB   训练状态(16/参数) 128.48 GB   -> 每卡:训练   2.01 GB + 参考 250.94 MB + 推理权重副本  16.06 GB
     KV cache 全部序列   9.33 TB(128 KiB/token)   集群显存   5.12 TB   -> 64 个实例 × 并发 92,分 1.4 波
  -- 时间(秒)
     生成(含长尾 196s)             601.7    74.3%
     prompt prefill             0.1     0.0%
     参考/旧策略/RM 前向              72.3     8.9%
     训练                       135.5    16.7%
     显存切换(共置)                   0.2     0.0%
     权重同步                       0.3     0.0%
     一步墙钟                     810.1   decode 步 35.6 ms,每卡 2587 token/s,decode MFU 2.8%
  -- 全步 MFU 13.3%   (  6.80 EFLOP / (64 GPU × 810 s × 989.00 TFLOPS))

脚本真正有用的地方是换一个参数、看账怎么变——这也是排一个 RL 任务之前要做的事。以上面的 8B 推理场景为基线,只改一个参数:

变体 一步 FLOP KV 总量 / 波数 生成(秒) 训练(秒) 一步墙钟 生成占比 全步 MFU
基线 GRPO,64 卡 6.80 E 9.3 TB / 1.4 602 136 810 74% 13.3%
--ppo(加价值模型) 11.37 E 9.3 TB / 1.4 602 316 991 61% 18.1%
--no-recompute(不重算旧策略) 5.66 E 9.3 TB / 1.4 602 136 774 78% 11.5%
--G 32(每题 32 条) 13.60 E 18.7 TB / 2.8 1007 271 1423 71% 15.1%
--L 16384(回答长一倍) 13.27 E 18.1 TB / 2.6 1662 263 2066 80% 10.1%
--gpus 128(卡数翻倍) 6.80 E 9.3 TB / 1.0 419 68 523 80% 10.3%
--gpus 32(卡数减半) 6.80 E 9.3 TB / 2.8 1007 271 1423 71% 15.1%

几个值得看的对比:

  • PPO 的 MFU 反而更高(13.3% → 18.1%):价值模型多出的 4.6 EFLOP 全落在训练段,训练是 compute-bound、MFU 0.4,把全步的平均拉上去了。但墙钟多 22%——MFU 高不等于快,它只说明多出来的活是”好算的活”。反过来,--no-recompute 省下 1.1 EFLOP、墙钟少 4.5%,MFU 却降到 11.5%:去掉的是高 MFU 的前向。看 RL 系统的效率要看墙钟,全步 MFU 只是分母的注脚。
  • G 翻倍与卡数减半是同一张账(两行数字几乎相同):都是”每张卡要生成的 token 数翻倍”,生成从 1.4 波变 2.8 波。差别在训练状态每卡 2 GB → 4 GB,8B 不构成问题,70B 就是另一回事。
  • 回答长一倍比 G 翻倍贵得多(2066 秒 vs 1423 秒,FLOP 却几乎一样):token 数相同,但每条序列的 KV 翻倍、单实例并发从 92 掉到 49,decode 每步喷出的 token 少一半——这就是第五章说的”每步读一遍 HBM,序列越长每步分到的 token 越少”。回答长度是所有参数里对墙钟最敏感的一个,而它在训练中会自己涨。
  • 卡数翻倍墙钟只降 35%(810 → 523 秒):吞吐部分随卡数线性缩短,长尾 196 秒一秒不少——它取决于最长那条序列的长度,不取决于卡数。128 卡时长尾占生成时间的 47%,再加卡就是给长尾陪跑。这是第五篇”部分 rollout”要解决的问题,也是这张账里唯一不能用钱买的项
  • MoE 场景(--model dsv3 --tp 32 --gpus 256)见第五章第 5 节的四场景表与第七章第 3 节:FLOP 按激活参数算、字节按全部参数算,两本账脱钩。

几点说明。输出里的 decode MFU(2.8%)把长尾的 196 秒也算进了分母,第五章第 1 节的 4% 只算吞吐部分。decode 的时间模型是带宽模型:每步读权重加在飞 KV,除以带宽乘利用率;它忽略了 kernel 启动、采样、调度这些 CPU 侧开销,也忽略了 TP 通信——这两项在 《大模型推理系统揭秘》里讨论,在这里都会让实际比估算慢。长尾模型是最简单的一种:多走 \(L_{max} - \bar L\) 步、每步只读权重;实际的长尾取决于长度分布的形状,第五篇会用分布代替 \(L_{max}\)。训练 MFU 0.4、前向 0.5、带宽利用率 0.6 是三个假设,都写在 RLConfig 里可以改;结论(生成占大半、全步 MFU 十几个点)对这些假设在合理范围内的变化不敏感。MoE 的 decode 按”读全部专家”算,只在并发大到几乎命中全部专家时成立;小并发下每步读的专家更少、更快,但 EP 的 all-to-all 又会加进来,本篇不建这个模型。

脚本与四个场景的完整输出在 ai-learning-labs/rl-post-training-infra

十、本文小结

1. 要点回顾

  • 在线 RL 的一步是三个形态不同的作业加两次同步:生成(serving,带宽受限,按序列数扩展)、打分(前向或外部服务)、训练(算力受限,按 token 数扩展);样本进训练器、权重回推理引擎。它是唯一把 serving 作业放进训练循环的负载。
  • FLOP 账:生成 \(2N_a\)、三个前向各 \(2N_a\)、训练 \(6N_a\),合计约 \(12N_a\) 每 token;训练占一半、生成六分之一。PPO 再加 \(8N_a\)。前缀缓存对单轮 RL 省的是 KV 不是 FLOP;旧策略重算是六分之一的 FLOP、8% 的时间,为训推不一致而付。
  • 字节账:训练状态 \(16N\) 均分后不是问题;推理权重副本每实例一份;KV cache 按 \(T_{all} \times k_{kv}\) 是集群显存的几倍,rollout 必须分波,单实例并发 \(c\) 由可用显存除以 \((P + \bar L/2) k_{kv}\) 决定。
  • 秒的账:decode 每步把整块 HBM 读一遍(约 36 毫秒,与模型和序列长度无关),每卡 token/s = \(c / t_{step}\);长思维链下 decode MFU 只有个位数。生成时间 = 吞吐部分 + 长尾(\(L_{max} - \bar L\) 步),8B 推理场景 602 秒里长尾 196 秒。
  • 8B 推理场景在 64 张 H100 上:一步 6.8 EFLOP、810 秒,生成 74%、训练 17%、前向 9%;全步 MFU 上限 13%。回答越长比例越极端;对话场景 29%。
  • 三个杠杆——生成时训练器不闲着、砍长尾、提高 decode 带宽效率——分别是第二 / 五篇、第五篇、推理引擎自己的事。FLOP 一侧没有杠杆。
  • 长思维链放大 decode 步数与长尾;Agent 环境把八成 token 变成环境的、把长尾变成环境耗时、加一列 CPU·小时;MoE 让 FLOP 与字节脱钩、让权重同步进入分钟级。
  • 账是动态的:回答长度随训练变长,配比不能一次定死。

2. 公式速查

FLOP        F = 2N_a T_resp + 2N_a BP + (2 + 2[重算] + 2[RM]) N_a T_all + 6N_a T_all (+ 8N_a T_all [PPO])
KV 总量     T_all × k_kv                    k_kv(GQA) = 2 × 层 × KV头 × 头维 × 2 B
单实例并发   c = ⌊(0.9 t M − 权重副本) / ((P + L̄/2) k_kv)⌋
decode 步   t_step = (权重副本 + c (P + L̄/2) k_kv) / (t · BW · η_bw)      ≈ 0.9 M / (BW η_bw) 当 KV 填满
生成时间     T_resp / (实例数 · c) × t_step  +  (L_max − L̄) × 权重副本 / (t · BW · η_bw)
前向 / 训练  FLOP / (n · F_peak · η)
全步 MFU    F / (n · F_peak · 一步墙钟)

3. 本篇涉及的算法侧结论

本篇只用了《后训练》的三个结论:GRPO 一步的采样结构(\(B\)、\(G\)、组内归一化)、需要参考模型与旧策略的对数概率、\(\mu = 1\) 是常见配方。方法本身(目标函数、优势、clip、KL 估计)见那个系列的第三、五、六篇。

4. 下一篇

有了三段的时间——生成 602 秒、前向 72 秒、训练 136 秒——下一个问题就是怎样把它们放到 64 张卡上:全部卡轮流做三件事(共置),还是 48 张生成、16 张训练同时跑(分离),还是训练不等生成(异步)?三种形态各把 GPU 用到几成、代价是什么、配比怎么定?

同样 64 张卡、同样的 GRPO 配置,共置、分离与异步三种形态下,一步的墙钟时间与 GPU 利用率各是多少?回答长度的方差从小变大时,哪种形态先撑不住?

下一篇:系统形态——共置、分离与异步。配套代码:rl-post-training-infra/rl_ledger.py

系列:RL 后训练基础设施:rollout 与训练如何共享一组 GPU

  1. 负载画像——一步 RL 里发生什么

本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/rl-step-anatomy-rollout-reward-train.html)的前提下,欢迎各种形式的转载、翻译或商业引用。


COMMENTS

评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。

×