[feat] Add MiniMax H3 MLX T2VA inference - #1770
Conversation
Merge Protections🔴 1 of 1 protections blocking · waiting on 👀 reviews and 🤖 CI
🔴 PR merge requirementsWaiting for
This rule is failing.
|
3359ec0 to
41d57b5
Compare
Pre-commit checks failedHi @aryan5v, the pre-commit checks have failed. To fix them locally: # Install pre-commit if you haven't already
uv pip install pre-commit
pre-commit install
# Run all checks and auto-fix what's possible
pre-commit run --all-filesCommon fixes:
After fixing, commit and push the changes. The checks will re-run automatically. For future commits, |
SolitaryThinker
left a comment
There was a problem hiding this comment.
Review — MiniMax H3 MLX T2VA inference
Overview
Adds a native Apple Silicon (MLX) text-to-video-with-audio path for MiniMax H3: a packed joint audio/video DiT with dual rectified-flow schedulers and an AdaLN precompute cache, a layer-streamed Qwen3-VL conditioner, native video/audio VAE decoders, a phased pipeline (condition → denoise → decode → mux), temporal --fast mode via RIFE, a checkpoint converter (INT8/INT6/INT4), docs, and parity tests against the upstream PyTorch reference. Scope is deliberately narrow (T2VA only) and honestly documented.
Overall: high quality. The memory engineering is thoughtful and explained in place (per-block mx.eval with the observed OOM rationale, streamed shard loading, one-heavyweight-component-at-a-time phasing), formats are versioned (H3_FORMAT_VERSION, prompt-cache version), the mux writes atomically via tmp+rename, quantization refuses to silently fall back, and the parity-test strategy (tiny random-weight models vs. the torch reference, plus real-weight bounded segments) carries the numerical-correctness burden well. Note: this review is static — I did not run the MLX/Metal paths.
Issues
Correctness / UX
--steps≠ 4 crashes with a rawKeyErroron every converted checkpoint. The converter always builds the AdaLN cache and drops the projection weights (convert_minimax_h3_mlx.pypassesadaln_cache_timesteps;_flatten_h3_weightsskips theNoneentries, so saved checkpoints have noadaln_proj.linear.*). When the pipeline sees a ladder mismatch it callsdit.precompute_adaln(union, drop_weights=True)(minimax_h3_pipeline.pydenoise()), which hitsblock["adaln_proj.linear.weight"]in_adaln_tables→KeyError: 'adaln_proj.linear.weight'— after ~80 s of conditioning and a full DiT load. Since the example advertises--steps, this deserves a first-class error ("checkpoint exported with a fixed 4-step AdaLN ladder; use --steps 4 or re-export") raised before any heavy work, or ideally inparse_args/pipeline preflight by reading the checkpoint manifest.- Late-failure preflight gaps.
ffmpegavailability is checked only inmux()— i.e., after 10–15 minutes of generation. Similarly,--fastwith the defaultfast_sharpen=0.6importscv2only after denoise + decode + RIFE (_sharpen_frames)._validate_inputs_before_loading()already validates model files up front; extend that pattern: check ffmpeg atgenerate()entry, and cv2/RIFE weights whenfastis requested. Also worth confirming opencv is a declared dependency of the MLX extra. mlx_h3_audio_vae_from_dirsilently ignores itsstorage_dtypeparameter — it's accepted but never forwarded tomlx_h3_audio_vae_from_file. The pipeline doesn't pass it so there's no behavior bug today, but a caller passingbf16gets fp32 with no warning. Honor it or remove it.MiniMaxH3StepCache.positions()can raiseIndexErrorinstead of its intendedValueError:np.searchsortedreturnslen(timesteps)for a value above the cached max, soself.timesteps[positions]goes out of bounds before the friendly mismatch message fires. Clamp or pre-check.
Robustness (minor)
- Prompt-cache writes aren't atomic (
encode_prompt): a crash mid-np.savezleaves a corrupt.npzthat every future run will try to load (and fail on) since the key is content-hashed. The mux already does tmp+replacecorrectly — same pattern would fit here. Relatedly, an ffmpeg failure leaves.tmp.mp4/.tmp.wavbehind. metal_wired_limit_gib=30.0is a fixed default regardless of device memory; consider deriving frommx.metal.device_info()(nit — docs already gate this at the 36 GB tier).
Performance (nits, non-blocking)
_reflect_pad_axis(minimax_h3_video_vae.py) concatenates one single-indexmx.takeper output index — including the entire unpadded body — so it's O(H) kernel launches per pad. Decode doesn't hit it (encoder-only), but it will hurt the future Ref2VA/encode path; concatenating[left_reflection, x, right_reflection]in 3 pieces (like the audio module's_replicate_pad) avoids it._ShardIndex.get/get_rowre-open and re-parse the safetensors JSON header on every call — once per token for embeddings and once per weight per layer. Caching the parsed header per shard path is a one-liner and should trim the 77–86 s conditioning phase.
Dead / vestigial code
mlx_h3_bf16_forward_streamed_from_diffusers_safetensors(~150 lines) is not called by any test or the pipeline — presumably the manual real-weight validation harness. Either wire a (skippable) test to it or move it out of the production module.- Also unused:
resolve_canvas_size, thecompileparam /_enable_compile/_compiled_forwardmachinery (never read in either forward),ACTIVATION_KERNEL_SIZEand theactivation_ratioproperty (audio VAE),_ShardIndex.has/keys_with_prefix, theimport mlx.core as _mx # noqaline inmlx_h3_audio_vae_from_dir, thetoken_pad()helper that ignores its argument and returns 0 (test_mlx_video_vae_parity.py), and apparently-unused imports in the parity test (AUDIO_TIMESTEP,VIDEO_TIMESTEP,build_schedulers). - Hardcoded model dims in the pipeline — noise shapes
(rows, 96)/(rows, 32), decode channels24,//16spatial ratio — are all derivable from the DiT/VAE configs already loaded. Fine for a fixed model, but one config change away from silent shape drift; at minimum assert againstdit.patch_dim/vae.latent_channels. - Style nit: the double-conditional in
_validate_inputs_before_loading(if not any(...) if vae_dir.exists() else True:) is a precedence puzzle;if not (vae_dir.exists() and any(...))reads instantly.
Test coverage
Strong where it matters most (DiT forward, scheduler, packing, AdaLN cache-vs-faithful, INT8 SNR, VAE primitives + real-weight bounded segments, RIFE frame-count math, geometry contracts). Gaps worth closing:
- No
save_mlx_h3_checkpoint→load_mlx_h3_checkpointround-trip test. This format is the shipping artifact (quantized rebuild, manifest versioning, AdaLN-cache persistence), it's pure logic that runs on the tiny fixture in CI, and a regression here bricks every user's converted checkpoint. A tiny round-trip (save quantized DiT + cache → load → compareforward_with_cacheoutput) would also have surfaced the--steps ≠ 4KeyError. - No test exercises
denoise()/generate()orchestration (understandable given hardware needs, but the steps-mismatch path above shows the cost). tests/local_tests/real-weight cases gate on a local snapshot — good that they self-skip; note they'll be perpetually skipped in CI, so the FP32 acceptance claims rest on local runs.
Security
No concerns: safetensors only (no pickle), np.load without allow_pickle, subprocess.run with list args and no shell, AutoTokenizer.from_pretrained on local dirs without trust_remote_code. The hand-rolled safetensors header parser trusts header_len from the file, but inputs are user-supplied local checkpoints — acceptable.
Conventions
Matches the repo: SPDX headers, init_logger, yapf/ruff/mypy per the pre-commit run, pytest.importorskip gating, and fastvideo.mlx_runtime stays importable without MLX (the pipeline lazily imports the module-level-mx VAE/conditioner modules — works, though the inconsistency with minimax_h3.py's function-level import mlx.core blocks is worth a follow-up cleanup). Docs/support-matrix updates and the MiniMax license note are appreciated.
Verdict
Approve with minor changes. Nothing here is architecturally wrong and the numerics are well-gated. Before merge: (1) a clear, early error for --steps ≠ 4 on AdaLN-dropped checkpoints, (2) fail-fast preflight for ffmpeg/cv2 in generate(), (3) fix or drop the ignored storage_dtype, and (4) a checkpoint save/load round-trip test. The rest (atomic cache writes, dead-code trim, header caching, reflect-pad perf) can be follow-ups.
🤖 Review generated with Claude Code
Fixed 5 file(s) based on 5 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
b7f5808 to
35840f1
Compare
|
Addressed the requested pre-merge changes in
Validation: The non-blocking cleanup and broader follow-up suggestions remain outside this PR's narrow T2VA scope. |
|
Pushed the remaining cleanup and performance review items in
Validation: |
Purpose
Add MiniMax H3 Preview text-to-video-with-audio inference to FastVideo's existing Apple Silicon MLX runtime.
The initial scope is intentionally narrow: T2VA baseline generation plus temporal
--fast. FL2VA, Ref2VA, spatial fast mode, two-pass refinement, VSA, andVideoGeneratorregistry dispatch remain follow-up work.Changes
Test Plan
Test Results
Validated locally on an Apple M4 Max with 36 GB unified memory.
--fastThe two runs use different resolutions, so their wall times are not a same-resolution speed comparison. Temporal fast mode reduced the 124-frame request from 37 target video latent frames to 22 denoised video latent frames while retaining all 207 audio latent frames.
Focused test output
SSIM: Not run. There is no MiniMax H3 MLX reference artifact in the SSIM suite yet. The PR instead includes independent PyTorch/MLX DiT, scheduler, packing, conditioner, and real-weight bounded VAE parity gates plus two end-to-end media runs.
Checklist
For model/pipeline changes, also check: