大模型激活函数全解析:从GELU到SwiGLU的选型与工程实践
做 LLM 训练或推理优化的人大概率都经历过这样一幕模型结构、数据、优化器都照着开源项目抄好了跑起来却总觉得哪里不对劲Loss 曲线毛毛糙糙或者明明参数量一样吞吐和显存却跟别人差一大截。这时候很少有人会想到问题可能出在一个最不起眼的小零件上——大模型激活函数。激活函数看似只是网络里一个固定的非线性映射但它在 Transformer 的 FFN 层里承担的责任、带来的显存开销、以及在低精度训练下的数值风险远比很多人以为的重要。这篇文章就是围绕“大模型激活函数”这件事把我实际训练、调优、推理部署过程中积累的理解和坑一次性梳理清楚。适合正在训练自己的小模型、准备换一个 FFN 结构、或者对开源模型里层出不穷的SiLU、GELU、SwiGLU感到困惑的开发者。我不打算堆公式而是把每个选择背后的“为什么”讲透再给可以直接抄走的实现和排查经验。1. 先说结论激活函数在大模型里到底是干嘛的1.1 激活函数只活在两个地方很多人一听到“激活函数”首先想到的是卷积网络里的 ReLU。但在大模型里激活函数的存在感要集中得多基本只出现在两个位置一个是FFN前馈网络中间的隐藏层另一个是注意力里的 softmax。softmax 因为性质特殊通常被单独讨论真正意义上决定大模型训练行为和推理成本的激活函数几乎全部集中在 FFN 内部。为什么因为 Transformer 里 Attention 负责的是“把信息从别的位置搬过来”而真正对每个 token 做非线性变换、把特征空间旋转和拉伸的是 FFN。一个标准的 FFN 长这样h Activation(x · W1) · W2输入先升维到 3-4 倍宽度过一遍非线性再降维回来。大模型里 60% 到 70% 的参数都堆在这个模块里每次前向传播的 FLOPs 大头也在这里。所以激活函数选得怎么样直接决定了这个模型能不能训得动、训得稳、跑得快。1.2 一个“固定小零件”为什么影响全局如果我们把整个网络里的非线性全部抽掉那不管堆多少层线性层最终都等价于一次线性变换模型表达能力瞬间塌掉。激活函数就是那个“不允许网络塌成一张纸”的结构件。但在大模型这个规模上它的影响远不止“提供非线性”这么简单。我实际对比过几组实验同一个 1B 参数的模型只把 FFN 的激活从 GELU 换成 SwiGLU最终 loss 能低一截但训练吞吐也掉了换成某些带饱和区的激活前期收敛倒是快后期 loss 就是压不下去。这说明激活函数至少同时在四个层面起作用梯度行为负半轴的斜率、饱和区的梯度衰减速度决定了优化器在深层网络里能不能把信息传回去输出分布激活后的值域、稀疏程度、是否容易出现离群大值直接影响后续层的数值范围和归一化稳定性硬件效率exp、tanh、erf这些超越函数在 GPU 上的代价不同低精度下的实现方式也不一样显存占用反向传播时要保存激活值FFN 中间张量是大头激活函数类型直接决定这部分显存大小。所以别再把激活函数当成“模型里随便填一个就行”的参数了。它就是那种平时不吭声、一换就出事的零件。2. 从 ReLU 到 GELU前 LLM 时代的两次关键迭代2.1 ReLU 的贡献和它的暗面ReLU也就是max(0, x)是整个深度学习兴起的最强功臣。它的优势极其朴素计算只要一次比较梯度在正半轴恒为 1不存在 sigmoid/tanh 那种两边饱和导致梯度消失的问题。所以 2012 年之后的视觉模型、早期的序列模型基本都被 ReLU 统治。但 ReLU 有一个臭名昭著的毛病神经元死亡dying ReLU。如果一个神经元的输入长期落在负半轴它的梯度就是 0权重再也不更新这个神经元就永久“死”掉了。在小模型里这个问题还能靠调学习率、加初始化技巧硬扛但在动辄几千亿参数的大模型里死一片神经元等于浪费一大块模型容量这是不可接受的。更微妙的问题是 ReLU 在零点处“硬转折”。这个不可导点让 loss 曲面变得很锋利优化器在后期需要非常小心地控制步长。用我自己的话说ReLU 像一个脾气很冲的开关要么全开要么全关训大模型时这种性格不太讨喜。2.2 GELU 的数学原理与工程近似GELUGaussian Error Linear Unit的出发点很有意思它不是硬性把负值截断而是给每个输入乘上一个 0 到 1 之间的“概率门”。公式长这样GELU(x) x · Φ(x)其中 Φ(x) 是标准正态分布的累积分布函数。x 越大门越接近 1x 越小门越接近 0。它保留了 ReLU 的“正半轴近似线性”特性但负半轴不是完全死寂而是有一个平滑的、微小的负输出意味着梯度永远有机会流动。问题在于 Φ(x) 涉及误差函数 erf在 GPU 上直接算不太划算。所以工程上绝大多数实现用的是近似公式GELU(x) ≈ 0.5 · x · (1 tanh(√(2/π) · (x 0.044715 · x³)))这个近似误差极小在 fp16 精度下完全够用。还有一个更简洁的近似x · sigmoid(1.702x)误差稍微大一点但胜在只依赖 sigmoid某些硬件加速库会这么用。2.3 为什么 BERT、GPT-2 时代选择了 GELU回头看历史BERT 系列和 GPT-2 系列都把 FFN 的激活从 ReLU 换成了 GELU这个选择不是拍脑袋。我在自己的实验里观察到几个实际原因第一GELU 更光滑。光滑的非线性让 loss 地形更平缓同样学习率下训练曲线更稳定不容易出现 loss 突然尖刺的情况。第二负半轴小梯度提供了“软纠错”能力。ReLU 一旦把神经元死掉就是永久性的GELU 则永远留了一条缝等特征分布变化后这个神经元还能“活过来”。第三GELU 可以理解成一种确定性的 dropout它按输入的相对大小对神经元做软性保留这在 Transformer 这种对正则化敏感的结构里副作用比随机 dropout 更可控。那个时代还有一个细节BERT 的 FFN 中间维度是 3072是隐层 768 的四倍。4 倍扩展是经验值因为激活函数的非线性会压缩信息密度宽度不够就无法充分发挥升维再降维的“记忆回忆”能力。到今天大部分不是 GLU 系的模型依然沿用 4 倍这个比例。3. SwiGLU 是如何接管大模型 FFN 的3.1 GLU 门控从卷积到 Transformer 的跨界GLUGated Linear Unit最早是 2017 年在一篇关于卷积序列建模的论文里提出来的。它的核心思想是给线性变换的输出加一个“门控通道”让网络自己学习什么信息该放行、什么信息该抑制。形式如下GLU(x) σ(W1 · x) ⊙ (W2 · x)σ 是 sigmoid⊙ 是逐元素相乘。左边那个σ(W1·x)就是“门”右边W2·x是“值”两者逐元素相乘得到最终输出。你可以把它想象成一个水龙头门决定流量值决定水的内容。相比单纯的非线性激活这种乘法交互让 FFN 的表达能力更强因为它允许不同维度之间互相调制而不是每个维度独立做标量变换。这套机制后来被证明和 Transformer 的 FFN 是绝配。原因也不难理解Attention 已经在做“选择重要 token”这件事FFN 如果能做“选择重要特征”整个模型的建模粒度就更加统一。3.2 SwiGLU 的数学形式和直觉SwiGLU 就是把 GLU 里的 sigmoid 门换成了 Swish 门。Swish 也叫 SiLU形式是x · sigmoid(x)它和 GELU 一样光滑但计算更简单而且在负半轴存在一个“先降后升”的凹陷这个凹陷让 Swish 具备一定的正则化效果。于是SwiGLU(x) (Swish(W1 · x)) ⊙ (W3 · x) (W1 · x) · σ(W1 · x) ⊙ (W3 · x)注意这里用了两个权重矩阵 W1 和 W3它们都从同一个输入 x 投影出来一个做门一个做值。外面再接一个 W2 把结果投影回原始维度。实际写出来就是FFN(x) W2 · ( Swish(x · W1) ⊙ (x · W3) )直觉上Swish 门比 sigmoid 门“温柔”。sigmoid 在输入绝对值较大时容易饱和梯度直接归零Swish 在正半轴没有饱和区门值可以大于 1梯度能一直传下去。这让 SwiGLU 在深层网络里比原始 GLU 更好训。顺便说一句把 Swish 换成 GELU 就是 GeGLU换成 ReLU 就是 ReGLU它们在很多数据集上效果非常接近。实践中之所以 SwiGLU 最流行很大程度上是因为 Swish 实现最简单、GPU 上算得快、低精度下数值最稳。3.3 参数量的账2 个线性层变 3 个这是换激活函数时最容易忽略的坑。标准 FFN 只需要两个权重矩阵升维矩阵 W1形状d → d_ff和降维矩阵 W2形状d_ff → d。而 SwiGLU 需要三个矩阵门矩阵、值矩阵、降维矩阵参数直接变成 1.5 倍。那为什么主流模型还愿意用它因为门控带来的效果提升值得这个代价而且可以通过缩小中间维度把参数量拉回原来的水平。具体算法是这样的标准 FFN 通常取d_ff 4d参数量是2 × d × 4d 8d²。SwiGLU 如果中间维度取d_ff (8/3)d参数量就是3 × d × (8/3)d 8d²和标准 FFN 完全持平。举一个实际例子某个 7B 级模型隐层维度 4096标准 FFN 中间维度应该是 16384但它用了 SwiGLU 后中间维度取 11008恰好就是 4096 的 2.69 倍非常接近 8/3。这说明模型设计者在选维度时是算过这笔账的。FLOPs 方面也可以验证每条 token 在标准 FFN 里大概是16d²次浮点运算SwiGLU 配 8/3 中间维度也是16d²所以“性价比”是持平的。3.4 主流模型怎么用 SwiGLU现在开源生态里从 LLaMA 系列到之后大量追随者FFN 几乎被 SwiGLU 统一了。PaLM 用的也是 SwiGLUMistral、Qwen 这些后续模型基本都沿用了这个结构只是中间维度的具体数值稍有出入。再往前看GPT-3 那一代用的还是 GELU所以“GELU 时代”和“SwiGLU 时代”的分界线其实非常清晰大概就是 2022 年前后。还有一个值得留意的趋势最近有一些工作尝试用ReLU²也就是(ReLU(x))²替代 SwiGLU因为它在推理时不需要计算 exp可以把 FFN 里的门控矩阵权重融合掉减少一半的矩阵乘对推理吞吐非常友好。我实测过类似方案在 1B 左右规模下 loss 差距很小所以“SwiGLU 永远最优”这种话别急着下硬件约束会在某个阶段重新改变选择。4. 工程实现手写 SwiGLU 与数值稳定性4.1 PyTorch 里的两种写法SwiGLU 的实现本身不复杂但工程上有两种风格我建议你根据需求二选一。第一种是“三矩阵分开写”结构清楚适合做研究和调试import torch import torch.nn.functional as F from torch import nn class SwiGLUFFN(nn.Module): def __init__(self, hidden: int, intermediate: int): super().__init__() self.gate_proj nn.Linear(hidden, intermediate, biasFalse) self.up_proj nn.Linear(hidden, intermediate, biasFalse) self.down_proj nn.Linear(intermediate, hidden, biasFalse) def forward(self, x): gate self.gate_proj(x) up self.up_proj(x) return self.down_proj(F.silu(gate) * up)第二种是“门和值合并成一个矩阵”也就是把 gate 和 up 的权重按最后一维拼接成一个2 * intermediate的大矩阵一次 GEMM 出两个结果然后chunk拆开class FusedSwiGLUFFN(nn.Module): def __init__(self, hidden: int, intermediate: int): super().__init__() self.gate_up_proj nn.Linear(hidden, 2 * intermediate, biasFalse) self.down_proj nn.Linear(intermediate, hidden, biasFalse) def forward(self, x): gate_up self.gate_up_proj(x) # [B, T, 2 * intermediate] gate, up gate_up.chunk(2, dim-1) return self.down_proj(F.silu(gate) * up)两种写法数学上完全等价实际效果也一致。第二种的好处是少发一次 GEMM kernel在 Tensor Parallel 场景下也更方便——gate_up_proj做成列并行down_proj做成行并行中间只要一次 all-reduce。很多开源推理框架内部也是这种融合布局。4.2 fp16 下的精度陷阱如果你用 fp16 训练SwiGLU 比 GELU 更容易踩数值坑原因出在 sigmoid 上。fp16 的最大表示范围只有 65504一旦x · W1出来的值超过这个范围sigmoid 算出来就是 inf反向传播直接变 NaN。更隐蔽的问题是精度损失。sigmoid 在输入大于 8 左右之后输出就已经非常接近 1 了而 fp16 在接近 1 的地方精度很稀碎导致门控值本身误差很大。我在一次 3B 模型的训练里遇到过 loss 周期性上跳最后定位到就是 gate 的 sigmoid 部分在 fp16 下精度不够等于给每个神经元加了一个随机抖动源。解决办法有两个要么用 bf16——它的指数范围和 fp32 一样大基本不会溢出代价是尾数精度低一些要么在算 sigmoid 时把 gate 临时 upcast 到 fp32算完再降回原精度。第二个方案几乎没有额外成本因为 sigmoid 不是 GEMM不涉及大规模访存一个 kernel 里顺手就做了。如果你自己写 cuda kernel请务必把 sigmoid 这一段用__expf配合 float 计算。4.3 激活值显存与重计算策略显存是训练大模型最硬的约束之一而 FFN 的激活值恰恰是显存大户。Flash Attention 已经把 Attention 部分从O(T²)降到O(T)但 FFN 的中间激活是O(T · d_ff)躲不掉。我算过一笔账假设隐层 4096、中间维度 11008、序列长度 2048、fp16 存储。单个中间张量gate 或 up就是2048 × 11008 × 2 字节 ≈ 45MB。反向传播时至少需要保留 gate、up 和输入 x 三份一层就是45MB 45MB 17MB ≈ 107MB。一个 32 层的 7B 模型光 FFN 激活就是3.4GB以上。这还没有算上 Attention 的中间值和其他开销。所以现在主流训练框架都会开激活重计算activation checkpointing前向时不保存中间张量只保存输入反向时再重算一次。代价是大约多 30% 到 40% 的前向计算换来的显存收益却非常可观——上面那个例子可以直接从 3.4GB 砍到 0.5GB 量级。如果你的模型在某个 batch size 下刚好爆显存先别急着调结构检查一下 FFN 中间激活是不是被完整保存了这通常比换激活函数更立竿见影。4.4 推理与量化场景下的激活值异常SwiGLU 在量化推理里比 GELU 更麻烦一点。原因也很直白silu(gate) * up这个逐元素乘法产生的中间值分布范围比单一非线性激活要宽而且容易出现个别维度上的离群大值。做 INT8 或 INT4 激活量化时这些离群值会拉高整体缩放系数导致其他本来精度足够的维度白白损失精度。业界处理这类问题的主流思路有两种一种是在量化前调整权重和激活的分布把离群值“熨平”另一种是保留部分敏感维度用高精度计算也就是混合精度切分。日志里如果看到某些权重矩阵的量化误差明显大于其他层优先检查它是不是门控矩阵——门矩阵对精度最敏感因为它直接决定了信息能不能通过。我个人在做 7B 模型 INT8 推理时把gate_proj单独留在 fp16 精度整体困惑度损失从 0.3 降到了 0.05 以下代价只是多占用一点点显存。5. 常见问题排查与经验速查5.1 换了激活函数loss 不收敛怎么办最常踩的坑是把预训练权重里的 GELU 直接替换成 SwiGLU然后拿原权重初始化结果 loss 直接放飞。这几乎必然发生因为两种激活的输出分布完全不同后续层的输入范围全变了原权重里的“经验”全部失效。正确的做法是要么从头训练要么做一小段 warmup 让模型适应新激活。如果是在自己的模型上从头训练换成 SwiGLU 后建议把学习率调低 20% 到 30%。原因不复杂门控的乘法效应相当于放大了梯度的方差同样学习率下更新步长会更激进尤其是前几个 step容易把权重推到不稳定区域。我试过保持学习率不变直接换 SwiGLU1B 模型在 2000 步时 loss 仍然比同配置 GELU 高调低学习率之后反而更快追平并反超。5.2 训练中出现 NaN 或 Inf出现 NaN 先别急着怀疑学习率先查激活函数。经验上fp16 训练里 SwiGLU 的 NaN 绝大多数来自 sigmoid 溢出。最简单的定位方法是在 FFN 前向里加几个断言检查gate的绝对最大值是否超过 60000或者silu(gate) * up是否有 inf。如果确认是 sigmoid 溢出要么换 bf16要么在 sigmoid 前做clamp。注意clamp不能太激进否则会影响门控精度我的建议是只限制极端值gate gate.clamp(-100, 100)这种量级基本不会影响正常训练。还有一类 NaN 来自 GELU 的 tanh 近似在 fp16 下的中间变量溢出x³这个项在 x 较大的时候数值增长很快。所以如果某个老模型用的是 GELU 且经常出 NaN优先把 tanh 近似换成内置的F.geluPyTorch 内部有更稳定的实现或者同样用 bf16。5.3 激活函数与初始化、归一化的关系激活函数不是孤立的它和归一化层、初始化策略绑定在一起。现代大模型基本都是 Pre-Norm 结构FFN 的输入已经过了一层 RMSNorm 或 LayerNorm所以输入的范围相对可控。但 SwiGLU 的门控分支会放大方差如果你用标准nn.Linear默认初始化第一轮前向时 gate 的值域可能偏大导致 sigmoid 一开始就进入饱和区。我的做法是给三个 FFN 线性层的初始化标准差乘个 0.7 左右的系数或者干脆用nn.init.normal_(weight, mean0.0, std0.02)这种更小的标准差起步。后面让优化器自己调。比起花里胡哨的初始化方案这招朴素但有效。5.4 激活函数选型速查表激活函数核心公式大模型中的用途参数量倍数数值风险ReLUmax(0, x)少见于现代 LLM早期基线2 个权重矩阵神经元死亡GELUx·Φ(x)GPT-3 时代主流BERT 系2 个权重矩阵fp16 下 tanh 近似溢出Swish/SiLUx·σ(x)单独使用较少多用于门控2 个权重矩阵无重大风险SwiGLUSwish(W1x) ⊙ W3x当前开源 LLM 主流3 个权重矩阵sigmoid 溢出量化困难GeGLUGELU(W1x) ⊙ W3xSwiGLU 的变体3 个权重矩阵同上ReLU²(ReLU(x))²推理优化方向的新选择可融合为 2 个低精度友好一句话总结选型逻辑从零训练新模型预算和显存都够优先 SwiGLU做极致的推理吞吐优化可以认真评估 ReLU²做老模型微调别动它的激活函数老老实实用原结构。6. 一些实际踩坑的个人体会最后分享几个我在真实项目里沉淀下来的经验。第一激活函数决定的是训练的天花板不是地板。换成更强的激活前期可能更痛苦但后期能做到的 loss 下限往往更低。所谓“更强的激活”核心抓手是梯度流动性和乘法交互不是公式看起来复不复杂。第二不要因为某个激活在 7B 上效果好就默认 1B 也会好。小模型对表达能力不敏感但对数值稳定性和优化难度更敏感有时候 GELU 反而比 SwiGLU 省心。我的习惯是先在 100M 到 300M 的小模型上把“激活函数 学习率 归一化”这个组合验证一遍再放大到目标规模。第三也是最想提醒的一点改激活函数从来不是改一行公式的事。它像多米诺骨牌一样牵动参数量、显存策略、混合精度方案、量化误差分布甚至连 Tensor Parallel 的切分方式都要跟着调。如果你只是想在别人的模型上小改一下我建议先跑一个 3 天的消融实验看看收益别拿一个月的训练预算去赌直觉。激活函数作为大模型里最“沉默”的组件值得被认真对待——毕竟模型能不能训到底很多时候就藏在这一次次平凡的映射里。