Add Bria FIBO support (#279)

Co-authored-by: Filip Strand <filip@host-022.local>
This commit is contained in:
Filip Strand 2025-11-27 13:01:55 +01:00 committed by GitHub
parent c3e72bd73c
commit c2c860f4de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
187 changed files with 8364 additions and 1756 deletions

343
README.md
View File

@ -4,7 +4,7 @@
### About
Run the powerful [FLUX](https://blackforestlabs.ai/#get-flux) and [Qwen Image](https://github.com/QwenLM/Qwen-Image) models locally on your Mac!
Run the powerful [FLUX](https://blackforestlabs.ai/#get-flux), [Qwen Image](https://github.com/QwenLM/Qwen-Image) and [FIBO](https://huggingface.co/briaai/FIBO) models locally on your Mac!
### Table of contents
@ -23,6 +23,7 @@ Run the powerful [FLUX](https://blackforestlabs.ai/#get-flux) and [Qwen Image](h
- [🦙 Qwen Models](#-qwen-models)
* [🖼️ Qwen Image](#%EF%B8%8F-qwen-image)
* [✏️ Qwen Image Edit](#%EF%B8%8F-qwen-image-edit)
- [🌀 FIBO](#-fibo)
- [🔌 LoRA](#-lora)
- [🎭 In-Context Generation](#-in-context-generation)
* [📸 Kontext](#-kontext)
@ -50,7 +51,7 @@ Run the powerful [FLUX](https://blackforestlabs.ai/#get-flux) and [Qwen Image](h
### Philosophy
MFLUX is a line-by-line port of the FLUX and Qwen implementations in the [Huggingface Diffusers](https://github.com/huggingface/diffusers) and [Huggingface Transformers](https://github.com/huggingface/transformers) libraries to [Apple MLX](https://github.com/ml-explore/mlx).
MFLUX is a line-by-line port of the FLUX, Qwen and Bria models implementations in the [Huggingface Diffusers](https://github.com/huggingface/diffusers) and [Huggingface Transformers](https://github.com/huggingface/transformers) libraries to [Apple MLX](https://github.com/ml-explore/mlx).
MFLUX is purposefully kept minimal and explicit - Network architectures are hardcoded and no config files are used
except for the tokenizers. The aim is to have a tiny codebase with the single purpose of expressing these models
(thereby avoiding too many abstractions). While MFLUX priorities readability over generality and performance, [it can still be quite fast](#%EF%B8%8F-image-generation-speed-updated), [and even faster quantized](#%EF%B8%8F-quantization).
@ -1087,6 +1088,344 @@ mflux-generate-qwen-edit \
---
### 🌀 FIBO
MFLUX supports [FIBO](https://huggingface.co/briaai/FIBO) from [Bria.ai](https://bria.ai), the first open-source JSON-native text-to-image model trained on long structured captions. FIBO delivers high image quality, strong prompt adherence, and professional-grade control—trained exclusively on licensed data. ([Technical Paper](https://arxiv.org/abs/2511.06876))
![FIBO Example](src/mflux/assets/fibo_example.jpg)
FIBO is an 8B-parameter DiT-based, flow-matching model using **SmolLM3-3B** as the text encoder with a novel **DimFusion** conditioning architecture for efficient long-caption training, and **Wan 2.2** as the VAE. The VLM-assisted prompting uses a fine-tuned **Qwen3-VL** to expand short user intents, fill in missing details, and extract/edit structured prompts from images.
Most text-to-image models excel at imagination—but not control. FIBO is trained on structured JSON captions up to 1,000+ words, enabling precise, reproducible control over lighting, composition, color, and camera settings. The structured captions foster native disentanglement, allowing targeted, iterative refinement without prompt drift.
#### Key Features
- **VLM-guided JSON-native prompting**: Transform short prompts into structured schemas with 1,000+ words (lighting, camera, composition, DoF)
- **Disentangled control**: Tweak a single attribute (e.g., camera angle) without breaking the scene
- **Strong prompt adherence**: High alignment on PRISM-style evaluations
- **Enterprise-grade**: 100% licensed data with governance, repeatability, and legal clarity
#### The three modes: ✨ Generate, 🔧 Refine, and 💡 Inspire
**✨ Generate**: While the actual prompt input to FIBO is a structured JSON file, the generate command provides an interface to input pure text prompts. These are then expanded into structured JSON prompts using FIBO's Vision-Language Model (VLM) before being passed to the diffusion model for image generation. For example, the following prompt produces one of the images above:
```sh
mflux-generate-fibo \
--prompt "Three cartoon animal chefs in a colorful bakery kitchen, Pixar style: a bunny with floppy ears wearing a tall white chef hat and pink apron holding a chocolate cake on the left, a raccoon with a striped tail wearing blue oven mitts and a yellow bandana frosting cupcakes in the center, a penguin wearing a red bowtie and checkered apron carrying a tray of golden croissants on the right, warm kitchen lighting with flour dust in air" \
--width 1200 \
--height 540 \
--steps 20 \
--guidance 4.0 \
--seed 42 \
--output animal_bakers.png
```
This command will output both the generated image (`animal_bakers.png`) and a JSON prompt file (`animal_bakers.json`) containing the expanded structured prompt used for generation.
When the input prompt is pure text, it will be processed through FIBO's VLM to create the structured JSON prompt automatically.
Conversely, if a JSON prompt file is provided, it will be used directly for image generation, thus bypassing the VLM step and giving you full control over the prompt structure.
Another way to call the model directly is to provide a JSON prompt file as input:
```sh
mflux-generate-fibo \
--prompt-file animal_bakers.json \
--width 1200 \
--height 540 \
--steps 20 \
--guidance 4.0 \
--seed 42 \
--output animal_bakers.png
```
A point worth emphasizing is that when working with a JSON prompt file, the user can use whatever tool they prefer to edit it and is not forced to use the built in FIBO-VLM. Other good alternatives are [coding](https://cursor.com/agents) [agents](https://www.claude.com/product/claude-code), other [LLMs](https://github.com/ml-explore/mlx-lm)/[VLMs](https://github.com/Blaizzy/mlx-vlm) etc.
<details>
<summary><strong>Click to expand the JSON prompt file used (animal_bakers.json)</strong></summary>
```json
{
"short_description": "Three cartoon animal chefs are in a bakery kitchen, each holding a culinary creation. A bunny chef on the left presents a chocolate cake, a raccoon chef in the center is frosting cupcakes, and a penguin chef on the right carries a tray of croissants. The kitchen is brightly lit with warm tones, and flour dusts the air, creating a lively and cheerful baking atmosphere.",
"objects": [
{
"description": "A cartoon bunny wearing a white chef's hat and a pink apron, holding a chocolate cake with white frosting and cherries.",
"location": "left foreground",
"relationship": "The bunny chef is presenting the chocolate cake.",
"relative_size": "medium",
"shape_and_color": "Rounded bunny shape, white hat, pink apron, brown cake, white frosting, red cherries.",
"texture": "smooth",
"appearance_details": "Floppy ears, rosy cheeks, smiling expression.",
"pose": "Standing upright, holding the cake with both hands.",
"expression": "Joyful and proud.",
"clothing": "White chef's hat, pink apron.",
"action": "Holding and presenting a cake.",
"gender": "female",
"skin_tone_and_texture": "White fur, smooth texture.",
"orientation": "Upright, facing forward."
},
{
"description": "A cartoon raccoon wearing blue oven mitts and a yellow bandana, actively frosting cupcakes with pink and yellow frosting.",
"location": "center midground",
"relationship": "The raccoon chef is in the process of frosting cupcakes.",
"relative_size": "medium",
"shape_and_color": "Distinct raccoon shape, blue mitts, yellow bandana, pink and yellow frosting, brown cupcakes.",
"texture": "smooth",
"appearance_details": "Striped tail, bushy fur, focused expression.",
"pose": "Leaning forward slightly, hands busy frosting.",
"expression": "Concentrated and happy.",
"clothing": "Blue oven mitts, yellow bandana.",
"action": "Frosting cupcakes.",
"gender": "male",
"skin_tone_and_texture": "Brown and black fur, smooth texture.",
"orientation": "Upright, slightly angled."
},
{
"description": "A cartoon penguin wearing a red bowtie and a checkered apron, carrying a tray of golden croissants.",
"location": "right foreground",
"relationship": "The penguin chef is carrying a tray of freshly baked croissants.",
"relative_size": "medium",
"shape_and_color": "Classic penguin shape, red bowtie, red and white apron, golden croissants.",
"texture": "smooth",
"appearance_details": "Black and white body, orange beak and feet, smiling expression.",
"pose": "Standing upright, holding the tray with both hands.",
"expression": "Cheerful and friendly.",
"clothing": "Red bowtie, checkered apron.",
"action": "Carrying a tray of croissants.",
"gender": "male",
"skin_tone_and_texture": "Feathered texture, smooth appearance.",
"orientation": "Upright, facing forward."
},
{
"description": "A chocolate cake with white frosting and two red cherries on top.",
"location": "left foreground",
"relationship": "Held by the bunny chef.",
"relative_size": "medium",
"shape_and_color": "Round cake, dark brown, white frosting, red cherries.",
"texture": "smooth frosting, slightly textured cake",
"appearance_details": "Decorated with cherries.",
"number_of_objects": 1,
"orientation": "Horizontal"
},
{
"description": "A tray filled with golden-brown croissants.",
"location": "right foreground",
"relationship": "Carried by the penguin chef.",
"relative_size": "medium",
"shape_and_color": "Elongated, crescent-shaped croissants, golden brown.",
"texture": "flaky, slightly crisp exterior",
"appearance_details": "Arranged neatly on a metal tray.",
"number_of_objects": 1,
"orientation": "Horizontal"
}
],
"background_setting": "A brightly lit bakery kitchen with wooden countertops, shelves stocked with baking ingredients and equipment, and a window in the background. Flour is lightly dusted in the air.",
"lighting": {
"conditions": "warm indoor lighting",
"direction": "front-lit and side-lit",
"shadows": "soft, diffused shadows"
},
"aesthetics": {
"composition": "centered composition with the three chefs forming a horizontal line",
"color_scheme": "warm and cheerful, with dominant browns, yellows, pinks, and whites",
"mood_atmosphere": "joyful, friendly, and inviting",
"aesthetic_score": "very high",
"preference_score": "very high"
},
"photographic_characteristics": {
"depth_of_field": "shallow",
"focus": "sharp focus on the chefs and their creations",
"camera_angle": "eye-level",
"lens_focal_length": "standard lens"
},
"style_medium": "digital illustration",
"text_render": [],
"context": "This image is a charming illustration, likely for a children's book, educational material, or a bakery-themed advertisement, designed to evoke feelings of happiness and the joy of baking.",
"artistic_style": "Pixar-style animation"
}
```
Note: This JSON prompt was generated using FIBO's "Generate" mode from a short text description. Note the strong alignment between the JSON prompt and the image!
</details>
**🔧 Refine**: While the JSON prompt can be edited manually, it can be quite complex and inconvenient to modify directly. The refinement mode helps to solve this issue by also expanding a simple user instruction in order to tweak specific attributes. The VLM processes these instructions and updates the JSON prompt accordingly before generating new images.
![FIBO Refine Example](src/mflux/assets/fibo_refine_example.jpg)
Assuming we already have a previous prompt file, like `owl_brown.json`, we can refine this prompt to change the owl's color and add some accessories:
```sh
mflux-refine-fibo \
--prompt-file owl_brown.json \
--instructions "Make the owl white instead of brown, and add round glasses and a black scarf. Keep everything else exactly the same - the same forest background, moonlight lighting, composition, and overall whimsical atmosphere." \
--output owl_white.json
```
<details>
<summary><strong>Click to expand the refined JSON prompt file (owl_white.json)</strong></summary>
```json
{
"short_description": "A hyper-detailed, ultra-fluffy owl sitting in the trees at night, looking directly at the camera with wide, adorable, expressive eyes. Its feathers are soft and voluminous, catching the cool moonlight with subtle silver highlights. The owl's gaze is curious and full of charm, giving it a whimsical, storybook-like personality. It is wearing round glasses and a black scarf.",
"objects": [
{
"description": "An adorable, fluffy owl with large, expressive eyes and soft, voluminous feathers. Its plumage is white, with subtle silver highlights from the moonlight. It is wearing round glasses and a black scarf.",
"location": "center",
"relationship": "The owl is the sole subject, perched comfortably within its environment.",
"relative_size": "large within frame",
"shape_and_color": "Round head, large eyes, bulky body, predominantly white with silver accents.",
"texture": "Extremely soft, fluffy, and detailed feathers, giving a plush toy-like appearance.",
"appearance_details": "The eyes are wide, dark, and reflective, conveying a sense of wonder and curiosity. The beak is small and light-colored, almost hidden by the feathers. Subtle silver highlights catch the moonlight on its feathers. It has round glasses on its nose and a black scarf around its neck.",
"orientation": "upright, facing forward"
}
],
"background_setting": "A dark, nocturnal forest setting with blurred trees and foliage, illuminated by a soft, cool moonlight. The background is out of focus, emphasizing the owl.",
"lighting": {
"conditions": "moonlight",
"direction": "backlit and side-lit from the left",
"shadows": "soft, diffused shadows on the right side of the owl and within the background foliage, indicating a single light source."
},
"aesthetics": {
"composition": "centered, portrait composition",
"color_scheme": "cool blues and silvers from the moonlight contrasting with white of the owl and forest.",
"mood_atmosphere": "mysterious, enchanting, whimsical, and serene.",
"aesthetic_score": "very high",
"preference_score": "very high"
},
"photographic_characteristics": {
"depth_of_field": "shallow",
"focus": "sharp focus on the owl's face and eyes, with a soft blur in the background.",
"camera_angle": "eye-level",
"lens_focal_length": "portrait lens (e.g., 50mm-85mm)"
},
"style_medium": "digital illustration",
"text_render": [],
"context": "A whimsical character illustration, possibly for a children's book, animated film, or fantasy art collection.",
"artistic_style": "fantasy, illustrative, detailed"
}
```
Note: This JSON prompt was refined from the original `owl_brown.json` by changing the owl's color to white and adding round glasses and a black scarf, while preserving the forest background, moonlight lighting, and whimsical atmosphere.
</details>
Finally, generate the refined white owl image using the updated JSON prompt:
```sh
mflux-generate-fibo \
--prompt-file owl_white.json \
--width 1024 \
--height 560 \
--steps 20 \
--guidance 4.0 \
--seed 42 \
--quantize 4 \
--output owl_white.png
```
The refine command reads the existing JSON prompt, applies the refinement instructions to change the owl's color and add accessories, and outputs a new refined JSON file. This refined prompt is then used to generate a new image that reflects the requested changes while maintaining the overall scene and composition.
It is worth noting that refine does not work the same way as other editing techniques like Flux Kontext or Qwen Image Edit. Instead of modifying an existing image, it modifies the underlying **structured prompt** to produce a new image that reflects the requested changes while maintaining the overall scene and composition.
**💡 Inspire**: Provide an image instead of text. FIBO's vision-language model extracts a detailed, structured prompt, blends it with your creative intent, and produces related images—ideal for inspiration without overreliance on the original.
![FIBO Inspire Example](src/mflux/assets/fibo_inspire_example.jpg)
Starting from an image, you can extract a structured JSON prompt that captures its visual characteristics. For example, using a [blue and brown bird on brown tree trunk](https://unsplash.com/photos/blue-and-brown-bird-on-brown-tree-trunk-DPXytK8Z59Y), we can extract a detailed prompt:
```sh
mflux-inspire-fibo \
--image-path bird.jpg \
--prompt "blue and brown bird on brown tree trunk" \
--output bird_inspired.json \
--seed 42
```
This command analyzes the image and generates a structured JSON prompt file (`bird_inspired.json`) that describes the visual elements, composition, lighting, and style. You can then use this JSON prompt to generate new images with similar characteristics:
```sh
mflux-generate-fibo \
--prompt-file bird_inspired.json \
--width 1024 \
--height 672 \
--steps 20 \
--guidance 4.0 \
--seed 42 \
-q 8 \
--output bird_inspired.png
```
<details>
<summary><strong>Click to expand the JSON prompt file used (bird_inspired.json)</strong></summary>
```json
{
"short_description": "A vibrant blue and brown kingfisher is perched on a weathered tree trunk, facing left. The bird's iridescent plumage is detailed, with striking orange and white markings on its chest and throat. Its long, sharp black beak is prominent. The background is a soft, out-of-focus gradient of warm yellow and green, creating a natural and serene environment. The lighting highlights the textures of the bird's feathers and the rough bark of the trunk.",
"objects": [
{
"description": "A male kingfisher with striking iridescent blue plumage on its back and wings, a rich orange and white chest, and a black beak. It has a small, dark eye and a red-orange patch on its face.",
"location": "center",
"relationship": "perched on the tree trunk",
"relative_size": "medium within frame",
"shape_and_color": "Bird shape, predominantly blue, orange, and white.",
"texture": "Feathers appear smooth and slightly glossy.",
"appearance_details": "The beak is long, thin, and black. The orange patch on its face is distinct.",
"number_of_objects": 1,
"pose": "Standing upright on its legs, head turned to the left.",
"expression": "Alert and focused.",
"action": "Perched, observing its surroundings.",
"gender": "male",
"orientation": "Facing left, upright"
},
{
"description": "A section of a weathered, rough tree trunk, providing a perch for the kingfisher. It has a natural, organic shape with visible bark texture.",
"location": "bottom-center foreground",
"relationship": "supports the kingfisher",
"relative_size": "medium",
"shape_and_color": "Irregular cylindrical shape, brown and grey tones.",
"texture": "Rough, gnarled bark texture.",
"appearance_details": "Some smaller branches or knots are visible on the trunk.",
"number_of_objects": 1,
"orientation": "Horizontal, lying on its side"
}
],
"background_setting": "A soft, blurred background with a warm gradient transitioning from a light yellow at the top to a muted green at the bottom. This creates a natural, out-of-focus environment, likely foliage or sky.",
"lighting": {
"conditions": "natural daylight",
"direction": "side-lit from the right",
"shadows": "soft shadows, particularly on the left side of the bird and the trunk"
},
"aesthetics": {
"composition": "rule of thirds, with the bird positioned slightly off-center",
"color_scheme": "vibrant blues and oranges contrasting with the soft yellow and green background",
"mood_atmosphere": "serene, natural, captivating",
"aesthetic_score": "very high",
"preference_score": "very high"
},
"photographic_characteristics": {
"depth_of_field": "shallow, with a strong bokeh effect in the background",
"focus": "sharp focus on the kingfisher",
"camera_angle": "eye-level",
"lens_focal_length": "telephoto lens"
},
"style_medium": "photograph",
"text_render": [],
"context": "This image is a wildlife photograph, likely intended for nature magazines, educational materials, or as a decorative print for nature enthusiasts.",
"artistic_style": "photorealistic, detailed"
}
```
Note: This JSON prompt was extracted from the input image using FIBO's VLM, capturing the visual characteristics of a kingfisher bird perched on a tree trunk with a bokeh background.
</details>
The inspire command is particularly useful when you want to:
- Extract the visual style and composition from a reference image
- Create variations of an existing image while maintaining its core characteristics
- Understand how FIBO interprets and structures visual information
- Blend your creative intent (via the optional `--prompt` parameter) with the visual content of the image
Note: The optional `--prompt` parameter allows you to guide the VLM's interpretation of the image. For example, you might use `--prompt "futuristic cityscape"` to influence how the image is analyzed and structured.
⚠️ *Note: FIBO requires downloading the `briaai/FIBO` model weights (~24GB) and the `briaai/FIBO-vlm` vision-language model (~8GB), totaling ~32GB for the full model, or use quantization for smaller sizes.*
---
### 🔌 LoRA
MFLUX support loading trained [LoRA](https://huggingface.co/docs/diffusers/en/training/lora) adapters (actual training support is coming).

View File

@ -17,7 +17,7 @@ source-exclude = [
[project]
name = "mflux"
version = "0.11.1"
version = "0.12.0.dev0"
description = "A MLX port of FLUX based on the Huggingface Diffusers implementation."
readme = "README.md"
keywords = ["diffusers", "flux", "mlx"]
@ -50,7 +50,7 @@ dependencies = [
"torch>=2.3.1,<3.0; python_version<'3.13'",
"torch>=2.8.0,<3.0; python_version>='3.13'",
"tqdm>=4.66.5,<5.0",
"transformers>=4.55.0,<5.0",
"transformers>=4.57,<5.0",
"twine>=6.1.0,<7.0",
]
classifiers = [
@ -86,6 +86,9 @@ mflux-generate-redux = "mflux.generate_redux:main"
mflux-generate-kontext = "mflux.generate_kontext:main"
mflux-generate-qwen = "mflux.generate_qwen:main"
mflux-generate-qwen-edit = "mflux.generate_qwen_edit:main"
mflux-generate-fibo = "mflux.generate_fibo:main"
mflux-refine-fibo = "mflux.refine_fibo:main"
mflux-inspire-fibo = "mflux.inspire_fibo:main"
mflux-concept = "mflux.concept:main"
mflux-concept-from-image = "mflux.concept_from_image:main"
mflux-save = "mflux.save:main"

6
src/mflux/__init__.py Normal file
View File

@ -0,0 +1,6 @@
import os
# Set TOKENIZERS_PARALLELISM to avoid fork warning
# This must be set before any tokenizers are imported/used
if "TOKENIZERS_PARALLELISM" not in os.environ:
os.environ["TOKENIZERS_PARALLELISM"] = "false"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

View File

@ -4,7 +4,7 @@ import re
import subprocess
from mflux.callbacks.callback import BeforeLoopCallback
from mflux.error.exceptions import StopImageGenerationException
from mflux.utils.exceptions import StopImageGenerationException
PMSET_AC_POWER_STATUS = "Now drawing from 'AC Power'"
PMSET_BATT_STATUS_PATTERN = r"InternalBattery-.+?(\d+)%"

View File

@ -6,7 +6,7 @@ import PIL.Image
from mflux.callbacks.callback import BeforeLoopCallback
from mflux.config.runtime_config import RuntimeConfig
from mflux.post_processing.image_util import ImageUtil
from mflux.utils.image_util import ImageUtil
class CannyImageSaver(BeforeLoopCallback):

View File

@ -6,7 +6,7 @@ import PIL.Image
from mflux.callbacks.callback import BeforeLoopCallback
from mflux.config.runtime_config import RuntimeConfig
from mflux.post_processing.image_util import ImageUtil
from mflux.utils.image_util import ImageUtil
class DepthImageSaver(BeforeLoopCallback):

View File

@ -6,8 +6,8 @@ import tqdm
from mflux.callbacks.callback import BeforeLoopCallback, InLoopCallback, InterruptCallback
from mflux.config.runtime_config import RuntimeConfig
from mflux.post_processing.array_util import ArrayUtil
from mflux.post_processing.image_util import ImageUtil
from mflux.utils.array_util import ArrayUtil
from mflux.utils.image_util import ImageUtil
class StepwiseHandler(BeforeLoopCallback, InLoopCallback, InterruptCallback):

View File

@ -1,11 +1,11 @@
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.concept_attention.flux_concept import Flux1Concept
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -41,7 +41,7 @@ def main():
# 3. Generate an image for each seed value
image = flux.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
concept=args.concept,
heatmap_timesteps=args.heatmap_timesteps,
heatmap_layer_indices=args.heatmap_layer_indices,

View File

@ -1,11 +1,11 @@
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.concept_attention.flux_concept_from_image import Flux1ConceptFromImage
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -41,7 +41,7 @@ def main():
# 3. Generate an image for each seed value
image = flux.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
concept=args.concept,
image_path=str(args.input_image_path),
heatmap_timesteps=args.heatmap_timesteps,

View File

@ -1,7 +1,7 @@
from functools import lru_cache
from typing import Literal
from mflux.error.error import InvalidBaseModel, ModelConfigError
from mflux.utils.exceptions import InvalidBaseModel, ModelConfigError
class ModelConfig:
@ -94,6 +94,11 @@ class ModelConfig:
def qwen_image_edit() -> "ModelConfig":
return AVAILABLE_MODELS["qwen-image-edit"]
@staticmethod
@lru_cache
def fibo() -> "ModelConfig":
return AVAILABLE_MODELS["fibo"]
def x_embedder_input_dim(self) -> int:
if "Fill" in self.model_name:
return 384
@ -319,4 +324,16 @@ AVAILABLE_MODELS = {
requires_sigma_shift=None,
priority=12,
),
"fibo": ModelConfig(
aliases=["fibo"],
model_name="briaai/FIBO",
base_model=None,
controlnet_model=None,
custom_transformer_model=None,
num_train_steps=1000,
max_sequence_length=512,
supports_guidance=True,
requires_sigma_shift=False,
priority=13,
),
}

View File

@ -5,8 +5,8 @@ import mlx.core as mx
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.schedulers import SCHEDULER_REGISTRY, try_import_external_scheduler
from mflux.schedulers.linear_scheduler import LinearScheduler
from mflux.models.common.schedulers import SCHEDULER_REGISTRY, try_import_external_scheduler
from mflux.models.common.schedulers.linear_scheduler import LinearScheduler
logger = logging.getLogger(__name__)

View File

@ -1,6 +0,0 @@
class ModelConfigError(ValueError):
"""User error in model config."""
class InvalidBaseModel(ModelConfigError):
"""Invalid base model, cannot infer model properties."""

View File

@ -1,11 +1,11 @@
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.txt2img.flux import Flux1
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_negative_prompt, get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -40,8 +40,8 @@ def main():
# 3. Generate an image for each seed value
image = model.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
negative_prompt=get_effective_negative_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
negative_prompt=PromptUtils.get_effective_negative_prompt(args),
config=Config(
num_inference_steps=args.steps,
height=args.height,

View File

@ -1,11 +1,11 @@
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.controlnet.flux_controlnet import Flux1Controlnet
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -40,7 +40,7 @@ def main():
# 3. Generate an image for each seed value
image = flux.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
controlnet_image_path=args.controlnet_image_path,
config=Config(
num_inference_steps=args.steps,

View File

@ -1,10 +1,10 @@
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.depth.flux_depth import Flux1Depth
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -38,7 +38,7 @@ def main():
# 3. Generate an image for each seed value
image = flux.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
config=Config(
num_inference_steps=args.steps,
height=args.height,

View File

@ -0,0 +1,87 @@
import gc
import json
import mlx.core as mx
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.models.fibo.variants.txt2img.fibo import FIBO
from mflux.models.fibo_vlm.model.fibo_vlm import FiboVLM
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
# 0. Parse command line arguments
parser = CommandLineParser(description="Generate an image using FIBO model.")
parser.add_general_arguments()
parser.add_model_arguments(require_model_arg=False)
parser.add_lora_arguments()
parser.add_image_generator_arguments(supports_metadata_config=True)
parser.add_image_to_image_arguments(required=False)
parser.add_output_arguments()
args = parser.parse_args()
# 0. Set default guidance value if not provided by user
if args.guidance is None:
args.guidance = ui_defaults.GUIDANCE_SCALE
json_prompt = _get_json_prompt(args)
# 1. Load the FIBO model
fibo = FIBO(
model_config=ModelConfig.fibo(),
quantize=args.quantize,
local_path=args.path,
)
# 2. Register callbacks
memory_saver = CallbackManager.register_callbacks(args=args, model=fibo)
try:
for seed in args.seed:
# 3. Generate an image for each seed value
image = fibo.generate_image(
seed=seed,
prompt=json_prompt,
negative_prompt=PromptUtils.get_effective_negative_prompt(args),
config=Config(
num_inference_steps=args.steps,
height=args.height,
width=args.width,
guidance=args.guidance,
image_path=args.image_path,
image_strength=args.image_strength,
scheduler="flow_match_euler_discrete",
),
)
# 4. Save the image
image.save(path=args.output.format(seed=seed), export_json_metadata=args.metadata)
except (StopImageGenerationException, PromptFileReadError) as exc:
print(exc)
finally:
if memory_saver:
print(memory_saver.memory_stats())
def _get_json_prompt(args):
prompt = PromptUtils.get_effective_prompt(args)
try:
json.loads(prompt)
json_prompt = prompt
except json.JSONDecodeError:
vlm = FiboVLM()
json_prompt = vlm.generate(prompt=prompt, seed=42)
del vlm
gc.collect()
mx.clear_cache()
return json_prompt
if __name__ == "__main__":
main()

View File

@ -1,10 +1,10 @@
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.fill.flux_fill import Flux1Fill
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -38,7 +38,7 @@ def main():
# 3. Generate an image for each seed value
image = flux.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
config=Config(
num_inference_steps=args.steps,
height=args.height,

View File

@ -3,11 +3,11 @@ from pathlib import Path
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.in_context.flux_in_context_fill import Flux1InContextFill
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -51,7 +51,7 @@ def main():
# 3. Generate an image for each seed value
image = flux.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
left_image_path=args.garment_image,
right_image_path=args.person_image,
config=Config(

View File

@ -3,12 +3,12 @@ from pathlib import Path
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.in_context.flux_in_context_dev import Flux1InContextDev
from mflux.models.flux.variants.in_context.utils.in_context_loras import LORA_REPO_ID, get_lora_filename
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -49,7 +49,7 @@ def main():
# 3. Generate an image for each seed value
image = flux.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
config=Config(
num_inference_steps=args.steps,
height=args.height,

View File

@ -5,12 +5,12 @@ from PIL import Image
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.in_context.flux_in_context_fill import Flux1InContextFill
from mflux.models.flux.variants.in_context.utils.in_context_loras import prepare_ic_edit_loras
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -82,7 +82,7 @@ def _get_effective_ic_edit_prompt(args):
if hasattr(args, "instruction") and args.instruction:
return f"A diptych with two side-by-side images of the same scene. On the right, the scene is exactly the same as on the left but {args.instruction}"
else:
return get_effective_prompt(args)
return PromptUtils.get_effective_prompt(args)
def _resize_for_ic_edit_optimal_width(args):

View File

@ -2,11 +2,11 @@ from pathlib import Path
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.kontext.flux_kontext import Flux1Kontext
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -40,7 +40,7 @@ def main():
# 3. Generate an image for each seed value
image = flux.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
config=Config(
num_inference_steps=args.steps,
height=args.height,

View File

@ -1,11 +1,11 @@
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.qwen.variants.txt2img.qwen_image import QwenImage
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_negative_prompt, get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -40,8 +40,8 @@ def main():
# 3. Generate an image for each seed value
image = qwen.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
negative_prompt=get_effective_negative_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
negative_prompt=PromptUtils.get_effective_negative_prompt(args),
config=Config(
num_inference_steps=args.steps,
height=args.height,

View File

@ -2,11 +2,11 @@ from pathlib import Path
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.qwen.variants.edit.qwen_image_edit import QwenImageEdit
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_negative_prompt, get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -53,7 +53,7 @@ def main():
# 4. Generate an image for each seed value
image = qwen.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
config=Config(
num_inference_steps=args.steps,
height=args.height,
@ -61,7 +61,7 @@ def main():
guidance=args.guidance,
image_path=config_image_path,
),
negative_prompt=get_effective_negative_prompt(args),
negative_prompt=PromptUtils.get_effective_negative_prompt(args),
image_paths=image_paths,
)

View File

@ -3,11 +3,11 @@ from pathlib import Path
from mflux.callbacks.callback_manager import CallbackManager
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.error.exceptions import PromptFileReadError, StopImageGenerationException
from mflux.models.flux.variants.redux.flux_redux import Flux1Redux
from mflux.ui import defaults as ui_defaults
from mflux.ui.cli.parsers import CommandLineParser
from mflux.ui.prompt_utils import get_effective_prompt
from mflux.ui.prompt_utils import PromptUtils
from mflux.utils.exceptions import PromptFileReadError, StopImageGenerationException
def main():
@ -48,7 +48,7 @@ def main():
# 3. Generate an image for each seed value
image = flux.generate_image(
seed=seed,
prompt=get_effective_prompt(args),
prompt=PromptUtils.get_effective_prompt(args),
config=Config(
num_inference_steps=args.steps,
height=args.height,

View File

@ -4,8 +4,8 @@ import sys
from datetime import datetime
from pathlib import Path
from mflux.post_processing.metadata_reader import MetadataReader
from mflux.ui.cli.parsers import CommandLineParser
from mflux.utils.metadata_reader import MetadataReader
def format_metadata(metadata: dict) -> str:
@ -121,4 +121,3 @@ def main():
if __name__ == "__main__":
main()

68
src/mflux/inspire_fibo.py Normal file
View File

@ -0,0 +1,68 @@
import json
from pathlib import Path
from PIL import Image
from mflux.models.fibo_vlm.model.fibo_vlm import FiboVLM
from mflux.ui.cli.parsers import CommandLineParser
from mflux.utils.exceptions import PromptFileReadError
def main():
# 0. Parse command line arguments
parser = CommandLineParser(description="Generate FIBO JSON prompts from images using VLM.")
# fmt: off
parser.add_argument("--image-path", type=Path, required=True, help="Path to image file to inspire from")
parser.add_argument("--prompt", type=str, default=None, help="Optional text prompt to blend with the image (e.g., 'Make futuristic')")
parser.add_argument("--output", type=Path, default=Path("inspired.json"), help="Output path for generated JSON prompt (default: inspired.json)")
parser.add_argument("--path", type=str, default=None, help="Local path for loading the VLM model from disk")
parser.add_argument("--top-p", type=float, default=0.9, help="Top-p sampling for VLM (default: 0.9)")
parser.add_argument("--temperature", type=float, default=0.2, help="Temperature for VLM (default: 0.2)")
parser.add_argument("--max-tokens", type=int, default=4096, help="Max tokens for VLM generation (default: 4096)")
parser.add_argument("--seed", type=int, default=None, help="Seed for VLM generation")
# fmt: on
args = parser.parse_args()
try:
# 1. Load the image
image = _load_image(args.image_path)
# 2. Generate JSON prompt from image
vlm = FiboVLM(local_path=args.path)
inspired_json = vlm.inspire(
image=image,
prompt=args.prompt,
top_p=args.top_p,
temperature=args.temperature,
max_tokens=args.max_tokens,
seed=args.seed,
)
# 3. Parse and save generated JSON prompt
_save_prompt(args, inspired_json)
except (PromptFileReadError, ValueError) as exc:
print(exc)
def _save_prompt(args, inspired_json):
try:
inspired_json_parsed = json.loads(inspired_json)
except json.JSONDecodeError as e:
raise ValueError(f"VLM did not return valid JSON: {e}")
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w") as f:
json.dump(inspired_json_parsed, f, indent=2, ensure_ascii=False)
def _load_image(image_path: Path):
if not image_path.exists():
raise PromptFileReadError(f"Image file does not exist: {image_path}")
try:
image = Image.open(image_path).convert("RGB")
except (OSError, IOError, ValueError) as e:
raise PromptFileReadError(f"Failed to load image: {e}")
return image
if __name__ == "__main__":
main()

View File

@ -1,85 +0,0 @@
from pathlib import Path
import mlx.core as mx
from mlx import nn
from mflux.post_processing.array_util import ArrayUtil
from mflux.post_processing.image_util import ImageUtil
class Img2Img:
def __init__(
self,
vae: nn.Module,
sigmas: mx.array,
init_time_step: int,
image_path: str | Path | None,
):
self.vae = vae
self.sigmas = sigmas
self.init_time_step = init_time_step
self.image_path = image_path
class LatentCreator:
@staticmethod
def create(
seed: int,
height: int,
width: int,
) -> mx.array:
return mx.random.normal(
shape=[1, (height // 16) * (width // 16), 64],
key=mx.random.key(seed),
)
@staticmethod
def create_for_txt2img_or_img2img(
seed: int,
height: int,
width: int,
img2img: Img2Img,
) -> mx.array:
# 0. Determine type of image generation
if img2img.image_path is None:
# 1. Create the pure noise
return LatentCreator.create(
seed=seed,
height=height,
width=width,
)
else:
# 1. Create the pure noise
pure_noise = LatentCreator.create(
seed=seed,
height=height,
width=width,
)
# 2. Encode the image
encoded = LatentCreator.encode_image(vae=img2img.vae, image_path=img2img.image_path, height=height, width=width) # fmt: off
latents = ArrayUtil.pack_latents(latents=encoded, height=height, width=width)
# 3. Find the appropriate sigma value
sigma = img2img.sigmas[img2img.init_time_step]
# 4. Blend the appropriate amount of noise based on linear interpolation
return LatentCreator.add_noise_by_interpolation(
clean=latents,
noise=pure_noise,
sigma=sigma,
)
@staticmethod
def encode_image(vae: nn.Module, image_path: str | Path, height: int, width: int):
scaled_user_image = ImageUtil.scale_to_dimensions(
image=ImageUtil.load_image(image_path).convert("RGB"),
target_width=width,
target_height=height,
)
encoded = vae.encode(ImageUtil.to_array(scaled_user_image))
return encoded
@staticmethod
def add_noise_by_interpolation(clean: mx.array, noise: mx.array, sigma: float) -> mx.array:
return (1 - sigma) * clean + sigma * noise

View File

@ -7,7 +7,7 @@ import sys
from collections import defaultdict
from pathlib import Path
from mflux.utils.lora_library import _discover_lora_files
from mflux.models.common.lora.download.lora_library import _discover_lora_files
def list_loras(paths: list[str] | None = None) -> int:

View File

@ -0,0 +1,63 @@
from pathlib import Path
from typing import TYPE_CHECKING
import mlx.core as mx
from mlx import nn
from mflux.utils.image_util import ImageUtil
if TYPE_CHECKING:
from mflux.models.fibo.latent_creator.fibo_latent_creator import FiboLatentCreator
from mflux.models.flux.latent_creator.flux_latent_creator import FluxLatentCreator
from mflux.models.qwen.latent_creator.qwen_latent_creator import QwenLatentCreator
class Img2Img:
def __init__(
self,
vae: nn.Module,
latent_creator: type["FiboLatentCreator"] | type["FluxLatentCreator"] | type["QwenLatentCreator"],
sigmas: mx.array,
init_time_step: int,
image_path: str | Path | None,
):
self.vae = vae
self.sigmas = sigmas
self.init_time_step = init_time_step
self.image_path = image_path
self.latent_creator = latent_creator
class LatentCreator:
@staticmethod
def create_for_txt2img_or_img2img(
seed: int,
height: int,
width: int,
img2img: Img2Img,
) -> mx.array:
latent_creator = img2img.latent_creator
if img2img.image_path is None:
# txt2img: just create noise
return latent_creator.create_noise(seed, height, width)
else:
# img2img: blend encoded image with noise
pure_noise = latent_creator.create_noise(seed, height, width)
encoded = LatentCreator.encode_image(vae=img2img.vae, image_path=img2img.image_path, height=height, width=width) # fmt: off
latents = latent_creator.pack_latents(encoded, height, width)
sigma = img2img.sigmas[img2img.init_time_step]
return LatentCreator.add_noise_by_interpolation(clean=latents, noise=pure_noise, sigma=sigma)
@staticmethod
def encode_image(vae: nn.Module, image_path: str | Path, height: int, width: int) -> mx.array:
scaled_user_image = ImageUtil.scale_to_dimensions(
image=ImageUtil.load_image(image_path).convert("RGB"),
target_width=width,
target_height=height,
)
return vae.encode(ImageUtil.to_array(scaled_user_image))
@staticmethod
def add_noise_by_interpolation(clean: mx.array, noise: mx.array, sigma: float) -> mx.array:
return (1 - sigma) * clean + sigma * noise

View File

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
import mlx.nn as nn
if TYPE_CHECKING:
from mflux.models.fibo.weights.fibo_weight_handler import FIBOWeightHandler
from mflux.models.flux.variants.controlnet.weight_handler_controlnet import WeightHandlerControlnet
from mflux.models.flux.weights.weight_handler import WeightHandler
from mflux.models.qwen.weights.qwen_weight_handler import QwenWeightHandler
@ -19,9 +20,6 @@ class QuantizationUtil:
weights: "WeightHandler",
) -> None:
q_level = weights.meta_data.quantization_level
# mx.save_tensors saves metadata dict kv 'quantization_level': 'None' as a str: str mapping
# we coerce both configs to NoneType to help users use non-quantized saved model files
if q_level == "None":
q_level = None
if quantize == "None":
@ -92,3 +90,23 @@ class QuantizationUtil:
nn.quantize(vae, bits=bits)
nn.quantize(transformer, bits=bits)
# nn.quantize(text_encoder, bits=bits) # Quantization of text encoder causes significant semantic degradation
@staticmethod
def quantize_fibo_models(
vae: nn.Module,
transformer: nn.Module,
text_encoder: nn.Module | None,
quantize: int,
weights: "FIBOWeightHandler",
) -> None:
q_level = weights.meta_data.quantization_level
if q_level == "None":
q_level = None
if quantize == "None":
quantize = None
if quantize is not None or q_level is not None:
bits = int(q_level) if q_level is not None else quantize
nn.quantize(vae, class_predicate=QuantizationUtil.quantization_predicate, bits=bits)
nn.quantize(transformer, class_predicate=QuantizationUtil.quantization_predicate, bits=bits)
nn.quantize(text_encoder, class_predicate=QuantizationUtil.quantization_predicate, bits=bits)

View File

@ -66,7 +66,7 @@ def try_import_external_scheduler(scheduler_object_path: str):
if not issubclass(SchedulerClass, BaseScheduler):
raise InvalidSchedulerType(
f"{scheduler_object_path!r} does not inherit from BaseScheduler. "
f"All schedulers must inherit from mflux.schedulers.BaseScheduler."
f"All schedulers must inherit from mflux.models.common.schedulers.BaseScheduler."
)
return SchedulerClass

View File

@ -6,7 +6,7 @@ import mlx.core as mx
if TYPE_CHECKING:
from mflux.config.runtime_config import RuntimeConfig
from mflux.schedulers.base_scheduler import BaseScheduler
from mflux.models.common.schedulers.base_scheduler import BaseScheduler
class FlowMatchEulerDiscreteScheduler(BaseScheduler):

View File

@ -5,7 +5,7 @@ import mlx.core as mx
if TYPE_CHECKING:
from mflux.config.runtime_config import RuntimeConfig
from mflux.schedulers.base_scheduler import BaseScheduler
from mflux.models.common.schedulers.base_scheduler import BaseScheduler
class LinearScheduler(BaseScheduler):

View File

@ -1 +1,5 @@
"""Common weight mapping utilities."""
from mflux.models.common.weights.model_saver import ModelSaver
__all__ = ["ModelSaver"]

View File

@ -133,8 +133,11 @@ class WeightMapper:
flat[concrete_hf] = (concrete_mlx, target.transform)
elif has_block:
# Expand {block} only (for transformer blocks or visual blocks)
# Check if target has max_blocks override
if target.max_blocks is not None:
max_blocks = target.max_blocks
# Check if this is for visual blocks (32 blocks) or transformer blocks
if "visual.blocks" in hf_pattern or "visual.blocks" in target.mlx_path:
elif "visual.blocks" in hf_pattern or "visual.blocks" in target.mlx_path:
max_blocks = 32 # Visual blocks are always 32
else:
max_blocks = num_blocks if num_blocks > 0 else 4 # Default 4 for up_blocks

View File

@ -22,6 +22,7 @@ class WeightTarget:
hf_patterns: List[str] # HuggingFace naming patterns, e.g., ["transformer_blocks.{block}.attn.to_q.weight"]
transform: Optional[Callable[[mx.array], mx.array]] = None # Optional transform (reshape, transpose, etc.)
required: bool = True # If False, weight is optional (may not exist in all models)
max_blocks: Optional[int] = None # Override num_blocks for this target (e.g., for fixed-size lists)
class WeightMapping(Protocol):

View File

@ -0,0 +1,114 @@
from pathlib import Path
from typing import Any
import mlx.core as mx
from mlx import nn
from mlx.utils import tree_flatten
from transformers import PreTrainedTokenizer
from mflux.utils.version_util import VersionUtil
class ModelSaver:
@staticmethod
def save_model(
model: Any,
bits: int,
base_path: str,
tokenizers: list[tuple[str, str]] | None = None,
components: list[tuple[str, str]] | None = None,
) -> None:
# Default tokenizers: try common patterns
if tokenizers is None:
tokenizers = ModelSaver._detect_tokenizers(model)
# Default components: try common patterns
if components is None:
components = ModelSaver._detect_components(model)
# Save tokenizers
for attr_path, subdir in tokenizers:
tokenizer = ModelSaver._get_nested_attr(model, attr_path)
if tokenizer is not None:
ModelSaver._save_tokenizer(base_path, tokenizer, subdir)
# Save model components
for attr_name, subdir in components:
component = getattr(model, attr_name, None)
if component is not None:
ModelSaver._save_weights(base_path, bits, component, subdir)
@staticmethod
def _detect_tokenizers(model: Any) -> list[tuple[str, str]]:
tokenizers = []
if hasattr(model, "clip_tokenizer") and hasattr(model.clip_tokenizer, "tokenizer"):
tokenizers.append(("clip_tokenizer.tokenizer", "tokenizer"))
if hasattr(model, "t5_tokenizer") and hasattr(model.t5_tokenizer, "tokenizer"):
tokenizers.append(("t5_tokenizer.tokenizer", "tokenizer_2"))
if hasattr(model, "qwen_tokenizer") and hasattr(model.qwen_tokenizer, "tokenizer"):
tokenizers.append(("qwen_tokenizer.tokenizer", "tokenizer"))
if hasattr(model, "fibo_tokenizer") and hasattr(model.fibo_tokenizer, "tokenizer"):
tokenizers.append(("fibo_tokenizer.tokenizer", "tokenizer"))
return tokenizers
@staticmethod
def _detect_components(model: Any) -> list[tuple[str, str]]:
components = []
if hasattr(model, "vae"):
components.append(("vae", "vae"))
if hasattr(model, "transformer"):
components.append(("transformer", "transformer"))
if hasattr(model, "clip_text_encoder"):
components.append(("clip_text_encoder", "text_encoder"))
if hasattr(model, "t5_text_encoder"):
components.append(("t5_text_encoder", "text_encoder_2"))
if hasattr(model, "text_encoder"):
# Only add if we haven't already added clip_text_encoder or t5_text_encoder
if not any(c[1] == "text_encoder" for c in components):
components.append(("text_encoder", "text_encoder"))
return components
@staticmethod
def _get_nested_attr(obj: Any, attr_path: str) -> Any:
attrs = attr_path.split(".")
result = obj
for attr in attrs:
if not hasattr(result, attr):
return None
result = getattr(result, attr)
return result
@staticmethod
def _save_tokenizer(base_path: str, tokenizer: PreTrainedTokenizer, subdir: str) -> None:
path = Path(base_path) / subdir
path.mkdir(parents=True, exist_ok=True)
tokenizer.save_pretrained(path)
@staticmethod
def _save_weights(base_path: str, bits: int, model: nn.Module, subdir: str) -> None:
path = Path(base_path) / subdir
path.mkdir(parents=True, exist_ok=True)
weights = ModelSaver._split_weights(base_path, dict(tree_flatten(model.parameters())))
for i, weight in enumerate(weights):
mx.save_safetensors(
str(path / f"{i}.safetensors"),
weight,
{
"quantization_level": str(bits),
"mflux_version": VersionUtil.get_mflux_version(),
},
)
@staticmethod
def _split_weights(base_path: str, weights: dict, max_file_size_gb: int = 2) -> list:
max_file_size_bytes = max_file_size_gb << 30
shards = []
shard, shard_size = {}, 0
for k, v in weights.items():
if shard_size + v.nbytes > max_file_size_bytes:
shards.append(shard)
shard, shard_size = {}, 0
shard[k] = v
shard_size += v.nbytes
shards.append(shard)
return shards

View File

@ -9,7 +9,7 @@ from PIL import Image
from mflux.models.depth_pro.depth_pro_initializer import DepthProInitializer
from mflux.models.depth_pro.model.depth_pro_model import DepthProModel
from mflux.models.depth_pro.model.depth_pro_util import DepthProUtil
from mflux.post_processing.image_util import ImageUtil
from mflux.utils.image_util import ImageUtil
@dataclass

View File

@ -0,0 +1,47 @@
from mflux.config.model_config import ModelConfig
from mflux.models.fibo.model.fibo_text_encoder import SmolLM3_3B_TextEncoder
from mflux.models.fibo.model.fibo_transformer import FiboTransformer
from mflux.models.fibo.model.fibo_vae.wan_2_2_vae import Wan2_2_VAE
from mflux.models.fibo.tokenizer import FiboTokenizerHandler
from mflux.models.fibo.weights.fibo_weight_handler import FIBOWeightHandler
from mflux.models.fibo.weights.fibo_weight_util import FIBOWeightUtil
class FIBOInitializer:
@staticmethod
def init(
fibo_model,
model_config: ModelConfig | None = None,
quantize: int | None = None,
local_path: str | None = None,
) -> None:
# 1. Load VAE weights
weights = FIBOWeightHandler.load_regular_weights(
repo_id=model_config.model_name,
local_path=local_path,
)
# 2. Initialize tokenizers
tokenizer_handler = FiboTokenizerHandler(
repo_id=model_config.model_name,
local_path=local_path,
)
fibo_model.fibo_tokenizer = tokenizer_handler.fibo
# 3. Initialize all models
fibo_model.vae = Wan2_2_VAE()
fibo_model.text_encoder = SmolLM3_3B_TextEncoder()
fibo_model.transformer = FiboTransformer(
in_channels=48,
num_layers=8,
num_single_layers=38,
)
# 4. Apply weights and quantize VAE, transformer, and text encoder
fibo_model.bits = FIBOWeightUtil.set_weights_and_quantize(
quantize_arg=quantize,
weights=weights,
vae=fibo_model.vae,
transformer=fibo_model.transformer,
text_encoder=fibo_model.text_encoder,
)

View File

@ -0,0 +1,19 @@
import mlx.core as mx
class FiboLatentCreator:
@staticmethod
def create_noise(seed: int, height: int, width: int) -> mx.array:
latents = mx.random.normal(
shape=(1, 48, (height // 16), (width // 16)),
key=mx.random.key(seed),
)
return FiboLatentCreator.pack_latents(latents, height, width)
@staticmethod
def pack_latents(latents: mx.array, height: int, width: int) -> mx.array:
if latents.ndim == 5:
latents = latents[:, :, 0, :, :]
batch_size, channels, latent_height, latent_width = latents.shape
latents = mx.transpose(latents, (0, 2, 3, 1))
return mx.reshape(latents, (batch_size, latent_height * latent_width, channels))

View File

@ -0,0 +1 @@
"""FIBO model components."""

View File

@ -0,0 +1,7 @@
from .prompt_encoder import PromptEncoder
from .smol_lm3_3b_text_encoder import SmolLM3_3B_TextEncoder
__all__ = [
"PromptEncoder",
"SmolLM3_3B_TextEncoder",
]

View File

@ -0,0 +1,166 @@
import json
from typing import List, Union
import mlx.core as mx
from mflux.models.fibo.model.fibo_text_encoder.smol_lm3_3b_text_encoder import SmolLM3_3B_TextEncoder
from mflux.models.fibo.tokenizer.fibo_tokenizer import TokenizerFibo
class PromptEncoder:
@staticmethod
def encode_prompt(
prompt: str,
negative_prompt: str | None,
tokenizer: TokenizerFibo,
text_encoder: SmolLM3_3B_TextEncoder,
) -> tuple[str, mx.array, List[mx.array]]:
# 0. Set default negative prompt if not provided
if negative_prompt is None or negative_prompt == "":
negative_prompt = "ugly, blurry, low quality"
# 1. Convert prompt to JSON format
json.loads(prompt)
json_prompt = prompt
# 2. Get prompt embeddings for positive and negative prompt
prompt_embeds, prompt_layers, prompt_attention_mask = PromptEncoder._get_prompt_embeds(
prompt=json_prompt,
tokenizer=tokenizer,
text_encoder=text_encoder,
num_images_per_prompt=1,
max_sequence_length=2048,
tokenization_prefix="positive",
)
neg_prompt_embeds, neg_prompt_layers, neg_prompt_attention_mask = PromptEncoder._get_prompt_embeds(
prompt=negative_prompt,
tokenizer=tokenizer,
text_encoder=text_encoder,
num_images_per_prompt=1,
max_sequence_length=2048,
tokenization_prefix="negative",
)
encoder_hidden_states, max_tokens = PromptEncoder._get_encoder_hidden_states(
prompt_embeds=prompt_embeds,
neg_prompt_embeds=neg_prompt_embeds,
prompt_attention_mask=prompt_attention_mask,
neg_prompt_attention_mask=neg_prompt_attention_mask,
)
prompt_layers = PromptEncoder._get_prompt_layers(
max_tokens=max_tokens,
prompt_layers=prompt_layers,
neg_prompt_layers=neg_prompt_layers,
)
return json_prompt, encoder_hidden_states, prompt_layers
@staticmethod
def _get_encoder_hidden_states(neg_prompt_attention_mask, neg_prompt_embeds, prompt_attention_mask, prompt_embeds):
max_tokens = max(neg_prompt_embeds.shape[1], prompt_embeds.shape[1])
prompt_embeds, prompt_attention_mask = PromptEncoder._pad_embedding(
max_tokens=max_tokens,
prompt_embeds=prompt_embeds,
attention_mask=prompt_attention_mask,
)
neg_prompt_embeds, neg_prompt_attention_mask = PromptEncoder._pad_embedding(
max_tokens=max_tokens,
prompt_embeds=neg_prompt_embeds,
attention_mask=neg_prompt_attention_mask,
)
encoder_hidden_states = mx.concatenate([neg_prompt_embeds, prompt_embeds], axis=0)
return encoder_hidden_states, max_tokens
@staticmethod
def _get_prompt_layers(max_tokens, neg_prompt_layers, prompt_layers):
prompt_layers = [PromptEncoder._pad_embedding(layer, max_tokens)[0] for layer in prompt_layers]
neg_prompt_layers = [PromptEncoder._pad_embedding(layer, max_tokens)[0] for layer in neg_prompt_layers]
prompt_layers = [mx.concatenate([neg_prompt_layers[i], prompt_layers[i]], axis=0) for i in range(len(prompt_layers))] # fmt: off
total_num_layers_transformer = 46 # FIBO transformer has 8 joint + 38 single blocks = 46 total layers
if len(prompt_layers) >= total_num_layers_transformer:
prompt_layers = prompt_layers[len(prompt_layers) - total_num_layers_transformer :]
else:
prompt_layers = prompt_layers + [prompt_layers[-1]] * (total_num_layers_transformer - len(prompt_layers))
return prompt_layers
@staticmethod
def _get_prompt_embeds(
prompt: Union[str, List[str]],
text_encoder: SmolLM3_3B_TextEncoder,
tokenizer: TokenizerFibo,
num_images_per_prompt: int = 1,
max_sequence_length: int = 2048,
tokenization_prefix: str | None = None,
) -> tuple[mx.array, List[mx.array], mx.array]:
prompts = [prompt] if isinstance(prompt, str) else list(prompt)
if not prompts:
raise ValueError("`prompt` must be a non-empty string or list of strings.")
# 1) Tokenize and convert to MX
input_ids_mx, attention_mask_mx = tokenizer.tokenize(
prompts=prompts,
max_length=max_sequence_length,
padding="longest",
truncation=True,
add_special_tokens=True,
)
# 2) Run MLX text encoder and collect hidden states
hidden_states_list = text_encoder(
input_ids=input_ids_mx,
attention_mask=attention_mask_mx,
output_hidden_states=True,
)
# 3) Build prompt_embeds from last two layers
last_hidden = hidden_states_list[-1]
second_last_hidden = hidden_states_list[-2]
prompt_embeds = mx.concatenate(
[last_hidden, second_last_hidden],
axis=-1,
)
# 4) Repeat along batch dimension for num_images_per_prompt
prompt_embeds = PromptEncoder._repeat_interleave_batch(prompt_embeds, num_images_per_prompt)
attention_mask = PromptEncoder._repeat_interleave_batch(attention_mask_mx, num_images_per_prompt)
prompt_layers = [PromptEncoder._repeat_interleave_batch(layer, num_images_per_prompt) for layer in hidden_states_list] # fmt: off
return prompt_embeds, prompt_layers, attention_mask
@staticmethod
def _pad_embedding(
prompt_embeds: mx.array,
max_tokens: int,
attention_mask: mx.array | None = None,
) -> tuple[mx.array, mx.array]:
batch_size, seq_len, dim = prompt_embeds.shape
if attention_mask is None:
attention_mask = mx.ones((batch_size, seq_len), dtype=prompt_embeds.dtype)
else:
attention_mask = attention_mask.astype(prompt_embeds.dtype)
if max_tokens < seq_len:
raise ValueError("`max_tokens` must be >= current sequence length.")
if max_tokens > seq_len:
pad_length = max_tokens - seq_len
padding = mx.zeros((batch_size, pad_length, dim), dtype=prompt_embeds.dtype)
prompt_embeds = mx.concatenate([prompt_embeds, padding], axis=1)
mask_padding = mx.zeros((batch_size, pad_length), dtype=attention_mask.dtype)
attention_mask = mx.concatenate([attention_mask, mask_padding], axis=1)
return prompt_embeds, attention_mask
@staticmethod
def _repeat_interleave_batch(
tensor: mx.array,
num_images_per_prompt: int,
) -> mx.array:
if num_images_per_prompt == 1:
return tensor
batch, *rest = tensor.shape
tensor = mx.expand_dims(tensor, axis=1)
tensor = mx.broadcast_to(tensor, (batch, num_images_per_prompt, *rest))
new_shape = (batch * num_images_per_prompt, *rest)
return mx.reshape(tensor, new_shape)

View File

@ -0,0 +1,133 @@
import math
from typing import Tuple
import mlx.core as mx
from mlx import nn
from mlx.core.fast import scaled_dot_product_attention
from .smol_lm3_3b_rope import SmolLM3_3B_RotaryEmbedding
class SmolLM3_3B_SelfAttention(nn.Module):
def __init__(
self,
hidden_size: int,
num_attention_heads: int,
num_key_value_heads: int,
max_position_embeddings: int = 65_536,
rope_theta: float = 5_000_000.0,
attention_dropout: float = 0.0,
):
super().__init__()
self.hidden_size = hidden_size
self.num_attention_heads = num_attention_heads
self.num_key_value_heads = num_key_value_heads
self.head_dim = hidden_size // num_attention_heads
self.num_key_value_groups = num_attention_heads // num_key_value_heads
self.scale = 1.0 / math.sqrt(self.head_dim)
self.attention_dropout = attention_dropout
# Projections no bias, matching SmolLM3 config (attention_bias = False)
self.q_proj = nn.Linear(hidden_size, num_attention_heads * self.head_dim, bias=False)
self.k_proj = nn.Linear(hidden_size, num_key_value_heads * self.head_dim, bias=False)
self.v_proj = nn.Linear(hidden_size, num_key_value_heads * self.head_dim, bias=False)
self.o_proj = nn.Linear(num_attention_heads * self.head_dim, hidden_size, bias=False)
self.rotary_emb = SmolLM3_3B_RotaryEmbedding(
dim=self.head_dim,
max_position_embeddings=max_position_embeddings,
base=rope_theta,
)
def __call__(
self,
hidden_states: mx.array,
attention_mask: mx.array | None = None,
cos_sin: Tuple[mx.array, mx.array] | None = None,
) -> mx.array:
batch_size, seq_len, _ = hidden_states.shape
# Projections: Use nn.Linear directly (like Qwen/Flux models)
q = self.q_proj(hidden_states)
k = self.k_proj(hidden_states)
v = self.v_proj(hidden_states)
# Cast back to input dtype if Linear output is float32 (for consistency)
if q.dtype != hidden_states.dtype:
q = q.astype(hidden_states.dtype)
k = k.astype(hidden_states.dtype)
v = v.astype(hidden_states.dtype)
# Force evaluation to ensure computation is complete
mx.eval(q, k, v)
# Reshape to (batch, heads, seq, head_dim)
q = q.reshape(batch_size, seq_len, self.num_attention_heads, self.head_dim).transpose(0, 2, 1, 3)
k = k.reshape(batch_size, seq_len, self.num_key_value_heads, self.head_dim).transpose(0, 2, 1, 3)
v = v.reshape(batch_size, seq_len, self.num_key_value_heads, self.head_dim).transpose(0, 2, 1, 3)
# Rotary embeddings
if cos_sin is None:
cos_sin = self.rotary_emb(seq_len)
cos, sin = cos_sin
q, k = SmolLM3_3B_SelfAttention._apply_rope(q, k, cos, sin)
# Grouped-query attention: repeat kv heads if needed
if self.num_key_value_heads != self.num_attention_heads:
k = SmolLM3_3B_SelfAttention._repeat_kv(k, self.num_key_value_groups)
v = SmolLM3_3B_SelfAttention._repeat_kv(v, self.num_key_value_groups)
# Prepare mask for scaled_dot_product_attention
attn_mask = None
if attention_mask is not None:
seq_len_k = k.shape[2]
causal_mask = attention_mask[:, :, :, :seq_len_k]
if causal_mask.shape[1] == 1:
causal_mask = mx.broadcast_to(causal_mask, (batch_size, self.num_attention_heads, seq_len, seq_len_k))
attn_mask = causal_mask.astype(q.dtype)
# Use MLX fast SDPA
attn_output = scaled_dot_product_attention(
q,
k,
v,
scale=self.scale,
mask=attn_mask,
)
attn_output = attn_output.transpose(0, 2, 1, 3).reshape(batch_size, seq_len, self.hidden_size)
attn_output = self.o_proj(attn_output)
return attn_output
@staticmethod
def _rotate_half(x: mx.array) -> mx.array:
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return mx.concatenate([-x2, x1], axis=-1)
@staticmethod
def _apply_rope(
q: mx.array,
k: mx.array,
cos: mx.array,
sin: mx.array,
) -> Tuple[mx.array, mx.array]:
q_dtype = q.dtype
k_dtype = k.dtype
q = q.astype(mx.float32)
k = k.astype(mx.float32)
cos = cos.astype(mx.float32)
sin = sin.astype(mx.float32)
q_embed = (q * cos) + (SmolLM3_3B_SelfAttention._rotate_half(q) * sin)
k_embed = (k * cos) + (SmolLM3_3B_SelfAttention._rotate_half(k) * sin)
return q_embed.astype(q_dtype), k_embed.astype(k_dtype)
@staticmethod
def _repeat_kv(hidden_states: mx.array, n_rep: int) -> mx.array:
batch, num_kv_heads, seq_len, head_dim = hidden_states.shape
hidden_states = mx.expand_dims(hidden_states, axis=2)
hidden_states = mx.broadcast_to(hidden_states, (batch, num_kv_heads, n_rep, seq_len, head_dim))
return hidden_states.reshape(batch, num_kv_heads * n_rep, seq_len, head_dim)

View File

@ -0,0 +1,64 @@
from typing import Tuple
import mlx.core as mx
from mlx import nn
from .smol_lm3_3b_attention import SmolLM3_3B_SelfAttention
from .smol_lm3_3b_mlp import SmolLM3_3B_MLP
from .smol_lm3_3b_rms_norm import SmolLM3_3B_RMSNorm
class SmolLM3_3B_EncoderLayer(nn.Module):
def __init__(
self,
hidden_size: int = 2048,
num_attention_heads: int = 16,
num_key_value_heads: int = 4,
intermediate_size: int = 11_008,
rms_norm_eps: float = 1e-6,
max_position_embeddings: int = 65_536,
rope_theta: float = 5_000_000.0,
attention_dropout: float = 0.0,
hidden_act: str = "silu",
):
super().__init__()
self.input_layernorm = SmolLM3_3B_RMSNorm(hidden_size, eps=rms_norm_eps)
self.self_attn = SmolLM3_3B_SelfAttention(
hidden_size=hidden_size,
num_attention_heads=num_attention_heads,
num_key_value_heads=num_key_value_heads,
max_position_embeddings=max_position_embeddings,
rope_theta=rope_theta,
attention_dropout=attention_dropout,
)
self.post_attention_layernorm = SmolLM3_3B_RMSNorm(hidden_size, eps=rms_norm_eps)
self.mlp = SmolLM3_3B_MLP(
hidden_size=hidden_size,
intermediate_size=intermediate_size,
hidden_act=hidden_act,
)
def __call__(
self,
hidden_states: mx.array,
attention_mask: mx.array | None,
cos_sin: Tuple[mx.array, mx.array],
layer_idx: int | None = None,
) -> mx.array:
# Self-attention block
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
hidden_states = self.self_attn(
hidden_states=hidden_states,
attention_mask=attention_mask,
cos_sin=cos_sin,
)
hidden_states = residual + hidden_states
# Feed-forward block
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states
return hidden_states

View File

@ -0,0 +1,30 @@
import mlx.core as mx
from mlx import nn
class SmolLM3_3B_MLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str = "silu",
):
super().__init__()
self.hidden_size = hidden_size
self.intermediate_size = intermediate_size
self.hidden_act = hidden_act
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
def _activation(self, x: mx.array) -> mx.array:
if self.hidden_act == "silu":
return x * mx.sigmoid(x)
return x * mx.sigmoid(x)
def __call__(self, hidden_states: mx.array) -> mx.array:
gate = self._activation(self.gate_proj(hidden_states))
up = self.up_proj(hidden_states)
hidden = gate * up
hidden = self.down_proj(hidden)
return hidden

View File

@ -0,0 +1,17 @@
import mlx.core as mx
from mlx import nn
class SmolLM3_3B_RMSNorm(nn.Module):
def __init__(self, hidden_size: int, eps: float = 1e-6):
super().__init__()
self.weight = mx.ones((hidden_size,))
self.eps = eps
def __call__(self, hidden_states: mx.array) -> mx.array:
input_dtype = hidden_states.dtype
hidden_states = hidden_states.astype(mx.float32)
variance = mx.mean(mx.square(hidden_states), axis=-1, keepdims=True)
hidden_states = hidden_states * mx.rsqrt(variance + self.eps)
result = self.weight.astype(mx.float32) * hidden_states
return result.astype(input_dtype)

View File

@ -0,0 +1,31 @@
from typing import Tuple
import mlx.core as mx
from mlx import nn
class SmolLM3_3B_RotaryEmbedding(nn.Module):
def __init__(
self,
dim: int,
max_position_embeddings: int = 65_536,
base: float = 5_000_000.0,
):
super().__init__()
self.inv_freq = 1.0 / (base ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim))
self.max_position_embeddings = max_position_embeddings
def __call__(self, seq_len: int) -> Tuple[mx.array, mx.array]:
cos, sin = SmolLM3_3B_RotaryEmbedding._build_cos_sin(self.inv_freq, seq_len)
cos = mx.expand_dims(mx.expand_dims(cos, axis=0), axis=0)
sin = mx.expand_dims(mx.expand_dims(sin, axis=0), axis=0)
return cos, sin
@staticmethod
def _build_cos_sin(inv_freq, seq_len: int) -> Tuple[mx.array, mx.array]:
positions = mx.arange(seq_len, dtype=mx.float32)
freqs = mx.outer(positions, inv_freq)
emb = mx.concatenate([freqs, freqs], axis=-1)
cos = mx.cos(emb)
sin = mx.sin(emb)
return cos, sin

View File

@ -0,0 +1,105 @@
from typing import List
import mlx.core as mx
from mlx import nn
from .smol_lm3_3b_encoder_layer import SmolLM3_3B_EncoderLayer
from .smol_lm3_3b_rms_norm import SmolLM3_3B_RMSNorm
from .smol_lm3_3b_rope import SmolLM3_3B_RotaryEmbedding
class SmolLM3_3B_TextEncoder(nn.Module):
def __init__(
self,
vocab_size: int = 128_256,
hidden_size: int = 2048,
intermediate_size: int = 11_008,
num_hidden_layers: int = 36,
num_attention_heads: int = 16,
num_key_value_heads: int = 4,
max_position_embeddings: int = 65_536,
rope_theta: float = 5_000_000.0,
rms_norm_eps: float = 1e-6,
hidden_act: str = "silu",
):
super().__init__()
self.vocab_size = vocab_size
self.hidden_size = hidden_size
self.intermediate_size = intermediate_size
self.num_hidden_layers = num_hidden_layers
self.num_attention_heads = num_attention_heads
self.num_key_value_heads = num_key_value_heads
self.max_position_embeddings = max_position_embeddings
self.rope_theta = rope_theta
self.rms_norm_eps = rms_norm_eps
self.hidden_act = hidden_act
self.embed_tokens = nn.Embedding(vocab_size, hidden_size)
self.layers: List[SmolLM3_3B_EncoderLayer] = [
SmolLM3_3B_EncoderLayer(
hidden_size=hidden_size,
num_attention_heads=num_attention_heads,
num_key_value_heads=num_key_value_heads,
intermediate_size=intermediate_size,
rms_norm_eps=rms_norm_eps,
max_position_embeddings=max_position_embeddings,
rope_theta=rope_theta,
hidden_act=hidden_act,
)
for _ in range(num_hidden_layers)
]
self.norm = SmolLM3_3B_RMSNorm(hidden_size, eps=rms_norm_eps)
self.rotary_emb = SmolLM3_3B_RotaryEmbedding(
dim=hidden_size // num_attention_heads,
max_position_embeddings=max_position_embeddings,
base=rope_theta,
)
def __call__(
self,
input_ids: mx.array,
attention_mask: mx.array,
output_hidden_states: bool = True,
) -> List[mx.array] | mx.array:
batch_size, seq_len = input_ids.shape
hidden_states = self.embed_tokens(input_ids)
attention_mask_4d = SmolLM3_3B_TextEncoder._build_attention_mask(attention_mask)
cos, sin = self.rotary_emb(seq_len)
hidden_states_list: List[mx.array] = [hidden_states]
for layer_idx, layer in enumerate(self.layers):
hidden_states = layer(
hidden_states=hidden_states,
attention_mask=attention_mask_4d,
cos_sin=(cos, sin),
layer_idx=layer_idx,
)
if output_hidden_states:
hidden_states_list.append(hidden_states)
hidden_states = self.norm(hidden_states)
if output_hidden_states:
hidden_states_list[-1] = hidden_states
return hidden_states_list
return hidden_states
@staticmethod
def _build_attention_mask(attention_mask: mx.array) -> mx.array:
batch_size, seq_len = attention_mask.shape
mask_dtype = mx.float32
min_dtype_value = mx.finfo(mask_dtype).min
padding_mask = mx.where(
attention_mask == 1,
mx.zeros_like(attention_mask).astype(mask_dtype),
mx.ones_like(attention_mask).astype(mask_dtype) * min_dtype_value,
)
padding_mask = mx.expand_dims(mx.expand_dims(padding_mask, axis=1), axis=1)
idx = mx.arange(seq_len, dtype=mx.int32)
j = mx.expand_dims(idx, axis=0)
i = mx.expand_dims(idx, axis=1)
tri_bool = j > i
zeros_2d = mx.zeros((seq_len, seq_len), dtype=mask_dtype)
minval_2d = mx.ones((seq_len, seq_len), dtype=mask_dtype) * min_dtype_value
causal_tri_mask = mx.where(tri_bool, minval_2d, zeros_2d)
causal_tri_mask = mx.expand_dims(mx.expand_dims(causal_tri_mask, axis=0), axis=0)
causal_tri_mask = mx.broadcast_to(causal_tri_mask, (batch_size, 1, seq_len, seq_len))
attention_mask_4d = causal_tri_mask + padding_mask
return attention_mask_4d

View File

@ -0,0 +1,3 @@
from .transformer import FiboTransformer
__all__ = ["FiboTransformer"]

View File

@ -0,0 +1,38 @@
import math
import mlx.core as mx
from mlx import nn
class BriaFiboTimesteps(nn.Module):
def __init__(
self,
num_channels: int,
flip_sin_to_cos: bool,
downscale_freq_shift: float,
scale: float = 1.0,
time_theta: int = 10000,
):
super().__init__()
self.num_channels = num_channels
self.flip_sin_to_cos = flip_sin_to_cos
self.downscale_freq_shift = downscale_freq_shift
self.scale = scale
self.time_theta = time_theta
def __call__(self, timesteps: mx.array) -> mx.array:
half_dim = self.num_channels // 2
exponent = -math.log(self.time_theta) * mx.arange(0, half_dim, dtype=mx.float32)
exponent = exponent / (half_dim - self.downscale_freq_shift)
emb = mx.exp(exponent)
emb = mx.expand_dims(timesteps.astype(mx.float32), axis=-1) * mx.expand_dims(emb, axis=0)
emb = self.scale * emb
sin = mx.sin(emb)
cos = mx.cos(emb)
emb = mx.concatenate([sin, cos], axis=-1)
if self.flip_sin_to_cos:
emb = mx.concatenate([emb[:, half_dim:], emb[:, :half_dim]], axis=-1)
if self.num_channels % 2 == 1:
pad = mx.zeros((emb.shape[0], 1), dtype=emb.dtype)
emb = mx.concatenate([emb, pad], axis=-1)
return emb

View File

@ -0,0 +1,28 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_transformer.fibo_gelu import FiboGELU
class FiboFeedForward(nn.Module):
def __init__(
self,
dim: int,
dim_out: int | None = None,
mult: int = 4,
activation_fn: str = "gelu-approximate",
dropout: float = 0.0,
):
super().__init__()
inner_dim = int(dim * mult)
dim_out = dim if dim_out is None else dim_out
self.net: list[nn.Module] = [
FiboGELU(dim_in=dim, dim_out=inner_dim, approximate="tanh", bias=True),
nn.Dropout(dropout),
nn.Linear(inner_dim, dim_out, bias=True),
]
def __call__(self, hidden_states: mx.array) -> mx.array:
for module in self.net:
hidden_states = module(hidden_states)
return hidden_states

View File

@ -0,0 +1,31 @@
import mlx.core as mx
from mlx import nn
class FiboAdaLayerNormZero(nn.Module):
def __init__(self, embedding_dim: int, eps: float = 1e-6):
super().__init__()
self.embedding_dim = embedding_dim
self.eps = eps
self.linear = nn.Linear(embedding_dim, 6 * embedding_dim)
def __call__(self, hidden_states: mx.array, text_embeddings: mx.array):
emb = self.linear(nn.silu(text_embeddings))
chunk = self.embedding_dim
shift_msa = emb[:, 0 * chunk : 1 * chunk]
scale_msa = emb[:, 1 * chunk : 2 * chunk]
gate_msa = emb[:, 2 * chunk : 3 * chunk]
shift_mlp = emb[:, 3 * chunk : 4 * chunk]
scale_mlp = emb[:, 4 * chunk : 5 * chunk]
gate_mlp = emb[:, 5 * chunk : 6 * chunk]
norm_hidden_states = FiboAdaLayerNormZero._layer_norm(self.eps, hidden_states)
hidden_states = norm_hidden_states * (1 + scale_msa[:, None, :]) + shift_msa[:, None, :]
return hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp
@staticmethod
def _layer_norm(eps, x: mx.array) -> mx.array:
x_f32 = x.astype(mx.float32)
mean = mx.mean(x_f32, axis=-1, keepdims=True)
var = mx.mean((x_f32 - mean) ** 2, axis=-1, keepdims=True)
y = (x_f32 - mean) / mx.sqrt(var + eps)
return y.astype(x.dtype)

View File

@ -0,0 +1,50 @@
import mlx.core as mx
from mlx import nn
class FiboEmbedND(nn.Module):
def __init__(self, theta: int = 10000, axes_dim: list[int] | None = None):
super().__init__()
self.theta = theta
self.axes_dim = axes_dim or [16, 56, 56]
def __call__(self, ids: mx.array) -> tuple[mx.array, mx.array]:
if ids.ndim == 3 and ids.shape[0] == 1:
ids = ids[0]
n_axes = ids.shape[-1]
pos = ids.astype(mx.float32)
cos_out: list[mx.array] = []
sin_out: list[mx.array] = []
for i in range(n_axes):
axis_dim = self.axes_dim[i]
cos_axis, sin_axis = FiboEmbedND._get_1d_rotary_pos_embed(
dim=axis_dim,
pos=pos[:, i],
theta=self.theta,
)
cos_out.append(cos_axis)
sin_out.append(sin_axis)
freqs_cos = mx.concatenate(cos_out, axis=-1)
freqs_sin = mx.concatenate(sin_out, axis=-1)
return freqs_cos, freqs_sin
@staticmethod
def _get_1d_rotary_pos_embed(
dim: int,
pos: mx.array,
theta: float = 10000.0,
) -> tuple[mx.array, mx.array]:
if pos.ndim != 1:
pos = mx.reshape(pos, (-1,))
pos = pos.astype(mx.float32)
freqs = 1.0 / (theta ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim))
angles = pos[:, None] * freqs[None, :]
cos_base = mx.cos(angles)
sin_base = mx.sin(angles)
cos = mx.reshape(mx.stack([cos_base, cos_base], axis=-1), (pos.shape[0], -1))
sin = mx.reshape(mx.stack([sin_base, sin_base], axis=-1), (pos.shape[0], -1))
return cos, sin

View File

@ -0,0 +1,14 @@
import mlx.core as mx
from mlx import nn
class FiboGELU(nn.Module):
def __init__(self, dim_in: int, dim_out: int, approximate: str = "tanh", bias: bool = True):
super().__init__()
self.proj = nn.Linear(dim_in, dim_out, bias=bias)
self.approximate = approximate
def __call__(self, hidden_states: mx.array) -> mx.array:
hidden_states = self.proj(hidden_states)
hidden_states = nn.gelu_approx(hidden_states)
return hidden_states

View File

@ -0,0 +1,115 @@
import mlx.core as mx
from mlx import nn
from mlx.core.fast import scaled_dot_product_attention
from mflux.models.fibo.model.fibo_transformer.fibo_single_attention import FiboSingleAttention
class FiboJointAttention(nn.Module):
def __init__(self, dim: int, num_attention_heads: int, attention_head_dim: int, eps: float = 1e-6):
super().__init__()
self.head_dim = attention_head_dim
self.num_heads = num_attention_heads
self.inner_dim = dim
self.to_q = nn.Linear(dim, self.inner_dim)
self.to_k = nn.Linear(dim, self.inner_dim)
self.to_v = nn.Linear(dim, self.inner_dim)
self.norm_q = nn.RMSNorm(self.head_dim, eps=eps)
self.norm_k = nn.RMSNorm(self.head_dim, eps=eps)
# Added KV projections for encoder_hidden_states
self.add_q_proj = nn.Linear(dim, self.inner_dim)
self.add_k_proj = nn.Linear(dim, self.inner_dim)
self.add_v_proj = nn.Linear(dim, self.inner_dim)
self.norm_added_q = nn.RMSNorm(self.head_dim, eps=eps)
self.norm_added_k = nn.RMSNorm(self.head_dim, eps=eps)
# Output projections
self.to_out = [nn.Linear(self.inner_dim, dim)]
self.to_add_out = nn.Linear(self.inner_dim, dim)
def __call__(
self,
hidden_states: mx.array,
encoder_hidden_states: mx.array,
image_rotary_emb: tuple[mx.array, mx.array],
attention_mask: mx.array | None = None,
) -> tuple[mx.array, mx.array]:
batch_size, seq_img, dim = hidden_states.shape
_, seq_ctx, _ = encoder_hidden_states.shape
cos, sin = image_rotary_emb
# QKV for image stream: [B, S_img, inner_dim]
query = self.to_q(hidden_states)
key = self.to_k(hidden_states)
value = self.to_v(hidden_states)
# QKV for context stream (added_kv): [B, S_ctx, inner_dim]
enc_query = self.add_q_proj(encoder_hidden_states)
enc_key = self.add_k_proj(encoder_hidden_states)
enc_value = self.add_v_proj(encoder_hidden_states)
# Reshape to [B, S, H, D]
query = mx.reshape(query, (batch_size, seq_img, self.num_heads, self.head_dim))
key = mx.reshape(key, (batch_size, seq_img, self.num_heads, self.head_dim))
value = mx.reshape(value, (batch_size, seq_img, self.num_heads, self.head_dim))
enc_query = mx.reshape(enc_query, (batch_size, seq_ctx, self.num_heads, self.head_dim))
enc_key = mx.reshape(enc_key, (batch_size, seq_ctx, self.num_heads, self.head_dim))
enc_value = mx.reshape(enc_value, (batch_size, seq_ctx, self.num_heads, self.head_dim))
# RMSNorm over last dim: produce normalized Q/K matching BriaFiboAttention semantics.
query = self.norm_q(query.astype(mx.float32)).astype(query.dtype)
key = self.norm_k(key.astype(mx.float32)).astype(key.dtype)
enc_query = self.norm_added_q(enc_query.astype(mx.float32)).astype(enc_query.dtype)
enc_key = self.norm_added_k(enc_key.astype(mx.float32)).astype(enc_key.dtype)
# Concatenate encoder + image along sequence dim (matches BriaFiboAttnProcessor)
query = mx.concatenate([enc_query, query], axis=1)
key = mx.concatenate([enc_key, key], axis=1)
value = mx.concatenate([enc_value, value], axis=1)
seq_total = seq_ctx + seq_img
# Apply RoPE to Q,K (layout [B, S_total, H, D])
query = FiboSingleAttention.apply_rotary_emb(query, cos, sin)
key = FiboSingleAttention.apply_rotary_emb(key, cos, sin)
# Convert to [B, H, S, D] for fast SDPA
query_bhsd = mx.transpose(query, (0, 2, 1, 3))
key_bhsd = mx.transpose(key, (0, 2, 1, 3))
value_bhsd = mx.transpose(value, (0, 2, 1, 3))
# Prepare mask for scaled_dot_product_attention
attn_mask = None
if attention_mask is not None:
attn_mask = mx.broadcast_to(attention_mask, (batch_size, self.num_heads, seq_total, seq_total))
attn_mask = attn_mask.astype(query_bhsd.dtype)
scale = 1.0 / mx.sqrt(mx.array(self.head_dim, dtype=query_bhsd.dtype))
attn_output_bhsd = scaled_dot_product_attention(
query_bhsd,
key_bhsd,
value_bhsd,
scale=scale,
mask=attn_mask,
)
# Back to [B, S_total, H, D] then flatten to [B, S_total, inner_dim]
attn_output = mx.transpose(attn_output_bhsd, (0, 2, 1, 3))
attn_output = mx.reshape(attn_output, (batch_size, seq_total, self.inner_dim))
# Split back into context and image streams
context_attn_output = attn_output[:, :seq_ctx, :]
hidden_attn_output = attn_output[:, seq_ctx:, :]
# Output projections
hidden_attn_output = self.to_out[0](hidden_attn_output)
context_attn_output = self.to_add_out(context_attn_output)
return hidden_attn_output, context_attn_output

View File

@ -0,0 +1,80 @@
import mlx.core as mx
from mlx import nn
from mlx.core.fast import scaled_dot_product_attention
class FiboSingleAttention(nn.Module):
def __init__(self, dim: int, num_attention_heads: int, attention_head_dim: int, eps: float = 1e-6):
super().__init__()
self.head_dim = attention_head_dim
self.num_heads = num_attention_heads
self.inner_dim = dim
self.to_q = nn.Linear(dim, self.inner_dim)
self.to_k = nn.Linear(dim, self.inner_dim)
self.to_v = nn.Linear(dim, self.inner_dim)
self.norm_q = nn.RMSNorm(self.head_dim, eps=eps)
self.norm_k = nn.RMSNorm(self.head_dim, eps=eps)
def __call__(
self,
hidden_states: mx.array,
image_rotary_emb: tuple[mx.array, mx.array],
attention_mask: mx.array | None = None,
) -> mx.array:
batch_size, seq_len, _ = hidden_states.shape
cos, sin = image_rotary_emb
# [B, S, inner_dim]
query = self.to_q(hidden_states)
key = self.to_k(hidden_states)
value = self.to_v(hidden_states)
query = mx.reshape(query, (batch_size, seq_len, self.num_heads, self.head_dim))
key = mx.reshape(key, (batch_size, seq_len, self.num_heads, self.head_dim))
value = mx.reshape(value, (batch_size, seq_len, self.num_heads, self.head_dim))
# RMSNorm
query = self.norm_q(query.astype(mx.float32)).astype(query.dtype)
key = self.norm_k(key.astype(mx.float32)).astype(key.dtype)
# RoPE
query = FiboSingleAttention.apply_rotary_emb(query, cos, sin)
key = FiboSingleAttention.apply_rotary_emb(key, cos, sin)
# [B, H, S, D]
query_bhsd = mx.transpose(query, (0, 2, 1, 3))
key_bhsd = mx.transpose(key, (0, 2, 1, 3))
value_bhsd = mx.transpose(value, (0, 2, 1, 3))
scale = 1.0 / mx.sqrt(mx.array(self.head_dim, dtype=query_bhsd.dtype))
attn_output = scaled_dot_product_attention(
query_bhsd,
key_bhsd,
value_bhsd,
scale=scale,
mask=attention_mask,
)
attn_output = mx.transpose(attn_output, (0, 2, 1, 3))
attn_output = mx.reshape(attn_output, (batch_size, seq_len, self.inner_dim))
return attn_output
@staticmethod
def apply_rotary_emb(
x: mx.array,
freqs_cos: mx.array,
freqs_sin: mx.array,
) -> mx.array:
bsz, seq_len, num_heads, head_dim = x.shape
cos = mx.expand_dims(mx.expand_dims(freqs_cos, axis=0), axis=2)
sin = mx.expand_dims(mx.expand_dims(freqs_sin, axis=0), axis=2)
x2 = x.reshape(bsz, seq_len, num_heads, -1, 2)
x_real = x2[..., 0]
x_imag = x2[..., 1]
x_rotated_real = -x_imag
x_rotated_imag = x_real
x_rotated = mx.stack([x_rotated_real, x_rotated_imag], axis=-1).reshape(bsz, seq_len, num_heads, head_dim)
out = (x.astype(mx.float32) * cos + x_rotated.astype(mx.float32) * sin).astype(x.dtype)
return out

View File

@ -0,0 +1,76 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_transformer.feed_forward import FiboFeedForward
from mflux.models.fibo.model.fibo_transformer.fibo_ada_layer_norm_zero import FiboAdaLayerNormZero
from mflux.models.fibo.model.fibo_transformer.fibo_joint_attention import FiboJointAttention
class FiboJointTransformerBlock(nn.Module):
def __init__(
self,
layer: int,
dim: int = 3072,
num_attention_heads: int = 24,
attention_head_dim: int = 128,
eps: float = 1e-6,
):
super().__init__()
self.layer = layer
self.norm1 = FiboAdaLayerNormZero(embedding_dim=dim)
self.norm1_context = FiboAdaLayerNormZero(embedding_dim=dim)
self.attn = FiboJointAttention(dim=dim, num_attention_heads=num_attention_heads, attention_head_dim=attention_head_dim) # fmt: off
self.norm2 = nn.LayerNorm(dims=dim, eps=eps, affine=False)
self.ff = FiboFeedForward(dim=dim, dim_out=dim, mult=4, activation_fn="gelu-approximate")
self.norm2_context = nn.LayerNorm(dims=dim, eps=eps, affine=False)
self.ff_context = FiboFeedForward(dim=dim, dim_out=dim, mult=4, activation_fn="gelu-approximate")
def __call__(
self,
temb: mx.array,
hidden_states: mx.array,
encoder_hidden_states: mx.array,
image_rotary_emb: mx.array,
attention_mask: mx.array | None = None,
) -> tuple[mx.array, mx.array]:
# 1. AdaLayerNormZero for both streams
norm_hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.norm1(
hidden_states=hidden_states,
text_embeddings=temb,
)
norm_encoder_hidden_states, c_gate_msa, c_shift_mlp, c_scale_mlp, c_gate_mlp = self.norm1_context(
hidden_states=encoder_hidden_states,
text_embeddings=temb,
)
# 2. Joint attention over image + context streams.
attn_output, context_attn_output = self.attn(
hidden_states=norm_hidden_states,
encoder_hidden_states=norm_encoder_hidden_states,
image_rotary_emb=image_rotary_emb,
attention_mask=attention_mask,
)
# 3a. Process attention outputs for the image stream.
attn_output = mx.expand_dims(gate_msa, axis=1) * attn_output
hidden_states = hidden_states + attn_output
norm_hidden_states = self.norm2(hidden_states)
norm_hidden_states = norm_hidden_states * (1 + scale_mlp[:, None]) + shift_mlp[:, None]
ff_output = self.ff(norm_hidden_states)
ff_output = mx.expand_dims(gate_mlp, axis=1) * ff_output
hidden_states = hidden_states + ff_output
# 3b. Process attention outputs for the context stream.
context_attn_output = mx.expand_dims(c_gate_msa, axis=1) * context_attn_output
encoder_hidden_states = encoder_hidden_states + context_attn_output
norm_encoder_hidden_states = self.norm2_context(encoder_hidden_states)
norm_encoder_hidden_states = norm_encoder_hidden_states * (1 + c_scale_mlp[:, None]) + c_shift_mlp[:, None]
context_ff_output = self.ff_context(norm_encoder_hidden_states)
encoder_hidden_states = encoder_hidden_states + mx.expand_dims(c_gate_mlp, axis=1) * context_ff_output
return encoder_hidden_states, hidden_states

View File

@ -0,0 +1,55 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_transformer.fibo_single_attention import FiboSingleAttention
from mflux.models.flux.model.flux_transformer.ada_layer_norm_zero_single import AdaLayerNormZeroSingle
class FiboSingleTransformerBlock(nn.Module):
def __init__(
self,
layer: int,
dim: int = 3072,
num_attention_heads: int = 24,
attention_head_dim: int = 128,
mlp_ratio: float = 4.0,
):
super().__init__()
self.layer = layer
self.mlp_hidden_dim = int(dim * mlp_ratio)
self.norm = AdaLayerNormZeroSingle()
self.proj_mlp = nn.Linear(dim, self.mlp_hidden_dim)
self.act_mlp = nn.gelu_approx
self.proj_out = nn.Linear(dim + self.mlp_hidden_dim, dim)
self.attn = FiboSingleAttention(dim=dim, num_attention_heads=num_attention_heads, attention_head_dim=attention_head_dim) # fmt: off
def __call__(
self,
temb: mx.array,
hidden_states: mx.array,
image_rotary_emb: tuple[mx.array, mx.array],
attention_mask: mx.array | None = None,
) -> mx.array:
# 0. Residual connection
residual = hidden_states
# 1. AdaLayerNormZeroSingle
norm_hidden_states, gate = self.norm(
hidden_states=hidden_states,
text_embeddings=temb,
)
# 2. Attention
attn_output = self.attn(
hidden_states=norm_hidden_states,
image_rotary_emb=image_rotary_emb,
attention_mask=attention_mask,
)
# 3. MLP + projection
mlp_hidden_states = self.act_mlp(self.proj_mlp(norm_hidden_states))
hidden_states = mx.concatenate([attn_output, mlp_hidden_states], axis=2)
gate = mx.expand_dims(gate, axis=1)
hidden_states = gate * self.proj_out(hidden_states)
hidden_states = residual + hidden_states
return hidden_states

View File

@ -0,0 +1,10 @@
from mlx import nn
class BriaFiboTextProjection(nn.Module):
def __init__(self, in_features: int = 2048, hidden_size: int = 1536):
super().__init__()
self.linear = nn.Linear(in_features, hidden_size, bias=False)
def __call__(self, caption):
return self.linear(caption)

View File

@ -0,0 +1,17 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_transformer.bria_fibo_timesteps import BriaFiboTimesteps
from mflux.models.flux.model.flux_transformer.timestep_embedder import TimestepEmbedder
class BriaFiboTimestepProjEmbeddings(nn.Module):
def __init__(self, embedding_dim: int = 3072, time_theta: int = 10000):
super().__init__()
self.time_proj = BriaFiboTimesteps(num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0, time_theta=time_theta, scale=1.0) # fmt: off
self.timestep_embedder = TimestepEmbedder()
def __call__(self, timestep: mx.array, dtype) -> mx.array:
timesteps_proj = self.time_proj(timestep)
timesteps_emb = self.timestep_embedder(timesteps_proj.astype(dtype))
return timesteps_emb

View File

@ -0,0 +1,225 @@
import mlx.core as mx
from mlx import nn
from mflux.config.runtime_config import RuntimeConfig
from mflux.models.fibo.model.fibo_transformer.fibo_embed_nd import FiboEmbedND
from mflux.models.fibo.model.fibo_transformer.joint_transformer_block import FiboJointTransformerBlock
from mflux.models.fibo.model.fibo_transformer.single_transformer_block import FiboSingleTransformerBlock
from mflux.models.fibo.model.fibo_transformer.text_projection import BriaFiboTextProjection
from mflux.models.fibo.model.fibo_transformer.time_embed import BriaFiboTimestepProjEmbeddings
from mflux.models.flux.model.flux_transformer.ada_layer_norm_continuous import AdaLayerNormContinuous
class FiboTransformer(nn.Module):
def __init__(
self,
in_channels: int = 48,
num_layers: int = 8,
num_single_layers: int = 38,
):
super().__init__()
self.pos_embed = FiboEmbedND()
self.x_embedder = nn.Linear(in_channels, 3072)
self.time_embed = BriaFiboTimestepProjEmbeddings()
self.context_embedder = nn.Linear(4096, 3072)
self.transformer_blocks = [FiboJointTransformerBlock(i) for i in range(num_layers)]
self.single_transformer_blocks = [FiboSingleTransformerBlock(i) for i in range(num_single_layers)]
self.norm_out = AdaLayerNormContinuous(3072, 3072)
self.proj_out = nn.Linear(3072, in_channels)
self.caption_projection = [BriaFiboTextProjection() for _ in range(num_layers + num_single_layers)]
def __call__(
self,
t: int,
config: RuntimeConfig,
hidden_states: mx.array,
encoder_hidden_states: mx.array,
text_encoder_layers: list[mx.array],
) -> mx.array:
# 1. Create embeddings
hidden_states = FiboTransformer._handle_classifier_free_guidance(hidden_states, encoder_hidden_states)
hidden_states = self.x_embedder(hidden_states)
encoder_hidden_states = self.context_embedder(encoder_hidden_states)
time_embeddings = FiboTransformer._compute_time_embeddings(t, config, hidden_states.shape[0], hidden_states.dtype, self.time_embed) # fmt: off
image_rotary_emb = FiboTransformer._compute_rotary_embeddings(encoder_hidden_states, self.pos_embed, config, hidden_states.dtype) # fmt: off
# 2. Compute attention mask
attention_mask = FiboTransformer._compute_attention_mask(
config=config,
batch_size=hidden_states.shape[0],
encoder_hidden_states=encoder_hidden_states,
max_tokens=encoder_hidden_states.shape[1],
)
# 3. Project the fibo-specific text encoder layers
text_encoder_layers = [
self.caption_projection[i](text_layer)
for i, text_layer in enumerate(text_encoder_layers)
] # fmt: off
# 4. Run the joint transformer blocks
block_id = 0
for _, block in enumerate(self.transformer_blocks):
encoder_hidden_states, hidden_states = FiboTransformer._apply_joint_transformer_block(
block=block,
time_embeddings=time_embeddings,
hidden_states=hidden_states,
encoder_hidden_states=encoder_hidden_states,
text_encoder_layer=text_encoder_layers[block_id],
image_rotary_emb=image_rotary_emb,
attention_mask=attention_mask,
)
block_id += 1
# 5. Run the single transformer blocks
for _, block in enumerate(self.single_transformer_blocks):
encoder_hidden_states, hidden_states = FiboTransformer._apply_single_transformer_block(
block=block,
time_embeddings=time_embeddings,
hidden_states=hidden_states,
encoder_hidden_states=encoder_hidden_states,
text_encoder_layer=text_encoder_layers[block_id],
image_rotary_emb=image_rotary_emb,
attention_mask=attention_mask,
)
block_id += 1
# 6. Project the final output
hidden_states = self.norm_out(hidden_states, time_embeddings)
hidden_states = self.proj_out(hidden_states)
return hidden_states
@staticmethod
def _apply_joint_transformer_block(
time_embeddings: mx.array,
block: FiboJointTransformerBlock,
hidden_states: mx.array,
encoder_hidden_states: mx.array,
text_encoder_layer: mx.array,
image_rotary_emb: mx.array,
attention_mask: mx.array,
) -> tuple[mx.array, mx.array]:
encoder_hidden_states_half = encoder_hidden_states[:, :, :1536]
encoder_hidden_states = mx.concatenate([encoder_hidden_states_half, text_encoder_layer], axis=-1)
encoder_hidden_states, hidden_states = block(
temb=time_embeddings,
hidden_states=hidden_states,
encoder_hidden_states=encoder_hidden_states,
image_rotary_emb=image_rotary_emb,
attention_mask=attention_mask,
)
return encoder_hidden_states, hidden_states
@staticmethod
def _apply_single_transformer_block(
time_embeddings: mx.array,
block: FiboSingleTransformerBlock,
hidden_states: mx.array,
encoder_hidden_states: mx.array,
text_encoder_layer: mx.array,
image_rotary_emb: mx.array,
attention_mask: mx.array,
) -> tuple[mx.array, mx.array]:
encoder_hidden_states_half = encoder_hidden_states[:, :, :1536]
encoder_hidden_states = mx.concatenate([encoder_hidden_states_half, text_encoder_layer], axis=-1)
combined = mx.concatenate([encoder_hidden_states, hidden_states], axis=1)
combined = block(
temb=time_embeddings,
hidden_states=combined,
image_rotary_emb=image_rotary_emb,
attention_mask=attention_mask,
)
encoder_len = encoder_hidden_states.shape[1]
encoder_hidden_states = combined[:, :encoder_len, ...]
hidden_states = combined[:, encoder_len:, ...]
return encoder_hidden_states, hidden_states
@staticmethod
def _handle_classifier_free_guidance(hidden_states: mx.array, encoder_hidden_states: mx.array) -> mx.array:
batch_size = hidden_states.shape[0]
encoder_batch_size = encoder_hidden_states.shape[0]
if encoder_batch_size == batch_size * 2:
hidden_states = mx.concatenate([hidden_states, hidden_states], axis=0)
return hidden_states
@staticmethod
def _compute_time_embeddings(
t: int,
config: RuntimeConfig,
batch_size: int,
dtype: mx.Dtype,
time_embed: BriaFiboTimestepProjEmbeddings,
) -> mx.array:
timestep_value = config.scheduler.timesteps[t]
timestep = mx.full((batch_size,), timestep_value, dtype=dtype)
return time_embed(timestep, dtype=dtype)
@staticmethod
def _compute_rotary_embeddings(
encoder_hidden_states: mx.array,
pos_embed: FiboEmbedND,
config: RuntimeConfig,
dtype: mx.Dtype,
) -> mx.array:
max_tokens = encoder_hidden_states.shape[1]
txt_ids = mx.zeros((max_tokens, 3), dtype=dtype)
img_ids = FiboTransformer._prepare_latent_image_ids(height=config.height, width=config.width, dtype=dtype)
if txt_ids.ndim == 3 and txt_ids.shape[0] == 1:
txt_ids = txt_ids[0]
if img_ids.ndim == 3 and img_ids.shape[0] == 1:
img_ids = img_ids[0]
ids = mx.concatenate((txt_ids, img_ids), axis=0)
ids = mx.expand_dims(ids, axis=0)
return pos_embed(ids)
@staticmethod
def _prepare_latent_image_ids(height: int, width: int, dtype=mx.float32) -> mx.array:
vae_scale_factor = 16
latent_height = height // vae_scale_factor
latent_width = width // vae_scale_factor
row_indices = mx.arange(0, latent_height, dtype=dtype)[:, None]
row_indices = mx.broadcast_to(row_indices, (latent_height, latent_width))
col_indices = mx.arange(0, latent_width, dtype=dtype)[None, :]
col_indices = mx.broadcast_to(col_indices, (latent_height, latent_width))
zeros_channel = mx.zeros((latent_height, latent_width), dtype=dtype)
latent_image_ids = mx.stack([zeros_channel, row_indices, col_indices], axis=-1)
latent_image_ids = mx.reshape(latent_image_ids, (latent_height * latent_width, 3))
return latent_image_ids
@staticmethod
def _prepare_attention_mask(attention_mask_2d: mx.array) -> mx.array:
attention_matrix = mx.einsum("bi,bj->bij", attention_mask_2d, attention_mask_2d)
mask_dtype = attention_mask_2d.dtype
min_dtype_value = mx.finfo(mask_dtype).min
attention_matrix = mx.where(
attention_matrix == 1,
mx.zeros_like(attention_matrix).astype(mask_dtype),
(mx.ones_like(attention_matrix) * min_dtype_value).astype(mask_dtype),
)
attention_matrix = mx.expand_dims(attention_matrix, axis=1)
return attention_matrix
@staticmethod
def _compute_attention_mask(
batch_size: int,
config: RuntimeConfig,
encoder_hidden_states: mx.array,
max_tokens: int,
) -> mx.array:
vae_scale_factor = 16
latent_height = config.height // vae_scale_factor
latent_width = config.width // vae_scale_factor
latent_seq_len = latent_height * latent_width
prompt_attention_mask = mx.ones((batch_size, max_tokens), dtype=mx.float32)
latent_attention_mask = mx.ones((batch_size, latent_seq_len), dtype=mx.float32)
attention_mask_2d = mx.concatenate([prompt_attention_mask, latent_attention_mask], axis=1)
attention_mask = FiboTransformer._prepare_attention_mask(attention_mask_2d)
attention_mask = attention_mask.astype(encoder_hidden_states.dtype)
return attention_mask

View File

@ -0,0 +1 @@
"""FIBO VAE components."""

View File

@ -0,0 +1,17 @@
"""FIBO VAE common/shared components."""
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_attention_block import Wan2_2_AttentionBlock
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_causal_conv_3d import Wan2_2_CausalConv3d
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_mid_block import Wan2_2_MidBlock
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_resample import Wan2_2_Resample
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_residual_block import Wan2_2_ResidualBlock
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_rms_norm import Wan2_2_RMSNorm
__all__ = [
"Wan2_2_AttentionBlock",
"Wan2_2_CausalConv3d",
"Wan2_2_MidBlock",
"Wan2_2_ResidualBlock",
"Wan2_2_Resample",
"Wan2_2_RMSNorm",
]

View File

@ -0,0 +1,48 @@
import mlx.core as mx
from mlx import nn
from mlx.core.fast import scaled_dot_product_attention
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_rms_norm import Wan2_2_RMSNorm
class Wan2_2_AttentionBlock(nn.Module):
def __init__(self, dim: int):
super().__init__()
self.dim = dim
self.norm = Wan2_2_RMSNorm(dim, images=True)
self.to_qkv = nn.Conv2d(dim, dim * 3, kernel_size=1)
self.proj = nn.Conv2d(dim, dim, kernel_size=1)
def __call__(self, x: mx.array) -> mx.array:
identity = x
batch_size, channels, time, height, width = x.shape
x = mx.transpose(x, (0, 2, 1, 3, 4))
x = mx.reshape(x, (batch_size * time, channels, height, width))
x = self.norm(x)
x = mx.transpose(x, (0, 2, 3, 1))
qkv = self.to_qkv(x)
qkv = mx.transpose(qkv, (0, 3, 1, 2))
qkv = mx.reshape(qkv, (batch_size * time, 1, channels * 3, height * width))
qkv = mx.transpose(qkv, (0, 1, 3, 2))
q, k, v = mx.split(qkv, 3, axis=3)
scale = 1.0 / (channels**0.5)
x = scaled_dot_product_attention(
q,
k,
v,
scale=scale,
mask=None,
)
x = mx.reshape(x, (batch_size * time, height * width, channels))
x = mx.transpose(x, (0, 2, 1))
x = mx.reshape(x, (batch_size * time, channels, height, width))
x = mx.transpose(x, (0, 2, 3, 1))
x = self.proj(x)
x = mx.transpose(x, (0, 3, 1, 2))
x = mx.reshape(x, (batch_size, time, channels, height, width))
x = mx.transpose(x, (0, 2, 1, 3, 4))
return x + identity

View File

@ -0,0 +1,42 @@
import mlx.core as mx
from mlx import nn
class Wan2_2_CausalConv3d(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int = 3,
stride: int = 1,
padding: int = 1,
name: str | None = None,
):
super().__init__()
self.conv3d = nn.Conv3d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=0,
)
self.padding = padding
self.stride = stride
self.kernel_size = kernel_size
self.name = name or f"conv3d_{in_channels}to{out_channels}"
def __call__(self, x: mx.array) -> mx.array:
pad_t = pad_h = pad_w = self.padding
if pad_t > 0 or pad_h > 0 or pad_w > 0:
pad_spec = [
(0, 0),
(0, 0),
(2 * pad_t, 0),
(pad_h, pad_h),
(pad_w, pad_w),
]
x = mx.pad(x, pad_spec)
x = mx.transpose(x, (0, 2, 3, 4, 1))
x = self.conv3d(x)
x = mx.transpose(x, (0, 4, 1, 2, 3))
return x

View File

@ -0,0 +1,22 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_attention_block import Wan2_2_AttentionBlock
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_residual_block import Wan2_2_ResidualBlock
class Wan2_2_MidBlock(nn.Module):
def __init__(self, dim: int, non_linearity: str = "silu", num_layers: int = 1):
super().__init__()
self.resnets = [Wan2_2_ResidualBlock(dim, dim, non_linearity)]
self.attentions = []
for _ in range(num_layers):
self.attentions.append(Wan2_2_AttentionBlock(dim))
self.resnets.append(Wan2_2_ResidualBlock(dim, dim, non_linearity))
def __call__(self, x: mx.array) -> mx.array:
x = self.resnets[0](x)
for attn, resnet in zip(self.attentions, self.resnets[1:]):
x = attn(x)
x = resnet(x)
return x

View File

@ -0,0 +1,64 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_causal_conv_3d import Wan2_2_CausalConv3d
class Wan2_2_Resample(nn.Module):
def __init__(self, dim: int, mode: str, upsample_out_dim: int = None):
super().__init__()
self.dim = dim
self.mode = mode
if upsample_out_dim is None:
upsample_out_dim = dim // 2
if mode == "upsample3d":
self.time_conv = Wan2_2_CausalConv3d(dim, dim * 2, kernel_size=(3, 1, 1), stride=1, padding=(1, 0, 0))
self.resample_conv = nn.Conv2d(dim, upsample_out_dim, kernel_size=3, stride=1, padding=1)
elif mode == "upsample2d":
self.resample_conv = nn.Conv2d(dim, upsample_out_dim, kernel_size=3, stride=1, padding=1)
self.time_conv = None
elif mode == "downsample2d":
self.resample_conv = nn.Conv2d(dim, dim, kernel_size=3, stride=2, padding=0)
self.time_conv = None
elif mode == "downsample3d":
self.resample_conv = nn.Conv2d(dim, dim, kernel_size=3, stride=2, padding=0)
self.time_conv = None
else:
raise ValueError(f"Unsupported resample mode: {mode}")
def __call__(self, x: mx.array, block_idx: int | None = None) -> mx.array:
b, c, t, h, w = x.shape
if self.mode in ("upsample2d", "upsample3d"):
if self.mode == "upsample3d" and self.time_conv is not None:
x = self.time_conv(x)
x = mx.reshape(x, (b, 2, c, t, h, w))
x = mx.transpose(x, (0, 2, 3, 1, 4, 5))
x = mx.reshape(x, (b, c, t * 2, h, w))
t = t * 2
x = mx.transpose(x, (0, 2, 1, 3, 4))
x = mx.reshape(x, (b * t, c, h, w))
x = mx.transpose(x, (0, 2, 3, 1))
x = mx.repeat(x, 2, axis=1)
x = mx.repeat(x, 2, axis=2)
x = self.resample_conv(x)
x = mx.transpose(x, (0, 3, 1, 2))
new_c = x.shape[1]
new_h, new_w = x.shape[2], x.shape[3]
x = mx.reshape(x, (b, t, new_c, new_h, new_w))
x = mx.transpose(x, (0, 2, 1, 3, 4))
return x
# downsample modes
x = mx.transpose(x, (0, 2, 1, 3, 4))
x = mx.reshape(x, (b * t, c, h, w))
x = mx.transpose(x, (0, 2, 3, 1))
x = mx.pad(x, [(0, 0), (0, 1), (0, 1), (0, 0)])
x = self.resample_conv(x)
x = mx.transpose(x, (0, 3, 1, 2))
new_c = x.shape[1]
new_h, new_w = x.shape[2], x.shape[3]
x = mx.reshape(x, (b, t, new_c, new_h, new_w))
x = mx.transpose(x, (0, 2, 1, 3, 4))
return x

View File

@ -0,0 +1,35 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_causal_conv_3d import Wan2_2_CausalConv3d
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_rms_norm import Wan2_2_RMSNorm
class Wan2_2_ResidualBlock(nn.Module):
def __init__(
self,
in_dim: int,
out_dim: int,
non_linearity: str = "silu",
):
super().__init__()
self.norm1 = Wan2_2_RMSNorm(in_dim, images=False)
self.conv1 = Wan2_2_CausalConv3d(in_dim, out_dim, 3, padding=1)
self.norm2 = Wan2_2_RMSNorm(out_dim, images=False)
self.conv2 = Wan2_2_CausalConv3d(out_dim, out_dim, 3, padding=1)
if in_dim != out_dim:
self.conv_shortcut = Wan2_2_CausalConv3d(in_dim, out_dim, 1, padding=0)
else:
self.conv_shortcut = None
def __call__(self, x: mx.array, resnet_idx: int | None = None, block_idx: int | None = None) -> mx.array:
h = self.conv_shortcut(x) if self.conv_shortcut is not None else x
x = self.norm1(x)
x = nn.silu(x)
x = self.conv1(x)
x = self.norm2(x)
x = nn.silu(x)
x = self.conv2(x)
result = x + h
return result

View File

@ -0,0 +1,32 @@
import mlx.core as mx
from mlx import nn
class Wan2_2_RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-12, images: bool = True):
super().__init__()
self.eps = eps
self.scale = float(dim) ** 0.5
self.images = images
if images:
self.weight = mx.ones((dim, 1, 1))
else:
self.weight = mx.ones((dim, 1, 1, 1))
def __call__(self, x: mx.array) -> mx.array:
sum_sq = mx.sum(x * x, axis=1, keepdims=True)
l2_norm = mx.sqrt(sum_sq)
denom = mx.maximum(l2_norm, mx.array(self.eps, dtype=l2_norm.dtype))
x_normalized = x / denom
if x.ndim == 5 and not self.images:
weight = self.weight.reshape(1, -1, 1, 1, 1)
elif x.ndim == 4 and self.images:
weight = self.weight.reshape(1, -1, 1, 1)
else:
if x.ndim == 5:
weight = self.weight.reshape(1, -1, 1, 1, 1)
elif x.ndim == 4:
weight = self.weight.reshape(1, -1, 1, 1)
else:
weight = self.weight
return x_normalized * self.scale * weight

View File

@ -0,0 +1,5 @@
"""FIBO VAE decoder components."""
from mflux.models.fibo.model.fibo_vae.decoder.wan_2_2_decoder_3d import Wan2_2_Decoder3d
__all__ = ["Wan2_2_Decoder3d"]

View File

@ -0,0 +1,58 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_causal_conv_3d import Wan2_2_CausalConv3d
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_mid_block import Wan2_2_MidBlock
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_rms_norm import Wan2_2_RMSNorm
from mflux.models.fibo.model.fibo_vae.decoder.wan_2_2_residual_up_block import Wan2_2_ResidualUpBlock
class Wan2_2_Decoder3d(nn.Module):
def __init__(
self,
dim: int = 256,
z_dim: int = 48,
dim_mult: list[int] = [1, 2, 4, 4],
num_res_blocks: int = 2,
temporal_upsample: list[bool] | None = None,
non_linearity: str = "silu",
out_channels: int = 12,
):
super().__init__()
self.dim = dim
self.z_dim = z_dim
self.dim_mult = dim_mult
self.num_res_blocks = num_res_blocks
self.temporal_upsample = temporal_upsample or []
dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
self.conv_in = Wan2_2_CausalConv3d(z_dim, dims[0], 3, padding=1, name="decoder_conv_in")
self.mid_block = Wan2_2_MidBlock(dims[0], non_linearity, num_layers=1)
self.up_blocks: list[Wan2_2_ResidualUpBlock] = []
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
up_flag = i != len(dim_mult) - 1
temporal_flag = (
bool(self.temporal_upsample and i < len(self.temporal_upsample) and self.temporal_upsample[i])
if up_flag
else False
)
up_block = Wan2_2_ResidualUpBlock(
in_dim=in_dim,
out_dim=out_dim,
num_res_blocks=num_res_blocks,
temporal_upsample=temporal_flag,
up_flag=up_flag,
non_linearity=non_linearity,
)
self.up_blocks.append(up_block)
self.norm_out = Wan2_2_RMSNorm(out_dim, images=False)
self.conv_out = Wan2_2_CausalConv3d(out_dim, out_channels, 3, padding=1, name="decoder_conv_out")
def __call__(self, x: mx.array) -> mx.array:
x = self.conv_in(x)
x = self.mid_block(x)
for i, up_block in enumerate(self.up_blocks):
x = up_block(x, block_idx=i, first_chunk=True)
x = self.norm_out(x)
x = nn.silu(x)
x = self.conv_out(x)
return x

View File

@ -0,0 +1,53 @@
import mlx.core as mx
from mlx import nn
class Wan2_2_DupUp3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
factor_t: int,
factor_s: int = 1,
):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.factor_t = factor_t
self.factor_s = factor_s
self.factor = self.factor_t * self.factor_s * self.factor_s
self.repeats = out_channels * self.factor // in_channels
def __call__(self, x: mx.array, first_chunk: bool = False) -> mx.array:
b, c, t, h, w = x.shape
x = mx.repeat(x, self.repeats, axis=1)
x = mx.reshape(
x,
(
b,
self.out_channels,
self.factor_t,
self.factor_s,
self.factor_s,
t,
h,
w,
),
)
x = mx.transpose(x, (0, 1, 5, 2, 6, 3, 7, 4))
x = mx.reshape(
x,
(
b,
self.out_channels,
t * self.factor_t,
h * self.factor_s,
w * self.factor_s,
),
)
if first_chunk and self.factor_t > 1:
x = x[:, :, self.factor_t - 1 :, :, :]
return x

View File

@ -0,0 +1,60 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_resample import Wan2_2_Resample
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_residual_block import Wan2_2_ResidualBlock
from mflux.models.fibo.model.fibo_vae.decoder.wan_2_2_dup_up_3d import Wan2_2_DupUp3D
class Wan2_2_ResidualUpBlock(nn.Module):
def __init__(
self,
in_dim: int,
out_dim: int,
num_res_blocks: int,
temporal_upsample: bool = False,
up_flag: bool = False,
non_linearity: str = "silu",
):
super().__init__()
self.in_dim = in_dim
self.out_dim = out_dim
if up_flag:
self.avg_shortcut: Wan2_2_DupUp3D | None = Wan2_2_DupUp3D(
in_channels=in_dim,
out_channels=out_dim,
factor_t=2 if temporal_upsample else 1,
factor_s=2,
)
else:
self.avg_shortcut = None
resnets: list[Wan2_2_ResidualBlock] = []
current_dim = in_dim
for _ in range(num_res_blocks + 1):
resnets.append(Wan2_2_ResidualBlock(current_dim, out_dim, non_linearity))
current_dim = out_dim
self.resnets = resnets
if up_flag:
upsample_mode = "upsample3d" if temporal_upsample else "upsample2d"
self.upsampler: Wan2_2_Resample | None = Wan2_2_Resample(
out_dim, mode=upsample_mode, upsample_out_dim=out_dim
)
else:
self.upsampler = None
def __call__(self, x: mx.array, block_idx: int | None = None, first_chunk: bool = False) -> mx.array:
x_copy = x
for i, resnet in enumerate(self.resnets):
x = resnet(x, resnet_idx=i, block_idx=block_idx)
if self.upsampler is not None:
x = self.upsampler(x, block_idx=block_idx)
if self.avg_shortcut is not None:
x = x + self.avg_shortcut(x_copy, first_chunk=first_chunk)
return x

View File

@ -0,0 +1,34 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_resample import Wan2_2_Resample
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_residual_block import Wan2_2_ResidualBlock
class Wan2_2_UpBlock(nn.Module):
def __init__(
self,
in_dim: int,
out_dim: int,
num_res_blocks: int,
upsample_mode: str | None = None,
non_linearity: str = "silu",
):
super().__init__()
self.resnets: list[Wan2_2_ResidualBlock] = []
current_dim = in_dim
for _ in range(num_res_blocks + 1):
self.resnets.append(Wan2_2_ResidualBlock(current_dim, out_dim, non_linearity))
current_dim = out_dim
self.upsampler: Wan2_2_Resample | None = None
if upsample_mode is not None:
self.upsampler = Wan2_2_Resample(out_dim, mode=upsample_mode, upsample_out_dim=out_dim)
def __call__(self, x: mx.array, block_idx: int | None = None) -> mx.array:
for i, resnet in enumerate(self.resnets):
x = resnet(x, resnet_idx=i, block_idx=block_idx)
if self.upsampler is not None:
x = self.upsampler(x, block_idx=block_idx)
return x

View File

@ -0,0 +1,5 @@
"""FIBO VAE encoder components."""
from mflux.models.fibo.model.fibo_vae.encoder.wan_2_2_encoder_3d import Wan2_2_Encoder3d
__all__ = ["Wan2_2_Encoder3d"]

View File

@ -0,0 +1,75 @@
import mlx.core as mx
from mlx import nn
class Wan2_2_AvgDown3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
factor_t: int,
factor_s: int = 1,
):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.factor_t = factor_t
self.factor_s = factor_s
self.factor = self.factor_t * self.factor_s * self.factor_s
assert in_channels * self.factor % out_channels == 0
self.group_size = in_channels * self.factor // out_channels
def __call__(self, x: mx.array) -> mx.array:
pad_t = (self.factor_t - x.shape[2] % self.factor_t) % self.factor_t
if pad_t > 0:
x = mx.pad(
x,
[
(0, 0), # batch
(0, 0), # channels
(pad_t, 0), # time
(0, 0), # height
(0, 0), # width
],
)
b, c, t, h, w = x.shape
x = mx.reshape(
x,
(
b,
c,
t // self.factor_t,
self.factor_t,
h // self.factor_s,
self.factor_s,
w // self.factor_s,
self.factor_s,
),
)
x = mx.transpose(x, (0, 1, 3, 5, 7, 2, 4, 6))
x = mx.reshape(
x,
(
b,
c * self.factor,
t // self.factor_t,
h // self.factor_s,
w // self.factor_s,
),
)
x = mx.reshape(
x,
(
b,
self.out_channels,
self.group_size,
t // self.factor_t,
h // self.factor_s,
w // self.factor_s,
),
)
x = mx.mean(x, axis=2)
return x

View File

@ -0,0 +1,57 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_attention_block import Wan2_2_AttentionBlock
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_resample import Wan2_2_Resample
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_residual_block import Wan2_2_ResidualBlock
from mflux.models.fibo.model.fibo_vae.encoder.wan_2_2_avg_down_3d import Wan2_2_AvgDown3D
class Wan2_2_DownBlock(nn.Module):
def __init__(
self,
in_dim: int,
out_dim: int,
num_res_blocks: int,
attn_scales: list[float] | None = None,
scale: float = 1.0,
temporal_downsample: bool = False,
non_linearity: str = "silu",
is_last: bool = False,
):
super().__init__()
if attn_scales is None:
attn_scales = []
resnets: list[nn.Module] = []
current_dim = in_dim
for _ in range(num_res_blocks):
resnets.append(Wan2_2_ResidualBlock(current_dim, out_dim, non_linearity))
if scale in attn_scales:
resnets.append(Wan2_2_AttentionBlock(out_dim))
current_dim = out_dim
self.resnets = resnets
# Shortcut path with downsample (mirrors AvgDown3D in diffusers)
self.avg_shortcut = Wan2_2_AvgDown3D(
in_dim,
out_dim,
factor_t=2 if temporal_downsample else 1,
factor_s=2 if not is_last else 1,
)
# Main path downsampler
if not is_last:
mode = "downsample3d" if temporal_downsample else "downsample2d"
self.downsampler = Wan2_2_Resample(out_dim, mode=mode)
else:
self.downsampler = None
def __call__(self, x: mx.array) -> mx.array:
x_copy = x
for layer in self.resnets:
x = layer(x)
if self.downsampler is not None:
x = self.downsampler(x)
return x + self.avg_shortcut(x_copy)

View File

@ -0,0 +1,75 @@
import mlx.core as mx
from mlx import nn
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_causal_conv_3d import Wan2_2_CausalConv3d
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_mid_block import Wan2_2_MidBlock
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_rms_norm import Wan2_2_RMSNorm
from mflux.models.fibo.model.fibo_vae.encoder.wan_2_2_down_block import Wan2_2_DownBlock
class Wan2_2_Encoder3d(nn.Module):
def __init__(
self,
in_channels: int = 3,
dim: int = 128,
z_dim: int = 4,
dim_mult: list[int] | None = None,
num_res_blocks: int = 2,
attn_scales: list[float] | None = None,
temporal_downsample: list[bool] | None = None,
non_linearity: str = "silu",
is_residual: bool = False,
):
super().__init__()
if dim_mult is None:
dim_mult = [1, 2, 4, 4]
if attn_scales is None:
attn_scales = []
if temporal_downsample is None:
temporal_downsample = [False, True, True]
if is_residual:
raise NotImplementedError("Residual down blocks are not implemented for Wan2_2_Encoder3d in MLX.")
self.dim = dim
self.z_dim = z_dim
self.dim_mult = dim_mult
self.num_res_blocks = num_res_blocks
self.attn_scales = attn_scales
dims = [dim * u for u in [1] + dim_mult]
self.temporal_downsample = temporal_downsample
self.conv_in = Wan2_2_CausalConv3d(in_channels, dims[0], 3, padding=1)
scale = 1.0
self.down_blocks: list[Wan2_2_DownBlock] = []
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
block = Wan2_2_DownBlock(
in_dim=in_dim,
out_dim=out_dim,
num_res_blocks=num_res_blocks,
attn_scales=attn_scales,
scale=scale,
temporal_downsample=temporal_downsample[i] if i < len(temporal_downsample) else False,
non_linearity=non_linearity,
is_last=i == len(dim_mult) - 1,
)
self.down_blocks.append(block)
if i != len(dim_mult) - 1:
scale /= 2.0
self.mid_block = Wan2_2_MidBlock(out_dim, non_linearity, num_layers=1)
self.norm_out = Wan2_2_RMSNorm(out_dim, images=False)
self.conv_out = Wan2_2_CausalConv3d(out_dim, z_dim, 3, padding=1)
def __call__(self, x: mx.array) -> mx.array:
x = self.conv_in(x)
for block in self.down_blocks:
x = block(x)
x = self.mid_block(x)
x = self.norm_out(x)
x = nn.silu(x)
x = self.conv_out(x)
return x

View File

@ -0,0 +1,111 @@
import mlx.core as mx
import numpy as np
from mlx import nn
from mflux.models.fibo.model.fibo_vae.common.wan_2_2_causal_conv_3d import Wan2_2_CausalConv3d
from mflux.models.fibo.model.fibo_vae.decoder.wan_2_2_decoder_3d import Wan2_2_Decoder3d
from mflux.models.fibo.model.fibo_vae.encoder.wan_2_2_encoder_3d import Wan2_2_Encoder3d
class Wan2_2_VAE(nn.Module):
Z_DIM = 48
ENCODER_BASE_DIM = 160
DECODER_BASE_DIM = 256
DIM_MULT = [1, 2, 4, 4]
NUM_RES_BLOCKS = 2
OUT_CHANNELS = 12
LATENTS_MEAN = np.array([-0.2289, -0.0052, -0.1323, -0.2339, -0.2799, 0.0174, 0.1838, 0.1557, -0.1382, 0.0542, 0.2813, 0.0891, 0.157, -0.0098, 0.0375, -0.1825, -0.2246, -0.1207, -0.0698, 0.5109, 0.2665, -0.2108, -0.2158, 0.2502, -0.2055, -0.0322, 0.1109, 0.1567, -0.0729, 0.0899, -0.2799, -0.123, -0.0313, -0.1649, 0.0117, 0.0723, -0.2839, -0.2083, -0.052, 0.3748, 0.0152, 0.1957, 0.1433, -0.2944, 0.3573, -0.0548, -0.1681, -0.0667], dtype=np.float32) # fmt: off
LATENTS_STD = np.array([0.4765, 1.0364, 0.4514, 1.1677, 0.5313, 0.499, 0.4818, 0.5013, 0.8158, 1.0344, 0.5894, 1.0901, 0.6885, 0.6165, 0.8454, 0.4978, 0.5759, 0.3523, 0.7135, 0.6804, 0.5833, 1.4146, 0.8986, 0.5659, 0.7069, 0.5338, 0.4889, 0.4917, 0.4069, 0.4999, 0.6866, 0.4093, 0.5709, 0.6065, 0.6415, 0.4944, 0.5726, 1.2042, 0.5458, 1.6887, 0.3971, 1.06, 0.3943, 0.5537, 0.5444, 0.4089, 0.7468, 0.7744], dtype=np.float32) # fmt: off
def __init__(self):
super().__init__()
self.encoder = Wan2_2_Encoder3d(
in_channels=3,
dim=self.ENCODER_BASE_DIM,
z_dim=self.Z_DIM * 2,
dim_mult=self.DIM_MULT,
num_res_blocks=self.NUM_RES_BLOCKS,
attn_scales=[],
temporal_downsample=[False, True, True],
)
self.quant_conv = Wan2_2_CausalConv3d(self.Z_DIM * 2, self.Z_DIM * 2, 1, padding=0, name="quant_conv")
self.decoder = Wan2_2_Decoder3d(
dim=self.DECODER_BASE_DIM,
z_dim=self.Z_DIM,
dim_mult=self.DIM_MULT,
num_res_blocks=self.NUM_RES_BLOCKS,
temporal_upsample=None,
out_channels=self.OUT_CHANNELS,
)
self.post_quant_conv = Wan2_2_CausalConv3d(self.Z_DIM, self.Z_DIM, 1, padding=0, name="post_quant_conv")
def encode(self, images: mx.array) -> mx.array:
if images.ndim == 4:
x = images.reshape(images.shape[0], images.shape[1], 1, images.shape[2], images.shape[3])
elif images.ndim == 5:
x = images
else:
raise ValueError(f"Expected 4D or 5D input for VAE.encode, got shape {images.shape}")
patch_size = 2
x = self._patchify(x, patch_size=patch_size)
h = self.encoder(x)
h = self.quant_conv(h)
mean = h[:, : self.Z_DIM, :, :, :]
latents_mean = mx.array(self.LATENTS_MEAN).reshape(1, self.Z_DIM, 1, 1, 1)
latents_std = mx.array(self.LATENTS_STD).reshape(1, self.Z_DIM, 1, 1, 1)
encoded = (mean - latents_mean) / latents_std
return encoded
def decode(self, latents: mx.array) -> mx.array:
if latents.ndim == 4:
latents = latents.reshape(latents.shape[0], latents.shape[1], 1, latents.shape[2], latents.shape[3])
latents_mean = mx.array(self.LATENTS_MEAN).reshape(1, self.Z_DIM, 1, 1, 1)
latents_std = mx.array(self.LATENTS_STD).reshape(1, self.Z_DIM, 1, 1, 1)
latents = latents * latents_std + latents_mean
latents = self.post_quant_conv(latents)
decoded = self.decoder(latents)
patch_size = 2
decoded = self._unpatchify(decoded, patch_size=patch_size)
if decoded.shape[2] == 1:
decoded = decoded[:, :, 0, :, :]
return decoded
@staticmethod
def _patchify(x: mx.array, patch_size: int) -> mx.array:
if patch_size == 1:
return x
batch_size, channels, frames, height, width = x.shape
x = mx.reshape(
x,
(
batch_size,
channels,
frames,
height // patch_size,
patch_size,
width // patch_size,
patch_size,
),
)
x = mx.transpose(x, (0, 1, 6, 4, 2, 3, 5))
x = mx.reshape(
x,
(batch_size, channels * patch_size * patch_size, frames, height // patch_size, width // patch_size),
)
return x
@staticmethod
def _unpatchify(x: mx.array, patch_size: int) -> mx.array:
if patch_size == 1:
return x
batch_size, c_patches, frames, height, width = x.shape
channels = c_patches // (patch_size * patch_size)
x = mx.reshape(x, (batch_size, channels, patch_size, patch_size, frames, height, width))
x = mx.transpose(x, (0, 1, 4, 5, 3, 6, 2))
x = mx.reshape(x, (batch_size, channels, frames, height * patch_size, width * patch_size))
return x

View File

@ -0,0 +1,7 @@
from mflux.models.fibo.tokenizer.fibo_tokenizer import TokenizerFibo
from mflux.models.fibo.tokenizer.smol_lm3_3b_tokenizer import FiboTokenizerHandler
__all__ = [
"FiboTokenizerHandler",
"TokenizerFibo",
]

View File

@ -0,0 +1,37 @@
import mlx.core as mx
import numpy as np
from transformers import PreTrainedTokenizer
class TokenizerFibo:
def __init__(self, tokenizer: PreTrainedTokenizer, bot_token_id: int = 128000):
self.tokenizer = tokenizer
self.bot_token_id = bot_token_id
def tokenize(
self,
prompts: list[str],
max_length: int = 2048,
padding: str = "longest",
truncation: bool = True,
add_special_tokens: bool = True,
) -> tuple[mx.array, mx.array]:
prompts = [p if p is not None else "" for p in prompts]
if all(p == "" for p in prompts):
batch_size = len(prompts)
input_ids_mx = mx.array(np.empty((batch_size, 0), dtype=np.int32))
attention_mask_mx = mx.array(np.empty((batch_size, 0), dtype=np.int32))
else:
tokenized = self.tokenizer(
prompts,
padding=padding,
max_length=max_length,
truncation=truncation,
add_special_tokens=add_special_tokens,
return_tensors="mlx",
)
input_ids_mx = tokenized["input_ids"]
attention_mask_mx = tokenized["attention_mask"]
return input_ids_mx, attention_mask_mx

View File

@ -0,0 +1,14 @@
from mflux.models.qwen.tokenizer.qwen_image_processor import QwenImageProcessor
class Qwen2VLImageProcessor(QwenImageProcessor):
def __init__(self):
super().__init__(
min_pixels=256 * 28 * 28,
max_pixels=1024 * 28 * 28,
patch_size=16,
temporal_patch_size=2,
merge_size=2,
image_mean=[0.5, 0.5, 0.5],
image_std=[0.5, 0.5, 0.5],
)

View File

@ -0,0 +1,205 @@
from typing import Optional, Union
import numpy as np
from PIL import Image
from mflux.models.fibo.tokenizer.qwen2vl_image_processor import Qwen2VLImageProcessor
class Qwen2VLProcessor:
def __init__(self, tokenizer):
self.tokenizer = tokenizer
self.image_processor = Qwen2VLImageProcessor()
def apply_chat_template(
self,
messages,
tokenize: bool = True,
add_generation_prompt: bool = False,
return_tensors: Optional[str] = None,
return_dict: bool = True,
**kwargs,
):
formatted = self.tokenizer.apply_chat_template(
messages,
tokenize=tokenize,
add_generation_prompt=add_generation_prompt,
return_tensors=return_tensors,
return_dict=return_dict,
**kwargs,
)
if tokenize and return_dict:
if return_tensors == "pt":
import torch
if isinstance(formatted, dict):
result = {}
for key, value in formatted.items():
if isinstance(value, torch.Tensor):
result[key] = value.numpy()
else:
result[key] = value
return result
elif return_tensors == "np" or return_tensors is None:
if isinstance(formatted, dict):
result = {}
for key, value in formatted.items():
if hasattr(value, "numpy"):
result[key] = value.numpy()
elif isinstance(value, (list, tuple)):
result[key] = np.array(value)
else:
result[key] = value
return result
return formatted
def __call__(
self,
text: Optional[Union[str, list[str]]] = None,
images: Optional[Union[Image.Image, list[Image.Image]]] = None,
padding: bool = True,
return_tensors: Optional[str] = None,
**kwargs,
):
result = {}
if images is not None:
pixel_values, image_grid_thw = self.image_processor.preprocess(images)
result["pixel_values"] = pixel_values
result["image_grid_thw"] = image_grid_thw
if text is not None:
if not isinstance(text, list):
text = [text]
text_inputs = self.tokenizer(
text,
padding=padding,
return_tensors=return_tensors or "np",
**kwargs,
)
# If images are provided, replace <|image_pad|> tokens with actual image tokens
# The number of image tokens is calculated as: prod(image_grid_thw) // merge_size^2
if images is not None and "input_ids" in text_inputs:
image_token_id = 151655 # <|image_pad|> token ID
input_ids = text_inputs["input_ids"]
# Calculate number of image tokens per image based on image_grid_thw and merge_size
# This matches transformers behavior: num_tokens = prod(image_grid_thw) // merge_size^2
merge_size = self.image_processor.merge_size
merge_length = merge_size**2
num_images = len(images) if isinstance(images, list) else 1
# Get image_grid_thw from result (already computed above)
image_grid_thw = result["image_grid_thw"]
# Calculate tokens per image
if return_tensors == "pt":
import torch
if isinstance(image_grid_thw, torch.Tensor):
image_grid_thw_np = image_grid_thw.cpu().numpy()
else:
image_grid_thw_np = image_grid_thw
else:
if isinstance(image_grid_thw, np.ndarray):
image_grid_thw_np = image_grid_thw
else:
image_grid_thw_np = np.array(image_grid_thw)
# Calculate tokens for each image
num_image_tokens_per_image_list = []
for i in range(num_images):
if i < len(image_grid_thw_np):
grid = image_grid_thw_np[i]
num_tokens = int(np.prod(grid)) // merge_length
num_image_tokens_per_image_list.append(num_tokens)
else:
num_image_tokens_per_image_list.append(256) # fallback
# Replace image pad tokens with actual image tokens
if return_tensors == "pt":
import torch
new_input_ids_list = []
for seq in input_ids:
new_seq = []
image_idx = 0
for token_id in seq:
if token_id == image_token_id and image_idx < num_images:
# Replace with calculated number of image tokens for this image
num_tokens = (
num_image_tokens_per_image_list[image_idx]
if image_idx < len(num_image_tokens_per_image_list)
else 256
)
new_seq.extend([image_token_id] * num_tokens)
image_idx += 1
else:
new_seq.append(token_id)
new_input_ids_list.append(new_seq)
# Pad sequences to same length
max_len = max(len(seq) for seq in new_input_ids_list)
padded_input_ids = []
padded_attention_mask = []
for seq in new_input_ids_list:
pad_len = max_len - len(seq)
padded_seq = (
seq + [self.tokenizer.pad_token_id] * pad_len
if self.tokenizer.pad_token_id is not None
else seq + [0] * pad_len
)
padded_input_ids.append(padded_seq)
padded_attention_mask.append([1] * len(seq) + [0] * pad_len)
text_inputs["input_ids"] = torch.tensor(padded_input_ids)
if "attention_mask" in text_inputs:
text_inputs["attention_mask"] = torch.tensor(padded_attention_mask)
else:
# NumPy version
new_input_ids_list = []
for seq in input_ids:
new_seq = []
image_idx = 0
for token_id in seq:
if token_id == image_token_id and image_idx < num_images:
# Replace with calculated number of image tokens for this image
num_tokens = (
num_image_tokens_per_image_list[image_idx]
if image_idx < len(num_image_tokens_per_image_list)
else 256
)
new_seq.extend([image_token_id] * num_tokens)
image_idx += 1
else:
new_seq.append(token_id)
new_input_ids_list.append(new_seq)
# Pad sequences to same length
max_len = max(len(seq) for seq in new_input_ids_list)
padded_input_ids = []
padded_attention_mask = []
for seq in new_input_ids_list:
pad_len = max_len - len(seq)
padded_seq = (
seq
+ [self.tokenizer.pad_token_id if self.tokenizer.pad_token_id is not None else 0] * pad_len
)
padded_input_ids.append(padded_seq)
padded_attention_mask.append([1] * len(seq) + [0] * pad_len)
text_inputs["input_ids"] = np.array(padded_input_ids)
if "attention_mask" in text_inputs:
text_inputs["attention_mask"] = np.array(padded_attention_mask)
if return_tensors == "pt":
result["input_ids"] = text_inputs["input_ids"]
result["attention_mask"] = text_inputs.get("attention_mask")
else:
result["input_ids"] = np.array(text_inputs["input_ids"])
if "attention_mask" in text_inputs:
result["attention_mask"] = np.array(text_inputs["attention_mask"])
return result

View File

@ -0,0 +1,42 @@
from pathlib import Path
import transformers
from mflux.models.fibo.tokenizer.fibo_tokenizer import TokenizerFibo
from mflux.utils.download import snapshot_download
class FiboTokenizerHandler:
def __init__(
self,
repo_id: str,
bot_token_id: int = 128000,
local_path: str | None = None,
):
root_path = Path(local_path) if local_path else FiboTokenizerHandler._download_or_get_cached_tokenizer(repo_id)
# Try different possible tokenizer paths
tokenizer_path = root_path / "tokenizer"
if not tokenizer_path.exists():
tokenizer_path = root_path / "text_encoder"
if not tokenizer_path.exists():
tokenizer_path = root_path
# Load the raw tokenizer
self.tokenizer_raw = transformers.AutoTokenizer.from_pretrained(
pretrained_model_name_or_path=str(tokenizer_path),
local_files_only=True,
fix_mistral_regex=True, # Fix Mistral regex pattern warning
)
# Wrap in our TokenizerFibo class
self.fibo = TokenizerFibo(self.tokenizer_raw, bot_token_id=bot_token_id)
@staticmethod
def _download_or_get_cached_tokenizer(repo_id: str) -> Path:
return Path(
snapshot_download(
repo_id=repo_id,
allow_patterns=["tokenizer/**", "text_encoder/**"],
)
)

View File

@ -0,0 +1 @@
"""FIBO model variants."""

View File

@ -0,0 +1 @@
"""FIBO text-to-image variant."""

View File

@ -0,0 +1,187 @@
import mlx.core as mx
from mlx import nn
from tqdm import tqdm
from mflux.callbacks.callbacks import Callbacks
from mflux.config.config import Config
from mflux.config.model_config import ModelConfig
from mflux.config.runtime_config import RuntimeConfig
from mflux.models.common.latent_creator.latent_creator import Img2Img, LatentCreator
from mflux.models.common.weights.model_saver import ModelSaver
from mflux.models.fibo.fibo_initializer import FIBOInitializer
from mflux.models.fibo.latent_creator.fibo_latent_creator import FiboLatentCreator
from mflux.models.fibo.model.fibo_text_encoder.prompt_encoder import PromptEncoder
from mflux.models.fibo.model.fibo_text_encoder.smol_lm3_3b_text_encoder import SmolLM3_3B_TextEncoder
from mflux.models.fibo.model.fibo_transformer import FiboTransformer
from mflux.models.fibo.model.fibo_vae.wan_2_2_vae import Wan2_2_VAE
from mflux.models.fibo.tokenizer.fibo_tokenizer import TokenizerFibo
from mflux.utils.exceptions import StopImageGenerationException
from mflux.utils.generated_image import GeneratedImage
from mflux.utils.image_util import ImageUtil
class FIBO(nn.Module):
vae: Wan2_2_VAE
transformer: FiboTransformer
text_encoder: SmolLM3_3B_TextEncoder
fibo_tokenizer: TokenizerFibo
def __init__(
self,
model_config: ModelConfig,
quantize: int | None = None,
local_path: str | None = None,
):
super().__init__()
self.model_config = model_config
self.bits = quantize
self.local_path = local_path
FIBOInitializer.init(
fibo_model=self,
model_config=model_config,
quantize=quantize,
local_path=local_path,
)
def generate_image(
self,
seed: int,
prompt: str,
config: Config,
negative_prompt: str | None = None,
) -> GeneratedImage:
# 0. Create a new runtime config based on the model type and input parameters
runtime_config = RuntimeConfig(config, self.model_config)
time_steps = tqdm(range(runtime_config.init_time_step, runtime_config.num_inference_steps))
# 1. Create the initial latents
latents = LatentCreator.create_for_txt2img_or_img2img(
seed=seed,
height=runtime_config.height,
width=runtime_config.width,
img2img=Img2Img(
vae=self.vae,
latent_creator=FiboLatentCreator,
image_path=runtime_config.image_path,
sigmas=runtime_config.scheduler.sigmas,
init_time_step=runtime_config.init_time_step,
),
)
# 2. Encode the prompt
json_prompt, encoder_hidden_states, text_encoder_layers = PromptEncoder.encode_prompt(
prompt=prompt,
negative_prompt=negative_prompt,
tokenizer=self.fibo_tokenizer,
text_encoder=self.text_encoder,
)
# (Optional) Call subscribers for beginning of loop
Callbacks.before_loop(
seed=seed,
prompt=json_prompt,
latents=latents,
config=runtime_config,
)
for t in time_steps:
try:
# 3.t Predict the noise
noise_pred = self.transformer(
t=t,
config=runtime_config,
hidden_states=latents,
encoder_hidden_states=encoder_hidden_states,
text_encoder_layers=text_encoder_layers,
)
noise_pred = FIBO._apply_classifier_free_guidance(noise_pred, runtime_config.guidance)
# 4.t Take one denoise step
latents = runtime_config.scheduler.step(
model_output=noise_pred,
timestep=t,
sample=latents,
)
# (Optional) Call subscribers in-loop
Callbacks.in_loop(
t=t,
seed=seed,
prompt=json_prompt,
latents=latents,
config=runtime_config,
time_steps=time_steps,
)
# (Optional) Evaluate to enable progress tracking
mx.eval(latents)
except KeyboardInterrupt: # noqa: PERF203
Callbacks.interruption(
t=t,
seed=seed,
prompt=json_prompt,
latents=latents,
config=runtime_config,
time_steps=time_steps,
)
raise StopImageGenerationException(
f"Stopping image generation at step {t + 1}/{runtime_config.num_inference_steps}"
)
# (Optional) Call subscribers after loop
Callbacks.after_loop(
seed=seed,
prompt=json_prompt,
latents=latents,
config=runtime_config,
)
# 5. Decode the latent array and return the image
latents = FIBO._unpack_latents(latents, runtime_config.height, runtime_config.width)
decoded = self.vae.decode(latents)
return ImageUtil.to_image(
decoded_latents=decoded,
config=runtime_config,
seed=seed,
prompt=json_prompt,
quantization=self.bits,
lora_paths=None,
lora_scales=None,
image_path=runtime_config.image_path,
image_strength=runtime_config.image_strength,
generation_time=time_steps.format_dict["elapsed"],
)
@staticmethod
def _apply_classifier_free_guidance(noise_pred: mx.array, guidance: float) -> mx.array:
half = noise_pred.shape[0] // 2
noise_uncond = noise_pred[:half]
noise_text = noise_pred[half:]
return noise_uncond + guidance * (noise_text - noise_uncond)
@staticmethod
def _unpack_latents(latents: mx.array, height: int, width: int) -> mx.array:
batch_size, seq_len, channels = latents.shape
vae_scale_factor = 16
latent_height = height // vae_scale_factor
latent_width = width // vae_scale_factor
latents = mx.reshape(latents, (batch_size, latent_height, latent_width, channels))
latents = mx.transpose(latents, (0, 3, 1, 2))
return latents
def save_model(self, base_path: str) -> None:
ModelSaver.save_model(
model=self,
bits=self.bits,
base_path=base_path,
tokenizers=[
("fibo_tokenizer.tokenizer", "tokenizer"),
],
components=[
("vae", "vae"),
("transformer", "transformer"),
("text_encoder", "text_encoder"),
],
)

View File

@ -0,0 +1,7 @@
"""FIBO weight handling."""
from mflux.models.fibo.weights.fibo_weight_handler import FIBOWeightHandler
from mflux.models.fibo.weights.fibo_weight_mapping import FIBOWeightMapping
from mflux.models.fibo.weights.fibo_weight_util import FIBOWeightUtil
__all__ = ["FIBOWeightHandler", "FIBOWeightMapping", "FIBOWeightUtil"]

View File

@ -0,0 +1,179 @@
from pathlib import Path
import mlx.core as mx
import torch
from mlx.utils import tree_unflatten
from safetensors.torch import load_file as torch_load_file
from mflux.models.common.weights.mapping.weight_mapper import WeightMapper
from mflux.models.fibo.weights.fibo_weight_mapping import FIBOWeightMapping
from mflux.models.flux.weights.weight_handler import (
MetaData,
WeightHandler as FluxWeightHandler,
)
class FIBOWeightHandler:
def __init__(
self,
meta_data: MetaData,
vae: dict | None = None,
transformer: dict | None = None,
text_encoder: dict | None = None,
decoder: dict | None = None,
visual: dict | None = None,
config: dict | None = None,
):
self.vae = vae
self.transformer = transformer
self.text_encoder = text_encoder
self.decoder = decoder
self.visual = visual
self.config = config
self.meta_data = meta_data
@staticmethod
def load_regular_weights(
repo_id: str | None = None,
local_path: str | None = None,
) -> "FIBOWeightHandler":
root_path: Path | None = None
if local_path:
root_path = Path(local_path)
elif repo_id:
root_path = FluxWeightHandler.download_or_get_cached_weights(repo_id)
vae_weights = None
transformer_weights = None
text_encoder_weights = None
quantization_level: int | None = None
mflux_version: str | None = None
if root_path is not None:
vae_weights, _, _ = FIBOWeightHandler._try_load_saved_component(root_path, "vae")
text_encoder_weights, _, _ = FIBOWeightHandler._try_load_saved_component(root_path, "text_encoder") # fmt: off
transformer_weights, quantization_level, mflux_version = FIBOWeightHandler._try_load_saved_component(root_path, "transformer") # fmt: off
if vae_weights is None or transformer_weights is None or text_encoder_weights is None:
vae_weights = FIBOWeightHandler._load_vae_weights(repo_id, local_path)
transformer_weights = FIBOWeightHandler._load_transformer_weights(repo_id, local_path)
text_encoder_weights = FIBOWeightHandler._load_text_encoder_weights(repo_id, local_path)
quantization_level = None
mflux_version = None
return FIBOWeightHandler(
vae=vae_weights,
transformer=transformer_weights,
text_encoder=text_encoder_weights,
meta_data=MetaData(
quantization_level=quantization_level,
scale=None,
is_lora=False,
mflux_version=mflux_version,
),
)
@staticmethod
def _load_vae_weights(
repo_id: str | None = None,
local_path: str | None = None,
) -> dict:
root_path = FIBOWeightHandler._get_root_path(repo_id, local_path)
vae_path = root_path / "vae"
raw_weights = FIBOWeightHandler._load_safetensors_shards(vae_path)
mapping = FIBOWeightMapping.get_vae_mapping()
mapped_weights = WeightMapper.apply_mapping(raw_weights, mapping, num_blocks=4)
return mapped_weights
@staticmethod
def _load_transformer_weights(
repo_id: str | None = None,
local_path: str | None = None,
) -> dict:
root_path = FIBOWeightHandler._get_root_path(repo_id, local_path)
transformer_path = root_path / "transformer"
if transformer_path.exists() and list(transformer_path.glob("*.safetensors")):
raw_weights = FIBOWeightHandler._load_safetensors_shards(transformer_path)
else:
raw_weights = FIBOWeightHandler._load_safetensors_shards(root_path)
mapping = FIBOWeightMapping.get_transformer_mapping()
mapped_weights = WeightMapper.apply_mapping(
raw_weights,
mapping,
num_blocks=38,
num_layers=46,
)
return mapped_weights
@staticmethod
def _load_text_encoder_weights(
repo_id: str | None = None,
local_path: str | None = None,
) -> dict:
root_path = FIBOWeightHandler._get_root_path(repo_id, local_path)
text_encoder_path = root_path / "text_encoder"
raw_weights = FIBOWeightHandler._load_safetensors_shards(text_encoder_path)
mapping = FIBOWeightMapping.get_text_encoder_mapping()
mapped_weights = WeightMapper.apply_mapping(
raw_weights,
mapping,
num_blocks=36,
)
return mapped_weights
@staticmethod
def _get_root_path(
repo_id: str | None = None,
local_path: str | None = None,
) -> Path:
if local_path:
return Path(local_path)
return Path(FluxWeightHandler.download_or_get_cached_weights(repo_id or "briaai/FIBO"))
@staticmethod
def _load_safetensors_shards(path: Path) -> dict[str, mx.array]:
shard_files = sorted(f for f in path.glob("*.safetensors") if not f.name.startswith("._"))
if not shard_files:
raise FileNotFoundError(f"No safetensors files found in {path}")
all_weights: dict[str, mx.array] = {}
for shard in shard_files:
torch_weights = torch_load_file(str(shard))
for key, tensor in torch_weights.items():
if tensor.dtype == torch.bfloat16:
tensor = tensor.to(torch.float16)
all_weights[key] = mx.array(tensor.numpy())
return all_weights
@staticmethod
def _try_load_saved_component(
root_path: Path,
component_name: str,
) -> tuple[dict | None, int | None, str | None]:
component_path = root_path / component_name
if not component_path.exists():
return None, None, None
shard_files = sorted(f for f in component_path.glob("*.safetensors") if not f.name.startswith("._"))
if not shard_files:
return None, None, None
all_weights: dict[str, mx.array] = {}
quantization_level: int | None = None
mflux_version: str | None = None
for idx, shard in enumerate(shard_files):
data = mx.load(str(shard), return_metadata=True)
weights_dict = data[0]
all_weights.update(dict(weights_dict.items()))
if idx == 0 and len(data) > 1:
quantization_level = data[1].get("quantization_level")
mflux_version = data[1].get("mflux_version")
if quantization_level is None and mflux_version is None:
return None, None, None
unflattened = tree_unflatten(list(all_weights.items()))
return unflattened, quantization_level, mflux_version

View File

@ -0,0 +1,635 @@
from typing import List
from mflux.models.common.weights.mapping.weight_mapping import WeightMapping, WeightTarget
from mflux.models.qwen.weights.qwen_weight_mapping import (
reshape_gamma_to_1d,
transpose_conv2d_weight,
transpose_conv3d_weight,
)
class FIBOWeightMapping(WeightMapping):
@staticmethod
def get_transformer_mapping() -> List[WeightTarget]:
return [
# ========== Global projections ==========
WeightTarget(
mlx_path="time_embed.timestep_embedder.linear_1.weight",
hf_patterns=["time_embed.timestep_embedder.linear_1.weight"],
),
WeightTarget(
mlx_path="time_embed.timestep_embedder.linear_1.bias",
hf_patterns=["time_embed.timestep_embedder.linear_1.bias"],
),
WeightTarget(
mlx_path="time_embed.timestep_embedder.linear_2.weight",
hf_patterns=["time_embed.timestep_embedder.linear_2.weight"],
),
WeightTarget(
mlx_path="time_embed.timestep_embedder.linear_2.bias",
hf_patterns=["time_embed.timestep_embedder.linear_2.bias"],
),
WeightTarget(
mlx_path="context_embedder.weight",
hf_patterns=["context_embedder.weight"],
),
WeightTarget(
mlx_path="context_embedder.bias",
hf_patterns=["context_embedder.bias"],
),
WeightTarget(
mlx_path="x_embedder.weight",
hf_patterns=["x_embedder.weight"],
),
WeightTarget(
mlx_path="x_embedder.bias",
hf_patterns=["x_embedder.bias"],
),
# ========== Joint transformer blocks ==========
# AdaLayerNormZero (image + context streams)
WeightTarget(
mlx_path="transformer_blocks.{block}.norm1.linear.weight",
hf_patterns=["transformer_blocks.{block}.norm1.linear.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.norm1.linear.bias",
hf_patterns=["transformer_blocks.{block}.norm1.linear.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.norm1_context.linear.weight",
hf_patterns=["transformer_blocks.{block}.norm1_context.linear.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.norm1_context.linear.bias",
hf_patterns=["transformer_blocks.{block}.norm1_context.linear.bias"],
),
# Attention weights (BriaFiboAttention)
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.norm_q.weight",
hf_patterns=["transformer_blocks.{block}.attn.norm_q.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.norm_k.weight",
hf_patterns=["transformer_blocks.{block}.attn.norm_k.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_q.weight",
hf_patterns=["transformer_blocks.{block}.attn.to_q.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_q.bias",
hf_patterns=["transformer_blocks.{block}.attn.to_q.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_k.weight",
hf_patterns=["transformer_blocks.{block}.attn.to_k.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_k.bias",
hf_patterns=["transformer_blocks.{block}.attn.to_k.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_v.weight",
hf_patterns=["transformer_blocks.{block}.attn.to_v.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_v.bias",
hf_patterns=["transformer_blocks.{block}.attn.to_v.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_out.0.weight",
hf_patterns=["transformer_blocks.{block}.attn.to_out.0.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_out.0.bias",
hf_patterns=["transformer_blocks.{block}.attn.to_out.0.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.norm_added_q.weight",
hf_patterns=["transformer_blocks.{block}.attn.norm_added_q.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.norm_added_k.weight",
hf_patterns=["transformer_blocks.{block}.attn.norm_added_k.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.add_q_proj.weight",
hf_patterns=["transformer_blocks.{block}.attn.add_q_proj.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.add_q_proj.bias",
hf_patterns=["transformer_blocks.{block}.attn.add_q_proj.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.add_k_proj.weight",
hf_patterns=["transformer_blocks.{block}.attn.add_k_proj.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.add_k_proj.bias",
hf_patterns=["transformer_blocks.{block}.attn.add_k_proj.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.add_v_proj.weight",
hf_patterns=["transformer_blocks.{block}.attn.add_v_proj.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.add_v_proj.bias",
hf_patterns=["transformer_blocks.{block}.attn.add_v_proj.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_add_out.weight",
hf_patterns=["transformer_blocks.{block}.attn.to_add_out.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.attn.to_add_out.bias",
hf_patterns=["transformer_blocks.{block}.attn.to_add_out.bias"],
),
# LayerNorm / FFN for image stream
WeightTarget(
mlx_path="transformer_blocks.{block}.norm2.weight",
hf_patterns=["transformer_blocks.{block}.norm2.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.norm2.bias",
hf_patterns=["transformer_blocks.{block}.norm2.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.ff.net.0.proj.weight",
hf_patterns=["transformer_blocks.{block}.ff.net.0.proj.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.ff.net.0.proj.bias",
hf_patterns=["transformer_blocks.{block}.ff.net.0.proj.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.ff.net.2.weight",
hf_patterns=["transformer_blocks.{block}.ff.net.2.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.ff.net.2.bias",
hf_patterns=["transformer_blocks.{block}.ff.net.2.bias"],
),
# LayerNorm / FFN for context stream
WeightTarget(
mlx_path="transformer_blocks.{block}.norm2_context.weight",
hf_patterns=["transformer_blocks.{block}.norm2_context.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.norm2_context.bias",
hf_patterns=["transformer_blocks.{block}.norm2_context.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.ff_context.net.0.proj.weight",
hf_patterns=["transformer_blocks.{block}.ff_context.net.0.proj.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.ff_context.net.0.proj.bias",
hf_patterns=["transformer_blocks.{block}.ff_context.net.0.proj.bias"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.ff_context.net.2.weight",
hf_patterns=["transformer_blocks.{block}.ff_context.net.2.weight"],
),
WeightTarget(
mlx_path="transformer_blocks.{block}.ff_context.net.2.bias",
hf_patterns=["transformer_blocks.{block}.ff_context.net.2.bias"],
),
# ========== Single transformer blocks ==========
WeightTarget(
mlx_path="single_transformer_blocks.{block}.norm.linear.weight",
hf_patterns=["single_transformer_blocks.{block}.norm.linear.weight"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.norm.linear.bias",
hf_patterns=["single_transformer_blocks.{block}.norm.linear.bias"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.attn.norm_q.weight",
hf_patterns=["single_transformer_blocks.{block}.attn.norm_q.weight"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.attn.norm_k.weight",
hf_patterns=["single_transformer_blocks.{block}.attn.norm_k.weight"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.attn.to_q.weight",
hf_patterns=["single_transformer_blocks.{block}.attn.to_q.weight"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.attn.to_q.bias",
hf_patterns=["single_transformer_blocks.{block}.attn.to_q.bias"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.attn.to_k.weight",
hf_patterns=["single_transformer_blocks.{block}.attn.to_k.weight"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.attn.to_k.bias",
hf_patterns=["single_transformer_blocks.{block}.attn.to_k.bias"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.attn.to_v.weight",
hf_patterns=["single_transformer_blocks.{block}.attn.to_v.weight"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.attn.to_v.bias",
hf_patterns=["single_transformer_blocks.{block}.attn.to_v.bias"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.proj_mlp.weight",
hf_patterns=["single_transformer_blocks.{block}.proj_mlp.weight"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.proj_mlp.bias",
hf_patterns=["single_transformer_blocks.{block}.proj_mlp.bias"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.proj_out.weight",
hf_patterns=["single_transformer_blocks.{block}.proj_out.weight"],
),
WeightTarget(
mlx_path="single_transformer_blocks.{block}.proj_out.bias",
hf_patterns=["single_transformer_blocks.{block}.proj_out.bias"],
),
# ========== Caption projection & output head ==========
WeightTarget(
mlx_path="norm_out.linear.weight",
hf_patterns=["norm_out.linear.weight"],
),
WeightTarget(
mlx_path="norm_out.linear.bias",
hf_patterns=["norm_out.linear.bias"],
),
WeightTarget(
mlx_path="proj_out.weight",
hf_patterns=["proj_out.weight"],
),
WeightTarget(
mlx_path="proj_out.bias",
hf_patterns=["proj_out.bias"],
),
# Caption_projection layers: we rely on explicit layer index to be
# expanded via num_layers; we pass num_layers explicitly from the loader.
WeightTarget(
mlx_path="caption_projection.{layer}.linear.weight",
hf_patterns=["caption_projection.{layer}.linear.weight"],
),
]
@staticmethod
def get_text_encoder_mapping() -> List[WeightTarget]:
return [
WeightTarget(
mlx_path="embed_tokens.weight",
hf_patterns=["model.embed_tokens.weight"],
),
WeightTarget(
mlx_path="layers.{block}.self_attn.q_proj.weight",
hf_patterns=["model.layers.{block}.self_attn.q_proj.weight"],
),
WeightTarget(
mlx_path="layers.{block}.self_attn.k_proj.weight",
hf_patterns=["model.layers.{block}.self_attn.k_proj.weight"],
),
WeightTarget(
mlx_path="layers.{block}.self_attn.v_proj.weight",
hf_patterns=["model.layers.{block}.self_attn.v_proj.weight"],
),
WeightTarget(
mlx_path="layers.{block}.self_attn.o_proj.weight",
hf_patterns=["model.layers.{block}.self_attn.o_proj.weight"],
),
WeightTarget(
mlx_path="layers.{block}.mlp.gate_proj.weight",
hf_patterns=["model.layers.{block}.mlp.gate_proj.weight"],
),
WeightTarget(
mlx_path="layers.{block}.mlp.up_proj.weight",
hf_patterns=["model.layers.{block}.mlp.up_proj.weight"],
),
WeightTarget(
mlx_path="layers.{block}.mlp.down_proj.weight",
hf_patterns=["model.layers.{block}.mlp.down_proj.weight"],
),
WeightTarget(
mlx_path="layers.{block}.input_layernorm.weight",
hf_patterns=["model.layers.{block}.input_layernorm.weight"],
),
WeightTarget(
mlx_path="layers.{block}.post_attention_layernorm.weight",
hf_patterns=["model.layers.{block}.post_attention_layernorm.weight"],
),
WeightTarget(
mlx_path="norm.weight",
hf_patterns=["model.norm.weight"],
),
]
@staticmethod
def get_vae_mapping() -> List[WeightTarget]:
return [
# ========== Encoder conv_in ==========
WeightTarget(
mlx_path="encoder.conv_in.conv3d.weight",
hf_patterns=["encoder.conv_in.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="encoder.conv_in.conv3d.bias",
hf_patterns=["encoder.conv_in.bias"],
),
# ========== Encoder down_blocks ==========
WeightTarget(
mlx_path="encoder.down_blocks.{block}.resnets.{res}.norm1.weight",
hf_patterns=["encoder.down_blocks.{block}.resnets.{res}.norm1.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.resnets.{res}.conv1.conv3d.weight",
hf_patterns=["encoder.down_blocks.{block}.resnets.{res}.conv1.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.resnets.{res}.conv1.conv3d.bias",
hf_patterns=["encoder.down_blocks.{block}.resnets.{res}.conv1.bias"],
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.resnets.{res}.norm2.weight",
hf_patterns=["encoder.down_blocks.{block}.resnets.{res}.norm2.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.resnets.{res}.conv2.conv3d.weight",
hf_patterns=["encoder.down_blocks.{block}.resnets.{res}.conv2.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.resnets.{res}.conv2.conv3d.bias",
hf_patterns=["encoder.down_blocks.{block}.resnets.{res}.conv2.bias"],
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.resnets.{res}.conv_shortcut.conv3d.weight",
hf_patterns=["encoder.down_blocks.{block}.resnets.{res}.conv_shortcut.weight"],
transform=transpose_conv3d_weight,
required=False,
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.resnets.{res}.conv_shortcut.conv3d.bias",
hf_patterns=["encoder.down_blocks.{block}.resnets.{res}.conv_shortcut.bias"],
required=False,
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.downsampler.resample_conv.weight",
hf_patterns=["encoder.down_blocks.{block}.downsampler.resample.1.weight"],
transform=transpose_conv2d_weight,
required=False,
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.downsampler.resample_conv.bias",
hf_patterns=["encoder.down_blocks.{block}.downsampler.resample.1.bias"],
required=False,
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.downsampler.time_conv.conv3d.weight",
hf_patterns=["encoder.down_blocks.{block}.downsampler.time_conv.weight"],
transform=transpose_conv3d_weight,
required=False,
),
WeightTarget(
mlx_path="encoder.down_blocks.{block}.downsampler.time_conv.conv3d.bias",
hf_patterns=["encoder.down_blocks.{block}.downsampler.time_conv.bias"],
required=False,
),
# ========== Encoder mid_block ==========
WeightTarget(
mlx_path="encoder.mid_block.resnets.{i}.norm1.weight",
hf_patterns=["encoder.mid_block.resnets.{i}.norm1.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="encoder.mid_block.resnets.{i}.conv1.conv3d.weight",
hf_patterns=["encoder.mid_block.resnets.{i}.conv1.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="encoder.mid_block.resnets.{i}.conv1.conv3d.bias",
hf_patterns=["encoder.mid_block.resnets.{i}.conv1.bias"],
),
WeightTarget(
mlx_path="encoder.mid_block.resnets.{i}.norm2.weight",
hf_patterns=["encoder.mid_block.resnets.{i}.norm2.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="encoder.mid_block.resnets.{i}.conv2.conv3d.weight",
hf_patterns=["encoder.mid_block.resnets.{i}.conv2.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="encoder.mid_block.resnets.{i}.conv2.conv3d.bias",
hf_patterns=["encoder.mid_block.resnets.{i}.conv2.bias"],
),
WeightTarget(
mlx_path="encoder.mid_block.attentions.{i}.norm.weight",
hf_patterns=["encoder.mid_block.attentions.{i}.norm.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="encoder.mid_block.attentions.{i}.to_qkv.weight",
hf_patterns=["encoder.mid_block.attentions.{i}.to_qkv.weight"],
transform=transpose_conv2d_weight,
),
WeightTarget(
mlx_path="encoder.mid_block.attentions.{i}.to_qkv.bias",
hf_patterns=["encoder.mid_block.attentions.{i}.to_qkv.bias"],
),
WeightTarget(
mlx_path="encoder.mid_block.attentions.{i}.proj.weight",
hf_patterns=["encoder.mid_block.attentions.{i}.proj.weight"],
transform=transpose_conv2d_weight,
),
WeightTarget(
mlx_path="encoder.mid_block.attentions.{i}.proj.bias",
hf_patterns=["encoder.mid_block.attentions.{i}.proj.bias"],
),
# ========== Encoder output ==========
WeightTarget(
mlx_path="encoder.norm_out.weight",
hf_patterns=["encoder.norm_out.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="encoder.conv_out.conv3d.weight",
hf_patterns=["encoder.conv_out.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="encoder.conv_out.conv3d.bias",
hf_patterns=["encoder.conv_out.bias"],
),
# ========== Decoder conv_in ==========
WeightTarget(
mlx_path="decoder.conv_in.conv3d.weight",
hf_patterns=["decoder.conv_in.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="decoder.conv_in.conv3d.bias",
hf_patterns=["decoder.conv_in.bias"],
),
# ========== Decoder mid_block ==========
# Mid block resnets
WeightTarget(
mlx_path="decoder.mid_block.resnets.{i}.norm1.weight",
hf_patterns=["decoder.mid_block.resnets.{i}.norm1.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="decoder.mid_block.resnets.{i}.conv1.conv3d.weight",
hf_patterns=["decoder.mid_block.resnets.{i}.conv1.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="decoder.mid_block.resnets.{i}.conv1.conv3d.bias",
hf_patterns=["decoder.mid_block.resnets.{i}.conv1.bias"],
),
WeightTarget(
mlx_path="decoder.mid_block.resnets.{i}.norm2.weight",
hf_patterns=["decoder.mid_block.resnets.{i}.norm2.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="decoder.mid_block.resnets.{i}.conv2.conv3d.weight",
hf_patterns=["decoder.mid_block.resnets.{i}.conv2.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="decoder.mid_block.resnets.{i}.conv2.conv3d.bias",
hf_patterns=["decoder.mid_block.resnets.{i}.conv2.bias"],
),
# Mid block attention
WeightTarget(
mlx_path="decoder.mid_block.attentions.{i}.norm.weight",
hf_patterns=["decoder.mid_block.attentions.{i}.norm.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="decoder.mid_block.attentions.{i}.to_qkv.weight",
hf_patterns=["decoder.mid_block.attentions.{i}.to_qkv.weight"],
transform=transpose_conv2d_weight,
),
WeightTarget(
mlx_path="decoder.mid_block.attentions.{i}.to_qkv.bias",
hf_patterns=["decoder.mid_block.attentions.{i}.to_qkv.bias"],
),
WeightTarget(
mlx_path="decoder.mid_block.attentions.{i}.proj.weight",
hf_patterns=["decoder.mid_block.attentions.{i}.proj.weight"],
transform=transpose_conv2d_weight,
),
WeightTarget(
mlx_path="decoder.mid_block.attentions.{i}.proj.bias",
hf_patterns=["decoder.mid_block.attentions.{i}.proj.bias"],
),
# ========== Decoder up_blocks ==========
# Up blocks resnets
WeightTarget(
mlx_path="decoder.up_blocks.{block}.resnets.{res}.norm1.weight",
hf_patterns=["decoder.up_blocks.{block}.resnets.{res}.norm1.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="decoder.up_blocks.{block}.resnets.{res}.conv1.conv3d.weight",
hf_patterns=["decoder.up_blocks.{block}.resnets.{res}.conv1.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="decoder.up_blocks.{block}.resnets.{res}.conv1.conv3d.bias",
hf_patterns=["decoder.up_blocks.{block}.resnets.{res}.conv1.bias"],
),
WeightTarget(
mlx_path="decoder.up_blocks.{block}.resnets.{res}.norm2.weight",
hf_patterns=["decoder.up_blocks.{block}.resnets.{res}.norm2.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="decoder.up_blocks.{block}.resnets.{res}.conv2.conv3d.weight",
hf_patterns=["decoder.up_blocks.{block}.resnets.{res}.conv2.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="decoder.up_blocks.{block}.resnets.{res}.conv2.conv3d.bias",
hf_patterns=["decoder.up_blocks.{block}.resnets.{res}.conv2.bias"],
),
# Up blocks resnets - conv_shortcut (optional, when in_dim != out_dim)
WeightTarget(
mlx_path="decoder.up_blocks.{block}.resnets.{res}.conv_shortcut.conv3d.weight",
hf_patterns=["decoder.up_blocks.{block}.resnets.{res}.conv_shortcut.weight"],
transform=transpose_conv3d_weight,
required=False, # Optional - only exists when dimensions differ
),
WeightTarget(
mlx_path="decoder.up_blocks.{block}.resnets.{res}.conv_shortcut.conv3d.bias",
hf_patterns=["decoder.up_blocks.{block}.resnets.{res}.conv_shortcut.bias"],
required=False, # Optional - only exists when dimensions differ
),
# Up blocks upsamplers - time_conv (blocks 0, 1 only)
WeightTarget(
mlx_path="decoder.up_blocks.{block}.upsampler.time_conv.conv3d.weight",
hf_patterns=["decoder.up_blocks.{block}.upsampler.time_conv.weight"],
transform=transpose_conv3d_weight,
required=False, # Only exists for blocks 0, 1
),
WeightTarget(
mlx_path="decoder.up_blocks.{block}.upsampler.time_conv.conv3d.bias",
hf_patterns=["decoder.up_blocks.{block}.upsampler.time_conv.bias"],
required=False, # Only exists for blocks 0, 1
),
# Up blocks upsamplers - resample_conv (blocks 0, 1, 2)
WeightTarget(
mlx_path="decoder.up_blocks.{block}.upsampler.resample_conv.weight",
hf_patterns=["decoder.up_blocks.{block}.upsampler.resample.1.weight"],
transform=transpose_conv2d_weight,
required=False, # Only exists for blocks 0, 1, 2
),
WeightTarget(
mlx_path="decoder.up_blocks.{block}.upsampler.resample_conv.bias",
hf_patterns=["decoder.up_blocks.{block}.upsampler.resample.1.bias"],
required=False, # Only exists for blocks 0, 1, 2
),
# ========== Decoder output ==========
WeightTarget(
mlx_path="decoder.norm_out.weight",
hf_patterns=["decoder.norm_out.gamma"],
transform=reshape_gamma_to_1d,
),
WeightTarget(
mlx_path="decoder.conv_out.conv3d.weight",
hf_patterns=["decoder.conv_out.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="decoder.conv_out.conv3d.bias",
hf_patterns=["decoder.conv_out.bias"],
),
# ========== Quant conv ==========
WeightTarget(
mlx_path="quant_conv.conv3d.weight",
hf_patterns=["quant_conv.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="quant_conv.conv3d.bias",
hf_patterns=["quant_conv.bias"],
),
# ========== Post quant conv ==========
WeightTarget(
mlx_path="post_quant_conv.conv3d.weight",
hf_patterns=["post_quant_conv.weight"],
transform=transpose_conv3d_weight,
),
WeightTarget(
mlx_path="post_quant_conv.conv3d.bias",
hf_patterns=["post_quant_conv.bias"],
),
]

Some files were not shown because too many files have changed in this diff Show More