1. 为什么模型部署绕不开量化这道坎做过模型部署的人都有一个共同体会训练出一个精度漂亮的模型只是万里长征第一步真正把它塞进生产环境、跑出可接受的延迟和吞吐才是折磨人的开始。我最早接触量化是在一个边缘设备项目上当时手里有一个参数量不到 10M 的图像分类模型FP32 权重文件大概 40MB在服务器上跑得好好的一放到端侧设备上推理延迟直接飙到 200ms 以上内存占用也吃紧。那时候我第一反应是换更小的模型但精度掉得厉害后来才认真去研究量化这条路。量化的本质其实很朴素用更少的比特位来表示原本用 32 位浮点存储的权重和激活值。你可以把它想象成把一张高精度照片压缩成 JPEG——信息有损但只要压缩策略得当肉眼几乎看不出差别。深度学习模型里大量的参数其实存在冗余FP32 的精度对于推理任务来说往往是过剩的用 INT8 表示完全够用。这一压模型体积直接变成原来的四分之一内存带宽需求同步下降而在支持 INT8 指令集的硬件上矩阵乘的吞吐量能提升 2 到 4 倍。但量化绝不是简单地把浮点数乘以一个缩放因子再取整就完事了。核心难点在于如何在压缩表示的同时把精度损失控制在可接受范围内。这就引出了量化领域几个关键问题——量化粒度怎么选、缩放因子和零点怎么定、校准数据怎么喂、要不要做量化感知训练QAT以及面对 LLM 这种超大模型时又该用什么策略。这篇内容我会把 INT8 矩阵乘的底层原理、校准流程、QAT 的实操细节以及 LLM 量化的主流方案完整拆一遍尽量把每个参数背后的“为什么”讲清楚让你看完能直接上手做自己的量化实验。适合阅读的人群做模型部署的工程师、需要在端侧或边缘设备跑推理的开发者、对推理性能优化感兴趣的研究者以及正在被 LLM 显存和推理成本困扰的同学。哪怕你之前没接触过量化只要了解基本的神经网络推理流程这篇内容都能帮你建立起完整的量化知识框架。2. 量化基础与 INT8 矩阵乘的底层逻辑2.1 从浮点到定点量化的数学本质量化的核心是一个仿射映射。给定一个浮点张量 $x$我们想把它映射到 INT8 的整数空间 $[-128, 127]$映射公式是$$x_{int} \text{round}\left(\frac{x}{s}\right) z$$其中 $s$ 是缩放因子scale$z$ 是零点zero point。反量化的时候就是$$x_{float} s \cdot (x_{int} - z)$$这个 $s$ 和 $z$ 怎么来的对于对称量化$z 0$$s \frac{\max(|x|)}{127}$映射范围是 $[-127, 127]$。对于非对称量化$z$ 不为零$s \frac{x_{max} - x_{min}}{255}$$z \text{round}(-x_{min} / s)$映射范围是 $[-128, 127]$。为什么要有对称和非对称两种这跟数据分布有关。权重通常近似对称分布用对称量化就够了而且对称量化在计算上更简单因为零点为零矩阵乘的时候可以省掉一些加法项。激活值就不一样了ReLU 之后的激活全是非负的分布严重偏斜这时候用非对称量化能更充分地利用 INT8 的表示范围精度损失更小。提示实际工程中权重几乎都用对称量化per-channel激活值用非对称量化per-tensor是常见组合。这个组合在 TensorRT、ONNX Runtime 里都是默认推荐。2.2 INT8 矩阵乘到底怎么算的理解了量化映射再看 INT8 矩阵乘就清晰了。假设有两个矩阵 $A$ 和 $B$量化后分别是 $A_q$、$B_q$对应的缩放因子是 $s_A$、$s_B$零点分别是 $z_A$、$z_B$。浮点矩阵乘 $C A \cdot B$ 可以写成$$C_{float} s_A s_B \cdot (A_q - z_A) \cdot (B_q - z_B)$$展开后$$C_{float} s_A s_B \cdot (A_q B_q - z_A B_q - z_B A_q z_A z_B)$$如果是对称量化$z_A z_B 0$那公式就退化成$$C_{float} s_A s_B \cdot A_q B_q$$这就是为什么对称量化在硬件上更受欢迎——INT8 的乘加运算可以直接做最后统一乘一个缩放因子 $s_A s_B$ 就行。而非对称量化需要额外处理零点带来的交叉项计算量更大。实际硬件里INT8 矩阵乘通常走的是DP4ADot Product of 4 8-bit integers and Accumulate指令或者VNNIVector Neural Network Instructions指令。以 DP4A 为例它一次能算 4 组 INT8 乘加累加结果存在 INT32 里。为什么累加器要用 INT32因为 INT8 乘 INT8 的结果范围是 $[-128 \times 127, 127 \times 127]$也就是大约 $[-16256, 16129]$如果累加 256 次结果可能到百万级别INT16 根本装不下必须用 INT32 才不会溢出。2.3 量化粒度per-tensor、per-channel 与 per-group量化粒度决定了缩放因子和零点在多大范围内共享。粒度越细精度越高但元数据开销也越大。粒度类型缩放因子数量精度开销适用场景per-tensor1 个/张量最低最小激活值量化per-channel1 个/通道中等中等权重量化CNNper-group1 个/组最高最大LLM 权重量化per-tensor 是整个张量共用一个 scale实现最简单但对分布不均匀的张量精度损失大。per-channel 是每个输出通道一个 scale对卷积权重特别友好因为不同卷积核的权重分布差异可能很大。per-group 是把通道再分组每组一个 scale这是 LLM 量化里的主流做法比如 group size 取 128就是每 128 个权重共享一个 scale。我实测下来对于 CNN 类模型权重量化从 per-tensor 换成 per-channelImageNet top-1 精度通常能回升 0.5 到 1.5 个百分点而元数据开销只增加了通道数那么多个 float完全可以接受。所以只要硬件支持权重量化优先用 per-channel。2.4 对称与非对称的取舍实战前面讲了对称和非对称的数学区别实际选型时还要考虑硬件支持。很多推理加速器比如某些 NPU只支持对称量化因为对称量化的矩阵乘可以直接映射到硬件指令非对称需要额外的零点处理逻辑会增加电路复杂度。另一个考虑是校准难度。非对称量化需要同时确定 $x_{min}$ 和 $x_{max}$对异常值更敏感。如果校准数据里混入了一个极端大的激活值$x_{max}$ 会被拉高导致整个张量的量化精度下降。对称量化只看绝对值最大值虽然也受异常值影响但至少零点固定在零不会出现偏移。注意校准数据的质量直接决定量化精度。我踩过的坑是拿训练集的一个子集做校准结果里面有几张标注错误的异常样本激活值分布被带偏量化后模型在正常样本上精度掉了 3 个点。后来换成从验证集里随机采样并且做了异常值裁剪精度就回来了。3. 校准量化精度的胜负手3.1 校准到底在做什么校准Calibration是训练后量化PTQ的核心步骤。它的任务是用一批有代表性的数据跑一遍模型统计每一层激活值的分布然后根据这个分布确定量化参数scale 和 zero point。为什么不能直接用权重的分布来定激活的量化参数因为激活值是动态的取决于输入数据。同一个模型输入一张猫的图片和输入一张风景图片中间层的激活分布可能完全不同。所以必须用真实数据去“探测”激活值的范围。校准的流程大致是准备一批校准数据通常 100 到 1000 个样本就够把模型切换到校准模式插入观测器Observer来统计激活值前向传播校准数据观测器记录每层激活的最小值、最大值或直方图根据统计结果计算量化参数把量化参数固化到模型里切换到推理模式3.2 校准算法怎么选校准算法的核心问题是如何从激活值分布中确定最优的 $x_{min}$ 和 $x_{max}$。直接取全局最小最大值是最简单的但容易被异常值带偏。实际常用的算法有几种MinMax 校准直接取观测到的最小值和最大值。实现最简单但对异常值零容忍。如果校准数据里有一个极端值整个量化范围就被拉大了大部分正常值挤在很小的区间里量化误差剧增。Moving Average MinMax对多个 batch 的最小最大值做滑动平均平滑掉单 batch 的波动。比纯 MinMax 稳一些但还是受异常值影响。Entropy 校准KL 散度这是 TensorRT 默认的校准算法也是我认为效果最稳的一种。它的思路是不直接取最小最大值而是找一个截断阈值使得截断后的分布和原始 FP32 分布的 KL 散度最小。换句话说它主动丢弃掉尾部那些极端值用剩下的数据来确定量化范围。实测在 CNN 上Entropy 校准比 MinMax 通常能提升 0.5 到 1 个点的精度。Percentile 校准取激活值的某个百分位数比如 99.9%作为最大值直接砍掉尾部。比 Entropy 简单效果也不错适合快速实验。校准算法实现复杂度抗异常值精度表现推荐场景MinMax低弱一般快速验证Moving Average低中中等数据分布稳定Entropy高强好CNN 生产部署Percentile中强好通用场景3.3 校准数据的准备技巧校准数据不需要标注但必须和真实推理数据同分布。我一般从验证集里随机采样 200 到 500 张覆盖所有类别。如果模型有多个输入分支比如多模态每个分支都要有对应的数据。一个容易被忽略的点是数据预处理必须和推理时完全一致。归一化参数、resize 方式、通道顺序任何一个环节不一致校准统计到的激活分布就是错的。我曾经因为校准数据用了 BGR 而推理用了 RGB量化后精度掉了 5 个点排查了大半天才发现是通道顺序的问题。提示校准样本数量不是越多越好。超过 1000 个样本后统计分布基本收敛再增加样本对精度提升微乎其微反而浪费时间。200 到 500 个是性价比最高的区间。3.4 校准实操以 ONNX Runtime 为例ONNX Runtime 提供了完整的 PTQ 工具链。下面是一个典型的校准配置流程from onnxruntime.quantization import quantize_static, CalibrationDataReader, QuantType, QuantFormat class MyCalibrationReader(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_name: batch} def rewind(self): self.index 0 quantize_static( model_inputmodel.onnx, model_outputmodel_int8.onnx, calibration_data_readerMyCalibrationReader(calib_data), quant_formatQuantFormat.QDQ, activation_typeQuantType.QUInt8, weight_typeQuantType.QInt8, per_channelTrue, calibrate_methodCalibrationMethod.Entropy )这里有几个参数值得说明。quant_format选 QDQ 还是 QOperatorQDQ 是在模型里插入 QuantizeLinear 和 DequantizeLinear 节点兼容性好方便调试QOperator 是把量化算子融合成单个节点性能更好但可读性差。生产部署我一般先用 QDQ 验证精度确认没问题再转 QOperator。activation_type选 QUInt8 还是 QInt8激活值经过 ReLU 后非负用 QUInt8无符号能多利用一位表示范围精度更好。权重用 QInt8有符号因为权重有正有负。per_channelTrue对权重量化启用 per-channel这个几乎必开。4. QAT当 PTQ 精度不够时怎么办4.1 PTQ 的天花板在哪里PTQ 的优点是快不需要重新训练几个小时就能搞定。但它有个硬伤量化误差是在训练完成后才引入的模型没有机会去适应这种误差。对于大模型或者对精度敏感的任务比如检测、分割PTQ 后的精度损失可能超过可接受范围。我遇到过一个案例一个轻量级分割模型PTQ 后 mIoU 从 72% 掉到 65%怎么调校准算法都回不来。这种情况下就得上 QAT。QAT 的思路是在训练过程中模拟量化误差让模型在训练时就“感受”到量化带来的精度损失从而调整权重去补偿。具体做法是在前向传播时插入伪量化节点Fake Quantization把权重和激活量化后再反量化模拟量化误差但反向传播时用直通估计器STE让梯度正常回传。4.2 QAT 的实操流程QAT 不是从头训练而是在一个已经训练好的 FP32 模型上做微调。典型流程是加载预训练的 FP32 模型插入伪量化节点配置量化参数用较小的学习率通常是原始学习率的 1/100 到 1/10微调几个 epoch固化量化参数导出量化模型以 PyTorch 为例用torch.quantization模块import torch.quantization as tq model MyModel() model.load_state_dict(torch.load(fp32_model.pth)) model.eval() model.qconfig tq.get_default_qat_qconfig(fbgemm) model_fused tq.fuse_modules(model, [[conv1, bn1, relu1]]) model_prepared tq.prepare_qat(model_fused, inplaceFalse) optimizer torch.optim.SGD(model_prepared.parameters(), lr1e-5, momentum0.9) for epoch in range(5): for data, target in train_loader: optimizer.zero_grad() output model_prepared(data) loss criterion(output, target) loss.backward() optimizer.step() model_prepared.eval() model_int8 tq.convert(model_prepared, inplaceFalse) torch.save(model_int8.state_dict(), int8_model.pth)几个关键点fuse_modules把 ConvBNReLU 融合成一个模块减少量化误差累积这一步几乎必做。qconfig选 fbgemm 还是 qnnpackfbgemm 面向 x86qnnpack 面向 ARM根据部署平台选。微调学习率不能太大否则会破坏预训练权重我一般从 1e-5 开始试。4.3 QAT 的注意事项与踩坑记录QAT 有几个容易翻车的地方。第一是伪量化节点的位置不是所有层都适合量化。第一层和最后一层通常对精度影响大可以考虑跳过量化保持 FP32。第二是BN 层的处理QAT 时 BN 的统计量会变化需要在微调结束后重新校准 BN 的 running mean 和 var否则推理时会有偏差。第三是微调数据量QAT 不需要全量训练数据但也不能太少。我一般用训练集的 10% 到 20%太少会导致过拟合太多则浪费时间。第四是学习率调度前几个 epoch 用正常学习率最后一个 epoch 把学习率降到接近零让模型收敛到稳定的量化参数。注意QAT 微调后一定要在验证集上完整评估不要只看 loss。我遇到过 loss 下降但精度反而变差的情况原因是伪量化节点的梯度估计有偏导致权重更新方向不对。这时候可以尝试降低学习率或者减少微调 epoch。4.4 PTQ 与 QAT 的选型决策到底用 PTQ 还是 QAT我的经验是如果 PTQ 后精度损失在 1 个点以内直接用 PTQ省时省力如果损失在 1 到 3 个点先尝试优化校准算法和数据可能能救回来如果损失超过 3 个点或者任务对精度极度敏感上 QAT如果模型是 LLMPTQ 的 weight-only 量化通常是首选QAT 成本太高对比维度PTQQAT训练成本无需要微调时间开销小时级天级精度表现一般好适用模型CNN、小模型检测、分割、大模型实现难度低中高5. LLM 量化当模型大到装不下5.1 LLM 量化的特殊挑战LLM 的量化和小模型完全不是一个玩法。第一个挑战是激活值异常值。LLM 的激活值里存在极少数维度上的超大值这些异常值比正常值大几十甚至上百倍。如果直接做 per-tensor 量化这些异常值会把量化范围拉得极大导致正常值全部挤在很小的区间里精度崩溃。第二个挑战是显存瓶颈。一个 7B 的模型FP16 权重就要 14GBINT8 降到 7GBINT4 降到 3.5GB。量化不仅是加速手段更是能不能跑起来的问题。第三个挑战是量化粒度。LLM 的权重分布在不同通道间差异很大per-tensor 量化效果很差必须用 per-channel 甚至 per-group。但 per-group 的元数据开销又不能太大所以 group size 的选择很关键。5.2 主流 LLM 量化方案对比目前 LLM 量化主要有几条技术路线GPTQ基于二阶信息的逐层量化方法。它对每一层的权重做量化用 Hessian 矩阵来指导量化误差的最小化。GPTQ 的特点是量化速度快精度好支持 INT4 甚至 INT3。缺点是只量化权重激活值还是 FP16所以推理时是 weight-only 量化。AWQActivation-aware Weight Quantization。核心观察是权重里只有一小部分约 1%对激活值影响大这部分权重应该保留更高精度。AWQ 通过激活值分布来识别这些重要权重对它们做保护。实测 AWQ 在 INT4 下比 GPTQ 精度略好尤其是小模型。SmoothQuant解决激活值异常值问题的方案。它通过一个数学等价变换把激活值的量化难度迁移到权重上。具体来说对激活值除以一个平滑因子 $s$对权重乘以 $s$保持数学等价但让激活值分布更平滑更容易量化。SmoothQuant 支持 W8A8权重和激活都 INT8比 weight-only 方案加速更明显。GGUF/GGML 量化这是 llama.cpp 生态用的格式支持从 2-bit 到 8-bit 的多种量化级别。它的特点是 CPU 推理友好量化级别灵活适合在消费级硬件上跑 LLM。方案量化对象典型位宽精度加速比适用场景GPTQ权重INT4/INT3好2-3xGPU 推理AWQ权重INT4很好2-3xGPU 推理SmoothQuant权重激活INT8好3-4x高吞吐服务GGUF权重2-8bit中等CPU 友好本地部署5.3 GPTQ 量化的实操细节GPTQ 的量化流程大致是加载 FP16 模型准备校准数据通常用 C4 或 WikiText 的几百条样本逐层做量化。以 AutoGPTQ 为例from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig quantize_config BaseQuantizeConfig( bits4, group_size128, desc_actFalse, damp_percent0.01 ) model AutoGPTQForCausalLM.from_pretrained( meta-llama/Llama-2-7b-hf, quantize_configquantize_config ) model.quantize(calibration_dataset) model.save_quantized(llama-2-7b-gptq-4bit)group_size是最关键的参数。128 是默认值精度和开销的平衡点。降到 64 精度会好一点但元数据翻倍升到 256 精度会掉但显存更省。desc_act控制是否按激活值重要性排序开启后精度略好但推理速度会慢一些。damp_percent是 Hessian 矩阵的阻尼系数防止数值不稳定一般 0.01 到 0.1 之间。提示GPTQ 量化 7B 模型大概需要 20 到 40 分钟取决于 GPU13B 模型翻倍。量化过程中显存占用会比推理时高因为要计算 Hessian。如果显存不够可以减小 calibration 的 batch size。5.4 AWQ 与 SmoothQuant 的选型建议AWQ 的量化速度比 GPTQ 快精度在 INT4 下通常略好尤其是 7B 以下的小模型。它的实现也更简单不需要计算 Hessian。如果你的场景是 INT4 weight-only 量化我优先推荐 AWQ。SmoothQuant 适合需要 W8A8 的场景也就是既要量化权重又要量化激活。它的优势是能真正利用 INT8 矩阵乘的硬件加速吞吐量提升比 weight-only 方案更明显。但 SmoothQuant 的实现复杂度更高需要调整平滑因子对校准数据也更敏感。实际选型时如果只是想在单卡上跑起更大的模型AWQ 或 GPTQ 的 INT4 就够了。如果是做在线服务追求高吞吐和低延迟SmoothQuant 的 W8A8 更合适。6. 常见问题与排查技巧实录6.1 量化后精度掉得厉害怎么排查精度下降是量化最常见的问题。排查思路是从粗到细第一步确认校准数据是否正确。检查预处理是否和推理一致样本是否覆盖了所有类别有没有异常样本。这一步能解决 50% 以上的精度问题。第二步检查量化配置。权重量化是否开了 per-channel激活量化用的是对称还是非对称校准算法是 MinMax 还是 Entropy把 MinMax 换成 Entropy 通常能救回 0.5 到 1 个点。第三步定位敏感层。逐层分析量化误差找出哪些层的量化误差最大。通常第一层、最后一层、以及某些特殊结构如 depthwise conv对量化更敏感。这些层可以跳过量化保持 FP32。第四步如果以上都不行上 QAT。6.2 量化模型推理速度反而变慢这个问题听起来反直觉但确实会发生。原因通常有几个一是硬件不支持 INT8 加速。如果部署平台没有 INT8 指令集量化模型会先反量化成 FP32 再计算反而多了一步转换开销。这种情况量化不仅不加速还会变慢。二是量化算子没有融合。ConvBNReLU 如果没有融合每个算子单独量化再反量化中间会有大量的 Quantize/Dequantize 节点开销很大。三是内存带宽瓶颈。如果模型本身是 memory-bound 而不是 compute-bound量化减少的内存访问可能被反量化的开销抵消。排查方法是看推理时的算子耗时分布确认瓶颈在哪里。如果 Quantize/Dequantize 节点占比高说明算子融合没做好。6.3 常见问题速查表问题现象可能原因排查方法解决方案精度掉 3 个点以上校准数据分布不对对比校准和推理的预处理重新准备校准数据精度掉 1-3 个点校准算法不佳换 Entropy 或 Percentile调整校准算法特定层误差大该层对量化敏感逐层分析量化误差跳过该层量化推理变慢硬件不支持 INT8查看算子耗时换支持 INT8 的平台推理变慢算子未融合检查计算图做算子融合显存没降量化未生效检查模型权重类型确认量化配置输出全零或 NaN量化范围溢出检查 scale 和 zero point调整量化范围6.4 几个容易被忽略的细节第一个细节是量化模型的保存格式。不同框架的量化模型格式不通用ONNX 的 QDQ 模型可以跨平台但 PyTorch 的量化模型导出到 ONNX 时经常出问题。建议在量化前就确定好部署平台选对应的量化工具链。第二个细节是动态量化和静态量化的区别。动态量化只在推理时统计激活范围不需要校准数据适合 NLP 模型如 LSTM、Transformer。静态量化需要校准数据但推理速度更快适合 CNN。选错了类型要么精度不够要么速度上不去。第三个细节是量化后的模型微调。有些框架支持在量化后做少量微调来恢复精度这其实是 QAT 的简化版。如果 PTQ 精度差一点但不想做完整 QAT可以试试这个。第四个细节是多线程和 batch size 的影响。量化模型的性能表现和 batch size 关系很大。小 batch 时可能是 latency-bound大 batch 时是 throughput-bound。调优时要根据实际场景选 batch size不要盲目追求大 batch。7. 我个人的量化实践体会做了这么多量化项目我最大的体会是量化不是一锤子买卖而是一个需要反复迭代的过程。同一个模型换一个校准数据集、换一种校准算法、调整一下量化粒度精度可能就差好几个点。所以不要指望一次配置就能达到最优要留出足够的实验时间。另一个体会是量化的收益和硬件强相关。在支持 INT8 的服务器 GPU 上量化能带来实打实的加速在一些边缘设备上量化更多是为了省内存而不是提速。做方案设计时一定要先确认目标硬件的量化支持能力否则可能白忙一场。最后LLM 量化和传统模型量化是两个不同的技术栈。传统量化那套 per-tensor、校准、QAT 的思路直接搬到 LLM 上往往不work。LLM 量化要关注异常值处理、group-wise 量化、weight-only 策略这些专门的技术。如果你是从 CNN 量化转过来的建议先把 GPTQ 和 AWQ 的论文读一遍理解它们的核心假设和适用边界。再分享一个小技巧量化实验一定要做好记录。每次实验的配置、校准数据、精度结果都记下来形成一个实验矩阵。这样当精度不达标时你能快速定位是哪个参数的问题而不是盲目试错。我一般用表格记录列包括模型、量化位宽、粒度、校准算法、校准样本数、精度、推理延迟一目了然。