你搬过家吗?冬天的羽绒服蓬松松的,直接塞进纸箱,一箱只能装三件;但套上真空压缩袋,用吸尘器一抽——砰,变成一块硬邦邦的“饼干”,同样的箱子能塞进去十件。衣服还是那些衣服,只是把里面的空气挤掉了。AI 模型部署时也面临同样的“搬家难题”:一个训练好的模型默认用 FP32(32 位浮点数)存储权重,动辄几百兆字节,塞进手机、摄像头、车载芯片这些“小户型”里非常吃力。ONNX Runtime 的量化支持,就是给模型准备的一套“真空压缩袋”:把 FP32 模型压缩成 INT8(8 位整数),体积直接缩到四分之一,推理时内存带宽压力大减,速度明显变快。本文就用“搬家打包”这个比喻,带你看懂 ONNX Runtime 里动态量化、静态量化、校准工具这些“打包神器”怎么工作,以及什么时候该用哪一个。

岗位关联

量化是 AI 推理部署中最实用的“瘦身”手段之一,掌握它能让你在资源受限场景中游刃有余,是推理优化工程师的必备技能。

目录
  1. 可视化演示
  2. 量化初识
  3. 动态量化
  4. 静态量化
  5. 校准工具
  6. 量化优势
  7. 精度挑战
  8. ORT 配置
  9. 应用场景
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

三步展示ONNX Runtime量化:QuantizeLinear/DequantizeLinear算子映射(FP32→INT8)、动态/静态/QAT三种量化方式、以及MinMax/Entropy/Percentile精度校准方法。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Q/DQ算子映射机制 FP32: 浮点权重 Q: 量化算子 INT8: 低精度激活 y=round(x/scale)+zero_point 降低内存占用并加速推理。

量化初识

搬家时最占地方的往往不是衣服本身,而是衣服里裹着的空气。羽绒服套上压缩袋一抽气,体积只剩四分之一。AI 模型里的 FP32 权重,就是一件件“充满空气的羽绒服”——每个参数占 4 字节,一个大模型的参数动辄上亿,搬起来又重又慢。量化的本质,就是把这 4 字节的浮点数,映射成 1 字节的整数:32 位浮点能表达约 40 亿个不同数值,而 8 位整数只有 256 个取值,就像压缩袋不能把羽绒服压成龟壳,量化也会丢掉一部分“空气”,但保暖本质还在。

ONNX Runtime 的量化模块做的正是这件事:读入 FP32 模型,为每个张量找到一对“压缩刻度”,把浮点权重批量转换成 INT8。这里的“压缩刻度”就是量化领域的两个核心参数:缩放因子 s 和零点 z。s 决定了每个整数刻度代表多大范围的浮点数,z 负责把浮点分布的起点对齐到整数刻度上。s 太大,压缩得狠但精度低;s 太小,精度高却省不了多少空间——这正是压缩袋“抽气量”的旋钮。

量化映射的标准公式如下:

回到行李箱前:FP32 是原封不动的羽绒服,INT8 是压成饼的羽绒服。量化初识告诉你,压缩袋存在、怎么用、以及压缩的代价是什么。记住一个概念:模型体积与内存占用由 s 和 z 的配合决定,精度损失则来自压缩回不去的“空气”。

q=round⁡(rs+z),r^=s(q−z)\displaystyle q = \operatorname{round}\left(\frac{r}{s} + z\right), \quad \hat{r} = s(q - z)
这个公式是量化世界的“压缩袋说明书”:左边把浮点数 r 变成整数 q,右边把整数 q 还原成近似浮点数 \hat{r},中间由缩放因子 s 与零点 z 决定压缩强度和对齐方式。
  1. 缩放因子 ss 相当于压缩袋的“刻度尺”,决定一个整数单位代表多大范围的浮点数。
    s=rmax⁡−rmin⁡2b−1\displaystyle s = \frac{r_{\max} - r_{\min}}{2^b - 1}
  2. 零点 zz 是整数坐标的“起点”,让量化区间对齐浮点分布的起始位置。
    z=round⁡(−rmin⁡s)\displaystyle z = \operatorname{round}\left(-\frac{r_{\min}}{s}\right)
  3. 量化映射将浮点数 r 除以 s、加上 z,再四舍五入,得到整数 q。
    q=round⁡(rs+z)\displaystyle q = \operatorname{round}\left(\frac{r}{s} + z\right)
  4. 反量化还原从整数 q 推回近似浮点值,压缩袋展开后虽然不如原样蓬松,但大体可恢复。
    r^=s(q−z)\displaystyle \hat{r} = s(q - z)

