跳转到主要内容
第 13 章解决了采样与训练如何协同;本章进一步检查时间和显存花在了哪里。padding 浪费计算,60k token 的长轨迹可能让一个 microbatch 超出显存预算,完整 logits 张量可能比模型还大,错误的梯度累积归一化还会改变更新。这些问题既关乎效率,也关乎正确性:packing、分块和过滤只有在保留目标函数含义的前提下才算优化。 我们构建并保存四个工具:带有正确块对角注意力掩码的序列打包(packing)、按 token 预算划分微批次、带检查点的分块对数概率(log-probability),以及按通过率过滤任务。每个工具都与它所替代的直接计算进行核对,本章最后给出把 token 数换算成 GPU 小时的算术。

14.1 填充及其浪费

collate 把一个批次中的每个回合(episode)都填充到最长回合的长度。仅从长度就能看出代价:一个填充后的批次要计算 B⋅Tmax⁡B \cdot T_{\max} 个位置,其中真实的只有 ∑iTi\sum_i T_i 个。
Output:
TinyAgent 的回合长度从 10 个 token(立即提交)到 149 个(包含多次观测的长回合),中位数为 18。把它们全部填充到 149,要为 2,472 个真实 token 计算 9,536 个位置,因此前向和反向传播中有 74% 花在了填充上。智能体(agent)轨迹在任何规模上都有这种形状:大多数回合很短,少数很长,而长回合决定了批次的宽度。 序列打包把多条序列拼接到一个固定长度的行中,把各行填得几乎满。它消除了浪费,同时也引入了两种算错的方式。在打包的行中,第二条序列的 token 位于第一条序列的 token 之后。普通的因果掩码允许它关注前面的 token,于是它的对数概率以一个不相关的回合为条件:这就是注意力泄漏(attention leak)。此外,在使用绝对位置或旋转位置编码时,它的位置索引会从第一条序列接着往下数,而不是从 0 开始。正确的打包需要一个块对角因果掩码(block-diagonal causal mask,token 只关注自己序列中更早的 token),以及在每个序列边界处重新开始的位置 id。FlashAttention 的变长接口等打包训练 kernel 接收的是以累积序列长度表示的边界,而不是掩码;计算是相同的。 pack_sequences 用首次适应递减(first-fit decreasing)把轨迹放入长度为 max_tokens 的行:最长的先放,每条放进第一个有空间的行。它返回打包后的 id、逐 token 的位置 id 和段 id(序列在所在行中的索引,填充为 −1-1),以及与 collate 相同 [R, L-1] 布局的动作掩码(action mask)和采样器对数概率。一条序列的最后一个位置不为自己预测任何东西,因此本会预测下一条序列第一个 token 的那个掩码项保持为 0。
TinyAgent 的 forward 根据序列长度自行构建因果掩码,所以打包后的前向传播必须由各个部件拼装:token 嵌入加上给定位置 id 处的位置嵌入,带显式掩码的 transformer 块,最终的归一化层和输出头。packed_logits 根据段 id 构建掩码:如果 i≤ji \le j 且两者属于同一段,位置 jj 就可以关注位置 ii。PyTorch 的注意力接受形状为 [R * n_heads, L, L] 的浮点张量作为逐行掩码,禁止关注的位置为 −∞-\infty。填充位置只关注自身,所以 softmax 的每一行都不为空。
我们把 64 个回合打包进长度为 256 个 token(TinyAgent 的最大长度)的行中,并把每个策略 token 的打包对数概率与每个回合单独运行时的结果比较。三个变体分别展示两个 bug 和修复。
Output:
打包把 64 个回合放进 10 行,填充率为 96.6%,而填充批次中真实 token 只占 26%。使用普通因果掩码、位置贯穿整行时,策略 token 的对数概率与未打包时相差最多 6.4 nats,概率上约差 600 倍。重置位置修复了一部分;仅通过注意力的泄漏仍会让某些对数概率偏移 1.7 nats。使用块掩码并重置位置后,打包计算与未打包计算的差异在 1.4×10−61.4 \times 10^{-6} 以内,即 float32 的舍入误差。泄漏版本不会大声报错:形状是对的,损失是有限的,训练在错误的条件分布上继续进行。 图 14.1:同样 64 个 TinyAgent 回合的两种布局:填充批次(左,每个回合一行,填充到最长回合)与打包进 256 token 的行(右)。蓝色:策略 token;水绿色:提示、模板和工具 token;灰色:填充。 图 14.1 以相同比例绘制了两种布局,因此面积就是计算量。填充批次大部分是灰色;打包后的行几乎是实心的。打包行中的深色刻度是块掩码必须遵守的序列边界。

