博主介绍:程序喵大人

在这里插入图片描述

咱们先来看看这个完整的车间全景。经历了前面两章,你现在知道了模型是一台大机器,也知道了里面流淌的数据(Tensor)长什么样。现在咱们要把这台机器拆开,看看里面具体是怎么干活的。

你可以把深度学习里的“算子”(Operator)或者说“层”(Layer),直接想象成流水线上的一个个专业工位。你看画面里,数据箱沿着传送带走,会经过好几个长得完全不一样的机器:有负责矩阵乘加的 Linear,有负责整形的激活函数,还有负责校准的 LayerNorm,等等。

每个工位只干一件特定的事。它要么改变数据的 shape,要么改变数据的具体数值,或者两个都改。对于做 AI Infra 的人来说,每次看到一个新的算子,其实脑子里最先转的就两件事:第一,它把 shape 怎么变了?第二,这个工位贵不贵?(也就是它消耗多少算力 FLOPs 和多少显存)。带着这两个问题,咱们挨个工位走一遍。

一、Linear:一次矩阵乘加

在这里插入图片描述

这是咱们最常打交道,也是车间里最庞大的一个工位。Linear,也就是全连接层或者线性层。

你看这台机器,其实它里面就分了两步动作。第一步是个大块头参数板 W W W,输入的数据箱过来,先跟它做一次矩阵乘法。乘完之后,第二步有个小一点的托盘 b b b(偏置向量),把里面的数值挨个加上去。整个公式就是我们再熟悉不过的 y = x W + b y = xW + b y=xW+b

这里有个很有意思的细节:虽然公式简单,但它实实在在地改变了数据的 shape。你看,原本输入进来的时候,最后一个维度还是 768 格,经过这台机器出来,就被拉长成了 3072 格。前边的 batch 和 seq 维度它都不管,就专门揪住最后一个维度做变换。

顺便提一句,这台机器非常“吃”算力。Transformer 里面大头算力都花在 Linear 上了,因为每一次矩阵乘法背后的乘加操作数量是极其庞大的。

二、激活函数:ReLU / GELU / SiLU 都干嘛

在这里插入图片描述

刚才的数据经过了 Linear(矩阵乘法)之后,接下来会立刻进入一个“数值过滤”环节。这就是大名鼎鼎的激活函数。

深度学习之所以能干复杂的活,全靠这一步的非线性转换。

如果没有激活函数会怎样?我们可以举个最简单的数学例子:

假设你的第一层 Linear 的作用是把输入乘以 2( y = 2 y = 2 y=2),第二层 Linear 是乘以 3( z = 3 z = 3 z=3)。如果你直接把这两层首尾相连,结果就变成了 z = 3 × ( 2 x ) = 6 z = 3 \times (2x) = 6 z=3×(2x)=6。你看,不管你堆叠多少万层 Linear,数学上最终都能被合并成一个等效的单层矩阵乘法。这就叫“线性坍缩”,这种网络只能画直线,永远学不会复杂的规律。

激活函数是怎么打破这个局面的? 我们拿最老牌的 ReLU 举个例子。它的规则极其简单直接:大于 0 的数原样保留,小于等于 0 的数全变成 0。 假设 Linear 层算完之后,输出了一组中间结果 [-2.5, 0.1, 5.0]

  • 经过 ReLU 之后,负数被“一刀切”强行抹平,结果变成了 [0, 0.1, 5.0]

就是这么一个看似极其粗暴的“归零”操作,让原本直来直去的函数图像“拐了个弯”。有了这个非线性的弯,你再往后接下一层 Linear 时,公式就再也无法合并了。神经网络就是靠着千万个这种小小的“拐弯”,才有了拟合出世界上所有复杂规律的能力。

至于图里画的 GELU 和 SiLU,逻辑和 ReLU 是一脉相承的,只不过它们在处理 0 附近的数字时没有直接“一刀切”,而是给了一个带有弧度的平滑过渡。这能让模型在训练时梯度更好算,所以现在的当红大模型(比如 Llama、GPT)基本都换成了 GELU 或 SiLU。