动态量化

有些搬家是“临时起意”:到了新家才发现衣柜不够用,于是跑去楼下超市买几个压缩袋,把最占地方的羽绒被紧急压一压。动态量化就是这样的“临时压缩”——模型部署好之后,在每次推理运行时才动手压缩数据,主打一个快速落地、不用提前准备。

在 ONNX Runtime 中,动态量化把权重(模型的“大型衣物”)在部署前离线量化一次,而激活值(推理过程中各层传递的中间数据,相当于“当天要穿的衣服”)则在每次推理时动态统计 min/max,计算缩放因子再量化。这样做的好处是权重只压一次、开销小,且无需准备校准数据集;代价是每次推理都要额外统计激活范围,带来少量运行时开销。对于 CPU 上以权重搬运为主的模型,性价比非常高。

动态量化的权重缩放与激活动态缩放公式如下:

动态量化像是搬家当天的“超市救急袋”:省去了提前规划,压缩效果却是“够用就好”。它不追求极致压缩率,但胜在接入简单、无须额外数据。当你手里只有一个 FP32 模型、又希望立刻在 CPU 上看到速度提升时,动态量化就是那包最值得买的应急压缩袋。

sw=max⁡(∣w∣)127,qw=round⁡(wsw)\displaystyle s_w = \frac{\max(|w|)}{127}, \quad q_w = \operatorname{round}\left(\frac{w}{s_w}\right)
sa(t)=max⁡(∣a(t)∣)127,qa(t)=round⁡(a(t)sa(t))\displaystyle s_a^{(t)} = \frac{\max(|a^{(t)}|)}{127}, \quad q_a^{(t)} = \operatorname{round}\left(\frac{a^{(t)}}{s_a^{(t)}}\right)
权重只看一次“绝对值最大值”就定下刻度;激活则在每一轮推理时临时测量,像每天出门前才决定今天穿什么、要不要加外套。127 是 INT8 正半轴能表示的极限刻度。
  1. 权重缩放因子用权重绝对值的最大值确定步长,127 是 INT8 正数部分的最大刻度。
    sw=max⁡(∣w∣)127\displaystyle s_w = \frac{\max(|w|)}{127}
  2. 权重量化权重值除以缩放因子取整,实现一次性的离线压缩。
    qw=round⁡(wsw)\displaystyle q_w = \operatorname{round}\left(\frac{w}{s_w}\right)
  3. 激活动态缩放每次推理时,根据当前激活张量的最大值临时计算缩放因子。
    sa(t)=max⁡(∣a(t)∣)127\displaystyle s_a^{(t)} = \frac{\max(|a^{(t)}|)}{127}
  4. 激活量化用临时缩放因子将激活换算成整数,完成运行时的“紧急压缩”。
    qa(t)=round⁡(a(t)sa(t))\displaystyle q_a^{(t)} = \operatorname{round}\left(\frac{a^{(t)}}{s_a^{(t)}}\right)

静态量化

专业的搬家团队从不到了现场才想对策。他们出发前就用电动压缩泵把衣物一件件压好、贴上标签,新家开箱直接归位。静态量化就是“出发前全部压好”的极致方案:在部署前用一批代表性数据校准模型,把每层激活值的 min/max 提前算清楚,推理时直接用预计算好的刻度,全程零额外统计开销。

静态量化的关键一步是“预计算激活范围”:拿一批校准数据跑一遍模型,记录每一层激活张量的最大值与最小值,然后由这些统计量直接算出缩放因子 s 和零点 z。推理阶段遇到量化算子时,只需读配置好的参数查表换算,不必再动态测量。ONNX Runtime 内部还会把相邻的量化/反量化算子融合成 QDQ(Quantize-Dequantize)模式,进一步减少运行时运算。

静态量化的激活量化公式如下:

静态量化像是提前用电动泵压好的行李箱:打包时费了点功夫,但到了新家、上了路,每一步都轻快自如。相比动态量化,静态量化在推理阶段没有任何“临时测量”的额外开销,是追求极致低延迟部署时的首选。

