☰
nanoGPT OpenWebText 数据集预处理实战:从 8 万篇文档到 9B token 的 train.bin/val.bin
2026/9/30 2:11:53 网站建设 项目流程
  • 人工智能
  • 大模型
  • 预训练
  • 深度学习
  • 微调

【免费下载链接】nanoGPT

The simplest, fastest repository for training/finetuning medium-sized GPTs.

项目地址:https://gitcode.com/GitHub_Trending/na/nanoGPT
点击查看免费下载

导读

OpenWebText(OWT)是 OpenAI 私有 WebText 数据集的公开复刻版本,是 nanoGPT 复现 GPT-2 的关键训练语料。本文以 data/openwebtext/readme.md 为骨架,结合 data/openwebtext/prepare.py 与 train.py 源码,完整拆解 OpenWebText 的下载、切分、GPT-2 BPE 分词与二进制落盘全流程。读完本文,你将掌握:为什么训练/验证集会产出 17GB 与 8.5MB 的 .bin 文件、9,035,582,198 个训练 token 是如何产生的、.bin 文件的底层格式约定,以及 nanoGPT 训练循环如何通过np.memmap零拷贝读取这批数据。

一、OpenWebText 在 nanoGPT 中的定位

OpenWebText 是 OpenAI 在 GPT-2 论文 中讨论的 WebText 数据集的开源复刻(原始网页快照来自 Reddit 外链过滤后的网页集合)。由于 OpenAI 从未公开 WebText 本身,社区用爬虫与过滤流水线尽力复刻出 OpenWebTextCorpus(约 800 万篇文档),托管于 Hugging Facedatasets生态的openwebtext数据集。

在 nanoGPT 中,OpenWebText 承担两个角色:

  • 从零训练(from scratch):默认配置 config/train_gpt2.py 的目标就是在 OWT 上把 GPT-2 (124M) 从头训练到约 2.85 的验证损失;
  • 基线评测(baseline eval):config/eval_gpt2.py 等一系列配置直接加载 OpenAI 官方 GPT-2 权重,在 OWT 的 val 集上报告损失。

因此,prepare.py产出的train.bin/val.bin是整个复现链路的数据地基。该脚本参考了 HazyResearch flash-attention 训练管线中language_modeling_hf.py的数据处理思路(见 data/openwebtext/prepare.py 注释)。

二、运行环境与依赖

运行prepare.py需要以下依赖(与项目根目录 README.md 中列出的依赖一致):

pip install numpy tiktoken datasets tqdm

其中:

  • datasets:Hugging Face 数据集库,负责下载并缓存 OpenWebText(约 8,013,769 篇文档,缓存占用约 54GB,见脚本注释);
  • tiktoken:OpenAI 的快速 BPE 编码器,prepare.py使用其gpt2编码;
  • numpy:用于把 token 序列以uint16类型写入 memmap 二进制文件;
  • tqdm:落盘阶段的分批进度条。

执行入口极其简单:

python data/openwebtext/prepare.py

在仓库根目录运行后,会在data/openwebtext/目录下生成train.bin与val.bin(脚本通过os.path.join(os.path.dirname(__file__), f'{split}.bin')将文件写到脚本所在目录,见 data/openwebtext/prepare.py)。

三、预处理全流程源码级拆解

prepare.py的主流程在if __name__ == '__main__':块内,共四个阶段:下载加载 → 划分数据集 → 分词 → 二进制落盘。下面逐阶段展开。

3.1 下载与加载:load_dataset

dataset = load_dataset("openwebtext", num_proc=num_proc_load_dataset)
  • 脚本通过 Hugging Facedatasets的load_dataset("openwebtext")拉取并缓存原始语料,默认只包含trainsplit;
  • num_proc_load_dataset(默认等于num_proc = 8)控制加载阶段的多进程数。脚本注释特别提醒:加载阶段的较优进程数可能与分词阶段不同,因为它还受网络带宽影响,但通常大于 1 比等于 1 更好。

3.2 切分:train_test_split

split_dataset = dataset["train"].train_test_split(test_size=0.0005, seed=2357, shuffle=True) split_dataset['val'] = split_dataset.pop('test') # rename the test split to val
  • 由于 OWT 原始数据只有 train 一个 split,脚本用train_test_split自行切出验证集,test_size=0.0005、固定随机种子2357、并先shuffle=True再切分,保证可复现;
  • 随后把test改名为val。切分结果(脚本中保留的注释)为:
DatasetDict({ train: Dataset({ features: ['text'], num_rows: 8009762 }) val: Dataset({ features: ['text'], num_rows: 4007 }) })

