Skip to content

Commit feb7323

Browse files
committed
Support dynamic Tokamax Flash Attention for DeepSeek-V4 HCA and CSA layers
1 parent 3ff6eb6 commit feb7323

6 files changed

Lines changed: 153 additions & 30 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright 2026 Google LLC
2+
# Small model config for DeepSeek-V4 testing and compilation profiling
3+
4+
base_emb_dim: 1024
5+
base_num_query_heads: 16
6+
base_num_kv_heads: 1
7+
base_num_decoder_layers: 4
8+
base_mlp_dim: 1024
9+
base_moe_mlp_dim: 1024
10+
vocab_size: 32000
11+
head_dim: 128
12+
13+
# --- Standard Defaults ---
14+
enable_dropout: false
15+
logits_via_embedding: false
16+
normalization_layer_epsilon: 1.0e-6
17+
18+
# --- V4 Specific Architectural Keys ---
19+
decoder_block: "deepseek4"
20+
mhc_expansion_rate: 4
21+
first_num_hash_layers: 1
22+
indexer_head_dim: 64
23+
indexer_n_heads: 16
24+
indexer_topk: 64
25+
26+
compress_ratios: [0, 4, 8, 4]
27+
28+
# --- MoE configuration ---
29+
mlp_activations: ["silu", "linear"]
30+
num_experts: 16
31+
num_experts_per_tok: 2
32+
mlp_activations_limit: 10
33+
shared_experts: 1
34+
routed_score_func: "sqrtsoftplus"
35+
36+
# --- Attention configuration ---
37+
attention_type: 'compressed'
38+
q_lora_rank: 256
39+
o_groups: 2
40+
o_lora_rank: 256
41+
sliding_window_size: 128
42+
43+
# --- RoPE ---
44+
rope_type: "default"
45+
rope_max_timescale: 10000 # Main RoPE theta
46+
compressed_rope_max_timescale: 160000 # Compressed RoPE theta
47+
max_position_embeddings: 65536

src/maxtext/configs/types.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ class ProfilerType(str, Enum):
229229
"deepseek3-tiny",
230230
"deepseek3.2-671b",
231231
"deepseek4-284b",
232+
"deepseek4-small",
232233
"deepseek-custom",
233234
"kimi-k2-1t",
234235
"gemma-7b",
@@ -3189,8 +3190,9 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
31893190
raise ValueError("`local_checkpoint_period` must be > 0 for emergency checkpointing.")
31903191
if self.moba and self.attention not in ("dot_product"):
31913192
raise ValueError("MoBA is only supported with dot_product attention.")
3192-
if self.decoder_block == DecoderBlockType.DEEPSEEK4 and self.attention != "dot_product":
3193-
raise ValueError("DeepSeek4 decoder block currently only supports dot_product attention.")
3193+
if self.decoder_block == DecoderBlockType.DEEPSEEK4 and self.attention not in ("dot_product", "flash"):
3194+
raise ValueError("DeepSeek4 decoder block currently supports dot_product and flash attention.")
3195+
31943196
if self.use_indexer:
31953197
if self.q_lora_rank == 0:
31963198
raise NotImplementedError("Sparse indexer has not implemented for q_lora_rank = 0.")

src/maxtext/layers/attention_compressed.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,23 @@ def __call__(
10581058

10591059
kv = checkpoint_name(kv, "kv_proj")
10601060

1061+
# Pad total KV length to tile size multiple for Tokamax block alignment
1062+
if self.attention_kernel == "flash":
1063+
block_size = self.config.sa_block_kv
1064+
pad_kv_total = (block_size - (kv.shape[1] % block_size)) % block_size
1065+
1066+
if pad_kv_total > 0:
1067+
c_len = compressed_kv.shape[1] if compressed_kv is not None else 0
1068+
if c_len > 0:
1069+
# Prepend padding to the compressed blocks so they remain at the end of the sequence
1070+
local_kv = kv[:, :-c_len]
1071+
comp_kv = kv[:, -c_len:]
1072+
comp_kv_padded = jnp.pad(comp_kv, ((0, 0), (pad_kv_total, 0), (0, 0), (0, 0)))
1073+
kv = jnp.concatenate([local_kv, comp_kv_padded], axis=1)
1074+
else:
1075+
# Fallback: Pad at the end if no compressed blocks exist
1076+
kv = jnp.pad(kv, ((0, 0), (0, pad_kv_total), (0, 0), (0, 0)))
1077+
10611078
# Prepare the mask shape for the underlying AttentionOp
10621079
if compressed_mask is not None:
10631080
compressed_mask = jnp.expand_dims(compressed_mask, axis=2)
@@ -1066,6 +1083,17 @@ def __call__(
10661083
if self.query_pre_attn_scalar and self.query_pre_attn_scalar != 1.0:
10671084
q = q * self.query_pre_attn_scalar
10681085

1086+
# Build indexer mask explicitly for tokamax splash kernel
1087+
indexer_mask = None
1088+
if self.attention_kernel == "flash" and compressed_mask is not None:
1089+
indexer_mask = self.attention_op.generate_attention_mask(
1090+
q, kv, decoder_segment_ids, model_mode, compressed_mask=compressed_mask
1091+
)
1092+
1093+
if indexer_mask is not None:
1094+
# Robustly extract the first head & first key dimension slices to match splash expectations
1095+
indexer_mask = indexer_mask[:, 0, 0, :, :]
1096+
10691097
# Compute Attention
10701098
# -> [batch, q_length, num_query_heads, head_dim]
10711099
attn_out = self.attention_op(
@@ -1077,6 +1105,7 @@ def __call__(
10771105
model_mode,
10781106
sinks=self.sinks.value,
10791107
compressed_mask=compressed_mask,
1108+
indexer_mask=indexer_mask,
10801109
)
10811110

10821111
# Reverse RoPE on Values

src/maxtext/layers/attention_op.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,7 @@ def apply_attention(
10331033
decoder_segment_ids,
10341034
self.attn_logits_soft_cap,
10351035
sinks,
1036+
indexer_mask,
10361037
record_max_logits=record_max_logits,
10371038
)
10381039
if max_logits is not None:
@@ -1297,8 +1298,18 @@ def create_sa_config(config, query, key, attn_logits_soft_cap):
12971298
return sa_config
12981299

12991300
sa_config = create_sa_config(self.config, query, key, attn_logits_soft_cap)
1300-
mask_shape = (query.shape[2], key.shape[2]) # (q_seq_len, kv_seq_len)
1301+
block_q = sa_config.block_q
1302+
block_kv = sa_config.block_kv
1303+
if self.attention_type == AttentionType.COMPRESSED and (
1304+
(query.shape[2] % block_q != 0) or (key.shape[2] % block_kv != 0)
1305+
):
1306+
padded_q_len = ((query.shape[2] + block_q - 1) // block_q) * block_q
1307+
padded_kv_len = ((key.shape[2] + block_kv - 1) // block_kv) * block_kv
1308+
mask_shape = (padded_q_len, padded_kv_len)
1309+
else:
1310+
mask_shape = (query.shape[2], key.shape[2]) # (q_seq_len, kv_seq_len)
13011311
mask_module = tokamax_splash_mask if self.config.use_tokamax_splash else splash_attention_mask
1312+
13021313
if self.attention_type == AttentionType.FULL:
13031314
mask = mask_module.FullMask(mask_shape)
13041315
else:
@@ -1315,14 +1326,14 @@ def create_sa_config(config, query, key, attn_logits_soft_cap):
13151326
local_window_size = (self.sliding_window_size - 1, self.sliding_window_size)
13161327
if use_load_balanced_cp:
13171328
mask &= LoadBalancedLocalMask(
1318-
shape=(query.shape[2], key.shape[2]),
1329+
shape=mask_shape,
13191330
window_size=local_window_size,
13201331
offset=0,
13211332
cp_size=cp_size,
13221333
)
13231334
else:
13241335
mask &= mask_module.LocalMask(
1325-
shape=(query.shape[2], key.shape[2]),
1336+
shape=mask_shape,
13261337
window_size=local_window_size,
13271338
offset=0,
13281339
)
@@ -1332,16 +1343,15 @@ def create_sa_config(config, query, key, attn_logits_soft_cap):
13321343

13331344
if use_load_balanced_cp:
13341345
mask &= LoadBalancedChunkedCausalMask(
1335-
shape=(query.shape[2], key.shape[2]),
1346+
shape=mask_shape,
13361347
chunk_size=self.chunk_attn_window_size,
13371348
cp_size=cp_size,
13381349
)
13391350
else:
13401351
mask &= ChunkedCausalMask(
1341-
shape=(query.shape[2], key.shape[2]),
1352+
shape=mask_shape,
13421353
chunk_size=self.chunk_attn_window_size,
13431354
)
1344-
13451355
max_logit_value = None
13461356
if self.config.use_tokamax_splash:
13471357
# Create mask
@@ -1364,9 +1374,12 @@ def wrap_splash_kernel(single_head_mask):
13641374
)
13651375
return splash_kernel
13661376

1367-
splash_kernel = wrap_splash_kernel(single_head_mask)
13681377
segment_axis_names_splash_kernel = self._logical_to_mesh_axes((Q_LENGTH,))
1369-
splash_kernel = self._maybe_shard_with_pspec(splash_kernel, segment_axis_names_splash_kernel)
1378+
if indexer_mask is None:
1379+
splash_kernel = wrap_splash_kernel(single_head_mask)
1380+
splash_kernel = self._maybe_shard_with_pspec(splash_kernel, segment_axis_names_splash_kernel)
1381+
else:
1382+
splash_kernel = None
13701383
elif self.config.use_jax_splash:
13711384
if self.config.use_max_logit_estimate > 0:
13721385
sa_config = dataclasses.replace(sa_config, max_logit_const=self.config.use_max_logit_estimate)
@@ -1489,7 +1502,7 @@ def wrap_flash_attention(
14891502
decoder_segment_ids_tuple = None
14901503

14911504
if self.config.use_tokamax_splash:
1492-
if self.config.use_indexer and indexer_mask is not None:
1505+
if indexer_mask is not None:
14931506
# Construct the splash kernel call with dynamic mask
14941507
def dynamic_mask_splash_kernel(q, k, v, segment, sinks, indexer_mask):
14951508
splash_kernel = tokamax_splash_kernel.make_dynamic_splash_mha(
@@ -1533,6 +1546,7 @@ def kernel_fn(q, k, v, d, s):
15331546
query, key, value, decoder_segment_ids_tuple, sinks
15341547
)
15351548
return attention_output, None
1549+
15361550
elif self.config.use_jax_splash:
15371551
materialized_mask = jnp.asarray(mask[:, :])
15381552
attention_output = jax_flash_attention.flash_attention_block_masked(

src/maxtext/layers/nnx_decoders.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,6 +1272,7 @@ def get_norm_layer(self, num_features: int, rngs: nnx.Rngs):
12721272
DecoderBlockType.MISTRAL,
12731273
DecoderBlockType.MIXTRAL,
12741274
DecoderBlockType.DEEPSEEK,
1275+
DecoderBlockType.DEEPSEEK4,
12751276
DecoderBlockType.GEMMA,
12761277
DecoderBlockType.GEMMA2,
12771278
DecoderBlockType.GEMMA3,

0 commit comments

Comments
 (0)