Skip to content

Commit 0b3bd20

Browse files
committed
feat: implement Muon optimizer support for DeepSeek V4 architecture
1 parent 67c79bb commit 0b3bd20

8 files changed

Lines changed: 276 additions & 7 deletions

File tree

src/maxtext/checkpoint_conversion/utils/param_mapping.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3873,6 +3873,7 @@ def reshape_vision_attn_out(input_tensor, target_shape):
38733873

38743874

38753875
def DEEPSEEKV4_MAXTEXT_TO_HF_PARAM_MAPPING(config, maxtext_config, scan_layers=False):
3876+
"""Maps MaxText parameter keys to HuggingFace parameter keys for DeepSeek V4."""
38763877
n_layers = config["num_hidden_layers"]
38773878
num_experts = config.get("n_routed_experts", 8)
38783879

@@ -4027,6 +4028,8 @@ def get_hf_expert_keys(expert_subpath_template):
40274028

40284029

40294030
def DEEPSEEKV4_MAXTEXT_TO_HF_PARAM_HOOK_FN(config, maxtext_config, scan_layers=False, saving_to_hf=False):
4031+
"""Returns hook functions for transforming weights between MaxText and HuggingFace for DeepSeek V4."""
4032+
40304033
def transpose(input_tensor, target_shape=None):
40314034
return np.transpose(input_tensor)
40324035

@@ -4046,7 +4049,6 @@ def reshape_transpose_wq_b(input_tensor, target_shape=None):
40464049
# HF: [n_heads * q_head_dim, kv_lora_rank]
40474050
# MaxText: [kv_lora_rank, n_heads, q_head_dim]
40484051
tensor = np.transpose(input_tensor) # [kv_lora_rank, n_heads * q_head_dim]
4049-
n_heads = config["num_attention_heads"]
40504052
return tensor.reshape(target_shape)
40514053

40524054
def reshape_transpose_wkv(input_tensor, target_shape=None):
@@ -4132,7 +4134,7 @@ def mhc_split_scale_res(input_tensor, target_shape=None):
41324134
mapping[key] = transpose
41334135
elif "hc_head-hc_base" in key or "hc_head-hc_scale" in key:
41344136
mapping[key] = identity
4135-
elif type(hf_key) == list:
4137+
elif isinstance(hf_key, list):
41364138
mapping[key] = transpose if not saving_to_hf else transpose_stack
41374139
elif "-kernel" in key or "-embedding" in key or "-sinks" in key:
41384140
mapping[key] = transpose

src/maxtext/configs/base.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ mu_dtype: "" # data type to store "mu" of AdamW tracking the first moment. Inher
927927
# Muon optimizer parameters
928928
# https://github.com/google-deepmind/optax/blob/main/optax/contrib/_muon.py
929929
# "mu_dtype", "adam_eps" are shared by AdamW
930-
# "nesterov", "ns_coeffs", "ns_steps", "weight_decay_mask", "adaptive" use default
930+
# "nesterov", "weight_decay_mask", "adaptive" use default
931931
muon_beta: 0.95 # Decay rate for the exponentially weighted average of grads.
932932
muon_weight_decay: 0 # Strength of the weight decay regularization. This is multiplied with the learning rate.
933933
muon_consistent_rms: None # If None, apply width scaling to updates. If float, apply consistent rms scaling (recommend 0.2).

src/maxtext/configs/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3376,6 +3376,7 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
33763376

