Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Most inference requests look small to a Python caller and irregular to a GPU. One request may have 17 cached tokens, another 2,401, and a third may share a prefix with five neighbors. Their keys and values live in different physical pages, yet one kernel launch must produce the next-token states.
FlashInfer is a library and kernel generator for that boundary. It gives serving systems common APIs for attention, matrix multiplication, mixture-of-experts operations, sampling, and cache updates while choosing specialized implementations for a workload and GPU.[1] This deep dive follows one decode step from logical token rows to GPU work, then asks what the design buys and what it leaves to the serving engine.
Project identity and adoption
FlashInfer began in 2023 with researchers from the University of Washington, Carnegie Mellon University, and OctoAI. Current source, review paths, and extended CI show active NVIDIA participation alongside the FlashInfer community.[2][3] The paper lists Zihao Ye, Lequn Chen, Ruihang Lai, Wuwei Lin, Yineng Zhang, Stephanie Wang, Tianqi Chen, Baris Kasikci, Vinod Grover, Arvind Krishnamurthy, and Luis Ceze as authors.[4] That mix matters: the project sits between academic kernel research, compiler and GPU engineering, and the serving frameworks that need a stable operator boundary.
| Field | Current project fact |
|---|---|
| Origin | University of Washington, Carnegie Mellon, and OctoAI researchers started the project; the launch post names the initial engineering team.[2] |
| Stewardship | The flashinfer-ai community repository uses core-maintainer review, public CI, and an additional NVIDIA GPU test path.[1][3] |
| Founding contributors | Zihao Ye, Lequn Chen, Ruihang Lai, and the paper's compiler, systems, and GPU collaborators form the documented research lineage.[4] |
| Source license | Apache-2.0 for FlashInfer's core source.[5] Bundled CUDA components and dependencies can retain BSD, MIT, or other notices. |
| Commercial boundary | NVIDIA participates in maintenance and CI, but FlashInfer remains a public kernel library rather than a model or hosted inference product.[3] |
| Asset boundary | Tutorials may download gated or separately licensed checkpoints. FlashInfer's Apache license doesn't grant rights to those models or datasets. |
The repository names SGLang, vLLM, TensorRT-LLM, Text Generation Inference, MLC-LLM, LightLLM, lorax, and ScaleLLM as adopters or integrations.[1] Adoption doesn't mean every framework enables every operator. Check the framework's attention backend, FlashInfer release, CUDA version, and model feature matrix before treating a project-level capability as a service guarantee.
Why inference attention becomes irregular
The transformer equation is regular: each query compares itself with keys, turns scores into probabilities, and combines values. A serving engine isn't regular. Requests arrive and finish independently, sequence lengths change every step, and key-value (KV) cache pages are allocated from a shared pool.
For a batch with requests r = 0, 1, 2, the logical lengths could be [3, 1, 4]. A ragged query buffer stores eight rows back-to-back. Its row boundaries are carried by an index pointer array:
| request | query rows | qo_indptr interval | logical KV length |
|---|---|---|---|
| 0 | 3 | [0, 3) | 6 |
| 1 | 1 | [3, 4) | 2 |
| 2 | 4 | [4, 8) | 9 |
The query tensor is compact, but the KV cache is usually paged. A page table maps each request's logical page to a physical page. Page size P makes allocation independent of the exact sequence length, much like virtual memory maps a process address to a physical frame.[6]
Why does a serving kernel need qo_indptr instead of a padded [batch, max_q, heads, dim] tensor?
Answer
qo_indptr preserves each request's true row range, so the kernel skips padding work and can load a compact ragged buffer. In exchange, every row carries an indirect request boundary.

