SeFi-Image: A Text-to-Image Foundation Model with Semantic-First Diffusion

SeFi-Image 生成样例画廊
Fig. 1 — SeFi-Image 的生成样例(论文 Figure 1/2,来自仓库 assets/teaser_canvas.jpg)。覆盖照片级真实感、复杂排版长文字、中英双语文字渲染等场景。

1. 出发点 (Motivation)

训练一个文生图基础模型很烧钱。即便是号称「资源友好」的 Z-Image,也报告用了 314K H800 GPU·时[§1]。能不能更省?

学术界早有一条省钱思路:用预训练视觉编码器(DINOv2 等)的语义信息来加速扩散训练。这条线上有一堆方法——RAE / VA-VAE 重塑或对齐 latent 空间、REPA 做特征级正则、ReDi / REG 做语义-纹理联合生成、SFD 做语义-纹理异步建模。它们都在 ImageNet 256×256 类别条件生成上拿到了显著的收敛加速和更好的 FID。

但有一个尴尬的鸿沟:这些结果几乎只在「小模型(通常 <1B)+ 玩具数据集 + 类别条件」的设定下验证过[§1]。三个真正要紧的问题没人回答:

  1. 语义引导在更大模型上还有效吗?
  2. 更高分辨率上还有效吗?
  3. 最关键的——它能迁移到**真正实用的「文生图」**设定(文本条件、开放域、SOTA 级别)吗?

SeFi-Image 就是来填这个坑的:它以 Semantic-First Diffusion (SFD) 为骨架,做出一个能和 Qwen-Image / Z-Image 掰手腕的 T2I 基础模型,并提供 1B / 2B / 5B 三档,系统性地研究 scaling 行为。卖点是**重建-生成权衡(reconstruction-generation trade-off)**做得好:它跑在一个高保真重建的 VAE latent 空间里(利于文字、细节、未来的编辑任务),却依然收敛飞快、最终质量很高。

一句话动机:过去「语义引导加速扩散」只在 ImageNet 上玩过;SeFi-Image 第一次把它做成了一个工业级、追平 SOTA 的文生图基础模型,而且只花了对手 ~1/5 的算力。

2. 方法 (Method)

2.1 直觉:扩散本来就是「先粗后细」,那就让语义先去噪

普通的 latent 扩散 / flow-matching 模型,把图像压成一个 latent,然后所有信息共用同一个时间步一起去噪——这隐式地强迫「语义」和「纹理」同步演化。

但图像生成天然是 coarse-to-fine(先粗后细):全局语义和物体布局通常在高频纹理细节之前就确定了。SFD 的核心 idea 就是顺着这个规律——把语义和纹理沿扩散时间轴拆开,让语义比纹理早一点被解出来,于是纹理生成永远是在一个「更干净的语义锚点」条件下进行的。

SFD 三阶段异步去噪示意
Fig. 2(论文 Fig. 5)— SFD 的三阶段异步去噪。横轴是去噪进度 t。上面虚线是语义流 s 的轨迹,下面实线是纹理流 z 的轨迹:语义始终领先纹理一个偏移 Δt。Stage I(t∈[0,Δt))只去噪语义、纹理还是纯噪声;Stage II(中段)两路一起异步去噪,语义领跑;Stage III(末段)语义已干净、只继续精修纹理。最终只解码纹理 latent z₁ 得到图像。

2.2 复合 latent:语义一路、纹理一路

给一张图 \(x\),SFD 从两个来源构造一个复合 latent

语义与纹理 latent 的构造
Fig. 3(论文 Fig. 6)— 复合 latent 的构造。上路:图像 → Texture VAE → 纹理 latent z₁ → 按纹理时间步 tz 加噪。下路:图像 → 视觉基础模型 → 语义特征 fs → Semantic VAE → 语义 latent s₁ → 按语义时间步 ts 加噪。两路加**独立**的高斯噪声、用**不同**的时间步。

按 flow matching 的线性插值路径,给两路分别加噪:

