Skip to content

Commit 76dae1b

Browse files
committed
Add optional Approximate Top-K configuration for MLA Indexer
1 parent ae15d1c commit 76dae1b

4 files changed

Lines changed: 70 additions & 1 deletion

File tree

src/maxtext/configs/base.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,8 @@ use_indexer: false
397397
indexer_head_dim: 128
398398
indexer_n_heads: 64
399399
indexer_topk: 2048
400+
indexer_use_approx_top_k: false
401+
indexer_approx_top_k_recall: 0.95
400402
# Determines the training strategy for the indexer:
401403
# - false (Dense Warm-up): Computes indexer loss over all tokens. Used with `trainable_parameters_mask` to freeze other model parameters.
402404
# - 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,10 @@ class AttentionIndexer(BaseModel):
668668
description="Determines the training strategy for the indexer: Dense Warm-up or Sparse Training stage.",
669669
)
670670
indexer_loss_scaling_factor: float = Field(0.0, description="Multiplier for the indexer KL divergence loss.")
671+
indexer_use_approx_top_k: bool = Field(
672+
False, description="Whether to use approximate top-k selection for the indexer on TPU."
673+
)
674+
indexer_approx_top_k_recall: float = Field(0.95, description="Recall target for approximate top-k selection.")
671675

672676

673677
class Llama4Attention(BaseModel):

src/maxtext/layers/attention_mla.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,14 @@ def __call__(
366366
indexer_score += attention_mask
367367

368368
# TopK selection based on index score
369-
_, topk_indices = jax.lax.top_k(indexer_score, k=self.indexer_topk) # topk_indices [b, t, k]
369+
if self.config.indexer_use_approx_top_k:
370+
_, topk_indices = jax.lax.approx_max_k(
371+
indexer_score,
372+
k=self.indexer_topk,
373+
recall_target=self.config.indexer_approx_top_k_recall,
374+
)
375+
else:
376+
_, topk_indices = jax.lax.top_k(indexer_score, k=self.indexer_topk) # topk_indices [b, t, k]
370377

371378
# Create Sparse Index Mask: 0 and large negatives
372379
indexer_mask = self.generate_mask(topk_indices, k.shape[1]) # [b, t, s]

tests/unit/attention_test.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1802,6 +1802,62 @@ def test_indexer_loss_kl_divergence_zero(self):
18021802

18031803
np.testing.assert_allclose(loss, 0.0, atol=1e-5)
18041804

1805+
def test_indexer_with_approx_top_k(self):
1806+
"""Verify indexer runs with both approx and exact top-k."""
1807+
for use_approx in [False, True]:
1808+
with self.subTest(indexer_use_approx_top_k=use_approx):
1809+
mla_config_args = self.config_arguments.copy()
1810+
mla_config_args["use_indexer"] = True
1811+
mla_config_args["indexer_use_approx_top_k"] = use_approx
1812+
mla_config_args["indexer_topk"] = 4 # Force indexer to run instead of returning early
1813+
mla_config_args["attention"] = "dot_product"
1814+
1815+
cfg, mla = self.init_mla(mla_config_args, rope_type="default")
1816+
1817+
lnx, decoder_segment_ids, decoder_positions = self.get_structured_data(cfg, cfg.dtype)
1818+
1819+
# Run forward pass which triggers indexer
1820+
out, _ = mla(
1821+
lnx,
1822+
lnx,
1823+
decoder_segment_ids=decoder_segment_ids,
1824+
inputs_positions=decoder_positions,
1825+
deterministic=True,
1826+
model_mode=MODEL_MODE_TRAIN,
1827+
)
1828+
self.assertIsNotNone(out)
1829+
1830+
def test_approx_top_k_recall(self):
1831+
"""Verify that approx_max_k meets the specified recall target compared to exact top_k."""
1832+
jax_rng = jax.random.PRNGKey(0)
1833+
1834+
# We need a large enough N to make the approximation meaningful.
1835+
# Use shape [batch=4, queries=16, N=1024]
1836+
batch, queries, N = 4, 16, 1024
1837+
K = 64
1838+
recall_target = 0.95
1839+
1840+
# Generate random scores
1841+
scores = jax.random.normal(jax_rng, (batch, queries, N))
1842+
1843+
# 1. Run exact Top-K
1844+
_, true_indices = jax.lax.top_k(scores, k=K) # [batch, queries, K]
1845+
1846+
# 2. Run approx Top-K
1847+
_, approx_indices = jax.lax.approx_max_k(scores, k=K, recall_target=recall_target) # [batch, queries, K]
1848+
1849+
# 3. Calculate Recall
1850+
# Broadcast compare true_indices [B, Q, K, 1] and approx_indices [B, Q, 1, K]
1851+
matches = (true_indices[..., None] == approx_indices[..., None, :]).any(axis=-1) # [B, Q, K]
1852+
num_matches = matches.sum(axis=-1) # [B, Q]
1853+
actual_recalls = num_matches / K # [B, Q]
1854+
mean_recall = jnp.mean(actual_recalls)
1855+
1856+
print(f"\nApprox Top-K Recall Target: {recall_target}, Actual Mean Recall: {mean_recall:.4f}")
1857+
1858+
# Assert that the actual recall is equal or exceeds the target.
1859+
self.assertGreaterEqual(mean_recall, recall_target)
1860+
18051861
def test_indexer_gradients(self):
18061862
# Test that gradients do NOT flow back to inputs
18071863
bsz, seqlen = 2, 8

0 commit comments

Comments
 (0)