Star
模型网络3 个知识点

前馈网络与归一化

阅读原文 教学代码
This article is written in Chinese. The original equations, source links, and code remain unchanged.
本页目录

FFN 在每个 token 上共享一组特征变换参数,注意力负责跨位置汇聚;归一化控制输入尺度,残差提供直接传递路径。这些机制会组合出现,但参数预算、归一化统计和梯度通路必须分别推导,不能用“采用新架构”解释所有性能差异。Transformer 的子层与逐位置FFN

前置学习:Transformer与自回归生成。完整示例:ffn-norm.py。

学习目标

  • 写出 SwiGLU 的两输入分支,核算等参数预算需要的宽度变化。
  • 推导 RMSNorm 的统计量与雅可比,并构造与 LayerNorm 不同的平移反例。
  • 比较 Pre-Norm、Post-Norm 的链式法则,识别残差直通项的条件与局限。

前置知识

输入 XX 为 [B,T,d], mm 为 FFN 中间宽度。本文数学使用列向量,线性矩阵按 [out,in] 存储,与示例一致。 ⊙\odot 是逐元素乘法; JFJ_F 是函数 FF 对输入的雅可比; ϵ\epsilon 是归一化分母中的小正数,不是训练学习率。三个知识点都只做小型确定性计算,没有自动求导或真实网络训练。

基本机制

普通无偏置 FFN 为 W2ϕ(W1x)W_2\phi(W_1x) ,门控 FFN 把一个投影的激活与另一投影逐元素相乘,再投影回隐藏宽度。LLaMA 论文采用 Pre-Norm、RMSNorm 和 SwiGLU;该版本 Transformers 的实现分别落在 LlamaDecoderLayer、LlamaRMSNorm、LlamaMLP。读取时要核对 hidden_act、中间宽度和 bias 配置,不能仅依据类名推断模型必然使用某个固定激活。LLaMA 架构说明 LlamaMLP 与解码层

推导或示例

知识点一:SwiGLU 的门控计算与等参数宽度

问题与机制。 SwiGLU 不是给原 FFN 的激活换一个名字,它增加了一个输入投影。设 Wg,Wu∈Rm×d,Wd∈Rd×mW_g,W_u\in\mathbb R^{m\times d},W_d\in\mathbb R^{d\times m} ,则

g=Wgx,u=Wux,SiLU(g)=g⊙σ(g),h=SiLU(g)⊙u,y=Wdh.g=W_gx,\quad u=W_ux,\quad \mathrm{SiLU}(g)=g\odot\sigma(g),\quad h=\mathrm{SiLU}(g)\odot u,\quad y=W_dh.

普通两矩阵 FFN 的无偏置参数量是 2dm2dm ;上述门控版本为 3dm3dm 。要与宽度 m0m_0 的普通 FFN 等参数,需 m=2m0/3m=2m_0/3 。若 m0=4dm_0=4d ,则 m=8d/3m=8d/3 ,实际模型往往再按实现要求取整。这个预算是矩阵元素数,尚不包括归一化、偏置或对齐填充。GLU变体的公式与等预算比较

def swiglu(x, gate_matrix, up_matrix, down_matrix):
    """单token无偏置SwiGLU;返回两分支、门控激活、输出与参数量。"""
    if not x or len(gate_matrix) != len(up_matrix) or len(down_matrix) != len(x):
        raise ValueError("分支宽度与输出宽度须匹配")
    gate, up = _linear(gate_matrix, x), _linear(up_matrix, x)
    hidden = [g * _sigmoid(g) * u for g, u in zip(gate, up)]
    output = _linear(down_matrix, hidden)
    return {"gate": gate, "up": up, "hidden": hidden, "output": output,
            "parameters": 3 * len(x) * len(gate), "hidden_elements": len(hidden)}

完整文件中 _linear 做矩阵乘向量,_sigmoid 用正负分支避免无必要的指数上溢。取 x=[1,−1]x=[1,-1] ,三个矩阵都为二维单位矩阵,得到 g=u=[1,−1]g=u=[1,-1] 、 h≈[0.731059,0.268941]h\approx[0.731059,0.268941] ,输出相同。第二项为正,是负的 SiLU 值乘负的上投影值;这说明门控不是只能把非负激活乘一个零到一概率的简单开关。

