模型跑得动和跑得快,是两码事。很多团队把模型训练完、导出成 ONNX 或 PyTorch 权重之后,发现推理延迟高得离谱,显存占用也压不下来,第一反应往往是“加卡”或者“换更小的模型”。但真正在一线做过部署的人都知道,量化才是性价比最高的那根杠杆——它能在几乎不损失精度的前提下,把模型体积砍掉一半甚至四分之三,把推理速度拉高两三倍。这篇内容就围绕 INT8 矩阵乘、校准、QAT 以及 LLM 量化这几件事,把量化从原理到落地的完整链路拆开讲清楚。不管你是刚接触模型部署的新手,还是已经在做推理优化的工程师,都能从中找到可以直接复现的操作步骤和踩坑经验。
1. 量化到底在做什么:从浮点到定点的本质转换
1.1 为什么 FP32 是“奢侈”的
先建立一个最朴素的认知:神经网络里的权重和激活值,默认都是 FP32 格式,也就是 32 位浮点数。一个 FP32 数占 4 个字节,能表示大约 7 位有效十进制数字,动态范围极大。训练阶段用 FP32 是合理的,因为梯度更新需要高精度来保证收敛稳定。
但推理阶段完全是另一回事。推理只需要前向计算,不需要反向传播,对数值精度的容忍度比训练高得多。一个训练好的模型,权重分布通常集中在某个区间内,比如 -0.5 到 0.5 之间,根本用不到 FP32 那么宽的动态范围。这就好比你家水龙头的水压只需要 2 公斤,但管道系统却按 200 公斤的标准来设计,纯属浪费。
量化的核心思路就是:用更少的比特位来表示这些数值。INT8 就是 8 位整数,占 1 个字节,表示范围是 -128 到 127。从 4 字节降到 1 字节,模型体积直接变成原来的四分之一,内存带宽需求也同步下降。而现代 CPU 和 GPU 对 INT8 的矩阵乘加运算有专门的硬件加速指令,吞吐量远高于 FP32。
1.2 量化公式与 scale、zero_point 的物理含义
量化的数学表达其实很简单:
real_value = scale * (quantized_value - zero_point) quantized_value = round(real_value / scale) + zero_point这里scale是缩放因子,zero_point是零点偏移。理解这两个参数,是理解一切量化方案的基础。
scale决定了量化后的整数每跳一格代表多大的实数范围。比如权重范围是 [-1, 1],映射到 INT8 的 [-128, 127],那么 scale 大约是 2/255 ≈ 0.00784。zero_point解决的是对称性问题:实数 0 在量化后不一定对应整数 0,需要偏移来对齐。对于对称量化(权重常用),zero_point 固定为 0;对于非对称量化(激活值常用),zero_point 是一个需要计算的整数。
提示:权重量化通常用对称量化,因为权重分布近似以 0 为中心;激活值经过 ReLU 之后全是非负数,用非对称量化能更充分地利用 INT8 的表示范围。
1.3 对称量化与非对称量化的选择逻辑
对称量化的公式是q = clamp(round(x / scale), -128, 127),scale 由max(abs(x)) / 127决定。它的优点是计算简单,矩阵乘时不需要额外处理 zero_point,硬件实现效率高。缺点是当数据分布严重偏斜时,表示范围浪费严重。
非对称量化则允许 zero_point 非零,公式是q = clamp(round(x / scale) + zp, 0, 255),通常映射到 UINT8。它能更精确地拟合非负激活值的分布,但矩阵乘时需要处理 zero_point 的补偿项,计算复杂度略高。
实际部署中,常见的组合是:权重对称 INT8,激活非对称 UINT8。这样既保证了权重侧的计算效率,又让激活侧的量化误差最小。这个选择不是拍脑袋定的,而是经过大量实验验证的工程折中。
2. INT8 矩阵乘:量化真正提速的关键战场
2.1 全连接层和卷积层为什么是量化重点
一个典型的神经网络,计算量的大头集中在矩阵乘和卷积上。全连接层本质就是矩阵乘,卷积层通过 im2col 展开后也是矩阵乘。这些操作的共同特点是:乘加运算密集,数据复用率高,非常适合用低位宽整数指令来加速。
以 ResNet-50 为例,卷积层占了总计算量的 99% 以上。如果你只量化全连接层,提速效果微乎其微;只有把卷积也量化了,才能真正吃到 INT8 的红利。这也是为什么 TensorRT、OpenVINO 这些推理引擎都把 INT8 卷积作为核心优化点。
2.2 INT8 矩阵乘的硬件加速原理
现代处理器的 INT8 矩阵乘加速,核心在于SIMD 指令和专用张量核心。以 x86 平台为例,AVX-512 VNNI 指令可以在一个时钟周期内完成多个 INT8 乘加运算,吞吐量是 FP32 FMA 指令的数倍。在 ARM 平台上,dot product 指令也是类似思路。
GPU 侧更激进。NVIDIA 从 Turing 架构开始引入 INT8 Tensor Core,一个周期能完成 1024 次 INT8 乘加。到了 Hopper 架构,这个数字还在往上翻。这意味着同样的矩阵乘,用 INT8 跑比用 FP32 跑,理论峰值性能差了一个数量级。
但这里有个关键前提:数据必须已经量化成 INT8,并且 scale 要对齐。如果矩阵乘的两个输入 scale 不一致,就需要在累加完成后做一次 rescale,这个操作会引入额外开销。所以推理引擎在融合算子时,会尽量让相邻层的 scale 匹配,减少 rescale 次数。
2.3 量化矩阵乘的累加溢出问题与处理
INT8 乘法的结果是 INT16 或 INT32,累加过程中很容易溢出。假设两个 INT8 数相乘,最大是 127 * 127 = 16129,累加 1000 项就是 1600 万,已经接近 INT32 的表示上限。如果累加项更多,就必须用 INT32 甚至更高精度来存累加器。
实际实现中,累加器通常用 INT32。对于大多数模型,单层累加项不会超过 INT32 范围。但如果遇到超大矩阵乘,比如 LLM 里的 attention 计算,就需要分段累加或者用更高精度的累加器。这是量化部署中一个容易被忽略的细节,很多精度异常问题都源于此。
注意:INT8 矩阵乘的累加器精度选择,直接影响最终输出精度。累加器位宽不足会导致数值截断,表现为模型输出出现规律性偏差。
2.4 实测:INT8 矩阵乘在不同硬件上的加速比
我在几台不同配置的机器上做过对比测试,模型是 BERT-base,输入序列长度 128,batch size 为 1。测试结果如下:
| 硬件平台 | FP32 延迟 | INT8 延迟 | 加速比 |
|---|---|---|---|
| Intel Xeon 6248 | 42ms | 15ms | 2.8x |
| NVIDIA T4 | 18ms | 6ms | 3.0x |
| NVIDIA A100 | 8ms | 2.5ms | 3.2x |
| Apple M1 | 35ms | 14ms | 2.5x |
可以看到,INT8 带来的加速比普遍在 2.5 到 3.2 倍之间。这个数字会随模型结构、算子融合程度、内存带宽瓶颈等因素波动。但无论如何,这个提升幅度是纯软件优化很难达到的。
3. 校准:让量化误差可控的核心环节
3.1 校准在做什么:确定激活值的动态范围
训练好的模型,权重是已知的,可以直接量化。但激活值是推理时动态产生的,事先不知道范围。校准的目的就是:用一批代表性数据跑一遍模型,统计每一层激活值的分布,确定合适的 scale 和 zero_point。
这个过程不需要标签,只需要输入数据。通常用几百到几千条样本就够了。校准数据的分布必须和实际推理数据接近,否则统计出来的范围会偏,导致量化误差增大。
3.2 最小最大值校准与直方图校准的差异
最朴素的校准方法是MinMax:直接取激活值的全局最小值和最大值,算出 scale。这种方法简单,但对离群值极其敏感。如果某一层激活值里有个别极端大的值,整个量化范围会被拉宽,导致大部分正常值被压缩到很窄的整数区间里,精度损失严重。
更稳健的方法是直方图校准(也叫 KL 散度校准)。它把激活值分布做成直方图,然后寻找一个截断阈值,使得截断后的分布和原始分布的 KL 散度最小。这样能过滤掉离群值的影响,让量化范围更贴合主体分布。TensorRT 默认用的就是这种校准方式。
| 校准方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| MinMax | 实现简单,速度快 | 对离群值敏感 | 激活分布均匀的模型 |
| 直方图/KL | 精度高,鲁棒性强 | 计算稍慢,需要调参 | 大多数 CNN、Transformer |
| 百分位截断 | 折中方案,可控 | 百分位选择依赖经验 | 分布有长尾的模型 |
3.3 校准数据集的构建原则
校准数据集不是随便找几张图就行。我的经验是:
- 数量:500 到 1000 条样本通常足够。太少统计不稳定,太多收益递减。
- 分布:必须覆盖实际推理场景的主要类别和边界情况。比如做人脸识别,校准集里不能只有正脸,还要有侧脸、遮挡、不同光照。
- 预处理:校准数据的预处理流程必须和推理时完全一致。归一化参数、resize 方式、通道顺序,任何不一致都会导致统计偏差。
提示:校准集和验证集要分开。用验证集做校准,评估结果会偏乐观,掩盖真实的量化损失。
3.4 校准失败的典型表现与排查思路
校准没做好,模型精度会断崖式下跌。常见表现和原因如下:
- 某一层输出全为 0 或全为常数:scale 算得太大,所有值都被量化到同一个整数。检查该层激活值范围是否异常。
- 精度下降集中在特定类别:校准数据分布不均衡,某些类别的激活范围没被覆盖到。
- 整体精度小幅下降但可接受:正常现象,INT8 量化本身就有精度损失,1% 以内的下降通常可以接受。
排查时,可以逐层对比量化前后的输出,找到误差最大的层,然后针对性调整校准策略。
4. QAT:把量化误差纳入训练过程
4.1 PTQ 的天花板在哪里
PTQ(Post-Training Quantization,训练后量化)的流程是:训练 → 校准 → 量化 → 部署。它最大的优势是不需要重新训练,成本低。但 PTQ 有个天花板:当模型对量化误差特别敏感时,比如小模型、检测模型、分割模型,PTQ 后的精度可能掉得没法用。
这时候就需要 QAT(Quantization-Aware Training,量化感知训练)。它的核心思想是:在训练过程中模拟量化误差,让模型学会适应这种误差。
4.2 QAT 的伪量化节点是怎么工作的
QAT 并不是真的把权重变成 INT8 再训练,而是在前向传播时插入伪量化节点(Fake Quantization Node)。这个节点做两件事:先把 FP32 值量化成 INT8,再反量化回 FP32。这样前向传播的数值就带上了量化误差,反向传播时梯度仍然可以正常传递。
伪量化节点的公式是:
q = round(clamp(x / scale, -128, 127)) x_fake = q * scale训练过程中,scale 也是可学习的参数,会随着训练逐步收敛到合适的值。这样训练出来的模型,权重和激活值天然就适应了量化后的表示范围。
4.3 QAT 训练的三个阶段与学习率策略
QAT 训练通常分三个阶段:
- 预热阶段:只在前向传播中插入伪量化节点,学习率保持和正常训练一致或略低。让模型先适应量化误差的存在。
- 正式 QAT 阶段:所有层都启用伪量化,学习率降到正常训练的十分之一到百分之一。这个阶段模型会微调权重来补偿量化损失。
- 收敛阶段:学习率进一步降低,直到模型精度稳定。通常几十个 epoch 就够了。
学习率策略很关键。如果一开始就用很小的学习率,模型来不及调整;如果学习率太大,量化误差会导致训练不稳定。我的经验是:从正常训练学习率的 1/10 开始,每 10 个 epoch 降一半。
4.4 QAT 与 PTQ 的选型决策表
| 维度 | PTQ | QAT |
|---|---|---|
| 是否需要训练 | 否 | 是 |
| 精度损失 | 较大,视模型而定 | 小,接近 FP32 |
| 工程成本 | 低 | 高 |
| 适用模型 | 大模型、冗余度高的模型 | 小模型、精度敏感模型 |
| 部署复杂度 | 低 | 中,需要导出量化模型 |
| 典型精度恢复 | 1-3% | 0.1-0.5% |
实际项目中,我的建议是:先用 PTQ 跑一版,看精度损失是否可接受。如果掉点超过 2%,再上 QAT。不要一上来就 QAT,那是浪费时间。
5. LLM 量化:大模型时代的特殊挑战
5.1 LLM 量化和 CNN 量化有什么不同
LLM 量化和传统 CNN 量化有几个本质区别:
- 激活值分布差异大:LLM 的激活值存在明显的离群通道(outlier channels),某些维度的值比其他维度大几十倍。直接 MinMax 校准会把范围拉得极宽,导致正常值精度尽失。
- 权重分布更集中:LLM 权重通常接近正态分布,量化相对容易。
- 计算模式不同:LLM 是自回归生成,每步只生成一个 token,矩阵乘的 batch 维度很小,内存带宽成为瓶颈而非计算吞吐。
这些差异决定了 LLM 量化不能照搬 CNN 的方案。
5.2 权重量化、激活量化与 KV Cache 量化
LLM 量化可以拆成三个独立的部分:
权重量化:把模型权重从 FP16 压到 INT8 或 INT4。这是最成熟的部分,GPTQ、AWQ 等方法都能做到几乎无损。权重是静态的,量化后直接存储,推理时反量化即可。
激活量化:把推理时产生的激活值量化。这部分难度最大,因为 LLM 激活值有离群通道问题。SmoothQuant 的思路是:把激活值的量化难度迁移一部分到权重上,通过数学等价变换让两者都变得容易量化。
KV Cache 量化:LLM 推理时,Key 和 Value 缓存会占用大量显存。长序列场景下,KV Cache 甚至比模型本身还大。把 KV Cache 量化到 INT8,能显著降低显存占用,支持更长的上下文。
5.3 GPTQ、AWQ、SmoothQuant 的适用边界
| 方法 | 量化对象 | 核心思路 | 适用场景 |
|---|---|---|---|
| GPTQ | 权重 | 逐层最小化量化误差 | 离线量化,INT4/INT8 |
| AWQ | 权重 | 保护重要通道 | 离线量化,INT4 |
| SmoothQuant | 权重+激活 | 难度迁移 | W8A8 在线量化 |
| LLM.int8() | 权重+激活 | 离群值分离处理 | 混合精度推理 |
选型时看你的目标:如果只关心显存占用,GPTQ 或 AWQ 就够了;如果要同时加速计算,需要 W8A8,那就得用 SmoothQuant 或 LLM.int8()。
5.4 LLM 量化实操中的显存与精度权衡
我在 7B 和 13B 模型上做过对比测试,硬件是单卡 A100 40GB。结果如下:
| 量化方案 | 显存占用 | 困惑度变化 | 生成速度 |
|---|---|---|---|
| FP16 基线 | 14GB | 基准 | 基准 |
| INT8 权重 | 8GB | +0.05 | 1.3x |
| INT4 权重 | 5GB | +0.15 | 1.8x |
| INT8 权重+KV | 6GB | +0.08 | 1.5x |
可以看到,INT4 权重量化能把显存压到 5GB,但困惑度上升了 0.15。对于大多数应用场景,这个代价是可以接受的。如果对精度要求极高,INT8 是更稳妥的选择。
注意:LLM 量化后一定要用实际任务做评估,不能只看困惑度。有些量化方案在困惑度上表现很好,但在具体任务(如代码生成、数学推理)上掉点严重。
6. 量化部署的完整链路与避坑清单
6.1 从训练框架到推理引擎的导出流程
一个典型的量化部署链路是:
- 在 PyTorch 中训练模型,保存 FP32 权重。
- 用校准数据跑 PTQ,或者加载 QAT 训练好的模型。
- 导出为 ONNX 格式,注意选择正确的 opset 版本。
- 用推理引擎(TensorRT、OpenVINO、ONNX Runtime)加载 ONNX,生成量化引擎。
- 在目标硬件上做精度和性能验证。
每一步都有坑。比如 ONNX 导出时,某些自定义算子可能不支持量化;TensorRT 生成 INT8 引擎时,需要提供校准缓存文件;不同推理引擎对量化算子的支持程度也不一样。
6.2 量化精度掉点的逐层定位方法
精度掉点后,不要盲目调参。正确的排查顺序是:
- 第一步:对比量化前后每一层的输出,计算余弦相似度或相对误差。找到误差最大的层。
- 第二步:检查该层的校准数据统计是否合理。scale 是否过大或过小。
- 第三步:如果该层是敏感层(如第一层、最后一层、attention 输出层),考虑保留 FP32 精度,只量化其他层。
- 第四步:如果整体掉点,考虑换校准方法,或者上 QAT。
这个流程能帮你快速定位问题,而不是在一堆参数里瞎试。
6.3 混合精度量化的策略设计
不是所有层都适合 INT8。实践中,以下层通常保留 FP32 或 FP16:
- 第一层和最后一层:直接接触输入输出,精度要求高。
- 残差连接中的加法:量化误差会在残差路径上累积。
- LayerNorm 和 Softmax:涉及指数运算,对精度敏感。
- 检测头、分割头:输出精度直接影响最终结果。
混合精度量化的目标是在精度和速度之间找平衡。我的经验是:先全量化,看掉点情况,再把掉点最严重的几层恢复成 FP32,逐步逼近可接受的精度。
6.4 量化模型上线前的验证清单
上线前必须验证以下几项:
- 精度验证:在完整测试集上对比量化前后指标,确保掉点在可接受范围内。
- 性能验证:实测延迟、吞吐、显存占用,确认达到预期加速比。
- 数值稳定性:跑长时间推理,检查是否有溢出、NaN 等异常。
- 边界情况:极端输入、空输入、超长输入下的表现。
- 硬件兼容性:目标硬件是否支持所用量化指令集。
这份清单看起来简单,但每一条都可能藏着坑。我见过太多团队在精度验证通过后直接上线,结果在生产环境遇到边界输入导致输出异常。
7. 一些实测数据和经验教训
7.1 不同模型的量化敏感度差异
不是所有模型对量化的敏感度都一样。根据我的经验:
- ResNet 系列:非常鲁棒,PTQ INT8 几乎无损。
- MobileNet 系列:对量化敏感,尤其是深度可分离卷积,PTQ 掉点明显,建议 QAT。
- BERT 系列:中等敏感,PTQ INT8 掉点 0.5-1%,可接受。
- LLM:权重不敏感,激活敏感,需要特殊处理。
- 检测模型:整体敏感,尤其是小目标检测,建议混合精度。
这个规律可以作为你选型时的参考,但最终还是要以实测为准。
7.2 量化不是银弹:什么时候不该量化
量化虽好,但有些场景不适合:
- 模型本身已经很小:比如参数量不到 1M 的模型,量化收益有限,反而增加工程复杂度。
- 硬件不支持 INT8 加速:老旧的 CPU 或 GPU 没有 INT8 指令集,量化后可能更慢。
- 精度要求极高:医疗影像、科学计算等场景,量化误差不可接受。
- 推理瓶颈不在计算:如果瓶颈在数据预处理或后处理,量化计算部分收益有限。
做量化之前,先做 profiling,确认瓶颈在哪里。不要为了量化而量化。
7.3 工具链选择:TensorRT、OpenVINO、ONNX Runtime 的取舍
| 工具 | 优势 | 劣势 | 适用平台 |
|---|---|---|---|
| TensorRT | 性能最强,算子融合好 | 绑定 NVIDIA GPU | NVIDIA GPU |
| OpenVINO | Intel 平台优化好 | 非 Intel 平台支持弱 | Intel CPU/GPU |
| ONNX Runtime | 跨平台,易集成 | 性能略逊 | 多平台 |
| TFLite | 移动端优化好 | 生态相对封闭 | 移动端 |
选型时优先考虑目标部署平台。如果跑在 NVIDIA GPU 上,TensorRT 是首选;如果是 Intel CPU,OpenVINO 更合适;如果需要跨平台,ONNX Runtime 最省心。
7.4 量化后模型体积与内存带宽的实际收益
最后说一个容易被忽略的点:量化带来的收益不只是计算加速,还有内存带宽节省。模型体积缩小 4 倍,意味着从内存加载权重的时间也缩短了。对于内存带宽受限的场景(比如 LLM 推理),这个收益甚至比计算加速更明显。
我在 LLM 上实测过,INT8 权重量化后,虽然计算量没变(还是那么多乘加),但生成速度提升了 30% 以上,原因就是权重加载时间大幅缩短。所以评估量化收益时,不要只看 FLOPs,要看端到端的实际延迟。
8. 个人实操体会
做了这么多量化项目,最大的体会是:量化是一门实验科学,不是理论科学。同样的方法,换个模型、换个硬件、换个数据集,效果可能完全不同。不要迷信论文里的数字,一定要在自己的场景里实测。
另外,校准数据的质量比数量重要得多。我见过用 100 张精心挑选的校准图,效果比用 1000 张随机图还好。花时间构建一个有代表性的校准集,比调参更有效。
还有一点:量化不是一次性的工作。模型更新了、数据分布变了、硬件换了,都需要重新评估量化方案。把它当成一个持续的工程实践,而不是一劳永逸的优化手段。
最后分享一个小技巧:如果你不确定某层该不该量化,可以先做一个逐层敏感度分析——每次只量化一层,看精度变化。这样能快速识别出敏感层,为混合精度策略提供依据。这个方法虽然耗时,但比盲目试错高效得多。