如何把 800 万篇网页炼成 17GB 的 train.binnanoGPT OpenWebText 数据预处理全解【免费下载链接】nanoGPTThe simplest, fastest repository for training/finetuning medium-sized GPTs.项目地址: https://gitcode.com/GitHub_Trending/na/nanoGPT想在 nanoGPT 上复现 GPT-2第一道关就是 nanoGPT OpenWebText 数据预处理在仓库根目录执行python data/openwebtext/prepare.py一次脚本目录里会多出两个产物——约 17GB 的train.bin与约 8.5MB 的val.bin。没有日志报告只有两个二进制文件。这篇文章不按从下载到落盘的正序讲而是先把产物的验收标准给你再回头拆出它们的生产线。从一条命令和两个 .bin 文件先问三个问题python data/openwebtext/prepare.py入口就这一行背后是下载、切分、分词、落盘四个阶段。但读代码之前建议先钉死三个验收问题否则十几 GB 的文件只是字节堆90 亿 token 从哪来几百万篇网页文档每篇不过几百 token怎么凑出 90 亿.bin 里到底存了什么有头部吗有分隔符吗有元数据吗训练时怎么读文件远大于内存训练脚本如何在不装进内存的前提下取数数据从哪来、变成什么、到哪去——这三个问题的答案就是产物与消费者之间的契约。先看契约。train.bin 里到底存了什么一份无头部的数据契约把train.bin和val.bin想象成一卷没有目录的磁带一条连续的 token id 流无头部、无 padding、无任何元数据。完整契约只有四条元素类型每个 token 占 2 字节即uint16。为什么不用 4 字节因为 GPT-2 BPE字节对编码把高频字节子串合并成更大单元的分词方式词表最大值 50256 落在 16 位之内省一半体积。文档分隔每篇文档的 token 序列末尾追加一个 EOTend of text文末token取值 50256。所有文档首尾相接新文章开始了的线索只有这一个数字。总量两个文件来自共 8,013,769 篇文档train.bin装 9,035,582,198 个 token约 90 亿val.bin装 4,434,897 个约 440 万权威数字见 data/openwebtext/readme.md。切分来源OpenWebTextOWTOpenAI 私有 WebText 的公开复刻版原始数据只有 train 一个 split脚本按 0.0005 的比例自行切出验证集于是训练侧 8,009,762 篇、验证侧 4,007 篇差约 2000 倍——验证集只用来估计损失不需要大。手里有了契约任何一份 .bin 都能被 numpy 验收dtype 对不上、流里找不到分隔 token就是残次品。现在的问题是800 万篇文档是怎么被炼进这卷磁带的生产线prepare.py 的备料、加工、封装三段式data/openwebtext/prepare.py 全文不足 80 行压缩成工厂隐喻恰好三段备料下载与切分、加工分词、封装写盘。备料拉取原文切出 train 与 valload_dataset(openwebtext)从 Hugging Face 拉取全部文档并缓存在本地缓存目录。原始数据没有验证集脚本自己切split_dataset dataset[train].train_test_split(test_size0.0005, seed2357, shuffleTrue) split_dataset[val] split_dataset.pop(test)这里最容易被忽略的一点先 shuffle 再切分。若直接按顺序切同一站点、同一时期的文档会整段聚在一侧验证集分布就偏了固定种子则保证任何两次运行切出的结果严格一致是对账的前提。加工GPT-2 BPE 编码原文每篇文档只经过一个process函数def process(example): ids enc.encode_ordinary(example[text]) ids.append(enc.eot_token) return {ids: ids, len: len(ids)}细节在encode_ordinary而不是encode前者忽略所有特殊 token不掺入任何控制符号id 流是纯 BPE 内容文档边界完全由显式追加的那一个 EOT 负责。这样文章结束成了干净的内容信号而不是编码器行为的副产品。外层的map还会多进程并行分词并在remove_columns里立刻删掉原文列——内存峰值里只留 id 序列不留原始文本。封装memmap 分片批量写盘这是全脚本最有手感的部分arr_len np.sum(dset[len], dtypenp.uint64) arr np.memmap(filename, dtypenp.uint16, modew, shape(arr_len,)) idx 0 for batch_idx in tqdm(range(1024), descfwriting {filename}): batch dset.shard(num_shards1024, indexbatch_idx, contiguousTrue).with_format(numpy) arr_batch np.concatenate(batch[ids]) arr[idx : idx len(arr_batch)] arr_batch idx len(arr_batch) arr.flush()memmap内存映射相当于给磁盘文件挂了一层索引文件已打开但内容不整体进 RAM你摸到哪一片才映射哪一片。写盘时也不是一 token 一 token 地滴而是把数据集切成 1024 个连续分片每片先concatenate成大数组再一次性写入把海量小写合并成百次大写吞吐立刻上来最后flush()强制落盘。如何用 memmap 零拷贝吃下 17GB 数据读 train.py 的 get_batch数据备好了到哪去直接进 train.py 的get_batch——作者自称穷人版数据加载器data np.memmap(os.path.join(data_dir, train.bin), dtypenp.uint16, moder) ix torch.randint(len(data) - block_size, (batch_size,)) x torch.stack([torch.from_numpy((data[i:iblock_size]).astype(np.int64)) for i in ix]) y torch.stack([torch.from_numpy((data[i1:i1block_size]).astype(np.int64)) for i in ix])四行逻辑在整条 token 流上随机取batch_size个起点每个起点切一段长度为block_size1024的窗口当输入xy整体右移一位当预测目标——这对错位窗口就是语言模型的自回归训练信号。三个设计点值得留意uint16 → int64uint16只承担存储压缩读出后转int64再进模型模型层完全不感知磁盘上的 2 字节宽度。为什么敢在单条 90 亿 token 的流上直接随机取点EOT 分隔符已经保证了边界语义窗口跨文档边界是常态模型靠 EOT 学会翻页不需要按文档喂。为什么每次调用都重建 memmap 对象这不是懒是刻意绕开 numpy memmap 对象长生命周期占用内存的问题。每次迭代新建对象训练进程峰值内存就只剩当前这一批。于是 17GB 的文件全程不必进内存训练循环按需映射、切片走人——这就是零拷贝取数的完整路径。动手验证与调优反向读 .bin 和磁盘预算核算prepare.py跑完后先做反向校验。文件是裸uint16序列任何 numpy 环境都能直接映射m np.memmap(train.bin, dtypenp.uint16, moder)取前几个字节看一眼再在流里找 EOT 分隔值。能看到连续数字流与周期性分隔符产物即合格——这一步花不了十分钟比等训练失败再排查便宜得多。正式跑之前把预算算清楚磁盘Hugging Face 缓存约 54GB加上产物本体建议预留至少 70GB 空闲。并行度num_proc分词与num_proc_load_dataset下载是两个独立旋钮。前者吃 CPU注释建议取核心数的一半左右后者还受网络带宽制约两者最优值可能不同。可复现性切分种子已固定在上文代码里任何机器、任何时间重跑得到的 .bin 逐字节一致。延伸8 卡复现、基线评测与三个可动的手数据就绪复现只差一条命令torchrun --standalone --nproc_per_node8 train.py config/train_gpt2.pyconfig/train_gpt2.py 的要点8 卡 DDP分布式数据并行同一模型切到多卡训练、同步梯度配合梯度累积有效 batch 约 491,520 token/iter600,000 次迭代合计 3000 亿 token在 8×A100 40GB 节点上约 4 天收敛到 2.85 的损失。如果只想验证数据管线而不启动训练用基线评测配置config/eval_gpt2.py 设了eval_onlyTrue直接加载 OpenAI 官方权重在 val 集上报损失——gpt2 (124M) 约 3.11gpt2-xl (1558M) 约 2.56。还有三处值得动手试EOT 位置作者在process里留了原话——EOT 或许该前置而不是追加。改一行、重跑脚本再生成 .bin就是一个现成的消融实验。换自定义数据集data/shakespeare/prepare.py 是同一套脚本结构的迷你版几秒跑完照它替换数据源与切分参数你的新语料就能走上同一条生产线。采样策略get_batch里的随机起点分布是数据侧唯一的采样逻辑可以改成沿文档边界对齐采样做对照。从网页到 GPU整条链路 30 秒回顾回头看整条链路只有四手交接网页原文 → BPE token 流 → uint16 裸 .bin → GPU 上的随机窗口。每一次交接都是一份可验收的契约哪一环出问题都能独立验证。明晚就可以做的下一步先跑python data/openwebtext/prepare.py把产物生成缓存和磁盘预算按上一节留足如果已有现成 .bin就直接用np.memmap反读它确认分隔符周期性出现。验收过自己的 train.bin那 17GB 才算真正属于你的训练数据。【免费下载链接】nanoGPTThe simplest, fastest repository for training/finetuning medium-sized GPTs.项目地址: https://gitcode.com/GitHub_Trending/na/nanoGPT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考