生成模型 / 图像与视频架构 原理笔记进阶 13 分钟阅读 查看源文件 ↗

SD3 与 FLUX 架构案例#

本页内容2 个部分

1. 从 DiT 到 SD3 / Flux#

1.1 SD3:MM-DiT 架构#

Stability AI 在 2024 年发布的 SD3 是首个将 DiT 思想落地到文生图产品的里程碑,核心创新是 MM-DiT(Multi-Modal DiT):

  • 图像 token 和文本 token 在同一个 Transformer 里联合处理:不再像 SD 1.x 那样文本通过 cross-attention 注入,而是把图像 patch token 和文本 token 拼成一个长序列,一起做 self-attention
  • 双流设计:图像和文本各自有独立的 W_Q/W_K/W_V 投影矩阵、FFN 和 LayerNorm 参数(因为两个模态的特征分布差异大)。"共享"的是 attention 的计算过程——两路的 K/V 拼接后做同一次 attention,图文互相可见(双向信息流),而非共享权重。
  • Flow Matching:使用 rectified flow 代替 DDPM 的加噪/去噪(更直的采样轨迹,详见第 6 讲)
  • 三个文本编码器:CLIP ViT-L(局部语义)+ OpenCLIP ViT-bigG(全局语义)+ T5-XXL(深层语言理解)

MM-DiT:concat(img_tokens,txt_tokens)→joint self-attentionfused_features\text{MM-DiT}: \text{concat}(\text{img\_tokens}, \text{txt\_tokens}) \xrightarrow{\text{joint self-attention}} \text{fused\_features}

图像和文本在同一个 attention 空间中交互

1.2 Flux:SD3 团队出走之作#

Black Forest Labs 由 SD 系列的核心开发者 Robin Rombach 等人创立,Flux 模型基于类似的 MM-DiT 架构,但做了多项改进:

  • 模型参数量达到 12B(SD3 最大约 8B)
  • 前 19 层双流 + 后 38 层单流,总共 57 层。设计逻辑如下:
    • 前 19 层(双流):图像和文本特征空间差异极大(图像是空间纹理,文本是语义语法),过早共享 FFN 会相互干扰。双流阶段图文各自用独立 FFN 在自己的特征空间充分发展,同时通过共享 attention 的 K/V 互相交换信息,逐步对齐。
    • 后 38 层(单流):经过前 19 层充分交互后,图文特征已高度融合对齐,分布趋于一致。此时两个模态不再需要各自独立的特征空间,切换为 concat(img, txt) → 共享 FFN。单流阶段层数更多(38层),专注于融合后特征的深度精细化,负责最终图像的细节质量。
    • 比例 19:38 = 1:2:实验调出的超参数。前期对齐需要的层数少,后期 38 层负责图像细节质量的精细生成。
    • 双流 vs 单流的参数量与计算量(按层对比):FFN 是逐 token 独立计算的,两种设计处理的 token 总数都是 Nimg+NtxtN_{img}+N_{txt},因此每层计算量相同。但双流每层有两套独立 FFN(FFNimg+FFNtxt\text{FFN}_{img} + \text{FFN}_{txt}),参数量为 2×8D22 \times 8D^2;单流共享一套 FFN,参数量为 8D28D^2,每层 FFN 参数量减半。切换单流不是为了省计算,而是因为特征对齐后不再需要两套独立参数空间。
  • 使用旋转位置编码 RoPE 替代可学习位置编码,对多分辨率泛化更好
  • 支持多种推理模式:Flux.1 [pro](闭源最强)、Flux.1 [dev](开源蒸馏版)、Flux.1 [schnell](4步快速版)

1.3 架构演进对比#

特性 SD 1.x / 2.x SD-XL SD3 Flux
Backbone U-Net U-Net (更大) MM-DiT MM-DiT 变体
文本编码器 CLIP ViT-L CLIP ViT-L + OpenCLIP ViT-bigG CLIP + OpenCLIP + T5-XXL CLIP + T5
条件注入 Cross-Attention Cross-Attention Joint Self-Attention Joint Self-Attention
采样方法 DDPM/DDIM DDPM/DDIM Flow Matching Flow Matching
参数量 ~860M ~2.6B ~2B / 8B ~12B
位置编码 固定 固定 可学习 RoPE

