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/benchmark.py from devin-lai/MiniMax-H3-Image-mlx: direct link, hf CLI and curl.
- Browser
- Download file 6.85 kB
-
https://huggingface.co/devin-lai/MiniMax-H3-Image-mlx/resolve/main/h3_mlx/benchmark.py
- Command line
-
hf download hf://devin-lai/MiniMax-H3-Image-mlx/h3_mlx/benchmark.py
-
curl -L -o benchmark.py https://huggingface.co/devin-lai/MiniMax-H3-Image-mlx/resolve/main/h3_mlx/benchmark.py
6.85 kB
| """Synchronized operator experiments on real H3 dimensions, with numerical error.""" | |
| from __future__ import annotations | |
| import argparse | |
| import gc | |
| import json | |
| import statistics | |
| import time | |
| from pathlib import Path | |
| import mlx.core as mx | |
| from .kernels import norm_modulate, qk_norm_rope | |
| from .ops import attention, modulate, rms_norm, rotate | |
| from .runtime import configure_memory, environment | |
| def measure(fn, *, warmup=2, repeats=7): | |
| for _ in range(warmup): | |
| mx.eval(fn()) | |
| samples = [] | |
| for _ in range(repeats): | |
| mx.synchronize() | |
| tick = time.perf_counter() | |
| mx.eval(fn()) | |
| mx.synchronize() | |
| samples.append((time.perf_counter() - tick) * 1000) | |
| return { | |
| "median_ms": statistics.median(samples), | |
| "min_ms": min(samples), | |
| "max_ms": max(samples), | |
| "samples_ms": samples, | |
| } | |
| def error(reference, candidate): | |
| a, b = reference.astype(mx.float32), candidate.astype(mx.float32) | |
| diff = a - b | |
| return { | |
| "max_abs": mx.max(mx.abs(diff)).item(), | |
| "relative_rmse": mx.sqrt( | |
| mx.mean(diff * diff) / (mx.mean(a * a) + 1e-30) | |
| ).item(), | |
| "equal_fraction": mx.mean((a == b).astype(mx.float32)).item(), | |
| } | |
| def run(output, *, tokens=(256, 4096), repeats=7): | |
| configure_memory() | |
| mx.random.seed(731) | |
| result = { | |
| "environment": environment(), | |
| "warmup": 2, | |
| "repeats": repeats, | |
| "scope": "Synthetic operator microbenchmarks; excludes loading/quantization. Not image latency.", | |
| "trials": [], | |
| } | |
| def record(group, variant, shape, fn, reference): | |
| data = { | |
| "operator": group, | |
| "variant": variant, | |
| "shape": shape, | |
| **measure(fn, repeats=repeats), | |
| **error(reference, fn()), | |
| } | |
| result["trials"].append(data) | |
| print(json.dumps(data), flush=True) | |
| # Persist each completed trial so interrupted sweeps retain evidence. | |
| Path(output).parent.mkdir(parents=True, exist_ok=True) | |
| Path(output).write_text(json.dumps(result, indent=2) + "\n") | |
| for n in tokens: | |
| x = mx.random.normal((n, 5376)).astype(mx.bfloat16) | |
| w = mx.random.normal((5376,)).astype(mx.bfloat16) | |
| shift = (mx.random.normal((3, 5376)) * 0.1).astype(mx.bfloat16) | |
| scale = (mx.random.normal((3, 5376)) * 0.1).astype(mx.bfloat16) | |
| ids = mx.arange(n, dtype=mx.int32) % 3 | |
| mx.eval(x, w, shift, scale, ids) | |
| ref = modulate(x, w, shift, scale, ids) | |
| mx.eval(ref) | |
| record( | |
| "rms_adaln", | |
| "native_rounding", | |
| [n, 5376], | |
| lambda x=x, w=w, shift=shift, scale=scale, ids=ids: modulate( | |
| x, w, shift, scale, ids | |
| ), | |
| ref, | |
| ) | |
| for threads in (128, 256, 512): | |
| record( | |
| "rms_adaln", | |
| f"metal_tg{threads}", | |
| [n, 5376], | |
| lambda x=x, w=w, shift=shift, scale=scale, ids=ids, threads=threads: ( | |
| norm_modulate(x, w, shift, scale, ids, threads=threads) | |
| ), | |
| ref, | |
| ) | |
| qkv = mx.random.normal((n, 3, 56, 128)).astype(mx.bfloat16) | |
| qw = mx.random.normal((128,)).astype(mx.bfloat16) | |
| angles = mx.random.normal((1, n, 1, 48)) | |
| angles = mx.concatenate((angles, angles), axis=-1) | |
| cos, sin = ( | |
| mx.cos(angles).astype(mx.bfloat16), | |
| mx.sin(angles).astype(mx.bfloat16), | |
| ) | |
| def native_qk(qkv=qkv, qw=qw, cos=cos, sin=sin): | |
| return tuple( | |
| rotate(rms_norm(qkv[None, :, i], qw), cos, sin) for i in (0, 1) | |
| ) | |
| def fused_qk(qkv=qkv, qw=qw, cos=cos, sin=sin): | |
| return qk_norm_rope(qkv, qw, qw, cos, sin) | |
| qr, kr = native_qk() | |
| both = mx.concatenate((qr, kr), axis=0) | |
| mx.eval(both) | |
| record( | |
| "qk_norm_rope", | |
| "native", | |
| [n, 56, 128], | |
| lambda fn=native_qk: mx.concatenate(fn(), axis=0), | |
| both, | |
| ) | |
| record( | |
| "qk_norm_rope", | |
| "metal", | |
| [n, 56, 128], | |
| lambda fn=fused_qk: mx.concatenate(fn(), axis=0), | |
| both, | |
| ) | |
| del qkv, qw, angles, cos, sin, qr, kr, both, native_qk, fused_qk | |
| mx.clear_cache() | |
| for k, out in ((5376, 21504), (5376, 28672), (14336, 5376)): | |
| a = mx.random.normal((n, k)).astype(mx.bfloat16) | |
| weight = (mx.random.normal((out, k)) * 0.02).astype(mx.bfloat16) | |
| mx.eval(a, weight) | |
| baseline = a @ weight.T | |
| mx.eval(baseline) | |
| record( | |
| "gemm", | |
| "bf16", | |
| [n, k, out], | |
| lambda a=a, weight=weight: a @ weight.T, | |
| baseline, | |
| ) | |
| for bits in (8, 4): | |
| for size in (32, 64, 128): | |
| q, s, z = mx.quantize(weight, bits=bits, group_size=size) | |
| mx.eval(q, s, z) | |
| record( | |
| "gemm", | |
| f"q{bits}_g{size}", | |
| [n, k, out], | |
| lambda a=a, q=q, s=s, z=z, size=size, bits=bits: ( | |
| mx.quantized_matmul( | |
| a, q, s, z, transpose=True, group_size=size, bits=bits | |
| ) | |
| ), | |
| baseline, | |
| ) | |
| del a, weight, baseline, q, s, z | |
| gc.collect() | |
| mx.clear_cache() | |
| q = mx.random.normal((1, n, 56, 128)).astype(mx.bfloat16) | |
| k = mx.random.normal(q.shape).astype(mx.bfloat16) | |
| v = mx.random.normal(q.shape).astype(mx.bfloat16) | |
| mx.eval(q, k, v) | |
| baseline = attention(q, k, v) | |
| mx.eval(baseline) | |
| for chunk in (0, 128, 512, 1024): | |
| record( | |
| "sdpa", | |
| f"query_chunk_{chunk}", | |
| list(q.shape), | |
| lambda q=q, k=k, v=v, chunk=chunk: attention( | |
| q, k, v, query_chunk=chunk | |
| ), | |
| baseline, | |
| ) | |
| del q, k, v, baseline, x, w, shift, scale, ids, ref | |
| gc.collect() | |
| mx.clear_cache() | |
| return result | |
| def main(): | |
| p = argparse.ArgumentParser(description=__doc__) | |
| p.add_argument("--output", default="reports/operators.json") | |
| p.add_argument("--tokens", type=int, nargs="+", default=[256, 4096]) | |
| p.add_argument("--repeats", type=int, default=7) | |
| a = p.parse_args() | |
| if a.repeats < 1 or any(n < 1 for n in a.tokens): | |
| p.error("tokens and repeats must be positive") | |
| run(a.output, tokens=a.tokens, repeats=a.repeats) | |
| if __name__ == "__main__": | |
| main() | |