14.2 Token 预算与长度均衡的微批次

打包解决了行内的浪费;下一个问题是一个微批次里放多少条序列。每个微批次固定序列条数,会让内存与恰好落入其中的最长序列成正比。智能体轨迹有长尾,所以固定条数要么在大多数微批次上浪费内存,要么在运气不好的那个上内存溢出。token 预算固定的则是内存真正依赖的量:每个微批次容纳尽可能多的序列,只要总填充位置不超过 max_tokens。先按长度排序(长度均衡的微批次)可以让长度相近的序列放在一起,进一步减少填充。
TinyAgent 的回合过于均匀,显示不出这种效果,因此我们使用一种典型的智能体轨迹长尾长度分布:中位数为 8k token 的对数正态分布,在 64k 上下文处截顶。三种方案看到的是同样的 512 条轨迹。对每种方案,我们报告微批次数量(反向传播步数的代理指标)、填充位置所占比例,以及按填充 token 计最大的微批次,它决定了激活内存的峰值。
Output:
随机顺序下每个微批次 8 条轨迹时,59% 的计算位置是填充,最大的微批次有 524k 个填充 token:它包含一条达到 64k 上限的轨迹,代价就是 8 乘以 64k。分组前先按长度排序可以去掉大部分填充(3.6%),因为相邻序列长度相近,但最大微批次仍是 524k,因为最长的 8 条轨迹仍然在同一个微批次里。token 预算让填充保持在低水平(3.0%),并把每个微批次限制在 131k token,最长的轨迹会单独组成微批次,或两条组成一批。它需要的微批次也更少,是 51 个而不是 64 个,因为许多短轨迹被归入同一个微批次。这样便能通过预算控制每个微批次的 token 数,不再被某条长轨迹带来的 padding 主导。实际显存还取决于注意力实现等因素。

14.3 logits 的内存