Layouts are an API, not an implementation detail
FlashInfer documents several logical layouts because no single physical arrangement wins every phase. Ragged storage is contiguous over tokens. Paged storage breaks a sequence into fixed-size pages. Block-sparse row (BSR) and compressed sparse row (CSR) metadata describe which page blocks are active. Multi-head latent attention (MLA) can store compressed latent states instead of ordinary K/V heads.[7]
The kernel needs three kinds of information:
- Payload: query, key, value, or latent tensors.
- Shape metadata: head counts, head dimensions, page size, and request lengths.
- Indirection: row pointers, page indices, last-page lengths, and optional sparse masks.
Payload buffers can remain stable while metadata changes every scheduling step. A serving engine can recycle pages without asking a kernel to understand allocator policy. A kernel can specialize memory access around a known page size without owning the allocator.
Ragged prefill and paged decode
Prefill consumes many new tokens per request. A ragged layout keeps those tokens contiguous, so a batch prefill wrapper can traverse row ranges with one set of offsets. Decode usually consumes one token per live request and reads a long history. A paged layout avoids copying each history into a newly padded tensor.
The two phases can meet in one mixed batch. FlashInfer exposes wrappers for single-request operations, batch decode with paged KV, batch prefill with paged KV, and batch prefill with ragged KV. Its unified BatchAttention wrapper can dispatch between paged prefill and paged decode based on per-request lengths, although a serving engine still owns the policy deciding which requests enter the batch.[1]
BSR as a memory-access contract
BSR metadata doesn't mean the data are mathematically sparse in every model. It says the kernel may load fixed-size blocks through an index table. If a request references pages [7, 2, 14], the GPU can gather those pages without a defragmentation copy. If a prefix is shared, several request rows can point at the same physical pages while their suffix pages diverge.
That indirection adds pointer arithmetic and less predictable memory access. It pays off when avoiding copies and padding saves more work than the extra gathers cost. Tiny sequences can lose to a simpler contiguous kernel. This is a workload decision, not a universal speedup.
Core mechanism: schedule work around state
An attention kernel normally looks like a matrix multiplication followed by a softmax and a value multiplication. FlashInfer's important move is to treat the state needed to finish attention as the composable unit.
For one query row, keep the output vector o and log-sum-exp statistic l instead of every score. If two workers process disjoint key ranges, each returns (o_1, l_1) and (o_2, l_2). The states can be merged exactly:
FlashInfer's public state tensors store base-2 log-sum-exp. The equations and worked values below use natural log-sum-exp for readability. For API state , natural LSE is ; to pass natural back to merge_state, use .[1]
m = max(l_1, l_2)
o = (exp(l_1 - m) o_1 + exp(l_2 - m) o_2) / (exp(l_1 - m) + exp(l_2 - m))
l = m + log(exp(l_1 - m) + exp(l_2 - m))
The max subtraction keeps the exponentials in a safe range. A tree can merge many chunks without replaying all key-value tiles. FlashInfer calls this family of operations merge-state, cascade, or recursive attention depending on the wrapper.[8]

A worked split-KV example
Take one query row and two key chunks. Chunk A has l_A = 2 and output o_A = [1, 0]. Chunk B has l_B = 1 and output o_B = [0, 2].
- Pick
m = max(2, 1) = 2. - Compute weights
w_A = exp(0) = 1andw_B = exp(-1) ≈ 0.3679. - Sum weights
z = 1.3679. - Combine outputs:
o ≈ ([1, 0] + 0.3679 · [0, 2]) / 1.3679 ≈ [0.7311, 0.5379]. - Keep
l = 2 + log(1.3679) ≈ 2.3133for a parent merge.
The merge doesn't need the original scores. Split-KV workers can run on separate pages or sequence ranges, then publish a small state record. Matching state precision and masking rules makes the merged value mathematically equivalent to one larger softmax.
What must a split-KV worker return so another worker can combine its result without seeing raw scores?
Answer
It must return an output vector and a log-sum-exp normalization statistic for the same query row. The merge uses the largest statistic as a shift, combines weighted outputs, and carries a new statistic upward.
Plan then run: inspector-executor for GPU kernels
FlashInfer wrappers separate a metadata pass (plan) from the hot data pass (run). The plan computes offsets, request-to-tile assignments, temporary-buffer sizes, and choices such as whether to split KV work. The run consumes that plan with query and cache tensors.
This is an inspector-executor pattern:

