Skip to content

perf(indexed_attention): query-tiled QSA selection scores + deterministic radix select - #23

Merged
faresobeid merged 1 commit into
mainfrom
feat/qsa-selection-query-tiles
Oct 26, 2026
Merged

faresobeid merged 1 commit into
mainfrom
feat/qsa-selection-query-tiles

Conversation

@faresobeid

Copy link
Copy Markdown
Contributor

Speeds up select_indexed_blocks (Qwen3.8-Flash-Next QSA selection) by ~4x at real shapes. The selection stays the same: the scores are bitwise equal to main's, and so is the set of selected blocks, ties included. The output order is now deterministic (ascending block index).

Problem

  • selection_score_kernel ran one query per CTA. The 4 indexer heads were padded to 16 MMA rows, and every query re-read all of its visible keys (~550 GB of key reads at 131K).
  • radix_select_kernel wrote picks with atomic_add, so the output order changed from run to run. TileLang also put two __syncthreads() around every histogram atomic, because digit was a shared alloc_var. A single thread scanned the 256 bins serially, and partial ties at the threshold were resolved by a single thread looping over every column.

Change

  • Score kernel. One CTA now takes 16 consecutive queries x 4 heads = 64 MMA rows, over the union of their key ranges (absolute key tiles, so documents inside a tile are handled). The heads are combined as (relu(h0)+relu(h1)) + (relu(h2)+relu(h3)), the same order main's reduce_sum used, so the scores are bitwise equal.
  • Radix select.
    • Vectorized 16-byte loads. The score rows are padded to a multiple of 4 floats (score_workspace, allocated once per call and reused for every chunk).
    • No barriers inside the histogram loop.
    • Parallel threshold-bin search with T.cumsum.
    • The final pass compacts in column order with a block prefix sum. It takes the greater blocks plus the lowest-index tied blocks, which is the rule main already used. There are no atomics, so the output is sorted and deterministic. The sentinel is still num_blocks.
  • API: select_indexed_blocks is unchanged. compute_selection_scores returns the same thing (a view into the padded buffer).

Numbers

H200. Single-document micro-batch. Indexer inputs from prime-rl's IndexedGatedAttention with random weights on locally correlated hidden states, the same setup as #22. "Layer" is one QSA attention module fwd+bwd, no AC. CP rows emulate the last CP rank on one GPU (keys and KV of the other ranks are precomputed; communication is not included).

queries x keys select main select this PR speedup layer main -> PR selection share of layer
16K x 16K 2.8 ms 0.7 ms 3.9x 54 -> 52 ms 5% -> 1%
64K x 64K 32.0 ms 7.2 ms 4.4x 326 -> 300 ms 10% -> 2%
131K x 131K 126.8 ms 29.7 ms 4.3x 847 -> 748 ms 15% -> 4%
CP8 last rank: 16K x 131K 27.2 ms 7.1 ms 3.8x 137 -> 115 ms 20% -> 6%
CP8 last rank at 1M: 131K x 1M 2186 ms 516 ms 4.2x 3511 -> 1830 ms 62% -> 28%

Per 1 GiB chunk at CP8 16K x 131K (8192 rows):

main this PR
score kernel 9.30 ms 1.74 ms
radix select 4.61 ms 1.85 ms
  • On main, the score kernel reads keys at an effective 4.7 TB/s, mostly from L2. At 1M the 64 MB key array no longer fits in L2.
  • The radix select now reads the fp32 score table 5 times at about 2.7 TB/s. That table traffic is most of what is left. Removing it would need a fused score+select, which this PR does not do.

Validation

  • Against main's kernels on 18 cases: score bits on all visible entries, the selected set per row, the exact output against a sorted torch reference (stable sort, lowest index first among ties), and repeat equality. All equal. The cases cover odd block counts (37, 1021, 1023), up to 5 documents, query counts that are not a multiple of 16, heavy ties (zeroed and duplicated keys), and topk 64 and 512.
  • Through prime-rl's IndexedGatedAttention (20K tokens, 2 documents): sorted indexer indices, the attention output, d(hidden) and d(q_proj.weight) are bitwise equal to main (feat(indexed_attention): windowed GQA forward + deterministic key-tile dK/dV (Triton) #22's plan sorts the indices anyway).
  • New tests/indexed_attention/test_selection.py (GPU): exact output against the reference, including ties, plus repeat equality. 6 passed on H200.