\[s_{t_s} = (1 - t_s)\,s_0 + t_s\,s_1, \qquad z_{t_z} = (1 - t_z)\,z_0 + t_z\,z_1\]

—— 翻译: s₀、z₀ 是纯高斯噪声,s₁、z₁ 是干净 latent。时间步 t=0 是纯噪声、t=1 是干净。两路各走各的插值,关键在于它们用不同的 t。

2.3 核心机关:让语义时间步「领先」纹理一个固定偏移 Δt

怎么保证「语义比纹理干净」?SFD 用一个非常简洁的采样+钳位规则给每张图分配两个时间步:

\[t_s \sim \mathcal{U}(0,\, 1 + \Delta t), \qquad t_z = \max(0,\, t_s - \Delta t), \qquad t_s = \min(t_s,\, 1)\]

—— 翻译: 先把语义时间步 t_s 从一个「加长」的区间 [0, 1+Δt] 里采样;纹理时间步 t_z 就是它减去固定偏移 Δt 再钳到 ≥0;最后把 t_s 钳到 ≤1。结果保证 t_s ≥ t_z,即语义经历的噪声污染始终不超过纹理——也就是「语义更干净,给纹理当结构向导」。

把区间从 \([0,1]\) 加长到 \([0,1+\Delta t]\) 是为了让「偏移 Δt」在两端也不越界(这样三个阶段才能凑齐)。\(\Delta t\) 是这套机制唯一的新超参,训练时取 0.1–0.2(见后文训练表)。

2.4 双时间步 DiT 与训练目标

主干是一个 Transformer \(v_\theta(\cdot)\),吃复合噪声 latent \([s_{t_s}, z_{t_z}]\)两个独立时间步 \([t_s, t_z]\)、以及文本条件 \(c\),同时预测两路的速度:

\[[\hat v_s,\, \hat v_z] = v_\theta\big([s_{t_s}, z_{t_z}],\, [t_s, t_z],\, c\big)\]

—— 翻译: 一个网络同时输出语义速度 v̂_s 和纹理速度 v̂_z;它必须「知道」两路各自处在什么噪声水平(所以要喂两个时间步)。

SeFi-Image 整体框架 DiT
Fig. 4(论文 Fig. 7)— 整体框架。DiT 接收 [复合噪声 latent、双时间步嵌入、文本嵌入],输出语义和纹理两路的速度。

训练目标是两路速度预测损失的加权和(flow-matching 的 velocity 目标就是 \(v = x_1 - x_0\)):

\[\mathcal{L}_{\text{pred}} = \mathbb{E}\Big[\,\lVert \hat v_z - (z_1 - z_0)\rVert^2 + \beta\,\lVert \hat v_s - (s_1 - s_0)\rVert^2\,\Big]\]

—— 翻译: 让网络预测的纹理速度逼近真实纹理速度 (z₁−z₀),语义速度逼近 (s₁−s₀);β 是语义损失的权重(训练表里取 1~2)。

此外还叠加了 REPA[11] 的表示对齐损失,把扩散隐藏态对齐到预训练视觉编码器的表示。最终目标是:

\[\mathcal{L}_{\text{total}} = \mathcal{L}_{\text{pred}} + \lambda\,\mathcal{L}_{\text{REPA}}\]

—— 翻译: 主损失(双路速度预测)+ λ 倍的 REPA 对齐损失。注意 REPA 对齐的目标特征 y*,恰好就是喂给 Semantic VAE 的那个语义特征 f_s——语义信号在这里被「用了两遍」。

代码:双时间步嵌入

SFD 对 FLUX.2 DiT 最直接的改造,就是把单一时间步嵌入换成双时间步嵌入——语义、纹理各过一个独立的 TimestepEmbedding,再 concat。这正是 Eq. (5) 里「网络要同时知道两个 t」在代码里的样子:

repo/sefi/modeling/flux2_sefi_transformer.py:L24-L56 — 双时间步嵌入:concat([emb_sem, emb_tex])

