-
Notifications
You must be signed in to change notification settings - Fork 589
DSV4: Route compressed and sliding train attention onto the splash kernels #4908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -134,6 +134,59 @@ def apply_mask_to_logits(logits: Array, mask: Array): | |||||||||
| return jnp.where((mask >= DEFAULT_MASK_VALUE * 0.5), logits, DEFAULT_MASK_VALUE) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def build_local_sliding_splash_mask( | ||||||||||
| batch: int, | ||||||||||
| decoder_segment_ids: Array | None, | ||||||||||
| segment_positions: Array | None, | ||||||||||
| q_seq_len: int, | ||||||||||
| kv_seq_len: int, | ||||||||||
| sliding_window_size: int | None, | ||||||||||
| ) -> Array: | ||||||||||
| """Builds the causal sliding mask for the uncompressed COMPRESSED prefix. | ||||||||||
|
|
||||||||||
| To match the dense path under packing, query positions may reset by segment while key | ||||||||||
| positions remain physical offsets. | ||||||||||
| """ | ||||||||||
| if segment_positions is not None: | ||||||||||
| row_ids = segment_positions[:, :, None] | ||||||||||
| else: | ||||||||||
| row_ids = jnp.arange(q_seq_len)[None, :, None] | ||||||||||
| col_ids = jnp.arange(kv_seq_len)[None, None, :] | ||||||||||
| distance = row_ids - col_ids | ||||||||||
| mask = distance >= 0 | ||||||||||
| if sliding_window_size is not None: | ||||||||||
| mask &= distance < sliding_window_size | ||||||||||
| mask = jnp.broadcast_to(mask, (batch, q_seq_len, kv_seq_len)) | ||||||||||
| 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]) | ||||||||||
| return mask | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def build_compressed_splash_mask( | ||||||||||
| compressed_mask: Array, | ||||||||||
| decoder_segment_ids: Array | None, | ||||||||||
| segment_positions: Array | None, | ||||||||||
| q_seq_len: int, | ||||||||||
| kv_seq_len: int, | ||||||||||
| sliding_window_size: int | None, | ||||||||||
| ) -> Array: | ||||||||||
| """Builds the boolean COMPRESSED mask for dynamic splash attention. | ||||||||||
|
|
||||||||||
| The result has shape `[batch, q_seq_len, kv_seq_len]` and matches the dense path's | ||||||||||
| `apply_mask_to_logits` keep predicate. | ||||||||||
| """ | ||||||||||
| c_len = compressed_mask.shape[-1] | ||||||||||
| s_len = kv_seq_len - c_len | ||||||||||
| b = compressed_mask.shape[0] | ||||||||||
|
|
||||||||||
| uncompressed = build_local_sliding_splash_mask( | ||||||||||
| b, decoder_segment_ids, segment_positions, q_seq_len, s_len, sliding_window_size | ||||||||||
| ) | ||||||||||
| compressed_keep = compressed_mask.reshape(b, q_seq_len, c_len) >= DEFAULT_MASK_VALUE * 0.5 | ||||||||||
| return jnp.concatenate([uncompressed, compressed_keep], axis=-1) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| def validate_gpu_flash_attention(sinks: Array | None, record_max_logits: bool) -> None: | ||||||||||
| """Helper function to check for unsupported features with flash attention on GPU.""" | ||||||||||
| if sinks is not None: | ||||||||||
|
|
@@ -568,14 +621,18 @@ def __init__( | |||||||||
| raise ValueError("causal_block_size must be positive for block-diffusion attention") | ||||||||||
| if self.attention_kernel not in ("autoselected", "dot_product", "flash"): | ||||||||||
| raise ValueError("Block-diffusion attention is supported only by dot_product attention and TPU Splash attention.") | ||||||||||
| # Block sizes are only used by TPU splash attention kernels. Exclude non-splash kernels | ||||||||||
| # The opt-in COMPRESSED and LOCAL_SLIDING routes need splash block sizes despite using | ||||||||||
| # attention_kernel="dot_product". | ||||||||||
| if self.attention_kernel not in ( | ||||||||||
| "dot_product", | ||||||||||
| "paged", | ||||||||||
| "vllm_rpa", | ||||||||||
| "vllm_batched_rpa", | ||||||||||
| "cudnn_flash_te", | ||||||||||
| "cudnn_flash_jax", | ||||||||||
| ) or ( | ||||||||||
| self.attention_type in (AttentionType.COMPRESSED, AttentionType.LOCAL_SLIDING) | ||||||||||
| and self.config.compressed_use_dynamic_splash | ||||||||||
| ): | ||||||||||
| if self.attention_type == AttentionType.LOCAL_SLIDING: | ||||||||||
| self.block_q = self.config.local_sa_block_q | ||||||||||
|
|
@@ -1387,6 +1444,44 @@ def apply_attention( | |||||||||
| self.max_logits = nnx.Intermediate(local_max) | ||||||||||
| return local_out, local_max, local_sum | ||||||||||
|
|
||||||||||
| # LOCAL_SLIDING has a static splash mask; COMPRESSED needs the dynamic mask from its | ||||||||||
| # compressor. Dense-only mask modifiers must continue through the dot-product path. | ||||||||||
| elif ( | ||||||||||
| self.config.compressed_use_dynamic_splash | ||||||||||
| and model_mode == MODEL_MODE_TRAIN | ||||||||||
| and target_hardware == "tpu" | ||||||||||
| and previous_chunk is None | ||||||||||
| and bidirectional_mask is None | ||||||||||
| and ( | ||||||||||
| self.attention_type == AttentionType.LOCAL_SLIDING | ||||||||||
| or (self.attention_type == AttentionType.COMPRESSED and compressed_mask is not None) | ||||||||||
| ) | ||||||||||
| ): | ||||||||||
| if self.attention_type == AttentionType.LOCAL_SLIDING: | ||||||||||
| out, max_logits = self.tpu_flash_attention( | ||||||||||
| query, | ||||||||||
| key, | ||||||||||
| value, | ||||||||||
| decoder_segment_ids, | ||||||||||
| self.attn_logits_soft_cap, | ||||||||||
| sinks, | ||||||||||
| model_mode=model_mode, | ||||||||||
| record_max_logits=record_max_logits, | ||||||||||
| ) | ||||||||||
| if max_logits is not None: | ||||||||||
| self.max_logits = nnx.Intermediate(max_logits) | ||||||||||
| return out, None, None | ||||||||||
| return self.dynamic_splash_attention( | ||||||||||
| query, | ||||||||||
| key, | ||||||||||
| value, | ||||||||||
| decoder_segment_ids, | ||||||||||
| segment_positions, | ||||||||||
| compressed_mask, | ||||||||||
| sinks, | ||||||||||
| record_max_logits, | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| # 'vllm_rpa' uses the same dot-attention wrapper but routes to the vLLM | ||||||||||
| # ragged paged attention kernel in `Attention.__call__`. | ||||||||||
| elif ( | ||||||||||
|
|
@@ -1597,6 +1692,65 @@ def wrap_ragged_attention(query, key, value, lengths, block_size): | |||||||||
|
|
||||||||||
| return wrap_ragged_attention(query, key, value, lengths, block_size) | ||||||||||
|
|
||||||||||
| def dynamic_splash_attention( | ||||||||||
| self, | ||||||||||
| query: Array, | ||||||||||
| key: Array, | ||||||||||
| value: Array, | ||||||||||
| decoder_segment_ids: Array | None, | ||||||||||
| segment_positions: Array | None, | ||||||||||
| compressed_mask: Array, | ||||||||||
| sinks: Array | None, | ||||||||||
| record_max_logits: bool = False, | ||||||||||
| ) -> tuple[Array, None, None]: | ||||||||||
| """Runs COMPRESSED train attention with the tokamax dynamic splash kernel. | ||||||||||
|
|
||||||||||
| Packing is folded into the boolean mask because no segment ids exist for the compressed | ||||||||||
| KV columns. KV and mask columns are padded to satisfy all splash block sizes. | ||||||||||
| """ | ||||||||||
| q_seq_len = query.shape[1] | ||||||||||
| kv_seq_len = key.shape[1] | ||||||||||
|
|
||||||||||
| if self.mesh.shape.get(self.config.context_sharding, 1) > 1: | ||||||||||
| raise NotImplementedError( | ||||||||||
| "compressed_use_dynamic_splash does not support context parallelism for COMPRESSED " | ||||||||||
| "attention (the dynamic-mask splash path has no load-balanced reorder for the " | ||||||||||
| "compressed kv concat). LOCAL_SLIDING layers take the static splash path and do." | ||||||||||
| ) | ||||||||||
| for name, blk in (("block_q", self.block_q), ("block_q_dkv", self.block_q_dkv)): | ||||||||||
| eff = min(blk, q_seq_len) | ||||||||||
| if q_seq_len % eff: | ||||||||||
| raise ValueError( | ||||||||||
| f"compressed_use_dynamic_splash: query length {q_seq_len} must be a multiple of " | ||||||||||
| f"min({name}={blk}, q_len) = {eff}." | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| bool_mask = build_compressed_splash_mask( | ||||||||||
| compressed_mask, decoder_segment_ids, segment_positions, q_seq_len, kv_seq_len, self.sliding_window_size | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| lcm = math.lcm(self.block_kv, self.block_kv_compute, self.block_kv_dkv, self.block_kv_dkv_compute) | ||||||||||
| padded_kv_len = math.ceil(kv_seq_len / lcm) * lcm | ||||||||||
| if padded_kv_len != kv_seq_len: | ||||||||||
| pad = padded_kv_len - kv_seq_len | ||||||||||
| key = jnp.pad(key, ((0, 0), (0, pad), (0, 0), (0, 0))) | ||||||||||
| value = jnp.pad(value, ((0, 0), (0, pad), (0, 0), (0, 0))) | ||||||||||
| bool_mask = jnp.pad(bool_mask, ((0, 0), (0, 0), (0, pad))) | ||||||||||
|
|
||||||||||
| out, max_logits = self.tpu_flash_attention( | ||||||||||
| query, | ||||||||||
| key, | ||||||||||
| value, | ||||||||||
| decoder_segment_ids=None, | ||||||||||
| attn_logits_soft_cap=self.attn_logits_soft_cap, | ||||||||||
| sinks=sinks, | ||||||||||
| indexer_mask=bool_mask, | ||||||||||
| record_max_logits=record_max_logits, | ||||||||||
| ) | ||||||||||
| if max_logits is not None: | ||||||||||
| self.max_logits = nnx.Intermediate(max_logits) | ||||||||||
| return out, None, None | ||||||||||
|
|
||||||||||
| def tpu_flash_attention( | ||||||||||
| self, | ||||||||||
| query: Array, | ||||||||||
|
|
@@ -2068,8 +2222,9 @@ def wrap_flash_attention( | |||||||||
| decoder_segment_ids_tuple = None | ||||||||||
|
|
||||||||||
| if self.config.use_tokamax_splash: | ||||||||||
| if self.config.use_indexer and indexer_mask is not None: | ||||||||||
| # Construct the splash kernel call with dynamic mask | ||||||||||
| if indexer_mask is not None: | ||||||||||
| # Pallas interpret mode does not propagate input_output_aliases across grid steps, so | ||||||||||
| # dynamic-splash GQA gradients must be checked on TPU. | ||||||||||
| def dynamic_mask_splash_kernel(q, k, v, segment, sinks, indexer_mask): | ||||||||||
| splash_kernel = tokamax_splash_kernel.make_dynamic_splash_mha( | ||||||||||
| mask=indexer_mask, | ||||||||||
|
|
@@ -2083,9 +2238,9 @@ def dynamic_mask_splash_kernel(q, k, v, segment, sinks, indexer_mask): | |||||||||
| else: | ||||||||||
| return kernel(q, k, v, segment, sinks=sinks), None | ||||||||||
|
|
||||||||||
| # Iterate over batch dimension for (query, key, value, segment, sinks, mask) | ||||||||||
| attn_fn = jax.vmap(dynamic_mask_splash_kernel, (0, 0, 0, 0, None, 0)) | ||||||||||
| indexer_mask = jnp.isclose(indexer_mask, 0.0) | ||||||||||
| if indexer_mask.dtype != jnp.bool_: | ||||||||||
| indexer_mask = jnp.isclose(indexer_mask, 0.0) | ||||||||||
|
Comment on lines
+2242
to
+2243
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using Since MaxText additive masks consistently use
Suggested change
|
||||||||||
|
|
||||||||||
| if record_max_logits: | ||||||||||
| attention_output, max_logits = attn_fn(query, key, value, decoder_segment_ids_tuple, sinks, indexer_mask) | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
decoder_segment_idsis not None, creating the full[batch, q_seq_len, q_seq_len]intermediate tensorsegmentand then slicing it tokv_seq_lencan be highly inefficient, especially for large sequence lengths (e.g., 16k).We can avoid this large intermediate allocation by slicing
decoder_segment_idsdirectly during the comparison.