Port the Self-Forcing video diffusion algorithm from CUDA/PyTorch to native MLX (Apple Silicon), with a clean repo at audiohacking/self-forcing-mlx.
All core MLX modules verified with random weights:
| Module | Status | Params | Notes |
|---|---|---|---|
utils.py |
✓ Verified | — | sinusoidal_embedding, RoPE, unpatchify, activations |
attention.py |
✓ Verified | — | Self-attn, cross-attn, FFN, RMSNorm, LayerNorm |
model.py (WanModel) |
✓ Verified | 1.4B | Full transformer forward pass, KV cache support |
vae.py (WanVAE) |
✓ Verified | 73M | Decode: (1,16,4,60,80) → (1,3,4,480,640) |
t5.py (T5Encoder) |
✓ Verified | 5.7B | Token→embedding forward pass |
scheduler.py |
✓ Verified | — | Flow-matching scheduler, timestep sampling |
converter.py |
✓ Verified | — | PyTorch→MLX weight conversion with axis permutation |
config.py |
✓ Verified | — | Architecture constants |
- MLX Conv3d NHWC format (not NCHW like PyTorch)
- MLX
mx.split()uses numpy-style index splitting (not PyTorch size splitting) - MLX
mx.broadcast_to()is standalone function (not tensor method) - MLX
nn.Sequentialstores modules in.layers(iterate via.layers, not directly) - MLX
nn.Dropout.__call__has notrainingparameter - MLX
mx.array.clip()doesn't exist — usemx.clip() - MLX Conv2d/Conv3d weight shapes differ from PyTorch (channel-last vs channel-first)
- CausalConv3d kernel=1 needs padding=0 (kernel=3 needs padding=1)
- float64 not supported on M3 Ultra GPU
- Run
converter.pyon Wan2.1-T2V-1.3B pretrained weights - Verify transformer output against PyTorch baseline (same latent input → same output)
- Verify VAE decode output against PyTorch baseline
- Verify T5 encoder output against PyTorch baseline
- Fix
T5Encoder.load_weights()to match MLX naming conventions - Add weight shape validation (check dims before loading)
- Add missing-weight warnings for partial checkpoints
mlx_weights/transformer.safetensors(825 weights)mlx_weights/t5_encoder.safetensors(242 weights)mlx_weights/vae_decoder.safetensors(194 weights)
-
CausalInferencePipelinewith loaded weights (not random) - KV cache initialization and management
- Classifier-free guidance (CFG) — conditional + unconditional passes
- Block-by-block autoregressive generation
-
HuggingfaceTokenizerintegration (load from hub or local) - T5 encoder with loaded weights
- Context length handling (padding/truncation to TEXT_LEN=512)
- Frame-by-frame VAE decode with feature caching
- Latent normalization (mean/std scaling)
- Pixel output (clamped to [-1,1] or [0,1])
-
demo_mlx.py— text-to-video generation script - CLI arguments: prompt, num_frames, guidance_scale, etc.
- Output video file saving (use MLX image tools or raw numpy)
- Generate video with same prompt on PyTorch baseline → compare pixel-level similarity
- Verify numerical stability across multiple denoising steps
- Test edge cases: single frame, max frames, empty prompt
- Measure inference time per denoising step on M3 Ultra
- Memory usage profiling (peak memory, cache sizes)
- Compare with PyTorch MPS backend performance
-
tests/test_utils.py— all utility functions -
tests/test_attention.py— attention, cross-attention, FFN -
tests/test_model.py— WanModel forward/backward -
tests/test_vae.py— VAE encode/decode roundtrip -
tests/test_t5.py— T5 encoder -
tests/test_scheduler.py— scheduler steps -
tests/test_pipeline.py— full pipeline integration
- Create
audiohacking/self-forcing-mlxon GitHub - Set up clean
mainbranch with only MLX port files - Remove all original PyTorch code (pipeline/, model/, wan/, trainer/, etc.)
- Keep only:
sforcing/,demo_mlx.py,scripts/,tests/, config files
self-forcing-mlx/
├── sforcing/
│ ├── __init__.py
│ ├── config.py # Architecture constants
│ ├── utils.py # Math utilities, RoPE
│ ├── attention.py # Attention primitives
│ ├── model.py # Wan diffusion transformer
│ ├── vae.py # VAE decoder
│ ├── t5.py # T5 text encoder
│ ├── scheduler.py # Flow-matching scheduler
│ ├── pipeline.py # Inference pipeline (KV cache, CFG)
│ ├── converter.py # Weight conversion utilities
│ ├── tokenizer_bridge.py # HuggingFace tokenizer wrapper
│ ├── inference.py # High-level inference API
│ └── trainers/ # Training code (future)
├── scripts/
│ └── download_models.py # Model download helper
├── tests/
│ ├── test_utils.py
│ ├── test_attention.py
│ ├── test_model.py
│ ├── test_vae.py
│ ├── test_t5.py
│ ├── test_scheduler.py
│ └── test_pipeline.py
├── demo_mlx.py # Text-to-video demo
├── requirements.txt
├── setup.py
└── README.md
mlx>=0.31.0
safetensors>=0.4.0
tokenizers>=0.15.0
numpy>=1.24.0
- GitHub Actions: install MLX, run tests
- Optional: output comparison workflow (against cached PyTorch reference)
- Quantize transformer weights (4-bit or 8-bit via
mx.quantize()) - Optimize KV cache sizes (only cache needed tokens)
- Lazy VAE decode (decode only visible frames)
- Gradient checkpointing for training (trade memory for compute)
- Metal GPU shader compilation (warmup pass)
- Fused attention (if MLX adds flash-attention support)
- Async data pipeline for frame decoding
- Batch denoising (process multiple noise levels in parallel)
- Evaluate
mlx-arsenalfor optimized ops - Custom Metal kernels for critical paths (if needed)
- Model parallelism for 5.7B T5 + 1.4B transformer
- Port training loops from
sforcing/trainers/ - Self-Forcing distillation (DMD, SID, ODE regression)
- LoRA fine-tuning
- Data pipeline for video datasets
Phase 0: ████████████████████ 100% Foundation complete
Phase 1: ████████████████████ 100% All 3 weight files load correctly (T5: 242/242, VAE: 112/112, Transformer: 826/826)
Phase 2: ████████████████████ 100% Pipeline verified end-to-end with KV cache, CFG, clean-update fix
Phase 3: ████████████████████ 100% 52 unit tests passing across all modules
Phase 4: ░░░░░░░░░░░░░░░░░░░░ 0% New repo not yet created
Phase 5: ░░░░░░░░░░░░░░░░░░░░ 0% Optimization not started
Phase 6: ░░░░░░░░░░░░░░░░░░░░ 0% Training not started
- Root cause: KV cache used padded sequence length (
x.shape[1]= max_seq_len = 32760) instead of actual token count for store/retrieve operations. After one forward pass the cache was "full", causing subsequent writes to overflow. - Fix: Pass
seq_lens(actual token counts) throughAttentionBlock→WanSelfAttention. Cache stores only actual (non-padding) tokens. - Clean update fix: The pipeline's clean update (timestep=0) was appending to the cache instead of replacing noisy entries. Added cache position save/restore around each block's denoising loop so the clean update overwrites the same positions.
- Root cause:
AttentionBlockandHeadsqueezed modulation tensors from(B, 1, dim)to(B, dim), which failed to broadcast with(B, L, dim)when batch_size > 1. - Fix: Removed
squeeze(1)calls, keeping(B, 1, dim)shape for correct broadcasting.
- Applied same cache position save/restore logic to the demo's independent cache management.
- Fixed
mx.array.clip()→mx.clip().
- Verify output quality against PyTorch baseline (same prompt → similar video)
- Create new GitHub repo at
audiohacking/self-forcing-mlx - Push clean codebase to new repo
- Run full multi-step inference and measure performance
- Quantize transformer weights for memory optimization