class SEFIDualTimestepEmbeddings(nn.Module):
    """SEFI dual timestep embeddings: concat([emb_sem, emb_tex])."""

    def __init__(self, in_channels: int, embedding_dim: int, bias: bool = False):
        super().__init__()
        half_dim = embedding_dim // 2
        self.time_proj = Timesteps(
            num_channels=int(in_channels), flip_sin_to_cos=True, downscale_freq_shift=0,
        )
        self.semantic_embedder = TimestepEmbedding(   # 语义专用
            in_channels=int(in_channels), time_embed_dim=half_dim, sample_proj_bias=bias,
        )
        self.texture_embedder = TimestepEmbedding(    # 纹理专用
            in_channels=int(in_channels), time_embed_dim=half_dim, sample_proj_bias=bias,
        )

    def forward(self, timestep_sem: Tensor, timestep_tex: Tensor) -> Tensor:
        sem_proj = self.time_proj(timestep_sem)
        tex_proj = self.time_proj(timestep_tex)
        sem_emb = self.semantic_embedder(sem_proj.to(timestep_sem.dtype))
        tex_emb = self.texture_embedder(tex_proj.to(timestep_tex.dtype))
        return torch.cat([sem_emb, tex_emb], dim=-1)   # 两路嵌入拼起来注入 DiT

值得一提的是,FLUX.2 原本的 time_guidance_embed 被直接换成了 nn.Identity()flux2_sefi_transformer.py:L71)——SFD「自己显式管理语义/纹理两个时间步,不再复用 guidance 那一套」。

2.5 推理:三阶段异步去噪(论文用二值 mask,代码用 sigma 钳位)

推理时 SFD 用一个三阶段异步去噪表(对应 Fig. 2)。论文用两个二值 mask \(M_s, M_z\) 来形式化「哪一路在当前阶段更新」:

\[[M_s, M_z] = \begin{cases} [1, 0], & t_s \in [0, \Delta t),\ t_z = 0 \\ [1, 1], & t_s \in [\Delta t, 1],\ t_z \in [0, 1 - \Delta t] \\ [0, 1], & t_s = 1,\ t_z \in [1 - \Delta t, 1] \end{cases}\]

—— 翻译: Stage I 只更新语义(M_s=1, M_z=0);Stage II 两路都更新;Stage III 语义冻结、只更新纹理。被 mask 掉的那路速度乘 0,即 v̂ = [M_s⊙v̂_s, M_z⊙v̂_z]。

一个关键细节:SFD 不增加推理步数。它虽然把去噪区间延长了 Δt,但同时按比例拉大每步间隔,总步数不变;所以三阶段是「免费」的。最后只解码完全去噪的纹理 latent \(z_1\) 出图。

⚠️ 论文-代码差异(等价但更巧):仓库 runner.py 的实现里根本没有显式的二值 mask 张量。它用「sigma 钳位让步长 dt 自动变 0」达到同样效果——见 §4.1。这是本文交叉精读最值得记的一处。

3. 结论 (Key findings)

3.1 算力账:5B 只用 125K A800 GPU·时

最硬的卖点:5B 模型只用了 125K A800 GPU·时,约为 Z-Image(314K H800·时)训练算力的 10–20%[§Abstract/§1]。换来的是在一票基准上追平甚至反超 Qwen-Image / Z-Image。

3.2 多基准全面对标(数字说话)

基准 SeFi-5B SeFi-2B SeFi-1B Qwen-Image Z-Image FLUX.2-Klein-9B
GenEval Overall↑ 0.88 0.87 0.87 0.87 0.84 0.85
DPG-Bench Overall↑ 87.27 87.31 87.17 88.32 88.14
LongTextBench Avg↑ 0.978 0.847 0.855 0.945 0.936 0.541
CVTG-2K Word Acc↑ 0.895 0.773 0.718 0.829 0.867
OneIG-EN Overall↑ 0.5606 0.5367 0.5247 0.5390 0.5460 0.5324
OneIG-ZH Overall↑ 0.5379 0.4979 0.5095 0.5480 0.5350 0.4302