第 3.6 节介绍了 chunked_log_probs,并指出仅靠分块只能节省前向内存,不能节省反向内存。下面是一个训练微批次的算术。对 NtokN_{\text{tok}} 个 token 和词表(vocabulary)大小 VV,完整的 logits 张量有 NtokVN_{\text{tok}} V 个数。Qwen3 的 VV 是 151,936,所以一个 token 的 logits 在 fp32 下是 0.6 MB,一个 32k token 的微批次在 log-softmax 输出和梯度之前就有 20 GB 的 logits,而这两者各自也同样大。在隐藏宽度 DD 下,模型自身的逐 token 激活每层只是 DD 的几倍,而对 Qwen3 的各个规模,VV 是 DD 的 37 到 148 倍。
Output:
修复分两部分。沿时间轴分块,使前向传播中任一时刻只存在一个分块的 logits;并在反向传播中重新计算每个分块的 logits,而不是保存它们。torch.utils.checkpoint 负责第二部分:它运行分块时不保存中间结果,并在反向时重放。checkpointed_log_probs 就是第 3 章的 chunked_log_probs 加上这一处改动。
我们在 GPU 上测量经过 Qwen3-0.6B 输出层的一次前向和反向传播的峰值内存,用随机的 bf16 隐藏状态代替最后一层的输出。只测量 logits 的计算:不包括 transformer 自身的激活。
Output:
分配器的峰值内存会随着 GPU 上之前运行过的东西而有零点零几 GB 的波动,所以只打印到一位小数。三种方法返回完全相同的对数概率。在 4,096 个 token 时,仅一个输出层的完整计算峰值就达到 7.5 GB,约每个 logit 12 字节:bf16 矩阵乘法输出、它的 fp32 副本,以及 fp32 的 log-softmax,其中一些在反向传播时同时存活。分块把它减半,但它同样随 TT 增长(16k token 时为 11.2 GB),因为 autograd 为反向传播保存了每个分块的 log-softmax:分块只是去掉了临时的完整张量。在这个只测输出层的实验中,重新计算让峰值基本不随序列长度增长:两个长度下都是 2.2 GB,即一个分块的量加上权重梯度。代价是对输出层再做一次前向传播,占该步 FLOPs 的百分之几。 图 14.2:一个 8B 模型在 8 块 GPU 上完全切分时,一个 RL 训练微批次的每 GPU 内存,按组成部分划分(在明确假设下的算术)。logits 分别显示完整张量、分块、分块加重新计算三种情况。 图 14.2 把 logits 与 GPU 上的其他所有内容放在一起比较:一个 8B 模型,参数、梯度和 Adam 状态在 8 块 GPU 上完全切分,每个微批次 32k token。使用完整 logits 张量时,logits 是最大的一项,比切分后的模型和优化器加起来还大。分块把它降到第二位;重新计算则把它降为很小的一项。这就是为什么对长上下文 RL 来说,logits 的处理方式是第一个内存决策,排在激活检查点或切分策略之前。

14.4 梯度累积与损失归一化

内存放不下的批次被拆分成多个微批次,在优化器步之前把它们的梯度加起来。累加结果应等于对完整批次求梯度。但若每个微批次各自使用 token_mean,通常就不相等。token_mean 除以批次中策略 token 的数量。如果微批次循环对每个微批次计算 token_mean,再除以微批次数量,那么每个微批次得到相等的总权重,于是一个 token 的权重变成 1/(Knk)1 / (K n_k) 而不是 1/N1/N,其中 nkn_k 是其所在微批次的 token 数,KK 是微批次数量,N=∑knkN = \sum_k n_k。短微批次中的 token 被加权过高。修复方法是把每个微批次的 token 损失之和都除以全局 NN,aggregate_loss 已经通过 mode='constant' 加 norm_const=N 支持这一点。当每个数据并行 rank 在梯度 all-reduce 之前用自己的 token 数做归一化时,跨 rank 也会出现同样的 bug。
参照是对全部 64 个回合做一次反向传播。我们把它拆成 4 个微批次,分别按随机顺序和按长度排序的顺序(token 预算调度器就会这样做)。
Output:
使用全局归一化因子时,两种拆分都以 1.3×10−71.3 \times 10^{-7} 的精度复现了整批梯度,即舍入误差。使用逐微批次的 token_mean 时,随机拆分就已经有 8% 的相对误差(余弦相似度 0.997),因为各微批次含有 159 到 208 个策略 token。按长度排序的拆分(token 预算调度器产生的正是这种拆分)每个微批次含 96 到 355 个 token,误差升到 52%(余弦相似度 0.88):梯度指向一个可测量的不同方向。由于短微批次由短回合组成,而在这个 gym 中短回合多数是立即提交,这个 bug 会系统性地过度加权某一种行为。训练期间打印的损失值对此毫无提示。

14.5 按通过率过滤任务