sa=amax⁡−amin⁡255,za=round⁡(−amin⁡sa),qa=round⁡(asa+za)\displaystyle s_a = \frac{a_{\max} - a_{\min}}{255}, \quad z_a = \operatorname{round}\left(-\frac{a_{\min}}{s_a}\right), \quad q_a = \operatorname{round}\left(\frac{a}{s_a} + z_a\right)
校准数据就是搬家前的“衣柜尺寸图”:先量好每一层激活的边界,再据此定刻度。255 是 INT8 从最小值到最大值之间的总刻度数。
  1. 校准激活范围用校准数据统计每层激活张量的全局最小值与最大值。
    amin⁡=min⁡x∈caliba(x),amax⁡=max⁡x∈caliba(x)\displaystyle a_{\min} = \min_{x \in \text{calib}} a(x), \quad a_{\max} = \max_{x \in \text{calib}} a(x)
  2. 计算激活缩放因子用最大值与最小值的差除以 255,得到每个整数刻度的步长。
    sa=amax⁡−amin⁡255\displaystyle s_a = \frac{a_{\max} - a_{\min}}{255}
  3. 计算激活零点将浮点最小值对齐到整数刻度上,得到零点偏移。
    za=round⁡(−amin⁡sa)\displaystyle z_a = \operatorname{round}\left(-\frac{a_{\min}}{s_a}\right)
  4. 量化激活推理时按预计算参数直接将激活转成 INT8。
    qa=round⁡(asa+za)\displaystyle q_a = \operatorname{round}\left(\frac{a}{s_a} + z_a\right)

校准工具

搬家前,有经验的管家会做两件事:称一称每个箱子的重量,量一量新家衣柜的尺寸,然后决定哪些衣服要压、压到什么程度。校准工具就是量化世界的“卷尺和秤”——它用具有代表性的数据去试跑模型,收集每一层激活值的分布,据此找到最优的量化刻度。

ONNX Runtime 的校准工具围绕“校准数据集”展开。校准数据必须贴近模型真实的线上输入分布:做物体检测就抽真实监控截图,做语音识别就抽真实麦克风音频。拿到数据后,工具会记录每层激活值的直方图,然后尝试不同的截断阈值 T,选出让量化前后分布差异最小的那一个。常见方法有 MinMax(直接用最大最小值)、Percentile(用分位数排除极端离群值)以及 Entropy(基于 KL 散度选阈值)。

Entropy 校准的核心公式如下:

校准工具是在告诉你:不要闭着眼乱压,先量再压。校准数据选得好,量化后精度损失可能只有 0.5%;选得差,可能直接掉 5 个点。它是静态量化前最重要的一道工序,也是 ONNX Runtime 量化工具链中被低估的核心组件。

T∗=arg⁡min⁡TDKL(PT∥QT),DKL(P∥Q)=∑iP(i)log⁡(P(i)Q(i))\displaystyle T^* = \arg\min_{T} D_{KL}(P_T \parallel Q_T), \quad D_{KL}(P \parallel Q) = \sum_{i} P(i) \log \left(\frac{P(i)}{Q(i)}\right)
通俗地说:先把原始激活分布看成一件羽绒服的“蓬松版型”,量化后看成“压缩版型”,KL 散度就是两个版型之间的“版型差”。校准工具的目标,是找到最合适的压缩力度 T,让压完的版型与原始版型差异最小。
  1. 候选阈值 T将激活分布从两侧截断,只保留中间主要部分,T 是截断边界。
    PT(i)=P(i)∑j≤TP(j),for i≤T\displaystyle P_T(i) = \frac{P(i)}{\sum_{j \le T} P(j)}, \quad \text{for } i \le T
  2. 量化后分布 Q_T截断后的分布经过量化再反量化,得到一个 k 个桶的近似分布。
    QT(k)=∑i∈bin(k)PT(i)\displaystyle Q_T(k) = \sum_{i \in \text{bin}(k)} P_T(i)
  3. KL 散度计算逐个桶计算原始分布与量化分布的差异并求和。
    DKL(PT∥QT)=∑kPT(k)log⁡(PT(k)QT(k))\displaystyle D_{KL}(P_T \parallel Q_T) = \sum_{k} P_T(k) \log \left(\frac{P_T(k)}{Q_T(k)}\right)
  4. 选择最优阈值遍历所有候选 T,取 KL 散度最小者作为最终量化范围。
    T∗=arg⁡min⁡TDKL(PT∥QT)\displaystyle T^* = \arg\min_{T} D_{KL}(P_T \parallel Q_T)

量化优势

压完的行李箱有多能装?同样是 24 寸箱子,原先只能塞两件羽绒服,现在能塞八件。托运费用省了,到了新家打开箱子找衣服也更快——因为每件衣服都叠得整整齐齐,一目了然。量化带来的优势同样直接:内存占用、模型体积、推理延迟、能耗,全面受益。

最直观的收益是内存缩减。FP32 每个参数 4 字节,INT8 每个参数 1 字节,光参数部分就小了 75%。更关键的是推理时的内存带宽:CPU/GPU 读取重量参数的速度往往远慢于计算速度,参数小了四倍,等数据的时间也大致缩短四倍。数据搬运减少了,缓存命中率提高,推理延迟自然下降。对内存带宽受限的模型,量化后的端到端提速甚至可以接近 4 倍,而非理论上的 3-5%。

内存缩减率的公式如下:

量化优势就像压缩袋的容积魔法:同样一只行李箱装下更多东西,托运更省钱、开箱更快、走路更轻快。落在部署指标上,就是内存占用降低 75%、访问延迟缩短、单位时间能处理更多请求。更重要的是,它把“装进口袋”变成可能——许多模型量化后首次能塞进手机和边缘设备。

SR=1−N×1 byteN×4 bytes=1−14=75%\displaystyle \text{SR} = 1 - \frac{N \times 1 \text{ byte}}{N \times 4 \text{ bytes}} = 1 - \frac{1}{4} = 75\%
公式非常直白:N 个参数从每个 4 字节换成每个 1 字节,总字节数变为原来的四分之一,省下 75% 内存。这就是压缩袋的“容积奇迹”。
  1. FP32 参数字节数N 个 FP32 参数共占 4N 字节。
    MFP32=N×4 bytes\displaystyle M_{\text{FP32}} = N \times 4 \text{ bytes}
  2. INT8 参数字节数N 个 INT8 参数共占 N 字节。
    MINT8=N×1 byte\displaystyle M_{\text{INT8}} = N \times 1 \text{ byte}
  3. 节省率计算用 1 减去两者之比,得到内存缩减比例。
    SR=1−MINT8MFP32=1−14=75%\displaystyle \text{SR} = 1 - \frac{M_{\text{INT8}}}{M_{\text{FP32}}} = 1 - \frac{1}{4} = 75\%

精度挑战

压缩袋用多了也有翻车时刻:羽绒服压得太狠、压得太久,里面的羽绒打结出死褶,再也回不到当初的蓬松度。量化同样面临“压过头”的风险:当缩放因子 s 被个别极端值撑得过大,或者量化粒度太粗时,模型精度就会明显下滑,推理结果“皱巴巴”的不可用。

量化误差来源于几个方面。首先是最基本的取整误差:每个浮点数被四舍五入到最近的整数刻度,最多偏差半个步长 s/2。其次是离群值问题:某个激活值特别大时,会撑大整个张量的 s,导致绝大多数普通值被压到很粗的刻度上。第三是粒度选择:per-tensor 用全张量一个刻度,per-channel 则为每个通道单独配刻度,后者精度更好但开销稍大。面对这些问题,ONNX Runtime 支持 per-channel 量化、混合精度(敏感层保持 FP16/FP32),也可配合量化感知训练(QAT)让模型在训练时就适应量化噪声。

量化误差的核心公式如下:

精度挑战提醒我们:压缩袋不是越用力越好。如果校准数据太偏、量化粒度太粗、离群值没有提前处理,精度可能断崖式下跌。好在 ONNX Runtime 提供了多种“回弹手段”——per-channel、混合精度、QAT,让你在压缩与质量之间找到微妙的平衡点。

∣r−r^∣≤s2,E=E[(r−r^)2]\displaystyle |r - \hat{r}| \le \frac{s}{2}, \quad E = \mathbb{E}\left[(r - \hat{r})^2\right]
绝对误差的“最大褶皱”是半个步长 \frac{s}{2},而整个模型的期望误差是所有参数平方误差的平均。s 越大,褶皱越深,误差越大。
  1. 反量化值量化后的整数 q 经缩放还原为近似浮点值 \hat{r}。
    r^=s(q−z)\displaystyle \hat{r} = s(q - z)
  2. 绝对误差上界四舍五入最多偏出半步长,误差被限制在 s/2 以内。
    ∣r−r^∣≤s2\displaystyle |r - \hat{r}| \le \frac{s}{2}
  3. 期望平方误差对所有权重取误差平方的期望,衡量整体精度损失。
    E=E[(r−r^)2]\displaystyle E = \mathbb{E}\left[(r - \hat{r})^2\right]

ORT 配置

