登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

ONNX Runtime 量化怎样比较动态量化与静态量化的部署差异怎样在批处理代码中减少额外分配

来源:17golang原创

时间:2026-11-10 02:45:41 139浏览 收藏

比较 ONNX Runtime 动态量化和静态量化时,不能只看模型文件变小了多少。两者最关键的区别是激活值的 scale 与 zero_point 何时得到:动态量化在每次前向计算时处理,静态量化在离线校准阶段提前写入模型。前者省掉了校准数据准备,后者减少了推理时的计算开销。要比较部署差异,应固定同一份 FP32 模型、输入分布、执行后端和批处理方式,再同时观察延迟、吞吐、内存与任务精度。

简单判断:Transformer 或 RNN 可以先试动态量化;CNN 和有稳定校准集的服务通常更适合静态量化。最终选择仍要以目标硬件上的实测和精度预算为准。

官方参考地址:https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html

触发信号:两种量化结果不能直接横比

动态量化会根据当前前向输入在线计算激活量化参数,因此每个批次可能承担额外的参数计算成本;静态量化先用校准数据运行模型,把激活参数作为常量保存,推理时直接使用。权重的量化方式、模型预处理、执行提供者和线程配置如果不一致,最后的差异就不能归因于“动态”或“静态”。

比较时先固定四件事:原始模型文件、输入预处理、batch 形状和 Execution Provider。模型还必须使用 opset 10 或更高版本。ONNX Runtime 官方文档说明,量化工具主要处理 8 位线性量化,并提供 quantize_dynamic() 与 quantize_static() 两类 Python API。

FP32 模型经过预处理后进入动态量化或静态量化,最终交给 ONNX Runtime 推理的静态结构说明图
图1:动态量化与静态量化在模型、量化参数和推理边界上的关系说明图,不是截图或运行证据。

快速判断:先锁定模型、输入和执行后端

先把输入组织成可重复读取的批次。静态量化需要校准集,但校准集不应被当成训练集:它的任务是覆盖线上常见的激活范围,而不是更新模型参数。校准样本应尽量使用与线上一致的预处理、长度分布和类别比例;如果样本过窄,静态量化的激活范围可能不代表真实流量。

from pathlib import Path
import numpy as np
import onnxruntime as ort

# 量化脚本只创建一次会话,避免在每个批次里重复加载模型。
model_path = Path("model.onnx")
session = ort.InferenceSession(
    str(model_path),
    providers=["CPUExecutionProvider"],  # 对照实验中固定执行后端
)
input_name = session.get_inputs()[0].name

def calibration_reader(samples, batch_size=8):
    # 固定批大小,保证动态量化和静态量化使用相同输入形状。
    for start in range(0, len(samples), batch_size):
        batch = np.asarray(samples[start:start + batch_size], dtype=np.float32)
        yield {input_name: batch}

# 生产代码中将 samples 替换为脱敏后的真实分布样本。
for feed in calibration_reader(samples):
    _ = session.run(None, feed)  # 这里只示意校准输入的形状与读取方式

上面的读取器只展示批次边界,不代表已经在本机运行。真正生成静态模型时,需要把同样的读取逻辑接到 CalibrationDataReader,并确保校准输入与部署输入的 dtype、布局和维度约定一致。

处理步骤:分别生成动态模型和静态模型

动态量化的最小路径不需要校准数据,适合先快速得到一个对照模型。静态量化则需要校准读取器,并选择 MinMax、Entropy 或 Percentile 等校准方法。两份输出文件应使用不同路径,避免后续测试时把模型文件混淆。

from onnxruntime.quantization import (
    CalibrationDataReader,
    QuantType,
    QuantFormat,
    quantize_dynamic,
    quantize_static,
)

fp32_path = "model.onnx"
dynamic_path = "model.dynamic.int8.onnx"
static_path = "model.static.int8.onnx"

# 动态量化在推理时计算激活参数,先生成独立的对照文件。
quantize_dynamic(
    model_input=fp32_path,
    model_output=dynamic_path,
    weight_type=QuantType.QInt8,
)

class Reader(CalibrationDataReader):
    def __init__(self, batches):
        self._batches = iter(batches)

    def get_next(self):
        # 读完返回 None,量化器才能结束校准读取。
        return next(self._batches, None)

# batches 必须来自与线上一致的预处理,不能用动态生成的随机数据代替。
reader = Reader(calibration_batches)
quantize_static(
    model_input=fp32_path,
    model_output=static_path,
    calibration_data_reader=reader,
    quant_format=QuantFormat.QDQ,
    activation_type=QuantType.QUInt8,
    weight_type=QuantType.QInt8,
)