第 9 章的默认运行把后期的大部分 rollout 花在了优势(advantage)为零的组上:到第 60 到 80 步,约 80% 的采样组要么每次尝试都通过,要么每次都失败。这些回合每个都要付出一次完整 rollout 的代价,却不贡献任何梯度。最便宜的修复位于损失的上游:不要采样结果已经确定的任务。有了对每个任务通过率的滚动估计 p^j\hat p_j,通过率过滤只在 low<p^j<high\text{low} < \hat p_j < \text{high} 时保留任务 jj 用于采样。当 low=0\text{low} = 0、high=1\text{high} = 1 时,它恰好丢弃上一组全部失败或全部通过的任务。一个更柔和的变体按与 p^j(1−p^j)\hat p_j (1 - \hat p_j) 成正比的概率采样任务,这是伯努利奖励的方差,也就是一个组的优势信号的期望大小。
train_curriculum 就是任务池会变化的 train_rl。每个任务的估计值是它上一组的平均奖励,初始化为 0.5,使每个任务一开始都会被采样;这些估计不需要额外的 rollout。每一步从过滤范围内的任务中采样 prompts_per_step 个任务,再从范围外的任务中随机抽取 revisit 个,这样被排除任务的估计会不时得到更新,策略遗忘的任务也能回来。当通过过滤的任务很少时,这一步采样的回合就更少:这正是目的所在。大型任务集可以用有信息量的任务把批次填满;我们的 20 个训练任务则把节省直接体现为没有花掉的 rollout。
当 low=-1、high=2 时,每个任务都能通过过滤,任务池就是整个训练集(train split),循环每步采样 8 个任务,与 train_rl 完全相同,因此不过滤的一组跑的是同一份代码。每次运行训练 60 步,每组两个随机种子(seed),并在留出(held-out)任务上评估,每个任务 16 个回合。
Output:
前 20 步两组相同:每个估计都从 0.5 开始,策略还很弱,几乎每个任务都产生混合结果的组。之后,开始有任务在全部 8 次尝试中都通过并离开过滤后的任务池,到第 60 步,池中只剩一到三个任务。过滤组在 60 步中分别采样了 2,088 和 2,184 个回合,不过滤组为 3,840 个,少了 43% 到 46%;在最后 20 步中,过滤组的零优势比例为 0.38 到 0.47,不过滤组为 0.61 到 0.68。过滤组的留出通过率为 0.73 和 0.77,不过滤组为 0.76 和 0.82:两个种子中都低约 0.04,约为一次 160 回合评估的一个标准误。诚实的解读是:只用略多于一半的 rollout 达到了大致相同的留出通过率,可能有一点小代价,但两个种子无法分辨。这个代价有一个合理的机制:训练后期,过滤组每一步只包含少数几个任务,因此每个梯度涉及的任务更少,这与第 9.7 节中组大小与提示数之间的权衡相同。 只按上一组过滤是最粗糙的估计量(estimator)。一个曾经 8 次中 8 次通过的任务,下一次可能只有 8 次中 6 次通过;当 G=8G = 8、真实通过率为 0.9 时,一个组有 43% 的概率全部通过。生产中的课程学习使用多次访问的滚动平均,保留一个小概率重新采样被排除的任务,并在数据集很大时于每个轮次(epoch)之前离线过滤。
**证据。**本书测得,每组两个随机种子,在 20 个训练任务上训练 60 步。rollout 的节省是过滤的直接结果,是稳健的;留出通过率的比较则在种子噪声范围之内。图 14.3 又增加了两个种子。
图 14.3:BugFix gym 上的通过率过滤,每组四个随机种子。左:留出通过率(每个任务 8 个回合)随累计采样回合数的变化。右:每步采样的回合数。 图 14.3 用每组四个种子展示了同样的比较。左图是累计回合数:两组在约第 20 步之后分开,此时任务开始饱和,过滤组的曲线随着任务池清空而变平。右图中每次运行是一个点:过滤组的运行位于约 2,100 到 2,600 个回合处,不过滤组位于 3,840。它们的留出通过率相互重叠(0.72 到 0.81,对比 0.76 到 0.85),过滤组平均略低,与上面两个种子的情况一致。

14.6 吞吐量核算

