测量填充浪费的计算并打包语料
目标
用基准分词器切分预训练语料,量出 MiniMind 式填充浪费了计算的百分之几,并把文档套上 [bos] … [eos] 首尾相接,做成 uint16 数组(打包)作为训练和验证数据。验证泄漏、文档边界以及训练预算,也都用数字来确认。
为什么重要
矩阵乘法不知道哪里是填充。填充位置只是被排除在损失之外,计算照样会做。文档很短而最大长度设得很长时,GPU 忙个不停,学习的令牌却很少。打包消除了这种浪费,代价是同一个窗口里放进多篇文档,让后一篇文档看到前一篇——这是大多数预训练都接受的取舍。 数据准备阶段出的错,要等训练结束才会暴露。验证文档一旦混进训练一侧,验证损失就会说谎;不知道每一步的令牌数,就读不懂损失曲线。所以在训练之前,先用数字确认。
步骤
- 用
/opt/mm/ref/tokenizer.json统计/opt/mm/data/pretrain.jsonl中每篇文档的令牌数,以docs、mean、max、p95写入 /root/mm/data/lengths.json。 - 如果像 MiniMind 的
PretrainDataset那样,把一篇文档包成[bos] + 토큰(최대 126) + [eos](占位符为令牌序列,最多 126 个)并填充到长度 128,那么填充有多少个,以max_len、rows、pad_tokens、pad_fraction、truncated_docs写入 /root/mm/data/padding.json。 - 把训练语料中每篇文档套上
[bos](1) 토큰들 [eos](2)(占位符为令牌序列)并首尾相接,把uint16数组保存为 /root/mm/data/train.npy。 - 用同样的方法把
/opt/mm/data/pretrain_val.jsonl保存为 /root/mm/data/val.npy。 - 把验证文档中原样出现在训练语料里的数量,以
val_docs、dup_in_train写入 /root/mm/data/leak.json。 - 把
train.npy按每 128 个令牌不重叠地切开,把每个窗口的平均文档数以及能看到前一篇文档的令牌所占的比例,以seq_len、windows、docs_per_window、cross_doc_fraction写入 /root/mm/data/windows.json。 - 批次为 16、长度为 128 时,把每一步的令牌数和走一遍(一个轮次)的步数,以
train_tokens、tokens_per_step、steps_per_epoch写入 /root/mm/data/budget.json。 - 在 /root/mm/data/report.md 中写
## 패딩、## 패킹、## 검증 분리三节(三个标题为韩文,依次意为“填充”“打包”“验证分离”),并以数字放入第 2 步的 pad_fraction 和第 6 步的 cross_doc_fraction。
参考
- 切分用
tok.encode(글, add_special_tokens=False).ids(占位符为文本),保存用numpy.save(경로, numpy.array(ids, dtype=numpy.uint16))(占位符为文件路径)。 - 常见错误:在首尾相接之前漏掉
[bos]、[eos](会使长度与评分器重新生成的数组不同);用 int64 保存;把窗口的第一个令牌就是[bos]的情况也算作“越过了边界”。 - 原文:MiniMind lm_dataset.py — PretrainDataset · numpy.save
文档有多少个令牌
用 /opt/mm/ref/tokenizer.json 逐篇切分 /opt/mm/data/pretrain.jsonl 的 text,把文档数、平均值、最大值、第 95 百分位数(numpy.percentile(길이, 95),占位符为长度列表),以 docs、mean、max、p95 写入 /root/mm/data/lengths.json。
只看平均值和最大值,就能猜出会产生多少填充。如果把最大长度设成最大值,就什么都不会被截断,但平均长度的文档,其余部分会全部用填充补满。
MiniMind 式填充浪费的份额
如果把一篇文档包成 [bos] + 토큰(126개까지 자름) + [eos](占位符为令牌序列,最多截取 126 个)并填充到长度 128,把填充令牌数、它占全部(文档数×128)的比例、被截断的文档数,以 max_len、rows、pad_tokens、pad_fraction、truncated_docs 写入 /root/mm/data/padding.json。
MiniMind 的 PretrainDataset.__getitem__ 做的正是这个计算——按 max_length - 2 截断,加上 bos 和 eos,再用 pad 补足剩下的部分。填充虽然被排除在损失之外(-100),矩阵乘法却连那些位置也会算。
把训练语料连成一条
编写脚本 /root/mm/data/pack.py(参数:输入 jsonl、要保存的 .npy),把 /opt/mm/data/pretrain.jsonl 中的文档按文件顺序套上 1(bos) 토큰들 2(eos)(占位符为令牌序列)并首尾相接,以 uint16 数组保存到 /root/mm/data/train.npy。
改变文件顺序或打乱顺序,就会与评分器重新生成的数组不同——打乱的事由训练循环在抽取批次时去做。词表是 1024,所以 uint16(最大 65535)就够了,大小是 int64 的四分之一。
验证语料也用同样的方法
用与第 3 步相同的方法打包 /opt/mm/data/pretrain_val.jsonl,保存到 /root/mm/data/val.npy。
验证数据必须用与训练 相同的方式 来做,两个损失才能比较。如果第 3 步的脚本把输入和输出路径作为参数,这一步就只要一行。
验证文档有没有泄漏
把验证文档(pretrain_val.jsonl 的 text)中原样出现在训练语料(pretrain.jsonl)里的数量,以 val_docs、dup_in_train 写入 /root/mm/data/leak.json。
把训练一侧的 text 做成集合(set),每篇只查一次就行。如果不是 0,那篇文档的验证损失测的就是背下来的东西。
一个窗口里有多少篇文档,越过边界的令牌占多少
把 train.npy 按每 128 个令牌不重叠地切开(多出的尾巴丢弃),把每个窗口的平均 [bos] 个数(docs_per_window)以及能看到前一篇文档的令牌的比例(cross_doc_fraction),连同 seq_len、windows 一起写入 /root/mm/data/windows.json。某个令牌“能看到前一篇文档”,指的是在窗口内到该令牌的位置为止(包括它自己)出现过一次以上的文档起始([bos])——但是,如果窗口的第一个令牌就是 [bos],那篇文档是在窗口内第一次开始的,所以不计入。
用 numpy.cumsum(창 == 1, axis=1)(占位符为窗口数组)求出每个位置上截至当时出现的 bos 个数,首位是 bos 的窗口减去 1 即可。因果掩码让每个令牌看到前面所有的令牌,所以只要不另外构造每篇文档的掩码,这些令牌就会看到前一篇文档。
走一遍要多少步
以批次 16、长度 128 训练时,把每一步吃掉的令牌数,以及完整看一遍 train.npy 所需的步数(向上取整),以 train_tokens、tokens_per_step、steps_per_epoch 写入 /root/mm/data/budget.json。
在下一个模块里训练几百步时,凭这些数字就知道语料会被走几遍。同一份数据看了多遍,训练损失就会开始比验证损失下降得更快。
留下准备数据的依据
在 /root/mm/data/report.md 中写 ## 패딩、## 패킹、## 검증 분리 三节(三个标题为韩文,依次意为“填充”“打包”“验证分离”),并以数字(小数或百分比)放入第 2 步的 pad_fraction 和第 6 步的 cross_doc_fraction。
在填充和打包之间选哪一个,以及为此要接受什么代价,各用一行写下来。验证分离一节里,放入第 5 步的结果即可。