nanoGPT config 不是参数表:一张 LLM 训练配方地图

从 LLM 训练的大背景出发,系统讲解 nanoGPT config 目录里的训练、微调、评估配置,以及 batch、context、模型大小、AdamW、学习率、DDP、dtype 等参数如何在源码中生效。

如果只把 nanoGPT 的 config/ 当成“参数说明书”,很容易陷进一个低效问题:

这个变量是什么意思?
那个数字为什么是 1024?
为什么这里学习率是 1e-3,那里又是 3e-5?

这些问题都对,但顺序反了。

LLM 训练配置的本质不是一堆散落的超参数,而是一张训练配方。它同时回答七个问题:

1. 训练目标是什么?从头训练、微调,还是只评估?
2. 数据是什么?字符级 Shakespeare、BPE Shakespeare,还是 OpenWebText?
3. 一次看多长上下文?也就是 context window。
4. 模型有多大?多少层、多少头、多少 embedding 维。
5. 一步吃多少 token?batch、block、梯度累积、GPU 数共同决定。
6. 学习率怎么走?warmup、cosine decay、constant LR。
7. 用什么系统资源跑?DDP、dtype、compile、device。

nanoGPT 的 config/ 目录,就是把这些问题压成几个可运行的 recipe。

这篇不是逐行翻译源码,而是先搭一张大图:LLM 训练配置到底在控制什么。然后再回到 nanoGPT,解释每个配置文件为什么长成那样,以及这些参数最终怎样进入 train.pymodel.py

源码版本是 karpathy/nanoGPT commit 3adf61e。主要阅读范围是 config/train.pymodel.pyconfigurator.py

1. 大背景:config 是训练控制面

Transformer 论文把序列建模主干变成了 self-attention。GPT-2 则展示了另一条更贴近今天 LLM 的路线:用 decoder-only Transformer,在大规模文本上做 next-token prediction,再通过规模和数据获得越来越强的泛化能力。

这里有一个很重要的转变:

模型代码决定“这个网络能做什么计算”。
训练配置决定“这次实验要把这个网络推向哪里”。

同一个 GPT 类,可以被配置成:

小字符模型:几百万参数,在 Tiny Shakespeare 上玩具训练
GPT-2 复现:124M 参数,在 OpenWebText 上大规模预训练
GPT-2 XL 微调:加载 1.5B 级别 checkpoint,在 Shakespeare 上短训练
GPT-2 baseline:加载权重,只跑 eval,不更新参数

所以 config 不是模型定义本身。它更像控制面:

flowchart TD
  Q["训练目标"] --> C["config recipe"]
  D["数据与 tokenizer"] --> C
  M["模型规模"] --> C
  O["优化器与学习率"] --> C
  S["算力与并行方式"] --> C
  C --> T["train.py 训练循环"]
  C --> G["GPTConfig / GPT"]
  T --> R["loss / checkpoint / samples"]

一旦你把 config/ 看成训练控制面,很多数字就不再孤立。

例如 batch_size = 12 单独看没有意义;它要和 block_size = 1024gradient_accumulation_steps = 5 * 8、DDP world size 一起看,才知道每一步训练消耗多少 token。learning_rate = 1e-3 也不能脱离场景看;它在小模型从头训练时合理,在 GPT-2 XL 微调时就太猛。

2. 最小数学地基:token budget、context 和模型大小

先把三组最核心的量讲清楚。

第一组是一次训练迭代实际吃多少 token:

tokens_per_iter = gradient_accumulation_steps * ddp_world_size * batch_size * block_size

这不是我推出来的,nanoGPT 的 train.py#L101 就是这样算的。

举个小例子:

batch_size = 2
block_size = 4
gradient_accumulation_steps = 3
ddp_world_size = 1

tokens_per_iter = 2 * 4 * 3 * 1 = 24

意思是:一次 optimizer step 前,模型总共看了 24 个 token 的训练目标。这里的 batch_size 是 micro-batch,不一定是“真正的大 batch”。真正的大 batch 是 micro-batch 乘上梯度累积,再乘上 GPU 数。

第二组是 context:

block_size = 一个样本里最多放多少个连续 token

训练时 get_batch() 会从 train.binval.bin 里随机截一段长度为 block_sizex,再取右移一位的 y。所以 block_size=1024 的意思不是“batch 有 1024 条样本”,而是“每条样本最多 1024 个 token 的上下文”。源码在 train.py#L114-L131

第三组是模型大小:

n_layer = Transformer block 层数
n_head  = attention head 数
n_embd  = hidden width / embedding width

model.py 里,它们进入 GPTConfig,再创建 token embedding、position embedding、n_layer 个 block 和最终 lm_head。如果 n_embd=768n_head=12,每个 head 的维度就是:

head_dim = n_embd / n_head = 768 / 12 = 64

这和 Transformer 论文里的多头注意力直觉是一致的:多个 head 并行看不同关系,再拼回模型宽度。

3. nanoGPT 的 config 机制:不是 YAML,是 Python 片段

nanoGPT 的默认参数写在 train.py#L35-L74。随后它会执行:

exec(open('configurator.py').read())

configurator.py 的规则很朴素:

python train.py config/train_shakespeare_char.py --batch_size=12

执行顺序是:

1. 先 exec 配置文件
2. 再用 --key=value 覆盖

源码在 configurator.py#L20-L47。它还会做两件保护:

1. key 必须已经存在于 globals(),否则 Unknown config key。
2. literal_eval 后类型必须和原变量一致。

所以它不是 Hydra、YAML、TOML 那种配置系统。它就是一个“穷人版 configurator”:把 train.py 的全局变量改掉,然后继续往下跑。

这个设计粗糙,但学习价值很高。因为你能直接看到参数从哪里来,又流向哪里。

4. config/ 目录里到底有哪些 recipe

这个 checkout 里 config/ 只有 7 个文件:

文件目的关键词
train_shakespeare_char.py从头训练一个小字符级 GPT小模型、短 context、较大学习率、容易过拟合
train_gpt2.py在 OpenWebText 上复现 GPT-2 124M 预训练8×A100、491,520 tokens/iter、600k iters
finetune_shakespeare.py加载 GPT-2 XL,在 Shakespeare BPE 数据上微调pretrained、很小 LR、constant LR、短训练
eval_gpt2.py评估 GPT-2 small baselineeval_only、124M
eval_gpt2_medium.py评估 GPT-2 medium baselineeval_only、350M
eval_gpt2_large.py评估 GPT-2 large baselineeval_only、774M
eval_gpt2_xl.py评估 GPT-2 XL baselineeval_only、1558M

这几个文件不是“参数全集”。它们只覆盖默认值里需要改变的部分。没有写的参数继续用 train.py 的默认值。

5. 第一类参数:实验身份、日志、checkpoint

这些参数不改变模型数学,但改变实验怎么被记录和保存:

out_dir
eval_interval
eval_iters
log_interval
eval_only
always_save_checkpoint
wandb_log / wandb_project / wandb_run_name

out_dir 是 checkpoint 目录。eval_interval 决定隔多少 iteration 跑一次 train/val loss 估计。eval_iters 决定估计 loss 时采样多少个 batch。eval_only=True 时,train.py 在第 0 步 eval 完就退出,见 train.py#L262-L288

为什么 eval 配置都写:

eval_iters = 500
eval_only = True
init_from = 'gpt2'

因为它们不是训练 recipe,而是 baseline measurement recipe。你只想加载 OpenAI GPT-2 checkpoint,比较它在 OpenWebText 上的 loss,不想动权重。

always_save_checkpoint 很容易被忽略。小数据集容易过拟合,所以 train_shakespeare_char.pyfinetune_shakespeare.py 都设成 False:只有 val loss 变好才保存。大规模预训练默认可以更频繁留 checkpoint。

6. 第二类参数:数据、上下文和有效 batch

这些参数回答“每一步模型看什么、看多长、看多少”:

dataset
batch_size
block_size
gradient_accumulation_steps

dataset 不是数据文件名,而是 data/<dataset>/ 目录名。train.py 会读:

