| name | flux2-swift-mlx |
| description | Use when generating images with Flux.2 on Apple Silicon, working with MLX Swift, implementing text-to-image/image-to-image pipelines, LoRA training, or quantized model inference in Swift. |
Flux.2 Swift MLX
Native Swift implementation of Flux.2 image generation for Apple Silicon using MLX.
Installation
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/VincentGourbin/flux-2-swift-mlx.git", branch: "main")
]
.target(
name: "MyApp",
dependencies: [
.product(name: "Flux2Core", package: "flux-2-swift-mlx"),
.product(name: "FluxTextEncoders", package: "flux-2-swift-mlx"),
]
)
Requires macOS 14+ and Apple Silicon (M1/M2/M3/M4). Build with Xcode (not swift build) for Metal GPU support.
Quick Start
import Flux2Core
let pipeline = Flux2Pipeline(model: .klein4B)
try await pipeline.loadModels()
let image = try await pipeline.generateTextToImage(
prompt: "a cat sitting on a chair",
height: 1024,
width: 1024,
steps: 4,
guidance: 1.0
)
Model Selection
| Model | Steps | Guidance | Transformer (qint8) | Speed (1024x1024) | License |
|---|
| Klein 4B | 4 | 1.0 | ~4 GB | ~28s | Apache 2.0 |
| Klein 9B | 4 | 1.0 | ~9 GB | ~60s | Non-commercial |
| Dev (32B) | 28 | 4.0 | ~33 GB | ~30 min | Non-commercial |
Quantization (On-the-fly)
All models support on-the-fly quantization to reduce memory usage:
import Flux2Core
let balanced = Flux2QuantizationConfig.balanced
let memoryEfficient = Flux2QuantizationConfig.memoryEfficient
let ultraMinimal = Flux2QuantizationConfig.ultraMinimal
let custom = Flux2QuantizationConfig(
textEncoder: .mlx4bit,
transformer: .int4
)
let pipeline = Flux2Pipeline(
model: .klein9B,
quantization: custom
)
try await pipeline.loadModels()
Measured transformer memory per quantization level:
| Model | bf16 | qint8 (-47%) | int4 (-72%) |
|---|
| Klein 4B | 7.4 GB | 3.9 GB | 2.1 GB |
| Klein 9B | 17.3 GB | 9.2 GB | 4.9 GB |
| Dev (32B) | 61.5 GB | 32.7 GB | 17.3 GB |
Klein 9B with 8-bit Quantization
Klein 9B provides better quality than Klein 4B. With qint8, it fits comfortably on 32GB+ Macs.
Basic Klein 9B qint8 Usage
import Flux2Core
let pipeline = Flux2Pipeline(
model: .klein9B,
quantization: .balanced
)
try await pipeline.loadModels()
let image = try await pipeline.generateTextToImage(
prompt: "a beaver building a dam in a forest stream",
height: 1024,
width: 1024,
steps: 4,
guidance: 1.0
)
Explicit qint8 Configuration for Klein 9B
import Flux2Core
let quantConfig = Flux2QuantizationConfig(
textEncoder: .mlx8bit,
transformer: .qint8
)
let pipeline = Flux2Pipeline(
model: .klein9B,
quantization: quantConfig
)
try await pipeline.loadModels()
let image = try await pipeline.generateTextToImage(
prompt: "a detailed landscape with mountains and a lake",
height: 1024,
width: 1024,
steps: 4,
guidance: 1.0,
seed: 42
)
Klein 9B Memory Breakdown
The pipeline uses two-phase loading, so text encoder and transformer are never in memory simultaneously:
| Phase | Components | Memory |
|---|
| Phase 1 — Text Encoding | Qwen3-8B (8-bit) | ~8 GB |
| (text encoder unloaded) | | |
| Phase 2 — Generation | Klein 9B (qint8: 9.2 GB) + VAE (3 GB) | ~12 GB |
| Peak | Maximum of phase 1 or phase 2 | ~12 GB |
Even More Memory-Efficient Klein 9B
let tightConfig = Flux2QuantizationConfig(
textEncoder: .mlx4bit,
transformer: .qint8
)
let pipeline = Flux2Pipeline(model: .klein9B, quantization: tightConfig)
try await pipeline.loadModels()
Klein 9B with int4 for Minimum Memory
let minimalConfig = Flux2QuantizationConfig(
textEncoder: .mlx4bit,
transformer: .int4
)
let pipeline = Flux2Pipeline(model: .klein9B, quantization: minimalConfig)
try await pipeline.loadModels()
let image = try await pipeline.generateTextToImage(
prompt: "a cat on a sunny windowsill",
height: 1024,
width: 1024,
steps: 4,
guidance: 1.0
)
Text-to-Image
Basic Generation
import Flux2Core
let pipeline = Flux2Pipeline(model: .dev)
try await pipeline.loadModels()
let image = try await pipeline.generateTextToImage(
prompt: "a beautiful sunset over mountains",
height: 1024,
width: 1024,
steps: 28,
guidance: 4.0,
seed: 42
)
With Prompt Upsampling
let result = try await pipeline.generateTextToImageWithResult(
prompt: "a sunset",
upsamplePrompt: true
)
print("Original: \(result.originalPrompt)")
print("Enhanced: \(result.usedPrompt)")
let image = result.image
With Progress and Checkpoints
let image = try await pipeline.generateTextToImage(
prompt: "a beaver building a dam",
height: 1024,
width: 1024,
steps: 4,
guidance: 1.0,
checkpointInterval: 2
) { currentStep, totalSteps in
print("Step \(currentStep)/\(totalSteps)")
} onCheckpoint: { step, checkpointImage in
saveImage(checkpointImage, to: "step_\(step).png")
}
Image-to-Image
Style Transfer
import Flux2Core
let image = try await pipeline.generateImageToImage(
prompt: "transform into watercolor style",
images: [referenceImage],
strength: 0.7,
steps: 28
)
Multi-Image Conditioning
let image = try await pipeline.generateImageToImage(
prompt: "Modify the cat on image 1 to wear the hat from image 2",
images: [catImage, hatImage],
steps: 28,
guidance: 4.0
)
Image Interpretation (VLM)
let image = try await pipeline.generateTextToImage(
prompt: "Describe what the red arrow is pointing at",
interpretImage: mapImage,
height: 1024,
width: 1024,
steps: 28
)
LoRA Adapters
Loading a LoRA
import Flux2Core
let pipeline = Flux2Pipeline(model: .klein4B)
try await pipeline.loadModels()
let loraConfig = LoRAConfig(
filePath: "/path/to/lora.safetensors",
scale: 1.0
)
try pipeline.loadLoRA(loraConfig)
let image = try await pipeline.generateTextToImage(
prompt: "a photo of sks cat toy",
height: 1024,
width: 1024,
steps: 4,
guidance: 1.0
)
LoRA with Activation Keyword
var config = LoRAConfig(filePath: "/path/to/object-removal.safetensors")
config.scale = 1.1
config.activationKeyword = "RMVOBJ"
try pipeline.loadLoRA(config)
LoRA Training
Train custom LoRA adapters on Apple Silicon:
import Flux2Core
let config = LoRATrainingConfig(
datasetPath: URL(fileURLWithPath: "examples/cat-toy/train"),
rank: 32,
alpha: 32.0,
learningRate: 1e-4,
maxSteps: 250,
batchSize: 1,
resolution: 512,
gradientCheckpointing: true,
outputPath: URL(fileURLWithPath: "output/my-lora")
)
let trainer = SimpleLoRATrainer(config: config, model: .klein4B)
try await trainer.train { step, loss, gradNorm in
print("Step \(step): loss=\(loss), gradNorm=\(gradNorm)")
}
Training YAML Config
model: klein-4b
dataset:
path: examples/cat-toy/train
trigger_word: "sks"
lora:
rank: 32
alpha: 32.0
target_layers: all
training:
max_steps: 250
learning_rate: 1e-4
resolution: [512]
gradient_checkpointing: true
validation:
enabled: true
every_n_steps: 50
prompt: "a photo of sks cat toy on a beach"
flux2 train-lora --config my_training.yaml
Training Control
touch output/my-lora/.pause
rm output/my-lora/.pause
touch output/my-lora/.checkpoint
touch output/my-lora/.stop
Memory Management
Two-Phase Loading Architecture
The pipeline never loads text encoder and transformer simultaneously. This is the key to fitting large models on limited RAM:
- Phase 1 — Text Encoding: Load text encoder → encode prompt → unload text encoder → free memory
- Phase 2 — Image Generation: Load transformer + VAE → denoise → decode → output image
This means peak memory is the maximum of phase 1 or phase 2, not their sum.
Total Memory Requirements per Model
| Model | Quantization | Phase 1 (Text) | Phase 2 (Transformer + VAE) | Peak Memory |
|---|
| Klein 4B | int4 | ~4 GB | ~5 GB | ~8 GB |
| Klein 4B | qint8 | ~4 GB | ~7 GB | ~10 GB |
| Klein 4B | bf16 | ~8 GB | ~10 GB | ~16 GB |
| Klein 9B | int4 | ~4 GB | ~8 GB | ~16 GB |
| Klein 9B | qint8 | ~8 GB | ~12 GB | ~24 GB |
| Klein 9B | bf16 | ~8 GB | ~20 GB | ~32 GB |
| Dev (32B) | int4 | ~14 GB | ~20 GB | ~32 GB |
| Dev (32B) | qint8 | ~25 GB | ~36 GB | ~64 GB |
| Dev (32B) | bf16 | ~25 GB | ~65 GB | ~96 GB |
Recommended Quantization for Available RAM
import Flux2Core
let memoryManager = Flux2MemoryManager.shared
print("System RAM: \(memoryManager.physicalMemoryGB) GB")
let recommended = memoryManager.recommendedConfig()
let pipeline = Flux2Pipeline(model: .klein9B, quantization: recommended)
| Available RAM | Recommended Model | Recommended Quantization |
|---|
| 8–16 GB | Klein 4B | .ultraMinimal (4bit text + int4 transformer) |
| 16–24 GB | Klein 4B or Klein 9B | .memoryEfficient (4bit text + qint8 transformer) |
| 24–48 GB | Klein 9B | .balanced (8bit text + qint8 transformer) |
| 48–64 GB | Dev | .memoryEfficient (4bit text + qint8 transformer) |
| 64–96 GB | Dev | .balanced (8bit text + qint8 transformer) |
| 96+ GB | Dev | .highQuality (bf16 text + bf16 transformer) |
Handling Out-of-Memory (OOM) Scenarios
import Flux2Core
let pipeline = Flux2Pipeline(
model: .klein4B,
quantization: .ultraMinimal
)
let pipeline = Flux2Pipeline(
model: .klein9B,
quantization: .balanced,
memoryOptimization: .aggressive
)
let image = try await pipeline.generateTextToImage(
prompt: "a landscape",
height: 512,
width: 512,
steps: 4,
guidance: 1.0
)
pipeline.clearCacheEveryNSteps = 2
await pipeline.clearAll()
MemoryOptimizationConfig for Denoising
Controls how often the computation graph is evaluated during denoising steps, trading speed for memory:
import Flux2Core
let light = MemoryOptimizationConfig.light
let moderate = MemoryOptimizationConfig.moderate
let aggressive = MemoryOptimizationConfig.aggressive
let ultraLow = MemoryOptimizationConfig.ultraLowMemory
let pipeline = Flux2Pipeline(
model: .dev,
quantization: .balanced,
memoryOptimization: .aggressive
)
| System RAM | Recommended Preset |
|---|
| < 32 GB | .ultraLowMemory |
| 32–64 GB | .aggressive |
| 64–96 GB | .moderate |
| 96+ GB | .light or .disabled |
Monitoring GPU Memory
import MLX
print("Active: \(MLX.Memory.activeMemory / 1_073_741_824) GB")
print("Peak: \(MLX.Memory.peakMemory / 1_073_741_824) GB")
print("Cache: \(MLX.Memory.cacheMemory / 1_073_741_824) GB")
MLX.Memory.cacheLimit = 2 * 1_073_741_824
MLX.Memory.clearCache()
MLX.Memory.peakMemory = 0
print("Operation peak: \(MLX.Memory.peakMemory / 1_073_741_824) GB")
Pre-Generation Memory Checks
import Flux2Core
let memoryManager = Flux2MemoryManager.shared
let textCheck = memoryManager.checkTextEncodingPhase(config: quantization)
if !textCheck.isOK {
print("Warning: \(textCheck.suggestion)")
}
let genCheck = memoryManager.checkImageGenerationPhase(config: quantization)
if !genCheck.isOK {
print("Warning: \(genCheck.suggestion)")
}
let sizeCheck = memoryManager.checkImageSize(width: 1024, height: 1024)
Training Memory Optimization
For LoRA training, gradient checkpointing reduces activation memory by ~50% at the cost of ~2x slower forward passes:
let config = LoRATrainingConfig(
datasetPath: datasetURL,
gradientCheckpointing: true,
outputPath: outputURL
)
| Model | Without Checkpointing | With Checkpointing | Reduction |
|---|
| Klein 4B | ~60 GB | ~42 GB | -30% |
| Klein 9B | N/A (OOM) | ~94 GB | Required |
CLI Usage
unzip Flux2CLI-v1.0.1-macOS.zip
flux2 t2i "a beaver building a dam" --model klein-4b
flux2 t2i "a beautiful sunset" --model dev --steps 28 --output sunset.png
flux2 i2i "transform into watercolor" --images photo.jpg --strength 0.7
flux2 i2i "cat wearing this hat" --images cat.jpg --images hat.jpg
flux2 t2i "a photo of sks" --lora my-lora.safetensors --lora-scale 1.0
flux2 t2i "landscape painting" --transformer-quant int4 --text-quant 4bit
flux2 t2i "a cat" --upsample-prompt --model klein-4b
flux2 download --model klein-4b
flux2 download --model dev --hf-token hf_xxxxx
flux2 info
Model Downloading
import Flux2Core
let pipeline = Flux2Pipeline(model: .klein4B)
try await pipeline.loadModels { component, progress in
print("Downloading \(component): \(Int(progress * 100))%")
}
let isAvailable = ModelRegistry.isDownloaded(.transformer(.klein4B_bf16))
Troubleshooting
Common issues and solutions:
let pipeline = Flux2Pipeline(
model: .klein4B,
quantization: .ultraMinimal
)
let config = LoRATrainingConfig(
datasetPath: datasetURL,
gradientCheckpointing: true,
outputPath: outputURL
)
let pipeline = Flux2Pipeline(model: .dev, hfToken: "hf_xxxxx")
Hardware Requirements
| Model | Minimum RAM | Recommended |
|---|
| Klein 4B (int4) | 8 GB | 16 GB+ |
| Klein 4B (qint8) | 16 GB | 32 GB+ |
| Klein 9B (qint8) | 32 GB | 48 GB+ |
| Dev (qint8) | 64 GB | 96 GB+ |
| Dev (int4) | 32 GB | 64 GB+ |