LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnProject Deep DivesDeep Dive - FlashInfer
🚀HardInference Optimization

Deep Dive - FlashInfer

Trace FlashInfer from irregular KV-cache layouts through load-balanced attention kernels, composable state, and production serving boundaries.

34 min read
Learning path
Step 183 of 196 in the full curriculum
Deep Dive - FlashAttentionDeep Dive - DeepGEMM

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Three requests reach one GPU step together. R0 processes 3 new tokens after a 3-token prefix, R1 processes 1 after a 1-token prefix, and R2 processes 4 after a 5-token prefix. Their 8 query rows fit in one compact buffer. After appending the new keys and values, their KV lengths are 6, 2, and 9, scattered across physical pages. Attention still has to return one correct state per row without attending to a future token.

Before naming an API, predict the contract. The kernel needs query boundaries, a page list plus tail length for each history, and a work assignment that can split a long history without losing softmax normalization. Those pieces explain FlashInfer: a library and kernel generator for attention, matrix multiplication, mixture-of-experts operations, sampling, and cache updates.[1]Reference 1FlashInferhttps://github.com/flashinfer-ai/flashinfer The serving engine still owns allocation and admission.

FlashAttention supplies the tiled softmax, and paged KV caching supplies the logical-to-physical mapping. Keep those two mechanisms separate: avoiding a score matrix and avoiding cache relocation save different memory traffic. The Python exercises here run on CPU and check indexing and attention arithmetic. They don't execute FlashInfer kernels or measure GPU speed.

Who ships it, who calls it

Start with ownership, because a kernel benchmark and a serving guarantee belong to different layers.

The API walkthrough is pinned to FlashInfer 0.6.18, released August 29, 2026, at commit 69ff11fc4954396d98326656dc85debd2223f637, checked September 2, 2026.[2]Reference 2FlashInfer v0.6.18 Releasehttps://github.com/flashinfer-ai/flashinfer/releases/tag/v0.6.18[3]Reference 3FlashInfer v0.6.18 Sourcehttps://github.com/flashinfer-ai/flashinfer/tree/69ff11fc4954396d98326656dc85debd2223f637 The paper benchmarks later in the lesson used v0.2. They aren't measurements of this release.

FlashInfer began in summer 2023 with researchers from the University of Washington, Carnegie Mellon University, and OctoAI.[4]Reference 4FlashInfer: Kernel Library for LLM Servinghttps://flashinfer.ai/2024/02/02/introduce-flashinfer.html The MLSys 2025 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, with affiliations spanning UW, NVIDIA, Perplexity, CMU, and independent research.[5]Reference 5FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.https://arxiv.org/abs/2501.01005 The cited contribution snapshot documents NVIDIA participation alongside the FlashInfer community, with public GitHub Actions and an NVIDIA-internal GPU matrix on GitLab.[6]Reference 6Contributing to FlashInferhttps://github.com/flashinfer-ai/flashinfer/blob/d020372b068f335e2fe427372e134977a2235c49/CONTRIBUTING.md

FieldDocumented project fact
OriginUW, CMU, and OctoAI researchers started the project in 2023; the launch post names that initial team.[4]Reference 4FlashInfer: Kernel Library for LLM Servinghttps://flashinfer.ai/2024/02/02/introduce-flashinfer.html
StewardshipThe cited contribution snapshot documents public review and CI plus an additional NVIDIA GPU test path.[6]Reference 6Contributing to FlashInferhttps://github.com/flashinfer-ai/flashinfer/blob/d020372b068f335e2fe427372e134977a2235c49/CONTRIBUTING.md
Founding contributorsZihao Ye, Lequn Chen, Ruihang Lai, and the paper's compiler, systems, and GPU collaborators form the documented research lineage.[5]Reference 5FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.https://arxiv.org/abs/2501.01005
Source licenseApache-2.0 for FlashInfer's core source.[7]Reference 7FlashInfer Apache License 2.0https://github.com/flashinfer-ai/flashinfer/blob/d020372b068f335e2fe427372e134977a2235c49/LICENSE Bundled CUDA components and dependencies can retain BSD, MIT, or other notices.
Commercial boundaryNVIDIA participates in maintenance and CI, but FlashInfer remains a public kernel library rather than a model or hosted inference product.[6]Reference 6Contributing to FlashInferhttps://github.com/flashinfer-ai/flashinfer/blob/d020372b068f335e2fe427372e134977a2235c49/CONTRIBUTING.md
Asset boundaryTutorials 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]Reference 1FlashInferhttps://github.com/flashinfer-ai/flashinfer 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

FlashAttention gives exact attention a streaming recipe: load tiles, update online softmax state, and avoid writing a full score matrix to global memory. In serving, the pressure moves to the batch around that kernel. The transformer equation stays regular, but requests arrive and finish independently, sequence lengths change every step, and the key-value (KV) cache comes from a shared page pool.

Start by counting two axes. R0 and R2 are doing chunked prefill, also called append attention. R1 is in the decode loop. Here, every reported KV length includes the current query tokens' keys and values, which the caller has already appended. A ragged query buffer stores the 8 query rows back-to-back. What tells a kernel where one request ends and the next begins? An index-pointer array qo_indptr:

requestphasequery rowsqo_indptr intervallogical KV length
R0prefill3[0, 3)6
R1decode1[3, 4)2
R2prefill4[4, 8)9

The query tensor is compact. Now predict what the cache needs: a way to name physical pages and a way to mark the occupied part of the last page. Page size P=4P = 4P=4 makes allocation independent of exact sequence length, much like virtual memory maps a process address to a physical frame.[8]Reference 8Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180 FlashInfer's length contract is page_size * (len(page_indices) - 1) + last_page_len, with last_page_len in 1 .. page_size.[9]Reference 9KV-Cache Layoutshttps://docs.flashinfer.ai/tutorials/kv_layout.html

requestpage indiceslast_page_lenKV length check
R0[7, 2]24×(2−1)+2=64 \times (2 - 1) + 2 = 64×(2−1)+2=6
R1[11]24×(1−1)+2=24 \times (1 - 1) + 2 = 24×(1−1)+2=2
R2[3, 14, 6]14×(3−1)+1=94 \times (3 - 1) + 1 = 94×(3−1)+1=9

Packed metadata for the whole batch is kv_indptr = [0, 2, 3, 6], kv_indices = [7, 2, 11, 3, 14, 6], and kv_last_page_len = [2, 2, 1]. Read the two pointer arrays in different coordinate systems: qo_indptr counts query rows, while kv_indptr counts page IDs. Confusing those units can still produce in-bounds pointers with the wrong sequence.

A cooperative thread array (CTA) is CUDA's name for a thread block: threads that share on-chip memory and run one tile of work. Long histories may benefit from several CTAs and a later merge, but these tiny lengths are indexing examples, not evidence that nine tokens should be split. Actual launches also depend on query tiles, heads, backend, and hardware.

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.

Can you rebuild both pointer arrays before invoking attention? The script below does so from the same page lists. If a later figure or kernel dump disagrees with qo_indptr = [0, 3, 4, 8] or KV lengths [6, 2, 9], stop there: metadata is wrong before any attention math runs.

page_map.py
1PAGE_SIZE = 4 2pages = {0: [7, 2], 1: [11], 2: [3, 14, 6]} 3last_page_len = {0: 2, 1: 2, 2: 1} 4 5def kv_len(page_ids: list[int], tail: int) -> int: 6 if not page_ids or any(type(p) is not int or p < 0 for p in page_ids): 7 raise ValueError("this example requires nonempty, nonnegative page IDs") 8 if type(tail) is not int or not 1 <= tail <= PAGE_SIZE: 9 raise ValueError("tail must occupy 1..PAGE_SIZE slots") 10 return PAGE_SIZE * (len(page_ids) - 1) + tail 11 12qo_indptr = [0] 13for query_len in (3, 1, 4): 14 qo_indptr.append(qo_indptr[-1] + query_len) 15 16kv_indptr = [0] 17for request in (0, 1, 2): 18 kv_indptr.append(kv_indptr[-1] + len(pages[request])) 19 20lengths = [kv_len(pages[request], last_page_len[request]) for request in (0, 1, 2)] 21assert qo_indptr == [0, 3, 4, 8] 22assert kv_indptr == [0, 2, 3, 6] 23assert lengths == [6, 2, 9] 24for bad_pages, bad_tail in (([], 1), ([3], 0), ([3], 5), ([-1], 1)): 25 try: 26 kv_len(bad_pages, bad_tail) 27 except ValueError: 28 pass 29 else: 30 raise AssertionError("invalid page metadata was accepted") 31print("qo_indptr", qo_indptr) 32print("kv_indptr", kv_indptr) 33print("kv_len", lengths)
Output
1qo_indptr [0, 3, 4, 8] 2kv_indptr [0, 2, 3, 6] 3kv_len [6, 2, 9]
R2 logical token ranges 0 through 3, 4 through 7, and 8 map to physical pages 3, 14, and 6. Physical pages are shown in numeric order to make the indirection visible. Only slot zero of page 6 is valid; logical token 5 is at page 14 slot 1.
Follow logical order, not physical page-number order. R2 token 5 maps to page 14, slot 1; its last page contributes only one token. The allocator doesn't have to move these pages into a contiguous sequence.

Layouts are an API, not an implementation detail

FlashInfer documents several logical layouts because no single physical arrangement wins every phase. Ask what each phase wants to keep contiguous. Ragged storage packs tokens without padding; paged storage breaks a sequence into fixed-size pages; block-sparse row (BSR) metadata views that page table as a sparse matrix. Its CSR-style indptr / indices arrays tell the kernel which blocks to load. Multi-head latent attention (MLA) can store compressed latent states instead of ordinary K/V heads.[9]Reference 9KV-Cache Layoutshttps://docs.flashinfer.ai/tutorials/kv_layout.html

The same choice extends to precision. Selected backends accept 8-bit floating point (FP8) or FP4 representations, with format-specific scales and packing. Those aren't drop-in replacements for FP16: scale granularity, cache layout, supported GPU, and dequantization path belong to the selected operator's contract. The example below stays in FP16 rather than assuming every wrapper supports every quantized cache.

The kernel needs three kinds of information:

  1. Payload: query, key, value, or latent tensors (in FP16, BF16, FP8, or sub-byte quantized formats).
  2. Shape metadata: head counts, head dimensions, page size, and request lengths.
  3. 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.

Our batch is mixed: R0 and R2 have several query rows, R1 has one. A paged-prefill wrapper can represent all three through qo_indptr; a dedicated decode wrapper assumes one query per request. FlashInfer also has other mixed-phase paths, but those aren't interchangeable APIs. Choose a named wrapper and verify its signature before passing scheduler metadata.[1]Reference 1FlashInferhttps://github.com/flashinfer-ai/flashinfer

Causality uses logical positions, not page numbers

R2 has five earlier tokens and four new ones. Its first query is at logical position 5, so it can attend to positions 0 through 5, not the whole nine-token cache. For local query index iii, query length QQQ, and total KV length KKK, the allowed key positions satisfy j≤K−Q+ij\le K-Q+ij≤K−Q+i. This is a bottom-right-aligned causal mask. A top-left triangle would expose too little prefix; no mask would expose future tokens.

Run the indexing check before any GPU call. It also shows why sorting the page IDs would change the sequence:

causal_page_lookup.py
1page_ids, page_size, q_len, kv_len = [3, 14, 6], 4, 4, 9 2addresses = [(page_ids[j // page_size], j % page_size) for j in range(kv_len)] 3mask = [[j <= kv_len - q_len + i for j in range(kv_len)] for i in range(q_len)] 4assert addresses[5] == (14, 1) 5assert addresses[-1] == (6, 0) 6assert [sum(row) for row in mask] == [6, 7, 8, 9] 7assert [p for p, _ in addresses] != sorted(p for p, _ in addresses) 8print("logical token 5 -> page, slot", addresses[5]) 9for row in mask: 10 print("".join("1" if visible else "." for visible in row))
Output
1logical token 5 -> page, slot (14, 1) 2111111... 31111111.. 411111111. 5111111111

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 R0 references pages [7, 2], 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 extra gathers cost. Tiny sequences can lose to a simpler contiguous kernel. Use the layout only when its saved movement outweighs its indexing cost; this is a workload decision, not a universal speedup.

Core mechanism: schedule work around state

Suppose one query's KV history is split across two CTAs. What can each worker discard while still letting a parent recover exact attention? Normalization has to survive. An attention kernel normally looks like a matrix multiplication followed by a softmax and a value multiplication, but FlashAttention already showed that the full score matrix is unnecessary. FlashInfer makes the output vector plus a log-sum-exp statistic the composable unit across workers, devices, and shared prefixes.[5]Reference 5FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.https://arxiv.org/abs/2501.01005[10]Reference 10Recursive Attentionhttps://docs.flashinfer.ai/tutorials/recursive_attention.html

If two workers process disjoint key ranges for the same query row and head, each returns an output vector ooo and a base-2 log-sum-exp bbb. For ordinary scaled logits sj=qkjT/ds_j=qk_j^T/\sqrt{d}sj​=qkjT​/d​, that statistic is b=log⁡2∑jesjb=\log_2\sum_j e^{s_j}b=log2​∑j​esj​, not log⁡2∑j2sj\log_2\sum_j 2^{s_j}log2​∑j​2sj​. The conversion changes the stored normalization base, not the attention distribution. FlashInfer's public merge_state expects base-2 statistics; don't pass a natural-log LSE without converting it by division by ln⁡2\ln 2ln2.[11]Reference 11FlashInfer v0.6.18: Cascade Attentionhttps://github.com/flashinfer-ai/flashinfer/blob/69ff11fc4954396d98326656dc85debd2223f637/include/flashinfer/attention/cascade.cuh

Before reading the recurrence, keep one invariant in mind: every partial output is normalized over its own key range, and its statistic records that range's total weight. The merge must restore the weighting between ranges.

m=max⁡(bA,bB)m = \max(b_A, b_B)m=max(bA​,bB​) wA=2bA−m,wB=2bB−mw_A = 2^{b_A - m},\qquad w_B = 2^{b_B - m}wA​=2bA​−m,wB​=2bB​−m o=wA oA+wB oBwA+wBo = \frac{w_A\, o_A + w_B\, o_B}{w_A + w_B}o=wA​+wB​wA​oA​+wB​oB​​ b=m+log⁡2(wA+wB)b = m + \log_2(w_A + w_B)b=m+log2​(wA​+wB​)

The max subtraction keeps the powers of two in a safe range. The operator is associative in exact arithmetic, so a tree can merge many chunks without replaying their key-value tiles.[10]Reference 10Recursive Attentionhttps://docs.flashinfer.ai/tutorials/recursive_attention.html Floating-point reduction order can still change the result. Overlapping chunks double-count keys; mismatched masks, positions, or score scales combine different attention problems.

Worker A has base-2 LSE 2 and output [1,0], corresponding to normalization mass 4. Worker B has base-2 LSE 1 and output [0,1], corresponding to mass 2. Combining disjoint key ranges gives weights two thirds and one third, output [2/3,1/3], and LSE approximately 2.585.
The normalization masses are 4 and 2. Dividing by their total gives weights 2/3 and 1/3. The implementation below subtracts a common maximum before exponentiating, preserving those weights without overflowing.

A worked split-KV example

Take one query row and two key chunks. Chunk A has bA=2b_A = 2bA​=2 and output oA=[1,0]o_A = [1, 0]oA​=[1,0]. Chunk B has bB=1b_B = 1bB​=1 and output oB=[0,1]o_B = [0, 1]oB​=[0,1].

  1. Pick m=max⁡(2,1)=2m = \max(2, 1) = 2m=max(2,1)=2.
  2. Compute weights wA=20=1w_A = 2^{0} = 1wA​=20=1 and wB=2−1=0.5w_B = 2^{-1} = 0.5wB​=2−1=0.5.
  3. Sum weights z=1.5z = 1.5z=1.5.
  4. Combine outputs: o=([1,0]+0.5⋅[0,1])/1.5=[2/3,1/3]o = ([1, 0] + 0.5 \cdot [0, 1]) / 1.5 = [2/3, 1/3]o=([1,0]+0.5⋅[0,1])/1.5=[2/3,1/3].
  5. Keep b=2+log⁡2(1.5)≈2.585b = 2 + \log_2(1.5) \approx 2.585b=2+log2​(1.5)≈2.585 for 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, base, and masking rules makes the merged value mathematically equivalent to one larger softmax.

merge_state.py
1import math 2 3def merge_state( 4 o_a: list[float], b_a: float, o_b: list[float], b_b: float 5) -> tuple[list[float], float]: 6 """CPU reference; -inf with a zero vector represents an empty key set.""" 7 if not o_a or len(o_a) != len(o_b): 8 raise ValueError("state vectors must have the same positive dimension") 9 if not all(math.isfinite(x) for x in o_a + o_b): 10 raise ValueError("state vectors must be finite") 11 if any(math.isnan(b) or b == math.inf for b in (b_a, b_b)): 12 raise ValueError("LSE must be finite or negative infinity") 13 if any(b == -math.inf and any(o) for o, b in ((o_a, b_a), (o_b, b_b))): 14 raise ValueError("empty states must use a zero vector") 15 if b_a == -math.inf: 16 return list(o_b), b_b 17 if b_b == -math.inf: 18 return list(o_a), b_a 19 shift = max(b_a, b_b) 20 w_a = 2 ** (b_a - shift) 21 w_b = 2 ** (b_b - shift) 22 z = w_a + w_b 23 merged = [(w_a * x + w_b * y) / z for x, y in zip(o_a, o_b, strict=True)] 24 return merged, shift + math.log2(z) 25 26output, log_sum_exp = merge_state([1.0, 0.0], 2.0, [0.0, 1.0], 1.0) 27assert abs(output[0] - 2 / 3) < 1e-12 28assert abs(output[1] - 1 / 3) < 1e-12 29assert abs(log_sum_exp - (2 + math.log2(1.5))) < 1e-12 30assert merge_state([0., 0.], -math.inf, output, log_sum_exp) == (output, log_sum_exp) 31assert merge_state([0., 0.], -math.inf, [0., 0.], -math.inf) == ([0., 0.], -math.inf) 32print([round(value, 4) for value in output], round(log_sum_exp, 4))
Output
1[0.6667, 0.3333] 2.585

An empty or fully masked chunk has no normalized output. This reference defines it as a zero vector with b=−∞b=-\inftyb=−∞ and handles it explicitly. Blindly evaluating −∞−(−∞)-\infty-(-\infty)−∞−(−∞) produces NaN. The pinned public CUDA merge kernel uses the finite-state recurrence directly, so our empty-state branch isn't a claim that this API accepts arbitrary empty sentinels.[11]Reference 11FlashInfer v0.6.18: Cascade Attentionhttps://github.com/flashinfer-ai/flashinfer/blob/69ff11fc4954396d98326656dc85debd2223f637/include/flashinfer/attention/cascade.cuh Check the producing kernel's masked-state convention before passing its output to a merge.

Now compare split attention with a direct stable softmax, including large logits and a chunk with no visible keys. This continues the preceding cell. The random numbers are synthetic arithmetic inputs, not model outputs or a performance benchmark.

check_split_attention.py
1import random 2 3def direct_state(scores, values): 4 if not scores: 5 return [0.0, 0.0], -math.inf 6 shift = max(scores) 7 weights = [math.exp(s - shift) for s in scores] 8 denominator = sum(weights) 9 output = [sum(w * v[d] for w, v in zip(weights, values, strict=True)) 10 / denominator for d in range(2)] 11 return output, (shift + math.log(denominator)) / math.log(2) 12 13rng = random.Random(7) 14cases = 0 15for length in (1, 2, 6, 9, 33): 16 for offset in (-1000.0, 0.0, 1000.0): 17 scores = [offset + rng.uniform(-4, 4) for _ in range(length)] 18 values = [[rng.uniform(-1, 1), rng.uniform(-1, 1)] for _ in scores] 19 expected_o, expected_b = direct_state(scores, values) 20 for cut in range(length + 1): 21 left = direct_state(scores[:cut], values[:cut]) 22 right = direct_state(scores[cut:], values[cut:]) 23 actual_o, actual_b = merge_state(*left, *right) 24 assert max(abs(a - b) for a, b in zip(actual_o, expected_o)) < 1e-10 25 assert abs(actual_b - expected_b) < 1e-10 26 cases += 1 27 28# Overlap counts a key twice. It is not a valid partition. 29scores, values = [0.0, 0.0], [[1.0, 0.0], [0.0, 1.0]] 30whole = direct_state(scores, values) 31duplicate = merge_state(*whole, *direct_state(scores[:1], values[:1])) 32assert abs(whole[0][0] - 0.5) < 1e-12 33assert abs(duplicate[0][0] - 2 / 3) < 1e-12 34print("split/direct checks:", cases) 35print("overlapping chunks change 0.5000 to", f"{duplicate[0][0]:.4f}")
Output
1split/direct checks: 168 2overlapping chunks change 0.5000 to 0.6667

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

The state merge solves how to combine work. A second question is when to decide where that work goes. 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.

Ask which work should happen once per batch and which should happen on every layer launch. The paper describes this split as an inspector-executor pattern: inspect irregular metadata on the CPU, then execute a stable GPU kernel, including under CUDA Graphs, which want the same launch shape and pointer layout each replay.[5]Reference 5FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.https://arxiv.org/abs/2501.01005

Diagram showing Scheduler metadata qo_indptr, kv_indptr, pages, plan assign tiles and workspace, workspace schedule and scratch space, and query tensor.
Scheduler metadata qo_indptr, kv_indptr, pages, plan assign tiles and workspace, workspace schedule and scratch space, and query tensor.

Metadata values can change while allocated tensor shapes stay fixed. Reuse a plan across compatible layer calls for the same batch specification, not blindly across decode steps whose sequence lengths have grown. The pinned prefill plan performs host-side work and device copies; it can't run inside CUDA Graph capture or torch.compile.[12]Reference 12FlashInfer v0.6.18: Prefill APIhttps://github.com/flashinfer-ai/flashinfer/blob/69ff11fc4954396d98326656dc85debd2223f637/flashinfer/prefill.py Compatible run calls can be captured. Graph-mode wrappers reserve metadata buffers, require a fixed batch size, and bound total query rows and page-index capacity. Stable buffer addresses don't make stale buffer contents correct.

It also creates an explicit lifetime contract:

objectcreated byconsumed byinvalidated when
page indicesallocator/schedulerplan and runlogical-to-physical mapping changes; refresh before use
plan workspaceplanrunincompatible lengths, shapes, mask settings, or buffer replacement
partial statesrun kernelmerge kernelcurrent query step ends
output tensorwrapper or callermodel blockowner reuses it after dependent work completes

The ordinary batch-decode wrapper doesn't take qo_indptr, because each request contributes one query row. Our paged-prefill wrapper does. In v0.6.18 the keyword is head_dim_qk, and paged metadata names start with paged_kv_. The earlier shorthand head_dim / kv_indptr would raise a keyword error for this wrapper.[12]Reference 12FlashInfer v0.6.18: Prefill APIhttps://github.com/flashinfer-ai/flashinfer/blob/69ff11fc4954396d98326656dc85debd2223f637/flashinfer/prefill.py

The following is a GPU-only smoke check, source-checked against v0.6.18 but not executed for this review. It needs a compatible NVIDIA GPU, CUDA-enabled PyTorch, and flashinfer-python==0.6.18; JIT dependencies and artifacts must match the chosen CUDA build. It allocates synthetic Q/K/V directly, so there are no model weights or downloads of checkpoints. The 128 MiB scratch allocation follows the wrapper's documented recommendation, not a measured minimum.

flashinfer_plan_run.py
1import torch 2import flashinfer 3from importlib.metadata import version 4 5assert version("flashinfer-python") == "0.6.18" 6assert torch.cuda.is_available(), "requires NVIDIA CUDA; CPU reference is above" 7torch.manual_seed(7) 8device = "cuda:0" 9workspace = torch.empty(128 * 1024 * 1024, dtype=torch.uint8, device=device) 10wrapper = flashinfer.BatchPrefillWithPagedKVCacheWrapper( 11 workspace, kv_layout="NHD", backend="fa2" 12) 13index = lambda values: torch.tensor(values, dtype=torch.int32, device=device) 14q = torch.randn(8, 32, 128, dtype=torch.float16, device=device) 15paged_kv_cache = torch.randn(15, 2, 4, 8, 128, dtype=torch.float16, device=device) 16wrapper.plan( 17 qo_indptr=index([0, 3, 4, 8]), 18 paged_kv_indptr=index([0, 2, 3, 6]), 19 paged_kv_indices=index([7, 2, 11, 3, 14, 6]), 20 paged_kv_last_page_len=index([2, 2, 1]), 21 num_qo_heads=32, 22 num_kv_heads=8, 23 head_dim_qk=128, 24 page_size=4, 25 causal=True, 26 q_data_type=torch.float16, 27 kv_data_type=torch.float16, 28) 29output = wrapper.run(q, paged_kv_cache) 30assert output.shape == q.shape and torch.isfinite(output).all() 31 32# Independent dense reference: gather logical pages and repeat GQA KV heads. 33for row_start, row_end, ids, length in ( 34 (0, 3, [7, 2], 6), (3, 4, [11], 2), (4, 8, [3, 14, 6], 9) 35): 36 k = paged_kv_cache[ids, 0].reshape(-1, 8, 128)[:length].float() 37 v = paged_kv_cache[ids, 1].reshape(-1, 8, 128)[:length].float() 38 k, v = k.repeat_interleave(4, dim=1), v.repeat_interleave(4, dim=1) 39 query = q[row_start:row_end].float() 40 scores = torch.einsum("qhd,khd->hqk", query, k) / (128 ** 0.5) 41 q_len = row_end - row_start 42 visible = torch.arange(length, device=device)[None, :] <= ( 43 length - q_len + torch.arange(q_len, device=device)[:, None] 44 ) 45 probabilities = scores.masked_fill(~visible[None], -torch.inf).softmax(-1) 46 expected = torch.einsum("hqk,khd->qhd", probabilities, v) 47 torch.testing.assert_close(output[row_start:row_end].float(), expected, 48 atol=2e-3, rtol=2e-3) 49torch.cuda.synchronize() 50print("paged causal GQA reference passed:", tuple(output.shape))

Here NHD means each K or V page has shape [page_size, kv_heads, head_dim]. The 32 query heads share 8 KV heads in groups of four. Those are grouped-query attention (GQA) semantics, not four copies of each physical cache allocation. The dense comparison repeats heads only in the reference. Its tolerance is a starting check for these random FP16 inputs, not an accuracy budget for every model.

Refresh page mappings and lengths through the public planning path when the batch changes. Some integrations can update compatible fixed-address buffers without rebuilding all work assignments, but that is backend-specific, not a general permission to mutate private wrapper fields. Replanning metadata doesn't normally require recompiling a kernel or recapturing a compatible graph. Conversely, fixed_split_size can change the number of launched CTAs as KV lengths grow; v0.6.18 explicitly warns that fixed splitting doesn't guarantee CUDA Graph compatibility.[12]Reference 12FlashInfer v0.6.18: Prefill APIhttps://github.com/flashinfer-ai/flashinfer/blob/69ff11fc4954396d98326656dc85debd2223f637/flashinfer/prefill.py

Load balancing and split-KV decisions

If one request has 128K cached tokens and seven requests have 128 tokens, assigning only one CTA per request can leave the GPU underfilled or dominated by the long request. Head and query tiling also contribute parallelism, so request count alone doesn't determine utilization. FlashInfer can partition long KV ranges across CTAs, compute partial states, and merge them. The [6, 2, 9] example teaches indexing; it doesn't justify a particular split threshold.

The scheduler must choose a split policy. A tiny chunk raises merge overhead and metadata traffic. A large chunk raises tail latency because one CTA owns too much work. Treat occupancy as a constraint, not the objective: enough resident warps can hide memory stalls, but chasing the highest occupancy can sacrifice tile reuse or register headroom. The current mix of decode lengths, page locality, head dimension, and concurrent streams needs balanced work.

What split-KV changes

choicelikely benefitcost or risk
no split for short historieslow metadata overheadlong request can become a tail
split long historiesbetter CTA balancepartial output and merge workspace
split plus CUDA graphstable launch pathgraph shape must stay compatible
dynamic plan each stepfollows request churnplan 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.[5]Reference 5FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.https://arxiv.org/abs/2501.01005 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]Reference 1FlashInferhttps://github.com/flashinfer-ai/flashinfer

For one attention call, think in a composition chain: a layout reader follows indirection, a backend microkernel computes tiles and online state, and a merge or output transform finishes the result when work was split. Backend label alone leaves that chain unspecified. Record layout, head grouping, dtype, split policy, and selected implementation together when comparing runs.

The just-in-time (JIT) path matters when a supported specialization isn't covered by a precompiled binary, or when a developer implements a custom attention variant. A template specializes code, compiles it, and caches the resulting module. JIT doesn't make an arbitrary head dimension, cache layout, or unsupported GPU valid by itself. Precompiled flashinfer-cubin and JIT-cache packages reduce first-use latency when their CUDA and architecture match.

The boundary is practical:

deployment modestartup behaviorbest fit
precompiled cubinload matching binaryknown GPU fleet, predictable startup
JIT modulecompile on first use, then cachecustom shape or attention variant
backend dispatchselect cuDNN/CUTLASS/TRT-LLM/FAreuse mature vendor path
separate dense referencecorrectness and diagnosis, usually slowercompare outputs; not an automatic CPU fallback

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.

layerresponsibilityFlashInfer boundary
API gatewayauth, rate limit, cancellationoutside
serving schedulercontinuous batching, priority, deadlinessupplies batch metadata
KV allocatorphysical page ownership and reusesupplies page table
FlashInfer wrapperplan offsets, choose kernel, run attentioncore
model blockprojections, residuals, logitscalls attention output
samplertop-k/top-p, stop rulesseparate 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

  1. Keep page-index, length, and workspace lifetimes tied to one stream or explicit event.
  2. Assert page size, head dimensions, dtype, and device at wrapper entry.
  3. Compare incremental decode against full-prefix attention on short randomized cases.
  4. Record selected backend, split-KV flag, and workspace size in a trace.
  5. 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 release spans several NVIDIA generations, but support is operator-specific. v0.6.18 adds Rubin paths and removes SM75 and single-request FA2 kernels from prebuilt JIT-cache wheels: those APIs may still require first-use compilation even with a cache package installed.[2]Reference 2FlashInfer v0.6.18 Releasehttps://github.com/flashinfer-ai/flashinfer/releases/tag/v0.6.18 A project-level GPU support label doesn't guarantee that the chosen attention backend, dtype, or graph mode works on that 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.[8]Reference 8Efficient Memory Management for Large Language Model Serving with PagedAttention.https://arxiv.org/abs/2309.06180

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.

A kernel library isn't a training stack

FlashInfer's main focus is inference kernels and serving operators.[3]Reference 3FlashInfer v0.6.18 Sourcehttps://github.com/flashinfer-ai/flashinfer/tree/69ff11fc4954396d98326656dc85debd2223f637 The attention walkthrough here checks forward computation only. It doesn't establish an autograd contract, and FlashInfer doesn't replace a distributed training framework, optimizer, or checkpoint system. Verify any backward path separately before using an operator during training.

What the paper actually measured

The FlashInfer paper is an MLSys 2025 paper, posted as arXiv:2501.01005. The evaluation used FlashInfer v0.2, CUDA 12.4, and PyTorch 2.4.0 on A100 40GB SXM and H100 80GB SXM GPUs, with FP16 storage and compute.[5]Reference 5FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.https://arxiv.org/abs/2501.01005 The abstract's ranges are useful evidence that layout-aware kernels can matter, not universal service-level SLOs.

The end-to-end SGLang experiment used ShareGPT plus a synthetic Variable workload with sequence lengths uniformly distributed from 512 to 2,048 tokens. It measured time-to-first-token (TTFT) and ITL under online serving, adjusting request rate to keep P99 TTFT below 200 ms.[5]Reference 5FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.https://arxiv.org/abs/2501.01005 Those workload and admission details belong beside the percentage, because changing them changes the question the benchmark answers.

Read every number with its denominator:

paper-era resultwhat it supportswhat it doesn't support
29% to 69% lower ITL vs compiler backendsSGLang v0.3.4 with FlashInfer vs its Triton v3.0 backend on Llama 3.1 8B/70B serving tracessame gain on another model, GPU, or scheduler
28% to 30% lower long-context latencyStreaming-LLM with a fused rotary positional embedding (RoPE)-attention kernel vs unfused kernels on Vicuna-13B / MT-Bencha fixed latency budget for all context lengths
About 14% to 17% lower ITL for parallel generationMLC-Engine composable formats at parallel width n=4n=4n=4 on Llama 3.1 8B/70B ShareGPTthe same percentage increase in throughput or gain 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. Freeze a warm steady-state window, change one variable, rerun the same trace, then verify output and tail metrics. That measure-change-verify loop makes a kernel comparison an experiment rather than a before-and-after anecdote.

Correctness is part of benchmark coverage

A fast path is useful only across shapes it computes correctly. Your harness should compare each selected backend with a trusted reference on the same ragged and paged inputs. Cover prefill, decode, and mixed batches; short histories, non-full last pages, shared prefixes, split and unsplit KV ranges; the MHA, GQA, and MLA head layouts your model uses; and every dtype, mask, device, and fallback path you intend to ship. Include metadata dtypes in that matrix: current FlashInfer docs require indptr, page-index, and tail-length arrays to use int32; an int64 index can fail before numeric comparison.[9]Reference 9KV-Cache Layoutshttps://docs.flashinfer.ai/tutorials/kv_layout.html Compare outputs and attention-state statistics under an explicit tolerance, fail on NaN or Inf, and report unsupported shapes separately. One passing dense case is a smoke test, not coverage.

Diagnose the path before tuning the kernel

When a result regresses, first locate the time. Use Nsight Systems to inspect CPU planning, metadata copies, stream dependencies, kernel launch gaps, and overlap; use Nsight Compute to inspect the selected kernel's memory workload, arithmetic intensity, occupancy limits, and warp stalls.[13]Reference 13Nsight Systems User Guidehttps://docs.nvidia.com/nsight-systems/UserGuide/[14]Reference 14Nsight Compute Profiling Guidehttps://docs.nvidia.com/nsight-compute/ProfilingGuide/ Correlate that trace with the recorded backend, layout, split-KV choice, merge work, and p50/p99 TTFT or ITL. A memory-bound trace points first to page access, layout, or precision; a compute-bound trace points to tile or backend choice; a launch gap points to planning or scheduler work. High GPU utilization can still hide a bad tail, a synchronizing plan, or a silent fallback. Tune the boundary that the trace identifies, then rerun the same correctness matrix.

A small code-reading route

Use the local repository as a map rather than trying to read every kernel first:

  1. Start with flashinfer/decode.py and find BatchDecodeWithPagedKVCacheWrapper.plan and .run.
  2. Follow plan metadata into csrc/batch_decode.cu, where offsets and optional split-KV buffers become kernel parameters.
  3. Read flashinfer/cascade.py and include/flashinfer/attention/cascade.cuh to see how attention states compose, including the base-2 merge.
  4. Read flashinfer/page.py and csrc/page.cu for page append and slot mapping.
  5. 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.

What an integration review must defend

Use the three-request batch to explain the boundary before proposing a faster kernel:

  • Indexing: Reconstruct both pointer arrays, locate R2 token 5 at page 14 slot 1, and derive its four causal row lengths as 6, 7, 8, and 9.
  • Numerics: Match split attention to direct softmax with base-2 LSE, disjoint key ranges, empty-state handling, and an explicit floating-point tolerance.
  • Integration: Distinguish refreshing metadata, planning work, compiling a specialization, and capturing a graph. Record which changed before claiming a cache or graph is reusable.
  • Measurement: Keep the v0.2 paper results separate from v0.6.18, and report a correctness matrix and serving trace before generalizing a speedup.

Follow-up questions

R2's page IDs stay [3, 14, 6], but its last-page length grows from 1 to 2. Can an unchanged page list justify replaying old length metadata?

Answer

No. The logical KV length grew from 9 to 10 even though the page list stayed fixed. Update the tail length and any length-dependent plan or mask through the supported wrapper path before running; a stable allocation isn't a stable attention problem.

A shared prefix is included in both partial states before merging. Why can the output stay finite yet still be wrong?

Answer

The merge adds normalization mass from both states, so shared keys are counted twice. In the equal-logit example, duplicating the first value changes its weight from 1/2 to 2/3. Partition logical key positions, even when several requests share physical pages.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A ragged batch has query lengths 3, 1, and 4. Which pointer array describes the three request ranges in one compact query buffer?

Correct answer: qo_indptr = [0, 3, 4, 8], the prefix sums delimiting request lengths 3, 1, and 4.

qo_indptr stores prefix sums of query rows. The half-open intervals are [0,3), [3,4), and [4,8), so the compact buffer keeps request boundaries without padding.

2.R2 uses page size 4, page indices [3, 14, 6], and last_page_len 1. What logical KV length does its metadata describe?

Correct answer: Nine tokens, from two complete pages plus one occupied slot in the final page.

All pages except the tail are full: 4 × (3 - 1) = 8, then one occupied slot in the last page gives 9. kv_indptr counts pages, not tokens.

3.Two split-KV workers return outputs [1,0] and [0,1] with base-2 log-sum-exp values 2 and 1. What merged state follows the article's recurrence?

Correct answer: Output [0.667, 0.333] with merged statistic about 2.585 after normalizing worker weights 1 and 0.5.

Shift by max 2 gives weights 2^(2-2)=1 and 2^(1-2)=0.5. The weighted output divides by 1.5, and the merged statistic is 2 + log2(1.5), about 2.585.

4.A request gains a token within its last page. Page IDs stay fixed, but the caller replays old length-dependent metadata. What must the integration do?

Correct answer: Refresh the tail length and any dependent plan through the supported path before the next run.

Physical allocation can stay fixed while logical length and causal boundaries change. Update metadata and planning through the wrapper contract; this isn't automatically a kernel recompilation or graph recapture.

5.One request has a 128K KV history while seven requests have 128 tokens. Why might FlashInfer split the long history across CTAs?

Correct answer: Balance CTA work across the long history and merge partial states to reduce the latency tail instead of serializing one request.

One CTA per request leaves short requests idle while the long one runs. Split-KV assigns chunks to more CTAs, then merges output and normalization states, trading metadata and merge work for better balance.

6.A deployment has a custom attention score and head dimension not covered by a trusted precompiled binary. When is JIT the defensible path?

Correct answer: When the shape or operator needs specialization and CUDA, compiler, driver, cache, and warmup are pinned for reproducible startup.

JIT covers unusual combinations that precompiled artifacts don't. It also makes compilation and cache compatibility part of release readiness, so warmup and version pinning are required before exposing traffic.

7.A CUDA stream still reads physical KV pages when the allocator recycles them for another request. Which first check distinguishes this from a precision issue?

Correct answer: Check stream events and page ownership, then rerun deterministic reuse to expose the lifetime race.

Recycling pages before in-flight reads finish can expose another request's data. If outputs vary with ownership or synchronization order, metadata lifetime is the first boundary to inspect; precision errors behave differently under stable ownership.

8.R2 has 5 prefix tokens and processes 4 new query tokens, with all 9 K/V entries appended. Which keys may its first new query see?

Correct answer: Logical positions 0 through 5, including itself and the earlier prefix

The first query is at position K-Q = 9-4 = 5. Bottom-right causality permits j <= 5. Valid cache entries at positions 6 through 8 are still future tokens for this query.

9.A producer returns natural-log LSE l for each disjoint KV chunk. What normalization statistic should reach the base-2 merge?

Correct answer: l divided by ln(2), while keeping the normalized output unchanged

log2(Z) = ln(Z)/ln(2). Passing natural-log LSE directly to exp2 changes relative chunk weights and generally changes the merged output.

9 questions remaining.

Next Step
Continue to Deep Dive - DeepGEMM

You can now trace irregular attention through pages, plans, and composable states. Next, inspect the tensor-core GEMM kernels that sit beside that attention path, where layout, low-precision scaling, and MoE grouping decide whether the rest of the layer keeps the GPU busy.

PreviousDeep Dive - FlashAttention
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

FlashInfer

FlashInfer Community and NVIDIA · 2026

https://github.com/flashinfer-ai/flashinfer

FlashInfer v0.6.18 Release

FlashInfer Contributors · 2026

https://github.com/flashinfer-ai/flashinfer/releases/tag/v0.6.18

FlashInfer v0.6.18 Source

FlashInfer Contributors · 2026

https://github.com/flashinfer-ai/flashinfer/tree/69ff11fc4954396d98326656dc85debd2223f637

FlashInfer: Kernel Library for LLM Serving

FlashInfer Project · 2024

https://flashinfer.ai/2024/02/02/introduce-flashinfer.html

FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.

Ye, Z., et al. · 2025

https://arxiv.org/abs/2501.01005

Contributing to FlashInfer

FlashInfer Community · 2026

https://github.com/flashinfer-ai/flashinfer/blob/d020372b068f335e2fe427372e134977a2235c49/CONTRIBUTING.md

FlashInfer Apache License 2.0

FlashInfer Community · 2026

https://github.com/flashinfer-ai/flashinfer/blob/d020372b068f335e2fe427372e134977a2235c49/LICENSE

Efficient Memory Management for Large Language Model Serving with PagedAttention.

Kwon, W., et al. · 2023 · SOSP 2023

https://arxiv.org/abs/2309.06180

KV-Cache Layouts

FlashInfer Project · 2026

https://docs.flashinfer.ai/tutorials/kv_layout.html

Recursive Attention

FlashInfer Project · 2026

https://docs.flashinfer.ai/tutorials/recursive_attention.html

FlashInfer v0.6.18: Cascade Attention

FlashInfer Contributors · 2026

https://github.com/flashinfer-ai/flashinfer/blob/69ff11fc4954396d98326656dc85debd2223f637/include/flashinfer/attention/cascade.cuh

FlashInfer v0.6.18: Prefill API

FlashInfer Contributors · 2026

https://github.com/flashinfer-ai/flashinfer/blob/69ff11fc4954396d98326656dc85debd2223f637/flashinfer/prefill.py

Nsight Systems User Guide

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/nsight-systems/UserGuide/

Nsight Compute Profiling Guide

NVIDIA · 2026 · Official documentation

https://docs.nvidia.com/nsight-compute/ProfilingGuide/

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.