remove_padding
Infra · 长上下文训练 · 2021–2026

remove_padding:packing 去掉的是样本内 padding,不是边界

定长训练把每条样本都铺到 max_len;短样本的 pad token 仍占着序列维度,长上下文把这份浪费放大到接近 100%。packing 把多条样本拼进一条 1D token 缓冲,用 cu_seqlens 记录边界,让变长 attention 只在本样本内计算。 代价是把 position ids、loss mask、EOD 和后端支持都变成必须对齐的工程约束。

Krell, Kosec, Perez, Fitzgibbon · Megatron-Core @70f7a26 原文 ↗ Megatron-Core ↗ 博客笔记
TL;DR · 30 秒抓住核心

定长 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 可去,收益会缩小。

89%
GLUE-CoLA,长度 128
padding 占比上限(2107.02027 §1)
50%
Wikipedia 长度 512
8.33B token 中 4.17B 是 padding
1.913×
BERT phase-2 / IPU-M2000 16 芯片
NNLSHP depth 3,kernel cycles 估算
01

定长 padding 让短样本为整个 max_len 付费

先看清 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
定长槽位里没有被真实 token 使用的比例;未计 EOD、对齐 padding 和 drop_last。

长上下文把 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 越大,第二层越贵。

02

定长补齐稳定但浪费;按长度分桶省 pad,却牺牲吞吐与图形稳定性(IPU/静态图后端)

两条常见路线各有硬约束,packing 不是凭空出现。

A

定长 padding / SBHD

shape 稳定

实现最简单:每条样本补到 L,attention mask 遮住 pad。张量形状固定,kernel 选择和 CUDA graph 都稳定 推断。代价是 pad token 仍进入前向;当 L 从 2K 拉到 128K,短样本的槽位浪费接近 100%。

来源:gpt_dataset.py:502-507;SC21 §5 的 s=2048。

B

按长度分桶 / SORT(IPU/静态图后端)

批内少 pad

把长度相近的样本放同一批,批次内补到桶的最大长度,减少批内 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 的默认行为变成了必须显式实现的约束。

03

packing 把多条样本压成一条 THD 序列,cu_seqlens 负责边界

记住三件事:拼、切、遮——拼 token,切边界,遮住不该看的 position 与 loss。

拼:一条 1D token 缓冲,边界写进 cu_seqlens

给定样本长度 l1…lN,packing 把 token 依次拼接;cu_seqlens 是前缀和:

cu_seqlens = [0, l1, l1+l2, …, Σ li]
第 i 条样本占用 [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)。

切:THD 变长 attention 只在边界内算

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。

遮:position ids 与 loss mask 按样本重置

位置: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 在哪里切。

定长总 token
8,192
padding token
5,248
浪费率
64.1%
理想下界箱数
2
pack 总 token
4,096
pack 内 padding
1,152
理论节省上限
4,096
理论上限比例
50.0%

口径与简化

箱数按 ⌈T/L⌉ 计算,这是下界,不保证可装入;当有样本 > L/2 时实际箱数可能更多。Megatron 的 dp_balanced 按原顺序 first-fit(data_schedule.py:173-231),实际箱数可能高于下界。未计 EOD、每条样本的对齐 padding、drop_last;单条样本超过 max_len 时页面只提示,不做截断。

04

token 账本:示例里 64.1% 的定长槽位是 padding,理想下界装箱后仍有 28.1% 的尾垫

用一组 4 条样本,把 token、尾垫和注意力 score 的账算平。

用示例 A(4 条样本,L = 2048)把账算平。表 4-1 的所有数都是页面按公式算的示意,不是实测;口径是 token 槽位,不换算 GB。

项目定长 paddingpacking(理想下界)说明
真实 token T2,9442,944相同
序列槽位8,1924,096定长 / pack = 2.00
padding token5,2481,152定长是样本间 pad;pack 是尾垫
槽位浪费率64.1%28.1%占各自槽位
理想下界箱数—2⌈T/L⌉ 下界,不保证可装入
严格下三角 score 元素(不含对角线)8,388,6082,269,18427.1% of 定长
理论节省上限—4,09650.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。

手算对账:两组输入,逐位核对与上方交互同口径;结果四舍五入到 1 位小数

口径:T = Σ li;fixed = N·L;padding = fixed − T;packs = ⌈T/L⌉(下界,不保证可装入;当有样本 > L/2 时实际箱数可能更多);packed = packs·L;saved = fixed − packed。所有百分比按 值 / 分母 × 100 计算。