参数与激活还需分开: d=512,m0=2048d=512,m_0=2048 的普通 FFN 有2097152个矩阵参数;在同宽2048直接加门控则为3145728,增加一半。若 B=2,T=1024,m=2048B=2,T=1024,m=2048 ,一个中间分支有4194304个元素;改变 T 会改变这个激活数,不会改变共享矩阵的参数量。反向保存、融合及重计算会改变真实峰值,不能把两条分支的元素和当成最终显存。

flowchart LR
    X["单token隐藏向量"] --> G["门控投影"]
    X --> U["上投影"]
    G --> S["逐元素SiLU"]
    S --> M["两分支逐元素相乘"]
    U --> M
    M --> D["下投影回隐藏宽度"]

源码定位。 该版本 LlamaMLP.forward 明确执行 down_proj(act_fn(gate_proj(x))*up_proj(x));nanoGPT.MLP 则是两矩阵 GELU 路径,用于区分结构。LlamaMLP 三投影计算 nanoGPT MLP 两投影计算 LLaMA 论文明确提到用 2/3×4d2/3\times4d 宽度,和上述参数推导一致。

条件与反例。 示例无偏置且门控为 SiLU,输入维度必须匹配。把乘法改成相加、或把激活应用到两条分支,都是不同函数。固定参数量也不意味着相同训练显存和吞吐;要比较质量,仍需固定数据、优化器、训练 token 和其他层结构。

知识点二:RMSNorm 保持尺度而不去均值

问题与机制。 RMSNorm 与 LayerNorm 到底少了什么?对 x∈Rdx\in\mathbb R^d ,定义 μ=d−1∑ixi\mu=d^{-1}\sum_i x_i 、 s=d−1∑ixi2+ϵs=\sqrt{d^{-1}\sum_i x_i^2+\epsilon} 。单位增益、零偏置时

RMSNorm(x)i=xi/s,LayerNorm(x)i=(xi−μ)/d−1∑j(xj−μ)2+ϵ.\mathrm{RMSNorm}(x)_i=x_i/s,\qquad \mathrm{LayerNorm}(x)_i=(x_i-\mu)/\sqrt{d^{-1}\sum_j(x_j-\mu)^2+\epsilon}.

平方均值满足 E[x2]=Var(x)+μ2E[x^2]=\mathrm{Var}(x)+\mu^2 ;除非均值为零,两种分母也不同。对 x=[1,3]x=[1,3] 且暂取 ϵ=0\epsilon=0 ,RMSNorm 是 [1/sqrt5,3/sqrt5],LayerNorm 是 [-1,1]。平移到 [3,5] 后 LayerNorm 不变,RMSNorm 改变。RMSNorm 定义与不变性

推导雅可比: ∂s/∂xj=xj/(ds)\partial s/\partial x_j=x_j/(ds) ,因此

∂(xi/s)∂xj=δijs−xixjds3,JN(x)=Is−xxTds3.\frac{\partial (x_i/s)}{\partial x_j}=\frac{\delta_{ij}}s-\frac{x_ix_j}{ds^3},\qquad J_N(x)=\frac Is-\frac{xx^T}{ds^3}.

若有逐通道增益 γ\gamma ,左乘 diag(γ)\mathrm{diag}(\gamma) ;示例把增益固定为1,使计算更容易核对。径向作用 JNx=ϵx/s3J_Nx=\epsilon x/s^3 ,所以 ϵ=0\epsilon=0 时缩放方向被消去,但保留正 epsilon 后只近似如此。这也是“缩放不变”必须注明 epsilon 条件的原因。

