定长训练把每条样本都铺到 max_len;短样本的 pad token 仍占着序列维度,长上下文把这份浪费放大到接近 100%。packing 把多条样本拼进一条 1D token 缓冲,用 cu_seqlens 记录边界,让变长 attention 只在本样本内计算。
代价是把 position ids、loss mask、EOD 和后端支持都变成必须对齐的工程约束。
定长 padding 把每个样本都铺到 max_len。pad token 不是免费占位:在 SBHD 张量里,注意力的形状按 L 算,mask 只是把不该看的位置置 −∞,并没有把 token 从张量里删掉。短样本越多、L 越大,浪费越接近 100%。
packing 把多条样本写进一条 1D token 缓冲,用 cu_seqlens 记录每条样本的起点和终点;变长 FlashAttention 只在这个区间内做 causal attention。THD 格式下 q 的形状是 (total_tokens, heads, head_dim),没有 batch 维。
Megatron-Core 有两条路径:SFTDataset 在一个 sequence_length 缓冲里预打包多个 conversation;VarlenDataset 返回单条变长样本,由调度器跨 DP×CP 装箱,最后把 cu_seqlens / cu_seqlens_padded 装进 PackedSeqParams。
收益取决于长度分布。Krell et al. 在 BERT phase-2 / IPU-M2000 上用 NNLSHP depth 3 报告 1.913×(kernel cycles 估算);HuggingFace 在 8×A100-80GB 上报告 FLAN 2×、OrcaMath 1.4×。接近满长的数据没有多少 padding 可去,收益会缩小。
先看清 pad token 在定长张量里占了多少槽位,再谈 packing 能省什么。
训练 batch 需要矩形张量。设 batch 有 N 条样本,第 i 条长度 li,定长方案把每条样本补到 L = max_len。Megatron 的 GPTDataset 在样本不足 sequence_length 时补 pad token(megatron/core/datasets/gpt_dataset.py:502-507);SC21 的 GPT 实验统一使用 s = 2048(arXiv:2104.04473 §5.1)。
waste = (N·L − Σ li) / (N·L) = 1 − mean(l) / L
长上下文把 L 放大,浪费率趋近 1 − mean(l)/L。取两条样本长度 2,048 和 128,L = 131,072:定长槽位 262,144,真实 token 2,176,padding 259,968,浪费率 99.2% 页面计算。Krell et al. 只把长度 128 的 GLUE-CoLA 称为「less common, but not extreme」;89% 是该数据集在长度 128 下的 padding 占比上限(arXiv:2107.02027 §1)。短指令在 128K 训练数据中的占比取决于具体分布 推断。
pad token 不是免费的占位。FlashAttention 的 key padding mask 把 padding 位置的 score 置 −∞,但没有把 token 从张量里删掉(arXiv:2205.14135 Appendix B.3);在定长张量里,kernel 看到的序列长度仍是 L。代价因此有两层:token 槽位(显存与带宽)和 attention 的 L×L 形状(计算)。短样本越多、L 越大,第二层越贵。
两条常见路线各有硬约束,packing 不是凭空出现。
实现最简单:每条样本补到 L,attention mask 遮住 pad。张量形状固定,kernel 选择和 CUDA graph 都稳定 推断。代价是 pad token 仍进入前向;当 L 从 2K 拉到 128K,短样本的槽位浪费接近 100%。
来源:gpt_dataset.py:502-507;SC21 §5 的 s=2048。
把长度相近的样本放同一批,批次内补到桶的最大长度,减少批内 padding。但在 IPU/静态图后端上,排序破坏随机 shuffle,短桶算力利用率低,改变序列长度还会触发计算图重编译;Krell et al. 的 Table 1 里 SORT 的 packing efficiency 是 99.9%,但 overhead ≫100,realized speed-up ≪1(IPU-M2000,BERT phase-2)。
来源:arXiv:2107.02027 §C 与 Table 1。
两条路都保留了「一条样本一个序列槽位」的布局。packing 改的是布局:多条样本共享一条序列,边界从 padding mask 变成 cu_seqlens。这让 token 槽位按真实长度计,但也把「跨样本不能互相注意」从 mask 的默认行为变成了必须显式实现的约束。
记住三件事:拼、切、遮——拼 token,切边界,遮住不该看的 position 与 loss。
给定样本长度 l1…lN,packing 把 token 依次拼接;cu_seqlens 是前缀和:
cu_seqlens = [0, l1, l1+l2, …, Σ li]
[cu_seqlens[i], cu_seqlens[i+1]);长度 3 和 2 的两条样本得到 [0, 3, 5]。
FlashAttention 的 varlen 接口把 q 描述为 (total_q, nheads, headdim),cu_seqlens_q 是 (batch_size + 1,) 的 int32,用来索引 q;具体签名以所用 flash-attn 版本的 docstring 为准,本页不逐行回源。Megatron 的 PackedSeqParams 把同一组边界拆成 q/kv 和 padded 版本(megatron/core/packed_seq_params.py:19-25)。
Megatron 在 qkv_format='thd' 时把 q/k/v 的 batch 维 squeeze 掉(megatron/core/transformer/attention.py:1590-1594),优先使用 cu_seqlens_q_padded / cu_seqlens_kv_padded,否则退回 cu_seqlens_q/kv(attention.py:1605-1614)。
TE 的 TEDotProductAttention 把 PackedSeqParams 的字段转成 flash-attn 参数(megatron/core/extensions/transformer_engine.py:2617-2622);thd 格式下 causal mask 被映射为 padding_causal(transformer_engine.py:2641-2648),packing 路径的 attention_mask 是 None(megatron/core/datasets/data_schedule.py:915)。这层的语义是:token 只看同一 cu_seqlens 区间内的前驱;跨样本的 score 不参与 softmax。
位置:SFTDataset 每写一个 conversation 就重新 extend(range(len(tokens_list)))(megatron/training/datasets/sft_dataset.py:149-150);VarlenDataset 每条样本 arange(padded_seq_len)(varlen_dataset.py:453);GPTDataset 的 inter_document_masking 也按 document 重置(gpt_dataset.py:386-391)。THD RoPE 用 searchsorted 从 cu_seqlens 还原每条样本的局部位置(megatron/core/models/common/embeddings/rope_utils.py:266-284)。
loss:SFTDataset 把 pad 和 IGNORE_INDEX 位置置 0(sft_dataset.py:192-195);VarlenDataset 把右 padding 和 IGNORE_INDEX 置 0(varlen_dataset.py:454-456)。packing 后每个 microbatch 的有效 token 数不同,Megatron 要求 --calculate-per-token-loss(megatron/training/arguments.py:1575-1583)。
EOD:VarlenDataset 保证样本以 EOD 结尾(varlen_dataset.py:407-410);GPTDataset 可选 eod_mask_loss 把 EOD 的 loss 置 0(gpt_dataset.py:882-883)。EOD 是样本结束符;在 THD 路径里,attention 的边界仍由 cu_seqlens 显式给出,EOD 本身不告诉 kernel 在哪里切。
箱数按 ⌈T/L⌉ 计算,这是下界,不保证可装入;当有样本 > L/2 时实际箱数可能更多。Megatron 的 dp_balanced 按原顺序 first-fit(data_schedule.py:173-231),实际箱数可能高于下界。未计 EOD、每条样本的对齐 padding、drop_last;单条样本超过 max_len 时页面只提示,不做截断。
用一组 4 条样本,把 token、尾垫和注意力 score 的账算平。
用示例 A(4 条样本,L = 2048)把账算平。表 4-1 的所有数都是页面按公式算的示意,不是实测;口径是 token 槽位,不换算 GB。
| 项目 | 定长 padding | packing(理想下界) | 说明 |
|---|---|---|---|
| 真实 token T | 2,944 | 2,944 | 相同 |
| 序列槽位 | 8,192 | 4,096 | 定长 / pack = 2.00 |
| padding token | 5,248 | 1,152 | 定长是样本间 pad;pack 是尾垫 |
| 槽位浪费率 | 64.1% | 28.1% | 占各自槽位 |
| 理想下界箱数 | — | 2 | ⌈T/L⌉ 下界,不保证可装入 |
| 严格下三角 score 元素(不含对角线) | 8,388,608 | 2,269,184 | 27.1% of 定长 |
| 理论节省上限 | — | 4,096 | 50.0% of 定长槽位 |
读法:padding 从 5,248 降到 1,152;按 ⌈T/L⌉ 下界,理论节省上限为 4,096 token,占定长槽位 50.0%。packing 不是零 padding:下界的 2 箱尾垫仍有 1,152(28.1%);当有样本 > L/2 时实际箱数可能更多。attention score 元素(严格下三角,不含对角线)从 8,388,608 降到 2,269,184(27.1%),少 72.9%;这是 causal 计数,不代表 HBM 实测,kernel 的块粒度可能让实际节省更小。token/KV 槽位按同模型同层数同比 2.00:1;attention score 另按 27.1% 计。均未换算 GB。
口径:T = Σ li;fixed = N·L;padding = fixed − T;packs = ⌈T/L⌉(下界,不保证可装入;当有样本 > L/2 时实际箱数可能更多);packed = packs·L;saved = fixed − packed。所有百分比按 值 / 分母 × 100 计算。
N = 4;T = 2048 + 512 + 256 + 128 = 2,944
fixed = 4 × 2048 = 8,192
padding = 8,192 − 2,944 = 5,248
waste = 5,248 / 8,192 × 100 = 64.0625% → 64.1%
packs = ⌈2,944 / 2,048⌉ = 2
packed = 2 × 2,048 = 4,096
pack 内 padding = 4,096 − 2,944 = 1,152
saved = 8,192 − 4,096 = 4,096
理论上限比例 = 4,096 / 8,192 × 100 = 50.0%
N = 5;T = 1024 + 768 + 512 + 256 + 96 = 2,656
fixed = 5 × 2048 = 10,240
padding = 10,240 − 2,656 = 7,584
waste = 7,584 / 10,240 × 100 = 74.0625% → 74.1%
packs = ⌈2,656 / 2,048⌉ = 2
packed = 2 × 2,048 = 4,096
pack 内 padding = 4,096 − 2,656 = 1,440
saved = 10,240 − 4,096 = 6,144
理论上限比例 = 6,144 / 10,240 × 100 = 60.0%
简化:按 ⌈T/L⌉ 计算下界,不保证可装入;当有样本 > L/2 时实际箱数可能更多。未计 EOD、每条样本的对齐 padding、drop_last;Megatron 的 dp_balanced 按原顺序 first-fit(data_schedule.py:173-231),实际可能比下界多一箱。交互对超过 max_len 的单条样本只提示,不做截断。
从 dataset 的 original_seq_len 到 TE 的 padding_causal,路径只有五步。
max_len 个槽位;packing 把两条样本写进同一条 1D 序列,cu_seqlens 的边界 3 把 A 和 B 分开。示意 max_len = 5,未计 EOD、对齐 padding。VarlenDataset 返回单条样本,tokens/labels/loss_mask/position_ids 的长度等于该样本真实长度(补齐到 pad_granularity),并带 original_seq_len / padded_seq_len(varlen_dataset.py:435-467)。DpBalancedScheduler 按原顺序把样本塞进容量为 max_seqlen_per_dp_cp_rank × cp_size 的箱(data_schedule.py:173-231);DefaultDynamicCPScheduler 先按长度降序,再选运行时 CP 组(data_schedule.py:460-474)。_pack_sequences 拼接 tokens/labels/loss_mask/position_ids,算 cu_seqlens 与 cu_seqlens_padded(data_schedule_utils.py:286-322)。PackedSeqParams(qkv_format='thd', cu_seqlens_q/kv, cu_seqlens_*_padded, max_seqlen_q/kv, total_tokens, pad_between_seqs)(data_schedule.py:900-913)。cu_seqlens 选边界、RoPE 按边界重置位置,TE 用 padding_causal 调 flash-attn(attention.py:1590-1614; transformer_engine.py:2641-2657)。
SFTDataset 在一个 sequence_length 缓冲里放多个 conversation,cu_seqlens 按 conversation 切;不足缓冲长度的部分补 pad。它减少的是「一条样本内部」的 padding,不跨 DP rank 装箱。
来源:sft_dataset.py:116-225。
VarlenDataset 每次只返回一条变长样本;调度器把多条样本装进 max_seqlen_per_dp_cp_rank × cp_size 的容量,再跨 rank 重排。它减少的是「整个 global batch 内」的 padding。
来源:varlen_dataset.py:311-325;data_schedule.py:173-231。
只引用有硬件、模型、长度分布条件的数字。
表 6-1(Krell Table 1 节选)给出 packing 算法在 BERT phase-2 上的 kernel-cycle 估算。实验条件是 Wikipedia、序列长度 512、IPU-M2000 16 个加速芯片;realized speed-up 由 kernel cycles 估算,不是端到端 wall-clock。
| 方法 | EFF (%) | packing factor | OH (%) | realized speed-up |
|---|---|---|---|---|
| NONE(baseline) | 50.0 | 1.00 | 0.000 | 1.000 |
| GREEDY | ≈78 | ≈1.6 | ≈4.48 | ≈1.5 |
| SPFHP depth 3 | 89.4 | 1.79 | 4.287 | 1.716 |
| NNLSHP depth 3 | 99.7 | 2.00 | 4.287 | 1.913 |
Table 1 的 baseline NONE 在 Wikipedia 长度 512 上只有 50.0% 的 slot 装真实 token,所以 packing factor 的理论上限是 2.001(Table 1 取整为 2.00);NNLSHP depth 3 的 1.913 已接近这个上限。OH 是掩码与 loss adjustment 等开销,4.287% 不是零成本。
端到端实测看另一组条件:HuggingFace 的 packing-with-FA2 在 8×A100-80GB 上,用 Llama2-7B、Mistral-7B、granite-8B-code 跑 20K 样本一个 epoch;FLAN 吞吐 2×,OrcaMath 1.4×;峰值显存相对 baseline 下降 20% 和 6%。原文把差异归因于 FLAN 的样本更短、长度方差更大,OrcaMath 更长、更均匀,能去掉的 padding 更少。
FlashAttention 论文报告注意力显存效率最多高 20×(A100 40GB,序列到 64K,含 padding mask;arXiv:2205.14135 §4.3 / Figure 3)。这是注意力算子相对 exact attention 的显存,不是 packing 的 A/B;它说明变长 kernel 能避免物化 L×L 矩阵,但不会替你决定 packing 的收益。
收益来自被去掉的 padding 比例:如果所有样本都接近 max_len,packing factor 接近 1,收益接近 0。它还取决于序列长度分布、装箱算法开销、对齐 padding 和后端 kernel。Megatron-Core 源码与 README 中,本页未获取到 packing 的端到端吞吐数字;上面的 1.913× 是 IPU 上的 kernel-cycle 估算,1.4–2× 是 A100 上的外部工程实测,二者不能混用。
这些条件下 packing 会失效或变贵。
把多条样本拼成一条 1D 序列后,如果 attention 仍按普通 causal mask 跑,第 i 条样本的 token 会看到第 i−1 条样本的尾部。Krell et al. 用 block-diagonal mask 阻断跨样本注意力(arXiv:2107.02027 §3.2.2);分隔符 token 不够:论文引用 RoBERTa 的结果,下游 F1 平均下降 0.35%(原文未标明相对或绝对口径,§1)。Megatron 的 THD 路径把 attention_mask 设为 None,靠 cu_seqlens + padding_causal 做隔离(data_schedule.py:915; transformer_engine.py:2641-2648)。
如果后端不支持 varlen,非 TE 的 DotProductAttention 直接 assert packed_seq_params is None(megatron/core/transformer/dot_product_attention.py:161-164)。这不是配置问题,而是实现没有 packed 分支。
SFTDataset 每写一个 conversation 就 pack_positions.extend(range(len(tokens_list)))(sft_dataset.py:149-150);VarlenDataset 每条样本 arange(padded_seq_len)(varlen_dataset.py:453);GPTDataset 的 inter_document_masking 按 document 重置(gpt_dataset.py:386-391)。THD RoPE 用 cu_seqlens 的 searchsorted 还原局部位置(rope_utils.py:266-284)。位置不重置,样本 B 的第一个 token 会从样本 A 的末尾继续数,RoPE 的相对距离全错。
EOD 是样本结束符。VarlenDataset 保证样本以 EOD 结尾(varlen_dataset.py:407-410);GPTDataset 可选 eod_mask_loss 把 EOD 的 loss 置 0(gpt_dataset.py:882-883),它的 reset_position_ids / reset_attention_mask 也会用 EOD 找 document 边界(gpt_dataset.py:891-908)。但 THD packing 路径把边界放在 cu_seqlens 里:如果 EOD 在 pack 内但 cu_seqlens 没切开,attention 仍会跨样本;反过来,cu_seqlens 切开了但 loss mask 没盖住 padding,尾垫会进 loss。
packing 后每个 microbatch 的有效 token 数不同,默认的 per-microbatch loss 平均会让 token 权重随装箱结果变化;Megatron 要求 --calculate-per-token-loss(arguments.py:1575-1583)。
max_seqlen_per_dp_cp_rank 是每个 DP×CP rank 的容量;调度器把 original_seq_len / padded_seq_len 塞进这个上限(data_schedule.py:183-190)。单条 padded 样本超过 static CP 容量会直接 ValueError(data_schedule.py:186-187)。当 context_parallel_size > 1 时,--seq-length 还要满足 seq_length % (2 * context_parallel_size) == 0(arguments.py:1179-1181),因为 CP 切片要求。
max_num_seqs 限制每箱样本数(data_schedule.py:188-190);CUDA graph 路径还会把 packed 序列数固定到 cuda_graph_thd_max_packed_sequences(data_schedule.py:868-878)。
TE 的版本也有限制:cu_seqlens_q_padded 需要 TE ≥ 1.10,pad_between_seqs 需要 ≥ 2.2(transformer_engine.py:2486-2503)。非 TE 的 DotProductAttention 不支持 packed(dot_product_attention.py:161-164)。升级内核之前,packing 只能停在数据层,进不了 attention。
packing 去掉的是样本内 padding,不是边界、EOD、对齐 padding。把这四件事混为一谈,就会把「吞吐提升」误当成无条件结论。