为什么 Joint Attention 比 Cross-Attention 好?

Cross-Attention:文本 token 只作为 K/V 被图像 token(Q)查询,文本不能主动"看到"图像特征。信息流是单向的。

Joint Self-Attention:图像和文本在同一序列中互相 attend,信息流是双向的。文本能根据图像特征调整表达,图像也能更精确地理解文本语义。代价是序列更长,计算量稍大。

DiT 风格 vs MM-DiT:条件到底怎么注入?

先澄清一点:原始 DiT(Peebles & Xie 2023)是 ImageNet 上的类别条件模型,只有 class label + timestep,根本没有文本编码器。这里的对比是"把 DiT 的条件机制迁移到文本"时的两种思路——把 pooled 向量当作类别槽位走 adaLN,还是把序列 token 塞进 joint attention。两者的根本区别在于文本表示是否会被更新:

DiT 风格(pooled → adaLN) MM-DiT
条件注入方式 文本 pooled 向量 + t 一起进 adaLN,生成 γ, β, α 调制参数(占用原始 DiT 的 class 槽位) 文本序列 token 与图像 token 拼接,参与 Joint Self-Attention
文本表示变化 冻结不变——始终是编码器的原始 pooled 输出 逐层更新——在与图像的交互中不断演化
信息流方向 单向:文本 → 图像(全局调制) 双向:文本 ↔ 图像(token 级交互)
类比 文本是"遥控器",拨一下旋钮就结束 文本是"参会者",在讨论中不断修正自己的理解

具体来说:DiT 风格注入把文本压成一个固定的 pooled 向量(丢掉了序列信息),送入 adaLN 的 MLP 生成调制参数 (γ,β,α)(\gamma, \beta, \alpha),仅影响图像特征的归一化,文本自身从第 1 层到最后一层始终不变。而 MM-DiT 把文本作为 token 序列和图像拼在同一个序列里,每一层 attention 后文本表示都会被更新(融入了图像信息),这让模型能实现更深层次的图文对齐——例如,当图像中出现歧义场景时,文本表示可以动态调整以消歧。

现实中两者并存:SD3 / Flux 同时用了这两种机制——CLIP 的 pooled embedding 走 adaLN(像类别标签那样全局调制),而 T5-XXL 的序列 token 走 Joint Self-Attention。前者提供全局语义锚点,后者提供细粒度的 token 级对齐。

1.4 DiT 四种主流变体横向对比#

从原始 DiT 衍生出的变体,创新点分别在输入处理和 block 内部结构两个维度:

变体 代表模型 模态数 创新点 文本注入方式 FFN
原始 DiT DiT-XL 1(图像) 基础结构,adaLN-Zero adaLN(类别标签) 共享
DiT Spacetime Sora 1(视频) 改了输入:3D patch 把时空打包成 token Cross-Attention(推测) 共享
MM-DiT SD3、Flux 前19层 2(图+文) 改了 block:图文 Joint Self-Attention Joint Self-Attention(双向) 图文各自独立
Dual-stream DiT HunyuanVideo 2(视频+文) 双流→单流渐进融合 前段 Joint Attn,后段共享 前段独立,后段共享

各变体 block 结构示意

原始 DiT block:tokens → Self-Attn → FFN → 输出(adaLN-Zero 注入 t + class)

DiT Spacetime block:spacetime tokens → Self-Attn → Cross-Attn(注入文本) → FFN → 输出,block 结构和原始 DiT 基本相同,核心创新在输入处理——用 3D Patchify 代替原来的 2D Patchify:

  • 原始 DiT(2D):对单帧 latent 按空间切 patch,每个 token 只含空间信息,patch=ph×pw×C\text{patch} = p_h \times p_w \times C
  • DiT Spacetime(3D):对视频 latent 同时沿时间和空间切 patch,每个 token 天然同时含时空信息,patch=pt×ph×pw×C\text{patch} = p_t \times p_h \times p_w \times C

