
做推理优化这几年我最常被问的一句话就是模型能不能再小一点跑得再快一点量化通常是第一板斧——把FP32换成INT8显存立刻砍到四分之一推理吞吐也能上一个台阶。但量化从来不是简单的“降精度”它牵扯到数值表示、校准集选择、算子支持、训练与推理的配合一个环节没搞清楚模型精度可能当场崩掉。这篇文章就围绕“量化原理与实践”来展开重点讲清楚INT8矩阵乘到底怎么算、校准是怎么回事、QAT量化感知训练和PTQ训练后量化怎么选以及大模型LLM量化为什么比传统CNN量化更麻烦。目标读者是正在做模型部署、推理加速、端侧落地的工程师也适合想真正搞懂量化底层逻辑的算法同学。你会拿到可以直接用的方案选型思路也会看到我踩过的一些坑。1. 量化原理与数值表示1.1 为什么量化能把模型“变小变快”要理解量化先要理解推理时模型的瓶颈在哪。早期的CNN模型如ResNet50算力开销和访存开销都很高部署时显存和算力同时紧张。到了Transformer和LLM时代情况变成了两层权重动不动几十上百GB生成每个token都要把全部权重读一遍访存带宽成了绝对的瓶颈。量化的收益正好对应这两层。第一层是存储收益INT8每个权重只占1字节相比FP32的4字节直接减到四分之一模型从4GB变成1GB显存压力瞬间小很多。第二层是计算收益现代GPU和AI加速芯片都有专门的INT8计算单元比如NVIDIA Tensor Core的INT8算力通常是FP32的数倍到数十倍只要算子真的走量化内核计算吞吐也能明显提升。打个比方把FP32模型比作一卡车整箱矿泉水INT8就是把每个箱子换成小瓶装同样是跑一趟运的“瓶数”多得多。量化做的就是在尽量不影响“口感”的前提下把包装和物流换得更高效。1.2 FP16、BF16、INT8到底差在哪很多刚接触量化的同学会混淆“精度”和“表示能力”。FP16、BF16、INT8都是低精度格式但各自的数值分布方式完全不同。格式位数指数位/尾数位动态范围精度特征主要用途FP32328/23约1e-38到3e38高精度通用计算训练基线、高精度推理FP16165/10约6e-5到65504尾数精度尚可但接近65504会溢出混合精度训练、部分推理BF16168/7和FP32一致动态范围大但尾数精度很低大模型训练、对精度不敏感的推理INT88无指数位由scale缩放决定整数均匀分布范围由缩放因子控制量化推理、端侧部署FP16的问题在于动态范围窄。训练大模型时梯度动辄上万很容易就超过65504变成inf所以现在训练更流行用BF16它保留了和FP32一样的指数范围只是牺牲尾数精度。推理时FP16是安全的因为权重和激活值通常都在合理范围内。INT8完全不同它没有指数位本质上只表示整数范围是-128到127有符号或者0到255无符号。要把浮点数塞进INT8必须引入缩放因子和零点偏移也就是“每个浮点数值都能通过一个线性映射转成整数”。这个映射会引入量化误差误差大小取决于数值分布和我们的校准策略。很多人问“INT8和BF16哪个好”答案取决于场景BF16保留FP32动态范围、适合训练和部分容忍度高的推理INT8能真正压内存和调动整数算力适合对延迟和显存敏感的生产环境。两者不是替代关系。1.3 INT8矩阵乘的底层逻辑INT8量化矩阵乘的核心公式很简单r ≈ S * (q - Z)其中r是原始浮点值q是量化后的整数S是缩放因子scaleZ是零点zero point。如果用对称量化Z固定为0公式进一步简化为r ≈ S * q。矩阵乘Y X·W量化后变成先分别对输入X和权重W量化得到Xq和Wq然后做整数矩阵乘乘积结果累加到INT32最后再乘上两个缩放因子的乘积还原到浮点。为什么要累加到INT32而不是INT8因为两个INT8相乘的最大值是127×12716129而一维向量可能有上千个元素累加结果轻松超过32767更别说INT8的-128到127范围了。硬件在设计上普遍采用INT8乘法INT32累加器这已经是行业标准做法。在TensorRT、OpenVINO、TFLite里都是这个套路。PyTorch里的表示方式类似下面这样伪代码# 量化浮点 - INT8 scale (r_max - r_min) / 255 zero_point round(-r_min / scale) q torch.round(r / scale zero_point).clamp(0, 255) # 反量化INT8 - 浮点 r_dequant (q - zero_point) * scale这里有一个很容易踩的坑零点的计算。如果用不对称量化zero_point往往不是0反量化时漏掉它输出分布就会整体偏移表现为精度诡异下降但又不完全崩溃。排查时先检查反量化公式是否和量化严格互逆。另外量化的粒度也很关键。per-tensor是整层共用一个scale简单但对分布不均匀的层误差大per-channel是每个输出通道一个scale能显著减小权重量化误差更细的per-group比如每128个元素一组常用于大模型量化平衡精度和存储开销。粒度越细需要存储的scale越多但精度越好选择上要在存储成本和精度之间找平衡。2. 校准给量化找一个靠谱的缩放因子2.1 校准集怎么选样本、数量与预处理很多人以为量化就是把模型参数直接丢进公式算一下范围这是最大的误区。确定scale的过程称为校准calibration需要跑推理统计每一层激活值的分布。这里的“校准”和硬件设备里的传感器校准不是一回事但核心思想一致你得先用一批能代表真实情况的数据把“尺度”摸清楚。校准集的选择直接决定量化误差的上限。最理想的是从训练集或验证集中抽一批样本模拟真实业务的数据分布。比如你做的是人脸检测校准集不能拿一堆风景图去跑你做的是中文文本生成校准集不能只放英文语料。数据分布一旦偏移量化后遇到真实数据某些激活值会超出校准范围被截断或放大精度直接崩盘。样本数量方面并没有一个“越多越好”的结论。我实测下来分类模型用500到2000张图检测模型用500到1000张图已经能覆盖绝大多数激活分布。数量再多统计分布不会明显变化反而浪费校准时间数量太少少数离群样本会主导max值导致scale过大量化精度变差。一个关键细节校准时的数据预处理必须和推理时完全一致——归一化参数、缩放尺寸、通道顺序都不能变。很多人校准时用原始图片部署时用预处理后的数据两者分布对不上量化效果自然对不上。2.2 四种校准方法对比Max、Percentile、KL与MSE确定每层的scale本质是选择“量化后能最大程度保真原始分布”的映射方式。常见方法有四种方法原理优点缺点适用场景MinMax直接取绝对值最大值作为范围实现简单、速度快对离群点极其敏感一个异常值可能毁掉整层初始快速验证Percentile取一定分位数如99.99%作为边界抗离群点范围更紧凑分位数需要调参分布存在长尾的模型KL散度最小化量化前后分布的KL散度信息损失最小NVIDIA常用计算量较大需要做分布直方图精度要求高的CV模型MSE最小化量化前后数值的均方误差目标与精度损失直接相关需要搜索阈值耗时长对数值误差敏感的任务MinMax最容易理解直接看整层数值覆盖范围。问题是神经网络里总有那么几个特别大的值比如归一化前的某个中间特征突然冲到几十MinMax会为了照顾这个离群点把scale拉大导致绝大多数正常数值被压缩到INT8的几个离散档位上精度丢失严重。Percentile的思路是“容忍一定比例的极端值被截断”。99.99%分位数意味着放弃万分之一的极端值但换来了对99.99%正常数值更精细的表示。对于有长尾分布的层这个方法非常实用。KL散度校准是TensorRT早期默认使用的方案核心思想是“量化后的分布尽可能与原始分布接近”。它把原始激活值统计成直方图然后尝试不同的截断边界选择KL散度最小的那个。从信息论角度看这是最稳妥的校准方式但需要先把数值分布做成直方图再迭代搜索耗时会比MinMax高一截。MSE方法更直接枚举不同的量化范围计算量化前后数值的平均平方误差选误差最小的方案。这种方法在低比特量化中表现稳定因为它把“量化误差”本身作为优化目标而不是间接地衡量分布差异。代价是耗时更长一般需要离线校准不适合运行时快速校准。我的建议是先用MinMax快速跑一遍看精度损失是否在可接受范围如果损失超过0.5%到1%具体取决于任务再切换到Percentile或KL通常能把损失拉回来。2.3 量化后如何评估“到底掉了多少精度”量化完不是看几个loss数值拍板完事需要建立一套评估流程。最直接的指标是验证集上的精度对比比如Top-1 Accuracy、mAP、BLEU等。除此之外还需要观察数值层面的信号。我常用两个手段。第一个是绝对误差分析在层级别对比浮点模型和量化模型的激活输出用余弦相似度Cosine Similarity衡量。如果某一层的余弦相似度掉到0.99以下先检查这一层是不是校准不到位或量化粒度不合适。第二个是端到端的输出扰动分析拿几个典型样例对比浮点模型和量化模型的最终输出分布。如果量化模型的输出方差明显变大大概率是某些敏感层的量化误差被放大了。这里有个耐心活要提醒量化误差是逐层累积的。第2层的微小误差经过第3、4、5层的非线性变换后可能被放大成明显偏差。所以校准结果不好时不要盲目重跑先逐层定位找出是哪些层贡献了大部分误差再针对性调整校准策略或对敏感层采取混合精度保留FP16。3. QAT量化感知训练从“事后补救”到“事前适应”3.1 PTQ和QAT怎么选训练后量化PTQ是把训练好的模型直接转成量化版本不需要训练数据参与迭代成本低、速度快几十行代码就能把模型压缩到INT8。但它的上限也明显量化误差在转换那一刻就注定了校准只是尽量减轻误差无法消除。量化感知训练QAT则是在训练过程中就把量化误差“模拟”进前向计算让模型参数在训练时就能适应量化带来的扰动。QAT的精度上限更高尤其适合低比特量化INT4、INT3或者对精度敏感的任务比如语义分割、关键点检测。维度PTQQAT所需数据量校准集几百张即可需要训练集/验证集量级更大所需算力CPU即可分钟级完成需要GPU训练几小时到几天精度上限高比特8bit通常可控低比特也相对稳实施复杂度低直接转换需要改训练脚本、调学习率适用场景快速上线、模型较大不好重训精度敏感、低比特、模型可重训经验法则INT8部署优先试PTQ掉点严重再上QATINT4或更低比特直接考虑QAT别在PTQ上浪费太多时间。3.2 伪量化节点与直通估计器QAT的核心机制是“伪量化”fake quantization。它在模型前向计算时插入量化-反量化操作数值先被量化成整数再反量化回浮点。这样模型在训练时拿到的已经是模拟量化误差的数值但权重本身仍以FP32存储和更新。伪量化在反向传播时会遇到一个数学问题量化函数是阶梯函数几乎处处导数为0梯度传不过去。业界主流的解决方案是直通估计器STEStraight-Through Estimator也就是在反向传播时把量化节点的梯度近似为“直接通过”假定量化对梯度的影响可以忽略。这样权重更新能正常进行同时前向模拟了误差。这个设计非常巧妙但也有副作用伪量化节点的前向和反向不对称训练后期需要小心学习率否则权重会在量化边界附近震荡。PyTorch中实现伪量化通常是这样from torch.ao.quantization import FakeQuantize from torch.ao.quantization.observer import MovingAverageMinMaxObserver observer MovingAverageMinMaxObserver(dtypetorch.quint8, qschemetorch.per_tensor_affine) fake_quant FakeQuantize(observerobserver, quant_min0, quant_max255)这段代码创建了一个标准伪量化节点前向计算时会把输入映射到0-255范围再映射回来反向传播则由框架自动处理STE逻辑。3.3 QAT的标准执行流程与参数选择QAT的标准流程分为四步。第一步加载预训练模型。这里的预训练模型最好是已经收敛良好的浮点模型不要在随机初始化状态下直接开始QAT否则模型本身还没学到有效特征加上量化误差只会更糟糕。第二步融合可融合的算子最典型的是ConvBN。BN在前向推理时只是个线性变换可以折叠到卷积里折叠后既能减少计算量也能让量化时统计分布更稳定。PyTorch中用torch.ao.quantization.fuse_modules完成这一步。第三步插入伪量化节点然后开始微调。微调阶段的关键参数有三个学习率、训练轮数和batch size。我的经验是学习率降为原训练的十分之一到百分之一训练轮数控制在1到10个epochbatch size不需要太大128或256足够。学习率太大会导致权重在量化边界附近反复跳变太小则来不及适应量化误差。第四步把伪量化节点换成真实量化节点导出为量化模型。以TensorRT部署为例QAT训练完导出带QDQQuantize-Dequantize节点的ONNX模型然后TensorRT再转换成INT8引擎。一定要记住导出时的量化参数和QAT训练时完全一致通道粒度、对称性等配置至少要同步。还有一个容易忽略的点QAT时是否更新BatchNorm的统计量。如果在训练阶段BN继续更新running_mean和running_var可能会对量化后的数值分布产生额外影响。行业里没有绝对统一的答案我习惯在训练初期保持BN更新后期冻结让模型在接近推理的状态下收敛。4. LLM量化专题为什么大模型量化不能照搬CNN经验4.1 LLM量化的三个特殊难点把CNN量化经验直接搬到大模型上通常会碰壁因为LLM有三个明显不同点。第一访存瓶颈远大于计算瓶颈。LLM生成是逐token自回归的每个token生成都需要读取全部权重显存带宽决定生成速度。量化权重能直接换算出更低的带宽需求所以LLM量化的首要目标是压权重的bit数这也解释了为什么W4A16权重4bit、激活16bit在大模型部署里如此流行——它主要省带宽。第二激活中存在显著离群值。研究表明LLM的激活值并不像CNN那样服从近似高斯分布而是少数几个channel的值特别大被称为outlier。如果对激活做per-tensor量化一个离群值就能撑大整个scale其他正常值被压缩得毫无区分度。解决思路有两个要么对权重做per-channel量化要么对激活做per-token量化每个token单独统计scale。第三量化误差会随生成长度累积。LLM是自回归生成前面一个token的误差会影响后续所有token相当于误差异常放大的循环。所以量化评估不能只看静态层的相似度还要看长序列生成时的稳定性。4.2 主流方案速览GPTQ、AWQ与SmoothQuant这几年LLM量化方案层出不穷我这里只提炼几个真正在生产环境里用得多的。GPTQGenerative Pretrained Transformer Quantization是经典的一步式量化方案。它针对每层权重利用Hessian矩阵的信息在量化当前权重的同时调整同层未量化的权重来补偿量化造成的误差。这种方法不需要重新训练在校准集上跑一遍即可支持INT4/INT3精度保持得相当好。缺点是校准对数据分布敏感校准数据一旦偏离真实场景生产环境会出现精度漂移。AWQActivation-aware Weight Quantization从另一个角度切入它观察到不是所有权重对精度同等重要那些对应激活离群值大的权重通道更重要。AWQ通过少量统计得到激活幅度再给重要通道乘上缩放系数在量化前把不同通道的权重拉到更适合量化的分布。AWQ的优势是不依赖反向传播速度快精度与GPTQ相当甚至更稳。SmoothQuant专门解决激活量化难的问题。它的核心技巧是“平滑化”把激活中的离群值转移一部分到权重中通过数学等价变换让激活分布变得平缓从而让激活也能安全量化到INT8。这是W8A8权重8bit、激活8bit方案里绕不开的技术。方案量化比特是否需要训练特点适用场景GPTQW4/W3不需要基于Hessian重建误差离线校准后部署AWQW4不需要保护重要权重通道数据分布敏感时替代GPTQSmoothQuantW8A8不需要平滑激活离群值需要真正INT8计算加速的场景GGUF格式里的Q4_K_M、Q5_K_M等量化档位本质上是结构化量化如分组成块量化的工程化实现。它们把权重分成小块根据不同块的特点选择量化策略兼顾内存占用和精度。实际选型中Q4_K_M在多数模型上表现均衡Q5_K_M精度更高但体积大一些Q8_0几乎是无损但省不了太多内存。如果你在llama.cpp或Ollama里跑本地大模型优先试Q4_K_M内存足够再上Q5_K_M或Q8_0。4.3 KV Cache量化一个容易被忽视的显存救星权重量化解决了权重占用的显存但LLM推理还有一个隐性显存杀手KV Cache。随着生成序列变长KV Cache跟着线性增长长文本对话尤其明显。一条常见的经验8K上下文下KV Cache占用可能比模型权重还多。KV Cache量化就是把缓存的Key和Value张量从FP16降到INT8或FP8。由于KV Cache的数值分布相对稳定量化引入的误差对生成质量影响有限。在vLLM等推理引擎中启用KV Cache量化通常只需配置一个参数比如kv_cache_dtype设为fp8或int8实测能显著提升长序列场景的吞吐。需要提醒的是KV Cache量化要和权重量化分开评估。权重量化影响的是模型的“知识保留”KV Cache量化影响的是“上下文一致性”。如果KV量化后出现重复生成或逻辑断裂检查一下量化粒度是否per-head或per-token多数情况下改成per-head能缓解。5. 实操中的典型问题与排查经验5.1 问题速查表量化部署过程中问题和原因通常集中在几个固定位置上。我把自己在不同框架里踩过的坑整理成了一个速查表。现象可能原因排查方向量化后精度暴跌超过2%校准集分布偏移 / 存在极端离群值更换校准集改用Percentile或KL校准模型输出NaN或Infscale过小导致整数溢出检查scale下限必要时用非对称量化输出整体偏移但不发散反量化漏减zero_point核对量化/反量化公式互逆性性能不升反降算子没有走量化内核用profiler查看算子耗时确认后端支持某些输入样例特别差敏感层量化误差被放大逐层做余弦相似度分析找出敏感层长序列生成逻辑混乱KV Cache量化粒度过粗换成per-head量化或提高KV精度5.2 掉点大多不是量化本身的锅而是校准的锅我反复强调一个观点绝大多数INT8量化失败案例问题不在INT8这种格式而在校准环节。这里分享一个真实经历。有一回做目标检测模型量化MinMax校准后mAP从0.62掉到0.55怎么看都不正常。后来逐层检查激活分布发现某个下采样层在少数样本上出现了特别大的响应幅度是正常数值的20倍以上。MinMax校准把scale拉得极大正常数值压缩到了INT8的个位数区间信息几乎全丢了。换成99.99% Percentile校准后mAP恢复到0.61误差很小。从那以后我习惯在量化前先做一遍“分布体检”统计每一层激活的最大值、分位数和直方图直接肉眼看看数据长什么样。如果发现明显的长尾或离群值聚集提前就能预判量化难度不至于等模型上线了才发现精度不对。另一个常犯错误是校准集与部署环境的前处理不一致。比如训练时代码里对图像做了center crop校准脚本却直接resize最后量化出来的scale根本对不齐。这种问题最难排查因为模型结构没变数据格式变了但肉眼看不出来。建议校准脚本直接复用训练或评估的数据加载逻辑不要另起炉灶。5.3 性能不升反降的排查思路INT8理论算力是FP32的好几倍但实测中经常有人跑出“量化后更慢”的结果。这种情况在CPU上尤其常见。原因很简单很多CPU上没有INT8乘加指令的完整支持或者推理框架没有针对目标CPU做算子优化INT8推理还是通过模拟方式执行的性能自然更差。GPU上出现性能倒挂通常是算子没有映射到Tensor Core。比如某些小算子如LayerNorm、激活函数本身是访存密集型INT8和FP16区别不大加上量化反量化的额外开销反而更慢。解决办法是用profiler逐个算子看耗时把那些“量化了但没有增益”的层留在FP16只对真正计算密集的卷积和矩阵乘做INT8。还有一个容易被忽略的点动态shape。如果你的模型输入尺寸不是固定的某些推理引擎在量化时会对动态维度做保守处理导致算子退回到低效实现。如果业务允许固定输入尺寸如检测模型的输入resize到固定分辨率通常能让量化算子走更优的执行路径。5.4 敏感层分析与混合精度最后的保底手段当常规校准手段都试过精度还是差一点时混合精度是最终兜底方案。核心思想是识别出对量化最敏感的层保留FP16其余层继续用INT8。这个方法相当于用极小的内存代价换取精度回稳。识别敏感层的方法很简单逐层替换成FP16并评估精度变化。以ResNet为例从最后一层开始往前逐层切换每切换一层跑一次验证集。如果某个层从INT8换成FP16后精度明显回升说明该层是敏感层后续保留FP16。这个过程有点笨但非常有效。实际项目中我通常能找出3到5个敏感层全部保留FP16后模型内存只增加5%左右精度却基本无损。我在实际使用中的体会是量化更像一个调配工程每一步选择都服务于同一个目标在压缩倍数和精度损失之间找到可接受的点。没有万能公式但有一条清晰的实践路径先用MinMax校准快速验证可行性再按需升级校准策略INT8优先PTQ低比特转QATLLM从权重W8A8或W4A16起步再评估KV Cache量化最后用混合精度兜底精度问题。按这个顺序走下来绝大多数模型都能在半天内完成量化并交付一个靠谱的方案。最后再分享一个小技巧把量化前的浮点模型和量化后的模型放在同一个评测脚本里每次改动校准策略时同时跑一次对比。坚持记录每轮校准时用的数据量、方法、分位数和精度结果你会发现可复现性比灵光一现的调参重要得多。