我们可以把这几个激活函数的数学公式列出来对比一下:

  1. ReLU (Rectified Linear Unit)

f ( x ) = max ⁡ ( 0 , x ) f(x) = \max(0, x) f(x)=max(0,x)

最经典的“一刀切”激活函数。大于 0 时导数为 1,小于等于 0 时导数和输出都是 0。

  1. GELU (Gaussian Error Linear Unit)

G E L U ( x ) = x Φ ( x ) = x ⋅ P ( X ≤ x ) GELU(x) = x \Phi(x) = x \cdot P(X \le x) GELU(x)=xΦ(x)=xP(Xx)

其中 Φ ( x \Phi(x Φ(x 是标准正态分布的累积分布函数。由于精确计算 Φ ( x \Phi(x Φ(x 比较慢,在工程实现(比如 C++ 推理引擎)中,通常会使用一个基于双曲正切( tan ⁡ \tan tan)的近似公式:

G E L U ( x ) ≈ 0.5 x ( 1 + tanh ⁡ ( 2 π ( x + 0.044715 x 3 ) ) ) GELU(x) \approx 0.5x \left(1 + \tanh\left(\sqrt{\frac{2}{\pi}} \left(x + 0.044715 x^3\right)\right)\right) GELU(x)0.5x(1+tanh(π2 (x+0.044715x3)))

而在一些大模型中,也会采用另一个基于 Sigmoid 的近似计算:

G E L U ( x ) ≈ x ⋅ σ ( 1.702 x ) GELU(x) \approx x \cdot \sigma(1.702 x) GELU(x)xσ(1.702x)

  1. SiLU (Sigmoid Linear Unit / Swish)

S i L U ( x ) = x ⋅ σ ( x ) = x 1 + e − x SiLU(x) = x \cdot \sigma(x) = \frac{x}{1 + e^{-x}} SiLU(x)=xσ(x)=1+exx

其中 σ ( x \sigma(x σ(x 是 Sigmoid 函数。SiLU 是 Swish 激活函数在 β = \beta= β= 时的特例,它在大模型(如 LLaMA)中被广泛作为 SwiGLU 的一部分。

从底层的算子视角来看,激活函数对 AI Infra 工程师非常友好:

  • 完全不改变 Shape:它是典型的逐元素(Element-wise)操作。输入是 (4, 128, 3072),输出一模一样还是 (4, 128, 3072)
  • 算力开销极小:它不需要像矩阵乘法那样做高强度的乘加运算,GPU 只需要顺着内存把每个数字读出来判断一下,改完数值再写回去就行了。

三、LayerNorm 与 RMSNorm:让数值稳一点

在这里插入图片描述

数据继续往下走,有时候数值跑着跑着就容易“飘”——要么太大,要么太小。这时候就需要 LayerNorm 这样的校准台来稳一稳局势。

你看左边那箱进来的零件,有的数特别大,有的特别小,乱七八糟的。进了校准机之后,出来的数字就规矩多了,基本都收敛在零附近。LayerNorm 会先算平均值,再算方差,然后做缩放;而现在很多 LLM 里更流行用右边那个 RMSNorm,它觉得算平均值太麻烦了,干脆只算个均方根,效果差不多,速度还能快个 10% 左右。

和激活函数一样,Norm 层也不改 shape,只负责把数值理顺,防止后面越算越崩。不过它机器上有两个可以微调的旋钮(参数 KaTeX parse error: Undefined control sequence: \gamm at position 1: \̲g̲a̲m̲m̲KaTeX parse error: Undefined control sequence: \bet at position 1: \̲b̲e̲t̲),是可以在训练时学习的。

四、Softmax:把分数变成概率

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个概率转换台(Softmax)可是个关键角色,尤其在算注意力机制(Attention)和最后输出预测词的时候。

假设上一道工序给过来的分数是随便算出来的实数,有正有负,这怎么看谁的概率大呢?Softmax 这个工位分两步走:先通过“指数放大器”,把大家都变成正数,而且稍微大一点的分数会被瞬间拉得特别高;然后再通过一个“分配器”,把所有数字除以它们的总和。

数学上,假设输入给 Softmax 的向量是 x = [ x 1 , x 2 , … , x n \mathbf{x} = [x_1, x_2, \dots, x_n x=[x1,x2,,xn,那么第 $$$$ 个元素的输出概率为:

Softmax ( x i ) = e x i ∑ j = 1 n e x j \text{Softmax}(x_i) = \frac{e^{x_i}}{\sum_{j=1}^n e^{x_j}} Softmax(xi)=j=1nexjexi

不过在工程上,直接按这个公式计算会遇到一个致命问题:数值上溢出(Overflow)。

因为 KaTeX parse error: Expected group after '^' at position 2: e^̲ 的增长速度极快,一旦某个分数值 KaTeX parse error: Expected group after '_' at position 2: x_̲ 稍大(例如对于单精度 FP32,只要 x i > 88. x_i > 88. xi>88.),KaTeX parse error: Expected '}', got 'EOF' at end of input: e^{x_i 就会超出浮点数所能表示的最大范围,从而导致溢出并产生 NaN(Not a Number)。

为了保证计算的数值稳定性,在工程实现(如 CuDNN、llama.cpp 等)中,底层普遍会采用安全 Softmax(Safe Softmax):

Softmax ( x i ) = e x i − M ∑ j = 1 n e x j − M \text{Softmax}(x_i) = \frac{e^{x_i - M}}{\sum_{j=1}^n e^{x_j - M}} Softmax(xi)=j=1nexjMexiM

其中 M = max ⁡ ( x M = \max(\mathbf{x} M=max(x 是输入向量中的最大元素。在分子分母中同时减去最大值后,所有指数项的指数都小于等于 0( x i − M ≤ x_i - M \le xiM)。这使指数项的最大值为 e 0 = e^0 = e0=,值域完全被限制在 ( 0 , 1 (0, 1 (0,1 之间,彻底避免了指数运算上溢出的风险。

这么一倒腾,右边出来的结果就变成了一个规规矩矩的概率分布了,大家加起来刚刚好等于 1.0。形状当然还是没变,但数值的意义完全不同了。在工程上,算 Softmax 时数字很容易溢出,所以底层经常会有各种优化来保证它的稳定性。

五、Embedding:查表,不是矩阵乘法

在这里插入图片描述

刚开始学的时候,很多人会把 Embedding 和 Linear 搞混,觉得它们都有个巨大的参数矩阵。但你一看这个查表柜就明白了,它俩干活的方式完全不一样。

这根本不是在做乘法,而纯粹是个巨大的文件柜。比如左边送过来一个数字编号“1024”(代表某个词的 token),工位的动作极其简单粗暴:直接拉开第 1024 号抽屉,把里面那条长长的向量拿出来,放到传送带上。

就这么简单。没有任何复杂的浮点数乘加运算,计算量几乎为零。但这玩意儿非常占地方,比如一个 32000 个词汇的表,每个词对应 4096 维的向量,光这个柜子本身就要吃掉不少显存。

六、卷积:infra 工程师只需要知道它存在

在这里插入图片描述

这个扫描机工位,也就是卷积(Conv)层,咱们在 LLM 这边其实很少遇到,但还是得认个脸熟。

它的工作方式不是一通乱算,而是拿个小窗口在输入数据上一点点滑动扫描,每次只看一小块局部特征。这种干活方式特别适合处理图片里的边缘、或者音频里的波形,所以如果你以后去做视觉(CV)相关的优化,会天天跟它打交道。

但如果你主要做大语言模型,那蓝色机器人主任的手势很明确了:大概知道它是怎么回事就行,咱们的重点不在它身上。

七、算子的计算成本:FLOPs 怎么估

在这里插入图片描述

认识完各种机器,咱们来算算经济账。在跑模型的时候,机器到底累不累,也就是计算量(FLOPs,浮点运算次数)有多大?

你看成本核算台上这张图就很直观。那个高高的柱子基本上全被 Linear(矩阵乘法)占了。我们可以粗略算一下:对于输入特征维度是 i i i,输出特征维度是 o u ou ou,序列长度是 s e se se 的数据,经过一次 Linear 层的计算量大约是 2 × s e q × i n × o u 2 \times seq \times in \times ou 2×seq×in×ou

这个公式是怎么来的?我们举个具体的例子推演一下: 假设你的输入是 (seq=128, in=768) 的矩阵,要乘上一个权重矩阵 (in=768, out=3072),最后算出一个 (128, 3072) 的输出矩阵。

  1. 先看输出矩阵里的“1 个数字”是怎么算出来的:你需要拿输入的一整行(768个数字),去和权重的一整列(768个数字)挨个相乘然后再相加。这需要做 768 次乘法,以及大约 768 次加法。
  2. 所以,为了算出这 1 个数字,你需要做 2 × 76 2 \times 76 2×76(也就是 2 × i 2 \times i 2×i)次浮点运算(一乘一加算 2 次操作)。
  3. 再看总体:最后的输出矩阵总共有 128 × 307 128 \times 307 128×307(也就是 s e q × o u seq \times ou seq×ou)个格子,你要算出这么多数字。
  4. 总计算量就是:总格子数 KaTeX parse error: Undefined control sequence: \time at position 1: \̲t̲i̲m̲e̲ 每个格子需要的运算次数 = ( s e q × o u t ) × ( 2 × i n ) = 2 × s e q × i n × o u (seq \times out) \times (2 \times in) = 2 \times seq \times in \times ou (seq×out)×(2×in)=2×seq×in×ou

(如果前面还有 batch 维度,直接把 batch 乘进去就行了)。

至于刚才看的什么激活函数、LayerNorm,它们的计算量在 Linear 面前真的连个零头都算不上(因为它们只是对每个数做简单的加减,没有这种疯狂的交叉点积,通常所有算子加起来连 10% 都不到)。所以面试时如果让你估算整个大模型的算力消耗,先盯紧那几个大号的 Linear 算式就对了。

八、算子的显存成本:weights 与 activations 不是一回事

在这里插入图片描述

算完计算量,再来看看咱们的“仓库”——也就是显存是怎么被占用的。这里一定要把两种东西分清楚,不然排查 OOM(显存溢出)的时候绝对会抓瞎。

你看这张图,仓储区被明确分成了两半。

左边是权重仓库(Weights)。那些 Linear 的参数矩阵、Embedding 的大柜子都存在这儿。它们的特点是死板:不管你送进来多长的数据,或者一次送进来几个样本(batch),这些参数箱子该占多少地儿就占多少地儿,雷打不动。

右边是激活值临时区(Activations)。这里放的是数据流过每一个工位时产生的中间结果。这可是个无底洞,它的占用大小直接跟你的 batch size 和序列长度挂钩。

这里的显存记账规则,分三种情况:

  1. 常规模型推理:最省空间。数据算完第 1 层丢给第 2 层,第 1 层的中间结果就可以立刻释放了,时刻只存当前层。
  2. 大模型(LLM)推理:这就是最著名的吃显存怪兽——KV Cache 出场的地方。虽然不需要存所有算子的结果,但为了“吐词”时不重复计算历史数据,模型必须把每一层 Attention 算出来的 Key 和 Value 激活值一直攒在显存里。
  3. 模型训练:最耗显存。不仅要存 KV,还得把所有层、所有算子的中间结果全死死存下来,因为等会儿反向传播(算梯度)时要原封不动地拿出来用。

所以,排查 OOM(显存溢出)时:训练阶段多查查全套的激活值;LLM 推理阶段,就要盯紧你的 KV Cache 是不是撑爆仓库了。

九、MLP 与 FFN:算子拼装的第一步

在这里插入图片描述

在大模型的结构里,你经常会听到 MLP(Multi-Layer Perceptron,多层感知机) 或者 FFN(Feed-Forward Network,前馈神经网络) 这两个词。它们其实是一回事,都是把前面我们学到的几个基础算子打包在一起,拼装成一个复合功能的“大工位”。

1. 结构与公式

在最经典的 Transformer(如 GPT-2)中,FFN 通常由两个线性层(Linear)和一个非线性激活函数(如 GELU)串联组成:

FFN ( x ) = GELU ( x W 1 + b 1 ) W 2 + b 2 \text{FFN}(x) = \text{GELU}(x W_1 + b_1) W_2 + b_2 FFN(x)=GELU(xW1+b1)W2+b2

这里的流程非常直观:

  1. 第一层升维(Up-projection):通过第一个 Linear 层 KaTeX parse error: Expected group after '_' at position 2: W_̲,把通道数(Hidden Size,比如 768)放大到 4 倍(比如 3072)。
  2. 激活函数(Activation):通过非线性激活函数进行一次“过滤”。
  3. 第二层降维(Down-projection):通过第二个 Linear 层 KaTeX parse error: Expected group after '_' at position 2: W_̲,再把维度从 3072 压缩回 768,并把结果加到残差连接中。

2. 现代大模型里的变种:GLU 家族

现代大模型(如 LLaMA、Gemma)在这个组合的基础上做了一点小改良,把经典的 FFN 换成了 SwiGLU(或者更通用的 GLU 门控线性单元结构)。

它的计算公式变成了:

SwiGLU ( x ) = ( SiLU ( x W gate ) ⊗ x W up ) W down \text{SwiGLU}(x) = \left( \text{SiLU}(x W_{\text{gate}}) \otimes x W_{\text{up}} \right) W_{\text{down}} SwiGLU(x)=(SiLU(xWgate)xWup)Wdown

其中 KaTeX parse error: Undefined control sequence: \otime at position 1: \̲o̲t̲i̲m̲e̲ 代表逐元素相乘(Element-wise Multiplication),KaTeX parse error: Unexpected end of input in a macro argument, expected '}' at end of input: \text{SiLU 就是我们前面介绍的 SiLU 激活函数(有时也被称为 Swish,故名 SwiGLU)。

从结构上来看,它用了三个矩阵乘法:

  • KaTeX parse error: Expected '}', got 'EOF' at end of input: W_{\text{gate}KaTeX parse error: Expected '}', got 'EOF' at end of input: W_{\text{up} 把输入投影成两个不同的分支;
  • 门控分支用 SiLU 激活后,再和升维分支做逐元素相乘;
  • 最后用 KaTeX parse error: Expected '}', got 'EOF' at end of input: W_{\text{down} 投影还原维度。

十、第 3 章总结:从算子拼出一个网络

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

好了,逛完了所有的工位,咱们往后退一步,再看看整个车间。

其实再复杂的大模型,也就是把咱们刚才看的这些算子,像搭积木一样串联起来。比如一个标准的 Transformer Block,大概也就是:进门先做个 Norm,然后跑两个 Linear 算 Attention,再做个 Norm,最后跑两个 Linear 算 MLP。

咱们 AI Infra 工程师看一个网络,其实视角很简单:第一步,认出它是由哪些工位拼出来的;第二步,顺着传送带走一遍,看看 shape 在每个节点是怎么变的;第三步,估算一下最吃算力的节点在哪,显存的瓶颈会在哪。

把这三步理清楚了,咱们下一章就可以去看看,当这套流水线正着转(推理)和反着转(训练)的时候,到底有什么根本的区别了。

码字不易,欢迎大家点赞,关注,评论,谢谢!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