组 A:l = [2048, 512, 256, 128],L = 2048

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%

组 B:l = [1024, 768, 512, 256, 96],L = 2048

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 的单条样本只提示,不做截断。

05

数据流:样本先变长,再由调度器装箱,attention 只读边界

从 dataset 的 original_seq_len 到 TE 的 padding_causal,路径只有五步。

定长 padding 与 packing 的槽位对比 上两行是长度 3 和 2 的两条样本,各自补到 5 个槽位;下一行把它们拼成一条 5 token 的序列,边界 3 把两条样本分开,cu_seqlens 为 0、3、5。 定长 padding:每条样本各占 5 个槽位(示意) 样本 A(3 token) A1 A2 A3 pad pad padding ×2 样本 B(2 token) B1 B2 pad pad pad padding ×3 packing:一条 THD 序列,cu_seqlens 划界 A1 A2 A3 B1 B2 边界 3 只有同一边界内互相 attention cu_seqlens = [0, 3, 5] A: [0, 3) B: [3, 5)
定长 padding 每条样本各占 max_len 个槽位;packing 把两条样本写进同一条 1D 序列,cu_seqlens 的边界 3 把 A 和 B 分开。示意 max_len = 5,未计 EOD、对齐 padding。
  1. dataset:VarlenDataset 返回单条样本,tokens/labels/loss_mask/position_ids 的长度等于该样本真实长度(补齐到 pad_granularity),并带 original_seq_len / padded_seq_len(varlen_dataset.py:435-467)。
  2. scheduler:DpBalancedScheduler 按原顺序把样本塞进容量为 max_seqlen_per_dp_cp_rank × cp_size 的箱(data_schedule.py:173-231);DefaultDynamicCPScheduler 先按长度降序,再选运行时 CP 组(data_schedule.py:460-474)。
  3. pack:_pack_sequences 拼接 tokens/labels/loss_mask/position_ids,算 cu_seqlens 与 cu_seqlens_padded(data_schedule_utils.py:286-322)。
  4. metadata:构造 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)。
  5. attention:THD 路径 squeeze batch 维、按 cu_seqlens 选边界、RoPE 按边界重置位置,TE 用 padding_causal 调 flash-attn(attention.py:1590-1614; transformer_engine.py:2641-2657)。
SFT

固定 buffer 内预打包

SFTDataset 在一个 sequence_length 缓冲里放多个 conversation,cu_seqlens 按 conversation 切;不足缓冲长度的部分补 pad。它减少的是「一条样本内部」的 padding,不跨 DP rank 装箱。

来源:sft_dataset.py:116-225。

THD

跨 DP×CP 装箱

VarlenDataset 每次只返回一条变长样本;调度器把多条样本装进 max_seqlen_per_dp_cp_rank × cp_size 的容量,再跨 rank 重排。它减少的是「整个 global batch 内」的 padding。

来源:varlen_dataset.py:311-325;data_schedule.py:173-231。

06

收益取决于长度分布:BERT phase-2 上 1.913×,A100 上 1.4–2×

只引用有硬件、模型、长度分布条件的数字。

表 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 factorOH (%)realized speed-up
NONE(baseline)50.01.000.0001.000
GREEDY≈78≈1.6≈4.48≈1.5
SPFHP depth 389.41.794.2871.716
NNLSHP depth 399.72.004.2871.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 的收益。

条件

不要把 1.4–2× 外推到所有训练

收益来自被去掉的 padding 比例:如果所有样本都接近 max_len,packing factor 接近 1,收益接近 0。它还取决于序列长度分布、装箱算法开销、对齐 padding 和后端 kernel。Megatron-Core 源码与 README 中,本页未获取到 packing 的端到端吞吐数字;上面的 1.913× 是 IPU 上的 kernel-cycle 估算,1.4–2× 是 A100 上的外部工程实测,二者不能混用。

07

边界:跨样本注意力、position ids、EOD、pack 上限与后端支持

这些条件下 packing 会失效或变贵。

跨样本污染:没有边界,packing 会把邻居 token 当上下文

把多条样本拼成一条 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 分支。

position ids:每条样本从 0 重新开始

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 与 loss mask:边界要对齐,loss 要按 token 归一

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)。

pack 上限:单条样本不能超过容量

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)。

实现后端:不是所有 attention 都支持 THD

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。把这四件事混为一谈,就会把「吞吐提升」误当成无条件结论。