Skip to content

Commit bb75291

Browse files
committed
feat(models): Integrate DeepSeek V4 architecture and routing
1 parent 45b0f2b commit bb75291

14 files changed

Lines changed: 579 additions & 79 deletions

File tree

src/maxtext/checkpoint_conversion/utils/hf_model_configs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,7 @@ class DeepseekV32Config(PTConfig):
10161016

10171017
def __init__(self, **kwargs):
10181018
self.max_position_embeddings = kwargs.get("max_position_embeddings", 163840)
1019+
self.rope_scaling = kwargs.pop("rope_scaling", None)
10191020
super().__init__(**kwargs)
10201021

10211022

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: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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-Flash 284B (https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash)
16+
17+
base_emb_dim: 4096
18+
base_num_query_heads: 64
19+
base_num_kv_heads: 1
20+
base_num_decoder_layers: 43
21+
base_mlp_dim: 2048
22+
base_moe_mlp_dim: 2048
23+
vocab_size: 129280
24+
head_dim: 512
25+
26+
# --- Standard Defaults ---
27+
enable_dropout: false
28+
logits_via_embedding: false
29+
normalization_layer_epsilon: 1.0e-6
30+
31+
# --- V4 Specific Architectural Keys ---
32+
decoder_block: "deepseek4"
33+
mhc_expansion_rate: 4
34+
first_num_hash_layers: 3
35+
indexer_head_dim: 128
36+
indexer_n_heads: 64
37+
indexer_topk: 512
38+
39+
# Note: Layers (0, 1, 2) are prefix layers.
40+
# The 44th layer (MTP module with compress_ratio=0) has been explicitly dropped for now.
41+
# This leaves exactly 43 layers: 3 prefix [0,0,4] + 40 scanned.
42+
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]
43+
44+
# --- MoE configuration ---
45+
mlp_activations: ["silu", "linear"]
46+
num_experts: 256
47+
num_experts_per_tok: 6
48+
mlp_activations_limit: 10
49+
shared_experts: 1
50+
routed_score_func: "sqrtsoftplus"
51+
52+
# --- Attention configuration ---
53+
attention_type: 'compressed'
54+
q_lora_rank: 1024
55+
o_groups: 8
56+
o_lora_rank: 1024
57+
sliding_window_size: 128
58+
59+
# --- RoPE ---
60+
61+
rope_type: "default"
62+
rope_max_timescale: 10000 # Main RoPE theta
63+
compressed_rope_max_timescale: 160000 # Compressed RoPE theta
64+
max_position_embeddings: 1048576