这正好对应 readme 中"来自 8,013,769 篇文档"的总量(8,009,762 + 4,007)。

3.3 分词:GPT-2 BPE 编码

enc = tiktoken.get_encoding("gpt2") def process(example): ids = enc.encode_ordinary(example['text']) # encode_ordinary ignores any special tokens ids.append(enc.eot_token) # add the end of text token, e.g. 50256 for gpt2 bpe out = {'ids': ids, 'len': len(ids)} return out tokenized = split_dataset.map( process, remove_columns=['text'], desc="tokenizing the splits", num_proc=num_proc, )

几个值得注意的细节:

  • encode_ordinary而非encode:encode_ordinary忽略所有特殊 token,文档中的原始文本被纯 BPE 编码为 id 序列;
  • 追加 EOT token:每篇文档编码后追加enc.eot_token(GPT-2 BPE 中为 50256,即<|endoftext|>),作为文档边界分隔符。脚本注释还留下了一个耐人寻味的讨论:EOT 或许应该"前置"而非"追加",因为名字叫 eot(end of text)——这也是 nanoGPT 作者留给读者的一个可实验点;
  • map阶段并行:num_proc=8并行分词,remove_columns=['text']在分词后立即丢弃原文,只保留ids与len,节省缓存。

3.4 二进制落盘:np.memmap 顺序写入

for split, dset in tokenized.items(): arr_len = np.sum(dset['len'], dtype=np.uint64) filename = os.path.join(os.path.dirname(__file__), f'{split}.bin') dtype = np.uint16 # (can do since enc.max_token_value == 50256 is < 2**16) arr = np.memmap(filename, dtype=dtype, mode='w+', shape=(arr_len,)) total_batches = 1024 idx = 0 for batch_idx in tqdm(range(total_batches), desc=f'writing {filename}'): batch = dset.shard(num_shards=total_batches, 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()

这是全脚本最讲究效率的一段:

  • dtype = np.uint16的依据:GPT-2 BPE 的max_token_value == 50256 < 2**16,即所有 token id 都落在uint16范围内,因此可以用 2 字节存储一个 token,这是train.bin体量(约 9B × 2 字节 ≈ 18GB 量级)的决定性因素;
  • memmap 免内存拷贝:np.memmap以w+模式创建文件并直接映射,避免一次性把 9B 个 token 全部装入 RAM;
  • 1024 批分片写入:用dset.shard(num_shards=total_batches, index=batch_idx, contiguous=True)把数据集切成 1024 个连续分片,逐批np.concatenate后写进 memmap 对应区间,最后arr.flush()确保落盘。脚本注释说明这是"Batch together samples for faster write",即通过批量拼接减少小写入次数、提高写盘吞吐。

四、产出文件的规格(readme 核心数据)

运行完成后,data/openwebtext/readme.md 给出了权威的产出规格:

文件大小token 数
train.bin~17GB9,035,582,198(约 90 亿)
val.bin~8.5MB4,434,897(约 440 万)
  • 数据源总计8,013,769 篇文档(train 8,009,762 + val 4,007);
  • 两个文件内部都是单条连续的 token id 流(每篇文档的 ids 首尾相接,文档间以 EOT token 50256 分隔),以uint16原始字节存储,没有头部、没有 padding、没有任何元数据;
  • 训练集与验证集规模相差约 2000 倍,这与test_size=0.0005的切分比例一致,符合"验证只需足够估计损失、训练需要海量数据"的常规做法。

五、如何验证与读取 .bin 文件

readme 与脚本末尾都给出了用 numpy 反向读取的验证方式:

# m = np.memmap('train.bin', dtype=np.uint16, mode='r')

由于文件只是裸的 uint16 序列,任何语言都可以用memmap/mmap按同样的 dtype 直接映射读取,无需解析格式。这是 nanoGPT 数据管线"poor man's data loader"能够高效工作的前提。

六、下游消费:train.py 如何吃进 17GB 的 train.bin

nanoGPT 的 train.py 用"穷人版数据加载器"get_batch直接消费这批文件(train.py):

data_dir = os.path.join('data', dataset) # dataset = 'openwebtext' def get_batch(split): if split == 'train': data = np.memmap(os.path.join(data_dir, 'train.bin'), dtype=np.uint16, mode='r') else: data = np.memmap(os.path.join(data_dir, 'val.bin'), dtype=np.uint16, mode='r') 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. 每次迭代都重新np.memmap:这是刻意为之,目的是避免 numpy memmap 对象在长训练进程中的内存泄漏(脚本注释引用了 Stack Overflow 上关于 memmap 内存占用的经典讨论);
  2. 随机位置采样:torch.randint(len(data) - block_size, (batch_size,))在整条 token 流上随机取batch_size个起点,每个起点切出block_size=1024的连续窗口;x是窗口内 token,y是右移一位的目标 token(data[i+1 : i+1+block_size]),构成自回归预测对;
  3. uint16 → int64 转换:memmap 读出后经.astype(np.int64)再进模型,uint16只承担存储压缩角色。