把压缩袋买回家还不够,你得知道怎么操作它:拉链拉到哪里、阀门怎么接吸尘器、压到什么程度停止。ONNX Runtime 的量化 API 就是这套“操作说明书”,你只需要在代码里调用两个核心接口,剩下的交给工具链完成。

第一个接口是 quantize_dynamic,对应动态量化:传入 FP32 模型路径,指定输出路径和权重类型,一行代码完成转换,全程不需要校准数据。第二个接口是 quantize_static,对应静态量化:除了模型路径,还要传入一个校准数据读取器(CalibrationDataReader),并可通过 quant_format 参数选择量化格式——QDQ 模式会插入 Quantize/Dequantize 算子对,更利于推理引擎融合优化;QOperator 模式则直接替换算子,部署更精简但优化空间略小。转换完成后用 onnxruntime.InferenceSession 加载新模型即可,外部推理代码完全不用改。

QDQ 两个算子的数学语义如下:

ORT 配置就是把“压缩袋使用说明”背熟:动态量化一枝笔就能开工,静态量化要配合校准读取器,量化格式选 QDQ 还是 QOperator 取决于你对优化与部署体积的取舍。配置文件本身不复杂,复杂的是理解每个参数背后的“压缩逻辑”,也就是前面所有章节的知识。

q=clip⁡(⌊rs+z⌉, 0, 255),r^=s(q−z)\displaystyle q = \operatorname{clip}\left(\left\lfloor \frac{r}{s} + z \right\rceil,\ 0,\ 255\right), \quad \hat{r} = s(q - z)
Q(Quantize)负责把浮点数压成整数并截断到合法区间,DQ(Dequantize)负责把整数展开回浮点。推理引擎看到这样一对算子,会自动做融合优化,就像吸尘器把压缩袋抽紧后,拉链一拉就固定住了。
  1. QuantizeLinear先缩放取整,再截断到 0 到 255 的整数区间。
    q=clip⁡(⌊rs+z⌉, 0, 255)\displaystyle q = \operatorname{clip}\left(\left\lfloor \frac{r}{s} + z \right\rceil,\ 0,\ 255\right)
  2. DequantizeLinear将整数按同一缩放参数还原为浮点近似值。
    r^=s(q−z)\displaystyle \hat{r} = s(q - z)
  3. QDQ 融合相邻的量化与反量化算子在优化阶段相互抵消,减少运行时计算。
    Dequantize⁡(Quantize⁡(r))≈r\displaystyle \operatorname{Dequantize}(\operatorname{Quantize}(r)) \approx r

应用场景

户外爱好者都懂“轻装原则”:牙膏带旅行装、毛巾用速干压缩巾、帐篷睡袋全部真空压缩,因为每多一克重量都要用体力买单。量化之后的小模型,正是 AI 世界的“轻装装备”——让模型真正走进手机、摄像头、车载盒子这些资源有限的“户外环境”。

最典型的应用是边缘设备部署。智能摄像头要在 2GB 内存的嵌入式板卡上实时跑目标检测,原版模型 300MB 直接爆内存,量化后 75MB 轻松装下,推理速度还翻了倍。其次是实时在线服务:推荐系统每天处理千万级请求,延迟每增加 10ms 都是成本,量化后的模型内存带宽压力减小,服务吞吐量显著提升。还有车载语音助手这类硬实时场景,内存和功耗都有严格预算,量化几乎是唯一能在有限芯片上满足延迟要求的方案。

部署可行性最终由两个硬约束决定:

应用场景是量化故事的高潮:从云端机房到边缘设备,从大服务器到小芯片,量化让 AI 像轻装背包客一样走得更远。无论是移动端 App、工业质检相机还是车载盒子,只要你面对资源受限的环境,ONNX Runtime 的量化支持就是你行囊里那只最值得信赖的真空压缩袋。

Latency≤Tdeadline,Memory≤Mdevice\displaystyle \text{Latency} \le T_{\text{deadline}}, \quad \text{Memory} \le M_{\text{device}}
延迟必须小于业务红线,内存必须小于设备上限。量化正是让这两个不等式同时成立的最常用手段——把指标压进预算线内。
  1. 延迟约束单次推理耗时不能超过业务设定的截止时间。
    Latency≤Tdeadline\displaystyle \text{Latency} \le T_{\text{deadline}}
  2. 内存约束模型运行时占用的内存峰值不能超过设备物理内存。
    Memory≤Mdevice\displaystyle \text{Memory} \le M_{\text{device}}
  3. 综合判定两个约束同时满足,模型才具备部署条件。
    Deployable  ⟺  Latency≤Tdeadline∧Memory≤Mdevice\displaystyle \text{Deployable} \iff \text{Latency} \le T_{\text{deadline}} \wedge \text{Memory} \le M_{\text{device}}

