
量化这件事说起来简单做起来坑多。我最早接触量化是在一个边缘设备上部署视觉模型当时FP32的ResNet跑一帧要接近200毫秒完全没法满足实时性要求。尝试了剪枝、蒸馏之后发现精度掉得厉害最后用INT8量化把推理速度拉到了原来的3倍多精度只掉了不到1个点。从那以后量化就成了我部署模型时的默认选项。这篇文章我会把量化从原理到落地的完整链路讲清楚包括INT8矩阵乘的底层逻辑、校准方法的选择、QAT的训练技巧以及LLM量化的特殊处理。不管你是刚接触模型部署的新手还是已经在做推理优化的老手应该都能从中找到有用的东西。1. 量化到底在做什么从浮点到定点的本质转换1.1 一个生活化类比理解量化的核心思想你可以把量化想象成把一把精确到毫米的尺子换成只标注厘米的尺子。原来你可以量出3.7厘米现在只能量出大概4厘米。精度确实损失了但如果你只是要判断一个东西能不能放进抽屉厘米级的精度完全够用。量化做的事情本质上就是这个——把模型参数和激活值从高精度的浮点数映射到低精度的定点数用可控的精度损失换取显著的存储和计算收益。具体来说FP32有32位能表示大约7位有效十进制数字动态范围极大。INT8只有8位能表示-128到127共256个整数值。从FP32到INT8存储直接压缩到四分之一而且在支持INT8指令集的硬件上矩阵乘的吞吐量可以提升2到4倍。这个账算下来非常划算。但量化不是简单地把小数点后面的数字砍掉。关键在于如何设计映射关系让有限的256个整数值尽可能均匀地覆盖原始浮点数的分布范围。这就引出了量化的核心公式real_value scale * (quantized_value - zero_point) quantized_value round(real_value / scale) zero_point其中scale是缩放因子zero_point是零点偏移。对于对称量化zero_point为0映射区间是[-127, 127]对于非对称量化zero_point是一个非零整数映射区间是[0, 255]。选择哪种方式取决于数据的分布特征。1.2 对称量化与非对称量化的选择逻辑对称量化的映射区间关于原点对称scale max(abs(min_val), abs(max_val)) / 127。这种方式实现简单矩阵乘的时候不需要额外处理零点偏移计算效率高。但它的问题在于如果数据分布明显偏向一侧比如ReLU之后的激活值全是非负的对称量化会浪费一半的表示空间。非对称量化则根据实际的数据范围来确定映射区间scale (max_val - min_val) / 255zero_point round(-min_val / scale)。它能更充分地利用256个整数值精度通常更好。但代价是计算时需要额外处理zero_point在矩阵乘中会增加一些开销。我在实际项目中的经验是权重通常用对称量化因为权重分布一般比较均匀且接近零均值激活值用非对称量化因为经过ReLU之后的数据往往是非负的非对称量化能更好地覆盖这个范围。当然这不是绝对的具体还是要看数据的实际分布。1.3 量化粒度的权衡per-tensor还是per-channel量化粒度决定了scale和zero_point的共享范围。Per-tensor量化对整个张量使用同一组参数实现最简单但精度损失可能较大尤其是当张量内不同通道的数据分布差异明显时。Per-channel量化对每个通道单独计算量化参数精度更好但需要存储更多的scale和zero_point计算时也要做相应的索引。对于卷积层的权重per-channel量化几乎是标配。因为卷积核不同通道的权重分布可能差异很大用同一组量化参数会导致某些通道的量化误差特别大。而对于激活值per-tensor量化通常就够了因为激活值的分布相对一致而且per-channel量化激活值在推理时会带来额外的计算开销。注意per-channel量化在ONNX中的支持需要确认目标推理引擎是否兼容。有些老版本的TensorRT对per-channel量化的支持不完整部署前一定要验证。2. INT8矩阵乘的底层实现为什么能快这么多2.1 从FP32乘加到INT8点积的硬件逻辑要理解INT8矩阵乘为什么快得先看看FP32的乘加是怎么做的。FP32的乘法器需要处理23位尾数的乘法、8位指数的加法以及规格化处理整个电路面积大、延迟高。而INT8的乘法器只需要处理8位整数的乘法结果用16位或32位累加器存储电路简单得多。在同样的芯片面积下可以塞进更多的INT8乘法单元这就是吞吐量提升的硬件基础。具体到矩阵乘C A * B其中A是M×K的矩阵B是K×N的矩阵。FP32的实现需要对每个输出元素做K次浮点乘加总共M×N×K次浮点运算。INT8的实现则是把A和B都量化成INT8做K次整数乘加最后再乘以scale_A * scale_B还原到浮点。整数乘加的吞吐量远高于浮点乘加这就是加速的来源。但这里有个关键细节INT8乘法的结果需要用INT32累加器来存储。因为两个INT8相乘最大可以到127×12716129K次累加后很容易溢出INT16的范围。用INT32累加可以保证在K不超过几万的情况下不会溢出这对于绝大多数模型层来说都足够了。2.2 量化矩阵乘的完整计算流程假设我们有两个量化后的矩阵A_q round(A / scale_A) zero_point_A形状M×KB_q round(B / scale_B) zero_point_B形状K×N那么量化矩阵乘的计算过程是对A_q和B_q做INT8矩阵乘得到INT32的结果C_int32计算输出scalescale_C scale_A * scale_B如果需要处理zero_point的修正项把C_int32乘以scale_C再加上zero_point_C得到最终的量化输出对于对称量化zero_point0第3步可以省略计算非常干净。对于非对称量化需要额外计算zero_point_A * sum(B_q)和zero_point_B * sum(A_q)等修正项这也是为什么对称量化在推理时更受欢迎的原因之一。import numpy as np def quantize_symmetric(tensor, num_bits8): 对称量化将浮点张量映射到INT8 qmin -(2 ** (num_bits - 1)) 1 # -127 qmax 2 ** (num_bits - 1) - 1 # 127 abs_max np.max(np.abs(tensor)) scale abs_max / qmax q_tensor np.round(tensor / scale).astype(np.int8) q_tensor np.clip(q_tensor, qmin, qmax) return q_tensor, scale def quantized_matmul(A, B, scale_A, scale_B): INT8矩阵乘模拟 C_int32 np.dot(A.astype(np.int32), B.astype(np.int32)) scale_C scale_A * scale_B C_float C_int32 * scale_C return C_float2.3 实际推理引擎中的优化技巧真实的推理引擎如TensorRT、ONNX Runtime在实现INT8矩阵乘时还会做很多额外的优化。比如把卷积操作通过im2col转换成矩阵乘然后调用高度优化的INT8 GEMM库。这些库会利用SIMD指令如AVX512-VNNI、ARM的Dot Product指令来一次处理多个INT8乘法进一步提升吞吐量。另一个重要优化是算子融合。比如Conv BN ReLU的组合在量化后可以融合成一个INT8卷积操作中间结果不需要反复量化再反量化。这减少了内存访问次数和量化误差的累积。我在TensorRT上实测过融合后的INT8推理比未融合的版本快了将近40%。提示如果你的模型中有大量的逐元素操作如Add、Mul考虑把它们和相邻的卷积或全连接层融合。TensorRT的polygraphy工具可以帮助分析哪些层可以融合。3. 校准决定量化精度的关键一步3.1 校准的本质用少量数据估计数据分布校准Calibration是训练后量化PTQ中最关键的一步。它的核心任务是用一小批有代表性的数据跑一遍模型统计每一层激活值的分布范围从而确定合适的scale和zero_point。校准做得好不好直接决定了量化模型的精度。为什么不能直接用权重的分布来推断激活值的范围因为激活值依赖于输入数据不同的输入会导致不同的激活分布。而且激活值的动态范围通常比权重大得多尤其是经过ReLU之后有些通道的激活值可能非常大有些则接近零。如果不做校准直接用默认范围要么截断太多导致精度损失要么范围太大导致量化分辨率不够。校准数据的选取有几个原则首先数量不需要太多通常几百到几千个样本就够了其次数据分布要能代表实际推理时的输入分布最后如果模型有多个输入分支要确保每个分支都能被覆盖到。3.2 几种主流校准方法的对比与选择目前主流的校准方法有几种各有适用场景校准方法原理优点缺点适用场景Min-Max取激活值的全局最小最大值实现简单无超参对离群值敏感数据分布均匀时Moving Average Min-Max滑动平均更新min/max对离群值更鲁棒需要调整滑动窗口数据分布有波动时KL散度最小化量化前后分布的KL散度精度通常最好计算量大需要调参对精度要求高的场景Percentile取百分位数作为范围能排除极端离群值百分位数的选择需要实验存在明显离群值时MSE最小化量化前后的均方误差直接优化目标指标计算量较大回归类任务我在实际项目中最常用的是KL散度和Percentile这两种。KL散度在TensorRT中是默认的校准方法效果稳定。Percentile在激活值存在明显离群值时表现更好比如Transformer中的注意力分数。选择校准方法时我通常的做法是先用KL散度跑一遍看看每层的量化误差如果发现某些层的误差特别大再针对这些层尝试Percentile或其他方法。这种混合策略往往能取得比单一方法更好的效果。3.3 校准中的常见陷阱与排查方法校准过程中最容易踩的坑是校准数据不具有代表性。我曾经遇到过一个案例用随机噪声做校准结果量化后的模型在真实数据上精度掉了十几个点。后来换成真实场景的采样数据精度损失降到了1个点以内。这个教训说明校准数据的选择比校准方法本身更重要。另一个常见问题是校准时的batch size设置不当。Batch size太小统计量不稳定太大又可能超出内存。我的经验是对于大多数模型batch size设在8到32之间比较合适。如果模型有BatchNorm层要确保校准时的batch统计量和推理时一致。还有一个隐蔽的坑是预处理不一致。校准时的数据预处理归一化、resize等必须和推理时完全一致否则统计出来的激活分布会有偏差。这个问题在图像模型中特别常见因为resize的方式双线性、最近邻会直接影响激活值。排查校准问题的方法可以逐层对比量化前后的输出找出误差最大的层。如果某一层的量化误差异常大检查该层的激活值分布看是否存在极端的离群值或者分布过于集中。针对性地调整该层的量化策略往往能显著改善整体精度。4. QAT让模型在训练中学会适应量化4.1 QAT与PTQ的本质区别PTQ是在模型训练完成后直接量化不需要重新训练。它的优点是简单快捷但精度损失可能较大尤其是对于小模型或者对量化敏感的模型。QATQuantization-Aware Training则是在训练过程中模拟量化的效果让模型参数逐渐适应量化带来的误差。QAT的核心思想是在前向传播时插入伪量化节点Fake Quantization模拟量化的舍入和截断操作但反向传播时仍然使用浮点梯度。这样模型在训练过程中就能感知到量化的影响从而调整参数来补偿量化误差。伪量化节点的操作是output round(clamp(input / scale, qmin, qmax)) * scale。前向传播时这个操作会引入量化误差反向传播时由于round函数的梯度几乎处处为零通常使用直通估计器Straight-Through Estimator, STE来近似梯度即把round的梯度当作1来处理。4.2 QAT的训练策略与学习率调度QAT的训练不是从头开始而是在一个已经训练好的浮点模型基础上进行微调。学习率通常设得比较小因为模型已经收敛得差不多了只需要微调来适应量化。我一般会用初始学习率的十分之一到百分之一作为QAT的学习率。训练轮数也不需要太多通常几个epoch就够了。太多反而可能导致过拟合。在训练过程中可以逐步收紧量化的范围让模型有一个适应的过程。比如前几个epoch用较大的范围后面逐渐缩小到实际的量化范围。另一个重要技巧是在QAT时冻结BatchNorm的统计量。因为QAT的batch size通常比较小BatchNorm的统计量可能不准确。冻结之后用浮点模型训练时累积的统计量效果更稳定。import torch import torch.nn as nn class FakeQuantize(nn.Module): def __init__(self, num_bits8): super().__init__() self.num_bits num_bits self.qmin -(2 ** (num_bits - 1)) 1 self.qmax 2 ** (num_bits - 1) - 1 self.scale nn.Parameter(torch.tensor(1.0), requires_gradTrue) def forward(self, x): # 前向模拟量化 x_q torch.round(x / self.scale) x_q torch.clamp(x_q, self.qmin, self.qmax) x_deq x_q * self.scale # 反向直通估计器 return x (x_deq - x).detach()4.3 QAT实战中的调参经验QAT中最需要调的是scale的初始化。如果scale初始值设得太大量化误差会很大模型很难收敛设得太小又会截断太多信息。我的做法是用PTQ校准得到的scale作为QAT的初始值这样起点比较合理。另一个经验是QAT时不要对所有层都一视同仁。有些层对量化特别敏感比如第一层和最后一层可以对这些层使用更高的精度如16位或者跳过量化。这种混合精度的策略在实践中效果很好。还有一个容易忽略的点是QAT后的模型导出。QAT训练时用的是伪量化节点导出时需要把这些节点转换成真正的量化算子。不同的推理引擎对QAT模型的支持程度不同TensorRT对PyTorch的QAT模型支持比较好ONNX Runtime则需要通过ONNX的QDQ格式来导出。注意QAT训练时使用的数据增强策略要和实际推理时保持一致。如果训练时用了颜色抖动、随机裁剪等增强推理时没有量化模型可能会因为分布偏移而精度下降。5. LLM量化的特殊挑战与解决方案5.1 为什么LLM量化比CNN更难LLM的量化比传统CNN困难得多原因有几个。首先LLM的参数量巨大动辄几十亿甚至上千亿量化误差会在深层网络中累积放大。其次LLM中的激活值分布存在严重的离群值问题某些通道的激活值可能是其他通道的几十倍甚至上百倍这让传统的min-max校准完全失效。具体来说LLM的激活值中经常出现一些超级通道它们的值特别大。如果按照全局最大值来确定量化范围大部分正常值的量化分辨率会非常低导致精度严重下降。这个问题在Transformer的注意力层和前馈层中尤为突出。另一个挑战是LLM的推理是自回归的每一步的输出都依赖于前面的输出。量化误差会在生成过程中逐步累积导致生成的文本质量下降甚至出现重复、乱码等问题。5.2 LLM量化的主流方案GPTQ、AWQ与SmoothQuant针对LLM量化的挑战学术界和工业界提出了几种有效的方案GPTQ是一种基于二阶信息的逐层量化方法。它利用Hessian矩阵的近似来指导权重的量化在量化每一列权重时会调整剩余未量化的权重来补偿误差。GPTQ可以把LLM的权重压缩到4位甚至3位而精度损失很小。它的缺点是量化过程比较慢因为需要计算Hessian矩阵。AWQActivation-aware Weight Quantization的核心观察是权重的量化误差对激活值大的通道影响更大。因此AWQ在量化时会根据激活值的分布对权重进行缩放保护那些对应大激活值的权重通道。AWQ的量化速度比GPTQ快精度也相当。SmoothQuant则从另一个角度解决问题。它发现激活值的离群值可以通过数学等价变换转移到权重上从而让激活值更容易量化。具体来说SmoothQuant对激活值和权重同时做缩放Y (X / s) * (W * s)其中s是一个平滑因子。这样激活值的范围被压缩了而权重的范围扩大了但权重的量化本来就比激活值容易所以整体效果更好。方案量化对象位宽支持量化速度精度保持适用场景GPTQ权重2/3/4/8 bit慢很好离线量化追求极致压缩AWQ权重4 bit中等好平衡速度与精度SmoothQuant权重激活8 bit快好需要同时量化激活值LLM.int8()权重激活8 bit快好对离群值有专门处理5.3 实际部署LLM量化模型的注意事项在实际部署LLM量化模型时有几个关键点需要注意。首先是推理框架的选择。不同的框架对量化模型的支持程度不同比如vLLM对GPTQ和AWQ的支持比较好TensorRT-LLM则对SmoothQuant和INT8量化有专门优化。选择框架时要考虑模型格式、硬件平台和性能需求。其次是量化位宽的选择。4位量化可以把模型大小压缩到原来的四分之一但精度损失比8位大。对于大多数场景我建议先用8位量化如果显存或存储实在不够再考虑4位。3位和2位量化目前还不太成熟除非对精度要求极低否则不建议使用。还有一个实际问题是量化模型的加载和推理速度。量化模型虽然权重变小了但推理时的反量化操作会带来额外开销。如果推理框架没有针对量化做专门优化实际加速可能没有预期的大。我在测试中发现用GPTQ量化的模型在vLLM上的推理速度比FP16快了大约1.5到2倍但如果用普通的PyTorch加载加速效果可能只有1.2倍左右。提示部署LLM量化模型前一定要用实际业务数据做端到端的评测不能只看困惑度Perplexity指标。困惑度低不代表生成质量好有些量化模型在困惑度上表现不错但实际生成时会出现重复、逻辑混乱等问题。6. 量化落地的完整工作流与避坑清单6.1 从浮点模型到量化部署的完整流程一个完整的量化工作流通常包含以下步骤模型准备确保浮点模型已经训练收敛精度达到预期。导出为ONNX或TorchScript格式方便后续处理。量化方案选择根据模型类型、硬件平台和精度要求选择PTQ还是QAT选择对称还是非对称量化选择per-tensor还是per-channel。校准准备有代表性的校准数据运行校准算法收集每层的量化参数。量化模型生成根据校准结果生成量化模型插入量化/反量化节点。精度验证在验证集上评测量化模型的精度和浮点模型对比。如果精度损失超过阈值需要调整量化策略。性能测试在目标硬件上测试量化模型的推理速度和内存占用确认是否达到预期。部署上线将量化模型集成到推理服务中监控实际运行时的精度和性能。这个流程看起来线性但实际上经常需要迭代。比如精度验证不通过时可能需要回到校准步骤调整方法或者回到量化方案选择步骤换一种策略。6.2 量化精度损失的诊断与修复当量化模型精度下降时可以按照以下步骤排查首先逐层对比量化前后的输出找出误差最大的层。这一步可以用PyTorch的hook机制或者ONNX Runtime的调试工具来实现。如果误差集中在某几层说明这些层对量化特别敏感。然后检查这些敏感层的激活值分布。如果存在明显的离群值可以尝试用Percentile校准或者对激活值做裁剪。如果分布过于集中可能是量化范围设置不当需要重新校准。如果逐层分析没有发现明显问题但整体精度就是上不去可以尝试以下策略对敏感层使用更高的位宽如16位对其他层保持8位或者对敏感层跳过量化保持浮点计算。这种混合精度的方案虽然会增加一些计算开销但能有效保住精度。还有一个容易被忽略的点是量化后的模型可能需要重新调参。比如量化会改变模型输出的分布原来设定的后处理阈值可能不再适用。这时候需要根据量化模型的输出重新调整后处理参数。6.3 我踩过的量化坑与经验总结第一个坑是校准数据的预处理不一致。有一次我用ImageNet的验证集做校准但推理时的输入是经过不同归一化处理的结果量化模型在实际场景中精度掉了8个点。后来统一了预处理流程问题就解决了。第二个坑是忽略了BatchNorm层的影响。在PTQ时如果校准数据的batch统计量和训练时差异较大BatchNorm的输出分布会偏移导致量化误差增大。我的做法是在量化前先跑一遍校准数据更新BatchNorm的统计量然后再做量化。第三个坑是过度追求压缩率。曾经尝试把模型量化到4位结果精度掉了太多不得不回退到8位。后来我明白了一个道理量化位宽的选择要在精度和压缩率之间找平衡不能一味追求极致压缩。对于大多数场景8位量化已经能带来足够的收益4位量化只在显存极度受限时才考虑。第四个坑是忽略了推理引擎的兼容性。有一次用PyTorch的QAT训练了模型导出ONNX后发现某些量化算子不被目标推理引擎支持不得不重新用PTQ的方式做量化。所以在开始量化之前一定要确认目标推理引擎支持哪些量化格式和算子。6.4 量化工具链的选型建议目前主流的量化工具链有几种各有优劣TensorRT的量化工具链最成熟支持PTQ和QAT对INT8矩阵乘的优化最好。但它的生态相对封闭主要面向NVIDIA GPU。如果你的部署环境是NVIDIA GPUTensorRT是首选。ONNX Runtime的量化工具支持跨平台可以在CPU、GPU和边缘设备上运行。它的PTQ流程比较简单但对QAT的支持不如TensorRT完善。适合需要跨平台部署的场景。PyTorch原生的量化工具torch.quantization对QAT的支持很好和PyTorch生态无缝集成。但导出到其他推理引擎时可能会遇到兼容性问题。OpenVINO针对Intel硬件做了深度优化在CPU上的INT8推理性能很好。如果你的部署环境是Intel CPU或集成显卡OpenVINO是不错的选择。选择工具链时我通常的建议是先确定目标硬件平台然后选择该平台上优化最好的工具。不要为了用某个工具而换硬件那样成本太高。提示无论选择哪个工具链都建议先用一个小模型跑通完整的量化流程确认工具链的各个环节都能正常工作再应用到实际的大模型上。这样可以避免在复杂模型上遇到问题时无从下手。7. 量化技术的边界与未来方向量化不是万能的。它最适合的场景是模型已经训练收敛推理时的精度要求不是极端苛刻硬件支持低精度计算。如果模型本身精度就不够量化只会让情况更糟。如果应用场景对精度要求极高比如医疗影像诊断量化可能不是好的选择或者需要非常谨慎地使用混合精度策略。从技术趋势来看量化正在和其他压缩技术结合使用。比如量化剪枝、量化蒸馏这些组合往往能取得比单一技术更好的效果。另一个方向是自动化量化用神经网络架构搜索NAS的思路来自动搜索每层的最优量化位宽和策略减少人工调参的工作量。对于LLM来说量化仍然是降低部署成本的关键技术。随着模型规模越来越大量化的重要性只会增加。未来可能会出现更智能的量化方法能够根据输入内容动态调整量化精度在简单输入上用低精度快速推理在复杂输入上用高精度保证质量。这种动态量化策略在理论上很有吸引力但实现起来还有不少工程挑战。我在实际工作中最大的体会是量化不是一次性的任务而是一个需要持续优化的过程。模型在更新数据分布在变化硬件在升级量化策略也需要随之调整。建立一个可复现的量化流水线记录每次量化的配置和结果对于长期维护非常重要。