云计算百科
云计算领域专业知识百科平台

nanoGPT 逐行讲解

一、model.py 完整解析

model.py 是整个项目的核心,只有 330 行代码,却实现了完整的 GPT 模型。

1. LayerNorm(第18-27行)

class LayerNorm(nn.Module):
    def __init__(self, ndim, bias):
        super().__init__()
        self.weight = nn.Parameter(torch.ones(ndim))
        self.bias = nn.Parameter(torch.zeros(ndim)) if bias else None

    def forward(self, input):
        return F.layer_norm(input, self.weight.shape, self.weight, self.bias, 1e-5)
 

作用:层归一化,让每层的输出分布更稳定

关键点:
– `weight` 和 `bias` 是可学习参数
– PyTorch 的 `F.layer_norm` 默认不支持 bias=False,这里做了封装
– `1e-5` 是 epsilon,防止除零

 2. CausalSelfAttention(第29-76行)—— 最重要!

class CausalSelfAttention(nn.Module):
    def __init__(self, config):
        super().__init__()
        assert config.n_embd % config.n_head == 0
        
        # QKV 投影:一个线性层同时计算 Q, K, V
        self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias)
        # 输出投影
        self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias)
        # 正则化
        self.attn_dropout = nn.Dropout(config.dropout)
        self.resid_dropout = nn.Dropout(config.dropout)
        
        self.n_head = config.n_head
        self.n_embd = config.n_embd
        self.dropout = config.dropout
        
        # Flash Attention 检查(PyTorch >= 2.0)
        self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention')
        if not self.flash:
            # 因果掩码:确保只能看到左边的token
            self.register_buffer("bias", torch.tril(torch.ones(config.block_size, config.block_size))
                                        .view(1, 1, config.block_size, config.block_size))

    def forward(self, x):
        B, T, C = x.size()  # Batch, Time, Channel
        
        # 1. 投影得到 Q, K, V
        q, k, v = self.c_attn(x).split(self.n_embd, dim=2)
        
        # 2. 多头重塑:[B, T, C] → [B, heads, T, head_dim]
        k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        
        # 3. 计算注意力
        if self.flash:
            # 高效实现(PyTorch 2.0+)
            y = torch.nn.functional.scaled_dot_product_attention(
                q, k, v, attn_mask=None, 
                dropout_p=self.dropout if self.training else 0, 
                is_causal=True
            )
        else:
            # 手动实现
            att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
            att = att.masked_fill(self.bias[:,:,:T,:T] == 0, float('-inf'))
            att = F.softmax(att, dim=-1)
            att = self.attn_dropout(att)
            y = att @ v
        
        # 4. 合并多头输出
        y = y.transpose(1, 2).contiguous().view(B, T, C)
        
        # 5. 输出投影
        y = self.resid_dropout(self.c_proj(y))
        return y

数据流详解:
输入: [B, T, C]  (Batch, Sequence, Embedding Dim)
         │
         ▼
┌─────────────────────────────────────┐
│  c_attn: Linear(C → 3*C)            │
│  一次投影同时得到 Q, K, V            │
└─────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────┐
│  重塑为多头                         │
│  [B, T, 3*C] → [B, heads, T, hs]   │
│  其中 hs = C / heads                 │
└─────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────┐
│  Attention(Q, K, V)                 │
│  = softmax(QK^T / √hs) @ V          │
└─────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────┐
│  合并多头 + 输出投影                 │
│  [B, heads, T, hs] → [B, T, C]      │
└─────────────────────────────────────┘
 

为什么叫"Causal"(因果)?

因为掩码矩阵 `self.bias` 是下三角矩阵:

[[1, 0, 0, 0],
 [1, 1, 0, 0],
 [1, 1, 1, 0],
 [1, 1, 1, 1]]

这确保位置 i 只能看到位置 0~i,看不到未来。

 3. MLP / Feed-Forward Network(第78-92行)

class MLP(nn.Module):
    def __init__(self, config):
        super().__init__()
        # 输入投影:C → 4C
        self.c_fc    = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias)
        self.gelu    = nn.GELU()  # 激活函数
        # 输出投影:4C → C
        self.c_proj  = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias)
        self.dropout = nn.Dropout(config.dropout)

    def forward(self, x):
        x = self.c_fc(x)      # [B, T, C] → [B, T, 4C]
        x = self.gelu(x)      # GELU 激活
        x = self.c_proj(x)    # [B, T, 4C] → [B, T, C]
        x = self.dropout(x)
        return x

作用:逐位置处理,每个 token 独立变换

为什么是 4 倍?
– GPT-2 论文中的标准设置
– 扩大容量后再压缩,学习更复杂的特征

4. Block(第94-106行)—— Transformer 层

class Block(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.ln_1 = LayerNorm(config.n_embd, bias=config.bias)
        self.attn = CausalSelfAttention(config)
        self.ln_2 = LayerNorm(config.n_embd, bias=config.bias)
        self.mlp = MLP(config)

    def forward(self, x):
        # 预归一化 + 残差连接
        x = x + self.attn(self.ln_1(x))   # 注意力子层
        x = x + self.mlp(self.ln_2(x))    # FFN 子层
        return x

结构图示:
        x
        │
        ├─────────────────────────┐
        │                                                                     │
        ▼                                                                  ▼
    ┌───────┐                                      ┌───────┐
    │ Layer         │                                      │ Layer         │
    │ Norm         │                                      │ Norm         │
    └───────┘                                     └───────┘
        │                                                                  │
        ▼                                                                ▼
    ┌─────────┐                               ┌───────┐
    │ Attention         │                                │  MLP         │
    │ (Multi-              │                               │(FFN)         │
    │  Head)            │                               └───────┘
    └─────────┘                                    │
        │                                                            │
        └──────────┬───────────┘
                                      │
                                     ▼
                                 x (输出)
 

残差连接的作用:
– 梯度可以直通,缓解消失梯度问题
– 保留原始信息,神经网络只学"增量"

5. GPTConfig(第108-116行)

@dataclass
class GPTConfig:
    block_size: int = 1024      # 最大序列长度
    vocab_size: int = 50304     # GPT-2 词表大小 (50257 补齐到 64 的倍数)
    n_layer: int = 12           # Transformer 层数
    n_head: int = 12            # 注意力头数
    n_embd: int = 768           # 嵌入维度
    dropout: float = 0.0        #  dropout 率
    bias: bool = True           # 是否使用 bias

6. GPT 模型(第118-330行)

class GPT(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config

        # Transformer 主体
        self.transformer = nn.ModuleDict(dict(
            wte = nn.Embedding(config.vocab_size, config.n_embd),  # 词嵌入
            wpe = nn.Embedding(config.block_size, config.n_embd),  # 位置嵌入
            drop = nn.Dropout(config.dropout),
            h = nn.ModuleList([Block(config) for _ in range(config.n_layer)]),  # 多层 Block
            ln_f = LayerNorm(config.n_embd, bias=config.bias),  # 最终归一化
        ))
        
        # 语言模型头
        self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
        
        # Weight Tying:输出投影复用词嵌入权重
        self.transformer.wte.weight = self.lm_head.weight

        # 权重初始化
        self.apply(self._init_weights)
        for pn, p in self.named_parameters():
            if pn.endswith('c_proj.weight'):
                torch.nn.init.normal_(p, mean=0.0, std=0.02/math.sqrt(2 * config.n_layer))
```

**前向传播(第170-193行):**

```python
def forward(self, idx, targets=None):
    device = idx.device
    b, t = idx.size()
    
    # 位置编码
    pos = torch.arange(0, t, dtype=torch.long, device=device)
    
    # 1. Token Embedding + Position Embedding
    tok_emb = self.transformer.wte(idx)      # [B, T, C]
    pos_emb = self.transformer.wpe(pos)      # [T, C]
    x = self.transformer.drop(tok_emb + pos_emb)
    
    # 2. 通过所有 Transformer 层
    for block in self.transformer.h:
        x = block(x)
    
    # 3. 最终归一化
    x = self.transformer.ln_f(x)
    
    # 4. 输出 logits
    if targets is not None:
        # 训练模式:计算所有位置的损失
        logits = self.lm_head(x)
        loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
    else:
        # 推理模式:只取最后一个位置的 logits(效率优化)
        logits = self.lm_head(x[:, [-1], :])
        loss = None
    
    return logits, loss

关键优化:推理时只取最后一个位置

logits = self.lm_head(x[:, [-1], :])  # 只取最后一步
 

因为 GPT 是自回归模型,我们只需要预测下一个 token,前面的 position 的 logits 不用计算。

7. 生成函数(第305-330行)

@torch.no_grad()
def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None):
    for _ in range(max_new_tokens):
        # 1. 如果序列太长,只保留最后 block_size 个 token
        idx_cond = idx if idx.size(1) <= self.config.block_size else idx[:, -self.config.block_size:]
        
        # 2. 前向传播
        logits, _ = self(idx_cond)
        
        # 3. 取最后一个位置的 logits,除以 temperature
        logits = logits[:, -1, :] / temperature
        
        # 4. Top-k 裁剪
        if top_k is not None:
            v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
            logits[logits < v[:, [-1]]] = -float('Inf')
        
        # 5. Softmax 转概率
        probs = F.softmax(logits, dim=-1)
        
        # 6. 采样
        idx_next = torch.multinomial(probs, num_samples=1)
        
        # 7. 追加到序列
        idx = torch.cat((idx, idx_next), dim=1)
    
    return idx

采样策略:

| 参数                | 作用                                                    | 推荐值 |
|——————–|————————————————-|———–|
| `temperature` | 控制随机性,越低越保守                | 0.8-1.0 |
| `top_k`            | 只从概率最高的 k 个 token 中采样 | 50-200 |

二、train.py 完整解析

train.py 是训练循环,约 330 行。

1. 配置参数(第32-78行)

# 数据
dataset = 'openwebtext'
batch_size = 12           # 每个 GPU 的 batch size
block_size = 1024         # 序列长度
gradient_accumulation_steps = 5 * 8  # 梯度累积步数

# 模型
n_layer = 12
n_head = 12
n_embd = 768
dropout = 0.0

# 优化器
learning_rate = 6e-4
max_iters = 600000
weight_decay = 1e-1
beta1 = 0.9
beta2 = 0.95
grad_clip = 1.0

# 学习率调度
warmup_iters = 2000
lr_decay_iters = 600000
min_lr = 6e-5

2. 数据加载(第114-131行)

def get_batch(split):
    # 使用 memmap 避免内存泄漏
    if split == 'train':
        data = np.memmap(os.path.join(data_dir, 'train.bin'), dtype=np.uint16, mode='r')
    else:
        data = np.memmap(os.path.join(data_dir, 'val.bin'), dtype=np.uint16, mode='r')
    
    # 随机选择 batch_size 个起始位置
    ix = torch.randint(len(data) – block_size, (batch_size,))
    
    # 提取输入 x 和目标 y
    x = torch.stack([torch.from_numpy((data[i:i+block_size]).astype(np.int64)) for i in ix])
    y = torch.stack([torch.from_numpy((data[i+1:i+1+block_size]).astype(np.int64)) for i in ix])
    
    # 移到 GPU
    if device_type == 'cuda':
        x, y = x.pin_memory().to(device, non_blocking=True), y.pin_memory().to(device, non_blocking=True)
    else:
        x, y = x.to(device), y.to(device)
    
    return x, y

关键设计:
– `memmap`:内存映射文件,不占用实际内存
– `x` 和 `y` 的关系:y 是 x 右移一位(预测下一个 token)
– `pin_memory()`:加速 CPU→GPU 数据传输

3. 模型初始化(第146-193行)

# 三种初始化方式
if init_from == 'scratch':
    # 从零开始
    gptconf = GPTConfig(**model_args)
    model = GPT(gptconf)
elif init_from == 'resume':
    # 从检查点恢复
    checkpoint = torch.load(ckpt_path, map_location=device)
    model.load_state_dict(checkpoint['model'])
elif init_from.startswith('gpt2'):
    # 从 OpenAI 预训练权重加载
    model = GPT.from_pretrained(init_from, override_args)

 4. 损失估算(第215-228行)

@torch.no_grad()
def estimate_loss():
    out = {}
    model.eval()
    for split in ['train', 'val']:
        losses = torch.zeros(eval_iters)
        for k in range(eval_iters):
            X, Y = get_batch(split)
            with ctx:
                logits, loss = model(X, Y)
            losses[k] = loss.item()
        out[split] = losses.mean()
    model.train()
    return out

为什么用 `@torch.no_grad()`?
– 评估时不需要计算梯度,节省内存
– 速度更快

 5. 学习率调度(第230-242行)

def get_lr(it):
    # 1. 线性 warmup
    if it < warmup_iters:
        return learning_rate * (it + 1) / (warmup_iters + 1)
    
    # 2. 超过 decay_iters 后使用最小学习率
    if it > lr_decay_iters:
        return min_lr
    
    # 3. Cosine 衰减
    decay_ratio = (it – warmup_iters) / (lr_decay_iters – warmup_iters)
    coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))
    return min_lr + coeff * (learning_rate – min_lr)

