跳转至

0x05 计算资源和效率

概述

先前介绍了 Transformer 模型的架构,本节讨论对于 Transformer 模型的参数量等相关计算。

首先回顾一下计算机中基本的数据类型,及其所占的字节数

数据类型 字节数
int4 0.5 bytes
int8 1 byte
int16 2 bytes
int32 4 bytes
float16 (fp16,半精度) 2 bytes
bfloat16 (bf16,半精度) 2 bytes
float32 (fp32,单精度) 4 bytes
float64 (fp64,双精度) 8 bytes

在大模型领域,经常提及的模型参数量用 B (billion) 来表示,我们自然会问 一个 7B 的模型权重,占用磁盘空间权重是多少呢?

为了回答这个问题,首先需要搞清楚数字进制,

  • 1K = 1,000 = 1e3,千
  • 1M = 1,000,000 = 1e6,百万
  • 1B = 1,000,000,000 = 1e9,十亿

一个 7B 参数量的模型,就包含 七十亿 的参数。另外,还需要看模型的数据类型,比如 7B 的 fp16 存储的模型,和 7B fp32 存储的模型,显然是不同的。在存储计量中,一般按照 byte 也就是字节作为最小的计算单元,有如下的转换:

  • 1 KB = 1024 bytes = 2^10 bytes
  • 1 MB = 1024 KB = 2^10 KB = 2^20 bytes
  • 1 GB = 1024 MB = 2^10 MB = 2^30 bytes

而 2^30 = 1,073,741,824 ≈ 10^9。一般为了简便转换,可以认为 1B 字节 的参数,约等于 2^30 bytes 也就是 1 GB。因此,如果一个 7B 模型是 bf16 格式,那么他的总参数量权重大概就是 14 GB,如果是 fp32 保存的,那么需要占用的磁盘空间大概为 28 GB。

如果考虑到将模型权重加载到显卡进行推理呢,甚至需要进行训练呢?大概可以分别以下几个部分,用表格形式更直观总结如下:

使用方式 空间
存储 模型权重
推理 模型权重+激活值(+KV-Cache)
训练 模型权重+激活值+梯度+优化器

到这里读者肯定有很多疑问,比如:

  • 模型权重大小如何根据 Transformer 结构计算出来?
  • 模型推理中的激活值如何计算?
  • 梯度和优化器状态在训练中是如何考虑的?

这些将在后续的章节尝试解答。

Transformer 的参数量

此处以 Decoder Only 的 LLM 为例,其参数主要包括三个部分:Embedding, Decoder, LM head。其中占大头的是 Decoder,一般有多个 decoder-layer 组成,而如前所述,每个 decoder-layer 又分为 Multi-head Self-Attention 以及 FFN 两部分。

首先给出一些符号定义

符号 说明
\(h\) 词表维度 / hidden states 维度
\(a\) 多头注意力中注意力头个数
\(l\) Decoder Layer 的层数
\(V\) 词表大小
\(b\) Batch Size
\(s\) 输入的序列长度

Embedding 层

词表是一个二维的矩阵,参数量为 \(V\times h\)

当一个输入为 \(b\times s\) 输入到 Embedding 进行 Embedding look up 之后,维度会变为 \(b\times s \times h\)

Transformer 层

先计算单层的参数量,然后再乘上层数。每一层的 Decoder Layer 大概有如下几个部分:MHA 中的 \(Q,K,V,O\) 变换矩阵,LayerNorm 中的可学习参数,FFN 中的升维和降维矩阵。

  1. MHA

    MHA 包含四个权重矩阵,分别用于不同的变换。此处认为每个线性变换都包含偏置项,因此参数量为 \(4\times(h\times h +h) = 4h^2 + 4h\)

  2. LayerNorm

    每个 LN 包含缩放和平移两个参数,维度均为 \(d\)。一层包含两个 LN,总参数为 \(4h\)

  3. FFN

    包含一个升维变换,和一个降维变换,参数量分别为 \(h\times 4h +4h\)\(4h \times h + h\),共计为\(8h^2 + 5h\)

一层 Transformer 层的参数量为 \(12h^2 + 13h\)

LM Head 层

LM Head 是自然语言处理模型中的一个组件,主要作用是将模型的输出(通常是经过 Transformer 编码器处理后的隐藏状态)转换成预测下一个词的概率分布。

Head 与 embedding的参数量相同。如果是 tied embedding(即,head 权重矩阵与词嵌入矩阵是参数共享的),则两者共用一个参数。

总参数量

\(l\) 层的 Transformer 的总参数量为 \(l(12h^2 + 13h) + 2Vh\),当 \(h\) 很大的时候,可以忽略一次项,近似为 \(12lh^2\)

LLama3 的参数量估计

再用LLaMA3来看看在工业界落地中的一些特殊之处。

  1. SwiGLU

    LLaMA 等模型在 FFN 中会使用 SwiGLU 激活,这也就导致其会额外多了一个权重矩阵。LLaMA论文中提到,使用 SwiGLU 后将 \(d_{ffn}\)\(4d\) 降低到了 \(8h/3\)。这样 3 个权重矩阵的参数量还是 \(8h\),总的参数量依然可以使用 \(12×nlayer×h×h\) 来 预估。

Transformer 的显存占用

根据之前的分析,在训练过程中,显存的占用分为 模型权重+激活值+梯度+优化器 这个四部分。此处以 AdamW 优化器为例,并用混合精度来加速训练,假设模型的参数量为 \(\theta\),除激活值外,其他几个显存部分占用分别为:

  • 模型权重:\(2\theta\),权重需要加载进显存,同时会保存一份全精度副本,\(4\theta\),共计 \(6\theta\)
  • 梯度:\(2\theta\),和权重保持一致
  • 优化器:\(4\theta + 4\theta\) 全精度的一阶和二阶动量

总计为 \(16 \theta\) 。表格总结如下:

类别 占用
模型权重 \(2\theta + 4\theta = 6\theta\)
梯度 \(2\theta\)
优化器 \(4\theta + 4\theta = 8\theta\)
共计 \(16\theta\)

image.png

Transformer 的激活值

以 Megatron 架构为例,论文中的符号表如下:

符号 含义
\(a\) Attention 中,注意力头的个数
\(b\) 每个 GPU 上的 batch size
\(h\) Transformer 的隐层维度
\(d\) 单个Attention Head 的隐层维度,即 \(d = h / a\)
\(L\) Transformer Decoder 的层数
\(s\) 输入的 token 个数
\(V\) 词表的大小

此处计算按照半精度浮点计算,也就是 fp16 或者 bf16 的情况,每个参数占用 2 bytes。根据刚才介绍的 Transformer 架构,主要激活可以大致分为三部分,一部分是 Multi-head Self-Attention,另一部分是 FFN,再者就是两者中间的 Add & LayerNorm 层。下面分别针对这三部分分别计算激活值

Multi-head Self-Attention

张量内容 张量形状 对应激活值 保存内容 保存原因
\(X\) \([b,s,h]\) \(2bsh\) 输入到 Transformer Block 的张量 \(X\) 用于后续 \(QKV\) 计算
\(Q、K\) \([b,s,h]\) \(4bsh\) \(X\) 经过转换后的 \(Q\)\(K\) 矩阵 用于计算 Attention 的 \(QK^\top\)
\(QK^\top\) \([b,a,s,s]\) \(2bas^2\) 每个注意力头的注意力分数 用于计算后续的 Softmax 操作
Dropout Mask \([b,a,s,s]\) \(bas^2\) Softmax 后接的 Dropout Mask 矩阵,注意只需要 1 个 byte 的 Mask 矩阵 用于经过 Softmax 得到注意力权重后,做 Attention Weights 的 Drop
\(V\) \([b,s,h]\) \(2bsh\) \(X\) 经过转换后的 \(V\) 矩阵 用于计算 \(\text{Softmax}(\frac{QK^\top}{\sqrt{d}})V\)
Attention Weights \([b,a,s,s]\) \(2bas^2\) 经过 \(\text{Softmax}()\) 和 Dropout 的 Attention Weights 同样用于计算 \(\text{Softmax}(\frac{QK^\top}{\sqrt{d}})V\)
Attention Context \([b,s,h]\) \(2bsh\) 经过多头自注意力计算后的张量 用于作为后续的输出线性层的输入
Output Mask \([b,s,h]\) \(bsh\) 经过线性层输出的 Dropout Mask 矩阵 用于后续 LayerNorm 等的输入
共计 \(11bsh+5bas^2\)

MLP