几个要点:

SeFi-Image 与基线的综合性能对比
Fig. 5 — 综合性能概览(来自仓库 assets/performance_overview.png):在显著更低的训练算力下,SeFi-Image 在多个评测轴上达到竞争性或 SOTA 水平。

3.3 为什么省:重建-生成权衡 + 收敛加速 + 更好的 scaling

论文 §4 用受控消融论证了 SFD「为什么能又快又好」:

(a) 更好的 VAE 重建。 因为纹理生成总是被「更干净的语义 latent」引导,纹理建模负担大幅降低,于是可以放心地把纹理 VAE 往重建质量方向激进微调,而不太担心伤害收敛。微调后的 FLUX.2 VAE 在 Kodak 上把 PSNR 从 33.18 → 36.40;在文字密集的 OmniDoc-TokenBench 上 PSNR/LPIPS/FID/NED 全面领先(无需任何文字专项训练)。

VAE (Kodak) PSNR↑ SSIM↑ LPIPS↓
FLUX.1 32.37 0.9063 0.0554
FLUX.2 33.18 0.9194 0.0442
FLUX.2-finetuned (ours) 36.40 0.9565 0.0235

(b) 收敛更快。 在 50M 样本、256² 的受控对比里,「微调 VAE + SFD」相比「微调 VAE 无 SFD」「原版 VAE 无 SFD」两个基线,在 GenEval/DPG 上从头到尾保持领先、收敛明显更快(Fig. 6)。

(c) scaling 更好。 在 0.5B / 2B / 4B 三档上,SFD 的优势全程保持;带 SFD 的 2B 甚至大幅超过不带 SFD 的 4B(Fig. 7)——语义先行显著提升了参数效率。

SFD 收敛加速对比
Fig. 6(论文 Fig. 9)— 收敛对比。蓝(finetuned VAE + SFD)全程领先绿(finetuned VAE 无 SFD)和橙(vanilla VAE 无 SFD),且收敛更快。
SFD 随模型规模的 scaling 对比
Fig. 7(论文 Fig. 10)— scaling 对比。在 0.5B/2B/4B 上「带 SFD(绿)」都优于「不带 SFD(灰)」,2B+SFD 反超 4B 无 SFD。

4. 实现细节 (Implementation notes)

以下都对到仓库 jmliu206/SeFi-Image 的具体代码行。这是「论文之外」最有信息量的部分。

4.1 三阶段 mask 在代码里其实是「sigma 钳位让 dt=0」

这是最值得记的一处实现细节。论文 Eq. (9)(10) 用二值 mask \(M_s,M_z\) 控制「哪一路更新」,但 runner.py 的去噪循环里没有任何 mask 张量。它的做法是:构造一条「加长版」的语义进度表 u_sem_raw(终点是 \(1+\Delta t\)),纹理进度就是 u_sem_raw - Δt 再钳到 \([0,1]\);语义进度本身钳到 \(\le 1\)

repo/sefi/runner.py:L680-L694 — 三阶段调度:用钳位从单条语义进度表派生双时间步

for step in range(num_inference_steps):
    u_sem_raw_cur  = torch.full((batch_size,), float(u_sem_raw_schedule[step].item()),     device=self.device)
    u_sem_raw_next = torch.full((batch_size,), float(u_sem_raw_schedule[step + 1].item()), device=self.device)

    # 纹理时间步 = 语义时间步 - Δt,再钳到 [0,1];语义时间步钳到 ≤1
    u_tex_cur  = torch.clamp(u_sem_raw_cur  - self.delta_t, min=0.0, max=1.0)
    u_sem_cur  = torch.clamp(u_sem_raw_cur,  max=1.0)
    u_tex_next = torch.clamp(u_sem_raw_next - self.delta_t, min=0.0, max=1.0)
    u_sem_next = torch.clamp(u_sem_raw_next, max=1.0)

