第一次跑通TensorRT量化的时候我的感受是又喜又惑。喜的是同一个模型从FP32切到INT8延迟直接砍了一半还多惑的是同样的校准流程换到另一个模型上精度掉得完全不能用。后来我才明白量化不是把精度模式一改就行PTQ训练后量化和QAT量化感知训练这两条路线各自的适用前提、操作细节和坑都完全不一样。这篇文章就按我自己的实战顺序来写先讲PTQ怎么快速上手再讲QAT怎么把量化误差压回去中间会穿插实测数据和踩坑记录适合正在做模型部署、被推理延迟困住的朋友参考。1. 为什么我劝你先想清楚量化再动手1.1 推理慢的根源不是算力不够是带宽卡脖子很多人一上来就盯着量化两个字觉得这是某种神奇的加速魔法。但先说一个容易被忽略的事实绝大多数推理瓶颈不在计算单元而在数据搬运。GPU的核心算力这些年涨得非常猛但显存带宽的增长幅度远没跟上。一个卷积层要做的事无非是把数据从显存搬到计算单元算完再搬回去。当搬运数据的速度成了天花板你的GPU算力再强也发挥不出来。量化正好打在带宽这个痛点上。FP32一个数占4字节FP16占2字节INT8占1字节。同样一条数据通道单位时间内能搬进计算单元的INT8数据量是FP32的4倍。这就是为什么你的TensorRT引擎从FP32切到INT8之后哪怕计算方式没变速度也能翻倍——很多时候不是算得快了而是同样的时间里搬运了更多有效数据。这个道理也想清楚了后面遇到量化后速度没提升的情况就不会慌。先检查是不是内存带宽真的饱和再检查是不是引擎里某些层根本没走INT8。我见过不少所谓的量化失败打开profiler一看模型里一大半算子还停在FP32速度自然没有奇迹。1.2 三种精度模式到底差在哪FP32、FP16、INT8三者不只是精度高低的区别它们的数值表示逻辑完全不同这一点在定位精度问题时至关重要。FP32是32位浮点动态范围大训练和常规推理都用它。FP16是16位浮点指数位和尾数位都缩水了动态范围变小数值上限大概是65504超过就容易变成无穷大。FP32和FP16本质都是浮点只是刻度粗细不同。INT8就不一样了它是个定点整数没有指数位只能表示256个离散的整数刻度要想表达小数值必须靠一个scale因子去缩放。这个scale因子就是量化的核心。以对称量化为例子# 对称量化概念示意 scale max(abs(x)) / 127 # 把动态范围压到 [-127, 127] x_int clamp(round(x / scale), -127, 127) # 量化到整数 x_dequant x_int * scale # 反量化回浮点非对称量化还会多一个zero_point来修正偏置公式稍微复杂一丁点但思路是一样的用一个缩放因子把一个连续大范围的浮点值塞进256个离散整数。这个压缩过程天生就有信息损失——离群点会被截断中间的小数会被舍入。模型里的激活值分布越不均匀、越存在极端离群点INT8量化后损失就越大。这也是为什么同一个量化流程在不同模型上效果天差地别有的网络激活分布很规矩量化后几乎不掉点有的网络个别层动态范围巨大一截断就崩掉。理解到这一层后续选PTQ还是QAT心里就有底了。2. PTQ实战校准数据与校准器选型这两件大事2.1 校准数据怎么选才不坑PTQ的思路很简单模型已经训练好了我不动权重只通过看一批数据确定每一层激活值的动态范围然后算出scale把FP32权重和激活都压成INT8。这里的关键词是看一批数据这一批数据就叫做校准集。校准集的选取是PTQ最容易翻车的地方。我的原则是三条代表性、多样性、适量性。代表性是指数据要覆盖真实推理时会遇到的分布你要是部署的是一个街景检测模型校准集却拿了一堆室内图像那卡出来的阈值必然偏移。多样性要覆盖不同光照、不同类别、不同难度的样本。适量性则要把握节奏我用的经验是500到1000张左右太多反而可能把极端分布的平均值拉偏太少又统计不到真实分布的全貌。另外有一个细节很多人忽略校准数据尽量不要和训练集完全重合。用训练集做校准模型在那些样本上是见过世面的激活分布会偏向乐观量化后一上真实场景就露馅。我习惯从验证集里随机抽抽完看一眼类别分布确认没有哪一类占比异常高。抽样之后还要注意预处理的一致性——归一化的mean/std、通道顺序、缩放尺寸必须和训练时完全一样否则你校准得到的分布根本不对。2.2 校准器选型Entropy和MinMax的区别确定校准集之后下一步是选校准器。TensorRT里常见的INT8校准器有MinMax、Entropy也叫基于KL散度的校准、Legacy和Percentile。文档上看着只是不同算法实际差别非常大。MinMax校准器最直白拿所有校准样本里每一层激活值的绝对最大值作为量化范围。优点是简单、无参数、速度快缺点是它对离群点毫无抵抗力。只要校准数据里有几个极端值动态范围就被拉到很大真正的主体分布反而被压缩到几个量化刻度上精度损失严重。Entropy校准器的思路是找一个阈值把分布两侧的尾部截掉不用使得截断前后信息的损失最小。它本质上在做一件事衡量用INT8去近似FP32时哪种量化范围能让KL散度最小。这种校准器对离群点很友好也是分类网络里我默认的选择。Legacy是旧版TensorRT的熵校准新项目我一般不推荐只在复现老结果时用。还有一个经验要分享分类网络用Entropy通常表现好但某些检测网络反而更适合MinMax。我猜原因是检测任务的背景大面积且数值分布平稳离群点相对少MinMax能保留更完整的空间信息。所以不要迷信默认校准器我一般会跑两个版本每个都测精度哪个稳用哪个。整个选择成本很低但很多人卡在默认第一个校准器效果不行就直接放弃了。2.3 精度掉点后的三层补救手段PTQ之后掉点是常态不掉点是惊喜。我在实际项目里见过掉0.2%的也见过掉8%的。关键是掉点之后别急着上QAT先按顺序做三件事。第一换校准器和校准集。上面说过校准器选型对精度的影响很大Entropy不行换MinMaxMinMax不行换Percentile或者重新采样一批校准集。这个成本最低值得第一个尝试。第二做敏感层分析。把模型按层或按块分组逐组实验保持某组INT8、其余组FP32看精度变化。重复几次就能找出最玻璃心的那几层。常见的敏感层包括第一个卷积层、最后的全连接/检测头、以及一些激活值动态范围特别大的层。第三对这少数敏感层做混合精度处理——让它们保持FP16或FP32其余层还是INT8。这样既能保住大部分加速收益又能把精度拉回来。很多教程把混合精度说得高大上实际操作时就是配置一个层名列表指定哪些层不量化而已。我见过最极端的一个项目模型有200多层最后只把其中5层留在FP16速度和全INT8几乎没差精度却从掉5%救回到了掉0.4%。所以不要一上来就全盘否定PTQ。3. QAT实战把量化误差揉回训练里的完整管线3.1 伪量化节点和直通估计器当PTQ怎么救都救不回来的时候就该QAT上场了。QAT的核心思想不是事后补偿而是把量化的损伤预演给训练过程看让模型权重在微调阶段就适应这种损伤。实现方式是在前向传播里插入伪量化节点。这个节点对输入的浮点值做一次模拟量化先量化成INT8再反量化回浮点。注意这里并不是真的要输出整数而是让网络在前向时看到的数值已经带上了量化才会有的舍入和截断误差。训练的反向传播里round这个操作是不可导的梯度根本传不过去。业界通常用STE直通估计器来处理既然量化这一步没有梯度那就把梯度原样直通过去权重的更新方向基本还是对的。用生活里的话讲伪量化节点就像给网络戴了一副模拟视力模糊的眼镜训练的时候让网络学会在这种模糊下仍能认出东西等正式部署真的戴上模糊眼镜时它早习惯了。这也是QAT比PTQ精度更高的根本原因——PTQ是训练完才戴眼镜模型当然不适应。3.2 微调阶段的配置QAT的具体流程并不复杂但超参配置比普通微调讲究。我通常的做法是先加载一份在FP32下训练得很好的权重插入伪量化节点然后用很小的学习率去微调。这个很小不是客气的说法而是真的要缩到原来学习率的十分之一到百分之一。普通训练用1e-3QAT我就用5e-5到1e-4这个量级微调1到5个epoch就收手。很多人QAT失败败在把QAT当成重新训练来跑学习率太大、跑了十几个epoch、甚至换了数据增广策略。结果模型不但没在FP32精度基础上稳定下来反而过拟合到了训练集部署时一塌糊涂。QAT的目标不是学新知识而是让权重在量化误差的约束下做小幅调整所以它更像校准式的微调而不是再训练。还有一个实操细节如果模型里有BN批归一化层在插入伪量化节点之后最好先用少量数据重新统计一遍BN的均值和方差。因为伪量化改变了前面的数值分布BN跑着跑着可能就不稳定了。我的习惯是先用一个epoch的数据做BN统计预热再正式开始微调这样整个训练曲线会平滑很多。3.3 导出ONNX与TensorRT解析Q/DQ节点QAT训练完成之后把模型导出成ONNX。这时伪量化节点会变成显式的Q量化和DQ反量化算子TensorRT在构建引擎时能直接识别这些节点不需要再提供校准数据。这是TensorRT新版的一个关键能力带Q/DQ的引擎构建方式和传统PTQ的构建方式完全是两条路。# QAT导出与TensorRT构建的概念示意 torch.onnx.export( qat_model, dummy_input, qat_model.onnx, opset_version13, # Q/DQ节点完整支持 input_names[input], output_names[output] ) # 构建引擎时直接解析Q/DQ不需要INT8校准器 # build_engine_with_network(onnx_file, use_qdqTrue)导出之后一定要先做一步ONNX层面的精度验证用同样的输入分别跑PyTorch模型和ONNX模型确认导出过程没有引入额外误差。这一步很多人偷懒跳过结果后面排查半天其实是导出的Q/DQ节点顺序错了。另一个要注意的是模型里的某些算子组合会让Q/DQ解析失效典型的是残差分支上的量化节点没对齐导致TensorRT无法对该分支做融合优化。遇到这种情况可以考虑调整量化算子的插入位置或者手动简化一部分结构。4. PTQ和QAT怎么选一张表外加四句话对比维度PTQ训练后量化QAT量化感知训练额外训练成本无只需跑一次校准需要微调一般1到5个epoch数据需求需要500到1000张代表性校准样本需要少量训练数据可复用原训练集子集精度恢复能力通常掉0.5%到3%严重时更多通常掉0.2%到1%结合敏感层混合精度可控制在0.5%以内工程复杂度低配置校准器即可中需要插入伪量化节点、调整训练管线、导出Q/DQ耗时校准加验证几小时以内需要训练时间视模型规模和算力而定适用场景模型冗余度高、精度余量大、样本不好拿精度敏感、PTQ掉点明显、对部署质量要求高选型这件事说复杂也复杂说简单也简单。我用四句话概括第一先跑PTQ成本最低大部分模型都能直接搞定。第二PTQ掉点严重时先试换校准器和混合精度别急着跳QAT。第三QAT也需要标定好数据分布如果连校准样本都没有代表性QAT也救不回来。第四如果模型要在低延迟高精度的场景上线而PTQ怎么都不行QAT是值得投入的那笔账它比重新蒸馏一个模型要便宜得多。5. 实测数据FP32、FP16、INT8的真实差距5.1 测试平台和模型怎么定下面这些数据都是我自己实际跑出来的平台是某块常见的数据中心GPU单卡TensorRT构建版本是8.x序列输入分辨率统一用224x224x3batch size固定为1连续推理500次取稳态均值。测试模型选了两种有代表性的结构一个图像分类网络结构类似ResNet-50的骨干设计参数量中等一个单阶段检测网络结构类似常用的YOLO系设计。选这两个模型的原因很简单分类网络能反映常见的CNN骨干特征检测网络则更容易暴露量化带来的空间和回归精度问题。两类都验证过结论会更有说服力。测量的精度指标分类网络用Top-1准确率检测网络用mAP。延迟指标用端到端的推理耗时统计单位是毫秒。需要强调一点这里的对比都是TensorRT引擎之间在比而不是和PyTorch原生推理比因为我要看的是量化到底在TensorRT上带来多少增量这样基线才是公平的。5.2 延迟和精度数据直接看结果吧这是我在固定batch1下的实测记录引擎模式分类网络延迟分类网络Top-1检测网络延迟检测网络mAPFP32 TensorRT引擎8.4 ms78.2%22.1 ms0.612FP16 TensorRT引擎4.5 ms78.2%11.6 ms0.612INT8 PTQ TensorRT引擎2.6 ms76.9%7.9 ms0.601INT8 QAT TensorRT引擎2.6 ms77.6%7.9 ms0.608先从延迟看FP32到FP16分类网络快了约46%检测网络快了约47%这是白捡的收益精度几乎不掉。FP16到INT8分类网络又快了约42%检测网络快了约32%相比FP32已经达到约2.2到2.8倍的加速。这个加速比在不同GPU上会有浮动但整体趋势非常清晰。再看精度FP16相比FP32基本无损。INT8的PTQ让分类网络掉了1.3个百分点检测网络mAP掉了0.011。这个幅度对很多任务来说还能接受但对精度要求严格的项目来说已经足以触发QAT了。QAT之后分类网络掉点收窄到0.6个百分点检测网络mAP只掉0.004。这就是QAT的价值——延迟完全一样但精度回来了大半个身位。5.3 动态形状对性能的影响固定batch1测出来的数据很漂亮但如果你的服务是动态batch或者动态分辨率要提前做好性能预期。TensorRT的引擎构建需要指定优化配置优化profile同时提供最小、常用、最大三档形状。线上shape如果每次都变TensorRT有可能选择相对通用的内核就会偏离按固定shape优化的最优路径。我实测过同样的INT8引擎固定224x224输入时2.6ms动态形状下常见输入也是224附近但延迟经常跳出到3.5ms以上。这个波动不是量化造成的而是动态shape的调度开销。所以我的建议很直接能固定shape就固定固定不了就把实际流量分布统计清楚把常用档配置在profile的opt档位否则优化可能完全偏掉。6. 踩坑实录校准、融合、动态形状这些细节6.1 校准时的batch size和BN坑第一坑校准batch size会影响直方图的分布统计。同一个校准集用4张一批和用64张一批去统计可能得到不太一样的量化范围。原因是逐样本的统计在小batch下噪声更大大batch下分布更平滑。我一般习惯把校准batch size设为推理时的常用batch size保持一致。第二坑带BN的模型在PTQ前要先确认BN层已经被吸收或固定。训练模式下BN会按batch统计更新均值和方差推理模式下应该用全局统计量。如果你在导出模型前忘了把模型切到eval模式或者BN层没有被融合到卷积里校准时的激活分布就会偏量出来的scale自然不可信。这也是为什么我每次都先跑一遍校准流程预检用一个小batch在校准器里跑一遍确认输出数值的量级正常。第三坑是输入编码的细节。图像的通道顺序RGB还是BGR、归一化系数、缩放尺寸只要和训练预处理不一致校准分布就全部偏移。这类错误很难从精度数值上直接看出原因因为模型还能跑只是结果微妙地变差。排查这类问题最容易的方式是固定一个输入比较预处理前后张量的数值分布确认和训练时的预期一致。6.2 层融合把量化误差藏哪了TensorRT构建引擎时会大量做层融合比如ConvBNReLU会融合成一个算子。融合本身是好事速度和显存都受益。但融合之后整个子结构内部的中间数值不再有被单独量化的机会原本每层一个scale的量化方式就变成了融合块一个scale。有些层的误差在单独看时很小融合后反而被放大。我自己遇到过一个典型案例某个检测模型的第一个卷积和后面的激活融合后权重里的大数值引起激活分布偏移导致后续层的量化范围整体被压缩。从精度上看是都掉了一点但定位了很久才发现是融合后的数值范围变化。排查这类问题可以先把融合关掉构建一个不融合的引擎做对照组对比两者精度。如果精度差异明显再逐块定位是哪个融合块出了问题。TensorRT本身有这样的调试开关虽然会牺牲性能但用来排查量化误差是值得的。6.3 工作空间、显存和校准缓存构建推理引擎时工作空间这个参数也会影响INT8的性能。工作空间设置得过小TensorRT会放弃某些需要较大临时内存的融合策略内核选择变保守速度就上不去。设置得过大也不一定更快因为超过实际需求的部分并不会被利用。我的经验是从默认值往上试观察引擎构建时间、显存占用和延迟三者之间的平衡找到平台上的甜蜜点。校准缓存是另一个实用工具。TensorRT允许把校准结果缓存下来下次构建引擎时直接读取跳过校准过程。这在迭代部署时非常方便但有一个前提如果校准集、输入预处理或模型结构发生了任何变化必须删掉旧缓存重新校准。我见过有人改了预处理归一化参数却还在复用旧缓存文件结果精度一直不对排查了半天才发现是缓存没失效。最后分享一个我自己的习惯每次做量化我都先拿一个很小的子集比如50张图把整个流水线跑通确认校准、构建、推理、精度统计各个环节都正常再上全量校准集正式跑。这个习惯帮我省了非常多冤枉时间毕竟全量校准加构建引擎跑一次可能要几个小时如果因为一个低级错误报废一轮那才叫真正的得不偿失。
阅读完成 · 觉得有帮助?