The split matters because request metadata changes more often than tensor shapes. A scheduler can call plan after admission or batch reshaping, then call run repeatedly for a CUDA graph or a decode step. Reusing the plan avoids repeating integer arithmetic and allows the run kernel to use precomputed offsets.
It also creates an explicit lifetime contract:
| object | created by | consumed by | invalidated when |
|---|---|---|---|
| page indices | allocator/scheduler | plan and run | pages move or requests finish |
| plan workspace | plan | run | batch shape, head shape, or policy changes |
| partial states | run kernel | merge kernel | current query step ends |
| output tensor | serving engine | sampler/model block | next layer overwrites it |
1# Shape-only sketch. Exact argument names vary by wrapper and release.
2wrapper.plan(
3 qo_indptr=qo_indptr,
4 kv_indptr=kv_indptr,
5 kv_indices=kv_indices,
6 kv_last_page_len=kv_last_page_len,
7 num_qo_heads=32,
8 num_kv_heads=8,
9 head_dim=128,
10 page_size=16,
11)
12output = wrapper.run(q, paged_kv_cache)The sketch is intentionally not a copy-paste training recipe. Production callers must match dtypes, device, page layout, head grouping, and wrapper lifetime. The repository's tests and generated API docs are the contract for a specific release.[1]
Load balancing and split-KV decisions
If one request has 128K cached tokens and seven requests have 128 tokens, assigning one CTA per request leaves most warps idle behind the long request. FlashInfer can partition long KV ranges into chunks and distribute them across CTAs. The plan records the mapping and the run phase computes partial states.
The scheduler must choose a chunking policy. A tiny chunk raises merge overhead and metadata traffic. A large chunk raises tail latency because one CTA owns too much work. Maximum occupancy alone is the wrong target; the current mix of decode lengths, page locality, head dimension, and concurrent streams needs balanced work.
What split-KV changes
| choice | likely benefit | cost or risk |
|---|---|---|
| no split for short histories | low metadata overhead | long request can become a tail |
| split long histories | better CTA balance | partial output and merge workspace |
| split plus CUDA graph | stable launch path | graph shape must stay compatible |
| dynamic plan each step | follows request churn | plan CPU/GPU work and synchronization |
The paper describes a load-balanced scheduler designed to cope with dynamic user requests while remaining compatible with CUDA Graphs, which prefer stable execution shapes.[4] That claim is paper-era and workload-specific. Measure inter-token latency (ITL), tail latency, and memory traffic on the exact model and GPU rather than copying a chunk threshold from a benchmark.
Why isn't assigning one request to one CTA always fair?
Answer
Request lengths can differ by orders of magnitude. A long KV history keeps one CTA busy after short requests finish. Splitting that history into chunks lets several CTAs contribute, but requires partial states and a merge step.
Backends and JIT compilation
FlashInfer isn't one monolithic CUDA kernel. Its Python and C++ APIs select among implementations such as FlashAttention-2/3, cuDNN, CUTLASS, TensorRT-LLM, and generated kernels. The choice depends on architecture, operation, dtype, layout, and optional features.[1]
The JIT path matters when the combination is unusual: a custom attention score, a new head dimension, a novel cache layout, or a deployment GPU that isn't covered by a precompiled binary. A template specializes code, compiles it, and caches the resulting module. Precompiled flashinfer-cubin and JIT-cache packages reduce first-use latency when their CUDA and architecture match.
The boundary is practical:
| deployment mode | startup behavior | best fit |
|---|---|---|
| precompiled cubin | load matching binary | known GPU fleet, predictable startup |
| JIT module | compile on first use, then cache | custom shape or attention variant |
| backend dispatch | select cuDNN/CUTLASS/TRT-LLM/FA | reuse mature vendor path |
| Python fallback or reference | correctness and diagnosis | development, unsupported shape |
JIT can make a feature possible, but it moves compiler compatibility into your release. Pin CUDA, compiler, driver, and cache location. Warm kernels before exposing production traffic. Log which backend was selected, so a silent fallback doesn't look like a model regression.
The repository exposes module status, cache management, and API logging commands. API logging can capture calls and system information for diagnosis, but tensor-dumping modes may write sensitive prompts and outputs to disk. Treat diagnostics as production data handling, not as a free debug switch.
Where FlashInfer sits in a serving stack
FlashInfer owns kernel-level execution. It doesn't decide admission, tenant quotas, request cancellation, model weights, or page allocation. A stack such as vLLM or SGLang owns those policies, then passes shape and indirection metadata into FlashInfer. TensorRT-LLM can supply one backend implementation, but backend availability isn't the same as serving-engine integration.
| layer | responsibility | FlashInfer boundary |
|---|---|---|
| API gateway | auth, rate limit, cancellation | outside |
| serving scheduler | continuous batching, priority, deadlines | supplies batch metadata |
| KV allocator | physical page ownership and reuse | supplies page table |
| FlashInfer wrapper | plan offsets, choose kernel, run attention | core |
| model block | projections, residuals, logits | calls attention output |
| sampler | top-k/top-p, stop rules | separate FlashInfer operators can help |
The allocator boundary is a source of bugs. Reusing pages while a CUDA stream still reads them can corrupt another request. A disagreement between the page table and kv_last_page_len can expose uninitialized values in the last tile. Reusing a plan after page indices change may produce numerically plausible output for the wrong sequence.
A release checklist at the boundary
- Keep page-index, length, and workspace lifetimes tied to one stream or explicit event.
- Assert page size, head dimensions, dtype, and device at wrapper entry.
- Compare incremental decode against full-prefix attention on short randomized cases.
- Record selected backend, split-KV flag, and workspace size in a trace.
- Stress request finish, cancellation, and page reuse while CUDA Graph capture is enabled.
What evidence would distinguish a page-table lifetime bug from a numerical precision issue?
Answer
Run a short deterministic case with page reuse and stream synchronization checks. If outputs change when page ownership or execution order changes, inspect metadata lifetimes first. If outputs stay stable but differ gradually with dtype or accumulator precision, compare the merge and softmax states.
Strengths
One vocabulary for many inference phases
The same project covers prefill, decode, append, mixed prefill/decode, MLA, sparse patterns, sampling, quantization, and communication. A serving engine can keep a stable integration surface while the selected kernel changes.
Explicit composability
Ragged and paged layouts, plan/run wrappers, split-KV state, and cascade attention expose the metadata that a scheduler already has. The pieces compose instead of requiring one opaque end-to-end graph.
Customization without abandoning fast paths
JIT templates let an unusual attention variant stay close to the established wrapper contract. Mature backend implementations can handle common shapes while generated code covers new combinations.
Hardware breadth with caveats
The repository lists support from Turing (SM75) through Ampere, Ada, Hopper, and Blackwell families, while noting that features differ by compute capability. That range is useful for mixed fleets, but an operation advertised at project level can still be unavailable on a particular GPU.
Weaknesses and failure modes
Metadata complexity moves upward
The engine must now maintain indptr, page indices, last-page lengths, workspaces, events, and plan validity. PagedAttention makes memory efficient, but correctness depends on allocator and kernel agreeing on the mapping.[6]
JIT is a release dependency
First-use compilation can create startup spikes or fail because a driver, CUDA toolkit, compiler, or architecture is missing. A warm cache can go stale after a version change. Precompiled wheels reduce risk but narrow supported combinations.
Kernel breadth raises test burden
Every backend, dtype, head grouping, page layout, causal mode, and architecture multiplies the test matrix. A green unit test on one GPU doesn't prove a mixed-fleet deployment.
GPU specificity limits portability
FlashInfer targets NVIDIA CUDA architectures rather than CPU, AMD, or Apple inference backends. Porting the API idea is possible, but the CUDA kernels, backend dispatch, and cache packages are hardware-specific.
Training isn't its target
FlashInfer optimizes inference kernels and serving operators. It can be useful around evaluation or generation during training, but it isn't a distributed training framework, optimizer, checkpoint format, or gradient engine. Use it for forward-time serving work, not as a replacement for a training stack.
What the paper actually measured
The FlashInfer paper was submitted in January 2025 and revised in April 2025. Its abstract reports 29% to 69% inter-token-latency reduction against compiler backends on an LLM-serving benchmark, 28% to 30% latency reduction for long-context inference, and 13% to 17% speedup for parallel generation.[4] Those ranges are useful evidence that layout-aware kernels can matter, not universal service-level SLOs.
Read every number with its denominator:
| paper-era result | what it supports | what it doesn't support |
|---|---|---|
| 29% to 69% lower ITL vs compiler backends | kernel and serving comparison under reported conditions | same gain on another model, GPU, or scheduler |
| 28% to 30% lower long-context latency | benefit when long KV reads dominate | a fixed latency budget for all context lengths |
| 13% to 17% faster parallel generation | useful split or batching behavior in tested setup | guaranteed throughput at another concurrency |
For a production decision, replay your trace with the same prompt length distribution, output length, batch policy, quantization, CUDA graph mode, and error budget. Compare p50 and p99 time-to-first-token, ITL, tokens per second, GPU memory, compile time, and correctness against a reference implementation.
A small code-reading route
Use the local repository as a map rather than trying to read every kernel first:
- Start with
flashinfer/decode.pyand findBatchDecodeWithPagedKVCacheWrapper.planand.run. - Follow plan metadata into
csrc/batch_decode.cu, where offsets and optional split-KV buffers become kernel parameters. - Read
flashinfer/cascade.pyto see how attention states compose across shared prefixes. - Read
flashinfer/page.pyandcsrc/page.cufor page append and slot mapping. - Compare a Python wrapper test with its reference-correctness test before reading a specialized backend.
The productive question at each layer is: what shape and lifetime does this function assume, and who owns the next buffer? That question finds more bugs than memorizing kernel names.
Mastery check
By this point, you can:
- Draw the mapping from ragged query rows to paged K/V pages.
- Explain why output plus log-sum-exp is enough to merge split-KV work.
- State which metadata invalidates a plan/run workspace.
- Choose between precompiled, JIT, and backend-dispatch paths for a deployment.
- Name one allocator lifetime bug and one benchmark confounder.