Skip to content

Commit 2d1c867

Browse files
committed
add DeepSeek4 HyperHead, check for DEEPSEEK4 decoder block type in moe.py, and match maxtext sinkhorn implementation to HF reference.
1 parent 409f43b commit 2d1c867

9 files changed

Lines changed: 1032 additions & 30 deletions

File tree

src/maxtext/checkpoint_conversion/utils/param_mapping.py

Lines changed: 293 additions & 6 deletions
Large diffs are not rendered by default.

src/maxtext/configs/models/deepseek4-284b.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ num_experts_per_tok: 6
4949
mlp_activations_limit: 10
5050
shared_experts: 1
5151
routed_score_func: "sqrtsoftplus"
52+
norm_topk_prob: true
53+
routed_bias: true
54+
routed_scaling_factor: 1.5
55+
5256

5357
# --- Attention configuration ---
5458
attention_type: 'compressed'
@@ -62,3 +66,4 @@ rope_type: "default"
6266
rope_max_timescale: 10000 # Main RoPE theta
6367
compressed_rope_max_timescale: 160000 # Compressed RoPE theta
6468
max_position_embeddings: 1048576
69+
original_max_position_embeddings: 65536
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
# Test Model config for DeepSeek-V4-Flash 284B (https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash)
15+
16+
base_emb_dim: 4096
17+
base_num_query_heads: 64
18+
base_num_kv_heads: 1
19+
base_num_decoder_layers: 7
20+
base_mlp_dim: 2048
21+
base_moe_mlp_dim: 2048
22+
vocab_size: 129280
23+
head_dim: 512
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, 2) are prefix layers as `first_num_hash_layers=3`.
39+
# The 6th layer (MTP module with compress_ratio=0) has been explicitly dropped for now.
40+
# This leaves exactly 7 layers: 3 prefix [0,0,4] + 4 scanned.
41+
# `compress_ratio=0` uses sliding window attention. In this case, layer (0, 1).
42+
# This is a tiny version of deepseek4 with fewer layers and less experts for debugging.
43+
compress_ratios: [0, 0, 4, 128, 4, 128, 4]
44+
45+
# --- MoE configuration ---
46+
mlp_activations: ["silu", "linear"]
47+
num_experts: 8
48+
num_experts_per_tok: 3
49+
mlp_activations_limit: 10
50+
shared_experts: 1
51+
routed_score_func: "sqrtsoftplus"
52+
routed_bias: true
53+
routed_scaling_factor: 1.5
54+
55+
56+
# --- Attention configuration ---
57+
attention_type: 'compressed'
58+
attention: 'dot_product'
59+
q_lora_rank: 1024
60+
o_groups: 8
61+
o_lora_rank: 1024
62+
sliding_window_size: 128
63+
64+
# --- RoPE ---
65+
rope_type: "default"
66+
rope_max_timescale: 10000 # Main RoPE theta
67+
compressed_rope_max_timescale: 160000 # Compressed RoPE theta
68+
max_position_embeddings: 1048576
69+
original_max_position_embeddings: 65536
70+

src/maxtext/configs/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ class ProfilerType(str, Enum):
228228
"deepseek3-test",
229229
"deepseek3-tiny",
230230
"deepseek3.2-671b",
231+
"deepseek4-tiny",
231232
"deepseek4-284b",
232233
"deepseek-custom",
233234
"kimi-k2-1t",

