资讯动态

PyTorch量化入门:INT8静态量化与QAT实战全解析

发布时间:2026/10/21 3:39:58 来源:尧图企业网站定制
去年我接的一个部署需求遇到一个特别典型的问题模型精度不错但单张推理时间太慢在只允许用 CPU 推理的服务器上根本扛不住并发。一开始我试着优化预处理、减少后处理逻辑、换更轻的骨干网络效果始终有限。后来把模型的 FP32 权重压缩成 INT8单张延迟直接降了将近一半内存占用也砍到原来的四分之一左右这时候我才真正意识到 PyTorch 量化 API 的价值。这篇基础向的量化入门主要围绕 PyTorch 的 Python API 展开目标是把“量化”这个听起来很学术的概念讲成一套能直接落地的工具。我会先从量化的收益和适用场景说起再拆解量化背后的核心原理然后对比 PyTorch 2.x 里 Eager 模式和 FX 图模式这两套 API 的差异接着走一遍静态量化的完整流程最后聊聊精度不够时怎么转 QAT以及我踩过的一些坑。如果你已经在用 PyTorch 训练模型正处于“模型跑通但部署吃力”的阶段这篇文章应该能帮上忙。1. 量化的收益与边界先判断你的模型该不该动很多人一听到量化第一反应是“精度会掉吧”然后就绕着走。其实量化不是玄学它本质上是在做一件事把模型里的浮点数计算换成低比特整数计算从而换来更小的体积、更少的内存占用和更快的推理速度。要不要做量化首先得看清收益和代价。1.1 量化到底省了什么深度学习模型默认用 FP32 表示权重和激活值一个 float 占 4 字节。如果换成 INT8一个数只占 1 字节单单权重这一项体积就能降到原来的四分之一。比如一个 100MB 的 FP32 模型量化后大约 25MB。这对磁盘占用、加载时间、内存带宽都是实打实的压缩。更关键的是推理速度。现在的 CPU 对 INT8 指令做了很多硬件级加速比如常见的 AVX512 和 VNNI 指令集一条指令就能完成多组 INT8 乘加运算。相比之下FP32 的乘加要慢不少。我在实际项目里测下来卷积和全连接结构占比较高的模型量化后 CPU 推理速度往往能提升 2 到 3 倍某些结构简单的模型能接近 4 倍。延迟降下来之后服务器的吞吐量自然就上去了。1.2 什么场景不适合量化量化不是万能的。如果模型主要由逐元素操作、动态形状或者大量自定义算子组成比如强依赖向量检索、复杂控制流的模型量化带来的加速可能很有限甚至因为频繁的反量化开销而变慢。另外模型的精度余量也很关键。如果你的模型在验证集上本来就只有 80% 出头的准确率稍有波动就跌到可用线以下那直接做量化就有点冒险。我自己的判断标准一般是三步先看模型推理是不是 CPU以及有没有硬性延迟要求再看模型里卷积、矩阵乘这类算子的占比高不高最后用一小批校验数据跑一次量化对比精度掉点是否在可接受范围内。只要这三个条件基本满足量化就值得试。反之如果模型本身就很小推理已经很快量化带来的速度收益可能不显著但增加的复杂度却是实打实的。2. 量化核心机制缩放因子、零点与校准很多教程一上来就教你怎么调用 API但遇到精度问题就不知道怎么调。我建议先花十几分钟理解量化背后的数学映射这样后面调参、排查问题才会有方向感。2.1 从 FP32 到 INT8 的映射公式量化本质上是在浮点实数和有限范围的整数之间找一个映射关系。以最常见的线性量化为例假设原始浮点值是 r量化后的整数值是 q那么映射关系可以写成q round(r / S Z)反推回来则是r ≈ S × (q - Z)这里的 S 是一个浮点数表示每个量化步长Z 是零点用来对齐浮点范围里的某个值和整数里的某个值比如要保证浮点 0 在量化后能被精确表示。S 和 Z 怎么来取决于你在校准阶段观察到的浮点数值范围。假设观察到的最小值和最大值分别是 rmin 和 rmax量化范围是 qmin 和 qmax常用的计算方式是S (rmax - rmin) / (qmax - qmin)比如 FP32 数值范围是 -1.0 到 1.0量化到 INT8范围 -128 到 127S 大约就是 (1.0 - (-1.0)) / (127 - (-128)) ≈ 0.0078。原始值 0.5 量化后约等于 round(0.5 / 0.0078 Z)中间会有舍入误差这个误差就是量化误差的根源。2.2 对称量化、非对称量化与 per-channel 选择映射方式有两种常见变体对称量化和非对称量化。对称量化把零点固定为 0浮点范围关于原点对称实现简单但遇到数值整体偏正或偏负的情况比如 ReLU 之后的激活值都是非负的对称量化会浪费一半的整数表示范围。非对称量化允许零点非 0可以充分利用整个 INT8 范围精度表现通常更好代价是实现稍微复杂一些。还有一个维度是统计粒度。per-tensor 意味着整层权重或激活共用一个 S 和 Zper-channel 则是对每个输出通道单独统计。权重分布在不同通道之间可能差异很大尤其在小模型里per-channel 量化带来的精度提升非常明显而计算量增加却很小。我在实践中基本上对权重都选 per-channel激活通常还是 per-tensor因为激活的通道统计在部署时实现成本更高。2.3 校准observer 如何收集浮点范围校准这个步骤是决定量化精度的核心。校准的目的是在量化之前收集一批有代表性的浮点数据估算每一层激活值和权重的真实范围。PyTorch 里负责这项工作的是 observer常见的几种MinMaxObserver直接记录观察到的全局最小值和最大值简单但容易被离群点带偏。MovingAverageMinMaxObserver对每次前向的数据做滑动平均更稳定但需要调 averaging_constant。HistogramObserver统计数值分布直方图可以配合百分位截断适合对付长尾分布。校准用的数据不需要带标签但必须能代表真实推理时的输入分布。我一般从验证集里均匀抽个几百到一千张图类别覆盖尽量均衡跑几遍前向让 observer 把范围估准。这里有个很容易犯的错误校准集不要从训练集里抽。训练集分布和真实场景经常有偏差用训练集校准出的范围一上验证集就会露馅。3. PyTorch 量化 API 的演进与选型从 Eager 到 FXPyTorch 的量化 API 在 2.x 时代有一个明显的变化趋势老的torch.quantization命名空间逐渐向torch.ao.quantization迁移而推荐的使用方式也从手工侵入式的 Eager 模式转向基于图重写的 FX Graph Mode。选错 API 会平白多踩很多坑所以这个选择值得单独说。3.1 为什么老的 Eager 模式这么麻烦Eager 模式是我最早接触 PyTorch 量化时用的方式。它的思路很直接在模型定义里手动插入QuantStub和DeQuantStub指定每一层用的 qconfig然后调用prepare和convert。好处是灵活每一层的类型都可以精细控制坏处是手动工作量大而且模型代码需要为量化改结构。比如一个带残差的结构Eager 模式下你要自己把握量化和反量化插在哪些边上插错了精度损失会莫名放大。我试过一次在某个分类模型里漏掉了残差分支上的 observer结果量化后掉点超过 10%查了很久才发现是哪里没插对。这种问题在 FX 模式下基本能自动避免。3.2 FX Graph Mode 的核心优势FX 模式利用torch.fx对模型做符号化追踪把模型转换成一个图结构然后由框架自动分析哪些算子应该量化、哪里需要插入 fake quant 或量化思想的节点。对使用者来说你不需要改模型主体只需要提供一个example_inputs让 FX 完成追踪剩下的插入工作交给prepare_fx和convert_fx。它还有一个很实用的特性自动跳过不适合量化的算子或者把它们保留为 FP32。像某些对精度异常敏感的层框架可以通过配置让它们走浮点计算而其余部分仍然保持 INT8。这种“混合精度”的粒度比手工 Eager 模式自动得多。当然 FX 也有边界。模型里如果有动态控制流、依赖数据值的循环或者过于复杂的自定义 autograd Functionsymbolic_trace可能会失败或者追踪出来的语义不对。遇到这种情况要么把模型重构到可以被追踪的程度要么退回 Eager 模式处理局部量化。3.3 后端的差异与 qconfig 选择PyTorch 量化的后端配置一句话总结就是CPU 推理和移动端推理的默认量化配置不一样。在 PyTorch 2.x 里我常用x86作为 CPU 后端的 qconfig它对应的是面向服务器级 CPU 的算子实现移动端场景则更多使用qnnpack相关配置。不同后端支持的算子集合和性能特性都有差异跨后端直接迁移 qconfig 可能效果不好。实际使用中我建议直接通过get_default_qconfig拿框架推荐的配置而不是自己从头拼一个。除非你清楚知道默认配置哪里不满足需求否则自定义 qconfig 只会引入不确定性。比如下面这段就是我在静态量化准备阶段用的标准操作from torch.ao.quantization import get_default_qconfig qconfig_x86 get_default_qconfig(x86)拿到这个 qconfig 之后FX 模式会把它传给prepare_fx自动应用到模型里合适的算子上。4. 端到端静态量化实操校准、转换与精度评估这一节我会用一个可运行的简化例子带你走完静态量化的完整流程。为了方便演示我构造一个很小的 CNN 分类模型实际项目中你直接替换成自己的模型即可。4.1 构造模型与 FX 追踪先看模型定义import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, stride2), nn.ReLU(), nn.Conv2d(32, 64, kernel_size3, stride2), nn.ReLU(), nn.AdaptiveAvgPool2d(1), ) self.classifier nn.Linear(64, num_classes) def forward(self, x): x self.features(x) return self.classifier(x.flatten(1))接着用torch.fx.symbolic_trace追踪模型。要注意的是FX 模式要求模型在给定示例输入的情况下可以被静态追踪所以forward里尽量不要写依赖输入数据值的逻辑。对这个例子追踪没问题model SimpleCNN().eval() fx_model torch.fx.symbolic_trace(model, concrete_argsNone)model必须是eval()模式这一点非常关键。如果留在训练模式批量归一化和 dropout 的行为会在校准阶段捣乱导致量化后的统计信息错乱。4.2 prepare_fx 与校准接下来指定 qconfig并调用prepare_fx。这一步的作用是在图里插入 observer 节点但此时权重还是 FP32只是它们会按照量化配置被“模拟”量化。from torch.ao.quantization import get_default_qconfig from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx qconfig get_default_qconfig(x86) prepared_model prepare_fx( fx_model, {: qconfig}, example_inputstorch.randn(1, 3, 32, 32), )然后跑校准数据。校准阶段不要更新梯度直接用no_grad把几百张代表性图片喂进去。我通常写一个简单的循环def calibrate(model, loader): model.eval() with torch.no_grad(): for images, _ in loader: model(images)校准数据多少合适我个人的经验是类别越少数据可以越少但一般不要少于 200 张。更关键的是覆盖度尽量把真实场景里可能出现的高频特征、光照变化、不同角度都包含了否则 observer 统计出来的范围会偏。4.3 convert 得到真正的 INT8 模型校准完成后调用convert_fx框架会把 observer 替换成实际的量化、反量化节点并按训练好的缩放因子真正把权重转成 INT8 表示。这一步之后模型里的权重很多就不再是 FP32 了直接用来做 CPU 推理quantized_model convert_fx(prepared_model)这里有个很容易被忽略的细节convert_fx之后模型的输入输出接口通常仍然期待 FP32 张量框架会在模型入口自动量化、出口自动反量化。所以你不用在调用侧额外做类型转换直接quantized_model(images)就行。4.4 精度评估量化前后对比量化做得好不好最终看的是精度对比。我在每个量化项目里都会维护一个统一评估函数量化前后用完全相同的数据流跑一遍def evaluate(model, loader): model.eval() correct, total 0, 0 with torch.no_grad(): for images, labels in loader: preds model(images).argmax(dim1) correct (preds labels).sum().item() total labels.numel() return correct / total在我手头一个几十类的图像分类任务上量化前准确率约 92%静态量化后约 90.5%掉了 1.5 个百分点这个结果对 CPU 部署完全可接受。如果你发现掉点超过 3 个点先别急着优化模型回头检查两件事校准数据是否有代表性以及有没有把某些精度敏感层强制保留成 FP32。5. 量化感知训练QAT适用场景与关键细节静态量化很多时候能直接过关但总有一些模型对量化误差特别敏感常见于目标检测、分割这类对回归输出和边界信息要求高的任务。这时候就要上量化感知训练也就是 QAT。5.1 QAT 的基本思路QAT 的核心思想很简单在训练或微调过程中前向计算时就模拟量化的舍入误差让模型权重“提前适应”量化误差。PyTorch 里有专门的 fake quant 算子它们会前向做一次量化再反量化回浮点计算的梯度则近似认为反向传播是可导的这样模型就能在训练中学会补偿量化带来的损失。从实现上看QAT 的 API 和静态量化非常像只是把prepare_fx换成prepare_qat_fxfrom torch.ao.quantization.quantize_fx import prepare_qat_fx qat_model prepare_qat_fx(fx_model, {: qconfig}, example_inputstorch.randn(1, 3, 32, 32))这个阶段模型处于训练模式可以正常跑优化器更新参数。注意qconfig要相应地选择带 fake quant 的配置而不是纯推理用的静态配置好在默认配置下prepare_qat_fx会自动处理好。5.2 QAT 的训练策略不是每个项目都适合从零开始做 QAT绝大多数情况是在已有模型权重基础上做几轮微调。我通常这样设置加载训练好的 FP32 权重用较小的初始学习率大概是原训练学习率的十分之一到百分之一跑几个 epoch 就够。更稳的做法是先冻结 backbones只微调靠近输出的分类层让模型先适应量化的统计分布后面几个 epoch 再解冻全部层小幅更新。整个过程要把验证精度盯紧一旦发现验证集下降明显就回退到上一个 checkpoint。因为 QAT 本质上还是训练流程网络结构、数据增强、优化器等配置都会影响最终结果所以每家的参数都得自己试。训练结束后照常调用convert_fx把 fake quant 节点替换成真正的量化节点。这里一定要记住convert_fx前必须切到eval()模式否则 batch norm 等层的统计更新还会继续产出的推理模型状态不稳定。5.3 什么时候该上 QAT我给自己的判断标准很简单静态量化掉点在 2 个点以内不加戏掉点在 3 到 5 个点之间先尝试调整校准数据和 observer 配置再决定是否上 QAT掉点超过 5 个点且校准确认无误直接上 QAT 比反复调参更省时间。QAT 的代价是训练时间和资源。如果你只有一两个 GPU一个模型微调几轮也要几个小时。不过从最终部署效果来看这些投入通常能换来比静态量化更稳的精度尤其对检测、分割这类模型来说非常划算。6. 实际项目里的坑与调优心得这部分是我最想写的。网上关于 PyTorch 量化的正规文档其实不少但真正让人原地爆炸的往往是那些文档里没细写的边角问题。我把几个高频坑按排查顺序整理出来。6.1 坑一校准数据的代表性比数量更重要我见过一个项目校准用了五千张训练集图片量化后验证集掉点严重。原因是训练集和真实场景存在明显的分布偏差模型在真实场景里见到的光照、噪声模式和训练集差别很大observer 统计出的范围自然不准。后来我换成验证集里随机抽的五百张图同样的量化配置掉点肉眼可见地变小了。校准数据的第一原则不是“越多越好”而是“越贴近真实推理分布越好”。6.2 坑二MinMax 对离群点过度敏感默认的 MinMaxObserver 实现很直接记下看到的最小值和最大值。如果某一层在少数样本上出现了极端值整个缩放因子会被拉大导致正常数值的量化步长变粗精度就受损。我在训练一个带噪声输入的任务时遇到过类似问题换成 HistogramObserver 并设置百分位截断后问题才缓解。你可以这样配置from torch.ao.quantization.observer import HistogramObserver from torch.ao.quantization import QConfig qconfig_calibrated QConfig( activationHistogramObserver.with_args(bins2048, dtypetorch.quint8, qschemetorch.per_tensor_affine), weighttorch.ao.quantization.default_per_channel_weight_observer, )百分比截断的本质是允许少量极端值被“剪”掉换取绝大多数数值范围更精确。这个思想在统计建模里很常见量化里同样适用。6.3 坑三第一层和最后一层的精度敏感问题输入层接收的原始像素范围通常比较固定输出层直接决定预测结果它们在量化时往往贡献了不成比例的误差。PyTorch 的默认配置里其实已经考虑了这一点很多情况下会保留输入输出附近的 FP32 算子。如果你的模型是自定义结构量化后掉点明显优先检查这几层有没有被量化必要的时候手动把它们排除在量化范围之外。我一般会在 FX 的 qconfig_map 参数里单独给这些层指定None从而让它们保持浮点运算prepared_model prepare_fx( fx_model, {: qconfig, classifier: None}, example_inputstorch.randn(1, 3, 32, 32), )这里的“classifier”是模型子模块名实际使用时换成你自己的层名。这种局部保留 FP32 的混合方案经常能以很小的速度代价换回可观的精度回升。6.4 坑四BatchNorm 与量化模式切换的顺序BatchNorm 在训练模式和eval()模式下行为不同训练模式用当前 batch 的统计量更新 running_mean 和 running_vareval()模式则使用已经累积好的 running 统计量。量化校准和 convert 过程必须在eval()模式下做否则 BatchNorm 参数会被污染量化模型的实际表现和预估差异巨大。更隐蔽的是 QAT 过程。训练阶段需要 BatchNorm 正常工作但转换前必须切eval()并且要在切完模式后再跑一两个校准 batch确保 running 统计量稳定。如果切eval()后直接 convert有些情况下也能用但精度并不总是稳定多跑一遍校准数据没有坏处。6.5 坑五性能验证要分清算法和硬件最后提醒一个容易误判的点量化模型在 CPU 上的加速效果和核心数、指令集、线程数强相关。有时候在笔记本上测发现几乎没有加速换到服务器 CPU 上却快了 2 倍多。原因很简单INT8 指令集和内存带宽在不同硬件上的表现不一样。我的建议是量化模型的性能验证直接用目标部署机器的配置跑并且测延迟时把预热和多次平均都做上只看一次推理时间很容易被冷启动噪声影响。如果哪天量化掉点怎么都压不回来记得回来看一眼校准数据和最后一层这两处往往才是真正的“祸根”。把静态量化流程跑顺再根据精度反馈决定要不要上 QAT这一套组合下来基本能覆盖绝大多数 CPU 部署场景。希望这份从原理到实操的入门梳理能帮你少踩几个我踩过的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