跳到正文
Hugging Face:Blog·· 19 小时前精选AI 评分85

FLUX.3 Image Klein 上线 Diffusers,支持边界框布局控制

Welcome FLUX.3 Image Klein in Diffusers

AI 导读

Black Forest Labs 发布 FLUX.3 Image Klein,首发即支持在 Diffusers 中运行,单一模型可做文生图、多参考图生图和边界框布局控制。

推荐理由

原文给出 FLUX.3 Image Klein 的布局控制用法、显存与速度实测和架构拆解,读者可据此评估落地成本。

正文 · AI 翻译

Black Forest Labs 的 FLUX.3 Image Klein 来了,并且从第一天起就能在 Diffusers 中运行。一个模型同时完成文生图和多参考图生图,还带来了一个新东西:布局控制。你把边界框写进提示词,就能指定图像每个部分应该放在哪里,或者参考图中的哪些部分需要移动。

两个开放权重检查点都在 FLUX.3 Image Klein 合集中:

检查点 说明 默认值
black-forest-labs/flux-3-image-klein 时间步蒸馏版,用于快速推理 16 步,无 CFG
black-forest-labs/flux-3-image-klein-base 基础模型 50 步,CFG

a red fox in a snowy forest (text-to-image), the same fox re-rendered from that image (image-to-image), a black stippled running silhouette on a chartreuse background, drawn inside its labeled bounding box, and a knitted knight moved from a "from" box to a "to" box next to a knitted dragon (layout editing)

你现在就可以前往 Hugging Face space 生成一些图像!

TL;DR

  • 一个模型即可完成文生图、多参考图生图以及边界框布局控制。
  • 蒸馏版检查点在 H100 上生成一张 1024 × 1024 图像大约只需 2 秒。
  • NVFP4 和 FP8 权重通过 from_single_file 加载,适用于较小的 GPU。
  • 提供用于文生图和图像编辑的 LoRA 训练脚本。
  • 以 Modular Diffusers 流水线的形式发布,因此你可以取出其中的块,或添加自己的块。

目录

快速开始

安装最新的 diffusers 和 transformers(用于 Qwen3-VL 文本编码器):

pip install -U diffusers transformers

加载蒸馏版检查点并生成一张图像:

import torch
from diffusers import ModularPipeline

pipe = ModularPipeline.from_pretrained("black-forest-labs/flux-3-image-klein")
pipe.load_components(dtype=torch.bfloat16)
pipe.to("cuda")

pipe(prompt="a red fox", output="images")[0].save("fox.png")

FLUX.3 能做什么

文生图

FLUX.3 对包含主体、场景、光线、构图和风格的完整场景描述反应最好。Black Forest Labs 的官方提示词指南有更多技巧。蒸馏版检查点默认用 16 步生成一张 1024 × 1024 图像。传入 height、width 或 num_inference_steps 可以改变这些设置。

prompt = (
    "A wide-angle wildlife photograph of a red fox standing alert in a snow-covered forest clearing at golden hour. "
    "The fox is centered in the frame, its thick orange winter coat contrasting with the fresh white snow, breath "
    "visible in the cold air. Snow-laden pine trees fill the softly blurred background, and low warm sunlight rakes "
    "across the scene from the left, casting long blue shadows."
)

fox = pipe(
    prompt=prompt,
    generator=torch.Generator().manual_seed(0),
    output="images"
)[0]
fox.save("fox.png")
A red fox standing alert in a snow-covered forest clearing, lit by low golden sunlight from the left
文生图

图生图

通过 image 参数传入一张或多张参考图。把提示词写成你想要的图像说明,并包含所做的改动。

prompt = (
    "The same red fox in the same snow-covered forest clearing, now at night under a full moon: cool blue "
    "moonlight, deep shadows, stars visible through the pine branches, the fox's eyes catching the light."
)
night = pipe(
    prompt=prompt,
    image=fox, # from the previous text to image example
    generator=torch.Generator().manual_seed(0),
    output="images",
)[0]
night.save("night.png")
The fox from the previous image, relit in cold blue light
图生图

如果有多个参考图,在提示词中按传入 image 的顺序将它们称为 <ref_image_0>、<ref_image_1>……:

red_fox = pipe(prompt="a red fox", generator=torch.Generator().manual_seed(0), output="images")[0]
bench = pipe(prompt="a wooden bench", generator=torch.Generator().manual_seed(0), output="images")[0]

combined = pipe(
    prompt="The fox from <ref_image_0> sitting on the wooden bench from <ref_image_1> in a snowy setting.",
    image=[red_fox, bench],
    output="images",
)[0]

combined.save("combined.png")
A red fox sitting on dry grass A slatted wooden bench against a plain grey wall The same fox sitting on the same bench, with snow on the ground
<ref_image_0>:一只红狐 <ref_image_1>:一条木凳 凳子上的狐狸

布局控制

在说明文字后面添加一个 JSON 区域列表。每个区域有一个 id、一个 bbox 和一个 desc,说明文字中以 <id> 来提及它。

边界框写作 [y_min, x_min, y_max, x_max],y 在前,采用 0–1000 的刻度。由于刻度是归一化的,同一个边界框在任何图像尺寸下都适用。

在新图像中放置区域

layout_prompt = (
    "Minimalist graphic illustration featuring a black silhouette of a person <silhouette_1> centered against a "
    "solid, vibrant chartreuse background <background_1>. The figure is captured in a dynamic, mid-stride running "
    "pose, facing toward the left side of the frame. "
    '[{"id": "background_1", "bbox": [0, 0, 1000, 1000], "desc": "A flat field of neon yellow-green with a subtle '
    'paper texture and fine, organic grain."}, {"id": "silhouette_1", "bbox": [150, 150, 850, 850], "desc": "A '
    "black silhouette of a person in motion, made of dense stippled ink. The leading edges are solid; the trailing "
    'arm and leg dissolve into coarse square pixels and scattered dots."}]'
)

runner = pipe(prompt=layout_prompt, generator=torch.Generator().manual_seed(0), output="images")[0]
Diagram: the caption and the two region descriptions on the left, the two boxes on a 0–1000 grid in the middle, and the generated image on the right, where a black stippled runner fills the silhouette box on a chartreuse background
每个区域的 desc 绘制在其 bbox 内

在参考图内移动区域

对于编辑,每个区域会说明其内容来自哪里(参考图中的 src_bbox)以及要移动到哪里(输出中的 tgt_bbox)。两个边界框相同的区域会留在原地。

这个例子把针织骑士向左上方移动,并固定住背景幕、悬崖、针、龙和火焰:

from diffusers.utils import load_image

knight_image = load_image("knight_orig.png")
edit_prompt = (
    "In <ref_image_0>, move the miniature grey amigurumi knight figure <knight_1> upwards and to the left along the "
    "yarn cliff <cliff_1>. Keep the rest of the image completely unchanged, preserving the blurred yarn backdrop "
    "<backdrop_1>, the large knitted dragon <dragon_1>, and the fiery orange thread <fire_1>. "
    '[{"id": "knight_1", "from": "ref_image_0", "src_bbox": [500, 150, 850, 350], "tgt_bbox": [194, 55, 544, 255], '
    '"desc": "A miniature amigurumi knight crocheted from thick grey woolen yarn."}, '
    '{"id": "backdrop_1", "from": "ref_image_0", "src_bbox": [0, 0, 1000, 1000], "tgt_bbox": [0, 0, 1000, 1000], '
    '"desc": "A soft, out-of-focus backdrop of cream and indigo knitted yarn."}, '
    '{"id": "cliff_1", "from": "ref_image_0", "src_bbox": [300, 0, 1000, 450], "tgt_bbox": [300, 0, 1000, 450], '
    '"desc": "A steep cliff of stacked yarn skeins in mustard, terracotta, and indigo."}, '
    '{"id": "needle_1", "from": "ref_image_0", "src_bbox": [420, 250, 550, 450], "tgt_bbox": [420, 250, 550, 450], '
    '"desc": "An oversized polished steel tapestry needle."}, '
    '{"id": "dragon_1", "from": "ref_image_0", "src_bbox": [100, 600, 950, 1000], "tgt_bbox": [100, 600, 950, 1000], '
    '"desc": "A large, fluffy knitted dragon in terracotta and mustard yarn."}, '
    '{"id": "fire_1", "from": "ref_image_0", "src_bbox": [350, 400, 650, 650], "tgt_bbox": [350, 400, 650, 650], '
    '"desc": "Curled puffs of fiery orange angora thread suspended in mid-air."}]'
)

moved = pipe(prompt=edit_prompt, image=knight_image, generator=torch.Generator().manual_seed(0), output="images")[0]
Left: the reference with the knight's source box. Right: the output with an arrow from the source box to the target box; the pinned regions are outlined in white
骑士从它的 src_bbox 移动到它的 tgt_bbox,场景的其余部分保持不动

基础模型与量化变体

基础模型

以上全部内容使用的都是蒸馏版检查点。基础版检查点使用无分类器引导运行 50 步,它是适合微调的版本(参见 LoRA 微调)。

pipe = ModularPipeline.from_pretrained("black-forest-labs/flux-3-image-klein-base")
pipe.load_components(dtype={"vae": torch.float32, "default": torch.bfloat16})
pipe.to("cuda")

pipe.guider = pipe.guider.new(guidance_scale=5.0)  # optional
image = pipe(prompt=prompt, output="images")[0]

单文件格式检查点

Black Forest Labs 还以单个文件的形式提供该 transformer,包括 bf16 和两种量化格式:fp8r(逐行 FP8)和 nvfp4。使用 from_single_file 加载其中任意一个,然后按名称加载其他组件,这样就不会再下载 bf16 transformer。

from diffusers import BFLQuantizationConfig, Flux3Transformer2DModel, ModularPipeline

model_id = "black-forest-labs/flux-3-image-klein-base"
variant = "fp8r"  # or "nvfp4"

transformer = Flux3Transformer2DModel.from_single_file(
    f"https://huggingface.co/{model_id}/blob/main/variants/{variant}/model.safetensors",
    config=model_id,
    subfolder="transformer",
    quantization_config=BFLQuantizationConfig(compute_dtype=torch.bfloat16),
    dtype=torch.bfloat16,
)

pipe = ModularPipeline.from_pretrained(model_id)
pipe.load_components(dtype=torch.bfloat16)
pipe.update_components(transformer=transformer)
pipe.to("cuda")

对于未量化的单个文件,使用 https://huggingface.co/{model_id}/blob/main/model.safetensors 并去掉 quantization_config。

变体 启用快速内核 其他 GPU
fp8r 计算能力 8.9+(Ada、Hopper、Blackwell) 权重每步都会被反量化:内存更低,但没有加速
nvfp4 Blackwell,搭配 flashinfer-python 和 flashinfer-cubin 且版本相同 同上

LoRA 微调

FLUX.3 同时适用于文生图和图生图,这使其成为自定义 LoRA 的优秀基础。diffusers 为这两项任务提供了 DreamBooth LoRA 训练脚本(你也可以使用 Ostris 的 AI Toolkit 进行训练)。

以下是一个文生图示例,它从 multimodalart/1920-raider-waite-tarot-public-domain 数据集中学习一种塔罗牌风格,并以 trtcrd 触发:

accelerate launch train_dreambooth_lora_flux3_klein.py \
  --pretrained_model_name_or_path="black-forest-labs/flux-3-image-klein-base" \
  --dataset_name="multimodalart/1920-raider-waite-tarot-public-domain" \
  --caption_column="caption" \
  --instance_prompt="trtcrd" \
  --output_dir="trained-flux3-tarot-lora" \
  --mixed_precision="bf16" \
  --resolution=1024 \
  --use_aspect_ratio_buckets \
  --train_batch_size=1 \
  --rank=16 \
  --lora_alpha=16 \
  --learning_rate=1e-4 \
  --lr_scheduler="constant" \
  --max_train_steps=1500 \
  --validation_prompt="a trtcrd of a cat, tarot style" \
  --validation_epochs=25 \
  --seed=0 \
  --push_to_hub

为减少内存占用,可以添加几个优化标志:

  --gradient_checkpointing \
  --cache_latents \
  --use_8bit_adam

--gradient_checkpointing 在反向传播中重新计算激活值(显存更低,速度略慢),--cache_latents 预先编码图像并释放 VAE,--use_8bit_adam 将优化器状态保持为 8 位。

有几个参数值得尝试。下方的网格使用不同的 LoRA 秩和优化器训练同一种塔罗风格;这些适配器发布在 Hub 上:diffusers-modular/flux3-tarot-lora。更多训练功能和配置记录在 DreamBooth FLUX.3 README 中。