精彩之处在 Euler 更新这一步——每路的步长是 dt = sigma_next - sigma_cur

repo/sefi/runner.py:L774-L787 — 两路各自用自己的 dt 做 Euler 更新;钳位让被「冻结」那路的 dt 自动为 0

    vel_sem = velocity[:, : self.semantic_channels]
    vel_tex = velocity[:, self.semantic_channels :]
    lat_sem = latents[:, : self.semantic_channels]
    lat_tex = latents[:, self.semantic_channels :]

    dt_sem = sigmas_sem_next - sigmas_sem_cur   # Stage I:语义还没到 Δt,纹理被钳在 0 → dt_tex=0
    dt_tex = sigmas_tex_next - sigmas_tex_cur   # Stage III:语义已钳在 1 → dt_sem=0

    lat_sem = lat_sem + dt_sem * vel_sem
    lat_tex = lat_tex + dt_tex * vel_tex
    latents = torch.cat([lat_sem, lat_tex], dim=1)

为什么这和二值 mask 等价:Stage I 里纹理时间步被 clamp(..., min=0) 钉死在 0,所以连续两步的 sigma_tex 相等、dt_tex = 0,纹理 latent 原地不动——等价于 \(M_z=0\)。Stage III 里语义时间步被 clamp(..., max=1) 钉死在 1,dt_sem=0,语义冻结——等价于 \(M_s=0\)论文写 mask 是为了讲清楚语义;代码用钳位是因为它更省、且天然兼容变步长。 这是「论文形式化 ≠ 代码实现,但数学等价」的典型案例。

4.2 复合 latent 的通道布局:[语义 | 纹理] 沿 channel 维拼接

latents 张量就是 torch.cat([lat_sem, lat_tex], dim=1),前 semantic_channels 个通道是语义、其余是纹理。出图时只取后半段纹理通道解码(runner.py:L789)。纹理通道数的推导也有个易错点——FLUX 系 VAE 的 latent 会做 2×2 packing,所以纹理通道是 VAE latent 通道的 4 倍

repo/sefi/modeling/texture_latent_codec.py:L28-L29 — 纹理通道 = VAE latent 通道 × 4(2×2 patch packing)

self.latent_channels = int(latent_channels)
self.texture_channels = int(self.latent_channels * 4)

4.3 网络入口:时间步在喂进 DiT 前先 ÷1000 再 ×1000

_predict_velocity 把两个时间步都除以 1000 传给 transformer,transformer 内部又乘回 1000(flux2_sefi_transformer.py:L170)——这是为了和 FLUX.2 原始 forward 的数值约定对齐,纯工程兼容性细节,不影响语义:

repo/sefi/runner.py:L588-L595 — 双时间步进入 DiT 前的归一化约定

pred = transformer(
    hidden_states=packed_latents,
    timestep_sem=timesteps_sem / 1000,   # 语义时间步
    timestep_tex=timesteps_tex / 1000,   # 纹理时间步
    encoder_hidden_states=encoder_hidden_states,
    txt_ids=txt_ids, img_ids=img_ids,
)

4.4 推理默认 Δt = 训练上界,且可被覆盖

_configure_delta_t 要求 config 里必须给 delta_t_min / delta_t_max;推理默认取 delta_t_max,但允许命令行 --delta-t 覆盖(越界会告警但不报错)。配合训练表里 256px 阶段 Δt=0.2、768px 之后 Δt=0.1,说明 Δt 是一个随分辨率/阶段调整的超参,不是定死的常数

4.5 架构与训练栈(散点)

5. 批判性总结 (Critical assessment)

优点 (Strengths)

局限 / 开放问题 (Limitations / open questions)

何时用 / 不用 (When to use / not use)

延伸阅读 (Further reading)

讨论 / Comments

评论托管在本仓库的 GitHub Discussions, 需 GitHub 账号。