DSV4: Route compressed and sliding train attention onto the splash kernels - #4908
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the compressed_use_dynamic_splash configuration to route COMPRESSED and LOCAL_SLIDING train attention to the tokamax splash kernel on TPU. It adds mask building utilities, implements dynamic_splash_attention within AttentionOp, and includes comprehensive unit tests. The review feedback focuses on performance and memory optimizations: first, slicing decoder_segment_ids directly during comparison in build_local_sliding_splash_mask to avoid large intermediate tensor allocations; second, replacing the computationally expensive jnp.isclose with a simpler inequality comparison when processing the indexer mask.
| if decoder_segment_ids is not None: | ||
| segment = decoder_segment_ids[:, :, None] == decoder_segment_ids[:, None, :] | ||
| mask = jnp.logical_and(mask, segment[..., :kv_seq_len]) |
There was a problem hiding this comment.
When decoder_segment_ids is not None, creating the full [batch, q_seq_len, q_seq_len] intermediate tensor segment and then slicing it to kv_seq_len can be highly inefficient, especially for large sequence lengths (e.g., 16k).
We can avoid this large intermediate allocation by slicing decoder_segment_ids directly during the comparison.
| if decoder_segment_ids is not None: | |
| segment = decoder_segment_ids[:, :, None] == decoder_segment_ids[:, None, :] | |
| mask = jnp.logical_and(mask, segment[..., :kv_seq_len]) | |
| if decoder_segment_ids is not None: | |
| segment = decoder_segment_ids[:, :, None] == decoder_segment_ids[:, None, :kv_seq_len] | |
| mask = jnp.logical_and(mask, segment) |
| if indexer_mask.dtype != jnp.bool_: | ||
| indexer_mask = jnp.isclose(indexer_mask, 0.0) |
There was a problem hiding this comment.
Using jnp.isclose(indexer_mask, 0.0) is computationally expensive because it involves absolute difference calculations, tolerances, and handling of special float values (NaNs/Infs).
Since MaxText additive masks consistently use 0.0 for keep and DEFAULT_MASK_VALUE for discard, we can use a simple inequality comparison indexer_mask >= DEFAULT_MASK_VALUE * 0.5. This is significantly faster and matches the masking logic used elsewhere in this file.
| if indexer_mask.dtype != jnp.bool_: | |
| indexer_mask = jnp.isclose(indexer_mask, 0.0) | |
| if indexer_mask.dtype != jnp.bool_: | |
| indexer_mask = indexer_mask >= DEFAULT_MASK_VALUE * 0.5 |
…ash kernels Opt-in TPU path (compressed_use_dynamic_splash) that avoids materializing dense seq^2 logits. At seq 16384 the dense path needs 96.8 GiB per chip and does not compile on v6e. Co-authored-by: Sudarsanan <sudarsanan@systalyze.com> Co-authored-by: Armin <armin@systalyze.com> Co-authored-by: utlz <utilyze@systalyze.com>
d721735 to
92da629
Compare
Description
Adds an opt-in TPU path that routes DeepSeek-V4
COMPRESSEDandLOCAL_SLIDINGtrain attention to the existing splash kernels. This avoids materializing dense q x kv logits.Performance
Setup:
ici_expert_parallelism=16,ici_tensor_parallelism=8,ici_fsdp_parallelism=1,max_target_length=16384,per_device_batch_size=0.125(global batch 16),LIBTPU_INIT_ARGS=--xla_tpu_scoped_vmem_limit_kib=98304, dynamic-splash attention path enabled. The seq 16384 memory figure is the AOT compile requirement at that setup with this change removed.Tests
pytest tests/unit/dynamic_splash_mask_test.py tests/unit/attention_test.py:137 passed.Checklist
Before submitting this PR, please make sure (put X in square brackets):
gemini-reviewlabel.