数值例子:视频 latent 形状 16×32×32×416 \times 32 \times 32 \times 4,用 3D patch size (2,2,2)(2,2,2):

token 数 = (16/2) × (32/2) × (32/2) = 8 × 16 × 16 = 2048 个
每个 token 维度 = 2×2×2×4 = 32 维 → 线性投影到 hidden_dim D

对比 2D patchify(patch_size=2,16帧不做时间压缩):token 数 = 16×256 = 4096,且每个 token 只含单帧信息,时间关系需要额外的 Temporal Attention 模块来建立。3D patchify 把时空打包进 token,self-attention 自动覆盖时空关系,无需额外模块。另一个优势是天然支持可变分辨率和时长——不同输入产生不同数量的 token,Transformer 天然支持可变长序列。

MM-DiT block:concat(img tokens, txt tokens) → Joint Self-Attn → 拆分 → 各自独立 FFN,attention 权重共享,FFN 参数各自独立

Dual-stream DiT block(HunyuanVideo):前 19 层图文各自过独立 Self-Attn + FFN,但 attention 的 K/V 互相共享;后 38 层 concat 后完全共享 Self-Attn + FFN

这里的"K/V 互相共享"具体指:图文各自用独立的 WQ,WK,WVW_Q, W_K, W_V 投影矩阵算出自己的 Q/K/V,随后把两路的 Q、K、V 分别沿 token 维度拼成长序列,只跑一次 attention,算完后按 NtxtN_{txt} 的位置切回两路,各自送入独立 FFN。伪代码如下:

# Dual-stream attention(HunyuanVideo 前 19 层)

# Step 1: 各自独立投影(W 参数不共享,所以说"双流")
Q_img = W_Q_img @ x_img   # [N_img, D]
K_img = W_K_img @ x_img   # [N_img, D]
V_img = W_V_img @ x_img   # [N_img, D]

Q_txt = W_Q_txt @ x_txt   # [N_txt, D]
K_txt = W_K_txt @ x_txt   # [N_txt, D]
V_txt = W_V_txt @ x_txt   # [N_txt, D]

# Step 2: Q / K / V 全部沿 token 维拼接成长序列
Q = concat([Q_img, Q_txt], dim=0)   # [N_img + N_txt, D]
K = concat([K_img, K_txt], dim=0)   # [N_img + N_txt, D]
V = concat([V_img, V_txt], dim=0)   # [N_img + N_txt, D]

# Step 3: 跑一次标准 attention(内积 → 缩放 → softmax → 加权求和)
scores = Q @ K.T / sqrt(d)          # [N_img+N_txt, N_img+N_txt]
attn   = softmax(scores, dim=-1)    # 逐行归一化
out    = attn @ V                    # [N_img+N_txt, D]

# Step 4: 按 text_len 切回两路(split 的位置就是 N_txt 的边界)
out_img, out_txt = split(out, [N_img, N_txt], dim=0)

# Step 5: 各自独立 FFN(参数不共享)
x_img = x_img + FFN_img(out_img)
x_txt = x_txt + FFN_txt(out_txt)

几个容易混淆的点:

  • "双流"不是指两次 attention,而是指 Q/K/V 投影和 FFN 有两套独立参数。Attention 本身只算一次,就是拼接后的那个 Nimg+NtxtN_{img}+N_{txt} 长序列上的标准 self-attention。
  • "K/V 共享"是拼接的副产物:Q 拼在一起后,每个 query token(无论来自图还是文)都能看到全部 K,自然就"共享"了 K/V 池——不需要显式写一步共享。
  • 和 MM-DiT / Flux 的关系:SD3 的 MM-DiT block 在思想上也是"独立 proj + 联合 attention + 独立 FFN",Flux 的 double-stream block 同样如此。三者数学上等价,但各家代码里的具体拼接顺序、是否融进一次 kernel、adaLN 的作用点等细节可能不同,本伪代码只对 HunyuanVideo 的写法负责。
  • 和单流的区别:单流阶段投影矩阵 WQ,WK,WVW_Q, W_K, W_V 也共享(一套),FFN 也共享(一套),不再按模态切分。

