flashinfer.top_k_varlen¶
- flashinfer.top_k_varlen(logits: Tensor, seq_lens: Tensor, top_k: int, *, row_starts: Tensor | None = None, max_window_len: int | None = None, absolute_indices: bool = False, page_table: Tensor | None = None, page_size: int | None = None, compress_ratio: int | None = None, next_n: int | None = None, pre_idx: Tensor | None = None, return_values: bool = False, out_indices: Tensor | None = None, out_values: Tensor | None = None, backend: Literal['radix', 'gvr', 'gvr_2', 'radix_cutlass', 'radix_filter', 'auto'] = 'auto', load_balance: bool = True, workspace: dict | None = None) Tuple[Tensor, Tensor | None]¶
Top-k selection per row over rows of different valid lengths.
logitsholds one row per query token and one score per key column;num_colsis the width of that static buffer, the same for every row. In decode mode (the default), each row is a request’s newest token (one row per request, ornext_nrows with speculative tokens) and its valid columns are the request’s cached keys, which start at column0: with one row per request and no compression they are[0, seq_lens[i]), and theseq_lensentry below gives their count undernext_n > 1orcompress_ratio > 1. In windowed (prefill) mode (row_startsgiven), each row is one prompt token of a packed chunk and its valid columns are its request’s window[row_starts[r], row_starts[r] + seq_lens[r]). The result is thetop_kindices of every row’s valid columns,-1padded where a row has fewer valid columns thantop_k, and withreturn_values=Truealso the selected scores. In decode mode, an index is the logits column, which is also the key’s position in its request’s cache, or the key’s physical KV slot whenpage_tableis given. In windowed (prefill) mode, an index is by default the position inside the window:0isrow_starts[r], the request’s first key, so a window position addresses the request’s cache exactly like a decode index. Withabsolute_indices=Truean index is instead the logits columnrow_starts[r] + position, which addresses the chunk’s packed key sequence directly.Backend selection¶
backend="auto"(default) runs the highest-ranked backend among those whose admission checks pass for the call. The checks cover dtype,top_k, layout and alignment, the mode arguments, the GPU and the installed CuTe DSL. The ranking is a static table over dtype,top_k,num_cols, request count and whether a valid hint (pre_idx) is given; it never reads the contents ofseq_lens, which would cost a device synchronization. Every backend returns the exact top-k set of each row for finite and infinite scores (ties at the K-th value may be broken differently), soautoonly ever affects speed. The table is maintained from measurements, revised between releases, and not part of the contract. Force a backend withbackend="radix","gvr","gvr_2","radix_filter"or"radix_cutlass".top_k_varlen.is_backend_supported(name, compute_capability)tells whether a backend exists for a GPU.Windowed (prefill) mode¶
row_startsswitches a call into windowed (prefill) mode.max_window_lenandabsolute_indicesapply only there (a decode call refusesmax_window_lenandabsolute_indices=TruewithValueError);next_n,compress_ratioandpre_idxbelong to decode mode (a windowed call refuses any value butNone,next_n=1andcompress_ratio=1included, withValueError).The mode exists for the DSA indexer’s prefill layout, where two things move a row’s keys away from column 0: several requests share one packed key axis, and chunked prefill or prefix caching puts a request’s earlier keys on that axis without rows. In decode every row is one request whose keys start at column 0. In prefill the indexer scores one chunk of the batch in a single grouped GEMM: the keys of every request, including the prefix cached by earlier chunks, occupy consecutive column ranges of one packed key axis, while only the chunk’s new tokens have rows. A row’s valid keys therefore start at its request’s column offset,
row_starts[r], and span the cached prefix plus the causal part of the chunk,seq_lens[r]columns; cells outside that window belong to other requests or to later tokens and are never read. Toy chunk, request A (2 new tokens) and request B (1 cached tokenb0, 3 new tokens),top_k=2:col: 0 1 | 2 3 4 5 row_starts seq_lens a0 a1 | b0 b1 b2 b3 row 0 a0 [ * . | . . . . ] 0 1 row 1 a1 [ * * | . . . . ] 0 2 row 2 b1 [ . . | * * . . ] 2 2 row 3 b2 [ . . | * * * . ] 2 3 row 4 b3 [ . . | * * * * ] 2 4
*marks a key inside the row’s window,.a cell outside it (garbage from the GEMM, never read as a candidate).row_startssays where the row’s request begins;seq_lensbecomes the window length (cached prefix + position + 1; the cachedb0is a column, not a row). Row 4 selectingb0andb2as its two top-k indices (top_k=2here) is returned as the positions0and2inside B (the default: a paged consumer maps them through B’s page table) or as the columns2and4oflogits(absolute_indices=True: the ragged consumer reads the packed KV buffer by column).max_window_len(here 4, the longest window) is only a bound the backend specializes on before the launch and defaults tonum_cols; while every window stays within the bound, it never changes which keys win.Rules of the mode: a window shorter than
top_kreturns all its positions; a window end pastnum_colsis clamped tonum_cols. Under a captured graph the contents ofrow_startsandseq_lensmay change between replays while every window stays withinmax_window_len. The backends that serve the mode, with their limits and CUDA-graph warm-up rule, are listed underbackend.- param logits:
2-D CUDA tensor of shape
(num_rows, num_cols)infloat32,bfloat16orfloat16; the dtypes, layouts and alignments each backend accepts are listed underbackend.- type logits:
torch.Tensor
- param seq_lens:
1-D
int32tensor with one entry per request, shape(num_rows // next_n,): each request’s KV-cache length. Units: in decode mode, KV tokens, before compression (the kernel divides bycompress_ratio); in windowed (prefill) mode, columns oflogits, after compression. Base case,next_n == 1andcompress_ratio == 1: requestiis rowiand ranks its firstseq_lens[i]columns; columns at or beyond that count are not candidates. With speculative decoding,next_n > 1: requestiowns rowsi * next_ntoi * next_n + next_n - 1, and rowtof the request (tfrom0tonext_n - 1) ranks its firstseq_lens[i] - next_n + t + 1columns, so the last draft token sees the whole cache and each earlier one sees one token less. With KV compression,compress_ratio > 1: every column stands forcompress_ratioKV tokens and the token count is divided by it (floor). The general per-row count is thereforemax(0, (seq_lens[i] - next_n + t + 1) // compress_ratio).seq_lensstays in tokens even withcompress_ratio > 1: the kernel converts tokens to columns itself, so a value erroneously given in columns would be converted twice and rank too few keys. A row with nothing to rank (its count is zero, as for an empty padding slot) comes back as-1in every slot. A row whose length is larger than the buffer is wide (count abovenum_cols) is ranked over thenum_colskeys the buffer holds; nothing is read beyond it. In windowed (prefill) mode there is one entry per row andseq_lens[r]is the window length of rowrin columns, after compression (seerow_starts).- type seq_lens:
torch.Tensor
- param top_k:
Number of top elements per row; the accepted values are backend-specific (see
backend).- type top_k:
int
- param row_starts:
Windowed (prefill) mode only; passing it switches the mode on (see “Windowed (prefill) mode” above). 1-D
int32tensor, contiguous, one entry per row: the column where rowr’s window begins, so rowrrankslogits[r, row_starts[r] : row_starts[r] + seq_lens[r]]. Starts must lie within the row; they are device data and are not validated on the host. DefaultNone.- type row_starts:
torch.Tensor, optional
- param max_window_len:
Windowed (prefill) mode only: an upper bound on every window length
seq_lens[r], in columns; defaults tonum_cols. The bound is consumed before the launch and fixed for a captured graph, so it must hold at every replay. Pass the batch’s longest window (seq_lens.max()) when it is smaller thannum_cols; the tighter the bound, the more the backend can specialize. A positiveint; a value abovenum_colscounts asnum_cols. Refused withValueErrorwithoutrow_starts.- type max_window_len:
int, optional
- param absolute_indices:
Windowed (prefill) mode only: write each hit as its column of
logits(window-local index + row_starts[r]) instead of the window-local position;-1padding is unchanged. In the DSA prefill layout the column axis is the chunk’s packed key sequence, so this is the flattened-KV position a ragged sparse-attention kernel consumes directly; a paged consumer keeps the default and maps the local position through its page table.Truewithoutrow_startsraisesValueError. DefaultFalse.- type absolute_indices:
bool, optional
- param page_table:
Decode mode only. The request’s page map: an
int32tensor, contiguous, with one row per request and at leastceil(num_cols / page_size)columns (one entry per page of the request’s cache, so every column oflogitsmaps through it); columns andpage_sizeare in the units oflogits. When given, each selected columncof requestqis written as the physical KV slotpage_table[q, c // page_size] * page_size + c % page_sizeinstead of the column;-1padding is unchanged. The backends that serve it, and their limits, are listed underbackend. DefaultNone.- type page_table:
torch.Tensor, optional
- param page_size:
Columns per
page_tableentry, a power of two up to2**30. Any value butNonewithoutpage_tableraisesValueError. DefaultNone, meaning1.- type page_size:
int, optional
- param compress_ratio:
Decode mode only: KV-index compression factor (
1for DSv3.2,4for DSv4), every logit column standing forcompress_ratioconsecutive KV tokens. It converts the token lengths inseq_lensinto per-row column counts,max(0, (seq_lens[i] - next_n + t + 1) // compress_ratio)(seeseq_lens); only the kernel can do that when one request length servesnext_nrows. The accepted values are backend-specific (seebackend). Any value butNonewithrow_startsraisesValueError: windowed (prefill) mode has one length per row and takes it in columns already, so there is nothing to convert, and a compressed layer passes its windows in compressed columns. DefaultNone, meaning1.- type compress_ratio:
int, optional
- param next_n:
Decode mode only: rows per request, the query tokens a request contributes to the current step (
1without speculative decoding); rowrbelongs to requestr // next_nand the per-row column counts are given underseq_lens. Any value butNonewithrow_startsraisesValueError: windowed (prefill) mode has one length per row, so a request’s extra tokens are plain rows with their own windows. DefaultNone, meaning1.- type next_n:
int, optional
- param pre_idx:
Decode mode only: a hint of each request’s top-k indices, a warm start for the backends that take one (how each backend consumes or ignores it is under
backend), as a contiguous, 16-byte-alignedint32tensor of shape[seq_lens.shape[0], top_k], one row per request. A hint that breaks this is discarded with aRuntimeWarningand the call runs hint-free (an explicit backend that requires one is refused). Any value butNonewithrow_startsraisesValueError. DefaultNone.- type pre_idx:
torch.Tensor, optional
- param return_values:
When
True, also return the selected logit values. Padded slots hold a backend-specific value (seebackend); testindices >= 0rather than the value. DefaultFalse.- type return_values:
bool, optional
- param out_indices:
Pre-allocated buffer for the indices:
int32, contiguous, 16-byte aligned, of shape(num_rows, top_k)or flat withnum_rows * top_kelements. The result is written into it and returned as a(num_rows, top_k)view of it. DefaultNone: a new tensor is allocated.- type out_indices:
torch.Tensor, optional
- param out_values:
Pre-allocated buffer for the values, in the dtype of
logitsand under the layout rules ofout_indices; used only withreturn_values=True. DefaultNone.- type out_values:
torch.Tensor, optional
- param backend:
Backend to use. Default
"auto"."radix"— CuTe DSL radix top-K.SM100, SM103, SM107, SM110, SM120 and SM121 with an installed CuTe DSL. fp32, bf16 or fp16; contiguous
logits; notop_klimit; anycompress_ratio;pre_idxaccepted and ignored; padded value slots are left unwritten. Decode mode only."gvr"— GVR (Guess-Verify-Refine) kernel.SM100, SM103 or SM107 with an installed CuTe DSL. fp32, bf16 or fp16;
num_colsa multiple of16 // itemsize(8 for fp16/bf16, 4 for fp32);top_kin {512, 1024, 2048};compress_ratio1 or 4. Requires a well-formedpre_idx; its contents affect only the speed, never the result.load_balance=Truebalances rows of unequal length and caps the batch at 1024 requests;Falsehas no cap and is the better choice when all rows have similar lengths; both are CUDA-graph safe. Padded value slots hold-FLT_MAXconverted todtype. Decode mode only."gvr_2"— Self-sampling GVR V2 kernel.SM100, SM103 or SM107 with an installed CuTe DSL. fp32 only; unit inner stride, row stride a multiple of 4 elements (for a single row, the width), 16-byte-aligned base, non-overlapping rows (a row stride wider than
num_colsis fine);top_kin {512, 1024, 2048};compress_ratio1 or 4;pre_idxoptional, affecting only the speed;load_balanceignored; padded value slots holdfinfo(float32).min; NaN ordering is implementation-specific. Decode mode, and the only backend of windowed (prefill) mode. CUDA graphs: a geometry must be compiled before it is captured. Warm up each decode geometry (row count,num_colsand row stride,top_k,next_n,compress_ratio, hinted or not) with one eager call on the stream that will capture, and each windowed geometry (row count,top_k,absolute_indicesand the bound:max_window_len, elsenum_cols) with an eager call orflashinfer.topk_varlen.warmup_prefillon that stream;flashinfer.topk_varlen.prefill_readytells whether a windowed geometry would launch without compiling. The warm-up also creates the backend’s workspace slab (21 MB per device and CUDA stream) that the graphs captured on that stream share, so those graphs must not be replayed concurrently with each other; graphs captured on different streams never share one. A capture whose geometry or slab is missing raisesRuntimeError. Slabs live for the process unless released withflashinfer.topk_varlen.release_gvr2_resources(device), which synchronizes the device, drops them and returns the bytes freed; graphs captured against them must then be re-captured after a fresh warm-up. A caller-provided"gvr2_workspace"(seeworkspace) replaces the slab. Paged output (page_table) is decode mode only, hint-free (nopre_idx) and indices only (noreturn_values);page_sizeis part of its warm-up key, so warm up each paged geometry with one eager paged call on the capturing stream."radix_cutlass"— Masked CUTLASS radix top-K.Every GPU FlashInfer supports (SM75 and up). fp32, bf16 or fp16; contiguous
logits; notop_klimit; anycompress_ratio;pre_idxaccepted and ignored; padded value slots hold0. Decode mode only."radix_filter"— Filtered-radix top-K.SM100, SM103 or SM107 with nvidia-cutlass-dsl >= 4.8. fp32, bf16 or fp16; any layout (a non-unit inner stride or a base not 32-byte aligned costs a copy);
1 <= top_k <= 16384;compress_ratio == 1only;pre_idxaccepted and ignored; nondeterministic ties; padded value slots hold-inf. Decode mode only."auto"— Automatic selection.Picks among the entries above as described under “Backend selection”.
- type backend:
{“radix”, “gvr”, “gvr_2”, “radix_cutlass”, “radix_filter”, “auto”}, optional
- param load_balance:
"gvr"only: chooses between its two kernel paths (seebackend). Every other backend ignores it. DefaultTrue.- type load_balance:
bool, optional
- param workspace:
Caller-provided scratch buffers, keyed by backend (the
"gvr_2"slab’s lifetime is underbackend):"gvr"withload_balance=True:"gvr_order_row"(int32[M],Mthe smallest power of 2 in[64, 1024]that is>= seq_lens.shape[0]) and"gvr_counters"(int32[2]); without them the path allocates per call, which is safe for any concurrency."gvr_2":"gvr2_workspace", a tensor of at leastflashinfer.topk_varlen.kernels.gvr2_topk_host.workspace_bytes()= 20,973,568 bytes, zero-initialized before first use and 16-byte aligned (a smaller or misaligned tensor raisesRuntimeError); it replaces the backend’s cached per-stream slab, for decode and windowed calls alike.
A workspace dict must not be shared by launches that can overlap (concurrent streams, or graphs replayed concurrently).
- type workspace:
dict, optional
- returns:
(indices, values) – Always a 2-tuple.
indicesisint32[num_rows, top_k].valuesholds the selected logits (same dtype aslogits) whenreturn_values=True, otherwiseNone.- rtype:
Tuple[torch.Tensor, Optional[torch.Tensor]]
- raises BackendSupportedError:
If
backend="auto"finds no backend whose checks pass for the call (which backends serve each mode, and on which GPUs, is underbackend), or an explicit backend is not registered for the device. Admission runs before the argument checks below, so such a call raises this error even when aValueErrorcondition also holds.skip_check=Truebypasses admission, not the argument checks, and keeps two refusals that prevent a silently wrong launch:row_startswith a backend that does not serve windowed (prefill) mode, and an explicit"gvr"without a hint.- raises ValueError:
If
logits/seq_lens/next_nviolate the shape, dtype or grouping contract; if an argument is used outside its mode (next_n,compress_ratioorpre_idxwithrow_starts;max_window_lenorabsolute_indices=Truewithout it); ifrow_startsis not a contiguous 1-Dint32tensor with one entry per row,max_window_lennot a positiveintorabsolute_indicesnot abool; ifpage_tablecomes withrow_starts,pre_idxorreturn_values, is not a contiguous 2-Dint32tensor with one row per request covering the buffer width, orpage_sizeis given withoutpage_tableor is not a power of two up to2**30; if anout_indices/out_valuesbuffer is outside the contract above; or if an explicit backend’s checker rejects the problem (reported as “Problem size is not supported”).- raises RuntimeError:
Under CUDA-graph capture, when a backend’s launcher for the geometry is not compiled yet or the capturing stream has no workspace slab (the warm-up and slab rules are in that backend’s entry under
backend); or when a caller-passedworkspacebuffer fails its size or alignment check.- Warns:
RuntimeWarning –
pre_idxwas malformed and has been discarded; the call ran hint-free.
Examples
Decode mode (the default): one row per request,
seq_lensin KV tokens, indices are logits columns.>>> import torch, flashinfer >>> _ = torch.manual_seed(42) >>> B, N, top_k = 32, 8192, 1024 >>> logits = torch.randn(B, N, dtype=torch.float32, device="cuda") >>> seq_lens = torch.randint(top_k + 1, N, (B,), dtype=torch.int32, device="cuda") >>> indices, _ = flashinfer.top_k_varlen(logits, seq_lens, top_k) >>> for i in range(B): # every hit is >= the K-th largest valid score ... s = seq_lens[i].item() ... kth = torch.topk(logits[i, :s], top_k).values[-1] ... assert (logits[i, :s][indices[i].long()] < kth).sum() == 0 >>> # the next step may pass the previous result as a hint >>> logits_next = torch.randn(B, N, dtype=torch.float32, device="cuda") >>> indices_next, _ = flashinfer.top_k_varlen(logits_next, seq_lens + 1, top_k, pre_idx=indices)
Decode mode with speculative tokens and KV compression:
next_nrows per request (rowrbelongs to requestr // next_n), one column percompress_ratioKV tokens;seq_lensstays in tokens and each row’s column count follows from it (seeseq_lens).>>> next_n, compress_ratio = 2, 4 >>> logits_s = torch.randn(B * next_n, N, dtype=torch.float32, device="cuda") >>> tokens = torch.randint((top_k + 1) * compress_ratio, N * compress_ratio, (B,), dtype=torch.int32, device="cuda") >>> indices_s, _ = flashinfer.top_k_varlen(logits_s, tokens, top_k, next_n=next_n, compress_ratio=compress_ratio) >>> for r in range(B * next_n): ... i, t = divmod(r, next_n) ... c = (tokens[i].item() - next_n + t + 1) // compress_ratio # row r's valid columns ... kth = torch.topk(logits_s[r, :c], top_k).values[-1] ... assert (logits_s[r, :c][indices_s[r].long()] < kth).sum() == 0
Windowed (prefill) mode: one row per query token of a packed chunk, each ranking its own window
[row_starts[r], row_starts[r] + seq_lens[r])given in columns;next_n,compress_ratioandpre_idxare not passed (the backends that serve the mode are listed underbackend).>>> # two requests of L keys each on one packed key axis of 2 * L columns; >>> # a request's j-th token (0-based) ranks the request's first j + 1 keys >>> R, L, top_k = 2, 2048, 512 >>> logits_p = torch.randn(R * L, R * L, dtype=torch.float32, device="cuda") >>> row_starts = torch.tensor([q * L for q in range(R) for _ in range(L)], dtype=torch.int32, device="cuda") >>> seq_lens_p = torch.tensor([j + 1 for _ in range(R) for j in range(L)], dtype=torch.int32, device="cuda") >>> local, _ = flashinfer.top_k_varlen(logits_p, seq_lens_p, top_k, row_starts=row_starts, max_window_len=L) >>> columns, _ = flashinfer.top_k_varlen(logits_p, seq_lens_p, top_k, row_starts=row_starts, max_window_len=L, absolute_indices=True) >>> # the two frames address the same hits: window position + row_starts[r] >>> # is the logits column (compared through the selected values: a row's >>> # slot order is not fixed, and a tie at the K-th value may go either way) >>> hit = local >= 0 >>> assert torch.equal(hit, columns >= 0) >>> v_pos = logits_p.gather(1, torch.where(hit, local + row_starts[:, None], 0).long()) >>> v_col = logits_p.gather(1, torch.where(hit, columns, 0).long()) >>> assert torch.equal(v_pos.sort(dim=1).values, v_col.sort(dim=1).values) >>> assert local[0].max() == 0 and (local[0] >= 0).sum() == 1 # a 1-key window: one hit, -1 padded >>> r = R * L - 1 # the last token of request 1 ranks the request's whole window, columns [L, 2L) >>> w = logits_p[r, L : 2 * L] >>> assert (w[local[r].long()] < torch.topk(w, top_k).values[-1]).sum() == 0
See also
flashinfer.top_kGeneral-purpose radix/clusters top-K (uniform lengths).