张量内容 张量大小 对应激活值 保存内容 保存原因
\(X\) \([b,s,h]\) \(2bsh\) 输入进 MLP 的张量 \(X\) 用于第一个 Linear 的计算
Linear1 Output \([b,s,4h]\) \(8bsh\) 第一个 Linear 的升维输出 用于激活函数的计算
Act Function Output \([b,s,4h]\) \(8bsh\) 激活函数的输出 用于第二个 Linear 的计算
Dropout Mask \([b,s,h]\) \(bsh\) Dropout Mask 矩阵
共计 \(19bsh\)

LayerNorm

Self-Attention 和 MLP 后续分别接了一个 Layer Normalization。每个 LayerNorm 需要保存其输入,大小为 \(2bsh\)。2 个 Layer Norm需要保存的中间激活为 \(4bsh\)

综上,每个transformer层需要保存的中间激活占用显存大小为\(34bsh+5bas^2\)。对于 \(l\) 层的 Transformer 模型,中间激活占用的显存大小可以近似为 \(l\times (34bsh+5bas^2)\)

Transformer 的浮点计算量

计算一般即为 FLOPs,在 LLM 的解码阶段,主要涉及的计算和操作是矩阵/向量的乘法。首先看一下,对于一个\((m,n) \times (n,k)\)的矩阵乘法,按照 FLOPs=乘法+加法执行的次数计算,需要执行\(2mkn\) 的计算。

值得注意的是,FLOPsFLOPS 的含义不同,区别如下:

  • FLOPs :Floating Point Operations 指 浮点运算次数,用于描述计算量。
  • FLOPS :Floating Point Operations per Second 指 每秒浮点运算次数,用于描述计算速度,常作为计算卡/显卡的性能指标。

  • QKV 转换

    • 输入\(X\in \mathbb{R}^{b\times s\times h}\),权重\(W_Q,W_K,W_v \in \mathbb{R}^{h \times h}\)
    • Flops=\(3 \cdot 2bsh^2 = 6bsh^2\)
  • QK的转置得到 Attention Score
    • 输入\([b,a,s,d]\)
    • 每个 Attention Head 的计算是\(2s^2d\)
    • 整个的 Flops 是 \(2bas^2d = 2bs^2h\)
  • Softmax乘 V得到 Attention Context
    • 输入 \([b,a,s,s]\) \(V=[b,a,s,d]\)
    • 每个 Attention Head 的 Flops 为 \(2s^2d\)
    • 整个的 Flops 为 \(2bas^2d = 2bs^2h\)
  • 输出投影
    • 输入 \([b,s,h]\) 权重 \(W_O \in \mathbb{R}^{h\times h}\)
    • 整个的 FLOPs 为 \(2bsh^2\)
  • Self-Attention 的整体的 FLOPS为
    • \(6bsh^2 + 2bs^2h + 2bs^2h + 2bsh^2 = 8bsh^2 + 4bs^2h\)

在考虑 FFN 部分, FFN 主要是由两个线性层组成

  • 第一个线性层
    • 输入 \([b,s,h]\) \(W_{l1}\in \mathbb{R}^{h\times 4h}\)
    • Flops 为 \(2bsh\cdot 4h = 8bsh^2\)
  • 第二个线性层
    • 输入 \([b,s,4h]\) \(W_{l2} \in \mathbb{R^{4h\times h}}\)
    • Flops 为 \(2bs4h \cdot h = 8bsh^2\)
  • FFN 部分的整体 FLOPS为
    • \(8bsh^2 \cdot 2 = 16bsh^2\)

所以,一层 Transformer Block 的 前向传播的 FLOPs 为 \(24bsh^2 + 4bs^2h\)。反向传播过程的 FLOPs是前向传播过程的 2 倍,也就是\(48bsh^2+8bs^2h\),因为对一个矩阵乘操作,反向传播需要分别把梯度回传给两个矩阵,按照链式法则会出现两次矩阵乘。

综上,一层 Transformer Block 总体前向+反向的 FLOPs 为 \(72bsh^2 + 12bs^2h\)

最后,再来考虑一下 Logits 的计算部分。在经过 \(l\) 层的 Transformer 之后,需要将隐层映射为词表,具体将 \([b,s,h]\) 的张量,经过一个 LM Head,转换为 \([b,s,V]\) 的形状,总体的FLOPs为 \(2bshV\),如果再加上反向传播,那么是 \(6bshV\)

