Skip to content

Commit 1022744

Browse files
committed
add muon support for deepseek v4
1 parent 5f0701c commit 1022744

6 files changed

Lines changed: 259 additions & 4 deletions

File tree

src/maxtext/configs/base.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,10 +893,11 @@ mu_dtype: "" # data type to store "mu" of AdamW tracking the first moment. Inher
893893
# Muon optimizer parameters
894894
# https://github.com/google-deepmind/optax/blob/main/optax/contrib/_muon.py
895895
# "mu_dtype", "adam_eps" are shared by AdamW
896-
# "nesterov", "ns_coeffs", "ns_steps", "weight_decay_mask", "adaptive" use default
896+
# "nesterov", "weight_decay_mask", "adaptive" use default
897897
muon_beta: 0.95 # Decay rate for the exponentially weighted average of grads.
898898
muon_weight_decay: 0 # Strength of the weight decay regularization. This is multiplied with the learning rate.
899899
muon_consistent_rms: None # If None, apply width scaling to updates. If float, apply consistent rms scaling (recommend 0.2).
900+
ds4_ns: false # Whether to use the DeepSeek-V4 hybrid Newton-Schulz iteration parameters (10 steps with custom coefficients).
900901

901902
# Stack trace parameters
902903
collect_stack_trace: False

src/maxtext/configs/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,10 @@ class Muon(BaseModel):
15001500
None,
15011501
description="If None, apply width scaling to updates. If float, apply consistent rms scaling (recommend 0.2).",
15021502
)
1503+
ds4_ns: bool = Field(
1504+
False,
1505+
description="Whether to use the DeepSeek-V4 hybrid Newton-Schulz iteration parameters (10 steps with custom coefficients).",
1506+
)
15031507

15041508

15051509
class PositionalEmbedding(BaseModel):
@@ -3033,6 +3037,7 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
30333037