AI 应用场景

智能摄像头改造

阶段:部署优化与推理加速

解决的问题:一款工厂质检摄像头原本需要在 8GB 内存的工控机上跑 FP32 目标检测模型,帧率仅 15 FPS,且主机价格昂贵。通过 ONNX Runtime 静态量化将模型从 FP32 转为 INT8,内存占用从 280MB 降至 70MB,帧率提升到 45 FPS,硬件成本下降一半。

具体案例:某视觉检测公司用 quantize_static 配合 500 张真实产线照片做校准,将 YOLO 风格检测模型量化后部署到 Jetson Nano 设备上。校准数据覆盖白天、夜间、逆光等工况,量化后 mAP 只下降了 0.8%,而推理延迟从 62ms 降到 24ms。

在线推荐系统提速

阶段:在线推理服务

解决的问题:推荐服务每天亿级请求,模型权重搬运成为瓶颈。动态量化无需校准数据,一行代码即可让模型体积缩小 4 倍,内存带宽压力大幅缓解。

具体案例:某电商平台对召回阶段的排序模型执行 quantize_dynamic,权重从 FP32 转为 INT8 后,单次推理的 P99 延迟从 35ms 降至 18ms,服务吞吐量提升约 1.9 倍,且无需重新训练或准备额外数据。

车载语音助手落地

阶段:嵌入式模型压缩

解决的问题:车载语音助手运行在车机芯片上,内存预算仅 256MB、功耗严格受限。利用静态量化加混合精度策略,将敏感层保留 FP32,其余层量化 INT8,既满足内存预算又保住唤醒词识别率。

具体案例:某车厂将语音唤醒模型从 55MB 压缩到 15MB,内存峰值从 120MB 降至 42MB,唤醒率仅下降 0.3%,满足车规级实时响应要求。

Python 演示

python81 行
import numpy as npimport onnxfrom onnx import TensorProto, helperimport onnxruntimefrom onnxruntime.quantization import (    CalibrationDataReader,    QuantFormat,    QuantType,    quantize_dynamic,    quantize_static,)  def build_demo_model():    """构建一个两层全连接 FP32 模型用于演示"""    x_info = helper.make_tensor_value_info("input", TensorProto.FLOAT, [1, 4])    y_info = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1, 2])     w1 = helper.make_tensor(        "w1", TensorProto.FLOAT, [4, 8],        np.random.randn(4, 8).astype(np.float32).flatten(),    )    w2 = helper.make_tensor(        "w2", TensorProto.FLOAT, [8, 2],        np.random.randn(8, 2).astype(np.float32).flatten(),    )    nodes = [        helper.make_node("MatMul", ["input", "w1"], ["fc1"], name="matmul1"),        helper.make_node("Relu", ["fc1"], ["act1"], name="relu1"),        helper.make_node("MatMul", ["act1", "w2"], ["output"], name="matmul2"),    ]    graph = helper.make_graph(        nodes, "demo_mlp", [x_info], [y_info], initializer=[w1, w2],    )    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])    model.ir_version = 8    return model  class DemoCalibReader(CalibrationDataReader):    """校准数据读取器:每次返回一个样本字典,供静态量化使用"""     def __init__(self, samples):        self._samples = samples        self._index = 0     def get_next(self):        if self._index < len(self._samples):            data = self._samples[self._index].astype(np.float32)            self._index += 1            return {"input": data}        return None  def main():    onnx.save(build_demo_model(), "demo_fp32.onnx")     calib_samples = [np.random.randn(1, 4).astype(np.float32) for _ in range(16)]     quantize_static(        "demo_fp32.onnx",        "demo_int8_static.onnx",        DemoCalibReader(calib_samples),        quant_format=QuantFormat.QDQ,    )     quantize_dynamic(        "demo_fp32.onnx",        "demo_int8_dynamic.onnx",        weight_type=QuantType.QInt8,    )     test_data = np.random.randn(1, 4).astype(np.float32)    for name in ["demo_fp32.onnx", "demo_int8_static.onnx", "demo_int8_dynamic.onnx"]:        sess = onnxruntime.InferenceSession(name, providers=["CPUExecutionProvider"])        out = sess.run(None, {"input": test_data})[0]        print(f"{name}: {out[0]}")  if __name__ == "__main__":    main()
目的:演示 ONNX Runtime 中动态量化与静态量化的完整流程——构建 FP32 模型、准备校准数据、执行两种量化、对比推理输出。
  1. model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])生成一个 opset 13 的 ONNX 模型,确保量化工具支持。
  2. class DemoCalibReader(CalibrationDataReader)继承 ORT 的校准读取器协议,get_next() 每次返回一个输入字典。
  3. quantize_static(..., DemoCalibReader(calib_samples), quant_format=QuantFormat.QDQ)静态量化需要校准数据预计算激活范围,QDQ 格式利于推理优化。
  4. quantize_dynamic("demo_fp32.onnx", "demo_int8_dynamic.onnx", weight_type=QuantType.QInt8)动态量化只需一行,权重转为 INT8,无需校准。
  5. sess.run(None, {"input": test_data})[0]量化后的模型直接用同一套推理代码,部署侧零改动。

