📑 本页目录(点开跳转)
05 · Latent Diffusion:在压缩过的空间里扩散
⏱ 40 分钟 | ⭐ 为什么 Stable Diffusion 能在一张消费级显卡上跑,以及它为什么画不好小字
🎯 一句话
先用一个自编码器把图压小 48 倍,扩散全程只在压缩后的表示上做,最后再解码回像素 —— 省下的算力从几十倍到几千倍不等,代价是压缩那一步丢掉的细节,后面谁也补不回来。
上一章开头说过,想让出图变快只有两条路:少走几步(那是 DDIM),或者让每一步更便宜。这一章走的是第二条。
💸 一、先把账算出来:直接在像素上扩散有多贵
一张 512×512 的 RGB 图是 512 × 512 × 3 = 786432 个数。扩散的每一步都要把这 786432 个数喂进网络、再吐出同样多的数,中间的特征图分辨率也一路都是 512×512。而采样要走几十到上百步,步与步之间还是硬串行的。
拿 U-Net 主干里最普通的一层 3×3 卷积(128 通道进、128 通道出)算:在 512×512 上是 77.31 GFLOPs。走 50 步就是 3.87 TFLOPs —— 而这只是一层,一个 U-Net 有几十层。
真正劝退的是注意力。把每个空间位置当一个 token,512×512 就是 262144 个 token,注意力矩阵有 $262144^2 \approx 6.87\times10^{10}$ 项 —— 光存下这一个矩阵,按 fp16 算就要 137 GB,而这还只是一层里的一个头。
⭐ 所以问题不是「慢一点」,是根本放不下。 早期的像素空间扩散模型只能在 256×256 甚至 64×64 上训,再靠一串超分模型把图撑大。
🗜️ 二、解法:把扩散整个搬进潜空间
Latent Diffusion 的做法是把生成拆成三段:
① 编码:训练一个自编码器,编码器 $E$ 把 512×512×3 的图压成 64×64×4 的张量(空间边长下采样 8 倍,通道数 3→4)。 ② 扩散:前向加噪、训练目标、DDIM 采样,公式一个字都不改,只是作用对象从像素换成了这个 64×64×4。 ③ 解码:解码器 $D$ 把去噪完的潜变量还原回 512×512×3 的图。
压缩倍数就是 786432 ÷ 16384 = ⭐ 48 倍。Latent 这个词指的正是第 ② 步的作用对象。
⭐ 关键在于次数不对等:$E$ 和 $D$ 各只跑一次,而扩散要跑几十上百次。省掉的正是那个要乘以步数的部分 —— 编解码这点固定开销,摊到 50 步里几乎看不见。
🔢 三、三个倍数,别混成一个
| 每一步里的这一项 | 像素空间 512×512×3 | 潜空间 64×64×4 | 倍数 |
|---|---|---|---|
| 要去噪的数的个数 | 786432 | 16384 | 48 |
| 一层 128→128 的 3×3 卷积 | 77.31 GFLOPs | 1.208 GFLOPs | 64 |
| 一层全局注意力的矩阵项数 | $6.87\times10^{10}$ | $1.68\times10^{7}$ | 4096 |
⚠️ 三个数不一样,来路也不同:48 是元素个数之比(通道 3→4 抵消掉了一点空间上的压缩);主干里两边的通道数是一样的,差别只剩空间面积,就是 $8^2=64$;而注意力的代价按面积的平方走,于是 $(8^2)^2 = 4096$。
⭐ 越贵的那一层,省得越狠。 这才是它真正 work 的原因 —— 如果所有层都只省 48 倍,故事远没有这么好听。
🧪 四、动手:这笔账自己算一遍
不需要模型也不需要 GPU,纯计数,跑多少遍结果都一模一样:
# 像素空间 vs 潜空间:同一件事各要花多少算力(纯计数,不测秒)
import torch
def conv_flops(cin, cout, k, hw):
"""一层 k×k 卷积:每个输出元素做 cin*k*k 次乘加,一次乘加算 2 FLOPs"""
return 2 * cin * cout * k * k * hw * hw
C_PIX, H_PIX = 3, 512 # 一张 512×512 的 RGB 图
C_LAT, H_LAT = 4, 64 # Stable Diffusion 的潜空间
n_pix = torch.zeros(C_PIX, H_PIX, H_PIX).numel() # ⭐ 每一步要去噪的东西有多少个数
n_lat = torch.zeros(C_LAT, H_LAT, H_LAT).numel()
print(f"每步要去噪的量: {n_pix} 个数 vs {n_lat} 个数 → 压缩 {n_pix // n_lat} 倍")
f_in_p = conv_flops(C_PIX, 128, 3, H_PIX) # ① 首层:把输入通道抬到 128
f_in_l = conv_flops(C_LAT, 128, 3, H_LAT)
print(f"首层 3x3 卷积: {f_in_p/1e9:7.2f} GFLOPs vs {f_in_l/1e9:6.3f} GFLOPs → {f_in_p/f_in_l:.0f} 倍")
f_bd_p = conv_flops(128, 128, 3, H_PIX) # ② 主干:通道数一样,差别全在分辨率
f_bd_l = conv_flops(128, 128, 3, H_LAT)
print(f"主干 128→128 卷积:{f_bd_p/1e9:6.2f} GFLOPs vs {f_bd_l/1e9:6.3f} GFLOPs → {f_bd_p/f_bd_l:.0f} 倍")
a_p, a_l = (H_PIX ** 2) ** 2, (H_LAT ** 2) ** 2 # ③ 全局自注意力:每个空间位置一个 token
print(f"注意力矩阵项数: {a_p:.3e} vs {a_l:.3e} → {a_p // a_l} 倍")
for steps in (50, 1000): # ④ 再乘上采样步数
print(f"走 {steps:4d} 步,光②这一层就要: 像素空间 {f_bd_p*steps/1e12:6.2f} TFLOPs"
f" vs 潜空间 {f_bd_l*steps/1e12:5.3f} TFLOPs")
真实输出:
每步要去噪的量: 786432 个数 vs 16384 个数 → 压缩 48 倍
首层 3x3 卷积: 1.81 GFLOPs vs 0.038 GFLOPs → 48 倍
主干 128→128 卷积: 77.31 GFLOPs vs 1.208 GFLOPs → 64 倍
注意力矩阵项数: 6.872e+10 vs 1.678e+07 → 4096 倍
走 50 步,光②这一层就要: 像素空间 3.87 TFLOPs vs 潜空间 0.060 TFLOPs
走 1000 步,光②这一层就要: 像素空间 77.31 TFLOPs vs 潜空间 1.208 TFLOPs
💡 最后两行值得停一下:在潜空间里走满 1000 步(1.208 TFLOPs),都比在像素空间里走 50 步(3.87 TFLOPs)便宜。上一章省下来的步数,和这一章省下来的单步开销,是可以叠乘的。
🔍 五、代价:丢掉的细节,是在编码那一步丢的
自编码器不是无损压缩。48 倍压下去,一定有东西回不来。
⭐ 要命的是顺序:$E$ 在扩散之前跑。这意味着整条链路的画质上限是 $D(E(x))$ —— 把原图编码再解码得到的那张图。⚠️ 扩散模型无论学得多好、步数走得多足,都只能逼近这个上限,不可能超过它。
具体丢的是什么?做一次除法就清楚了:512×512 的图压到 64×64,一个潜空间格子对应原图 8×8 = 64 个像素,也就是 192 个数被压成 4 个数。而一个小字的笔画只有一两个像素宽、整个字可能只占十几个像素 —— 它整个人就住在一两个格子里。
💀 这就是这类模型画不好小字的第一层原因,同一条解释也覆盖了远景人脸、手指关节、密集纹理和网格:⭐ 凡是「在 8×8 的块尺度上无法从周围推断出来」的高频细节,编码那一步就已经交代了。
⚠️ 但别把锅全甩给自编码器:提示词理解不到位、训练数据里就没有清晰小字,同样会让字画歪。自编码器那一份是下界 —— 你可以拿一张写着清晰小字的照片,只做一次编码再解码、扩散模型完全不参与,字通常就已经糊了。这一步不解决,后面怎么调都白搭。
于是有了三条对着这个上限使劲的路:
- 别压那么狠。下采样 8 倍是权衡点,压 4 倍更清楚但每步贵 4 倍 —— 又回到第一节那笔账。
- ⭐ 加宽潜空间的通道。SD 1.x / SDXL 都是 4 通道,SD3、FLUX 这一代提到了 16 —— 空间尺寸没变,每个格子能装的信息多了,买回来的正是这部分细节。
- 直接出更大的图。1024×1024 对应 128×128 的潜空间,同一个字占的格子多了自然清楚些 —— 代价还是那笔按面积走的账。
⚖️ 六、KL 项在这里到底管什么
自编码器如果只用重建损失训,潜空间可以长得非常离谱:整体尺度可能是几十上百,分布里全是空洞和悬崖。
⚠️ 扩散模型在这样的空间上学不动。 原因很直接:前向加噪那条公式 $x_t=\sqrt{\bar\alpha_t}x_0+\sqrt{1-\bar\alpha_t}\,\epsilon$ 里的噪声方差是 1,它默认了数据本身的尺度也在 1 附近。要是潜变量的幅度是 100,整张 $\bar\alpha$ 调度表就全错位 —— 前面大半段的「加噪」相对信号约等于没加,模型在那些步上没东西可学。
所以 LDM 在重建损失之外加了一项 KL,把每个位置的后验 $q(z\mid x)$ 往标准正态 $\mathcal N(0,I)$ 推。
$$\mathcal L = \underbrace{\|x - D(z)\|}_{\text{重建,要准}} + \underbrace{\lambda \cdot D_{KL}\big(q(z\mid x)\,\|\,\mathcal N(0,I)\big)}_{\text{别太散}},\qquad \lambda \text{ 极小}$$
⭐⭐ 这里的 $\lambda$ 小得几乎可以忽略,而这是故意的,不是调参调出来的。 关键在于目标不同:
| 普通 VAE | LDM 的自编码器 | |
|---|---|---|
| 生成时 $z$ 从哪来 | ⭐ 直接从先验 $\mathcal N(0,I)$ 里采 | 由扩散模型生成 |
| 所以潜分布必须 | 真的是标准正态,否则采出来的 $z$ 解码成噪声 | 只要不太离谱:尺度受控、别有大片空洞 |
| KL 权重 | 大(它是正经的正则项) | ⭐ 极小 |
💡 一句话:LDM 根本不从先验里采样,那 KL 就没必要收得那么紧。 而 KL 一大就会挤占重建质量 —— 直接把上一节那个 $D(E(x))$ 上限拉低,那是 LDM 最不能让的东西。
⭐ 有个能自己核对的痕迹:Stable Diffusion 的代码里,潜变量送进扩散模型之前要乘一个常数 0.18215,解码前再除回去。⚠️ 如果 KL 真把潜空间压成了标准正态,这个缩放常数根本没有存在的理由。 它的存在恰好说明:KL 只是把潜空间「拴住」了,实际标准差离 1 还差着好几倍,得靠一个事后量出来的常数补上。
📐 另一条路:不用 KL,用 VQ(想看再点)
LDM 原论文给了两种正则方式,KL 只是其中一种。另一种是 VQ:不再往标准正态上推,而是维护一本码本,把每个位置的潜向量量化到码本里最近的那一条。
- KL 版:潜变量是连续的,扩散模型直接在实数张量上跑。Stable Diffusion 走的这条。
- VQ 版:潜变量成了一串离散 token,天然接得上自回归那一套生成方式。
⭐ 两者的共同点才是重点:都不是为了得到一个漂亮的先验分布,而是为了不让潜空间散成扩散模型学不动的样子。
🔗 这一章连到哪里
| 去哪 | 为什么 |
|---|---|
| 数学原理 01b · KL 散度:交叉熵的真身 | ⭐ 那一页的「前向 KL vs 反向 KL」对照表里,直接点名了 VAE / 变分推断用的是反向 KL,mode-seeking,所以估出来的后验偏窄。本章第六节那个 $D_{KL}(q(z\mid x)\,\|\,\mathcal N(0,I))$ 正是这一类 —— 它罚的是「潜变量跑到先验不敢去的地方」,而不是「盖住先验的每个角落」 |
| ML 基础 06 · 无监督:聚类与降维 | 压缩表示这件事最经典的那一版:PCA 找方差最大的几个方向投影上去,用途表里就写着「去噪:丢掉的小方差方向往往是噪声」。⭐ 值得对照的是丢的东西不一样 —— PCA 是线性的、丢掉的是小方差方向;自编码器是学出来的非线性压缩,丢掉的是重建损失不在乎的东西,而小字恰好就属于那一类 |
| AI 基础设施 09 · 显存优化全家桶 | 本章省的是每一步的计算量;那一页是省显存的完整清单,七件武器按性价比排好了序(梯度检查点把激活从 $O(L)$ 降到 $O(\sqrt L)$、换 8-bit 优化器、LoRA/QLoRA…)。⭐ 想自己训一个扩散模型时,这两条路是叠加的,不是二选一 |
| 全景导论 10b · 图像生成:扩散模型 | 14 分钟的索引版。⭐ 那一页的组件表里,VAE 只有一行字:「把图压缩到低维潜空间再扩散,省算力(Latent Diffusion)」—— 而它的深挖清单把 Latent Diffusion 整块标成「📕 要外找」。你正在读的就是那一行的展开 |
✅ 检查点
- 512×512×3 的图有多少个数?压到 Stable Diffusion 的潜空间之后剩多少?压缩几倍?
- 为什么说像素空间扩散「不是慢一点,是根本放不下」?举出那个最劝退的数字。
- 编码器和解码器各跑几次?扩散跑几次?省下来的到底是哪一部分?
- 48 倍、64 倍、4096 倍这三个数分别是什么之比?为什么它们不一样?
- 整条链路的画质上限是什么?为什么扩散模型再准也超不过它?
- 一个潜空间格子对应原图多少个像素、多少个数压成多少个数?这怎么解释小字画不好?
- LDM 的 KL 权重为什么设得极小?普通 VAE 为什么不能这么干?
0.18215那个缩放常数又说明了 KL 项的什么事?
👀 答案
- 786432 个数(512×512×3),压到 64×64×4 = 16384 个数,⭐ 48 倍。
- 因为注意力的代价按面积的平方走:512×512 = 262144 个 token,注意力矩阵 $6.87\times10^{10}$ 项,光存这一个矩阵按 fp16 算就要 137 GB —— 而这还只是一层里的一个头。所以早期像素空间的模型只能在 256×256 甚至 64×64 上训。
- 编码器和解码器各只跑 1 次,扩散要跑几十上百次。 省的正是那个要乘以步数的部分;编解码那点固定开销摊到 50 步里几乎看不见。
- 48 是元素个数之比(通道 3→4 抵消掉一点空间压缩);64 是主干卷积之比 —— 两边通道数一样,差别只剩空间面积 $8^2$;4096 是注意力矩阵之比 —— 它按面积的平方走,$(8^2)^2$。⭐ 越贵的层省得越狠,这才是它 work 的原因。
- 上限是 $D(E(x))$,也就是原图编码再解码得到的那张图。因为 $E$ 在扩散之前跑,细节在那一步就已经没了,后面的扩散只在潜空间里工作,无从补起。
- 一个格子对应 8×8 = 64 个像素,即 192 个数压成 4 个数。小字的笔画只有一两个像素宽、整个字只占十几个像素,整个住在一两个格子里 —— 凡是「在 8×8 块尺度上无法从周围推断」的高频细节,编码那步就交代了。⚠️ 但它只是下界,提示词理解和训练数据同样有份。
- 因为 LDM 不从先验里采样:$z$ 由扩散模型生成,不是从 $\mathcal N(0,I)$ 里抽的。所以潜空间只要「不太离谱」(尺度受控、别有大片空洞)就够,KL 收紧只会挤占重建质量、拉低 $D(E(x))$ 这个上限。⭐ 普通 VAE 生成时就是从先验里采 $z$,潜分布不真的接近标准正态,采出来的 $z$ 解码就是噪声。而
0.18215的存在说明 KL 没有真把潜空间压成标准正态 —— 实际标准差离 1 还差好几倍,得靠一个事后量出来的常数补上;真是标准正态的话,这个常数没有存在的理由。
🛑 可以停在这里
读到这里,你已经能解释 Stable Diffusion 为什么塞得进一张消费级显卡,也知道了「换更好的模型就能写对小字」这个期待错在哪一层。
出现这些情况时再回来:出图细节糊、小字或手指崩(第五节,先确认是不是自编码器的下界问题);想训自己的扩散模型却 OOM(第六节末尾那条链接);看到别人说「换了 16 通道 VAE」不知道在换什么(第五节最后)。
⚡ 走神救援
先记住这几件事
- Latent Diffusion 先压缩图像,在潜空间执行反复去噪,最后解码回像素。
- 编码解码只占部分流程,反复运行的主干因此能在更小空间工作。
- 压缩也会丢失细节;先看重构质量,再判断问题来自编码器还是去噪模型。
下一节 👉 06-条件控制与CFG.md