Skip to content

扩散模型量化:如何在 ONNX 中优雅地处理「重复 N 步」问题 #2

Description

@parallelarc

扩散模型量化:如何在 ONNX 中优雅地处理「重复 N 步」问题

当你需要让 Stable Diffusion 在手机或边缘设备上跑起来时,一个关键问题摆在面前:扩散模型需要采样几十甚至上百步,到底该怎么量化?本文将从工程实践角度,帮你理清三种主流方案,并给出明确的选型建议。


问题的诞生

以 Stable Diffusion 为代表的扩散模型,其核心是一个去噪网络(通常是 UNet)。在生成一张图的过程中,这个网络要被反复调用 N 次——每调用一次,就从噪声状态往前走一小步。

这个看似简单的「重复 N 次」,在模型部署时却带来了一个让人头疼的问题:

应该把 UNet 在 ONNX 图里复制 N 份然后一次性跑完,还是只导出一份然后在运行时用 for 循环反复调用?

这个问题看似简单,背后却涉及模型大小、推理性能、工程维护成本等多个维度。今天我们就来深入剖析。


两种方案的抽象对比

先让我们把问题抽象清楚,假设 N = 50 步:

方案 ONNX 图的结构 推理时的执行方式
方案 A:展开 N 份 把 UNet 拷贝 50 份,按时间步串联成一个大图 一次 Session.run 跑完整个图
方案 B:单步复用 ONNX 里只有一份 UNet 写个 for 循环,每次调用 Session.run 传新的 x_t

如果你正在考虑方案 A,先别急着动手——往下看。


方案 A:把模型展开 N 遍

看起来很美,实则问题重重

把模型展开 N 次,乍一看像是「一步到位」:不需要循环调用,减少了函数调用开销,多么优雅!

但现实很骨感。

首先是模型体积爆炸。 扩散模型本身就很大(UNet + VAE + Text Encoder),如果按 50 步展开,ONNX 文件大小直接乘以 50。加载时间、传输时间、磁盘占用都会成倍增加。更糟糕的是,过大的图会让可视化工具直接罢工,调试变成噩梦。

其次是维护成本极高。 产品中经常需要调整采样步数:极速模式 10 步、质量模式 30 步、细节模式 50 步。一旦把 N 固死在图里,想改步数就得重新导出整套 ONNX + 重新量化。这运维成本,任谁都得掂量掂量。

最后是性能并不占优。 很多人觉得减少 API 调用能提速,但现代推理引擎(ONNX Runtime、TensorRT、OpenVINO)对重复结构都有不错的融合和缓存优化,反而是图膨胀带来的缓存失效、调度开销可能抵消理论优势。在内存紧张的边缘设备上,这个问题尤为突出。

所以,方案 A 一般只用于「一次性学术实验」或「对固定步数做极端 profiling」的场景,不推荐作为工程实践。


方案 B:单步量化 + 运行时循环

当前业界的主流选择

这正是当前 Stable Diffusion + ONNX Runtime 部署教程采用的方式:

text_encoder.onnx  →  处理文本提示
unet.onnx          →  单步去噪(量化后)
vae_decoder.onnx   →  解码成最终图像

推理时在 Python/C# 中写采样循环,把 x_t(当前隐变量)和 t(当前时间步)一步步喂给量化后的 UNet。

为什么这是更合理的选择

量化更简单。 只需要对一个 UNet 做 PTQ/QAT,shape 推断更稳定,可以使用 ONNX Runtime 官方量化工具以及 AMD Quark、Intel Neural Compressor 等成熟工具链。

文件小,易部署。 ONNX 模型大小 ≈ 原 UNet 大小,适合移动端、边缘设备、Web 浏览器等资源受限场景。

灵活性最高。 步数由外层 for 循环控制,随时可以调参,轻松支持 DDIM、DPM-Solver 等不同调度器,甚至可以实现自适应终止(early stopping)。

性能可以通过工程手段补足。 使用 ONNX Runtime 的 IO Binding 避免重复内存拷贝,预热 session 消除首轮编译开销,在 GPU 上结合 CUDA Graph,循环开销大多可以摊平。

一个隐藏的陷阱:量化误差累积

但这里有个关键问题:扩散模型的量化误差会在多次去噪步骤中累积。早期步骤的小误差会沿着轨迹不断放大,导致最终图像质量下降。

解决方案包括:

  • 多步感知校准(如 AccuQuant):校准时模拟真实的多步采样轨迹,而不只是单步激活分布。
  • 时间步感知修正:为不同时间步设计不同的量化参数,在关键早期 timestep 更保守,后期更激进。

这些都是基于「单步网络 + 循环调用」设计的,和方案 B 完全兼容。


方案 C:ONNX 图内显式循环(进阶最佳实践)

用 Loop/Scan 算子封装循环

ONNX 标准提供了 LoopScan 运算符,用来在图内部表达循环。这是一种介于 A、B 之间,但综合优势更明显的做法。

结构特征:

  • 图里只有一份「单步 UNet 子图」,但这份子图被作为 Loop 的 body 反复执行 N 次;
  • 步数 N 可以是常量(图中写死,类似 A 但不复制结构),也可以是输入张量(运行时指定,实现动态步数)。

核心优势:

  1. 保持模型紧凑——不发生 N 倍结构复制,文件大小和方案 B 类似。
  2. 减少框架层调度开销——循环在 ONNX Runtime 内部执行,相比「外层 Python for + 内层 Session.run」,调用边界更少,内存复用和算子调度优化更容易做。
  3. 更利于高端推理后端优化——对 TensorRT、QNN、OpenVINO 等运行时,图内 Loop 可能被特殊对待,如展开少量迭代做 kernel fusion,或对循环体进行更强的运算图优化。
  4. 仍具备灵活性——可以通过输入控制步数,仍然是单一量化子网络。

什么时候不必用 Loop:

  • 如果部署环境不支持 ONNX Loop 或支持不佳;
  • 或者你的工程栈已经在 C++/Rust 侧对 for 循环做了充分优化(IO Binding + CUDA Graph)。

参考文献

[1] Quantize ONNX models - ONNX Runtime
[2] Inference Stable Diffusion with C# and ONNX Runtime
[3] How to Run Stable Diffusion with ONNX
[4] Quantizing a Diffusion Model using Quark - AMD
[5] ONNX Models Repository
[6] AccuQuant: Simulating Multiple Denoising Steps for Quantizing Diffusion Models
[7] Timestep-Aware Correction for Quantized Diffusion Models
[8] Q-Diffusion: Quantizing Diffusion Models - ICCV 2023
[9] Error Propagation Mechanisms and Compensation for Quantized Diffusion Models
[10] Loop - ONNX Operator Documentation
[11] Training and Optimizing ONNX Models with DSPy - ONNX Loop usage gist

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions