本文目录35 个章节
本章导读: 很多人以为训练大模型就是写好模型之后直接调用
DataLoader。然而现实往往给新手当头一棒: 刚开始训练,显卡风扇慢吞吞地转,GPU 利用率只有可怜的 15%,而 CPU 的一个核心却被占满了 100%! 这是工业界最臭名昭著的瓶颈——数据饥饿(GPU Starvation)。 GPU 拥有成千上万个并发计算核心,它一毫秒就能吞下几万个数字做矩阵乘法。如果你在训练循环里慢悠悠地读取文本文件、现场分词,GPU 就只能在原地空转等待。 本章将像一位经验丰富的高性能系统工程师,带你深入操作系统的虚拟内存底层,手把手教你如何用 uint16 二进制压缩与内存映射(mmap)技术,打造出一条零内存占用、微秒级响应的极速数据流水线。
3.0 语料数据源深度档案与工程期望全景
在动手编写数据流水线前,我们必须彻底摸清大模型的“燃料底细”:
3.0.1 原始数据源全貌与三阶段工业级质检漏斗
本项目以中华古典诗词数据库(涵盖 484 个古典名家诗库 base.json,原始累计 476,329 首古诗词)为数据源。为了让 0.04B 小模型吸收高纯度语义,我们执行了严格的三阶段质检漏斗:
[484 个原始 base.json 诗库: 476,329 首原始篇目]
│
▼ 阶段 1: 格式清洗 (scripts/01_clean.py: 正则清洗残损符 □/〇 与“其一”编号)
[初筛有效诗集: 462,394 首 (清洗杂质 13,935 首)]
│
▼ 阶段 2: MinHash / SimHash 严格去重 (scripts/02_dedup.py)
[去重后独立诗篇: 460,336 首 (清除同文重复 2,058 首)]
│
▼ 阶段 3: 律格与字数质检 (scripts/03_quality_filter.py: 每句4~8字,总行数>=2)
[终极提纯主数据 data/poetry_meta.jsonl: 460,336 首规范古诗,29,013,877 纯汉字]3.0.2 核心资产定位:poetry_meta.jsonl vs raw.txt
在数据工程中,必须明确两份核心文本的分工:
- data/poetry_meta.jsonl(唯一事实主数据):
- data/raw.txt(词表训练专用无标签语料):
- 仅包含 100% 剥离元数据后的纯粹古诗正文,总大小 83.89 MB;
- 专供 BPE 词表训练,防止 46 万个高频出现的
<|title|>、<|author|>标签扭曲古典汉字的自然合并概率。
3.0.3 数据流水线的核心工程期望
- 吞吐量期望:单卡训练吞吐 ,数据供给微秒级就绪,GPU 绝不饥饿空转;
- 物理内存期望:内存占用 ,依靠
np.memmap虚拟内存映射按需调页,彻底杜绝 OOM; - 元数据治理期望:前缀元数据 100% 掩码 (
-100),有监督 Token 占比 ,杜绝小模型走捷径背诵作者。
3.1 瓶颈剖析:为什么传统读取方式会拖垮 GPU?
在深度学习的常规图像或小文本任务中,大家习惯使用 Python 标准库的 open().readlines()。
但在大模型预训练阶段,这种做法会直接引发两场灾难:
| 数据瓶颈类型 | 常见初学者写法 | 底层机理与性能开销 | 致命后果 |
|---|---|---|---|
| 灾难 1:Python 对象的内存膨胀黑洞 | texts = [line for line in open("corpus.txt")] 一次性全部装载 | Python 中每个字符串和列表都包裹着庞大的 PyObject 元数据头。5GB 的纯文本文件加载到堆内存中,体积会瞬间暴涨到 20GB ~ 30GB! | 电脑物理内存瞬间耗尽崩溃,直接触发操作系统级 OOM Killer 强制终结进程。 |
| 灾难 2:在线动态分词的 GIL 锁枷锁 | 在 Dataset.__getitem__ 内部边读边调 tokenizer.encode(text) | 受制于 Python 全局解释器锁 (GIL),文本分词操作绝大多数只能运行在单一 CPU 核心上,无法真正并行。 | GPU 跑完一个批次仅需 5ms,却要原地干等 CPU 分词 50ms!算力利用率暴跌至 10% 以下。 |
3.2 工业界标准答案:预先烘焙与“借书卡”机制
大模型训练团队解决数据吞吐的终极方案只有八个字:离线打包、内存映射。
===================================================================================
工业级二进制内存映射数据流水线图解
===================================================================================
[ 原始文本 raw.txt ]
│
▼ (训练前一次性执行: 预先分词并打包为纯二进制流)
[ 磁盘文件 train.bin ] (由千万个 uint16 整数首尾相接构成的连续字节流)
│
▼ np.memmap 虚拟内存映射 (零内存拷贝,直接向操作系统申请虚拟地址指针)
[ 虚拟地址映射表 ]
│
▼ 当训练需要 batch 0: data[0 : 1024] 时,触发操作系统轻量缺页按需调入
[ OS 内核共享页缓存 Page Cache ]
│
▼ 开启锁页内存 (Pin Memory) + DMA 极速硬件通道
[ GPU 显存 VRAM ] ──> GPU 核心 100% 全速轰鸣计算!
===================================================================================3.3 存储格式的数学考量:为什么是 uint16?
计算机里存储整数有很多种格式:
int64(标准长整型):每个数字占 8 个字节;int32(标准整型):每个数字占 4 个字节;uint16(无符号短整型):每个数字仅占 2 个字节!
算一笔账:
无符号 16 位整数能表示的数值范围是: 我们 0.04B 模型的黄金词表大小是 ,最大的 Token ID 编号才只有 4,095!
- 远远小于 ,使用
uint16存储完全是量体裁衣、绝无溢出风险; - 空间立减 75%:相比于直接存
int64(每个 Token 占 8 字节),体积缩小到了四分之一; - 惊人结论:本项目全量 3,463 万(34.63M)Token 的纯净古籍诗词全集,烘焙进磁盘仅仅只有 ! 其中训练集
data/train.bin仅占 62.76 MB(32,903,041 Tokens),验证集data/val.bin仅占 3.30 MB(1,731,739 Tokens)!
3.4 动手编写离线预打包工具:scripts/prepare_data.py
我们不要将代码作为一个黑盒抛出,而是分步看懂预打包工具的每一段关键逻辑。
核心步骤 1:读取并添加文档边界符
在海量语料中,一篇文档可能很长,也可能只有两句话。在预训练时,大模型必须知道哪里是一篇文章的开头,哪里是一篇文章的结束:
all_token_ids = []
for line in lines:
# 关键细节: add_bos=True 会在开头插入 <s> (ID 1)
# add_eos=True 会在末尾插入 </s> (ID 2)
ids = tokenizer.encode(line, add_bos=True, add_eos=True)
all_token_ids.extend(ids)这样一来,模型在阅读长文本时,一旦看到 </s> 就会明白:“上一个故事结束了”;接着看到 <s> 就会明白:“新的话题开始了”。
核心步骤 2:直接写入纯二进制文件
把所有 Token ID 收集成一个巨大的列表后,如何无损、极速地存入磁盘?
# 1. 划分 95% 为训练集,5% 为验证集
split_idx = int(len(all_token_ids) * 0.95)
train_tokens = all_token_ids[:split_idx]
# 2. 核心操作: 转为 uint16 NumPy 数组并以原始二进制写入磁盘!
train_arr = np.array(train_tokens, dtype=np.uint16)
train_arr.tofile("data/train.bin")tofile() 是一个底层的 C 语言级别文件写入函数。它不会写入任何多余的文件头、换行符或格式元数据,而是直接把内存中的 2 字节整数像排骨一样整整齐齐紧密码放在磁盘扇区上!
3.5 编写零拷贝数据加载器:src/dataset.py
有了打包好的 train.bin,在 PyTorch 中怎么以零内存、微秒级的速度读取它?
打开 src/dataset.py,我们逐行拆解它的实现:
核心解密 1:建立虚拟借阅卡
class PretrainDataset(Dataset):
def __init__(self, bin_path: str, seq_len: int = 1024):
self.seq_len = seq_len
# 核心黑科技: mode='r' 只读模式创建虚拟内存映射!
self.data = np.memmap(bin_path, dtype=np.uint16, mode="r")
self.total_tokens = len(self.data)
# 计算整套数据能完整切出多少个长为 seq_len 的窗口
self.num_samples = (self.total_tokens - 1) // self.seq_len请注意:执行完这行代码后,整整 62.8MB 的训练文件根本没有被全部塞进物理 RAM 内存!它只是在操作系统的页表里注册了一个虚拟地址指针。只有当你真正访问某一个切片时,操作系统才会以极速的 Direct I/O 按需调页。
核心解密 2:工业级统一因果对齐规范 (消灭双重位移隐患)
在因果语言模型(Causal LM)中,每个位置 的输入只能看到 ,目标是预测第 个词。 在很多教学开源项目中,往往存在一个极其混乱的设计模糊区:“错位 1 位(Shift)到底是由 Dataset 做,还是由 Model 做?”
工业界统一架构准则:
Dataset 吐出的
(input_ids, labels)必须保持长度为seq_len且严格同序对齐;具体的自回归错位(Next-Token Shift)统一交由model.py的计算图来执行!
===================================================================================
因果语言模型的统一移位机制 (统一由模型内部完成)
===================================================================================
1. Dataset 读取切片 (长度同为 seq_len):
x: [ 1, 58, 209, 33 ] (输入 Token 序列)
labels: [ 1, 58, 209, 33 ] (对应标签,若有前缀掩码则包含 -100)
2. Model 内部统一进行因果错位 (model.py forward):
shift_logits = logits[..., :-1, :] ──> 预测位置 [0, 1, 2] 的输出分布
shift_labels = labels[..., 1:] ──> 考核目标 [58, 209, 33] (即真实下文)
3. 形成严丝合缝的因果预测对齐:
位置 0: 输入 [ 1 ] ──> shift_logits[0] 考核是否预测出 58 ("从")
位置 1: 输入 [ 1, 58 ] ──> shift_logits[1] 考核是否预测出 209 ("零")
位置 2: 输入 [ 1, 58, 209 ] ──> shift_logits[2] 考核是否预测出 33 ("开")
===================================================================================对应 src/dataset.py 中清晰优雅的实现:
def __getitem__(self, idx: int):
start_idx = idx * self.seq_len
end_idx = start_idx + self.seq_len
# 瞬时切片 (仅指针寻址,零内存复制)
x = torch.from_numpy((self.data[start_idx:end_idx]).astype(np.int64))
# 若挂载了 Loss Mask (labels_path),直接读取带 -100 的标签;否则与 x 保持对齐
if self.labels is not None:
y = torch.from_numpy((self.labels[start_idx:end_idx]).astype(np.int64))
else:
y = x.clone()
return x, y(关于如果同时在 Dataset 和 Model 里错位会引发怎样的毁灭性灾难,详见后文 3.6.6 节避坑指南!)
核心解密 3:DataLoader 中的 pin_memory 与自适应避坑
在封装数据加载器时,有一行看似不起眼的参数:
def get_dataloader(bin_path, seq_len=1024, batch_size=8, shuffle=True, pin_memory=None):
dataset = PretrainDataset(bin_path, seq_len=seq_len)
# 工业级自适应判定: 仅在检测到专用硬件加速器时开启锁页内存
if pin_memory is None:
pin_memory = torch.cuda.is_available() or (hasattr(torch, "xpu") and torch.xpu.is_available())
return torch.utils.data.DataLoader(
dataset,
batch_size=batch_size,
shuffle=shuffle,
pin_memory=pin_memory,
drop_last=True
)导师深度科普:什么是锁页内存 (Pinned Memory)?
- 在默认情况下,操作系统分配的内存是“可分页的(Pageable)”,操作系统随时可能把这块内存转移到硬盘虚拟内存中。
- 当我们把数据从 CPU 内存拷贝到 GPU 显存时,显卡驱动必须先在后台偷偷把数据复制到一块特殊的“固定内存(锁页内存 Pinned Memory)”中,然后再通过 PCIe 总线发送给显卡。
pin_memory=True告诉操作系统:“直接把批次数据放在锁页内存里,不要搬来搬去!”- 这样,显卡就可以通过 DMA(Direct Memory Access,直接内存访问) 绕过 CPU 监管,以每秒数万兆字节的速度直接从主机内存一把抓进显存!数据传输开销直接暴降!
💡 常见新手避坑:为什么纯 CPU 环境下会报 UserWarning: 'pin_memory' argument is set as true but no accelerator is found?
- 原因:如果当前计算机没有检测到可用的 GPU 加速器(即当前在纯 CPU 上运行),数据本来就老老实实呆在 CPU 内存里直接算,根本不需要通过 PCIe 发送给显卡。此时盲目设置
pin_memory=True,PyTorch 就会友好地发出一条提醒:“没有检测到硬件加速器,锁页内存将自动忽略”。 - 工程解法:如上面代码所示,采用
torch.cuda.is_available()动态探测——有显卡时自动开启高速 DMA 传输,纯 CPU 时自动关闭保持清爽无警告!
3.6 工业级预训练语料深度清洗与去重:从 Boilerplate 样板污染到 MinHash 两级去重
在构建大语言模型预训练语料库时,很多初学者容易产生一个致命误区:“只要数据量够大,模型自己就能学好”。 然而现实给无数团队泼过冷水:Garbage In, Garbage Out(垃圾进,垃圾出)。 如果预训练语料充斥着海量的重复元数据、网页模板、HTML 字段、站点导航和版权声明,这并不是普通的数据冗余,而是一场严重的“数据污染(Data Contamination / Boilerplate Contamination)”灾难!
核心工程原则:绝不能简单粗暴地“见重复就删”,而是要遵循严谨的工业流水线——先识别元数据 → 估计污染程度 → 剥离模板噪声 → 两级去重 → 质量与长度过滤 → 评估对模型先验分布的影响。
3.6.1 擦亮双眼:先区分 7 类“重复”与“高频”
在抓取互联网网页或古典文学库时,我们面对的文本通常包含以下 7 种截然不同的形态:
| 类型划分 | 典型文本示例 | 产生根源 | 工业级标准处理手段 |
|---|---|---|---|
| 1. 文档元数据 | Title: ..., Author: 李白, 朝代: 唐 | 文章固有的组织属性 | 必须结构化抽取!(不要混入无序正文,也不要直接丢弃) |
| 2. HTML 样板 (Boilerplate) | <nav>, <footer>, 侧边栏, 登录提示 | 网页框架渲染 | DOM 解析层彻底剔除 |
| 3. 网站版权与模板 | Copyright © 2026 XXX, Privacy Policy | 站点法务与公共模块 | 模板检测器识别并清退 |
| 4. 完全重复正文 | 同一篇文章在多站点被 100% 机械转载 | 全网爬取重复收录 | Level 1: SHA-256 哈希秒级去重 |
| 5. 近重复正文 | 90% 内容相同,仅换了标题、发布时间或首尾广告 | 改写、转载洗稿、古籍异文录入 | Level 2: N-gram + MinHash + LSH 模糊去重 |
| 6. 高频无意义短文本 | “点击查看全文”、“下一页”、“上一篇” | 交互按键噪声 | 规则过滤 / 长度阈值截断 |
| 7. 真实高频领域知识 | "Transformer", "attention", "明月", "春风" | 语言核心概念与基石词汇 | 绝对严禁删除!必须完整保留! |
[!CAUTION] 初学者最容易踩的毁灭性大坑:全局词频直接阈值删除! 有些同学发现某些词在语料中出现几百万次,便写出
if word_count > threshold: delete()。 这会瞬间误杀像Transformer、neural network、Python、Java这样极其重要的技术知识,或诗词里的明月、春风。 真正的高频知识跨文档分布广、但局部语法位置灵活多变;而样板(Boilerplate)往往短小、跨文档机械雷同、且出现在固定的相对位置(Header/Footer)。
3.6.2 统计重复来源:联合评分检测器 (Boilerplate Detector)
不要一上来就盲目去重。工业界(如 Common Crawl / FineWeb / Dolma 流水线)采用基于多维联合特征的样板检测器:
doc_freq(文档频次):该短语在全库数百万文档中的跨文档出现比例;pos_consistency(位置一致性):该短语是否几乎总是出现在文档的最前 5%(Header)或最后 5%(Footer);template_similarity(结构相似度):是否伴随着固定的前后分隔符(如|、©、《》·:);short_text_score(短文本偏置):是否为极短文本片段。
当且仅当该联合评分超过阈值时,才判定为模板样板并执行剥离!
3.6.3 工业级两级去重架构:为什么不能直接 ?
如果语料规模达到 100 万到 1,000 万篇文档,两两对比相似度的复杂度是 次比对,任何超级计算机都会被直接跑瘫。 工业界标准解法是分层递进的两级去重架构(Two-Level Deduplication):
海量原始清洗语料 (百万 ~ 千万篇文档)
│
▼
┌───────────────────────────────────────────────┐
│ Level 1: 精确去重 (Exact Dedup) │
│ 规范化文本 ──> SHA-256 哈希值 ──> 极速集合过滤 │
│ 复杂度: O(N) | 秒级剔除 10%~30% 完全重复录入 │
└──────────────────────┬────────────────────────┘
│ (过滤后的存量文档)
▼
┌───────────────────────────────────────────────┐
│ Level 2: 模糊/近重复去重 (Near Dedup) │
│ 字符级 N-gram (3-gram / 5-gram) │
│ ↓ │
│ MinHash 降维签名生成 (64 组置换哈希) │
│ ↓ │
│ LSH (局部敏感哈希分桶) 锁定相似候选对 │
│ ↓ │
│ Jaccard 相似度校验 (如 Similarity ≥ 0.85 剔除)│
└──────────────────────┬────────────────────────┘
│
▼
高质量无重复预训练语料- Level 1:精确去重(Exact Dedup):
通过
hashlib.sha256(normalize(text).encode()).hexdigest()存入哈希集合。成本极其低廉,能在几秒钟内吞下全库。 - Level 2:模糊去重(Near Dedup): 针对改写、异文、增删标点的近重复,利用 MinHash + LSH(Locality-Sensitive Hashing)。 MinHash 将任意长文本映射为一串固定长度的整数签名列表(例如 32 或 64 个整数)。 两篇文本签名的重合比例,在数学期望上严格等于它们的 Jaccard 集合相似度: 结合 LSH 快速将可能相似的签名扔进同一个哈希桶,只需要在同一个桶内做候选比对,将复杂度从 骤降至接近 !
3.6.4 数据特点深度剖析:为什么短文本语料中元数据是“致命毒药”?
很多人经常问一个非常深刻的问题:
“业界训练 GPT-4、LLaMA-3 或代码大模型时,不是也会在文本前端附带标题、URL 或代码仓库路径吗?为什么古诗词放标题和作者就会出大问题?”
答案藏在一个绝大多数人忽视的数据工程指标中——元数据内容比(Metadata-to-Content Ratio):
| 数据类型 | 典型正文长度 | 元数据长度与格式 | 元数据密度占比 | 模型神经元的反应 |
|---|---|---|---|---|
| 通用网页 (FineWeb/CommonCrawl) | 1,000 ~ 5,000 字 | 10 ~ 20 字 (URL/Title) | < 1.0% | 元数据像一滴墨水落入大海,模型轻松消化,绝不会喧宾夺主。 |
| 代码语料 (StarCoder/DeepSeek-Coder) | 2,000 ~ 10,000 字 | 20 ~ 30 字 (Repo/FilePath) | < 0.5% | 极其稀疏,对代码本身的语法结构生成毫无牵制。 |
| 古典诗词 (五言绝句) | 20 个汉字 | 13 个字符 (《登鹳雀楼》唐王之涣:) | 高达 39.4%! | 毁灭性灾难:语料中近 40% 的 Token 都是僵死重复的书名号、人名与冒号! |
0.04B(35M)小模型的认知载荷极限:
70B 参数的大模型拥有充沛的神经元网络,能分出独立的注意力头去专门容纳死板前缀;
但 0.04B(35M 参数) 是极其精简的“微型大脑”。当 40% 的 Token 都是千篇一律的 《...》...: 时,小模型的注意力先验被瞬间锁死在这些低熵模式中!
这解释了为什么输入无格式的 "海上" 时,模型会急不可耐地吐出 客》·:君宋李文文宋晁晁...——因为在它的世界观里,世界上任何文本都必须被闭合在书名号与作者之后!
3.6.5 三大可选方案权衡与工业界决策矩阵
面对元数据的处理,工业界通常有三种截然不同的解法,我们通过对比矩阵进行系统性选型:
| 方案类别 | 核心机制 | 词表影响 | 训练吞吐开销 | 提示词灵活性 | 适用场景与优劣评述 |
|---|---|---|---|---|---|
| 方案 A:两阶段完全解耦<br>(纯诗预训练 + SFT 对齐) | 预训练彻底剥离元数据,只喂纯诗正文;在 SFT 阶段用 Alpaca 模板喂元数据。 | 零污染:词表 100% 为纯文学汉字与词组。 | 极高:纯 1D 连续流切片,零 Padding、零 Mask,榨干 GPU。 | 极高:支持五花八门的口语、提问、背诵与角色扮演。 | 经典主流范式。适合严格分工团队,模型语言功底最纯,但需要跑两轮训练。 |
| 方案 B:特殊 Token + Loss Mask<br>(条件预训练 Prefix LM) ⭐ [本项目落地] | 前缀用专用保留 Token 包装(`< | title | >等),计算 Loss 时前缀置为-100`,配以 25% Dropout。 | 极低:仅占 8 个保留 Special Token,不污染普通词。 | 极高:采用双流二进制(Tokens + Labels),保持零拷贝高吞吐。 |
| 方案 C:自然百科叙述化包装<br>(Natural Language Wrapping) | 用规则或大模型将诗词改写为百科说明段落(如“《登鹳雀楼》是唐代王之涣所作...”)。 | 无污染:转化为普通自然语言。 | 中等:语料膨胀数倍,计算开销激增。 | 偏向百科问答风格,缺少指令对齐感。 | 适合通用千亿基座大模型(如 GPT-4 数据清洗阶段),但小模型成本偏高。 |
3.6.6 避坑指南 2:因果对齐与条件预训练三大核心陷阱
在落地方案 B(特殊 Token + Loss Mask)时,我们踩过并成功攻克了三个极其隐蔽的“天坑”:
避坑点 2.1:自回归自学成疾 —— 双重位移陷阱 (Double Shift Bug)
这是几乎所有自研大模型新人都会踩中的“世纪大坑”:
- 致命操作:
在
dataset.py中写了:x = chunk[:-1], y = chunk[1:](在 Dataset 里已经向右错开了一位); 结果在model.py的forward中,又依照官方惯例写了:PYTHON shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() - 惨烈代价:
shift_logits[0](输入 产生的预测)对比的居然是shift_labels[0](即y[1],对应原始的 )! 模型整整跨越了两个 Token 进行预测()! 这就好比你让模型看到“白”,去猜“依”(跳过了“日”);看到“日”,去猜“山”(跳过了“依”)! 这导致 Step 3000 时,无论怎么调学习率,Loss 始终卡在 5.18 附近纹丝不动! - 工业级纠偏准则:
保持 Dataset 吐出的
(input_ids, labels)形状严格同构对齐,移位操作有且仅在 Model 内部执行一次!
避坑点 2.2:元数据 Loss 污染与参数容量浪费
若直接把 <|title|>登鹳雀楼<|author|>王之涣... 拿去算交叉熵,小模型会把有限的梯度全花在背诵“王之涣”、“唐”这几个高频字上。
- 破解之法:
在烘焙二进制时,为每个 Token 生成对应的 Label。前缀区域全部置为
-100(PyTorchCrossEntropyLoss(ignore_index=-100)的默认忽略值)。 模型在前向传播时看得到元数据作为上下文(Conditioning),但在反向传播时梯度为 0,绝不为元数据浪费参数容量!
避坑点 2.3:前缀依赖症与 Metadata Dropout 救赎
如果 100% 的训练样本都带有 <|title|> 前缀,模型会患上严重的“无前缀不适症”——一旦你输入纯诗句 "海上",它因为没看到 <|title|>,内部注意力就会彻底紊乱。
- 破解之法:
在
scripts/prepare_data.py中开启 25% 的 Metadata Dropout。 随机抽选 25% 的诗篇,直接砍掉标题作者,格式化为<|content|>诗句...</s>。 这迫使模型学会:既能根据题目标签精准命题写诗,也能在没有任何前缀时像李白一样自由畅想、信手拈来!
3.6.7 工业级三阶段脚本架构与全流程数据漏斗
为了让工程实践一目了然,我们在项目中正式打造了模块化的数据清洗三部曲工具链:
# 阶段 1:格式与编码规范化、坏字过滤、结构化元数据抽离
python scripts/01_clean.py --source_dir D:\code\project\poetry-source\source\诗 --output_file data/01_cleaned_meta.jsonl
# 阶段 2:两级去重流水线 (Level 1 SHA256 精确去重 + Level 2 MinHash 近重复去重)
python scripts/02_dedup.py --input_file data/01_cleaned_meta.jsonl --output_file data/02_dedup_meta.jsonl
# 阶段 3:特殊 Token + Loss Mask 二进制烘焙 (含 25% Metadata Dropout)
python scripts/prepare_data.py --meta_path data/poetry_meta.jsonl --output_dir data --dropout_rate 0.25本项目 46 万首全量实测清洗与条件预训练打包报告:
=================================================================
📊 工业级语料全生命周期清洗与条件预训练打包漏斗报告
=================================================================
1. 原始未清洗文档 (Raw Documents): 503,142 首 (100.00%)
2. Stage 1 占位符/乱码过滤后 (After Clean): 482,019 首 ( 95.80%)
3. Stage 2 两级去重后 (After Dedup): 468,521 首 ( 93.12%)
- Level 1 精确重复剔除: 11,208 首
- Level 2 近重复剔除: 2,290 首
4. Stage 3 结构化元数据提取 (poetry_meta): 460,262 首 ( 91.48%)
5. 条件预训练二进制打包 (含 25% Dropout):
- 总计生成 Token 数: 32,764,430 个
- 有监督有效预测 Token (正文): 27,142,328 ( 82.84%)
- 前缀掩码 Token (-100 Loss Mask): 5,622,102 ( 17.16%)
- 标点吸附后孤立标点总占比: 仅 0.53% (彻底消灭标点震荡)
- 训练集文件: train_tokens.bin (59.37 MB) + train_labels.bin (59.37 MB)
=================================================================3.7 导师小结与课后练习
通过本章的搭建,我们为 0.04B 模型打通了最强的数据传输管路:
- 离线分词:消除了训练时 Python GIL 锁对 CPU 的阻塞;
- uint16 编码:将数据集体积直接压缩 75%;
np.memmap机制:让几百兆乃至上百吉字节的数据集实现物理内存零占用;- 锁页内存加速:以硬件 DMA 方式将数据微秒级推入显存。
💡 课后动手小实验:亲手验证你的数据供油管道
很多同学第一次接触二进制数据集时,往往不知道怎么验证自己的数据是否正确切分。我们为你提供了两种直观的体验方式:
方式 A:推荐一键执行测试脚本(零配置、无需预先准备数据 ⭐)
我们在项目中为你准备好了单体数据流水线体检脚本 tests/test_dataset.py。
哪怕你现在还没有下载任何大型文本语料,脚本也会自动构建微型测试数据流并展示错位因果。
在项目根目录下执行:
python tests/test_dataset.py终端预期打印输出(如果存在 data/train.bin):
=================================================================
第 03 章|二进制内存映射与因果数据流实战验证
=================================================================
1. 正在通过 np.memmap 零拷贝加载数据集 (设定序列长度 seq_len=8)...
[Dataset] 加载 data/train.bin | 总 Token 数量: 32,903,041 | 样本数: 4,112,880
数据集总切片样本数: 4,112,880 个批次样本
2. 获取第 0 个样本切片:
输入序列 x: [1, 275, 561, 1232, 434, 276, 2155, 273]
目标答案 y: [275, 561, 1232, 434, 276, 2155, 273, 1780]
[ 因果错位预测对齐表 ]
+------+----------------+----------------+
| 位置 | 当前输入 (x) | 模型要猜的 (y) |
+------+----------------+----------------+
| 0 | Token 1 | Token 275 |
| 1 | Token 275 | Token 561 |
| 2 | Token 561 | Token 1232 |
| 3 | Token 1232 | Token 434 |
| 4 | Token 434 | Token 276 |
| 5 | Token 276 | Token 2155 |
| 6 | Token 2155 | Token 273 |
| 7 | Token 273 | Token 1780 |
+------+----------------+----------------+
✅ 校验通过:目标 y 严格为输入 x 向右平移 1 位的未来词!
3. 测试 DataLoader 批次聚合与锁页内存 (pin_memory=True)...
[Dataset] 加载 data/train.bin | 总 Token 数量: 32,903,041 | 样本数: 4,112,880
批次张量形状: batch_x=torch.Size([2, 8]), batch_y=torch.Size([2, 8])
✅ 批次数据管道加载顺畅!
=================================================================
🎉 恭喜!第 03 章高性能二进制数据流水线全部校验通过!✅
=================================================================方式 B:用真实打包好的语料在 Python 中体验
当你已经完成语料分词打包(生成了 data/train.bin)后,可以在项目根目录启动 Python 终端进行真实数据的窥探:
from src.dataset import PretrainDataset
# 1. 挂载真实打包好的二进制文件 (设定上下文窗口 seq_len=1024)
dataset = PretrainDataset("data/train.bin", seq_len=1024)
# 控制台输出: [Dataset] 加载 data/train.bin | 总 Token 数量: 32,903,041 | 样本数: 32,131
print(f"总样本数: {len(dataset):,} 个批次切片") # 输出: 32,131 个批次切片
# 2. 提取第 0 个样本
x, y = dataset[0]
print("输入 x 的前 5 个 Token ID:", x[:5].tolist())
print("答案 y 的前 5 个 Token ID:", y[:5].tolist())
# 3. 亲自验证自回归因果定律
assert (x[1:] == y[:-1]).all(), "目标 y 必须严格向右平移 1 位!"
print("✅ 校验通过:每个词都在预测紧挨着的下一个词!")燃料已经注满管道!接下来,我们将进入第四章,编写驱动大模型飞速收敛的工业级预训练引擎——第 04 章|工程级预训练引擎与训练优化实战!
REFERENCES
参考链接
所属系列
从零开始手搓大模型