Skip to content

Commit ca6c82b

Browse files
committed
Optimize MLA generate_mask with TPU-native vectorized threshold cutoff and configurable exact-k pruning
Replace the broadcast comparison in `generate_mask` with a vectorized threshold cutoff comparison, adding the `indexer_mask_exact_topk` configuration flag to dynamically support raw thresholding or exact-k prefix-sum pruning while retaining `generate_mask_broadcast` for cross-checking in unit tests. - Two options: - Option 1 (`indexer_mask_exact_topk = True`, default): Runs a cumulative prefix-sum pruning scan (`jnp.cumsum`) over active boundaries, keeping exactly the first k selected tokens and discarding tied overflows. - Option 2 (`indexer_mask_exact_topk = False`): Executes raw vectorized threshold comparison. Highly performant (enables speedups), but may unmask > k tokens under boundary ties. - Verification: - Retained original broadcast implementation as `generate_mask_broadcast` and verified bit-for-bit equivalence in `attention_test.py` across equivalence, exact-k tie, and unsorted tests (Passed). - Added unit tests for equivalence, raw thresholding ties, exact-k pruning ties, and sequence-length bypass in `attention_test.py` (Passed). TAG=agy CONV=859f8f98-4c27-4a27-8df2-9fd1c7fa5d61
1 parent f13ccd1 commit ca6c82b

4 files changed

Lines changed: 262 additions & 20 deletions

File tree

src/maxtext/configs/base.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ indexer_n_heads: 64
422422
indexer_topk: 2048
423423
indexer_use_approx_top_k: false
424424
indexer_approx_top_k_recall: 0.95
425+
indexer_mask_exact_topk: true
425426
# Determines the training strategy for the indexer:
426427
# - false (Dense Warm-up): Computes indexer loss over all tokens. Used with `trainable_parameters_mask` to freeze other model parameters.
427428
# - true (Sparse Training): Computes indexer loss over top-k tokens only and detaches the indexer input for independent optimization.

src/maxtext/configs/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,14 @@ class AttentionIndexer(BaseModel):
687687
False, description="Whether to use approximate top-k selection for the indexer on TPU."
688688
)
689689
indexer_approx_top_k_recall: float = Field(0.95, description="Recall target for approximate top-k selection.")
690+
indexer_mask_exact_topk: bool = Field(
691+
True,
692+
description=(
693+
"When True, enforces that exactly k elements are unmasked by the indexer under boundary ties."
694+
" When False, uses raw thresholding which is faster but may unmask > k elements"
695+
" during ties."
696+
),
697+
)
690698

691699

692700
class Llama4Attention(BaseModel):

