Instructions to use MrMofer/MiniMax-H3-MLX-8bit with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use MrMofer/MiniMax-H3-MLX-8bit with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir MiniMax-H3-MLX-8bit MrMofer/MiniMax-H3-MLX-8bit
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
File size: 15,585 Bytes
166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f dd7a16d 166a35f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | # PATCHES.md β MiniMax-H3 MLX 8-bit pipeline modifications (complete port)
All modifications relative to upstream `PipeNetwork/minimax-h3-mlx` base commit `b2f7e4d2b7861cefe68b75e4b59ab81cc4e7c318` (2026-08-10). The first committed patch is `video-lab-8bit-fixes` `7210b93e6df86bf9c7206091c9542b6983c10c30` (2026-08-17, two files); fixes S8/S9/S10 extend the same branch in the working tree (dit.py two-phase, pipeline release + TeaCache hook, text_encoder processor+scatter, teacache.py, generate.py flags, processor/ + vision shards). Everything else is byte-identical to the base commit.
| | |
|---|---|
| Base commit | `b2f7e4d2b7861cefe68b75e4b59ab81cc4e7c318` |
| Branch | `video-lab-8bit-fixes` (+ working-tree S8/S9/S10 on `video-lab`) |
| Patch commit (committed) | `7210b93e6df86bf9c7206091c9542b6983c10c30` |
| Date (committed) | 2026-08-17 |
| Working-tree S8/S9/S10 | 2026-08-18β2026-08-19 (pipeline.py, dit.py extension, teacache.py, text_encoder.py B1/B2, generate.py, processor/ + vision shards) |
| Validated with | Real generations incl. Turbo LoRA merges + I2V encode checks (see README Validation report + tmp/S9_teacache_spike_RESULT.md) |
| License | Port: Apache-2.0 (PipeNetwork). Weights: MiniMax H3 Community License (`LICENSE`). Turbo LoRA: Apache-2.0 (larryvrh). |
---
## (a) `minimax_h3_mlx/dit.py` β DiT attention QKV blocked layout + TeaCache two-phase hooks
**File:** `minimax_h3_mlx/dit.py` (~20 lines β ~230 lines added vs base)
**QKV blocked layout (S7, committed 7210b93, lines ~12β17 docstring + lines 153β165 in `Attention.__call__`):**
- Raw-checkpoint `attn.qkv_proj` rows are **blocked** `[q | k | v]` β three contiguous thirds of width `heads * head_dim` β not per-head interleaved `[h0: q,k,v][h1: q,k,v]...`. The old code reshaped to `(B, S, heads, 3, head_dim)` and indexed `qkv[...,0]` which scrambled heads. Fix splits and reshapes each third independently (same layout `mlx-serve` consumes via `splitEqual(qkv,3)`; video VAE `to_qkv` remains per-head interleaved β different codebase, different convention):
```python
# minimax_h3_mlx/dit.py:160-164 (Attention)
qkv = self.qkv_proj(x)
q, k, v = mx.split(qkv, 3, axis=-1)
q = q.reshape(B, S, self.heads, self.head_dim)
k = k.reshape(B, S, self.heads, self.head_dim)
v = v.reshape(B, S, self.heads, self.head_dim)
```
- Module docstring (lines 12β17, 157β158) now documents the blocked layout and the VAE contrast.
**TeaCache two-phase forward (S9, working-tree, lines ~335β510):**
- New helpers `TYPE_CHECKING` import of `ModulationCache`, `_resolve_cache_layer` (lines 347β354), `_prepare_stages` (355β391, shared input projections + token_refiner + packed buffer + temb + adaln indices so the prefix graph is built exactly once), `_run_blocks` (393β409, iterates `blocks[i]` with optional `ModulationCache.get(i)`), `_finish_heads` (411β423, `final_layer.norm_out` + `video_out`/`audio_out` heads).
- Public two-phase entry points: `forward_to_cache_layer` (425β464, partial forward up to and including `cache_layer` β default last block `len(blocks)-1` β returning `(hidden, ctx)` where `hidden` is pre-final-norm residual after that block and `ctx` carries rotary/temb/adaln/layer bookkeeping) and `finish_from_cache_layer` (466β487, completes from cached `hidden`/`ctx` without recomputing prefix, returns `(video_velocity, audio_velocity)`). `__call__` (489β510) now delegates to `_prepare_stages`/`_run_blocks`/`_finish_heads` and gains `return_hidden` for probing. Saving per skip = suffix `blocks[layer+1..49]` + `final_layer.norm_out` + both heads β with default last block only heads+norm are saved (~<5% wall); moving to block 40 saves ~20% blocks per skip (see S9 spike result: 0/7 skips at last block, no wall saving).
---
## (b) `minimax_h3_mlx/text_encoder.py` β 8-bit quantized loader + processor fallback (B1) + positional scatter (B2)
**File:** `minimax_h3_mlx/text_encoder.py` (+84 lines vs base, ~6 β ~17 imports)
**8-bit quantized loader (S7, committed 7210b93, lines ~64β110 + ~143β180):**
- Detects MLX affine 8-bit pack by presence of `quant_config.json` (line 71: `self.quantized = (model_dir / "quant_config.json").exists()`). Before loading, quantizes the module tree so packed U32/scales/biases key 1:1 β same recipe `load.py` replays from `quant_config.json`:
```python
# lines 90-92
nn.quantize(self.language, group_size=64, bits=8,
class_predicate=lambda _path, m: isinstance(m, nn.Linear))
```
- Vision tower: if `load_vision=True` and `vision_quant_config.json` exists, replays its recorded `quantized` paths (89 tensors) with same group/bits (lines 98β110):
```python
with open(model_dir / "vision_quant_config.json") as fh: vq = json.load(fh)
paths = set(vq.get("quantized", ()))
nn.quantize(self.vision, group_size=vq.get("group_size",64), bits=vq.get("bits",8),
class_predicate=lambda _p, m: isinstance(m, nn.Linear) and _p in paths)
```
- Quantized shards loaded raw (no `astype(dtype)`, lines 166β170: `buckets[bucket][path] = tensor` when `self.quantized` else `tensor.astype(dtype)`), preserving packed integers. Only dense path casts.
- Serve pack omits `model.norm` (never evaluated β H3 conditions pre-norm); loader fabricates zeros like dense converter (lines 176β180):
```python
buckets["language"]["norm.weight"] = mx.zeros(tuple(module.norm.weight.shape), dtype=mx.bfloat16)
```
**Processor property with torch-free fallback (B1, S10, lines 207β233):**
- `@property processor` (lines 207β233) tries `AutoProcessor.from_pretrained(processor_dir)` (full Qwen3VLProcessor with torch video/image sub-processors). On `ImportError` (no torch/torchvision; `Qwen3VLVideoProcessor` hard-requires them, and transformers 5.15 `AutoImageProcessor` is itself gated), falls back to PIL-only `Qwen2VLImageProcessorPil`:
```python
try: self._processor = AutoProcessor.from_pretrained(processor_dir)
except Exception:
try: image_processor = AutoImageProcessor.from_pretrained(processor_dir)
except Exception:
from transformers.models.qwen2_vl.image_processing_pil_qwen2_vl import Qwen2VLImageProcessorPil
image_processor = Qwen2VLImageProcessorPil.from_pretrained(processor_dir)
self._processor = SimpleNamespace(image_processor=image_processor)
```
The facade only ever reads `.image_processor` (`build_request` line 251), tokenizer comes from separate `tokenizer` property (lines 197β205), so image-only processor is sufficient for I2V.
**Encode positional scatter fix (B2, S10, lines 324β344):**
- `encode()` (lines 306β344) previously did `mx.where(image_mask[...,None], hidden[None], inputs_embeds)` which mis-broadcasts `(1,3096,1)` vs `(1,3072,5120)` because vision hidden rows are compact while image pads sit at arbitrary positions in the longer token row β a **positional scatter**, not a broadcast.
- Fix validates `hidden.shape[0] == num_image` (lines 330β334), then does positional REPLACE scatter via `scatter_add` (mlx 0.32 has no `nonzero`/`index_put`, only `at[].add`):
```python
pos_np = np.nonzero(np.array(image_mask))[0] # line 339 (mlx has no nonzero, use numpy)
pos = mx.array(pos_np) # line 340
target = inputs_embeds[0] # line 341
inputs_embeds = target.at[pos].add(hidden.astype(target.dtype) - target[pos]) # line 342
inputs_embeds = inputs_embeds[None] # line 343
```
Validated with `tmp/s10_validate_encode.py` (1,3096,5120) finite, 3074 video-tagged rows (start+3072 pads+end). `build_request` (lines 237β277) tags whole vision block as `TAG_VIDEO` (not text) β the DiT AdaLN key.
---
## (c) `minimax_h3_mlx/pipeline.py` β `release_text_encoder` headroom (S8/S8b) + TeaCache hook (S9) + checkpoint resume
**File:** `minimax_h3_mlx/pipeline.py` (+~205 lines vs base)
**Release text encoder (S8/S8b, lines 59β73 `__init__`, 77β118 `from_pretrained`, 228β331 `__call__`, 470β532 `_release_text_encoder_now`):**
- `MiniMaxH3Pipeline.__init__` gains `release_text_encoder: bool` (line 66, stored as `self._release_text_encoder` line 73). `from_pretrained` threads it (lines 84, 118).
- In `__call__` (lines 328β331), right after `prompt_embeds` built (line 1, text conditioning + vision rows already encoded, step-4 noise already drawn β trajectory fixed), opt-in `release_text_encoder or self._release_text_encoder` drops the encoder:
```python
if (release_text_encoder or self._release_text_encoder) and getattr(self, "text_encoder", None) is not None:
self._release_text_encoder_now()
```
- `_release_text_encoder_now` (lines 470β532) drops `self.text_encoder` (`del` + `gc.collect()` pattern), sizes correctly via `mlx.utils.tree_flatten` (lines 484,496,510 β previous `module.parameters().values()` summed dicts not arrays β 0.0 GB), calls `mx.clear_cache()` to return unified Metal memory (line 528), logs `released text encoder after conditioning (freeing ~N GB)` (line 532). Second call is no-op (line 471 guard). Safe: inner estimator errors just leave freed-size as "(sizing indeterminado)" while delete still happens. Measured: frees ~27.5 GB / ~22 GB resident before denoise loop; pipeline then only touches DiT+VAEs.
- CLI: `scripts/generate.py` `--release-encoder` (line 56) β `pipeline(release_text_encoder=args.release_encoder)` (line 96).
- Checkpoint resume (bonus, lines 371β389): `resume_from`/`checkpoint_dir` floats inside the loop (not a pipeline-level patch for HF, but present in `pipeline.py` working tree).
**TeaCache hook (S9, lines 43, 228β230, 342β410):**
- Imports `TeaCacheConfig/TeaCacheController` (line 43), `__call__` gains `teacache`, `teacache_config`, `teacache_layer` (lines 228β230 docstring 244β252). Controller instantiated when `teacache=True` (lines 342β350, resolves `tc_layer = num_layers-1 if None else int`, validates range). Per-step two-phase: `hidden, ctx = dit.forward_to_cache_layer(..., cache_layer=tc_layer)` β `mx.eval(hidden)` β `skip = controller.decide(i, hidden)` β on skip reuse `cached_preds = (video_pred, audio_pred)` joint (lines 391β399), else `finish_from_cache_layer` and cache preds (lines 406β410). Log: `step N/T teacache skip (rel_l1=..., thresh=...)` (lines 398β401). Default last-block hook measured **0/7 skips** at 768x448 turbo8 (see teacache.py below β OFF recommended).
**Other:** `load.py` one-line import fix for quantized path (line 1 added), `scripts/generate.py` adds `--teacache*`/`--release-encoder` flags (lines 45β56, 85β96).
---
## (d) `minimax_h3_mlx/teacache.py` β two-phase TeaCache controller (S9, new file)
**File:** `minimax_h3_mlx/teacache.py` (11 KB, new β 223 lines)
- **Intent:** feature-caching (ByteDance/ali-vilab line) β each step runs partial forward to a probe hidden, compares against previous step's feature, reuses previous `(video, audio)` velocity if similar. Joint reuse (both modalities from one forward must be reused together). Noise/keyframe sampling untouched β seed still fixes trajectory.
- **`TeaCacheConfig` (lines 40β71):** `rel_l1_thresh=0.2` (default, ref 0.15 for 10s 544x960), `metric='rel_l1'|'cosine'`, `start_at_step=3`, `compute_last_step=True` (protects first 3 and last step), `cache_type='avg'|'last'`. `__post_init__` validates.
- **Pure helpers (lines 87β134):** `relative_l1_distance` (mean|curr-prev|/mean|prev|, lines 87β100), `cosine_similarity` (lines 103β113), `teacache_gate` (1-min(dist,1) for rel_l1 so higher=more similar, lines 115β124), `teacache_should_skip` (lines 126β134: `rel_l1 <= thresh` or `cosine >= thresh`).
- **`TeaCacheController` (lines 148β242):** stateful per-run (`total_steps`, `cached_feature`, `computed/skipped`, `by_step`). `decide(step_index, feature)` (lines 171β210): `must_compute` if `step==0` or `step<start` or `cache is None` or `last step` β prime cache; else compute `metric_value` (cosine or rel_l1 distance), `skip = metric >=/<= thresh`, smoothing `avg` folds current into cache as `(prev+curr)/2` on skip, else replaces. Stats: `last_gate` (similarity-domain), `last_metric` (raw distance/similarity for honest log), `metric_name`, `to_stats()`.
- **Pipeline integration:** see (c) above; `dit.py` hooks provide the probe feature. **Measurement (M4 Max, 768x448 turbo8, 7 forwards):** default last-block `thresh 0.2` β 0 skips (+9.7% overhead), `0.35` β 1/7 skip (rel_l1 0.297) still no wall saving (partial forward already 49/50 blocks, only `final_layer.norm_out`+heads saved). `tmp/S9_teacache_spike_RESULT.md` β **verdict: TeaCache OFF by default**; aggressive needs earlier hook `layer 40` + `thresh 0.25β0.35` for double-digit % (trades quality).
---
## (e) `processor/` + vision shards (S10 I2V)
**Files added to checkpoint (not code patches but data required for I2V):**
- `processor/` (7 files, ~11.6 MB): `preprocessor_config.json` (390 B), `video_preprocessor_config.json` (385 B), `chat_template.json` (5.5 KB), `tokenizer_config.json` (11 KB), `tokenizer.json` (6.7 MB), `vocab.json` (2.6 MB), `merges.txt` (1.6 MB) β copied from `models/minimax-h3-ckpt/processor/` (`Qwen3VLProcessor`, `Qwen2VLImageProcessorFast` + `Qwen3VLVideoProcessor` configs; runtime uses PIL fallback per (b) B1).
- `text_encoder/model-00005-of-00008-vision.safetensors` (291,980,081 B), `model-00006-of-00008-vision.safetensors` (341,371,441 B), `model-00007-of-00008-vision.safetensors` (129,267,789 B) β 529 `model.visual.*` tensors (vision tower: depth 27, hidden 1152, patch 16, merge 2, out 5120). Built by `scripts/rebuild_h3_text_encoder_vision_8bit.py` which transposes `patch_embed.proj.weight` from torch `(O,C,kD,kH,kW)` to MLX channels-last and quantizes 89 paths.
- `text_encoder/vision_quant_config.json` (2,461 B, 89 quantized paths: `blocks.*.attn.qkv/proj`, `blocks.*.mlp.linear_fc1`, `merger.*`, `deepstack_merger_list.*`).
**Why this matters:** without `processor/` the pipeline cannot build `pixel_values`/`image_grid_thw` (I2V falls back to T2V). Without vision shards `text_encoder.encode(prompt, images)` raises `load_vision=False` or missing-tensor error. With both, `encode()` (b-B2) scatters 3072 vision patches into `inputs_embeds` and hits the DiT as `TAG_VIDEO` rows, then `pipeline._encode_keyframes` patches conditioning latents β FL2VA keyframe path (anchors `first`/`last`) validated end-to-end (see README `## Image-to-Video`).
---
## Other minor port fixes
- `minimax_h3_mlx/load.py`: quantization structure replay for DiT (already in committed 7210b93) β one-line import guard retained.
- `scripts/generate.py`: flags `--teacache`, `--teacache-thresh` (default 0.2), `--teacache-metric` (rel_l1/cosine), `--teacache-layer`, `--teacache-start` (default 3), `--release-encoder` (all S8/S9).
- `tests/test_release_text_encoder.py`, `tests/test_teacache.py`: unit/mocked tests for (c)/(d) (not shipped in HF package, kept in repo).
## Reproducing the diff
```bash
git -C models/minimax-h3-mlx diff b2f7e4d2 -- minimax_h3_mlx/dit.py minimax_h3_mlx/text_encoder.py minimax_h3_mlx/pipeline.py minimax_h3_mlx/teacache.py scripts/generate.py
# committed two-file patch:
git -C models/minimax-h3-mlx show 7210b93 --stat
```
## License
- **Weights**: MiniMax H3 Community License (included as `LICENSE`) β not open source; territorial exclusions apply (EU/UK/KR/US excluded per mlx-serve pack note).
- **Port code**: Apache-2.0 (PipeNetwork/minimax-h3-mlx).
- **Turbo LoRA**: Apache-2.0 (larryvrh/MiniMax-H3-Turbo-Lora).
|