训练的起点 · 配置与初始化
读 train.py 的开头——一大堆超参当全局变量摊着,configurator.py 用一句 exec 实现「穷人配置法」从命令行覆盖它们;以及三种起手式:从零 scratch、断点 resume、还是从 GPT-2 权重接着练。
训练的起点 · 配置与初始化
模型搭好了,但它还是个只会吐乱码的新生儿。第三站,我们把它扔进 train.py 的训练循环里调教。这一章先读开头的准备工作:配置怎么来、模型怎么起手。
一大堆超参,直接摊成全局变量
train.py 一开头(约 33–74 行),是几十个模块级全局变量,就是训练的全部旋钮:
# I/O
out_dir = 'out'
eval_interval = 2000
eval_iters = 200
init_from = 'scratch' # 'scratch' or 'resume' or 'gpt2*'
# data
gradient_accumulation_steps = 5 * 8
batch_size = 12
block_size = 1024
# model
n_layer = 12
n_head = 12
n_embd = 768
dropout = 0.0
bias = False
# adamw optimizer
learning_rate = 6e-4
max_iters = 600000
weight_decay = 1e-1
# ... 还有学习率调度、DDP、system 若干
compile = True没有 argparse、没有配置类,就是一堆裸变量。默认这套,是冲着「在 OpenWebText 上训一个 GPT-2(124M)」去的。乍看很糙,但配上下面这个「穷人配置器」,它意外地好用。
configurator.py:一句 exec 的「穷人配置法」
紧接着两行,是 nanoGPT 最有争议、也最有个性的设计:
config_keys = [k for k,v in globals().items() if not k.startswith('_') and isinstance(v, (int, float, bool, str))]
exec(open('configurator.py').read()) # overrides from command line or config file
config = {k: globals()[k] for k in config_keys} # will be useful for logging- 第一行:把上面那些「简单类型的全局变量名」收集起来,记成
config_keys。 - 第二行:
exec执行configurator.py的代码,让它去覆盖这些全局变量。 - 第三行:把最终值打包成
config字典,留着记日志、存进 checkpoint。
configurator.py 本身只有 40 行,核心逻辑是:
for arg in sys.argv[1:]:
if '=' not in arg:
# 没有等号,当成配置文件名,直接 exec 它
config_file = arg
exec(open(config_file).read())
else:
# 形如 --key=value,解析后覆盖对应的全局变量
key, val = arg.split('=')
key = key[2:]
attempt = literal_eval(val) # 尽量按字面量解析(数字/布尔/...)
assert type(attempt) == type(globals()[key]) # 类型要对上
globals()[key] = attempt于是你就能这么用(还记得第一站跑莎士比亚的命令吗):
python train.py config/train_shakespeare_char.py --batch_size=32 --compile=Falseconfig/train_shakespeare_char.py是个普通 Python 文件,里面全是batch_size = 64这样的赋值——被exec一执行,就覆盖了默认值;--batch_size=32再进一步覆盖;--compile=False把编译关掉。
作者本人在 configurator.py 顶部就自嘲:「Poor Man's Configurator. Probably a terrible idea.」用 exec + 改 globals() 是公认的「歪门邪道」——不安全、IDE 也不认。但它换来的是:加一个新超参,只需在顶部加一行 xxx = 默认值,命令行立刻就能覆盖它,零样板代码。这是一个非常 nanoGPT 的取舍:为了极致的简单,接受一点「不优雅」。 读源码,读的就是这种真实世界里的权衡。
定好随机种子、精度与设备
接着是一串环境设置(约 104–112 行):
torch.manual_seed(1337 + seed_offset)
torch.backends.cuda.matmul.allow_tf32 = True # allow tf32 on matmul
torch.backends.cudnn.allow_tf32 = True # allow tf32 on cudnn
device_type = 'cuda' if 'cuda' in device else 'cpu'
ptdtype = {'float32': torch.float32, 'bfloat16': torch.bfloat16, 'float16': torch.float16}[dtype]
ctx = nullcontext() if device_type == 'cpu' else torch.amp.autocast(device_type=device_type, dtype=ptdtype)manual_seed(1337 + ...):固定随机种子,保证可复现(1337是作者的口味)。allow_tf32 = True:允许用 TF32 做矩阵乘法——一种在 A100 等卡上「精度略降、速度大涨」的模式。ptdtype、ctx:根据dtype(float32 / bf16 / fp16)准备好 autocast 上下文——这是混合精度训练的开关,细节留到进阶那站。CPU 上则用空的nullcontext()(不做混合精度)。
三种起手式:scratch / resume / gpt2
最关键的初始化,是模型从哪来(约 146–192 行),由 init_from 三选一:
if init_from == 'scratch':
# 从零初始化一个新模型
model_args['vocab_size'] = meta_vocab_size if meta_vocab_size is not None else 50304
gptconf = GPTConfig(**model_args)
model = GPT(gptconf)
elif init_from == 'resume':
# 从 out_dir/ckpt.pt 断点续训
checkpoint = torch.load(ckpt_path, map_location=device)
gptconf = GPTConfig(**model_args)
model = GPT(gptconf)
model.load_state_dict(state_dict)
iter_num = checkpoint['iter_num']
best_val_loss = checkpoint['best_val_loss']
elif init_from.startswith('gpt2'):
# 直接载入 OpenAI 的 GPT-2 权重,在此基础上继续训
model = GPT.from_pretrained(init_from, override_args)三种路子:
scratch:全新随机初始化——就是上一章那个新生儿。词表大小优先从数据的meta.pkl读(莎士比亚是 65),读不到就默认 GPT-2 的 50304。resume:从out_dir/ckpt.pt加载,接着上次的进度练——iter_num、best_val_loss都一并恢复。训练中断了不怕。gpt2*:直接装载 OpenAI 公开的 GPT-2 权重(from_pretrained,这个我们进阶那站逐行讲),用来微调。
一个细节:meta_vocab_size 来自数据那站存的 meta.pkl。这就是为什么当时要多存那张「翻译表」——训练时要靠它知道「输出层该开 65 宽还是 50304 宽」。前后呼应上了。
模型起好后,还有 model.to(device)(搬到 GPU)、GradScaler、torch.compile、DDP 包装几步——它们都属于「怎么训得更快更省」,统一放到进阶那站细讲,这里先知道有这么回事。
准备工作里还剩一个大件没讲:优化器。model.configure_optimizers(...) 里藏着一个很值得一看的小设计——不是所有参数都一视同仁地做权重衰减。下一章专门拆它。👉 优化器:哪些参数该做权重衰减
参考文献
- 源码:Andrej Karpathy, nanoGPT,
train.py与configurator.py. https://github.com/karpathy/nanoGPT - [1] Hoffmann et al. “Training Compute-Optimal Large Language Models (Chinchilla).” 2022. arXiv:2203.15556. —— 默认超参(如
max_iters、min_lr)背后的算力最优思想。