- 人工智能
- 大模型
- 预训练
- 深度学习
- 微调
【免费下载链接】nanoGPT
The simplest, fastest repository for training/finetuning medium-sized GPTs.
导读
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 Face
datasets的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 | ~17GB | 9,035,582,198(约 90 亿) |
val.bin | ~8.5MB | 4,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]) ...三个要点:
- 每次迭代都重新
np.memmap:这是刻意为之,目的是避免 numpy memmap 对象在长训练进程中的内存泄漏(脚本注释引用了 Stack Overflow 上关于 memmap 内存占用的经典讨论); - 随机位置采样:
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]),构成自回归预测对; - 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.pytorchrun --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.
相关推荐
nanoGPT训练实战:从莎士比亚到OpenWebText
nanoGPT训练实战:从莎士比亚到OpenWebText 本文详细介绍了nanoGPT项目在字符级语言模型训练和OpenWebText数据集预处理方面的完整流
人工智能大模型预训练深度学习微调NanoGPT 数据准备完全指南:字符级 Shakespeare 与 BPE OpenWebText 的二进制 Token 流水线
NanoGPT 数据准备完全指南:字符级 Shakespeare 与 BPE OpenWebText 的二进制 Token 流水线 nanoGPT 是 Andr
AI 技能人工智能大模型深度学习scikit-learn 手写数字数据集(digits)完全指南:从 NIST 预处理到 8×8 像素的 10 类分类实战
scikit learn 手写数字数据集(digits)完全指南:从 NIST 预处理到 8×8 像素的 10 类分类实战 导读 :digits 数据集是 sc
人工智能机器学习数据科学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考