静态模型的好处是推理阶段不必为每个输入重新估计激活参数,但代价是需要维护校准数据和校准过程。若量化后精度下降,应先比较浮点模型与量化模型的权重、激活差异,再决定更换校准方法、调整量化范围或排除特定张量;不要直接把一次低分归咎于 batch 大小。

处理步骤:让批处理循环复用资源

公平比较性能时,量化方法之外的 Python 分配也要控制住。常见浪费包括每个 batch 重建 InferenceSession、反复创建输入字典、把列表多次转换成数组,以及为了记录结果复制整块输出。可复用的最小结构是:会话只创建一次,输入数组按固定形状复用,输出只保留当前比较需要的字段。

import numpy as np
import onnxruntime as ort

def run_batches(model_path, batches, input_name):
    # 会话和输入容器都放在循环外,避免每次推理重复分配。
    session = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
    reusable_input = {input_name: None}
    latency_records = []

    for batch in batches:
        # 只在形状变化时重新生成数组;固定 batch 时可直接复用缓冲区。
        reusable_input[input_name] = np.asarray(batch, dtype=np.float32)
        outputs = session.run(None, reusable_input)
        # 只提取评估所需结果,避免长期保存整个输出对象。
        latency_records.append(outputs[0].shape)

    return latency_records

# 动态模型和静态模型应分别调用同一个函数,保持循环结构一致。
dynamic_shapes = run_batches("model.dynamic.int8.onnx", batches, "input")
static_shapes = run_batches("model.static.int8.onnx", batches, "input")

如果输入来自固定大小的 NumPy 缓冲区,可以在循环外分配目标数组,再用切片或 np.copyto 更新内容;如果最后一个批次大小不同,则应单独处理尾批,不能为了复用而错误填充数据。这里的“减少额外分配”是减少测试噪声和垃圾回收压力,不等于保证所有 Execution Provider 都会原地复用底层内存。

DataReader、校准批次、输入缓冲区、量化会话、输出缓冲区和比较指标之间的静态关系说明图
图2:批处理输入、会话复用、输出对象和比较指标的结构说明图,不是截图或运行证据。

告警确认:不要只用一次耗时决定部署方案

建议为浮点、动态量化、静态量化三份模型建立同一份测试表。先预热,再记录稳定批次的 p50/p95 延迟、吞吐、峰值内存和分配行为;同时用固定评估集计算任务指标。对于分类模型,至少记录准确率或 F1;对于生成或检测模型,则使用该任务原本的质量指标。示例表格只定义口径,不填入未经运行得到的数字。

维度动态量化静态量化比较时的注意点
激活参数每次前向在线计算校准后离线固定在线开销与校准代表性要分开看
数据准备不要求校准集需要校准读取器和样本样本分布会影响精度
适用倾向Transformer、RNN 可优先试CNN 或激活范围稳定时可优先试模型结构不是绝对规则
部署成本流程简单,推理有额外计算流程多一步,推理更可预测必须在目标硬件实测

硬件也会改变结论。官方文档指出,支持 VNNI、Tensor Core INT8 或 Arm 点积指令的平台更可能从量化中获得收益;老硬件可能因为缺少高效指令和量化/反量化开销而变慢。若静态和动态都达不到精度目标,可以回到原框架做量化感知训练,再转换为 ONNX。

回滚路径与复盘:按模型、硬件和数据做选择

如果目标是快速得到一个可部署的 INT8 对照,先用动态量化;如果服务有稳定的线上样本、对推理时延更敏感,且愿意维护校准流程,静态量化更值得投入。两者都应保持相同的模型预处理和批处理循环,避免把输入转换、会话创建或输出拷贝的成本混进量化方法本身。

常见追问:为什么量化后模型变小,却没有变快?

模型体积减少只说明存储或内存压力可能下降,实际速度还取决于执行后端是否有高效 INT8 内核、算子是否被支持,以及量化和反量化开销是否超过计算收益。

常见追问:静态量化一定比动态量化精度低吗?

不一定。静态量化依赖校准集覆盖真实激活范围;校准集合适时可以获得稳定结果,集合不合适时才更容易出现明显精度损失。应使用同一评估集比较,而不是凭方法名称判断。

常见追问:怎样避免批处理测试被分配抖动干扰?

让 InferenceSession 在循环外创建,固定 batch 形状,复用输入容器,预热后再记录多批结果,并把尾批单独统计。这样能减少 Python 层噪声,但仍需结合目标执行提供者的内存行为分析。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>