常见误区

误区很多人听说量化把 4 字节变成 1 字节,第一时间想到“精度肯定崩了”,于是宁愿扛着大模型也不用量化。

正解神经网络对参数的冗余度有很强的容忍力,好比羽绒服压缩后抖一抖还能恢复蓬松。绝大多数模型在 INT8 量化后精度损失小于 1%,尤其配合校准良好的静态量化,RetNet、ResNet 这类模型甚至能做到几乎无损。真正该担心的不是量化本身,而是校准数据是否有代表性。

为何常见:把量化和“粗暴取整”划等号,忽略了缩放因子和零点这些补偿机制——压缩袋不只是简单压扁,还附带了“分层收纳”的精细设计。

误区新手常觉得“动态”听起来更智能,应该比“静态”更快,于是所有场景都无脑选动态量化。

正解恰恰相反,静态量化把激活的缩放参数在离线阶段全部算好,推理时零额外统计开销;动态量化每次推理都要现场计算激活的 min/max,反而多了一步。动态量化优势在“无需校准数据、接入快”,而不是“推理快”。追求极致低延迟时,静态量化才是首选。

为何常见:望文生义,“动态”这个词让人联想到灵活高效,忽略了它背后的运行时计算代价。

误区跑静态量化时,有人从测试集里随机抽两张图就当校准数据,“反正都是数,量出来不都差不多吗?”

正解校准数据决定了激活值 min/max 的统计质量。如果校准数据只覆盖了窄分布,真实线上数据一来,激活值直接超出量化范围,精度立刻崩掉。就像搬家前只量了主卧衣柜,到了新家才发现次卧衣柜矮半截。校准数据要覆盖典型工况,样本量通常是几百到几千条。

为何常见:校准过程在离线阶段透明不可见,用户只看到输出模型,容易低估数据分布对量化效果的决定性影响。

误区量化完模型文件变小了,推理也跑通了,就以为万事大吉直接上线。

正解量化是近似操作,离群值或某个敏感层可能导致特定输入精度崩溃。上线前必须用完整测试集评估量化前后精度,最好再做几条典型样本的逐层输出对比。压缩袋压完衣服,至少也要拎起来抖一抖检查有没有压坏哪件。

为何常见:模型体积下降和推理速度提升太直观,让人忽略了“压缩过程可能有损坏”这一面,而精度验证恰恰是量化工程中不可省略的一环。

面试题

Q1. ONNX Runtime 中动态量化最突出的特点是什么?

单选题 · easy

  • A.需要额外的校准数据集
  • B.权重离线量化激活在线计算← 正确答案
  • C.必须对模型进行重新训练
  • D.仅能用于 GPU 加速设备

答案:B。动态量化把权重在部署前离线量化,激活值在推理时动态计算 min/max,因此无需校准数据集。它不需要重训练,也不限于特定硬件。A、C、D 都是对动态量化特性的错误描述。

Q2. 将 FP32 模型量化为 INT8 后,模型参数体积大约缩小多少倍?

单选题 · easy

  • A.大约两倍左右
  • B.大约四倍左右← 正确答案
  • C.大约八倍左右
  • D.大约十六倍左右

答案:B。FP32 每个参数占 4 字节,INT8 占 1 字节,因此参数体积缩小 4 倍,内存节省 75%。选项中的 2 倍、8 倍、16 倍均不符合 4 字节到 1 字节的换算关系。

Q3. 静态量化与动态量化相比,根本区别是什么?

