深入理解 MMDiT:多模态扩散 Transformer 与 Stable Diffusion 3 革命

5279 字
26 分钟
深入理解 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-AttentionSD 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 流

  • 图像流:长度为 NvN_v(patch 数)
  • 文本流:长度为 NtN_t(文本 token 数)

每个 token 流有自己独立的 LayerNormQ/K/V 矩阵(不共享),但在注意力内部拼接

Q = [Q_img ; Q_txt] # concat along sequence dim
K = [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 torch
import torch.nn as nn
import 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_tokens

3.4 注意力矩阵的物理含义#

MMDiT 的注意力矩阵是 (Nv+Nt)×(Nv+Nt)(N_v + N_t) \times (N_v + N_t),天然分成 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/14768×77整体语义对齐、拼写、风格全局 prompt 理解
T5-XXL4096×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 的 xt=αˉtx0+1αˉtϵx_t = \sqrt{\bar\alpha_t} x_0 + \sqrt{1-\bar\alpha_t}\epsilon 加噪过程,而是采用 Rectified Flow (RF) ——一种更”线性”的路径设计。

5.2 数学定义#

Rectified Flow 把样本和噪声之间拉成直线轨迹

xt=(1t)x0+tϵ,t[0,1]x_t = (1 - t) \cdot x_0 + t \cdot \epsilon, \quad t \in [0, 1]

直觉:让模型学习一个”速度场” vθ(xt,t)ϵx0v_\theta(x_t, t) \approx \epsilon - x_0。无论哪个时刻都沿同一条直线。

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 loss

5.3 RF vs DDPM 的关键差异#

特性DDPMRectified Flow
路径弧线(随机过程)直线
时间步离散 [0, T]连续 [0, 1]
学习目标噪声 ϵ\epsilon速度 v=ϵx0v = \epsilon - x_0
训练误差分布集中在高噪声均匀
推理步数30-504-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 x

6. SD3 vs FLUX vs SDXL:架构对比#

6.1 三大现代生图架构#

维度SDXL (2023)SD3 / MMDiT (2024)FLUX.1 (2024)
主体网络UNetMM-DiT (双流)MM-DiT + 串行块
文本编码CLIP-L + CLIP-GCLIP-G + T5-XXLCLIP + T5
时间步DDPMRectified FlowRectified Flow
文字渲染
默认分辨率1024²1024²1024²
参数量2.6B2B → 8B (Large)12B (Dev)
推理步数30-5020-304 (Schnell) ~ 30 (Dev)
开源⚠️ (社区多)⚠️ (Dev 开源)

6.2 FLUX 的进一步变化#

FLUX (Black Forest Labs) 在 MMDiT 基础上加了:

  1. 串行双 Transformer 块 (Double Block)
    • MMDiT 块 → 独立 MLP 块交替
  2. 并行单 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
FLUXMix 双流 + 单流的混合更深度双向,最后共享算子

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 Matching

7.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 / ALiBi

7.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_rot

RoPE 优势:可外推到训练时未见过的分辨率,无需插值。

7.4 推理优化技巧#

# 1) 量化 (FP8 → INT8)
torch.cuda.amp.autocast(dtype=torch.bfloat16)
# 2) TensorRT / compile
diffusion_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 img

10. MMDiT 与多模态生成的其它生态#

10.1 与其它多模态架构的关系#

架构核心思路MMDiT 与之对比
CLIP视觉-语言对比学习单纯表征对齐,不生成
LLaVALLM + 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 模型最值得理解的核心架构。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

深入理解 MMDiT:多模态扩散 Transformer 与 Stable Diffusion 3 革命
https://aiattnstudio.link/posts/mmdit/
作者
Federico
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author

Federico

AI Research Lab

Hello, I'm Federico.

关于实验室 / About
公告

欢迎来到Federico的个人博客

分类
标签
站点统计
57文章
7分类
404标签