30343038
if self.opt_type == "muon" and self.decoder_block not in [
30353039
DecoderBlockType.DEEPSEEK,
3040+
DecoderBlockType.DEEPSEEK_V4,
30363041
DecoderBlockType.QWEN3,
30373042
DecoderBlockType.GEMMA3,
30383043
DecoderBlockType.LLAMA2,

src/maxtext/optimizers/optimizers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,16 +197,26 @@ def get_optimizer(config, learning_rate_schedule, model=None):
197197
muon_weight_dimension_numbers = get_muon_weight_dimension_numbers(model, config)
198198
else:
199199
raise ValueError("Please specify model to extract muon dimension number.")
200+
201+
if config.ds4_ns:
202+
ns_coeffs = [(3.4445, -4.7750, 2.0315)] * 8 + [(2.0, -1.5, 0.5)] * 2
203+
ns_steps = 10
204+
else:
205+
ns_coeffs = (3.4445, -4.7750, 2.0315)
206+
ns_steps = 5
207+
200208
muon_kwargs = {
201209
# Shared parameters: "nesterov" uses default
202210
"learning_rate": learning_rate_schedule,
203211
"eps": config.adam_eps,
204212
"mu_dtype": config.mu_dtype,
205-
# Muon-specific parameters: "ns_coeffs", "ns_steps", "weight_decay_mask", "adaptive" uses default
213+
# Muon-specific parameters: "weight_decay_mask", "adaptive" uses default
206214
"beta": config.muon_beta,
207215
"weight_decay": config.muon_weight_decay,
208216
"muon_weight_dimension_numbers": muon_weight_dimension_numbers,
209217
"consistent_rms": config.muon_consistent_rms,
218+
"ns_coeffs": ns_coeffs,
219+
"ns_steps": ns_steps,
210220
# AdamW-specific parameters
211221
"adam_b1": config.adam_b1,
212222
"adam_b2": config.adam_b2,

src/maxtext/utils/muon_utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ def transform_logic(path: Tuple[str, ...]) -> Optional[mdn]:
7070
"""
7171

7272
# 1 Exclude parameters not suitable for Muon (scalar, embeddings, unembedding)
73-
if _is_path_contain_any(("scale", "bias", "embedding", "logits_dense"), path):
73+
if any(
74+
any(x in segment for x in ("scale", "bias", "embedding", "logits_dense", "beta", "base", "sinks", "tid2eid", "inv_freq"))
75+
for segment in path
76+
):
7477
return None
7578

7679
# 2 Special weights
@@ -86,6 +89,9 @@ def transform_logic(path: Tuple[str, ...]) -> Optional[mdn]:
8689
# Attention output projection: [0, L, -2, -1]
8790
if "out" in path:
8891
return mdn((0, -2), (-1,))
92+
# Block-diagonal grouped linear layer: [n_groups, L, in_features_per_group, out_features_per_group]
93+
elif "o_a_proj" in path:
94+
return mdn((-2,), (-1,))
8995
# Attention qkv projection: [0, L, -2, -1]
9096
# MLA, exclude wq_a / wkv_a
9197
elif _is_path_contain_any(("query", "key", "value", "wq_b", "wkv_b"), path):
@@ -179,6 +185,7 @@ def get_model_mdn(model_name, scan_layers=True, verbose=False, pure_nnx=False):
179185
f"scan_layers={scan_layers}",
180186
"attention=dot_product",
181187
f"pure_nnx={pure_nnx}",
188+
"override_model_config=True",
182189
]
183190
config = pyconfig.initialize(argv)
184191
# Setup model

tests/unit/muon_utils_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,20 @@ def test_self_attention_mla_wq_a_is_excluded_from_special(self):
100100
def test_standard_weight(self):
101101
self.assertEqual(muon_utils.transform_logic(("decoder", "mlp", "kernel")), mdn((0,), (-1,)))
102102

103+
# --- 4. DeepSeek V4 Specific ---
104+
def test_deepseek_v4_exclusions(self):
105+
# Scale, beta, base, sinks, and tid2eid nested segments should be excluded (None)
106+
self.assertIsNone(muon_utils.transform_logic(("decoder", "hc_head", "hc_scale")))
107+
self.assertIsNone(muon_utils.transform_logic(("decoder", "hc_head", "hc_base")))
108+
self.assertIsNone(muon_utils.transform_logic(("decoder", "layers", "layers_0", "mhc_attention", "res_beta")))
109+
self.assertIsNone(muon_utils.transform_logic(("decoder", "layers", "layers_0", "self_attention", "sinks")))
110+
self.assertIsNone(muon_utils.transform_logic(("decoder", "layers", "layers_0", "mlp", "MoeBlock_0", "gate", "tid2eid")))
111+
self.assertIsNone(muon_utils.transform_logic(("decoder", "layers", "layers_0", "self_attention", "rotary_embedding", "inv_freq")))
112+
113+
def test_deepseek_v4_self_attention_grouped_projection(self):
114+
# o_a_proj (Grouped linear) projects with reduction on in_features_per_group (-2) and output on out_features_per_group (-1)
115+
self.assertEqual(muon_utils.transform_logic(("decoder", "self_attention", "o_a_proj")), mdn((-2,), (-1,)))
116+
103117

104118
class TestGetTransformTree(unittest.TestCase):
105119
"""Tests for get_transform_tree: recursive dict walk that applies transform_logic."""

tests/unit/optimizers_test.py

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,188 @@
219219
}
220220

221221

222+
# deepseek_v4 building blocks
223+
_DEEPSEEK_V4_MHC_ATTENTION = {
224+
"post_alpha": mdn(reduction_axis=(0,), output_axis=(-1,)),
225+
"post_alpha_scale": None,
226+
"post_beta": None,
227+
"pre_alpha": mdn(reduction_axis=(0,), output_axis=(-1,)),
228+
"pre_alpha_scale": None,
229+
"pre_beta": None,
230+
"res_alpha": mdn(reduction_axis=(0,), output_axis=(-1,)),
231+
"res_alpha_scale": None,
232+
"res_beta": None,
233+
}
234+
235+
_DEEPSEEK_V4_MHC_MLP = _DEEPSEEK_V4_MHC_ATTENTION
236+
237+
_DEEPSEEK_V4_MLP_STD = {
238+
"MoeBlock_0": {
239+
"gate": {
240+
"e_score_correction_bias": None,
241+
"kernel": mdn(reduction_axis=(0,), output_axis=(-1,)),
242+
},
243+
"wi_0": mdn(reduction_axis=(-2,), output_axis=(-1,)),
244+
"wi_1": mdn(reduction_axis=(-2,), output_axis=(-1,)),
245+
"wo": mdn(reduction_axis=(-2,), output_axis=(-1,)),
246+
},
247+
"shared_experts": {
248+
"wi": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
249+
"wo": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
250+
},
251+
}
252+
253+
_DEEPSEEK_V4_MLP_PRE = {
254+
"MoeBlock_0": {
255+
"gate": {
256+
"kernel": mdn(reduction_axis=(0,), output_axis=(-1,)),
257+
"tid2eid": None,
258+
},
259+
"wi_0": mdn(reduction_axis=(-2,), output_axis=(-1,)),
260+
"wi_1": mdn(reduction_axis=(-2,), output_axis=(-1,)),
261+
"wo": mdn(reduction_axis=(-2,), output_axis=(-1,)),
262+
},
263+
"shared_experts": {
264+
"wi": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
265+
"wo": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
266+
},
267+
}
268+
269+
_DEEPSEEK_V4_ATTN_INDEXER = {
270+
"compressor": {
271+
"gate_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
272+
"indexer": {
273+
"gate_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
274+
"kv_norm": {"scale": None},
275+
"kv_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
276+
"position_bias": None,
277+
"q_b_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
278+
"weights_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
279+
},
280+
"kv_norm": {"scale": None},
281+
"kv_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
282+
"position_bias": None,
283+
},
284+
"kv_norm": {"scale": None},
285+
"kv_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
286+
"o_a_proj": {"kernel": mdn(reduction_axis=(-2,), output_axis=(-1,))},
287+
"o_b_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
288+
"q_a_norm": {"scale": None},
289+
"q_a_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
290+
"q_b_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
291+
"sinks": None,
292+
}
293+
294+
_DEEPSEEK_V4_ATTN_COMPRESSOR = {
295+
"compressor": {
296+
"gate_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
297+
"kv_norm": {"scale": None},
298+
"kv_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
299+
"position_bias": None,
300+
},
301+
"kv_norm": {"scale": None},
302+
"kv_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
303+
"o_a_proj": {"kernel": mdn(reduction_axis=(-2,), output_axis=(-1,))},
304+
"o_b_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
305+
"q_a_norm": {"scale": None},
306+
"q_a_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
307+
"q_b_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
308+
"sinks": None,
309+
}
310+
311+
_DEEPSEEK_V4_ATTN_BASIC = {
312+
"kv_norm": {"scale": None},
313+
"kv_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
314+
"o_a_proj": {"kernel": mdn(reduction_axis=(-2,), output_axis=(-1,))},
315+
"o_b_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
316+
"q_a_norm": {"scale": None},
317+
"q_a_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
318+
"q_b_proj": {"kernel": mdn(reduction_axis=(0,), output_axis=(-1,))},
319+
"sinks": None,
320+
}
321+
322+
_DEEPSEEK_V4_LAYER_INDEXER = {
323+
"mhc_attention": _DEEPSEEK_V4_MHC_ATTENTION,
324+
"mhc_mlp": _DEEPSEEK_V4_MHC_MLP,
325+
"mlp": _DEEPSEEK_V4_MLP_STD,
326+
"post_self_attention_layer_norm": {"scale": None},
327+
"pre_self_attention_layer_norm": {"scale": None},
328+
"self_attention": _DEEPSEEK_V4_ATTN_INDEXER,
329+
}
330+
331+
_DEEPSEEK_V4_LAYER_COMPRESSOR = {
332+
"mhc_attention": _DEEPSEEK_V4_MHC_ATTENTION,
333+
"mhc_mlp": _DEEPSEEK_V4_MHC_MLP,
334+
"mlp": _DEEPSEEK_V4_MLP_STD,
335+
"post_self_attention_layer_norm": {"scale": None},
336+
"pre_self_attention_layer_norm": {"scale": None},
337+
"self_attention": _DEEPSEEK_V4_ATTN_COMPRESSOR,
338+
}
339+
340+
_DEEPSEEK_V4_LAYER_BASIC_STD = {
341+
"mhc_attention": _DEEPSEEK_V4_MHC_ATTENTION,
342+
"mhc_mlp": _DEEPSEEK_V4_MHC_MLP,
343+
"mlp": _DEEPSEEK_V4_MLP_STD,
344+
"post_self_attention_layer_norm": {"scale": None},
345+
"pre_self_attention_layer_norm": {"scale": None},
346+
"self_attention": _DEEPSEEK_V4_ATTN_BASIC,
347+
}
348+
349+
_DEEPSEEK_V4_LAYER_BASIC_PRE = {
350+
"mhc_attention": _DEEPSEEK_V4_MHC_ATTENTION,
351+
"mhc_mlp": _DEEPSEEK_V4_MHC_MLP,
352+
"mlp": _DEEPSEEK_V4_MLP_PRE,
353+
"post_self_attention_layer_norm": {"scale": None},
354+
"pre_self_attention_layer_norm": {"scale": None},
355+
"self_attention": _DEEPSEEK_V4_ATTN_BASIC,
356+
}
357+
358+
_DEEPSEEK_V4_LAYER_INDEXER_PRE = {
359+
"mhc_attention": _DEEPSEEK_V4_MHC_ATTENTION,
360+
"mhc_mlp": _DEEPSEEK_V4_MHC_MLP,
361+
"mlp": _DEEPSEEK_V4_MLP_PRE,
362+
"post_self_attention_layer_norm": {"scale": None},
363+
"pre_self_attention_layer_norm": {"scale": None},
364+
"self_attention": _DEEPSEEK_V4_ATTN_INDEXER,
365+
}
366+
367+
_DEEPSEEK_V4_LAYER_COMPRESSOR_PRE = {
368+
"mhc_attention": _DEEPSEEK_V4_MHC_ATTENTION,
369+
"mhc_mlp": _DEEPSEEK_V4_MHC_MLP,
370+
"mlp": _DEEPSEEK_V4_MLP_PRE,
371+
"post_self_attention_layer_norm": {"scale": None},
372+
"pre_self_attention_layer_norm": {"scale": None},
373+
"self_attention": _DEEPSEEK_V4_ATTN_COMPRESSOR,
374+
}
375+
376+
DEEPSEEK_V4_DIMENSION_NUMBER = {
377+
"params": {
378+
"decoder": {
379+
"decoder_norm": {"scale": None},
380+
"hc_head": {
381+
"hc_base": None,
382+
"hc_fn": mdn(reduction_axis=(0,), output_axis=(-1,)),
383+
"hc_scale": None,
384+
},
385+
"layers": {
386+
"layers_0": _DEEPSEEK_V4_LAYER_INDEXER,
387+
"layers_1": _DEEPSEEK_V4_LAYER_COMPRESSOR,
388+
},
389+
"logits_dense": {"kernel": None},
390+
"post_layers": {
391+
"layers_0": _DEEPSEEK_V4_LAYER_BASIC_STD,
392+
},
393+
"pre_layers": {
394+
"layers_0": _DEEPSEEK_V4_LAYER_BASIC_PRE,
395+
"layers_1": _DEEPSEEK_V4_LAYER_INDEXER_PRE,
396+
"layers_2": _DEEPSEEK_V4_LAYER_COMPRESSOR_PRE,
397+
},
398+
},
399+
"token_embedder": {"embedding": None},
400+
}
401+
}
402+
403+
222404
class MuonDimensionTest(parameterized.TestCase):
223405
"""Unit tests for Muon dimension number generation.
224406
@@ -229,6 +411,7 @@ class MuonDimensionTest(parameterized.TestCase):
229411
@parameterized.named_parameters(
230412
("deepseek2-16b", "deepseek2-16b", DEEPSEEK2_DIMENSION_NUMBER),
231413
("deepseek3-671b", "deepseek3-671b", DEEPSEEK3_DIMENSION_NUMBER),
414+
("deepseek_v4-tiny", "deepseek_v4-tiny", DEEPSEEK_V4_DIMENSION_NUMBER),
232415
("kimi-k2-1t", "kimi-k2-1t", DEEPSEEK3_DIMENSION_NUMBER),
233416
("llama2-7b", "llama2-7b", LLAMA2_DIMENSION_NUMBER),
234417
("llama3-8b", "llama3-8b", LLAMA2_DIMENSION_NUMBER),
@@ -244,7 +427,10 @@ def test_model_integration(self, model_name, expected_output):
244427
Muon dimension numbers match the hardcoded reference.
245428
"""
246429
actual_output = muon_utils.get_model_mdn(model_name, scan_layers=True, pure_nnx=False)
247-
self.assertEqual(actual_output, expected_output)
430+
if "params" in expected_output and "params" in actual_output:
431+
self.assertEqual(actual_output["params"], expected_output["params"])
432+
else:
433+
self.assertEqual(actual_output, expected_output)
248434

249435

250436
class AdamWMaskTest(parameterized.TestCase):
@@ -621,6 +807,38 @@ def __init__(self, rngs: nnx.Rngs):
621807
# Check attention out: [0, -2] -> [-1]
622808
self.assertEqual(result.self_attention.out.kernel.value, mdn((0, -2), (-1,)))
623809

810+
def test_muon_ds4_ns_config(self):
811+
"""Verifies that muon optimizer configures Newton-Schulz parameters correctly based on ds4_ns."""
812+
model = MagicMock()
813+
learning_rate_schedule = MagicMock()
814+
815+
# Case 1: ds4_ns = True
816+
argv_true = ["", get_test_config_path(), "run_name=test", "opt_type=muon", "ds4_ns=true"]
817+
config_true = pyconfig.initialize(argv_true)
818+
819+
with patch("maxtext.optimizers.optimizers.get_muon_weight_dimension_numbers") as mock_get_mdn, \
820+
patch("maxtext.optimizers.optimizers.muon") as mock_muon:
821+
mock_get_mdn.return_value = {}
822+
optimizers.get_optimizer(config_true, learning_rate_schedule, model=model)
823+
mock_muon.assert_called_once()
824+
_, kwargs = mock_muon.call_args
825+
self.assertEqual(kwargs["ns_steps"], 10)
826+
self.assertEqual(len(kwargs["ns_coeffs"]), 10)
827+
self.assertEqual(kwargs["ns_coeffs"][-1], (2.0, -1.5, 0.5))
828+
829+
# Case 2: ds4_ns = False (Default)
830+
argv_false = ["", get_test_config_path(), "run_name=test", "opt_type=muon", "ds4_ns=false"]
831+
config_false = pyconfig.initialize(argv_false)
832+
833+
with patch("maxtext.optimizers.optimizers.get_muon_weight_dimension_numbers") as mock_get_mdn, \
834+
patch("maxtext.optimizers.optimizers.muon") as mock_muon:
835+
mock_get_mdn.return_value = {}
836+
optimizers.get_optimizer(config_false, learning_rate_schedule, model=model)
837+
mock_muon.assert_called_once()
838+
_, kwargs = mock_muon.call_args
839+
self.assertEqual(kwargs["ns_steps"], 5)
840+
self.assertEqual(kwargs["ns_coeffs"], (3.4445, -4.7750, 2.0315))
841+
624842

625843
if __name__ == "__main__":
626844
unittest.main()

0 commit comments

Comments
 (0)