Text-to-Image
MLX
MiniMax H3
Safetensors
English
apple-silicon
macos
image-to-image
image-editing
image-generation
Instructions to use devin-lai/MiniMax-H3-Image-mlx with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use devin-lai/MiniMax-H3-Image-mlx with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download devin-lai/MiniMax-H3-Image-mlx --local-dir MiniMax-H3-Image-mlx
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Download h3_mlx/encoder.py from devin-lai/MiniMax-H3-Image-mlx: direct link, hf CLI and curl.
- Browser
- Download file 7.25 kB
-
https://huggingface.co/devin-lai/MiniMax-H3-Image-mlx/resolve/main/h3_mlx/encoder.py
- Command line
-
hf download hf://devin-lai/MiniMax-H3-Image-mlx/h3_mlx/encoder.py
-
curl -L -o encoder.py https://huggingface.co/devin-lai/MiniMax-H3-Image-mlx/resolve/main/h3_mlx/encoder.py
7.25 kB
| from __future__ import annotations | |
| import gc | |
| import hashlib | |
| import importlib.metadata | |
| import json | |
| import logging | |
| import os | |
| from pathlib import Path | |
| import mlx.core as mx | |
| from transformers import AutoTokenizer | |
| from .ops import attention, rms_norm, rotate, silu | |
| from .weights import Linear, WeightStore | |
| log = logging.getLogger(__name__) | |
| class TextEncoder: | |
| """Qwen3-VL's first fifty language layers, with no final RMS normalization. | |
| Layers stream one at a time. Conditioning is cached by weights, exact token | |
| presentation and precision policy; it is independent of diffusion seed. | |
| """ | |
| def __init__( | |
| self, path, *, cache_dir="cache", bits=None, group_size=64, query_chunk=0 | |
| ): | |
| self.path = Path(path) | |
| self.config = json.loads((self.path / "config.json").read_text()) | |
| self.text_config = self.config["text_config"] | |
| self.store = WeightStore(self.path) | |
| self.tokenizer = AutoTokenizer.from_pretrained( | |
| str(self.path), local_files_only=True | |
| ) | |
| self.cache_dir = Path(cache_dir) | |
| self.bits, self.group_size, self.query_chunk = bits, group_size, query_chunk | |
| def encode(self, prompt, *, images=(), on_layer=None): | |
| self.cache_hit = False | |
| if not prompt.strip(): | |
| raise ValueError("Prompt must be nonempty") | |
| if images: | |
| from .vision import image_presentation | |
| ids, tags, positions, vision = image_presentation(self, prompt, images) | |
| else: | |
| raw = self.tokenizer.encode(prompt, add_special_tokens=False)[:4096] | |
| ids = raw + [0] * (-len(raw) % 8) | |
| tags = mx.ones(len(raw), mx.int32) | |
| positions = None | |
| vision = None | |
| identity = { | |
| "version": 5, | |
| "weights": self.store.fingerprint(), | |
| "config": self.config, | |
| "versions": { | |
| p: importlib.metadata.version(p) | |
| for p in ("mlx", "numpy", "Pillow", "transformers") | |
| }, | |
| "ids": ids, | |
| "bits": self.bits, | |
| "group_size": self.group_size, | |
| "query_chunk": self.query_chunk, | |
| "tap": 50, | |
| "tf32": os.environ.get("MLX_ENABLE_TF32"), | |
| "images": [ | |
| hashlib.sha256(im.tobytes()).hexdigest() + str(im.size) for im in images | |
| ], | |
| } | |
| if images: | |
| identity["vision_layer_norm_revision"] = 2 | |
| key = hashlib.sha256(json.dumps(identity, sort_keys=True).encode()).hexdigest()[ | |
| :24 | |
| ] | |
| target = self.cache_dir / f"conditioning-{key}.safetensors" | |
| if target.exists(): | |
| data = mx.load(str(target)) | |
| mx.eval(data) | |
| self.cache_hit = True | |
| log.info( | |
| "Conditioning cache hit: %d effective tokens", data["hidden"].shape[0] | |
| ) | |
| return data["hidden"], data["tags"] | |
| prefix = "model.language_model" | |
| x = self.store.get(prefix + ".embed_tokens.weight", rows=ids)[None] | |
| mx.eval(x) | |
| if vision is not None: | |
| from .vision import encode_vision | |
| features, deep = encode_vision( | |
| self.store, self.config["vision_config"], vision | |
| ) | |
| image_indices = mx.array( | |
| [i for i, t in enumerate(ids) if t == self.config["image_token_id"]] | |
| ) | |
| x[0, image_indices] = features | |
| cfg = self.text_config | |
| dim, nq, nk = ( | |
| cfg["head_dim"], | |
| cfg["num_attention_heads"], | |
| cfg["num_key_value_heads"], | |
| ) | |
| if positions is None: | |
| positions = mx.broadcast_to( | |
| mx.arange(len(ids), dtype=mx.float32)[None], (3, len(ids)) | |
| ) | |
| else: | |
| positions = mx.array(positions, mx.float32) | |
| inv = 1 / (cfg["rope_theta"] ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim)) | |
| angles = positions[:, :, None] * inv[None, None] | |
| # Qwen3-VL interleaves spatial axes into the temporal frequency stream. | |
| sections = cfg["rope_scaling"]["mrope_section"] | |
| freq = angles[0] | |
| for axis, offset in ((1, 1), (2, 2)): | |
| stop = sections[axis] * 3 | |
| freq[:, offset:stop:3] = angles[axis, :, offset:stop:3] | |
| freq = mx.concatenate((freq, freq), axis=-1) | |
| rope = ( | |
| mx.cos(freq).astype(mx.bfloat16)[None, :, None], | |
| mx.sin(freq).astype(mx.bfloat16)[None, :, None], | |
| ) | |
| mx.eval(rope) | |
| for i in range(50): | |
| layer = f"{prefix}.layers.{i}" | |
| def linear(suffix, layer=layer): | |
| return Linear.load( | |
| self.store, | |
| layer + "." + suffix, | |
| bits=self.bits, | |
| group_size=self.group_size, | |
| ) | |
| qproj, kproj, vproj = ( | |
| linear("self_attn." + p + "_proj") for p in ("q", "k", "v") | |
| ) | |
| oproj = linear("self_attn.o_proj") | |
| h = rms_norm( | |
| x, | |
| self.store.get(layer + ".input_layernorm.weight"), | |
| cfg["rms_norm_eps"], | |
| rounded=True, | |
| ) | |
| q = qproj(h).reshape(1, len(ids), nq, dim) | |
| k = kproj(h).reshape(1, len(ids), nk, dim) | |
| v = vproj(h).reshape(1, len(ids), nk, dim) | |
| q = rms_norm( | |
| q, | |
| self.store.get(layer + ".self_attn.q_norm.weight"), | |
| cfg["rms_norm_eps"], | |
| rounded=True, | |
| ) | |
| k = rms_norm( | |
| k, | |
| self.store.get(layer + ".self_attn.k_norm.weight"), | |
| cfg["rms_norm_eps"], | |
| rounded=True, | |
| ) | |
| q, k = rotate(q, *rope), rotate(k, *rope) | |
| h = attention(q, k, v, causal=True, query_chunk=self.query_chunk).reshape( | |
| 1, len(ids), -1 | |
| ) | |
| x = x + oproj(h) | |
| mx.eval(x) | |
| del qproj, kproj, vproj, oproj, q, k, v, h | |
| gate, up, down = ( | |
| linear("mlp." + p + "_proj") for p in ("gate", "up", "down") | |
| ) | |
| h = rms_norm( | |
| x, | |
| self.store.get(layer + ".post_attention_layernorm.weight"), | |
| cfg["rms_norm_eps"], | |
| rounded=True, | |
| ) | |
| x = x + down(silu(gate(h)) * up(h)) | |
| if vision is not None and i < len(deep): | |
| x = x.at[0, image_indices].add(deep[i]) | |
| mx.eval(x) | |
| del gate, up, down, h | |
| if on_layer: | |
| on_layer(i, x) | |
| if (i + 1) % 10 == 0: | |
| log.info("Text encoder %d/50", i + 1) | |
| mx.clear_cache() | |
| hidden = x[0, : len(tags)].astype(mx.bfloat16) | |
| mx.eval(hidden) | |
| self.cache_dir.mkdir(parents=True, exist_ok=True) | |
| tmp = target.with_suffix(".tmp.safetensors") | |
| mx.save_safetensors( | |
| str(tmp), | |
| {"hidden": hidden, "tags": tags}, | |
| metadata={"identity": json.dumps(identity)}, | |
| ) | |
| tmp.replace(target) | |
| gc.collect() | |
| mx.clear_cache() | |
| return hidden, tags | |