def norm_statistics(x, eps=1e-6):
    """计算单位增益的RMSNorm、LayerNorm及RMSNorm解析雅可比。"""
    if not x or not math.isfinite(eps) or eps < 0 or not all(math.isfinite(a) for a in x):
        raise ValueError("输入须有限非空且epsilon非负")
    n = len(x)
    mean = sum(x) / n
    square_mean = sum(a * a for a in x) / n
    variance = sum((a - mean) ** 2 for a in x) / n
    if square_mean + eps <= 0 or variance + eps <= 0:
        raise ValueError("归一化分母不能为零")
    scale = math.sqrt(square_mean + eps)
    rms = [a / scale for a in x]
    layer = [(a - mean) / math.sqrt(variance + eps) for a in x]
    jacobian = [[(1.0 if i == j else 0.0) / scale - x[i] * x[j] / (n * scale**3)
                 for j in range(n)] for i in range(n)]
    return {"mean": mean, "square_mean": square_mean, "variance": variance,
            "rmsnorm": rms, "layernorm": layer, "jacobian": jacobian}

函数同时返回均值、平方均值、方差、两种输出及解析雅可比,便于将统计量错误与梯度错误分开。对 [1,3] 可手算分母;对全零输入,正 epsilon 保证分母非零且输出零。若 epsilon 设为零而输入常量,LayerNorm 分母为零;因为函数同时比较两种归一化,所以会拒绝该输入。

flowchart LR
    X["当前token全部隐藏通道"] --> Q["平方均值加epsilon"]
    Q --> S["求平方根得到尺度"]
    X --> D["逐通道除以尺度"]
    S --> D
    D --> G["生产实现乘可学习增益"]
    S --> J["结合输入外积构造雅可比"]
    X --> J

源码定位。 该版本 LlamaRMSNorm.forward 先升到 float32 算平方均值和逆平方根,恢复输入 dtype 后乘权重;变量名 variance 在这里装的是平方均值,不是减均值后的方差。LlamaRMSNorm.forward 梯度形式对应 RMSNorm 梯度分析。

条件与反例。 正比例缩放且忽略 epsilon 时 N(ax)=N(x)N(ax)=N(x) ;负比例会翻转符号,不满足同一个等式。每个通道各乘不同系数也不具有统一尺度不变性。统计量是在单个 token 的隐藏维求得,与 batch 统计无关;不能把其训练/推理行为照搬 BatchNorm 的运行均值逻辑。

知识点三:归一化位置改变残差雅可比

问题与机制。 为什么 Pre-Norm 的残差常被称为直接通路?设子层为 FF ,归一化为 NN 。两种连接为

ypre=x+F(N(x)),ypost=N(x+F(x)).y_{pre}=x+F(N(x)),\qquad y_{post}=N(x+F(x)).

按链式法则,

Jpre=I+JF(N(x))JN(x),Jpost=JN(x+F(x))(I+JF(x)).J_{pre}=I+J_F(N(x))J_N(x),\qquad J_{post}=J_N(x+F(x))(I+J_F(x)).

Pre-Norm 在这一层的 Jacobian 中有裸的 II ;Post-Norm 的整条局部通路都经过归一化。为可核算起见令 F(x)=AxF(x)=Ax ,于是 JF=AJ_F=A ,无需自动微分即可比较。这里为了隔离“位置”,两者统一使用 RMSNorm;原 Transformer 使用 LayerNorm 的 Post-Norm,不能把此教学对照称为原模型重现。Transformer Post-Norm 结构 LLaMA Pre-Norm 结构

def residual_paths(x, matrix, eps=1e-6):
    """以线性子层F(x)=Ax比较前归一化、后归一化输出与雅可比。"""
    n = len(x)
    if not n or len(matrix) != n or any(len(row) != n for row in matrix):
        raise ValueError("子层矩阵须为输入宽度的方阵")
    norm_x = norm_statistics(x, eps)
    pre = [a + b for a, b in zip(x, _linear(matrix, norm_x['rmsnorm']))]
    inner = [a + b for a, b in zip(x, _linear(matrix, x))]
    norm_inner = norm_statistics(inner, eps)
    identity_plus_a = [[float(i == j) + matrix[i][j] for j in range(n)] for i in range(n)]
    pre_jac = [[float(i == j) + sum(matrix[i][k] * norm_x['jacobian'][k][j] for k in range(n))
                for j in range(n)] for i in range(n)]
    post_jac = [[sum(norm_inner['jacobian'][i][k] * identity_plus_a[k][j] for k in range(n))
                 for j in range(n)] for i in range(n)]
    return {"pre": pre, "post": norm_inner['rmsnorm'], "pre_jacobian": pre_jac,
            "post_jacobian": post_jac}

把 AA 设为全零,输入 [1,3]。Pre-Norm 输出保留 [1,3] 且雅可比为单位阵;Post-Norm 输出约 [0.447214,1.341641],其雅可比是上一知识点的 JNJ_N 。因此即便子层没有提供任何变换,两种残差块仍不同。再用非零对角 A 检查矩阵乘法顺序,通常 AJN≠JNAAJ_N\ne J_NA ,交换两者会改变反向信号。

flowchart LR
    X["同一输入与线性子层"] --> N["前归一化分支"]
    N --> F["先归一化再过子层"]
    F --> A["加回原始输入"]
    X --> B["后归一化分支"]
    B --> C["子层输出先与输入相加"]
    C --> D["对残差和归一化"]
    A --> J["分别核算链式雅可比"]
    D --> J

源码定位。 LlamaDecoderLayer.forward 在 attention 前做 input_layernorm,相加后再以该和为下一残差,MLP 前做 post_attention_layernorm。第二个名字中的 post-attention 指位于注意力之后,它仍然是 MLP 的 Pre-Norm,不能按变量名误判成 Post-Norm 架构。LlamaDecoderLayer.forward 的两次残差 nanoGPT 的 Block.forward 也可直接看到同类次序。nanoGPT Block.forward

条件与反例。 局部有单位项不等于完整深层网络梯度永不消失或爆炸,多个雅可比相乘、最终归一化、优化器和初始化仍重要。Post-Norm 也不是必然不能训练。比较训练稳定性时不能同时改变宽度、激活、学习率和归一化位置后只归因其中一项。

适用条件与限制

解析示例不模拟混合精度、融合内核、激活重计算或优化器状态。RMSNorm 原论文的运行时间改善属于其任务与实现条件,本页没有独立测量,因此不报告通用加速比例。标准库中的 double 精度也不能替代 float16/bfloat16 对溢出和舍入的验证。

实践任务

交付普通 FFN 与 SwiGLU 的参数/中间激活预算,分别只改变 B、T、d、m;记录 RMSNorm 与 LayerNorm 的平移及缩放对照;用零矩阵、对角矩阵和非对称矩阵核算两种残差雅可比。常驻 UT 可用有限差分检查解析雅可比,本组未运行该检查。真实 Pre/Post-Norm 训练对照仍为已设计。

掌握标准

能说明门控新增哪一个矩阵、为什么等参数需改宽度;能区分方差与平方均值;能指出 epsilon 对严格不变性的影响;能写对两个雅可比的乘法顺序。能构造零子层反例,且不会把一个局部代数性质直接提升为任意深度的训练结论。

源码与后续阅读

证据包括 Transformer、LLaMA、GLU Variants 和 RMSNorm 原论文,以及固定提交下的 Llama、nanoGPT 实现。本页未独立复现任一大型模型或论文质量结果。下一步可读预训练目标与数据,连接结构计算与训练损失。

2026研究进展

MoSE在专家内部使用嵌套宽度,说明FFN中间维度也可以成为条件计算的控制点。理解本页的前馈计算后,可以进一步比较“选哪些专家”和“每个专家用多少宽度”。多宽度训练是方法的一部分,不能把普通模型直接切片视为已实现论文。 会议来源与读取范围

更多公式、代码示例、结构图及当前开源版本见模型网络研究进展。新内容未执行真实模型实验。

教学示例帮助理解机制。真实模型、框架与设备实验,请结合文章中的条件和实践任务分别验证。

发现新的理解,或需要修正的地方?一起完善这篇笔记 ↗
试试 Transformer、RAG、Codex 或 KV 缓存