Notes

  • TileLang prints a false-positive data-race warning for the threshold-bin write in T.Parallel (only one bin satisfies the condition) and for the score store.
  • Using this in prime-rl needs a wheel release and a pin bump.

@faresobeid
faresobeid marked this pull request as ready for review October 26, 2026 11:37
@faresobeid
faresobeid merged commit 76d8d82 into main Oct 26, 2026
12 checks passed
samsja pushed a commit to PrimeIntellect-ai/prime-rl that referenced this pull request Oct 26, 2026
…on plan under AC (#4279)

Bumps prime-kernels from `v0.1.0-3101960` to `v0.1.0-b52bd9d` (wheel
URLs, `deps/prime-kernels` submodule, `uv.lock`). Also adds
`prime_kernels::indexed_attention_plan` to `MANDATORY_SAVE_OPERATIONS`,
so full AC does not rebuild the plan in the recompute.

## What the new kernels bring (Qwen3.8-Flash-Next QSA; API unchanged)
- PrimeIntellect-ai/prime-kernels#22: `indexed_attention` as windowed
GQA Triton kernels. A new plan op groups 5 queries per window. 1.2-3.7x
per QSA layer fwd+bwd on H200 (16K: 116 -> 31 ms, 64K: 352 -> 191 ms,
CP8 rank 16K x 131K: 84 -> 65 ms) and lower peak memory. The dK/dV
backward is key-major with no atomics, so it is bitwise deterministic.
- PrimeIntellect-ai/prime-kernels#23: `select_indexed_blocks` uses
query-tiled scores and a radix select without atomics. 3.8-4.4x on
selection (64K: 32 -> 7.2 ms, CP8 at 1M: 2186 -> 516 ms). Scores and
selected sets are bitwise equal to before, and the output order is now
deterministic (ascending).
- The release also carries the DeepSelect wheel (#17), used by #3958;
this PR does not pin it.

## Plan save under full AC
The plan depends only on the indices and syncs to host for its sizes.
Without the save it is built twice per step (forward and recompute).

Profiled step (4-layer Qwen3.8, one QSA layer, full AC):
`_window_keys_kernel` launches 4 -> 2, so the plan is built once.

## Numbers (1 node, 8x H200, FSDP, 4-layer Qwen3.8 `q38-4l-small`, full
AC, SGD; median step after 2 warmup steps, 8 steps)

| tokens / rank | main (3101960) | new kernels, plan not saved | this PR
|
|---|---|---|---|
| 16K | 1.087 s | 0.908 s | 0.904 s |
| 64K | 3.754 s | 3.613 s | 3.593 s |

- Only 1 of the 4 layers is QSA, so the full-model gain is smaller than
the per-layer gain. Saving the plan saves about 20 ms per QSA layer at
64K.
- Loss per step matches main to about 1e-4 relative, the same run-to-run
noise main shows. The two new-kernel arms give bitwise-equal losses over
all 8 steps.

## Validation
- H200 with the release wheel: `test_qwen3_8_flash_next.py` +
`test_checkpointing.py` (16 passed). prime-kernels `smoke_test.py
prime-kernels` OK, prime-kernels `tests/` (6 passed).
- `pytest -m "not gpu" tests/unit/orchestrator
tests/unit/test_configs.py tests/unit/train` (620 passed), `ruff check`,
`ruff format --check`, `uv lock --check` (uv 0.13.5).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches training correctness under full activation checkpointing and
swaps prebuilt GPU kernel wheels; mis-saving the plan could change
numerics or performance on QSA paths.
> 
> **Overview**
> Bumps **prime-kernels** from `v0.1.0-3101960` to **`v0.1.0-b52bd9d`**
on Linux x86_64 and aarch64 (`pyproject.toml` wheel URLs and matching
**`uv.lock`** hashes).
> 
> Adds **`prime_kernels::indexed_attention_plan`** to
**`MANDATORY_SAVE_OPERATIONS`** in activation checkpointing so **full
AC** keeps the indexed-attention plan across forward and recompute
instead of rebuilding it (paired with the new kernels’ plan op).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
96d8822. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants