源码地图
nanochat 的文件不多,但第一次打开时很容易陷进细节。这一页给出三条路线:一条按训练流程读,一条按推理流程读,一条按“先跳过什么”读。
先看仓库自带说明README.md↗顶层目录
| 目录 | 作用 | 第一遍要不要读 |
|---|---|---|
nanochat/ | 可复用的核心模块:分词器、模型、数据、优化器、推理引擎 | 要,按章节读 |
scripts/ | 可以直接运行的训练、评估、对话脚本 | 要,先读主脚本 |
tasks/ | 对话微调和评测用的数据集封装 | 第二遍再读 |
runs/ | 把多条命令串起来的 shell 脚本 | 先读,帮助建立全局顺序 |
tests/ | 针对关键模块的测试 | 需要确认行为时读 |
dev/ | 数据准备、缩放实验和历史记录 | 初学可跳过 |
训练主链
先看 runs/runcpu.sh 或 runs/speedrun.sh。它们告诉你“先做什么、后做什么”,比直接钻进模型代码更容易建立方向感。
text
runs/runcpu.sh
├─ python -m nanochat.dataset 下载数据分片
├─ python -m scripts.tok_train 训练分词器
├─ python -m scripts.base_train 预训练基础模型
├─ python -m scripts.base_eval 评估基础模型
├─ python -m scripts.chat_sft 监督微调
└─ python -m scripts.chat_cli 命令行对话1
2
3
4
5
6
7
2
3
4
5
6
7
沿着代码往下看,训练时的调用关系是:
text
scripts/base_train.py
→ 构建 GPT 模型
→ tokenizing_distributed_data_loader_bos_bestfit
→ nanochat/dataset.py 读取 Parquet
→ nanochat/dataloader.py 打包成固定长度样本
→ GPT.forward
→ CausalSelfAttention 计算注意力
→ MLP 逐位置变换
→ lm_head 得到每个词元的分数
→ 交叉熵损失
→ loss.backward
→ MuonAdamW.step 更新参数
→ 保存检查点1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
推理主链
推理从命令行脚本开始:
text
scripts/chat_cli.py
→ checkpoint_manager.load_model 读取模型参数和配置
→ Engine.generate
→ KVCache 保存历史 Key 和 Value
→ GPT.forward 每次只处理新词元
→ sample_next_token 从 logits 选出下一个词元
→ 逐词元解码并打印1
2
3
4
5
6
7
2
3
4
5
6
7
Engine 不关心文字,只接收词元编号,返回词元编号。分词器负责把用户输入变成编号,把编号变回文字。
按章节阅读
| 章 | 主要文件 | 读完后应该能回答 |
|---|---|---|
| 分词器 | nanochat/tokenizer.py、scripts/tok_train.py | 文本怎样变成整数?特殊词元是什么? |
| 训练数据 | nanochat/dataset.py、nanochat/dataloader.py | 文档怎样变成固定长度样本?输入和目标为什么错开一格? |
| GPT 模型 | nanochat/gpt.py | 词元怎样经过注意力和多层感知机变成 logits? |
| 注意力 | nanochat/gpt.py、nanochat/flash_attention.py | Q、K、V 分别是什么?因果掩码和滑动窗口怎样限制可见范围? |
| 训练目标 | nanochat/gpt.py、nanochat/loss_eval.py | 交叉熵和每字节比特数是什么关系? |
| 训练循环 | scripts/base_train.py | 一次参数更新经过了哪些步骤? |
| 优化器 | nanochat/optim.py | AdamW 和 Muon 分别更新哪些参数? |
| 评估 | scripts/base_eval.py、nanochat/core_eval.py | BPB、CORE、ChatCORE 各衡量什么? |
| 监督微调 | scripts/chat_sft.py、nanochat/tokenizer.py | 对话怎样渲染?哪些词元计算损失? |
| 强化学习 | scripts/chat_rl.py、nanochat/engine.py | 奖励怎样变成梯度?工具输出为什么不被训练? |
| 推理引擎 | nanochat/engine.py、nanochat/checkpoint_manager.py | 键值缓存省掉了什么计算?工具调用状态机怎样工作? |
| 采样与命令行 | scripts/chat_cli.py、nanochat/engine.py | 温度、top-k、停止词元分别影响什么? |
第一遍可以跳过的部分
不是所有文件都值得在第一遍逐行阅读:
nanochat/optim.py里的分布式通信和融合内核。先理解“矩阵用 Muon,其他用 AdamW”即可。nanochat/fp8.py。只有 H100 及以上显卡开启--fp8时才会用到。nanochat/core_eval.py的任务渲染细节。先理解 CORE 是多个任务的归一化平均分即可。nanochat/execution.py的沙箱实现。先理解模型可以请求执行代码、再把结果读回来。dev/里的历史实验和数据准备脚本。它们不影响主流程。
怎样读一个函数
不要从第一行开始逐字翻译。按这个顺序看:
- 函数签名:输入是什么,返回什么。
- 张量形状:
B是批量,T是序列长度,C是隐藏维度,H是头数,D是每个头的维度。 - 关键调用:这个函数把工作交给了谁?
- 副作用:有没有写文件、改缓存、更新位置?
- 配置开关:哪些参数会改变行为?
例如 GPT.forward(idx, targets=None, kv_cache=None):
idx的形状是(B, T),里面是词元编号。- 训练时传入
targets,函数返回损失。 - 推理时传入
kv_cache,函数返回 logits,并把新的 K/V 写进缓存。 targets和kv_cache是两条不同的分支,但前半段的 Transformer 计算是共用的。
固定版本
本教程的源码链接全部指向提交:
text
92d63d4e8bb4df75c3b71618f31ddde2378b2bcd1
如果你拉取了更新的代码,看到行号有偏移是正常的。先看函数名,再看行号。每章的结论如果和最新代码冲突,以仓库代码为准。
下一步:回到全景,开始真正的主线。