本章和上一章中的每个设计决策都是在用一种资源换另一种资源,而只有在预算下才能评判这种交换。预算有两半,行为各不相同:生成和训练。 生成是一系列解码步。每一步为每条序列产生一个 token,并且必须从内存中读取全部权重和每条序列的 KV cache,因此它的速度由内存带宽而不是 FLOPs 决定。TinyAgent 先展示了问题的另一半。rollout_batch 没有 KV cache:每个解码步都在整个前缀上重新运行模型。我们统计它在第 14.1 节的 64 个回合上处理的位置数,并与带 KV cache 的解码器比较,后者每个位置只处理一次。
Output:
没有 KV cache 时,采样这 64 个回合处理了 22,661 个位置,而不是 2,472 个,多了 9.2 倍,而且这个倍数随回合长度线性增长。TinyAgent 负担得起;更大的模型都负担不起。这里统计的是前向计算处理的 token 位置数:KV cache 避免反复处理完整前缀,使这个数量随生成长度线性增长。这不等于注意力总计算量也变成线性;每个解码步仍需读取历史缓存,长上下文下内存访问可能成为瓶颈。 对于真实模型,一个每 GPU 的一阶 roofline 估计就足以看清时间花在哪里。对 bb 条上下文长度为 cc 的序列,一个解码步读取一次权重(bf16 下 2N2N 字节)以及每条序列的 KV cache(cc 乘以每 token 的 KV 大小),因此至少需要 (2N+b c sKV)/bandwidth(2N + b\, c\, s_{\text{KV}})/\text{bandwidth} 秒,产出 bb 个 token。训练要处理每条轨迹的每个 token(提示、策略和工具 token 一视同仁,因为它们都是梯度的输入),前向加反向每个 token 约 6N6N FLOPs,按某个模型 FLOPs 利用率(MFU)计算。下面的代码块从本地缓存中 Qwen3-8B 的真实配置读取每 token 的 KV 大小,并使用 H200 公布的带宽(4.8 TB/s)和稠密 bf16 吞吐量(989 TFLOP/s,是 NVIDIA 所列含稀疏性的 1,979 TFLOP/s 的一半)。利用率和批大小是假设。
Output:
对 Qwen3-8B 来说,每个上下文 token 需要 147 KB 的 KV cache(36 层,8 个维度为 128 的 KV 头,键和值均为 bf16)。在 2k 上下文下,64 条序列的批次每 GPU 每秒解码约 8,600 个 token,与约 8,000 的训练速率大致相当。在 16k 上下文下(这对编程智能体来说已经算短),KV 读取压倒了权重读取,解码降到每秒约 1,800 个 token。更大的批次几乎没有帮助(256 条时每秒 1,935 个 token),因为每增加一条序列就要多读一份它自己的缓存。在假设的 40% MFU 下,训练每 GPU 每秒处理的 token 数约为长上下文解码的 4.5 倍。这种不对称正是 RL 系统把大部分加速器分给推理引擎的原因,也是每个 rollout token 远比每个训练 token 昂贵的原因。 把两半合在一起就得到一次运行的 GPU 小时数。下面的代码块估算一次假想的 Qwen3-8B 智能体 RL 运行中一步的成本,风格与附录 B 问题 19相同,后者为一个 32B 模型做了同样的估算。每个输入都是假设,并在代码中标明。
Output:
在这些假设下,一步约耗费 1.6 GPU 小时,300 步约 490 GPU 小时。其中 59% 是解码,尽管策略 token 只占全部 token 的四分之一;训练前向反向本身约占四分之一。两个最大的不确定因素是解码速率(如上一个代码块所示,它取决于上下文长度和批大小)和忙碌比例(它综合反映了工具延迟和第 13 章中的拖尾等待):把它从 0.5 改为 0.25,每步约增加 1 GPU 小时。附录 B 对具有相同回合形状的 32B 模型的估算约为每步 7.7 GPU 小时,比例上是一致的。
**证据。**H200 的带宽和吞吐量数字是 NVIDIA 公布的规格(查阅于 2026-10-13);KV 大小由 Qwen3-8B 的配置文件计算得出。本节其余内容(回合形状、批大小、MFU、忙碌比例)都是为说明而选的假设,结果是一个一阶估计,忽略了注意力 FLOPs、通信、评估和失败的运行。在规划预算之前,请用试点运行的测量值替换每一项假设。

