目录

端侧模型 INT8 量化实战:从 PyTorch 到 ONNX 的部署压缩全流程

模型训练完只是一个 FP32 的"大胖子"——一个 8 bit 的比特、32 位浮点权重,参数动不动几 MB。手机、树莓派、边缘盒子内存有限,直接部署往往力不从心。量化就是把这段"高清视频压成 MP4"的过程:用更少的 bit 存权重(INT8 只用 8 bit),换更小的体积和更快的推理,代价是少量精度损失。今天我们用 onnxruntime 走一遍端侧模型 INT8 静态量化的完整套路:导出 → 校准 → 量化 → 移动端推理。

为什么量化能省这么多

FP32 每个数占 4 字节,INT8 只占 1 字节。参数直接除以 4,内存占用和最终安装包体积都能砍掉约 75%。同时 INT8 运算在支持 SIMD 加速的 CPU、以及各家 NPU/GPU 上通常更快(读内存少、带宽省了)。

以一个小型的 MobileNetV2 分类模型为例(约 3.4M 参数):

  • FP32:约 14 MB
  • FP16:约 7 MB(精度几乎无损失,带宽减半)
  • INT8:约 3.5 MB(体积再砍一半,精度可能会掉 0.5%-2%)

量化位宽与精度: FP16 对多数任务几乎没有可感知损失;INT8 适合分类、检测、语音等对精度不太敏感的任务;INT4 压得最狠但只推荐给 LLM(配合 GGUF 格式),普通 CNN 用 INT4 精度掉得偏多。检查每种模型时第一件事是确认它的输入模态——量化流程对所有模态通用,但校准数据要选对(音频模型喂音频,图像模型喂图像)。

第一步:把 PyTorch 模型导出成 ONNX

端侧部署前,先把 PyTorch 模型转成通用格式 ONNX(.onnx),这样无论后面接 onnxruntime、TFLite 还是 Core ML 都方便。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import torch
import torchvision.models as models

model = models.mobilenet_v2(weights=models.MobileNet_V2_Weights.DEFAULT)
model.eval()

# 动态尺寸,方便不同分辨率输入
dummy = torch.randn(1, 3, 224, 224)
torch.onnx.export(
    model, dummy, "mobilenetv2.onnx",
    input_names=["input"], output_names=["output"],
    dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}},
    opset_version=17,
)
print("导出完成")

第二步:静态量化(PTQ)——离线玩转校准集

静态量化(Post-Training Quantization,PTQ)是端侧最常用的路子:不需要重新训练,只需提供一小批校准数据,让工具统计每层的激活值范围,从而定出最合适的量化缩放系数。对比 QAT(量化感知训练,需重训,精度更高但成本高),PTQ 是"开箱即用"的最优性价比。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from onnxruntime.quantization import quantize_static, QuantType, CalibrationDataReader
import numpy as np
import torchvision.datasets as datasets
import torchvision.transforms as T

# 用真实数据做校准,别用随机数——范围和真实分布要对齐
transform = T.Compose([
    T.Resize((256, 256)),
    T.CenterCrop(224),
    T.ToTensor(),
    T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
# 示例:用 ImageFolder 目录训练,取一批样本做校准
dataset = datasets.ImageFolder("/path/to/calib_images", transform=transform)

class ImgReader(CalibrationDataReader):
    def __init__(self, dataset, n=200):
        self.gen = iter([{"input": np.expand_dims(x.numpy(), 0).astype(np.float32)}
                         for x, _ in [next(iter(dataset)) for i in range(n)]])
    def get_next(self):
        return next(self.gen, None)

# weights 用 INT8,激活也用 INT8,最大化压缩
quantize_static(
    "mobilenetv2.onnx",
    "mobilenetv2_int8.onnx",
    ImgReader(dataset),
    per_channel=True,
    activation_type=QuantType.QInt8,
    weight_type=QuantType.QInt8,
)
print("量化完成,输出 mobilenetv2_int8.onnx")

关键细节

  • 校准集几百张图或几十秒音频就够,不需要太多。
  • per_channel=True 按每个输出通道单独定缩放系数,精度比 per-tensor 更高,几乎可媲美 FP32。
  • 量化后务必用测试集验证精度:top1 掉了超过 2 个点就要回退用 FP16 或改用 QAT。

第三步:端侧推理(onnxruntime-mobile)

量化完就能扔到手机/边缘设备上跑。onnxruntime 有专门为移动端裁剪的 onnxruntime-mobile(Android/iOS 通用),直接用同一个 .onnx 文件,不用二次转换。

1
2
3
4
5
6
7
import onnxruntime as ort
import numpy as np

sess = ort.InferenceSession("mobilenetv2_int8.onnx", providers=["CPUExecutionProvider"])
img = np.random.randn(1, 3, 224, 224).astype(np.float32)  # 实际用处理好的图片
out = sess.run(["output"], {"input": img})[0]
print("推理完成,输出 shape:", out.shape)

Android 上通过 Gradle 集成 com.microsoft.onnxruntime:onnxruntime-android 后,用 C API 或 JNI 调用即可,推理代码和桌面端完全一致。量化模型的优势在这里显现:不仅体积小,CPU 推理在这种支持 int8 指令集(ARMv8.1 及以上的 NEON)的芯片上往往能快 30%-70%。

开源方案怎么选

方案 项目 适用场景
ONNX Runtime github.com/microsoft/onnxruntime 跨平台、PTQ/QAT 都支持,最通用
TensorFlow Lite github.com/tensorflow/tflite Android/嵌入式,生态成熟
llama.cpp + GGUF github.com/ggml-org/llama.cpp LLM 的量化(INT4 等),桌面到移动全平台
MLC-LLM github.com/mlc-ai/mlc-llm 手机上跑量化大模型,NPU 加速最好

怎么选:传统 CNN/检测/语音模型用 ONNX Runtime 或 TFLite,INT8 足够;如果目标是端侧跑 LLM,那就走 llama.cpp/MLC-LLM 的 GGUF INT4 路线。

小结

量化是端侧模型的"刚需操作",尤其 INT8 静态量化路径最短、收益最直接:torch.onnx.export 导出 → quantize_static 配校准集量化 → onnxruntime-mobile 推理,三步打通。记住三个原则:校准数据必须真实、per_channel 精度更高、做完必验证。下次你的模型塞不进手机时,先量化,别急着换网络结构。