Repository navigation
Add Bria fibo-2 - #14991
Add Bria fibo-2#14991
Conversation
e3fdebc to
2b7b792
Compare
553af68 to
5c38cb1
Compare
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.
5c38cb1 to
4f7c850
Compare
sayakpaul
left a comment
There was a problem hiding this comment.
Looks pretty good actually! Can we see some examples, please?
Also, we can remove tests where we're calling private methods.
| def apply_rotary_emb(x_in: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor: | ||
| with torch.amp.autocast("cuda", enabled=False): | ||
| x = torch.view_as_complex(x_in.float().reshape(*x_in.shape[:-1], -1, 2)) | ||
| freqs_cis = freqs_cis.unsqueeze(2) | ||
| x_out = torch.view_as_real(x * freqs_cis).flatten(3) | ||
| return x_out.type_as(x_in) |
There was a problem hiding this comment.
We usually avoid autocast blocks within the code. Possible to explicitly handle the casting?
There was a problem hiding this comment.
Sure. The same block is in TimestepEmbedder, which is # copied from Z-Image’s. will change it there too if you’d like.
| @pytest.mark.skip( | ||
| "RopeEmbedder, copied from Z-Image, builds its frequency table under a `torch.device` context on first use, " | ||
| "which Dynamo cannot trace in a full graph. Z-Image skips this test too." | ||
| ) | ||
| def test_torch_compile_recompilation_and_graph_break(self): | ||
| pass |
| @pytest.mark.skip( | ||
| "RopeEmbedder, copied from Z-Image, cannot be traced in a full graph (see above). Z-Image skips this test too." | ||
| ) | ||
| def test_compile_on_different_shapes(self): | ||
| pass |
| def test_edit_prompt(self): | ||
| # The prompt shape fibo-2 was trained to edit with: a vision marker per image, the cleaned caption, then the | ||
| # instruction | ||
| pipe = self.get_pipeline() | ||
| marker = "<|vision_start|><|image_pad|><|vision_end|>" | ||
| caption = { | ||
| "short_description": "a blue bicycle", | ||
| "objects": [], | ||
| "aesthetics": {"mood": "calm", "aesthetic_score": "high"}, | ||
| "edit_instruction": "Paint <image_1> like <image_2>", | ||
| } | ||
|
|
||
| assert pipe._get_edit_prompt("Make it blue", 1) == f'{{"image":"{marker}","edit_instruction":"Make it blue"}}' | ||
| assert pipe._get_edit_prompt(json.dumps(caption), 2) == ( | ||
| f'{{"image_1":"{marker}","image_2":"{marker}","structured_caption":{{"short_description":"a blue bicycle",' | ||
| f'"aesthetics":{{"mood":"calm"}}}},"edit_instruction":"Paint <image_1> like <image_2>"}}' | ||
| ) |
There was a problem hiding this comment.
We can remove tests for methods are private.
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.
…pping 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.
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.
|
@sayakpaul, regarding the example, let me confirm with my team about the best way to share weights with you since we don't plan to make it public before the official release |
|
Cool, we can merge once the date is up. |
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
| encoder_attention_mask: torch.Tensor | None = None, | ||
| context_latents: list[torch.Tensor] | None = None, | ||
| return_dict: bool = True, | ||
| ) -> torch.Tensor | Transformer2DModelOutput: |
There was a problem hiding this comment.
Looks like this should be a tuple[torch.Tensor]? I know that this persists across different models under src/diffusers/models (which we can fix in a different PR).
There was a problem hiding this comment.
you are right. fixed
…er 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.
…-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.
yiyixuxu
left a comment
There was a problem hiding this comment.
thanks for the PR!
sorry i'm bit late on review. left some comments
| ] | ||
|
|
||
| @classmethod | ||
| def _mark_text_images(cls, caption: dict, marker: str) -> dict: |
There was a problem hiding this comment.
Kept it since it is used in 3 places: the edit prompt, the generation prompt, and a generation’s first steps (with empty markers). Happy to inline it if you still prefer.
| def _get_text_image_fonts(self) -> list: | ||
| # The fallback fonts, read on first need from the `fonts` folder of the folder the pipeline was loaded from, or | ||
| # downloaded from fibo-2's repo when it has none | ||
| if self._text_image_fonts is None: | ||
| folder = os.path.join(self.name_or_path, "fonts") if self.name_or_path else None | ||
| paths = [ | ||
| os.path.join(folder, name) | ||
| if folder is not None and os.path.isdir(folder) | ||
| else hf_hub_download(FONTS_REPO_ID, name, subfolder="fonts") | ||
| for name in FALLBACK_FONT_FILES | ||
| ] | ||
| self._text_image_fonts = [load_font(path) for path in paths] | ||
| return self._text_image_fonts | ||
|
|
There was a problem hiding this comment.
| def _get_text_image_fonts(self) -> list: | |
| # The fallback fonts, read on first need from the `fonts` folder of the folder the pipeline was loaded from, or | |
| # downloaded from fibo-2's repo when it has none | |
| if self._text_image_fonts is None: | |
| folder = os.path.join(self.name_or_path, "fonts") if self.name_or_path else None | |
| paths = [ | |
| os.path.join(folder, name) | |
| if folder is not None and os.path.isdir(folder) | |
| else hf_hub_download(FONTS_REPO_ID, name, subfolder="fonts") | |
| for name in FALLBACK_FONT_FILES | |
| ] | |
| self._text_image_fonts = [load_font(path) for path in paths] | |
| return self._text_image_fonts |
can we remove this? pipeline should not resolve or download fonts at call time. we can add a fonts argument to the pipeline so that draw_tet_image can take it directly; you can also move this code to the doc/model card to show user how to get the font outside of the pipeline
There was a problem hiding this comment.
sure. __call__ now takes fonts and passes them to draw_text_image, and the docs show how to get the ones training used from the model repo
| latents = latents.reshape(batch_size, num_channels_latents // (2 * 2), height * 2, width * 2) | ||
| return latents | ||
|
|
||
| def _get_image_size(self, aspect_ratio: float) -> tuple[int, int]: |
There was a problem hiding this comment.
can you move image sizing into an image processr? if you need a custom one, you can take a look at flux3 https://github.com/huggingface/diffusers/blob/main/src/diffusers/modular_pipelines/flux3/image_processor.py
There was a problem hiding this comment.
done. moved into BriaFibo2ImageProcessor
| if image is not None: | ||
| if mask is not None: | ||
| # As in training: flat (128, 128, 128) grey, through a binary mask | ||
| grey = PIL.Image.new("RGB", image[0].size, (128, 128, 128)) |
There was a problem hiding this comment.
is it possible for both image and mask to go through an image processor like the other inpainting pipelines? https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/flux/pipeline_flux_fill.py#L922
There was a problem hiding this comment.
sure. I made both an image and a mask to go through an image processor, but we have one difference from Flux Fill. It greys out the region at the image’s own size before resizing, because that’s the order training used, so the region’s edges look the way Fibo 2 saw them
…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.
…eprocess, as Flux3's image processor does
…ibo 2.0, as its model cards do
|
/diffusers-bot pytest tests/pipelines/bria_fibo2/test_pipeline_bria_fibo2.py tests/models/transformers/test_models_transformer_bria_fibo2.py |
|
✅ |
What does this PR do?
Adds fibo-2, Bria's text-to-image and image-editing model.
BriaFibo2Transformer2DModel: an 8.1B flow-matching transformer with a single token stream, in the style of Z-Image. Its text encoder is Qwen3-VL-4B.context_latents. Each is patchified by the shared embedder and gets its own RoPE plane.BriaFibo2Pipeline:"structured_caption": "..."in an edit), or a short description with itstext_renderlist;render_text_images=Falseturns them off), or the caller passes them astext_images.briaai/Fibo-2.0-dev, the full model, runs 30 steps with guidance 5, the pipeline's defaults.briaai/Fibo-2.0-edit-turbois distilled for editing in 4 steps, withguidance_scale=1.0turning guidance off.BriaFibo2ImageProcessor: prepares the images to edit and the mask the way training did, and gives the size fibo-2 was trained at for an aspect ratio.scripts/convert_bria_fibo2_to_diffusers.pyconverts the original checkpoints.Docs and tests.
Weights:
briaai/Fibo-2.0-devandbriaai/Fibo-2.0-edit-turbo, in diffusers format. They're private until the release, and we're happy to give reviewers early access.Verification
I compared the pipeline against Bria's reference inference code, using the original checkpoints, the same seeds and bf16:
A100, SDPA:
H100: the reference code runs FlashAttention-3 there. With
pipe.transformer.set_attention_backend("_flash_3_varlen_hub"):Prompts and text images: compared with Bria's training and evaluation code, every prompt is byte-identical:
With the fonts training used passed as
fonts, the drawn images are also byte-identical on 17 test strings, including non-Latin scripts, wrapped lines and characters no font has.Text-encoder images: by default the pipeline thumbnails each image straight to 768 px for Qwen3-VL, as in training. The reference code upsamples 4× first, which changes that input slightly.
Review changes: the refactor for review (one prompt parser,
fontsas an argument,BriaFibo2ImageProcessor) changes no output. On 153 generation and edit cases through the tiny test pipeline, the text and images Qwen3-VL receives, the VAE's inputs and the outputs are byte-identical before and after. At full scale, so are the trained sizes (75 aspect ratios), the crops, the masking and the prepared images to edit.Batching: in float32, a batch of prompts of different lengths matches the separate single-prompt calls. This includes the varlen attention backends; see the self-review notes.
Checks (rebased on
main):make style,make qualityandmake fix-copiespass.check_copies,check_dummies,check_support_list,check_forward_call_docstringsandcheck_return_annotationspass.New tests: they pass on a Mac (CPU/MPS), except the group offloading and layerwise casting memory tests. Those fail the same way for Z-Image and Flux 2 on that machine, because MPS has no CUDA streams and no float8.
Notes
mask: fibo-2 was trained on edits whose image has the region to regenerate greyed out. The pipeline's image processor does the same: flat (128, 128, 128) grey, through the mask made binary at 128, at the image's own size before it's resized.fontsfolder of both release repos. The pipeline doesn't download them:__call__takes them asfonts, and the docs show how to get them. Without them, it warns when a text needs them.aesthetic_scoreandpreference_scorefields. The pipeline drops them from text-to-image and edit captions alike and serializes compactly. The negative prompt is used as given.Self-review notes
I ran the
self-reviewskill on the diff.Fixed:
forwarddocstring had noReturns:section, whichcheck_forward_call_docstringsflags.flash_varlen,_flash_3_varlen_hub,sage_varlen) keep the firstmask.sum()keys, so they kept the wrong ones.test_padded_batch_matches_single_promptscovers this.__call__read the first image's size beforecheck_inputshad validated it.Not fixed, on purpose:
RopeEmbedder, copied from Z-Image, builds its frequency table under a device context on first use. Z-Image skips these tests for the same reason.torch.compile(transformer)..tolist().compile_repeated_blocks) is not affected.Before submitting
self-reviewskill on the diff?Who can review?
@yiyixuxu @sayakpaul @DN6
🤖 Generated with Claude Code