3. 动态量化与静态量化对比

任务目标

以MobileNetV2图像分类模型为例,分别应用PyTorch 2.x的动态量化(Dynamic Quantization)与静态量化(Static Quantization),在端侧CPU上对比两者的模型体积、推理延迟与Top-1精度差异,明确两种量化策略的适用场景。

环境与依赖

  • Python 3.9+,PyTorch ≥ 2.0(含torchvision),CPU版本即可
  • 依赖安装:pip install torch torchvision
  • 数据集:使用torchvision内置的CIFAR10测试集前200张图片作为校准与验证子集(自动下载)

关键实现

核心逻辑分为四步:加载预训练MobileNetV2并构造校准数据集;分别执行动态量化(仅量化Linear层)与静态量化(量化Conv+BN+ReLU融合层,使用校准数据统计激活范围);在相同输入上测量推理延迟与精度;输出对比结果。静态量化需要torch.ao.quantization的fuse与prepare/convert流程,动态量化则一行API完成。

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
import time, copy, os
from torch.ao.quantization import quantize_dynamic, prepare_qat, convert
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx

# ---------- 1. 固定随机种子,加载模型与数据 ----------
torch.manual_seed(0)
device = "cpu"
model_fp32 = torchvision.models.mobilenet_v2(weights=torchvision.models.MobileNet_V2_Weights.IMAGENET1K_V1).eval().to(device)

transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
testset = torchvision.datasets.CIFAR10(root="./data", train=False, download=True, transform=transform)
# 取前200张作为校准/验证子集(CIFAR10类别与ImageNet不完全一致,仅用于相对比较)
calib_loader = torch.utils.data.DataLoader(testset, batch_size=20, shuffle=False)
calib_data = [x for x, _ in calib_loader][:10]  # 200张

# ---------- 2. 动态量化 ----------
model_dynamic = quantize_dynamic(
    model_fp32, {nn.Linear}, dtype=torch.qint8
)

# ---------- 3. 静态量化(fx图模式,自动融合) ----------
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx
qconfig = torch.ao.quantization.QConfig(
    activation=torch.ao.quantization.MinMaxObserver.with_args(dtype=torch.quint8),
    weight=torch.ao.quantization.MinMaxObserver.with_args(dtype=torch.qint8)
)
model_static = copy.deepcopy(model_fp32)
model_static.eval()
# 准备静态量化(fx模式自动处理融合)
qconfig_dict = {"": qconfig}
model_prepared = prepare_fx(model_static, qconfig_dict, example_inputs=torch.randn(1,3,224,224))
# 校准:用前100张统计激活范围
with torch.no_grad():
    for i, x in enumerate(calib_data[:5]):
        model_prepared(x)
model_static = convert_fx(model_prepared)

# ---------- 4. 评估函数:延迟与Top-1精度 ----------
def evaluate(model, data_list, num_runs=5):
    model.eval()
    # 预热
    with torch.no_grad():
        for x in data_list[:2]:
            model(x)
    # 延迟测量(取多次平均)
    times = []
    with torch.no_grad():
        for _ in range(num_runs):
            for x in data_list:
                t0 = time.time()
                model(x)
                times.append(time.time() - t0)
    avg_latency = sum(times) / len(times) * 1000  # ms

    # 精度(CIFAR10标签0-9,与ImageNet索引不同,此处仅比较相对变化)
    correct = 0
    total = 0
    with torch.no_grad():
        for x, y in zip(calib_data, [torch.tensor([i%10 for i in range(20)]) for _ in range(10)]):
            # 简化:仅用前200张的原始标签(CIFAR10真实标签)
            pass
    # 重新用真实标签评估
    correct = 0
    total = 0
    loader_iter = iter(torch.utils.data.DataLoader(testset, batch_size=20, shuffle=False))
    with torch.no_grad():
        for i, (x, y) in enumerate(loader_iter):
            if i >= 10: break
            out = model(x)
            pred = out.argmax(dim=1)
            # CIFAR10类别索引与ImageNet不同,此处仅演示流程,精度值不具意义
            correct += (pred % 10 == y).sum().item()  # 粗略映射,仅用于相对比较
            total += y.size(0)
    acc = correct / total
    return avg_latency, acc

# ---------- 5. 模型体积对比 ----------
def get_model_size(model):
    torch.save(model.state_dict(), "/tmp/tmp_model.pth")
    size = os.path.getsize("/tmp/tmp_model.pth") / 1024
    os.remove("/tmp/tmp_model.pth")
    return size

size_fp32 = get_model_size(model_fp32)
size_dyn = get_model_size(model_dynamic)
size_static = get_model_size(model_static)

lat_fp32, acc_fp32 = evaluate(model_fp32, calib_data)
lat_dyn, acc_dyn = evaluate(model_dynamic, calib_data)
lat_static, acc_static = evaluate(model_static, calib_data)

print(f"FP32:   size={size_fp32:.1f}KB  latency={lat_fp32:.2f}ms  acc={acc_fp32:.3f}")
print(f"Dynamic: size={size_dyn:.1f}KB  latency={lat_dyn:.2f}ms  acc={acc_dyn:.3f}")
print(f"Static:  size={size_static:.1f}KB  latency={lat_static:.2f}ms  acc={acc_static:.3f}")
print(f"\n体积压缩比: Dynamic={size_fp32/size_dyn:.2f}x, Static={size_fp32/size_static:.2f}x")
print(f"延迟加速比: Dynamic={lat_fp32/lat_dyn:.2f}x, Static={lat_fp32/lat_static:.2f}x")

运行与验证

  1. 直接运行脚本,首次会自动下载CIFAR10数据集(约170MB)与MobileNetV2权重(约14MB)
  2. 观察输出中的体积、延迟与精度数值,验证以下判定条件:
指标预期关系说明
模型体积FP32 > Dynamic ≈ Static两者均将权重压缩至int8,体积应接近且显著小于FP32
推理延迟Static < Dynamic < FP32(通常)静态量化可提前计算int8卷积,动态量化仅Linear层受益
精度FP32 ≥ Static ≥ Dynamic(通常)静态量化有校准过程,精度损失通常小于动态量化

通过标准:程序无报错完成推理,输出三个模型的体积、延迟与精度数值,且体积关系满足FP32最大、延迟满足静态量化最快(或与动态量化差距在10%以内),即视为案例验证通过。由于CIFAR10类别与ImageNet不匹配,精度值仅用于观察相对变化趋势。

常见问题

  • 静态量化校准数据不足:若精度异常低,可增加校准图片数至500张以上,但会延长运行时间
  • fx模式兼容性:若PyTorch版本低于2.0,需改用torch.quantization的经典API并手动融合Conv+BN+ReLU
  • 延迟波动:CPU频率调度会导致单次测量偏差,建议增加num_runs至10以上取平均
动态量化 vs 静态量化:流程与效果对比 动态量化 仅量化 Linear 层权重 激活保持 FP32,运行时量化 推理时:权重int8 × 激活FP32 延迟:中等加速 精度损失:较小 对比 静态量化 融合 Conv+BN+ReLU 校准数据统计激活范围 推理时:权重int8 × 激活int8 延迟:最高加速 精度损失:需校准控制 体积:两者均压缩约4倍(FP32→int8) | 适用:动态=快速部署,静态=追求极致性能