Sparse attention#
Lightning indexer#
lightning_indexer selects a shared pool of key positions using multi-head weighted
ReLU scores. It returns indices, not attention outputs or differentiable scores:
from attn_gym.sparse import lightning_indexer
indices = lightning_indexer(q, k, weights, topk=128, causal=True)
# q: [B, T, H, D], k: [B, T, D], weights: [B, T, H]
# indices: [B, T, 128], int32
# DeepSeek compressed sparse attention: one candidate per 4 consecutive tokens.
indices = lightning_indexer(q, k_compressed, weights, topk=128, causal=True, compress_ratio=4)
# k_compressed: [B, T // 4, D]; query t may select candidates s < (t + 1) // 4
The score for a query/candidate pair is
sum_h(weights[h] * relu(dot(q[h], k))) / sqrt(H * D).
Weights may be negative. Without packed offsets, k holds S = T // compress_ratio
candidates, each summarizing compress_ratio consecutive tokens (a trailing partial window
forms no candidate). For packed documents, pass paired cu_seqlens and cu_seqlens_k:
compression and causal positions restart per document, and output indices are document-local.
With causal selection, query t considers only candidates s < (t + 1) // compress_ratio, those
whose tokens all lie at positions <= t (candidates 0..t with the default ratio of 1);
rows with fewer than topk candidates contain -1 padding. compress_ratio != 1 requires
causal=True. Mask padding before gathering: a raw PyTorch index of -1 selects the last
position rather than an invalid position. topk=0 returns an empty last dimension.
Output order and tie-breaking are unspecified, including between repeated calls.
Implementations and device dispatch#
impl="reference"evaluates PyTorch scoring and Top-K on CPU or CUDA. FP16/BF16/FP32 inputs accumulate in FP32; FP64 inputs retain FP64. It materializes the score intermediates and is intended for correctness checks and small inputs.impl="fused"(the default) selects CuTe on SM100/SM103, or Triton on other NVIDIA GPUs with compute capability 9.0 or newer, including Hopper. Both optimized implementations keep their selection state on chip. CuTe separates score generation from radix Top-K and reuses a per-call FP32 score slab capped at 32 MiB and 1024 query rows, independent of batch size. Large inputs are processed in slabs rather than an unbounded quadratic score allocation. This scratch is additional to the returned indices; Triton needs no global score scratch.kernel_options={"backend": "cute"}or{"backend": "triton"}overrides that choice. Omit options for automatic selection. Options are rejected forimpl="reference". Unsupported shapes, missing dependencies, and launch errors propagate; there is no retry with another backend.
| Restriction | CuTe | Triton |
|---|---|---|
| GPU | SM100 or SM103 | SM90 or newer |
| Input dtype | FP16 or BF16, shared by all inputs | FP16 or BF16, shared by all inputs |
Heads H |
Positive and even | 1..256 |
Head dimension D |
Positive, divisible by 16 | 8..256, divisible by 8 |
Sequence length T |
1..2**20 |
1..2**20 |
topk |
Non-negative; missing candidates pad with -1 |
Non-negative; missing candidates pad with -1; per-tile cost grows with topk |
| Layout | Q/K last stride 1, bases and non-singleton outer strides 16-byte aligned; weights may be strided | Q/K last stride 1, bases and outer strides 16-byte aligned; weights may be strided |
For nonzero topk, CuTe limits candidate capacity S to 2**22 so at least one query
pair's FP32 scores fit its 32 MiB workspace. This includes inactive packed capacity.
CuTe accepts independently permuted or padded outer dimensions and broadcast inputs without materializing contiguous copies. Weights need only element alignment, not TMA alignment.
CuTe additionally requires the optional linear dependencies. It supports SM100 and SM103
(including GB300), not every Blackwell variant; other supported devices use Triton by default.
Compilation and training scope#
Both fused backends support the public API under torch.compile(fullgraph=True),
including changing batch/sequence sizes with dynamic=True. Backend selection and TMA
setup happen inside a private operator on real CUDA tensors, rather than during tracing.
Static-shape CUDA Graph capture/replay is supported after warming up compilation:
import torch
compiled_indexer = torch.compile(lightning_indexer, fullgraph=True, dynamic=True)
indices = compiled_indexer(q, k, weights, 128, causal=True)
Selection is nondifferentiable. Inputs may require gradients, but the integer result has no gradient function. Gather attention can train its own Q/K/V computation with these indices held fixed. It does not propagate gradients through the selection step into the indexer's queries, keys, or scoring weights. This API supplies no surrogate gradient or indexer-training loss.
Selection with NaN/Inf scores is unspecified and may differ across backends.
attn_gym.sparse.indexer.lightning_indexer(q, k, weights, topk, *, causal=False, compress_ratio=1, cu_seqlens=None, cu_seqlens_k=None, impl=Impl.FUSED, kernel_options=None)
#
Return the Top-K candidate indices for every (batch, query) row.
Computes a multi-head weighted ReLU score for each query-candidate pair and selects the topk highest-scoring candidates per query::
dots[b, t, h, s] = q[b, t, h, :] ยท k[b, s, :]
score[b, t, s] = sum_h(w[b, t, h] * relu(dots[b, t, h, s]))
/ sqrt(H * D)
output[b, t, :] = topk(score[b, t, :]).indices
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
q
|
Tensor
|
Query tensor, [B, T, H, D]. |
required |
k
|
Tensor
|
Key candidate pool shared across heads, [B, S, D]. Without packed
offsets, |
required |
weights
|
Tensor
|
Per-head weights, [B, T, H]. May be negative. |
required |
topk
|
int
|
Non-negative output width. Rows with fewer valid candidates are padded with -1, including when S is zero or topk exceeds S. |
required |
causal
|
bool
|
If True, query t can only select candidates
|
False
|
compress_ratio
|
int
|
Number of consecutive tokens summarized by each
candidate, as in DeepSeek compressed sparse attention. A trailing
partial window forms no candidate. In packed mode grouping and the
causal query position restart at each document boundary: document d has
|
1
|
cu_seqlens
|
Tensor | None
|
Packed offsets shaped |
None
|
cu_seqlens_k
|
Tensor | None
|
Packed candidate offsets shaped |
None
|
impl
|
Impl | str
|
Impl.REFERENCE (or |
FUSED
|
kernel_options
|
dict[str, str] | None
|
Fused backend override: |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Contiguous [B, T, topk] INT32 indices. Order and tie-breaking are unspecified, and |
Tensor
|
CuTe's may differ across repeated calls. Under |
Tensor
|
|
Tensor
|
identical inputs (results may differ between backends), and CuTe returns valid |
Tensor
|
indices ascending, resolving equal ordered-FP32 scores toward lower indices. |
Tensor
|
Rows with fewer than topk valid candidates contain -1 padding after the valid |
Tensor
|
indices; topk=0 returns an empty last dimension. Packed selection excludes other |
Tensor
|
documents before top-k, not by filtering its result. |
Fused backends support torch.compile(fullgraph=True) and CUDA Graph replay.
Both require FP16/BF16 inputs and T <= 220. CuTe requires
SM100/SM103, even H, D divisible by 16, and Q/K with unit last strides and 16-byte-aligned
bases and non-singleton outer strides. Other Q/K strides may vary independently;
weights may have arbitrary strides and need only element alignment.
Triton requires SM90 or newer, H <= 256, D <= 256 divisible by 8, and Q/K with
unit last strides and 16-byte-aligned bases and outer strides; weights may be strided.
Its register-resident selection makes per-tile cost grow with topk.
CuTe reuses a per-call FP32 score workspace capped at 32 MiB and 1024 query
rows. For nonzero topk, candidate capacity S must be at most 222 so one query
pair fits that budget. Large inputs use slabs rather than an unbounded quadratic
score allocation.
This workspace is additional to the returned indices and is not shared across calls.
Packed CuTe calls also prepare an INT32 [T, 2] bounds buffer, reused by scoring and
top-k to restrict work to each document. Triton computes those bounds in its kernel
prologue without a separate buffer.
Selection with NaN/Inf scores is unspecified and may differ across backends. Indices are nondifferentiable even when inputs require gradients. Training attention over the selected positions does not propagate gradients through selection into q, k, or weights.