Skip to content

Commit e476a22

Browse files
authored
Add Mellum (Mellum 2) model support (ml-explore#1339)
1 parent bdb77da commit e476a22

1 file changed

Lines changed: 264 additions & 0 deletions

File tree

mlx_lm/models/mellum.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# Copyright © 2026 Apple Inc.
2+
3+
from dataclasses import dataclass, field
4+
from typing import Any, Dict, List, Optional
5+
6+
import mlx.core as mx
7+
import mlx.nn as nn
8+
9+
from .activations import swiglu
10+
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
11+
from .cache import KVCache, RotatingKVCache
12+
from .rope_utils import initialize_rope
13+
from .switch_layers import SwitchGLU
14+
15+
16+
@dataclass
17+
class ModelArgs(BaseModelArgs):
18+
model_type: str
19+
hidden_size: int
20+
num_hidden_layers: int
21+
intermediate_size: int
22+
num_attention_heads: int
23+
num_experts: int
24+
num_experts_per_tok: int
25+
moe_intermediate_size: int
26+
rms_norm_eps: float
27+
vocab_size: int
28+
num_key_value_heads: int
29+
head_dim: int
30+
tie_word_embeddings: bool
31+
max_position_embeddings: int
32+
norm_topk_prob: bool
33+
sliding_window: int
34+
layer_types: List[str]
35+
rope_parameters: Dict[str, Any] = field(default_factory=dict)
36+
37+
38+
def _rope_for(layer_type: str, args: ModelArgs):
39+
params = args.rope_parameters[layer_type]
40+
base = params["rope_theta"]
41+
rope_type = params.get("rope_type", "default")
42+
if rope_type in ("default", "linear"):
43+
return initialize_rope(args.head_dim, base=base, traditional=False)
44+
scaling_config = dict(params)
45+
scaling_config["type"] = rope_type
46+
return initialize_rope(
47+
args.head_dim,
48+
base=base,
49+
traditional=False,
50+
scaling_config=scaling_config,
51+
max_position_embeddings=args.max_position_embeddings,
52+
)
53+
54+
55+
class Attention(nn.Module):
56+
def __init__(self, args: ModelArgs, layer_idx: int):
57+
super().__init__()
58+
59+
dim = args.hidden_size
60+
self.n_heads = n_heads = args.num_attention_heads
61+
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
62+
head_dim = args.head_dim
63+
self.scale = head_dim**-0.5
64+
65+
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
66+
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
67+
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
68+
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
69+
70+
self.q_norm = nn.RMSNorm(head_dim, eps=args.rms_norm_eps)
71+
self.k_norm = nn.RMSNorm(head_dim, eps=args.rms_norm_eps)
72+
73+
self.rope = _rope_for(args.layer_types[layer_idx], args)
74+
75+
def __call__(
76+
self,
77+
x: mx.array,
78+
mask: Optional[mx.array] = None,
79+
cache: Optional[Any] = None,
80+
) -> mx.array:
81+
B, L, D = x.shape
82+
83+
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
84+
85+
queries = self.q_norm(queries.reshape(B, L, self.n_heads, -1)).transpose(
86+
0, 2, 1, 3
87+
)
88+
keys = self.k_norm(keys.reshape(B, L, self.n_kv_heads, -1)).transpose(
89+
0, 2, 1, 3
90+
)
91+
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
92+
93+
if cache is not None:
94+
queries = self.rope(queries, offset=cache.offset)
95+
keys = self.rope(keys, offset=cache.offset)
96+
keys, values = cache.update_and_fetch(keys, values)
97+
else:
98+
queries = self.rope(queries)
99+
keys = self.rope(keys)
100+
101+
output = scaled_dot_product_attention(
102+
queries, keys, values, cache=cache, scale=self.scale, mask=mask
103+
)
104+
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
105+
return self.o_proj(output)
106+
107+
108+
class MellumSparseMoeBlock(nn.Module):
109+
def __init__(self, args: ModelArgs):
110+
super().__init__()
111+
dim = args.hidden_size
112+
self.num_experts = args.num_experts
113+
self.top_k = args.num_experts_per_tok
114+
self.norm_topk_prob = args.norm_topk_prob
115+
116+
self.gate = nn.Linear(dim, self.num_experts, bias=False)
117+
self.switch_mlp = SwitchGLU(dim, args.moe_intermediate_size, self.num_experts)
118+
119+
def __call__(self, x: mx.array) -> mx.array:
120+
gates = self.gate(x)
121+
gates = mx.softmax(gates, axis=-1, precise=True)
122+
123+
k = self.top_k
124+
inds = mx.argpartition(gates, kth=-k, axis=-1)[..., -k:]
125+
scores = mx.take_along_axis(gates, inds, axis=-1)
126+
if self.norm_topk_prob:
127+
scores /= mx.sum(scores, axis=-1, keepdims=True)
128+
129+
y = self.switch_mlp(x, inds)
130+
y = (y * scores[..., None]).sum(axis=-2)
131+
return y
132+
133+
134+
class MellumDecoderLayer(nn.Module):
135+
def __init__(self, args: ModelArgs, layer_idx: int):
136+
super().__init__()
137+
self.self_attn = Attention(args, layer_idx)
138+
self.mlp = MellumSparseMoeBlock(args)
139+
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
140+
self.post_attention_layernorm = nn.RMSNorm(
141+
args.hidden_size, eps=args.rms_norm_eps
142+
)
143+
144+
def __call__(
145+
self,
146+
x: mx.array,
147+
mask: Optional[mx.array] = None,
148+
cache: Optional[Any] = None,
149+
) -> mx.array:
150+
r = self.self_attn(self.input_layernorm(x), mask, cache)
151+
h = x + r
152+
r = self.mlp(self.post_attention_layernorm(h))
153+
return h + r
154+
155+
156+
class MellumModel(nn.Module):
157+
def __init__(self, args: ModelArgs):
158+
super().__init__()
159+
self.args = args
160+
self.vocab_size = args.vocab_size
161+
assert self.vocab_size > 0
162+
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
163+
self.layers = [
164+
MellumDecoderLayer(args=args, layer_idx=i)
165+
for i in range(args.num_hidden_layers)
166+
]
167+
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
168+
169+
self._first_full = next(
170+
i for i, t in enumerate(args.layer_types) if t == "full_attention"
171+
)
172+
self._first_sliding = next(
173+
(i for i, t in enumerate(args.layer_types) if t == "sliding_attention"),
174+
None,
175+
)
176+
177+
def __call__(
178+
self,
179+
inputs: mx.array,
180+
cache=None,
181+
input_embeddings: Optional[mx.array] = None,
182+
) -> mx.array:
183+
if input_embeddings is not None:
184+
h = input_embeddings
185+
else:
186+
h = self.embed_tokens(inputs)
187+
188+
if cache is None:
189+
cache = [None] * len(self.layers)
190+
191+
full_mask = create_attention_mask(h, cache[self._first_full])
192+
if self._first_sliding is not None:
193+
sliding_mask = create_attention_mask(
194+
h, cache[self._first_sliding], window_size=self.args.sliding_window
195+
)
196+
else:
197+
sliding_mask = None
198+
199+
for layer, c, t in zip(self.layers, cache, self.args.layer_types):
200+
mask = full_mask if t == "full_attention" else sliding_mask
201+
h = layer(h, mask, c)
202+
203+
return self.norm(h)
204+
205+
206+
class Model(nn.Module):
207+
def __init__(self, args: ModelArgs):
208+
super().__init__()
209+
self.args = args
210+
self.model_type = args.model_type
211+
self.model = MellumModel(args)
212+
if not args.tie_word_embeddings:
213+
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
214+
215+
def __call__(
216+
self,
217+
inputs: mx.array,
218+
cache=None,
219+
input_embeddings: Optional[mx.array] = None,
220+
) -> mx.array:
221+
out = self.model(inputs, cache, input_embeddings)
222+
if self.args.tie_word_embeddings:
223+
out = self.model.embed_tokens.as_linear(out)
224+
else:
225+
out = self.lm_head(out)
226+
return out
227+
228+
def sanitize(self, weights):
229+
if self.args.tie_word_embeddings:
230+
weights.pop("lm_head.weight", None)
231+
if "model.layers.0.mlp.experts.0.up_proj.weight" not in weights:
232+
return weights
233+
for l in range(self.args.num_hidden_layers):
234+
prefix = f"model.layers.{l}"
235+
for n in ["up_proj", "down_proj", "gate_proj"]:
236+
if f"{prefix}.mlp.experts.0.{n}.weight" in weights:
237+
to_join = [
238+
weights.pop(f"{prefix}.mlp.experts.{e}.{n}.weight")
239+
for e in range(self.args.num_experts)
240+
]
241+
weights[f"{prefix}.mlp.switch_mlp.{n}.weight"] = mx.stack(to_join)
242+
return weights
243+
244+
@property
245+
def quant_predicate(self):
246+
def predicate(path, _):
247+
if path.endswith("mlp.gate"):
248+
return {"group_size": 64, "bits": 8}
249+
return True
250+
251+
return predicate
252+
253+
@property
254+
def layers(self):
255+
return self.model.layers
256+
257+
def make_cache(self):
258+
caches = []
259+
for t in self.args.layer_types:
260+
if t == "full_attention":
261+
caches.append(KVCache())
262+
else:
263+
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
264+
return caches

0 commit comments

Comments
 (0)