☰
nanoGPT OpenWebText 数据预处理完整实操:800 万篇网页文档如何变成 17GB、90 亿 token 的 GPT-2 训练集
2026/10/1 1:56:47 网站建设 项目流程

nanoGPT OpenWebText 数据预处理完整实操:800 万篇网页文档如何变成 17GB、90 亿 token 的 GPT-2 训练集

【免费下载链接】nanoGPTThe simplest, fastest repository for training/finetuning medium-sized GPTs.项目地址: https://gitcode.com/GitHub_Trending/na/nanoGPT

800 万篇网页原文,怎么变成喂给 GPT-2 的 17GB train.bin?这篇文章带你走一遍 nanoGPT 里的 OpenWebText 数据预处理链路:一条命令下载语料、GPT-2 BPE 分词出 90 亿 token、uint16 二进制落盘,最后看训练循环如何用 memmap 零拷贝读取这批数据。

语料从哪来:WebText 的开源复刻版

GPT-2 论文用的 WebText 数据集,OpenAI 自己从未放出过原始数据,nanoGPT 靠什么训练?答案是 OpenWebText(OWT)——社区用爬取与过滤流水线复刻出来的 WebText 开源版本,合计 8,013,769 篇文档。

它在 nanoGPT 里身兼两职:其一,从零训练的语料,config/train_gpt2.py 的目标就是在 OWT 上把 124M 参数的 GPT-2 从头训到约 2.85 的验证损失;其二,基线评测的考卷,config/eval_gpt2.py 等配置直接加载 OpenAI 官方权重,在 OWT 的 val 集上报告损失。无论哪条路,data/openwebtext/prepare.py 产出的 train.bin 和 val.bin 都是整条链路的地基。

📦 一条命令生成 train.bin:依赖与磁盘预算

跑 prepare.py 之前你需要准备什么?其实不多:四个库、一条命令、一块够大的磁盘。

  • datasets:Hugging Face 的数据集库,负责把 OWT 下载并缓存到本地,原始缓存约占用 54GB;
  • tiktoken:OpenAI 的 BPE 编码器,脚本使用其中的gpt2编码;
  • numpy:把 token 流以uint16类型写进二进制文件;
  • tqdm:落盘阶段的进度条。

装依赖并启动,一共两行:

pip install numpy tiktoken datasets tqdm python data/openwebtext/prepare.py

从仓库根目录执行,脚本会把train.bin与val.bin写到脚本所在的 data/openwebtext/ 目录。耗时上全程主要受下载带宽拖累,一般得放几个小时让它跑完;真正要盯住的是磁盘——缓存 54GB 加上产出约 17GB,建议预留 70GB 以上空间。

流水线怎么转:文档到二进制的四步走

脚本跑起来之后,里面发生了什么?主线是四个阶段:下载、切分、分词、落盘。

下载:load_dataset 拉取语料

load_dataset("openwebtext")从 Hugging Face 拉取原始数据并缓存,OWT 默认只含一个trainsplit。这一步的并行度由独立变量num_proc_load_dataset(默认 8)控制——脚本注释特意提醒,加载阶段的最优进程数未必和分词阶段一致,因为它还受网络带宽约束,不过通常取大于 1 都比单进程好。

切分:切出一个极小的 val 集

原始数据没有现成的验证集,脚本自己切:先shuffle再train_test_split,取test_size=0.0005、固定种子2357,并把得到的test改名为val。结果 train 侧 8,009,762 篇、val 侧 4,007 篇,刚好凑足 8,013,769。种子写死意味着任何人重跑都会得到同一份划分,结果才谈得上可对账。

分词:GPT-2 BPE 与文档边界标记

为什么用 GPT-2 BPE 分词?因为目标是复刻官方 GPT-2,分词器必须和它一致。每篇文档的编码逻辑如下:

def process(example): ids = enc.encode_ordinary(example['text']) # 忽略特殊 token ids.append(enc.eot_token) # 50256,gpt2 bpe 的文本结束符 return {'ids': ids, 'len': len(ids)}
  • 选encode_ordinary而不是encode:特殊 token 一律忽略,得到纯粹的 BPE id 流;
  • 每篇末尾追加eot_token(50256,即</text>)作为文档之间的边界分隔——源码注释留了个悬念:叫"end of text",也许前置比追加更合理,这是留给读者的消融点;
  • map阶段用num_proc=8并行分词,remove_columns=['text']让原文分词完即丢,省缓存空间。

落盘:memmap 分 1024 批顺序写入

这是全脚本最讲究效率的一段。先累加出每个 split 的 token 总数,用np.memmap按该长度建一个可写映射,然后把数据集切成 1024 个连续分片,逐片拼接写入对应区间,最后 flush 强制落盘:

arr = np.memmap(filename, dtype=np.uint16, mode='w+', shape=(arr_len,)) for batch_idx in tqdm(range(1024)): batch = dset.shard(1024, index=batch_idx, contiguous=True).with_format('numpy') arr_batch = np.concatenate(batch['ids']) arr[idx : idx + len(arr_batch)] = arr_batch idx += len(arr_batch) arr.flush()

memmap 相当于"把书摊开,写到哪一页就动哪一页",90 亿个 token 从不需要同时装进内存;按批批量拼接再写,则是为了减少碎片小写、拉高写盘吞吐。

uint16 为什么够存

因为 GPT-2 BPE 的max_token_value是 50256,小于 2 的 16 次方,所有 token id 都能塞进 2 字节。这一条决定了 train.bin 停在 17GB 量级,而不是 34GB。

🔍 落盘之后:train.bin 格式与验证方式

17GB 和 8.5MB 这两个文件里到底装着什么?

  • 没有头部、没有 padding、没有任何元数据,只有一条连续的uint16token 流:每篇文档的 ids 首尾相接,文档之间以 50256 隔开;
  • train.bin 约 17GB、共 9,035,582,198 个 token;val.bin 约 8.5MB、4,434,897 个 token,两者相差约 2000 倍,与 0.0005 的切分比例严丝合缝——验证集只需要"够稳地估损失"而已;
  • 权威规格见 data/openwebtext/readme.md。

验证也简单粗暴:既然文件就是裸的 uint16 序列,任何语言的 mmap 都能按同样 dtype 直接映射,比如用np.memmap('train.bin', dtype=np.uint16, mode='r')以只读方式打开,立刻得到一个 90 亿元素的数组,无需解析任何格式。这份"无格式"正是训练端"穷人版数据加载器"能写得那么轻的前提。

⚡ 零拷贝数据加载:train.py 怎么读 17GB

17GB 数据不装内存,train.py 的get_batch是怎么把数据送进 GPU 的?核心就几行:

ix = torch.randint(len(data) - block_size, (batch_size,)) x = torch.stack([torch.from_numpy(data[i:i+block_size].astype(np.int64)) for i in ix]) y = torch.stack([torch.from_numpy(data[i+1:i+1+block_size].astype(np.int64)) for i in ix])

三个设计点:

  1. 每次迭代都重建 memmap 对象——刻意为之,规避 numpy memmap 在长训练进程中的内存泄漏,注释里引用了社区的经典讨论;
  2. 随机起点采样:在整条 token 流上随机取batch_size个起点,各切一段block_size=1024的窗口;x是窗口内容,y右移一位,天然构成自回归预测对;
  3. uint16 只负责存储:读出来立刻转成 int64 再进模型。

吞吐可以推算:默认配置batch_size=12、block_size=1024、gradient_accumulation_steps=5*8(DDP 下每卡摊 5),8 卡合计 5 × 8 × 12 × 1024 = 491,520,约 0.5M token/iter;max_iters=600000全程约 300B token,正好贴着 Chinchilla 推荐的算力配比。

🚀 8 卡 DDP 启动与基线评测:跑起来与避坑

数据就绪后怎么启动、要注意什么?

启动:用torchrun --standalone --nproc_per_node=8 train.py config/train_gpt2.py拉起 8 卡 DDP,README 给出的参考是 8×A100 40GB 单节点约 4 天收敛到 ~2.85 损失。

基线:不想训练的话,跑eval_gpt2系列配置(如config/eval_gpt2.py、config/eval_gpt2_medium.py),加载 OpenAI 官方权重直接在 OWT 上评估:gpt2(124M)约 3.11 train / 3.12 val,gpt2-xl(1558M)约 2.56 / 2.54。官方 GPT-2 比 2.85 略差,根源在 WebText 与开源复刻版之间的领域差距。

调参与消融:

  • 磁盘预算:HF 缓存约 54GB 加 train.bin 17GB,至少留 70GB;
  • 进程数:num_proc取 CPU 核数的一半左右;加载与分词两个阶段的 worker 数是独立变量,可分别调,前者还受带宽拖累;
  • 可复现性:种子 2357 固定,重跑必得相同划分与产出;
  • 消融实验:process()里 EOT 前置还是追加,是官方留在代码注释里的实验位,你可以自行尝试并对比损失。

收尾

从 8,013,769 篇文档到 90 亿个 token,OpenWebText 预处理就是下载、切分、GPT-2 BPE 分词、uint16 落盘四步,训练端再靠 memmap 随机窗口零拷贝取数,全程不把数据塞进内存。下一步很简单:把那条命令跑起来,或者照着 data/shakespeare_char/prepare.py 换一个更小的数据集,把整条链路再走一遍。

【免费下载链接】nanoGPTThe simplest, fastest repository for training/finetuning medium-sized GPTs.项目地址: https://gitcode.com/GitHub_Trending/na/nanoGPT

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询