AI 工程基础体系 · 第 79/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

模型量化:PTQ、QAT、INT8、INT4、精度损失与硬件适配

模型量化(Quantization)是把模型中的浮点数表示转换为低比特整数或其他低精度表示的过程。最常见的目标是将 FP32FP16BF16 的权重、激活值、梯度或缓存,转换为 INT8INT4 等整数格式,从而降低模型内存占用、内存带宽需求和部分计算成本。

量化并不等于简单地“把小数截断为整数”。一个可部署的量化系统至少要同时处理以下问题:

  • 浮点值如何映射到有限的整数范围;
  • 哪些张量需要量化,权重、激活值、梯度和 KV Cache 是否采用同一精度;
  • 量化参数如何获得;
  • 误差如何传播到模型输出;
  • PTQ 和 QAT 分别在什么阶段处理误差;
  • INT8INT4 的计算、存储与精度含义是什么;
  • 目标硬件是否真正支持对应的数据格式和算子;
  • 量化后的模型如何通过任务评测、延迟、吞吐、成本和安全检查。

量化的基本对象:权重、激活值与中间状态

深度学习模型中的数值通常不是一种对象。以 Transformer 为例,模型至少包含:

  1. 权重(Weight):线性层、嵌入层、归一化层等保存的参数;
  2. 激活值(Activation):输入经过每一层计算后产生的中间张量;
  3. 梯度(Gradient):训练时反向传播得到的参数梯度;
  4. 缓存(Cache):生成式模型推理时保存的 Key、Value,即 KV Cache;
  5. 累加结果(Accumulator):低精度乘法之后,通常使用更高精度进行累加。

这些对象可以使用不同的精度。例如:

  • W8A8:权重 INT8,激活值 INT8;
  • W4A16:权重 INT4,激活值保持 FP16 或 BF16;
  • W8A16:权重 INT8,激活值保持 FP16 或 BF16;
  • W4A8:权重 INT4,激活值 INT8;
  • 权重使用 INT4,KV Cache 使用 INT8,计算累加使用 FP16 或 INT32。

因此,“模型是 INT8”并不一定意味着模型中所有数值都使用 INT8。必须明确量化对象、存储格式、乘法格式和累加格式。

Transformer 的核心计算包括线性变换和注意力。以注意力中的一个矩阵乘法为例:

Y=XWY = XW

其中:

  • XX 是输入激活值;
  • WW 是权重;
  • YY 是输出激活值。

如果只量化 WW,则是权重量化;如果 XXWW 都量化,则属于权重和激活值联合量化。注意力机制本身通过 QKTQK^T 生成相关性分数,再经过 Softmax 加权 VV。这种指数归一化结构会放大量化误差,因此注意力相关张量通常比普通线性层更需要单独评估。


从浮点数到整数:缩放因子与零点

最常见的仿射量化(affine quantization)使用缩放因子 ss 和零点 zz

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

x^=s(qz)\hat{x}=s(q-z)

其中:

  • xx 是原始浮点数;
  • qq 是量化后的整数;
  • x^\hat{x} 是反量化后的近似浮点数;
  • ss 是 scale,表示一个整数单位对应的浮点数间隔;
  • zz 是 zero point,使浮点数 0 能映射到整数空间中的某个位置;
  • qminq_{\min}qmaxq_{\max} 是整数类型的范围;
  • round 表示舍入;
  • clip 表示超出范围时截断。

量化部署时通常保存整数 qq、scale 和 zero point。需要进行浮点计算的算子可以先反量化:

x^=s(qz)\hat{x}=s(q-z)

如果硬件支持整数内核,也可以直接改写矩阵乘法,使缩放操作延迟到累加之后。

对称量化

对称量化通常令:

z=0z=0

对于有符号 INT8,整数范围一般为:

[128,127][-128,127]

实际实现中常使用:

[127,127][-127,127]

以保持正负范围对称。设张量最大绝对值为:

a=maxixia=\max_i |x_i|

则:

s=a127s=\frac{a}{127}

量化公式为:

q=clip(round(xs),127,127)q=\operatorname{clip}\left(\operatorname{round}\left(\frac{x}{s}\right),-127,127\right)

对称量化的优点是计算简单,整数乘法和硬件实现较方便;缺点是如果数据分布明显偏向一侧,会浪费一部分整数编码空间。

非对称量化

如果张量范围是 [xmin,xmax][x_{\min},x_{\max}],整数范围是 [qmin,qmax][q_{\min},q_{\max}],则:

s=xmaxxminqmaxqmins=\frac{x_{\max}-x_{\min}}{q_{\max}-q_{\min}}

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

非对称量化可以更充分地利用整数范围,尤其适合激活值通常非负的场景,例如经过 ReLU 的张量。但它在整数矩阵乘法中需要处理 zero point,计算路径可能比对称量化复杂。