33773377
if self.opt_type == "muon" and self.decoder_block not in [
33783378
DecoderBlockType.DEEPSEEK,
3379+
DecoderBlockType.DEEPSEEK4,
33793380
DecoderBlockType.QWEN3,
33803381
DecoderBlockType.GEMMA3,
33813382
DecoderBlockType.LLAMA2,

src/maxtext/optimizers/optimizers.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,16 +197,29 @@ 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+
# pylint: disable=import-outside-toplevel
202+
from maxtext.configs.types import DecoderBlockType
203+
204+
if config.decoder_block == DecoderBlockType.DEEPSEEK4:
205+
ns_coeffs = [(3.4445, -4.7750, 2.0315)] * 8 + [(2.0, -1.5, 0.5)] * 2
206+
ns_steps = 10
207+
else:
208+
ns_coeffs = (3.4445, -4.7750, 2.0315)
209+
ns_steps = 5
210+
200211
muon_kwargs = {
201212
# Shared parameters: "nesterov" uses default
202213
"learning_rate": learning_rate_schedule,
203214
"eps": config.adam_eps,
204215
"mu_dtype": config.mu_dtype,
205-
# Muon-specific parameters: "ns_coeffs", "ns_steps", "weight_decay_mask", "adaptive" uses default
216+
# Muon-specific parameters: "weight_decay_mask", "adaptive" uses default
206217
"beta": config.muon_beta,
207218
"weight_decay": config.muon_weight_decay,
208219
"muon_weight_dimension_numbers": muon_weight_dimension_numbers,
209220
"consistent_rms": config.muon_consistent_rms,
221+
"ns_coeffs": ns_coeffs,
222+
"ns_steps": ns_steps,
210223
# AdamW-specific parameters
211224
"adam_b1": config.adam_b1,
212225
"adam_b2": config.adam_b2,

src/maxtext/utils/muon_utils.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ 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", "embedding", "logits_dense", "beta", "base", "sinks", "tid2eid", "inv_freq"))
75+
or segment == "bias"
76+
for segment in path
77+
):
7478
return None
7579

7680
# 2 Special weights
@@ -86,9 +90,12 @@ def transform_logic(path: Tuple[str, ...]) -> Optional[mdn]:
8690
# Attention output projection: [0, L, -2, -1]
8791
if "out" in path:
8892
return mdn((0, -2), (-1,))
93+
# Block-diagonal grouped linear layer: [n_groups, L, in_features_per_group, out_features_per_group]
94+
elif "o_a_proj" in path:
95+
return mdn((-2,), (-1,))
8996
# Attention qkv projection: [0, L, -2, -1]
9097
# MLA, exclude wq_a / wkv_a
91-
elif _is_path_contain_any(("query", "key", "value", "wq_b", "wkv_b"), path):
98+
elif _is_path_contain_any(("query", "key", "value", "wq_b", "wkv_b", "wkv"), path):
9299
return mdn((0,), (-2, -1))
93100

94101
# 3 Standard weights, [0, L, -1]
@@ -182,7 +189,16 @@ def get_model_mdn(model_name, scan_layers=True, verbose=False, pure_nnx=False):
182189
f"scan_layers={scan_layers}",
183190
"attention=dot_product",
184191
f"pure_nnx={pure_nnx}",
192+
"override_model_config=True",
193+
"skip_jax_distributed_system=True",
185194
]
195+
if not pure_nnx:
196+
argv.extend(
197+
[
198+
"enable_nnx=False",
199+
"pure_nnx_decoder=False",
200+
]
201+
)
186202
config = pyconfig.initialize(argv)
187203
# Setup model
188204
devices_array = maxtext_utils.create_device_mesh(config)

tests/unit/deepseek_v4_vs_reference_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,7 @@ def setUp(self):
12491249
self.mesh = jax.sharding.Mesh(devices, ("tensor",))
12501250

12511251
def _apply_param_mapping(self, mt_layer, pt_layer, l):
1252+
"""Maps PT weights to MaxText layer."""
12521253
from maxtext.checkpoint_conversion.utils.param_mapping import (
12531254
DEEPSEEKV4_MAXTEXT_TO_HF_PARAM_MAPPING,
12541255
DEEPSEEKV4_MAXTEXT_TO_HF_PARAM_HOOK_FN,

tests/unit/muon_utils_test.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,25 @@ 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(
111+
muon_utils.transform_logic(("decoder", "layers", "layers_0", "mlp", "MoeBlock_0", "gate", "tid2eid"))
112+
)
113+
self.assertIsNone(
114+
muon_utils.transform_logic(("decoder", "layers", "layers_0", "self_attention", "rotary_embedding", "inv_freq"))
115+
)
116+
117+
def test_deepseek_v4_self_attention_grouped_projection(self):
118+
# o_a_proj projects with reduction on in_features_per_group (-2)
119+
# and output on out_features_per_group (-1)
120+
self.assertEqual(muon_utils.transform_logic(("decoder", "self_attention", "o_a_proj")), mdn((-2,), (-1,)))
121+
103122

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

0 commit comments

Comments
 (0)