Files

3226 lines
110 KiB
Python

# Copyright © 2024 Apple Inc.
import copy
import importlib
import unittest
import mlx.core as mx
import mlx.nn as nn
from mlx.utils import tree_flatten, tree_map
from mlx_lm.models import rope_utils
from mlx_lm.models.base import create_causal_mask, scaled_dot_product_attention
from mlx_lm.models.cache import KVCache, RotatingKVCache, make_prompt_cache
from mlx_lm.models.gated_delta import (
gated_delta_kernel,
gated_delta_ops,
gated_delta_update,
)
from mlx_lm.models.ssm import ssm_attn, ssm_update
class TestModels(unittest.TestCase):
def test_kv_cache(self):
cache = KVCache()
k = mx.ones((1, 4, 1, 32), mx.float16)
v = mx.ones((1, 4, 1, 32), mx.float16)
k_up, v_up = cache.update_and_fetch(k, v)
self.assertTrue(mx.array_equal(k_up, k))
self.assertTrue(mx.array_equal(v_up, v))
self.assertEqual(cache.offset, 1)
k = mx.ones((1, 4, cache.step, 32), mx.float16)
v = mx.ones((1, 4, cache.step, 32), mx.float16)
k_up, v_up = cache.update_and_fetch(k, v)
expected = mx.ones((1, 4, cache.step + 1, 32), mx.float16)
self.assertTrue(mx.array_equal(k_up, expected))
self.assertTrue(mx.array_equal(v_up, expected))
self.assertEqual(cache.offset, cache.step + 1)
def test_rotating_kv_cache(self):
b, h, d = 1, 2, 32
cache = RotatingKVCache(max_size=8)
k = mx.random.uniform(shape=(b, h, 2, d))
v = mx.random.uniform(shape=(b, h, 2, d))
k_up, v_up = cache.update_and_fetch(k, v)
self.assertTrue(mx.array_equal(k_up, k))
self.assertTrue(mx.array_equal(v_up, v))
self.assertEqual(cache.offset, 2)
k = mx.random.uniform(shape=(b, h, 5, d))
v = mx.random.uniform(shape=(b, h, 5, d))
k_up, v_up = cache.update_and_fetch(k, v)
self.assertTrue(mx.array_equal(k_up[..., 2:, :], k))
self.assertTrue(mx.array_equal(v_up[..., 2:, :], v))
k = mx.random.uniform(shape=(b, h, 4, d))
v = mx.random.uniform(shape=(b, h, 4, d))
k_up, v_up = cache.update_and_fetch(k, v)
self.assertTrue(mx.array_equal(k_up[..., -4:, :], k))
self.assertTrue(mx.array_equal(v_up[..., -4:, :], v))
idx = 0
for _ in range(10):
k = mx.random.uniform(shape=(b, h, 1, d))
v = mx.random.uniform(shape=(b, h, 1, d))
k_up, v_up = cache.update_and_fetch(k, v)
self.assertTrue(mx.array_equal(k_up[..., idx : idx + 1, :], k))
self.assertTrue(mx.array_equal(v_up[..., idx : idx + 1, :], v))
idx += 1
idx %= 8
# Try with nonzero keep
cache = RotatingKVCache(max_size=8, keep=2)
# Check a large update
k = mx.random.uniform(shape=(b, h, 20, d))
v = mx.random.uniform(shape=(b, h, 20, d))
k_up, v_up = cache.update_and_fetch(k, v)
self.assertTrue(mx.array_equal(k_up, k))
self.assertTrue(mx.array_equal(v_up, v))
# A bunch of small updates
self.assertEqual(cache.offset, 20)
idx = 2
for i in range(10):
k = mx.random.uniform(shape=(b, h, 1, d))
v = mx.random.uniform(shape=(b, h, 1, d))
k_up, v_up = cache.update_and_fetch(k, v)
self.assertTrue(mx.array_equal(k_up[..., idx : idx + 1, :], k))
self.assertTrue(mx.array_equal(v_up[..., idx : idx + 1, :], v))
self.assertEqual(cache.offset, 21 + i)
idx += 1
if idx >= 8:
idx = 2
def test_rotating_kv_cache_chat_mode(self):
# Test that the rotating kv cache can handle
# alternating prompt/prefill with generation
d = 4
h = 2
cache = RotatingKVCache(max_size=18)
x = mx.random.uniform(shape=(1, h, 8, d))
k, v = cache.update_and_fetch(x, x)
self.assertEqual(k.shape[2], 8)
self.assertEqual(cache.offset, 8)
x = mx.random.uniform(shape=(1, h, 1, d))
k, v = cache.update_and_fetch(x, x)
self.assertEqual(k.shape[2], 9)
self.assertEqual(cache.offset, 9)
self.assertTrue(mx.allclose(x, k[..., 8:9, :]))
x = mx.random.uniform(shape=(1, h, 2, d))
k, v = cache.update_and_fetch(x, x)
self.assertEqual(k.shape[2], 11)
self.assertEqual(cache.offset, 11)
self.assertTrue(mx.allclose(x, k[..., 9:11, :]))
x = mx.random.uniform(shape=(1, h, 3, d))
k, v = cache.update_and_fetch(x, x)
self.assertEqual(k.shape[2], 14)
self.assertEqual(cache.offset, 14)
self.assertTrue(mx.allclose(x, k[..., 11:14, :]))
x = mx.random.uniform(shape=(1, h, 6, d))
k, v = cache.update_and_fetch(x, x)
self.assertEqual(cache.offset, 20)
self.assertTrue(mx.allclose(x, k[..., -6:, :]))
x = mx.random.uniform(shape=(1, h, 2, d))
k, v = cache.update_and_fetch(x, x)
self.assertEqual(cache.offset, 22)
self.assertTrue(mx.allclose(x, k[..., -2:, :]))
def test_causal_mask_padding(self):
right_padding = mx.array([2, 1, 0])
mask = create_causal_mask(3, right_padding=right_padding)
causal_mask = create_causal_mask(3)
self.assertTrue(
mx.array_equal(mask[0, 0], causal_mask & mx.array([True, False, False]))
)
self.assertTrue(
mx.array_equal(mask[1, 0], causal_mask & mx.array([True, True, False]))
)
self.assertTrue(mx.array_equal(mask[2, 0], causal_mask))
left_padding = mx.array([2, 1, 0])
mask = create_causal_mask(3, left_padding=left_padding)
self.assertTrue(
mx.array_equal(mask[0, 0], causal_mask & mx.array([False, False, True]))
)
self.assertTrue(
mx.array_equal(mask[1, 0], causal_mask & mx.array([False, True, True]))
)
self.assertTrue(mx.array_equal(mask[2, 0], causal_mask))
def test_mask_with_window(self):
mask = create_causal_mask(5, 0, window_size=3)
expected_sums = mx.array([1, 2, 3, 3, 3])
sums = mask.sum(axis=1)
self.assertTrue(mx.array_equal(sums, expected_sums))
mask = create_causal_mask(5, 1, window_size=3)
self.assertEqual(mask.shape, (5, 6))
expected_sums = mx.array([2, 3, 3, 3, 3])
sums = mask.sum(axis=1)
self.assertTrue(mx.array_equal(sums, expected_sums))
mask = create_causal_mask(5, 2, window_size=3)
self.assertEqual(mask.shape, (5, 7))
expected_sums = mx.array([3, 3, 3, 3, 3])
sums = mask.sum(axis=1)
self.assertTrue(mx.array_equal(sums, expected_sums))
def test_llama_model_sliding_attention(self):
from mlx_lm.models import llama
args = llama.ModelArgs(
model_type="llama",
hidden_size=64,
num_hidden_layers=4,
intermediate_size=256,
num_attention_heads=8,
num_key_value_heads=4,
rms_norm_eps=1e-5,
vocab_size=128,
sliding_window=4,
layer_types=[
"full_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
],
tie_word_embeddings=False,
rope_theta=10000.0,
)
model = llama.Model(args)
tokens = mx.array([[1, 2, 3, 4, 5]], dtype=mx.int32)
out = model(tokens)
mx.eval(out)
self.assertEqual(out.shape, (1, 5, args.vocab_size))
caches = model.make_cache()
self.assertIsInstance(caches[0], KVCache)
self.assertIsInstance(caches[1], RotatingKVCache)
self.assertIsInstance(caches[2], RotatingKVCache)
self.assertIsInstance(caches[3], KVCache)
caches = model.make_cache()
step = model(tokens[:, :2], cache=caches)
mx.eval(step)
step = model(tokens[:, 2:3], cache=caches)
mx.eval(step)
self.assertEqual(caches[0].offset, 3)
self.assertEqual(caches[1].offset, 3)
def test_rope(self):
rope = rope_utils.initialize_rope(32, base=100, traditional=False)
self.assertTrue(isinstance(rope, nn.RoPE))
rope = rope_utils.initialize_rope(
32,
base=100,
traditional=False,
scaling_config={"rope_type": "linear", "factor": 10.0},
)
self.assertTrue(isinstance(rope, nn.RoPE))
rope = rope_utils.initialize_rope(
32,
base=100,
traditional=False,
scaling_config={"rope_type": "llama3", "factor": 2.0},
)
self.assertTrue(isinstance(rope, rope_utils.Llama3RoPE))
rope = rope_utils.initialize_rope(
16,
base=100.0,
traditional=False,
scaling_config={
"rope_type": "proportional",
"partial_rotary_factor": 0.5,
},
)
self.assertTrue(isinstance(rope, rope_utils.ProportionalRoPE))
expected_freqs = 100.0 ** (mx.arange(0, 8, 2, dtype=mx.float32) / 16)
self.assertTrue(mx.allclose(rope._freqs[:4], expected_freqs))
self.assertTrue(mx.all(mx.isinf(rope._freqs[4:])))
x = mx.arange(16, dtype=mx.float32).reshape(1, 1, 1, 16)
y = rope(x, offset=1)
expected_rotated = mx.fast.rope(
mx.concatenate([x[..., :4], x[..., 8:12]], axis=-1),
8,
traditional=False,
base=None,
scale=1.0,
offset=1,
freqs=expected_freqs,
)
expected = mx.concatenate(
[
expected_rotated[..., :4],
x[..., 4:8],
expected_rotated[..., 4:],
x[..., 12:],
],
axis=-1,
)
mx.eval(y, expected)
self.assertTrue(mx.allclose(y, expected))
def test_su_scaled_rope_no_mutation(self):
rope = rope_utils.SuScaledRoPE(
dims=8,
max_position_embeddings=131072,
original_max_position_embeddings=4096,
long_factor=[1.0] * 4,
)
x = mx.ones((1, 2, 4, 8))
rope(x)
mx.eval(x)
self.assertTrue((x == 1).all())
def test_yarn_rope_no_mutation(self):
rope = rope_utils.YarnRoPE(
dims=8,
scaling_factor=2.0,
mscale=1.0,
mscale_all_dim=0,
)
x = mx.ones((1, 2, 4, 8))
rope(x)
mx.eval(x)
self.assertTrue((x == 1).all())
def test_quantized_sdpa(self):
cache = KVCache()
k = 1e-1 * mx.random.normal(shape=(1, 1, 256, 32))
v = 1e-1 * mx.random.normal(shape=(1, 1, 256, 32))
cache.update_and_fetch(k, v)
quant_cache = cache.to_quantized(group_size=32, bits=8)
k = 1e-1 * mx.random.normal(shape=(1, 1, 1, 32))
v = 1e-1 * mx.random.normal(shape=(1, 1, 1, 32))
k_up, v_up = cache.update_and_fetch(k, v)
qk_up, qv_up = quant_cache.update_and_fetch(k, v)
q = 1e-1 * mx.random.normal(shape=(1, 4, 257, 32))
mask = "causal"
out = scaled_dot_product_attention(
q,
k_up,
v_up,
cache=cache,
mask=mask,
scale=1.0,
)
qout = scaled_dot_product_attention(
q,
qk_up,
qv_up,
cache=quant_cache,
mask=mask,
scale=1.0,
)
self.assertTrue(mx.allclose(out, qout, rtol=1e-2, atol=1e-2))
def model_test_runner(self, model, model_type, vocab_size, num_layers):
self.assertEqual(len(model.layers), num_layers)
self.assertEqual(model.model_type, model_type)
for t in [mx.float32, mx.float16]:
model.update(tree_map(lambda p: p.astype(t), model.parameters()))
inputs = mx.array([[0, 1]])
outputs = model(inputs)
self.assertEqual(outputs.shape, (1, 2, vocab_size))
self.assertEqual(outputs.dtype, t)
cache = make_prompt_cache(model)
outputs = model(inputs, cache=cache)
self.assertEqual(outputs.shape, (1, 2, vocab_size))
self.assertEqual(outputs.dtype, t)
outputs = model(mx.argmax(outputs[0, -1:, :], keepdims=True), cache=cache)
self.assertEqual(outputs.shape, (1, 1, vocab_size))
self.assertEqual(outputs.dtype, t)
# Test batch size > 1
inputs = mx.array([[0, 1], [2, 3]])
outputs = model(inputs)
self.assertEqual(outputs.shape, (2, 2, vocab_size))
# Make sure the model can be copied / pickled
copy.deepcopy(model)
def test_llama(self):
from mlx_lm.models import llama
args = llama.ModelArgs(
model_type="llama",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
rms_norm_eps=1e-5,
vocab_size=10_000,
)
model = llama.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_lfm2(self):
from mlx_lm.models import lfm2
args = lfm2.ModelArgs(
model_type="lfm2",
hidden_size=1024,
num_hidden_layers=4,
num_attention_heads=4,
num_key_value_heads=2,
norm_eps=1e-5,
vocab_size=10_000,
full_attn_idxs=[0, 1, 2],
rope_theta=10000,
block_dim=1024,
block_ffn_dim_multiplier=1.5,
block_auto_adjust_ff_dim=True,
block_ff_dim=2048,
block_multiple_of=256,
max_position_embeddings=1000,
conv_bias=True,
conv_L_cache=3,
)
model = lfm2.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_lfm2_moe(self):
from mlx_lm.models import lfm2_moe
args = lfm2_moe.ModelArgs(
model_type="lfm2_moe",
hidden_size=1024,
intermediate_size=7168,
num_hidden_layers=4,
num_attention_heads=4,
num_key_value_heads=2,
norm_eps=1e-5,
vocab_size=10_000,
full_attn_idxs=[0, 1, 2],
rope_theta=10000,
max_position_embeddings=1000,
conv_bias=True,
conv_L_cache=3,
moe_intermediate_size=1792,
num_dense_layers=2,
num_experts=4,
num_experts_per_tok=2,
norm_topk_prob=True,
use_expert_bias=True,
)
model = lfm2_moe.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_bitnet(self):
from mlx_lm.models import bitnet
args = bitnet.ModelArgs(
model_type="bitnet",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
num_key_value_heads=4,
rms_norm_eps=1e-5,
vocab_size=10_000,
)
model = bitnet.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_phi2(self):
from mlx_lm.models import phi
args = phi.ModelArgs()
model = phi.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_phi3(self):
from mlx_lm.models import phi3
args = phi3.ModelArgs(
model_type="phi3",
hidden_size=3072,
num_hidden_layers=32,
intermediate_size=8192,
num_attention_heads=32,
rms_norm_eps=1e-5,
vocab_size=32064,
)
model = phi3.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_gemma(self):
from mlx_lm.models import gemma
args = gemma.ModelArgs(
model_type="gemma",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
head_dim=128,
rms_norm_eps=1e-5,
vocab_size=10_000,
num_key_value_heads=4,
)
model = gemma.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_mixtral(self):
from mlx_lm.models import mixtral
# Make a baby mixtral, because it will actually do the
# eval
args = mixtral.ModelArgs(
model_type="mixtral",
vocab_size=100,
hidden_size=32,
intermediate_size=128,
num_hidden_layers=2,
num_attention_heads=4,
num_experts_per_tok=2,
num_key_value_heads=2,
num_local_experts=4,
)
model = mixtral.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
@unittest.skip("requires ai2-olmo")
def test_olmo(self):
from mlx_lm.models import olmo
args = olmo.ModelArgs(
model_type="olmo",
d_model=1024,
n_layers=4,
mlp_hidden_size=2048,
n_heads=2,
vocab_size=10_000,
embedding_size=10_000,
)
model = olmo.Model(args)
self.model_test_runner(
model,
args.model_type,
args.vocab_size,
args.n_layers,
)
def test_qwen3_moe(self):
from mlx_lm.models import qwen3_moe
args = qwen3_moe.ModelArgs(
model_type="qwen3_moe",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
num_key_value_heads=4,
rms_norm_eps=1e-5,
head_dim=128,
vocab_size=10_000,
decoder_sparse_step=1,
mlp_only_layers=[],
num_experts_per_tok=4,
num_experts=16,
moe_intermediate_size=1024,
rope_theta=1000,
max_position_embeddings=4096,
tie_word_embeddings=False,
norm_topk_prob=True,
)
model = qwen3_moe.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_qwen3(self):
from mlx_lm.models import qwen3
args = qwen3.ModelArgs(
model_type="qwen3",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
num_key_value_heads=4,
rms_norm_eps=1e-5,
vocab_size=10_000,
head_dim=128,
max_position_embeddings=4096,
tie_word_embeddings=False,
rope_theta=1000,
)
model = qwen3.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_qwen3_5_family_convert_then_load_norm_not_shift_twice(self):
text_config = {
"hidden_size": 8,
"intermediate_size": 16,
"num_hidden_layers": 1,
"num_attention_heads": 1,
"num_key_value_heads": 1,
"rms_norm_eps": 1e-5,
"vocab_size": 32,
"linear_num_value_heads": 1,
"linear_num_key_heads": 1,
"linear_key_head_dim": 4,
"linear_value_head_dim": 4,
"linear_conv_kernel_dim": 1,
"full_attention_interval": 1,
"tie_word_embeddings": False,
"max_position_embeddings": 64,
}
hf_norm_key = "model.language_model.layers.0.input_layernorm.weight"
mlx_norm_key = "language_model.model.layers.0.input_layernorm.weight"
for model_type, hf_mtp_key in (
("qwen3_5", "mtp.fc.weights"),
("qwen3_5_moe", "mtp.fc.weight"),
):
module = importlib.import_module(f"mlx_lm.models.{model_type}")
args = module.ModelArgs.from_dict(
{
"model_type": model_type,
"text_config": {"model_type": model_type, **text_config},
}
)
model = module.Model(args)
base = mx.arange(8, dtype=mx.float32)
# Simulate convert sanitize on HF-style keys.
converted = model.sanitize(
{
hf_norm_key: base,
hf_mtp_key: mx.zeros((1,), dtype=mx.float32),
}
)
self.assertIn(mlx_norm_key, converted)
self.assertTrue(mx.array_equal(converted[mlx_norm_key], base + 1.0))
self.assertFalse(any("mtp." in k for k in converted))
# Simulate load sanitize on already-converted keys.
loaded = model.sanitize(converted)
self.assertTrue(
mx.array_equal(loaded[mlx_norm_key], converted[mlx_norm_key])
)
def test_gemma4_convert_then_load_keeps_language_model_prefix(self):
from mlx_lm.models import gemma4
args = gemma4.ModelArgs.from_dict(
{
"model_type": "gemma4",
"vocab_size": 32,
"text_config": {
"model_type": "gemma4_text",
"hidden_size": 8,
"num_hidden_layers": 1,
"intermediate_size": 16,
"num_attention_heads": 1,
"num_key_value_heads": 1,
"num_global_key_value_heads": 1,
"head_dim": 8,
"global_head_dim": 8,
"sliding_window": 8,
"sliding_window_pattern": 1,
"layer_types": ["full_attention"],
"hidden_size_per_layer_input": 0,
"num_kv_shared_layers": 0,
"tie_word_embeddings": True,
},
}
)
model = gemma4.Model(args)
base = mx.arange(8, dtype=mx.float32)
hf_norm_key = "model.language_model.layers.0.input_layernorm.weight"
mlx_norm_key = "language_model.model.layers.0.input_layernorm.weight"
converted = model.sanitize(
{
hf_norm_key: base,
"model.vision_tower.stub": mx.zeros((1,), dtype=mx.float32),
}
)
self.assertIn(mlx_norm_key, converted)
self.assertNotIn(
"language_model.model.model.layers.0.input_layernorm.weight", converted
)
self.assertTrue(mx.array_equal(converted[mlx_norm_key], base))
self.assertFalse(any("vision_tower" in k for k in converted))
loaded = model.sanitize({mlx_norm_key: base})
self.assertIn(mlx_norm_key, loaded)
self.assertNotIn(
"language_model.model.model.layers.0.input_layernorm.weight", loaded
)
self.assertTrue(mx.array_equal(loaded[mlx_norm_key], base))
def test_gemma4_raw_hf_language_model_prefixes_model(self):
from mlx_lm.models import gemma4
args = gemma4.ModelArgs.from_dict(
{
"model_type": "gemma4",
"vocab_size": 32,
"text_config": {
"model_type": "gemma4_text",
"hidden_size": 8,
"num_hidden_layers": 1,
"intermediate_size": 16,
"num_attention_heads": 1,
"num_key_value_heads": 1,
"num_global_key_value_heads": 1,
"head_dim": 8,
"global_head_dim": 8,
"sliding_window": 8,
"sliding_window_pattern": 1,
"layer_types": ["full_attention"],
"hidden_size_per_layer_input": 0,
"num_kv_shared_layers": 0,
"tie_word_embeddings": True,
},
}
)
model = gemma4.Model(args)
base = mx.arange(8, dtype=mx.float32)
hf_norm_key = "model.language_model.layers.0.input_layernorm.weight"
mlx_norm_key = "language_model.model.layers.0.input_layernorm.weight"
converted = model.sanitize({hf_norm_key: base})
self.assertIn(mlx_norm_key, converted)
self.assertTrue(mx.array_equal(converted[mlx_norm_key], base))
def test_gemma4_raw_hf_moe_expert_weights_split_for_switch_glu(self):
from mlx_lm.models import gemma4
args = gemma4.ModelArgs.from_dict(
{
"model_type": "gemma4",
"vocab_size": 32,
"text_config": {
"model_type": "gemma4_text",
"hidden_size": 8,
"num_hidden_layers": 1,
"intermediate_size": 16,
"num_attention_heads": 1,
"num_key_value_heads": 1,
"num_global_key_value_heads": 1,
"head_dim": 8,
"global_head_dim": 8,
"sliding_window": 8,
"sliding_window_pattern": 1,
"layer_types": ["full_attention"],
"hidden_size_per_layer_input": 0,
"num_kv_shared_layers": 0,
"tie_word_embeddings": True,
"enable_moe_block": True,
"num_experts": 2,
"top_k_experts": 1,
"moe_intermediate_size": 3,
},
}
)
model = gemma4.Model(args)
gate_up = mx.arange(2 * 6 * 8, dtype=mx.float32).reshape(2, 6, 8)
down = mx.arange(2 * 8 * 3, dtype=mx.float32).reshape(2, 8, 3)
converted = model.sanitize(
{
"model.language_model.layers.0.experts.gate_up_proj": gate_up,
"model.language_model.layers.0.experts.down_proj": down,
}
)
gate_key = "language_model.model.layers.0.experts.switch_glu.gate_proj.weight"
up_key = "language_model.model.layers.0.experts.switch_glu.up_proj.weight"
down_key = "language_model.model.layers.0.experts.switch_glu.down_proj.weight"
self.assertIn(gate_key, converted)
self.assertIn(up_key, converted)
self.assertIn(down_key, converted)
self.assertTrue(mx.array_equal(converted[gate_key], gate_up[:, :3, :]))
self.assertTrue(mx.array_equal(converted[up_key], gate_up[:, 3:, :]))
self.assertTrue(mx.array_equal(converted[down_key], down))
self.assertFalse(any("gate_up_proj" in k for k in converted))
def test_gemma4_moe_router_quantizes_to_8bit(self):
from mlx_lm.models import gemma4
from mlx_lm.models.switch_layers import QuantizedSwitchLinear
from mlx_lm.utils import quantize_model
args = gemma4.ModelArgs.from_dict(
{
"model_type": "gemma4",
"vocab_size": 64,
"text_config": {
"model_type": "gemma4_text",
"hidden_size": 64,
"num_hidden_layers": 1,
"intermediate_size": 128,
"moe_intermediate_size": 128,
"num_attention_heads": 1,
"num_key_value_heads": 1,
"num_global_key_value_heads": 1,
"head_dim": 64,
"global_head_dim": 64,
"sliding_window": 8,
"sliding_window_pattern": 1,
"layer_types": ["full_attention"],
"hidden_size_per_layer_input": 0,
"num_kv_shared_layers": 0,
"tie_word_embeddings": True,
"enable_moe_block": True,
"num_experts": 8,
"top_k_experts": 2,
},
}
)
model = gemma4.Model(args)
model, config = quantize_model(
model,
{"model_type": "gemma4", "text_config": copy.deepcopy(args.text_config)},
group_size=64,
bits=4,
)
layer = model.language_model.model.layers[0]
self.assertIsInstance(layer.router.proj, nn.QuantizedLinear)
self.assertEqual(layer.router.proj.bits, 8)
self.assertIsInstance(layer.experts.switch_glu.gate_proj, QuantizedSwitchLinear)
self.assertEqual(layer.experts.switch_glu.gate_proj.bits, 4)
self.assertEqual(
config["quantization"]["language_model.model.layers.0.router.proj"]["bits"],
8,
)
self.assertEqual(config["quantization"]["bits"], 4)
def test_qwen2_moe(self):
from mlx_lm.models import qwen2_moe
args = qwen2_moe.ModelArgs(
model_type="qwen2_moe",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
rms_norm_eps=1e-5,
vocab_size=10_000,
num_experts_per_tok=4,
num_experts=16,
moe_intermediate_size=1024,
shared_expert_intermediate_size=2048,
)
model = qwen2_moe.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_qwen2(self):
from mlx_lm.models import qwen2
args = qwen2.ModelArgs(
model_type="qwen2",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
num_key_value_heads=4,
rms_norm_eps=1e-5,
vocab_size=10_000,
)
model = qwen2.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_qwen(self):
from mlx_lm.models import qwen
args = qwen.ModelArgs(
model_type="qwen",
)
model = qwen.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_plamo(self):
from mlx_lm.models import plamo
args = plamo.ModelArgs(
model_type="plamo",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=8,
rms_norm_eps=1e-5,
vocab_size=10_000,
)
model = plamo.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_plamo2(self):
from mlx_lm.models import plamo2
args = plamo2.ModelArgs(
model_type="plamo2",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=8,
rms_norm_eps=1e-5,
vocab_size=10_000,
)
model = plamo2.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_stablelm(self):
from mlx_lm.models import stablelm
args = stablelm.ModelArgs(
model_type="stablelm",
vocab_size=10_000,
hidden_size=1024,
num_attention_heads=4,
num_hidden_layers=4,
num_key_value_heads=2,
partial_rotary_factor=1.0,
intermediate_size=2048,
layer_norm_eps=1e-2,
rope_theta=10_000,
use_qkv_bias=False,
)
model = stablelm.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
# StableLM 2
args = stablelm.ModelArgs(
model_type="stablelm",
vocab_size=10000,
hidden_size=512,
num_attention_heads=8,
num_hidden_layers=4,
num_key_value_heads=2,
partial_rotary_factor=0.25,
intermediate_size=1024,
layer_norm_eps=1e-5,
rope_theta=10000,
use_qkv_bias=True,
)
model = stablelm.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_starcoder2(self):
from mlx_lm.models import starcoder2
args = starcoder2.ModelArgs(
model_type="starcoder2",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
num_key_value_heads=4,
)
model = starcoder2.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_step3p5(self):
from mlx_lm.models import step3p5
args = step3p5.ModelArgs(
model_type="step3p5",
hidden_size=256,
num_hidden_layers=4,
vocab_size=1024,
num_attention_heads=4,
num_attention_groups=2,
head_dim=64,
intermediate_size=512,
rms_norm_eps=1e-5,
rope_theta=[10000.0, 10000.0, 10000.0, 10000.0],
sliding_window=64,
layer_types=[
"full_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
],
partial_rotary_factors=[0.5, 1.0, 1.0, 0.5],
attention_other_setting={
"num_attention_heads": 8,
"num_attention_groups": 2,
},
use_head_wise_attn_gate=True,
moe_num_experts=4,
moe_top_k=2,
moe_intermediate_size=256,
share_expert_dim=256,
moe_layers_enum="1,2,3",
)
model = step3p5.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_step3p5_make_cache_uses_rotating_for_sliding_layers(self):
from mlx_lm.models import step3p5
args = step3p5.ModelArgs(
model_type="step3p5",
hidden_size=256,
num_hidden_layers=4,
vocab_size=1024,
num_attention_heads=4,
num_attention_groups=2,
head_dim=64,
intermediate_size=512,
rms_norm_eps=1e-5,
rope_theta=[10000.0, 10000.0, 10000.0, 10000.0],
sliding_window=4,
layer_types=[
"full_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
],
partial_rotary_factors=[0.5, 1.0, 1.0, 0.5],
attention_other_setting={
"num_attention_heads": 8,
"num_attention_groups": 2,
},
use_head_wise_attn_gate=True,
moe_num_experts=4,
moe_top_k=2,
moe_intermediate_size=256,
share_expert_dim=256,
moe_layers_enum="1,2,3",
)
model = step3p5.Model(args)
caches = model.make_cache()
self.assertIsInstance(caches[0], KVCache)
self.assertIsInstance(caches[1], RotatingKVCache)
self.assertIsInstance(caches[2], RotatingKVCache)
self.assertIsInstance(caches[3], KVCache)
tokens = mx.array([[1, 2, 3, 4, 5, 6, 7]], dtype=mx.int32)
step = model(tokens[:, :3], cache=caches)
mx.eval(step)
for i in range(3, 7):
step = model(tokens[:, i : i + 1], cache=caches)
mx.eval(step)
self.assertEqual(caches[0].size(), 7)
self.assertEqual(caches[1].size(), args.sliding_window)
self.assertEqual(caches[2].size(), args.sliding_window)
self.assertEqual(caches[3].size(), 7)
def test_step3p5_make_cache_uses_fallback_sliding_pattern(self):
from mlx_lm.models import step3p5
args = step3p5.ModelArgs(
model_type="step3p5",
hidden_size=256,
num_hidden_layers=5,
vocab_size=1024,
num_attention_heads=4,
num_attention_groups=2,
head_dim=64,
intermediate_size=512,
rms_norm_eps=1e-5,
rope_theta=10000.0,
sliding_window=4,
partial_rotary_factors=[1.0] * 5,
use_head_wise_attn_gate=True,
moe_num_experts=4,
moe_top_k=2,
moe_intermediate_size=256,
share_expert_dim=256,
moe_layers_enum="1,2,3,4",
)
model = step3p5.Model(args)
caches = model.make_cache()
self.assertIsInstance(caches[0], RotatingKVCache)
self.assertIsInstance(caches[1], KVCache)
self.assertIsInstance(caches[2], RotatingKVCache)
self.assertIsInstance(caches[3], KVCache)
self.assertIsInstance(caches[4], RotatingKVCache)
tokens = mx.array([[1, 2, 3, 4, 5, 6]], dtype=mx.int32)
step = model(tokens[:, :2], cache=caches)
mx.eval(step)
for i in range(2, 6):
step = model(tokens[:, i : i + 1], cache=caches)
mx.eval(step)
self.assertEqual(caches[0].size(), args.sliding_window)
self.assertEqual(caches[1].size(), 6)
self.assertEqual(caches[2].size(), args.sliding_window)
self.assertEqual(caches[3].size(), 6)
self.assertEqual(caches[4].size(), args.sliding_window)
def test_cohere(self):
from mlx_lm.models import cohere
args = cohere.ModelArgs(
model_type="cohere",
)
model = cohere.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_dbrx(self):
from mlx_lm.models import dbrx
args = dbrx.ModelArgs(
model_type="dbrx",
d_model=1024,
ffn_config={"ffn_hidden_size": 2048, "moe_num_experts": 4, "moe_top_k": 2},
attn_config={"kv_n_heads": 2, "clip_qkv": True, "rope_theta": 10000},
n_layers=4,
n_heads=4,
vocab_size=10_000,
)
model = dbrx.Model(args)
self.model_test_runner(model, args.model_type, args.vocab_size, args.n_layers)
def test_minicpm(self):
from mlx_lm.models import minicpm
args = minicpm.ModelArgs(
model_type="minicpm",
hidden_size=1024,
dim_model_base=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
rms_norm_eps=1e-4,
vocab_size=10000,
num_key_value_heads=2,
scale_depth=1.0,
scale_emb=1.0,
)
model = minicpm.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_mamba(self):
from mlx_lm.models import mamba
args = mamba.ModelArgs(
model_type="mamba",
vocab_size=10000,
use_bias=False,
use_conv_bias=True,
conv_kernel=4,
hidden_size=768,
num_hidden_layers=24,
state_size=16,
intermediate_size=1536,
time_step_rank=48,
)
model = mamba.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_falcon_h1(self):
from mlx_lm.models import falcon_h1
args = falcon_h1.ModelArgs(
model_type="falcon_h1",
num_hidden_layers=12,
vocab_size=10000,
)
model = falcon_h1.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_gpt2(self):
from mlx_lm.models import gpt2
args = gpt2.ModelArgs(
model_type="gpt2",
n_ctx=1024,
n_embd=768,
n_head=12,
n_layer=12,
n_positions=1024,
layer_norm_epsilon=1e-5,
vocab_size=50256,
)
model = gpt2.Model(args)
self.model_test_runner(model, args.model_type, args.vocab_size, args.n_layer)
def test_gpt_neox(self):
from mlx_lm.models import gpt_neox
args = gpt_neox.ModelArgs(
model_type="gpt_neox",
max_position_embeddings=2048,
hidden_size=6144,
num_attention_heads=64,
num_hidden_layers=44,
layer_norm_eps=1e-5,
vocab_size=50432,
rotary_emb_base=10_000,
rotary_pct=0.25,
)
model = gpt_neox.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_openelm(self):
from mlx_lm.models import openelm
args = openelm.ModelArgs(
model_type="openelm",
ffn_dim_divisor=256,
ffn_multipliers=[
0.5,
0.73,
0.97,
1.2,
1.43,
1.67,
1.9,
2.13,
2.37,
2.6,
2.83,
3.07,
3.3,
3.53,
3.77,
4.0,
],
head_dim=64,
model_dim=1280,
normalize_qk_projections=True,
num_kv_heads=[3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5],
num_query_heads=[
12,
12,
12,
12,
12,
16,
16,
16,
16,
16,
16,
16,
20,
20,
20,
20,
],
num_transformer_layers=16,
vocab_size=32000,
)
model = openelm.Model(args)
self.model_test_runner(
model,
args.model_type,
args.vocab_size,
len(args.ffn_multipliers),
)
def test_internlm2(self):
from mlx_lm.models import internlm2
args = internlm2.ModelArgs(
model_type="internlm2",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
rms_norm_eps=1e-5,
vocab_size=10000,
)
model = internlm2.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_llama3_1(self):
from mlx_lm.models import llama
args = llama.ModelArgs(
model_type="llama",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
rms_norm_eps=1e-5,
vocab_size=10_000,
max_position_embeddings=128,
mlp_bias=False,
num_key_value_heads=2,
rope_scaling={
"factor": 8.0,
"low_freq_factor": 1.0,
"high_freq_factor": 4.0,
"original_max_position_embeddings": 8192,
"rope_type": "llama3",
},
)
model = llama.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_deepseek(self):
from mlx_lm.models import deepseek
args = deepseek.ModelArgs(
model_type="deepseek",
vocab_size=1024,
hidden_size=128,
intermediate_size=256,
moe_intermediate_size=256,
num_hidden_layers=4,
num_attention_heads=8,
num_key_value_heads=4,
)
model = deepseek.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_deepseek_v2(self):
from mlx_lm.models import deepseek_v2
args = deepseek_v2.ModelArgs(
model_type="deepseek_v2",
vocab_size=1024,
hidden_size=128,
intermediate_size=256,
moe_intermediate_size=256,
num_hidden_layers=4,
num_attention_heads=4,
num_key_value_heads=2,
kv_lora_rank=4,
q_lora_rank=4,
qk_rope_head_dim=32,
v_head_dim=16,
qk_nope_head_dim=32,
rope_scaling={
"beta_fast": 32,
"beta_slow": 1,
"factor": 40,
"mscale": 1.0,
"mscale_all_dim": 1.0,
"original_max_position_embeddings": 4096,
"type": "yarn",
},
)
model = deepseek_v2.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_deepseek_v3(self):
from mlx_lm.models import deepseek_v3
args = deepseek_v3.ModelArgs(
model_type="deepseek_v3",
vocab_size=1024,
hidden_size=128,
intermediate_size=256,
moe_intermediate_size=256,
num_hidden_layers=4,
num_attention_heads=4,
num_key_value_heads=2,
n_routed_experts=4,
n_group=2,
topk_group=1,
num_experts_per_tok=2,
n_shared_experts=1,
kv_lora_rank=4,
q_lora_rank=4,
qk_rope_head_dim=32,
v_head_dim=16,
qk_nope_head_dim=32,
rope_scaling={
"beta_fast": 32,
"beta_slow": 1,
"factor": 40,
"mscale": 1.0,
"mscale_all_dim": 1.0,
"original_max_position_embeddings": 4096,
"type": "yarn",
},
)
model = deepseek_v3.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_gemma2(self):
from mlx_lm.models import gemma2
args = gemma2.ModelArgs(
model_type="gemma2",
hidden_size=128,
num_hidden_layers=4,
intermediate_size=256,
num_attention_heads=2,
head_dim=32,
rms_norm_eps=1e-4,
vocab_size=1024,
num_key_value_heads=2,
)
model = gemma2.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_gemma3_text(self):
from mlx_lm.models import gemma3_text
args = gemma3_text.ModelArgs(
model_type="gemma3_text",
hidden_size=128,
num_hidden_layers=12,
intermediate_size=256,
num_attention_heads=4,
head_dim=32,
rms_norm_eps=1e-4,
num_key_value_heads=1,
sliding_window=1024,
sliding_window_pattern=6,
)
model = gemma3_text.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_gemma4_text(self):
from mlx_lm.models import gemma4_text
args = gemma4_text.ModelArgs(
model_type="gemma4_text",
hidden_size=128,
num_hidden_layers=10,
intermediate_size=256,
num_attention_heads=4,
head_dim=32,
global_head_dim=64,
rms_norm_eps=1e-6,
vocab_size=1000,
vocab_size_per_layer_input=1000,
num_key_value_heads=1,
num_kv_shared_layers=4,
hidden_size_per_layer_input=32,
sliding_window=8,
sliding_window_pattern=5,
final_logit_softcapping=30.0,
layer_types=[
"sliding_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
],
rope_parameters={
"full_attention": {
"partial_rotary_factor": 0.25,
"rope_theta": 1000000.0,
},
"sliding_attention": {
"rope_theta": 10000.0,
},
},
)
model = gemma4_text.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_gemma4_quantized_embedding_preserves_lookup_scale(self):
from mlx_lm.models import gemma4_text
args = gemma4_text.ModelArgs(
model_type="gemma4_text",
hidden_size=32,
num_hidden_layers=1,
intermediate_size=64,
num_attention_heads=2,
num_key_value_heads=1,
num_global_key_value_heads=1,
head_dim=16,
global_head_dim=16,
sliding_window=8,
sliding_window_pattern=1,
layer_types=["full_attention"],
hidden_size_per_layer_input=0,
vocab_size=4,
num_kv_shared_layers=0,
)
model = gemma4_text.Gemma4TextModel(args)
model.embed_tokens.weight = mx.ones((4, 32), dtype=mx.float32)
model.embed_tokens = model.embed_tokens.to_quantized(group_size=32, bits=8)
token_ids = mx.array([[0, 1]], dtype=mx.int32)
lookup = model.embed_tokens(token_ids) * model.embed_scale
logits = model.embed_tokens.as_linear(mx.ones((1, 1, 32), dtype=mx.float32))
mx.eval(lookup, logits)
self.assertTrue(
mx.allclose(
lookup,
mx.ones((1, 2, 32), dtype=mx.float32) * (32.0**0.5),
)
)
self.assertTrue(
mx.allclose(logits, mx.ones((1, 1, 4), dtype=mx.float32) * 32.0)
)
def test_gemma4_kv_shared_layers_omit_kv_projections(self):
"""KV-shared layers must not create k_proj/v_proj/k_norm/v_norm so that
models saved without redundant weights (e.g. via transformers
save_pretrained) can be loaded with strict=True."""
from mlx_lm.models import gemma4_text
args = gemma4_text.ModelArgs(
model_type="gemma4_text",
hidden_size=128,
num_hidden_layers=10,
intermediate_size=256,
num_attention_heads=4,
head_dim=32,
global_head_dim=64,
rms_norm_eps=1e-6,
vocab_size=1000,
vocab_size_per_layer_input=1000,
num_key_value_heads=1,
num_kv_shared_layers=4,
hidden_size_per_layer_input=32,
sliding_window=8,
sliding_window_pattern=5,
final_logit_softcapping=30.0,
layer_types=[
"sliding_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
],
rope_parameters={
"full_attention": {
"partial_rotary_factor": 0.25,
"rope_theta": 1000000.0,
},
"sliding_attention": {
"rope_theta": 10000.0,
},
},
)
model = gemma4_text.Model(args)
# Non-shared layers (0-5) should have KV projections
for i in range(6):
attn = model.model.layers[i].self_attn
self.assertTrue(attn.has_kv)
self.assertTrue(hasattr(attn, "k_proj"))
self.assertTrue(hasattr(attn, "k_norm"))
# Shared layers (6-9) should NOT have KV projections
for i in range(6, 10):
attn = model.model.layers[i].self_attn
self.assertFalse(attn.has_kv)
self.assertFalse(hasattr(attn, "k_proj"))
self.assertFalse(hasattr(attn, "k_norm"))
self.assertFalse(hasattr(attn, "v_proj"))
# Verify the model can load weights that omit shared-layer KV params
weights = dict(tree_flatten(model.parameters()))
kv_keys = [
k for k in weights if "k_proj" in k or "v_proj" in k or "k_norm" in k
]
for k in kv_keys:
# All KV keys should belong to non-shared layers (0-5)
layer_idx = int(k.split("layers.")[1].split(".")[0])
self.assertLess(layer_idx, 6)
def test_gemma4_input_embeddings_reconstruct_per_layer_inputs(self):
from mlx_lm.models import gemma4_text
args = gemma4_text.ModelArgs(
model_type="gemma4_text",
hidden_size=32,
num_hidden_layers=2,
intermediate_size=64,
num_attention_heads=2,
num_key_value_heads=1,
num_global_key_value_heads=1,
head_dim=16,
global_head_dim=16,
sliding_window=8,
sliding_window_pattern=1,
layer_types=["full_attention", "full_attention"],
hidden_size_per_layer_input=8,
vocab_size=32,
vocab_size_per_layer_input=32,
num_kv_shared_layers=0,
)
model = gemma4_text.Model(args)
tokens = mx.array([[1, 2, 3]], dtype=mx.int32)
embeddings = model.model.embed_tokens(tokens)
per_layer_inputs = model.model._get_per_layer_inputs(tokens)
direct = model(tokens)
from_embeddings = model(None, input_embeddings=embeddings)
explicit = model(
None,
input_embeddings=embeddings,
per_layer_inputs=per_layer_inputs,
)
mx.eval(direct, from_embeddings, explicit)
self.assertTrue(
mx.allclose(direct.astype(mx.float32), from_embeddings.astype(mx.float32))
)
self.assertTrue(
mx.allclose(direct.astype(mx.float32), explicit.astype(mx.float32))
)
def test_gpt_bigcode(self):
from mlx_lm.models import gpt_bigcode
args = gpt_bigcode.ModelArgs(
model_type="gpt_bigcode",
n_embd=128,
n_layer=128,
n_inner=256,
n_head=4,
n_positions=1000,
layer_norm_epsilon=1e-5,
vocab_size=1024,
)
model = gpt_bigcode.Model(args)
self.model_test_runner(model, args.model_type, args.vocab_size, args.n_layer)
def test_nemotron(self):
from mlx_lm.models import nemotron
args = nemotron.ModelArgs(
model_type="nemotron",
hidden_size=128,
hidden_act="gelu",
num_hidden_layers=4,
intermediate_size=256,
num_attention_heads=4,
norm_eps=1e-5,
vocab_size=1024,
num_key_value_heads=2,
)
model = nemotron.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_phi3small(self):
from mlx_lm.models import phi3small
args = phi3small.ModelArgs(
model_type="phi3small",
hidden_size=128,
dense_attention_every_n_layers=2,
ff_intermediate_size=256,
gegelu_limit=1.0,
num_hidden_layers=4,
num_attention_heads=4,
num_key_value_heads=2,
layer_norm_epsilon=1e-4,
vocab_size=1000,
)
model = phi3small.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_phimoe(self):
from mlx_lm.models import phimoe
args = phimoe.ModelArgs(
model_type="phimoe",
vocab_size=320,
hidden_size=128,
intermediate_size=256,
num_hidden_layers=4,
num_attention_heads=4,
num_key_value_heads=4,
rope_scaling={
"long_factor": [1.0] * 16,
"long_mscale": 1.243163121016122,
"original_max_position_embeddings": 4096,
"short_factor": [1.0] * 16,
"short_mscale": 1.243163121016122,
"type": "longrope",
},
)
model = phimoe.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_recurrent_gemma(self):
from mlx_lm.models import recurrent_gemma
args = recurrent_gemma.ModelArgs(
model_type="recurrent_gemma",
hidden_size=128,
attention_bias=False,
conv1d_width=3,
intermediate_size=256,
logits_soft_cap=1.0,
num_attention_heads=4,
num_hidden_layers=4,
num_key_value_heads=2,
rms_norm_eps=1e-4,
rope_theta=1000,
attention_window_size=1024,
vocab_size=1000,
block_types=["recurrent", "recurrent", "attention"],
)
model = recurrent_gemma.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_hunyuan(self):
from mlx_lm.models import hunyuan
args = hunyuan.ModelArgs(
model_type="hunyuan",
hidden_size=128,
attention_bias=False,
intermediate_size=256,
num_attention_heads=4,
num_hidden_layers=4,
num_key_value_heads=2,
rms_norm_eps=1e-4,
rope_theta=1000,
vocab_size=1000,
moe_topk=2,
num_experts=2,
num_shared_expert=1,
use_mixed_mlp_moe=True,
use_qk_norm=True,
rope_scaling={
"alpha": 1000.0,
"factor": 1.0,
"type": "dynamic",
},
use_cla=True,
cla_share_factor=2,
)
model = hunyuan.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_hunyuan_v1_dense(self):
from mlx_lm.models import hunyuan_v1_dense
args = hunyuan_v1_dense.ModelArgs(
model_type="hunyuan_v1_dense",
hidden_size=128,
attention_bias=False,
intermediate_size=256,
num_attention_heads=4,
num_hidden_layers=4,
num_key_value_heads=2,
rms_norm_eps=1e-4,
rope_theta=1000,
vocab_size=1000,
use_qk_norm=True,
rope_scaling={
"alpha": 1000.0,
"factor": 1.0,
"type": "dynamic",
"beta_fast": 32,
"beta_slow": 1,
"mscale": 1.0,
"mscale_all_dim": 0.0,
"original_max_position_embeddings": 8192,
},
max_position_embeddings=32768,
)
model = hunyuan_v1_dense.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_olmo2(self):
from mlx_lm.models import olmo2
args = olmo2.ModelArgs(
model_type="olmo2",
hidden_size=128,
attention_bias=False,
intermediate_size=256,
num_attention_heads=4,
num_hidden_layers=4,
num_key_value_heads=2,
rms_norm_eps=1e-4,
rope_theta=1000,
vocab_size=1000,
)
model = olmo2.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_exaone(self):
from mlx_lm.models import exaone
args = exaone.ModelArgs(
model_type="exaone",
hidden_size=128,
num_layers=4,
intermediate_size=256,
num_attention_heads=8,
num_key_value_heads=2,
vocab_size=1000,
layer_norm_epsilon=1e-4,
rope_theta=10000,
)
model = exaone.Model(args)
self.model_test_runner(model, args.model_type, args.vocab_size, args.num_layers)
def test_cohere2(self):
from mlx_lm.models import cohere2
args = cohere2.ModelArgs(
model_type="cohere2",
hidden_size=4096,
head_dim=128,
num_hidden_layers=40,
sliding_window=4096,
sliding_window_pattern=4,
)
model = cohere2.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_internlm3(self):
from mlx_lm.models import internlm3
args = internlm3.ModelArgs(
model_type="internlm3",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
rms_norm_eps=1e-5,
vocab_size=10_000,
)
model = internlm3.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_iquestloopcoder(self):
from mlx_lm.models import iquestloopcoder
args = iquestloopcoder.ModelArgs(
model_type="iquestloopcoder",
hidden_size=256,
num_hidden_layers=2,
intermediate_size=512,
num_attention_heads=4,
num_key_value_heads=2,
rms_norm_eps=1e-5,
head_dim=32,
vocab_size=1000,
rope_theta=500000.0,
tie_word_embeddings=False,
loop_num=2,
loop_window_size=32,
)
model = iquestloopcoder.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_smollm3(self):
from mlx_lm.models import smollm3
args = smollm3.ModelArgs(
model_type="smollm3",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=4,
rms_norm_eps=1e-5,
vocab_size=10_000,
)
model = smollm3.Model(args)
self.model_test_runner(
model, "smollm3", args.vocab_size, args.num_hidden_layers
)
def test_gpt_oss(self):
from mlx_lm.models import gpt_oss
args = gpt_oss.ModelArgs(
model_type="gpt_oss",
hidden_size=1024,
num_hidden_layers=4,
intermediate_size=2048,
num_attention_heads=8,
num_key_value_heads=2,
num_local_experts=16,
num_experts_per_tok=2,
sliding_window=128,
rope_theta=10000,
vocab_size=10_000,
layer_types=[
"sliding_attention",
"full_attention",
"sliding_attention",
"full_attention",
],
)
model = gpt_oss.Model(args)
self.model_test_runner(
model, args.model_type, args.vocab_size, args.num_hidden_layers
)
def test_all_models(self):
test_configs = [
{
"model_type": "afm7",
"vocab_size": 1000,
"hidden_dim": 256,
"num_layers": 16,
"num_hidden_layers": 16,
"num_kv_reuse_layers": 8,
"num_heads": 8,
"num_kv_heads": 4,
},
{
"model_type": "apertus",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 256,
"mlp_bias": True,
"num_attention_heads": 8,
"attention_bias": False,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"num_key_value_heads": 4,
"max_position_embeddings": 1000,
"rope_theta": 1000,
"post_norm": True,
"qk_norm": True,
"tie_word_embeddings": False,
},
{
"model_type": "baichuan_m1",
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 256,
"num_hidden_layers": 4,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"rope_theta": 1000,
"sliding_window": 128,
"sliding_window_layers": [0, 2],
"conv_window": 2,
"rms_norm_eps": 1e-5,
},
{
"model_type": "bailing_moe",
"hidden_size": 64,
"intermediate_size": 128,
"max_position_embeddings": 1000,
"moe_intermediate_size": 128,
"num_experts": 4,
"num_shared_experts": 1,
"norm_topk_prob": True,
"num_attention_heads": 4,
"num_experts_per_tok": 2,
"num_hidden_layers": 4,
"num_key_value_heads": 2,
"rms_norm_eps": 1e-5,
"rope_theta": 1000,
"vocab_size": 1000,
"first_k_dense_replace": 2,
},
{
"model_type": "dots1",
"hidden_size": 64,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"max_position_embeddings": None,
"num_key_value_heads": 2,
"first_k_dense_replace": 1,
"moe_intermediate_size": 64,
"n_routed_experts": 4,
"n_shared_experts": 1,
"norm_topk_prob": True,
"num_experts_per_tok": 1,
"rope_theta": 1000,
"routed_scaling_factor": 1.0,
},
{
"hidden_size": 128,
"intermediate_size": 128,
"model_type": "ernie4_5",
"max_position_embeddings": 1000,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"head_dim": None,
"num_hidden_layers": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"rope_theta": 10000,
"use_bias": False,
"tie_word_embeddings": True,
},
{
"hidden_size": 128,
"intermediate_size": 128,
"model_type": "ernie4_5_moe",
"max_position_embeddings": 1000,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"num_hidden_layers": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"rope_theta": 1000,
"use_bias": False,
"tie_word_embeddings": False,
"moe_num_experts": 4,
},
{
"model_type": "exaone4",
"hidden_size": 128,
"intermediate_size": 128,
"num_attention_heads": 4,
"vocab_size": 1000,
"rms_norm_eps": 1e-5,
"num_hidden_layers": 4,
"max_position_embeddings": 1000,
"rope_theta": 10000,
"layer_norm_epsilon": 1e-5,
"num_key_value_heads": 2,
"head_dim": 32,
"tie_word_embeddings": False,
"rope_scaling": None,
"sliding_window": 8,
"sliding_window_pattern": "LLGL",
},
{
"model_type": "gemma4",
"num_hidden_layers": 10,
"vocab_size": 1000,
"text_config": {
"model_type": "gemma4_text",
"hidden_size": 128,
"num_hidden_layers": 10,
"intermediate_size": 128,
"num_attention_heads": 4,
"head_dim": 32,
"global_head_dim": 64,
"rms_norm_eps": 1e-6,
"vocab_size": 1000,
"vocab_size_per_layer_input": 1000,
"num_key_value_heads": 1,
"num_kv_shared_layers": 4,
"hidden_size_per_layer_input": 32,
"sliding_window": 8,
"sliding_window_pattern": 5,
"final_logit_softcapping": 30.0,
"layer_types": [
"sliding_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
],
"rope_parameters": {
"full_attention": {
"partial_rotary_factor": 0.25,
"rope_theta": 1000000.0,
},
"sliding_attention": {
"rope_theta": 10000.0,
},
},
},
},
{
"model_type": "gemma3n",
"num_hidden_layers": 4,
"vocab_size": 1000,
"text_config": {
"model_type": "gemma3n",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 4,
"head_dim": 32,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"num_key_value_heads": 2,
"num_kv_shared_layers": 2,
"vocab_size_per_layer_input": 1000,
"sliding_window": 8,
"max_position_embeddings": 1000,
"rope_local_base_freq": 1.0,
"rope_theta": 1000.0,
"final_logit_softcapping": 1.0,
"layer_types": [
"sliding_attention",
"full_attention",
"sliding_attention",
"full_attention",
],
"activation_sparsity_pattern": [0.5, 0.5, 0.5, 0.5],
"hidden_size_per_layer_input": 256,
"altup_num_inputs": 4,
"altup_coef_clip": 1.0,
"altup_correct_scale": True,
"altup_active_idx": 0,
"laurel_rank": 8,
},
},
{
"model_type": "glm4",
"hidden_size": 256,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 4,
"attention_bias": False,
"head_dim": 64,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"num_key_value_heads": 2,
"partial_rotary_factor": 0.5,
"rope_theta": 1000,
},
{
"model_type": "glm4_moe",
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 128,
"max_position_embeddings": 1000,
"moe_intermediate_size": 128,
"norm_topk_prob": True,
"num_attention_heads": 4,
"n_group": 2,
"head_dim": 32,
"topk_group": 1,
"n_shared_experts": 1,
"n_routed_experts": 4,
"routed_scaling_factor": 1.0,
"num_experts_per_tok": 2,
"first_k_dense_replace": 1,
"num_hidden_layers": 4,
"num_key_value_heads": 2,
"rms_norm_eps": 1e-5,
"rope_theta": 1000,
"rope_scaling": None,
"use_qk_norm": True,
"tie_word_embeddings": False,
"attention_bias": False,
"partial_rotary_factor": 0.5,
},
{
"model_type": "glm4_moe_lite",
"vocab_size": 1000,
"hidden_size": 64,
"intermediate_size": 128,
"moe_intermediate_size": 32,
"num_hidden_layers": 4,
"num_attention_heads": 4,
"num_key_value_heads": 4,
"n_shared_experts": 1,
"n_routed_experts": 4,
"routed_scaling_factor": 1.0,
"kv_lora_rank": 8,
"q_lora_rank": 8,
"qk_rope_head_dim": 8,
"qk_nope_head_dim": 16,
"v_head_dim": 8,
"topk_method": "noaux_tc",
"scoring_func": "sigmoid",
"norm_topk_prob": True,
"n_group": 1,
"topk_group": 1,
"num_experts_per_tok": 2,
"moe_layer_freq": 1,
"first_k_dense_replace": 1,
"max_position_embeddings": 256,
"rms_norm_eps": 1e-5,
"rope_theta": 1000,
"rope_scaling": None,
"attention_bias": False,
"partial_rotary_factor": 1.0,
"tie_word_embeddings": False,
"num_nextn_predict_layers": 1,
},
{
"model_type": "granite",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"logits_scaling": 1.0,
"attention_multiplier": 1.0,
"embedding_multiplier": 1.0,
"residual_multiplier": 1.0,
"max_position_embeddings": 1000,
"num_key_value_heads": 2,
"attention_bias": False,
"mlp_bias": False,
"rope_theta": 1000,
},
{
"model_type": "granitemoe",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"logits_scaling": 1.0,
"attention_multiplier": 1.0,
"embedding_multiplier": 1.0,
"residual_multiplier": 1.0,
"max_position_embeddings": 1000,
"num_key_value_heads": 2,
"attention_bias": False,
"rope_theta": 1000,
"num_local_experts": 4,
"num_experts_per_tok": 2,
},
{
"hidden_size": 256,
"num_hidden_layers": 4,
"intermediate_size": 256,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"attention_bias": False,
"head_dim": 64,
"max_position_embeddings": 1000,
"mlp_bias": False,
"model_type": "helium",
"rope_theta": 1000,
"tie_word_embeddings": False,
},
{
"text_config": {
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 128,
"moe_intermediate_size": 128,
"num_hidden_layers": 4,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"n_shared_experts": 1,
"n_routed_experts": 4,
"kv_lora_rank": 32,
"q_lora_rank": 32,
"qk_rope_head_dim": 8,
"v_head_dim": 16,
"qk_nope_head_dim": 16,
},
"model_type": "kimi_vl",
"num_hidden_layers": 4,
"vocab_size": 1000,
},
{
"model_type": "lfm2-vl",
"vocab_size": 1000,
"num_hidden_layers": 4,
"text_config": {
"model_type": "lfm2",
"vocab_size": 1000,
"hidden_size": 128,
"num_hidden_layers": 4,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"max_position_embeddings": 1000,
"norm_eps": 1e-5,
"conv_bias": False,
"conv_L_cache": 3,
"block_dim": 128,
"block_ff_dim": 128,
"block_multiple_of": 4,
"block_ffn_dim_multiplier": 2,
"block_auto_adjust_ff_dim": True,
"layer_types": ["full_attention", "", "full_attention", ""],
"rope_theta": 1000,
},
},
{
"model_type": "llama4",
"text_config": {
"attention_bias": False,
"attention_chunk_size": 8,
"head_dim": 32,
"hidden_size": 128,
"interleave_moe_layer_step": 2,
"intermediate_size": 128,
"intermediate_size_mlp": 128,
"max_position_embeddings": 1000,
"model_type": "llama4",
"num_attention_heads": 4,
"num_experts_per_tok": 1,
"num_hidden_layers": 4,
"num_key_value_heads": 2,
"num_local_experts": 2,
"rms_norm_eps": 1e-4,
"rope_scaling": None,
"rope_theta": 1000,
"use_qk_norm": True,
"vocab_size": 1000,
},
"num_hidden_layers": 4,
"vocab_size": 1000,
},
{
"model_type": "longcat_flash_ngram",
"attention_method": "MLA",
"zero_expert_type": "identity",
"hidden_size": 128,
"ffn_hidden_size": 128,
"moe_topk": 2,
"expert_ffn_hidden_size": 128,
"n_routed_experts": 2,
"zero_expert_num": 2,
"num_layers": 4,
"num_hidden_layers": 4,
"vocab_size": 1000,
"max_position_embeddings": 1000,
"num_attention_heads": 4,
"kv_lora_rank": 16,
"q_lora_rank": 16,
"qk_rope_head_dim": 8,
"qk_nope_head_dim": 8,
"v_head_dim": 8,
"routed_scaling_factor": 1.0,
"rms_norm_eps": 1e-5,
"rope_theta": 1000,
"mla_scale_q_lora": True,
"mla_scale_kv_lora": True,
"attention_bias": False,
},
{
"model_type": "longcat_flash",
"attention_method": "MLA",
"zero_expert_type": "identity",
"hidden_size": 128,
"ffn_hidden_size": 128,
"moe_topk": 2,
"expert_ffn_hidden_size": 128,
"n_routed_experts": 2,
"zero_expert_num": 2,
"num_layers": 4,
"num_hidden_layers": 4,
"vocab_size": 1000,
"max_position_embeddings": 1000,
"num_attention_heads": 4,
"kv_lora_rank": 16,
"q_lora_rank": 16,
"qk_rope_head_dim": 8,
"qk_nope_head_dim": 8,
"v_head_dim": 8,
"routed_scaling_factor": 1.0,
"rms_norm_eps": 1e-5,
"rope_theta": 1000,
"mla_scale_q_lora": True,
"mla_scale_kv_lora": True,
"attention_bias": False,
},
{
"model_type": "mimo",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"num_key_value_heads": 2,
},
{
"model_type": "nemotron-nas",
"hidden_size": 256,
"num_hidden_layers": 4,
"num_attention_heads": 8,
"rms_norm_eps": 1e-5,
"vocab_size": 128256,
"block_configs": [
{
"attention": {
"n_heads_in_group": 8,
"no_op": False,
"replace_with_linear": False,
},
"ffn": {
"ffn_mult": 1.3125,
"no_op": False,
"replace_with_linear": False,
},
},
]
* 4,
},
{
"model_type": "nemotron_h",
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 128,
"num_hidden_layers": 4,
"max_position_embeddings": 1000,
"num_attention_heads": 8,
"num_key_value_heads": 4,
"attention_bias": False,
"mamba_num_heads": 8,
"mamba_head_dim": 64,
"mamba_proj_bias": False,
"ssm_state_size": 128,
"conv_kernel": 3,
"n_groups": 4,
"time_step_limit": [1.0, 2.0],
"mlp_bias": False,
"layer_norm_epsilon": 1e-4,
"rms_norm_eps": 1e-5,
"use_bias": True,
"use_conv_bias": True,
"residual_in_fp32": True,
"hybrid_override_pattern": ["*", "M", "*", "M"],
},
{
"model_type": "olmoe",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 2,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"num_experts": 4,
"num_experts_per_tok": 2,
},
{
"model_type": "pixtral",
"text_config": {
"model_type": "llama",
"hidden_size": 1024,
"num_hidden_layers": 4,
"intermediate_size": 2048,
"num_attention_heads": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
},
"num_hidden_layers": 4,
"vocab_size": 1000,
},
{
"model_type": "qwen3_vl_moe",
"text_config": {
"model_type": "qwen3_moe",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 256,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"rms_norm_eps": 1e-5,
"head_dim": 32,
"vocab_size": 1000,
"decoder_sparse_step": 1,
"mlp_only_layers": [],
"num_experts_per_tok": 2,
"num_experts": 4,
"moe_intermediate_size": 128,
"rope_theta": 1000,
"max_position_embeddings": 1000,
"tie_word_embeddings": False,
"norm_topk_prob": True,
},
"num_hidden_layers": 4,
"vocab_size": 1000,
},
{
"model_type": "qwen3_vl",
"text_config": {
"model_type": "qwen3",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 256,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"head_dim": 32,
"max_position_embeddings": 1000,
"tie_word_embeddings": False,
"rope_theta": 1000,
},
"num_hidden_layers": 4,
"vocab_size": 1000,
},
{
"model_type": "seed_oss",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 2,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"num_key_value_heads": 2,
"head_dim": 64,
},
{
"model_type": "Klear",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 4,
"attention_bias": False,
"mlp_only_layers": [0],
"num_experts": 4,
"num_experts_per_tok": 2,
"decoder_sparse_step": 2,
"n_shared_experts": 1,
"moe_intermediate_size": 128,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"num_key_value_heads": 4,
"rope_theta": 1000.0,
"max_position_embeddings": 1000,
"norm_topk_prob": True,
},
{
"model_type": "lille-130m",
"block_size": 128,
"num_hidden_layers": 4,
"n_layer": 4,
"n_head": 4,
"n_kv_heads": 4,
"n_embd": 128,
"vocab_size": 1000,
"rope_theta": 1000,
"layer_norm_eps": 1e-5,
},
{
"model_type": "granitemoehybrid",
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 128,
"num_hidden_layers": 4,
"max_position_embeddings": 1000,
"num_attention_heads": 8,
"num_key_value_heads": 4,
"attention_bias": False,
"embedding_multiplier": 1.0,
"attention_multiplier": 1.0,
"logits_scaling": 1.0,
"residual_multiplier": 1.0,
"num_local_experts": 8,
"num_experts_per_tok": 2,
"shared_intermediate_size": 128,
"mamba_n_heads": 8,
"mamba_d_head": 16,
"mamba_proj_bias": False,
"mamba_d_state": 128,
"mamba_d_conv": 4,
"mamba_n_groups": 1,
"mamba_conv_bias": False,
"layer_types": ["mamba", "attention", "mamba", "attention"],
"rms_norm_eps": 1e-5,
"rope_theta": 1000.0,
},
{
"model_type": "glm",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"head_dim": 32,
"num_key_value_heads": 2,
},
{
"model_type": "llama4_text",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 1000,
"head_dim": 32,
"num_key_value_heads": 2,
"intermediate_size_mlp": 128,
"rope_theta": 1000.0,
"head_dim": 8,
"tie_word_embeddings": False,
"no_rope_layers": [0, 0, 1, 1],
"use_qk_norm": True,
},
{
"model_type": "mamba2",
"num_heads": 8,
"head_dim": 16,
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 128,
"state_size": 32,
"num_hidden_layers": 4,
"layer_norm_epsilon": 1e-4,
"conv_kernel": 3,
"n_groups": 4,
"use_bias": False,
"use_conv_bias": False,
"tie_word_embeddings": True,
"time_step_limit": (0.01, 10),
"time_step_rank": "auto",
},
{
"model_type": "olmo3",
"num_heads": 8,
"head_dim": 16,
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 128,
"num_attention_heads": 8,
"rope_theta": 1000,
"num_hidden_layers": 8,
"rms_norm_eps": 1e-4,
"sliding_window": 128,
"tie_word_embeddings": True,
"max_position_embeddings": 1000,
},
{
"model_type": "jamba",
"hidden_size": 128,
"intermediate_size": 128,
"num_hidden_layers": 8,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"attn_layer_offset": 1,
"attn_layer_period": 2,
"expert_layer_offset": 1,
"expert_layer_period": 2,
"mamba_d_conv": 4,
"mamba_d_state": 128,
"mamba_expand": 128,
"num_experts": 4,
"num_experts_per_tok": 2,
"rms_norm_eps": 1e-5,
"max_position_embeddings": 1000,
"vocab_size": 1000,
},
{
"model_type": "nanochat",
"hidden_size": 1280,
"num_hidden_layers": 20,
"vocab_size": 32,
"intermediate_size": 128,
},
{
"model_type": "minimax",
"hidden_size": 128,
"intermediate_size": 128,
"num_attention_heads": 8,
"num_key_value_heads": 8,
"max_position_embeddings": 1000,
"num_experts_per_tok": 2,
"num_local_experts": 8,
"shared_intermediate_size": 128,
"num_hidden_layers": 4,
"rms_norm_eps": 1e-4,
"rope_theta": 1000,
"rotary_dim": 16,
"vocab_size": 1000,
},
{
"model_type": "bailing_moe_linear",
"hidden_size": 1024,
"num_hidden_layers": 4,
"intermediate_size": 2048,
"moe_intermediate_size": 1024,
"num_experts_per_tok": 2,
"num_experts": 4,
"norm_topk_prob": True,
"num_shared_experts": 2,
"num_attention_heads": 4,
"num_key_value_heads": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 10_000,
"rope_theta": 1000,
"first_k_dense_replace": 0,
"layer_group_size": 2,
"group_norm_size": 1,
"max_position_embeddings": 1000,
},
{
"model_type": "qwen3_next",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 8,
"num_key_value_heads": 4,
"vocab_size": 1000,
"linear_num_value_heads": 4,
"linear_num_key_heads": 4,
"linear_key_head_dim": 32,
"linear_value_head_dim": 32,
"linear_conv_kernel_dim": 3,
"num_experts": 4,
"num_experts_per_tok": 2,
"decoder_sparse_step": 1,
"shared_expert_intermediate_size": 128,
"mlp_only_layers": [0],
"moe_intermediate_size": 128,
"rms_norm_eps": 1e-5,
"head_dim": 64,
"rope_theta": 1000.0,
"partial_rotary_factor": 0.5,
"max_position_embeddings": 1000,
},
{
"model_type": "qwen3_5",
"hidden_size": 128,
"num_hidden_layers": 4,
"intermediate_size": 128,
"num_attention_heads": 8,
"num_key_value_heads": 4,
"vocab_size": 1000,
"linear_num_value_heads": 4,
"linear_num_key_heads": 4,
"linear_key_head_dim": 32,
"linear_value_head_dim": 32,
"linear_conv_kernel_dim": 3,
"rms_norm_eps": 1e-5,
"head_dim": 64,
"rope_theta": 1000.0,
"partial_rotary_factor": 0.5,
"max_position_embeddings": 1000,
},
{
"model_type": "qwen3_5_moe",
"hidden_size": 128,
"num_hidden_layers": 4,
"num_attention_heads": 8,
"num_key_value_heads": 4,
"vocab_size": 1000,
"linear_num_value_heads": 4,
"linear_num_key_heads": 4,
"linear_key_head_dim": 32,
"linear_value_head_dim": 32,
"linear_conv_kernel_dim": 3,
"num_experts": 4,
"num_experts_per_tok": 2,
"shared_expert_intermediate_size": 128,
"moe_intermediate_size": 128,
"rms_norm_eps": 1e-5,
"head_dim": 64,
"rope_theta": 1000.0,
"partial_rotary_factor": 0.5,
"max_position_embeddings": 1000,
},
{
"model_type": "kimi_linear",
"vocab_size": 1000,
"hidden_size": 128,
"num_hidden_layers": 4,
"num_attention_heads": 8,
"num_key_value_heads": 4,
"intermediate_size": 128,
"head_dim": 32,
"rope_theta": 100.0,
"rms_norm_eps": 1e-6,
"linear_attn_config": {
"num_heads": 8,
"head_dim": 32,
"kda_layers": [1],
},
"model_max_length": 1000,
"num_experts": 2,
"moe_intermediate_size": 128,
"kv_lora_rank": 8,
"qk_nope_head_dim": 16,
"qk_rope_head_dim": 16,
"v_head_dim": 16,
},
{
"model_type": "afmoe",
"vocab_size": 1000,
"hidden_size": 128,
"num_hidden_layers": 4,
"num_attention_heads": 8,
"num_key_value_heads": 4,
"intermediate_size": 128,
"head_dim": 32,
"rope_theta": 100.0,
"layer_types": [
"full_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
],
"num_experts": 4,
"num_experts_per_tok": 2,
"moe_intermediate_size": 128,
},
{
"model_type": "deepseek_v32",
"vocab_size": 1024,
"hidden_size": 128,
"intermediate_size": 256,
"moe_intermediate_size": 256,
"num_hidden_layers": 4,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"n_routed_experts": 4,
"n_group": 2,
"topk_group": 1,
"num_experts_per_tok": 2,
"n_shared_experts": 1,
"kv_lora_rank": 4,
"q_lora_rank": 4,
"qk_rope_head_dim": 32,
"v_head_dim": 16,
"qk_nope_head_dim": 32,
"rope_scaling": {
"beta_fast": 32,
"beta_slow": 1,
"factor": 40,
"mscale": 1.0,
"mscale_all_dim": 1.0,
"original_max_position_embeddings": 4096,
"type": "yarn",
},
},
{
"model_type": "mimo_v2_flash",
"num_experts_per_tok": 2,
"hybrid_layer_pattern": [0, 1, 0, 1],
"moe_layer_freq": [0, 1, 0, 1],
"add_swa_attention_sink_bias": True,
"add_full_attention_sink_bias": False,
"sliding_window_size": 32,
"vocab_size": 1000,
"hidden_size": 512,
"intermediate_size": 512,
"moe_intermediate_size": 128,
"num_hidden_layers": 4,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"n_shared_experts": 1,
"n_routed_experts": 8,
"routed_scaling_factor": None,
"topk_method": "noaux_tc",
"scoring_func": "sigmoid",
"norm_topk_prob": True,
"n_group": 2,
"topk_group": 1,
"max_position_embeddings": 1000,
"layernorm_epsilon": 1e-5,
"rope_theta": 1000.0,
"swa_rope_theta": 1000.0,
"swa_num_attention_heads": 4,
"swa_num_key_value_heads": 2,
"head_dim": 128,
"v_head_dim": 64,
"swa_head_dim": 128,
"swa_v_head_dim": 64,
"partial_rotary_factor": 0.5,
},
{
"model_type": "rwkv7",
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 128,
"norm_eps": 1e-5,
"head_dim": 32,
"num_hidden_layers": 4,
"a_low_rank_dim": 16,
"v_low_rank_dim": 16,
"gate_low_rank_dim": 16,
"decay_low_rank_dim": 16,
},
{
"model_type": "exaone_moe",
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 256,
"moe_intermediate_size": 64,
"num_hidden_layers": 4,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"head_dim": 32,
"num_experts": 4,
"num_experts_per_tok": 2,
"num_shared_experts": 1,
"n_group": 1,
"topk_group": 1,
"routed_scaling_factor": 2.5,
"norm_topk_prob": True,
"sliding_window": 32,
"max_position_embeddings": 1000,
"rms_norm_eps": 1e-5,
"rope_theta": 1000.0,
"layer_types": [
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
],
"is_moe_layer": [False, True, True, True],
"tie_word_embeddings": False,
},
{
"model_type": "youtu_llm",
"vocab_size": 1000,
"hidden_size": 128,
"intermediate_size": 128,
"num_hidden_layers": 4,
"kv_lora_rank": 128,
"q_lora_rank": 256,
},
{
"model_type": "telechat3",
"hidden_size": 64,
"num_hidden_layers": 4,
"intermediate_size": 256,
"num_attention_heads": 8,
"num_key_value_heads": 4,
"rms_norm_eps": 1e-5,
"vocab_size": 128,
"rope_theta": 10000.0,
"max_position_embeddings": 1000,
},
]
for config in test_configs:
model_type = config["model_type"]
with self.subTest(f"Testing {model_type}", model_type=model_type):
arch = importlib.import_module(f"mlx_lm.models.{model_type}")
args = arch.ModelArgs.from_dict(config)
model = arch.Model(args)
self.model_test_runner(
model,
args.model_type,
config["vocab_size"],
config["num_hidden_layers"],
)
def test_ssm(self):
for batch_size in [1, 2]:
for n_group in [1, 4]:
num_heads = 48
head_dim = 64
state_dim = 128
hidden_states = mx.random.normal(
shape=(batch_size, 1, num_heads, head_dim)
)
B = mx.random.normal(shape=(batch_size, 1, n_group, state_dim))
C = mx.random.normal(shape=(batch_size, 1, n_group, state_dim))
dt = mx.random.normal(shape=(batch_size, 1, num_heads))
dt_bias = mx.random.normal(shape=(num_heads,))
A_log = mx.random.normal(shape=(num_heads,))
D = mx.random.normal(shape=(num_heads,))
state = mx.random.normal(
shape=(batch_size, num_heads, head_dim, state_dim)
)
out, out_state = ssm_attn(
hidden_states, A_log, B, C, D, dt, dt_bias, state
)
out_c, out_state_c = ssm_update(
hidden_states, A_log, B, C, D, dt, dt_bias, state
)
self.assertTrue(mx.allclose(out, out_c, atol=1e-4, rtol=1e-4))
self.assertTrue(
mx.allclose(out_state, out_state_c, atol=1e-4, rtol=1e-4)
)
def test_ssm_masked(self):
batch_size = 1
n_group = 1
num_heads = 48
head_dim = 64
state_dim = 128
seq_len = 4
pad = 2
hidden_states = mx.random.normal(
shape=(batch_size, seq_len + pad, num_heads, head_dim)
)
B = mx.random.normal(shape=(batch_size, seq_len + pad, n_group, state_dim))
C = mx.random.normal(shape=(batch_size, seq_len + pad, n_group, state_dim))
dt = mx.random.normal(shape=(batch_size, seq_len + pad, num_heads))
dt_bias = mx.random.normal(shape=(num_heads,))
A_log = mx.random.normal(shape=(num_heads,))
D = mx.random.normal(shape=(num_heads,))
out, out_state = ssm_attn(
hidden_states[:, pad:],
A_log,
B[:, pad:],
C[:, pad:],
D,
dt[:, pad:],
dt_bias,
)
mask = mx.array([[False] * pad + [True] * seq_len])
out_m, out_state_m = ssm_attn(
hidden_states, A_log, B, C, D, dt, dt_bias, mask=mask
)
out_m = out_m[:, pad:]
self.assertTrue(mx.allclose(out, out_m, atol=1e-4, rtol=1e-4))
self.assertTrue(mx.allclose(out_state, out_state_m, atol=1e-4, rtol=1e-4))
def test_ssm_right_pad(self):
batch_size = 1
n_group = 1
num_heads = 48
head_dim = 64
state_dim = 128
seq_len = 4
pad = 2
hidden_states = mx.random.normal(
shape=(batch_size, seq_len + pad, num_heads, head_dim)
)
B = mx.random.normal(shape=(batch_size, seq_len + pad, n_group, state_dim))
C = mx.random.normal(shape=(batch_size, seq_len + pad, n_group, state_dim))
dt = mx.random.normal(shape=(batch_size, seq_len + pad, num_heads))
dt_bias = mx.random.normal(shape=(num_heads,))
A_log = mx.random.normal(shape=(num_heads,))
D = mx.random.normal(shape=(num_heads,))
out, out_state = ssm_attn(
hidden_states[:, :-pad],
A_log,
B[:, :-pad],
C[:, :-pad],
D,
dt[:, :-pad],
dt_bias,
)
mask = mx.array([[True] * seq_len + [False] * pad])
lengths = mx.array([seq_len])
out_m, out_state_m = ssm_attn(
hidden_states,
A_log,
B,
C,
D,
dt,
dt_bias,
mask=mask,
lengths=lengths,
)
out_m = out_m[:, :-pad]
self.assertTrue(mx.allclose(out, out_m, atol=1e-4, rtol=1e-4))
self.assertTrue(mx.allclose(out_state, out_state_m, atol=1e-4, rtol=1e-4))
def test_gated_delta(self):
mx.random.seed(0)
for B in [1, 2]:
for T in [1, 2]:
Hk = 16
Hv = 32
Dk = 128
Dv = 128
q = mx.random.normal(shape=(B, T, Hk, Dk))
k = mx.random.normal(shape=(B, T, Hk, Dk))
v = mx.random.normal(shape=(B, T, Hv, Dv))
g = mx.random.uniform(shape=(B, T, Hv))
beta = mx.random.uniform(shape=(B, T, Hv))
state = mx.random.normal(shape=(B, Hv, Dk, Dv))
y_op, st_op = gated_delta_ops(q, k, v, g, beta, state)
y_c, st_c = gated_delta_kernel(q, k, v, g, beta, state)
self.assertTrue(mx.allclose(y_op, y_c, rtol=1e-4, atol=1e-4))
self.assertTrue(mx.allclose(st_op, st_c, rtol=1e-4, atol=1e-4))
def test_gated_delta_precision(self):
mx.random.seed(42)
N_STEPS = 512
B = 1
Hk = 4
Hv = 4
Dk = 64
Dv = 64
A_log = mx.zeros((Hv,))
dt_bias = mx.ones((Hv,))
all_q = mx.random.normal(shape=(N_STEPS, B, 1, Hk, Dk)) * 0.1
all_k = mx.random.normal(shape=(N_STEPS, B, 1, Hk, Dk)) * 0.1
all_v = mx.random.normal(shape=(N_STEPS, B, 1, Hv, Dv)) * 0.1
all_a = -7.0 + mx.random.normal(shape=(N_STEPS, B, 1, Hv)) * 0.3
all_b = mx.random.normal(shape=(N_STEPS, B, 1, Hv))
mx.eval(all_q, all_k, all_v, all_a, all_b, A_log, dt_bias)
state_ref = mx.zeros((B, Hv, Dv, Dk), dtype=mx.float32)
for t in range(N_STEPS):
y_ref, state_ref = gated_delta_update(
all_q[t],
all_k[t],
all_v[t],
all_a[t],
all_b[t],
A_log,
dt_bias,
state_ref,
use_kernel=False,
)
mx.eval(y_ref, state_ref)
for use_kernel in (False, True):
state_lo = mx.zeros((B, Hv, Dv, Dk), dtype=mx.bfloat16)
for t in range(N_STEPS):
y_lo, state_lo = gated_delta_update(
all_q[t].astype(mx.bfloat16),
all_k[t].astype(mx.bfloat16),
all_v[t].astype(mx.bfloat16),
all_a[t].astype(mx.bfloat16),
all_b[t].astype(mx.bfloat16),
A_log,
dt_bias,
state_lo,
use_kernel=use_kernel,
)
mx.eval(y_lo, state_lo)
self.assertTrue(mx.allclose(state_lo, state_ref, rtol=0.05, atol=0.01))
self.assertTrue(mx.allclose(y_lo, y_ref, rtol=0.05, atol=0.01))
def test_gated_delta_masked(self):
B = 1
T = 3
Hk = 16
Hv = 32
Dk = 128
Dv = 128
mx.random.seed(0)
q = mx.random.normal(shape=(B, T, Hk, Dk))
k = mx.random.normal(shape=(B, T, Hk, Dk))
v = mx.random.normal(shape=(B, T, Hv, Dv))
g = mx.random.normal(shape=(B, T, Hv))
beta = mx.random.normal(shape=(B, T, Hv))
state = mx.random.normal(shape=(B, Hv, Dk, Dv))
for s, e, mask in [
(1, 3, mx.array([[False, True, True]])),
(0, 2, mx.array([[True, True, False]])),
]:
y_gt, st_gt = gated_delta_ops(
q[:, s:e],
k[:, s:e],
v[:, s:e],
g[:, s:e],
beta[:, s:e],
state,
)
for fn in [gated_delta_ops, gated_delta_kernel]:
y, st = fn(q, k, v, g, beta, state, mask)
y = y[:, s:e]
self.assertTrue(mx.allclose(y, y_gt, rtol=1e-4, atol=1e-4))
self.assertTrue(mx.allclose(st, st_gt, rtol=1e-4, atol=1e-3))
if __name__ == "__main__":
unittest.main()