1. 模型优化器到底在解决什么问题
第一次接触 Model-Optimizer 这个概念,很多人会把它和优化算法(比如 SGD、Adam)搞混。其实它跟训练时用的优化器完全是两码事。Model-Optimizer 是一类工具链的统称,核心目标只有一个:在尽量不损失精度的前提下,让训练好的模型跑得更快、占得更少、部署更省。你可以把它理解成模型出厂前的“瘦身+提速”流水线。
我最早接触这类工具是在一个图像分类项目上。当时训练出来的模型在服务器上跑得好好的,一放到边缘设备上就卡得没法看——推理一次要 800 多毫秒,内存占用接近 2GB。后来用 Model-Optimizer 做了一轮量化加剪枝,推理时间直接降到 120 毫秒,模型体积从 240MB 压到 60MB,精度只掉了 0.8 个百分点。这个收益在真实项目里是相当可观的。
所以这篇文章适合谁看?如果你手上有训练好的模型,正准备往生产环境部署,或者你正在做端侧、嵌入式、移动端的推理落地,那 Model-Optimizer 这套东西你绕不开。哪怕你只是想让本地跑模型的时候显存别爆,这里面的技巧也用得上。我会从整体设计思路讲到具体操作步骤,再到踩过的坑,尽量把每个环节都讲透。
需要先说明一点:Model-Optimizer 不是一个单一工具的名字,而是一类技术的集合。不同框架下有不同的实现,比如 PyTorch 生态里有 TorchScript、FX Graph Mode Quantization,TensorFlow 生态里有 TFLite Converter 和 TF-TRT,ONNX 生态里有 ONNX Runtime 的量化工具。它们思路相通,但操作细节差异不小。下面我主要以 PyTorch 和 ONNX 这两条最常用的路线来展开,其他框架你可以类比迁移。
2. 整体设计思路与方案选型拆解
2.1 为什么不能直接拿训练好的模型去部署
训练和推理是两个完全不同的场景。训练时我们关心的是梯度能不能顺利回传、数值稳不稳定、batch 能不能开大;推理时关心的是延迟、吞吐、内存占用、功耗。训练框架为了支持自动求导和动态图,会保留大量推理时根本用不到的计算节点和中间变量。这就好比你要搬家,训练阶段是把整个仓库都搬走,推理阶段其实只需要搬走真正要用的那几件家具。
具体来说,训练好的模型直接部署会面临几个典型问题。第一是计算图冗余,比如 Dropout 层在推理时应该被去掉,BatchNorm 应该被折叠进卷积,但原始模型里这些节点还在。第二是数值精度过剩,训练时用 FP32 是为了梯度稳定,推理时 FP16 甚至 INT8 往往就够了。第三是算子实现不最优,训练框架的算子为了通用性做了很多妥协,推理引擎可以针对特定硬件做深度优化。
Model-Optimizer 要做的就是把这些冗余和浪费一层层剥掉。它的设计思路可以概括为三步:图优化 → 精度压缩 → 硬件适配。图优化负责去掉无用节点、合并可合并的算子;精度压缩负责把 FP32 降到 FP16 或 INT8;硬件适配负责把算子映射到目标设备的最优实现上。这三步是有先后顺序的,先做图优化再做量化,效果通常比反过来好,因为图优化之后计算图更干净,量化时需要考虑的边界情况更少。
2.2 量化、剪枝、蒸馏到底该选哪个
Model-Optimizer 这个大类下面,最常用的三种手段是量化(Quantization)、剪枝(Pruning)和知识蒸馏(Knowledge Distillation)。很多人一上来就纠结选哪个,其实它们解决的问题不一样,很多时候是组合使用的。
量化是把模型参数和激活值从高精度浮点降到低精度表示。FP32 降到 FP16 是最安全的,几乎不掉精度,模型体积直接减半,推理速度在支持 FP16 的硬件上能提升 1.5 到 2 倍。INT8 量化更激进,体积能压到四分之一,速度提升 2 到 4 倍,但精度损失需要仔细控制。量化的优势是通用性强、落地成本低,基本上任何模型都能做,所以我一般建议把它作为第一步。
剪枝是把模型中不重要的权重或通道去掉。这里有个反直觉的点:神经网络里大量权重其实接近零,去掉它们对输出影响很小。剪枝分非结构化剪枝和结构化剪枝,前者把单个权重置零,后者直接砍掉整个通道或层。非结构化剪枝压缩率高但需要专门硬件支持才能加速,结构化剪枝压缩率低一些但通用性好。剪枝的难点在于找到合适的剪枝比例,剪多了精度崩,剪少了没效果。
知识蒸馏是让一个小模型去学大模型的输出分布。它不改变原模型结构,而是训练一个新模型。蒸馏的效果取决于教师模型的质量和学生模型的能力上限,适合你有充足训练资源、且对模型结构有重新设计空间的场景。
下面这张表可以帮你快速判断该用哪种手段:
| 手段 | 压缩效果 | 精度影响 | 落地难度 | 适用场景 |
|---|---|---|---|---|
| FP16 量化 | 体积减半,速度 1.5-2x | 几乎无损 | 低 | 几乎所有模型 |
| INT8 量化 | 体积 1/4,速度 2-4x | 可控,通常 <1% | 中 | 分类、检测、NLP |
| 结构化剪枝 | 体积减 30-50% | 需微调恢复 | 中高 | 通道冗余大的 CNN |
| 非结构化剪枝 | 体积减 70-90% | 需微调恢复 | 高 | 有稀疏硬件支持 |
| 知识蒸馏 | 取决于学生模型 | 取决于训练 | 高 | 有重训练资源 |
我的经验是:先做 FP16 量化拿到免费收益,再评估是否需要 INT8,最后才考虑剪枝和蒸馏。这个顺序能让你用最小成本拿到大部分收益,避免一上来就做复杂操作结果精度崩了还得回滚。
2.3 精度和速度的平衡点怎么找
这是 Model-Optimizer 最核心的取舍问题。没有免费的午餐,压缩率越高,精度损失风险越大。关键是要找到一个可接受的精度下限,然后在这个约束下最大化压缩率。
我的做法是先定一个精度容忍度。比如分类任务,如果原始模型 Top-1 准确率是 95%,那我可以接受降到 94.5%,也就是 0.5 个百分点的损失。然后从这个约束出发,逐步增加压缩强度,每加一档就测一次精度,直到精度掉到临界点为止。这个过程叫精度-压缩率曲线扫描,虽然费时间,但能帮你找到最优工作点。
还有一个容易被忽略的点:不同层对量化的敏感度差异很大。第一层和最后一层通常最敏感,中间层相对鲁棒。所以混合精度量化是个好策略——敏感层保持 FP16,其他层用 INT8。PyTorch 的 FX Graph Mode 和 ONNX Runtime 都支持这种按层配置精度的方式。
3. 核心细节解析与实操要点
3.1 计算图优化:先把模型“洗干净”
在动量化之前,一定要先做计算图优化。这一步不做,后面量化会引入很多不必要的误差。计算图优化主要包括算子融合、常量折叠、死代码消除这几类操作。
算子融合是最重要的一项。最常见的融合模式是 Conv + BatchNorm + ReLU 融成一个算子。为什么这个融合这么关键?因为 BatchNorm 在推理时本质上是一个线性变换,它的参数可以完全吸收进前面的卷积权重里。融合之后,原本三次内存读写变成一次,延迟能降 20% 到 30%。我实测过一个 ResNet-50,光做 Conv-BN-ReLU 融合,推理速度就提升了 25%。
常量折叠是把计算图中所有输入固定的子图提前算好。比如模型里有些形状计算、索引计算,输入是常量,那这些计算完全可以在导出阶段就算完,不用留到运行时。这个优化对动态图模型尤其重要。
死代码消除是去掉推理时用不到的节点。最典型的是 Dropout,训练时随机置零,推理时应该直接透传。还有像训练专用的损失计算分支、梯度相关节点,都应该在导出时清掉。
在 PyTorch 里,做这些优化最方便的方式是用torch.fx做图追踪,然后手动或自动地应用优化 pass。也可以用torch.jit.trace或torch.jit.script导出 TorchScript,TorchScript 的编译器会自动做一部分融合。但要注意,trace方式对动态控制流支持不好,如果模型里有 if-else 分支,建议用script。
注意:做图优化之前一定要先
model.eval(),把模型切到推理模式。否则 BatchNorm 会用训练时的统计量,Dropout 还会随机置零,导出的图就是错的。
3.2 量化实操:从 FP32 到 INT8 的完整路径
量化是 Model-Optimizer 里收益最直接的一步。我以 PyTorch 的 FX Graph Mode Quantization 为例,把完整流程走一遍。
第一步是准备模型和校准数据。量化需要一个校准集来统计激活值的分布范围,通常从训练集里抽 100 到 500 个样本就够了。校准集要覆盖真实数据的分布,不能只用一类样本,否则统计出来的范围会偏。
第二步是配置量化方案。PyTorch 提供两种模式:Post Training Quantization(PTQ)和 Quantization Aware Training(QAT)。PTQ 不需要重新训练,速度快,适合大多数场景;QAT 在训练时模拟量化误差,精度更高,但需要重新训练几个 epoch。我的建议是先用 PTQ 试,如果精度掉太多再上 QAT。
第三步是指定量化配置。这里要决定哪些层量化、用什么精度。默认配置是权重用 INT8、激活用 INT8,但你可以把敏感层排除掉。下面是一段典型的配置代码:
import torch from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx from torch.ao.quantization import QConfigMapping # 定义量化配置 qconfig_mapping = QConfigMapping().set_global( torch.ao.quantization.get_default_qconfig('fbgemm') ) # 指定不量化的层 qconfig_mapping.set_module_name('classifier', None) # 准备模型 model.eval() example_inputs = (torch.randn(1, 3, 224, 224),) prepared_model = prepare_fx(model, qconfig_mapping, example_inputs) # 用校准数据跑一遍 with torch.no_grad(): for data in calib_loader: prepared_model(data) # 转换为量化模型 quantized_model = convert_fx(prepared_model)第四步是验证精度。量化完一定要在验证集上跑一遍,对比原始模型的精度。如果掉得太多,可以尝试调整量化配置,比如把某些层改成 FP16,或者换用 QAT。
这里有个细节值得展开:fbgemm和qnnpack是两种不同的量化后端。fbgemm针对 x86 服务器优化,qnnpack针对 ARM 移动端优化。选错了后端,量化模型可能跑不起来或者性能很差。我一般会在服务器上用fbgemm,移动端用qnnpack,导出时分别处理。
3.3 剪枝的粒度选择与敏感度分析
剪枝比量化更“暴力”,因为它直接改变了模型结构。做剪枝之前,必须先做敏感度分析,搞清楚每一层对剪枝的容忍度。
敏感度分析的做法是:逐层尝试不同的剪枝比例,观察精度变化。比如对某一层分别剪 10%、20%、30%、40%,看精度掉多少。掉得慢的层可以多剪,掉得快的层少剪或不剪。这个分析虽然费时间,但能避免盲目剪枝导致精度崩溃。
剪枝的粒度选择也很关键。非结构化剪枝把单个权重置零,压缩率高但需要稀疏矩阵运算支持才能加速,普通硬件上反而可能变慢。结构化剪枝直接砍掉整个通道,压缩率低一些但通用性好,任何硬件都能加速。我一般优先选结构化剪枝,除非目标硬件明确支持稀疏计算。
PyTorch 里可以用torch.nn.utils.prune做剪枝,但它主要是非结构化的。结构化剪枝需要自己实现或者用第三方库。一个简单的结构化剪枝思路是:计算每个通道的权重 L2 范数,把范数最小的通道整条砍掉,然后微调恢复精度。
剪枝后一定要微调。剪枝相当于给模型做了一次“手术”,微调是让模型重新适应新的结构。微调的学习率要设小一点,通常是原始训练学习率的十分之一,训练 5 到 10 个 epoch 就够了。
3.4 导出与部署:ONNX 还是 TorchScript
优化完的模型要导出成部署格式。主流选择是 ONNX 和 TorchScript,两者各有优劣。
ONNX的优势是跨框架、跨平台,几乎所有推理引擎都支持。ONNX Runtime 的优化能力很强,支持图优化、量化、算子融合一条龙。缺点是某些自定义算子导出可能有问题,动态形状支持也不如 TorchScript 灵活。
TorchScript的优势是和 PyTorch 生态无缝衔接,动态形状支持好,自定义算子容易集成。缺点是跨框架能力弱,基本只能在 PyTorch 生态里用。
我的选择标准是:如果部署环境是 ONNX Runtime、TensorRT 这类通用推理引擎,就导出 ONNX;如果部署环境是 PyTorch 自己的推理框架,或者模型里有复杂的动态控制流,就用 TorchScript。
导出 ONNX 的时候有个常见坑:opset 版本选择。opset 版本太低不支持某些算子,太高可能推理引擎还不支持。我一般用 opset 13 到 15 之间,兼容性和功能比较平衡。导出命令大概是这样:
torch.onnx.export( model, dummy_input, "model.onnx", opset_version=14, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}} )导出之后一定要用onnx.checker验证一下模型合法性,再用onnxruntime跑一遍对比输出,确保导出没有引入误差。
4. 完整实操流程与关键环节实现
4.1 环境准备与依赖安装
动手之前先把环境搭好。我以 PyTorch 路线为例,列出需要装的包:
pip install torch torchvision pip install onnx onnxruntime pip install onnxruntime-tools pip install neural-compressorneural-compressor是 Intel 开源的一个 Model-Optimizer 工具包,集成了量化、剪枝、蒸馏等能力,用起来比较省心。如果你不想自己写量化配置,可以直接用它的一键量化 API。
环境搭好之后,先确认一下硬件支持情况。INT8 量化在 x86 上依赖 VNNI 指令集,在 ARM 上依赖 dotprod 指令集。可以用lscpu查看 CPU 标志位,如果没有对应指令集,INT8 量化可能不会加速,甚至变慢。
4.2 一个完整的量化+剪枝实战案例
我拿一个实际的图像分类模型来演示完整流程。假设我们有一个训练好的 ResNet-18,目标是部署到 x86 服务器上,要求推理延迟降低 3 倍以上,精度损失不超过 1%。
第一步:基线测试。先测原始模型的延迟和精度,作为对比基准。
import time import torch model.eval() model = model.cuda() # 预热 for _ in range(10): _ = model(torch.randn(1, 3, 224, 224).cuda()) # 测延迟 torch.cuda.synchronize() start = time.time() for _ in range(100): _ = model(torch.randn(1, 3, 224, 224).cuda()) torch.cuda.synchronize() print(f"平均延迟: {(time.time() - start) / 100 * 1000:.2f} ms")假设基线是 45ms,精度 94.2%。
第二步:图优化。用torch.fx做算子融合,把 Conv-BN-ReLU 融掉。这一步通常能带来 20% 左右的提速,而且精度无损。
第三步:INT8 量化。用 FX Graph Mode 做 PTQ,校准集用 200 张训练图。量化后测精度,假设掉到 93.8%,损失 0.4 个百分点,在容忍范围内。
第四步:结构化剪枝。对中间层做 20% 的通道剪枝,然后微调 5 个 epoch。剪枝后模型更小,但精度可能再掉一点,假设微调后恢复到 93.5%。
第五步:导出 ONNX。把优化后的模型导出成 ONNX,用 ONNX Runtime 加载,开启图优化和 INT8 执行提供器。
第六步:最终测试。在 ONNX Runtime 上测延迟,假设降到 12ms,相比基线 45ms 提升了 3.75 倍,精度 93.5%,损失 0.7 个百分点。目标达成。
这个流程里,每一步的收益和代价都要记录清楚,方便后续调优。我习惯用一个表格来跟踪:
| 阶段 | 延迟 | 精度 | 模型体积 |
|---|---|---|---|
| 基线 | 45ms | 94.2% | 45MB |
| 图优化后 | 36ms | 94.2% | 45MB |
| INT8 量化后 | 15ms | 93.8% | 12MB |
| 剪枝+微调后 | 12ms | 93.5% | 8MB |
4.3 校准集构建的讲究
校准集的质量直接决定量化精度。我见过太多人随便拿几张图做校准,结果量化后精度崩了,还以为是量化方法不行。其实问题出在校准集上。
校准集要满足几个条件。第一是数量足够,太少统计不准,太多浪费时间,100 到 500 个样本是比较合适的区间。第二是分布覆盖,要包含各类别的样本,不能只拿某一类。第三是预处理一致,校准时的预处理必须和推理时完全一致,包括归一化参数、resize 方式等。
还有一个进阶技巧:用真实数据而不是训练数据做校准。如果训练数据和线上数据分布有差异,用线上数据校准效果更好。我做过一个对比实验,用训练数据校准量化后精度掉 1.2%,用线上数据校准只掉 0.3%,差距很明显。
4.4 混合精度量化的配置方法
混合精度量化是平衡精度和速度的利器。核心思路是:敏感层用高精度,不敏感层用低精度。怎么判断哪些层敏感?前面说的敏感度分析可以用在这里。
在 ONNX Runtime 里,可以通过QuantizationConfig指定每层的精度。在 PyTorch 里,可以用QConfigMapping的set_module_name方法逐层配置。下面是一个混合精度配置的例子:
qconfig_mapping = QConfigMapping() # 默认 INT8 qconfig_mapping.set_global(torch.ao.quantization.get_default_qconfig('fbgemm')) # 第一层和最后一层用 FP16 qconfig_mapping.set_module_name('conv1', torch.ao.quantization.float16_static_qconfig) qconfig_mapping.set_module_name('fc', torch.ao.quantization.float16_static_qconfig)实测下来,这种配置能在精度损失减少一半的情况下,只牺牲 10% 到 15% 的速度收益,性价比很高。
5. 常见问题与排查技巧实录
5.1 量化后精度暴跌怎么排查
精度暴跌是量化最常见的问题。排查思路要按顺序来,不要一上来就怀疑量化方法。
先查校准集。校准集数量够不够?分布全不全?预处理对不对?我遇到过好几次都是校准集的问题,换了校准集精度就回来了。
再查敏感层。用逐层量化对比的方式,找出哪些层量化后误差最大。把这些层排除掉或者改成 FP16,通常能解决大部分精度问题。
然后查量化配置。fbgemm和qnnpack选对了吗?per-tensor 还是 per-channel?per-channel 量化精度更高,但某些硬件不支持。如果精度要求高,优先用 per-channel。
最后查模型本身。有些模型结构对量化天然不友好,比如含有大量小数值激活的模型。这种情况下可能需要 QAT 才能救回来。
5.2 推理速度没提升反而变慢
量化后变慢通常有几个原因。第一是硬件不支持 INT8 加速,比如老 CPU 没有 VNNI 指令集,INT8 计算反而要走模拟路径,比 FP32 还慢。第二是算子没有真正量化,有些算子量化后端不支持,会 fallback 到 FP32,导致频繁的数据类型转换,开销更大。第三是量化粒度太细,per-channel 量化在某些硬件上需要额外的重排操作,反而拖慢速度。
排查方法是先用 profiling 工具看每个算子的耗时,找出瓶颈在哪。ONNX Runtime 可以用onnxruntime_perf_test做 profiling,PyTorch 可以用torch.profiler。
5.3 导出 ONNX 报错怎么办
ONNX 导出报错是另一个高频问题。常见原因和解决方法我整理成了一张表:
| 报错类型 | 常见原因 | 解决方法 |
|---|---|---|
| 不支持的算子 | 用了 ONNX 没有的自定义算子 | 注册自定义算子或改用标准算子 |
| 动态形状错误 | 输入形状不确定 | 指定 dynamic_axes 或固定形状 |
| 类型不匹配 | 输入类型和模型期望不一致 | 检查 dummy_input 的 dtype |
| opset 版本问题 | 算子需要更高 opset | 提高 opset_version |
我踩过最坑的一次是模型里用了torch.nn.functional.interpolate的align_corners=False模式,ONNX 导出后结果和 PyTorch 不一致。后来发现是 ONNX 的 resize 算子和 PyTorch 的实现有细微差异,改成align_corners=True就对齐了。这种数值对齐问题很隐蔽,导出后一定要做输出对比。
5.4 剪枝后模型无法收敛
剪枝后微调不收敛,通常是剪得太狠了。剪枝比例要循序渐进,不要一次剪太多。我的经验是单次剪枝不超过 30%,剪完立即微调,微调后再评估是否需要继续剪。
另一个原因是微调学习率太大。剪枝后模型结构变了,大的学习率会让模型震荡。学习率要降到原始训练的十分之一甚至更低,用 cosine 衰减策略慢慢收敛。
还有一个容易忽略的点:剪枝后 BatchNorm 的统计量要重新校准。剪枝改变了通道数,原来的 running mean 和 running var 不再适用。微调时让 BatchNorm 重新统计,或者手动重置再统计。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 量化后精度掉 >2% | 校准集问题/敏感层未排除 | 换校准集,逐层排查 |
| 量化后速度无提升 | 硬件不支持/算子未量化 | 查 CPU 指令集,做 profiling |
| ONNX 导出失败 | 自定义算子/动态形状 | 换算子,指定 dynamic_axes |
| 剪枝后不收敛 | 剪枝比例过大/学习率过大 | 降低剪枝比例和学习率 |
| 推理结果不一致 | 数值精度差异/算子实现差异 | 逐层对比输出,定位差异层 |
| 显存占用没降 | 中间激活未优化 | 开启内存复用,用推理专用引擎 |
最后分享一个我常用的技巧:做任何优化之前,先把基线数据完整记录下来,包括延迟、精度、内存、模型体积。优化过程中每做一步就记录一次,这样出问题能快速定位是哪一步引入的,也方便回滚。
6. 工具选型与生态对比
6.1 主流 Model-Optimizer 工具横向对比
市面上做模型优化的工具不少,各有侧重。我把自己用过的几个整理一下,方便你按需选择。
PyTorch 原生工具链(FX Graph Mode、TorchScript)的优势是和 PyTorch 无缝集成,量化、图优化都能做,社区活跃,文档齐全。缺点是 ONNX 导出偶尔有坑,跨框架能力弱。
ONNX Runtime的优势是跨框架、跨平台,优化能力强,支持多种执行提供器(CPU、GPU、TensorRT 等)。量化工具onnxruntime.quantization用起来很简单,几行代码就能做 PTQ。缺点是某些自定义算子支持不好。
Neural Compressor是 Intel 开源的,集成了量化、剪枝、蒸馏、自动调优,支持 PyTorch、TensorFlow、ONNX 多种前端。它的自动调优功能很省心,能自动搜索最优量化配置。缺点是文档偏工程化,上手门槛稍高。
TensorRT是 NVIDIA 的推理优化引擎,针对 NVIDIA GPU 做了深度优化,性能极强。支持 FP16、INT8、稀疏化。缺点是绑定 NVIDIA 硬件,跨平台能力弱。
OpenVINO是 Intel 的推理优化工具,针对 Intel CPU、GPU、VPU 优化。支持模型转换、量化、图优化。缺点是主要面向 Intel 硬件。
选型建议:如果部署在 NVIDIA GPU 上,优先 TensorRT;如果部署在 Intel CPU 上,优先 OpenVINO 或 Neural Compressor;如果需要跨平台,优先 ONNX Runtime;如果不想引入额外依赖,用 PyTorch 原生工具链。
6.2 自动调优工具的使用体验
手动调量化配置很费时间,自动调优工具能省不少事。Neural Compressor 的自动调优我用得比较多,它的思路是:给定精度容忍度,自动搜索最优量化配置。
用法大概是这样:
from neural_compressor.config import PostTrainingQuantConfig from neural_compressor.quantization import fit # 定义精度容忍度 config = PostTrainingQuantConfig( accuracy_criterion={'relative': 0.01} # 允许 1% 精度损失 ) # 自动调优 q_model = fit( model=model, conf=config, calib_dataloader=calib_loader, eval_func=eval_func )它会自动尝试不同的量化方案,找到满足精度约束的最优配置。实测下来,自动调优的结果通常比手动配置好,因为它能逐层搜索最优精度组合。缺点是调优过程比较慢,可能需要跑几十轮评估。
6.3 不同硬件平台的优化策略差异
硬件平台不同,优化策略差异很大。x86 服务器上,INT8 量化配合 VNNI 指令集能获得最大收益,重点做算子融合和 INT8 量化。ARM 移动端上,重点做模型瘦身和功耗优化,INT8 量化用qnnpack后端,剪枝比例可以更激进。NVIDIA GPU 上,FP16 和 INT8 都有很好的支持,TensorRT 能自动做算子融合和精度校准,重点是用好 TensorRT 的 INT8 校准工具。
边缘 NPU 上情况更复杂,不同厂商的 NPU 支持的算子和精度都不一样,通常需要专门的模型转换工具。这种情况下,建议先用厂商提供的工具做转换和量化,再用通用工具做补充优化。
7. 我踩过的坑和实操心得
做模型优化这几年,踩过的坑不少,挑几个有代表性的说说。
第一个坑:以为量化是免费的。刚开始做量化的时候,我觉得 FP32 降到 INT8 就是简单地把数值范围映射一下,能有什么损失。结果第一次做 INT8 量化,精度直接掉了 5 个百分点。后来才明白,量化误差会在层与层之间累积,尤其是激活值分布不均匀的层,量化误差特别大。从那以后,我每次量化前都做敏感度分析,再也不敢盲目全量量化了。
第二个坑:忽略校准集的代表性。有一次做量化,校准集只用了 50 张图,而且都是同一类别的。量化后在验证集上精度还行,一上线上就崩了。原因是校准集没覆盖线上数据的分布,量化参数统计偏了。后来我把校准集扩到 300 张,覆盖所有类别,问题就解决了。
第三个坑:剪枝后忘记重置 BatchNorm。剪枝改变了通道数,BatchNorm 的 running 统计量失效了,但我忘了重置,直接微调,结果模型一直不收敛。排查了半天才发现是 BatchNorm 的问题。重置之后重新统计,很快就收敛了。
第四个坑:ONNX 导出后没做数值对比。有一次导出 ONNX 后直接部署,结果推理结果和 PyTorch 对不上。排查发现是某个算子的实现差异导致的。从那以后,我导出 ONNX 后一定做逐层输出对比,确保数值一致再部署。
第五个坑:在错误的硬件上做量化。有一次在开发机上做 INT8 量化,开发机 CPU 不支持 VNNI,量化后速度反而变慢了。后来换到支持 VNNI 的服务器上,速度才提上来。所以做量化前一定要确认目标硬件的指令集支持情况。
这些坑说到底都是细节问题,但每一个都能让你白干好几天。我的建议是:每做一步优化都做完整测试,不要跳步,不要想当然。优化是个精细活,急不得。
最后再分享一个实用技巧:如果你不确定该从哪一步开始优化,就先做 profiling,找出模型推理的瓶颈在哪。是计算密集还是内存密集?是某个算子特别慢还是整体都慢?搞清楚瓶颈再针对性优化,比盲目上量化剪枝效率高得多。我一般用torch.profiler或onnxruntime_perf_test做 profiling,几分钟就能定位问题。