Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
cf4d118
Support dynamic Tokamax Flash Attention for DeepSeek-V4 HCA and CSA l…
octatrifan Jul 16, 2026
b108e56
Remove deepseek4-small config and registration
octatrifan Jul 17, 2026
0399b7d
Add unit tests for CompressedAttention (DeepSeek-V4) compilation and …
octatrifan Jul 17, 2026
c48a8f8
Enable Flash Attention support for DeepSeek-V4 (MLA) and add HCA/CSA …
octatrifan Jul 18, 2026
d23768e
Merge origin/main and resolve conflicts
octatrifan Jul 20, 2026
aed93c8
Fix line length violations exceeding 120-char limit for PR #4488 lint…
octatrifan Jul 20, 2026
3d70121
Support non-multiple-of-128 sequence lengths in DeepSeek-V4 HCA/CSA F…
octatrifan Jul 24, 2026
a3ccb02
Revert experimental rotary_embedding.head_dim check in favor of stock…
octatrifan Jul 25, 2026
ee7dac9
Fix DeepSeek-V4 Flash Attention indexer_mask pad fill, dynamic splash…
octatrifan Jul 26, 2026
74050f4
Merge origin/main into octatrifan-dsv4-flash-attention resolving conf…
octatrifan Jul 26, 2026
eec5766
tests: calibrate and tighten DeepSeek-V4 unit test tolerances against…
octatrifan Jul 27, 2026
2aaef57
Support DeepSeek-V4 flash attention in config validation and fix loca…
octatrifan Jul 27, 2026
6f9a77c
Fix DeepSeek-V4 CompressedAttention segment ID padding guard for unal…
octatrifan Jul 28, 2026
768a84d
Fix pre-commit code quality linting (line-length) and formatting for …
octatrifan Jul 28, 2026
1821239
Fix pre-commit code formatting (pyink) and missing docstrings (pylint)
octatrifan Jul 28, 2026
74598a4
Fix DeepSeek-V4 CompressedAttention segment ID padding guard using ed…
octatrifan Jul 29, 2026
8c6dcd1
Restore ceil-padding in DeepseekV4HCACompressor to prevent sequence t…
octatrifan Jul 29, 2026
5cdbfc2
Add nn_partitioning.axis_rules context to forward_pass_logit_checker.py
octatrifan Jul 29, 2026
6ae808f
Revert "Add nn_partitioning.axis_rules context to forward_pass_logit_…
octatrifan Jul 29, 2026
6a5b05b
Implement static Splash Attention compilation path for DeepSeek-V4 HCA
octatrifan Aug 3, 2026
3883766
Fix TopKMoE router parity test tie-breaking on TPU and add TPU numeri…
octatrifan Aug 5, 2026
73a50ab
Merge origin/main into octatrifan-dsv4-flash-attention and resolve co…
octatrifan Aug 8, 2026
6063afd
Fix undefined variable usable in DeepseekV4HCACompressor
octatrifan Aug 10, 2026
a66df2b
Revert compressor input ceil-padding to truncation matching HuggingFa…
octatrifan Aug 10, 2026
79ad710
Forward full indexer_mask for compressed attention during prefill and…
octatrifan Aug 10, 2026
8b5f51b
Update src/maxtext/layers/attention_op.py
octatrifan Aug 10, 2026
e2d68b6
Manage jax_default_matmul_precision hermetically in setUpModule/tearD…
octatrifan Aug 10, 2026
0642d0a
Validate DeepSeek4 flash attention requires use_tokamax_splash set to…
octatrifan Aug 10, 2026
1b44489
Support CSA overlapping window document packing boundary invalidation…
octatrifan Aug 10, 2026
34fa9ed
Add document packing equivalence tests for CompressedAttention
octatrifan Aug 10, 2026
1c5b61b
Fix DeepseekV4Config import and pylint warning in deepseek_v4_vs_refe…
octatrifan Aug 10, 2026
4218c3c
Update test_packed_vs_unpacked_equivalence to match reviewer specific…
octatrifan Aug 10, 2026
b03f4d6
Merge origin/main into octatrifan-dsv4-flash-attention and resolve Ul…
octatrifan Aug 11, 2026
40dc518
Fix duplicate indexer mask conversion in AttentionOp and use ValueErr…
octatrifan Aug 11, 2026
e04c589
Fix packed sequence RoPE position indexing and configure 1D mesh in a…
octatrifan Aug 11, 2026
104eced
Fix pyink slice formatting in DeepseekV4HCACompressor
octatrifan Aug 12, 2026
07ac2af
Fix undefined inputs_positions, missing compressed_len, and unused fi…
octatrifan Aug 12, 2026
40f7ac5
Fix packed sequence segment masking for causal compressed blocks
octatrifan Aug 12, 2026
794b053
Fix CI test failures for DeepSeek-V4 Flash Attention
octatrifan Aug 12, 2026
313da0f
Fix document packing segment masking for compressed KV keys and avoid…
octatrifan Aug 12, 2026
87c07e4
Format slice syntax in attention_compressed.py for pyink
octatrifan Aug 12, 2026
25a4992
Fix flash masking overlap computation order for compressed constraints
octatrifan Aug 12, 2026
19747fe
Merge origin/main and apply all PR nits
octatrifan Aug 14, 2026
2086e60
Address Shuning Jin latest nits for configs, autoselected attention, …
octatrifan Aug 14, 2026
ed8118a
Merge PR 4488 and resolve conflicts for HCA static mask
octatrifan Aug 17, 2026
052ec78
Fix decoder segment IDs folding for CP tests
octatrifan Aug 17, 2026
0eaf54c
Restore standard jax.vmap for static splash attention
octatrifan Aug 17, 2026
afaeb6d
Compute GCD block sizes for static HCA splash attention
octatrifan Aug 17, 2026
13f0159
Align block_q_dkv and block_kv_dkv to computed block sizes
octatrifan Aug 17, 2026
874030b
Support static Splash Attention with query padding for DeepSeek-V4 HCA
octatrifan Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/maxtext/configs/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3644,8 +3644,17 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
raise ValueError("`local_checkpoint_period` must be > 0 for emergency checkpointing.")
if self.moba and self.attention not in ("dot_product"):
raise ValueError("MoBA is only supported with dot_product attention.")
if self.decoder_block == DecoderBlockType.DEEPSEEK4 and self.attention != "dot_product":
raise ValueError("DeepSeek4 decoder block currently only supports dot_product attention.")
if self.decoder_block == DecoderBlockType.DEEPSEEK4:
match (self.attention, self.use_tokamax_splash):
case ("dot_product", _):
pass
case ("flash", True):
pass
case _:
raise ValueError(
"DeepSeek4 is only supported with `dot_product` attention or `flash` attention "
"with `use_tokamax_splash=True`."
)
Comment on lines +3647 to +3657

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Python's structural pattern matching (match/case) here is overly verbose for a simple conditional check. Simplifying this to a standard if statement is more concise, readable, and idiomatic.

    if self.decoder_block == DecoderBlockType.DEEPSEEK4:
      if not (self.attention == "dot_product" or (self.attention == "flash" and self.use_tokamax_splash)):
        raise ValueError(
            "DeepSeek4 is only supported with dot_product attention or flash attention "
            "with use_tokamax_splash=True."
        )

