📑 本页目录(点开跳转)
04 · DDIM 与采样加速
⏱ 42 分钟 | ⭐ 为什么 50 步能顶 1000 步,以及 UI 上那个「种子」为什么真能复现
🎯 一句话
DDPM 每走一步都要掷一次骰子,所以一步都不能跳;DDIM 把骰子拿掉,采样变成一条确定的轨迹 —— 于是可以在时间步上跳着走,20~50 步就出图。
上一章讲的是训练:模型学会了「给一张带噪的图,说出里面掺了什么噪声」。 这一章讲生成:同一份权重,一个字都不用重训,怎么把 1000 次调用砍到 50 次。
🐌 一、慢在哪:不是图大,是步多
每一步都是一次完整的网络前向,而且步与步之间硬串行 —— 第 t 步的输出就是第 t−1 步的输入。 (同一批里的多张图倒是可以并排跑,所以「一次出 8 张」几乎不额外花时间。)
⭐ 于是出图耗时 ≈ 步数 × 单次前向。想快只有两条路:让每一步更便宜(下一章的潜空间),或者少走几步(这一章)。
🎲 二、DDPM 为什么一步都不能跳
DDPM 的反向过程是一条马尔可夫链:
$$x_{t-1} = \frac{1}{\sqrt{\alpha_t}}\left(x_t - \frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\,\epsilon_\theta(x_t,t)\right) + \sigma_t z,\qquad z\sim\mathcal N(0,I)$$
💡 人话:按模型预测的噪声把 $x_t$ 减掉一点,然后再重新掺一点全新的随机噪声回去。
末尾那个 $\sigma_t z$ 是关键 —— 它让每一步不是「算」出来的,而是从一个分布里抽出来的。 而这个分布的整套推导都建立在「$t$ 和 $t-1$ 相邻」上($\alpha_t$、$\beta_t$ 都是单步的量)。
⚠️ 所以你不能把式子里的 $t-1$ 改成 $t-20$ 当作跳了 20 步 —— 那样式子根本不成立。 1000 步不是保守,是这个公式自己要求的。
⏩ 三、DDIM 改了什么
⭐⭐ 先说最容易误会的一点:DDIM 不用重训,也不是另一个模型,它只是换了个采样器。
为什么能换得这么随便?因为训练只用到一个东西 —— 前向的边缘分布 $q(x_t\mid x_0)$。 DDIM 构造了一族非马尔可夫的前向过程,它们的 $q(x_t\mid x_0)$ 和 DDPM 完全一样, 只有「相邻两步之间怎么衔接」不同。训练目标看不见这个差别,所以同一份权重直接就能用。
📐 那一族过程是怎么构造出来的(想看再点)
DDPM 先定义单步加噪 $q(x_t\mid x_{t-1})$,再推出 $q(x_t\mid x_0)$。 DDIM 反过来:先钉死 $q(x_t\mid x_0)=\mathcal N(\sqrt{\bar\alpha_t}x_0,(1-\bar\alpha_t)I)$, 再去找所有满足它的联合分布。答案不唯一,是一族,由自由参数 $\sigma$ 索引:
$$q_\sigma(x_{t'}\mid x_t,x_0)=\mathcal N\!\left(\sqrt{\bar\alpha_{t'}}x_0+\sqrt{1-\bar\alpha_{t'}-\sigma^2}\cdot\frac{x_t-\sqrt{\bar\alpha_t}x_0}{\sqrt{1-\bar\alpha_t}},\;\sigma^2 I\right)$$
⭐ 这一族里没有一个成员用到「相邻」这个条件,$t'$ 可以是任何比 $t$ 小的步。
采样时的每一步,其实只有两个动作:
① 用当前的 $\epsilon$ 猜一张干净图(不管噪声水平多高都能算):
$$\hat x_0=\frac{x_t-\sqrt{1-\bar\alpha_t}\,\epsilon_\theta(x_t,t)}{\sqrt{\bar\alpha_t}}$$
② 把这张猜出来的图,按下一个噪声水平重新加噪回去:
$$x_{t'}=\sqrt{\bar\alpha_{t'}}\,\hat x_0+\sqrt{1-\bar\alpha_{t'}-\sigma^2}\,\epsilon_\theta(x_t,t)+\sigma z$$
💡 人话:每一步都先赌一把「最终这张图长什么样」,再退回到下一个噪声水平重新出发。
⭐ 看清楚:这两行里哪儿也没要求 $t'=t-1$。 第二步只需要知道目标噪声水平 $\bar\alpha_{t'}$ 是多少, $t'$ 想取 $t-20$ 就取 $t-20$。这就是能跳步的全部原因。
🎛️ 四、η:一个旋钮在 DDPM 和 DDIM 之间滑动
上面那个自由参数 $\sigma$,实践中写成一个更好用的形式($\eta$ 是随机性总开关,前面两个根号只是把它缩放到当前噪声水平该有的量级):
$$\sigma_{t\to t'}=\eta\sqrt{\frac{1-\bar\alpha_{t'}}{1-\bar\alpha_t}}\sqrt{1-\frac{\bar\alpha_t}{\bar\alpha_{t'}}}$$
| η | 是什么 | 后果 |
|---|---|---|
| 0 | ⭐ 确定性 DDIM | 整条轨迹没有一处随机;同一个初始噪声永远出同一张图;跳步基本不掉质量 |
| 0 ~ 1 | 中间地带 | 每步注一点随机,多样性回来一些,跳步的代价也回来一些 |
| 1 | 退回 DDPM | 和原来的 DDPM 采样等价 |
⚠️ η 越大,步数少了越吃亏:注进去的随机本来指望后面几百步慢慢洗掉,只给 20 步就洗不干净。 「少步数」和「高随机性」是一对矛盾,不能都要。
🔢 五、动手:把 1000 步的表抽成 50 步
不需要模型、不需要 GPU,几毫秒跑完:
# 把 1000 步的 DDPM 调度表,按 DDIM 的方式抽成 50 步
import torch
T = 1000
betas = torch.linspace(1e-4, 0.02, T) # DDPM 原论文的线性 beta 表
abar = torch.cumprod(1.0 - betas, dim=0) # abar[t] 就是公式里的 alpha_bar_t
S = 50
tau = torch.arange(0, T, T // S) # ⭐ 均匀跳步:0, 20, 40, ..., 980
print(f"原始 {T} 步 -> 抽成 {len(tau)} 步,步距 {T // S}")
print("前 6 个时间步:", tau[:6].tolist())
print("后 6 个时间步:", tau[-6:].tolist())
print("\n t alpha_bar 信号系数 噪声系数")
for t in tau[::7].tolist():
a = abar[t]
print(f"{t:5d} {a.item():10.6f} {a.sqrt().item():8.4f} {(1 - a).sqrt().item():8.4f}")
真实输出:
原始 1000 步 -> 抽成 50 步,步距 20
前 6 个时间步: [0, 20, 40, 60, 80, 100]
后 6 个时间步: [880, 900, 920, 940, 960, 980]
t alpha_bar 信号系数 噪声系数
0 0.999900 0.9999 0.0100
140 0.809844 0.8999 0.4361
280 0.443395 0.6659 0.7461
420 0.163926 0.4049 0.9144
560 0.040879 0.2022 0.9793
700 0.006868 0.0829 0.9966
840 0.000777 0.0279 0.9996
980 0.000059 0.0077 1.0000
⭐ 这张表里藏着一条后面要用的结论:均匀跳步很浪费。 看「信号系数」一列 —— 从 t=980 走到 t=560,信号只从 0.0077 涨到 0.2022, 这一整段肉眼都是雪花、几乎分不出区别,却吃掉了差不多一半的步数; 而 t 从 280 走到 0,信号从 0.6659 涨到 0.9999,一张图的细节全在这一段定下来。 步要花在「图还在明显变」的地方。
🎰 六、确定性带来的三个副产品
$\eta=0$ 时,采样从「一串抽样」变成了一个函数:给定 $x_T$,输出唯一确定。这件事的实用价值不比「快」小。
① 种子能复现。 seed 只是用来生成 $x_T$ 的随机数种子;既然 $x_T\mapsto x_0$ 是个确定映射, 同样的种子 + 模型 + 提示词 + 步数和调度 = 同一张图。 ⚠️ 这几个「同样」缺一不可,而且还有一条隐藏条件:浮点归约顺序也得一样。 换显卡、换推理框架、换 batch 大小都可能差那么一点点 —— 🔑 这和语言模型那边「temperature=0 也不保证逐位复现」是同一个坑(链接见章末)。
② 图生图不从纯噪声出发。 把真实图片按前向公式加噪到中间某个 $t$,再从那里去噪走完,出来的图就保留了原图的大结构。 UI 上的 strength 就是「加到多深」:小 → 只动细节和风格,大 → 越来越像重画。 ⚠️ 随机采样器也能做图生图,确定性带来的是可控 —— strength 每调一点,你能确定变化是这次调整的功劳,而不是重抽了个签。
③ 插值要用球面插值,不能取平均。 在 $x_T$ 上从 A 走到 B,输出会连续地从图 A 变成图 B。 ⚠️ 但线性中点会出问题,这一点可以直接算:Stable Diffusion 的潜空间 $d=4\times64\times64=16384$ 维, 标准高斯的样本几乎全落在半径 $\sqrt d=128$ 的球壳上(实测范数中位数 128.01),中心附近反而是空的; 而两个独立噪声的线性中点,范数中位数只有 90.51,正好是 $1/\sqrt2\approx$ 0.7071 倍 —— 掉进了模型训练时从没见过的区域,于是中间几帧发灰糊掉。沿球面走就没这个问题。
📉 七、步数不是越多越好
「50 步够不够」与其背结论,不如测一次。下面这组数是用解析解当去噪网络测的 (一维两峰高斯混合的 $\epsilon$ 有闭式解),所以它量的是纯粹的离散化误差,不掺任何网络自身误差; 误差用生成样本和真实样本的一维 Wasserstein 距离,两万个样本。
| 步数 | W1(生成 vs 真实) | 离地板还差 |
|---|---|---|
| 2 | 1.1473 | +1.1274 |
| 5 | 0.2759 | +0.2561 |
| 10 | 0.1326 | +0.1127 |
| 20 | 0.0682 | +0.0483 |
| 50 | 0.0298 | +0.0099 |
| 100 | 0.0225 | +0.0026 |
| 200 | 0.0228 | +0.0029 |
「地板」是两组真实样本之间的 W1,0.0199 —— 有限样本本身的抖动,谁也低不过它。
- 2 步是灾难,5 步已进入可用区间,之后每翻一倍误差差不多减半。
- 50 步离地板只差 0.0099,100 步差 0.0026 —— 50→100 这一倍算力只买回那点差距的四分之三。
- ⭐ 100 → 200 完全没有收益(0.0225 → 0.0228,反而略高,是采样抖动)。这就是饱和。
⚠️ 别把「50 步」这个数字搬到你的模型上。 饱和点取决于调度表、$\eta$、模型质量和分辨率, 真实模型还要叠上网络自己的预测误差,饱和点只会来得更早。 能搬走的只有形状:先陡后平,而且平得比大多数人想象得早。
🗓️ 八、步数和调度表怎么配
第五节那张表已经说明白了:均匀跳步把太多步花在了看不出区别的高噪声段。于是有了别的排法。
| 排法 | 怎么排 | 特点 |
|---|---|---|
| 均匀(uniform) | range(0, T, T // S) |
最简单,S 小的时候质量掉得最快 |
| 二次(quadratic) | 步距按平方分布,低噪声端更密 | DDIM 原论文就给了这一种,短步数下明显好过均匀 |
| ⭐ Karras 式 | 不在 $t$ 上排,改在噪声水平上按幂律排 | 现在多数推理框架的默认,对 10~20 步的场景收益最大 |
另一条正交的路:换更好的求解器。 ⭐ DDIM 本质上是把去噪写成一条常微分方程、再用一阶欧拉法去解。既然是解 ODE,就可以上二阶和多步法 —— DPM-Solver、DPM-Solver++、UniPC 干的就是这件事,常见说法是十几步能达到 DDIM 五十步上下的效果,同样不需要重训。
⚠️ 最后一条最容易被忽略:采样器和调度表是复现的一部分。 同一个种子、同一个提示词,把 DDIM 换成 DPM-Solver++ 就是另一张图。 分享参数时,步数、采样器、调度表要和种子一起给。
🔗 这一章连到哪里
| 去哪 | 为什么 |
|---|---|
| 大模型全景导论 06c · 解码参数怎么调,出问题怎么查 | ⭐ 那一页有一整节讲「temperature=0 也不保证逐位复现」:GPU 浮点加法不满足结合律,batch 组成一变归约顺序就跟着变。DDIM 的「同种子同图」踩的是同一个坑 —— 数学上确定,工程上仍会差一点 |
| AI基础设施 19 · 投机解码 | 语言模型那边同一个病(串行步数太多)的另一种药:小模型先猜几个 token,大模型一次验完。⭐ 值得对照的是代价不同 —— 投机解码可以证明输出分布和原模型完全一致,而 DDIM 跳步是近似,20 步和 1000 步不是同一张图 |
| 大模型全景导论 10b · 图像生成:扩散模型 | 14 分钟的全景版:扩散和自回归为什么是两种范式、五个关键组件各管什么。⭐ 那一页的深挖清单白纸黑字写着 DDIM「整块要外找」—— 你正在读的就是它点名要的那一块 |
✅ 检查点
- DDPM 的反向过程为什么一步都不能跳?式子里哪一项是罪魁祸首?
- DDIM 需要重新训练吗?为什么?(提示:训练目标只用到了什么)
- DDIM 的一步拆成哪两个动作?哪一个动作允许了跳步?
- η=0 和 η=1 分别对应什么?η 大了以后,为什么少步数更吃亏?
- 第五节那张表里,t 从 980 到 560,信号系数只从多少涨到多少?这说明均匀跳步有什么毛病?
- 步数-误差表里 50 / 100 / 200 步各离地板差多少?这三个数说明什么?
- 为什么两个噪声之间要用球面插值?线性中点的范数掉到了原来的几倍?
👀 答案
- 因为它是马尔可夫链,每步的分布都按「$t$ 和 $t-1$ 相邻」推导($\alpha_t$、$\beta_t$ 都是单步量)。罪魁祸首是末尾的 $\sigma_t z$ —— 它让每一步是从分布里抽样,而不是算出来的。把 $t-1$ 换成 $t-20$ 式子就不成立。
- 不需要。 训练只用到前向的边缘分布 $q(x_t\mid x_0)$,而 DDIM 那一族非马尔可夫前向过程的这个边缘分布和 DDPM 完全一样,差别只在相邻两步怎么衔接 —— 训练目标看不见它。
- ① 用当前 $\epsilon$ 猜一张干净图 $\hat x_0=(x_t-\sqrt{1-\bar\alpha_t}\epsilon)/\sqrt{\bar\alpha_t}$;② 按下一个噪声水平重新加噪回去。⭐ 第二个动作允许跳步 —— 它只需要目标噪声水平 $\bar\alpha_{t'}$,从没要求 $t'=t-1$。
- η=0 是确定性 DDIM(同一个初始噪声永远出同一张图),η=1 退回 DDPM。η 大了以后,注进去的随机要靠后面几百步慢慢洗掉,只给 20 步洗不干净 —— 少步数和高随机性是一对矛盾。
- 只从 0.0077 涨到 0.2022。这一大段肉眼全是雪花、分不出区别,却吃掉近一半步数;而 t 从 280 到 0 那段信号从 0.6659 涨到 0.9999,细节全在那里定下来。说明均匀跳步把步花错了地方。
- 50 步差 +0.0099,100 步差 +0.0026,200 步差 +0.0029(比 100 步还略高,是采样抖动)。说明收益先陡后平:50→100 只买回一点点,100→200 完全没有收益。⚠️ 而且这还是没有网络误差的理想条件,真实模型饱和得更早。
- 因为高维标准高斯的样本几乎全落在半径 $\sqrt d$ 的球壳上($d=16384$ 时是 128,实测范数中位数 128.01),中心是空的。两个独立噪声的线性中点范数只有 90.51,是原来的 0.7071 倍,落进模型没见过的区域 → 中间帧发灰糊掉。沿球面走不会离开那层壳。
🛑 可以停在这里
读到这里,你已经知道 UI 上那三个旋钮(步数、采样器、种子)各自在动什么, 也知道了「把步数从 50 调到 150」大概率是白花钱。这些今天就能用上。
出现这些情况时再回来:出图不可复现(第六节那几个「同样」); 少步数下图发灰或糊(第四节的 η、第八节的调度表);换个采样器就变成另一张图(第八节最后一条)。
⚡ 走神救援
先记住这几件事
- 采样开销取决于需要执行多少次去噪网络,换采样器可以减少步数。
- DDIM 利用预测的干净样本与目标噪声水平构造更新,允许选择较稀疏的时间步。
- 随机性参数、步数和调度都会影响结果,复现实验不能只保存种子。
- 用质量与耗时一起选择步数,不能假设步数翻倍就一定更好。
下一节 👉 05-Latent-Diffusion.md