学习率曲线:

lr
│    ╭─── warmup
│   ╱
│  ╱
│ ╱
╭─╯
│        ╲_________ cosine decay

└─────────────────────────── iter
     0   2000          600000

 6. 训练循环(第249-333行)

# 获取第一批数据
X, Y = get_batch('train')
t0 = time.time()

while True:
    # 1. 设置学习率
    lr = get_lr(iter_num)
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr
    
    # 2. 定期评估和保存
    if iter_num % eval_interval == 0 and master_process:
        losses = estimate_loss()
        print(f"step {iter_num}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
        # 保存最佳模型
        if losses['val'] < best_val_loss:
            best_val_loss = losses['val']
            torch.save(checkpoint, os.path.join(out_dir, 'ckpt.pt'))
    
    # 3. 前向 + 反向 + 更新
    for micro_step in range(gradient_accumulation_steps):
        if ddp:
            model.require_backward_grad_sync = (micro_step == gradient_accumulation_steps – 1)
        
        with ctx:
            logits, loss = model(X, Y)
            loss = loss / gradient_accumulation_steps  # 梯度累积
        
        # 异步预取下一批数据
        X, Y = get_batch('train')
        
        # 反向传播
        scaler.scale(loss).backward()
    
    # 4. 梯度裁剪
    if grad_clip != 0.0:
        scaler.unscale_(optimizer)
        torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip)
    
    # 5. 优化器步进
    scaler.step(optimizer)
    scaler.update()
    
    # 6. 清空梯度
    optimizer.zero_grad(set_to_none=True)
    
    # 7. 计时和日志
    t1 = time.time()
    dt = t1 – t0
    # … 打印 loss, time, mfu …
    
    iter_num += 1
    if iter_num > max_iters:
        break

 三、整体数据流图

                    训练流程