src/maxtext/layers/attention_compressed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def __call__(
304304

305305
# Skip causal mask generation during decoding (seq_len == 1) or if no blocks were pooled
306306
if seq_len == 1 or compressed_len == 0:
307-
return compressed_kv, None
307+
return compressed_kv, jnp.zeros((batch_size, 1, seq_len, compressed_len), dtype=self.dtype)
308308

309309
# Construct a causal mask preventing early queries from attending to future compressed blocks
310310
entry_indices = jnp.arange(compressed_len)

src/maxtext/layers/decoders.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,8 +1263,15 @@ def __call__(
12631263

12641264
# After the final transformer layer, `y` holds the raw, un-normalized hidden state.
12651265
if cfg.mhc_expansion_rate > 1:
1266-
# (batch, length, mhc_expansion_rate, emb_dim) --> (batch, length, emb_dim)
1267-
hidden_state = mhc_reduce(y)
1266+
if cfg.decoder_block == DecoderBlockType.DEEPSEEK4:
1267+
hidden_state = mhc.DeepSeek4HyperHeadToLinen(
1268+
config=cfg,
1269+
mesh=mesh,
1270+
name="hc_head",
1271+
)(y)
1272+
else:
1273+
# (batch, length, mhc_expansion_rate, emb_dim) --> (batch, length, emb_dim)
1274+
hidden_state = mhc_reduce(y)
12681275
else:
12691276
hidden_state = y
12701277

src/maxtext/layers/mhc.py

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from jax.sharding import Mesh
2525
from maxtext.common.common_types import Array, Config
2626
from maxtext.common.common_types import HyperConnectionType
27-
from maxtext.layers.initializers import default_bias_init, default_scalar_init, nd_dense_init
27+
from maxtext.layers.initializers import default_bias_init, default_scalar_init, nd_dense_init, variable_to_logically_partitioned
28+
from maxtext.layers import nnx_wrappers
2829
from maxtext.layers.normalizations import RMSNorm
2930

3031

@@ -61,21 +62,18 @@ def sinkhorn(t, iters=20):
6162
# Use float32 precision for numerical stability during normalization
6263
initial_dtype = t.dtype
6364
t = t.astype(jnp.float32)
65+
eps = 1e-6
6466

65-
# Column-wise normalization (axis=-2) - positive and sum up to 1 across columns
66-
# Equivalent to t = exp(t) / jnp.sum(jnp.exp(t), axis=-2)
67-
t = jax.nn.softmax(t, axis=-2)
67+
t = jax.nn.softmax(t, axis=-1) + eps
68+
t = t / (jnp.sum(t, axis=-2, keepdims=True) + eps)
6869

6970
def body_fun(i, val):
70-
# L1 Normalization: val / sum(val) with clipping of denominator
71-
# Normalize rows (axis -1)
72-
val = val / jnp.clip(jnp.sum(val, axis=-1, keepdims=True), min=1e-12)
73-
# Normalize columns (axis -2)
74-
val = val / jnp.clip(jnp.sum(val, axis=-2, keepdims=True), min=1e-12)
71+
val = val / (jnp.sum(val, axis=-1, keepdims=True) + eps)
72+
val = val / (jnp.sum(val, axis=-2, keepdims=True) + eps)
7573
return val
7674

7775
# Use lax.fori_loop for an efficient, JIT-friendly loop
78-
t = jax.lax.fori_loop(0, iters, body_fun, t)
76+
t = jax.lax.fori_loop(0, iters - 1, body_fun, t)
7977
return t.astype(initial_dtype)
8078

8179

@@ -224,7 +222,7 @@ def res_mapping(self, x: Array):
224222
output = sinkhorn(intermediate, self.sinkhorn_iterations)
225223
return output
226224

227-
def mapping(self, x: Array, alpha_scale: Array, alpha: Array, beta: Array, scale: int):
225+
def mapping(self, x: Array, alpha_scale: Array, alpha: Array, beta: Array, scale: float, eps: float = 0.0):
228226
"""Helper function for both pre and post mappings."""
229227
# In MaxText, we match weight precision to activations before Matmul
230228
alpha = jnp.asarray(alpha, self.dtype)
@@ -233,7 +231,7 @@ def mapping(self, x: Array, alpha_scale: Array, alpha: Array, beta: Array, scale
233231
# Apply projection: (b, s, k*d) @ (k*d, k) -> (b, s, k)
234232
h = jnp.einsum("bsm,mk -> bsk", x, alpha, precision=self.matmul_precision)
235233
intermediate = alpha_scale * h + beta[None, None, :]
236-
output = scale * jax.nn.sigmoid(intermediate)
234+
output = scale * jax.nn.sigmoid(intermediate) + eps
237235
return output
238236

239237
def __call__(
@@ -269,6 +267,7 @@ def __call__(
269267
self.pre_alpha[...],
270268
self.pre_beta[...],
271269
1.0,
270+
eps=1e-6,
272271
)
273272
layer_input = jnp.einsum("bskd,bsk -> bsd", x, pre_mapping, precision=self.matmul_precision)
274273

@@ -307,3 +306,71 @@ def __call__(
307306
res_mapping = self.res_mapping(norm_x)
308307
res_out = jnp.einsum("bskd,bskm -> bsmd", x, res_mapping, precision=self.matmul_precision)
309308
return res_out + post_out, metadata
309+
310+
311+
class DeepSeek4HyperHead(nnx.Module):
312+
"""Final HC-stream collapse; used by DeepSeek V4 before the shared RMSNorm."""
313+
314+
def __init__(
315+
self,
316+
config: Config,
317+
mesh: Mesh,
318+
rngs: nnx.Rngs,
319+
):
320+
self.config = config
321+
self.mesh = mesh
322+
self.rngs = rngs
323+
self.dtype = config.dtype
324+
self.weight_dtype = config.weight_dtype
325+
self.mhc_expansion_rate = config.mhc_expansion_rate
326+
self.emb_dim = config.emb_dim
327+
self.eps = 1e-6
328+
329+
# Weight matrices
330+
weight_init = nd_dense_init(1.0, "fan_in", "normal")
331+
self.hc_fn = nnx.Param(
332+
weight_init(
333+
rngs.params(),
334+
(self.mhc_expansion_rate * self.emb_dim, self.mhc_expansion_rate),
335+
self.weight_dtype,
336+
in_axis=0,
337+
out_axis=1,
338+
),
339+
out_sharding=("activation_embed", None),
340+
)
341+
self.hc_base = nnx.Param(
342+
default_bias_init(rngs.params(), (self.mhc_expansion_rate,), self.weight_dtype),
343+
out_sharding=(None,),
344+
)
345+
self.hc_scale = nnx.Param(
346+
default_scalar_init(rngs.params(), (1,), self.weight_dtype),
347+
out_sharding=(None,),
348+
)
349+
350+
def __call__(self, x: Array) -> Array:
351+
# x shape: [batch, length, k, d]
352+
b, s, k, d = x.shape
353+
assert k == self.mhc_expansion_rate
354+
assert d == self.emb_dim
355+
356+
flat = jnp.reshape(x, (b, s, k * d))
357+
flat_f32 = flat.astype(jnp.float32)
358+
variance = jnp.mean(jnp.square(flat_f32), axis=-1, keepdims=True)
359+
flat_norm = flat_f32 * jax.lax.rsqrt(variance + self.eps)
360+
361+
hc_fn = jnp.asarray(self.hc_fn[...], jnp.float32)
362+
hc_base = jnp.asarray(self.hc_base[...], jnp.float32)
363+
hc_scale = jnp.asarray(self.hc_scale[...], jnp.float32)
364+
365+
mixes = jnp.einsum("bsm,mk->bsk", flat_norm, hc_fn, precision=jax.lax.Precision(self.config.matmul_precision))
366+
pre = jax.nn.sigmoid(mixes * hc_scale[None, None, :] + hc_base[None, None, :]) + self.eps
367+
368+
x_f32 = x.astype(jnp.float32)
369+
out = jnp.sum(pre[:, :, :, None] * x_f32, axis=2)
370+
return out.astype(self.dtype)
371+
372+
373+
DeepSeek4HyperHeadToLinen = nnx_wrappers.to_linen_class(
374+
DeepSeek4HyperHead,
375+
base_metadata_fn=variable_to_logically_partitioned,
376+
)

src/maxtext/layers/moe.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -694,14 +694,15 @@ def get_topk(self, gate_logits, pre_bias_logits, rngs=None, input_ids=None):
694694
else:
695695
top_k_weights, top_k_indices = jax.lax.top_k(gate_logits, self.num_experts_per_tok)
696696

697-
if self.config.decoder_block == ctypes.DecoderBlockType.DEEPSEEK:
697+
if self.config.decoder_block in (ctypes.DecoderBlockType.DEEPSEEK, ctypes.DecoderBlockType.DEEPSEEK4):
698698
top_k_weights = self.deepseek_scale_weights(top_k_weights)
699-
elif self.config.decoder_block not in (ctypes.DecoderBlockType.LLAMA4, ctypes.DecoderBlockType.GEMMA4):
700-
top_k_weights = jax.nn.softmax(top_k_weights.astype(jnp.float32), axis=-1).astype(self.dtype)
699+
else:
700+
if self.config.decoder_block not in (ctypes.DecoderBlockType.LLAMA4, ctypes.DecoderBlockType.GEMMA4):
701+
top_k_weights = jax.nn.softmax(top_k_weights.astype(jnp.float32), axis=-1).astype(self.dtype)
701702

702-
# Normalization of router weights (e.g. used by Qwen3, Gemma4).
703-
if self.config.norm_topk_prob:
704-
top_k_weights /= top_k_weights.sum(axis=-1, keepdims=True)
703+
# Normalization of router weights (e.g. used by Qwen3, Gemma4).
704+
if self.config.norm_topk_prob:
705+
top_k_weights /= top_k_weights.sum(axis=-1, keepdims=True)
705706

706707
return top_k_weights, top_k_indices
707708

@@ -788,7 +789,10 @@ def apply_ffn_activation(self, layer_w0, layer_w1):
788789
layer_act = self.activation_fn(layer_w0 * 1.702)
789790
glu = jnp.multiply(layer_w0, layer_act)
790791
intermediate_layer = jnp.multiply(glu, (layer_w1 + 1))
791-
elif self.config.decoder_block == ctypes.DecoderBlockType.DEEPSEEK and self.config.mlp_activations_limit > 0.0:
792+
elif (
793+
self.config.decoder_block in (ctypes.DecoderBlockType.DEEPSEEK, ctypes.DecoderBlockType.DEEPSEEK4)
794+
and self.config.mlp_activations_limit > 0.0
795+
):
792796
# DeepSeek V4 uses bounds to clip the SwiGLU activations
793797
layer_w0 = jnp.clip(layer_w0, min=None, max=self.config.mlp_activations_limit)
794798
layer_w1 = jnp.clip(layer_w1, min=-self.config.mlp_activations_limit, max=self.config.mlp_activations_limit)

0 commit comments

Comments
 (0)