1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-23 09:09:01 +03:00
Files
diffusers/scripts/convert_bria_fibo2_to_diffusers.py
muhammadali-afridi de988b7ee7 Add Bria fibo-2 (#14991)
* 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>
2026-10-20 21:07:11 +05:30

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)