AI 计算中的数值精度与量化基础

结合chatgpt生成

在大模型训练和推理中,FP32、FP16、BF16、INT8 等格式决定了数据的表示范围、计算精度、存储空间和运行速度。本文简要梳理常见浮点格式、误差来源、Softmax 数值稳定性以及 INT8 量化。

1. FP32、FP16 与 BF16

浮点数一般由三部分组成:

\[x=(-1)^s \times 2^e \times m \]

其中:

  • 符号位决定正负;
  • 指数位决定数值范围;
  • 尾数位决定有效精度。
格式 符号位 指数位 尾数位 特点
FP32 1 8 23 范围大、精度高、开销较大
FP16 1 5 10 精度较高,但数值范围较小
BF16 1 8 7 范围接近 FP32,但精度低于 FP16

FP32

FP32,也就是通常所说的 float32,是 IEEE 754 单精度浮点数。

它大约能够提供 7 位十进制有效数字,最大有限值约为:

\[3.4\times10^{38} \]

FP32 精度和动态范围都比较好,但每个数需要 4 字节,计算与访存开销较大。

FP16

FP16 每个数只需要 2 字节,因此能减少显存占用和内存带宽,并且可以使用 GPU Tensor Core 加速。

但 FP16 只有 5 位指数,最大有限值只有:

\[65504 \]

之所以不是 65535,是因为浮点数在这个数量级下不能逐个表示整数。

因此 FP16 在训练中容易发生:

  • 上溢:数值过大,变成 inf
  • 下溢:数值过小,变成 0;
  • 舍入误差:较小的增量无法被保留。

BF16

BF16 也占 16 bit,但它保留了与 FP32 相同的 8 位指数,因此动态范围与 FP32 接近。

代价是 BF16 只有 7 位尾数,精度比 FP16 更低。

可以这样理解:

  • FP16:尾数更多,精度更高,但范围较小;
  • BF16:指数更多,范围更大,但精度较低。

大模型训练中的激活值和梯度可能变化很大,因此 BF16 通常比 FP16 更不容易溢出,也往往不需要复杂的 loss scaling。

2. 为什么 FP16 乘法常用 FP32 累加

矩阵乘法中的一个输出元素,本质上是点积:

\[y=\sum_{i=1}^{n}a_i b_i \]

即使 \(a_i\)\(b_i\) 使用 FP16 存储,累加过程也经常使用 FP32:

\[\text{FP16}\times\text{FP16}\rightarrow\text{FP32累加} \]

原因是点积通常包含大量加法。FP16 精度有限,直接用 FP16 累加会让舍入误差不断积累。

例如,当前累加结果已经很大,而新加入的数很小时:

\[s+x \approx s \]

较小的 \(x\) 可能因为 FP16 尾数不足而完全丢失。

使用 FP32 累加可以:

  • 降低累计舍入误差;
  • 减少溢出风险;
  • 提高矩阵乘法的数值稳定性。

很多 GPU 的混合精度矩阵乘法都支持 FP16 或 BF16 输入、FP32 累加,最后再根据需要转换为低精度格式。

类似地,INT8 矩阵乘法一般使用 INT32 进行累加。

3. 绝对误差与相对误差

设真实值为 \(x\),计算结果为 \(\hat{x}\)

绝对误差

\[E_{\mathrm{abs}}=|\hat{x}-x| \]

绝对误差表示计算结果与真实值相差多少。

例如:

\[x=1000,\qquad \hat{x}=1001 \]

则绝对误差为:

\[|1001-1000|=1 \]

相对误差

\[E_{\mathrm{rel}} =\frac{|\hat{x}-x|}{|x|} \]

上面的相对误差为:

\[\frac{1}{1000}=0.001=0.1% \]

相对误差反映误差相对于真实值的大小。

如果真实值接近 0,相对误差会变得非常大甚至无法定义。因此工程测试中常使用:

\[|\hat{x}-x| \leq \text{atol}+\text{rtol}\cdot|x| \]

其中:

  • atol:允许的绝对误差;
  • rtol:允许的相对误差。

这也是 NumPy、PyTorch 等框架进行数值对齐时常见的判断方式。

4. Reduction 顺序为什么会影响结果

Reduction 指把一组数据归约成一个值,例如求和、求最大值或者计算均值。

数学中的加法满足结合律:

\[(a+b)+c=a+(b+c) \]

但是浮点数精度有限,每次运算都可能发生舍入,因此浮点加法并不严格满足结合律:

