Skip to content

Commit 2a19018

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 194fb8a commit 2a19018

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:
@@ -1351,6 +1368,111 @@ def _apply_gemma4_scanned_blocks(
13511368

13521369
return y
13531370

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

src/maxtext/layers/moe.py

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

208208

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

@@ -397,8 +401,11 @@ def __init__(
397401
# DeepSeek V4 Hash Routing
398402
if self.is_hash_routing:
399403
# Token-ID to Expert-ID lookup table for static routing
400-
self.tid2eid = nnx.Variable(
401-
jnp.zeros((self.config.vocab_size, self.num_experts_per_tok), dtype=jnp.int32),
404+
# Must be stored as float32 because MaxText passes the entire variable tree
405+
# through jax.value_and_grad, which strictly requires all leaves to be inexact types
406+
# (even if they receive no gradients). We cast to int32 dynamically during routing.
407+
self.tid2eid = Tid2EidVar(
408+
jnp.zeros((self.config.vocab_size, self.num_experts_per_tok), dtype=jnp.float32),
402409
out_sharding=None, # Replicated across shards for local lookup
403410
)
404411
else:
@@ -663,7 +670,13 @@ def get_topk(self, gate_logits, pre_bias_logits, rngs=None, input_ids=None):
663670
return top_k_weights, top_k_indices
664671

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

0 commit comments

Comments
 (0)