═══════════════════════════════════════════════════

  train.bin / val.bin (uint16 token IDs)
           │
           ▼
  ┌─────────────────┐
  │   get_batch()   │  ← 随机采样 block_size 个连续 token
  └────────┬────────┘
           │
           ▼
  ┌─────────────────┐
  │           GPT Model                │
  │  ┌───────────┐         │
  │  │        wte + wpe     │         │  ← Token + Position Embedding
  │  ├───────────┤         │
  │  │        Block × N      │         │  ← N 层 Transformer
  │  │           – Attn           │        │
  │  │           – FFN          │        │
  │  └───────────┘        │
  │                    │                       │
  │                   ▼                       │
  │              lm_head                  │  ← 投影到词表大小
  └────────┬────────┘
                           │
                          ▼
  ┌─────────────────┐
  │             CrossEntropy           │  ← 计算 loss
  │                    Loss                   │
  └────────┬────────┘
                           │
                          ▼
  ┌─────────────────┐
  │               Backward +            │  ← 反向传播
  │                Optimizer               │
  └────────┬────────┘
                           │
                          ▼
                   更新权重

 四、关键数字总结

| 参数 | GPT-2 (124M) | 说明 |
|——————-|————-|————————-|
| `vocab_size` | 50304   | 词表大小               |
| `n_embd`       | 768       | 嵌入维度               |
| `n_layer`         | 12         | Transformer 层数 |
| `n_head`         | 12        | 注意力头数           |
| `block_size`   | 1024    | 最大序列长度       |
| 参数量            | ~124M | 1.24 亿参数          |

计算量估算:

每 token FLOPs ≈ 6 × 参数量 = 6 × 124M ≈ 744M FLOPs

 五、学习建议

1. 先跑通:`python train.py config/train_shakespeare_char.py`
2. 读代码:逐行理解 model.py 的每个类
3. 改参数:尝试修改 n_layer, n_head, n_embd 观察变化
4. 加功能:尝试加入新的层或修改 attention 机制
 

赞(0)
未经允许不得转载:网硕互联帮助中心 » nanoGPT 逐行讲解
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!