一个完整的 INT4 量化算例

假设一个张量为:

x=[1.2, 0.2, 1.8, 2.1]x=[-1.2,\ 0.2,\ 1.8,\ 2.1]

采用非对称无符号 INT4,整数范围为:

[0,15][0,15]

如果校准范围取:

xmin=1.2,xmax=1.8x_{\min}=-1.2,\quad x_{\max}=1.8

则:

s=1.8(1.2)150=3.015=0.2s=\frac{1.8-(-1.2)}{15-0}=\frac{3.0}{15}=0.2

z=round(01.20.2)=6z=\operatorname{round}\left(0-\frac{-1.2}{0.2}\right)=6

量化过程如下:

原始值 xx x/s+zx/s+z 量化值 qq 反量化值 s(qz)s(q-z)
-1.2 0 0 -1.2
0.2 7 7 0.2
1.8 15 15 1.8
2.1 16.5 15,发生截断 1.8

最后一个值超过了校准范围,因此被截断为 1.8。这里的误差不是舍入误差,而是裁剪误差(clipping error)

2.11.8=0.32.1-1.8=0.3

这个例子说明,量化精度不只由“4 bit”决定,还取决于量化范围。如果把范围扩大到 [1.2,2.1][-1.2,2.1],则不再截断 2.1,但 scale 会变大,区间内其他值的舍入误差也会增加。

量化本质上是在以下两类误差之间取舍:

  • 舍入误差:量化间隔太大,数值被映射到附近整数;
  • 裁剪误差:范围太窄,异常值被压到边界。

为什么 INT4 的误差通常明显大于 INT8

对于对称均匀量化,假设数据范围为 [a,a][-a,a],整数正半轴最大值为 QQ,则量化步长近似为:

Δ=aQ\Delta=\frac{a}{Q}

如果不发生裁剪,均匀舍入误差大致落在:

[Δ2,Δ2]\left[-\frac{\Delta}{2},\frac{\Delta}{2}\right]

在理想均匀分布假设下,误差方差近似为:

Var(e)Δ212\operatorname{Var}(e)\approx \frac{\Delta^2}{12}

INT8 的 QQ 通常约为 127,而 INT4 的有符号正半轴只有约 7。因此在相同范围下,INT4 的量化步长大约是 INT8 的:

127718.1\frac{127}{7}\approx18.1

倍,理想化的误差方差约大:

18.1232818.1^2\approx328

这不是所有模型上的实际精度损失比例,因为真实张量并不均匀,且不同层的误差敏感性不同。但它解释了为什么 INT4 通常不能简单替换 INT8:INT4 必须依赖更细粒度的 scale、特殊的权重重排、校准算法、模型结构冗余或更高精度的激活值来弥补误差。


量化粒度:Per-Tensor、Per-Channel 与 Per-Group

量化参数不一定对整个张量只有一组。量化粒度决定了 scale 和 zero point 的数量。

Per-Tensor

整个张量共享一个 scale:

qij=round(xijs)q_{ij}=\operatorname{round}\left(\frac{x_{ij}}{s}\right)

它实现简单,元数据少,但容易受到少量异常值影响。如果一个张量中大部分值接近 0,只有极少数值很大,则 scale 被异常值决定,普通值会集中映射到很少的整数格子。

Per-Channel

每个输出通道拥有独立 scale。对于线性层权重 WRCout×CinW\in\mathbb{R}^{C_{\text{out}}\times C_{\text{in}}},通常按输出通道量化:

sc=maxjWc,jQs_c=\frac{\max_j |W_{c,j}|}{Q}

这样每个输出神经元可以使用自己的动态范围,通常比 Per-Tensor 更准确,代价是需要保存更多 scale。

Per-Group

将连续的若干权重分为一组,每组使用独立 scale。例如每 32、64 或 128 个权重共享一个 scale。Per-Group 常见于 LLM 的 INT4 权重量化,因为它在精度、元数据开销和硬件读取效率之间取得折中。

量化粒度不是越细越好。过细的分组会增加 scale 存储、解码和内存访问成本;如果硬件内核只针对固定 group size 优化,非标准分组可能无法获得预期性能。


权重、激活值和累加器的精度组合

矩阵乘法的整数实现可以写成:

Xsx(Qxzx),Wsw(Qwzw)X\approx s_x(Q_x-z_x),\quad W\approx s_w(Q_w-z_w)

于是:

Y=XWY=XW

可以近似为:

Ysxsw(Qxzx)(Qwzw)Y\approx s_xs_w(Q_x-z_x)(Q_w-z_w)

实际实现通常在更宽的整数类型中累加,例如:

A=kqx,kqw,kA=\sum_k q_{x,k}q_{w,k}

然后再应用 scale。即使输入是 INT8,累加器也不能无条件使用 INT8,因为多个乘积相加可能溢出。常见实现会使用 INT32 累加,再转换到 FP16、BF16、FP32 或重新量化为 INT8。

因此,下面三件事必须区分:

  • 存储精度:参数在内存中占多少 bit;
  • 乘法精度:乘法单元处理什么格式;
  • 累加精度:部分和使用什么格式保存。

“INT8 计算”可能意味着 INT8×INT8→INT32,也可能是 INT8 权重解码成 FP16 后由 FP16 单元计算。两者的性能、误差和硬件要求并不相同。


PTQ:训练后量化

PTQ(Post-Training Quantization)是在模型训练完成后进行量化,不通过量化误差反向更新原始模型参数。

典型过程如下:

flowchart LR
    A[已训练浮点模型] --> B[选择量化对象]
    B --> C[准备校准数据]
    C --> D[统计权重和激活值范围]
    D --> E[计算 scale/zero point]
    E --> F[转换为整数权重或量化算子]
    F --> G[离线精度评测]
    G --> H{是否满足指标}
    H -- 是 --> I[编译目标硬件内核]
    H -- 否 --> J[调整粒度/范围/层策略]
    J --> D

仅权重量化

仅权重量化只将 WW 转换为 INT8 或 INT4,激活值保持 FP16、BF16 或 FP32。典型计算路径是:

  1. 从内存读取压缩的 INT4 权重;
  2. 使用 scale 将权重恢复为计算所需的高精度表示,或者由专用内核直接参与计算;
  3. 与高精度激活值进行矩阵乘法;
  4. 使用高精度累加器得到结果。

这种方式的主要收益来自显著降低权重内存占用和内存带宽压力。对于大语言模型,线性层权重通常占据主要参数存储,因此 W4A16 往往比强制所有激活值都 INT4 更容易保持质量。

但它不会自动减少所有计算成本。若硬件只能高效处理 FP16 矩阵乘法,权重解码成本、访存方式和 kernel 融合方式会决定实际收益。

动态激活量化

动态量化在推理时根据当前输入或当前激活值计算 scale:

  1. 读取一批浮点激活值;
  2. 运行时统计范围;
  3. 计算 scale;
  4. 转换为整数;
  5. 执行整数算子。

它不需要完整的离线校准数据,适合输入分布变化较大的场景,但运行时统计和量化会增加开销,且延迟更难预测。

静态激活量化

静态量化在离线阶段使用代表性校准数据估计激活值范围,部署时直接使用保存的 scale。这样推理阶段不必每次重新统计,延迟通常更稳定。

它的前提是校准数据能够覆盖真实输入分布。如果线上请求与校准数据差异很大,激活值可能大量超出范围,导致裁剪和精度下降。

PTQ 的优点和限制

PTQ 不需要重新训练,实施周期短,也不需要保存完整训练数据和训练状态。它适合:

  • 已经训练完成、无法方便重新训练的模型;
  • 需要快速降低模型存储成本的场景;
  • 主要采用 W8A16、W4A16 等权重量化的 LLM 推理场景。

它的限制是量化误差只能在部署前通过量化策略、校准数据和层级保护来缓解,模型本身不会主动学习抵抗误差。对于异常值明显、层间敏感性差异大或需要 W8A8/W4A8 的模型,PTQ 可能无法达到目标精度。


QAT:量化感知训练

QAT(Quantization-Aware Training)在训练或微调阶段模拟部署时的量化过程,使模型参数逐渐适应量化误差。

训练时通常不直接使用整数算子更新权重,而是在前向传播中插入“伪量化”(fake quantization):

FakeQuant(x)=s(clip(round(xs)+z,qmin,qmax)z)\operatorname{FakeQuant}(x) = s\left( \operatorname{clip}\left( \operatorname{round}\left(\frac{x}{s}\right)+z, q_{\min},q_{\max} \right)-z \right)

前向传播看到的是量化后的近似值,反向传播仍然需要对浮点参数求梯度。由于 roundclip 在严格数学意义下不可导,常见实现使用直通估计器(STE,Straight-Through Estimator)近似梯度:

