Asynchronous KV Cache Prefetching

An ~10-line PTX patch that issues an early L2 prefetch for the next KV block during PagedAttention compute — and why its L2 win is categorically outside the <0.65% passive-residency bound of "Worth the Bytes?"

AAAI 2026 arXiv:2504.06319 vLLM XFORMERS cp.async.bulk.prefetch.L2 Hopper sm_90

Alibaba — github.com/alibaba/vllm_xformers_prefetch

Alibaba Group · experiments on H20 (Hopper) · analysed here as an adversarial L2-reuse case for our measurement study

1. TL;DR

During the PagedAttention decode kernel, each warp iterates over the sequence's KV blocks one at a time. When a warp finishes block $b$ and reaches for block $b{+}\text{NUM\_WARPS}$, the address of that next block is only resolved as the iteration begins, so the warp stalls on a long-latency global load before its compute can proceed (an exposed long-scoreboard stall). This paper adds a single PTX instruction — cp.async.bulk.prefetch.L2.global — that, while the current block is still being multiplied, issues an asynchronous bulk prefetch of the next block's key (and later value) tensor toward L2. Because the request is issued early, those bytes are moving toward L2 ahead of use, so when the warp reaches that block its long-latency load is hidden behind compute rather than serialized in front of it. (Caveat, measured: the baseline KV is already ~85% L2-resident from natural reuse — see Sections 6-7 — so the prefetch is hiding the exposed load latency, not removing traffic or changing what sits in L2.)

~2.15×
attention-kernel speedup (paper, H20)
~1.97×
end-to-end throughput (paper, H20)
~10
lines added over stock vLLM
4096 B
bytes prefetched per instruction
Why this paper is in our study. Our measurement study "Worth the Bytes?" argues that the passive, unmanaged residency of activations in L2 between two consecutive eager-mode kernels is worth less than 0.65% of step time. This paper is the deliberately adversarial stress case: it is a large, real L2 win. But on close reading it is a different lever — an active, software-issued, intra-kernel prefetch of the KV-cache (a long-lived, high-reuse tensor), whose payoff is latency-hiding, not incidental cross-kernel carryover. Reproducing it and attributing the kernel-level cold-replay delta (ncu, 1.01-1.02x) to a drop in memory stalls — while reporting the headline 1.80x as the separate warm-loop e2e figure, not to passive residency — sharpens our bound: it shows precisely where real L2 wins come from, and that they live outside the quantity we measure.

Problem

In memory-bound decode, the PagedAttention kernel reads KV blocks. The next block's address is resolved too late for the hardware to start its long-latency load early, so that load latency is exposed as a warp stall in front of compute — even though the block is often already in L2.

Mechanism

Compute the physical address of the block NUM_WARPS iterations ahead and issue cp.async.bulk.prefetch.L2.global for it — once for K, once for V — guarded against the last block. No new launch, no extra shared memory.

Our finding (measured)

Measured on PACE H100 (job 9948442): e2e 1.80x at batch 1, 1.46x at batch 8, 0.99x at batch 32 — reproducing the paper's regime. ncu confirms the mechanism: long-scoreboard mem-stall drops 9.2%->8.5% while HBM read stays flat (2.01 MB). The baseline is already ~85% L2-resident from natural KV reuse, so the prefetch's marginal L2-hit gain is tiny (+0.2-0.4 pp); ~0% of the win is the passive activation-residency mechanism we bound.

2. Motivation — Why Prefetch the KV-Cache At All?

2.1 Why now: decode is memory-bound and getting worse

Autoregressive decoding generates one token per step and, at each step, must read the entire key/value history of every active sequence. With batch size $B$, sequence length $L$, number of KV heads $H_{kv}$, and head dimension $d$ in two bytes, the per-step KV read is roughly $2 \cdot B \cdot L \cdot H_{kv} \cdot d \cdot 2$ bytes, while the matmul work per step is tiny — a single query vector against the cached keys. The arithmetic intensity of decode is therefore far below the GPU's compute-to-bandwidth ratio, so the attention kernel spends most of its time waiting on HBM. As context windows grow into the tens of thousands of tokens and agentic workloads accumulate ever-longer histories, the fraction of decode time spent moving KV bytes only increases.

2.2 The bad scenario: a warp stalls every block boundary

PagedAttention stores the KV-cache in fixed-size physical blocks scattered across HBM, indexed through a per-sequence block table. The decode kernel walks these blocks in a strided loop — warp $w$ handles blocks $w, w{+}\text{NUM\_WARPS}, w{+}2\,\text{NUM\_WARPS}, \dots$ Each loop iteration must (1) translate the logical block to a physical block via the block table, (2) issue a global load for that block's keys, and (3) do the dot products. Because the load address is only known once the iteration begins, the kernel cannot issue that load early enough for the hardware to hide its long latency; the warp issues the load and then sits idle for hundreds of cycles waiting on it — what Nsight calls a long-scoreboard stall — before compute can resume. The stall is the exposed latency of a load that was issued too late, not a claim that the bytes had to travel from HBM (in steady state much of the KV is already L2-resident); the cost is that the warp waits in front of compute instead of behind it. Repeated across every block of every sequence, these stalls dominate the kernel's runtime.

2.3 Baseline pain: what stock vLLM already does, and where it stops

Stock vLLM's PagedAttention is already a heavily-tuned kernel: it vectorizes the loads, uses shared memory for the running softmax, and overlaps thread-group work. What it does not do is look ahead in the block table. The hardware prefetcher cannot help, because the next block's address is a pointer-chase through the block table rather than a linear stride in virtual memory — the very indirection that makes paging flexible also defeats automatic prefetch. So the baseline issues each block's load only at the start of its iteration and pays that load's full latency in the open, even though the address of the next block is, in fact, computable several iterations early from the same block table the kernel already holds in registers. (The latency is exposed because the load is issued late, not because the bytes are necessarily cold in HBM — in steady state much of the KV is already L2-resident; what's missing is the early issue, not the data.)

2.4 Strawman approaches and why they fall short

Strawman A: stage KV into shared memory

One could double-buffer the next block into SRAM with cp.async. But the decode KV working set per warp is small and shared memory is already spent on the softmax reduction; the win is marginal and the occupancy cost is real. Crucially it changes the kernel's memory hierarchy and register pressure.

Strawman B: bigger blocks / fused kernels

Larger paged blocks amortize the per-block setup but waste HBM and capacity, and a fully fused FlashAttention-style kernel is a major rewrite that the XFORMERS V0 path was not built around. Neither is a drop-in for an existing deployment.

Strawman C: trust the hardware prefetcher

As 2.3 explains, the block-table indirection defeats the stride-based hardware prefetcher. Doing nothing leaves the latency fully exposed. The address is known to software but not to the prefetcher — so software must volunteer it.

2.5 The obstacle and the positioning

The obstacle the paper must clear is precision-without-disruption: it needs to issue the next block's load/prefetch early enough to hide exposed long-scoreboard latency before use, without consuming shared memory, without blocking the issuing warp, and without rewriting the kernel. Hopper (sm_90+) supplies exactly the right primitive — cp.async.bulk.prefetch.L2.global — a non-blocking bulk-prefetch hint that requests data be moved from global memory toward L2 and returns immediately, touching neither registers nor shared memory of the issuing warp. (It is a scalar L2-prefetch instruction in the same async-bulk-copy family as TMA, but unlike a full TMA tensor copy it needs no tensor-map descriptor and writes nothing into shared memory.) The paper's positioning is therefore narrow and honest: it is not a new attention algorithm; it is a minimal, additive latency-hiding patch that exploits a Hopper hardware feature to prefetch a tensor whose address the software already knows.

2.6 Contributions

  1. A one-iteration-ahead block lookahead computed from the existing block table (block_idx + NUM_WARPS), reusing data the kernel already touches.
  2. An asynchronous L2 prefetch for both the key and the value cache of that next block, issued by a single lane while the rest of the warp computes the current block.
  3. A drop-in deployment story: a single header file replaces vLLM's attention_kernels.cuh; no scheduler, model, or API changes. Measured ~2.15× kernel and up to ~1.97× e2e on H20.

3. Core Mechanism

3.1 The overlap, visualized

The whole idea is to convert serial "load-then-compute" into overlapped "compute-while-loading". In the baseline, each block iteration begins with an exposed late global-load (long-scoreboard) stall (the warp waits in front of compute), then computes. In the patched kernel, lane 0 fires a prefetch for the next block at the start of the current iteration; the bulk-prefetch engine issues that next block's load toward L2 in the background while the warp computes the current block. By the time the next iteration needs that block, its load is already in flight, so the long-latency global load is hidden behind compute rather than serialized in front of it. (Measured caveat: the baseline KV is already ~85% L2-resident, so the observable signature is a small drop in memory stalls, not a large L2-hit jump — see Sections 6-7.)

Baseline: exposed late global-load (long-scoreboard) latency

warp timeline
late load b0
compute b0
stall (b1 wait)
compute b1
stall (b2 wait)
compute b2

Patched: prefetch overlapped with compute

warp (compute)
late load b0
compute b0
compute b1 (no stall)
compute b2 (no stall)
bulk prefetch → L2
prefetch b1
prefetch b2

The orange dashed cells (baseline stalls) shrink once the next block's bytes have been pulled toward L2 ahead of use. The bottom lane is the non-blocking bulk-prefetch path through the cache subsystem, independent of the warp's compute, which is what makes the overlap free of register and shared-memory cost.

3.2 The lookahead distance: why NUM_WARPS, not 1

Because the block loop is strided by NUM_WARPS — warp $w$ processes $w, w{+}\text{NUM\_WARPS}, \dots$ — the block a given warp will touch next is exactly block_idx + NUM_WARPS, not block_idx + 1. The patch therefore looks ahead by the stride, which keeps each warp prefetching precisely the block it itself will consume. This is read straight from the block table the warp already holds, so the lookahead address costs one extra table read and a comparison.

// attention_kernels.cuh:258 — next physical block from the block table
const int64_t next_physical_block_number = (block_idx + NUM_WARPS) < end_block_idx ?
              block_table[block_idx + NUM_WARPS] : 0;

3.3 The key-cache prefetch (the one PTX line)

Inside the per-token vector loop, exactly one lane (lane 0, first vector) issues the prefetch, guarded so the last block — whose next_physical_block_number is the sentinel 0 — is never prefetched. The address is masked to a 16-byte boundary (the low nibble cleared by & 0xFFFFFFFFFFFFFFF0) as the bulk-prefetch instruction requires an aligned global address, and the size is a fixed 4096 bytes — one paged block's worth of key data.

// attention_kernels.cuh:281-287 — Prefetch Key Cache
if (j == 0 && lane == 0) {
    const cache_t* next_k_ptr = k_cache + next_physical_block_number * kv_block_stride +
                                kv_head_idx * kv_head_stride;
    if (next_physical_block_number != 0)
    asm volatile ("cp.async.bulk.prefetch.L2.global [%0], 4096;"
        ::"l"((unsigned long)next_k_ptr & 0xFFFFFFFFFFFFFFF0UL));
}

3.4 The value-cache prefetch (the symmetric second line)

The value cache is read in a separate loop (the second pass that computes the weighted sum of values). The patch repeats the same pattern there: recompute next_physical_block_number for the value loop's iteration, and on the first row of lane 0 issue an identical bulk prefetch against the value pointer. The structure is intentionally a mirror of the key prefetch, which is why the diff is so small.

// attention_kernels.cuh:425-431 — Prefetch Value Cache
if (i == 0 && lane == 0) {
    const cache_t* next_v_ptr = v_cache + next_physical_block_number * kv_block_stride +
                                kv_head_idx * kv_head_stride;
    if (next_physical_block_number != 0)
        asm volatile ("cp.async.bulk.prefetch.L2.global [%0], 4096;"
            ::"l"((unsigned long)next_v_ptr & 0xFFFFFFFFFFFFFFF0UL));
}
Where the prefetched bytes land — and why it matters for us. The instruction is .L2.global: data is moved from global memory (HBM) into the L2 cache, not into shared memory and not into registers. That is exactly the cache level our "Worth the Bytes?" study is about. But note three things the code makes unambiguous: the prefetch is (1) issued by software (an explicit asm volatile), (2) intra-kernel (the same kernel prefetches its own future iteration), and (3) on the KV-cache (k_cache / v_cache), a long-lived high-reuse tensor. Our bound concerns the opposite on all three axes: passive, inter-kernel, activation residency.

3.5 The cost model

The benefit per block iteration is the hidden latency $t_{hide} = \min(t_{load},\, t_{compute})$, where $t_{load} \approx t_{scoreboard}$ is the exposed global-load / cache-hierarchy latency of the late load — the prefetch can hide at most as much exposed global-load latency as there is compute to overlap with. The cost is one block-table read, one comparison, one address mask, and the launch of one asynchronous prefetch per block, all on a single lane. Net speedup appears when $t_{hide}$ accumulated over all blocks dominates the tiny issue overhead, which is exactly the memory-bound regime decode lives in. This is why the paper sees its largest wins on long sequences and decode-heavy workloads.

3.6 Three correctness details hidden in the two lines

Although the patch is tiny, each token in the PTX is load-bearing, and getting any of them wrong would either crash the kernel or silently corrupt the prefetch. Three details are worth spelling out because they are exactly the kind of thing a reproduction must preserve byte-for-byte.

(a) 16-byte alignment mask

The address is ANDed with 0xFFFFFFFFFFFFFFF0 to clear its low four bits. The bulk-prefetch instruction requires a 16-byte-aligned global address; without the mask, an unaligned pointer would fault. Clearing the low nibble is safe here because it only rounds the prefetch start down to the enclosing aligned line.

(b) sentinel-0 last-block guard

When block_idx + NUM_WARPS reaches past end_block_idx, the lookahead is set to 0, and the if (next_physical_block_number != 0) guard suppresses the prefetch. This prevents reading a non-existent next block — both an out-of-bounds risk and a wasted memory transaction at every sequence boundary. (0 is a conservative no-next sentinel for the optimization, not a correctness invariant: if physical block 0 were ever a valid next block, the guard would merely skip a legal prefetch — a missed optimization, never a correctness break.)

(c) single-lane issue

The prefetch fires only on lane == 0 (and j == 0 / i == 0), so one instruction is issued per warp per block, not 32. Bulk prefetch is a per-region request; duplicating it across all lanes would multiply traffic and serialize on the same L2 line for no benefit.

4. Repo Analysis

The repository is striking in its minimalism. It contains no build system, no benchmark scripts, no Python — just a license, a short README, and one CUDA header. The entire intellectual contribution is shipped as a drop-in replacement for one vLLM file. We cloned it to the path below and confirmed its contents are exactly as described.

vllm_xformers_prefetch/
LICENSE  (Apache 2.0, 11.4 KB)
README.md  (install = drop-in + recompile; usage = VLLM_ATTENTION_BACKEND=XFORMERS)
attention_kernels.cuh  (696 lines, 29.8 KB — THE entire patch)

4.1 The diff against stock vLLM v0.10.2

The header is a near-verbatim copy of vLLM's own csrc/attention/attention_kernels.cuh with the prefetch additions woven in. The additive change is small — two next-block computations, two PTX prefetch sites (one for K at line 286, one for V at line 430), each behind a last-block guard. The table below maps the paper's mechanism onto exact code locations we read in the cloned header.

Mechanism Location in attention_kernels.cuh What it does
Strided block loop :224-225, :389-390 warp w iterates blocks w, w+NUM_WARPS, ... (stock vLLM)
K next-block lookahead :258-259 next_physical_block_number = block_table[block_idx + NUM_WARPS] or 0
K prefetch (PTX) :281-287 lane 0 issues cp.async.bulk.prefetch.L2.global for next K block, 4096 B
V next-block lookahead :407-408 same lookahead recomputed for the value loop
V prefetch (PTX) :425-431 lane 0 issues cp.async.bulk.prefetch.L2.global for next V block, 4096 B
Last-block guard :285, :429 if (next_physical_block_number != 0) — 0 is a conservative no-next sentinel for the prefetch (skips, never corrupts)
Cloned repo (confirmed). We verified the repository at papers/kv-prefetch/repo/ contains exactly three files (LICENSE, README.md, attention_kernels.cuh) with a single git commit (0b804d0 "upload cuh and README"). The line numbers above are read from that file. Because the repo ships no setup.py and no benchmarks, our reproduction harness is reconstructed from vLLM's own benchmarks/benchmark_throughput.py — a deliberate, documented choice.

5. Claims vs Implementation

Each paper/README claim, checked against the cloned header. Performance claims are the paper's H20 numbers; our own measured H100 numbers are in Sections 6-7.

Claim Status Code evidence
Adds cp.async.bulk.prefetch.L2.global for next K and V blocks YES Two identical asm volatile sites: K @ :286, V @ :430
Prefetches the NEXT block during current-block compute YES lookahead = block_idx + NUM_WARPS (:258), issued inside the compute loop before the block is consumed
Minimal, additive patch over stock vLLM YES One header; the loops, softmax, and reductions are stock vLLM; only the lookahead + 2 prefetch sites are new (~10 lines)
Targets the XFORMERS backend's PagedAttention kernel YES File is csrc/attention/attention_kernels.cuh (paged_attention_v1/v2); README sets VLLM_ATTENTION_BACKEND=XFORMERS
~2.15x attention-kernel speedup MEASURED: ncu only ~1.01-1.02x Paper's H20 number. Our H100 ncu isolated-kernel time is only 1.01-1.02x (45.4->44.8 us b1; 45.6->44.8 us b8); the big win is async overlap across the warm decode loop, which cold-cache replay under-represents (Section 7).
up to ~1.97x end-to-end throughput MEASURED: up to 1.80x Reproduced on H100: 1.80x at batch 1, 1.46x at batch 8, 0.99x at batch 32. Big at low batch / decode-heavy, vanishing to ~1.0x at batch 32 (compute-bound) (Section 6.4).
Requires Hopper (sm_90) YES cp.async.bulk.prefetch is a Hopper bulk-async-prefetch instruction requiring sm_90+; our build targeted 9.0a. A100/L40S/RTX6000 (sm_80/89) are excluded.

Strengths

  • Surgical: one file, ~10 lines, no occupancy or shared-memory cost.
  • Exploits a real hardware gap: HW prefetcher can't follow block-table indirection, but software can.
  • Correct by construction: last-block guard prevents prefetching past the sequence.

Limitations

  • Hopper-only; no fallback path for sm_80 or below.
  • Lives in the V0 / XFORMERS PagedAttention path; vLLM's V1 default and FlashAttention backend don't use this kernel.
  • Fixed 4096 B prefetch size and one-ahead distance are not tuned per block size or dtype.
  • No build/benchmark scripts shipped — reproduction harness must be reconstructed.

6. Our Reproduction & Measurement MEASURED

Measured on PACE H100 (job 9948442). Platform: PACE H100 80GB (sm_90, 50 MB L2), job 9948442, Llama-3.1-8B fp16, XFORMERS V0 paged_attention_v1. Baseline = stock vLLM 0.10.2; patched = 0.10.3.dev with the cp.async.bulk.prefetch.L2 header. Both trees were built and run; the results below are real measurements, not predictions. FACT = directly measured; BOUND = derived/bounded.

6.1 How much of this gain is the mechanism we bound?

The measured answer is concrete: ~0% of this paper's speedup is the passive inter-kernel activation residency we bound at <0.65% BOUND. The reasoning follows directly from the code and is confirmed by the counters. The carryover here is (1) software-issued — an explicit PTX instruction, not data left behind incidentally; (2) intra-kernel — the same kernel prefetches a later iteration of itself, so no second kernel is involved at all; and (3) on the KV-cache — a tensor that is read every decode step, not a transient activation. Each of those three properties places it outside the definition of the quantity our study measures. The measured signature matches at the kernel level: the cold-replay per-kernel delta (ncu, 1.01-1.02x) tracks a memory-stall reduction (long-scoreboard 9.2%->8.5%) with HBM read flat — not any cross-kernel cache hand-off. The headline 1.80x is the warm-loop e2e figure; the cold-replay-vs-warm-loop gap is reported separately (Section 7), not folded into the stall drop.

6.2 Method: what we build and run, on which GPU

1
Build x2
stock v0.10.2 cuh + patched cuh, isolated conda envs, sm_90a
2
E2E
benchmark_throughput.py, Llama-3.1-8B, batch {1,8,32}
3
ncu
kernel-filtered Nsight Compute on both builds
4
Attribute
L2 hit up? stall down? bytes flat? -> separability

6.3 The Nsight metrics that isolate L2-reuse from the other levers

ncu metric Isolates Measured base → patch (H100)
lts__t_sector_op_read_hit_rate.pct L2 READ hit rate — the prefetch's direct fingerprint 85.2->85.4% (b1), 84.6->85.0% (b8) — already L2-resident
smsp__warp_issue_stalled_long_scoreboard_per_warp_active.pct Global-load memory stall — the actual speedup lever 9.2% -> 8.5% (latency hidden)
dram__bytes_read.sum HBM read traffic — separates this from a dedup/traffic-cut mechanism 2.01 MB -> 2.01 MB (flat; same KV reads issued earlier, not removed)
sm__throughput.avg.pct_of_peak_sustained_elapsed Compute utilization — rises as stalls are hidden 33.9% -> ~35.4% (up)
gpu__time_duration.sum Per-kernel time — the speedup numerator/denominator 45.4->44.8 us (1.01x, b1); 45.6->44.8 us (1.02x, b8)

We use the project's proven application-replay recipe (--replay-mode application --cache-control all --clock-control none --launch-count 40 --launch-skip 200, kernel-filtered to regex:paged_attention) to get cold-cache semantics without kernel-replay's host-RAM snapshot, which OOM'd this project at 67 GB on an H200.

6.4 E2E throughput (FACT) MEASURED

Random dataset, input-len 512, output-len 512, tok/s base -> patch. This reproduces the paper's e2e regime (up to ~1.97x on Hopper): the win is big at low batch / decode-heavy and vanishes by batch 32 (compute-bound). We ran H100; the paper's headline was H20.

Batch Baseline (tok/s) Patched (tok/s) Speedup
1115.5207.81.80×
81159.81692.21.46×
32505749940.99×

6.5 Per-kernel attribution (FACT) MEASURED

Nsight Compute on paged_attention_v1 (cold-cache application replay, --cache-control all, input-len 1024 output-len 256, mean over ~40 launches). The isolated kernel speedup is only ~1.01-1.02x — far below the e2e 1.80x — because the baseline is already ~85% L2-resident and cold-cache replay under-represents the warm-loop async overlap. The mechanism is visible as a memory-stall drop, not an L2-residency jump.

Quantity Batch 1 Batch 8 Tag
Kernel time base -> patch (speedup)45.4 -> 44.8 us (1.01×)45.6 -> 44.8 us (1.02×)FACT
L2 read-hit rate85.2% -> 85.4%84.6% -> 85.0%FACT
Long-scoreboard mem-stall9.2% -> 8.5%9.2% -> 8.5%FACT
DRAM read2.01 MB (flat)2.01 MB (flat)FACT
SM busy33.9% -> ~35.4%33.9% -> ~35.4%FACT
Persisting-L2 access-policy window9.8 MB (both builds)9.8 MB (both builds)FACT
Passive activation-residency share of gain~0% (outside our <0.65% bound)BOUND
Honest note: the e2e 1.80x vs per-kernel 1.01x gap. The headline e2e 1.80x and the per-kernel ncu 1.01x are both real and the gap is explained, not hidden. The prefetch's benefit is async overlap accumulated across the warm decode loop; ncu's cold-cache isolated-kernel replay (--cache-control all) resets caches each pass and under-represents that steady-state pipelined gain — an instance of the cold-vs-warm profiler gap our paper documents. We therefore report the warm e2e as the headline and the ncu as the mechanism-level attribution, noting the gap. Two further facts keep this honest: (1) the baseline attention kernel is already ~85% L2-resident from natural KV reuse, so the prefetch's marginal L2-residency gain is tiny (+0.2-0.4 pp); it mainly cuts memory stalls (active latency-hiding). (2) the same KV reads are issued earlier / overlapped — DRAM read is flat (2.01 MB), so the traffic was not removed, only its latency hidden.

7. L2 Attribution — Reading the Mechanism Off the Counters MEASURED

Profiling both builds on the same workload decides, from the hardware counters alone, which lever produces the speedup. There are four candidate explanations; the measured metric pattern distinguishes them. The H100 counters confirm lever (A): the per-kernel time delta tracks the long-scoreboard mem-stall drop (9.2%->8.5%), L2 read-hit barely moves (already ~85% resident), and HBM read is flat (2.01 MB) — so it is active latency-hiding of the prefetched KV, not residency or a traffic cut.

Observation: patched kernel is faster (gpu__time_duration.sum down)

Which lever?

(A) Active prefetch / latency-hiding — THE mechanism

Signature: long-scoreboard stall DOWN, time-delta tracks the stall drop. MEASURED: yes — stall 9.2%->8.5%, kernel 1.01-1.02x, L2 hit already ~85% so it barely rises.

(B) SRAM / shared-mem staging

Would show as shared-mem traffic change. CODE: ruled out — the asm targets .L2.global and adds no smem allocation, so the kernel's shared-memory use is unchanged.

(C) Kernel-launch overhead

Would show as fewer launches. CODE: ruled out — the patch is intra-kernel and adds no new launch, so the launch count is unchanged between builds.

(D) HBM traffic cut (weight dedup etc.)

Would show as dram__bytes_read DOWN. MEASURED: ruled out — bytes flat (2.01 MB); same KV read, just earlier.

The decisive test was correlation, and the counters bear it out: the per-kernel time reduction tracks the drop in long-scoreboard stall percentage, so the speedup is latency-hiding of the prefetched KV — lever (A). HBM bytes staying flat (2.01 MB) prove the same KV reads were issued earlier / overlapped — the traffic was not removed, only its latency hidden; and by construction the patch adds no kernel launch and no shared-memory allocation, ruling out levers (C) and (B). One honest subtlety: the L2 read-hit rate barely rises (+0.2-0.4 pp) because the baseline KV is already ~85% L2-resident — so the prefetch's fingerprint is the stall drop, not a residency jump. This is the same separation logic our measurement study applies to bound passive residency — here it identifies an entirely different, active lever.

9. Separability From Our <0.65% Bound

This is why the paper belongs in our study as a sharpening case rather than a counterexample. Our bound is about one specific quantity: the value of letting a prior kernel's output activation sit in L2 long enough that the next kernel reads it from L2 instead of HBM, with no software doing anything to make that happen. The prefetch patch differs on every axis that defines that quantity.

Axis Our bound (<0.65%) This paper's prefetch
Agency PASSIVE — data left in L2 incidentally ACTIVE — explicit cp.async.bulk.prefetch.L2 PTX
Scope INTER-kernel — kernel N -> kernel N+1 INTRA-kernel — same kernel, future iteration
Tensor ACTIVATION — transient, written then read once KV-CACHE — long-lived, re-read every decode step
Payoff Avoided HBM re-read of carried-over bytes Latency-hiding: overlap fetch with current-block compute
HBM bytes moved REDUCED (re-read avoided) UNCHANGED (same reads issued earlier, not removed)
Conclusion. On all five axes the prefetch patch is the opposite of the mechanism we bound, so ~0% of its gain is passive inter-kernel activation residency. Far from contradicting "Worth the Bytes?", confirming this separability sharpens it: real, large L2 wins in LLM inference come from actively prefetching a high-reuse tensor to hide latency — a categorically different lever from the incidental activation hand-off we measure, which remains worth <0.65% of step time. Our measured H100 attribution (job 9948442) identifies the kernel-level mechanism: in cold-cache isolated replay the per-kernel speedup is only 1.01-1.02x, carried by a memory-stall drop (9.2%->8.5%) with HBM read flat and L2 hit already ~85% in the baseline. The headline 1.80x is the warm-loop e2e figure; cold replay does not, in isolation, explain it — the cold-replay-vs-warm-loop gap is reported, not attributed to the stall drop.

10. How to Reproduce

The repo ships no build or benchmark scripts, so reproduction means: build stock and patched v0.10.2, then drive both with vLLM's own throughput benchmark and Nsight Compute. The two sbatch scripts below ran on PACE H100 (job 9948442); the build and the H100 run are complete and the numbers in Sections 6-7 are the result. (vLLM has since moved benchmark_throughput.py to `vllm bench throughput`.)

10.1 Install (per the repo README)

# 1. clone vLLM v0.10.2 (twice: baseline + patched)
git clone --depth=1 --branch v0.10.2 https://github.com/vllm-project/vllm.git baseline
git clone --depth=1 --branch v0.10.2 https://github.com/vllm-project/vllm.git patched

# 2. drop the repo header into the patched tree only
cp attention_kernels.cuh patched/csrc/attention/attention_kernels.cuh

# 3. build EACH tree into its OWN conda env (avoids two-tree .so drift)
export TORCH_CUDA_ARCH_LIST="9.0a"   # 'a' variant exposes the bulk-prefetch instruction
# module load cuda/12.9.1  (NOT 12.6.1 — ptxas SIGSEGVs on FA3 Hopper kernels)
pip install --no-cache-dir --no-build-isolation -e .
pip install xformers

10.2 Run e2e + attribution

export VLLM_ATTENTION_BACKEND=XFORMERS
export VLLM_USE_V1=0   # exercise the legacy PagedAttention decode kernel

# end-to-end throughput (decode-heavy: long output_len)
python benchmarks/benchmark_throughput.py --model Llama-3.1-8B \
  --dtype float16 --input-len 512 --output-len 512 --num-prompts 8 --output-json out.json

# kernel attribution (cold-cache, application-replay; NOT kernel-replay -> host-RAM OOM)
ncu --replay-mode application --cache-control all --clock-control none \
    --target-processes all -k "regex:paged_attention" \
    --launch-count 40 --launch-skip 200 --csv --page raw \
    --metrics gpu__time_duration.sum,dram__bytes_read.sum,\
lts__t_sector_op_read_hit_rate.pct,\
smsp__warp_issue_stalled_long_scoreboard_per_warp_active.pct,\
sm__throughput.avg.pct_of_peak_sustained_elapsed \
    python benchmarks/benchmark_throughput.py --model Llama-3.1-8B ...
Hardware & environment requirements.
  • GPU: Hopper (sm_90) only — H100/H200 on PACE. cp.async.bulk.prefetch will not compile for A100/L40S/RTX6000.
  • CUDA: load cuda/12.9.1 first (12.6.1 ptxas crashes on FA3 Hopper templates); fall back to 12.1.1 only if needed.
  • vLLM: build a fresh v0.10.2 — the existing PACE vllm-pace env is v0.19.0 with a different paged_attention API.
  • Build node: cpu-small (16 CPU, 96 GB); single-arch sm_90a; isolated conda env per tree.
Artifact (our project tree) Purpose
pace/prefetch_build_cpu.sbatchbuilds v0.10.2 baseline + patched, sm_90a, dumps torch pins + patch diff + sm-arch list
pace/prefetch_run_h100.sbatche2e throughput + ncu attribution on H100; emits prefetch_attribution.json
papers/kv-prefetch/repo/attention_kernels.cuhthe treatment header (drop-in over stock v0.10.2)