14.7 实践中的设计问题

问题:梯度累积下的损失归一化。 现象: 改变微批次大小、数据并行 rank 数或批次调度器会改变学习曲线,尽管全局批次相同;切换到按长度排序的微批次之后,短回合似乎学得更快。 原因: 逐微批次的 token_mean 给每个微批次相等的权重,于是一个 token 的权重是全局损失所分配权重的 N/(Knk)N / (K n_k) 倍(第 14.4 节)。按长度排序的微批次让 nkn_k 变化最大。 可选方案: (a) 按全局 token 数归一化(在数据并行中,于反向传播之前对计数做一次 all-reduce)。(b) 使用常数归一化因子(Dr. GRPO 的 constant 模式),它不需要全局计数,且与批次组成无关。(c) 等 token 数的微批次,这只能让误差变小。 建议做法: (a) 或 (b),并配一个单元测试,像第 14.4 节那样把累积梯度与单次传播的梯度相比较。下面的代码块打印按长度排序拆分下逐 token 的权重失真。
Output:
短回合微批次中的 token 得到了正确权重的 1.93 倍,长回合微批次中的 token 得到 0.52 倍:两者相差 3.7 倍,这个倍数由调度器而不是目标函数决定。
问题:打包的注意力泄漏。 现象: 打包训练运行无报错,但在第 0 步,训练器对数概率与采样器对数概率之间的比率就远离 1,裁剪比例(clip fraction)从第一个小批次(minibatch)起就很高,策略学得很慢或发散。 原因: 打包行上的因果掩码让每条序列都能关注排在它之前的序列,而贯穿整行的位置 id 让后面的每条序列都发生偏移;于是训练器在采样器从未见过的上下文下为 token 打分(第 14.1 节)。 可选方案: (a) 块对角掩码或变长注意力 kernel,并重置位置 id。(b) 不打包,承担填充的代价。 建议做法: (a),并配一个在真实轨迹上比较打包与未打包对数概率的测试。采样器自身的对数概率不需要任何参照运行就能让这个 bug 显形。
Output:
使用有泄漏的打包时,在任何更新之前,就有 5.8% 的策略 token 的训练器/采样器比率偏离 1 超过 20%;使用正确的打包时则一个都没有。在监控面板上,这看起来像训推不一致(第 13.6 节),而不一致校正会把它掩盖掉。第 0 步的比率分布是对两者都适用的低成本测试。
问题:浪费在零方差提示上的 rollout。 现象: 随着训练取得进展,零优势组的比例上升;每个有用梯度所需的 rollout 增加;学习曲线在留出通过率停止提升之前很久就变平了。 原因: 奖励全部相等的组在任何基线(baseline)下优势都为零,因此其中每个回合都是一次没有梯度的完整 rollout(第 9 章)。 可选方案: (a) 采样前的通过率过滤(第 14.5 节),它需要逐任务的估计,并可能让估计过时的任务得不到采样。(b) 动态采样,通过多采样来保持批次满额。(c) 对难任务使用更大的组,使罕见的成功能被采样到。 建议做法: (a),偶尔重访被排除的任务,并记录零优势比例。下面的代码块统计第 14.5 节的运行中落入零优势组的回合数。
Output:
在两个种子上,不过滤组 7,680 个回合中有 43% 落入零优势组,即 3,304 次没有产生梯度的 rollout。过滤组总共采样了 4,272 个回合,浪费了其中 1,136 个(27%)。剩余的浪费大部分来自前 20 步(此时估计还没有信息量),以及对被排除任务的重访,后者是有意花在被认为已解决的任务上的。
问题:一条长尾序列主导内存。 现象: 在运行开始很久之后的随机步骤出现内存溢出错误,总是发生在收到最长轨迹的那个 rank 上;降低批大小会让它更少发生,但不会杜绝。 原因: 每个微批次固定序列条数时,填充后的内存是条数乘以最长成员的长度,而在长尾长度分布下,最长成员终将达到上下文上限。这时一个 8 条序列的打包或填充微批次的代价是上下文长度的 8 倍。 可选方案: (a) token 预算(第 14.2 节),把最长的轨迹单独放进微批次。(b) 分块并重新计算的对数概率(第 14.3 节),它降低了每 token 的成本,但没有改变其增长方式。(c) 带截断的硬长度上限,这会改变任务。 建议做法: (a) 与 (b) 结合,并根据在最大上下文长度下测得的峰值来设定预算。下面的代码块找出固定 8 条时最差的微批次,以及 token 预算为同一条轨迹分配的微批次。
Output:
最长的轨迹达到了 64k 上限,是中位数的八倍。在固定 8 条的微批次中,它迫使微批次有 524,288 个填充位置,相当于 8 条最大长度轨迹的内存。token 预算给它分配的微批次里只有另一条同样长度的轨迹,共 131,072 个 token,正好是预算。那个“在第 2,000 步突然内存溢出”的运行,就是这条轨迹第一次出现的运行。