FLUX.3 tarot-style LoRA across ranks and optimizers: the base model with no LoRA, then rank 16, rank 8 and rank 32 (AdamW, lr 1e-4) and a Prodigy run, on two held-out prompts

对于基于指令的编辑,图生图脚本(train_dreambooth_lora_flux3_klein_img2img.py)在(参考图像、目标图像、指令)三元组上训练,其中参考图像在提示词中以 <ref_image_0> 表示。

有关所有选项和完整的图生图示例,请参阅 DreamBooth FLUX.3 README。

运行成本

在本节中,我们将讨论模型的内存消耗和生成速度。

内存

名称 峰值分配(加载后)GiB 峰值分配(推理后)GiB
nvfp4 (RTX6000) 13.4 15.3
fp8r (H100) 14.35 16.27
bf16 (H100) 18.80 20.72

如果你的内存较少:

  • 加载量化 transformer。
  • 单独运行文本编码器并在之后释放它。它每张图像只需在去噪前运行一次(请参阅编码一次,生成多次)。
  • 开启自动 CPU 卸载,这会在每个组件运行时才将其移到 GPU 上(我们未对此进行基准测试)
from diffusers import ComponentsManager, ModularPipeline

manager = ComponentsManager()
manager.enable_auto_cpu_offload(device="cuda")

pipe = ModularPipeline.from_pretrained("black-forest-labs/flux-3-image-klein", components_manager=manager)
pipe.load_components(dtype={"vae": torch.float32, "default": torch.bfloat16})
# no pipe.to("cuda"): the manager handles device placement

速度

蒸馏检查点(16 步)的端到端耗时,在预热后取三次运行的平均值:

工作流 输出尺寸 时间 峰值内存
文生图 1024 × 1024 1.93 s 23.8 GiB
图生图,一张 1024 × 1024 参考图像 1024 × 1024 3.87 s 23.8 GiB
布局编辑(骑士),一张 1360 × 768 参考图像 1360 × 768 4.10 s 23.8 GiB

单张 NVIDIA H100 80GB,无卸载,默认注意力后端。

参考图像大致会使时间翻倍。参考 token 与被生成的文本和图像一起进入同一序列,因此一张 1024 × 1024 的参考图像会为每次 transformer 调用增加 4,096 个 token

Stacked bars of tokens per transformer call: text-to-image 4,256 tokens; image-to-image 8,272; layout text-to-image 4,336; layout image-to-image 8,800
每次 transformer 调用的 token 堆叠条形图:文生图 4,256 个 token;图生图 8,272;布局文生图 4,336;布局图生图 8,800

架构

让我们逐块拆解这个模型。该模型接收三种 token:

  1. 文本
  2. 正在生成的噪声图像
  3. 以及可选的参考图像

从像素到 token

FLUX.3 是一个用于图像生成的潜空间流模型。这意味着去噪器以潜变量作为输入,并输出潜变量。我们使用 VAE 将潜变量解码回像素。FLUX.3 使用与 Flux.2 相同的 VAE 架构。

FLUX.2 VAE 在每个方向上对图像进行 8× 压缩,得到 32 个通道,然后将该潜变量的每个 2 × 2 图块打包为一个具有 128 个通道的 token。

因此 一个 token 覆盖 16 × 16 像素的图块,一张 1024 × 1024 的图像变为 64 × 64 的网格,即 4,096 个 token。带噪图像从恰好是该形状的随机噪声开始。参考图像经过相同的 VAE 编码器和打包处理,并保持其自身的大小。

从提示到 token

文本编码器是 Qwen3-VL,一个视觉语言模型。FLUX.3 并不只是取它的最后一层。它读取其 36 层中 八 层(第 4、8、…、32 层)的隐藏状态并将它们堆叠起来,因此每个文本 token 在通过一个线性层投影到 transformer 的宽度 3072 之前,会变成一个 8 × 2560 = 20,480 维的向量。

这为 transformer 提供了来自语言模型多个深度的特征,而不仅仅是其最后一层。提示被填充到 80 个 token 的倍数,transformer 也会关注填充部分,这就是为什么该流水线每次调用只接受一个提示。

