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 tests/test_numerics.py from devin-lai/MiniMax-H3-Image-mlx: direct link, hf CLI and curl.
- Browser
- Download file 6.78 kB
-
https://huggingface.co/devin-lai/MiniMax-H3-Image-mlx/resolve/main/tests/test_numerics.py
- Command line
-
hf download hf://devin-lai/MiniMax-H3-Image-mlx/tests/test_numerics.py
-
curl -L -o test_numerics.py https://huggingface.co/devin-lai/MiniMax-H3-Image-mlx/resolve/main/tests/test_numerics.py
6.78 kB
| import json | |
| import mlx.core as mx | |
| import numpy as np | |
| import pytest | |
| import torch | |
| import torch.nn.functional as F | |
| from h3_mlx.config import canvas_size, load_sigmas | |
| from h3_mlx.layout import image_positions, patchify, unpatchify | |
| from h3_mlx.ops import attention, layer_norm, modulate, rms_norm, rotate, silu | |
| from h3_mlx.weights import Linear, WeightStore | |
| def array(t): | |
| return mx.array(t.float().numpy()).astype( | |
| mx.bfloat16 if t.dtype == torch.bfloat16 else mx.float32 | |
| ) | |
| def numpy(x): | |
| return np.asarray(x.astype(mx.float32)) | |
| def assert_bf16_near(actual, expected, *, equality=0.9999): | |
| a, b = numpy(actual), expected.float().numpy() | |
| # CPU/Metal reduction orders may cross a BF16 rounding midpoint. Require | |
| # one ULP at most, while detecting the much larger wrong-arithmetic case. | |
| aa = np.asarray(actual.view(mx.uint16)).astype(np.int32) | |
| bb = expected.view(torch.int16).numpy().view(np.uint16).astype(np.int32) | |
| assert np.max(abs(aa - bb)) <= 1 | |
| assert np.mean(a == b) >= equality | |
| def test_distinct_aten_and_qwen_rms_rounding(width): | |
| gen = torch.Generator().manual_seed(9) | |
| x = torch.randn(17, width, generator=gen).bfloat16() | |
| w = torch.randn(width, generator=gen).bfloat16() | |
| aten = F.rms_norm(x, (width,), w, 1e-5) | |
| qwen = ( | |
| x.float() * torch.rsqrt(x.float().square().mean(-1, keepdim=True) + 1e-5) | |
| ).bfloat16() * w | |
| assert_bf16_near(rms_norm(array(x), array(w)), aten) | |
| assert_bf16_near(rms_norm(array(x), array(w), rounded=True), qwen) | |
| assert not torch.equal(aten, qwen) | |
| def test_bf16_bias_epilogue_and_silu(): | |
| torch.manual_seed(21) | |
| x, w, b = ( | |
| torch.randn(13, 128).bfloat16(), | |
| torch.randn(256, 128).bfloat16(), | |
| torch.randn(256).bfloat16(), | |
| ) | |
| layer = Linear(array(w), array(b)) | |
| assert_bf16_near(layer(array(x)), F.linear(x, w, b), equality=0.998) | |
| np.testing.assert_array_equal(numpy(silu(array(x))), F.silu(x).float().numpy()) | |
| def test_attention_dense_gqa_and_causal_chunks(): | |
| torch.manual_seed(8) | |
| q, k, v = ( | |
| torch.randn(1, 37, 8, 128), | |
| torch.randn(1, 37, 2, 128), | |
| torch.randn(1, 37, 2, 128), | |
| ) | |
| ref = F.scaled_dot_product_attention( | |
| q.transpose(1, 2), | |
| k.transpose(1, 2), | |
| v.transpose(1, 2), | |
| is_causal=True, | |
| enable_gqa=True, | |
| ).transpose(1, 2) | |
| for chunk in (0, 11): | |
| out = attention(array(q), array(k), array(v), causal=True, query_chunk=chunk) | |
| np.testing.assert_allclose(numpy(out), ref.numpy(), atol=2e-6, rtol=2e-5) | |
| def test_rotary_keeps_passthrough_and_bf16_products(): | |
| torch.manual_seed(41) | |
| x = torch.randn(1, 17, 3, 128).bfloat16() | |
| angles = torch.randn(1, 17, 1, 48) | |
| angles = torch.cat((angles, angles), dim=-1) | |
| cos, sin = angles.cos().bfloat16(), angles.sin().bfloat16() | |
| a = x[..., :96] | |
| ref = torch.cat( | |
| (a * cos + torch.cat((-a[..., 48:], a[..., :48]), -1) * sin, x[..., 96:]), -1 | |
| ) | |
| np.testing.assert_array_equal( | |
| numpy(rotate(array(x), array(cos), array(sin))), ref.float().numpy() | |
| ) | |
| def test_layer_norm_affine_does_not_round_normalized_input(): | |
| torch.manual_seed(98) | |
| x = torch.randn(17, 1024).bfloat16() | |
| w, b = torch.randn(1024).bfloat16(), torch.randn(1024).bfloat16() | |
| ref = F.layer_norm(x, (1024,), w, b, 1e-5) | |
| assert_bf16_near(layer_norm(array(x), array(w), array(b)), ref, equality=0.999) | |
| def test_safetensors_bf16_bit_loading_and_grouped_qkv(tmp_path): | |
| from safetensors.torch import save_file | |
| tensor = torch.arange(2 * 3 * 4 * 8, dtype=torch.float32).reshape(24, 8).bfloat16() | |
| save_file({"qkv.weight": tensor}, str(tmp_path / "test.safetensors")) | |
| store = WeightStore(tmp_path) | |
| np.testing.assert_array_equal( | |
| numpy(store.get("qkv.weight")), tensor.float().numpy() | |
| ) | |
| layer = Linear.load(store, "qkv", grouped_heads=(2, 4)) | |
| ref = tensor.reshape(2, 3, 4, 8).permute(1, 0, 2, 3).reshape(24, 8) | |
| np.testing.assert_array_equal(numpy(layer.weight), ref.float().numpy()) | |
| store.clear() | |
| def test_patch_order_matches_native_einsum(): | |
| x = torch.arange(24 * 8 * 10, dtype=torch.float32).reshape(1, 24, 8, 10) | |
| ref = torch.einsum("nchpwq->nhwcpq", x.reshape(1, 24, 4, 2, 5, 2)).reshape(-1, 96) | |
| rows = patchify(array(x)) | |
| np.testing.assert_array_equal(numpy(rows), ref.numpy()) | |
| np.testing.assert_array_equal(numpy(unpatchify(rows, 8, 10)), x.numpy()) | |
| def test_positions_are_native_ref2va_grid(): | |
| out = numpy(image_positions(7, 8, 12, [(4, 6)])) | |
| assert out.shape == (7 + 6 + 24, 3) | |
| np.testing.assert_array_equal(out[:7, 0], np.arange(7)) | |
| np.testing.assert_array_equal(out[7:13, 0], 7) | |
| np.testing.assert_array_equal(out[13:, 0], 8) | |
| np.testing.assert_allclose(out[13:, 1].min(), (1 - 8 / np.sqrt(96)) * 16, rtol=1e-6) | |
| def test_native_schedule_and_canvas_validation(tmp_path): | |
| assert canvas_size(1024, 1024) == (2048, 2048) | |
| assert canvas_size(512, 768, 1024) == (512, 768) | |
| for width, height in [(0, 2048), (512, 0), (-1, 32)]: | |
| with pytest.raises(ValueError): | |
| canvas_size(width, height) | |
| (tmp_path / "pdd_config.json").write_text(json.dumps({"sigmas": [1, 0.5, 0]})) | |
| with pytest.raises(ValueError, match="six-step"): | |
| load_sigmas(tmp_path) | |
| def test_metal_modulation_with_odd_row_count(threads): | |
| from h3_mlx.kernels import norm_modulate | |
| mx.random.seed(71) | |
| x = mx.random.normal((65, 5376)).astype(mx.bfloat16) | |
| w = mx.random.normal((5376,)).astype(mx.bfloat16) | |
| s = (mx.random.normal((3, 5376)) * 0.1).astype(mx.bfloat16) | |
| ids = mx.arange(65) % 3 | |
| ref = modulate(x, w, s, s, ids) | |
| out = norm_modulate(x, w, s, s, ids, threads=threads) | |
| a, b = numpy(ref), numpy(out) | |
| # Different parallel reduction orders are allowed only within one BF16 ULP. | |
| assert np.sqrt(np.mean((a - b) ** 2) / (np.mean(a * a) + 1e-20)) < 1e-4 | |
| assert np.mean(a == b) > 0.9999 | |
| def test_fused_qk_norm_rope_layout_and_rounding(): | |
| from h3_mlx.kernels import qk_norm_rope | |
| mx.random.seed(33) | |
| qkv = mx.random.normal((67, 3, 7, 128)).astype(mx.bfloat16) | |
| w = mx.random.normal((128,)).astype(mx.bfloat16) | |
| angles = mx.random.normal((1, 67, 1, 48)) | |
| angles = mx.concatenate((angles, angles), axis=-1) | |
| cos, sin = mx.cos(angles).astype(mx.bfloat16), mx.sin(angles).astype(mx.bfloat16) | |
| actual = qk_norm_rope(qkv, w, w, cos, sin) | |
| for i, out in enumerate(actual): | |
| ref = rotate(rms_norm(qkv[None, :, i], w), cos, sin) | |
| a, b = numpy(ref), numpy(out) | |
| assert np.sqrt(np.mean((a - b) ** 2) / (np.mean(a * a) + 1e-20)) < 1e-4 | |
| assert np.mean(a == b) > 0.9999 | |