Skip to content

Commit 23adce0

Browse files
committed
feat(models): Integrate DeepSeek V4 architecture and routing
This commit introduces full support for DeepSeek V4 by integrating its compressed attention mechanisms, MoE routing, and architectural layers. Key changes: - Add `deepseek4.yml` configuration and `DeepSeek4DecoderLayer` implementation. - Implement hybrid Hash Routing and Token Routing for MoE layers. - Add prefix/suffix layer unrolling for non-uniform compression blocks. - Fix Pydantic validation for base MLP dimensions. - Bypass MLA instantiation in favor of native CompressedAttention (CSA/HCA).
1 parent 5d4de12 commit 23adce0

10 files changed

Lines changed: 507 additions & 45 deletions

File tree

src/maxtext/common/common_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class DecoderBlockType(enum.Enum):
113113
SIMPLE_MLP = "simple_mlp"
114114
LLAMA4 = "llama4"
115115
OLMO3 = "olmo3"
116+
DEEPSEEK4 = "deepseek4"
116117

117118

118119
class AttentionType(enum.Enum):
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright 2023–2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# model config for DeepSeek V4
16+
17+
base_emb_dim: 4096
18+
base_num_query_heads: 64
19+
base_num_kv_heads: 1
20+
base_num_decoder_layers: 44
21+
base_mlp_dim: 2048
22+
base_moe_mlp_dim: 2048
23+
vocab_size: 129280
24+
25+
# --- Standard Defaults ---
26+
enable_dropout: false
27+
logits_via_embedding: false
28+
normalization_layer_epsilon: 1.0e-6
29+
30+
# --- V4 Specific Architectural Keys ---
31+
decoder_block: "deepseek4"
32+
mhc_expansion_rate: 4
33+
first_num_hash_layers: 3
34+
indexer_head_dim: 128
35+
indexer_n_heads: 64
36+
indexer_topk: 512
37+
38+
# Note: Layers (0,1,43) are not compressed.
39+
compress_ratios: [0, 0, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 0]
40+
41+
# --- MoE configuration ---
42+
mlp_activations: ["silu", "linear"]
43+
num_experts: 256
44+
num_experts_per_tok: 6
45+
shared_experts: 1
46+
routed_score_func: "sqrtsoftplus"
47+
48+
# --- Attention configuration ---
49+
attention: 'dot_product'
50+
attention_type: 'compressed'
51+
q_lora_rank: 1024
52+
o_groups: 8
53+
o_lora_rank: 1024
54+
sliding_window_size: 128
55+
56+
# --- RoPE ---
57+
58+
rope_type: "default"
59+
rope_max_timescale: 10000 # Main RoPE theta
60+
compressed_rope_max_timescale: 160000 # Compressed RoPE theta
61+
max_position_embeddings: 1048576