data/<dataset>/train.bin
data/<dataset>/val.bin

如果目录里有 meta.pkl,还会读取 vocab_size,见 train.py#L137-L155

block_size 同时影响三件事:

1. 数据切片长度:每个训练样本多少 token。
2. position embedding 长度:model.py 里 wpe = Embedding(block_size, n_embd)。
3. attention 计算成本:自注意力大致随 T^2 增长。

所以 README 里建议 MacBook 上可以把 block_size 从 256 调到 64。这不是“少看一点文本”这么简单,它会直接降低显存和计算压力。

gradient_accumulation_steps 是模拟大 batch 的关键。以 train_gpt2.py 为例:

batch_size = 12
block_size = 1024
gradient_accumulation_steps = 5 * 8
ddp_world_size = 8

在 DDP 里,train.py 会把每个进程的 gradient_accumulation_steps 除以 ddp_world_size,所以全局 token 公式仍然是:

12 * 1024 * 40 = 491,520 tokens/iter

600,000 个 iteration,大约是:

491,520 * 600,000 = 294,912,000,000 tokens

也就是约 295B tokens。配置注释里说“300B”,这是合理的数量级说法。

7. 第三类参数:模型规模

模型结构参数是:

n_layer
n_head
n_embd
dropout
bias

它们进入 GPTConfig,然后控制 model.py 里的结构:

wte: token embedding
wpe: position embedding
h:   n_layer 个 Transformer Block
ln_f: final LayerNorm
lm_head: hidden -> vocab logits

其中 n_layer/n_head/n_embd 决定主干规模;dropout 是正则化;bias 决定 Linear 和 LayerNorm 里是否带 bias。

train_shakespeare_char.py 是 baby GPT:

n_layer = 6
n_head = 6
n_embd = 384
block_size = 256
dropout = 0.2

它的目标不是复现 GPT-2,而是小数据集上快速看到语言模型学到 Shakespeare 格式。dropout=0.2 是因为小数据集容易过拟合。

train.py 的默认 GPT-2 small 结构是:

n_layer = 12
n_head = 12
n_embd = 768
block_size = 1024

这对应 GPT-2 124M 级别。model.pyfrom_pretrained() 里还写死了四个 GPT-2 尺寸:

gpt2:        12 layers, 12 heads, 768 width
gpt2-medium: 24 layers, 16 heads, 1024 width
gpt2-large:  36 layers, 20 heads, 1280 width
gpt2-xl:     48 layers, 25 heads, 1600 width

源码在 model.py#L207-L225。这也解释了为什么 eval 配置只需要改 init_from,不需要自己写 n_layer:加载 GPT-2 checkpoint 时,模型尺寸由 model_type 决定。

8. 第四类参数:从头训练、恢复训练、加载 GPT-2

init_from 控制模型起点:

scratch: 从随机初始化开始
resume:  从 out_dir/ckpt.pt 恢复
gpt2*:   加载 Hugging Face GPT-2 checkpoint

这三种路径在 train.py#L149-L188

从头训练时,vocab_size 优先来自数据目录的 meta.pkl;如果没有,就默认用 GPT-2 vocab size padding 到 50304。字符级 Shakespeare 有自己的 meta.pkl,所以它会得到 65 字符词表。

加载 GPT-2 时,nanoGPT 强制:

vocab_size = 50257
block_size = 1024
bias = True

这不是随便写的。GPT-2 checkpoint 的权重形状就是这个约定。你可以把 block_size crop 小一点,但不能随便把 GPT-2 checkpoint 的 vocab 改成自己的 tokenizer 后还期待权重对齐。

这也是微调配置的核心:

dataset = 'shakespeare'
init_from = 'gpt2-xl'
learning_rate = 3e-5
decay_lr = False

它不是“训练一个 GPT-2 XL”。它是“拿 GPT-2 XL 的能力,在很小的 Shakespeare 数据上轻轻调一下”。

9. 第五类参数:AdamW、学习率和训练时长

优化参数是另一组大头:

learning_rate
max_iters
weight_decay
beta1 / beta2
grad_clip
decay_lr
warmup_iters
lr_decay_iters
min_lr