if self.mla_qk_head_chunk_size > 0:
if self.mla_qk_head_chunk_size > self.num_query_heads or self.num_query_heads % self.mla_qk_head_chunk_size != 0:
raise ValueError(
Expand All @@ -3671,7 +3680,7 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
supports_dot_product = self.attention == "dot_product"
supports_flash_splash = self.attention == "flash" and self.use_tokamax_splash
if not (supports_dot_product or supports_flash_splash):
raise NotImplementedError(
raise ValueError(
"Sparse indexer is only supported with dot_product attention or flash attention with tokamax splash."
)
if self.indexer_loss_scaling_factor > 0.0 and self.indexer_topk >= self.max_target_length:
Expand Down
134 changes: 107 additions & 27 deletions src/maxtext/layers/attention_compressed.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,20 @@ def csa_overlap_pooling(
head_dim: int,
prior_kv: Optional[Array] = None,
prior_gate: Optional[Array] = None,
is_same_doc: Optional[Array] = None,
) -> Tuple[Array, Array, Array]:
"""Shared utility for Compressed Sparse Attention (CSA) overlap pooling.
"""Computes overlapping window pooling for Compressed Sparse Attention (CSA).

Implements the overlapping Ca/Cb pooling logic shared by both the CSA Compressor
and the CSA Indexer. It splits the projected states into two halves (Ca and Cb),
shifts the first half forward by one window, and concatenates them to form
overlapping windows over which softmax gating is applied.
DeepSeek-V4 CSA uses a stride-4, window-8 pooling mechanism where each output block
aggregates representations over a 2m (8-token) window formed by pairing the trailing
m (4) tokens of the previous window (Ca) with the leading m (4) tokens of the current
window (Cb).

Pipeline:
1. Split: `[batch, n_windows, compress_rate, 2 * head_dim]` -> 2x `[batch, n_windows, compress_rate, head_dim]`
2. Shift: Ca shifted forward by one window (prepending cache prior if available).
3. Concat (Ca + Cb): -> `[batch, n_windows, 2 * compress_rate, head_dim]`
4. Gating & Sum: -> `[batch, n_windows, head_dim]`

Args:
chunk_kv: Input KV projection chunks. Shape: `[batch, n_windows, compress_rate, 2 * head_dim]`.
Expand All @@ -79,6 +86,7 @@ def csa_overlap_pooling(
head_dim: Target head dimension.
prior_kv: Previous window KV prior from cache (optional).
prior_gate: Previous window gate prior from cache (optional).
is_same_doc: Boolean tensor indicating if a window belongs to the same document as its predecessor.

Returns:
Tuple of (compressed, next_prior_kv, next_prior_gate):
Expand Down Expand Up @@ -124,6 +132,11 @@ def csa_overlap_pooling(
a_kv_shifted = jnp.concatenate([prior_a_kv, a_kv[:, :-1]], axis=1)
a_gate_shifted = jnp.concatenate([prior_a_gate, a_gate[:, :-1]], axis=1)

if is_same_doc is not None:
is_same_doc_exp = is_same_doc[:, :, None, None]
a_kv_shifted = jnp.where(is_same_doc_exp, a_kv_shifted, 0.0)
a_gate_shifted = jnp.where(is_same_doc_exp, a_gate_shifted, -jnp.inf)

# 4. Concatenate shifted Ca and unshifted Cb to form the 2m overlapping window
# -> [batch, n_windows, 2 * compress_rate, head_dim]
new_kv = jnp.concatenate([a_kv_shifted, b_kv], axis=2)
Expand Down Expand Up @@ -284,12 +297,15 @@ def compute_csa_prefill_chunk_pooling(
chunk_kv_reshaped = chunk_kv.reshape((batch_size, n_windows, compress_rate, -1))
chunk_gate_reshaped = chunk_gate.reshape((batch_size, n_windows, compress_rate, -1)) + position_bias

block_positions = position_ids[:, :usable:compress_rate]
prior_block_positions = jnp.concatenate([block_positions[:, 0:1] - compress_rate, block_positions[:, :-1]], axis=1)
is_same_doc = block_positions == (prior_block_positions + compress_rate)

compressed, next_prior_kv, next_prior_gate = csa_overlap_pooling(
chunk_kv_reshaped, chunk_gate_reshaped, kv_norm, head_dim, prior_kv, prior_gate
chunk_kv_reshaped, chunk_gate_reshaped, kv_norm, head_dim, prior_kv, prior_gate, is_same_doc=is_same_doc
)
compressed_len = compressed.shape[1]

positions = jnp.arange(compressed_len) * compress_rate + position_ids[:, 0:1]
positions = position_ids[:, :usable:compress_rate]
compressed = rotary_emb(compressed, positions, unsqueeze_dim=None)
else:
compressed = jnp.zeros((batch_size, 0, head_dim), dtype=dtype)
Expand Down Expand Up @@ -584,11 +600,9 @@ def hca_compressor_fn(buf_kv, buf_gate):
return compressed_kv, compressed_mask

# --- PREFILL CHUNKING & PRIMING ---
# Truncate sequence to the nearest multiple of the compression rate
usable = (seq_len // self.compress_rate) * self.compress_rate
chunk_kv = kv[:, :usable]
chunk_gate = gate[:, :usable]
first_window_position = position_ids[:, 0:1]

# Process overlapping windows if there is enough sequence length
if chunk_kv.shape[1] > 0:
Expand All @@ -605,7 +619,7 @@ def hca_compressor_fn(buf_kv, buf_gate):
compressed = self.kv_norm(jnp.sum(chunk_kv * gate_weights, axis=2))

# Calculate positions for the compressed blocks
positions = jnp.arange(n_windows) * self.compress_rate + first_window_position
positions = position_ids[:, : usable : self.compress_rate]

# Apply Rotary Positional Embeddings to the pooled representations
# compressed is [batch, n_windows, head_dim]
Expand Down Expand Up @@ -637,9 +651,9 @@ def hca_compressor_fn(buf_kv, buf_gate):
return compressed_kv, compressed_mask

# Construct a causal mask preventing early queries from attending to future compressed blocks
entry_indices = jnp.arange(compressed_len)
causal_threshold = (position_ids + 1) // self.compress_rate
future_mask = entry_indices[None, None, None, :] >= jnp.expand_dims(causal_threshold, axis=(1, 3))
usable_len = compressed_len * self.compress_rate
block_positions = position_ids[:, : usable_len : self.compress_rate]
future_mask = (block_positions[:, None, None, :] + self.compress_rate) > (position_ids[:, None, :, None] + 1)
compressed_causal_mask = jnp.where(future_mask, DEFAULT_MASK_VALUE, 0.0).astype(self.dtype)

return compressed_kv, compressed_causal_mask
Expand Down Expand Up @@ -876,18 +890,21 @@ def indexer_compressor_fn(buf_kv, buf_gate):

# --- ONLY RUN MATHEMATICAL CAUSAL MASK IN PREFILL/TRAIN ---
if future_mask is None:
causal_threshold = (position_ids + 1) // self.compress_rate
entry_indices_mask = jnp.arange(compressed_len)
future_mask = entry_indices_mask[None, None, :] >= jnp.expand_dims(causal_threshold, axis=-1)
usable_len = compressed_len * self.compress_rate
block_positions = position_ids[:, : usable_len : self.compress_rate]
future_mask = (block_positions[:, None, :] + self.compress_rate) > (position_ids[:, :, None] + 1)

# Apply the mask to the scores
index_scores = jnp.where(future_mask, jnp.full_like(index_scores, -jnp.inf), index_scores)

combined_invalid = future_mask
if attention_mask is not None:
index_scores += attention_mask[:, :, :compressed_len]
att_m = attention_mask[:, :, :compressed_len]
index_scores += att_m
combined_invalid = combined_invalid | (att_m < -100.0)

top_k_indices = jax.lax.top_k(index_scores, k)[1]
invalid = jnp.take_along_axis(future_mask, top_k_indices, axis=-1)
invalid = jnp.take_along_axis(combined_invalid, top_k_indices, axis=-1)

final_indices = jnp.where(invalid, jnp.full_like(top_k_indices, -1), top_k_indices)

Expand Down Expand Up @@ -1501,14 +1518,33 @@ def __call__(
compressed_kv = None
compressed_mask = None
compressed_segment_mask = None
decoder_segment_ids_kv = decoder_segment_ids
compressed_segment_ids = None

if decoder_segment_ids is not None and self.compress_ratio > 0:
# Generate the standard segment mask
segment_mask = decoder_segment_ids[:, :, None] == decoder_segment_ids[:, None, :]
segment_mask_additive = jnp.where(segment_mask, 0.0, DEFAULT_MASK_VALUE)
# Downsample the kv dimension
compress_rate = self.compress_ratio
compressed_segment_mask = segment_mask_additive[:, :, ::compress_rate]
num_blocks = inputs_kv.shape[1] // compress_rate
usable = num_blocks * compress_rate
if decoder_segment_ids.shape[1] < usable:
pad_seg = usable - decoder_segment_ids.shape[1]
last_seg = decoder_segment_ids[:, -1:]
pad_block = jnp.repeat(last_seg, pad_seg, axis=1)
padded_seg_ids = jnp.concatenate([decoder_segment_ids, pad_block], axis=1)
else:
padded_seg_ids = decoder_segment_ids[:, :usable]

chunked_segment_ids = padded_seg_ids.reshape((decoder_segment_ids.shape[0], num_blocks, compress_rate))
min_seg = jnp.min(chunked_segment_ids, axis=-1)
max_seg = jnp.max(chunked_segment_ids, axis=-1)
is_valid_window = min_seg == max_seg

compressed_segment_ids = jnp.where(is_valid_window, min_seg, -1)
decoder_segment_ids_kv = jnp.concatenate([decoder_segment_ids, compressed_segment_ids], axis=1)

valid_comp_seg = (decoder_segment_ids[:, :, None] == compressed_segment_ids[:, None, :]) & (
compressed_segment_ids[:, None, :] >= 0
)
compressed_segment_mask = jnp.where(valid_comp_seg, 0.0, DEFAULT_MASK_VALUE)

# Route to the appropriate compressor depending on the layer's role in the architecture
if self.compress_ratio > 4:
Expand All @@ -1534,10 +1570,33 @@ def __call__(
compressed_segment_mask[:, :, : compressed_mask.shape[-1]], axis=1
)

# Note: Unlike standard pre-attention concatenation (extending local KV tensors with compressed blocks),
# compressed_kv is passed separately to attention_op to support custom kernels and decoding caching.
kv = checkpoint_name(kv, "kv_proj")

pad_kv_total = 0
unpadded_kv = jnp.concatenate([kv, compressed_kv], axis=1) if compressed_kv is not None else kv

# Pad total KV length to tile size multiple (config.sa_block_kv) for SPMD sequence divisibility and
# Tokamax dynamic splash tile boundary alignment. Note: Tokamax kernel inside AttentionOp additionally
# sets inner block size as min(block_kv, key_len) during kernel invocation.
if self.attention_kernel == "flash":
total_kv_len = kv.shape[1] + (compressed_kv.shape[1] if compressed_kv is not None else 0)
block_size = self.config.sa_block_kv
pad_kv_total = (block_size - (total_kv_len % block_size)) % block_size

if pad_kv_total > 0:
if compressed_kv is not None:
# Prepend padding to the compressed blocks so they remain at the end of the sequence
compressed_kv = jnp.pad(compressed_kv, ((0, 0), (pad_kv_total, 0), (0, 0), (0, 0)))

if decoder_segment_ids is not None and compressed_segment_ids is not None:
comp_seg_padded = jnp.pad(compressed_segment_ids, ((0, 0), (pad_kv_total, 0)), constant_values=-1)
decoder_segment_ids_kv = jnp.concatenate([decoder_segment_ids, comp_seg_padded], axis=1)
else:
# Fallback: Pad at the end if no compressed blocks exist
kv = jnp.pad(kv, ((0, 0), (0, pad_kv_total), (0, 0), (0, 0)))
if decoder_segment_ids_kv is not None:
decoder_segment_ids_kv = jnp.pad(decoder_segment_ids_kv, ((0, 0), (0, pad_kv_total)), constant_values=-1)

# Prepare the mask shape for the underlying AttentionOp
if compressed_mask is not None:
compressed_mask = jnp.expand_dims(compressed_mask, axis=2)
Expand All @@ -1546,7 +1605,25 @@ def __call__(
if self.query_pre_attn_scalar and self.query_pre_attn_scalar != 1.0:
q = q * self.query_pre_attn_scalar

# Compute Attention (Now safely passing kv_cache so the kernel doesn't assert!)
# Build indexer mask explicitly for tokamax splash kernel (CSA dynamic path)
indexer_mask = None
if self.attention_kernel == "flash" and compressed_mask is not None and self.compress_ratio == 4:
indexer_mask = self.attention_op.generate_attention_mask(
q,
unpadded_kv,
decoder_segment_ids,
model_mode,
compressed_mask=compressed_mask,
pad_kv_total=pad_kv_total,
decoder_segment_ids_kv=decoder_segment_ids_kv,
)

if indexer_mask is not None:
# Extract single KV head and Query-per-KV head group axes [batch, 1, 1, Q, KV] -> [batch, Q, KV]
indexer_mask = indexer_mask[:, 0, 0, :, :]
Comment on lines +1621 to +1623

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If decoder_segment_ids is None (e.g., when document packing is disabled), generate_attention_mask returns a 4D mask of shape [batch, 1, Q, KV] instead of a 5D mask of shape [batch, 1, 1, Q, KV]. Hardcoding the indexer mask extraction as indexer_mask[:, 0, 0, :, :] will raise an IndexError in this scenario. Squeezing out any extra dimensions of size 1 dynamically is much more robust and avoids potential crashes.

Suggested change
if indexer_mask is not None:
# Extract single KV head and Query-per-KV head group axes [batch, 1, 1, Q, KV] -> [batch, Q, KV]
indexer_mask = indexer_mask[:, 0, 0, :, :]
if indexer_mask is not None:
# Squeeze out any extra dimensions of size 1 between batch and Q/KV axes to get [batch, Q, KV]
while indexer_mask.ndim > 3:
indexer_mask = jnp.squeeze(indexer_mask, axis=1)


# Compute Attention
# -> [batch, q_length, num_query_heads, head_dim]
attn_out = self.attention_op(
q,
kv,
Expand All @@ -1558,6 +1635,9 @@ def __call__(
compressed_mask=compressed_mask,
compressed_kv=compressed_kv,
cached_values=current_kv_cache,
indexer_mask=indexer_mask,
decoder_segment_ids_kv=decoder_segment_ids_kv,
pad_kv_total=pad_kv_total,
)
Comment on lines 1635 to 1641

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Pass self.compress_ratio to self.attention_op to ensure the static mask compilation uses the correct compression ratio.

Suggested change
compressed_mask=compressed_mask,
compressed_kv=compressed_kv,
cached_values=current_kv_cache,
indexer_mask=indexer_mask,
decoder_segment_ids_kv=decoder_segment_ids_kv,
pad_kv_total=pad_kv_total,
)
compressed_mask=compressed_mask,
compressed_kv=compressed_kv,
cached_values=current_kv_cache,
indexer_mask=indexer_mask,
decoder_segment_ids_kv=decoder_segment_ids_kv,
pad_kv_total=pad_kv_total,
compress_ratio=self.compress_ratio,
)


# Reverse RoPE on Values
Expand Down
Loading
Loading