src/maxtext/configs/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ class Attention(BaseModel):
553553
"autoselected",
554554
description="The attention algorithm to use (dot_product, flash, etc).",
555555
)
556-
attention_type: Literal["global", "local_sliding", "chunk", "mla", "full"] = Field(
556+
attention_type: Literal["global", "local_sliding", "chunk", "mla", "full", "compressed"] = Field(
557557
"global", description="The variant of attention to use."
558558
)
559559
share_kv_projections: bool = Field(

src/maxtext/layers/attention_compressed.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -694,10 +694,6 @@ def __init__(
694694
self.q_lora_rank = q_lora_rank
695695
self.compress_ratio = compress_ratio
696696

697-
# Determine the correct underlying attention type based on the compress_ratio
698-
if self.compress_ratio == 0:
699-
attention_type = AttentionType.LOCAL_SLIDING
700-
701697
super().__init__(
702698
config=config,
703699
num_query_heads=num_query_heads,
@@ -727,6 +723,7 @@ def __init__(
727723
use_bias_in_projections=use_bias_in_projections,
728724
name=name,
729725
rngs=rngs,
726+
rope_type="deepseek4",
730727
**kwargs,
731728
)
732729

@@ -1047,7 +1044,7 @@ def __call__(
10471044
# -> [batch, q_length, emb_dim]
10481045
final_out = self.o_b_proj(grouped_flat)
10491046

1050-
return final_out
1047+
return final_out, None
10511048

10521049

10531050
def compressed_attention(

src/maxtext/layers/attentions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
Qwen3OmniMoeVisionRotaryEmbedding,
5959
RotaryEmbedding,
6060
YarnRotaryEmbedding,
61+
DeepSeekV4RotaryEmbedding,
6162
PartialRotaryEmbedding,
6263
Gemma4PartialRotaryEmbedding,
6364
)
@@ -850,6 +851,13 @@ def init_rotary_embedding(self):
850851
shard_mode=self.config.shard_mode,
851852
rngs=self.rngs,
852853
)
854+
elif rope_type == "deepseek4":
855+
rotary_embedding = DeepSeekV4RotaryEmbedding(
856+
head_dim=rope_embedding_dims,
857+
partial_rotary_factor=self.partial_rotary_factor if self.partial_rotary_factor is not None else 1.0,
858+
rope_theta=self.rope_max_timescale,
859+
dtype=self.dtype,
860+
)
853861
elif self.is_qwen3_hybrid:
854862
rotary_embedding = PartialRotaryEmbedding(
855863
min_timescale=self.config.rope_min_timescale,

src/maxtext/layers/decoders.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from maxtext.layers.quantizations import AqtQuantization as Quant
4141
from maxtext.models import (
4242
deepseek,
43+
deepseek4,
4344
deepseek_batchsplit,
4445
deepseek_batchsplit_fp8,
4546
gemma,
@@ -457,6 +458,10 @@ def get_decoder_layers(self):
457458
deepseek.DeepSeekDenseLayerToLinen,
458459
deepseek.DeepSeekMoELayerToLinen,
459460
]
461+
case DecoderBlockType.DEEPSEEK4:
462+
return (
463+
[deepseek4.DeepSeek4ScannableBlockToLinen] if self.config.scan_layers else [deepseek4.DeepSeek4LayerToLinen]
464+
)
460465
case DecoderBlockType.GEMMA:
461466
return [gemma.GemmaDecoderLayerToLinen]
462467
case DecoderBlockType.GEMMA2:
@@ -534,6 +539,7 @@ def get_norm_layer(self, num_features: int):
534539
DecoderBlockType.MISTRAL,
535540
DecoderBlockType.MIXTRAL,
536541
DecoderBlockType.DEEPSEEK,
542+
DecoderBlockType.DEEPSEEK4,
537543
DecoderBlockType.GEMMA,
538544
DecoderBlockType.GEMMA2,
539545
DecoderBlockType.GEMMA3,
@@ -999,6 +1005,17 @@ def __call__(
9991005
previous_chunk,
10001006
slot,
10011007
)
1008+
elif cfg.decoder_block == DecoderBlockType.DEEPSEEK4:
1009+
y = self._apply_deepseek4_scanned_blocks(
1010+
y,
1011+
decoder_segment_ids,
1012+
decoder_positions,
1013+
deterministic,
1014+
model_mode,
1015+
previous_chunk,
1016+
slot,
1017+
decoder_input_tokens,
1018+
)
10021019
else:
10031020
RemattedBlockLayer = RemattedBlockLayers[0]
10041021
scan_length = int(cfg.num_decoder_layers / cfg.inhomogeneous_layer_cycle_interval)
@@ -1133,7 +1150,7 @@ def __call__(
11331150
"is_nope_layer": llama4.determine_is_nope_layer(lyr, self.config.nope_layer_interval),
11341151
"is_moe_layer": llama4.determine_is_moe_layer(lyr, self.config.interleave_moe_layer_step),
11351152
}
1136-
if cfg.decoder_block in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5):
1153+
if cfg.decoder_block in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5, DecoderBlockType.DEEPSEEK4):
11371154
layer_kwargs = {"layer_idx": lyr}
11381155
kv_cache = None
11391156
if kv_caches is not None:
@@ -1355,6 +1372,111 @@ def _apply_gemma4_scanned_blocks(
13551372

13561373
return y
13571374

1375+
def _apply_deepseek4_scanned_blocks(
1376+
self,
1377+
y,
1378+
decoder_segment_ids,
1379+
decoder_positions,
1380+
deterministic,
1381+
model_mode,
1382+
previous_chunk,
1383+
slot,
1384+
decoder_input_tokens,
1385+
):
1386+
"""Applies DeepSeek V4 scanned decoder blocks.
1387+
1388+
DeepSeek V4 has 44 layers. The first 2 (0,1) and the last 2 (42,43) break the perfectly
1389+
repeating `[4, 128]` sequence of compression ratios. Therefore, we manually unroll the
1390+
prefix and suffix layers, and scan the perfectly repeating 40 layers in the middle
1391+
(20 chunks of 2).
1392+
"""
1393+
1394+
cfg = self.config
1395+
mesh = self.mesh
1396+
1397+
broadcast_args = (
1398+
decoder_segment_ids,
1399+
decoder_positions,
1400+
deterministic,
1401+
model_mode,
1402+
slot,
1403+
previous_chunk,
1404+
)
1405+
1406+
layer_call_kwargs = {
1407+
"previous_chunk": previous_chunk,
1408+
"slot": slot,
1409+
"decoder_input_tokens": decoder_input_tokens,
1410+
}
1411+
1412+
# 1. Prefix Unrolling (Layers 0, 1)
1413+
# These layers use Hash Routing and compress_ratio=0.
1414+
for layer_idx in range(2):
1415+
prefix_layer = deepseek4.DeepSeek4LayerToLinen(
1416+
config=cfg,
1417+
mesh=mesh,
1418+
name=f"layers_{layer_idx}",
1419+
quant=self.quant,
1420+
model_mode=self.model_mode,
1421+
layer_idx=layer_idx,
1422+
)
1423+
y, _ = prefix_layer(
1424+
y,
1425+
decoder_segment_ids,
1426+
decoder_positions,
1427+
deterministic,
1428+
model_mode,
1429+
**layer_call_kwargs,
1430+
)
1431+
1432+
# 2. Chunked Scanning (Layers 2 to 41)
1433+
# These 40 layers perfectly alternate CSA (4) and HCA (128).
1434+
num_full_blocks = 20
1435+
if num_full_blocks > 0:
1436+
ScannableBlockToLinen = deepseek4.DeepSeek4ScannableBlockToLinen
1437+
policy = self.get_remat_policy()
1438+
RemattedDeepSeek4Block = self.set_remat_policy([ScannableBlockToLinen], policy)[0]
1439+
1440+
y, _ = nn.scan(
1441+
RemattedDeepSeek4Block,
1442+
variable_axes={
1443+
"params": cfg.param_scan_axis,
1444+
"cache": 0,
1445+
"intermediates": 0,
1446+
"aqt": 0,
1447+
"_overwrite_with_gradient": 0,
1448+
},
1449+
split_rngs={"params": True, "dropout": cfg.enable_dropout},
1450+
in_axes=(nn.broadcast,) * len(broadcast_args),
1451+
length=num_full_blocks,
1452+
metadata_params={
1453+
nn.PARTITION_NAME: "layers",
1454+
"abstract_init": False,
1455+
},
1456+
)(config=cfg, mesh=mesh, quant=self.quant, model_mode=model_mode, name="scanned_blocks",)(y, *broadcast_args)
1457+
1458+
# 3. Suffix Unrolling (Layers 42, 43)
1459+
# Layer 42 is CSA (4), Layer 43 is full MoE (0). Both use Top-K routing.
1460+
for layer_idx in range(42, 44):
1461+
suffix_layer = deepseek4.DeepSeek4LayerToLinen(
1462+
config=cfg,
1463+
mesh=mesh,
1464+
name=f"layers_{layer_idx}",
1465+
quant=self.quant,
1466+
model_mode=self.model_mode,
1467+
layer_idx=layer_idx,
1468+
)
1469+
y, _ = suffix_layer(
1470+
y,
1471+
decoder_segment_ids,
1472+
decoder_positions,
1473+
deterministic,
1474+
model_mode,
1475+
**layer_call_kwargs,
1476+
)
1477+
1478+
return y
1479+
13581480
def _apply_gemma4_small_layers(
13591481
self,
13601482
y,

src/maxtext/layers/moe.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ def calculate_load_balance_updates(top_k_indices, num_experts, rate):
208208
return output
209209

210210

211+
class Tid2EidVar(nnx.Variable):
212+
"""Custom variable to hold tid2eid without trainable param overhead."""
213+
214+
211215
class GateLogit(nnx.Module):
212216
"""A layer used to compute gate logits, allowing to return the pre bias values for DeepSeek routing."""
213217

@@ -399,8 +403,11 @@ def __init__(
399403
# DeepSeek V4 Hash Routing
400404
if self.is_hash_routing:
401405
# Token-ID to Expert-ID lookup table for static routing
402-
self.tid2eid = nnx.Variable(
403-
jnp.zeros((self.config.vocab_size, self.num_experts_per_tok), dtype=jnp.int32),
406+
# Must be stored as float32 because MaxText passes the entire variable tree
407+
# through jax.value_and_grad, which strictly requires all leaves to be inexact types
408+
# (even if they receive no gradients). We cast to int32 dynamically during routing.
409+
self.tid2eid = Tid2EidVar(
410+
jnp.zeros((self.config.vocab_size, self.num_experts_per_tok), dtype=jnp.float32),
404411
out_sharding=None, # Replicated across shards for local lookup
405412
)
406413
else:
@@ -665,7 +672,13 @@ def get_topk(self, gate_logits, pre_bias_logits, rngs=None, input_ids=None):
665672
return top_k_weights, top_k_indices
666673

667674
if self.is_hash_routing:
668-
top_k_indices = self.tid2eid[input_ids]
675+
if input_ids is None:
676+
raise ValueError("input_ids cannot be None when is_hash_routing is True")
677+
# Access the static routing table
678+
tid2eid_int = self.tid2eid.value
679+
# Cast the float32 array to int32 (JAX automatically assigns 0.0 gradients to integer casts)
680+
tid2eid_int = tid2eid_int.astype(jnp.int32)
681+
top_k_indices = tid2eid_int[input_ids]
669682
top_k_weights = jnp.take_along_axis(pre_bias_logits, top_k_indices, axis=-1)
670683
# NOTE: deepseek2 has a different pattern
671684
elif self.config.model_name.startswith(("deepseek3", "deepseek4")):

0 commit comments

Comments
 (0)