# 训练的起点 · 配置与初始化

> 读 train.py 的开头——一大堆超参当全局变量摊着，configurator.py 用一句 exec 实现「穷人配置法」从命令行覆盖它们；以及三种起手式：从零 scratch、断点 resume、还是从 GPT-2 权重接着练。

- 作者：David（道雾轩）
- 专栏：nanoGPT 源码解读（https://daiw.org/manual/nanogpt.md）
- 最后更新：2026-07-13
- 原文：https://daiw.org/manual/nanogpt/train-setup
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 训练的起点 · 配置与初始化

模型搭好了，但它还是个只会吐乱码的新生儿。第三站，我们把它扔进 `train.py` 的训练循环里调教。这一章先读**开头的准备工作**：配置怎么来、模型怎么起手。

## 一大堆超参，直接摊成全局变量

`train.py` 一开头（约 33–74 行），是几十个**模块级全局变量**，就是训练的全部旋钮：

```python
# 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 最有争议、也最有个性的设计：

```python
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 行，核心逻辑是：

```python
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
```

于是你就能这么用（还记得[第一站跑莎士比亚](https://daiw.org/manual/nanogpt/what-is-nanogpt)的命令吗）：

```bash
python train.py config/train_shakespeare_char.py --batch_size=32 --compile=False
```

- `config/train_shakespeare_char.py` 是个**普通 Python 文件**，里面全是 `batch_size = 64` 这样的赋值——被 `exec` 一执行，就覆盖了默认值；
- `--batch_size=32` 再进一步覆盖；`--compile=False` 把编译关掉。

<Callout type="warn">
  作者本人在 `configurator.py` 顶部就自嘲：「Poor Man's Configurator. Probably a terrible idea.」用 `exec` + 改 `globals()` 是公认的「歪门邪道」——不安全、IDE 也不认。但它换来的是：**加一个新超参，只需在顶部加一行 `xxx = 默认值`，命令行立刻就能覆盖它**，零样板代码。这是一个非常 nanoGPT 的取舍：**为了极致的简单，接受一点「不优雅」。** 读源码，读的就是这种真实世界里的权衡。
</Callout>

## 定好随机种子、精度与设备

接着是一串环境设置（约 104–112 行）：

```python
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 上下文**——这是混合精度训练的开关，细节留到[进阶那站](https://daiw.org/manual/nanogpt/scaling-and-speed)。CPU 上则用空的 `nullcontext()`（不做混合精度）。

## 三种起手式：scratch / resume / gpt2

最关键的初始化，是**模型从哪来**（约 146–192 行），由 `init_from` 三选一：

```python
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`**：全新随机初始化——就是[上一章](https://daiw.org/manual/nanogpt/gpt-assembly)那个新生儿。词表大小优先从数据的 `meta.pkl` 读（莎士比亚是 65），读不到就默认 GPT-2 的 50304。
- **`resume`**：从 `out_dir/ckpt.pt` 加载，**接着上次的进度练**——`iter_num`、`best_val_loss` 都一并恢复。训练中断了不怕。
- **`gpt2*`**：直接装载 OpenAI 公开的 GPT-2 权重（`from_pretrained`，这个我们[进阶那站](https://daiw.org/manual/nanogpt/from-pretrained)逐行讲），用来微调。

<Callout type="info">
  一个细节：`meta_vocab_size` 来自[数据那站](https://daiw.org/manual/nanogpt/data-prepare-char)存的 `meta.pkl`。这就是为什么当时要多存那张「翻译表」——训练时要靠它知道「输出层该开 65 宽还是 50304 宽」。前后呼应上了。
</Callout>

模型起好后，还有 `model.to(device)`（搬到 GPU）、`GradScaler`、`torch.compile`、DDP 包装几步——它们都属于「怎么训得更快更省」，统一放到[进阶那站](https://daiw.org/manual/nanogpt/scaling-and-speed)细讲，这里先知道有这么回事。

准备工作里还剩一个大件没讲：**优化器**。`model.configure_optimizers(...)` 里藏着一个很值得一看的小设计——不是所有参数都一视同仁地做权重衰减。下一章专门拆它。👉 [优化器：哪些参数该做权重衰减](https://daiw.org/manual/nanogpt/optimizer)

## 参考文献

- 源码：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`）背后的算力最优思想。