src/maxtext/layers/attention_mla.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -221,26 +221,45 @@ def apply_partial_rope(
221221
x = jnp.concatenate([x_pe, x_nope], axis=-1)
222222
return x
223223

224-
def generate_mask(self, topk_indices, s):
225-
"""
226-
Creates a mask for top-k indices.
224+
def generate_mask(self, indexer_score: Array, topk_values: Array) -> Array:
225+
"""Creates a mask for top-k indices
227226
228227
Args:
229-
topk_indices: [b, t, k] int - The indices to keep.
230-
s: int - The total size to select from.
228+
indexer_score: [b, t, s] float - The computed relevance scores for all tokens.
229+
topk_values: [b, t, k] float - The top-k selected score values from jax.lax.top_k.
231230
232231
Returns:
233-
mask: [b, t, s] - `0.0` at topk_indices, `DEFAULT_MASK_VALUE` (large negative) elsewhere.
232+
mask: [b, t, s] - `0.0` for top-k selected elements, `DEFAULT_MASK_VALUE` elsewhere.
234233
"""
235-
# 1. Create a range [0, 1, ..., s-1]
236-
# 2. Broadcast compare against [b, t, k] to get [b, t, k, s]
237-
# 3. Use .any() to see if a s-index is present in any of the k slots
238-
is_topk = (jnp.arange(s) == topk_indices[..., None]).any(axis=-2)
239-
# 4. Use where to select between 0.0 and the mask value
240-
# cast values to dtype
234+
cutoff_threshold = topk_values[..., -1:]
235+
241236
val_true = jnp.array(0.0, dtype=self.dtype)
242237
val_false = jnp.array(DEFAULT_MASK_VALUE, dtype=self.dtype)
243-
return jnp.where(is_topk, val_true, val_false)
238+
239+
if self.config.indexer_mask_exact_topk:
240+
# Prune ties by keeping only the first k unmasked tokens along sequence dimension
241+
k = topk_values.shape[-1]
242+
is_strictly_greater = indexer_score > cutoff_threshold
243+
is_equal = indexer_score == cutoff_threshold
244+
245+
# Use cumsum to rank both strictly greater and equal tokens (XLA fuses these scans)
246+
sg_rank = jnp.cumsum(is_strictly_greater.astype(jnp.int32), axis=-1)
247+
eq_rank = jnp.cumsum(is_equal.astype(jnp.int32), axis=-1)
248+
249+
# Cap strictly greater elements to exactly k
250+
sg_kept = is_strictly_greater & (sg_rank <= k)
251+
252+
# Calculate how many equals we still have room for
253+
num_sg_kept = jnp.minimum(sg_rank[..., -1:], k)
254+
num_eq_to_keep = k - num_sg_kept
255+
256+
selected = sg_kept | (is_equal & (eq_rank <= num_eq_to_keep))
257+
258+
return jnp.where(selected, val_true, val_false)
259+
else:
260+
# Raw threshold cutoff masking (optional: enables speedups, but may unmask > k tokens under indexer scores ties)
261+
raw_mask = indexer_score >= cutoff_threshold
262+
return jnp.where(raw_mask, val_true, val_false)
244263

245264
def __call__(
246265
self,
@@ -367,16 +386,16 @@ def __call__(
367386

368387
# TopK selection based on index score
369388
if self.config.indexer_use_approx_top_k:
370-
_, topk_indices = jax.lax.approx_max_k(
389+
topk_values, topk_indices = jax.lax.approx_max_k(
371390
indexer_score,
372391
k=self.indexer_topk,
373392
recall_target=self.config.indexer_approx_top_k_recall,
374393
)
375394
else:
376-
_, topk_indices = jax.lax.top_k(indexer_score, k=self.indexer_topk) # topk_indices [b, t, k]
395+
topk_values, topk_indices = jax.lax.top_k(indexer_score, k=self.indexer_topk) # [b, t, k]
377396

378397
# Create Sparse Index Mask: 0 and large negatives
379-
indexer_mask = self.generate_mask(topk_indices, k.shape[1]) # [b, t, s]
398+
indexer_mask = self.generate_mask(indexer_score, topk_values) # [b, t, s]
380399

381400
# Re-apply attention mask after TopK: in case number of unmasked tokens < TopK
382401
if attention_mask is not None:

tests/unit/attention_test.py

Lines changed: 218 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2011,8 +2011,8 @@ def test_indexer_loss(self):
20112011
attention_mask = self.get_causal_mask_for_indexer(batch_size, q_len, kv_len)
20122012
indexer_score += attention_mask
20132013

2014-
topk_indices = jnp.array([[[0, 1], [0, 1], [0, 1]], [[0, 1], [0, 1], [0, 1]]])
2015-
indexer_mask = mla.indexer.generate_mask(topk_indices, kv_len) + attention_mask
2014+
topk_values, _ = jax.lax.top_k(indexer_score, k=2)
2015+
indexer_mask = mla.indexer.generate_mask(indexer_score, topk_values) + attention_mask
20162016

20172017
loss_dense = mla.calculate_indexer_loss(
20182018
indexer_score=indexer_score,
@@ -2061,8 +2061,8 @@ def test_indexer_loss_kl_divergence_zero(self):
20612061
# Indexer score matches the shape and is uniform
20622062
indexer_score = jnp.zeros((batch_size, q_len, kv_len)) + attention_mask
20632063

2064-
topk_indices = jnp.array([[[0, 1], [0, 1], [0, 1]], [[0, 1], [0, 1], [0, 1]]])
2065-
indexer_mask = mla.indexer.generate_mask(topk_indices, kv_len) + attention_mask
2064+
topk_values, _ = jax.lax.top_k(indexer_score, k=2)
2065+
indexer_mask = mla.indexer.generate_mask(indexer_score, topk_values) + attention_mask
20662066

20672067
loss = mla.calculate_indexer_loss(
20682068
indexer_score=indexer_score,
@@ -2230,6 +2230,220 @@ def full_indexer_loss_fn(inputs_q, inputs_kv, low_rank_q, mla, sparse_training=s
22302230
self.assertTrue(jnp.all(grad_kv == 0.0))
22312231
self.assertTrue(jnp.all(grad_low_rank_q == 0.0))
22322232

2233+
def old_generate_mask(self, topk_indices, s, dtype=jnp.float32):
2234+
"""Old baseline implementation using pairwise broadcast comparison.
2235+
2236+
Retained exclusively in unit tests as a ground-truth reference for cross-checking mathematical equivalence.
2237+
"""
2238+
is_topk = (jnp.arange(s) == topk_indices[..., None]).any(axis=-2)
2239+
val_true = jnp.array(0.0, dtype=dtype)
2240+
val_false = jnp.array(DEFAULT_MASK_VALUE, dtype=dtype)
2241+
return jnp.where(is_topk, val_true, val_false)
2242+
2243+
def test_generate_mask_threshold_equivalence(self):
2244+
"""Verifies that TPU-native threshold cutoff masking matches exact top-k selection when scores are unique."""
2245+
mla_config_args = self.config_arguments.copy()
2246+
mla_config_args["use_indexer"] = True
2247+
mla_config_args["indexer_topk"] = 64
2248+
mla_config_args["attention"] = "dot_product"
2249+
2250+
_, mla = self.init_mla(mla_config_args, rope_type="default")
2251+
2252+
jax_rng = jax.random.PRNGKey(0)
2253+
b, t, s, k = 2, 128, 1024, 64
2254+
dtype = jnp.float32
2255+
2256+
scores = jax.random.normal(jax_rng, (b, t, s), dtype=dtype)
2257+
# Add tiny position-dependent epsilon so all scores are strictly unique (preventing sorting tie-breaker divergence)
2258+
scores = scores + jnp.arange(s, dtype=dtype) * (1e-6 / s)
2259+
topk_values, topk_indices = jax.lax.top_k(scores, k=k)
2260+
2261+
# Call original broadcast Indexer logic for cross-checking
2262+
mask_original = self.old_generate_mask(topk_indices, s, dtype=dtype)
2263+
2264+
# Call actual optimized Indexer logic
2265+
mask_threshold = mla.indexer.generate_mask(scores, topk_values)
2266+
2267+
self.assertTrue(jnp.allclose(mask_original, mask_threshold, atol=1e-5))
2268+
2269+
def test_generate_mask_threshold_ties_exact_k(self):
2270+
"""Verifies that prefix-sum pruning guarantees exactly k unmasked tokens even with boundary ties."""
2271+
mla_config_args = self.config_arguments.copy()
2272+
mla_config_args["use_indexer"] = True
2273+
mla_config_args["indexer_topk"] = 3
2274+
mla_config_args["attention"] = "dot_product"
2275+
mla_config_args["indexer_mask_exact_topk"] = True
2276+
2277+
_, mla = self.init_mla(mla_config_args, rope_type="default")
2278+
2279+
k = 3
2280+
dtype = jnp.float32
2281+
scores = jnp.array(
2282+
[
2283+
[
2284+
[0.9, 0.8, 0.5, 0.5, 0.1, 0.0, -1.0, -2.0, -3.0, -4.0],
2285+
[0.5, 0.5, 0.9, 0.8, 0.1, 0.0, -1.0, -2.0, -3.0, -4.0],
2286+
]
2287+
],
2288+
dtype=dtype,
2289+
)
2290+
2291+
topk_values, topk_indices = jax.lax.top_k(scores, k=k)
2292+
mask = mla.indexer.generate_mask(scores, topk_values)
2293+
mask_original = self.old_generate_mask(topk_indices, s=scores.shape[-1], dtype=dtype)
2294+
2295+
val_true = jnp.array(0.0, dtype=dtype)
2296+
2297+
self.assertFalse(jnp.isnan(mask).any())
2298+
self.assertEqual(jnp.sum(mask[0, 0] == val_true), 3) # Exactly 3 tokens (exact k) unmasked
2299+
self.assertEqual(jnp.sum(mask[0, 1] == val_true), 3) # Exactly 3 tokens (exact k) unmasked
2300+
2301+
# Assert equivalence to original broadcast baseline
2302+
self.assertTrue(jnp.allclose(mask_original, mask, atol=1e-5))
2303+
2304+
# Assert exact unmasked elements
2305+
np.testing.assert_array_equal(
2306+
mask[0, 0] == val_true,
2307+
[True, True, True, False, False, False, False, False, False, False],
2308+
)
2309+
np.testing.assert_array_equal(
2310+
mask[0, 1] == val_true,
2311+
[True, False, True, True, False, False, False, False, False, False],
2312+
)
2313+
2314+
def test_generate_mask_threshold_ties_unsorted(self):
2315+
"""Verifies that elements strictly greater than cutoff are preserved even if they appear after ties."""
2316+
mla_config_args = self.config_arguments.copy()
2317+
mla_config_args["use_indexer"] = True
2318+
mla_config_args["indexer_topk"] = 3
2319+
mla_config_args["attention"] = "dot_product"
2320+
mla_config_args["indexer_mask_exact_topk"] = True
2321+
2322+
_, mla = self.init_mla(mla_config_args, rope_type="default")
2323+
2324+
k = 3
2325+
dtype = jnp.float32
2326+
scores = jnp.array(
2327+
[
2328+
[
2329+
# 0.9 is strictly greater but appears after three 0.5s.
2330+
# If cumsum was used unconditionally on (score >= cutoff), 0.9 would get rank 4 and be masked out!
2331+
# Correct behavior: keep 0.9, and the first two 0.5s to reach exactly k=3.
2332+
[0.5, 0.5, 0.5, 0.9, 0.1, 0.0, -1.0, -2.0, -3.0, -4.0],
2333+
]
2334+
],
2335+
dtype=dtype,
2336+
)
2337+
2338+
topk_values, topk_indices = jax.lax.top_k(scores, k=k)
2339+
mask = mla.indexer.generate_mask(scores, topk_values)
2340+
mask_original = self.old_generate_mask(topk_indices, s=scores.shape[-1], dtype=dtype)
2341+
2342+
val_true = jnp.array(0.0, dtype=dtype)
2343+
2344+
self.assertFalse(jnp.isnan(mask).any())
2345+
self.assertEqual(jnp.sum(mask[0, 0] == val_true), 3) # Exactly 3 tokens unmasked
2346+
2347+
# Assert equivalence to original broadcast baseline
2348+
self.assertTrue(jnp.allclose(mask_original, mask, atol=1e-5))
2349+
2350+
np.testing.assert_array_equal(
2351+
mask[0, 0] == val_true,
2352+
[True, True, False, True, False, False, False, False, False, False],
2353+
)
2354+
2355+
def test_generate_mask_approx_k_overflow(self):
2356+
"""Verifies exact-k guarantee when approx_top_k underestimates the threshold."""
2357+
mla_config_args = self.config_arguments.copy()
2358+
mla_config_args["use_indexer"] = True
2359+
mla_config_args["indexer_topk"] = 3
2360+
mla_config_args["attention"] = "dot_product"
2361+
mla_config_args["indexer_mask_exact_topk"] = True
2362+
2363+
_, mla = self.init_mla(mla_config_args, rope_type="default")
2364+
2365+
dtype = jnp.float32
2366+
scores = jnp.array(
2367+
[
2368+
[
2369+
# Simulating approx_max_k returning an underestimated threshold of 0.5.
2370+
# However, 0.9, 0.8, 0.7, 0.6 (4 elements) are strictly > 0.5.
2371+
[0.9, 0.8, 0.7, 0.6, 0.5, 0.0, -1.0, -2.0, -3.0, -4.0],
2372+
]
2373+
],
2374+
dtype=dtype,
2375+
)
2376+
2377+
# Artificially supply a threshold of 0.5 at the end
2378+
topk_values = jnp.array([[[1.0, 1.0, 0.5]]], dtype=dtype)
2379+
mask = mla.indexer.generate_mask(scores, topk_values)
2380+
2381+
val_true = jnp.array(0.0, dtype=dtype)
2382+
2383+
self.assertFalse(jnp.isnan(mask).any())
2384+
self.assertEqual(jnp.sum(mask[0, 0] == val_true), 3) # Exactly 3 tokens unmasked
2385+
2386+
# It should keep the first 3 elements that are > 0.5 which are 0.9, 0.8, 0.7
2387+
np.testing.assert_array_equal(
2388+
mask[0, 0] == val_true,
2389+
[True, True, True, False, False, False, False, False, False, False],
2390+
)
2391+
2392+
def test_generate_mask_threshold_ties_raw(self):
2393+
"""Verifies that raw thresholding allows more than k unmasked tokens under boundary ties."""
2394+
mla_config_args = self.config_arguments.copy()
2395+
mla_config_args["use_indexer"] = True
2396+
mla_config_args["indexer_topk"] = 3
2397+
mla_config_args["attention"] = "dot_product"
2398+
mla_config_args["indexer_mask_exact_topk"] = False
2399+
2400+
_, mla = self.init_mla(mla_config_args, rope_type="default")
2401+
2402+
k = 3
2403+
dtype = jnp.float32
2404+
scores = jnp.array(
2405+
[
2406+
[
2407+
[0.9, 0.8, 0.5, 0.5, 0.1, 0.0, -1.0, -2.0, -3.0, -4.0],
2408+
[0.7, 0.6, 0.5, 0.2, 0.1, 0.0, -1.0, -2.0, -3.0, -4.0],
2409+
]
2410+
],
2411+
dtype=dtype,
2412+
)
2413+
2414+
topk_values, _ = jax.lax.top_k(scores, k=k)
2415+
mask = mla.indexer.generate_mask(scores, topk_values)
2416+
2417+
self.assertFalse(jnp.isnan(mask).any())
2418+
self.assertEqual(jnp.sum(mask[0, 0] == 0.0), 4) # 4 tokens unmasked (tied >= 0.5)
2419+
self.assertEqual(jnp.sum(mask[0, 1] == 0.0), 3) # Exactly 3 tokens unmasked (no boundary ties)
2420+
2421+
def test_generate_mask_sequence_smaller_than_k(self):
2422+
"""Verifies that the indexer handles sequence length smaller than or equal to k by returning None."""
2423+
mla_config_args = self.config_arguments.copy()
2424+
mla_config_args["use_indexer"] = True
2425+
mla_config_args["indexer_topk"] = 10 # k = 10
2426+
mla_config_args["attention"] = "dot_product"
2427+
2428+
cfg, mla = self.init_mla(mla_config_args, rope_type="default")
2429+
2430+
dtype = jnp.float32
2431+
inputs_q = jnp.zeros((1, 5, cfg.emb_dim), dtype=dtype)
2432+
inputs_kv = jnp.zeros((1, 5, cfg.emb_dim), dtype=dtype) # s = 5 <= k
2433+
low_rank_q = jnp.zeros((1, 5, cfg.q_lora_rank), dtype=dtype)
2434+
inputs_positions = jnp.zeros((1, 5), dtype=jnp.int32)
2435+
2436+
mask, indices, score = mla.indexer(
2437+
inputs_q=inputs_q,
2438+
low_rank_q=low_rank_q,
2439+
inputs_kv=inputs_kv,
2440+
inputs_positions=inputs_positions,
2441+
)
2442+
2443+
self.assertIsNone(mask)
2444+
self.assertIsNone(indices)
2445+
self.assertIsNone(score)
2446+
22332447

22342448
class Qwen3NextGatedDeltaNetTest(unittest.TestCase):
22352449
"""Test for the Gated Delta Net in Qwen3-Next"""

0 commit comments

Comments
 (0)