Release 0.11.0 (#271)

This commit is contained in:
Filip Strand 2025-10-14 00:34:27 +02:00 committed by GitHub
parent 83354c3921
commit d314aa6889
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 303 additions and 61 deletions

View File

@ -5,6 +5,79 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.11.0] - 2025-10-12
# MFLUX v.0.11.0 Release Notes
### 🎨 New Model Support
- **Qwen Image Support**: Added support for the Qwen Image text-to-image model, enabling a new generation of visual content creation
- **New command**: `mflux-generate` now supports Qwen models for image generation
- **Qwen-specific features**: Full LoRA support with Qwen naming conventions, img2img support, and optimized weight handling
- **Qwen-Image-mflux-6bit Model**: Added [filipstrand/Qwen-Image-mflux-6bit](https://huggingface.co/filipstrand/Qwen-Image-mflux-6bit) quantized model to HF
### 🏗️ Major Architecture Improvements
- **Package Restructure**: Complete reorganization of the codebase to support multiple model architectures
- Moved from flat structure to organized `models/` hierarchy (`models/flux/`, `models/qwen/`, `models/depth_pro/`)
- Better separation of concerns with dedicated model, variant, tokenizer, and weight handler modules
- Improved maintainability and extensibility for future model additions
- **Namespace Package**: Converted mflux to a namespace package (in preparation for mflux.mcp extension)
- **Common Module**: Extracted shared functionality into `models/common/` for better code reuse
- Unified LoRA handling across different model types
- Shared attention utilities
- Common download and weight management utilities
### 📊 Metadata Enhancements
- **XMP/IPTC Metadata Support**: Added comprehensive metadata support for professional workflows
- Write XMP and IPTC metadata to generated images
- Industry-standard metadata formats for better compatibility with professional image tools
- Enhanced metadata reading and writing capabilities
- **New `mflux-info` command**: Display detailed metadata information from generated images
- View generation parameters, model information, and settings
- Extract metadata from any mflux-generated image
- Professional-grade metadata inspection
### 🔧 Scheduler System
- **Scheduler Interface**: Introduced a new scheduler abstraction for better extensibility
- Clean interface for implementing custom sampling schedulers
- Foundation for future scheduler additions (Euler, DPM++, etc.)
- Current implementation: Linear scheduler (existing behavior preserved)
- **Scheduler Selection**: Added `--scheduler` command-line argument for choosing schedulers
### 🐛 Bug Fixes
- **Non-Quantized Model Loading**: Fixed critical bug where locally saved non-quantized models failed to load properly
- **Model Weight Handling**: Improved weight loading reliability for edge cases
### 🔧 Developer Experience
- **MLX 0.29.2 Support**: Updated MLX dependency to support the latest version (mlx>=0.27.0,<0.30.0)
- **Python 3.13 Support**: Unblocked sentencepiece and torch dependencies for Python 3.13
- Updated dependency specifications for better Python 3.13 compatibility
- Ensured smooth experience on latest Python versions
- **Test Improvements**: Enhanced image comparison logic to allow similar images that are "close enough"
- More robust test suite that accommodates minor numerical differences
- Reduced false positives in image generation tests
- **CI Updates**: Removed Claude CI agent (replacement coming soon)
### 🔄 Breaking Changes
⚠️ **Import Path Changes**: Due to the package restructure, some internal import paths have changed. If you're using mflux as a library and importing internal modules directly, you may need to update your imports:
- Flux modules moved from `mflux.flux.*` to `mflux.models.flux.*`
- Common utilities moved to `mflux.models.common.*`
- CLI tools remain unchanged and fully backward compatible
### 👩‍💻 Contributors
- **Filip Strand (@filipstrand)**: Qwen model support, package restructure, core development
- **Alessandro Rizzo (@azrahello)**: XMP/IPTC metadata support, info command implementation
- **Anthony Wu (@anthonywu)**: Scheduler interface, namespace package conversion, Python 3.13 improvements, bug fixes
---
## [0.10.0] - 2025-08-04 ## [0.10.0] - 2025-08-04
# MFLUX v.0.10.0 Release Notes # MFLUX v.0.10.0 Release Notes

View File

@ -93,7 +93,7 @@ check: ensure-ruff
.PHONY: test .PHONY: test
test: ensure-pytest test: ensure-pytest
# 🏗️ Running tests... # 🏗️ Running tests...
uv pip install mlx==0.27.1 # Install pinned MLX version specifically for testing uv pip install mlx==0.29.2 # Install pinned MLX version specifically for testing
$(PYTHON) -m pytest $(PYTHON) -m pytest
# ✅ Tests completed # ✅ Tests completed

View File

@ -56,6 +56,7 @@ All models are implemented from scratch in MLX and only the tokenizers are used
[Huggingface Transformers](https://github.com/huggingface/transformers) library. Other than that, there are only minimal dependencies [Huggingface Transformers](https://github.com/huggingface/transformers) library. Other than that, there are only minimal dependencies
like [Numpy](https://numpy.org) and [Pillow](https://pypi.org/project/pillow/) for simple image post-processing. like [Numpy](https://numpy.org) and [Pillow](https://pypi.org/project/pillow/) for simple image post-processing.
As of v.0.11.0, MFLUX now supports the Qwen Image model.
--- ---
### 💿 Installation ### 💿 Installation
@ -152,6 +153,12 @@ This example uses the more powerful `dev` model with 25 time steps:
mflux-generate --model dev --prompt "Luxury food photograph" --steps 25 --seed 2 -q 8 mflux-generate --model dev --prompt "Luxury food photograph" --steps 25 --seed 2 -q 8
``` ```
This example uses the `qwen` model with 20 time steps:
```sh
mflux-generate --model qwen --prompt "Luxury food photograph" --steps 20 --seed 2 -q 6
```
You can also pipe prompts from other commands using stdin: You can also pipe prompts from other commands using stdin:
```sh ```sh
@ -168,7 +175,7 @@ from mflux.config.config import Config
# Load the model # Load the model
flux = Flux1.from_name( flux = Flux1.from_name(
model_name="schnell", # "schnell", "dev", or "krea-dev" model_name="schnell", # "schnell", "dev", "krea-dev", or "qwen"
quantize=8, # 3, 4, 5, 6, or 8 quantize=8, # 3, 4, 5, 6, or 8
) )
@ -177,7 +184,7 @@ image = flux.generate_image(
seed=2, seed=2,
prompt="Luxury food photograph", prompt="Luxury food photograph",
config=Config( config=Config(
num_inference_steps=2, # "schnell" works well with 2-4 steps, "dev" and "krea-dev" work well with 20-25 steps num_inference_steps=2, # "schnell" works well with 2-4 steps, "dev", "krea-dev", and "qwen" work well with 20-25 steps
height=1024, height=1024,
width=1024, width=1024,
) )
@ -188,7 +195,7 @@ image.save(path="image.png")
For more advanced Python usage and additional configuration options, you can explore the entry point files in the source code, such as [`generate.py`](src/mflux/generate.py), [`generate_controlnet.py`](src/mflux/generate_controlnet.py), [`generate_fill.py`](src/mflux/generate_fill.py), and others in the [`src/mflux/`](src/mflux/) directory. These files demonstrate how to use the Python API for various features and provide examples of advanced configurations. For more advanced Python usage and additional configuration options, you can explore the entry point files in the source code, such as [`generate.py`](src/mflux/generate.py), [`generate_controlnet.py`](src/mflux/generate_controlnet.py), [`generate_fill.py`](src/mflux/generate_fill.py), and others in the [`src/mflux/`](src/mflux/) directory. These files demonstrate how to use the Python API for various features and provide examples of advanced configurations.
⚠️ *If the specific model is not already downloaded on your machine, it will start the download process and fetch the model weights (~34GB in size for the Schnell or Dev model respectively). See the [quantization](#%EF%B8%8F-quantization) section for running compressed versions of the model.* ⚠️ ⚠️ *If the specific model is not already downloaded on your machine, it will start the download process and fetch the model weights (~34GB for Schnell/Dev models, ~58GB for Qwen). See the [quantization](#%EF%B8%8F-quantization) section for running compressed versions of the model.* ⚠️
*By default, mflux caches files in `~/Library/Caches/mflux/`. The Hugging Face model files themselves are cached separately in the Hugging Face cache directory (e.g., `~/.cache/huggingface/`).* *By default, mflux caches files in `~/Library/Caches/mflux/`. The Hugging Face model files themselves are cached separately in the Hugging Face cache directory (e.g., `~/.cache/huggingface/`).*
@ -228,9 +235,9 @@ mflux-generate \
- **`--prompt`** (required, `str`): Text description of the image to generate. Use `-` to read the prompt from stdin (e.g., `echo "A beautiful sunset" | mflux-generate --prompt -`). - **`--prompt`** (required, `str`): Text description of the image to generate. Use `-` to read the prompt from stdin (e.g., `echo "A beautiful sunset" | mflux-generate --prompt -`).
- **`--model`** or **`-m`** (required, `str`): Model to use for generation. Can be one of the official models (`"schnell"`, `"dev"`, or `"krea-dev"`) or a HuggingFace repository ID for a compatible third-party model (e.g., `"Freepik/flux.1-lite-8B-alpha"`). - **`--model`** or **`-m`** (required, `str`): Model to use for generation. Can be one of the official models (`"schnell"`, `"dev"`, `"krea-dev"`, or `"qwen"`) or a HuggingFace repository ID for a compatible third-party model (e.g., `"Freepik/flux.1-lite-8B-alpha"`).
- **`--base-model`** (optional, `str`, default: `None`): Specifies which base architecture a third-party model is derived from (`"schnell"`, `"dev"`, or `"krea-dev"`). Required when using third-party models from HuggingFace. - **`--base-model`** (optional, `str`, default: `None`): Specifies which base architecture a third-party model is derived from (`"schnell"`, `"dev"`, `"krea-dev"`, or `"qwen"`). Required when using third-party models from HuggingFace.
- **`--output`** (optional, `str`, default: `"image.png"`): Output image filename. If `--seed` or `--auto-seeds` establishes multiple seed values, the output filename will automatically be modified to include the seed value (e.g., `image_seed_42.png`). - **`--output`** (optional, `str`, default: `"image.png"`): Output image filename. If `--seed` or `--auto-seeds` establishes multiple seed values, the output filename will automatically be modified to include the seed value (e.g., `image_seed_42.png`).
@ -244,7 +251,7 @@ mflux-generate \
- **`--steps`** (optional, `int`, default: `4`): Number of inference steps. - **`--steps`** (optional, `int`, default: `4`): Number of inference steps.
- **`--guidance`** (optional, `float`, default: `3.5`): Guidance scale (only used for `"dev"` model). - **`--guidance`** (optional, `float`, default: `3.5`): Guidance scale (only used for `"dev"`, `"krea-dev"` and `qwen` models).
- **`--path`** (optional, `str`, default: `None`): Path to a local model on disk. - **`--path`** (optional, `str`, default: `None`): Path to a local model on disk.
@ -745,6 +752,8 @@ mflux-save \
--quantize 8 --quantize 8
``` ```
The `mflux-save` command works with both Flux and Qwen models.
*Note that when saving a quantized version, you will need the original huggingface weights.* *Note that when saving a quantized version, you will need the original huggingface weights.*
It is also possible to specify [LoRA](#-lora) adapters when saving the model, e.g It is also possible to specify [LoRA](#-lora) adapters when saving the model, e.g
@ -792,6 +801,7 @@ In other words, you can reclaim the 34GB diskspace (per model) by deleting the f
- [dhairyashil/FLUX.1-dev-mflux-4bit](https://huggingface.co/dhairyashil/FLUX.1-dev-mflux-4bit) - [dhairyashil/FLUX.1-dev-mflux-4bit](https://huggingface.co/dhairyashil/FLUX.1-dev-mflux-4bit)
- [akx/FLUX.1-Kontext-dev-mflux-4bit](https://huggingface.co/akx/FLUX.1-Kontext-dev-mflux-4bit) - [akx/FLUX.1-Kontext-dev-mflux-4bit](https://huggingface.co/akx/FLUX.1-Kontext-dev-mflux-4bit)
- [filipstrand/FLUX.1-Krea-dev-mflux-4bit](https://huggingface.co/filipstrand/FLUX.1-Krea-dev-mflux-4bit) - [filipstrand/FLUX.1-Krea-dev-mflux-4bit](https://huggingface.co/filipstrand/FLUX.1-Krea-dev-mflux-4bit)
- [filipstrand/Qwen-Image-mflux-6bit](https://huggingface.co/filipstrand/Qwen-Image-mflux-6bit)
Using the [community model support](#-third-party-huggingface-model-support), the quantized weights can be also be automatically downloaded when running the generate command: Using the [community model support](#-third-party-huggingface-model-support), the quantized weights can be also be automatically downloaded when running the generate command:
@ -940,7 +950,7 @@ mflux-generate --prompt "pikachu, Paper Cutout Style" --model schnell --steps 4
*Note that LoRA trained weights are typically trained with a **trigger word or phrase**. For example, in the latter case, the sentence should include the phrase **"Paper Cutout Style"**.* *Note that LoRA trained weights are typically trained with a **trigger word or phrase**. For example, in the latter case, the sentence should include the phrase **"Paper Cutout Style"**.*
*Also note that the same LoRA weights can work well with both the `schnell` and `dev` models. Refer to the original LoRA repository to see what mode it was trained for.* *Also note that the same LoRA weights can work well with both the `schnell` and `dev` models. Qwen models support LoRA but may require Qwen-specific LoRA weights. Refer to the original LoRA repository to see what mode it was trained for.*
#### Multi-LoRA #### Multi-LoRA
@ -1863,6 +1873,7 @@ See `uv run tools/rename_images.py --help` for full CLI usage help.
- Set up shell aliases for required args examples: - Set up shell aliases for required args examples:
- shortcut for dev model: `alias mflux-dev='mflux-generate --model dev'` - shortcut for dev model: `alias mflux-dev='mflux-generate --model dev'`
- shortcut for schnell model *and* always save metadata: `alias mflux-schnell='mflux-generate --model schnell --metadata'` - shortcut for schnell model *and* always save metadata: `alias mflux-schnell='mflux-generate --model schnell --metadata'`
- shortcut for qwen model: `alias mflux-qwen='mflux-generate --model qwen'`
- For systems with limited memory, use the `--low-ram` flag to reduce memory usage by constraining the MLX cache size and releasing components after use - For systems with limited memory, use the `--low-ram` flag to reduce memory usage by constraining the MLX cache size and releasing components after use
- On battery-powered Macs, use `--battery-percentage-stop-limit` (or `-B`) to prevent your laptop from shutting down during long generation sessions - On battery-powered Macs, use `--battery-percentage-stop-limit` (or `-B`) to prevent your laptop from shutting down during long generation sessions
- When generating multiple images with different seeds, use `--seed` with multiple values or `--auto-seeds` to automatically generate a series of random seeds - When generating multiple images with different seeds, use `--seed` with multiple values or `--auto-seeds` to automatically generate a series of random seeds

View File

@ -17,7 +17,7 @@ source-exclude = [
[project] [project]
name = "mflux" name = "mflux"
version = "0.10.0" version = "0.11.0"
description = "A MLX port of FLUX based on the Huggingface Diffusers implementation." description = "A MLX port of FLUX based on the Huggingface Diffusers implementation."
readme = "README.md" readme = "README.md"
keywords = ["diffusers", "flux", "mlx"] keywords = ["diffusers", "flux", "mlx"]
@ -30,7 +30,7 @@ dependencies = [
"filelock>=3.18.0", "filelock>=3.18.0",
"huggingface-hub>=0.24.5,<1.0", "huggingface-hub>=0.24.5,<1.0",
"matplotlib>=3.9.2,<4.0", "matplotlib>=3.9.2,<4.0",
"mlx>=0.27.0,<0.28.0", "mlx>=0.27.0,<0.30.0",
"numpy>=2.0.1,<3.0", "numpy>=2.0.1,<3.0",
"opencv-python>=4.10.0,<5.0", "opencv-python>=4.10.0,<5.0",
"piexif>=1.1.3,<2.0", "piexif>=1.1.3,<2.0",
@ -68,7 +68,7 @@ dev = [
"matplotlib>3.10,<4.0", "matplotlib>3.10,<4.0",
"pytest>=8.3.0,<9.0", "pytest>=8.3.0,<9.0",
"pytest-timer>=1.0,<2.0", "pytest-timer>=1.0,<2.0",
"mlx==0.27.1", # Used ONLY during test runs to ensure deterministic test results "mlx==0.29.2", # Used ONLY during test runs to ensure deterministic test results
] ]
[project.urls] [project.urls]

View File

@ -128,7 +128,9 @@ class ModelConfig:
default_base = next((b for b in base_models if base_model == b.model_name or base_model in b.aliases), None) default_base = next((b for b in base_models if base_model == b.model_name or base_model in b.aliases), None)
else: else:
# Infer from model_name substring - prefer longer matches (more specific) # Infer from model_name substring - prefer longer matches (more specific)
matching_bases = [(b, alias) for b in base_models for alias in b.aliases if alias and alias in model_name] # Use case-insensitive matching for better compatibility
model_name_lower = model_name.lower()
matching_bases = [(b, alias) for b in base_models for alias in b.aliases if alias and alias.lower() in model_name_lower]
if matching_bases: if matching_bases:
# Sort by alias length descending, then by priority ascending # Sort by alias length descending, then by priority ascending

View File

@ -56,7 +56,7 @@ def format_metadata(metadata: dict) -> str:
if lora_paths := exif.get("lora_paths"): if lora_paths := exif.get("lora_paths"):
lines.append("") lines.append("")
lines.append(f"LoRAs ({len(lora_paths)}):") lines.append(f"LoRAs ({len(lora_paths)}):")
lora_scales = exif.get("lora_scales", []) lora_scales = exif.get("lora_scales") or []
for i, lora in enumerate(lora_paths): for i, lora in enumerate(lora_paths):
scale = lora_scales[i] if i < len(lora_scales) else 1.0 scale = lora_scales[i] if i < len(lora_scales) else 1.0
lora_name = Path(lora).name lora_name = Path(lora).name

View File

@ -174,33 +174,49 @@ class LoRALoader:
effective_scale = scale effective_scale = scale
# Create new LoRA layer # Create new LoRA layer
if hasattr(current_module, 'weight'): # Check if it's a linear layer (either nn.Linear, LoRALinear, or FusedLoRALinear)
# Create LoRA layer is_linear = hasattr(current_module, 'weight')
is_lora_linear = isinstance(current_module, LoRALinear)
is_fused_linear = isinstance(current_module, FusedLoRALinear)
if is_linear or is_lora_linear or is_fused_linear:
# Handle fusion: if the current module is already a LoRA layer, fuse them
if is_lora_linear:
print(f" 🔀 Fusing with existing LoRA at {target_path}")
# Create a temporary LoRA layer from the base linear of the existing LoRA
lora_layer = LoRALinear.from_linear( lora_layer = LoRALinear.from_linear(
current_module, current_module.linear,
r=lora_A.shape[1], r=lora_A.shape[1],
scale=effective_scale scale=effective_scale
) )
# Set the LoRA matrices
# Set the LoRA matrices - use the correct dimensions from the LoRA file
lora_layer.lora_A = lora_A lora_layer.lora_A = lora_A
lora_layer.lora_B = lora_B lora_layer.lora_B = lora_B
# Apply alpha scaling to the matrices if present # Apply alpha scaling to the matrices if present
if "alpha" in lora_data: if "alpha" in lora_data:
lora_layer.lora_B = lora_layer.lora_B * alpha_scale lora_layer.lora_B = lora_layer.lora_B * alpha_scale
# Handle fusion: if the current module is already a LoRA layer, fuse them
if isinstance(current_module, LoRALinear):
print(f" 🔀 Fusing with existing LoRA at {target_path}")
# Create fused layer with the existing LoRA and the new one # Create fused layer with the existing LoRA and the new one
fused_layer = FusedLoRALinear( fused_layer = FusedLoRALinear(
base_linear=current_module.linear, base_linear=current_module.linear,
loras=[current_module, lora_layer] loras=[current_module, lora_layer]
) )
replacement_layer = fused_layer replacement_layer = fused_layer
elif isinstance(current_module, FusedLoRALinear): elif is_fused_linear:
print(f" 🔀 Adding to existing fusion at {target_path}") print(f" 🔀 Adding to existing fusion at {target_path}")
# Create a temporary LoRA layer from the base linear
lora_layer = LoRALinear.from_linear(
current_module.base_linear,
r=lora_A.shape[1],
scale=effective_scale
)
# Set the LoRA matrices
lora_layer.lora_A = lora_A
lora_layer.lora_B = lora_B
# Apply alpha scaling to the matrices if present
if "alpha" in lora_data:
lora_layer.lora_B = lora_layer.lora_B * alpha_scale
# Add to existing fusion # Add to existing fusion
fused_layer = FusedLoRALinear( fused_layer = FusedLoRALinear(
base_linear=current_module.base_linear, base_linear=current_module.base_linear,
@ -209,6 +225,19 @@ class LoRALoader:
replacement_layer = fused_layer replacement_layer = fused_layer
else: else:
# First LoRA on this layer # First LoRA on this layer
# Create LoRA layer
lora_layer = LoRALinear.from_linear(
current_module,
r=lora_A.shape[1],
scale=effective_scale
)
# Set the LoRA matrices - use the correct dimensions from the LoRA file
lora_layer.lora_A = lora_A
lora_layer.lora_B = lora_B
# Apply alpha scaling to the matrices if present
if "alpha" in lora_data:
lora_layer.lora_B = lora_layer.lora_B * alpha_scale
replacement_layer = lora_layer replacement_layer = lora_layer
# Replace the layer in the parent module # Replace the layer in the parent module

View File

@ -13,6 +13,7 @@ from mflux.models.qwen.model.qwen_text_encoder.qwen_text_encoder import QwenText
from mflux.models.qwen.model.qwen_transformer.qwen_transformer import QwenTransformer from mflux.models.qwen.model.qwen_transformer.qwen_transformer import QwenTransformer
from mflux.models.qwen.model.qwen_vae.qwen_vae import QwenVAE from mflux.models.qwen.model.qwen_vae.qwen_vae import QwenVAE
from mflux.models.qwen.qwen_initializer import QwenImageInitializer from mflux.models.qwen.qwen_initializer import QwenImageInitializer
from mflux.models.qwen.weights.qwen_model_saver import QwenModelSaver
from mflux.post_processing.array_util import ArrayUtil from mflux.post_processing.array_util import ArrayUtil
from mflux.post_processing.generated_image import GeneratedImage from mflux.post_processing.generated_image import GeneratedImage
from mflux.post_processing.image_util import ImageUtil from mflux.post_processing.image_util import ImageUtil
@ -168,6 +169,9 @@ class QwenImage(nn.Module):
negative_prompt=negative_prompt, negative_prompt=negative_prompt,
) )
def save_model(self, base_path: str) -> None:
QwenModelSaver.save_model(self, self.bits, base_path)
@staticmethod @staticmethod
def _compute_guided_noise( def _compute_guided_noise(
noise: mx.array, noise: mx.array,

View File

@ -0,0 +1,56 @@
from pathlib import Path
import mlx.core as mx
from mlx import nn
from mlx.utils import tree_flatten
from transformers import Qwen2Tokenizer
from mflux.utils.version_util import VersionUtil
class QwenModelSaver:
@staticmethod
def save_model(model, bits: int, base_path: str):
# Save the tokenizer
QwenModelSaver._save_tokenizer(base_path, model.qwen_tokenizer.tokenizer, "tokenizer")
# Save the models
QwenModelSaver.save_weights(base_path, bits, model.vae, "vae")
QwenModelSaver.save_weights(base_path, bits, model.transformer, "transformer")
QwenModelSaver.save_weights(base_path, bits, model.text_encoder, "text_encoder")
@staticmethod
def _save_tokenizer(base_path: str, tokenizer: Qwen2Tokenizer, subdir: str):
path = Path(base_path) / subdir
tokenizer.save_pretrained(path)
@staticmethod
def save_weights(base_path: str, bits: int, model: nn.Module, subdir: str):
path = Path(base_path) / subdir
path.mkdir(parents=True, exist_ok=True)
weights = QwenModelSaver._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:
# Copied from mlx-examples repo
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

@ -56,18 +56,80 @@ class QwenWeightHandler:
@staticmethod @staticmethod
def load_transformer(root_path: Path) -> tuple[dict, int | None, str | None]: def load_transformer(root_path: Path) -> tuple[dict, int | None, str | None]:
flat = QwenWeightHandler._load_safetensors_shards(root_path / "transformer", loading_mode="multi_glob") flat = QwenWeightHandler._load_safetensors_shards(root_path / "transformer", loading_mode="multi_glob")
# Check if this is a saved quantized model (with metadata) or HuggingFace weights
# Saved models from mflux-save are already in MLX structure and don't need manual mapping
quantization_level = None
mflux_version = None
# Try to get metadata from the first weight shard
import mlx.core as mx
from mlx.utils import tree_unflatten
file_glob = sorted((root_path / "transformer").glob("*.safetensors"))
if file_glob:
data = mx.load(str(file_glob[0]), return_metadata=True)
if len(data) > 1:
quantization_level = data[1].get("quantization_level")
mflux_version = data[1].get("mflux_version")
# If this is a saved model (has metadata), use tree_unflatten directly
if quantization_level is not None or mflux_version is not None:
return tree_unflatten(list(flat.items())), quantization_level, mflux_version
# Otherwise, it's HuggingFace weights that need manual mapping
mapped_weights = QwenWeightHandler._manual_transformer_mapping(flat) mapped_weights = QwenWeightHandler._manual_transformer_mapping(flat)
return mapped_weights, None, None return mapped_weights, None, None
@staticmethod @staticmethod
def _load_qwen_text_encoder(root_path: Path) -> tuple[dict, int | None, str | None]: def _load_qwen_text_encoder(root_path: Path) -> tuple[dict, int | None, str | None]:
import mlx.core as mx
from mlx.utils import tree_unflatten
# Check for saved model metadata FIRST to determine loading mode
quantization_level = None
mflux_version = None
file_glob = sorted((root_path / "text_encoder").glob("*.safetensors"))
if file_glob:
data = mx.load(str(file_glob[0]), return_metadata=True)
if len(data) > 1:
quantization_level = data[1].get("quantization_level")
mflux_version = data[1].get("mflux_version")
# If this is a saved model, load without expecting index.json
if quantization_level is not None or mflux_version is not None:
all_weights = QwenWeightHandler._load_safetensors_shards(
root_path / "text_encoder", loading_mode="multi_glob"
)
return tree_unflatten(list(all_weights.items())), quantization_level, mflux_version
# Otherwise, it's HuggingFace weights that need manual mapping
all_weights = QwenWeightHandler._load_safetensors_shards(root_path / "text_encoder", loading_mode="multi_json") all_weights = QwenWeightHandler._load_safetensors_shards(root_path / "text_encoder", loading_mode="multi_json")
mapped_weights = QwenWeightHandler._manual_text_encoder_mapping(all_weights) mapped_weights = QwenWeightHandler._manual_text_encoder_mapping(all_weights)
return mapped_weights, None, None return mapped_weights, None, None
@staticmethod @staticmethod
def _load_vae(root_path: Path) -> tuple[dict, int | None, str | None]: def _load_vae(root_path: Path) -> tuple[dict, int | None, str | None]:
import mlx.core as mx
from mlx.utils import tree_unflatten
weights = QwenWeightHandler._load_safetensors_shards(root_path / "vae", loading_mode="single") weights = QwenWeightHandler._load_safetensors_shards(root_path / "vae", loading_mode="single")
# Check for saved model metadata
quantization_level = None
mflux_version = None
file_glob = sorted((root_path / "vae").glob("*.safetensors"))
if file_glob:
data = mx.load(str(file_glob[0]), return_metadata=True)
if len(data) > 1:
quantization_level = data[1].get("quantization_level")
mflux_version = data[1].get("mflux_version")
# If this is a saved model, use tree_unflatten directly
if quantization_level is not None or mflux_version is not None:
return tree_unflatten(list(weights.items())), quantization_level, mflux_version
# Otherwise, it's HuggingFace weights that need manual mapping and reshaping
reshaped_weights = [QwenWeightUtil.reshape_weights(k, v) for k, v in weights.items()] reshaped_weights = [QwenWeightUtil.reshape_weights(k, v) for k, v in weights.items()]
reshaped_weights = QwenWeightUtil.flatten(reshaped_weights) reshaped_weights = QwenWeightUtil.flatten(reshaped_weights)
weights = dict(reshaped_weights) weights = dict(reshaped_weights)

View File

@ -214,7 +214,8 @@ class MetadataBuilder:
Returns: Returns:
Comma-separated string of LoRA names and scales, or empty string Comma-separated string of LoRA names and scales, or empty string
""" """
if "lora_paths" not in metadata or not metadata["lora_paths"]: # Check if lora_paths exists and is not None/empty
if "lora_paths" not in metadata or metadata["lora_paths"] is None or not metadata["lora_paths"]:
return "" return ""
lora_list = [] lora_list = []

View File

@ -1,23 +1,27 @@
from mflux.config.model_config import ModelConfig from mflux.config.model_config import ModelConfig
from mflux.models.flux.variants.txt2img.flux import Flux1 from mflux.models.flux.variants.txt2img.flux import Flux1
from mflux.models.qwen.variants.txt2img.qwen_image import QwenImage
from mflux.ui.cli.parsers import CommandLineParser from mflux.ui.cli.parsers import CommandLineParser
def main(): def main():
# 0. Parse command line arguments # 0. Parse command line arguments
parser = CommandLineParser(description="Save a quantized version of Flux.1 to disk.") # fmt: off parser = CommandLineParser(description="Save a quantized version of a model to disk.") # fmt: off
parser.add_model_arguments(path_type="save", require_model_arg=True) parser.add_model_arguments(path_type="save", require_model_arg=True)
parser.add_lora_arguments() parser.add_lora_arguments()
args = parser.parse_args() args = parser.parse_args()
# 1. Load, quantize and save the model # 1. Determine model class based on model name
flux = Flux1( model_class = QwenImage if "qwen" in args.model.lower() else Flux1
# 2. Load, quantize and save the model
model = model_class(
model_config=ModelConfig.from_name(args.model, base_model=args.base_model), model_config=ModelConfig.from_name(args.model, base_model=args.base_model),
quantize=args.quantize, quantize=args.quantize,
lora_paths=args.lora_paths, lora_paths=args.lora_paths,
lora_scales=args.lora_scales, lora_scales=args.lora_scales,
) )
flux.save_model(args.path) model.save_model(args.path)
if __name__ == "__main__": if __name__ == "__main__":

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

48
uv.lock generated
View File

@ -755,7 +755,7 @@ wheels = [
[[package]] [[package]]
name = "mflux" name = "mflux"
version = "0.10.0" version = "0.11.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "accelerate" }, { name = "accelerate" },
@ -799,8 +799,8 @@ requires-dist = [
{ name = "huggingface-hub", specifier = ">=0.24.5,<1.0" }, { name = "huggingface-hub", specifier = ">=0.24.5,<1.0" },
{ name = "matplotlib", specifier = ">=3.9.2,<4.0" }, { name = "matplotlib", specifier = ">=3.9.2,<4.0" },
{ name = "matplotlib", marker = "extra == 'dev'", specifier = ">3.10,<4.0" }, { name = "matplotlib", marker = "extra == 'dev'", specifier = ">3.10,<4.0" },
{ name = "mlx", specifier = ">=0.27.0,<0.28.0" }, { name = "mlx", specifier = ">=0.27.0,<0.30.0" },
{ name = "mlx", marker = "extra == 'dev'", specifier = "==0.27.1" }, { name = "mlx", marker = "extra == 'dev'", specifier = "==0.29.2" },
{ name = "numpy", specifier = ">=2.0.1,<3.0" }, { name = "numpy", specifier = ">=2.0.1,<3.0" },
{ name = "opencv-python", specifier = ">=4.10.0,<5.0" }, { name = "opencv-python", specifier = ">=4.10.0,<5.0" },
{ name = "piexif", specifier = ">=1.1.3,<2.0" }, { name = "piexif", specifier = ">=1.1.3,<2.0" },
@ -827,38 +827,38 @@ requires-dist = [
[[package]] [[package]]
name = "mlx" name = "mlx"
version = "0.27.1" version = "0.29.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "mlx-metal", marker = "platform_system == 'Darwin'" }, { name = "mlx-metal", marker = "platform_system == 'Darwin'" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/ed/07a7145c500914ed8ab704a7a3828a5134e85f8b4458e1fee887306867d6/mlx-0.27.1-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a033b65fe46425ad5032867d5a71a556a5168108d89aa7092b457556c70d84fc", size = 557679 }, { url = "https://files.pythonhosted.org/packages/3f/f0/2c2f99a91ed9dfcc78d31d9e5d3bb2f5305a8d65953cbc41f34f8056c49a/mlx-0.29.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:b46c1a24b9b8f7145e4d84410552ddfa03f40f9afdbe8f819f6b4b52b4db5d30", size = 547369 },
{ url = "https://files.pythonhosted.org/packages/aa/ae/b3e46904ea04b5badb77195169880733f09976a3dfc81c954b99f9adbbc4/mlx-0.27.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:2836c6fd9803dc0c6cd06f204e31b3e0191e6c5b6bc8570b28661d926908cba3", size = 530556 }, { url = "https://files.pythonhosted.org/packages/3b/06/0edddf0a5facb58c17616cd33da6de2722636da8d8d3927272a5a88658e4/mlx-0.29.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:18c5b63c6e4b35f75f477f74d3942870e0bc9c9f1c6a071d8a058bbd46681bf4", size = 547367 },
{ url = "https://files.pythonhosted.org/packages/6e/24/85f5a7dfcf0b4974aa43a2f6dc0d59dfc04e4a1051aacebd7e7810b823ff/mlx-0.27.1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:8b0566054c46d84c470cc99cda2afc3914ad6c7808fbb724dc1ec235e5b2a98c", size = 530559 }, { url = "https://files.pythonhosted.org/packages/e1/cd/8c089cf1678a752ed4175a7ad5f08823ceef75d797217e12a44a71d6c062/mlx-0.29.2-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:ed53b8383ad4ee4311400558e0a5ec61105fc4553950b5732c66e7081cd1a9e8", size = 547365 },
{ url = "https://files.pythonhosted.org/packages/14/bd/91ad900837a3760ba056863c1350f0deabc3e462c3999b4d5ea875368177/mlx-0.27.1-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:91ef93ce09900c9a8ca662cf34e3c39ab5af2762822ecd6b12fecae518be167f", size = 628183 }, { url = "https://files.pythonhosted.org/packages/8b/cc/9a14ac57a251f0ee5047f42214fa50d1d5223d805f0b8b6627639c3692eb/mlx-0.29.2-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:fff27af8dca74546422e8400d32ab848baf42405123cbcfdf62674a9c0560ebe", size = 652302 },
{ url = "https://files.pythonhosted.org/packages/ac/63/88cb9a5019bea21244385eacc4c64deb4d074a8bc3b4b6d2d97cdacf97a2/mlx-0.27.1-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:d2e5dedbfbcbe558e51a5c476ca6a18e307676f9e49854eb27e53778bc474699", size = 557582 }, { url = "https://files.pythonhosted.org/packages/cb/f0/f57349f37cf5dd53f95127e141fc59fc435e4b6bfabba5a84c65de4d3597/mlx-0.29.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:e74965369227230374b3e8e8c8d46e209e5221a9b76bbb0fa788617e2c68f73c", size = 547581 },
{ url = "https://files.pythonhosted.org/packages/fa/87/c2d7343e7b054481c52e23a190911c40aed4f8c77a8dfeda1f48d5cb520a/mlx-0.27.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:9f04b9778897a879c9ca22e5413dfa1efc192d86d7211b184e079efec49dfb8b", size = 530820 }, { url = "https://files.pythonhosted.org/packages/66/04/e016ca28dc9e0738a2541581420125cfe6bba24466a64420600bdd6fd52c/mlx-0.29.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0f79194eeac78e85b96439d3bbc17aae5aba045a2af083c000b4fbbc501f253e", size = 547581 },
{ url = "https://files.pythonhosted.org/packages/06/bf/20497ca7411028baa56372c20e94a3eaddac973155b415f08fc12592c2cf/mlx-0.27.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:01d794f9e390438ab4f942a18d9a8ca65bef10c2c2007ef38ca988d039d6d9d3", size = 530821 }, { url = "https://files.pythonhosted.org/packages/1f/b3/e2595e70ef8d4438dff694857745b0e108911e5b5fb83259dde6e5dc5bd1/mlx-0.29.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:33bbbb0fd24895d5ff080bb4d10e3e77017bba675d9a12466c8866eaf9b47854", size = 547578 },
{ url = "https://files.pythonhosted.org/packages/48/68/234a0d846576502ea1bf8ea478c764d87eec9042349e298d2aea58c4e8e9/mlx-0.27.1-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:fae11432d0639789f1e172b19b35ac8987c8ab9716e55a23fc7a170d6545fc33", size = 628500 }, { url = "https://files.pythonhosted.org/packages/11/8c/5d51543ab128c2dff5e4b44ca799db8db5aa4f4ffc34af6531fd73627b54/mlx-0.29.2-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:32e159f2772be893bec580d2d50c0e6b32ad71a19ded7307bf6c871c8aaa9cf2", size = 651900 },
{ url = "https://files.pythonhosted.org/packages/65/43/125102bbb2be6825880ae2dc8d8702f99cfa7753407f574457b36e422218/mlx-0.27.1-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:0c570c9afb57c697bd864504115be8a7c4de97f0b80557a597d496ee426a6812", size = 549869 }, { url = "https://files.pythonhosted.org/packages/f3/84/7250237039e91d8e44ca0cf3522f189164844c196f262509afd29ef54710/mlx-0.29.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:eec950bf7118ad0865d0fc4686bd85d99bf8463fc717d836a5132e1a08b4f129", size = 548336 },
{ url = "https://files.pythonhosted.org/packages/f3/79/0bf681700fc8b165517e907f9ec777b5a5d628004a65a777148f68c6baa0/mlx-0.27.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ccff7bbbd9df302b26e79013ef6d0c3531c9ba5963ead521e2d85856811b86a0", size = 531671 }, { url = "https://files.pythonhosted.org/packages/13/47/428ac8d9b0cb5c136e5ce6c726cfdd55caa5b9497dafb6221acfee18f145/mlx-0.29.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bef7333268d6d02e50a9ac6b10f661b711cd02da4a5e2d7619cf198a7e530308", size = 548334 },
{ url = "https://files.pythonhosted.org/packages/ec/97/f1367b4892bef7f78e38737d3a28094e93124f11684a28a9e92ed5a13b2b/mlx-0.27.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9ccadaed449c07dfeae484620992b904c17dfea7564f8df63095c60eed3af02b", size = 531672 }, { url = "https://files.pythonhosted.org/packages/14/f0/7d5d3527ca3fdc664c900b4b822028691739e58c8e8f7975b33df4d3536e/mlx-0.29.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:f622fc6a84542a08ad2136e9251822d2c08106e5a1a0bd5d249a2d72bccd6577", size = 548330 },
{ url = "https://files.pythonhosted.org/packages/9c/0f/3ebec0806ed2c5ba8ba151394cd62a46b4d83ea347da33af91b86d8969d4/mlx-0.27.1-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:742c413e75605b71db69379a176da63e32ba19b9e9ad03b8763adbd1fcfcd394", size = 621661 }, { url = "https://files.pythonhosted.org/packages/09/18/e202e0f6232822f6768995cdbf50eda202137bb6547368f6e3993dbee00b/mlx-0.29.2-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:a1aa1aee8e1b6bd1e51361e6b692c70d281b8187b2e859e70ecc11daab306dac", size = 648728 },
{ url = "https://files.pythonhosted.org/packages/86/f6/4324386b0764deb692e14a97282a348a9a938aa8b441bf8b6c7599f418d4/mlx-0.27.1-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:803669a28031766c2b0fe94c0a3bfd030184e706092f0a831b33620c1e2ef865", size = 549847 }, { url = "https://files.pythonhosted.org/packages/a0/9a/91f6f5d031f109fa8c00ba9dd4f7a3fc42e1097a57c26783ce000069c264/mlx-0.29.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:05ea54173f4bde11b2c93e673d65d72523f5d850f5112d3874156a6fc74ca591", size = 548297 },
{ url = "https://files.pythonhosted.org/packages/cf/4b/3194ccb03527a050c04d837d731a11599f8620e6ce16d3971798caae1d44/mlx-0.27.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e9649b3d86ce564000797384510c9d07af38a9ce2a07df8e2f7c6a3e0f0f059e", size = 531664 }, { url = "https://files.pythonhosted.org/packages/2b/2d/dae7ca0b7fa68c6c1f2b896dfe1b8060647f144d5c5da2d53388e38809b1/mlx-0.29.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:199dd029b5e55b6d94f1ce366d0137824e46e4333891424dd00413c739f50ae9", size = 548305 },
{ url = "https://files.pythonhosted.org/packages/cc/57/a6e0d8dc6e7ba08a64d71fb89d743e77446040113ea1dbb7950be8f60f39/mlx-0.27.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:5c501ceec7c6aa2ea1c850dd4e14e679f5416a142264b9d5d405a4e0aeb991b2", size = 531663 }, { url = "https://files.pythonhosted.org/packages/b1/56/f02f5c9e1fc11c020982501a763fa92b497ea50671a587760543987ba8c8/mlx-0.29.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:b6dd4e5f227414882b1676d99250d99389228d1bdc14e4e4e88c95d4903810b7", size = 548302 },
{ url = "https://files.pythonhosted.org/packages/48/96/6932e0fa866d08cdf77a3fc37cf624e4cac8960d2e297c3fd80afb49417d/mlx-0.27.1-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:42bfbabdcd0b7aa1c94748c8963e66680a3cf3f599b01036db14e496bf276eb8", size = 621609 }, { url = "https://files.pythonhosted.org/packages/4b/b4/b61eeb92c424947675492dec3a411bdbeae307dfd78162d65ab47e8c3b4f/mlx-0.29.2-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:c3b9a9aee13f346d060966472954eebe99d9f1b295c9a237c9a000f1ef9adf2c", size = 648709 },
] ]
[[package]] [[package]]
name = "mlx-metal" name = "mlx-metal"
version = "0.27.1" version = "0.29.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/77/89fa3327011f018638c9e943e1edc081ce9ca0ed296fe64d6daf93c6ff51/mlx_metal-0.27.1-py3-none-macosx_13_0_arm64.whl", hash = "sha256:c66d9b1adb3c0ea19492fba6493f672bc7542e65dd65f7e2995918815fbeb907", size = 33523035 }, { url = "https://files.pythonhosted.org/packages/31/a5/a045006546fed791f6e9a74ed4451dac871d3c35f9e54a3a25d820668a85/mlx_metal-0.29.2-py3-none-macosx_13_0_arm64.whl", hash = "sha256:cf8f83a521e620357185c57945142718d526b9312ee112e5a89eb5600480f4d6", size = 35056194 },
{ url = "https://files.pythonhosted.org/packages/d7/a8/ac706ad6ce834834762d5146d791f77710efc896c13ef47fd7d672099056/mlx_metal-0.27.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:fe4415ddd242974d91c7ca0699cd01507d17da8a5ba304122ef137cdb5e7fff4", size = 32926383 }, { url = "https://files.pythonhosted.org/packages/4c/8c/4bdd3a7d04ed477b32aec30d30236dfca9f9ac27706cb309511278ddd281/mlx_metal-0.29.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:fa944001970813b296e8aff5616f2fa9daeda6bc1d190c17fbe8a7ca838ecef0", size = 34791708 },
{ url = "https://files.pythonhosted.org/packages/78/77/6963681fb54ecaa0ae5de4209c15504a803a0edd1a33fd074e6c558fd5e0/mlx_metal-0.27.1-py3-none-macosx_15_0_arm64.whl", hash = "sha256:d025dea30bda8baa32c928cfa333eac64a5adc8d07656f8fc55072d99403ebc9", size = 32897065 }, { url = "https://files.pythonhosted.org/packages/b1/11/12e158848fe4d3316c999ffb6c2d88f554bde98d69022b3385e25ece997e/mlx_metal-0.29.2-py3-none-macosx_15_0_arm64.whl", hash = "sha256:08d8b7fe305425a14b74ebf36cee176575bfd4cd8d34a2aaae8f05b9983d2d71", size = 34784506 },
] ]
[[package]] [[package]]