TT Lab
开始
学习 学习路径 课程

MiniMind — 亲手从头到尾训练一个小型语言模型

测量填充浪费的计算并打包语料

在 TT Lab 中继续学习

目标

用基准分词器切分预训练语料,量出 MiniMind 式填充浪费了计算的百分之几,并把文档套上 [bos] … [eos] 首尾相接,做成 uint16 数组(打包)作为训练和验证数据。验证泄漏、文档边界以及训练预算,也都用数字来确认。

为什么重要

矩阵乘法不知道哪里是填充。填充位置只是被排除在损失之外,计算照样会做。文档很短而最大长度设得很长时,GPU 忙个不停,学习的令牌却很少。打包消除了这种浪费,代价是同一个窗口里放进多篇文档,让后一篇文档看到前一篇——这是大多数预训练都接受的取舍。 数据准备阶段出的错,要等训练结束才会暴露。验证文档一旦混进训练一侧,验证损失就会说谎;不知道每一步的令牌数,就读不懂损失曲线。所以在训练之前,先用数字确认。

步骤

  1. 用 /opt/mm/ref/tokenizer.json 统计 /opt/mm/data/pretrain.jsonl 中每篇文档的令牌数,以 docs、mean、max、p95 写入 /root/mm/data/lengths.json。
  2. 如果像 MiniMind 的 PretrainDataset 那样,把一篇文档包成 [bos] + 토큰(최대 126) + [eos](占位符为令牌序列,最多 126 个)并填充到长度 128,那么填充有多少个,以 max_len、rows、pad_tokens、pad_fraction、truncated_docs 写入 /root/mm/data/padding.json。
  3. 把训练语料中每篇文档套上 [bos](1) 토큰들 [eos](2)(占位符为令牌序列)并首尾相接,把 uint16 数组保存为 /root/mm/data/train.npy。
  4. 用同样的方法把 /opt/mm/data/pretrain_val.jsonl 保存为 /root/mm/data/val.npy。
  5. 把验证文档中原样出现在训练语料里的数量,以 val_docs、dup_in_train 写入 /root/mm/data/leak.json。
  6. 把 train.npy 按每 128 个令牌不重叠地切开,把每个窗口的平均文档数以及能看到前一篇文档的令牌所占的比例,以 seq_len、windows、docs_per_window、cross_doc_fraction 写入 /root/mm/data/windows.json。
  7. 批次为 16、长度为 128 时,把每一步的令牌数和走一遍(一个轮次)的步数,以 train_tokens、tokens_per_step、steps_per_epoch 写入 /root/mm/data/budget.json。
  8. 在 /root/mm/data/report.md 中写 ## 패딩、## 패킹、## 검증 분리 三节(三个标题为韩文,依次意为“填充”“打包”“验证分离”),并以数字放入第 2 步的 pad_fraction 和第 6 步的 cross_doc_fraction。

参考

文档有多少个令牌

用 /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 步的结果即可。