src/maxtext/configs/types.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ class ProfilerType(str, Enum):
227227
"deepseek3-test",
228228
"deepseek3-tiny",
229229
"deepseek3.2-671b",
230-
"deepseek4",
230+
"deepseek4-284b",
231231
"deepseek-custom",
232232
"kimi-k2-1t",
233233
"gemma-7b",
@@ -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(
@@ -2925,6 +2925,8 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
29252925
raise ValueError("`local_checkpoint_period` must be > 0 for emergency checkpointing.")
29262926
if self.moba and self.attention not in ("dot_product"):
29272927
raise ValueError("MoBA is only supported with dot_product attention.")
2928+
if self.decoder_block == DecoderBlockType.DEEPSEEK4 and self.attention != "dot_product":
2929+
raise ValueError("DeepSeek4 decoder block currently only supports dot_product attention.")
29282930
if self.use_indexer:
29292931
if self.q_lora_rank == 0:
29302932
raise NotImplementedError("Sparse indexer has not implemented for q_lora_rank = 0.")

src/maxtext/layers/attention_compressed.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -680,24 +680,23 @@ def __init__(
680680
rngs: Optional[nnx.Rngs] = None,
681681
**kwargs,
682682
):
683-
"""Initializes the CompressedAttention layer.
683+
"""Inherits all standard Attention hyperparameters and selectively instantiates
684+
an underlying HCA or CSA compressor based on the provided `compress_ratio`.
684685
685-
Inherits all standard Attention hyperparameters and selectively instantiates
686-
an underlying HCA or CSA compressor based on the provided `layer_type`.
686+
Highlights of DeepSeek-V4 attention integration:
687+
- Shared-KV: The layer supports decoupling Q and KV heads for heavy compression.
688+
- MQA: Multi-Query Attention used alongside heavy KV compression.
689+
- 3 Different Attention Modes: Sliding Window (prefix), HCA (128x), and CSA (4x).
690+
- Dual RoPE Theta: Uses 10000 for standard uncompressed tokens and 160000 for compressed.
687691
688692
Args:
689693
(See maxtext.layers.attentions.Attention for standard attention arguments)
690694
q_lora_rank: The rank for the LoRA projection in the compressed query.
691-
compress_ratio: The compression ratio for the compressor.
695+
compress_ratio: The compression ratio (0, 4, or 128) for the compressor.
692696
"""
693-
"""Initializes the Compressed Attention module."""
694697
self.q_lora_rank = q_lora_rank
695698
self.compress_ratio = compress_ratio
696699

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-
701700
super().__init__(
702701
config=config,
703702
num_query_heads=num_query_heads,
@@ -809,20 +808,22 @@ def _init_projections(self, inputs_q_shape: Tuple, inputs_kv_shape: Tuple) -> No
809808
rngs=self.rngs,
810809
)
811810

812-
# DeepSeek-V4 uses a separate RoPE theta (160000) for compressed tokens.
813-
# We must instantiate a dedicated rotary embedding for the compressors
814-
self.compress_rotary_embedding = DeepSeekV4RotaryEmbedding(
811+
# Override the base rotary embedding with the correct theta for this layer.
812+
# CSA / HCA layers use compressed_rope_max_timescale (160000).
813+
# Sliding window prefix layers use rope_max_timescale (10000).
814+
rope_theta = self.config.compressed_rope_max_timescale if self.compress_ratio > 0 else self.config.rope_max_timescale
815+
self.rotary_embedding = DeepSeekV4RotaryEmbedding(
815816
head_dim=self.config.head_dim,
816-
partial_rotary_factor=1.0,
817-
rope_theta=self.config.compressed_rope_max_timescale,
818-
dtype=self.dtype,
817+
partial_rotary_factor=self.config.qk_rope_head_dim / self.config.head_dim,
818+
rope_theta=rope_theta,
819+
fprop_dtype=self.dtype,
819820
)
820821

821822
if self.compress_ratio > 4:
822823
self.hca_compressor = DeepseekV4HCACompressor(
823824
config=self.config,
824825
compress_ratio=self.compress_ratio,
825-
rotary_embedding=self.compress_rotary_embedding,
826+
rotary_embedding=self.rotary_embedding,
826827
kernel_init=self.kernel_init,
827828
quant=self.quant,
828829
model_mode=self.model_mode,
@@ -832,7 +833,7 @@ def _init_projections(self, inputs_q_shape: Tuple, inputs_kv_shape: Tuple) -> No
832833
self.csa_compressor = DeepseekV4CSACompressor(
833834
config=self.config,
834835
compress_ratio=self.compress_ratio,
835-
rotary_embedding=self.compress_rotary_embedding,
836+
rotary_embedding=self.rotary_embedding,
836837
kernel_init=self.kernel_init,
837838
quant=self.quant,
838839
model_mode=self.model_mode,
@@ -1047,7 +1048,7 @@ def __call__(
10471048
# -> [batch, q_length, emb_dim]
10481049
final_out = self.o_b_proj(grouped_flat)
10491050

1050-
return final_out
1051+
return final_out, None
10511052

10521053

10531054
def compressed_attention(

src/maxtext/layers/attentions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,7 @@ def init_rotary_embedding(self):
850850
shard_mode=self.config.shard_mode,
851851
rngs=self.rngs,
852852
)
853+
853854
elif self.is_qwen3_hybrid:
854855
rotary_embedding = PartialRotaryEmbedding(
855856
min_timescale=self.config.rope_min_timescale,

src/maxtext/layers/decoders.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from maxtext.layers.quantizations import AqtQuantization as Quant
4242
from maxtext.models import (
4343
deepseek,
44+
deepseek4,
4445
deepseek_batchsplit,
4546
deepseek_batchsplit_fp8,
4647
gemma,
@@ -467,6 +468,10 @@ def get_decoder_layers(self):
467468
deepseek.DeepSeekDenseLayerToLinen,
468469
deepseek.DeepSeekMoELayerToLinen,
469470
]
471+
case DecoderBlockType.DEEPSEEK4:
472+
return (
473+
[deepseek4.DeepSeek4ScannableBlockToLinen] if self.config.scan_layers else [deepseek4.DeepSeek4LayerToLinen]
474+
)
470475
case DecoderBlockType.GEMMA:
471476
return [gemma.GemmaDecoderLayerToLinen]
472477
case DecoderBlockType.GEMMA2:
@@ -632,6 +637,7 @@ def get_norm_layer(self, num_features: int):
632637
DecoderBlockType.MISTRAL,
633638
DecoderBlockType.MIXTRAL,
634639
DecoderBlockType.DEEPSEEK,
640+
DecoderBlockType.DEEPSEEK4,
635641
DecoderBlockType.GEMMA,
636642
DecoderBlockType.GEMMA2,
637643
DecoderBlockType.GEMMA3,
@@ -1061,6 +1067,17 @@ def __call__(
10611067
previous_chunk,
10621068
slot,
10631069
)
1070+
elif cfg.decoder_block == DecoderBlockType.DEEPSEEK4:
1071+
y = self._apply_deepseek4_scanned_blocks(
1072+
y,
1073+
decoder_segment_ids,
1074+
decoder_positions,
1075+
deterministic,
1076+
model_mode,
1077+
previous_chunk,
1078+
slot,
1079+
decoder_input_tokens,
1080+
)
10641081
else:
10651082
RemattedBlockLayer = RemattedBlockLayers[0]
10661083
scan_length = int(cfg.num_decoder_layers / cfg.inhomogeneous_layer_cycle_interval)
@@ -1195,7 +1212,7 @@ def __call__(
11951212
"is_nope_layer": llama4.determine_is_nope_layer(lyr, self.config.nope_layer_interval),
11961213
"is_moe_layer": llama4.determine_is_moe_layer(lyr, self.config.interleave_moe_layer_step),
11971214
}
1198-
if cfg.decoder_block in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5):
1215+
if cfg.decoder_block in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5, DecoderBlockType.DEEPSEEK4):
11991216
layer_kwargs = {"layer_idx": lyr}
12001217
kv_cache = None
12011218
if kv_caches is not None:
@@ -1423,6 +1440,97 @@ def _apply_gemma4_scanned_blocks(
14231440

14241441
return y
14251442

1443+
def _apply_deepseek4_scanned_blocks(
1444+
self,
1445+
y,
1446+
decoder_segment_ids,
1447+
decoder_positions,
1448+
deterministic,
1449+
model_mode,
1450+
previous_chunk,
1451+
slot,
1452+
decoder_input_tokens,
1453+
):
1454+
"""Applies DeepSeek V4 scanned decoder blocks.
1455+
1456+
DeepSeek V4 has some number of prefix layers (defined by `first_num_hash_layers`)
1457+
that use static Hash Routing. The remaining layers alternate `compress_ratio=128` (HCA)
1458+
and `compress_ratio=4` (CSA) and are evaluated in a single `nn.scan` block.
1459+
1460+
For DeepSeek4-Flash (43 hidden layers total):
1461+
- 3 Prefix layers (Indices 0, 1, 2)
1462+
- 40 Scanned layers: 20 perfectly repeating chunks of [128, 4]
1463+
"""
1464+
1465+
cfg = self.config
1466+
mesh = self.mesh
1467+
1468+
broadcast_args = (
1469+
decoder_segment_ids,
1470+
decoder_positions,
1471+
deterministic,
1472+
model_mode,
1473+
slot,
1474+
previous_chunk,
1475+
)
1476+
1477+
layer_call_kwargs = {
1478+
"previous_chunk": previous_chunk,
1479+
"slot": slot,
1480+
"decoder_input_tokens": decoder_input_tokens,
1481+
}
1482+
1483+
# 1. Prefix Unrolling
1484+
# These layers use Hash Routing.
1485+
num_hash_layers = cfg.first_num_hash_layers
1486+
for layer_idx in range(num_hash_layers):
1487+
prefix_layer = deepseek4.DeepSeek4LayerToLinen(
1488+
config=cfg,
1489+
mesh=mesh,
1490+
name=f"layers_{layer_idx}",
1491+
quant=self.quant,
1492+
model_mode=self.model_mode,
1493+
layer_idx=layer_idx,
1494+
)
1495+
y, _ = prefix_layer(
1496+
y,
1497+
decoder_segment_ids,
1498+
decoder_positions,
1499+
deterministic,
1500+
model_mode,
1501+
**layer_call_kwargs,
1502+
)
1503+
1504+
# 2. Chunked Scanning
1505+
# The remaining layers perfectly alternate HCA (128) and CSA (4).
1506+
num_remaining_layers = cfg.num_decoder_layers - num_hash_layers
1507+
num_full_blocks = num_remaining_layers // 2
1508+
1509+
if num_full_blocks > 0:
1510+
ScannableBlockToLinen = deepseek4.DeepSeek4ScannableBlockToLinen
1511+
policy = self.get_remat_policy()
1512+
RemattedDeepSeek4Block = self.set_remat_policy([ScannableBlockToLinen], policy)[0]
1513+
1514+
y, _ = nn.scan(
1515+
RemattedDeepSeek4Block,
1516+
variable_axes={
1517+
"params": cfg.param_scan_axis,
1518+
"cache": 0,
1519+
"intermediates": 0,
1520+
"aqt": 0,
1521+
"_overwrite_with_gradient": 0,
1522+
},
1523+
split_rngs={"params": True, "dropout": cfg.enable_dropout},
1524+
in_axes=(nn.broadcast,) * len(broadcast_args),
1525+
length=num_full_blocks,
1526+
metadata_params={
1527+
nn.PARTITION_NAME: "layers",
1528+
"abstract_init": False,
1529+
},
1530+
)(config=cfg, mesh=mesh, quant=self.quant, model_mode=model_mode, name="scanned_blocks",)(y, *broadcast_args)
1531+
1532+
return y
1533+
14261534
def _apply_gemma4_small_layers(
14271535
self,
14281536
y,

src/maxtext/layers/embeddings.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,7 +1803,7 @@ def qwen3_omni_mrope_embedding_as_linen(
18031803
)
18041804

18051805

1806-
class DeepSeekV4RotaryEmbedding(nnx.Module):
1806+
class DeepSeekV4RotaryEmbedding(RotaryEmbedding):
18071807
"""DeepSeek-V4 partial rotary embedding with interleaved frequencies.
18081808
18091809
DeepSeek-V4 uses an interleaved positional encoding where consecutive channels
@@ -1822,12 +1822,23 @@ def __init__(
18221822
head_dim: int,
18231823
partial_rotary_factor: float = 64.0 / 512.0,
18241824
rope_theta: float = 10000.0,
1825-
dtype: Any = jnp.float32,
1825+
fprop_dtype: Any = jnp.float32,
1826+
min_timescale: int = 10000,
1827+
max_timescale: int = 10000,
1828+
mesh: Any = None,
1829+
**kwargs,
18261830
):
1831+
super().__init__(
1832+
min_timescale=min_timescale,
1833+
max_timescale=max_timescale,
1834+
mesh=mesh,
1835+
fprop_dtype=fprop_dtype,
1836+
**kwargs,
1837+
)
18271838
self.head_dim = head_dim
18281839
self.partial_rotary_factor = partial_rotary_factor
18291840
self.rope_theta = rope_theta
1830-
self.dtype = dtype
1841+
self.fprop_dtype = fprop_dtype
18311842

18321843
# Compute the partial rotary dimension (rope_head_dim)
18331844
self.dim = int(head_dim * partial_rotary_factor)

0 commit comments

Comments
 (0)