小结

  • 把 TinyAgent 的 64 个回合填充到最长回合,浪费了 74% 的计算位置。打包把各行填到 97%,但只有在使用块对角因果掩码、且位置 id 按序列重新开始时才正确:缺少它们,策略对数概率偏移最多 6.4 nats;有了它们,结果一致到 10−610^{-6}。
  • 每个微批次固定序列条数会让内存与最长序列成正比;对于长尾长度分布,带长度排序的 token 预算把最大微批次从 524k token 降到 131k token,填充率为 3%。
  • 在生产规模的词表下,logits 是长上下文 RL 中最大的内存项。在 Qwen3-0.6B 的输出层上测得:16k token 时完整计算为 29.9 GB,分块为 11.2 GB,分块加重新计算为 2.2 GB,且与长度无关。
  • 使用逐微批次 token_mean 的梯度累积会改变梯度:在 TinyAgent 上,随机拆分的相对误差为 8%,按长度排序的拆分为 52%。用全局 token 数归一化每个微批次(mode='constant')以 10−710^{-7} 的精度复现了整批梯度。
  • 按上一组的通过率过滤任务,在 60 步内把采样回合数减少了 43% 到 46%,留出通过率的差异在种子噪声范围内。对一次 8B 智能体运行的吞吐量算术表明,解码约占 GPU 小时的 60%,因为长上下文解码受内存带宽限制,token 速率大约只有训练的四分之一。

练习

  1. 为 pack_sequences 和 packed_logits 编写一个单元测试,使其分别对第 14.1 节的两个 bug 各自失败。能暴露位置 bug 但不暴露掩码 bug 的最小序列对是什么?
  2. 把 token_budget_batches 扩展到打包微批次,此时成本是长度之和,而不是条数乘以最长长度。在 128k 预算下,那个 512 条轨迹的例子需要多少个微批次?
  3. 对 KK 个数据并行 rank、每个 rank 有各自的微批次的情况,推导逐微批次 token_mean 下每个 token 的权重。证明按全局 token 数归一化只需要在反向传播之前对一个标量做一次 all-reduce,并用 aggregate_loss 实现它。
  4. 改变一个假设:重跑第 14.5 节,让过滤依据每个任务最近三组的平均值,而不是上一组。留出通过率的差距会消失吗?会多花多少个回合?
  5. 实现按与 p^(1−p^)\hat p (1 - \hat p) 成正比的概率采样,代替硬过滤,并与硬过滤比较所花的回合数和留出通过率。
  6. 针对 64k token 的上下文重做第 14.6 节的 GPU 小时估算。哪一项增长了,增长了多少?一个小 4 倍的 KV cache(例如来自更激进的分组查询注意力或 8 位 KV cache)能带来什么?
  7. 设计评审(design review):一位同事提议把微批次大小从 4 条序列提高到 16 条,以提高训练 MFU。这次运行使用带长尾的 32k token 智能体轨迹,并采用逐微批次的损失归一化。列出除 MFU 之外会改变的三件事,以及你会针对每件事要求的测量。