量化这两年几乎成了"部署必选项"。模型训完想往生产环境放,要么卡在显存不够,要么延迟打不进预算,而 INT8 量化恰好能把这两件事同时往前推一大截。我平时主要做推理侧的服务部署,也经常在边缘设备上调模型,这篇就把我实际跑通 INT8 量化、校准、QAT 以及 LLM 量化这几条路径的笔记整理出来,尽量讲清楚每一步在做什么、为什么非做不可、以及哪些地方容易翻车。
1. 为什么量化能提速,提速的钱到底从哪来
很多人一听说量化就直觉以为"模型变小了所以快了"。模型变小只是收益之一,真正的提速来自三个层面的联动。
1.1 数据搬运量直接减半
先看一个数字:FP32 一个数占 4 字节,FP16 占 2 字节,INT8 只占 1 字节。推理过程里,每一层都要从内存里把上一层的输出搬进计算单元,这个搬运成本在真实部署里往往比计算本身还贵。算子算得再快,数据喂不上来也是白等。
把激活值从 FP16 换成 INT8,单次搬运的字节数直接砍一半;从 FP32 换成 INT8,直接砍四分之三。对于内存带宽吃紧的设备(CPU、树莓派这类小板子、还有部分手机 NPU),这个收益立竿见影。
1.2 硬件对低精度计算"偏心"
这不是软件优化,是硬件设计上就偏心。现代 GPU 的 Tensor Core 对 INT8 的吞吐通常是 FP16 的两倍,有些架构甚至是四倍;CPU 上也有对应的 INT8 指令集,比如 AVX512_VNNI、ARM 的 SDOT,都支持一条指令打包多个 INT8 乘法。NPU 就更不用说了,INT8 基本是各家边缘芯片的主力数据类型。
我在 NVIDIA 的卡上实测过,同一份推理代码,从 FP16 切到 INT8,算力密集型的卷积层吞吐基本能翻倍。CPU 上虽然没有那么夸张,但配合带宽优势,整体延迟也能看到明显回落。
1.3 功耗和部署形态的连带收益
量化之后模型占用内存变小,缓存命中率提高,功耗随之下降。手机端、摄像头端的场景特别吃这个。有时候模型没量化前跑在边缘设备上直接过热降频,量化完反而能稳定运行。这块的收益不体现在跑分脚本里,但体现在真实设备体验上。
一个容易忽略的点:量化之后,单个设备能同时承载的模型实例变多。比如同一块 GPU,FP16 能塞下 8 个实例,INT8 可能塞 16 个。这在横向扩容的时候能省不少机器成本,做服务部署的同学应该懂这个价值。
2. INT8 矩阵乘的硬件真相:算得准,靠的是累加器的"高闲职"
要理解 INT8 矩阵乘为什么能保持精度,必须看硬件内部怎么处理乘法结果。这一步搞懂了,后面的校准思路就顺理成章。
2.1 低精度计算,高精度累加
INT8 乘 INT8,结果是 INT16 范围。但矩阵乘要做很多次乘加,GPU 的 Tensor Core 和 CPU 的 SIMD 指令都采用"INT8 乘法 + INT32 累加"的组合。也就是说,每一次乘法误差范围有限,累加的精度却保留得很宽。
这带来一个很有意思的结论:INT8 矩阵乘的误差源头主要在"把浮点数变成 INT8"这一步的舍入,而不是矩阵乘本身。只要缩放系数选得合适、数值范围卡得准,乘加过程的累积误差是完全可控的。
2.2 量化公式就是线性映射
标准做法是把浮点值按线性关系映射到整数域。
对称量化(symmetric)——就是实际做推理时默认的基础。公式是:
q = round(clamp(r / s, -127, 127))其中r是原始浮点值,s是缩放因子,q是量化后的整数。反量化就是r ≈ q * s。
非对称量化(asymmetric)会多一个零点偏移 z,公式变成:
q = round(clamp(r / s + z, 0, 255))非对称常用在激活值上,因为激活一般不是正负均匀分布的。权重量化因为数值分布相对稳定,对称量化居多。
为什么最小都用 -127 而不是 -128?细节在于 NVIDIA Tensor Core 有些 INT8 算子会把 -128 排除掉,某些硬件上 -128 有特殊含义,部署时如果算出的最小值恰好落在 -128 附近,建议检查算子是否支持。
2.3 量化的"变形金刚":GEMM 融合
光有矩阵乘还不够,真正提速要靠算子融合。实际推理时,量化不是简单在每个算子前后加转换,而是把量化和反量化"吃"进 GEMM 计算里。
典型流程是这样的:
- 权重提前离线量化成 INT8
- 上一层的输出如果已经是 INT8,就直接进入 GEMM
- GEMM 输出的 INT32 累加值,配合 weight scale 和 input scale 做一步反量化,同时写入下一层的量化逻辑
这一步合起来的公式大约是:
output_fp32 = (accum_int32) * (scale_weight * scale_input)实际实现里往往再合并 bias,变成单次乘加。我见过有些初学同学把量化算子逐个插入每一层,导致每层都要从 FP32 转来转去,速度不仅没提升反而变慢。关键是让 scale 的乘除和量化操作融合到主计算里,而不是当独立算子跑。
3. 校准(Calibration):量化误差的源头控制
校准这一步直接决定量化后模型掉不掉点。很多人跑完量化发现"模型傻了",八成问题出在校准数据和校准方法上,不是量化本身的锅。
3.1 校准到底是什么
量化的本质是找一组缩放系数,把浮点张量映射到整数域。问题是同一层在不同输入下的激活值范围不一样,不能等推理时再动态统计,所以需要在量化前用一小批数据跑一遍模型,统计出每一层激活值的分布,然后挑一个合适的范围。
这个过程叫校准(calibration),跑的数据叫校准集(calibration set)。校准集和训练集的关系有点像"让模型做一次摸底考试",用少量样本来判断每个激活值大概落在什么区间。
3.2 四种常见校准方法的特点和坑
| 方法 | 原理 | 适用场景 | 容易踩的坑 |
|---|---|---|---|
| MinMax | 直接用 min/max 作为量化边界 | 分布均匀的层 | 极端值出现时边界被拉爆,精度骤降 |
| Percentile(百分位) | 取 99.9% 或 99.99% 分位作为边界 | 激活值有轻微异常值 | 百分位选太大会回到 MinMax 问题 |
| KL 散度(熵) | 搜索使量化前后分布 KL 距离最小的边界 | TensorRT 默认方案,CNN 上很稳 | 校准集太少时结果不稳定 |
| MSE(均方误差) | 最小化量化前后激活值的均方误差 | 精度敏感模型 | 搜索粒度太大时可能跳过最优解 |
TensorRT 成名的校准方式就是 KL 散度,实质是"让量化后的分布尽量贴近原始分布的信息量"。这个方法对绝大部分 CNN 模型效果稳定,我建议默认优先试。
3.3 校准集选择,最容易翻车的地方
校准集的目的是"覆盖真实推理时的数值分布",不是为了"刷准确率"。我踩过的一个典型坑:用训练集中的高质量样本做校准,结果验证集(偏真实场景)掉点严重。后来发现训练集图片普遍明亮干净,真实部署场景有很多暗光、模糊、遮挡的情况,量化边界完全没覆盖。
三类数据不能做校准集:
- 增强过度的数据(旋转、裁剪、加噪幅度太大,分布脱离真实场景)
- 单类别占比过高的数据(比如只有猫没有狗,激活通道分布失衡)
- 批量太小(少于 100 张/条基本不够看)
我现在的习惯是:从验证集里随机抽 500~2000 个样本,确保覆盖各类标签和光照条件,再跑 1~2 轮前向统计。样本数量不是多多益善,超过 2000 张之后收益曲线基本上走了。
3.4 校准后的验证:只看准确率远远不够
量化完不能只看 Top-1 准确率,还要检查以下维度:
- 类别的混淆矩阵有没有出现明显的系统偏移(比如所有"猫"都被分到"狗")
- 回归任务要看最大误差,不能只看均方误差
- 输出 logits 的分布和量化前是否大体一致
- 延迟和吞吐有没有实际提升
我遇到过一次量化后 Top-1 几乎不掉,但某一类特定暗光场景下预测完全错误。后来查分布才发现这一类的激活和校准集分布偏差很大,属于"幸存者偏差"。保险起见,上线前一定要拿真实业务数据做一轮盲测。
4. QAT:训练感知量化,精度救不回来时的王牌
PTQ(训练后量化)跑完掉点超过 1~2%,常见手段是校准集调大、换校准方法、对敏感层做混合精度。如果这些都试过还是不行,就该上 QAT 了。
4.1 QAT 的核心机制:假装量化,但梯度照常回传
QAT 的做法是在训练过程里插入"伪量化节点"(fake quantize),前向直接把数值量化到低精度,让模型看到量化后的损失;反向传播时用一个直通估计器(STE,straight-through estimator)把量化函数的梯度近似成直通,保证训练能正常收敛。
公式理解起来很简单。前向:
q = quantize(r)反向的梯度:
∂loss / ∂r ≈ ∂loss / ∂q原因在于量化函数 round 的导数几乎处处为 0,不做 STE 的话梯度传不下去,模型根本学不动。
4.2 QAT 实操的通用套路
- 先做 PTQ 得到一套较好的量化边界,把它作为 QAT 的初始化
- 在模型中插入伪量化节点(PyTorch 里可用
torch.quantization.quantize_fx的 Fx 模式,或用torch.ao.quantization的 QAT 接口) - 用比原始训练更小的学习率(通常是原来的 1/10),跑几个 epoch 让模型"适应"量化误差
- 训练结束后移除伪量化节点,导出真正的 INT8 模型
关键点是:QAT 不是重新训练。所需 epoch 通常不超过原始训练的 20%,太多反而可能过拟合。
4.3 QAT 对哪些模型特别有效
- 分类网络(ResNet、MobileNet 系列)——QL 效果稳定
- 检测网络(YOLO、SSD)——PTQ 掉点经常在 3% 以上,QAT 能压回 1% 以内
- 分割模型(U-Net 这类)——整体稳定,但边界分割细节可能退化,QAT 有帮助
- 小模型(MobileNetV3、EfficientNet-Lite)——本身参数冗余少,PTQ 经常难受,QAT 基本是标配
之前在一款边缘设备上部署 YOLOv5,PTQ 直接掉 4 个点,换成 QAT 跑 15 个 epoch,掉点压回 0.8。代价只是多花了两小时训练时间,换来的是模型上线后稳定运行。
4.4 别上来就 QAT:先跑一遍完整排查链路
我建议的排查顺序是:PTQ 默认配置 → 调整校准集 → 换 KL/MSE → 排查敏感层 → 混合精度 → 最后才 QAT。很多场景前三步就能解决,直接上 QAT 属于用大炮打蚊子。
顺便提一个经验值:LayerNorm、Softmax、最后的分类头这类层对精度极度敏感,量化时建议保持 FP32,混合精度方案可以参考tensorrt的 per-channel 设置,用 per-tensor 还是 per-channel 也值得实验对比。
5. LLM 量化:权重还好,激活和 KV Cache 才是大头
LLM 量化和 CNN 量化看起来都是 INT8,实际难度的分布完全不同。我开始做 LLM 部署的时候踩了不少坑,这里把要点拆开说。
5.1 LLM 和 CNN 在量化上的本质差异
CNN 的激活值一般相对平滑,分布符合高斯之类的形态;LLM 的激活值则经常出现少量极端大的 outlier,出现在固定通道上。这些 outlier 会把量化范围拉得很大,其他正常值量完精度惨不忍睹。
这个问题在论文里被称为"激活中的异常值问题"。SmoothQuant 的思路就是把激活的 outlier 平滑掉一部分,转移到权重上,这样两边都好量化。
权重方面也要提防:LLM 里有个著名的"海量特征维度"现象——少数敏感通道对结果影响极大,AWQ 的核心贡献就是保住这些敏感通道的权重。
5.2 常见的几种 LLM 量化方案怎么选
| 方案 | 精度策略 | 算力开销 | 适用场景 |
|---|---|---|---|
| GPTQ | 仅权重量化,按层做误差补偿 | 低,可用 4bit kernel | 追求极限压缩比,可以配合 AWQ 使用 |
| AWQ | 仅权重量化,保护敏感通道 | 低,和 GPTQ 类似 | 压缩权重,保持精度 |
| SmoothQuant | 权重+激活 8bit 量化 | 中等 | 需要完整 W8A8 加速时 |
| KV Cache 量化 | 对 KV Cache 做 8bit/4bit 压缩 | 可忽略 | 长上下文场景下必做 |
实际部署时最常用的是 W8A16(权重 8bit、激活 16bit)或者 W4A16(权重 4bit、激活 16bit)。这两种方案只需要把权重换成低精度存储,不需要额外的激活量化算子,集成成本低。
想要真正提升生成速度,必须上 W8A8,也就是权重和激活都量化。此时需要配套 SmoothQuant 这类技术处理比激活异常值,否则精度损失在长上下文场景会迅速放大。
5.3 KV Cache 量化是长上下文的必做课
LLM 生成时每个 token 都要读一遍历史的 KV Cache,上下文越长,这部分开销越大。不做 KV 量化,一个 32K 上下文请求可能比权重占用的显存还多。
KV Cache 量化属于"C 端收益",不会体现在单 token 延迟上,但能显著提升吞吐。量化方案通常是把 Key 和 Value 分别量化到 8bit 甚至 4bit,精度下降在接受范围内,配合 strong quantization 策略甚至可以超过不量化版本。
我在 vLLM 上测过,开启 KV Cache INT8 量化后,相同显存下并发数提升接近一倍,生成的 PPL 变化 0.1 以内。对长上下文场景,这个优化几乎白捡。
5.4 LLM 量化的实际回收效率
量化位宽不是越低越好。4bit 权重的模型体积直降 75%,但激活还是 16bit 甚至 32bit,实际推理算力并没有全面加速,只是显存和带宽压力变小。如果瓶颈在显存,4bit 很合适;如果瓶颈在算力,W8A8 才是主赛道。
举个例子:在一张 3090 上跑 7B 模型,FP16 权重占 14GB 显存,INT8 占 7GB,4bit 占 3.5GB。但如果激活仍 FP16,单 token 生成速度基本不变,只是能塞更多人并行。想真正提升单 token 速度,必须做 W8A8 全量化。
5.5 量化后 LLM 的幻觉风险
这一条很多人忽视。量化会让模型对低概率 token 的预测更敏感,可能改变模型在某些输入上的生成稳定度。我做 RAG 场景时碰到过量化后模型多次重复同一句,或者突然开始输出和分析目标无关的内容。这个问题属"分布外行为",纯看 PPL 指标根本发现不了。
我的处理方式是保留一个 FP16 原始模型做候选对比,量化模型上线后前两周定期对比生成质量,发现异常再回退或换量化策略。上线前的"通用测试集"建议加上对抗样本组,比如诱导重复的提示语、指代混乱的句子、非常规的问题格式。
6. 量化部署的通用流程与一些碎碎念
最后给一个我在实际项目中反复使用的通用流程,把上面所有内容串起来。这个流程从拿到模型到上线基本可以照抄。
6.1 一套可以抄作业的流程
- 预处理:完成 BatchNorm 折叠,把 BN 参数融合进前面卷积层,否则量化边界会偏
- 选择精度策略:先用 PTQ INT8 跑一遍,配合 KL 校准,看掉点幅度
- 校准集迭代:如果掉点,先检查校准集,换数据源、加样本量,重跑
- 敏感层排查:逐个把层设回 FP16 或 FP32,找到掉点主要来源
- 混合精度:仅敏感层保持高精度,其余层 INT8
- 还是不行就 QAT:按 4.2 套路初始化、微调、导出
- 用真实业务数据做盲测:检查混淆矩阵、生成稳定性、延迟分布
- 上线后持续观测:尤其注意异常输入下的表现
6.2 工具链的选择
PyTorch 自带量化工具适合研究和 QAT;ONNX Runtime 的onnxruntime.quantization适合 CPU 部署;TensorRT 适合 NVIDIA GPU 服务端;RKNN Toolkit 适合瑞芯微边缘芯片;vLLM 自带 GPTQ、AWQ、FP8 支持,服务端 LLM 首选。
工具不是越高级越好,关键看你的部署目标平台。先看目标平台支持哪些量化格式,再选工具,顺序反了容易白干。
6.3 量化不是银弹
有些模型本身容量就很饱和,量化会直接把边缘能力削掉。典型的例子是轻量化模型本身就已经压得很紧,再量化等于二次伤害。这类场景我建议先考虑提升原始模型容量或蒸馏,不要死磕量化。
另一个建议是多做 AB 对比,别急着把量化模型设为默认。尽量保住"量化前版本"和"量化版本"两套出口,线上流量按百分百切,随时能回滚。
我个人实际部署的经验是:先把校准集质量搞对,大多数模型 PTQ 就够用;QAT 不是默认选项,是兜底手段;LLM 量化优先看 Kv Cache 和 W8A8 支持,权重位宽往 4bit 走前先确认带宽瓶颈在不在显存上。这一整套跑下来,量化的收益基本能稳定吃掉,剩下的是日常迭代时的细心活。