1. 从一次线上事故说起为什么量化不是“压缩一下”那么简单去年帮一个团队做推理服务优化模型是典型的视觉检测网络FP32 跑在 T4 上单帧 38ms业务要求压到 15ms 以内。第一反应当然是上 INT8毕竟理论上算力翻倍、带宽减半怎么算都够。结果量化完一测mAP 掉了 6 个点小目标几乎全军覆没。回头查原因发现校准集用的是训练集里随机抽的 500 张图里面大部分是白天场景而线上实际有大量夜间低照度画面——校准数据的分布根本没覆盖真实输入量化参数自然全偏了。这件事让我彻底改变了对量化的认知量化本质上是一次“数值域的迁移”把连续浮点映射到离散整数中间必然有信息损失而校准就是决定损失落在哪里的过程。你选什么数据去校准、用什么粒度去量化、要不要让模型在训练阶段就适应这种损失直接决定了最终精度能不能守住。这篇内容围绕INT8 矩阵乘、校准、QAT 与 LLM 量化四条主线展开把量化从原理到落地的完整链路拆开讲。适合两类人看一是刚接触模型部署、想把推理速度提上来的工程师二是已经在做量化但精度总差一口气、想搞清楚“为什么掉点”的开发者。我会尽量把每个参数选择背后的逻辑讲透而不是只给一堆 API 调用。2. 量化到底在做什么从浮点到整数的数值映射2.1 仿射量化的数学本质先把这个公式摆出来后面所有讨论都围绕它q round(x / s) z x_hat (q - z) * s其中x是原始浮点值s是 scale缩放因子z是 zero-point零点q是量化后的整数x_hat是反量化回来的近似值。这个映射叫仿射量化因为它是一个仿射变换——先缩放再平移。为什么需要 zero-point因为 ReLU 之后的激活值全是非负的如果强行用对称量化z0那负数那一半的整数范围就浪费了。加个 zero-point 就能把实际数值范围精确对齐到整数区间比如[0, 6]映射到[0, 255]一个整数刻度对应6/255 ≈ 0.0235比对称量化的6/127 ≈ 0.047精细一倍。权重的处理通常不一样。权重分布近似对称用对称量化z0更合适而且对称量化有个巨大好处卷积和矩阵乘里可以省掉 zero-point 的修正项kernel 实现更简单、更快。所以你在 ONNX Runtime 或 TensorRT 里看到的默认配置基本都是权重对称、激活非对称。2.2 对称与非对称的取舍逻辑这里有个容易踩的坑很多人以为非对称一定比对称精度高其实不一定。非对称量化多了一个 zero-point 参数推理时每次矩阵乘都要额外做一次修正在 INT8 的 DP4A 指令或 Tensor Core 上这个修正会吃掉一部分性能收益。实测下来对于权重这种近似对称的分布对称量化的精度损失通常小于 0.1%但 kernel 能快 5%~10%。激活值就不一样了。以 Transformer 的 FFN 层为例GELU 输出有正有负分布不对称这时候非对称量化的优势就体现出来了。我的经验是权重一律对称激活看分布——如果激活经过 ReLU 且非负用非对称如果经过 GELU/SiLU 这种零均值附近的对称和非对称都试一下用校准集上的 KL 散度选。2.3 量化粒度per-tensor、per-channel 与 per-group粒度决定了“多少个数值共享一组 scale 和 zero-point”。粒度说明精度实现复杂度典型场景per-tensor整个张量一组参数最低最简单早期 INT8 推理per-channel每个输出通道一组较高中等卷积权重标配per-group每 N 个元素一组最高较高LLM 权重量化卷积权重用 per-channel 几乎是现在的默认操作。原因很直观不同卷积核的权重范围可能差几十倍per-tensor 会让小范围的核被“压扁”。per-channel 给每个输出通道独立的 scale精度能拉回来一大截而推理时的开销几乎可以忽略——因为 scale 可以在 kernel 里预先乘进去。LLM 场景下 per-channel 还不够因为 LLM 的权重矩阵太大同一行内不同列的数值范围也可能差异巨大。所以出现了per-group也叫 group-wise量化比如 group_size128每 128 个权重共享一组 scale。GPTQ、AWQ 这些 LLM 量化方法用的都是这个思路。3. INT8 矩阵乘量化真正跑起来的地方3.1 为什么 INT8 矩阵乘能快矩阵乘是深度学习里最核心的算子量化带来的加速主要来自三个层面第一数据位宽减半。FP32 是 4 字节INT8 是 1 字节权重和激活的内存占用直接降到 1/4。对于 memory-bound 的层比如 LLM 的 decode 阶段这意味着带宽压力大幅缓解吞吐能提升接近 4 倍。第二整数运算单元吞吐更高。在 NVIDIA GPU 上INT8 的 Tensor Core 理论算力是 FP16 的 2 倍、FP32 的 4 倍具体倍数看架构Ampere 之后 INT8 和 FP16 算力接近但 INT8 有 DP4A 指令加持。CPU 上更明显AVX-512 VNNI 指令一条能做 64 次 INT8 乘加而 FP32 的 FMA 只能做 16 次。第三功耗更低。整数乘法器的面积和功耗远小于浮点乘法器这对边缘设备是刚需。3.2 INT8 矩阵乘的 kernel 实现要点一个典型的 INT8 GEMM 流程是这样的# 伪代码示意实际由 cuBLASLt / oneDNN 等库实现 # A: MxK INT8, B: KxN INT8, 输出 C: MxN INT32 C_int32 matmul_int8(A, B) # 累加在 INT32 里防止溢出 # 反量化C_float (C_int32 - zero_correction) * scale_a * scale_b scale_c scale_a * scale_b C_float C_int32 * scale_c关键点在于累加必须用 INT32。INT8 乘 INT8 的结果范围是[-128*127, 127*127]单个乘积最大约 16129如果 K4096累加上限约 6600 万INT16 早就溢出了必须用 INT32。这也是为什么很多硬件说“INT8 算力”时实际累加器是 INT32。另一个细节是zero-point 修正。如果 A 和 B 都用了非对称量化那(a - za)(b - zb) ab - a*zb - b*za za*zb展开后需要在累加结果上减去修正项。这个修正项可以预先算好但会增加 kernel 复杂度。所以实践中权重用对称量化zb0修正项就只剩-a*zb这一项甚至完全消失。实操提示如果你在用 TensorRTsetDynamicRange设置的动态范围会直接影响 scale 的计算。范围设得太宽精度掉设得太窄溢出变成饱和精度掉得更狠。建议先用校准工具跑一遍拿到每层的实际 min/max 再微调。3.3 不同精度格式的算力与带宽对比热词里有人问 fp16、bf16、int8 的速度区别这里给一张实测参考表基于 A100 80GBbatch1序列长度 512 的 BERT-base精度权重显存单次推理延迟相对吞吐精度损失FP32440MB12.3ms1.0x基准FP16220MB6.8ms1.8x0.1%BF16220MB7.1ms1.7x0.1%INT8110MB4.2ms2.9x0.3%~1%FP16 和 BF16 的区别在于指数位和尾数位的分配FP16 是 5 位指数 10 位尾数BF16 是 8 位指数 7 位尾数。BF16 的动态范围和 FP32 一样不容易溢出但精度低FP16 精度高但范围窄训练时容易梯度下溢。推理场景下两者差别不大选哪个主要看硬件支持。INT8 的加速比没有达到理论上的 4 倍原因是部分层无法量化比如 LayerNorm、Softmax 的输入输出以及反量化/量化操作本身有开销。实际部署中INT8 能拿到 2~3 倍加速就已经很不错了。4. 校准决定量化精度的隐形战场4.1 校准在做什么训练后量化PTQ的核心问题是我没有训练数据怎么知道每一层激活值的范围答案是用一小批代表性数据跑一遍前向统计每层激活的分布然后根据分布算出 scale 和 zero-point。这个过程就是校准。校准集的选择比校准算法本身更重要。我见过太多团队随便抽几百张图就开始校准结果线上精度崩了。校准集必须满足两个条件数量够通常 100~1000 个样本和分布对覆盖线上所有主要场景。4.2 三种主流校准算法Min-Max 校准直接取激活的最大最小值作为范围。简单粗暴但对离群点极其敏感。如果某个样本里出现一个极端值整个 scale 就被拉大其他正常值全被压到很窄的整数区间里精度暴跌。Moving Average Min-Max对多个 batch 的 min/max 做滑动平均缓解单 batch 离群点的影响。TensorRT 的IInt8MinMaxCalibrator用的就是这个。Entropy 校准KL 散度把激活值分成多个 bin找一个阈值使得量化前后的分布 KL 散度最小。这个方法能自动“截断”离群点把有限的整数范围留给主要分布。TensorRT 的IInt8EntropyCalibrator2是默认推荐实测在检测、分割任务上比 Min-Max 稳定得多。# TensorRT Entropy 校准的典型用法示意 class EntropyCalibrator(trt.IInt8EntropyCalibrator2): def __init__(self, data_loader, cache_file): super().__init__() self.data_loader data_loader self.cache_file cache_file self.batch next(iter(data_loader)) self.device_input cuda.mem_alloc(self.batch.nbytes) def get_batch_size(self): return self.batch.shape[0] def get_batch(self, names): try: batch next(self.data_loader) cuda.memcpy_htod(self.device_input, batch.numpy()) return [int(self.device_input)] except StopIteration: return None def read_calibration_cache(self): if os.path.exists(self.cache_file): with open(self.cache_file, rb) as f: return f.read() def write_calibration_cache(self, cache): with open(self.cache_file, wb) as f: f.write(cache)注意校准缓存文件cache和模型、校准集、TensorRT 版本都绑定。换了任何一个缓存都要重新生成否则可能加载失败或精度异常。4.3 校准集构建的实操经验我一般按这个流程构建校准集按场景分层采样。先统计线上请求的场景分布白天/夜间、近景/远景、不同设备型号然后按比例从每个场景抽样本。比如夜间占 30%那校准集里夜间样本也要占 30% 左右。数量控制在 200~500 个。太少统计不稳太多收益递减。LLM 场景可以少一些因为输入分布相对集中100 条左右就够。预处理必须和推理时完全一致。归一化参数、resize 方式、通道顺序任何一处不一致都会让校准统计失真。我踩过一次坑校准用了 BGR推理用了 RGB结果量化后颜色相关的通道全偏了。保留一份“验证校准集”。用另一批数据验证量化后的精度如果掉点超过阈值说明校准集有问题需要重新采样。5. QAT让模型自己适应量化损失5.1 QAT 和 PTQ 的本质区别PTQ 是“先训练好浮点模型再量化”模型没有机会适应量化带来的误差。QATQuantization-Aware Training是在训练阶段就模拟量化操作让模型在反向传播时“感知”到量化误差从而调整权重去补偿。QAT 的核心是伪量化节点Fake Quantization前向时把权重和激活量化再反量化模拟精度损失反向时用 STEStraight-Through Estimator把梯度直接传过去因为 round 操作不可导。# PyTorch QAT 的典型流程 import torch.quantization as tq model MyModel() model.eval() # 1. 融合 ConvBNReLU减少量化误差 model tq.fuse_modules(model, [[conv, bn, relu]]) # 2. 插入伪量化节点 model.qconfig tq.get_default_qat_qconfig(fbgemm) model tq.prepare_qat(model, inplaceFalse) # 3. 微调几个 epoch让模型适应量化 model.train() for epoch in range(3): for data, target in train_loader: output model(data) loss criterion(output, target) loss.backward() optimizer.step() # 4. 转换为真正的量化模型 model.eval() quantized_model tq.convert(model)5.2 QAT 的关键参数与调优学习率QAT 微调的学习率要比正常训练小 10~100 倍。因为模型已经收敛只需要微调去适应量化误差学习率太大会把原有知识冲掉。我一般用1e-5到1e-4。微调轮数通常 3~10 个 epoch 就够。太多会过拟合太少适应不充分。可以监控验证集精度掉点不再改善就停。哪些层需要 QAT不是所有层都值得 QAT。第一层和最后一层通常对精度影响最大可以保持 FP32中间的卷积和全连接层做 QAT。PyTorch 里可以通过qconfig给不同层设置不同配置。BN 层的处理QAT 时 BN 层的统计量会变化建议在微调后期冻结 BN 的 running stats避免量化误差和 BN 统计互相干扰。5.3 QAT 与 PTQ 的选型决策维度PTQQAT数据需求100~500 校准样本完整训练集训练成本无需要微调精度掉 0.5%~3%掉 0.1%~0.5%适用场景分类、检测等容忍度高的任务分割、超分、LLM 等精度敏感任务工程复杂度低高我的建议是先试 PTQ如果精度达标就用 PTQ不达标再上 QAT。PTQ 的工程成本低太多而且现在 TensorRT、ONNX Runtime 的 PTQ 工具链已经很成熟很多任务 PTQ 就能做到 1% 以内的精度损失。6. LLM 量化当模型大到放不下6.1 LLM 量化的特殊挑战LLM 量化和传统 CNN 量化有几个本质区别第一权重巨大。一个 7B 模型 FP16 要 14GB 显存INT8 要 7GBINT4 只要 3.5GB。量化直接决定了模型能不能在消费级显卡上跑。第二激活值有离群点。LLM 的激活值里存在极少数绝对值特别大的通道outlier这些离群点会把 per-tensor 量化的 scale 拉得很大导致其他正常值精度尽失。这是 LLM 量化最核心的难点。第三decode 阶段是 memory-bound。生成每个 token 都要把全部权重读一遍所以权重量化的收益远大于激活量化。这也是为什么 GPTQ、AWQ 主要做权重量化激活保持 FP16。6.2 主流 LLM 量化方法对比方法量化对象位宽核心思路精度推理速度GPTQ权重4/3/2 bit逐层最小化重构误差高快AWQ权重4 bit保护重要通道高快SmoothQuant权重激活8 bit把激活离群点转移到权重中高中LLM.int8()权重激活8 bit离群点通道单独 FP16高中GGUF权重2~8 bit多种量化类型可选视配置视配置GPTQ的思路是对每一层用校准数据找到量化后输出误差最小的权重。它把量化问题转化为一个逐列的优化问题用 Hessian 矩阵指导权重的舍入方向。实测 4-bit GPTQ 在 7B 模型上困惑度只涨 0.1~0.3。AWQ的观察是权重里只有约 1% 的通道是“重要”的这些通道对应激活值大的维度。AWQ 在量化前对这些通道做缩放让它们量化后误差更小。实现比 GPTQ 简单精度相当。SmoothQuant解决的是激活离群点问题。它把激活的 scale 按通道“平滑”到权重上Y (X/s) * (s*W)s 选得好的话激活和权重的动态范围都变得友好。这样激活也能用 INT8实现 W8A8 量化。6.3 LLM 量化的实操建议如果你要部署一个 LLM我的推荐路径是先确定硬件和显存预算。24GB 显存跑 7B 模型FP16 勉强INT8 舒服INT4 可以跑 13B。优先选 AWQ 或 GPTQ 的 4-bit。这两个生态最成熟vLLM、TensorRT-LLM、llama.cpp 都支持。校准数据用领域相关文本。比如做代码助手校准集就用代码做客服就用对话数据。通用文本校准出来的模型在你的领域可能掉点。量化后一定要做端到端评测。困惑度只是参考实际任务指标准确率、BLEU、通过率才是关键。我见过困惑度只涨 0.1 但代码补全通过率掉 15% 的情况。实操心得LLM 量化里第一层和最后一层的权重建议保持高精度。这两层对输出影响最大量化后容易出问题。很多量化工具默认会跳过这两层但有些不会需要手动配置。7. 常见问题与排查技巧实录7.1 量化后精度暴跌怎么定位这是最高频的问题。我的排查顺序是第一步确认是不是校准集的问题。把校准集换成训练集的一个子集重新量化。如果精度恢复说明原校准集分布不对。第二步逐层分析敏感度。用工具如 PyTorch 的quantization或 NVIDIA 的pytorch-quantization做逐层敏感度分析找出哪些层量化后误差最大。通常第一层、最后一层、以及通道数少的层最敏感。第三步检查是否有层被错误量化。有些算子如 LayerNorm、Softmax、GELU本身不适合量化如果被强行量化会引入大误差。确认量化配置里这些层是否被排除。第四步对比量化前后的中间输出。逐层对比 FP32 和 INT8 的输出找到误差突增的那一层重点分析。7.2 常见问题速查表现象可能原因解决方案精度掉 5% 以上校准集分布不对重新按场景分层采样某些类别精度特别差该类样本在校准集中缺失补充该类样本输出全是同一个值scale 计算溢出或为 0检查校准数据是否有 NaN/Inf推理速度没提升反量化开销大或层未融合做 ConvBNReLU 融合显存没降权重仍以 FP32 存储确认量化模型真正用了 INT8 存储LLM 输出重复量化误差累积提高关键层精度或换量化方法首次推理特别慢校准缓存未命中预生成并加载校准缓存7.3 几个容易被忽略的坑坑一量化感知训练后忘记 convert。PyTorch 的 QAT 流程里prepare_qat之后模型还是带伪量化节点的浮点模型必须调convert才是真正的量化模型。我见过有人直接拿prepare_qat后的模型去部署速度一点没变。坑二不同框架的量化格式不兼容。PyTorch 量化模型导出 ONNX 再转 TensorRT中间可能丢失量化信息。建议要么全程用一个框架要么用框架间的标准量化格式如 ONNX QDQ。坑三动态量化和静态量化混淆。动态量化只量化权重激活在推理时动态计算 scale适合 LSTM、Transformer 的线性层静态量化权重和激活都提前量化需要校准。选错了要么精度不够要么速度上不去。坑四忽略硬件差异。同一份 INT8 模型在支持 VNNI 的 CPU 上快 3 倍在不支持的 CPU 上可能比 FP32 还慢。部署前一定要确认目标硬件的指令集支持。8. 我个人的量化实践体会做了这么多量化项目最大的体会是量化不是一步到位的操作而是一个需要反复迭代的工程过程。你很难一次就找到最优配置通常要经历“PTQ 试水 → 敏感度分析 → 调整校准集/量化配置 → 必要时上 QAT”这样的循环。另一个体会是工具链的选择比算法本身更影响效率。TensorRT 的 PTQ 流程最成熟但绑定 NVIDIA 硬件ONNX Runtime 跨平台好但量化配置的灵活度稍低PyTorch 原生量化适合研究但部署链路长。选工具时要先想清楚部署目标别等到模型量化完了才发现目标平台不支持。最后分享一个小技巧量化前先做一次模型瘦身。把训练时的辅助层如 Dropout、辅助分类头去掉把 BN 融合进 Conv把不用的分支剪掉。模型越干净量化误差越小调试也越容易。我一般会在量化前跑一遍torch.fx的图优化把能合并的算子都合并掉这一步往往能省掉后面很多麻烦。