第一次运行
这一页的目标不是训练出一个聪明的模型,而是让你亲眼看到下面这条链路完整发生一次:
下载数据 → 训练分词器 → 预训练小模型 → 监督微调 → 在命令行对话第一次运行时,能完整走通比结果好坏重要。
准备环境
nanochat 使用 uv 管理 Python 依赖。先确认你已经安装 Python 3.10 或更高版本,并安装 uv:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh2
然后克隆仓库并安装 CPU 版本:
git clone https://github.com/karpathy/nanochat.git
cd nanochat
uv sync --extra cpu
source .venv/bin/activate2
3
4
如果你有 CUDA 显卡,把 --extra cpu 换成 --extra gpu。CPU 和 GPU 的代码路径相同,区别主要在速度和可用显存。
所有中间文件默认放在 ~/.cache/nanochat。你也可以显式指定:
export NANOCHAT_BASE_DIR="$HOME/.cache/nanochat"
mkdir -p "$NANOCHAT_BASE_DIR"2
第一步:下载一小部分训练数据
nanochat 的预训练数据来自 ClimbMix,每个分片是一个 Parquet 文件,约 2.5 亿字符。下载 8 个训练分片,同时会自动下载最后一个验证分片:
python -m nanochat.dataset -n 8第一次下载大约需要 800 MB 到 1 GB 的空间。可以用下面的命令确认文件在不在:
ls "$NANOCHAT_BASE_DIR/base_data_climbmix" | head你应该能看到类似 shard_00000.parquet 和 shard_06542.parquet 的文件。最后一个分片固定留给验证集,训练集使用前面的分片。
没有下载完怎么办
数据下载支持断点续传:已经存在的分片会跳过。重新执行同一条命令即可。不要手动删除 .tmp 之外的已完成分片。
第二步:训练分词器
先做一次小规模训练,确认分词器能正常工作:
python -m scripts.tok_train --max-chars=200000000
python -m scripts.tok_eval2
200000000 是 2 亿字符,约为正式训练量的十分之一。这样分词器训练会快很多,但词表质量会下降。学习流程足够;准备正式训练时,改用默认的 20 亿字符,或者直接运行 runs/speedrun.sh。
训练完成后会生成:
$NANOCHAT_BASE_DIR/tokenizer/tokenizer.pkl
$NANOCHAT_BASE_DIR/tokenizer/token_bytes.pt2
可以用几行 Python 检查“文字 → 整数 → 文字”是否能还原:
from nanochat.tokenizer import get_tokenizer
tokenizer = get_tokenizer()
text = "Hello, nanochat!"
ids = tokenizer.encode(text, prepend="<|bos|>")
print(ids)
print(tokenizer.decode(ids))2
3
4
5
6
7
你应该能还原出原来的文字,并在序列开头看到 <|bos|> 对应的特殊词元。
第三步:训练一个很小的基础模型
下面的命令会训练一个 4 层模型。它不会变聪明,但足以让你观察损失怎样下降、检查点怎样保存:
python -m scripts.base_train \
--depth=4 \
--max-seq-len=256 \
--window-pattern=L \
--device-batch-size=8 \
--total-batch-size=4096 \
--num-iterations=200 \
--eval-every=100 \
--eval-tokens=8192 \
--core-metric-every=-1 \
--sample-every=100 \
--save-every=100 \
--model-tag=d4-demo \
--run=dummy2
3
4
5
6
7
8
9
10
11
12
13
14
参数不是随便选的:
--depth=4:只叠 4 个 Transformer 块,适合 CPU。--max-seq-len=256:每条样本最多 256 个词元,降低计算量。--device-batch-size=8:每张卡、每次前向传播处理 8 条样本。--total-batch-size=4096:一次参数更新总共使用 4096 个词元。--num-iterations=200:只更新 200 次参数。--model-tag=d4-demo:给检查点起名,后面 SFT 要用同一个名字。--run=dummy:不登录 Weights & Biases,把日志直接打印到终端。
看到类似下面的日志就说明训练在正常进行:
step 00010/00200 ... | loss: ... | tok/sec: ...损失可能不会稳定下降,因为模型和数据都太小。先不要用这一步的结果判断代码是否正确。
训练结束后检查检查点:
ls "$NANOCHAT_BASE_DIR/base_checkpoints/d4-demo"你应该能看到 model_000200.pt、meta_000200.json 等文件。模型参数、优化器状态和训练配置是分开保存的。
显存不足时
先降低 --device-batch-size,例如从 8 改成 4、2、1。总批量大小 --total-batch-size 仍然可以被梯度累积补足。不要一上来就改模型结构。
第四步:做一次监督微调
这一步会下载对话数据,第一次可能比较慢。它只做少量迭代,目的是确认 SFT 脚本能读取上一步的检查点:
python -m scripts.chat_sft \
--model-tag=d4-demo \
--max-seq-len=256 \
--device-batch-size=8 \
--total-batch-size=4096 \
--num-iterations=50 \
--eval-every=25 \
--eval-tokens=8192 \
--chatcore-every=-1 \
--run=dummy2
3
4
5
6
7
8
9
10
完成后会生成:
$NANOCHAT_BASE_DIR/chatsft_checkpoints/d4-demo/如果这里报错说找不到 base_checkpoints,先确认上一步是否真的保存了模型,以及两次命令的 --model-tag 是否一致。
第五步:和模型说话
现在启动命令行对话:
python -m scripts.chat_cli -p "What is the capital of France?"也可以不加 -p,进入交互模式:
python -m scripts.chat_cli按回车发送消息,输入 clear 清空对话,输入 quit 退出。
小模型最常见的表现是重复、答非所问或输出乱码。这通常不是“程序没跑通”,而是模型太小、训练太少。此时应该检查的是:命令行有没有正常流式输出、回答有没有正常结束、历史对话有没有被追加。
官方 CPU 脚本
如果你不想逐条复制命令,可以直接运行:
bash runs/runcpu.sh这个脚本会自动安装依赖、下载 8 个数据分片、训练分词器、训练一个 6 层模型并做 SFT。仓库作者在 M3 Max 上的时间约为 30 分钟预训练加 10 分钟微调,普通 CPU 可能需要数小时。它适合确认整个流程,不适合追求效果。
脚本最后一行聊天命令是注释状态,需要你自己取消注释或手动执行:
python -m scripts.chat_cli -p "What is the capital of France?"从哪一步继续
| 你现在有什么 | 可以跳到 |
|---|---|
| 只有原始数据 | 训练分词器 |
| 已有分词器 | 预训练 |
已有 base_checkpoints | 监督微调或基础模型评估 |
已有 chatsft_checkpoints | 命令行对话、SFT 评估或强化学习 |
已有 chatrl_checkpoints | 用 -i rl 启动对话 |
常见问题
uv: command not found
安装 uv 后重新打开终端,或者确认 ~/.local/bin 在 PATH 里。
下载数据很慢
先只下载 1 个分片做实验:python -m nanochat.dataset -n 1。它能训练分词器和很小的模型,但正式训练需要更多数据。
训练时显存不足
先降低 --device-batch-size。如果仍然不足,再降低 --max-seq-len。检查点占用的显存和序列长度、模型层数都有关。
找不到 Flash Attention 3
没有关系。nanochat 会自动退回 PyTorch 的 SDPA 实现,结果仍然正确,只是训练更慢。CPU 本来就不会使用 FA3。
对话输出英文乱码
这是小模型在少量数据上训练的正常结果,不是编码问题。先用英文简单提示,并确认使用的是 SFT 检查点,而不是基础模型。