深入理解 MMDiT:多模态扩散 Transformer 与 Stable Diffusion 3 革命
1. MMDiT 的核心思想
1.1 文本-图像生成的难题
DiT 已经证明 Transformer 可以出色地担任扩散模型的去噪网络(详见 Diffusion Transformer 文章),但 DiT 论文聚焦在类条件生成,并未真正面对文本条件这一更复杂、多模态的场景。
在 DiT 之后,一个自然的问题出现:
当文本是高度结构化的序列(带语义、语法、指代),图像是连续的二维信号时,Transformer 该如何让两者”互相理解”,从而生成精确匹配 prompt 的图像?
2024 年 Stability AI 与论文 “Scaling Rectified Flow Transformers for High-Resolution Image Synthesis” 给出了答案——MMDiT (Multimodal Diffusion Transformer)。它在 Stable Diffusion 3 中作为核心架构出现,并在之后的 FLUX 系列中被进一步发扬光大。
1.2 核心思想
MMDiT 的核心思想可以一句话概括:
把文本 token 与图像 token 都看作”同等地位”的一等 token 流,在一个共享的 Transformer 块里通过”双流联合注意力 (Joint Attention)” 反复交融——文本看图像、图像看文本。
┌────────────────────────────────────────────────────────────────┐│ MMDiT 双流架构示意 │├────────────────────────────────────────────────────────────────┤│ ││ 文本 token (TxtStream) ─────┐ ││ │ ││ ▼ ││ ┌──────────────────┐ ││ │ Joint Attention │ ← 两流都喂 Q/K/V ││ │ (双流 Transformer Block) ││ └────────┬─────────┘ ││ │ ││ 图像 token (ImgStream) ───┘ ││ ││ 重复 N 次 ││ ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ ││ ││ 最终: 图像侧 token → 噪声预测 (用于 Rectified Flow) ││ │└────────────────────────────────────────────────────────────────┘1.3 一个生活化的比喻
把 MMDiT 想成两位雕塑家合作:
- 雕塑家 A(图像流):负责揉捏大理石,但他不识字
- 雕塑家 B(文本流):负责读 customer 的要求,但手不动石头
- 关键:他们坐在一起——每次同时抬眼看同一份设计稿(联合注意力),并轮流在大理石上”勾勒一轮”
每一轮两位雕塑家都能更清楚对方的意思,最终:
- A 雕出完美符合 B 理解的形状
- B 也理解了 A 在几何上哪些细节其实更”可行”
这就是”双向交融”——这就是 MMDiT。
1.4 MMDiT 简史
2020 CLIP: 图文对齐 (对比学习) │ 解决了"图像和文本能不能放在同一空间"的问题 │2022 DiT: Transformer 替换 UNet │ 解决了"扩散能不能用 Transformer 主干" │ 但还只是 class-conditional │2023.4 PixArt-α: 把 DiT 加上 T5 文本条件 → 早期多模态尝试 │2024.6 SD3 论文: "Multimodal Diffusion Transformer" (MMDiT 正式登场) │ Stability AI │ Esser et al. │ 核心创新: 双流联合注意力 │ 配套: Rectified Flow (RF) 替代 DDPM │2024.7 SD3 Medium 发布 │ 2B 参数的开源/中等模型 │2024.8 FLUX.1 (black forest labs) │ 把 MMDiT + RF + DiT 推到极致 │ 直追 SD3 表现 │2024-2025 FLUX 系列: Pro, Dev, Schnell, 1.1 └─ FLUX.1 一举成名2. 为什么需要专门的多模态架构?
2.1 早期做法:Cross-Attention
UNet 时代,文本条件通过交叉注意力注入到 UNet 每一层:
图像特征 (Q) │ ├── Q = image_proj(image_feat) ├── K, V = text_proj(text_embed) │ ▼Attention(Q, K, V) = softmax(QK^T/√d) V │ ▼图像特征被"加权"过文本语义这种方法在 Stable Diffusion 1.x / 2.x / SDXL 中效果不错,但有一个根本问题:
文本流是”外部信息源”,而图像永远单向接收文本——文本看不到图像在想什么。
2.2 MMDiT 的关键观察
观察:人类在创作图像时,文字本身也会被视觉启发。
- 我开始写 “a man rides a horse” 时,脑海中先出现大概构图
- 再细化为 “wears a red coat”,文本含义也在被图像反馈”调整”
如果把文本固定成纯条件,就**缺失了”双向交互”**带来的 prompt 跟随能力(prompt adherence)和细节理解能力。
2.3 五种文本-图像融合方式对比
| 方式 | 代表 | 文本是否能看图像 | 信息流 | 训练效率 |
|---|---|---|---|---|
| In-context 拼接 | 早期 DiT | 否(只 prepend) | 单向 | 高 |
| Cross-Attention | SD 1/2/xl | 否(仅 K,V 来自文本) | 单向 | 中 |
| MM-DiT (联合注意力) ★ | SD3, FLUX | 是 | 双向 | 中 |
| Self-Attention 双流 | 简化版 | 是 | 双向 | 中 |
| Per-Modality Linear | 文生文模型 | 否 | 单向 | 高 |
MMDiT 把 Q/K/V 都从两流拼接得到 → 注意力矩阵天然支持 “token 之间的任意模态组合”
3. MMDiT 核心架构详解
3.1 整体框图
文本 embeddings (B, T, D) │ ▼ ┌─────────────────────────────┐ │ T5 / CLIP 文本编码器 │ (冻结或微调) └─────────────────────────────┘ │ │ txt_tokens (B, N_t, D_t) ▼ Rectified Flow timestep t │ ▼ ┌─────────────────────────────┐ │ Sinusoidal timestep emb │ │ + MLP │ -- c_t └─────────────────────────────┘ │ ▼ 噪声图像 z_t (B, C, H, W) -- Patchify --> img_tokens (B, N_v, D) │ ▼ ┌────────────────────────────────────────────┐ │ N × MM-DiT Block │ │ ┌──────────────────────────────┐ │ │ │ 文本侧 LN → Q_txt, K_txt, V_txt│ │ │ │ 图像侧 LN → Q_img, K_img, V_img│ │ │ │ │ │ │ │ Joint Attn(Q_all=[Q_txt;Q_img],│ │ │ │ K_all=[K_txt;K_img],│ │ │ │ V_all=[V_txt;V_img])│ │ │ │ │ │ │ │ Resplit → txt_out, img_out │ │ │ └──────────────────────────────┘ │ │ + Modulation (AdaLN-Zero by c_t) │ │ + MLP (per-stream) │ └────────────────────────────────────────────┘ │ ▼ Linear Decode (img stream only) │ ▼ 噪声预测 ε_θ (B, C, H, W)3.2 双流的实质含义
MMDiT 维护两个独立的 token 流:
- 图像流:长度为 (patch 数)
- 文本流:长度为 (文本 token 数)
每个 token 流有自己独立的 LayerNorm、Q/K/V 矩阵(不共享),但在注意力内部拼接:
Q = [Q_img ; Q_txt] # concat along sequence dimK = [K_img ; K_txt]V = [V_img ; V_img]算完注意力后,再拆分回两个流,各自送入独立的 MLP:
def joint_attention(self, img_tokens, txt_tokens): """Joint Attention: 两流共享注意力矩阵,但有独立投影。""" # 1) 计算各流的 Q, K, V q_img, k_img, v_img = self.img_proj_qkv(img_tokens) # 各自的 Linear q_txt, k_txt, v_txt = self.txt_proj_qkv(txt_tokens) # 不同的 Linear
# 2) 拼接: (B, N_v+N_t, D) q = torch.cat([q_img, q_txt], dim=1) k = torch.cat([k_img, k_txt], dim=1) v = torch.cat([v_img, v_txt], dim=1)
# 3) 标准 Multi-Head Attention out = F.scaled_dot_product_attention(q, k, v)
# 4) 拆分回两流 out_img = out[:, :N_v, :] out_txt = out[:, N_v:, :]
# 5) 各自输出投影 out_img = self.img_out_proj(out_img) out_txt = self.txt_out_proj(out_txt)
return out_img, out_txt关键事实:两流的 QKV 矩阵各自独立,不共享权重。这让模型可以学到对文本和图像”不同的注意力模式”——图像一边的 Q/K 矩阵只对”图像信号”敏感,文本一边的对”语义关系”敏感,但通过共享注意力矩阵让二者交融。
3.3 完整 MMDiT Block 代码
import torchimport torch.nn as nnimport torch.nn.functional as F
class RMSNorm(nn.Module): """FLUX 风格: 用 RMSNorm 代替 LayerNorm, 训练更稳。"""
def __init__(self, dim, eps=1e-6): super().__init__() self.eps = eps self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x): normed = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) return normed * self.weight
class TimestepEmbedder(nn.Module): """Sinusoidal timestep embedding."""
def __init__(self, hidden_size, frequency_embedding_size=256): super().__init__() self.frequency_embedding_size = frequency_embedding_size self.mlp = nn.Sequential( nn.Linear(frequency_embedding_size, hidden_size), nn.SiLU(), nn.Linear(hidden_size, hidden_size), )
@staticmethod def timestep_embedding(t, dim, max_period=10000): half = dim // 2 freqs = torch.exp( -math.log(max_period) * torch.arange(half, dtype=torch.float32) / half ).to(t.device) args = t[:, None].float() * freqs[None] embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) return embedding
def forward(self, t): return self.mlp(self.timestep_embedding(t, self.frequency_embedding_size))
def modulate(x, shift, scale): return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
class MmDiTBlock(nn.Module): """MMDiT Single Block: Joint Attention + 双流 MLP。"""
def __init__( self, hidden_size, img_mod_dim, txt_mod_dim, num_heads, mlp_ratio=4.0, ): super().__init__() self.num_heads = num_heads self.head_dim = hidden_size // num_heads self.scale = self.head_dim ** -0.5
# 1) 双流 Norm self.img_norm1 = RMSNorm(hidden_size) self.txt_norm1 = RMSNorm(hidden_size)
# 2) 双流独立 QKV 投影 self.img_qkv = nn.Linear(hidden_size, hidden_size * 3, bias=False) self.txt_qkv = nn.Linear(hidden_size, hidden_size * 3, bias=False) # (实际生产代码常用 fused QKV / tensor parallel)
# 3) 输出投影 (双流独立) self.img_out = nn.Linear(hidden_size, hidden_size, bias=False) self.txt_out = nn.Linear(hidden_size, hidden_size, bias=False)
# 4) MLP (双流独立) self.img_mlp = nn.Sequential( nn.Linear(hidden_size, int(hidden_size * mlp_ratio)), nn.GELU(), nn.Linear(int(hidden_size * mlp_ratio), hidden_size), ) self.txt_mlp = nn.Sequential( nn.Linear(hidden_size, int(hidden_size * mlp_ratio)), nn.GELU(), nn.Linear(int(hidden_size * mlp_ratio), hidden_size), )
# 5) AdaLN-Zero 调制参数生成 # 图像侧: shift_attn, scale_attn, gate_attn, shift_mlp, scale_mlp, gate_mlp (×2 流) self.adaLN_img = nn.Sequential(nn.SiLU(), nn.Linear(img_mod_dim, 6 * hidden_size)) self.adaLN_txt = nn.Sequential(nn.SiLU(), nn.Linear(txt_mod_dim, 6 * hidden_size))
def forward(self, img_tokens, txt_tokens, c_img, c_txt): B, Nv, D = img_tokens.shape _, Nt, _ = txt_tokens.shape
# 调制参数: (B, 6D) img_params = self.adaLN_img(c_img).chunk(6, dim=-1) txt_params = self.adaLN_txt(c_txt).chunk(6, dim=-1) img_shift_a, img_scale_a, img_gate_a, img_shift_m, img_scale_m, img_gate_m = img_params txt_shift_a, txt_scale_a, txt_gate_a, txt_shift_m, txt_scale_m, txt_gate_m = txt_params
# ======= Joint Attention ======= # 调制 + Norm img_n = modulate(self.img_norm1(img_tokens), img_shift_a, img_scale_a) txt_n = modulate(self.txt_norm1(txt_tokens), txt_shift_a, txt_scale_a)
# QKV qi, ki, vi = self.img_qkv(img_n).chunk(3, dim=-1) qt, kt, vt = self.txt_qkv(txt_n).chunk(3, dim=-1)
# Multi-Head reshape qi = qi.view(B, Nv, self.num_heads, self.head_dim).transpose(1, 2) ki = ki.view(B, Nv, self.num_heads, self.head_dim).transpose(1, 2) vi = vi.view(B, Nv, self.num_heads, self.head_dim).transpose(1, 2)
qt = qt.view(B, Nt, self.num_heads, self.head_dim).transpose(1, 2) kt = kt.view(B, Nt, self.num_heads, self.head_dim).transpose(1, 2) vt = vt.view(B, Nt, self.num_heads, self.head_dim).transpose(1, 2)
# 拼接 → Attention q = torch.cat([qi, qt], dim=2) # (B, h, Nv+Nt, d) k = torch.cat([ki, kt], dim=2) v = torch.cat([vi, vt], dim=2)
attn = F.scaled_dot_product_attention(q, k, v)
attn = attn.transpose(1, 2).reshape(B, Nv + Nt, D)
# 拆分回两流 a_img = attn[:, :Nv, :] a_txt = attn[:, Nv:, :]
# 门控残差 img_tokens = img_tokens + img_gate_a.unsqueeze(1) * self.img_out(a_img) txt_tokens = txt_tokens + txt_gate_a.unsqueeze(1) * self.txt_out(a_txt)
# ======= MLP ======= img_norm2 = RMSNorm(D) txt_norm2 = RMSNorm(D)
img_n2 = modulate(img_norm2(img_tokens), img_shift_m, img_scale_m) txt_n2 = modulate(txt_norm2(txt_tokens), txt_shift_m, txt_scale_m)
img_tokens = img_tokens + img_gate_m.unsqueeze(1) * self.img_mlp(img_n2) txt_tokens = txt_tokens + txt_gate_m.unsqueeze(1) * self.txt_mlp(txt_n2)
return img_tokens, txt_tokens3.4 注意力矩阵的物理含义
MMDiT 的注意力矩阵是 ,天然分成 4 个块:
作为 Key ↓ ↓ Image Key Text Key ┌────────────┬────────────┐Image Q │ Image↔Image│ Image↔Text│ (a) 关注到的另一个图像 patch ├────────────┼────────────┤ Text Q │ Text↔Image │ Text↔Text │ (b) 文本关注图像布局 └────────────┴────────────┘ (c) 图像看文本语义 (d) 文本看文本- (a) 图像内部自组织
- (b) 文本”看到”图像布局,这是 MMDiT 比 cross-attn 更优的关键——文本侧能根据”图像当前生成进度”自我调整
- (c) 图像看到文本,强化 prompt 遵循
- (d) 文本内部语义理解
4. 文本编码器:CLIP × T5 双保险
4.1 为什么需要两个文本编码器?
SD3 / MMDiT 默认采用 CLIP-G/14 + T5-XXL 双文本编码器:
| 编码器 | 输出维度 | 强项 | 在 MMDiT 中负责 |
|---|---|---|---|
| CLIP-G/14 | 768×77 | 整体语义对齐、拼写、风格 | 全局 prompt 理解 |
| T5-XXL | 4096×256 | 长文本、复杂描述、计数 | 细节与复杂场景理解 |
经验发现:CLIP 决定”画什么”,T5 决定”画对不对”。
4.2 双编码器融合
class DualTextEmbedder(nn.Module): """CLIP-G + T5-XXL 双文本编码器组合。"""
def __init__(self): super().__init__() self.clip = CLIPTextModel.from_pretrained("openclip-vit-bigG-14") self.t5 = T5EncoderModel.from_pretrained("t5-xxl") # 冻结两个文本编码器 (实际部署时大多冻结) for p in self.clip.parameters(): p.requires_grad = False for p in self.t5.parameters(): p.requires_grad = False
def forward(self, prompt_ids): clip_out = self.clip(prompt_ids["clip_ids"]).last_hidden_state # (B, 77, 768) t5_out = self.t5(prompt_ids["t5_ids"]).last_hidden_state # (B, 256, 4096) return clip_out, t5_out下游有 2 种做法:
- MMDiT 原文: 两个编码器独立传入两块 MMDiT(早期版本)
- MMDiT 现代版(SD3 medium): 拼接两路 token 共享一个 MMDiT
5. Rectified Flow (RF) 训练范式
5.1 从 DDPM 到 Rectified Flow
SD3 / FLUX 不再使用传统 DDPM 的 加噪过程,而是采用 Rectified Flow (RF) ——一种更”线性”的路径设计。
5.2 数学定义
Rectified Flow 把样本和噪声之间拉成直线轨迹:
直觉:让模型学习一个”速度场” 。无论哪个时刻都沿同一条直线。
def rf_training_step(model, x0): """Rectified Flow 训练 (SD3 / FLUX 使用)。""" B = x0.size(0)
# 1. 采样时间步 t ∈ [0, 1] t = torch.rand(B) # uniform
# 2. 采样噪声 noise = torch.randn_like(x0)
# 3. 沿"直线"插值 xt = (1 - t.view(-1, 1, 1, 1)) * x0 + t.view(-1, 1, 1, 1) * noise
# 4. 模型预测速度场 (velocity) v_pred = model(xt, t)
# 5. 真实速度: ε - x0 v_target = noise - x0
# 6. MSE loss = F.mse_loss(v_pred, v_target) return loss5.3 RF vs DDPM 的关键差异
| 特性 | DDPM | Rectified Flow |
|---|---|---|
| 路径 | 弧线(随机过程) | 直线 |
| 时间步 | 离散 [0, T] | 连续 [0, 1] |
| 学习目标 | 噪声 | 速度 |
| 训练误差分布 | 集中在高噪声 | 均匀 |
| 推理步数 | 30-50 | 4-20 (FLUX Schnell) |
| 蒸馏难度 | 中 | 极易(轨迹直,ODE 简单) |
RF 的关键优势:训练更稳 + 推理更短 + 蒸馏更易。FLUX Schnell 只需 4 步就能出高质量图像。
5.4 推理过程 (Euler / Heun ODE)
@torch.no_grad()def rf_sample(model, shape, prompt_embed, num_steps=20, method="euler"): """Rectified Flow 的 ODE 采样。""" x = torch.randn(*shape)
# 时间步网格 timesteps = torch.linspace(1.0, 0.0, num_steps + 1) dt = timesteps[0] - timesteps[1]
for i in range(num_steps): t = timesteps[i] t_next = timesteps[i + 1] t_batch = t * torch.ones(shape[0], device=x.device)
# 预测速度场 v = model(x, t_batch, prompt_embed)
# Euler 步 (也可以用 Heun) x = x + (t_next - t) * v
return x6. SD3 vs FLUX vs SDXL:架构对比
6.1 三大现代生图架构
| 维度 | SDXL (2023) | SD3 / MMDiT (2024) | FLUX.1 (2024) |
|---|---|---|---|
| 主体网络 | UNet | MM-DiT (双流) | MM-DiT + 串行块 |
| 文本编码 | CLIP-L + CLIP-G | CLIP-G + T5-XXL | CLIP + T5 |
| 时间步 | DDPM | Rectified Flow | Rectified Flow |
| 文字渲染 | 弱 | 强 | 强 |
| 默认分辨率 | 1024² | 1024² | 1024² |
| 参数量 | 2.6B | 2B → 8B (Large) | 12B (Dev) |
| 推理步数 | 30-50 | 20-30 | 4 (Schnell) ~ 30 (Dev) |
| 开源 | ✅ | ⚠️ (社区多) | ⚠️ (Dev 开源) |
6.2 FLUX 的进一步变化
FLUX (Black Forest Labs) 在 MMDiT 基础上加了:
- 串行双 Transformer 块 (Double Block)
- MMDiT 块 → 独立 MLP 块交替
- 并行单 Transformer 块 (Single Block)
- 把图像 token 和文本 token 真的在 token 维度上拼接
- 让二者在同一组注意力矩阵里”完全等价地”交融
class FluxDoubleBlock(nn.Module): """FLUX 的 Double Block: MMDiT 块 + 独立 MLP。"""
def __init__(self, hidden_size): super().__init__() self.mmdit = MmDiTBlock(hidden_size) # 联合注意力 self.img_mlp = nn.Sequential( nn.Linear(hidden_size, 4 * hidden_size), nn.GELU(), nn.Linear(4 * hidden_size, hidden_size), ) self.txt_mlp = nn.Sequential( nn.Linear(hidden_size, 4 * hidden_size), nn.GELU(), nn.Linear(4 * hidden_size, hidden_size), )
def forward(self, img, txt, c): # 1) 联合注意力 (双流) img, txt = self.mmdit(img, txt, c, c)
# 2) 独立 MLP (独立流) img = img + self.img_mlp(img) txt = txt + self.txt_mlp(txt)
return img, txt
class FluxSingleBlock(nn.Module): """FLUX 的 Single Block: 真把两流拼接。"""
def __init__(self, hidden_size, num_heads): super().__init__() # 单一 QKV / 单一 MLP: 两流完全共享 self.qkv = nn.Linear(hidden_size, 3 * hidden_size) self.proj = nn.Linear(hidden_size, hidden_size) self.mlp = nn.Sequential( nn.Linear(hidden_size, 4 * hidden_size), nn.GELU(), nn.Linear(4 * hidden_size, hidden_size), )
def forward(self, img, txt): # 把两流拼接成一条序列 x = torch.cat([img, txt], dim=1) # 单 QKV + 单 MLP q, k, v = self.qkv(x).chunk(3, dim=-1) x = self.proj(F.scaled_dot_product_attention(q, k, v)) x = x + self.mlp(x)
Nv = img.shape[1] return x[:, :Nv, :], x[:, Nv:, :]6.3 块组合方式的演化
| 阶段 | 块结构 | 信息交融 |
|---|---|---|
| SDXL UNet | 单一 UNet + 局部 cross-attn | 文本单向 |
| MMDiT (SD3) | 全部 MMDiT 双流块 | 文本双向,独立 MLP |
| FLUX | Mix 双流 + 单流的混合 | 更深度双向,最后共享算子 |
7. 训练策略与工程细节
7.1 训练配方 (SD3 论文)
阶段 1: 预训练 - 数据: LAION, COYO 等 (10B+ 图文对) - 分辨率: 256 → 512 → 1024 (渐进式增长) - 优化: AdamW, lr=1e-4, beta=(0.9, 0.95), wd=0 - Batch size: 1024-4096 - 时间调度: Rectified Flow (uniform [0,1]) - 文本编码器: 部分微调 / 完全冻结
阶段 2: 微调 (可选) - 数据: 高质量美学数据集 - 主题: DPO / 人类反馈
阶段 3: 蒸馏 (FLUX Schnell) - 蒸馏到 1-4 步 ODE 求解 - 损失: 对抗 + LPIPS + Flow Matching7.2 关键超参
Rectified Flow: 时间采样: 均匀 (实际常采用重要性权重) 训练时间步数: 1000+ 推理步数: 20 (常规), 4 (Schnell)
文本条件: - 视觉 prompt 长度: 77 (CLIP) + 256 (T5) - CFG scale: 5-7 通常是 sweet spot
图像生成: - 图像 latent 维度: VAE 编码后 4 channels - Patch size: 通常 2 (与 DiT 时代相同) - 位置编码: RoPE / Sin-Cos / ALiBi7.3 RoPE 在 MMDiT 中的应用
FLUX 使用 2D Rotary Position Embedding (RoPE) 替代 DiT 的 1D 位置编码:
class RoPE2D(nn.Module): """2D RoPE for image. 行坐标 + 列坐标各拿一半维度。"""
def __init__(self, head_dim, base=10000): super().__init__() assert head_dim % 4 == 0, "head_dim must be divisible by 4" self.head_dim = head_dim self.base = base
def _rotate_half(self, x): d = x.shape[-1] half = d // 2 # 前一半: x 坐标, 后一半: y 坐标 x_rot, y_rot = x[..., :half], x[..., half:] # 旋转 90° return torch.cat([-x_rot, -y_rot], dim=-1)
def forward(self, q, k, grid_h, grid_w): B, H, N, D = q.shape half = D // 2
# 生成 sin / cos device = q.device # 简化的实现, 实际生产常用高效 fused RoPE ys = torch.arange(grid_h, device=device).repeat_interleave(grid_w) xs = torch.arange(grid_w, device=device).repeat(grid_h) # compute frequency → apply rotation ... return q_rot, k_rotRoPE 优势:可外推到训练时未见过的分辨率,无需插值。
7.4 推理优化技巧
# 1) 量化 (FP8 → INT8)torch.cuda.amp.autocast(dtype=torch.bfloat16)
# 2) TensorRT / compilediffusion_model = torch.compile(diffusion_model, mode="max-autotune")
# 3) Attention 蒸馏# - 用普通 Attention 训练# - 推理时换为 MemoryEfficient / Flash Attention
# 4) 流水线# - text encoder 与 VAE decoder 并行# - 推理时 tokenizer + text encoder 可以预计算 (prompt cache)8. Rectified Flow 与普通扩散的对应
8.1 关系图
DDPM/SD (噪声预测) x_t = α x_0 + σ ε 目标 = ε │ │ 通过 Linear Flow 等价变换 ▼Flow Matching x_t = (1-t)x_0 + t ε 目标 = ε - x_0 (速度场) │ │ 简化路径 (让轨迹更直) ▼Rectified Flow x_t = (1-t)x_0 + t ε 目标 = ε - x_0 (同, 但路径更直)8.2 SD3 论文中 Rectified Flow 的优点
- 训练效率:相比 DDPM 收敛更快
- 推理步数:30 步即得 SOTA 质量
- 蒸馏:1-4 步 ODE 求解仍能保留 90%+ 质量
FLUX Schnell:
训练: 用 1024 步 ODE 路径训练 (Rectified Flow)蒸馏: 引入对抗损失, 强制 1-4 步达到几乎相同输出推理: 4 步 Euler 即可生成 1024×1024 高质量图9. 完整 MMDiT 模型类
9.1 主类
class MMDiT(nn.Module): """Multimodal Diffusion Transformer (SD3 风格)。"""
def __init__( self, img_size=64, # latent 分辨率 patch_size=2, in_chans=4, # VAE latent 通道 hidden_size=1024, depth=12, num_heads=16, mlp_ratio=4.0, txt_seq_len=256, txt_dim=4096, # T5 输出维度 ): super().__init__() self.img_size = img_size self.patch_size = patch_size self.num_patches = (img_size // patch_size) ** 2
# 1) 图像侧 Patchify self.patch_embed = nn.Conv2d(in_chans, hidden_size, patch_size, patch_size) # 2D Sin-Cos 位置编码 self.pos_embed = nn.Parameter( get_2d_sincos_pos_embed(hidden_size, int(self.num_patches ** 0.5)), requires_grad=False, )
# 2) 文本侧 Projection self.txt_proj = nn.Linear(txt_dim, hidden_size) self.txt_pos = nn.Parameter(torch.zeros(1, txt_seq_len, hidden_size)) nn.init.trunc_normal_(self.txt_pos, std=0.02)
# 3) 条件注入 self.t_embedder = TimestepEmbedder(hidden_size) # 文本向量还会与 timestep 相加
# 4) N 个 MMDiT block self.blocks = nn.ModuleList([ MmDiTBlock(hidden_size, hidden_size, hidden_size, num_heads, mlp_ratio) for _ in range(depth) ])
# 5) Final Norm + AdaLN + Final Layer self.norm_final = RMSNorm(hidden_size) self.final_adaLN = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size)) self.final_layer = nn.Linear(hidden_size, patch_size ** 2 * in_chans)
# 6) Zero-init (AdaLN-Zero 必做) self._init_weights()
def _init_weights(self): # AdaLN 的 Linear 全部 zero-init for block in self.blocks: nn.init.zeros_(block.adaLN_img[1].weight) nn.init.zeros_(block.adaLN_img[1].bias) nn.init.zeros_(block.adaLN_txt[1].weight) nn.init.zeros_(block.adaLN_txt[1].bias) nn.init.zeros_(self.final_adaLN[1].weight) nn.init.zeros_(self.final_adaLN[1].bias) nn.init.zeros_(self.final_layer.weight) nn.init.zeros_(self.final_layer.bias)
def unpatchify(self, x): """(B, S, p*p*C) → (B, C, H, W).""" p = self.patch_size H = W = int(x.shape[1] ** 0.5) x = x.reshape(x.shape[0], H, W, p, p, -1) x = torch.einsum("bhwpqc->bchpwq", x) return x.reshape(x.shape[0], -1, H * p, W * p)
def forward(self, z_t, t, txt_tokens): """ z_t: (B, C, H, W) VAE latent (噪声后) t: (B,) timestep ∈ [0, 1] txt_tokens: (B, N_t, txt_dim) T5 输出 return: v_pred (B, C, H, W) """ B = z_t.shape[0]
# 1) Patchify + 位置编码 img = self.patch_embed(z_t).flatten(2).transpose(1, 2) # (B, N_v, D) img = img + self.pos_embed.unsqueeze(0)
# 2) 文本投影 txt = self.txt_proj(txt_tokens) # (B, N_t, D) txt = txt + self.txt_pos
# 3) 时间步 + 文本 (共享) c = self.t_embedder(t) + txt.mean(dim=1, keepdim=True).expand(-1, img.shape[1] + txt.shape[1], -1).reshape(B, -1).mean(dim=-1, keepdim=True) c_img = c c_txt = self.t_embedder(t) # 文本侧也独立一个调制信号 (MMDiT 现代做法) # 实践中 c_txt 与 c_img 同源 c_img = c_txt = c.squeeze() if c.dim() > 1 else c
# 4) N 个 MMDiT block for block in self.blocks: img, txt = block(img, txt, c_img, c_txt)
# 5) 最终 AdaLN + Linear 解码 shift, scale = self.final_adaLN(c).chunk(2, dim=-1) img = modulate(self.norm_final(img), shift, scale) img = self.final_layer(img) img = self.unpatchify(img)
return img10. MMDiT 与多模态生成的其它生态
10.1 与其它多模态架构的关系
| 架构 | 核心思路 | MMDiT 与之对比 |
|---|---|---|
| CLIP | 视觉-语言对比学习 | 单纯表征对齐,不生成 |
| LLaVA | LLM + vision encoder | 文本生文本 (image→text) |
| DiT + 文本 cross-attn | 文本单向条件 | 文本看不到图像 |
| MMDiT / FLUX ★ | 双流联合注意力,文本双向 | 文本与图像深度双向交融 |
| Transfusion | 离散文本扩散 + 连续图像扩散 | 异质融合,复杂但统一 |
| Show-o | 单流 + 离散潜空间 | 单流简化版 MMDiT |
10.2 SD3 和 FLUX 的”视觉 prompt”能力
MMDiT 因为双流融合,在以下方面特别强:
✓ 文字渲染: "happy birthday" 写在蛋糕上 → 准确拼写
✓ 复杂组合: "red cube above blue sphere on wooden table" → 准确空间关系
✓ 计数: "3 cats and 1 dog" → 准确数量
✓ 多语言支持: 中英文混合 prompt
✓ Prompt adherence: "穿着蓝色外套的老人,背后一棵松树" → 中文复杂 prompt 准确11. MMDiT 的局限与未来
11.1 当前局限
| 问题 | 描述 |
|---|---|
| 双流计算翻倍 | 文本+图像都过 QKV/O 投影,显存 ≥ 2× UNet |
| 文本长度上限 | T5 256 tokens, 超长 prompt 截断 |
| CFG 仍是必需 | 推理时还要算两次 (cond + uncond), 浪费 |
| 长视频昂贵 | 序列长度二次方增长 |
| 文本编码器是瓶颈 | T5-XXL 推理比 DiT 还慢 |
11.2 未来方向
效率: ├── 共享 QKV (双流合一) ├── 量化 (FP8/INT4) ├── 蒸馏到 1-4 步 (FLUX Schnell 路线) └── 移除 CFG, 用引导替代
能力: ├── 视频 + 多帧联合注意力 ├── 任意分辨率 / 长宽比 (RoPE) ├── 多语种 prompt ├── 文本编辑图像 (instruction-based inpainting) └── 4D 内容生成 (时空动态)
统一: ├── 与 LLM 共架构 (语言 + 视觉扩散统一预训练) ├── 与 AudioLDM / MusicLDM 统一 (音频 + 图像) └── 与 RL/VLA 融合 (机器人, 决策)11.3 一句话总结
MMDiT 把”图文对齐”从”加条件”提升到了”双向对话”——文本理解图像生成进度、图像感知文本意图细节。这种协同,是 SD3/FLUX 在 DALL·E 3 / Midjourney 之外杀出一片天的核心原因,也是”生成模型 Transformer 化”的最新里程碑。
12. 总结
12.1 核心要点
| 维度 | 关键要点 |
|---|---|
| 核心创新 | 双流联合注意力 (text ↔ image) |
| 关键设计 | 文本与图像 token 在同一注意力矩阵,但有独立 QKV/MLP |
| Rectified Flow | 直线 ODE 路径 + 速度场学习 → 训练更稳 + 推理更短 |
| 双文本编码器 | CLIP-G (整体) + T5-XXL (细节) |
| 时空定位 | AdaLN-Zero 调制 + RoPE 位置编码 (FLUX) |
| 代表实现 | SD3 Medium (2B/8B), FLUX Dev/Schnell (12B) |
12.2 推荐学习资源
论文: - SD3 (2024): "Scaling Rectified Flow Transformers for High-Resolution Image Synthesis" - Rectified Flow (2022): "Flow Straight and Fast: Learning to Generate and Transfer Data" - RF-LR (2024): "Scaling Rectified Flow for Image Understanding"
代码: - Stability-AI/sd3-ref (官方参考实现) - black-forest-labs/FLUX (FLUX.1 推理实现) - diffusers/StableDiffusion3Pipeline (HF)
工具: - HF diffusers - ComfyUI 节点 - A1111 WebUI (SD3 适配)一句话总结:MMDiT 通过”双流 + 联合注意力 + Rectified Flow”三大支柱,让 Transformer 同时吃透图像与文本——是 Stable Diffusion 3 与 FLUX 引领”生图 Transformer 化”的灵魂设计,也是这一代高质量 T2I 模型最值得理解的核心架构。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!