单选题 · mid

  • A.静态量化完全不需要离线准备
  • B.静态量化用校准数据预计算激活范围← 正确答案
  • C.静态量化只适用于文本分类模型
  • D.静态量化精度一定高于原始模型

答案:B。静态量化的核心是用校准数据在离线阶段确定每层激活的 min/max,从而预计算量化参数;动态量化则在推理时临时统计。A 说反了,C 的模型范围限制不存在,D 更是错误——量化是近似,精度不可能高于原始模型。

Q4. 下列哪些措施可以缓解量化带来的精度损失?

多选题 · mid

  • A.采用 per-channel 通道级量化粒度← 正确答案
  • B.对敏感层使用混合精度策略← 正确答案
  • C.取消校准流程直接进行量化
  • D.选用分布差异最大的校准集

答案:AB。per-channel 为每个通道单独配缩放因子,能显著降低粒度过粗带来的误差;混合精度让敏感层保持更高精度。C 取消校准会恶化量化,D 使用“差异最大”的校准集属于构造错误说法——校准集应具有代表性而非刻意差异化。

Q5. ONNX Runtime 的 weight_type=QuantType.QInt8 参数表示什么?

单选题 · mid

  • A.权重被量化为八位有符号整数← 正确答案
  • B.权重被量化为八位无符号整数
  • C.权重被量化为十六位浮点格式
  • D.权重保持三十二位浮点不变

答案:A。QInt8 明确表示 8 位有符号整数(范围 -128 到 127),常用于权重量化。QUInt8 才是无符号整数,FP16 是 16 位浮点,D 则是完全不量化的意思,均与 QInt8 语义不符。

Q6. Entropy 校准方法选择量化阈值的依据是什么?

单选题 · hard

  • A.最小化原始与量化激活分布的 KL 散度← 正确答案
  • B.最大化模型输出结果的熵值
  • C.随机挑选一个候选阈值进行量化
  • D.最小化权重矩阵的 L2 正则项

答案:A。Entropy 校准遍历候选截断阈值,计算原始激活分布与量化后分布的 KL 散度,选择最小者。它关注的是激活分布保真度,而非输出熵、随机选择或权重范数,因此 B、C、D 均不正确。

Q7. 关于缩放因子 s 与零点 z,下列说法正确的有?

多选题 · hard

  • A.s 由浮点范围与量化范围共同确定← 正确答案
  • B.z 使整数零对应浮点零偏移位置← 正确答案
  • C.s 数值越大代表量化精度越高
  • D.z 在对称量化中必须设为一半

答案:AB。s 是浮点范围除以量化级数(如 255)得到的步长,故 A 正确;z 负责把浮点最小值对齐到整数刻度,B 正确。s 越大步长越粗,精度反而越低,C 错误;对称量化中 z 通常取 0,而非一半,D 错误。

Q8. 动态量化在推理阶段哪一步产生了额外开销?

单选题 · easy

  • A.每个推理批次动态计算激活范围← 正确答案
  • B.模型文件加载阶段的磁盘读取
  • C.编译器优化阶段的图变换
  • D.校准阶段的数据集采集过程

答案:A。动态量化需要在每次推理时动态统计激活张量的 min/max,再计算缩放因子,这是它相对于静态量化的额外开销。B、C、D 描述的是加载、编译、校准阶段,不属于推理阶段的额外计算。

Q9. 下列哪一项符合量化部署在资源受限场景中的收益?

单选题 · mid

  • A.内存占用降低且推理延迟缩短← 正确答案
  • B.内存占用提升但精度明显改善
  • C.推理延迟上升但吞吐量提升
  • D.模型体积变大且兼容性增强

答案:A。量化最直接的收益就是参数从 4 字节变为 1 字节,内存占用大幅降低;数据搬运减少后延迟也通常缩短。B、C、D 描述的趋势与量化效果完全相反,均不符合。

Q10. 关于量化误差,下列说法正确的是?

多选题 · hard

  • A.离群值会撑大缩放因子导致精度下降← 正确答案
  • B.per-tensor 量化对所有模型精度影响相同
  • C.混合精度可保护部分敏感层免受损失← 正确答案
  • D.静态量化过程完全不会引入精度损失

答案:AC。个别极大的离群值会把 s 撑大,让主流数值落进更粗的刻度,精度下降,A 正确;混合精度让敏感层保持高位宽,C 正确。per-tensor 量化对不同分布模型的影响差异很大,B 错误;量化本身是近似操作,只是程度不同,D 说法过于绝对。