FakeQuant(x)x{1,x 未被裁剪0,x 被裁剪\frac{\partial \operatorname{FakeQuant}(x)}{\partial x} \approx \begin{cases} 1,& x \text{ 未被裁剪}\\ 0,& x \text{ 被裁剪} \end{cases}

一个简化的 QAT 训练过程是:

  1. 初始化浮点模型;
  2. 为权重和激活值插入伪量化节点;
  3. 使用训练数据执行前向传播;
  4. 通过 STE 近似反向传播;
  5. 更新浮点主权重;
  6. 重复训练或微调;
  7. 导出真正的整数权重和量化算子;
  8. 使用部署内核进行验证。

QAT 的目标函数可以写为:

minθE(x,y)D[L(fFakeQuant(θ)(x),y)]\min_{\theta} \mathbb{E}_{(x,y)\sim D} \left[ \mathcal{L} \left( f_{\operatorname{FakeQuant}(\theta)}(x),y \right) \right]

其中:

  • θ\theta 是浮点主权重;
  • DD 是训练或微调数据;
  • fFakeQuantf_{\operatorname{FakeQuant}} 是插入伪量化节点后的模型;
  • L\mathcal{L} 是任务损失函数。

与 PTQ 的关键差异在于:PTQ 先固定模型再量化,QAT 则让模型参数参与适应量化后的前向路径。

QAT 不等于训练时使用真实 INT4

许多 QAT 实现的训练权重仍保存为 FP32,优化器状态也可能使用 FP32。只有前向传播模拟了低精度,最终导出阶段才生成整数权重。因此,QAT 的训练显存和计算成本通常不能按部署时的 INT4 成本估算。

此外,如果训练时使用的伪量化算子与部署硬件的真实量化算子不一致,QAT 学到的补偿可能在部署后失效。例如:

  • 训练中使用 Per-Tensor,部署内核使用 Per-Channel;
  • 训练中允许动态 scale,部署中使用固定 scale;
  • 训练中激活值保持 FP16,部署中却使用 INT8;
  • 训练中使用一种舍入规则,部署内核使用另一种规则。

因此,QAT 的伪量化配置必须尽量匹配最终部署格式。

QAT 的成本和适用场景

QAT 通常需要:

  • 具有代表性的训练或微调数据;
  • 额外的训练时间;
  • 可保存训练状态和实验版本;
  • 对量化算子、导出格式和部署内核进行联调。

它适合:

  • PTQ 精度不足;
  • 目标是激活值也量化的 W8A8、W4A8;
  • 需要对特定任务进行恢复训练;
  • 模型供应方能够控制训练或微调流程。

如果只有少量无标签文本,QAT 不一定能直接解决问题;可以考虑蒸馏、校准增强或任务数据微调,但这些方法与 QAT 不是同一概念。


INT8 与 INT4:比特数之外还要看表示方式

INT8

INT8 使用 8 bit 整数存储。对于有符号对称量化,常用有效范围约为 [127,127][-127,127]。它通常具有:

  • 较小的量化误差;
  • 较成熟的 CPU、GPU、NPU 和推理库支持;
  • 较适合权重和激活值同时量化;
  • 相对稳定的跨模型表现。

如果模型采用 W8A8,并且硬件拥有原生 INT8 矩阵乘法,通常可以同时降低权重存储、激活存储和计算带宽。但实际效果取决于算子覆盖率、张量形状和数据布局。

INT4

INT4 每个权重只使用 4 bit,理论上原始权重存储量约为 FP16 的四分之一、INT8 的一半。但实际存储还需要考虑:

  • 每个 tensor、channel 或 group 的 scale;
  • zero point;
  • 可能的偏置;
  • 权重打包后的对齐填充;
  • 元数据和索引;
  • kernel 的临时解码缓冲区。

因此,理论压缩比不是最终模型文件或运行时内存的精确压缩比。

INT4 常见于 LLM 的权重量化,尤其是 W4A16。相比激活值,权重的统计分布可以离线分析,且专用 kernel 可以在读取时完成解码。INT4 激活量化则更困难,因为激活值随输入变化,并且异常值会直接影响运行时范围。

“4 bit”不是单一格式

以下方案都可能被称为 INT4,但行为不同:

  • 对称 INT4 或非对称 INT4;
  • 每个张量、每个通道或每组权重一个 scale;
  • 是否使用 zero point;
  • group size 是 32、64、128 还是其他值;
  • 权重是否经过重排;
  • 是否采用特殊的误差优化目标;
  • 硬件是原生 INT4 乘法,还是先解码再计算。

因此,比较两个“INT4 模型”时,必须同时记录量化方案、分组方式、激活精度、累加精度、权重布局和目标 kernel。


精度损失从哪里产生

量化误差可以从单个张量逐步传播到模型输出。对于一层线性变换:

Y=XWY=XW

若量化后使用:

X^=X+EX,W^=W+EW\hat{X}=X+E_X,\quad \hat{W}=W+E_W

则:

Y^=X^W^=(X+EX)(W+EW)\hat{Y} = \hat{X}\hat{W} = (X+E_X)(W+E_W)

展开得到:

Y^=XW+EXW+XEW+EXEW\hat{Y} = XW+E_XW+XE_W+E_XE_W

因此输出误差为:

Y^Y=EXW+XEW+EXEW\hat{Y}-Y = E_XW+XE_W+E_XE_W

这说明:

  • 激活误差会被权重放大;
  • 权重误差会被输入激活放大;
  • 权重和激活同时量化时还会产生交叉项;
  • 权重范数和输入范数较大的层可能对误差更敏感。

在深层网络中,这些误差还会经过残差连接、归一化、激活函数和 Softmax 继续传播。对生成式模型而言,某一步生成的错误 token 还可能影响后续上下文,使误差以序列形式累积。

异常值导致 scale 被“劫持”

假设一个通道的激活值为:

[0.01,0.02,0.03,8.0][0.01,0.02,0.03,8.0]

如果使用 Per-Tensor 对称 INT8,scale 由 8.0 决定:

s=8.01270.063s=\frac{8.0}{127}\approx0.063

前三个值量化后都可能接近 0,细节严重丢失。如果将异常值单独处理、改用 Per-Channel,或者采用能够迁移异常值影响的量化方法,普通值可以获得更细的量化间隔。

截断范围错误

如果校准数据没有覆盖线上输入,静态激活量化的范围可能过窄。例如校准阶段最大值是 2,线上出现 5,那么线上大量激活值会被映射到最大整数。模型输出可能表现为:

  • 分类置信度异常;
  • 生成文本重复;
  • 某些输入突然出现乱码;
  • 长上下文性能明显下降;
  • 特定语言、代码或数字任务退化。

层敏感性不同

同一个量化方案对不同层的影响可能完全不同。常见敏感部位包括:

  • 输入嵌入和输出语言模型头;
  • 注意力中的投影层;
  • 具有异常激活值的通道;
  • 归一化前后的张量;
  • 低维瓶颈层;
  • 直接影响 logits 的最后几层。

“所有层统一 INT4”通常不是理论必然正确的方案。混合精度可以将敏感层保留为 FP16、BF16 或 INT8,而把大部分普通线性层压缩到 INT4。


LLM 中的量化特殊性

大语言模型的主体通常由大量 Transformer 层堆叠组成。每层包含自注意力和前馈网络,其中线性层的权重规模很大,因此权重量化对内存占用尤其重要。

权重异常值与激活异常值

LLM 的权重通常相对适合离线量化,但激活值可能存在少量幅度显著更大的通道。少量异常值会扩大 scale,降低其他数值的有效分辨率。

工程上常见的处理方向包括:

  • 使用 Per-Channel 或 Per-Group scale;
  • 将异常值通道保留为高精度;
  • 对权重或激活做等价变换;
  • 对敏感层采用更高精度;
  • 通过 QAT 让模型适应量化;
  • 使用针对 LLM 权重误差优化的 PTQ 算法。

这些方法的共同目标不是消除量化误差,而是重新分配误差,使重要方向受到更小影响。

GPTQ、AWQ 与 SmoothQuant 的位置

GPTQ、AWQ、SmoothQuant 等名称经常出现在 LLM 量化工具链中,但它们不是 INT4 的同义词,也不都属于同一种训练流程。

  • GPTQ 通常指一类基于校准数据、逐层近似二阶信息或误差补偿的训练后权重量化方法;
  • AWQ 通常通过分析激活重要性,保护对输出更重要的权重方向;
  • SmoothQuant 的核心思路是通过等价缩放在权重和激活之间迁移量化难度,以改善激活量化;
  • QLoRA 是使用量化基础模型进行参数高效微调的方法,不等同于 QAT。基础权重的低比特存储、低秩适配器训练和最终部署格式需要分别说明。

具体算法的支持范围、导出格式和硬件兼容性依赖工具版本,不能仅依据算法名称推断部署性能。

KV Cache 量化

自回归生成时,模型会缓存历史 token 的 Key 和 Value:

K1:t,V1:tK_{1:t},V_{1:t}

随着上下文长度和并发请求增加,KV Cache 可能成为显存瓶颈。KV Cache 可以使用 FP16、BF16、INT8,某些系统也支持更低比特格式。

KV Cache 量化与权重量化不同:

  • 权重在服务启动前基本固定,可以离线量化;
  • KV Cache 随请求和生成过程动态增长;
  • 不同层、不同头和不同时间位置的分布可能不同;
  • 量化误差会直接影响后续注意力分数;
  • 需要处理缓存分配、释放、分页和并发请求生命周期。

因此,即使权重 INT4 已经通过精度测试,KV Cache 仍可能需要单独评估。长上下文、批量生成和高并发场景尤其不能只测单条短文本。


PTQ 的校准数据不是随便找几条文本

校准数据用于估计激活值范围、通道重要性或量化误差。它不一定需要完整标注,但必须尽量代表真实请求。

校准数据应覆盖:

  • 真实语言分布;
  • 输入长度分布;
  • 代码、数字、表格、多语言等业务类型;
  • 系统提示词和工具调用格式;
  • 长上下文场景;
  • 线上可能出现的边界输入。

如果只用短英文新闻校准一个需要处理中文、代码和长上下文的模型,激活范围和层间误差可能严重失配。

校准集还要受到数据权限约束。生产系统中的用户对话、企业文档和日志不能因为用于量化就自动获得训练或离线分析权限。应明确:

  • 数据是否允许用于模型校准;
  • 是否包含个人信息、机密信息或受合同限制的数据;
  • 是否需要脱敏、采样和访问审计;
  • 校准结果中是否可能残留原始数据;
  • 谁可以读取原始样本和导出的模型文件。

量化可以降低推理成本,但不会改变数据治理要求。


一个可运行的量化示例

下面的 Python 示例只依赖 NumPy,展示对称和非对称量化的基本过程。它不是完整的高性能推理内核,但可以用于验证 scale、zero point、截断和均方误差。

import numpy as np


def symmetric_quantize(x, bits=8):
    """
    对称有符号量化。
    返回:整数张量 q、scale、反量化张量 x_hat。
    """
    x = np.asarray(x, dtype=np.float32)

    qmax = 2 ** (bits - 1) - 1
    qmin = -qmax

    max_abs = np.max(np.abs(x))
    scale = max_abs / qmax if max_abs != 0 else 1.0

    q = np.round(x / scale)
    q = np.clip(q, qmin, qmax).astype(np.int32)
    x_hat = q.astype(np.float32) * scale

    return q, scale, x_hat


def asymmetric_quantize(x, bits=4, xmin=None, xmax=None):
    """
    非对称无符号量化。
    允许调用方显式传入校准范围,以展示范围不足时的 clipping。
    """
    x = np.asarray(x, dtype=np.float32)

    qmin = 0
    qmax = 2 ** bits - 1

    if xmin is None:
        xmin = float(np.min(x))
    if xmax is None:
        xmax = float(np.max(x))

    if xmax <= xmin:
        raise ValueError("xmax 必须大于 xmin")

    scale = (xmax - xmin) / (qmax - qmin)
    zero_point = int(np.round(qmin - xmin / scale))
    zero_point = int(np.clip(zero_point, qmin, qmax))

    q = np.round(x / scale + zero_point)
    q = np.clip(q, qmin, qmax).astype(np.int32)
    x_hat = (q.astype(np.float32) - zero_point) * scale

    return q, scale, zero_point, x_hat


x = np.array([-1.2, 0.2, 1.8, 2.1], dtype=np.float32)

q4, s4, z4, x4_hat = asymmetric_quantize(
    x,
    bits=4,
    xmin=-1.2,
    xmax=1.8,
)

q8, s8, x8_hat = symmetric_quantize(x, bits=8)

print("INT4 q       =", q4)
print("INT4 scale   =", s4)
print("INT4 zero    =", z4)
print("INT4 dequant =", x4_hat)
print("INT4 MSE     =", np.mean((x - x4_hat) ** 2))

print("INT8 q       =", q8)
print("INT8 scale   =", s8)
print("INT8 dequant =", x8_hat)
print("INT8 MSE     =", np.mean((x - x8_hat) ** 2))

在 INT4 示例中,输入值 2.1 超过校准上限 1.8,因此量化整数会被截断到 15,反量化结果接近 1.8。INT8 使用整个输入的最大绝对值,量化间隔更小,通常具有更低的数值误差。

这个示例还揭示了一个部署风险:如果只保存整数权重而没有保存正确的 scale 和 zero point,整数值无法恢复到正确的数值范围。模型文件格式必须明确记录量化参数的形状、布局、数据类型和作用维度。


如何定位量化导致的精度下降

不能只比较量化前后的最终准确率。应建立逐层、逐算子和端到端三层诊断。

第一层:数值级比较

对同一批输入,比较浮点模型和量化模型的:

  • 每层输出均值和方差;
  • 最大绝对误差;
  • 均方误差;
  • 相对误差;
  • 饱和比例;
  • 零值比例;
  • scale 分布;
  • 每个通道的误差。

饱和比例可以定义为:

rsat=#{q=qmin 或 q=qmax}#qr_{\text{sat}} = \frac{\#\{q=q_{\min}\ \text{或}\ q=q_{\max}\}}{\#q}

如果某个层的饱和比例异常高,通常说明校准范围过窄、异常值处理不当或量化粒度过粗。

第二层:逐层替换

将量化模型中的某一层或某一组层恢复为 FP16/BF16,再运行同一评测集。如果恢复某层后指标显著回升,该层就是候选敏感层。

这种方法可以区分:

  • 整个方案都不适合;
  • 只有少数层需要保留高精度;
  • 某个量化算子或导出过程存在错误。

第三层:任务和服务级比较

生成式模型至少应比较:

  • 困惑度(Perplexity);
  • 任务准确率、F1、EM 或代码测试通过率;
  • 长上下文检索准确率;
  • 结构化输出合法率;
  • 工具调用成功率;
  • 安全策略和拒答边界;
  • 首 token 延迟;
  • 单 token 解码延迟;
  • 吞吐;
  • 峰值显存;
  • 不同并发度下的尾延迟。

生成任务不能只比较字符串完全相等。量化模型可能生成语义相同但文本不同的答案,也可能在数字、代码、JSON 格式和工具参数上出现微小但关键的错误,因此应按业务风险选择评测指标。


常见失败表现与对应原因

准确率下降,但单层 MSE 不高

单层均方误差低,不代表任务影响低。一个直接影响 logits 排序的微小误差,可能改变最终 token;而某个中间层的较大误差,可能被残差和归一化部分抵消。

诊断时应观察:

  • logits top-k 是否发生变化;
  • 关键 token 的概率是否显著变化;
  • 量化误差是否集中在输出头、注意力层或少数通道;
  • 错误是否只发生在特定任务和输入长度。

短文本正常,长文本退化

这通常与误差累积、KV Cache 量化、注意力分数扰动或校准数据缺少长上下文有关。应分别测试:

  • 固定输入长度逐步增加;
  • 只量化权重、不量化 KV Cache;
  • 只量化 KV Cache、不量化权重;
  • 不同 batch size 和并发度;
  • Prefill 阶段与 Decode 阶段。

模型占用下降,但延迟没有下降

原因可能包括:

  • 硬件没有原生 INT4/INT8 内核;
  • 权重读取后仍需要高成本解码;
  • 量化算子没有覆盖所有层;
  • 张量维度不满足 kernel 的 tile 对齐要求;
  • 量化后引入了额外的格式转换;
  • 端到端瓶颈在 KV Cache、通信、采样或数据预处理,而不是权重读取。

因此,内存压缩比和延迟加速比不能直接等同。

部署后结果与离线结果不一致

常见原因包括:

  • 离线使用了伪量化,部署使用了不同的真实 kernel;
  • scale 的广播维度错误;
  • zero point 的有符号/无符号解释不一致;
  • 权重打包顺序与解包顺序不一致;
  • 舍入规则不同;
  • FP16、BF16、FP32 中间结果的转换位置不同;
  • 部署框架对某些算子自动回退到浮点实现。

恢复方法通常是先固定一个小输入,逐层导出中间张量,在离线参考实现、模型转换器和运行时之间做 bit-level 或近似数值对比,而不是直接从最终生成文本猜原因。


硬件适配:数据格式支持不等于端到端支持

硬件适配至少包含四个层次:

  1. 数据类型支持:硬件能否加载和保存 INT8、INT4;
  2. 算术支持:是否有原生低比特乘法和足够宽的累加器;
  3. 内核支持:目标框架是否有对应矩阵乘法、卷积、注意力和归一化 kernel;
  4. 图级支持:模型中大多数算子能否保持在低精度路径,是否频繁回退或转换格式。

例如,硬件可以支持 INT4 权重读取,但如果每个线性层都要先解码为 FP16,且解码和转置无法与矩阵乘法融合,那么理论上的 4 bit 存储优势可能无法转化为相同幅度的延迟收益。

CPU

CPU 上的 INT8 支持通常比 INT4 更成熟。需要关注:

  • 指令集是否支持向量化整数乘法;
  • INT32 累加是否高效;
  • 权重布局是否匹配缓存行;
  • 线程数增加后是否受内存带宽限制;
  • 不同 batch size 下的性能变化。

对于小 batch、低延迟服务,内存访问和线程调度可能比峰值算力更重要。

GPU

GPU 的低精度性能通常依赖具体架构、矩阵核心、CUDA 或其他运行时版本,以及 kernel 对张量维度和布局的要求。需要确认:

  • 目标 GPU 是否原生支持对应的 INT8/INT4 MMA;
  • 使用的是 W8A8、W4A16 还是其他路径;
  • scale 和 zero point 是否能在 kernel 内融合;
  • 权重是否按硬件要求打包;
  • attention、RMSNorm、RoPE、采样等非 GEMM 部分是否成为瓶颈;
  • 多卡场景中通信是否仍使用高精度格式。

“显卡支持 INT4”只能说明硬件或软件栈存在某种支持,不能推出特定模型一定会加速。

NPU、推理加速卡与专用芯片

专用加速器通常要求固定的:

  • 张量布局;
  • 对齐尺寸;
  • 分组大小;
  • scale 数据类型;
  • 累加器宽度;
  • 算子集合;
  • 编译器版本。

某些芯片支持 INT8 的静态图编译,但不支持任意动态激活量化;某些芯片支持 INT4 权重,但要求特定的 group size 和权重打包格式。部署前必须以目标芯片的编译结果和运行结果为准,而不是以模型转换成功为准。


量化系统中的数据流与生命周期

生产推理系统可以将量化模型看成一个带版本和状态的制品:

flowchart TD
    A[训练完成的浮点模型] --> B[模型与Tokenizer版本冻结]
    B --> C[权限审查与校准数据脱敏]
    C --> D[校准与量化]
    D --> E[导出量化制品]
    E --> F[目标硬件编译]
    F --> G[数值回归]
    G --> H[任务与安全评测]
    H --> I[延迟/吞吐/显存压测]
    I --> J{全部通过}
    J -- 否 --> K[定位层/算子/硬件问题]
    K --> D
    J -- 是 --> L[灰度发布]
    L --> M[监控质量、错误率、成本和尾延迟]
    M --> N{线上异常}
    N -- 是 --> O[回滚到上一制品]
    N -- 否 --> P[扩大流量]

量化制品不应只包含一个权重文件,还应记录:

  • 原始浮点模型版本;
  • tokenizer 和配置版本;
  • 量化方法;
  • 权重和激活精度;
  • 对称或非对称方式;
  • 量化粒度和 group size;
  • scale、zero point 的布局;
  • 校准数据版本及其权限范围;
  • 导出工具和运行时版本;
  • 目标硬件与 kernel 版本;
  • 离线评测结果;
  • 已知不支持的算子和回退路径。

如果量化模型上线后精度或延迟异常,应能够根据制品元数据恢复到对应的浮点模型、校准集和编译配置。否则,量化问题会与模型版本、数据分布和硬件变化混在一起,难以定位。


成本取舍:压缩收益不是只有模型文件大小

量化可以影响多个成本项:

Ctotal=Ccompute+Cmemory+Clatency+Cengineering+CqualityC_{\text{total}} = C_{\text{compute}} + C_{\text{memory}} + C_{\text{latency}} + C_{\text{engineering}} + C_{\text{quality}}

其中:

  • CcomputeC_{\text{compute}}:计算资源成本;
  • CmemoryC_{\text{memory}}:显存、内存和存储成本;
  • ClatencyC_{\text{latency}}:延迟和尾延迟带来的服务成本;
  • CengineeringC_{\text{engineering}}:校准、导出、调试和维护成本;
  • CqualityC_{\text{quality}}:精度下降、人工复核和业务损失成本。

例如,W4A16 可能显著降低权重占用,但如果需要更复杂的 kernel、无法合并算子,或者在关键业务上引入大量错误,其总成本未必低于 W8A16。相反,W8A8 可能压缩比不如 INT4,但如果目标硬件拥有成熟的 INT8 端到端路径,实际单位请求成本可能更低。

并发量也会改变结论。低并发交互服务关注首 token 延迟和尾延迟,高并发批处理更关注吞吐和显存容量;长上下文服务则需要把 KV Cache 成本纳入量化收益计算。


PTQ、QAT、INT8 和 INT4 的选择逻辑

可以按照问题来源做选择,而不是先指定一个比特数:

  • 如果主要问题是权重存储和内存带宽,优先评估 W8A16 或 W4A16;
  • 如果目标硬件对 INT8 矩阵乘法有成熟支持,且激活值范围可校准,评估 W8A8;
  • 如果 PTQ 后只有少数任务指标下降,先尝试 Per-Channel、Per-Group、敏感层保留高精度或改进校准数据;
  • 如果激活值异常值导致 W8A8 明显退化,检查异常通道、范围估计和等价变换方法;
  • 如果 PTQ 仍不能满足指标,且有可靠训练数据和训练资源,再使用 QAT;
  • 如果服务受长上下文显存限制,不要只量化权重,还要单独评估 KV Cache;
  • 如果低比特格式在目标硬件上只能回退或解码,先解决 kernel 和布局问题,再判断量化方案本身是否有效。

最终应选择满足约束的最小量化方案,而不是盲目追求最低 bit 数。一个可复现的决策需要同时给出精度、峰值内存、首 token 延迟、解码吞吐、尾延迟、硬件利用率、数据权限和回滚路径。

量化的核心不是把模型从浮点数改成整数,而是在有限的数值表示、误差传播、硬件执行路径和生产约束之间重新分配资源。PTQ 通过离线估计和转换降低实施成本,QAT 通过训练让模型适应量化噪声;INT8 通常提供更稳妥的精度与硬件兼容性,INT4 则以更高的误差管理和 kernel 要求换取更低的权重成本。只有当量化格式、校准方法、评测体系和目标硬件共同闭环时,低比特模型才是可用的生产模型。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。