mirror of
https://github.com/huggingface/diffusers.git
synced 2026-10-23 09:09:01 +03:00
* Add Bria fibo-2: BriaFibo2Pipeline and BriaFibo2Transformer2DModel
fibo-2 is Bria's 8.1B flow-matching DiT, built on the Z-Image block and
conditioned on Qwen3-VL hidden states two ways: a Perceiver resampler turns
them into gist tokens that share the stream with the image tokens, and five
blocks add gated cross-attention to their own bundle of Qwen3-VL layers. It
uses the Flux 2 VAE.
One pipeline class covers both of the model's jobs and both checkpoints:
text-to-image generation, instruction editing of up to five images with an
optional mask, the turbo checkpoint (4 steps, no guidance) and the guided one.
- BriaFibo2Transformer2DModel, with images to edit passed as context latents
- BriaFibo2Pipeline, including:
- captions cleaned and serialized the way training did, for generation and
edits, in every caption form fibo-2 was trained with: a full caption, a
short description, or a short description with its text_render list
- images of the caption's text, which Qwen3-VL sees beside the prompt:
drawn the way training drew them, by default (`render_text_images`), or
passed as `text_images`
- masked edits, the region greyed out through a binary mask as in training
- a conversion script from the original checkpoint layout
- model and pipeline tests, and docs
Verified against Bria's reference implementation on an A100 and an H100.
* Apply RoPE with explicit casting instead of an autocast block
The rotation already runs in float32 and casts back to the input's dtype, and
none of its ops are autocast ops, so the autocast block changed nothing.
* Mark the full-graph compile tests as expected failures instead of skipping them
Both fail with torch._dynamo.exc.Unsupported, on the torch.device context in
which RopeEmbedder, copied from Z-Image, builds its frequency table. Strict, so
they show up once Dynamo can trace it.
* Test the pipeline through its public call only
Removes the tests of private prompt and text-image helpers. The caption
cleaning test now checks that a caption renders like its trained form, and
the text images test draws its strips with the public draw_text_image.
* Annotate the transformer's tuple return as tuple[torch.Tensor]
* Make _get_qwen_prompt_embeds a staticmethod that takes the text encoder and processor
The chat template, the sequence length and the Qwen3-VL layers become
arguments, defaulting to module constants; encode_prompt passes the
pipeline's own. The edit and text prompt helpers become classmethods.
Embeddings are unchanged.
* Point to the release checkpoints, briaai/Fibo-2.0-dev and briaai/Fibo-2.0-turbo
The docs and the example load Fibo-2.0-dev and show the turbo checkpoint's
4-step edits without guidance. Text-image fonts download from Fibo-2.0-dev,
and the conversion script's usage converts a local original checkpoint.
* Parse prompts in one method, take fonts as an argument, and size and mask images in BriaFibo2ImageProcessor
- `_parse_prompt` replaces `_clean_caption`, `_normalize_caption`, `_split_edit_prompt`, `_get_generation_prompt` and
`_get_caption`. `_text_render_entries`, `_get_text_prompt` and `_draw_text_images` are inlined.
- The pipeline no longer fetches fonts: `__call__` takes `fonts`, and the docs show how to get the ones training
used from briaai/Fibo-2.0-dev. A text that needs them warns without them.
- `BriaFibo2ImageProcessor` holds the trained sizes, the cover-and-crop and the masking. The masking stays before the
crop, at the image's own size, where training greyed the images.
The prompts and images Qwen3-VL sees, the VAE's inputs and the outputs are byte-identical to before.
* Prepare the images to edit and the mask in BriaFibo2ImageProcessor.preprocess, as Flux3's image processor does
* Point to the turbo checkpoint's new name, briaai/Fibo-2.0-edit-turbo
* Show a working example for each use in the docs, and call the model Fibo 2.0, as its model cards do
---------
Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
Co-authored-by: Linoy Tsaban <57615435+linoytsaban@users.noreply.github.com>
171 lines
7.0 KiB
Python
171 lines
7.0 KiB
Python
import argparse
|
|
import json
|
|
import os
|
|
|
|
import safetensors.torch
|
|
import torch
|
|
from accelerate import init_empty_weights
|
|
from huggingface_hub import snapshot_download
|
|
from transformers import AutoImageProcessor, Qwen3VLForConditionalGeneration, Qwen3VLProcessor
|
|
|
|
from diffusers import (
|
|
AutoencoderKLFlux2,
|
|
BriaFibo2Pipeline,
|
|
BriaFibo2Transformer2DModel,
|
|
FlowMatchEulerDiscreteScheduler,
|
|
)
|
|
from diffusers.configuration_utils import FrozenDict
|
|
|
|
|
|
"""
|
|
Converts an original Fibo 2.0 checkpoint, with `transformer/`, `vae/`, `text_encoder/` and `scheduler/` folders, to
|
|
diffusers. The converted checkpoints are released as briaai/Fibo-2.0-dev and briaai/Fibo-2.0-edit-turbo.
|
|
|
|
# Transformer only
|
|
|
|
python scripts/convert_bria_fibo2_to_diffusers.py \
|
|
--checkpoint_path /path/to/original/checkpoint \
|
|
--output_path Fibo-2.0-dev
|
|
|
|
# Full pipeline
|
|
|
|
python scripts/convert_bria_fibo2_to_diffusers.py \
|
|
--checkpoint_path /path/to/original/checkpoint \
|
|
--output_path Fibo-2.0-dev \
|
|
--full_pipe
|
|
"""
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--original_repo_id", default=None, type=str)
|
|
parser.add_argument("--checkpoint_path", default=None, type=str)
|
|
parser.add_argument("--output_path", type=str, required=True)
|
|
parser.add_argument("--full_pipe", action="store_true")
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
TRANSFORMER_KEYS_RENAME_DICT = {
|
|
# one embedder and one final layer instead of Z-Image's one per patch size
|
|
"all_x_embedder.2-1.": "x_embedder.",
|
|
"all_final_layer.2-1.": "final_layer.",
|
|
# injection blocks and Perceiver layers: cross-attention to the text encoder
|
|
"cross_q.": "cross_attention.to_q.",
|
|
"cross_k.": "cross_attention.to_k.",
|
|
"cross_v.": "cross_attention.to_v.",
|
|
"cross_out.": "cross_attention.to_out.0.",
|
|
"cross_norm_q.": "cross_attention.norm_q.",
|
|
"cross_norm_k.": "cross_attention.norm_k.",
|
|
# Perceiver layers: self-attention among the gist tokens
|
|
"self_q.": "self_attention.to_q.",
|
|
"self_k.": "self_attention.to_k.",
|
|
"self_v.": "self_attention.to_v.",
|
|
"self_out.": "self_attention.to_out.0.",
|
|
"self_norm_q.": "self_attention.norm_q.",
|
|
"self_norm_k.": "self_attention.norm_k.",
|
|
}
|
|
|
|
# BriaFibo2Pipeline.text_encoder_out_layers: the Qwen3-VL layers behind the Perceiver, then each injection block
|
|
PIPELINE_TEXT_ENCODER_LAYERS = [[9, 20, 31], [5, 16, 27], [7, 18, 29], [9, 20, 31], [11, 22, 33], [13, 24, 35]]
|
|
|
|
|
|
def get_checkpoint_path(args):
|
|
if args.original_repo_id is not None:
|
|
allow_patterns = None if args.full_pipe else ["transformer/*"]
|
|
return snapshot_download(args.original_repo_id, allow_patterns=allow_patterns)
|
|
elif args.checkpoint_path is not None:
|
|
return args.checkpoint_path
|
|
else:
|
|
raise ValueError("please provide either `original_repo_id` or a local `checkpoint_path`")
|
|
|
|
|
|
def load_original_checkpoint(checkpoint_path):
|
|
transformer_path = os.path.join(checkpoint_path, "transformer")
|
|
with open(os.path.join(transformer_path, "config.json")) as f:
|
|
original_config = json.load(f)
|
|
with open(os.path.join(transformer_path, "diffusion_pytorch_model.safetensors.index.json")) as f:
|
|
shard_names = sorted(set(json.load(f)["weight_map"].values()))
|
|
|
|
original_state_dict = {}
|
|
for shard_name in shard_names:
|
|
original_state_dict.update(safetensors.torch.load_file(os.path.join(transformer_path, shard_name)))
|
|
return original_config, original_state_dict
|
|
|
|
|
|
def get_transformer_config(original_config):
|
|
if original_config["perceiver_num_heads"] not in (None, original_config["n_heads"]):
|
|
raise ValueError("The Perceiver must use as many attention heads as the main blocks.")
|
|
# BriaFibo2Pipeline feeds the transformer fixed Qwen3-VL layers: the Perceiver's, then three per injection block
|
|
text_encoder_layers = [original_config["perceiver_llm_layers"], *original_config["injection_llm_layers"]]
|
|
if text_encoder_layers != PIPELINE_TEXT_ENCODER_LAYERS:
|
|
raise ValueError(
|
|
f"The checkpoint reads Qwen3-VL layers {text_encoder_layers}, but BriaFibo2Pipeline feeds it"
|
|
f" {PIPELINE_TEXT_ENCODER_LAYERS}."
|
|
)
|
|
|
|
return {
|
|
"in_channels": original_config["in_channels"],
|
|
"patch_size": original_config["patch_size"],
|
|
"dim": original_config["dim"],
|
|
"n_layers": original_config["n_layers"],
|
|
"n_refiner_layers": original_config["n_refiner_layers"],
|
|
"n_heads": original_config["n_heads"],
|
|
"cap_feat_dim": original_config["cond_llm_dim"],
|
|
"injection_layer_ids": original_config["injection_layer_ids"],
|
|
"perceiver_num_layers": original_config["perceiver_num_layers"],
|
|
"min_num_gist_tokens": original_config["min_num_gist_tokens"],
|
|
"max_num_gist_tokens": original_config["max_num_gist_tokens"],
|
|
"gist_step": original_config["gist_step"],
|
|
"gist_min_text_len": original_config["gist_min_text_len"],
|
|
"gist_max_text_len": original_config["gist_max_text_len"],
|
|
}
|
|
|
|
|
|
def convert_bria_fibo2_transformer_to_diffusers(original_config, original_state_dict):
|
|
with init_empty_weights():
|
|
transformer = BriaFibo2Transformer2DModel.from_config(get_transformer_config(original_config))
|
|
|
|
converted_state_dict = {}
|
|
for key, value in original_state_dict.items():
|
|
new_key = key
|
|
for old, new in TRANSFORMER_KEYS_RENAME_DICT.items():
|
|
new_key = new_key.replace(old, new)
|
|
converted_state_dict[new_key] = value
|
|
|
|
transformer.load_state_dict(converted_state_dict, strict=True, assign=True)
|
|
return transformer
|
|
|
|
|
|
def main(args):
|
|
checkpoint_path = get_checkpoint_path(args)
|
|
original_config, original_state_dict = load_original_checkpoint(checkpoint_path)
|
|
transformer = convert_bria_fibo2_transformer_to_diffusers(original_config, original_state_dict)
|
|
|
|
if not args.full_pipe:
|
|
transformer.save_pretrained(os.path.join(args.output_path, "transformer"))
|
|
return
|
|
|
|
vae = AutoencoderKLFlux2.from_pretrained(checkpoint_path, subfolder="vae")
|
|
# the original config carries `force_upcast`, which the Flux 2 VAE doesn't take; left in, every load warns about it
|
|
vae_config = dict(vae.config)
|
|
vae_config.pop("force_upcast", None)
|
|
vae._internal_dict = FrozenDict(vae_config)
|
|
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(
|
|
checkpoint_path, subfolder="text_encoder", dtype=torch.bfloat16
|
|
)
|
|
# Fibo 2.0's limits on an image's size in pixels, not Qwen3-VL's defaults; they decide how many tokens an image to
|
|
# edit becomes
|
|
processor = Qwen3VLProcessor.from_pretrained(checkpoint_path, subfolder="text_encoder")
|
|
processor.image_processor = AutoImageProcessor.from_pretrained(
|
|
checkpoint_path, subfolder="text_encoder", size={"shortest_edge": 4 * 28 * 28, "longest_edge": 2048 * 28 * 28}
|
|
)
|
|
scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(checkpoint_path, subfolder="scheduler")
|
|
|
|
pipe = BriaFibo2Pipeline(
|
|
transformer=transformer, scheduler=scheduler, vae=vae, text_encoder=text_encoder, processor=processor
|
|
)
|
|
pipe.save_pretrained(args.output_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(args)
|