潜在扩散与Stable-Diffusion 本节摘要:在 512×512 像素空间跑扩散,是「计算上的战争罪行」。Rombach 等(2022)注意到——生成一张图不需要全部 786k 维,只需要够捕捉语义结构的维度,其余的交给一个独立解码器。把扩散搬进 VAE 的潜空间里跑,这一个想法就是 Stable Diffusion。本节讲透两阶段架构:第一阶段的 VAE 把 压到 (FLOPs 砍 64 倍),第二阶段的 U-Net/DiT 在潜空间里去噪;文本如何通过交叉注意力注入每个块( );以及无分类器引导(CFG)如何用 放大条件信号。我们会梳理 SD1.
本节摘要:在 512×512 像素空间跑扩散,是「计算上的战争罪行」。Rombach 等(2022)注意到——生成一张图不需要全部 786k 维,只需要够捕捉语义结构的维度,其余的交给一个独立解码器。把扩散搬进 VAE 的潜空间里跑,这一个想法就是 Stable Diffusion。本节讲透两阶段架构:第一阶段的 VAE 把
512×512×3压到64×64×4(FLOPs 砍 ~64 倍),第二阶段的 U-Net/DiT 在潜空间里去噪;文本如何通过交叉注意力注入每个块(Q=图像特征,K=V=文本 token);以及无分类器引导(CFG)如何用(1+w)·ε_cond − w·ε_uncond放大条件信号。我们会梳理 SD1.5→SDXL→SD3→Flux 的演化(U-Net→DiT、CLIP→T5、4 通道→16 通道),并给出在 8GB 消费级 GPU 上跑 12B Flux 的实战配方。
对应原课程:Phase 08 · Lesson 07 ·
latent-diffusion-stable-diffusion(原英文phases/08-generative-ai/07-latent-diffusion-stable-diffusion/docs/en.md)。
阅读完本节,你应当能够:
512²×3 张量让 500M U-Net 单步约 100 GFLOPS,五十步一张图,十亿张训练图的算力账单荒唐。z = E(x) 当数据,损失与第 06 节完全相同)。Q=图像,K=V=文本,这是 Stable Diffusion 与类别条件扩散的唯一实质区别。(1+w)·ε_cond − w·ε_uncond 控制条件强度,并说出 w ≈ 3-7 的甜点区。像素空间扩散在 512² 上跑,意味着 U-Net 跑在形状 [B, 3, 512, 512] 的张量上。对 500M 参数的 U-Net,每个采样步约 100 GFLOPS;五十步就是每张图 5 TFLOPS。在十亿张图上训练,算力账单荒唐。
而这些 FLOPs 里,大部分用在把感知上不重要的细节(有损 VAE 本可压掉的高频纹理)推过网络。Rombach 的主意:训一次 VAE(第一阶段),冻结它,把扩散整个跑在 4 通道 64×64 的潜空间(第二阶段)。同一个 U-Net,像素只有 1/16,FLOPs 砍 ~64 倍,质量相当。
这就是 Stable Diffusion 配方。SD 1.x/2.x 用 860M U-Net 跑 64×64×4;SDXL 用 2.6B U-Net 跑 128×128×4;SD3 把 U-Net 换成带流匹配的扩散 Transformer(DiT);Flux.1-dev(Black Forest Labs,2024)交付 12B 参数的 DiT-MMDiT。它们都跑在同一个两阶段底料上。
第一阶段——VAE。 编码器 E(x) → z、解码器 D(z) → x。目标压缩:每条空间轴 8 倍下采样 + 调整通道,使总潜变量大小约为像素数的 1/16。损失 = 重建(L1 + LPIPS 感知)+ KL(权重小,因为我们不需要从 z 精确采样,所以不必把 z 强行推向高斯)。常配一个对抗损失,让解码出的图像锐利。
第二阶段——在 z 上扩散。 把 z = E(x_real) 当作数据。训一个 U-Net(或 DiT)去去噪 z_t。推理时:用扩散采样出 z_0,再 x = D(z_0)。
[Q = 图像特征, K = V = 文本 token] 做混合。token 是文本影响图像的唯一通道。💡 关键洞察:第二阶段的损失函数与第 06 节完全相同——同一个 DDPM/流匹配的噪声 MSE。你只是换了数据域(从像素到潜变量)。这意味着潜在扩散没有引入新的损失数学,它是一个工程上的压缩技巧——但这一个技巧让扩散从「只有大厂能跑」变成「消费级 GPU 可跑」,是整个文生图民主化的转折点。
| 模型 | 年份 | 骨干 | 潜变量形状 | 文本编码器 | 参数 |
|---|---|---|---|---|---|
| SD 1.5 | 2022 | U-Net | 64×64×4 | CLIP-L(77 token) | 860M |
| SD 2.1 | 2022 | U-Net | 64×64×4 | OpenCLIP-H | 865M |
| SDXL | 2023 | U-Net + refiner | 128×128×4 | CLIP-L + OpenCLIP-G | 2.6B + 6.6B |
| SDXL-Turbo | 2023 | 蒸馏 | 128×128×4 | 同上 | 1-4 步采样 |
| SD3 | 2024 | MMDiT(多模态 DiT) | 128×128×16 | T5-XXL + CLIP-L + CLIP-G | 2B / 8B |
| Flux.1-dev | 2024 | MMDiT | 128×128×16 | T5-XXL + CLIP-L | 12B |
| Flux.1-schnell | 2024 | MMDiT 蒸馏 | 128×128×16 | T5-XXL + CLIP-L | 12B,1-4 步 |
趋势:用 DiT(在潜变量 patch 上的 Transformer)替换 U-Net;扩大文本编码器(T5 在提示遵从度上胜过 CLIP);增加潜变量通道(4→16 给更多细节余量)。
U-Net 的归纳偏置(卷积 + 跳跃连接)在小数据上有用,但在大数据 + 大算力下,Transformer 的扩展律更平滑。Peebles & Xie(2023)的 DiT 证明:把扩散骨干从 U-Net 换成「在潜变量 patch 上的 Transformer」,在 ImageNet 类条件生成上随参数与算力平滑提升。SD3 和 Flux 都走这条路。SD3 的 MMDiT 进一步让文本流与图像流共享注意力(「联合注意力」),让两种模态深度交互——这是它在提示遵从度上胜过 SDXL 的架构原因。
code/main.py 在第 06 节的 DDPM 上叠一个玩具一维「VAE」(恒等编码器 + 解码器,用于演示;真实 VAE 是卷积网络),再加带 CFG 的类别条件。它展示:同一个扩散损失在原始一维值或编码值上都工作——这是核心洞察。
def encode(x): return x * 0.5 # 玩具「压缩」到更小尺度 def decode(z): return z * 2.0
真实 VAE 有训练好的权重。教学上,这个线性映射足以展示扩散在 z 上操作、不关心原始数据空间。
z 空间扩散与第 06 节相同的 DDPM。网络看到的数据是 z = E(x)。采样出 z_0 后,用 D(z_0) 解码。
训练时,10% 概率丢弃类别标签(替换为 null token)。推理时,同时算 ε_cond 和 ε_uncond,然后:
eps_cfg = (1 + w) * eps_cond - w * eps_uncond
w = 0 = 无引导(满多样性),w = 3 = 默认,w = 7+ = 饱和 / 过锐。
💡 CFG 的直觉:把条件预测远离无条件预测的方向,等于「朝条件方向外推」。
w越大,外推越远,条件信号越强——但代价是多样性下降,且w > 10会出现「油画感」过饱和。数学上,CFG 等价于在修改后的分数场上采样:score_cfg = score_uncond + (1+w)·(score_cond − score_uncond)。
把类别标签替换为冻结文本编码器的输出,通过交叉注意力喂给 U-Net:
h = h + CrossAttention(Q=h, K=text_embed, V=text_embed)
这是类别条件扩散与 Stable Diffusion 的唯一实质区别。
VAE 尺度不匹配:SD 1.x VAE 编码后有一个缩放常数(scaling_factor ≈ 0.18215)。忘了它,U-Net 会在方差严重错误的潜变量上训练。每个 checkpoint 都附带一个。
文本编码器悄悄出错:SD3 需要带 >=128 token 的 T5-XXL,退化到只用 CLIP 是有损的。永远检查 use_t5=True,否则提示保真度塌陷。
混用潜空间:SDXL、SD3、Flux 用不同的 VAE。在 SDXL 潜变量上训的 LoRA 在 SD3 上不工作。Hugging Face diffusers 0.30+ 会拒绝加载不匹配的 checkpoint。
CFG 过高:w > 10 产出饱和、油腻的图像,过拟合提示而牺牲多样性。甜点是 w = 3-7。
负向提示泄漏:空负向提示变成 null token;有内容的负向提示变成 ε_uncond。两者不一样;有些流水线悄悄默认用 null。
⚠️ VAE 缩放因子的来历:扩散在「单位方差空间」上训练最稳。但 SD 的 VAE 编码出的潜变量方差不是 1,而是约
0.18215^{-2} ≈ 30。所以编码后乘scaling_factor ≈ 0.18215,把方差拉回 1,再喂给 U-Net。解码时除回去。这个「魔数」是 VAE 训练后统计出来的,每个 VAE 不同——混用 VAE 而忘了换scaling_factor,训练立刻崩。
| 目标 | 推荐骨干 |
|---|---|
| 窄领域、有配对数据、从零训 | SDXL 微调(LoRA / 全量)——最快交付 |
| 开放领域文生图、开源权重 | Flux.1-dev(12B,Apache / 非商用)或 SD3.5-Large |
| 最快推理、开源权重 | Flux.1-schnell(1-4 步,Apache)或 SDXL-Lightning |
| 最佳提示遵从度、托管 | GPT-Image / DALL-E 3、Midjourney v7、Imagen 4 |
| 编辑工作流 | Flux.1-Kontex(2024.12)——原生接受图像 + 文本 |
| 研究、基线 | SD 1.5——古老但研究透彻 |
本节产出一个提示技能文件(位于原课程 outputs/skill-sd-prompter.md)。
skill-sd-prompter.md:输入「文本提示 + 目标风格」,输出:模型 + checkpoint、CFG 比例、采样器、负向提示、分辨率、可选的 ControlNet/IP-Adapter 组合、逐步 QA 清单。参考 Flux 集成是「我只有消费级 GPU,能交付吗」的标准配方。技巧就是生产推理文献列出的、应用到扩散 DiT 上的同一个三旋钮配方:
分阶段加载:Flux 有三个永不需要同时驻留 VRAM 的网络——T5-XXL 文本编码器(fp32 约 10 GB)、CLIP-L(小)、12B MMDiT、VAE。先编码提示,删掉编码器,加载 DiT,去噪,删掉 DiT,加载 VAE,解码。8GB 消费 GPU 一次只能装一个阶段。
bitsandbytes 的 4-bit 量化:BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16) 同时用在 T5 编码器和 DiT 上。内存砍 8 倍,文生图的质量下降按 Aritra 的基准几乎不可察觉。
CPU 卸载:pipe.enable_model_cpu_offload() 在每次前向推进时自动在 CPU/GPU 间换模块。多 10-20% 延迟,但让流水线能跑起来。
内存核算:10 GB T5 / 8 = 1.25 GB 量化、12B × 0.5 字节 ≈ 6 GB 量化 DiT,加激活。用 stas00 的术语,这是 TP=1 推理的极端端——无模型并行、最大量化。生产里你会在 H100 上跑 TP=2 或 TP=4;对单个开发笔记本,这就是配方。
简单。 用引导 w ∈ {0, 1, 3, 7, 15} 跑 code/main.py。记录每类的样本均值。在哪个 w 下,类均值越过真实数据均值?
中等。 把玩具线性编码器换成「tanh-MLP 编码器/解码器 + 重建损失」。在新潜变量上重训扩散。样本质量变了吗?
困难。 用 diffusers 搭真实 Stable Diffusion 推理:加载 sdxl-base,跑 30 步 Euler + CFG=7,计时。然后切到 sdxl-turbo 跑 4 步 CFG=0。同一主题,不同质量——描述变了什么、为什么。
512²×3 让单步 ~100 GFLOPS;潜在扩散把扩散搬进 VAE 的 64×64×4 潜空间,FLOPs 砍 ~64 倍。Q=图像,K=V=文本;这是 SD 与类别条件扩散的唯一实质区别。(1+w)·ε_cond − w·ε_uncond,w≈3-7 甜点;>10 过饱和;空负向提示(null)≠ 有内容负向提示。scaling_factor≈0.18215 把潜变量方差拉回 1;每个 VAE 不同,混用必崩。下一节,我们进入对预训练扩散的精细控制——ControlNet 与 LoRA:前者用「零卷积副本」注入结构条件(边缘、深度、姿态),后者用低秩矩阵让一张消费级 GPU 就能微调一个十亿参数模型。