这种设计意味着:不必把 17GB 数据读进内存,训练循环只需按需映射并取窗口,这正是 nanoGPT 能在单节点单机(乃至 8×A100)上高效训练的关键之一。默认配置下每轮迭代的 token 吞吐为gradient_accumulation_steps * ddp_world_size * batch_size * block_size(train.py),在 config/train_gpt2.py 中为12 × 1024 × 5 × 8 = 491,520 ≈ 0.5Mtoken/iter,max_iters=600000总计约 300B token。

七、从数据到模型:复现 GPT-2 的启动方式

数据就绪后,即可按 README.md 的指引启动训练:

torchrun --standalone --nproc_per_node=8 train.py config/train_gpt2.py
  • torchrun --standalone --nproc_per_node=8:单节点 8 卡 PyTorch Distributed Data Parallel(DDP)启动方式;
  • config/train_gpt2.py 的配置要点:batch_size=12、block_size=1024、gradient_accumulation_steps=5*8,配合 8 卡使总 batch ≈ 0.5M token;max_iters=lr_decay_iters=600000(按 Chinchilla 规律对应 300B token);eval_interval=1000、eval_iters=200、log_interval=10、weight_decay=1e-1;
  • README 说明该训练在 8×A100 40GB 节点上约 4 天收敛到 ~2.85 损失;而 OpenAI 官方 GPT-2 直接在 OWT 上评估约 3.11(存在 WebText 与 OpenWebText 的领域差异),微调后可对齐到 ~2.85 区间。

若只想对已产出的 val 集做基线评测,可以运行:

python train.py config/eval_gpt2.py python train.py config/eval_gpt2_medium.py python train.py config/eval_gpt2_large.py python train.py config/eval_gpt2_xl.py

这些配置文件(如 config/eval_gpt2.py)通过eval_only=True、init_from='gpt2'/'gpt2-medium'/'gpt2-large'/'gpt2-xl'加载 OpenAI 权重并在 OWT 上评估,得到 README 基线表格中的 train/val loss(gpt2 124M 约 3.11/3.12,gpt2-xl 1558M 约 2.56/2.54)。

八、实践注意事项

  • 磁盘与缓存预算:Hugging Face 缓存目录需要约 54GB(脚本注释),产出train.bin~17GB、val.bin~8.5MB,请预留足够空间;.bin文件在脚本同目录data/openwebtext/下生成;
  • 进程数调优:num_proc=8是脚本默认值,注释建议取"CPU 核心数的一半左右";num_proc_load_dataset与分词进程数可分开调优,前者受网络带宽影响;
  • 采样随机性:train_test_split使用固定种子2357,任何人运行该脚本都会得到相同的 train/val 划分与相同的 .bin 产出,这是复现结果可对账的前提;
  • EOT 位置的可实验性:脚本注释明确提示 EOT"或许应该前置而不是追加",如果你做消融实验,可以直接修改process()中的ids.append(enc.eot_token)行为后重新生成 .bin;
  • 训练端无需改动:只要train.py中的dataset='openwebtext'指向正确目录,训练循环会自动映射train.bin/val.bin,无需任何额外配置。

九、小结

从 8,013,769 篇原始文档到 17GB 的train.bin(9,035,582,198 token)与 8.5MB 的val.bin(4,434,897 token),OpenWebText 预处理管线通过tiktokenGPT-2 BPE 编码、np.uint16压缩存储与 memmap 顺序写入,在合理的内存预算内完成了语料的"token 化 + 二进制化"。下游train.py的get_batch再以 memmap 随机窗口方式零拷贝取数,形成了一条从语料到 GPU 训练的高效数据链路。理解这条链路,是你在 nanoGPT 上复现 GPT-2、更换数据集或调整采样策略的第一步。

  • 人工智能
  • 大模型
  • 预训练
  • 深度学习
  • 微调

【免费下载链接】nanoGPT

The simplest, fastest repository for training/finetuning medium-sized GPTs.

项目地址:https://gitcode.com/GitHub_Trending/na/nanoGPT
点击查看免费下载

相关推荐

上一篇:终极暗黑2存档编辑器:网页版快速打造完美角色的完整指南
下一篇:终极指南:使用DDrawCompat让经典DirectX游戏在现代Windows上完美运行

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

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

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

立即咨询