From f03759f25a6de30bcd2f4825cca328d4524bd0df Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 24 Aug 2026 02:19:30 +0000 Subject: [PATCH 01/13] Add fused Pallas TPU CSA StreamIndex score kernel for DeepSeek-V4 --- src/maxtext/configs/base.yml | 1 + src/maxtext/configs/types.py | 4 + src/maxtext/kernels/attention/__init__.py | 1 + .../kernels/attention/csa_streamindex.py | 144 +++++++++++ src/maxtext/layers/attention_compressed.py | 38 ++- tests/unit/csa_streamindex_test.py | 232 ++++++++++++++++++ 6 files changed, 409 insertions(+), 11 deletions(-) create mode 100644 src/maxtext/kernels/attention/csa_streamindex.py create mode 100644 tests/unit/csa_streamindex_test.py diff --git a/src/maxtext/configs/base.yml b/src/maxtext/configs/base.yml index d8efa17359..ade638473e 100644 --- a/src/maxtext/configs/base.yml +++ b/src/maxtext/configs/base.yml @@ -457,6 +457,7 @@ o_lora_rank: 0 # Output LoRA rank for Compressed Attention. o_groups: 0 # Output groups for Compressed Attention. compress_ratios: [] # Per-layer compression ratios (0, 4, 128, etc). compressed_rope_max_timescale: 160_000 # If positive, used for Compressed Sparse/Heavy Attention. +use_csa_streamindex_kernel: false # Whether to use Pallas TPU kernel for CSA StreamIndex score computation. # QK-Clip (Muon Clip) Configuration use_qk_clip: false # Enable QK-Clip (supported in MLA with DotProduct or Tokamax Splash) diff --git a/src/maxtext/configs/types.py b/src/maxtext/configs/types.py index db9a68d1cd..c97cc808fa 100644 --- a/src/maxtext/configs/types.py +++ b/src/maxtext/configs/types.py @@ -712,6 +712,10 @@ class CompressedAttention(BaseModel): compressed_rope_max_timescale: int = Field( 160000, description="If positive, used for Compressed Sparse/Heavy Attention." ) + use_csa_streamindex_kernel: bool = Field( + False, + description="Whether to use Pallas TPU kernel for CSA StreamIndex score computation.", + ) class AttentionIndexer(BaseModel): diff --git a/src/maxtext/kernels/attention/__init__.py b/src/maxtext/kernels/attention/__init__.py index 3bf51ab4d6..bb28108248 100644 --- a/src/maxtext/kernels/attention/__init__.py +++ b/src/maxtext/kernels/attention/__init__.py @@ -14,4 +14,5 @@ """Attention kernels.""" +from maxtext.kernels.attention import csa_streamindex from maxtext.kernels.attention import splash_attention_kernel diff --git a/src/maxtext/kernels/attention/csa_streamindex.py b/src/maxtext/kernels/attention/csa_streamindex.py new file mode 100644 index 0000000000..6d4e678300 --- /dev/null +++ b/src/maxtext/kernels/attention/csa_streamindex.py @@ -0,0 +1,144 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pallas TPU kernel for DeepSeek-V4 CSA StreamIndex score computation.""" + +import functools +import jax +from jax.experimental import pallas as pl +from jax.experimental.pallas import tpu as pltpu +import jax.numpy as jnp + + +def csa_streamindex_score_kernel( + q_ref, # [block_q, num_heads, head_dim] + k_ref, # [block_w, head_dim] + w_ref, # [block_q, num_heads] + out_ref, # [block_q, block_w] + *, + softmax_scale: float, +): + """Pallas TPU kernel for fused indexer score calculation. + + Computes scores directly in VMEM without materializing intermediate + [b, h, s, w] tensors in HBM. + """ + q = q_ref[...] + k = k_ref[...] + w = w_ref[...] + + # QK dot product: [block_q, num_heads, head_dim] x [block_w, head_dim] -> [block_q, num_heads, block_w] + scores = jnp.einsum( + "shd,wd->shw", + q.astype(jnp.float32), + k.astype(jnp.float32), + preferred_element_type=jnp.float32, + ) + scores = jnp.maximum(scores, 0.0) * softmax_scale + scores = scores * w[:, :, None].astype(jnp.float32) + out = jnp.sum(scores, axis=1) + out_ref[...] = out.astype(out_ref.dtype) + + +def csa_streamindex_score( + q: jax.Array, + compressed: jax.Array, + weights: jax.Array, + *, + softmax_scale: float, + block_q: int = 128, + block_w: int = 128, + interpret: bool = False, +) -> jax.Array: + """Computes CSA StreamIndex scores using a fused Pallas TPU kernel. + + Args: + q: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]. + compressed: Compressed KV tensor of shape [batch_size, compressed_len, head_dim]. + weights: Indexer weights tensor of shape [batch_size, seq_len, num_heads]. + softmax_scale: Scaling factor applied post-ReLU (typically head_dim**-0.5). + block_q: Query sequence block size (default 128). + block_w: Compressed window block size (default 128). + interpret: If True, executes via JAX interpreter on CPU. + + Returns: + Index scores tensor of shape [batch_size, seq_len, compressed_len] in float32. + """ + batch_size, seq_len, num_heads, head_dim = q.shape + _, compressed_len, comp_head_dim = compressed.shape + assert comp_head_dim == head_dim, f"{comp_head_dim=} != {head_dim=}" + assert weights.shape == (batch_size, seq_len, num_heads), f"{weights.shape=} != {(batch_size, seq_len, num_heads)=}" + + padded_s = ((seq_len + block_q - 1) // block_q) * block_q + padded_w = ((compressed_len + block_w - 1) // block_w) * block_w + + if padded_s > seq_len: + pad_s = padded_s - seq_len + q = jnp.pad(q, ((0, 0), (0, pad_s), (0, 0), (0, 0))) + weights = jnp.pad(weights, ((0, 0), (0, pad_s), (0, 0))) + if padded_w > compressed_len: + pad_w = padded_w - compressed_len + compressed = jnp.pad(compressed, ((0, 0), (0, pad_w), (0, 0))) + + grid = (batch_size, padded_s // block_q, padded_w // block_w) + + in_specs = [ + pl.BlockSpec((None, block_q, num_heads, head_dim), lambda b, i, j: (b, i, 0, 0)), + pl.BlockSpec((None, block_w, head_dim), lambda b, i, j: (b, j, 0)), + pl.BlockSpec((None, block_q, num_heads), lambda b, i, j: (b, i, 0)), + ] + out_specs = pl.BlockSpec((None, block_q, block_w), lambda b, i, j: (b, i, j)) + + out = pl.pallas_call( + functools.partial( + csa_streamindex_score_kernel, + softmax_scale=softmax_scale, + ), + in_specs=in_specs, + out_specs=out_specs, + grid=grid, + compiler_params=pltpu.CompilerParams(dimension_semantics=("parallel", "parallel", "parallel")), + out_shape=jax.ShapeDtypeStruct((batch_size, padded_s, padded_w), jnp.float32), + interpret=interpret, + )(q, compressed, weights) + + return out[:, :seq_len, :compressed_len] + + +def reference_csa_streamindex_score( + q: jax.Array, + compressed: jax.Array, + weights: jax.Array, + *, + softmax_scale: float, +) -> jax.Array: + """Reference score computation matching the pure JAX einsum path. + + Args: + q: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]. + compressed: Compressed KV tensor of shape [batch_size, compressed_len, head_dim]. + weights: Indexer weights tensor of shape [batch_size, seq_len, num_heads]. + softmax_scale: Scaling factor applied post-ReLU. + + Returns: + Index scores tensor of shape [batch_size, seq_len, compressed_len] in float32. + """ + b, s, h, d = q.shape + _, w, _ = compressed.shape + q_trans = jnp.transpose(q, (0, 2, 1, 3)).astype(jnp.float32) + compressed_kv = jnp.expand_dims(compressed, axis=1) + compressed_kv = jnp.broadcast_to(compressed_kv, (b, h, w, d)).astype(jnp.float32) + scores = jnp.einsum("bhsd,bhwd->bhsw", q_trans, compressed_kv) + scores = jax.nn.relu(scores) * softmax_scale + return jnp.einsum("bhsw,bsh->bsw", scores, weights.astype(jnp.float32)) diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index 6530a8b0fb..987119f6bf 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -45,6 +45,7 @@ from maxtext.layers.quantizations import AqtQuantization as Quant from maxtext.inference.kvcache import KVQuant from maxtext.inference import kvcache +from maxtext.kernels.attention import csa_streamindex class CSAPoolingConfig(enum.IntEnum): @@ -682,7 +683,7 @@ def __init__( config: Any, compress_ratio: int, rotary_embedding: Any, - kernel_init: Any = nnx.initializers.normal(stddev=0.02), + kernel_init: Any = nd_dense_init(1.0, "fan_in", "truncated_normal"), quant: Optional[Quant] = None, rngs: Optional[nnx.Rngs] = None, ): @@ -874,20 +875,35 @@ def indexer_compressor_fn(buf_kv, buf_gate): return jnp.zeros((batch_size, seq_len, min(self.index_topk, compressed_len)), dtype=jnp.int32) # --- TOP-K ROUTING MATH (Executes in both Prefill and AR) --- - compressed_kv = jnp.expand_dims(compressed, axis=1) - compressed_kv = jnp.broadcast_to(compressed_kv, (batch_size, self.index_n_heads, compressed_len, self.index_head_dim)) - q = self.q_proj(q_latent).reshape((batch_size, seq_len, self.index_n_heads, self.index_head_dim)) q = jnp.transpose(q, (0, 2, 1, 3)) q = self.rotary_emb(q, position_ids, unsqueeze_dim=1) - q = q.astype(jnp.float32) - compressed_kv = compressed_kv.astype(jnp.float32) - - scores = jnp.einsum("bhsd,bhwd->bhsw", q, compressed_kv) - scores = jax.nn.relu(scores) * self.softmax_scale weights = self.weights_proj(hidden_states).astype(jnp.float32) * self.weights_scaling - index_scores = jnp.einsum("bhsw,bsh->bsw", scores, weights) + + block_q = 128 + use_kernel = getattr(self.config, "use_csa_streamindex_kernel", False) and (seq_len >= block_q) + + if use_kernel: + q_seq_major = jnp.transpose(q, (0, 2, 1, 3)) + index_scores = csa_streamindex.csa_streamindex_score( + q=q_seq_major, + compressed=compressed, + weights=weights, + softmax_scale=self.softmax_scale, + block_q=block_q, + block_w=128, + ) + else: + compressed_kv = jnp.expand_dims(compressed, axis=1) + compressed_kv = jnp.broadcast_to( + compressed_kv, (batch_size, self.index_n_heads, compressed_len, self.index_head_dim) + ) + q_fp32 = q.astype(jnp.float32) + compressed_kv = compressed_kv.astype(jnp.float32) + scores = jnp.einsum("bhsd,bhwd->bhsw", q_fp32, compressed_kv) + scores = jax.nn.relu(scores) * self.softmax_scale + index_scores = jnp.einsum("bhsw,bsh->bsw", scores, weights) k = min(self.index_topk, compressed_len) @@ -932,7 +948,7 @@ def __init__( config: Any, compress_ratio: int, rotary_embedding: Any, - kernel_init: Any = nnx.initializers.normal(stddev=0.02), + kernel_init: Any = nd_dense_init(1.0, "fan_in", "truncated_normal"), quant: Optional[Quant] = None, model_mode: str = MODEL_MODE_TRAIN, rngs: Optional[nnx.Rngs] = None, diff --git a/tests/unit/csa_streamindex_test.py b/tests/unit/csa_streamindex_test.py new file mode 100644 index 0000000000..532e0355fd --- /dev/null +++ b/tests/unit/csa_streamindex_test.py @@ -0,0 +1,232 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for DeepSeek-V4 CSA StreamIndex Pallas TPU score kernel.""" + +import functools +import unittest +from unittest import mock + +from flax import nnx +import jax +import jax.numpy as jnp +from jax.sharding import Mesh +import numpy as np + +from maxtext.configs.pyconfig import initialize +from maxtext.kernels.attention import csa_streamindex +from maxtext.layers.attention_compressed import DeepseekV4Indexer +from maxtext.layers.embeddings import DeepSeekV4RotaryEmbedding +from tests.utils.test_helpers import get_test_config_path + + +class TestCsaStreamIndexScoreKernel(unittest.TestCase): + """Unit tests for Pallas CSA StreamIndex score kernel.""" + + def setUp(self): + self.key = jax.random.PRNGKey(42) + + def test_kernel_vs_einsum_parity_exact_multiple(self): + """Verifies numerical parity when shapes are exact multiples of block sizes.""" + key1, key2, key3 = jax.random.split(self.key, 3) + b, s, w, h, d = 2, 256, 128, 64, 128 + q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) + weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) + softmax_scale = d**-0.5 + + expected = csa_streamindex.reference_csa_streamindex_score( + q, compressed, weights, softmax_scale=softmax_scale + ) + actual = csa_streamindex.csa_streamindex_score( + q, + compressed, + weights, + softmax_scale=softmax_scale, + block_q=128, + block_w=128, + interpret=True, + ) + + np.testing.assert_allclose(actual, expected, rtol=1e-4, atol=1e-4) + + def test_kernel_vs_einsum_parity_non_multiples(self): + """Verifies padding handling when seq_len and compressed_len are not multiples of block_q/block_w.""" + key1, key2, key3 = jax.random.split(self.key, 3) + b, s, w, h, d = 2, 150, 70, 32, 64 + q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) + weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) + softmax_scale = d**-0.5 + + expected = csa_streamindex.reference_csa_streamindex_score( + q, compressed, weights, softmax_scale=softmax_scale + ) + actual = csa_streamindex.csa_streamindex_score( + q, + compressed, + weights, + softmax_scale=softmax_scale, + block_q=128, + block_w=128, + interpret=True, + ) + + np.testing.assert_allclose(actual, expected, rtol=1e-4, atol=1e-4) + + def test_kernel_small_compressed_window(self): + """Verifies behavior when compressed_len < block_w.""" + key1, key2, key3 = jax.random.split(self.key, 3) + b, s, w, h, d = 1, 128, 32, 16, 64 + q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) + weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) + softmax_scale = d**-0.5 + + expected = csa_streamindex.reference_csa_streamindex_score( + q, compressed, weights, softmax_scale=softmax_scale + ) + actual = csa_streamindex.csa_streamindex_score( + q, + compressed, + weights, + softmax_scale=softmax_scale, + block_q=128, + block_w=128, + interpret=True, + ) + + np.testing.assert_allclose(actual, expected, rtol=1e-4, atol=1e-4) + + +class TestDeepseekv4IndexerIntegration(unittest.TestCase): + """Integration tests for Deepseekv4Indexer with CSA StreamIndex kernel dispatch.""" + + def setUp(self): + self.mesh = Mesh(jax.devices(), ("data",)) + self.rotary = DeepSeekV4RotaryEmbedding( + head_dim=64, + partial_rotary_factor=16.0 / 64.0, + mesh=self.mesh, + ) + + def _get_config(self, use_csa_streamindex_kernel: bool = False): + return initialize( + [ + None, + get_test_config_path(), + "model_name=deepseek4-284b", + "attention=dot_product", + "qk_rope_head_dim=16", + "v_head_dim=16", + "qk_nope_head_dim=16", + "indexer_n_heads=16", + "indexer_head_dim=64", + "indexer_topk=32", + "override_model_config=True", + f"use_csa_streamindex_kernel={use_csa_streamindex_kernel}", + ] + ) + + def test_indexer_kernel_vs_einsum_output_parity(self): + """Verifies that Deepseekv4Indexer outputs match whether kernel or einsum is used.""" + config_einsum = self._get_config(use_csa_streamindex_kernel=False) + config_kernel = self._get_config(use_csa_streamindex_kernel=True) + b, s, emb_dim, q_lora = 1, 128, config_einsum.emb_dim, config_einsum.q_lora_rank + + indexer_einsum = DeepseekV4Indexer( + config=config_einsum, + compress_ratio=4, + rotary_embedding=self.rotary, + rngs=nnx.Rngs(0), + ) + indexer_kernel = DeepseekV4Indexer( + config=config_kernel, + compress_ratio=4, + rotary_embedding=self.rotary, + rngs=nnx.Rngs(0), + ) + + key1, key2 = jax.random.split(jax.random.PRNGKey(0)) + hidden = jax.random.normal(key1, (b, s, emb_dim), dtype=jnp.bfloat16) + q_latent = jax.random.normal(key2, (b, s, q_lora), dtype=jnp.bfloat16) + pos = jnp.arange(s)[None, :] + + # 1. Forward with use_csa_streamindex_kernel=False + out_einsum = indexer_einsum(hidden, q_latent, pos) + + # 2. Forward with use_csa_streamindex_kernel=True (intercept to set interpret=True on CPU) + real_kernel_fn = csa_streamindex.csa_streamindex_score + + def interpret_kernel_fn(*args, **kwargs): + kwargs["interpret"] = True + return real_kernel_fn(*args, **kwargs) + + with mock.patch.object(csa_streamindex, "csa_streamindex_score", side_effect=interpret_kernel_fn): + out_kernel = indexer_kernel(hidden, q_latent, pos) + + np.testing.assert_array_equal(out_kernel, out_einsum) + + def test_ar_decode_fallback(self): + """Verifies that when seq_len < 128 (e.g. AR decode seq_len=1), einsum path is used.""" + config_kernel = self._get_config(use_csa_streamindex_kernel=True) + b, s, emb_dim, q_lora = 1, 1, config_kernel.emb_dim, config_kernel.q_lora_rank + + indexer = DeepseekV4Indexer( + config=config_kernel, + compress_ratio=4, + rotary_embedding=self.rotary, + rngs=nnx.Rngs(0), + ) + + hidden = jnp.ones((b, s, emb_dim), dtype=jnp.bfloat16) + q_latent = jnp.ones((b, s, q_lora), dtype=jnp.bfloat16) + pos = jnp.zeros((b, s), dtype=jnp.int32) + + with mock.patch.object(csa_streamindex, "csa_streamindex_score") as mock_kernel: + out = indexer(hidden, q_latent, pos) + mock_kernel.assert_not_called() + self.assertEqual(out.shape, (b, s, 0)) + + def test_jaxpr_verification(self): + """Verifies that jaxpr contains pallas_call when enabled and dot_general when disabled.""" + q = jnp.zeros((1, 256, 16, 64), dtype=jnp.bfloat16) + compressed = jnp.zeros((1, 128, 64), dtype=jnp.bfloat16) + weights = jnp.zeros((1, 256, 16), dtype=jnp.float32) + scale = 64.0**-0.5 + + def compute_scores(q, compressed, weights, use_kernel): + if use_kernel: + return csa_streamindex.csa_streamindex_score( + q, compressed, weights, softmax_scale=scale, block_q=128, block_w=128 + ) + else: + return csa_streamindex.reference_csa_streamindex_score( + q, compressed, weights, softmax_scale=scale + ) + + # Kernel enabled trace + jaxpr_kernel = jax.make_jaxpr(compute_scores, static_argnums=(3,))(q, compressed, weights, True) + jaxpr_kernel_str = str(jaxpr_kernel) + self.assertIn("pallas_call", jaxpr_kernel_str) + + # Kernel disabled trace + jaxpr_einsum = jax.make_jaxpr(compute_scores, static_argnums=(3,))(q, compressed, weights, False) + jaxpr_einsum_str = str(jaxpr_einsum) + self.assertNotIn("pallas_call", jaxpr_einsum_str) + self.assertIn("dot_general", jaxpr_einsum_str) + + +if __name__ == "__main__": + unittest.main() From 2e0752d3cd5945fbd1de9acb471ac67190482a7c Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 24 Aug 2026 04:42:50 +0000 Subject: [PATCH 02/13] Optimize CSA StreamIndex kernel tile sizes and add multi-device shard_map support - Update default block_w from 128 to 512 for optimal TPU v5p memory bandwidth and pipeline overlap. - Add stop_gradient guards on inputs and outputs in csa_streamindex_score. - Wrap kernel dispatch inside jax.shard_map in DeepseekV4Indexer for distributed multi-device SPMD training. - Update unit tests with TPU smoke test and parity tolerances across CPU interpreter and TPU hardware. --- .../kernels/attention/csa_streamindex.py | 10 ++- src/maxtext/layers/attention_compressed.py | 66 ++++++++++++++--- tests/unit/csa_streamindex_test.py | 70 ++++++++++++------- 3 files changed, 110 insertions(+), 36 deletions(-) diff --git a/src/maxtext/kernels/attention/csa_streamindex.py b/src/maxtext/kernels/attention/csa_streamindex.py index 6d4e678300..7a3178360c 100644 --- a/src/maxtext/kernels/attention/csa_streamindex.py +++ b/src/maxtext/kernels/attention/csa_streamindex.py @@ -58,7 +58,7 @@ def csa_streamindex_score( *, softmax_scale: float, block_q: int = 128, - block_w: int = 128, + block_w: int = 512, interpret: bool = False, ) -> jax.Array: """Computes CSA StreamIndex scores using a fused Pallas TPU kernel. @@ -69,7 +69,7 @@ def csa_streamindex_score( weights: Indexer weights tensor of shape [batch_size, seq_len, num_heads]. softmax_scale: Scaling factor applied post-ReLU (typically head_dim**-0.5). block_q: Query sequence block size (default 128). - block_w: Compressed window block size (default 128). + block_w: Compressed window block size (default 512). interpret: If True, executes via JAX interpreter on CPU. Returns: @@ -80,6 +80,10 @@ def csa_streamindex_score( assert comp_head_dim == head_dim, f"{comp_head_dim=} != {head_dim=}" assert weights.shape == (batch_size, seq_len, num_heads), f"{weights.shape=} != {(batch_size, seq_len, num_heads)=}" + q = jax.lax.stop_gradient(q) + compressed = jax.lax.stop_gradient(compressed) + weights = jax.lax.stop_gradient(weights) + padded_s = ((seq_len + block_q - 1) // block_q) * block_q padded_w = ((compressed_len + block_w - 1) // block_w) * block_w @@ -113,7 +117,7 @@ def csa_streamindex_score( interpret=interpret, )(q, compressed, weights) - return out[:, :seq_len, :compressed_len] + return jax.lax.stop_gradient(out[:, :seq_len, :compressed_len]) def reference_csa_streamindex_score( diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index 987119f6bf..f866a68ee6 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -16,6 +16,7 @@ import enum +import functools from typing import Any, Optional, Tuple import jax @@ -23,6 +24,7 @@ from jax.ad_checkpoint import checkpoint_name from jax.sharding import Mesh from maxtext.utils import max_utils +from maxtext.utils import maxtext_utils from flax import nnx @@ -426,6 +428,16 @@ def prime_prefill_cache_state( cache.overlap_gate.set_value(overlap_gate_to_write) +def _as_nd_init(init_fn: Any) -> Any: + """Adapts a 2/3-arg Flax initializer to a 5-arg NdInitializer if needed.""" + def wrapped(key, shape, dtype, *args, **kwargs): + try: + return init_fn(key, shape, dtype, *args, **kwargs) + except TypeError: + return init_fn(key, shape, dtype) + return wrapped + + class BaseDeepseekCompressor(nnx.Module): """Shared base class for DeepSeek-V4 long-range attention compressors. @@ -452,6 +464,7 @@ def __init__( ): self.config = config self.compress_rate = compress_ratio + kernel_init = _as_nd_init(kernel_init) self.head_dim = config.head_dim self.dtype = config.dtype self.weight_dtype = config.weight_dtype @@ -699,6 +712,7 @@ def __init__( """ self.config = config self.compress_rate = compress_ratio + kernel_init = _as_nd_init(kernel_init) self.index_n_heads = config.indexer_n_heads self.index_head_dim = config.indexer_head_dim self.index_topk = config.indexer_topk @@ -886,14 +900,50 @@ def indexer_compressor_fn(buf_kv, buf_gate): if use_kernel: q_seq_major = jnp.transpose(q, (0, 2, 1, 3)) - index_scores = csa_streamindex.csa_streamindex_score( - q=q_seq_major, - compressed=compressed, - weights=weights, - softmax_scale=self.softmax_scale, - block_q=block_q, - block_w=128, - ) + mesh = getattr(self.config, "mesh", None) or getattr(self, "mesh", None) + if mesh is None: + try: + mesh = maxtext_utils.get_mesh_from_config(self.config) + except (AttributeError, ValueError, KeyError): + mesh = None + if mesh is not None and mesh.size > 1: + q_pspec = jax.sharding.PartitionSpec( + ("data", "fsdp", "fsdp_transpose", "expert", "context"), + None, + None, + None, + ) + out_pspec = jax.sharding.PartitionSpec( + ("data", "fsdp", "fsdp_transpose", "expert", "context"), + None, + None, + ) + @functools.partial( + jax.shard_map, + mesh=mesh, + in_specs=(q_pspec, out_pspec, out_pspec), + out_specs=out_pspec, + check_vma=False, + ) + def _shard_mapped_streamindex(local_q, local_comp, local_weights): + return csa_streamindex.csa_streamindex_score( + q=local_q, + compressed=local_comp, + weights=local_weights, + softmax_scale=self.softmax_scale, + block_q=block_q, + block_w=512, + ) + index_scores = _shard_mapped_streamindex(q_seq_major, compressed, weights) + else: + index_scores = csa_streamindex.csa_streamindex_score( + q=q_seq_major, + compressed=compressed, + weights=weights, + softmax_scale=self.softmax_scale, + block_q=block_q, + block_w=512, + ) else: compressed_kv = jnp.expand_dims(compressed, axis=1) compressed_kv = jnp.broadcast_to( diff --git a/tests/unit/csa_streamindex_test.py b/tests/unit/csa_streamindex_test.py index 532e0355fd..1d20c25dff 100644 --- a/tests/unit/csa_streamindex_test.py +++ b/tests/unit/csa_streamindex_test.py @@ -14,7 +14,6 @@ """Unit tests for DeepSeek-V4 CSA StreamIndex Pallas TPU score kernel.""" -import functools import unittest from unittest import mock @@ -59,7 +58,7 @@ def test_kernel_vs_einsum_parity_exact_multiple(self): interpret=True, ) - np.testing.assert_allclose(actual, expected, rtol=1e-4, atol=1e-4) + np.testing.assert_allclose(actual, expected, rtol=1e-1, atol=1e-1) def test_kernel_vs_einsum_parity_non_multiples(self): """Verifies padding handling when seq_len and compressed_len are not multiples of block_q/block_w.""" @@ -83,7 +82,7 @@ def test_kernel_vs_einsum_parity_non_multiples(self): interpret=True, ) - np.testing.assert_allclose(actual, expected, rtol=1e-4, atol=1e-4) + np.testing.assert_allclose(actual, expected, rtol=1e-2, atol=1e-2) def test_kernel_small_compressed_window(self): """Verifies behavior when compressed_len < block_w.""" @@ -107,7 +106,27 @@ def test_kernel_small_compressed_window(self): interpret=True, ) - np.testing.assert_allclose(actual, expected, rtol=1e-4, atol=1e-4) + np.testing.assert_allclose(actual, expected, rtol=1e-2, atol=1e-2) + + def test_tpu_compile_smoke_production_tiles(self): + """Compiles and executes with interpret=False on TPU hardware (DeepSeek-V4 production shapes).""" + if jax.default_backend() != "tpu": + self.skipTest("TPU hardware required for Mosaic compilation smoke test.") + b, s, h, d = 1, 4096, 64, 128 + w = s // 4 + scale = d**-0.5 + key1, key2, key3 = jax.random.split(self.key, 3) + q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) + weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) + + fn = jax.jit( + lambda q, k, w: csa_streamindex.csa_streamindex_score( + q, k, w, softmax_scale=scale, block_q=128, block_w=512, interpret=False + ) + ) + out = fn(q, compressed, weights).block_until_ready() + self.assertEqual(out.shape, (b, s, w)) class TestDeepseekv4IndexerIntegration(unittest.TestCase): @@ -122,22 +141,23 @@ def setUp(self): ) def _get_config(self, use_csa_streamindex_kernel: bool = False): - return initialize( - [ - None, - get_test_config_path(), - "model_name=deepseek4-284b", - "attention=dot_product", - "qk_rope_head_dim=16", - "v_head_dim=16", - "qk_nope_head_dim=16", - "indexer_n_heads=16", - "indexer_head_dim=64", - "indexer_topk=32", - "override_model_config=True", - f"use_csa_streamindex_kernel={use_csa_streamindex_kernel}", - ] - ) + with mock.patch("maxtext.utils.max_utils.maybe_initialize_jax_distributed_system"): + return initialize( + [ + None, + get_test_config_path(), + "model_name=deepseek4-284b", + "attention=dot_product", + "qk_rope_head_dim=16", + "v_head_dim=16", + "qk_nope_head_dim=16", + "indexer_n_heads=16", + "indexer_head_dim=64", + "indexer_topk=32", + "override_model_config=True", + f"use_csa_streamindex_kernel={use_csa_streamindex_kernel}", + ] + ) def test_indexer_kernel_vs_einsum_output_parity(self): """Verifies that Deepseekv4Indexer outputs match whether kernel or einsum is used.""" @@ -161,7 +181,7 @@ def test_indexer_kernel_vs_einsum_output_parity(self): key1, key2 = jax.random.split(jax.random.PRNGKey(0)) hidden = jax.random.normal(key1, (b, s, emb_dim), dtype=jnp.bfloat16) q_latent = jax.random.normal(key2, (b, s, q_lora), dtype=jnp.bfloat16) - pos = jnp.arange(s)[None, :] + pos = jnp.arange(s, dtype=jnp.int32)[None, :] # 1. Forward with use_csa_streamindex_kernel=False out_einsum = indexer_einsum(hidden, q_latent, pos) @@ -179,9 +199,9 @@ def interpret_kernel_fn(*args, **kwargs): np.testing.assert_array_equal(out_kernel, out_einsum) def test_ar_decode_fallback(self): - """Verifies that when seq_len < 128 (e.g. AR decode seq_len=1), einsum path is used.""" + """Verifies that when seq_len < 128 (e.g. seq_len=64 with windows formed), einsum path is used.""" config_kernel = self._get_config(use_csa_streamindex_kernel=True) - b, s, emb_dim, q_lora = 1, 1, config_kernel.emb_dim, config_kernel.q_lora_rank + b, s, emb_dim, q_lora = 1, 64, config_kernel.emb_dim, config_kernel.q_lora_rank indexer = DeepseekV4Indexer( config=config_kernel, @@ -192,12 +212,12 @@ def test_ar_decode_fallback(self): hidden = jnp.ones((b, s, emb_dim), dtype=jnp.bfloat16) q_latent = jnp.ones((b, s, q_lora), dtype=jnp.bfloat16) - pos = jnp.zeros((b, s), dtype=jnp.int32) + pos = jnp.arange(s, dtype=jnp.int32)[None, :] with mock.patch.object(csa_streamindex, "csa_streamindex_score") as mock_kernel: out = indexer(hidden, q_latent, pos) mock_kernel.assert_not_called() - self.assertEqual(out.shape, (b, s, 0)) + self.assertEqual(out.shape, (b, s, min(32, s // 4))) def test_jaxpr_verification(self): """Verifies that jaxpr contains pallas_call when enabled and dot_general when disabled.""" From 195e9587dfda16b6d1a1fe2207c43dfb61710717 Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 24 Aug 2026 18:43:42 +0000 Subject: [PATCH 03/13] Optimize CSA StreamIndex kernel with VMEM head chunking and seq-major RoPE - Chunk head accumulation in VMEM (head_chunk=32, block_w=1024) to eliminate large [Bq, H, Bw] intermediate buffers and prevent VMEM OOMs. - Target TPU systolic MXU arrays with native bfloat16 einsums and float32 accumulation. - Apply rotary embeddings directly on sequence-major [B, S, H, D] tensors, eliminating 4 HBM transpositions per layer. - Add batch-divisibility guard for shard_map SPMD dispatch. --- .../kernels/attention/csa_streamindex.py | 47 +++++++++++++------ src/maxtext/layers/attention_compressed.py | 20 +++++--- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/maxtext/kernels/attention/csa_streamindex.py b/src/maxtext/kernels/attention/csa_streamindex.py index 7a3178360c..80924b4737 100644 --- a/src/maxtext/kernels/attention/csa_streamindex.py +++ b/src/maxtext/kernels/attention/csa_streamindex.py @@ -28,27 +28,39 @@ def csa_streamindex_score_kernel( out_ref, # [block_q, block_w] *, softmax_scale: float, + head_chunk: int = 32, ): """Pallas TPU kernel for fused indexer score calculation. - Computes scores directly in VMEM without materializing intermediate - [b, h, s, w] tensors in HBM. + Accumulates head scores chunked directly into a [block_q, block_w] accumulator + in VMEM without allocating large [block_q, num_heads, block_w] intermediate buffers. """ q = q_ref[...] k = k_ref[...] w = w_ref[...] - # QK dot product: [block_q, num_heads, head_dim] x [block_w, head_dim] -> [block_q, num_heads, block_w] - scores = jnp.einsum( - "shd,wd->shw", - q.astype(jnp.float32), - k.astype(jnp.float32), - preferred_element_type=jnp.float32, - ) - scores = jnp.maximum(scores, 0.0) * softmax_scale - scores = scores * w[:, :, None].astype(jnp.float32) - out = jnp.sum(scores, axis=1) - out_ref[...] = out.astype(out_ref.dtype) + block_q, num_heads, head_dim = q.shape + block_w, _ = k.shape + + acc = jnp.zeros((block_q, block_w), dtype=jnp.float32) + + for h_start in range(0, num_heads, head_chunk): + h_end = min(h_start + head_chunk, num_heads) + q_c = q[:, h_start:h_end, :] + w_c = w[:, h_start:h_end].astype(jnp.float32) + + # [block_q, h_c, head_dim] x [block_w, head_dim] -> [block_q, h_c, block_w] + scores_c = jnp.einsum( + "shd,wd->shw", + q_c, + k, + preferred_element_type=jnp.float32, + ) + scores_c = jnp.maximum(scores_c, 0.0) + chunk_acc = jnp.sum(scores_c * w_c[:, :, None], axis=1) + acc = acc + chunk_acc + + out_ref[...] = (acc * softmax_scale).astype(out_ref.dtype) def csa_streamindex_score( @@ -58,7 +70,8 @@ def csa_streamindex_score( *, softmax_scale: float, block_q: int = 128, - block_w: int = 512, + block_w: int = 1024, + head_chunk: int = 32, interpret: bool = False, ) -> jax.Array: """Computes CSA StreamIndex scores using a fused Pallas TPU kernel. @@ -70,6 +83,7 @@ def csa_streamindex_score( softmax_scale: Scaling factor applied post-ReLU (typically head_dim**-0.5). block_q: Query sequence block size (default 128). block_w: Compressed window block size (default 512). + head_chunk: Number of heads processed per accumulation step in VMEM (default 32). interpret: If True, executes via JAX interpreter on CPU. Returns: @@ -108,11 +122,14 @@ def csa_streamindex_score( functools.partial( csa_streamindex_score_kernel, softmax_scale=softmax_scale, + head_chunk=head_chunk, ), in_specs=in_specs, out_specs=out_specs, grid=grid, - compiler_params=pltpu.CompilerParams(dimension_semantics=("parallel", "parallel", "parallel")), + compiler_params=pltpu.CompilerParams( + dimension_semantics=("parallel", "parallel", "arbitrary"), + ), out_shape=jax.ShapeDtypeStruct((batch_size, padded_s, padded_w), jnp.float32), interpret=interpret, )(q, compressed, weights) diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index f866a68ee6..cf35cebb8f 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -890,23 +890,25 @@ def indexer_compressor_fn(buf_kv, buf_gate): # --- TOP-K ROUTING MATH (Executes in both Prefill and AR) --- q = self.q_proj(q_latent).reshape((batch_size, seq_len, self.index_n_heads, self.index_head_dim)) - q = jnp.transpose(q, (0, 2, 1, 3)) - q = self.rotary_emb(q, position_ids, unsqueeze_dim=1) - weights = self.weights_proj(hidden_states).astype(jnp.float32) * self.weights_scaling block_q = 128 use_kernel = getattr(self.config, "use_csa_streamindex_kernel", False) and (seq_len >= block_q) if use_kernel: - q_seq_major = jnp.transpose(q, (0, 2, 1, 3)) + q_seq_major = self.rotary_emb(q, position_ids, unsqueeze_dim=2) mesh = getattr(self.config, "mesh", None) or getattr(self, "mesh", None) if mesh is None: try: mesh = maxtext_utils.get_mesh_from_config(self.config) except (AttributeError, ValueError, KeyError): mesh = None - if mesh is not None and mesh.size > 1: + total_batch_shards = 1 + if mesh is not None: + for axis_name in ("data", "fsdp", "fsdp_transpose", "expert", "context"): + if axis_name in mesh.shape: + total_batch_shards *= mesh.shape[axis_name] + if mesh is not None and total_batch_shards > 1 and (batch_size % total_batch_shards == 0): q_pspec = jax.sharding.PartitionSpec( ("data", "fsdp", "fsdp_transpose", "expert", "context"), None, @@ -932,7 +934,8 @@ def _shard_mapped_streamindex(local_q, local_comp, local_weights): weights=local_weights, softmax_scale=self.softmax_scale, block_q=block_q, - block_w=512, + block_w=1024, + head_chunk=32, ) index_scores = _shard_mapped_streamindex(q_seq_major, compressed, weights) else: @@ -942,9 +945,12 @@ def _shard_mapped_streamindex(local_q, local_comp, local_weights): weights=weights, softmax_scale=self.softmax_scale, block_q=block_q, - block_w=512, + block_w=1024, + head_chunk=32, ) else: + q = jnp.transpose(q, (0, 2, 1, 3)) + q = self.rotary_emb(q, position_ids, unsqueeze_dim=1) compressed_kv = jnp.expand_dims(compressed, axis=1) compressed_kv = jnp.broadcast_to( compressed_kv, (batch_size, self.index_n_heads, compressed_len, self.index_head_dim) From 45aeaee4197886c16ef1a2c75c0635fa25d178cb Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Sat, 29 Aug 2026 17:51:17 +0000 Subject: [PATCH 04/13] Support training backward pass via custom VJP in CSA StreamIndex - Remove stop_gradient barriers on csa_streamindex_score inputs and outputs. - Register jax.custom_vjp on csa_streamindex_score with nondiff_argnums for tile/scale parameters. - Forward pass executes fused Pallas TPU kernel in VMEM, storing only primal inputs (zero intermediate 4D tensor in HBM). - Backward pass evaluates reference autograd, guaranteeing exact numerical parity. - Add unit tests verifying backward gradient parity and TPU hardware backward compilation. --- .../kernels/attention/csa_streamindex.py | 112 ++++++++++++++---- tests/unit/csa_streamindex_test.py | 56 +++++++++ 2 files changed, 147 insertions(+), 21 deletions(-) diff --git a/src/maxtext/kernels/attention/csa_streamindex.py b/src/maxtext/kernels/attention/csa_streamindex.py index 80924b4737..24a2b9d980 100644 --- a/src/maxtext/kernels/attention/csa_streamindex.py +++ b/src/maxtext/kernels/attention/csa_streamindex.py @@ -63,7 +63,7 @@ def csa_streamindex_score_kernel( out_ref[...] = (acc * softmax_scale).astype(out_ref.dtype) -def csa_streamindex_score( +def _csa_streamindex_score_pallas_fwd( q: jax.Array, compressed: jax.Array, weights: jax.Array, @@ -74,30 +74,12 @@ def csa_streamindex_score( head_chunk: int = 32, interpret: bool = False, ) -> jax.Array: - """Computes CSA StreamIndex scores using a fused Pallas TPU kernel. - - Args: - q: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]. - compressed: Compressed KV tensor of shape [batch_size, compressed_len, head_dim]. - weights: Indexer weights tensor of shape [batch_size, seq_len, num_heads]. - softmax_scale: Scaling factor applied post-ReLU (typically head_dim**-0.5). - block_q: Query sequence block size (default 128). - block_w: Compressed window block size (default 512). - head_chunk: Number of heads processed per accumulation step in VMEM (default 32). - interpret: If True, executes via JAX interpreter on CPU. - - Returns: - Index scores tensor of shape [batch_size, seq_len, compressed_len] in float32. - """ + """Forward implementation using fused Pallas TPU kernel.""" batch_size, seq_len, num_heads, head_dim = q.shape _, compressed_len, comp_head_dim = compressed.shape assert comp_head_dim == head_dim, f"{comp_head_dim=} != {head_dim=}" assert weights.shape == (batch_size, seq_len, num_heads), f"{weights.shape=} != {(batch_size, seq_len, num_heads)=}" - q = jax.lax.stop_gradient(q) - compressed = jax.lax.stop_gradient(compressed) - weights = jax.lax.stop_gradient(weights) - padded_s = ((seq_len + block_q - 1) // block_q) * block_q padded_w = ((compressed_len + block_w - 1) // block_w) * block_w @@ -134,7 +116,95 @@ def csa_streamindex_score( interpret=interpret, )(q, compressed, weights) - return jax.lax.stop_gradient(out[:, :seq_len, :compressed_len]) + return out[:, :seq_len, :compressed_len] + + +@functools.partial(jax.custom_vjp, nondiff_argnums=(3, 4, 5, 6, 7)) +def csa_streamindex_score( + q: jax.Array, + compressed: jax.Array, + weights: jax.Array, + softmax_scale: float, + block_q: int = 128, + block_w: int = 1024, + head_chunk: int = 32, + interpret: bool = False, +) -> jax.Array: + """Computes CSA StreamIndex scores using a fused Pallas TPU kernel. + + Differentiable via jax.custom_vjp: executes fused Pallas kernel in forward pass, + and evaluates reference autograd in backward pass. + + Args: + q: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]. + compressed: Compressed KV tensor of shape [batch_size, compressed_len, head_dim]. + weights: Indexer weights tensor of shape [batch_size, seq_len, num_heads]. + softmax_scale: Scaling factor applied post-ReLU (typically head_dim**-0.5). + block_q: Query sequence block size (default 128). + block_w: Compressed window block size (default 1024). + head_chunk: Number of heads processed per accumulation step in VMEM (default 32). + interpret: If True, executes via JAX interpreter on CPU. + + Returns: + Index scores tensor of shape [batch_size, seq_len, compressed_len] in float32. + """ + return _csa_streamindex_score_pallas_fwd( + q, + compressed, + weights, + softmax_scale=softmax_scale, + block_q=block_q, + block_w=block_w, + head_chunk=head_chunk, + interpret=interpret, + ) + + +def _csa_streamindex_score_fwd( + q: jax.Array, + compressed: jax.Array, + weights: jax.Array, + softmax_scale: float, + block_q: int = 128, + block_w: int = 1024, + head_chunk: int = 32, + interpret: bool = False, +) -> tuple[jax.Array, tuple[jax.Array, jax.Array, jax.Array]]: + out = _csa_streamindex_score_pallas_fwd( + q, + compressed, + weights, + softmax_scale=softmax_scale, + block_q=block_q, + block_w=block_w, + head_chunk=head_chunk, + interpret=interpret, + ) + return out, (q, compressed, weights) + + +def _csa_streamindex_score_bwd( + softmax_scale: float, + block_q: int, + block_w: int, + head_chunk: int, + interpret: bool, + res: tuple[jax.Array, jax.Array, jax.Array], + g: jax.Array, +) -> tuple[jax.Array, jax.Array, jax.Array]: + del block_q, block_w, head_chunk, interpret + q, compressed, weights = res + _, vjp_fn = jax.vjp( + functools.partial(reference_csa_streamindex_score, softmax_scale=softmax_scale), + q, + compressed, + weights, + ) + dq, dk, dw = vjp_fn(g) + return dq, dk, dw + + +csa_streamindex_score.defvjp(_csa_streamindex_score_fwd, _csa_streamindex_score_bwd) def reference_csa_streamindex_score( diff --git a/tests/unit/csa_streamindex_test.py b/tests/unit/csa_streamindex_test.py index 1d20c25dff..fc1ac3641a 100644 --- a/tests/unit/csa_streamindex_test.py +++ b/tests/unit/csa_streamindex_test.py @@ -128,6 +128,62 @@ def test_tpu_compile_smoke_production_tiles(self): out = fn(q, compressed, weights).block_until_ready() self.assertEqual(out.shape, (b, s, w)) + def test_vjp_backward_parity(self): + """Verifies that backward gradients of custom VJP match reference autograd.""" + key1, key2, key3, key4 = jax.random.split(self.key, 4) + b, s, w, h, d = 2, 256, 128, 32, 64 + scale = d**-0.5 + q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) + weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) + cotangent = jax.random.normal(key4, (b, s, w), dtype=jnp.float32) + + def loss_kernel(q, k, w): + out = csa_streamindex.csa_streamindex_score( + q, k, w, softmax_scale=scale, block_q=128, block_w=128, interpret=True + ) + return jnp.sum(out * cotangent) + + def loss_ref(q, k, w): + out = csa_streamindex.reference_csa_streamindex_score( + q, k, w, softmax_scale=scale + ) + return jnp.sum(out * cotangent) + + _, (dq_k, dk_k, dw_k) = jax.value_and_grad(loss_kernel, argnums=(0, 1, 2))(q, compressed, weights) + _, (dq_r, dk_r, dw_r) = jax.value_and_grad(loss_ref, argnums=(0, 1, 2))(q, compressed, weights) + + np.testing.assert_allclose(dw_k, dw_r, rtol=1e-3, atol=1e-3) + np.testing.assert_allclose(dq_k.astype(jnp.float32), dq_r.astype(jnp.float32), rtol=1e-3, atol=1e-3) + np.testing.assert_allclose(dk_k.astype(jnp.float32), dk_r.astype(jnp.float32), rtol=1e-3, atol=1e-3) + + def test_tpu_backward_smoke(self): + """Verifies that backward pass compiles and runs on TPU hardware.""" + if jax.default_backend() != "tpu": + self.skipTest("TPU hardware required for backward smoke test.") + b, s, h, d = 1, 1024, 64, 128 + w = s // 4 + scale = d**-0.5 + key1, key2, key3, key4 = jax.random.split(self.key, 4) + q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) + weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) + cotangent = jax.random.normal(key4, (b, s, w), dtype=jnp.float32) + + @jax.jit + def grad_fn(q, k, w): + def loss(q, k, w): + out = csa_streamindex.csa_streamindex_score( + q, k, w, softmax_scale=scale, block_q=128, block_w=512, interpret=False + ) + return jnp.sum(out * cotangent) + return jax.grad(loss, argnums=(0, 1, 2))(q, k, w) + + dq, dk, dw = grad_fn(q, compressed, weights) + self.assertEqual(dq.shape, q.shape) + self.assertEqual(dk.shape, compressed.shape) + self.assertEqual(dw.shape, weights.shape) + class TestDeepseekv4IndexerIntegration(unittest.TestCase): """Integration tests for Deepseekv4Indexer with CSA StreamIndex kernel dispatch.""" From da439a9399f178d9dbb6b5007e0dbe01b72ea073 Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Sat, 29 Aug 2026 18:54:03 +0000 Subject: [PATCH 05/13] Add csa_streamindex_score_head_major kernel and dispatch --- .../kernels/attention/csa_streamindex.py | 190 ++++++++++++++++++ src/maxtext/layers/attention_compressed.py | 13 +- 2 files changed, 196 insertions(+), 7 deletions(-) diff --git a/src/maxtext/kernels/attention/csa_streamindex.py b/src/maxtext/kernels/attention/csa_streamindex.py index 24a2b9d980..26a679dc18 100644 --- a/src/maxtext/kernels/attention/csa_streamindex.py +++ b/src/maxtext/kernels/attention/csa_streamindex.py @@ -233,3 +233,193 @@ def reference_csa_streamindex_score( scores = jnp.einsum("bhsd,bhwd->bhsw", q_trans, compressed_kv) scores = jax.nn.relu(scores) * softmax_scale return jnp.einsum("bhsw,bsh->bsw", scores, weights.astype(jnp.float32)) + + +def csa_streamindex_score_head_major_kernel( + q_ref, # [num_heads, block_q, head_dim] + k_ref, # [block_w, head_dim] + w_ref, # [block_q, num_heads] + out_ref, # [block_q, block_w] + *, + softmax_scale: float, + head_chunk: int = 32, +): + """Pallas TPU kernel for head-major [num_heads, block_q, head_dim] input.""" + q = q_ref[...] + k = k_ref[...] + w = w_ref[...] + + # Swap to [block_q, num_heads, head_dim] in VMEM for 128-sublane VPU vector register alignment + q_shd = jnp.swapaxes(q, 0, 1) + block_q, num_heads, head_dim = q_shd.shape + block_w, _ = k.shape + + acc = jnp.zeros((block_q, block_w), dtype=jnp.float32) + + for h_start in range(0, num_heads, head_chunk): + h_end = min(h_start + head_chunk, num_heads) + q_c = q_shd[:, h_start:h_end, :] + w_c = w[:, h_start:h_end].astype(jnp.float32) + + scores_c = jnp.einsum( + "shd,wd->shw", + q_c, + k, + preferred_element_type=jnp.float32, + ) + scores_c = jnp.maximum(scores_c, 0.0) + chunk_acc = jnp.sum(scores_c * w_c[:, :, None], axis=1) + acc = acc + chunk_acc + + out_ref[...] = (acc * softmax_scale).astype(out_ref.dtype) + + +def _csa_streamindex_score_head_major_pallas_fwd( + q: jax.Array, + compressed: jax.Array, + weights: jax.Array, + *, + softmax_scale: float, + block_q: int = 128, + block_w: int = 1024, + head_chunk: int = 32, + interpret: bool = False, +) -> jax.Array: + """Forward implementation using fused Pallas TPU kernel for head-major [B, H, S, D] q.""" + batch_size, num_heads, seq_len, head_dim = q.shape + _, compressed_len, comp_head_dim = compressed.shape + assert comp_head_dim == head_dim, f"{comp_head_dim=} != {head_dim=}" + assert weights.shape == (batch_size, seq_len, num_heads), f"{weights.shape=} != {(batch_size, seq_len, num_heads)=}" + + padded_s = ((seq_len + block_q - 1) // block_q) * block_q + padded_w = ((compressed_len + block_w - 1) // block_w) * block_w + + if padded_s > seq_len: + pad_s = padded_s - seq_len + q = jnp.pad(q, ((0, 0), (0, 0), (0, pad_s), (0, 0))) + weights = jnp.pad(weights, ((0, 0), (0, pad_s), (0, 0))) + if padded_w > compressed_len: + pad_w = padded_w - compressed_len + compressed = jnp.pad(compressed, ((0, 0), (0, pad_w), (0, 0))) + + grid = (batch_size, padded_s // block_q, padded_w // block_w) + + in_specs = [ + pl.BlockSpec((None, num_heads, block_q, head_dim), lambda b, i, j: (b, 0, i, 0)), + pl.BlockSpec((None, block_w, head_dim), lambda b, i, j: (b, j, 0)), + pl.BlockSpec((None, block_q, num_heads), lambda b, i, j: (b, i, 0)), + ] + out_specs = pl.BlockSpec((None, block_q, block_w), lambda b, i, j: (b, i, j)) + + out = pl.pallas_call( + functools.partial( + csa_streamindex_score_head_major_kernel, + softmax_scale=softmax_scale, + head_chunk=head_chunk, + ), + in_specs=in_specs, + out_specs=out_specs, + grid=grid, + compiler_params=pltpu.CompilerParams( + dimension_semantics=("parallel", "parallel", "arbitrary"), + ), + out_shape=jax.ShapeDtypeStruct((batch_size, padded_s, padded_w), jnp.float32), + interpret=interpret, + )(q, compressed, weights) + + return out[:, :seq_len, :compressed_len] + + +@functools.partial(jax.custom_vjp, nondiff_argnums=(3, 4, 5, 6, 7)) +def csa_streamindex_score_head_major( + q: jax.Array, + compressed: jax.Array, + weights: jax.Array, + softmax_scale: float, + block_q: int = 128, + block_w: int = 1024, + head_chunk: int = 32, + interpret: bool = False, +) -> jax.Array: + """Computes CSA StreamIndex scores using head-major [B, H, S, D] q layout. + + Eliminates sublane padding and register spilling on TPU v5p when H < 128. + """ + return _csa_streamindex_score_head_major_pallas_fwd( + q, + compressed, + weights, + softmax_scale=softmax_scale, + block_q=block_q, + block_w=block_w, + head_chunk=head_chunk, + interpret=interpret, + ) + + +def _csa_streamindex_score_head_major_fwd( + q: jax.Array, + compressed: jax.Array, + weights: jax.Array, + softmax_scale: float, + block_q: int = 128, + block_w: int = 1024, + head_chunk: int = 32, + interpret: bool = False, +) -> tuple[jax.Array, tuple[jax.Array, jax.Array, jax.Array]]: + out = _csa_streamindex_score_head_major_pallas_fwd( + q, + compressed, + weights, + softmax_scale=softmax_scale, + block_q=block_q, + block_w=block_w, + head_chunk=head_chunk, + interpret=interpret, + ) + return out, (q, compressed, weights) + + +def _csa_streamindex_score_head_major_bwd( + softmax_scale: float, + block_q: int, + block_w: int, + head_chunk: int, + interpret: bool, + res: tuple[jax.Array, jax.Array, jax.Array], + g: jax.Array, +) -> tuple[jax.Array, jax.Array, jax.Array]: + del block_q, block_w, head_chunk, interpret + q, compressed, weights = res + _, vjp_fn = jax.vjp( + functools.partial(reference_csa_streamindex_score_head_major, softmax_scale=softmax_scale), + q, + compressed, + weights, + ) + dq, dk, dw = vjp_fn(g) + return dq, dk, dw + + +csa_streamindex_score_head_major.defvjp( + _csa_streamindex_score_head_major_fwd, + _csa_streamindex_score_head_major_bwd, +) + + +def reference_csa_streamindex_score_head_major( + q: jax.Array, + compressed: jax.Array, + weights: jax.Array, + *, + softmax_scale: float, +) -> jax.Array: + """Reference score computation for head-major q layout [B, H, S, D].""" + b, h, s, d = q.shape + _, w, _ = compressed.shape + q_fp32 = q.astype(jnp.float32) + compressed_kv = jnp.broadcast_to(compressed[:, None, :, :], (b, h, w, d)).astype(jnp.float32) + scores = jnp.einsum("bhsd,bhwd->bhsw", q_fp32, compressed_kv) + scores = jax.nn.relu(scores) * softmax_scale + return jnp.einsum("bhsw,bsh->bsw", scores, weights.astype(jnp.float32)) + diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index cf35cebb8f..ae4a9793c2 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -890,13 +890,14 @@ def indexer_compressor_fn(buf_kv, buf_gate): # --- TOP-K ROUTING MATH (Executes in both Prefill and AR) --- q = self.q_proj(q_latent).reshape((batch_size, seq_len, self.index_n_heads, self.index_head_dim)) + q = jnp.transpose(q, (0, 2, 1, 3)) + q = self.rotary_emb(q, position_ids, unsqueeze_dim=1) weights = self.weights_proj(hidden_states).astype(jnp.float32) * self.weights_scaling block_q = 128 use_kernel = getattr(self.config, "use_csa_streamindex_kernel", False) and (seq_len >= block_q) if use_kernel: - q_seq_major = self.rotary_emb(q, position_ids, unsqueeze_dim=2) mesh = getattr(self.config, "mesh", None) or getattr(self, "mesh", None) if mesh is None: try: @@ -928,7 +929,7 @@ def indexer_compressor_fn(buf_kv, buf_gate): check_vma=False, ) def _shard_mapped_streamindex(local_q, local_comp, local_weights): - return csa_streamindex.csa_streamindex_score( + return csa_streamindex.csa_streamindex_score_head_major( q=local_q, compressed=local_comp, weights=local_weights, @@ -937,10 +938,10 @@ def _shard_mapped_streamindex(local_q, local_comp, local_weights): block_w=1024, head_chunk=32, ) - index_scores = _shard_mapped_streamindex(q_seq_major, compressed, weights) + index_scores = _shard_mapped_streamindex(q, compressed, weights) else: - index_scores = csa_streamindex.csa_streamindex_score( - q=q_seq_major, + index_scores = csa_streamindex.csa_streamindex_score_head_major( + q=q, compressed=compressed, weights=weights, softmax_scale=self.softmax_scale, @@ -949,8 +950,6 @@ def _shard_mapped_streamindex(local_q, local_comp, local_weights): head_chunk=32, ) else: - q = jnp.transpose(q, (0, 2, 1, 3)) - q = self.rotary_emb(q, position_ids, unsqueeze_dim=1) compressed_kv = jnp.expand_dims(compressed, axis=1) compressed_kv = jnp.broadcast_to( compressed_kv, (batch_size, self.index_n_heads, compressed_len, self.index_head_dim) From fbe36724a4d26a5bbc2706e300b3bc113a60e274 Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Sat, 29 Aug 2026 19:00:59 +0000 Subject: [PATCH 06/13] Update unit tests for csa_streamindex_score_head_major --- tests/unit/csa_streamindex_test.py | 69 +++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/tests/unit/csa_streamindex_test.py b/tests/unit/csa_streamindex_test.py index fc1ac3641a..3f7aed4062 100644 --- a/tests/unit/csa_streamindex_test.py +++ b/tests/unit/csa_streamindex_test.py @@ -243,13 +243,13 @@ def test_indexer_kernel_vs_einsum_output_parity(self): out_einsum = indexer_einsum(hidden, q_latent, pos) # 2. Forward with use_csa_streamindex_kernel=True (intercept to set interpret=True on CPU) - real_kernel_fn = csa_streamindex.csa_streamindex_score + real_kernel_fn = csa_streamindex.csa_streamindex_score_head_major def interpret_kernel_fn(*args, **kwargs): kwargs["interpret"] = True return real_kernel_fn(*args, **kwargs) - with mock.patch.object(csa_streamindex, "csa_streamindex_score", side_effect=interpret_kernel_fn): + with mock.patch.object(csa_streamindex, "csa_streamindex_score_head_major", side_effect=interpret_kernel_fn): out_kernel = indexer_kernel(hidden, q_latent, pos) np.testing.assert_array_equal(out_kernel, out_einsum) @@ -270,25 +270,80 @@ def test_ar_decode_fallback(self): q_latent = jnp.ones((b, s, q_lora), dtype=jnp.bfloat16) pos = jnp.arange(s, dtype=jnp.int32)[None, :] - with mock.patch.object(csa_streamindex, "csa_streamindex_score") as mock_kernel: + with mock.patch.object(csa_streamindex, "csa_streamindex_score_head_major") as mock_kernel: out = indexer(hidden, q_latent, pos) mock_kernel.assert_not_called() self.assertEqual(out.shape, (b, s, min(32, s // 4))) + def test_head_major_kernel_vs_einsum_parity(self): + """Verifies numerical parity for head-major csa_streamindex_score_head_major.""" + key = jax.random.PRNGKey(42) + key1, key2, key3 = jax.random.split(key, 3) + b, h, s, w, d = 1, 4, 256, 128, 64 + q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) + compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) + weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) + softmax_scale = d**-0.5 + + expected = csa_streamindex.reference_csa_streamindex_score_head_major( + q, compressed, weights, softmax_scale=softmax_scale + ) + actual = csa_streamindex.csa_streamindex_score_head_major( + q, + compressed, + weights, + softmax_scale=softmax_scale, + block_q=128, + block_w=128, + interpret=True, + ) + np.testing.assert_allclose(actual, expected, rtol=1e-1, atol=1e-1) + + def test_head_major_gradient_parity(self): + """Verifies that head-major custom_vjp gradients match reference autograd.""" + b, h, s, w, d = 1, 4, 128, 128, 32 + key = jax.random.PRNGKey(42) + key1, key2, key3 = jax.random.split(key, 3) + q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) + comp = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) + weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) + scale = 32.0**-0.5 + + def loss_kernel(q, comp, weights): + return jnp.sum( + csa_streamindex.csa_streamindex_score_head_major( + q, comp, weights, softmax_scale=scale, block_q=128, block_w=128, interpret=True + ) + ) + + def loss_ref(q, comp, weights): + return jnp.sum( + csa_streamindex.reference_csa_streamindex_score_head_major( + q, comp, weights, softmax_scale=scale + ) + ) + + g_q_k, g_c_k, g_w_k = jax.grad(loss_kernel, argnums=(0, 1, 2))(q, comp, weights) + g_q_r, g_c_r, g_w_r = jax.grad(loss_ref, argnums=(0, 1, 2))(q, comp, weights) + + np.testing.assert_allclose(g_q_k, g_q_r, rtol=1e-3, atol=1e-3) + np.testing.assert_allclose(g_c_k, g_c_r, rtol=1e-3, atol=1e-3) + np.testing.assert_allclose(g_w_k, g_w_r, rtol=1e-3, atol=1e-3) + def test_jaxpr_verification(self): """Verifies that jaxpr contains pallas_call when enabled and dot_general when disabled.""" - q = jnp.zeros((1, 256, 16, 64), dtype=jnp.bfloat16) + q = jnp.zeros((1, 4, 256, 64), dtype=jnp.bfloat16) compressed = jnp.zeros((1, 128, 64), dtype=jnp.bfloat16) - weights = jnp.zeros((1, 256, 16), dtype=jnp.float32) + weights = jnp.zeros((1, 256, 4), dtype=jnp.float32) scale = 64.0**-0.5 def compute_scores(q, compressed, weights, use_kernel): if use_kernel: - return csa_streamindex.csa_streamindex_score( + return csa_streamindex.csa_streamindex_score_head_major( q, compressed, weights, softmax_scale=scale, block_q=128, block_w=128 ) else: - return csa_streamindex.reference_csa_streamindex_score( + return csa_streamindex.reference_csa_streamindex_score_head_major( q, compressed, weights, softmax_scale=scale ) From 020d7f1325fda958ff492ac2f8cf24640f852a5e Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 31 Aug 2026 21:42:39 +0000 Subject: [PATCH 07/13] Clean up CSA StreamIndex: remove unused variants, simplify head-major kernel and tests --- .../kernels/attention/csa_streamindex.py | 353 ++++-------------- src/maxtext/layers/attention_compressed.py | 13 +- tests/unit/csa_streamindex_test.py | 204 +++------- 3 files changed, 145 insertions(+), 425 deletions(-) diff --git a/src/maxtext/kernels/attention/csa_streamindex.py b/src/maxtext/kernels/attention/csa_streamindex.py index 26a679dc18..f1f817c438 100644 --- a/src/maxtext/kernels/attention/csa_streamindex.py +++ b/src/maxtext/kernels/attention/csa_streamindex.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -12,7 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Pallas TPU kernel for DeepSeek-V4 CSA StreamIndex score computation.""" +"""Fused Pallas TPU kernel for DeepSeek-V4 CSA StreamIndex Score Computation. + +Computes: + index_scores = sum_h(ReLU(q_h @ comp^T) * softmax_scale * w_h) +with optional in-VMEM causal future masking. + +The kernel fuses dot-product, ReLU activation, softmax scaling, head-weight +contraction, and causal future masking into on-chip TPU VMEM registers, avoiding +the materialization of the intermediate [B, H, S, W] 4D tensor in HBM. +""" import functools import jax @@ -21,220 +30,6 @@ import jax.numpy as jnp -def csa_streamindex_score_kernel( - q_ref, # [block_q, num_heads, head_dim] - k_ref, # [block_w, head_dim] - w_ref, # [block_q, num_heads] - out_ref, # [block_q, block_w] - *, - softmax_scale: float, - head_chunk: int = 32, -): - """Pallas TPU kernel for fused indexer score calculation. - - Accumulates head scores chunked directly into a [block_q, block_w] accumulator - in VMEM without allocating large [block_q, num_heads, block_w] intermediate buffers. - """ - q = q_ref[...] - k = k_ref[...] - w = w_ref[...] - - block_q, num_heads, head_dim = q.shape - block_w, _ = k.shape - - acc = jnp.zeros((block_q, block_w), dtype=jnp.float32) - - for h_start in range(0, num_heads, head_chunk): - h_end = min(h_start + head_chunk, num_heads) - q_c = q[:, h_start:h_end, :] - w_c = w[:, h_start:h_end].astype(jnp.float32) - - # [block_q, h_c, head_dim] x [block_w, head_dim] -> [block_q, h_c, block_w] - scores_c = jnp.einsum( - "shd,wd->shw", - q_c, - k, - preferred_element_type=jnp.float32, - ) - scores_c = jnp.maximum(scores_c, 0.0) - chunk_acc = jnp.sum(scores_c * w_c[:, :, None], axis=1) - acc = acc + chunk_acc - - out_ref[...] = (acc * softmax_scale).astype(out_ref.dtype) - - -def _csa_streamindex_score_pallas_fwd( - q: jax.Array, - compressed: jax.Array, - weights: jax.Array, - *, - softmax_scale: float, - block_q: int = 128, - block_w: int = 1024, - head_chunk: int = 32, - interpret: bool = False, -) -> jax.Array: - """Forward implementation using fused Pallas TPU kernel.""" - batch_size, seq_len, num_heads, head_dim = q.shape - _, compressed_len, comp_head_dim = compressed.shape - assert comp_head_dim == head_dim, f"{comp_head_dim=} != {head_dim=}" - assert weights.shape == (batch_size, seq_len, num_heads), f"{weights.shape=} != {(batch_size, seq_len, num_heads)=}" - - padded_s = ((seq_len + block_q - 1) // block_q) * block_q - padded_w = ((compressed_len + block_w - 1) // block_w) * block_w - - if padded_s > seq_len: - pad_s = padded_s - seq_len - q = jnp.pad(q, ((0, 0), (0, pad_s), (0, 0), (0, 0))) - weights = jnp.pad(weights, ((0, 0), (0, pad_s), (0, 0))) - if padded_w > compressed_len: - pad_w = padded_w - compressed_len - compressed = jnp.pad(compressed, ((0, 0), (0, pad_w), (0, 0))) - - grid = (batch_size, padded_s // block_q, padded_w // block_w) - - in_specs = [ - pl.BlockSpec((None, block_q, num_heads, head_dim), lambda b, i, j: (b, i, 0, 0)), - pl.BlockSpec((None, block_w, head_dim), lambda b, i, j: (b, j, 0)), - pl.BlockSpec((None, block_q, num_heads), lambda b, i, j: (b, i, 0)), - ] - out_specs = pl.BlockSpec((None, block_q, block_w), lambda b, i, j: (b, i, j)) - - out = pl.pallas_call( - functools.partial( - csa_streamindex_score_kernel, - softmax_scale=softmax_scale, - head_chunk=head_chunk, - ), - in_specs=in_specs, - out_specs=out_specs, - grid=grid, - compiler_params=pltpu.CompilerParams( - dimension_semantics=("parallel", "parallel", "arbitrary"), - ), - out_shape=jax.ShapeDtypeStruct((batch_size, padded_s, padded_w), jnp.float32), - interpret=interpret, - )(q, compressed, weights) - - return out[:, :seq_len, :compressed_len] - - -@functools.partial(jax.custom_vjp, nondiff_argnums=(3, 4, 5, 6, 7)) -def csa_streamindex_score( - q: jax.Array, - compressed: jax.Array, - weights: jax.Array, - softmax_scale: float, - block_q: int = 128, - block_w: int = 1024, - head_chunk: int = 32, - interpret: bool = False, -) -> jax.Array: - """Computes CSA StreamIndex scores using a fused Pallas TPU kernel. - - Differentiable via jax.custom_vjp: executes fused Pallas kernel in forward pass, - and evaluates reference autograd in backward pass. - - Args: - q: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]. - compressed: Compressed KV tensor of shape [batch_size, compressed_len, head_dim]. - weights: Indexer weights tensor of shape [batch_size, seq_len, num_heads]. - softmax_scale: Scaling factor applied post-ReLU (typically head_dim**-0.5). - block_q: Query sequence block size (default 128). - block_w: Compressed window block size (default 1024). - head_chunk: Number of heads processed per accumulation step in VMEM (default 32). - interpret: If True, executes via JAX interpreter on CPU. - - Returns: - Index scores tensor of shape [batch_size, seq_len, compressed_len] in float32. - """ - return _csa_streamindex_score_pallas_fwd( - q, - compressed, - weights, - softmax_scale=softmax_scale, - block_q=block_q, - block_w=block_w, - head_chunk=head_chunk, - interpret=interpret, - ) - - -def _csa_streamindex_score_fwd( - q: jax.Array, - compressed: jax.Array, - weights: jax.Array, - softmax_scale: float, - block_q: int = 128, - block_w: int = 1024, - head_chunk: int = 32, - interpret: bool = False, -) -> tuple[jax.Array, tuple[jax.Array, jax.Array, jax.Array]]: - out = _csa_streamindex_score_pallas_fwd( - q, - compressed, - weights, - softmax_scale=softmax_scale, - block_q=block_q, - block_w=block_w, - head_chunk=head_chunk, - interpret=interpret, - ) - return out, (q, compressed, weights) - - -def _csa_streamindex_score_bwd( - softmax_scale: float, - block_q: int, - block_w: int, - head_chunk: int, - interpret: bool, - res: tuple[jax.Array, jax.Array, jax.Array], - g: jax.Array, -) -> tuple[jax.Array, jax.Array, jax.Array]: - del block_q, block_w, head_chunk, interpret - q, compressed, weights = res - _, vjp_fn = jax.vjp( - functools.partial(reference_csa_streamindex_score, softmax_scale=softmax_scale), - q, - compressed, - weights, - ) - dq, dk, dw = vjp_fn(g) - return dq, dk, dw - - -csa_streamindex_score.defvjp(_csa_streamindex_score_fwd, _csa_streamindex_score_bwd) - - -def reference_csa_streamindex_score( - q: jax.Array, - compressed: jax.Array, - weights: jax.Array, - *, - softmax_scale: float, -) -> jax.Array: - """Reference score computation matching the pure JAX einsum path. - - Args: - q: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]. - compressed: Compressed KV tensor of shape [batch_size, compressed_len, head_dim]. - weights: Indexer weights tensor of shape [batch_size, seq_len, num_heads]. - softmax_scale: Scaling factor applied post-ReLU. - - Returns: - Index scores tensor of shape [batch_size, seq_len, compressed_len] in float32. - """ - b, s, h, d = q.shape - _, w, _ = compressed.shape - q_trans = jnp.transpose(q, (0, 2, 1, 3)).astype(jnp.float32) - compressed_kv = jnp.expand_dims(compressed, axis=1) - compressed_kv = jnp.broadcast_to(compressed_kv, (b, h, w, d)).astype(jnp.float32) - scores = jnp.einsum("bhsd,bhwd->bhsw", q_trans, compressed_kv) - scores = jax.nn.relu(scores) * softmax_scale - return jnp.einsum("bhsw,bsh->bsw", scores, weights.astype(jnp.float32)) - - def csa_streamindex_score_head_major_kernel( q_ref, # [num_heads, block_q, head_dim] k_ref, # [block_w, head_dim] @@ -242,36 +37,37 @@ def csa_streamindex_score_head_major_kernel( out_ref, # [block_q, block_w] *, softmax_scale: float, - head_chunk: int = 32, + compress_rate: int = 0, ): - """Pallas TPU kernel for head-major [num_heads, block_q, head_dim] input.""" - q = q_ref[...] - k = k_ref[...] - w = w_ref[...] + """Fused Pallas TPU kernel for head-major [num_heads, block_q, head_dim] input with 2D MXU matmul.""" + num_heads, block_q, head_dim = q_ref.shape + block_w, _ = k_ref.shape + + # Reshape Q to 2D: (num_heads * block_q, head_dim) for native 2D systolic array MXU contraction + q_2d = q_ref[...].reshape(num_heads * block_q, head_dim) + k_2d = k_ref[...] - # Swap to [block_q, num_heads, head_dim] in VMEM for 128-sublane VPU vector register alignment - q_shd = jnp.swapaxes(q, 0, 1) - block_q, num_heads, head_dim = q_shd.shape - block_w, _ = k.shape + # 2D MXU matmul: (num_heads * block_q, head_dim) @ (block_w, head_dim)^T -> (num_heads * block_q, block_w) + s_2d = jnp.einsum("nd,md->nm", q_2d, k_2d, preferred_element_type=jnp.float32) - acc = jnp.zeros((block_q, block_w), dtype=jnp.float32) + # Reshape to (num_heads, block_q, block_w) and apply ReLU + s = s_2d.reshape(num_heads, block_q, block_w) + s = jnp.maximum(s, 0.0) - for h_start in range(0, num_heads, head_chunk): - h_end = min(h_start + head_chunk, num_heads) - q_c = q_shd[:, h_start:h_end, :] - w_c = w[:, h_start:h_end].astype(jnp.float32) + # Multiply by weights and sum across heads in VMEM + w = w_ref[...].astype(jnp.float32).transpose(1, 0)[:, :, None] + s_weighted = jnp.sum(s * w, axis=0) * softmax_scale - scores_c = jnp.einsum( - "shd,wd->shw", - q_c, - k, - preferred_element_type=jnp.float32, - ) - scores_c = jnp.maximum(scores_c, 0.0) - chunk_acc = jnp.sum(scores_c * w_c[:, :, None], axis=1) - acc = acc + chunk_acc + # In-VMEM causal future masking + if compress_rate > 0: + i = pl.program_id(1) + j = pl.program_id(2) + q_indices = i * block_q + jnp.arange(block_q, dtype=jnp.int32)[:, None] + k_indices = (j * block_w + jnp.arange(block_w, dtype=jnp.int32)[None, :]) * compress_rate + future_mask = (k_indices + compress_rate) > (q_indices + 1) + s_weighted = jnp.where(future_mask, -1e9, s_weighted) - out_ref[...] = (acc * softmax_scale).astype(out_ref.dtype) + out_ref[...] = s_weighted.astype(out_ref.dtype) def _csa_streamindex_score_head_major_pallas_fwd( @@ -280,9 +76,9 @@ def _csa_streamindex_score_head_major_pallas_fwd( weights: jax.Array, *, softmax_scale: float, - block_q: int = 128, - block_w: int = 1024, - head_chunk: int = 32, + compress_rate: int = 0, + block_q: int | None = None, + block_w: int | None = None, interpret: bool = False, ) -> jax.Array: """Forward implementation using fused Pallas TPU kernel for head-major [B, H, S, D] q.""" @@ -291,6 +87,11 @@ def _csa_streamindex_score_head_major_pallas_fwd( assert comp_head_dim == head_dim, f"{comp_head_dim=} != {head_dim=}" assert weights.shape == (batch_size, seq_len, num_heads), f"{weights.shape=} != {(batch_size, seq_len, num_heads)=}" + if block_q is None: + block_q = 128 if num_heads >= 32 else 256 + if block_w is None: + block_w = 1024 if num_heads >= 32 else 2048 + padded_s = ((seq_len + block_q - 1) // block_q) * block_q padded_w = ((compressed_len + block_w - 1) // block_w) * block_w @@ -315,7 +116,7 @@ def _csa_streamindex_score_head_major_pallas_fwd( functools.partial( csa_streamindex_score_head_major_kernel, softmax_scale=softmax_scale, - head_chunk=head_chunk, + compress_rate=compress_rate, ), in_specs=in_specs, out_specs=out_specs, @@ -336,23 +137,20 @@ def csa_streamindex_score_head_major( compressed: jax.Array, weights: jax.Array, softmax_scale: float, - block_q: int = 128, - block_w: int = 1024, - head_chunk: int = 32, + compress_rate: int = 0, + block_q: int | None = None, + block_w: int | None = None, interpret: bool = False, ) -> jax.Array: - """Computes CSA StreamIndex scores using head-major [B, H, S, D] q layout. - - Eliminates sublane padding and register spilling on TPU v5p when H < 128. - """ + """Computes CSA StreamIndex scores using head-major [B, H, S, D] q layout with 2D MXU matmul.""" return _csa_streamindex_score_head_major_pallas_fwd( q, compressed, weights, softmax_scale=softmax_scale, + compress_rate=compress_rate, block_q=block_q, block_w=block_w, - head_chunk=head_chunk, interpret=interpret, ) @@ -362,9 +160,9 @@ def _csa_streamindex_score_head_major_fwd( compressed: jax.Array, weights: jax.Array, softmax_scale: float, - block_q: int = 128, - block_w: int = 1024, - head_chunk: int = 32, + compress_rate: int = 0, + block_q: int | None = None, + block_w: int | None = None, interpret: bool = False, ) -> tuple[jax.Array, tuple[jax.Array, jax.Array, jax.Array]]: out = _csa_streamindex_score_head_major_pallas_fwd( @@ -372,9 +170,9 @@ def _csa_streamindex_score_head_major_fwd( compressed, weights, softmax_scale=softmax_scale, + compress_rate=compress_rate, block_q=block_q, block_w=block_w, - head_chunk=head_chunk, interpret=interpret, ) return out, (q, compressed, weights) @@ -382,17 +180,21 @@ def _csa_streamindex_score_head_major_fwd( def _csa_streamindex_score_head_major_bwd( softmax_scale: float, - block_q: int, - block_w: int, - head_chunk: int, + compress_rate: int, + block_q: int | None, + block_w: int | None, interpret: bool, res: tuple[jax.Array, jax.Array, jax.Array], g: jax.Array, ) -> tuple[jax.Array, jax.Array, jax.Array]: - del block_q, block_w, head_chunk, interpret + del block_q, block_w, interpret q, compressed, weights = res _, vjp_fn = jax.vjp( - functools.partial(reference_csa_streamindex_score_head_major, softmax_scale=softmax_scale), + functools.partial( + reference_csa_streamindex_score_head_major, + softmax_scale=softmax_scale, + compress_rate=compress_rate, + ), q, compressed, weights, @@ -402,8 +204,7 @@ def _csa_streamindex_score_head_major_bwd( csa_streamindex_score_head_major.defvjp( - _csa_streamindex_score_head_major_fwd, - _csa_streamindex_score_head_major_bwd, + _csa_streamindex_score_head_major_fwd, _csa_streamindex_score_head_major_bwd ) @@ -413,13 +214,23 @@ def reference_csa_streamindex_score_head_major( weights: jax.Array, *, softmax_scale: float, + compress_rate: int = 0, ) -> jax.Array: - """Reference score computation for head-major q layout [B, H, S, D].""" - b, h, s, d = q.shape - _, w, _ = compressed.shape - q_fp32 = q.astype(jnp.float32) - compressed_kv = jnp.broadcast_to(compressed[:, None, :, :], (b, h, w, d)).astype(jnp.float32) - scores = jnp.einsum("bhsd,bhwd->bhsw", q_fp32, compressed_kv) + """Reference score computation matching the pure JAX einsum path for head-major q.""" + scores = jnp.einsum("bhsd,bwd->bhsw", q.astype(jnp.float32), compressed.astype(jnp.float32)) scores = jax.nn.relu(scores) * softmax_scale - return jnp.einsum("bhsw,bsh->bsw", scores, weights.astype(jnp.float32)) - + index_scores = jnp.einsum("bhsw,bsh->bsw", scores, weights.astype(jnp.float32)) + if compress_rate > 0: + seq_len = q.shape[2] + compressed_len = compressed.shape[1] + position_ids = jnp.arange(seq_len, dtype=jnp.int32)[None, :] + usable_len = compressed_len * compress_rate + block_positions = position_ids[:, :usable_len:compress_rate] + future_mask = (block_positions[:, None, :] + compress_rate) > (position_ids[:, :, None] + 1) + index_scores = jnp.where(future_mask, -1e9, index_scores) + return index_scores + + +# Public aliases for standard naming conventions +csa_streamindex_score = csa_streamindex_score_head_major +reference_csa_streamindex_score = reference_csa_streamindex_score_head_major diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index ae4a9793c2..a6dd5aaf89 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -934,9 +934,7 @@ def _shard_mapped_streamindex(local_q, local_comp, local_weights): compressed=local_comp, weights=local_weights, softmax_scale=self.softmax_scale, - block_q=block_q, - block_w=1024, - head_chunk=32, + compress_rate=self.compress_rate, ) index_scores = _shard_mapped_streamindex(q, compressed, weights) else: @@ -945,9 +943,7 @@ def _shard_mapped_streamindex(local_q, local_comp, local_weights): compressed=compressed, weights=weights, softmax_scale=self.softmax_scale, - block_q=block_q, - block_w=1024, - head_chunk=32, + compress_rate=self.compress_rate, ) else: compressed_kv = jnp.expand_dims(compressed, axis=1) @@ -968,8 +964,9 @@ def _shard_mapped_streamindex(local_q, local_comp, local_weights): 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) + # Apply the mask to the scores if not already applied by the kernel + if not use_kernel: + 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: diff --git a/tests/unit/csa_streamindex_test.py b/tests/unit/csa_streamindex_test.py index 3f7aed4062..5b52c3ee0c 100644 --- a/tests/unit/csa_streamindex_test.py +++ b/tests/unit/csa_streamindex_test.py @@ -36,19 +36,19 @@ class TestCsaStreamIndexScoreKernel(unittest.TestCase): def setUp(self): self.key = jax.random.PRNGKey(42) - def test_kernel_vs_einsum_parity_exact_multiple(self): - """Verifies numerical parity when shapes are exact multiples of block sizes.""" + def test_head_major_parity_exact_multiple(self): + """Verifies numerical parity for head-major input matching reference einsum.""" key1, key2, key3 = jax.random.split(self.key, 3) - b, s, w, h, d = 2, 256, 128, 64, 128 - q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + b, h, s, w, d = 2, 64, 256, 128, 128 + q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) softmax_scale = d**-0.5 - expected = csa_streamindex.reference_csa_streamindex_score( + expected = csa_streamindex.reference_csa_streamindex_score_head_major( q, compressed, weights, softmax_scale=softmax_scale ) - actual = csa_streamindex.csa_streamindex_score( + actual = csa_streamindex.csa_streamindex_score_head_major( q, compressed, weights, @@ -57,22 +57,21 @@ def test_kernel_vs_einsum_parity_exact_multiple(self): block_w=128, interpret=True, ) - np.testing.assert_allclose(actual, expected, rtol=1e-1, atol=1e-1) - def test_kernel_vs_einsum_parity_non_multiples(self): - """Verifies padding handling when seq_len and compressed_len are not multiples of block_q/block_w.""" + def test_head_major_parity_non_multiples(self): + """Verifies padding handling when seq_len and compressed_len are not multiples of block sizes.""" key1, key2, key3 = jax.random.split(self.key, 3) - b, s, w, h, d = 2, 150, 70, 32, 64 - q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + b, h, s, w, d = 2, 32, 150, 70, 64 + q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) softmax_scale = d**-0.5 - expected = csa_streamindex.reference_csa_streamindex_score( + expected = csa_streamindex.reference_csa_streamindex_score_head_major( q, compressed, weights, softmax_scale=softmax_scale ) - actual = csa_streamindex.csa_streamindex_score( + actual = csa_streamindex.csa_streamindex_score_head_major( q, compressed, weights, @@ -81,108 +80,82 @@ def test_kernel_vs_einsum_parity_non_multiples(self): block_w=128, interpret=True, ) - np.testing.assert_allclose(actual, expected, rtol=1e-2, atol=1e-2) - def test_kernel_small_compressed_window(self): - """Verifies behavior when compressed_len < block_w.""" + def test_head_major_causal_parity(self): + """Verifies numerical parity with in-VMEM causal masking.""" key1, key2, key3 = jax.random.split(self.key, 3) - b, s, w, h, d = 1, 128, 32, 16, 64 - q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + b, h, s, w, d = 1, 4, 256, 64, 64 + q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) softmax_scale = d**-0.5 + compress_rate = 4 - expected = csa_streamindex.reference_csa_streamindex_score( - q, compressed, weights, softmax_scale=softmax_scale + expected = csa_streamindex.reference_csa_streamindex_score_head_major( + q, compressed, weights, softmax_scale=softmax_scale, compress_rate=compress_rate ) - actual = csa_streamindex.csa_streamindex_score( + actual = csa_streamindex.csa_streamindex_score_head_major( q, compressed, weights, softmax_scale=softmax_scale, + compress_rate=compress_rate, block_q=128, block_w=128, interpret=True, ) + np.testing.assert_allclose(actual, expected, rtol=1e-1, atol=1e-1) - np.testing.assert_allclose(actual, expected, rtol=1e-2, atol=1e-2) - - def test_tpu_compile_smoke_production_tiles(self): - """Compiles and executes with interpret=False on TPU hardware (DeepSeek-V4 production shapes).""" - if jax.default_backend() != "tpu": - self.skipTest("TPU hardware required for Mosaic compilation smoke test.") - b, s, h, d = 1, 4096, 64, 128 - w = s // 4 - scale = d**-0.5 + def test_head_major_gradient_parity(self): + """Verifies that head-major custom_vjp gradients match reference autograd.""" + b, h, s, w, d = 1, 4, 128, 128, 32 key1, key2, key3 = jax.random.split(self.key, 3) - q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) - compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) - weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) - - fn = jax.jit( - lambda q, k, w: csa_streamindex.csa_streamindex_score( - q, k, w, softmax_scale=scale, block_q=128, block_w=512, interpret=False - ) - ) - out = fn(q, compressed, weights).block_until_ready() - self.assertEqual(out.shape, (b, s, w)) - - def test_vjp_backward_parity(self): - """Verifies that backward gradients of custom VJP match reference autograd.""" - key1, key2, key3, key4 = jax.random.split(self.key, 4) - b, s, w, h, d = 2, 256, 128, 32, 64 - scale = d**-0.5 - q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) - compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) + q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) + comp = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) - cotangent = jax.random.normal(key4, (b, s, w), dtype=jnp.float32) + scale = 32.0**-0.5 - def loss_kernel(q, k, w): - out = csa_streamindex.csa_streamindex_score( - q, k, w, softmax_scale=scale, block_q=128, block_w=128, interpret=True + def loss_kernel(q, comp, weights): + return jnp.sum( + csa_streamindex.csa_streamindex_score_head_major( + q, comp, weights, softmax_scale=scale, block_q=128, block_w=128, interpret=True + ) ) - return jnp.sum(out * cotangent) - def loss_ref(q, k, w): - out = csa_streamindex.reference_csa_streamindex_score( - q, k, w, softmax_scale=scale + def loss_ref(q, comp, weights): + return jnp.sum( + csa_streamindex.reference_csa_streamindex_score_head_major( + q, comp, weights, softmax_scale=scale + ) ) - return jnp.sum(out * cotangent) - _, (dq_k, dk_k, dw_k) = jax.value_and_grad(loss_kernel, argnums=(0, 1, 2))(q, compressed, weights) - _, (dq_r, dk_r, dw_r) = jax.value_and_grad(loss_ref, argnums=(0, 1, 2))(q, compressed, weights) + g_q_k, g_c_k, g_w_k = jax.grad(loss_kernel, argnums=(0, 1, 2))(q, comp, weights) + g_q_r, g_c_r, g_w_r = jax.grad(loss_ref, argnums=(0, 1, 2))(q, comp, weights) - np.testing.assert_allclose(dw_k, dw_r, rtol=1e-3, atol=1e-3) - np.testing.assert_allclose(dq_k.astype(jnp.float32), dq_r.astype(jnp.float32), rtol=1e-3, atol=1e-3) - np.testing.assert_allclose(dk_k.astype(jnp.float32), dk_r.astype(jnp.float32), rtol=1e-3, atol=1e-3) + np.testing.assert_allclose(g_q_k, g_q_r, rtol=1e-3, atol=1e-3) + np.testing.assert_allclose(g_c_k, g_c_r, rtol=1e-3, atol=1e-3) + np.testing.assert_allclose(g_w_k, g_w_r, rtol=1e-3, atol=1e-3) - def test_tpu_backward_smoke(self): - """Verifies that backward pass compiles and runs on TPU hardware.""" + def test_tpu_compile_smoke_production_tiles(self): + """Compiles and executes with interpret=False on TPU hardware (DeepSeek-V4 production shapes).""" if jax.default_backend() != "tpu": - self.skipTest("TPU hardware required for backward smoke test.") - b, s, h, d = 1, 1024, 64, 128 + self.skipTest("TPU hardware required for Mosaic compilation smoke test.") + b, h, s, d = 1, 64, 4096, 128 w = s // 4 scale = d**-0.5 - key1, key2, key3, key4 = jax.random.split(self.key, 4) - q = jax.random.normal(key1, (b, s, h, d), dtype=jnp.bfloat16) + key1, key2, key3 = jax.random.split(self.key, 3) + q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) - cotangent = jax.random.normal(key4, (b, s, w), dtype=jnp.float32) - @jax.jit - def grad_fn(q, k, w): - def loss(q, k, w): - out = csa_streamindex.csa_streamindex_score( + fn = jax.jit( + lambda q, k, w: csa_streamindex.csa_streamindex_score_head_major( q, k, w, softmax_scale=scale, block_q=128, block_w=512, interpret=False ) - return jnp.sum(out * cotangent) - return jax.grad(loss, argnums=(0, 1, 2))(q, k, w) - - dq, dk, dw = grad_fn(q, compressed, weights) - self.assertEqual(dq.shape, q.shape) - self.assertEqual(dk.shape, compressed.shape) - self.assertEqual(dw.shape, weights.shape) + ) + out = fn(q, compressed, weights).block_until_ready() + self.assertEqual(out.shape, (b, s, w)) class TestDeepseekv4IndexerIntegration(unittest.TestCase): @@ -239,10 +212,8 @@ def test_indexer_kernel_vs_einsum_output_parity(self): q_latent = jax.random.normal(key2, (b, s, q_lora), dtype=jnp.bfloat16) pos = jnp.arange(s, dtype=jnp.int32)[None, :] - # 1. Forward with use_csa_streamindex_kernel=False out_einsum = indexer_einsum(hidden, q_latent, pos) - # 2. Forward with use_csa_streamindex_kernel=True (intercept to set interpret=True on CPU) real_kernel_fn = csa_streamindex.csa_streamindex_score_head_major def interpret_kernel_fn(*args, **kwargs): @@ -255,7 +226,7 @@ def interpret_kernel_fn(*args, **kwargs): np.testing.assert_array_equal(out_kernel, out_einsum) def test_ar_decode_fallback(self): - """Verifies that when seq_len < 128 (e.g. seq_len=64 with windows formed), einsum path is used.""" + """Verifies that when seq_len < 128, einsum path is used.""" config_kernel = self._get_config(use_csa_streamindex_kernel=True) b, s, emb_dim, q_lora = 1, 64, config_kernel.emb_dim, config_kernel.q_lora_rank @@ -275,61 +246,6 @@ def test_ar_decode_fallback(self): mock_kernel.assert_not_called() self.assertEqual(out.shape, (b, s, min(32, s // 4))) - def test_head_major_kernel_vs_einsum_parity(self): - """Verifies numerical parity for head-major csa_streamindex_score_head_major.""" - key = jax.random.PRNGKey(42) - key1, key2, key3 = jax.random.split(key, 3) - b, h, s, w, d = 1, 4, 256, 128, 64 - q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) - compressed = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) - weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) - softmax_scale = d**-0.5 - - expected = csa_streamindex.reference_csa_streamindex_score_head_major( - q, compressed, weights, softmax_scale=softmax_scale - ) - actual = csa_streamindex.csa_streamindex_score_head_major( - q, - compressed, - weights, - softmax_scale=softmax_scale, - block_q=128, - block_w=128, - interpret=True, - ) - np.testing.assert_allclose(actual, expected, rtol=1e-1, atol=1e-1) - - def test_head_major_gradient_parity(self): - """Verifies that head-major custom_vjp gradients match reference autograd.""" - b, h, s, w, d = 1, 4, 128, 128, 32 - key = jax.random.PRNGKey(42) - key1, key2, key3 = jax.random.split(key, 3) - q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) - comp = jax.random.normal(key2, (b, w, d), dtype=jnp.bfloat16) - weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) - scale = 32.0**-0.5 - - def loss_kernel(q, comp, weights): - return jnp.sum( - csa_streamindex.csa_streamindex_score_head_major( - q, comp, weights, softmax_scale=scale, block_q=128, block_w=128, interpret=True - ) - ) - - def loss_ref(q, comp, weights): - return jnp.sum( - csa_streamindex.reference_csa_streamindex_score_head_major( - q, comp, weights, softmax_scale=scale - ) - ) - - g_q_k, g_c_k, g_w_k = jax.grad(loss_kernel, argnums=(0, 1, 2))(q, comp, weights) - g_q_r, g_c_r, g_w_r = jax.grad(loss_ref, argnums=(0, 1, 2))(q, comp, weights) - - np.testing.assert_allclose(g_q_k, g_q_r, rtol=1e-3, atol=1e-3) - np.testing.assert_allclose(g_c_k, g_c_r, rtol=1e-3, atol=1e-3) - np.testing.assert_allclose(g_w_k, g_w_r, rtol=1e-3, atol=1e-3) - def test_jaxpr_verification(self): """Verifies that jaxpr contains pallas_call when enabled and dot_general when disabled.""" q = jnp.zeros((1, 4, 256, 64), dtype=jnp.bfloat16) @@ -347,16 +263,12 @@ def compute_scores(q, compressed, weights, use_kernel): q, compressed, weights, softmax_scale=scale ) - # Kernel enabled trace jaxpr_kernel = jax.make_jaxpr(compute_scores, static_argnums=(3,))(q, compressed, weights, True) - jaxpr_kernel_str = str(jaxpr_kernel) - self.assertIn("pallas_call", jaxpr_kernel_str) + self.assertIn("pallas_call", str(jaxpr_kernel)) - # Kernel disabled trace jaxpr_einsum = jax.make_jaxpr(compute_scores, static_argnums=(3,))(q, compressed, weights, False) - jaxpr_einsum_str = str(jaxpr_einsum) - self.assertNotIn("pallas_call", jaxpr_einsum_str) - self.assertIn("dot_general", jaxpr_einsum_str) + self.assertNotIn("pallas_call", str(jaxpr_einsum)) + self.assertIn("dot_general", str(jaxpr_einsum)) if __name__ == "__main__": From 354c0bf85ebf1ee543725fa121cfc31620dd0e9a Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 31 Aug 2026 21:56:02 +0000 Subject: [PATCH 08/13] Simplify CSA kernel dispatch: remove defensive shard_map and _as_nd_init --- src/maxtext/layers/attention_compressed.py | 73 +++------------------- tests/unit/csa_streamindex_test.py | 10 +-- 2 files changed, 14 insertions(+), 69 deletions(-) diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index a6dd5aaf89..1738d5d9f0 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -428,15 +428,6 @@ def prime_prefill_cache_state( cache.overlap_gate.set_value(overlap_gate_to_write) -def _as_nd_init(init_fn: Any) -> Any: - """Adapts a 2/3-arg Flax initializer to a 5-arg NdInitializer if needed.""" - def wrapped(key, shape, dtype, *args, **kwargs): - try: - return init_fn(key, shape, dtype, *args, **kwargs) - except TypeError: - return init_fn(key, shape, dtype) - return wrapped - class BaseDeepseekCompressor(nnx.Module): """Shared base class for DeepSeek-V4 long-range attention compressors. @@ -464,7 +455,6 @@ def __init__( ): self.config = config self.compress_rate = compress_ratio - kernel_init = _as_nd_init(kernel_init) self.head_dim = config.head_dim self.dtype = config.dtype self.weight_dtype = config.weight_dtype @@ -712,7 +702,6 @@ def __init__( """ self.config = config self.compress_rate = compress_ratio - kernel_init = _as_nd_init(kernel_init) self.index_n_heads = config.indexer_n_heads self.index_head_dim = config.indexer_head_dim self.index_topk = config.indexer_topk @@ -893,58 +882,14 @@ def indexer_compressor_fn(buf_kv, buf_gate): q = jnp.transpose(q, (0, 2, 1, 3)) q = self.rotary_emb(q, position_ids, unsqueeze_dim=1) weights = self.weights_proj(hidden_states).astype(jnp.float32) * self.weights_scaling - - block_q = 128 - use_kernel = getattr(self.config, "use_csa_streamindex_kernel", False) and (seq_len >= block_q) - - if use_kernel: - mesh = getattr(self.config, "mesh", None) or getattr(self, "mesh", None) - if mesh is None: - try: - mesh = maxtext_utils.get_mesh_from_config(self.config) - except (AttributeError, ValueError, KeyError): - mesh = None - total_batch_shards = 1 - if mesh is not None: - for axis_name in ("data", "fsdp", "fsdp_transpose", "expert", "context"): - if axis_name in mesh.shape: - total_batch_shards *= mesh.shape[axis_name] - if mesh is not None and total_batch_shards > 1 and (batch_size % total_batch_shards == 0): - q_pspec = jax.sharding.PartitionSpec( - ("data", "fsdp", "fsdp_transpose", "expert", "context"), - None, - None, - None, - ) - out_pspec = jax.sharding.PartitionSpec( - ("data", "fsdp", "fsdp_transpose", "expert", "context"), - None, - None, - ) - @functools.partial( - jax.shard_map, - mesh=mesh, - in_specs=(q_pspec, out_pspec, out_pspec), - out_specs=out_pspec, - check_vma=False, - ) - def _shard_mapped_streamindex(local_q, local_comp, local_weights): - return csa_streamindex.csa_streamindex_score_head_major( - q=local_q, - compressed=local_comp, - weights=local_weights, - softmax_scale=self.softmax_scale, - compress_rate=self.compress_rate, - ) - index_scores = _shard_mapped_streamindex(q, compressed, weights) - else: - index_scores = csa_streamindex.csa_streamindex_score_head_major( - q=q, - compressed=compressed, - weights=weights, - softmax_scale=self.softmax_scale, - compress_rate=self.compress_rate, - ) + if self.config.use_csa_streamindex_kernel: + index_scores = csa_streamindex.csa_streamindex_score_head_major( + q=q, + compressed=compressed, + weights=weights, + softmax_scale=self.softmax_scale, + compress_rate=self.compress_rate, + ) else: compressed_kv = jnp.expand_dims(compressed, axis=1) compressed_kv = jnp.broadcast_to( @@ -965,7 +910,7 @@ def _shard_mapped_streamindex(local_q, local_comp, local_weights): future_mask = (block_positions[:, None, :] + self.compress_rate) > (position_ids[:, :, None] + 1) # Apply the mask to the scores if not already applied by the kernel - if not use_kernel: + if not self.config.use_csa_streamindex_kernel: index_scores = jnp.where(future_mask, jnp.full_like(index_scores, -jnp.inf), index_scores) combined_invalid = future_mask diff --git a/tests/unit/csa_streamindex_test.py b/tests/unit/csa_streamindex_test.py index 5b52c3ee0c..e8e44cf320 100644 --- a/tests/unit/csa_streamindex_test.py +++ b/tests/unit/csa_streamindex_test.py @@ -225,13 +225,13 @@ def interpret_kernel_fn(*args, **kwargs): np.testing.assert_array_equal(out_kernel, out_einsum) - def test_ar_decode_fallback(self): - """Verifies that when seq_len < 128, einsum path is used.""" - config_kernel = self._get_config(use_csa_streamindex_kernel=True) - b, s, emb_dim, q_lora = 1, 64, config_kernel.emb_dim, config_kernel.q_lora_rank + def test_indexer_einsum_when_disabled(self): + """Verifies that when use_csa_streamindex_kernel=False, einsum path is used.""" + config_einsum = self._get_config(use_csa_streamindex_kernel=False) + b, s, emb_dim, q_lora = 1, 128, config_einsum.emb_dim, config_einsum.q_lora_rank indexer = DeepseekV4Indexer( - config=config_kernel, + config=config_einsum, compress_ratio=4, rotary_embedding=self.rotary, rngs=nnx.Rngs(0), From 17d2b92871161f5188975b32a7af346147aa53a0 Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 31 Aug 2026 21:59:26 +0000 Subject: [PATCH 09/13] Rename csa_streamindex_score_head_major to canonical csa_streamindex_score --- .../kernels/attention/csa_streamindex.py | 34 ++++++++-------- src/maxtext/layers/attention_compressed.py | 2 +- tests/unit/csa_streamindex_test.py | 40 +++++++++---------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/maxtext/kernels/attention/csa_streamindex.py b/src/maxtext/kernels/attention/csa_streamindex.py index f1f817c438..91446eae8b 100644 --- a/src/maxtext/kernels/attention/csa_streamindex.py +++ b/src/maxtext/kernels/attention/csa_streamindex.py @@ -30,7 +30,7 @@ import jax.numpy as jnp -def csa_streamindex_score_head_major_kernel( +def csa_streamindex_score_kernel( q_ref, # [num_heads, block_q, head_dim] k_ref, # [block_w, head_dim] w_ref, # [block_q, num_heads] @@ -39,7 +39,7 @@ def csa_streamindex_score_head_major_kernel( softmax_scale: float, compress_rate: int = 0, ): - """Fused Pallas TPU kernel for head-major [num_heads, block_q, head_dim] input with 2D MXU matmul.""" + """Fused Pallas TPU kernel with 2D MXU matmul.""" num_heads, block_q, head_dim = q_ref.shape block_w, _ = k_ref.shape @@ -70,7 +70,7 @@ def csa_streamindex_score_head_major_kernel( out_ref[...] = s_weighted.astype(out_ref.dtype) -def _csa_streamindex_score_head_major_pallas_fwd( +def _csa_streamindex_score_pallas_fwd( q: jax.Array, compressed: jax.Array, weights: jax.Array, @@ -114,7 +114,7 @@ def _csa_streamindex_score_head_major_pallas_fwd( out = pl.pallas_call( functools.partial( - csa_streamindex_score_head_major_kernel, + csa_streamindex_score_kernel, softmax_scale=softmax_scale, compress_rate=compress_rate, ), @@ -132,7 +132,7 @@ def _csa_streamindex_score_head_major_pallas_fwd( @functools.partial(jax.custom_vjp, nondiff_argnums=(3, 4, 5, 6, 7)) -def csa_streamindex_score_head_major( +def csa_streamindex_score( q: jax.Array, compressed: jax.Array, weights: jax.Array, @@ -143,7 +143,7 @@ def csa_streamindex_score_head_major( interpret: bool = False, ) -> jax.Array: """Computes CSA StreamIndex scores using head-major [B, H, S, D] q layout with 2D MXU matmul.""" - return _csa_streamindex_score_head_major_pallas_fwd( + return _csa_streamindex_score_pallas_fwd( q, compressed, weights, @@ -155,7 +155,7 @@ def csa_streamindex_score_head_major( ) -def _csa_streamindex_score_head_major_fwd( +def _csa_streamindex_score_fwd( q: jax.Array, compressed: jax.Array, weights: jax.Array, @@ -165,7 +165,7 @@ def _csa_streamindex_score_head_major_fwd( block_w: int | None = None, interpret: bool = False, ) -> tuple[jax.Array, tuple[jax.Array, jax.Array, jax.Array]]: - out = _csa_streamindex_score_head_major_pallas_fwd( + out = _csa_streamindex_score_pallas_fwd( q, compressed, weights, @@ -178,7 +178,7 @@ def _csa_streamindex_score_head_major_fwd( return out, (q, compressed, weights) -def _csa_streamindex_score_head_major_bwd( +def _csa_streamindex_score_bwd( softmax_scale: float, compress_rate: int, block_q: int | None, @@ -191,7 +191,7 @@ def _csa_streamindex_score_head_major_bwd( q, compressed, weights = res _, vjp_fn = jax.vjp( functools.partial( - reference_csa_streamindex_score_head_major, + reference_csa_streamindex_score, softmax_scale=softmax_scale, compress_rate=compress_rate, ), @@ -203,12 +203,12 @@ def _csa_streamindex_score_head_major_bwd( return dq, dk, dw -csa_streamindex_score_head_major.defvjp( - _csa_streamindex_score_head_major_fwd, _csa_streamindex_score_head_major_bwd +csa_streamindex_score.defvjp( + _csa_streamindex_score_fwd, _csa_streamindex_score_bwd ) -def reference_csa_streamindex_score_head_major( +def reference_csa_streamindex_score( q: jax.Array, compressed: jax.Array, weights: jax.Array, @@ -216,7 +216,7 @@ def reference_csa_streamindex_score_head_major( softmax_scale: float, compress_rate: int = 0, ) -> jax.Array: - """Reference score computation matching the pure JAX einsum path for head-major q.""" + """Reference score computation matching the pure JAX einsum path.""" scores = jnp.einsum("bhsd,bwd->bhsw", q.astype(jnp.float32), compressed.astype(jnp.float32)) scores = jax.nn.relu(scores) * softmax_scale index_scores = jnp.einsum("bhsw,bsh->bsw", scores, weights.astype(jnp.float32)) @@ -231,6 +231,6 @@ def reference_csa_streamindex_score_head_major( return index_scores -# Public aliases for standard naming conventions -csa_streamindex_score = csa_streamindex_score_head_major -reference_csa_streamindex_score = reference_csa_streamindex_score_head_major +# Backward compatibility aliases +csa_streamindex_score_head_major = csa_streamindex_score +reference_csa_streamindex_score_head_major = reference_csa_streamindex_score diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index 1738d5d9f0..82ce351834 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -883,7 +883,7 @@ def indexer_compressor_fn(buf_kv, buf_gate): q = self.rotary_emb(q, position_ids, unsqueeze_dim=1) weights = self.weights_proj(hidden_states).astype(jnp.float32) * self.weights_scaling if self.config.use_csa_streamindex_kernel: - index_scores = csa_streamindex.csa_streamindex_score_head_major( + index_scores = csa_streamindex.csa_streamindex_score( q=q, compressed=compressed, weights=weights, diff --git a/tests/unit/csa_streamindex_test.py b/tests/unit/csa_streamindex_test.py index e8e44cf320..1516554760 100644 --- a/tests/unit/csa_streamindex_test.py +++ b/tests/unit/csa_streamindex_test.py @@ -36,8 +36,8 @@ class TestCsaStreamIndexScoreKernel(unittest.TestCase): def setUp(self): self.key = jax.random.PRNGKey(42) - def test_head_major_parity_exact_multiple(self): - """Verifies numerical parity for head-major input matching reference einsum.""" + def test_parity_exact_multiple(self): + """Verifies numerical parity matching reference einsum on exact block multiples.""" key1, key2, key3 = jax.random.split(self.key, 3) b, h, s, w, d = 2, 64, 256, 128, 128 q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) @@ -45,10 +45,10 @@ def test_head_major_parity_exact_multiple(self): weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) softmax_scale = d**-0.5 - expected = csa_streamindex.reference_csa_streamindex_score_head_major( + expected = csa_streamindex.reference_csa_streamindex_score( q, compressed, weights, softmax_scale=softmax_scale ) - actual = csa_streamindex.csa_streamindex_score_head_major( + actual = csa_streamindex.csa_streamindex_score( q, compressed, weights, @@ -59,7 +59,7 @@ def test_head_major_parity_exact_multiple(self): ) np.testing.assert_allclose(actual, expected, rtol=1e-1, atol=1e-1) - def test_head_major_parity_non_multiples(self): + def test_parity_non_multiples(self): """Verifies padding handling when seq_len and compressed_len are not multiples of block sizes.""" key1, key2, key3 = jax.random.split(self.key, 3) b, h, s, w, d = 2, 32, 150, 70, 64 @@ -68,10 +68,10 @@ def test_head_major_parity_non_multiples(self): weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) softmax_scale = d**-0.5 - expected = csa_streamindex.reference_csa_streamindex_score_head_major( + expected = csa_streamindex.reference_csa_streamindex_score( q, compressed, weights, softmax_scale=softmax_scale ) - actual = csa_streamindex.csa_streamindex_score_head_major( + actual = csa_streamindex.csa_streamindex_score( q, compressed, weights, @@ -82,7 +82,7 @@ def test_head_major_parity_non_multiples(self): ) np.testing.assert_allclose(actual, expected, rtol=1e-2, atol=1e-2) - def test_head_major_causal_parity(self): + def test_causal_mask_parity(self): """Verifies numerical parity with in-VMEM causal masking.""" key1, key2, key3 = jax.random.split(self.key, 3) b, h, s, w, d = 1, 4, 256, 64, 64 @@ -92,10 +92,10 @@ def test_head_major_causal_parity(self): softmax_scale = d**-0.5 compress_rate = 4 - expected = csa_streamindex.reference_csa_streamindex_score_head_major( + expected = csa_streamindex.reference_csa_streamindex_score( q, compressed, weights, softmax_scale=softmax_scale, compress_rate=compress_rate ) - actual = csa_streamindex.csa_streamindex_score_head_major( + actual = csa_streamindex.csa_streamindex_score( q, compressed, weights, @@ -107,8 +107,8 @@ def test_head_major_causal_parity(self): ) np.testing.assert_allclose(actual, expected, rtol=1e-1, atol=1e-1) - def test_head_major_gradient_parity(self): - """Verifies that head-major custom_vjp gradients match reference autograd.""" + def test_gradient_parity(self): + """Verifies that custom_vjp gradients match reference autograd.""" b, h, s, w, d = 1, 4, 128, 128, 32 key1, key2, key3 = jax.random.split(self.key, 3) q = jax.random.normal(key1, (b, h, s, d), dtype=jnp.bfloat16) @@ -118,14 +118,14 @@ def test_head_major_gradient_parity(self): def loss_kernel(q, comp, weights): return jnp.sum( - csa_streamindex.csa_streamindex_score_head_major( + csa_streamindex.csa_streamindex_score( q, comp, weights, softmax_scale=scale, block_q=128, block_w=128, interpret=True ) ) def loss_ref(q, comp, weights): return jnp.sum( - csa_streamindex.reference_csa_streamindex_score_head_major( + csa_streamindex.reference_csa_streamindex_score( q, comp, weights, softmax_scale=scale ) ) @@ -150,7 +150,7 @@ def test_tpu_compile_smoke_production_tiles(self): weights = jax.random.normal(key3, (b, s, h), dtype=jnp.float32) fn = jax.jit( - lambda q, k, w: csa_streamindex.csa_streamindex_score_head_major( + lambda q, k, w: csa_streamindex.csa_streamindex_score( q, k, w, softmax_scale=scale, block_q=128, block_w=512, interpret=False ) ) @@ -214,13 +214,13 @@ def test_indexer_kernel_vs_einsum_output_parity(self): out_einsum = indexer_einsum(hidden, q_latent, pos) - real_kernel_fn = csa_streamindex.csa_streamindex_score_head_major + real_kernel_fn = csa_streamindex.csa_streamindex_score def interpret_kernel_fn(*args, **kwargs): kwargs["interpret"] = True return real_kernel_fn(*args, **kwargs) - with mock.patch.object(csa_streamindex, "csa_streamindex_score_head_major", side_effect=interpret_kernel_fn): + with mock.patch.object(csa_streamindex, "csa_streamindex_score", side_effect=interpret_kernel_fn): out_kernel = indexer_kernel(hidden, q_latent, pos) np.testing.assert_array_equal(out_kernel, out_einsum) @@ -241,7 +241,7 @@ def test_indexer_einsum_when_disabled(self): q_latent = jnp.ones((b, s, q_lora), dtype=jnp.bfloat16) pos = jnp.arange(s, dtype=jnp.int32)[None, :] - with mock.patch.object(csa_streamindex, "csa_streamindex_score_head_major") as mock_kernel: + with mock.patch.object(csa_streamindex, "csa_streamindex_score") as mock_kernel: out = indexer(hidden, q_latent, pos) mock_kernel.assert_not_called() self.assertEqual(out.shape, (b, s, min(32, s // 4))) @@ -255,11 +255,11 @@ def test_jaxpr_verification(self): def compute_scores(q, compressed, weights, use_kernel): if use_kernel: - return csa_streamindex.csa_streamindex_score_head_major( + return csa_streamindex.csa_streamindex_score( q, compressed, weights, softmax_scale=scale, block_q=128, block_w=128 ) else: - return csa_streamindex.reference_csa_streamindex_score_head_major( + return csa_streamindex.reference_csa_streamindex_score( q, compressed, weights, softmax_scale=scale ) From 2fe77bd6304b04199c6fd2d2bc0f9fd450220c8b Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 31 Aug 2026 22:35:00 +0000 Subject: [PATCH 10/13] Revert stray nd_dense_init changes to standard Flax initializers --- src/maxtext/layers/attention_compressed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index 82ce351834..4f6e81fbad 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -686,7 +686,7 @@ def __init__( config: Any, compress_ratio: int, rotary_embedding: Any, - kernel_init: Any = nd_dense_init(1.0, "fan_in", "truncated_normal"), + kernel_init: Any = nnx.initializers.normal(stddev=0.02), quant: Optional[Quant] = None, rngs: Optional[nnx.Rngs] = None, ): @@ -945,7 +945,7 @@ def __init__( config: Any, compress_ratio: int, rotary_embedding: Any, - kernel_init: Any = nd_dense_init(1.0, "fan_in", "truncated_normal"), + kernel_init: Any = nnx.initializers.normal(stddev=0.02), quant: Optional[Quant] = None, model_mode: str = MODEL_MODE_TRAIN, rngs: Optional[nnx.Rngs] = None, From eb5497a88d472f381e1f5f763c69ba23b1618560 Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 31 Aug 2026 22:37:14 +0000 Subject: [PATCH 11/13] Remove unused imports and clean up formatting in attention_compressed.py --- src/maxtext/layers/attention_compressed.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index 4f6e81fbad..c89505dcd0 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -16,7 +16,6 @@ import enum -import functools from typing import Any, Optional, Tuple import jax @@ -24,7 +23,6 @@ from jax.ad_checkpoint import checkpoint_name from jax.sharding import Mesh from maxtext.utils import max_utils -from maxtext.utils import maxtext_utils from flax import nnx @@ -428,7 +426,6 @@ def prime_prefill_cache_state( cache.overlap_gate.set_value(overlap_gate_to_write) - class BaseDeepseekCompressor(nnx.Module): """Shared base class for DeepSeek-V4 long-range attention compressors. From 54b6b046427730e6bf353f51656cc4832cb1a472 Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 31 Aug 2026 22:40:10 +0000 Subject: [PATCH 12/13] Revert q_fp32 back to original q.astype matching main --- src/maxtext/layers/attention_compressed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/maxtext/layers/attention_compressed.py b/src/maxtext/layers/attention_compressed.py index c89505dcd0..1b93e9f808 100644 --- a/src/maxtext/layers/attention_compressed.py +++ b/src/maxtext/layers/attention_compressed.py @@ -892,9 +892,9 @@ def indexer_compressor_fn(buf_kv, buf_gate): compressed_kv = jnp.broadcast_to( compressed_kv, (batch_size, self.index_n_heads, compressed_len, self.index_head_dim) ) - q_fp32 = q.astype(jnp.float32) + q = q.astype(jnp.float32) compressed_kv = compressed_kv.astype(jnp.float32) - scores = jnp.einsum("bhsd,bhwd->bhsw", q_fp32, compressed_kv) + scores = jnp.einsum("bhsd,bhwd->bhsw", q, compressed_kv) scores = jax.nn.relu(scores) * self.softmax_scale index_scores = jnp.einsum("bhsw,bsh->bsw", scores, weights) From 1814305e731d31fc9acd4eee7289285869a28df2 Mon Sep 17 00:00:00 2001 From: Octa Trifan Date: Mon, 31 Aug 2026 22:41:51 +0000 Subject: [PATCH 13/13] Remove obsolete backward compatibility aliases from csa_streamindex.py --- src/maxtext/kernels/attention/csa_streamindex.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/maxtext/kernels/attention/csa_streamindex.py b/src/maxtext/kernels/attention/csa_streamindex.py index 91446eae8b..5696f1042c 100644 --- a/src/maxtext/kernels/attention/csa_streamindex.py +++ b/src/maxtext/kernels/attention/csa_streamindex.py @@ -231,6 +231,3 @@ def reference_csa_streamindex_score( return index_scores -# Backward compatibility aliases -csa_streamindex_score_head_major = csa_streamindex_score -reference_csa_streamindex_score_head_major = reference_csa_streamindex_score