Skip to content

Commit 4bc21cc

Browse files
authored
Ministral3 (#642)
* attempt ministral3, no tokenizer * ministral3 works
1 parent 9fd3e41 commit 4bc21cc

2 files changed

Lines changed: 277 additions & 3 deletions

File tree

mlx_lm/models/ministral3.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# Copyright © 2023-2024 Apple Inc.
2+
3+
from dataclasses import dataclass
4+
from typing import Any, Dict, List, Optional, Union
5+
6+
import mlx.core as mx
7+
import mlx.nn as nn
8+
9+
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
10+
from .cache import KVCache, RotatingKVCache
11+
from .rope_utils import initialize_rope
12+
13+
14+
@dataclass
15+
class ModelArgs(BaseModelArgs):
16+
model_type: str
17+
hidden_size: int
18+
num_hidden_layers: int
19+
intermediate_size: int
20+
num_attention_heads: int
21+
rms_norm_eps: float
22+
vocab_size: int
23+
head_dim: Optional[int] = None
24+
max_position_embeddings: Optional[int] = None
25+
num_key_value_heads: Optional[int] = None
26+
rope_parameters: Optional[Dict[str, Union[float, str]]] = None
27+
tie_word_embeddings: bool = True
28+
layer_types: Optional[List[str]] = None
29+
sliding_window: Optional[int] = None
30+
31+
def __post_init__(self):
32+
if self.num_key_value_heads is None:
33+
self.num_key_value_heads = self.num_attention_heads
34+
35+
if self.layer_types is None:
36+
self.layer_types = ["full_attention"] * self.num_hidden_layers
37+
38+
39+
def _get_llama_4_attn_scale(
40+
start: int, stop: int, beta: float, max_position_embeddings: int
41+
):
42+
scaling = 1 + beta * mx.log(
43+
1 + mx.floor(mx.arange(start, stop) / max_position_embeddings)
44+
)
45+
return scaling[:, None]
46+
47+
48+
class Attention(nn.Module):
49+
def __init__(self, args: ModelArgs):
50+
super().__init__()
51+
52+
dim = args.hidden_size
53+
self.n_heads = n_heads = args.num_attention_heads
54+
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
55+
56+
self.head_dim = head_dim = args.head_dim or args.hidden_size // n_heads
57+
58+
self.scale = head_dim**-0.5
59+
60+
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
61+
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
62+
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
63+
self.o_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
64+
65+
self.rope = initialize_rope(
66+
self.head_dim,
67+
args.rope_parameters["rope_theta"],
68+
False,
69+
args.rope_parameters,
70+
args.max_position_embeddings,
71+
)
72+
73+
def __call__(
74+
self,
75+
x: mx.array,
76+
attn_scale: mx.array,
77+
mask: Optional[mx.array] = None,
78+
cache: Optional[Any] = None,
79+
) -> mx.array:
80+
B, L, D = x.shape
81+
82+
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
83+
84+
# Prepare the queries, keys and values for the attention computation
85+
queries = queries.reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
86+
keys = keys.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
87+
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
88+
89+
offset = 0
90+
if cache is not None:
91+
offset = cache.offset
92+
queries = self.rope(queries, offset=offset)
93+
keys = self.rope(keys, offset=offset)
94+
keys, values = cache.update_and_fetch(keys, values)
95+
else:
96+
queries = self.rope(queries)
97+
keys = self.rope(keys)
98+
queries = queries * attn_scale
99+
output = scaled_dot_product_attention(
100+
queries, keys, values, cache=cache, scale=self.scale, mask=mask
101+
)
102+
103+
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
104+
return self.o_proj(output)
105+
106+
107+
class MLP(nn.Module):
108+
def __init__(self, args: ModelArgs):
109+
super().__init__()
110+
111+
dim = args.hidden_size
112+
hidden_dim = args.intermediate_size
113+
self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
114+
self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
115+
self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
116+
117+
def __call__(self, x) -> mx.array:
118+
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
119+
120+
121+
class TransformerBlock(nn.Module):
122+
def __init__(self, args: ModelArgs, use_sliding: bool = False):
123+
super().__init__()
124+
self.num_attention_heads = args.num_attention_heads
125+
self.hidden_size = args.hidden_size
126+
self.use_sliding = use_sliding
127+
self.self_attn = Attention(args)
128+
self.mlp = MLP(args)
129+
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
130+
self.post_attention_layernorm = nn.RMSNorm(
131+
args.hidden_size, eps=args.rms_norm_eps
132+
)
133+
self.args = args
134+
135+
def __call__(
136+
self,
137+
x: mx.array,
138+
attn_scale: mx.array,
139+
mask: Optional[mx.array] = None,
140+
cache: Optional[Any] = None,
141+
) -> mx.array:
142+
r = self.self_attn(self.input_layernorm(x), attn_scale, mask, cache)
143+
h = x + r
144+
r = self.mlp(self.post_attention_layernorm(h))
145+
out = h + r
146+
return out
147+
148+
149+
class LanguageModel(nn.Module):
150+
def __init__(self, args: ModelArgs):
151+
super().__init__()
152+
self.args = args
153+
self.vocab_size = args.vocab_size
154+
self.num_hidden_layers = args.num_hidden_layers
155+
self.layer_types = args.layer_types
156+
self.sliding_window = args.sliding_window
157+
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
158+
self.layers = [
159+
TransformerBlock(args=args, use_sliding=layer_type == "sliding_attention")
160+
for layer_type in self.layer_types
161+
]
162+
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
163+
self.fa_idx = self.layer_types.index("full_attention")
164+
self.swa_idx = None
165+
for e, l in enumerate(self.layers):
166+
if l.use_sliding:
167+
self.swa_idx = e
168+
break
169+
170+
def __call__(
171+
self,
172+
inputs: mx.array,
173+
cache=None,
174+
input_embeddings: Optional[mx.array] = None,
175+
):
176+
if input_embeddings is not None:
177+
h = input_embeddings
178+
else:
179+
h = self.embed_tokens(inputs)
180+
181+
if cache is None:
182+
cache = [None] * len(self.layers)
183+
offset = 0
184+
else:
185+
offset = cache[0].offset
186+
187+
fa_mask = create_attention_mask(h, cache[self.fa_idx])
188+
if self.swa_idx is not None:
189+
swa_mask = create_attention_mask(
190+
h, cache[self.swa_idx], window_size=self.sliding_window
191+
)
192+
193+
attn_scale = _get_llama_4_attn_scale(
194+
offset,
195+
offset + inputs.shape[1],
196+
self.args.rope_parameters["llama_4_scaling_beta"],
197+
self.args.rope_parameters["original_max_position_embeddings"],
198+
).astype(h.dtype)
199+
200+
for layer, cache in zip(self.layers, cache):
201+
mask = swa_mask if layer.use_sliding else fa_mask
202+
h = layer(h, attn_scale, mask, cache=cache)
203+
204+
return self.norm(h)
205+
206+
207+
class Model(nn.Module):
208+
def __init__(self, args: ModelArgs):
209+
super().__init__()
210+
self.args = args
211+
self.model_type = args.model_type
212+
self.model = LanguageModel(args)
213+
if not args.tie_word_embeddings:
214+
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
215+
216+
def __call__(
217+
self,
218+
inputs: mx.array,
219+
cache=None,
220+
input_embeddings: Optional[mx.array] = None,
221+
):
222+
out = self.model(inputs, cache, input_embeddings)
223+
if self.args.tie_word_embeddings:
224+
out = self.model.embed_tokens.as_linear(out)
225+
else:
226+
out = self.lm_head(out)
227+
return out
228+
229+
def sanitize(self, weights):
230+
# Remove unused precomputed rotary freqs
231+
weights = {
232+
k: v for k, v in weights.items() if "self_attn.rotary_emb.inv_freq" not in k
233+
}
234+
if self.args.tie_word_embeddings:
235+
weights.pop("lm_head.weight", None)
236+
237+
new_weights = {}
238+
for k, v in weights.items():
239+
if "weight_scale_inv" in k:
240+
scale_inv = v
241+
wk = k.replace("_scale_inv", "")
242+
weight = weights[wk]
243+
new_weights[wk] = weight * scale_inv
244+
elif "activation_scale" in k:
245+
continue
246+
elif k not in new_weights:
247+
new_weights[k] = v
248+
weights = new_weights
249+
250+
return weights
251+
252+
@property
253+
def layers(self):
254+
return self.model.layers
255+
256+
def make_cache(self):
257+
return [
258+
(
259+
RotatingKVCache(max_size=self.model.sliding_window)
260+
if layer.use_sliding
261+
else KVCache()
262+
)
263+
for layer in self.layers
264+
]

mlx_lm/models/mistral3.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import mlx.nn as nn
88
from mlx.utils import tree_flatten, tree_unflatten
99

10-
from . import llama
10+
from . import llama, ministral3
1111
from .base import BaseModelArgs
1212

1313

@@ -17,15 +17,23 @@ class ModelArgs(BaseModelArgs):
1717
text_config: dict
1818

1919
def __post_init__(self):
20-
self.text_config["tie_word_embeddings"] = False
20+
if "tie_word_embeddings" not in self.text_config:
21+
self.text_config["tie_word_embeddings"] = False
2122

2223

2324
class Model(nn.Module):
2425
def __init__(self, args: ModelArgs):
2526
super().__init__()
2627
self.args = args
2728
self.model_type = args.model_type
28-
self.language_model = llama.Model(llama.ModelArgs.from_dict(args.text_config))
29+
if args.text_config.get("model_type") == "ministral3":
30+
self.language_model = ministral3.Model(
31+
ministral3.ModelArgs.from_dict(args.text_config)
32+
)
33+
else:
34+
self.language_model = llama.Model(
35+
llama.ModelArgs.from_dict(args.text_config)
36+
)
2937

3038
def __call__(
3139
self,
@@ -41,6 +49,8 @@ def sanitize(self, weights):
4149
weights = tree_unflatten(list(weights.items()))
4250
weights.pop("vision_tower", None)
4351
weights.pop("multi_modal_projector", None)
52+
lm_weights = dict(tree_flatten(weights["language_model"]))
53+
weights["language_model"] = self.language_model.sanitize(lm_weights)
4454
return dict(tree_flatten(weights))
4555

4656
@property

0 commit comments

Comments
 (0)