把一个在 GPU 上跑得飞起的 PyTorch 模型塞进边缘设备的 NPU,就像把大象装进冰箱——听起来简单,每一步都是坑。本文记录我在宠物面部识别项目中将 MobileNet-V3 从 PyTorch 一路推到 RK3588 NPU 的完整量化历程。
动态量化 vs 静态量化:怎么选
PyTorch 提供了两种量化方案:动态量化(只量化权重,激活在推理时动态计算 scale/zero_point)和静态量化(权重和激活都预先量化,需要校准数据)。动态量化实现简单,一行 torch.quantization.quantize_dynamic() 就能搞定,但激活仍是 FP32——NPU 无法直接消费。对于边缘 NPU 部署,静态量化是唯一可行的路径。代价是需要准备校准数据集:我们用了 500 张宠物面部图像,通过 100 次前向传播统计激活的 min/max 范围。
ONNX 导出的算子兼容性坑
从 PyTorch 导出 ONNX 时,MobileNet-V3 的 h-swish 激活函数成了一个棘手的拦路虎。h-swish 在 PyTorch 中是 x * ReLU6(x+3) / 6,ONNX 早期版本(opset 11 以下)不支持 HardSwish 算子,导致导出的图包含低效的逐元素操作序列。解决方案是强制使用 opset 13+,并在导出前将模型中的 h-swish 替换为 nn.Hardswish()(PyTorch 1.6+ 的原生实现)。另一个坑是 SE 模块中的全局平均池化——在动态输入尺寸下,ONNX 的 AdaptiveAvgPool 有时会生成冗余的 shape 计算节点,需要用 onnx-simplifier 做一次图清理。
INT8 量化:精度损失控制在 2% 以内
从 FP32(21.8MB)到 INT8 的量变带来了约 4 倍的压缩,但精度损失必须严格控制。我们的策略:
- 逐通道量化:对卷积权重采用 per-channel 量化而非 per-tensor——MobileNet-V3 的深度可分离卷积中各通道权重分布差异大,per-tensor 会导致某些通道的量化误差剧增。
- 校准集质量优先:校准集必须覆盖各种光照条件和宠物品种,不能只用"好图"。初期只用清晰正脸做校准,上线后模糊侧脸准确率跳水——补入 30% 的困难样本后精度恢复。
- 精度对比:Top-1 准确率从 FP32 的 94.2% 降至 INT8 的 92.6%,损失 1.6%,在可接受范围内。
RKNN 转换流程
RK3588 的 NPU 使用 Rockchip 自研的 RKNN 格式。转换链是:PyTorch → ONNX → RKNN。ONNX 作为中间表示起到了抽象层的作用。RKNN Toolkit 2 提供了混合量化选项——在敏感层(如 SE 模块的 sigmoid)保留 FP16,其余层 INT8。模型从 ONNX 的 21.8MB → RKNN 的 5.74MB,其中约 1.5MB 是 RKNN 的图结构和量化参数表。
量化感知训练(QAT)基础概念
PTQ(Post-Training Quantization,训练后量化)虽然方便,但在某些模型中精度损失可能超过 5%。QAT 的核心理念是在训练过程中插入伪量化节点(FakeQuant),让网络在训练时就"感知"到量化误差的存在,从而学习到对量化更鲁棒的权重分布。代价是训练时间增加约 30%。对于 MobileNet-V3 这个项目,PTQ 的 1.6% 精度损失已经在可接受范围内,QAT 的投入产出比不划算——但如果你在部署的是精度敏感的检测网络,QAT 值得认真考虑。
模型量化的真谛不在于"把数字变小",而在于理解你的硬件对什么数字格式友好,然后在精度和效率之间找到那个最优的折中点。ONNX 是桥梁,校准集是试金石,RKNN 是终点——每一步都值得认真对待。