# 其余三个零件 · LayerNorm、MLP、Block

> 啃完注意力，剩下三个零件轻松得像甜点。逐行读 nanoGPT 的 LayerNorm（为什么自己写）、MLP（先胀 4 倍再压回）、Block（Pre-LN + 残差怎么把注意力和 MLP 拼成一层）。

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

# 其余三个零件 · LayerNorm、MLP、Block

[注意力](https://daiw.org/manual/nanogpt/attention)是五个零件里最硬的骨头，啃完了。剩下三个——`LayerNorm`、`MLP`、`Block`——简单得多。这一章一口气收拾掉，你会发现它们干净利落，读完就集齐了拼一个 `GPT` 的全部积木。

## LayerNorm：给数据「校准量纲」

先看 nanoGPT 自己写的 `LayerNorm`（`model.py`，约 18–27 行）：

```python
class LayerNorm(nn.Module):
    """ LayerNorm but with an optional bias. PyTorch doesn't support simply bias=False """

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

**它在干嘛？** 归一化——把每个 token 向量，沿着特征维**减均值、除标准差**，拉回「零均值、单位方差」，再用一个可学习的 `weight`（缩放）和 `bias`（平移）调整回合适的范围。好处是：各层数值的「量纲」被稳住，梯度不容易爆或消，训练更顺。

逐行看：

- `self.weight = nn.Parameter(torch.ones(ndim))`：缩放系数，初始化为全 1（一开始「不缩放」）。
- `self.bias = ... if bias else None`：平移量，初始为全 0；**`bias=False` 时干脆设成 `None`**。
- `forward` 里直接调 PyTorch 的 `F.layer_norm`，最后那个 `1e-5` 是防止除零的 `eps`。

**为什么要自己写，不直接用 `nn.LayerNorm`？** 注释第一行就抱怨了：PyTorch 自带的 `nn.LayerNorm` **不支持简单地关掉 bias**。而 nanoGPT 想让 `bias` 成为一个可配的开关（[我们在 GPTConfig 里见过](https://daiw.org/manual/nanogpt/model-overview) `bias: bool`），所以只好手搓一个「bias 可选」的版本。**又一个「为了一个小小的可配置性，宁可自己写」的务实细节。**

## MLP：先胀 4 倍，再压回来

`MLP` 是每个 Block 里负责「逐位置深加工」的前馈网络（`model.py`，约 78–92 行）：

```python
class MLP(nn.Module):

    def __init__(self, config):
        super().__init__()
        self.c_fc    = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias)
        self.gelu    = nn.GELU()
        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)
        x = self.gelu(x)
        x = self.c_proj(x)
        x = self.dropout(x)
        return x
```

它的形状变化是一个漂亮的「**先胀后缩**」：

- `c_fc`：把 `n_embd` 维**放大 4 倍**到 `4 * n_embd`（默认 768 → 3072）。
- `gelu`：一个非线性激活（GELU，比 ReLU 更平滑）。**非线性是关键**——没有它，两个线性层叠起来还是线性，白搭。
- `c_proj`：再**压回** `n_embd`(3072 → 768)。
- `dropout`：随机失活，抗过拟合。

**为什么先放大再缩小？** 打个比方：把信息**摊到一个更大的空间里**，才有足够的「工作台面」做复杂的非线性变换，加工完再收拢回原尺寸。这个「4 倍」是 Transformer 沿用至今的惯例。注意力负责「跨位置」交换信息，MLP 负责「单个位置」把自己的向量揉得更有表达力——**一横一纵，分工明确**。

<Callout type="info">
  还记得[上一个专栏](https://daiw.org/manual/llm-anatomy/moe-dense-vs-sparse)说「前馈层是参数大头、也是 MoE 动刀的地方」吗？看这里就懂了：`c_fc` 和 `c_proj` 两个 `4×` 的大矩阵，正是整个 Block 里最占参数的部分。真实大模型的 MoE，拆的就是这个 `MLP`——把它换成一排专家。而 nanoGPT 里，它就是老老实实的一个稠密 MLP。
</Callout>

## Block：两行 forward，拼出一层 Transformer

最后，`Block` 把注意力和 MLP 组装成「一层」（`model.py`，约 94–106 行）：

```python
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))
        return x
```

`__init__` 备齐四个零件：两个 `LayerNorm`、一个注意力、一个 MLP。精华全在 `forward` 那**两行**——[模型全景那章](https://daiw.org/manual/nanogpt/model-overview)已经点过，这里对着真实代码再钉一遍：

- `x = x + self.attn(self.ln_1(x))`：先归一化 `ln_1(x)`，再进注意力，输出**加回** `x`。
- `x = x + self.mlp(self.ln_2(x))`：先归一化 `ln_2(x)`，再进 MLP，输出**加回** `x`。

两个关键点：

1. **Pre-LN**：归一化在子层**前面**（`ln_1(x)` 先算，再喂给 `attn`）。这比原始 Transformer 的 Post-LN 训练更稳，是现代 GPT 的标配。
2. **残差 `x + ...`**：子层输出是**加**到 `x` 上，不是替换。这条「加法高速路」让梯度能直通到底，深层网络也学得动。

<Callout type="warn">
  这两个设计——**Pre-LN 的位置**、**RMSNorm 还是 LayerNorm**——正是[《AI 大模型解构》里专门对比过的一整章](https://daiw.org/manual/llm-anatomy/normalization)。nanoGPT 用的是「LayerNorm + Pre-LN」这个 GPT-2 经典组合；而 Llama、DeepSeek 们把 LayerNorm 换成了 RMSNorm，OLMo 2 甚至把 Pre 换回了 Post。**你在这里读到的每个小设计，到了真实大模型那儿，都是一个可以拨动的旋钮。**
</Callout>

## 五个零件，集齐了

盘一下：`LayerNorm`（校准）、`CausalSelfAttention`（跨位置看）、`MLP`（单位置加工）、`Block`（把后两个拼成一层）。加上马上要讲的 `GPT`（把 `Block` 叠 N 层、配上入口出口），[model.py 的五个类](https://daiw.org/manual/nanogpt/model-overview)就全了。

积木备齐，下一章我们动手**搭整台机器**：`GPT` 类怎么把词嵌入、一摞 `Block`、输出层拼起来，前向传播怎么从 token 编号一路算到 loss，以及那个「权重绑定」和「推理期只算最后一个位置」的小机灵。👉 [搭起整台机器：GPT 的 init 与 forward](https://daiw.org/manual/nanogpt/gpt-assembly)

## 参考文献

- [1] Ba et al. “Layer Normalization.” 2016. arXiv:1607.06450. —— LayerNorm。
- [2] Hendrycks & Gimpel. “Gaussian Error Linear Units (GELUs).” 2016. arXiv:1606.08415. —— GELU 激活。
- [3] He et al. “Deep Residual Learning for Image Recognition.” CVPR 2016. arXiv:1512.03385. —— 残差连接。
- [4] Xiong et al. “On Layer Normalization in the Transformer Architecture.” ICML 2020. arXiv:2002.04745. —— Pre-LN 的稳定性。
- 源码：Andrej Karpathy, nanoGPT, `model.py`. https://github.com/karpathy/nanoGPT