总体的 FLOPs 为,\(l\times(72bsh^2 + 12bs^2h) + 6bshV\)

结合模型参数量对 FLOPs 训练和推理的估计

一个显示的场景是,给出模型的参数量,以及训练的语料 Token 数量,我们想估计一下,总计需要多少 FLOPS 能够将这些语料过一遍。首先看一下,在推理场景中 FLOPs 和模型参数量的关系,即:

\[ \frac{l(24bsh^2 + 4bs^2h) + 2bshV}{l(12h^2 + 13h) + 2Vh} = bs\cdot \frac{l(24h+4s) + 2V}{l(12h+13) + 2V} \]

对于常见大模型的参数而言,一般可以认为 \(l(12h+13) \gg 2V\),例如 Qwen3-8B 中的参数:

  • 层数 \(l\) :36
  • 隐层\(h\):4096
  • 词表\(V\): 151936

那么 \(l(12h+13) = 1,769,508 \approx 1.7M\)\(2V= 303,872 = 0.3M\),此时推理计算量和参数量的比值近似于 \(2bs\),而 \(bs\) 实际上就是输入到模型中,一个批次下的 token 总数。因此,推理的 FLOPs 近似为 2 倍的参数量,再乘 token 数量,即:

\[ \text{FLOPs}_{\text{推理}} = 2\times \#\text{prarms} \times \#\text{tokens}. \]

考虑模型训练的 FLOPs,根据刚才的计算,每个参数反向传播的时,是前向传播的 2 倍,按照上述估计,训练的 FLOPs 为

\[ \text{FLOPs}_{\text{训练}} = 6\times \#\text{params} \times \# \text{tokens}. \]

在获得了总的 FLOPs 后,可以根据 GPU 的参数,估计整体的训练时间,近似按照下面的计算方式:

\[ 训练时间= \frac{6\times \#\text{params} \times \# \text{tokens}}{\text{GPU 数量} \times \text{GPU\ FLOPS} \times \text{GPU 利用率}}. \]

有不少工具和网站可以快速查阅不同 GPU 的相关参数,如 gpupoet。 关于 A100 PCIe 80GB 的相关信息如下 https://gpupoet.com/gpu/learn/card/nvidia-a100-pcie:

image.png

和 Nvidia 官方的 数据 一致:

image.png

细心的朋友会发现,同样的一款显卡有不同的型号,而且在性能上差异很大,导致差异的主要原因之一是卡间通讯方式的不同:一个通过主板 PCIe 通讯, 而 SXM 通过 NVLink 互联,带来更高的通信带宽。这也引出下面的内容,LLM 另外一个除了计算量以外的受制约的因素,带宽。

One More Thing: 带宽

推理、训练不仅取决于 GPU 的算力大小,GPU 内存带宽也可能成为瓶颈。模型在训练过程中,参数、梯度需要不断在 GPU内存中进出,也会在不同的 GPU 之间通信。根据 LLM 在计算时候的特点,常把 LLM 在不同时期的运算操作,分为 ”计算密集型“ 和 ”访存密集型“ 两种类型。如何定量化区分这两种类型,可以引入算术强度 (Arithmetic Intensity, AI) 的概念:

\[ 算术强度 = \frac{\text{FLOPs}}{访问的字节数}. \]

举例来说,对于 GEMM ,也就是尺寸为 \((m,n) \times (n,k)\) 矩阵乘法的算数强度为:

\[ \text{AI} \approx \frac{2mnk}{b\times(mn+nk+mk)}. \]

其中 \(b\) 指代数据类型占用的字节数。

从直觉直觉来理解,AI 的含义是每搬运 1 字节数据,能做多少计算。因此,AI 越高,说明属于 计算密集型,主要受制于 GPU 的 FLOPS;而 AI 越低,说明是 访存密集型,此时主要受制于 GPU 显存带宽等因素。更严谨则需要结合 roof-line 模型来及时,有兴趣的读者可以深度阅读相关资料 [知乎: Roofline Model与深度学习模型的性能分析],此处就不再展开。

image.png

对 LLM 推理而言,prefill(处理 prompt)阶段主要由大矩阵乘组成,AI 相对更高,往往更接近计算上限;而 decode(逐 token 生成)阶段每步计算规模变小,但仍需频繁读写权重与 KV cache,因此更容易被显存带宽限制。