同样一块 GPU,为什么别人能训 4 倍数据?答案不在模型,在装箱—— Naive Padding 把 4096 的序列只填了 1000 个 token,剩下的 3000 个位置 GPU 在算空气。

全部数字由浏览器实算(120 条文档、max_length=4096),不硬编码任何百分比。§9.5.5 的 25% / 92% / 98% 只作为对照列出现在下方结论面板里。

🎛️语料与装箱参数

文档长度分布预设
书上 25% / 92% / 98% 三句话的复算基准:文档均值 1000 token,对数正态,形状参数 0.6。
文档数120 条
平均文档长度1,000 tok
§9.5.5 的基准是 1000 token
文档长度方差 σ(对数正态形状参数)0.60
↑σ = 长尾更重。这是书上三处百分比打架的真正根因——见下方「与书上对照」
随机种子#42
max_length(序列长度)
语料总 token
109,776
平均文档长度
915
实际抽样结果
超过 max_length 的文档
0 条
无需切 chunk
理论最少序列数
27
token 总量 / max_length

❌Naive Padding

单文档填充
❌ 不推荐§9.5.2 —— 一个文档一条序列,超长截断,其余补 pad。
序列数
120
平均填充率
22.3%
padding 浪费
381,744
token
被丢弃 token
0
语料保真率
100.0%
token 一条不丢
… 仅显示前 60 / 120 条序列
文档片段padding(GPU 白算的部分)虚线 = chunk 切分处

⚠️Concat Packing

顺序拼接
⚠️ 注意跨文档§9.5.3 —— 按原顺序塞,塞不下就封包;会跨文档串味。
序列数
32
平均填充率
83.8%
padding 浪费
21,296
token
切 chunk 数
0
语料保真率
100.0%
token 一条不丢
文档片段padding(GPU 白算的部分)虚线 = chunk 切分处

✅FFD(First-Fit Decreasing)

首次适配递减
✅ 工业常用§9.5.4 书上的实现其实是这条,不是 Best-Fit。
序列数
28
平均填充率
95.7%
padding 浪费
4,912
token
切 chunk 数
0
语料保真率
100.0%
token 一条不丢
文档片段padding(GPU 白算的部分)虚线 = chunk 切分处

✅BFD(Best-Fit Decreasing)

最佳适配递减
✅ 推荐§9.5.5 的 98% 与「4 倍」结论对应的是这条。
序列数
28
平均填充率
95.7%
padding 浪费
4,912
token
切 chunk 数
0
语料保真率
100.0%
token 一条不丢
文档片段padding(GPU 白算的部分)虚线 = chunk 切分处

📐§9.5.5 结论复现面板

策略书上(1000 tok 基准)本工具实算差值序列数
Naive Padding25%22.3%-2.7pp120
Concat Packing92%83.8%-8.2pp32
Best-Fit (BFD)98%95.7%-2.3pp28
书上说的提升倍数
4×
98% / 25%
本工具实算倍率(BFD / Naive)
4.29×
95.7% ÷ 22.3%
Naive 语料保真率
100.0%
无截断,token 一条不丢
口径回答的问题NaiveBFDBFD / Naive
填充率算力有没有白花22.3%95.7%4.29×
语料保真率数据有没有丢100.0%100.0%1.00×
交付 token真正训到了多少109,776109,7761.00×
消耗容量花了多少序列位置491,520114,6880.23×
「4 倍」是有条件的。把上面的「平均文档长度」或「σ」拖一下,实算倍率会从长文档场景的 ~1.3× 一路跳到 短文档场景的 ~8×。 Naive 的填充率恒等于「平均文档长度 / max_length」,文档越短它越亏,BFD 的收益就越大。 书上那句「同样的算力能训 4 倍数据」默认的是 1000 token 语料。

🧭「填充率」什么时候不能代表数据效率

填充率的定义是 已用 token ÷ 序列总容量,它回答的是 「GPU 算的位置里有多少是真数据」。它已经把截断丢弃算进去了——被截掉的 token 根本没进 bin,不会出现在分子里。所以「有效吞吐 = 未丢弃 token ÷ 消耗容量」这个写法 与填充率是同一个量,本工具不单设一列,避免用两个名字说一件事。

真正会骗人的是另一件事:填充率高,可能是靠丢数据换来的。切到「书籍长文」预设看 Naive 那一列——它的填充率比短文本场景高出一大截, 但这不是因为它更省,而是因为超长文档被截断后每条都填满了整条序列, 代价是超出 max_length 的部分直接消失。 只看填充率会得出「长文档场景下 Naive 更健康」的结论,方向正好相反。

判据:凡是「X 倍 / 多训 N%」这类比值,先代入一个「两方案交付的数据量不同」的输入,看方向是否翻。本工具的解法是把「算力浪费」与「数据丢失」拆成两列,不让任何一个单指标同时代表两件事。

当前参数下四个策略都没有丢弃 token,两个口径暂时重合——把「平均文档长度」拖到 max_length 以上就能看到它们分开。

🔬FFD 与 BFD 到底差多少?(实测,与直觉相反)

FFD 序列数
28
BFD 序列数
28
填充率差
0.00pp
两者序列数相同
本工具在 96 组参数组合(4 种分布 × 4 档文档数 × 6 个种子)上跑过对照: 开启递减预排序时,FFD 与 BFD 的装箱结果只有 1 组不同;关掉递减预排序后, BF 相对 FF 的优势也只有 0.2–0.8 个百分点,甚至在网页短文本分布上出现过负值。 结论:真正的功臣是「递减预排序」这一步,不是 ff 与 bf 的选择—— 书上把两者混用之所以没在实践中出事,正因为它们在有预排序时几乎等价。 取消上面的勾选框可以亲自验证:关掉预排序后两条曲线才会分开。

🐛§9.5 源码里的三个坑(可展开看反例)

内容参考 《AI 数据全书》第 9 章「Tokenization 与序列 Packing」(§9.1-9.3 分词与词表 · §9.5 Packing 三策略 · §9.6 Document Mask 与多模态特化 · §9.7 Megatron 落盘格式 · §9.9 踩坑)。 装箱算法、分词训练、利用率统计全部在浏览器内实算;示意值与文献值已在对应位置标注。
🔖 版本锚点:本页 §9.2 的 SentencePiece 示例对应 sentencepiece 0.2.2(PyPI 最新,2026-07-12 发布,2026-10-27 查;近期序列 0.2.0 → 0.2.1 → 0.2.2)。 ⚠️ 跨 minor 升级时请核对 spm.SentencePieceTrainer 参数名的变化—— 具体哪些参数名变了书上本轮未核,故只作提示不列举。