两个阶段

transformer 分两个阶段处理 token:

  1. 阶段 1:按模式分块。 每个流经过 5 个具有各自权重的块。文本有自己的块,带噪图像有自己的块,参考图像也有自己的块。
  2. 阶段 2:联合块。 各个流被拼接成一个序列 [text | image | reference],该序列经过 28 个共享一组权重的块。

这两个阶段的区别在于 谁能关注谁。

在阶段 1 中,每个流只关注自身,因此每种 token 首先按照自身的方式被精炼。在阶段 2 中,一切都关注一切,图像从文本和参考中学习。作为对比,FLUX.2 的双流块也为文本和图像赋予单独的权重,但从一开始就让它们互相关注。FLUX.3 先将各流完全分开,然后再合并。

最后,文本和参考 token 被直接丢弃。只有图像 token 经过输出层,输出层预测如何将它们向干净图像推进一步。

一个块的内部

无论在哪个阶段,每个块都具有相同的形态。注意力和 MLP 并行 运行在同一个归一化输入上,它们的和被加回到残差流中。

每个块以一个 LayerNorm 开始,该 LayerNorm 没有自己的学习缩放或偏移。缩放、偏移和门控来自时间步嵌入(这被称为 调制)。在注意力内部,查询和键经过 RMS 归一化并获得旋转位置嵌入(RoPE),我们将在下面讨论。

在联合块中,所有 token 共享相同的权重,但文本、图像和参考 token 各自获得自己的缩放、偏移和门控。

def block(x, shift, scale, gate):      # shift, scale, gate come from the token's timestep embedding
    h = layer_norm(x)                  # no learned scale or shift
    h = (1 + scale) * h + shift
    q, k, v = to_q(h), to_k(h), to_v(h)
    q, k = rope(rms_norm(q)), rope(rms_norm(k))
    out = attention(q, k, v) + mlp(h)  # parallel, MLP is SwiGLU
    return x + gate * out

每个 token 都有自己的时间步

在 FLUX.1 和 FLUX.2 中,一个时间步嵌入调制每个 token。在 FLUX.3 中,调制取决于 token 的种类。

含噪图像 token 由当前时间步 t 调制。文本和参考 token 始终由时间步 0 调制,这意味着“干净,这里没有任何东西需要去噪”。因此,即使参考看起来与输出几乎相同,它也被标记为给定上下文,而不是需要生成的内容。

蒸馏后的检查点是一个流映射。它还会被告知每一步跳转到的那个时间步。第二个时间步拥有自己的嵌入,该嵌入被加到每个 token 的调制中,因此它的 16 步中的每一步都可以做出更大的跳转。

四个维度中的位置

注意力本身并不知道一个 token 在哪里(这一现象被称为置换等变性)。RoPE 通过根据 token 的位置对每个 query 和 key 进行旋转来修正这一点。FLUX.3 使用四个位置轴,(t, h, w, l)(时间步、高度、宽度、序列长度),并为每个轴分配每个 128 维注意力头的四分之一(32 维):

Tokens t:时间步 h:高度 w:宽度 l:序列长度
文本 token i 0 0 0 i
位于(行,列)的图像 token 0 行 / 2 列 / 2 0
位于(行,列)的参考 k 0 行 / 2 列 / 2 −(k + 1)

文本 token 沿着 l 排列,就像句子中的单词。图像和参考是 (h, w) 中的 2D 网格,并沿着 l 彼此分开。由于参考使用与输出相同的 (h, w) 坐标,“参考中的同一位置”对于模型来说具有精确的含义,这正是 src_bbox / tgt_bbox 编辑所依赖的。

去噪循环

FLUX.3 是一个流匹配模型。生成从纯噪声开始,在 50 步中的每一步,transformer 都会预测一个速度。这是从噪声指向干净图像的方向。调度器沿着它前进一步。

在每一步,你还可以读取模型对最终图像的当前猜测。在几个步骤中解码该猜测可以展示图像如何成形:

step01 step05 step10 step15 step25 step50
第 1 步 第 5 步 第 10 步 第 15 步 第 25 步 第 50 步

