上一章《扩散模型 DDPM》,如果直接作用在 512×512 的图上, 光一次前向就要几千 GFLOPs(跑一遍要做几万亿次运算),还要跑一千步。这在工程上根本不可行。 真正的做法是:先用一个压缩器把图缩到 1/48,在那个小空间里扩散,最后再放大回来。
回想上一章的前提:扩散模型要反复迭代几十到上千次,每次都是一遍完整的前向。 原版 DDPM 也是在像素空间跑的,只是它实验用的是 32×32 到 256×256 这个量级的小图;图一大,同一笔账就做不动了。 如果前向跑在 512×512×3 = 78 万个元素上,账单是这样的:
| 看什么 | 像素空间扩散 | 潜空间扩散 |
|---|---|---|
| 每次迭代的输入 | 512 × 512 × 3 = 786,432 个元素 | 64 × 64 × 4 = 16,384 个元素 |
| 卷积类的计算量 | 正比于空间位置数 → 贵 64 倍 | 基准 |
| 注意力类(如果全图注意) | 正比于空间位置数平方 → 贵 4096 倍 | 基准 |
| 跑 50 步 | 计算量直接乘 50 | 计算量同样乘 50,但基数小得多 |
| 能不能做 | 能,但没人这么做 | Stable Diffusion、SDXL、FLUX 全部这么做 |
那张潜表示是 64×64×4:边长缩 8 倍,每个位置从 RGB 三个数变成 4 个数。 这 4 个数是 VAE 学出来的,不对应具体颜色;所以总元素数少 48 倍(786,432 ÷ 16,384)。
互动 · 一千步的账单:每跑一步,整张图都要重走一遍
像做雕塑。你不会直接对着三吨的大理石反复调整一千次——
先做一个小比例的泥稿,在小稿上改到满意,最后才放大到大理石上精修。
「小比例泥稿」就是潜空间,「放大精修」就是那个 VAE
但泥稿上丢掉的细节,放大时补不回来——这就是 VAE 的画质天花板。
下面这个计算器按结构逐层算 FLOPs(每跑一次前向要多少十亿次运算): 卷积的计算量正比于空间位置数(高×宽),注意力的计算量正比于它的平方。 滑块拖的是边长压缩倍数 F(论文里记作 f;512 ÷ 64 = 8)。
一张图在两种空间里各有几个数
互动 · 像素空间 vs 潜空间的算力差
压缩 8 倍之后:卷积省 64 倍,注意力省 4096 倍。
因为卷积是 O(n),注意力是 O(n²)。空间尺寸每降一半,注意力便宜 16 倍。
这就是为什么几乎所有图像生成模型都必须在
主干拿到潜表示之后,还要把它切成 p×p 的小块(patch),每块算一个 token,再排成一队。 点下面的按钮换 patch 大小,看 token 数怎么变(注意力矩阵在第 4 节)。
互动 · 数据在管道里被压扁、又被拉长的完整形状链
| 部件 | 输入 → 输出 | 干什么 | 为什么要单独训 |
|---|---|---|---|
| ① VAE |
512×512×3 → 64×64×4 | 把图压成潜表示 | 它只学"怎么压"和"怎么还原",和扩散完全无关。所以可以复用 |
| ② 文本 |
"一只穿西装的猫" → 77×768 张量 (最多 77 个词,每个词 768 个数) |
把提示词变成向量序列 | 用的就是阶段 3 讲的 Transformer。也是外挂的,可以换(CLIP → T5) |
| ③ 扩散主干 | (噪声潜 + 时刻 t + 文本) → 预测噪声 | 唯一真正要训练的部分 | 它才是"会画画"的那个。前两个是固定的工具 |
| ④ VAE |
64×64×4 → 512×512×3 | 把干净的潜表示还原成图 | 和 |
整个系统里,扩散主干只是中间那个环节:VAE 和文本
Stable Diffusion 1.5 的主干是一个 U-Net(8.6 亿参数):
一堆卷积 +
U-Net 和 DiT 的结构差别
U-Net:先
DiT:切成小块排成一队,全程同一分辨率
互动 · patch 切多大:token 数、注意力矩阵、显存一起算给你
| 对比项 | U-Net | DiT |
|---|---|---|
| 基本运算 | 卷积(局部) | 自注意力(全局) |
| 分辨率变化 | 先降后升,多个尺度 | 全程不变,靠 patch 化控制长度 |
| 归纳偏置 | 强(卷积天生假设「附近像素相关、挪一下还是同一个东西」) | 几乎没有,全靠数据学 |
| 和语言模型的关系 | 两套完全不同的代码 | 几乎就是 GPT 换了个输入 |
| 缩放表现 | 加大到一定程度收益变缓 | 论文发现:同 FID 下 GFLOPs 更少,且能持续 scale(FID:生成图和真实图的分布差多远,越低越好;ImageNet 256×256 上 2 左右已经很好) |
| 今天谁在用 | SD 1.5 / SDXL | SD3、FLUX、Sora——新模型基本全是它 |
U-Net 是一套图像专用的积木,语言模型是另一套,两边的工程经验没法互相迁移。 DiT 把主干变成标准 Transformer 之后:
① 训练技巧可以直接借用,混合精度、梯度检查点、张量并行、FlashAttention, 大模型训练那套现成的工具都能直接用; ② 视频、图像、音频可以用同一个骨架,只要换 patch 化方式; ③ "下一个 token 预测"的直觉可以搬过来,这直接催生了阶段 8 的扩散语言模型。
架构统一带来的复利,远比"这一版指标高一点"重要。
U-Net 在几个分辨率上各插一层交叉注意力,条件就有地方进;DiT 的主干每层长得一模一样, 条件从哪个口子塞进去就得专门设计。论文用类别标签在 ImageNet 上试了四种(道理对文本条件一样):
| 方式 | 怎么做 | 结果 |
|---|---|---|
| In-context | 把类别向量当成一个额外的 token,拼在序列最前面 | 能用,但效果最差。追加的那一个 token 要独自承担全部条件信息 |
| Cross-attention | 在每个 Block 里加一层交叉注意力,让图像 token 去查文本 | 经典做法(SD 1.5 就是它)。效果好,但每层都要多算一次注意力 |
| adaLN | 用条件向量算出每层的缩放 γ 和平移 β,直接调制归一化(只是多算几个缩放、平移的数, 不用像交叉注意力那样让图像和文本两两比一遍) | 比 cross-attention 更好,而且几乎不增加计算量 |
| adaLN-Zero | 在 adaLN 基础上,再把残差门的初始值设为 0(残差门: 这一层的输出加回输入之前先乘上的那个数,0 表示这层什么都不做) | 最好。DiT 论文里它是明确的赢家 |
最后一个「Zero」看起来只是个小改动,但它修复的问题非常真实。拖动下面的滑块模拟训练进度:
互动 · adaLN-Zero 为什么能让 12 层网络稳稳起步(演示用,原理对任意层数成立)
公式 · 把鼠标移到每一项上
悬停公式里带下划线的部分,看它在上面这张图上对应哪一块。
残差门的初始值设成 0 之后,整个 Block 一开始就是恒等映射——
输入原封不动地传过去,什么也不做。
这意味着:一个刚初始化的 12 层网络(演示用 12 层,原理对任意层数都成立),等价于一个 0 层网络。
训练从"什么都不做"开始,然后每层的门慢慢打开,逐层学会自己的任务。
这和我们在「初始化与训练稳定性」那一章讲的思路是同一个:
别让网络一开始就乱动,先让它学会"什么都不做"。
classifier-free guidance 是这么干的:训练时偶尔把条件换成"空",生成时拿"有条件"和"无条件" 两个输出的差,把画面往条件方向推得更狠。这就要求模型两种输入都能吃。条件空的时候, adaLN 能调制的信息就没了:γ、β 只剩从那组空条件里算出来的一份固定值,正好当那个"无条件"版本用。
上一节说 adaLN 用条件向量算出 γ 和 β。这一步展开有七个符号, 下面把它拆成一个 Block 里真实发生的一遍计算—— 拖下面的滑块,每一行都会跟着变。
动画 · 一个 DiT Block 的真实前向(数字全是当场算的)
把「残差门 α」拖到 0:第 4 行整个塌成一条零线,第 5 行和虚线画的第 1 行完全重合——
这就是上一节那张「12 层网络等于 0 层」的图在一层之内的样子。
再把「条件强度 c」拖到最大:γ 和 β 一起变,第 3 行整排被拉宽、推歪,
而输入 x 一个数都没动过——条件改的从来不是数据,是处理数据的方式。
训练时这些门 α 也是网络从条件 c 里算出来的,梯度会把有用的那几层的门推大。
| 架构 | 核心结构 | 代表模型 | 特点 |
|---|---|---|---|
| U-Net 2015 → 2022 |
卷积 + |
DDPM、SD 1.5(8.6 亿)、SDXL(26 亿) | |
| UViT 2023 |
保留了 U-Net 的多尺度结构,但把卷积块换成 Transformer 块 | 研究性质,较少直接使用 | 是 U-Net 到 DiT 的过渡形态,证明了"跳连仍然有用" |
| DiT 2023 |
纯 Transformer,patch 化潜特征,全程单分辨率,adaLN-Zero 注入条件 | DiT-XL/2(6.75 亿)、Sora | 结构极简、易 scale、和 LLM 共享全部基建 |
| MMDiT 2024 |
双流:图像和文本各有一套权重,只在注意力处汇合 | SD3 / SD3.5 | 文本理解明显更强(尤其是长提示、文字渲染); 代价是参数量翻倍 |
| DiT + Reflow 2024 → |
DiT 骨架 + |
FLUX、SD3.5 | 今天的最强组合:架构统一 + 采样步数少 |
互动 · 四种主干的接线差别(点按钮切换)
| 隐患 | 说明 | 什么时候真的会痛 → 换什么 |
|---|---|---|
| ① VAE 是画质的天花板 | 压缩是有损的。VAE 重建时丢掉的细节,扩散主干再怎么努力也补不回来,
它只能生成"能被这个 VAE 表示的东西"。潜空间里不存在的信息, |
要出小字海报、精细商品图,而字总是糊 → 换压缩倍数更小的 VAE(F=4),代价是算力 |
| ② 高频细节系统性偏弱 | 文本(尤其是小字)、细密的纹理、毛发、远处的人脸,这些正是压缩最容易丢的。 不是模型不行,是潜空间不够用。改进方向是把压缩倍数 F 从 8 降到 4,代价是算力涨回去。 | 要放大到 4K、要渲染文字排版 → 降压缩倍数,或改用像素空间 / 蒸馏路线 |
| ③ 两阶段训练的耦合 | VAE 先训好、然后冻死,扩散主干再去适配它,主干只能迁就 VAE 的缺陷。 有人尝试联合训练,但流程复杂很多、也难调。 | 想端到端改进画质、直接动 VAE 的缺陷 → 联合训练,代价是从头调一套更复杂的流程 |
互动 · 压缩掉的到底是什么:同一张 200×200 的小图,压了再放回来
"潜空间扩散质量比像素扩散差"这句话是不准确的。
正确的说法是:在相同
只有在算力无限的假设下,像素空间扩散的理论上限才更高(这是本站的推论,论文没这么说)。
而算力永远有限,这个理论优势从来没有真正兑现过。
主干参数量对比
互动 · 这些参数长在谁身上
SD 1.5 的 8.6 亿参数里,大部分花在了多尺度的卷积块上;DiT-XL 的 6.75 亿参数里, 绝大部分花在了自注意力的 QKV 和 FFN 上。参数总量差不多,但结构完全不同: U-Net 用"不同尺度重复处理"换取效率,DiT 用"全局注意力 + 更深的堆叠"换取上限。 这也解释了为什么 DiT 更能 scale:它的参数结构和大语言模型同构,而我们知道怎么把那种结构做大。
这一章是全站对「学习即压缩」最直白的一次展示—— 而且它把「压缩」拆成了两层,第一层才是这一章的主角。
回到第 2 节那张「像素空间 vs 潜空间的算力差」。图像边长保持 512, 把「VAE 压缩倍数」从 8 拖到 16:卷积那个数从 64 倍跳到 256 倍, 注意力那个从 4096 倍直接跳到 65536 倍;再拖回 4,两笔账一起塌下去。
你拖的这一个滑块,直接决定了主干能买多大的模型、能跑多少步。
| 暗线 | 这一章的回答 |
|---|---|
| A 信息流动 | 三条流在中间汇合。图像流:512×512×3(786,432 个元素)→ VAE 主干内部:64×64 切成 2×2 的小块 → 每块拉成 16 维 → 投影成 (1024, 1152) (DiT-XL/2 的 token 数与宽度)→ 28 层全程形状不变 → 反 patch 化 → VAE |
| B 什么被牺牲了 | 换来一个数量级的算力和「能 scale 的形状」,付出三样:① 细节上限:VAE 是天花板, 丢了就补不回来,而且专砍小字、毛发、远景人脸;② 压缩倍数的两难:想省算力就压到 16 倍, 想保细节就得回到 4 倍;③ 两阶段耦合:VAE 先训好再冻死,主干只能绕着它的缺陷学。 |
| C 参数账本 | SD 1.5 的 10 亿多参数里,真正在训练的只有 8.6 亿的 U-Net;VAE 约 0.83 亿
( 算力账:元素数少 48 倍;空间位置数少 64 倍,卷积少 64 倍、注意力少 4096 倍(F = 8)。 显存上,512×512×3 在 fp16(每个数 2 字节)下是 1.5 MB,压成 64×64×4 是 32 KB; 训练时每一层激活都要存一份,这个倍数会一路乘下去。 |
| D 跑在什么上 | 带宽是这一章被逼向潜空间的原因之一。 最贵的动作不是矩阵乘,是让那个 N² 的注意力矩阵真的落到显存里。 512×512 的图切成 1×1 的 token 做全图注意力,矩阵有 262,144² ≈ 687 亿个元素, fp16 下就是 137 GB,单卡装不下。压到 64×64 后 token 数是 4,096(p=1), 矩阵约 1,678 万个元素、33 MB;DiT-XL/2 用 2×2 的 patch,token 只有 1,024, 矩阵约 105 万个元素、2 MB,这才回到「可以算」的范围。 每一次省下来的读写,都是显存上的真实时间。完整的硬件账在 《硬件与算力账本》。 |
| E 它假设了什么 | 三条假设,每条都能被追问。 ① 像素的冗余远大于信息:照片里相邻像素几乎一样,所以固定 ② 「重要」和「高频」可以分开处理:压缩砍掉的主要是高频细节,语义内容被保住, 所以伤害集中在小字和纹理上。 ③ 「一开始什么都不做」是最好的初始化:adaLN-Zero 的残差门从 0 开始, 刚初始化的 12 层网络等价于 0 层,这和「初始化与训练稳定性」那一章是同一个信念。 |
| F 违背了哪个直觉 | 三条。 ① 「画质要靠更多像素」是反的:先把 786,432 个元素扔掉 48 分之 47,画质反而更好, 因为真正的约束是算力预算。 ② 「结构越贴合图像越好」也是反的:U-Net 的图像先验比 DiT 强得多, 算力充足时却被更弱先验的 DiT 反超。 ③ 「算力堆到同一档,旧结构也能追上」也反了:LDM-4 用 103.6 GFLOPs 拿到 FID 3.60, DiT-XL/2 用差不多的 118.6 GFLOPs 拿到 2.27,多一成多算力、质量差一个档。 |
它接住了上一章的什么:上一章讲扩散时那句「要在像素上跑一千步」, 在这一章被直接判了死刑——不是理论错,是工程上根本不成立。
它给后面留了什么:主干一旦变成标准 Transformer,
它就和大语言模型共享了同一套形状和同一套工程。
下一章《流匹配与整流流》把路径掰直、少跑几步(
没人真的在像素上跑一千步扩散——先用 VAE 压到 1/48,在那个小空间里扩散,最后放大回来。
省下的算力:卷积类 64 倍、注意力类 4096 倍。代价是 VAE 成了画质天花板。
DiT 把主干从 U-Net 换成了纯 Transformer:patch 化潜特征、全程单分辨率、
用 adaLN-Zero 注入条件。把残差门的初始值设为 0,等于让 12 层网络一开始就是 0 层——
这是它训练稳定的关键。
真正的大事件不是"指标涨了",而是扩散模型的主干变成了一个标准 Transformer——
从此可以共享 LLM 的全部工程基建;只是路径弯、步数多,下一章把路掰直。
上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。