**记忆口诀:**Spacetime DiT 改输入(3D patch),MM-DiT 改 block(Joint Attention),Dual-stream 是 MM-DiT 的进化版(双流渐进融合)。三者都继承了原始 DiT 的 adaLN-Zero 时间步注入。

2. FLUX 架构深度解析与 LoRA vs ControlNet#

上一节从宏观角度介绍了 SD3 和 Flux 的演进脉络。本节深入 FLUX.1 的完整架构细节、衍生模型(FLUX-Fill、FLUX-Kontext),以及 DiT/FLUX 生态中 LoRA 与 ControlNet 的对比——这些都是面试高频追问点。

2.1 FLUX.1 完整架构解析#

双流 → 单流架构(Double-stream → Single-stream)#

FLUX.1 的核心设计是前半部分双流 MM-DiT blocks + 后半部分单流 blocks,总计 57 层:

阶段 层数 结构 设计动机
前半(双流) 19 层 图文各自独立 Self-Attn + FFN,K/V 互相拼接做 Joint Attention 图像(空间纹理)和文本(语义语法)特征分布差异极大,独立 FFN 避免互相干扰,让各模态充分建立独立表示
后半(单流) 38 层 图文 token 拼接成统一序列,共享 Self-Attn + FFN 经双流对齐后特征分布趋于一致,共享参数促进深层融合,负责最终图像的细节质量

双流 vs 单流的信息流图示

# 双流阶段(前 19 层)
img_tokens ──→ [Self-Attn(Q_img, K_img∥K_txt, V_img∥V_txt)] → [FFN_img] → img_tokens'
txt_tokens ──→ [Self-Attn(Q_txt, K_img∥K_txt, V_img∥V_txt)] → [FFN_txt] → txt_tokens'
                        ↑ K/V 互相拼接,attention 共享 ↑

# 单流阶段(后 38 层)
concat(img_tokens, txt_tokens) → [Shared Self-Attn] → [Shared FFN] → output

参数量与计算量分析:

  • 双流每层有两套 FFN(FFNimg+FFNtxt\text{FFN}_{img} + \text{FFN}_{txt}),FFN 参数量为 2×8D22 \times 8D^2;单流每层共享一套 FFN,参数量为 8D28D^2
  • 但每层处理的 token 总数相同(Nimg+NtxtN_{img} + N_{txt}),所以每层 FLOPs 相同,切换单流是因为特征对齐后不需要两套参数空间,而非为了省计算
  • 总参数量:~12B(SD3 最大约 8B)

文本编码器:CLIP-L + T5-XXL#

FLUX.1 使用两个文本编码器(比 SD3 的三个少一个 CLIP-G),各司其职:

编码器 最大 token 数 注入方式 擅长 不足
CLIP ViT-L 77 pooled embedding → 类似类别条件(全局注入 adaLN) 语义对齐的全局特征,图文匹配能力强 弱于长文本理解、细节描述
T5-XXL 256 / 512 序列特征直接作为文本 token 进入双流 细粒度语义、长句理解、空间关系推理 缺乏视觉对齐预训练

为什么用两个编码器?

互补设计:CLIP 在图文对比学习中训练,天然擅长全局语义匹配("这张图像是什么"),但 77 token 上限限制了细粒度描述。T5 是纯语言模型,擅长理解复杂句子结构和空间关系("左边的红色杯子旁边有一本打开的书"),但没有视觉对齐能力。两者结合 = 全局语义 + 细节理解。

与 SD3 的区别:SD3 用 CLIP-L + CLIP-G + T5-XXL 三个编码器。FLUX 去掉了 CLIP-G(OpenCLIP ViT-bigG),用更大的模型容量(12B vs 8B)来弥补,简化了编码器管线。

Flow Matching 生成#

