Add HCA Static Compilation for Splash Attention [Deepseek v4] - #4924
Add HCA Static Compilation for Splash Attention [Deepseek v4]#4924octatrifan wants to merge 50 commits into
Conversation
…lash Attention - Replace sequence integer-truncation with ceil-padding and -1e9 gate score masking in DeepseekV4HCACompressor. - Guard decoder segment ID sequence slicing with background token padding for unaligned document packing bounds. - Use explicit rotary_embedding.head_dim in CompressedAttention to accommodate non-standard RoPE head dimensions. - Add numerical equivalence testing between dot_product and flash attention kernels and verify non-power-of-two sequence lengths (e.g. L=489).
… PR 4488 config.head_dim
… segment IDs, HCA truncation, and segment boundary mask
…ig types validation conflict
…l sliding block size alignment
…igned sequence lengths and clean debug config types
…runcation of unaligned prompt lengths (e.g. 489 tokens)
…checker.py" This reverts commit 5cdbfc2.
- Add HCAStaticMask with causal masking on compressed tokens - Align KV sequence length to sa_block_kv multiples for static Tokamax Splash attention - Support unaligned sequence lengths in Tokamax SplashConfig using GCD block size calculation - Route HCA (compress_ratio > 4) to static Splash attention with indexer_mask=None
…cal tolerance docstring note
… and test coverage
…ation with adversarial leakage check
…ysses splash conflict
…or in config validation
…rst_window_position in CompressedAttention
- Add autoselected attention support to deepseek4 and indexer config validators. - Set default use_tokamax_splash: true for deepseek4-tiny and deepseek4-284b models. - Separate flash and dot_product attention test methods in DeepSeekV4CompressedAttentionTest with tpu_only markers. - Update reference parity dimensions in DeepSeekV4ConversionMappingTest and DeepSeekV4HyperHeadTest. - Adjust CompressedAttentionTest numerical tolerances for TPU matmul accumulation.
… dynamic jnp.pad in CSA
…and missing comments
There was a problem hiding this comment.
Code Review
This pull request introduces support for DeepSeek-V4's Compressed Sparse Attention (CSA) and Heavily Compressed Attention (HCA) mechanisms, including overlapping window pooling, document-packing-aware masking, and sequence padding for Splash kernel alignment. It also adds extensive unit and parity tests. The reviewer feedback identifies a potential IndexError when decoder_segment_ids is None due to hardcoded indexing of the indexer mask, which can be resolved by dynamically squeezing size-1 dimensions. Additionally, the reviewer points out that compress_ratio is incorrectly inferred in HCAStaticMask for unaligned sequence lengths, suggesting explicitly passing compress_ratio through the attention pipeline. Finally, simplifying the pattern matching in configuration validation to a standard if statement is recommended for improved readability.
| 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, :, :] |
There was a problem hiding this comment.
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.
| 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) |
| def __init__( | ||
| self, | ||
| shape: tuple[int, int], | ||
| local_kv_len: int, | ||
| compressed_kv_len: int, | ||
| pad_kv_total: int = 0, | ||
| sliding_window_size: int | None = None, | ||
| shard_count: int = 1, | ||
| ): | ||
| compress_ratio = max(1, local_kv_len // max(1, compressed_kv_len)) |
There was a problem hiding this comment.
The compress_ratio is dynamically inferred in HCAStaticMask as max(1, local_kv_len // max(1, compressed_kv_len)). However, if the unpadded sequence length (local_kv_len) is not a multiple of the actual compression rate (e.g., seq_len = 489 with a compression rate of 128), this inferred ratio will be incorrect (e.g., 489 // 3 = 163), leading to a mathematically incorrect attention mask. Passing the actual compress_ratio explicitly from the layer configuration ensures correctness under all sequence lengths.
| def __init__( | |
| self, | |
| shape: tuple[int, int], | |
| local_kv_len: int, | |
| compressed_kv_len: int, | |
| pad_kv_total: int = 0, | |
| sliding_window_size: int | None = None, | |
| shard_count: int = 1, | |
| ): | |
| compress_ratio = max(1, local_kv_len // max(1, compressed_kv_len)) | |
| def __init__( | |
| self, | |
| shape: tuple[int, int], | |
| local_kv_len: int, | |
| compressed_kv_len: int, | |
| pad_kv_total: int = 0, | |
| sliding_window_size: int | None = None, | |
| shard_count: int = 1, | |
| compress_ratio: int = 0, | |
| ): | |
| if compress_ratio <= 0: | |
| compress_ratio = max(1, local_kv_len // max(1, compressed_kv_len)) |
| elif self.attention_type == AttentionType.COMPRESSED and indexer_mask is None: | ||
| local_kv_len = query.shape[2] | ||
| compressed_kv_len = key.shape[2] - query.shape[2] - pad_kv_total | ||
| mask = HCAStaticMask( | ||
| shape=mask_shape, | ||
| local_kv_len=local_kv_len, | ||
| compressed_kv_len=compressed_kv_len, | ||
| pad_kv_total=pad_kv_total, | ||
| sliding_window_size=self.sliding_window_size if self.sliding_window_size else None, | ||
| ) |
There was a problem hiding this comment.
Pass the explicit compress_ratio parameter to HCAStaticMask to ensure the attention mask is computed correctly when sequence lengths are not multiples of the compression rate.
| elif self.attention_type == AttentionType.COMPRESSED and indexer_mask is None: | |
| local_kv_len = query.shape[2] | |
| compressed_kv_len = key.shape[2] - query.shape[2] - pad_kv_total | |
| mask = HCAStaticMask( | |
| shape=mask_shape, | |
| local_kv_len=local_kv_len, | |
| compressed_kv_len=compressed_kv_len, | |
| pad_kv_total=pad_kv_total, | |
| sliding_window_size=self.sliding_window_size if self.sliding_window_size else None, | |
| ) | |
| elif self.attention_type == AttentionType.COMPRESSED and indexer_mask is None: | |
| local_kv_len = query.shape[2] | |
| compressed_kv_len = key.shape[2] - query.shape[2] - pad_kv_total | |
| mask = HCAStaticMask( | |
| shape=mask_shape, | |
| local_kv_len=local_kv_len, | |
| compressed_kv_len=compressed_kv_len, | |
| pad_kv_total=pad_kv_total, | |
| sliding_window_size=self.sliding_window_size if self.sliding_window_size else None, | |
| compress_ratio=compress_ratio, | |
| ) |
| bidirectional_mask: Any = None, | ||
| use_ragged_attention: bool = False, | ||
| record_max_logits: bool = False, | ||
| decoder_segment_ids_kv: Array | None = None, | ||
| pad_kv_total: int = 0, | ||
| ) -> tuple[Array, Array]: |
There was a problem hiding this comment.
Add compress_ratio as an optional parameter to tpu_flash_attention to support explicit compression ratio passing for static mask compilation.
| bidirectional_mask: Any = None, | |
| use_ragged_attention: bool = False, | |
| record_max_logits: bool = False, | |
| decoder_segment_ids_kv: Array | None = None, | |
| pad_kv_total: int = 0, | |
| ) -> tuple[Array, Array]: | |
| bidirectional_mask: Any = None, | |
| use_ragged_attention: bool = False, | |
| record_max_logits: bool = False, | |
| decoder_segment_ids_kv: Array | None = None, | |
| pad_kv_total: int = 0, | |
| compress_ratio: int = 0, | |
| ) -> tuple[Array, Array]: |
| indexer_mask: Array | None = None, | ||
| compressed_mask: Optional[Array] = None, | ||
| record_max_logits: bool = False, | ||
| decoder_segment_ids_kv: Optional[Array] = None, | ||
| pad_kv_total: int = 0, | ||
| *, |
There was a problem hiding this comment.
Add compress_ratio as an optional parameter to apply_attention to support explicit compression ratio passing.
| indexer_mask: Array | None = None, | |
| compressed_mask: Optional[Array] = None, | |
| record_max_logits: bool = False, | |
| decoder_segment_ids_kv: Optional[Array] = None, | |
| pad_kv_total: int = 0, | |
| *, | |
| indexer_mask: Array | None = None, | |
| compressed_mask: Optional[Array] = None, | |
| record_max_logits: bool = False, | |
| decoder_segment_ids_kv: Optional[Array] = None, | |
| pad_kv_total: int = 0, | |
| compress_ratio: int = 0, | |
| *, |
| bidirectional_mask=bidirectional_mask, | ||
| use_ragged_attention=use_ragged_attention, | ||
| record_max_logits=record_max_logits, | ||
| decoder_segment_ids_kv=decoder_segment_ids_kv, | ||
| pad_kv_total=pad_kv_total, | ||
| ) |
There was a problem hiding this comment.
| compressed_kv: Optional[Array] = None, | ||
| slot: Optional[int] = None, | ||
| record_max_logits: bool = False, | ||
| decoder_segment_ids_kv: Optional[Array] = None, | ||
| pad_kv_total: int = 0, | ||
| ): |
There was a problem hiding this comment.
| record_max_logits=record_max_logits, | ||
| qk_product_einsum=self.AqtEinsum_0, | ||
| wv_product_einsum=self.AqtEinsum_1, | ||
| decoder_segment_ids_kv=decoder_segment_ids_kv, | ||
| pad_kv_total=pad_kv_total, | ||
| ) |
There was a problem hiding this comment.
Pass compress_ratio to apply_attention inside AttentionOp.call.
| record_max_logits=record_max_logits, | |
| qk_product_einsum=self.AqtEinsum_0, | |
| wv_product_einsum=self.AqtEinsum_1, | |
| decoder_segment_ids_kv=decoder_segment_ids_kv, | |
| pad_kv_total=pad_kv_total, | |
| ) | |
| record_max_logits=record_max_logits, | |
| qk_product_einsum=self.AqtEinsum_0, | |
| wv_product_einsum=self.AqtEinsum_1, | |
| decoder_segment_ids_kv=decoder_segment_ids_kv, | |
| pad_kv_total=pad_kv_total, | |
| compress_ratio=compress_ratio, | |
| ) |
| 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, | ||
| ) |
There was a problem hiding this comment.
Pass self.compress_ratio to self.attention_op to ensure the static mask compilation uses the correct compression ratio.
| 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, | |
| ) |
| 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`." | ||
| ) |
There was a problem hiding this comment.
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."
)
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Description
Implements a static Tokamax Splash Attention compilation path for DeepSeek-V4 Heavily Compressed Attention (HCA,
compress_ratio > 4). This replaces the dynamic indexer mask dispatch with a statically compiledHCAStaticMask.Context & Motivation
Previously, both CSA and HCA were routed through
make_dynamic_splash_mha. While CSA requires dynamic boolean masks generated at runtime by the top-k sparse indexer, HCA's compressed attention pattern is completely deterministic and static (local causal window + evenly spaced compressed KV tokens). Running HCA through the dynamic mask path added unnecessary mask materialization and Pallas dynamic grid dispatch overhead.Key Changes
HCAStaticMask: Implemented a computable mask for static Tokamax splash attention that handles the local sliding window, causal boundaries, and compressed token indices without generating runtime boolean masks.AttentionOp.tpu_flash_attentionfor both CSA and HCA. When sequence lengths are not multiples of the block size (e.g., length 3968 with block size 512), query tensors and segment IDs are padded to block boundaries before kernel dispatch, and outputs are sliced back to the original sequence length.jax.vmapbatch dispatch for the static splash kernel, ensuring full compatibility with context parallelism (CP) and per-sequence document packing.Tests
Validated locally and on Cloud TPU v5p (
octatrifan-v5p8):All test suites passed on TPU-v5p (including unaligned lengths like 489 and 3968, and packing equivalence checks).
Checklist
Before submitting this PR, please make sure (put X in square brackets):
gemini-reviewlabel.