1. 为什么模型一上量化就“翻车”从一次线上事故说起去年帮一个团队排查推理服务的延迟抖动问题模型是典型的视觉检测网络FP32 跑在 T4 上单帧 38ms业务要求压到 15ms 以内。第一反应就是上 INT8 量化结果精度掉了 6 个点小目标几乎全丢。折腾了两周才定位到根因校准集用了训练集的随机采样里面大量是简单背景图激活值的动态范围根本没覆盖到真实推理时那些高对比度场景。这件事让我彻底明白一个道理——量化的难点从来不是“把 FP32 变成 INT8”这个动作本身而是如何让 INT8 的数值表示尽可能贴合真实推理时的数据分布。这篇内容就是围绕这个核心展开的。我会把 INT8 矩阵乘的底层原理、校准Calibration的几种主流做法、QAT量化感知训练的落地细节以及 LLM 量化这个当下最热的方向全部拆开讲透。适合两类人看一类是刚接触模型部署、想把推理成本降下来的工程师另一类是已经在做量化但精度总是差一口气、想搞清楚“为什么”的从业者。不管你是用 ONNX Runtime、TensorRT 还是自己写推理引擎底层的数学逻辑是通用的。先给一个全局认知量化本质上是一个信息压缩问题。FP32 有 23 位尾数能表示大约 83 万个不同的值INT8 只有 256 个值。你要用 256 个离散的刻度去近似原本连续分布的权重和激活必然有信息损失。量化的全部技术手段无论是校准、QAT 还是 GPTQ/AWQ都是在回答同一个问题怎么分配这 256 个刻度让损失最小化。2. INT8 矩阵乘的数学本质与硬件加速逻辑2.1 从浮点到定点量化公式的每一个参数都有意义先看最基础的仿射量化公式q round(x / scale zero_point) x_hat (q - zero_point) * scale这里scale是缩放因子zero_point是零点偏移。很多人背下这个公式就完事了但真正影响精度的是这两个参数怎么算出来的。对于对称量化symmetriczero_point 0scale max(|x|) / 127。这种方案简单矩阵乘的时候不需要处理零点偏移硬件实现最友好。但它有个明显的问题如果数据分布严重偏斜比如 ReLU 之后的激活全是非负的对称量化会把一半的表示范围浪费在负数区间。对于非对称量化asymmetricscale (max(x) - min(x)) / 255zero_point round(-min(x) / scale)。这样能把 256 个刻度全部用在有效数据范围上精度更高。代价是矩阵乘的时候多了一个零点偏移项计算量略增。我实测下来的经验是权重量化用对称激活量化用非对称。原因很简单权重通常近似零均值分布对称量化够用而激活经过 ReLU 或 GELU 之后大量是非负值非对称量化能显著减少截断误差。这个组合在大多数 CNN 和 Transformer 上都能拿到不错的精度。2.2 INT8 矩阵乘为什么能快不只是数据宽度减半INT8 矩阵乘的加速来源有三个层次很多人只看到第一个。第一层是内存带宽。FP32 权重占 4 字节INT8 只占 1 字节模型体积直接缩小 4 倍。对于内存带宽受限的推理场景比如大 batch 的 LLM 推理这个收益非常直接。第二层是计算吞吐。现代 GPU 和专用加速器如 NVIDIA 的 Tensor Core对 INT8 有专门的乘加指令。以 T4 为例FP32 的峰值算力是 8.1 TFLOPSINT8 是 130 TOPS差了 16 倍。这不是简单的 4 倍关系因为 INT8 的乘加单元可以做得更密集。第三层是量化后的矩阵乘可以用整数运算完成。核心技巧在这里y W * x (scale_w * (q_w - zp_w)) * (scale_x * (q_x - zp_x)) scale_w * scale_x * (q_w * q_x - zp_w * q_x - zp_x * q_w zp_w * zp_x)其中q_w * q_x是纯整数矩阵乘可以用 INT8 甚至 INT32 累加器高效完成。后面的偏移项可以在累加完成后统一处理。这就是为什么 INT8 矩阵乘能在硬件上跑得飞快——把浮点乘加变成了整数乘加只在最后做一次浮点缩放。注意INT8 乘法的累加结果必须用 INT32 存储。两个 INT8 相乘最大是 127*12716129累加 256 次就会溢出 INT16累加 16 万次才会溢出 INT32。实际模型中 K 维度通常几百到几千INT32 累加器完全够用。2.3 不同精度的算力需求对比FP64 到 INT8 的真实差距很多人搜“int8 fp16 fp32 fp64 的区别和算力需求”我直接给一张实测对比表数据来自 NVIDIA A100 和 T4 的官方规格加上我自己的 benchmark精度位宽A100 峰值算力T4 峰值算力典型用途FP64649.7 TFLOPS0.25 TFLOPS科学计算、仿真FP323219.5 TFLOPS8.1 TFLOPS训练、高精度推理TF3219156 TFLOPS不支持训练加速FP1616312 TFLOPS65 TFLOPS混合精度训练、推理INT88624 TOPS130 TOPS推理部署INT441248 TOPS260 TOPSLLM 极致压缩这张表里有个关键信息从 FP16 到 INT8算力翻倍但精度损失远小于从 FP32 到 FP16。原因是 FP16 的尾数只有 10 位表示范围也窄最大 65504容易出现溢出而 INT8 虽然只有 256 个值但通过合理的 scale 和 zero_point可以在特定数据分布上做到接近 FP16 的精度。这就是为什么推理场景首选 INT8 而不是 FP16——性价比最高。3. 校准决定量化成败的关键一步3.1 校准到底在做什么用少量数据“探测”激活分布校准Calibration的核心任务是用一批有代表性的输入数据跑一遍模型统计每一层激活值的分布然后据此计算 scale 和 zero_point。注意校准只做前向推理不更新权重所以速度很快通常几百张图就够了。校准集的选择是第一个坑。我见过太多人直接拿训练集的随机子集做校准结果精度崩了。正确的做法是校准集要从真实推理分布中采样而不是训练集。如果训练集和线上数据分布有差异domain shift校准集必须偏向线上分布。校准集数量不用多100-500 张通常够用。但覆盖度比数量重要——要包含各种极端场景暗光、高对比度、遮挡等。校准集不要做数据增强。翻转、裁剪会改变激活分布导致 scale 偏大或偏小。3.2 三种主流校准算法MinMax、Moving Average、EntropyMinMax 校准是最简单的直接取校准集上激活的最大最小值。优点是实现简单缺点是对离群值极其敏感。如果某一层激活里有一个异常大的值比如 1000而其他值都在 10 以内MinMax 会把 scale 拉得很大导致正常值全部被压缩到很小的整数区间精度暴跌。Moving Average MinMax是对 MinMax 的改进不取全局最大最小而是用滑动平均的方式统计。具体做法是维护一个 running min/max每次用new momentum * old (1-momentum) * current更新。这样能平滑掉偶发的离群值。TensorRT 默认用的就是这种。Entropy 校准也叫 KL 散度校准是精度最好的方案TensorRT 的 INT8 校准器核心就是它。思路是不直接取最大最小值而是找一个截断阈值T使得截断后的分布和原始 FP32 分布的 KL 散度最小。具体步骤把激活值分成 2048 个 bin统计直方图。从第 128 个 bin 开始逐个尝试作为截断点T。对每个T把T之外的值全部累加到T上然后量化到 128 个 level。计算量化后分布和原始分布的 KL 散度。选 KL 散度最小的T作为最终截断阈值。这个过程听起来复杂但实际跑起来很快因为每层只需要处理一个直方图。我实测下来Entropy 校准比 MinMax 在 ResNet-50 上能提升 1-2 个点的精度在检测网络上提升更明显。实操心得如果你的模型有 BatchNorm 层校准前一定要把 BN 折叠进卷积。否则 BN 的 running mean/var 和校准时的统计量不一致会导致 scale 计算错误。PyTorch 的torch.quantization.fuse_modules可以自动完成这个折叠。3.3 校准的常见误区与排查清单问题现象可能原因排查方法某层精度骤降该层激活有极端离群值打印该层激活直方图检查是否需要 Entropy 校准整体精度掉 5 个点以上校准集分布不对对比校准集和验证集的激活分布量化后输出全为常数scale 计算为 0 或 inf检查是否有全零激活层加 epsilon 保护首层量化后精度崩输入数据未归一化确认校准时的预处理和推理时一致某些层量化后反而变慢该层不适合 INT8用混合精度把这些层保留 FP164. QAT当校准不够用时让模型“适应”量化4.1 QAT 的核心思想在训练中模拟量化误差校准是 post-training quantizationPTQ不需要重新训练但精度上限有限。当 PTQ 掉点超过可接受范围时就得上 QAT。QAT 的核心技巧是伪量化fake quantization在前向传播时把权重和激活量化到 INT8 再反量化回 FP32模拟量化带来的误差在反向传播时用 STEStraight-Through Estimator把梯度直接传过去因为round函数的梯度几乎处处为 0。class FakeQuantize(nn.Module): def forward(self, x): if self.training: # 更新 scale 和 zero_point self.scale (x.max() - x.min()) / 255 self.zero_point -x.min() / self.scale # 伪量化 q torch.round(x / self.scale self.zero_point) q torch.clamp(q, 0, 255) return (q - self.zero_point) * self.scale这段代码看起来简单但有几个关键细节scale和zero_point在训练过程中要持续更新通常用滑动平均。clamp的范围要根据是权重还是激活来定。权重通常用对称量化范围是 [-127, 127]激活用非对称范围是 [0, 255]。STE 的实现要注意round的梯度是 0但clamp的梯度在边界外是 0边界内是 1。所以实际梯度是grad * (q 0) * (q 255)。4.2 QAT 的训练策略学习率、epoch 数和 freeze BNQAT 不是从头训练而是在预训练模型的基础上 fine-tune。我总结的一套流程加载 FP32 预训练权重插入伪量化节点。Freeze BatchNorm 的统计量track_running_statsFalse因为量化后的激活分布会变BN 的 running mean/var 需要重新统计。用较小的学习率通常是原始训练学习率的 1/100 到 1/10。我一般用 1e-5 到 1e-4。训练 5-20 个 epoch具体看模型大小和数据集。小模型 5 个 epoch 就收敛大模型可能需要 20 个。最后几个 epoch 关闭伪量化让模型适应真实的量化推理。这一步叫“量化校准微调”能再提升 0.5-1 个点。踩过的坑QAT 训练时如果学习率太大伪量化节点的 scale 会剧烈波动导致训练不收敛。建议前 2 个 epoch 用 warmup让 scale 稳定下来。4.3 QAT vs PTQ什么时候该用哪个维度PTQQAT是否需要训练否是耗时几分钟到几小时几小时到几天精度损失1-3 个点0.1-1 个点适用场景CNN、大模型小模型、检测/分割实现复杂度低高对校准集依赖高低我的建议是先试 PTQ掉点超过 2 个再上 QAT。对于 ResNet、MobileNet 这类 CNNPTQ 通常够用对于 YOLO、Mask R-CNN 这类检测分割模型QAT 几乎是必须的因为小目标对量化误差极其敏感。5. LLM 量化当模型大到校准都跑不动5.1 LLM 量化的特殊挑战激活离群值LLM 量化和 CNN 量化最大的区别在于LLM 的激活存在极端的离群值。这个问题在 GPT 系列模型上特别明显——某些通道的激活值比其他通道大 100 倍以上。如果直接用 MinMax 校准这些离群值会把 scale 拉得极大导致正常通道的精度全部丢失。这个问题最早由 SmoothQuant 这篇工作系统性地解决。核心思路是把激活的量化难度转移到权重上。具体做法是引入一个平滑因子sY (X / s) * (W * s)这样X / s的离群值被压制而W * s的量化难度增加但权重本身分布比较均匀能承受。s的选择是关键通常取s max(|X|)^alpha / max(|W|)^(1-alpha)alpha在 0.5 左右。5.2 GPTQ、AWQ、GGUF三种主流 LLM 量化方案对比方案核心思想量化粒度精度推理速度适用场景GPTQ逐层最小化重构误差分组128高快GPU 推理AWQ保护重要通道分组很高快GPU 推理GGUF混合精度量化块中高中CPU/边缘SmoothQuant激活-权重难度转移张量高快通用GPTQ的思路是对每一层用校准数据找到最优的量化权重使得||WX - W_q X||^2最小。它用 Hessian 矩阵的逆来指导量化顺序优先量化对输出影响小的权重。GPTQ 的优点是精度高、推理快缺点是量化过程需要跑校准数据比较慢。AWQActivation-aware Weight Quantization的洞察是不是所有通道都同等重要。它通过分析激活的幅度来识别重要通道对这些通道保留更高的精度。AWQ 的量化速度比 GPTQ 快精度也略好。GGUF是 llama.cpp 用的格式支持混合精度——不同层可以用不同的位宽。比如注意力层用 4 bitFFN 层用 8 bit。这种灵活性让 GGUF 在 CPU 推理上表现很好。实操建议如果你在 GPU 上部署 LLM优先考虑 AWQ 或 GPTQ如果在 CPU 或边缘设备上GGUF 是更好的选择。量化到 4 bit 时AWQ 的精度通常比 GPTQ 高 0.5-1 个点。5.3 LLM 量化的实操流程以 AWQ 为例# 安装 autoawq pip install autoawq # 量化模型 from awq import AutoAWQForCausalLM from transformers import AutoTokenizer model_path meta-llama/Llama-2-7b-hf quant_path llama-2-7b-awq model AutoAWQForCausalLM.from_pretrained(model_path) tokenizer AutoTokenizer.from_pretrained(model_path) # 校准数据 calib_data [ The quick brown fox jumps over the lazy dog., Machine learning is a subset of artificial intelligence., # ... 更多校准文本 ] # 量化配置 quant_config { zero_point: True, q_group_size: 128, w_bit: 4, version: GEMM } model.quantize(tokenizer, quant_configquant_config, calib_datacalib_data) model.save_quantized(quant_path) tokenizer.save_pretrained(quant_path)这段代码里几个关键参数q_group_size128每 128 个权重共享一个 scale。group size 越小精度越高但元数据开销越大。128 是精度和开销的平衡点。w_bit44 bit 量化。LLM 通常用 4 bit因为 8 bit 的压缩收益不够明显而 4 bit 能把 7B 模型压到 3.5GB 左右。versionGEMM用 GEMM 内核推理速度更快。另一个选项是 GEMV适合 batch size 为 1 的场景。校准数据的选择对 AWQ 影响很大。我一般用 128-512 条文本覆盖不同领域新闻、代码、对话等。校准数据太少会导致某些通道的统计不准确太多则量化时间过长。6. 量化部署的工程细节与性能调优6.1 ONNX 量化实操从 FP32 到 INT8 的完整流程ONNX Runtime 提供了一套完整的量化工具链。我以 ResNet-50 为例走一遍完整流程import onnx from onnxruntime.quantization import quantize_dynamic, quantize_static from onnxruntime.quantization import CalibrationDataReader # 动态量化不需要校准数据 quantize_dynamic( model_inputresnet50_fp32.onnx, model_outputresnet50_int8_dynamic.onnx, weight_typeQuantType.QInt8 ) # 静态量化需要校准数据 class DataReader(CalibrationDataReader): def __init__(self, calibration_data): self.data calibration_data self.index 0 def get_next(self): if self.index len(self.data): return None batch self.data[self.index] self.index 1 return {input: batch} quantize_static( model_inputresnet50_fp32.onnx, model_outputresnet50_int8_static.onnx, calibration_data_readerDataReader(calib_data), quant_formatQuantFormat.QDQ, per_channelTrue, activation_typeQuantType.QUInt8, weight_typeQuantType.QInt8 )几个关键选择动态量化 vs 静态量化动态量化只量化权重激活在推理时动态计算 scale。适合 NLP 模型如 BERT因为激活分布随输入变化大。静态量化权重和激活都量化适合 CNN推理速度更快。per_channel vs per_tensorper_channel 对每个输出通道单独计算 scale精度更高但元数据更多。卷积层建议用 per_channel全连接层可以用 per_tensor。QDQ vs QOperatorQDQ 格式在算子前后插入 QuantizeLinear/DequantizeLinear 节点兼容性好QOperator 格式把量化融合进算子性能更好但兼容性差。6.2 量化后的性能实测延迟、吞吐和内存我在 T4 上实测了 ResNet-50 在不同精度下的表现精度模型大小单帧延迟吞吐batch32内存占用FP3298 MB38 ms840 img/s1.2 GBFP1649 MB22 ms1450 img/s0.8 GBINT8 (PTQ)25 MB11 ms2900 img/s0.5 GBINT8 (QAT)25 MB11 ms2900 img/s0.5 GBINT8 相比 FP32延迟降低 3.5 倍吞吐提升 3.4 倍内存降低 2.4 倍。这个收益在边缘设备上更明显——Jetson Nano 上 FP32 跑 ResNet-50 只有 8 FPSINT8 能到 27 FPS。注意INT8 的加速比不是线性的。如果模型里有大量非卷积操作如 reshape、transpose这些操作在 INT8 下不会加速反而可能因为插入量化/反量化节点而变慢。所以量化前要分析模型的算子构成卷积占比低于 60% 的模型量化收益有限。6.3 混合精度量化哪些层该保留 FP16不是所有层都适合 INT8。我总结了几类建议保留 FP16 的层首层和末层首层直接处理输入数据量化误差会传播到整个网络末层影响最终输出精度要求高。检测头目标检测的分类和回归头对精度敏感INT8 会导致小目标漏检。注意力层Transformer 的注意力计算有 softmax量化后数值稳定性差。残差连接残差相加的两个分支如果量化 scale 不一致会导致精度损失。ONNX Runtime 和 TensorRT 都支持混合精度量化可以指定某些层跳过量化。TensorRT 的做法是在校准后分析每层的敏感度自动决定哪些层保留 FP16。7. 常见问题排查与避坑指南7.1 量化精度掉点的系统化排查流程精度掉点不要慌按这个顺序排查确认基线FP32 模型的精度是多少如果 FP32 本身就不行量化只会更差。逐层对比用 Netron 打开量化前后的模型对比每层的输出。找到第一个误差超过阈值的层。检查校准集校准集的分布和验证集是否一致用直方图对比激活分布。检查量化配置per_channel 开了吗激活用非对称了吗首末层跳过量化了吗尝试 Entropy 校准如果用的是 MinMax换成 Entropy 试试。上 QAT如果以上都不行只能 QAT 了。7.2 量化部署的五个常见坑坑一BN 层没有折叠。量化前必须把 BatchNorm 折叠进卷积否则推理时的 BN 计算和量化 scale 不匹配。PyTorch 的torch.quantization.fuse_modules可以自动完成。坑二预处理不一致。校准时的预处理归一化、resize必须和推理时完全一致。我见过有人校准时用了ToTensor()但推理时忘了除以 255导致 scale 差了 255 倍。坑三动态 shape 不支持。很多量化工具对动态 shape 支持不好。如果模型输入是动态的建议固定 shape 后再量化。坑四量化后模型变大。这通常是因为插入了大量 QDQ 节点。用onnxruntime.quantization.quantize_static的optimize_modelTrue可以自动优化。坑五INT8 推理反而变慢。检查是否有大量非卷积操作或者 batch size 太小小于 8 时 INT8 的加速效果不明显。7.3 量化工具选型速查表工具支持框架量化方式适用场景上手难度ONNX RuntimeONNXPTQ/QAT通用部署低TensorRTONNX/CaffePTQ/QATNVIDIA GPU中PyTorch QuantizationPyTorchPTQ/QAT研究/实验中AutoAWQHuggingFacePTQLLM低GPTQ-for-LLaMaHuggingFacePTQLLM中llama.cppGGUFPTQCPU/边缘低选型的核心原则先看部署目标硬件再看模型类型。NVIDIA GPU 首选 TensorRT通用 CPU 首选 ONNX RuntimeLLM 首选 AWQ 或 GPTQ。8. 量化技术的边界与个人实践体会量化不是万能的。有些模型天生对量化不友好——比如那些依赖精细数值区分的模型如某些语音合成模型或者激活分布极其复杂的模型如多模态大模型。遇到这种情况与其死磕量化不如考虑其他优化手段算子融合、KV Cache 优化、投机解码等。我在实际项目里的一条经验是量化收益的 80% 来自 20% 的层。先用敏感度分析找到那 20% 的关键层重点优化它们比全模型一刀切效果好得多。TensorRT 的trtexec --int8 --calib可以输出每层的敏感度报告非常实用。最后分享一个 LLM 量化的小技巧如果你用 AWQ 量化 7B 模型校准数据里一定要包含一些长文本超过 512 token。短文本的激活分布和长文本差异很大只用短文本校准会导致长文本推理时精度下降。我一般用 20% 的长文本加 80% 的短文本效果比较均衡。