狐狸、它的姿势和光线在第 10 步时已经就位,而噪声水平仍为 0.97。覆雪的树枝和毛发的纹理只在最后几步才出现。FLUX.3 的调度器使用较大的时间偏移(6.93),这使噪声水平在大部分运行过程中保持较高:50 步中有 44 步发生在 σ = 0.5 以上。我们通过在 pipeline 中添加一个小模块构建了这张图,这就把我们带到了 Modular Diffusers。

如果你对 flow matching 还不熟悉,我们为你准备了一个初学者友好教程和一个视频,帮助你理解它的工作原理。

Modular Diffusers

FLUX.3 以 Modular Diffusers pipeline 的形式发布。

查看工作流

你可以列出工作流并打印每个工作流的确切模块,而无需加载任何权重。

from diffusers import Flux3AutoBlocks

blocks = Flux3AutoBlocks()
print(blocks.available_workflows)
# ['text2image', 'image_conditioned']

print(list(blocks.get_workflow("text2image").sub_blocks))
# ['text_encoder', 'denoise.input', 'denoise.prepare_latents', 'denoise.set_timesteps',
#  'denoise.prepare_rope_inputs', 'denoise.denoise', 'denoise.after_denoise', 'decode']

get_workflow() 会解析自动分支,并给出将要运行的普通步骤序列。print(blocks.get_workflow("image_conditioned").doc) 会打印该工作流的组件、输入、默认值和输出。

编码一次,生成多次

弹出文本编码器,对提示编码一次,释放它,然后从缓存的嵌入进行生成。

import torch
from diffusers import ModularPipeline

model_id = "black-forest-labs/flux-3-image-klein-base"

blocks = ModularPipeline.from_pretrained(model_id).blocks
text_blocks = blocks.sub_blocks.pop("text_encoder")  # `blocks` now starts at the VAE encoder

text_pipe = text_blocks.init_pipeline(model_id)
text_pipe.load_components(dtype=torch.bfloat16)
text_pipe.to("cuda")

state = text_pipe(prompt=prompt, negative_prompt="")  # no guider here, so ask for the negative embeddings

prompt_embeds = state.get("prompt_embeds")
negative_prompt_embeds = state.get("negative_prompt_embeds")

del text_pipe
torch.cuda.empty_cache()

image_pipe = blocks.init_pipeline(model_id)  # no text encoder to load
image_pipe.load_components(dtype={"vae": torch.float32, "default": torch.bfloat16})
image_pipe.to("cuda")

for seed in range(3):
    image = image_pipe(
        prompt_embeds=prompt_embeds,
        negative_prompt_embeds=negative_prompt_embeds,
        generator=torch.Generator().manual_seed(seed),
        output="images",
    )[0]
    image.save(f"{seed}.png")

在循环内添加一个步骤

上面的去噪图来自一个额外模块,它记录模型在每一步对干净图像的猜测:

from diffusers.modular_pipelines import Flux3AutoBlocks, InputParam, ModularPipelineBlocks, OutputParam


class Flux3PreviewStep(ModularPipelineBlocks):
    model_name = "flux3"

    @property
    def inputs(self):
        return [InputParam("latents"), InputParam("noise_pred")]

    @property
    def intermediate_outputs(self):
        return [OutputParam("x0_previews", type_hint=list)]

    @torch.no_grad()
    def __call__(self, components, block_state, i, t):
        if i == 0:
            block_state.x0_previews = []
        sigma = t / 1000  # timesteps are sigmas scaled to [0, 1000]
        block_state.x0_previews.append(block_state.latents - sigma * block_state.noise_pred)  # x0 = x_t - sigma * v
        return components, block_state


blocks = Flux3AutoBlocks()
denoise_loop = blocks.sub_blocks["denoise"].sub_blocks["text2image"].sub_blocks["denoise"]
denoise_loop.sub_blocks.insert("preview", Flux3PreviewStep(), 1)  # between "denoiser" and "after_denoiser"

pipe = blocks.init_pipeline(model_id)
pipe.load_components(dtype={"vae": torch.float32, "default": torch.bfloat16})
pipe.to("cuda")

out = pipe(prompt=prompt, generator=torch.Generator().manual_seed(0), output=["x0_previews", "images"])

试一试

用 FLUX.3 构建了什么?在 Hub 上分享你的 LoRA 和流水线。我们很想看看!😎

来源:Hugging Face:Blog · huggingface.co