模型量化实战指南:从原理到PTQ/QAT部署全流程

📅 发布时间:2026/10/19 7:36:17
模型量化实战指南:从原理到PTQ/QAT部署全流程
最近在带新人做模型部署的时候发现很多人对“模型量化”的认知一直停在“把浮点数换成整数速度会变快”这个层面。一追问底层原理和实操细节基本都含糊。自己做这个也好几年了中间被量化后的精度飘移、校准数据选错、某些算子不支持量化这类问题反复折磨过所以想把整个模型量化技巧按我自己真正使用的路径重新梳理一遍。这篇不会从严格的数学定义讲起而是按实际踩坑的顺序来先搞清楚量化到底在解决什么问题再把量化原理里最关键的那几个概念讲明白然后对比 PTQ、QAT、动态量化三条主流路线的适用边界最后用一套完整的图像分类量化流程收尾并重点说清楚量化后精度崩了该怎么排查。适合正在做模型部署、对端侧推理速度和内存占用有硬性要求、或者准备把模型塞进低功耗设备上的开发者参考。1. 从“模型装不下”说起量化到底解决什么问题1.1 一次被迫换方案的部署经历先说一个我印象特别深的项目。团队花了几周训出一个图像分类模型就是很常规的卷积网络在 GPU 上跑推理完全没问题准确率也满足验收标准。结果客户要求把推理挪到一台无风扇的边缘设备上整机内存不到 2GB存储空间也扣得比较死。当时模型权重是 FP32 格式大概三百多兆光把权重加载进来就已经占了相当可观的内存切换成半精度之后设备 CPU 又不支持对应的指令集推理耗时直接从原来的二十多毫秒涨到数秒级。整条链路一下子变得不可用。后来我们做了三件事压缩输入图像尺寸、去掉部分冗余预处理逻辑以及引入 INT8 量化。前两件属于常规优化真正把模型“塞”进去的关键还是量化。权重从 FP32 变成 INT8 之后理论存储占用直接降到四分之一更重要的是推理过程中的矩阵计算可以使用低精度指令内存带宽压力也大幅下降。最终模型从三百多兆降到约九十兆推理时延也回到了可接受范围。1.2 量化、剪枝与蒸馏的分工差异很多教程会把量化、剪枝、蒸馏混在一起讲实际上三者的作用路径完全不同。剪枝把权重矩阵中接近零的冗余连接或者不重要的通道直接去掉得到稀疏模型。但稀疏模型需要在支持稀疏计算的硬件或软件栈上才能发挥性能通用 CPU 上的收益不一定理想。蒸馏用一个大的教师模型去指导一个小学生模型训练模型结构可能完全不一样需要重新设计、重新训练周期较长。量化基本不改变模型结构只是把原本用高位宽浮点数表示的张量数据映射到更低位的离散取值上。由于不涉及重新训练或者只做短周期微调对工程团队来说是最快能看到的压缩与加速手段。我的体会是剪枝和蒸馏偏向“结构级”优化量化则更像是“表示级”优化。三者并不冲突工程上经常组合使用但如果你只有很少的时间量化是性价比最高的一步。2. 先把量化原理吃透四个概念决定你能不能用好它量化听起来高大上本质却像一个朴素的换算问题如何用较少的数字去近似表示原来范围很大的数字。2.1 从连续到离散精度丢失到底丢在哪里FP32 的取值范围很大数值的连续程度也很高。INT8 只有 256 个离散级别范围也有限。要让一个 FP32 的张量变成 INT8本质就是找一个映射关系把每个原始浮点数对应到 256 个整数之一。这个映射必然带来误差。比如某个特征图的值大部分集中在 0 到 0.1 之间但偶尔有一个 100 的大值如果映射区间覆盖 0 到 100那 0 到 0.1 这一段就几乎全被压成同一个整数大量细节直接丢失。如果映射区间只覆盖 0 到 0.1那个大值就会被截断到上限形成异常点。这就是量化误差的两大来源舍入误差和截断误差。舍入误差来自连续值落到离散整数时的“取整”本质上是不可避免的截断误差来自实际数据范围超过量化区间导致部分数值被夹到边界。2.2 尺度因子 scale 与零点 zero_point量化公式通常写成q round(r / scale) zero_point反推回去则是r ≈ scale × (q - zero_point)scale尺度因子表示量化后每个整数单位对应的原始浮点数大小是区间范围除以 256对 INT8 而言的结果。zero_point零点表示原始浮点数值 0 对应的量化整数值。设计它的意义在于让真实浮点数的 0 即使在量化后也能被精确表示这对 padding、BN 层等运算很重要。举个例子某个权重张量的浮点范围是 -1.0 到 1.0scale 就是 2.0 / 255 ≈ 0.00784zero_point 会设置在 128 附近。计算时先除以 scale再四舍五入并加上 zero_point最终得到一个整数张量。这个阶段经常有人忽略一个问题每层张量的 scale 和 zero_point 可能都不一样。量化并不是给整个模型定一个统一映射而是按层或按张量分别计算。因为不同层、不同通道的数据分布差异极大统一映射会对分布较窄的层造成很大浪费。2.3 校准与数据范围最容易被忽略的一环想在推理阶段对激活值做量化必须提前知道激活值的范围。运行模型时用一批具有代表性的输入样本去前向推理统计每一层激活张量的 min 和 max这个过程叫“校准”。校准数据的质量直接决定量化模型的质量。如果校准集只有几十张图或者数据分布与实际部署场景差异很大统计出来的 min/max 就会与真实情况严重偏离。我见过一个内部实验用某个公开数据集校准后量化并在另一批真实场景图片上评估准确率直接掉到基线的一半后来换成从真实业务数据里面随机抽了 500 张重新校准精度又回到了接近基线水平。所以校准不是“随便找几十张图跑一下”就完事而是要尽量贴近线上真实数据分布且覆盖典型阴影、强光、低对比度等边界场景。数量上通常 200 到 1000 张即可太多反而慢太少又不稳。2.4 对称量化和非对称量化什么场景用哪个根据映射区间是否对称量化又分为对称量化和非对称量化。也就是如何定义尺度和零点对称量化令 min -max 或取其对称区间zero_point 固定为 0通常可以不做偏移计算硬件实现简单。非对称量化直接覆盖 [min, max]zero_point 不一定为 0需要引入偏移项略微增加计算复杂度和存储占用但表达能力更强。从实际项目经验看卷积层权重一般比较接近零均值这样的分布用对称量化能减少计算复杂度而激活值往往经过 ReLU 之后是非负分布或者分布有偏移用非对称量化会更合适。底层硬件或某些专用加速器对对称量化支持更好选型时还得结合目标平台的指令集来权衡。为了方便新人对照我把两个方式的核心差异整理成一张表维度对称量化非对称量化零点固定为0可为任意整数区间范围对称区间完整覆盖[min, max]表达能力相对受限更强软硬件实现实现简单运算少需处理偏移计算稍复杂适用场景权重分布接近零中心激活值分布明显偏移时3. 动手前想清楚PTQ、QAT 与动态量化怎么选量化不止一种做法不同做法的精度、成本和改造难度差别很大。真正动手之前先把路线选对能少走很多弯路。3.1 三条路线的适用边界后训练量化PTQPTQ 的思路最简单把已经训练好的 FP32 模型拿来用校准数据跑一遍统计每一层的量化参数然后直接转换。整个过程不需要重新训练模型甚至不需要标注数据只需要一批能代表线上分布的样本。PTQ 的优点是与现有模型训练流程解耦做完就是完事缺点是对激活值分布很敏感的模型直接量化精度会掉得很明显。小型分类模型一般不敏感但目标检测、语义分割这类对数值分布更敏感的模型量化掉点就很常见。量化感知训练QATQAT 是在训练阶段就把“量化误差”模拟进去。做法是在前向计算时插入伪量化节点让浮点数据先经历“量化再反量化”的过程使模型在前向传播时已经感受到了量化误差然后通过反向传播调整权重参数。微调几个周期之后模型对量化误差的鲁棒性就会强很多。QAT 的优点是在低比特、复杂任务上精度保持明显更好缺点是需要训练数据和训练流程周期变长操作比 PTQ 复杂不少还要小心伪量化节点的加入位置。动态量化动态量化与前面两种不太一样它只对权重做离线量化激活值仍然以浮点方式计算但推理过程中会在运行时动态计算激活值的量化范围。由于激活没有真正变成 INT8内存在某些场景下能省但计算加速效果不如全量化理想。不过动态量化对很多神经网络模型非常友好尤其是那些激活分布变化大、难以用静态校准集准确覆盖的模型。它不需要准备校准集实施成本低在文本类或序列类模型上经常是首选。3.2 用一张表看明白选型依据维度PTQQAT动态量化是否需要训练数据不需要只要有校准集需要不需要需要修改训练流程否是否量化范围权重激活权重激活仅权重精度保持能力中等好良好实施成本低高低常见适用场景分类任务快速压缩检测、分割、低比特量化Transformer 类模型、激活分布不稳定的模型3.3 具体判断思路我自己的选择逻辑通常是先用 PTQ 快速做一个量化模型在验证集上评估精度变化。如果精度下降小于 1 个百分点直接采用。如果下降超过可接受范围改用 QAT保留原数据集做几个周期的量化感知微调。如果任务本身对推理时延要求不高、但内存受限而且模型激活分布非常不稳定就退到动态量化先保证精度能接受。很多团队默认跳过 PTQ直接上 QAT其实是把简单问题复杂化。PTQ 出问题的概率并没有想象中高尤其是主干网络经过充分训练、BN 层没有失真的情况下PTQ 的结果往往已经足够。4. 完整落地对一个分类模型做量化从准备到导出下面用一个通用的图像分类模型来演示完整量化流程。我尽量用当前主流框架中比较通用的接口来写具体 API 名称在版本演进中可能有调整但核心流程和下面的顺序是一致的。4.1 先准备一个能跑的基线先把模型结构定义好并保存一份训练好的权重。以一个小型卷积模型为例import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 16, 3, padding1) self.bn1 nn.BatchNorm2d(16) self.conv2 nn.Conv2d(16, 32, 3, padding1) self.bn2 nn.BatchNorm2d(32) self.pool nn.MaxPool2d(2) self.fc nn.Linear(32 * 7 * 7, 10) def forward(self, x): x torch.relu(self.bn1(self.conv1(x))) x self.pool(x) x torch.relu(self.bn2(self.conv2(x))) x self.pool(x) x x.flatten(1) x self.fc(x) return x训练部分不做展开按常规流程训练到验证集准确率稳定即可。需要特别留意的是基线模型不要随随便便用未收敛的权重去量化。量化相当于对原模型的“搬运”如果基线本身就还有问题搬运之后问题会被放大。量化之前先在算子层面做一次融合也就是把Conv BN ReLU这类相邻算子合并成一个算子model.eval() torch.quantization.fuse_modules( model, [[conv1, bn1], [conv2, bn2]], inplaceTrue, )融合的目的是减少量化边界处的累计误差同时删除一部分冗余中间层。这一步在实际项目中比较关键不融合的模型量化后精度通常会更差因为每个算子单独量化和反量化会引入额外的量化误差。4.2 后训练量化 PTQ校准代码的完整套路首先给模型挂载量化配置并准备 QuantStub 和 DeQuantStub把量化范围限制在算子内部from torch.quantization import QuantStub, DeQuantStub class QuantableModel(nn.Module): def __init__(self, backbone): super().__init__() self.quant QuantStub() self.backbone backbone self.dequant DeQuantStub() def forward(self, x): x self.quant(x) x self.backbone(x) x self.dequant(x) return x定义完模型后执行prepare到calibrate再到convert的全流程model_with_quant QuantableModel(model) model_with_quant.qconfig torch.quantization.get_default_qconfig(fbgemm) model_with_quant.eval() model_prepared torch.quantization.prepare(model_with_quant) with torch.no_grad(): for images, _ in calibration_loader: model_prepared(images) model_int8 torch.quantization.convert(model_prepared, inplaceFalse)校准数据量我推荐 200 到 1000 张之间前面已经说过原因。校准时不更新权重也不计算梯度只是让模型跑前向推理。标注信息对 PTQ 来说没有用处所以用_直接忽略。get_default_qconfig里面包含了对观察器和量化方式的选择比如fbgemm适用于 x86 平台qnnpack适用于移动端平台。不需要自己到处设置 scale 的初始化方式框架会基于校准数据自动完成。4.3 量化感知训练 QAT精度不够时的升级方案如果 PTQ 精度掉了太多就把同样的模型切换到 QAT 模式。整个流程就是在普通训练脚本里加入量化感知训练特有的部分model_with_quant.train() model_with_quant.qconfig torch.quantization.get_default_qconfig(fbgemm) torch.quantization.fuse_modules( model_with_quant.backbone, [[conv1, bn1], [conv2, bn2]], inplaceTrue, ) model_qat torch.quantization.prepare_qat(model_with_quant, inplaceFalse) optimizer torch.optim.Adam(model_qat.parameters(), lr1e-4) criterion nn.CrossEntropyLoss() for epoch in range(20): for images, labels in train_loader: output model_qat(images) loss criterion(output, labels) optimizer.zero_grad() loss.backward() optimizer.step()训练结束后照样需要convert得到真正的 INT8 模型model_qat.eval() model_int8 torch.quantization.convert(model_qat, inplaceFalse)有一个容易忽略的点QAT 开始前不要直接把初始学习率降得太低。伪量化节点的梯度变化比较温和学习率太低会导致模型在量化误差的影响下无法快速恢复到原有精度。我一般用原训练学习率的五分之一到十分之一并观察前几个 epoch 的 loss 是否明显比纯浮点训练更抖。4.4 导出与推理量化模型最终的保存和加载量化模型在convert之后就已经是 INT8 权重结构不再需要重新计算浮点权重。保存方式有两种第一种是保存状态字典后面恢复模型结构后再加载。第二种是把整个量化后的模型序列化存成pt文件部署时直接torch.load加载。torch.save(model_int8.state_dict(), model_int8_weights.pt) torch.save(model_int8, model_int8_full.pt)这里要提醒一句保存整个量化模型时必须保证目标环境与量化时使用的算子版本一致。如果量化用的框架版本和部署环境版本差太多加载时可能出现算子不匹配或观察器结构不一致的问题导致推理结果错误。最稳妥的做法是先用保存的模型在部署环境中跑一遍小批量验证数据确认输出和量化前的 FP32 模型在误差范围内一致再接入线上服务。5. 量化后精度崩了怎么办我的排查顺序与修复经验量化精度这东西就像测试环境的 bug平时看不见一上真实业务数据就冒出来。不少团队第一次量化都会遇到“准确率掉了五六个点”的尴尬局面。下面按我习惯的排查顺序展开。5.1 先分清是全局下降还是个别样本崩坏量化后产生精度下降第一步不是直接改参数而是把验证集上的错误样本逐条拉出来看。如果精度是平均掉了几个点但错误样本看起来并没有规律通常说明是全局性的量化误差累积。如果错误样本集中在某几类、亮度极高或极低的图片、长文本样本等大概率是校准数据没有覆盖到这类分布或者某几层对异常值特别敏感。我在某次量化一个检测模型时发现所有误检都发生在夜间场景。后来才知道校准集是白天数据夜间图像亮度分布整体偏暗激活值范围和校准阶段统计出来的范围差异很大。把夜间图片补充到校准集里后误检率明显回落。这提醒我一个基本原则校准集必须和部署场景“同源”。千万不要偷懒刮一段原始训练集就用来校准即使总数太少也要优先从真实业务日志里随机抽取。5.2 校准数据分布的排查和百分位校准如果校准数据和部署数据同源精度还是下降可以考虑调整校准策略本身。默认的观察器会统计所有样本上的激活值 min/max再用这个区间做量化。如果数据里存在少量极端值比如某个点的激活值突然冲到 500区间被拉得很宽中间大部分数据反而只用到很少的量化级别精度就会受到明显影响。此时可以采用百分位校准把最大最小值换成第 99.9 或 99.99 百分位把极端值截掉。大多数框架都能自定义观察器例如通过设置percentage参数实现from torch.quantization import MinMaxObserver observer MinMaxObserver(dtypetorch.quint8, qschemetorch.per_tensor_affine)如果框架不支持直接做百分位校准可以自己离线统计激活分布找出合理的截断阈值再手动设置 scale 和 zero_point。这里有一个很好的类比量化就像用一张固定大小的集装箱去装货物。如果你统计范围时把几个特别大的畸形货物也算进来集装箱容积会被浪费普通货物只能受“挤压”。百分位校准就是把那 0.1% 的畸形货物单独放弃保证剩下的 99.9% 有更充裕的空间。5.3 敏感性分析逐层量化与选择性量化如果校准策略调整后依然不达标我建议做逐层敏感性分析。做法简单但很费时间逐层把该层设为量化状态其余层保持浮点在验证集上评估精度从而找出“最敏感的层”。实际操作中我会按以下顺序进行先用全图量化记录每个子模块的权重范围和激活范围找出数值分布异常宽的层。再把疑似敏感的那几层还原成 FP32其他层保留 INT8观察精度恢复情况。如果确认是某几层导致崩坏就对这些层单独设回浮点或者改用更高位宽的量化。这种“选择性量化”虽然会让整个模型的压缩比略有下降但在精度要求很苛刻、且模型整体比较复杂时往往是可接受的折中方案。还有几个比较常见的坑要注意BN 层没有融合或融合顺序不对量化后因 BN 参数更新反而扩大了数值范围。校准批次太小尤其是包含 BN 层的模型校准集少于 100 张时 BN 统计量不稳定。训练和部署时的图像预处理不一致例如训练用灰度均值归一化部署时却换了别的流程激活范围全变了。这些坑几乎都在“数据流”上出问题而不是量化算法本身的问题。所以排查时先把数据链路排掉再看算法层。6. 一些真实使用中的心得和补充技巧最后分享几个从实际项目里沉淀下来的经验技巧。6.1 量化后要测真实推理速度不要只看理论压缩比很多人量化完只看模型体积从 300MB 变成 90MB就觉得任务完成。实际上部署时真正的瓶颈可能是内存带宽、算子调度和硬件对 INT8 的支持程度。我见过一个模型量化后体积缩小到四分之一但实际推理速度只快了 10%因为底层算子库对某些小矩阵的 INT8 支持并不好反而转成了浮点计算。所以流程上应该在量化完成后立刻在目标设备上做一个端到端的基准测试分别记录 FP32、动态量化、全量化三者的推理时延、内存占用和功耗。用数据说话而不是凭感觉判断。6.2 结合蒸馏做“先瘦身后量化”效果更好量化本身不改变模型结构如果原始模型太大可以先做蒸馏得到一个小模型再对小模型量化。小模型和量化叠加之后的体积压缩非常可观精度也比直接对原模型暴力剪枝再量化更稳定。具体做法是用大模型作为教师模型训练一个适合部署的小模型。对小模型执行 PTQ记录量化后精度。如果精度不足改用 QAT 微调。这和单纯做量化相比多一次蒸馏训练项目周期会长一些但换来的是端侧体积和时延的大幅改善。资源允许的话非常值得。6.3 不建议量化的几个信号不是所有模型都适合量化。我自己遇到过的明显不适合量化的场景包括模型内部有大量对数值精度非常敏感的分支判断逻辑比如某些连续判断阈值。激活值的动态范围极端不稳定同一个 Batch 里差异能达到几个数量级。某些自定义算子底层实现没有经过 INT8 适配量化之后该算子被降级回浮点计算整体收益很低。如果碰到这些信号不要硬上量化可以考虑改模型结构、降低输入分辨率或者换一个更适配的部署框架。6.4 每次上手前给自己列一个清单考虑到量化涉及的点比较杂我一般在上手前给自己列一个检查清单这里也分享给大家基线 FP32 模型是否已经收敛且在验证集上稳定校准数据是否从真实部署数据中采样是否包含边界场景算子层面的 ConvBNReLU 融合是否已做量化配置是否匹配目标硬件平台校准后的 min/max 是否存在明显异常值量化后再部署环境的算子版本是否与量化时一致是否在真实设备上评估过端到端推理性能按这个清单走基本可以绕开大部分低级错误。模型量化这条路入门容易做深了全是细节。每次看到有人一上来就追求“能不能保住全部精度”我都想说量化的目标从来不是在真空中维持精度而是在给定硬件约束下让模型真正跑起来。只要数据链路正确、校准集靠谱、路线选得合理INT8 量化在绝大多数场景下都能交出不错的答案。最后补充一个小经验量化模型上线之后最好在线上持续监控一段时间的输入分布变化。真实世界的数据分布是会漂移的半年前校准出来的 scale 可能半年后就不适配了。定期重新校准也是模型迭代的一部分。