FLUX.1 使用 Rectified Flow(直线路径 ODE)作为生成范式:

  • 训练时采用 v-prediction:预测目标为速度场 v=x1−x0v = x_1 - x_0(即从噪声到数据的方向向量)
  • 前向插值路径:xt=(1−t)⋅x0+t⋅x1x_t = (1-t) \cdot x_0 + t \cdot x_1,其中 x0x_0 为纯噪声,x1x_1 为干净 latent
  • 推理时使用 Euler solver,通常 20-50 步

xt=(1−t)⋅x0+t⋅x1,vθ(xt,t)≈x1−x0x_t = (1-t) \cdot x_0 + t \cdot x_1, \quad v_\theta(x_t, t) \approx x_1 - x_0

Rectified Flow 的线性插值路径与 v-prediction 目标

Rectified Flow vs DDPM 的核心区别

DDPM:噪声调度是非线性的(αˉt\bar{\alpha}_t 曲线),采样需要数百步或使用 DDIM 加速。

Rectified Flow:前向路径是直线(线性插值),理论上一步就能到达,实际 20-50 步即可获得高质量结果。更直的轨迹 → 更少的截断误差 → 更少的采样步数。

2.2 FLUX-Fill:Inpainting 专用架构#

FLUX-Fill 是 FLUX.1 的 inpainting 变体,核心改动在输入层的通道拼接:

输入设计#

输入组件 通道数 说明
噪声 latent ztz_t 16 当前去噪步的带噪声隐变量
被遮挡图像 latent 16 原图经 VAE 编码后,mask 区域填零
二值 mask 1 降采样到 latent 尺寸,1 = 需重绘,0 = 保留
合计 33 通道拼接后送入第一层卷积

input=concat(zt,  zmasked,  m)∈RH′×W′×33\text{input} = \text{concat}(z_t, \; z_{masked}, \; m) \in \mathbb{R}^{H' \times W' \times 33}

FLUX-Fill 输入:原始 FLUX 的 16 通道扩展为 33 通道(16+16+1)

  • 架构改动极小:只需将第一层 patchify 卷积的 in_channels 从 16 改为 33,其余双流 → 单流结构完全不变
  • 训练方式:在大量 image-mask-text 三元组上训练,mask 形状随机生成(矩形、自由曲线、实例分割 mask 等),让模型学会根据上下文和文本提示填充任意区域
  • 推理时:mask 区域由模型生成,非 mask 区域在每步去噪后替换回原图 latent,确保未遮挡区域完全保留

2.3 FLUX-Kontext:参考图像条件生成#

FLUX-Kontext 解决的问题是给定一张参考图,生成与之风格/主体一致的新图,与 FLUX-Fill 有本质区别:

维度 FLUX-Fill FLUX-Kontext
任务 局部修复/重绘(inpainting) 参考图条件生成(style/subject transfer)
核心输入 原图 + 二值 mask 参考图(无 mask)
输入方式 通道拼接到噪声 latent 参考图 token 与文本 token 一起进入双流
位置编码 共享生成图的位置空间 独立的位置编码空间,与生成图分离
典型应用 去除/替换物体、背景填充 产品图变换、风格迁移、角色一致性生成

FLUX-Kontext 的关键设计

参考图作为条件 token:参考图经 VAE 编码后 patchify 为 token 序列,与文本 token 一同送入双流阶段。模型在 attention 中同时看到参考图 token、文本 token 和生成图 token。

位置编码分离:参考图 token 使用独立的位置编码空间,避免与生成图的位置编码混淆。这让模型明确区分"这是参考信息"和"这是要生成的内容"。

无需 mask:与 FLUX-Fill 不同,Kontext 不需要指定哪里要改——模型根据文本指令和参考图全局生成新图。

2.4 为什么用 LoRA 微调而不用 ControlNet?#

在 U-Net 时代,ControlNet 是结构化控制的标准方案。但在 DiT/FLUX 架构下,LoRA 成为了主流微调手段。这背后有深刻的架构原因:

ControlNet 的设计假设与 DiT 的冲突#

  • ControlNet 核心思路:复制 U-Net 的 encoder 半部分,将条件图(depth map、edge、pose 等)输入副本 encoder,通过 zero-conv 将副本的特征注入主 U-Net 的 decoder
  • 依赖 encoder-decoder 对称结构:U-Net 天然具有对称的 encoder-decoder + skip connection,ControlNet 正是利用这种对称性——副本 encoder 的各层输出通过 skip connection 路径注入对应的 decoder 层
  • DiT/FLUX 没有这种对称性:DiT 是堆叠相同 block 的平坦结构,没有 encoder/decoder 之分,没有 skip connection。ControlNet 的"复制 encoder"策略失去了结构基础

LoRA 的四大优势#

优势 具体说明
架构无关 LoRA 只在 attention/FFN 的权重矩阵上加低秩更新 W′=W+BAW' = W + BA(B∈Rd×r,A∈Rr×dB \in \mathbb{R}^{d \times r}, A \in \mathbb{R}^{r \times d}),不依赖任何特定网络拓扑
参数效率 一个 LoRA adapter 通常仅 50-300 MB(rank 16-64),而 ControlNet 副本通常 1-2 GB
可合并部署 推理前可将 LoRA 权重合并回原模型 Wmerged=W+BAW_{merged} = W + BA,无额外推理延迟。ControlNet 需要额外的前向计算
社区生态 LoRA 训练门槛低(单卡即可),文件小易于分享,Civitai 等平台上 FLUX LoRA 数量远超 ControlNet

W′=W+BA,B∈Rd×r,  A∈Rr×d,  r≪dW' = W + BA, \quad B \in \mathbb{R}^{d \times r}, \; A \in \mathbb{R}^{r \times d}, \; r \ll d

LoRA 低秩更新:rr 通常为 16-64,原始维度 dd 通常为 1024-3072

ControlNet 仍有不可替代的优势#

  • 强结构化控制:对 depth map、canny edge、human pose 等空间条件,ControlNet 的逐像素控制能力仍然更强——LoRA 只是整体风格/概念的微调,无法提供精确的空间约束
  • ControlNet for DiT 的变体已经出现:如 ControlNet-XS(用小型 Transformer 作为条件分支)和社区开发的 FLUX ControlNet 适配方案,通过在 DiT block 间插入条件注入层来实现类似功能
  • 不修改原模型权重:ControlNet 是外挂模块,不改变主模型参数,适合需要灵活切换多种控制条件的场景

面试答题思路:LoRA vs ControlNet

问:"FLUX 生态中为什么 LoRA 比 ControlNet 流行?"

答题框架:(1) ControlNet 设计依赖 U-Net 的 encoder-decoder 对称结构,DiT/FLUX 是平坦的 block 堆叠,缺乏这种结构基础。(2) LoRA 架构无关,只在线性层加低秩更新,适配任何 Transformer。(3) LoRA 参数效率高(几十 MB vs 几 GB)且可合并无延迟。(4) 但 ControlNet 在强结构化控制(depth/edge/pose)上仍有优势,ControlNet-XS 等 DiT 适配方案正在发展。结论:通用风格/概念微调选 LoRA,精确空间控制选 ControlNet 变体。

FLUX 系列架构全景对比

模型 任务 输入改动 架构改动 关键创新
FLUX.1 文生图 标准 noise latent (C=16) 19 双流 + 38 单流 = 57 层 双流→单流渐进融合,CLIP-L + T5-XXL
FLUX-Fill Inpainting 通道拼接 (C=33) 仅第一层卷积改变 noise + masked_image + mask 通道拼接
FLUX-Kontext 参考图生成 参考图 token 入双流 独立位置编码空间 参考图作为条件 token,无需 mask

**面试记忆口诀:**FLUX 三件套——基础版(文生图,双流→单流),Fill 版(inpainting,改输入通道 16→33),Kontext 版(参考图,加 token 不加 mask)。微调选 LoRA(架构无关、可合并),结构控制选 ControlNet 变体(但 DiT 适配还在发展中)。