nanoGPT 使用 AdamW。model.configure_optimizers() 会把参数分成两组:

dim >= 2: weight_decay = weight_decay
dim < 2:  weight_decay = 0

也就是矩阵权重和 embedding 做 weight decay,bias 和 LayerNorm 这类一维参数不做。源码在 model.py#L263-L287。AdamW 背后的关键论文是 Loshchilov 和 Hutter 的 Decoupled Weight Decay:在 Adam 这类自适应优化器里,把 weight decay 和梯度更新解耦很重要。

学习率调度在 train.py#get_lr

1. 前 warmup_iters 线性 warmup。
2. 中间 cosine decay。
3. 超过 lr_decay_iters 后固定 min_lr。

这和 Transformer / GPT 系训练里的常见经验相符:一开始别让学习率猛冲,稳定后逐渐降下来。cosine decay 可以追溯到 SGDR 一类调度思想;Chinchilla 之后大家也更重视“模型大小、token 数、训练预算”的配平。nanoGPT 在注释里写 lr_decay_iters should be ~= max_iters per Chinchilla,意思是衰减周期大致覆盖完整训练预算。

为什么三个 recipe 的学习率差这么多?

场景学习率原因
train_shakespeare_char.py1e-3小模型从头训,可以大胆一点
train.py / train_gpt2.py6e-4GPT-2 small 预训练主 recipe
finetune_shakespeare.py3e-5预训练大模型微调,不能把已有能力冲坏

beta2 也有场景差异。默认 GPT-2 pretrain 用 0.95,字符级 Shakespeare 配置改成 0.99,注释说因为 tokens per iter 小。可以直觉理解为:小 batch 噪声更大,让二阶动量更平滑一点。

grad_clip=1.0 是安全阀。训练循环会先 unscale_,再 clip_grad_norm_,避免某一步梯度爆炸把模型推飞。源码在 train.py#L306-L314

10. 第六类参数:系统、并行和数值类型

系统参数回答“这份 recipe 在什么硬件上跑”:

backend
device
dtype
compile

backend='nccl' 是 GPU DDP 常用后端。device 可以是 cudacpumps 等。dtype 默认会在 CUDA 支持 bf16 时用 bfloat16,否则用 float16。训练时通过 torch.amp.autocast 进入混合精度上下文,见 train.py#L109-L112

compile=True 会调用 torch.compile(model)。它可能更快,但第一次会有编译开销,也可能在 CPU 或某些环境里不值得开。所以 README 的 MacBook 示例会加:

--device=cpu --compile=False

DDP 还有一个容易误解的点:gradient_accumulation_steps = 5 * 8 不是说每张卡都累积 40 次。DDP 初始化后,每个进程会除以 ddp_world_size,见 train.py#L82-L101。这样全局有效 batch 保持不变。

训练循环里还做了一个小优化:只有最后一个 micro step 才同步 DDP 梯度:

model.require_backward_grad_sync = (micro_step == gradient_accumulation_steps - 1)

这让梯度累积期间少做不必要的跨卡通信。

11. 把三个主要 recipe 串起来看

现在回到 config/,就能看出每个文件的性格。

train_shakespeare_char.py:教学用小模型

它的目标是“快速从头训练一个字符级 GPT”。所以它选择:

dataset = shakespeare_char
block_size = 256
batch_size = 64
gradient_accumulation_steps = 1
n_layer = 6
n_head = 6
n_embd = 384
dropout = 0.2
learning_rate = 1e-3
max_iters = 5000

这是一份小数据、小模型、快速反馈 recipe。eval_interval=250 是因为小数据集很快过拟合,要勤看 val loss。always_save_checkpoint=False 是只保留更好的 checkpoint。

train_gpt2.py:复现 GPT-2 124M

它的目标是“在 OpenWebText 上训练 GPT-2 small 级别模型”。所以它不改 n_layer/n_head/n_embd,沿用 train.py 默认的 12/12/768,只改大训练需要的 batch、训练步数、日志和 weight decay:

batch_size = 12
block_size = 1024
gradient_accumulation_steps = 5 * 8
max_iters = 600000
lr_decay_iters = 600000
weight_decay = 1e-1

它不是玩具 recipe。它默认假设 8×A100 40GB,一步约 0.5M token,总训练预算约 295B token。

finetune_shakespeare.py:很短的 GPT-2 XL 微调

它的目标是“利用预训练能力,而不是重新学语言”。所以它选择:

dataset = shakespeare
init_from = gpt2-xl
batch_size = 1
gradient_accumulation_steps = 32
max_iters = 20
learning_rate = 3e-5
decay_lr = False

注释里已经算了:一次 iteration 是 1 * 32 * 1024 = 32,768 token。Shakespeare BPE 训练集约 301,966 token,所以一个 epoch 约 9.2 iteration。max_iters=20 大概就是两轮多一点。微调不是“训练到天荒地老”,而是很轻地把模型往目标风格推。

eval_gpt2*.py:baseline 仪表盘

四个 eval 文件只做:

batch_size = 8
eval_iters = 500
eval_only = True
init_from = gpt2 / gpt2-medium / gpt2-large / gpt2-xl

它们的价值是给 OpenWebText loss 一个标尺。README 里也列了这些 baseline loss:模型越大,loss 越低,但也更贵。

12. 你自己调参时,先问这几件事

不要从“把学习率改多少”开始。先从训练目标倒推。

如果你显存不够,优先看:

1. block_size 是否过长
2. batch_size 是否过大
3. n_layer / n_head / n_embd 是否过大
4. dtype 是否可以用 bf16/fp16
5. compile 是否在当前环境反而带来麻烦

如果 train loss 降、val loss 升,优先看:

1. 数据是否太少
2. max_iters 是否太长
3. dropout 是否太低
4. weight_decay 是否太弱
5. checkpoint 是否只按 best val 保存

如果 train loss 和 val loss 都高,优先看:

1. 模型是否太小
2. 训练是否不够久
3. learning_rate 是否太低或 schedule 太短
4. batch/token budget 是否太小
5. tokenizer / vocab_size / meta.pkl 是否匹配

如果是微调,先保守:

1. learning_rate 比 pretrain 小很多
2. max_iters 先短
3. eval_interval 设密一点
4. 只保存 val loss 变好的 checkpoint
5. 必要时缩短 block_size 保命

13. 边界:nanoGPT config 有意简单

nanoGPT 的 config 设计不是生产级实验平台。它有几个边界:

1. config 文件是 Python,会被 exec,有灵活性也有副作用风险。
2. 只接受 train.py 里已有的基础类型 key。
3. 不管理多实验 sweep、继承、schema、分布式集群任务队列。
4. 配置数字是 Karpathy 为教学和复现选出的 recipe,不是所有数据集的最优解。

但正因为它简单,它适合拿来学习 LLM 训练配置的骨架。

你最后应该带走的不是“某个参数该填多少”,而是这张地图:

数据与 tokenizer 决定 vocab 和 token stream
block_size 决定 context 和 attention 成本
batch_size * grad_accum * GPU 数决定每步 token budget
n_layer / n_head / n_embd 决定模型容量
init_from 决定从头学、接着学,还是加载 GPT-2
AdamW + LR schedule 决定怎么走优化路径
eval/checkpoint 决定你如何判断训练有没有变好
device/dtype/compile/DDP 决定这份 recipe 是否跑得动

看懂这张图,再回头看 config/,它就不是一堆神秘数字,而是几份非常清晰的训练配方。

自测

读完可以用这几个问题检查自己是不是真的懂了:

1. 为什么 batch_size=12 不等于有效 batch 只有 12 条样本?
2. 为什么 GPT-2 微调的 learning_rate 要远小于字符级从头训练?
3. 为什么 eval_gpt2.py 不需要写 n_layer/n_head/n_embd?
4. 为什么 block_size 同时影响数据切片、position embedding 和 attention 成本?
5. 如果换自己的 tokenizer,为什么要检查 meta.pkl 和 vocab_size?

能答上来,nanoGPT 的 config 目录基本就通了。

参考资料