Skip to content

Commit 7a4172f

Browse files
committed
[DeepSeek-V4] Implement model integration, decoders, and configuration stack
1 parent 85690da commit 7a4172f

13 files changed

Lines changed: 587 additions & 75 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: 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 as `first_num_hash_layers=3`.
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_ratio=0` uses sliding window attention. In this case, layer (0, 1).
43+
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]
44+
45+
# --- MoE configuration ---
46+
mlp_activations: ["silu", "linear"]
47+
num_experts: 256
48+
num_experts_per_tok: 6
49+
mlp_activations_limit: 10
50+
shared_experts: 1
51+
routed_score_func: "sqrtsoftplus"
52+
53+
# --- Attention configuration ---
54+
attention_type: 'compressed'
55+
q_lora_rank: 1024
56+
o_groups: 8
57+
o_lora_rank: 1024
58+
sliding_window_size: 128
59+
60+
# --- RoPE ---
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(
@@ -2936,6 +2936,8 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
29362936
raise ValueError("`local_checkpoint_period` must be > 0 for emergency checkpointing.")
29372937
if self.moba and self.attention not in ("dot_product"):
29382938
raise ValueError("MoBA is only supported with dot_product attention.")
2939+
if self.decoder_block == DecoderBlockType.DEEPSEEK4 and self.attention != "dot_product":
2940+
raise ValueError("DeepSeek4 decoder block currently only supports dot_product attention.")
29392941
if self.use_indexer:
29402942
if self.q_lora_rank == 0:
29412943
raise NotImplementedError("Sparse indexer has not implemented for q_lora_rank = 0.")

src/maxtext/layers/attention_compressed.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -680,17 +680,20 @@ 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

@@ -809,20 +812,22 @@ def _init_projections(self, inputs_q_shape: Tuple, inputs_kv_shape: Tuple) -> No
809812
rngs=self.rngs,
810813
)
811814

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(
815+
# Override the base rotary embedding with the correct theta for this layer.
816+
# CSA / HCA layers use compressed_rope_max_timescale (160000).
817+
# Sliding window prefix layers use rope_max_timescale (10000).
818+
rope_theta = self.config.compressed_rope_max_timescale if self.compress_ratio > 0 else self.config.rope_max_timescale
819+
self.rotary_embedding = DeepSeekV4RotaryEmbedding(
815820
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,
821+
partial_rotary_factor=self.config.qk_rope_head_dim / self.config.head_dim,
822+
rope_theta=rope_theta,
823+
fprop_dtype=self.dtype,
819824
)
820825

821826
if self.compress_ratio > 4:
822827
self.hca_compressor = DeepseekV4HCACompressor(
823828
config=self.config,
824829
compress_ratio=self.compress_ratio,
825-
rotary_embedding=self.compress_rotary_embedding,
830+
rotary_embedding=self.rotary_embedding,
826831
kernel_init=self.kernel_init,
827832
quant=self.quant,
828833
model_mode=self.model_mode,
@@ -832,7 +837,7 @@ def _init_projections(self, inputs_q_shape: Tuple, inputs_kv_shape: Tuple) -> No
832837
self.csa_compressor = DeepseekV4CSACompressor(
833838
config=self.config,
834839
compress_ratio=self.compress_ratio,
835-
rotary_embedding=self.compress_rotary_embedding,
840+
rotary_embedding=self.rotary_embedding,
836841
kernel_init=self.kernel_init,
837842
quant=self.quant,
838843
model_mode=self.model_mode,
@@ -1047,7 +1052,7 @@ def __call__(
10471052
# -> [batch, q_length, emb_dim]
10481053
final_out = self.o_b_proj(grouped_flat)
10491054

1050-
return final_out
1055+
return final_out, None
10511056

10521057

10531058
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: 116 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,8 +1212,10 @@ 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}
1217+
if cfg.decoder_block == DecoderBlockType.DEEPSEEK4:
1218+
layer_call_kwargs["decoder_input_tokens"] = decoder_input_tokens
12001219
kv_cache = None
12011220
if kv_caches is not None:
12021221
# For all decoder blocks (including QWEN3_NEXT/QWEN3_5 with vLLM flat-list
@@ -1423,6 +1442,102 @@ def _apply_gemma4_scanned_blocks(
14231442

14241443
return y
14251444

1445+
def _apply_deepseek4_scanned_blocks(
1446+
self,
1447+
y,
1448+
decoder_segment_ids,
1449+
decoder_positions,
1450+
deterministic,
1451+
model_mode,
1452+
previous_chunk,
1453+
slot,
1454+
decoder_input_tokens,
1455+
):
1456+
"""Applies DeepSeek V4 scanned decoder blocks.
1457+
1458+
DeepSeek V4 has some number of prefix layers (defined by `first_num_hash_layers`)
1459+
that use static Hash Routing. The remaining layers alternate `compress_ratio=128` (HCA)
1460+
and `compress_ratio=4` (CSA) and are evaluated in a single `nn.scan` block.
1461+
1462+
For DeepSeek4-Flash (43 hidden layers total):
1463+
- 3 Prefix layers (Indices 0, 1, 2)
1464+
- 40 Scanned layers: 20 perfectly repeating chunks of [128, 4]
1465+
"""
1466+
1467+
cfg = self.config
1468+
mesh = self.mesh
1469+
1470+
broadcast_args = (
1471+
decoder_segment_ids,
1472+
decoder_positions,
1473+
deterministic,
1474+
model_mode,
1475+
slot,
1476+
previous_chunk,
1477+
)
1478+
1479+
layer_call_kwargs = {
1480+
"previous_chunk": previous_chunk,
1481+
"slot": slot,
1482+
"decoder_input_tokens": decoder_input_tokens,
1483+
}
1484+
1485+
# 1. Prefix Unrolling
1486+
# Prefix layers are unrolled (unscanned) for two architectural reasons:
1487+
# 1. Heterogeneous Attention: JAX nn.scan requires identical computation graphs, but the first few layers
1488+
# use different attention configurations (e.g., DeepSeek-V4 uses compress_ratios [0, 0, 4] for layers 0, 1, 2).
1489+
# 2. Static Hash Routing: The first `first_num_hash_layers` (which is 3 for DeepSeek-V4) use deterministic
1490+
# token-to-expert Hash Routing instead of learned top-k routing.
1491+
# Therefore, these prefix layers are instantiated individually before we scan the remaining uniform blocks.
1492+
num_hash_layers = cfg.first_num_hash_layers
1493+
for layer_idx in range(num_hash_layers):
1494+
prefix_layer = deepseek4.DeepSeek4LayerToLinen(
1495+
config=cfg,
1496+
mesh=mesh,
1497+
name=f"layers_{layer_idx}",
1498+
quant=self.quant,
1499+
model_mode=self.model_mode,
1500+
layer_idx=layer_idx,
1501+
)
1502+
y, _ = prefix_layer(
1503+
y,
1504+
decoder_segment_ids,
1505+
decoder_positions,
1506+
deterministic,
1507+
model_mode,
1508+
**layer_call_kwargs,
1509+
)
1510+
1511+
# 2. Chunked Scanning
1512+
# The remaining layers perfectly alternate HCA (128) and CSA (4).
1513+
num_remaining_layers = cfg.num_decoder_layers - num_hash_layers
1514+
num_full_blocks = num_remaining_layers // 2
1515+
1516+
if num_full_blocks > 0:
1517+
ScannableBlockToLinen = deepseek4.DeepSeek4ScannableBlockToLinen
1518+
policy = self.get_remat_policy()
1519+
RemattedDeepSeek4Block = self.set_remat_policy([ScannableBlockToLinen], policy)[0]
1520+
1521+
y, _ = nn.scan(
1522+
RemattedDeepSeek4Block,
1523+
variable_axes={
1524+
"params": cfg.param_scan_axis,
1525+
"cache": 0,
1526+
"intermediates": 0,
1527+
"aqt": 0,
1528+
"_overwrite_with_gradient": 0,
1529+
},
1530+
split_rngs={"params": True, "dropout": cfg.enable_dropout},
1531+
in_axes=(nn.broadcast,) * len(broadcast_args),
1532+
length=num_full_blocks,
1533+
metadata_params={
1534+
nn.PARTITION_NAME: "layers",
1535+
"abstract_init": False,
1536+
},
1537+
)(config=cfg, mesh=mesh, quant=self.quant, model_mode=model_mode, name="scanned_blocks",)(y, *broadcast_args)
1538+
1539+
return y
1540+
14261541
def _apply_gemma4_small_layers(
14271542
self,
14281543
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)