\[\operatorname{fl}(\operatorname{fl}(a+b)+c) \neq \operatorname{fl}(a+\operatorname{fl}(b+c)) \]

例如在有限精度下:

\[(10^8+1)-10^8 \]

中间的 1 可能因为精度不足而丢失,结果得到 0。而改变计算顺序:

\[(10^8-10^8)+1 \]

则可以得到 1。

在并行计算中,不同线程会分别计算部分结果,最后再进行树形归约:

顺序求和:(((a+b)+c)+d)
树形归约:(a+b)+(c+d)

不同的线程数量、分块方式和执行顺序都可能改变加法顺序,从而产生微小差异。

因此:

  • CPU 与 GPU 的结果可能不完全相同;
  • 单线程与多线程结果可能有微小差异;
  • 并行程序有时难以做到逐 bit 一致。

这通常不是逻辑错误,而是浮点运算的正常现象。数值测试应该使用合理的误差范围,而不是直接使用 == 比较浮点结果。

5. INT8 量化基础

量化是把浮点数映射到有限的整数范围。对于 INT8:

\[q\in[-128,127] \]

常用的仿射量化公式为:

\[q= \operatorname{clip} \left( \operatorname{round}\left(\frac{x}{s}\right)+z, q_{\min},q_{\max} \right) \]

反量化公式为:

\[\hat{x}=s(q-z) \]

其中:

  • round 是四舍五入取整函数
  • \(x\):原始浮点数;
  • \(q\):量化后的整数;
  • \(s\):scale,缩放因子;
  • \(z\):zero point,零点;
  • \(\hat{x}\):反量化后的近似值。

Scale

Scale 表示整数单位对应多少浮点数值。

对于非对称量化,可以使用:

\[s= \frac{x_{\max}-x_{\min}} {q_{\max}-q_{\min}} \]

Scale 越小,量化间隔越细,但能够覆盖的数值范围也越小。

Zero Point

Zero point 用于保证浮点数 0 可以准确映射到某个整数:

\[z= \operatorname{round} \left( q_{\min}-\frac{x_{\min}}{s} \right) \]

并将其截断到合法整数范围。

对称量化通常令:

\[z=0 \]

它实现简单、计算高效,但如果数据分布明显不对称,可能浪费一部分 INT8 表示范围。

6. Per-Tensor 与 Per-Channel

Per-Tensor 量化

整个张量共享同一组 scale 和 zero point:

\[q_i=\operatorname{round}\left(\frac{x_i}{s}\right)+z \]

优点是实现简单、额外存储少。缺点是如果张量中不同区域的数值范围差异很大,量化误差可能较大。

Per-Channel 量化

不同通道分别使用自己的量化参数:

\[q_{c,i} = \operatorname{round} \left(\frac{x_{c,i}}{s_c}\right)+z_c \]

例如线性层权重:

\[W\in\mathbb{R}^{N_{\mathrm{out}}\times N_{\mathrm{in}}} \]

通常可以为每个输出通道设置一个 scale:

\[s_1,s_2,\ldots,s_{N_{\mathrm{out}}} \]

Per-channel 能适应不同通道的数值分布,通常比 per-tensor 精度更好,但会增加量化参数和实现复杂度。

实际推理中常见的选择是:

  • 权重:适合 per-channel 量化;
  • 激活值:经常采用 per-tensor 或 per-token 量化;
  • 对精度要求较高时,使用更细粒度的 group-wise 量化。

7.Group-wise 量化

Group-wise 量化(分组量化)是 per-tensor 和 per-channel 之间的一种折中方案:把张量切成若干组,每组共享一套量化参数 scalezero point

设一个权重矩阵为:

\[W\in\mathbb{R}^{N_{\text{out}}\times N_{\text{in}}} \]

沿输入维度每 \(G\) 个元素分为一组:

\[[W_{i,0},\ldots,W_{i,G-1}],\quad [W_{i,G},\ldots,W_{i,2G-1}],\ldots \]

\(g\) 组使用自己的 \(s_g,z_g\)

\[q_i=\operatorname{round}\left(\frac{W_i}{s_g}\right)+z_g \]

反量化为:

\[\hat W_i=s_g(q_i-z_g) \]

例如一行有 4096 个权重,group_size=128,那么这一行会被分为:

\[4096/128=32\text{ 组} \]

每组拥有独立的 scale。

posted @ 2026-06-28 15:43  wljss  阅读(31)  评论(0)    收藏  举报