kernelbench.com

KernelBench hard · RTX PRO 6000

Paged Attention DeepSeek V4 Flash (0731)

48.6%geomean peak fraction across shapes

manually audited: clean

Genuine hand-written CUDA paged-attention decode kernel via torch.utils.cpp_extension.load_inline (sm_120): cp.async.cg 16B double-buffered page streaming into smem, per-warp online softmax over the block's page range, split-K across (B*Hkv*SPLITS) blocks with either a separate reduce kernel (D=128) or an atomic-counter fused last-block reduction (D=64), GQA-aware so every KV byte is read once for all G query heads. Zero forbidden ops (no vllm/flashinfer/SDPA — grep hit count 0). Forward calls are captured into a torch.cuda.CUDAGraph and replayed when the SAME four input tensor objects are passed again (identity `is` check, lines 386-391) — this is the pattern needing the standard empirical recompute test. Static data-flow says it is benign: the graph is captured by calling _launch directly on the passed tensors with NO staging copies, so the captured kernels read query/kv_cache/block_table/seq_lens memory in place and replay recomputes on whatever bytes those buffers currently hold; the persistent buffers (_out/_pm/_pl/_pacc/_counters) are outputs and scratch, never cached inputs or memoized outputs. template_mutated=false; check.log PASS including numeric stress.

harnessor-fableagent session1h 53mtotal wall1h 55mcheck87sbenchmark9soutput tokensgpu-lock wait1sgpu-lock held2mregimememory

Per-shape vs governing ceilingeach shape graded against whichever binds — bf16 compute or HBM bandwidth

8×32×8×128×1024×160.043 ms43.5%0.78 TB/s · 44% of 1.8 TB/s HBM · also 3 TFLOPS (1% of compute)
32×32×8×128×2048×160.198 ms75.5%1.36 TB/s · 76% of 1.8 TB/s HBM · also 5 TFLOPS (1% of compute)
4×64×8×128×4096×160.075 ms50.0%0.90 TB/s · 50% of 1.8 TB/s HBM · also 7 TFLOPS (1% of compute)
16×32×8×128×1535×160.088 ms64.0%1.15 TB/s · 64% of 1.8 TB/s HBM · also 5 TFLOPS (1% of compute)
8×16×4×64×2000×160.035 ms25.8%0.46 TB/s · 26% of 1.8 TB/s HBM · also 2 TFLOPS (0% of compute)

compute-bound memory-bound · bar + right column = official fraction of the ceiling (the geomean input)

geomean(43.5% · 75.5% · 50.0% · 64.0% · 25.8%) = 48.6%

Kernel source (redacted)
"""Paged-attention decode kernel for RTX PRO 6000 (SM120, GDDR7).

Single-query decode over a paged KV cache with GQA. Memory-bound: the KV cache
must be streamed once. Grid is (B*Hkv*SPLITS) blocks; each block owns one
(b, kv_head, page-range) and computes all G query heads sharing that kv head
(so every KV byte is read exactly once). 128 threads = 4 warps; for G=8 each
warp owns two query heads (HPW=2) so K/V are read from smem once per token for
both heads; for G=4 each warp owns one head. Each warp keeps an online-softmax
partial (m, l, acc) over the block's page range.

Cross-split reduction: for head_dim=128 the partials are reduced by a tiny
second kernel (FUSE=0); for head_dim=64 the last block per (b, kv_head)
(tracked by a global atomic counter) combines the SPLITS partials and writes
the G output heads in the same launch (FUSE=1), which avoids a second kernel
launch whose cost would be a large fraction of the small D=64 kernel. Forward
calls are captured into a CUDA graph so repeated benchmark calls replay at
minimal launch overhead.

The KV cache is (num_blocks, page_size, num_kv_heads, 2*head_dim) bf16 with the
last dim packing [K | V]. Pages are gathered through block_table[b].
"""
import math
import os

import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

_CUDA_SRC = r"""
#include <torch/extension.h>
#include <ATen/cuda/CUDAContext.h>
#include <c10/cuda/CUDAException.h>
#include <cuda_bf16.h>
#include <cuda_runtime.h>
#include <math.h>

#define DEV_INLINE __device__ __forceinline__

DEV_INLINE void cp_async16(void* smem, const void* gmem) {
    unsigned saddr = (unsigned)__cvta_generic_to_shared(smem);
    asm volatile("cp.async.cg.shared.global [%0], [%1], 16;\n" ::"r"(saddr), "l"(gmem));
}
DEV_INLINE void cp_commit() { asm volatile("cp.async.commit_group;\n"); }
DEV_INLINE void cp_wait_group(int n) {
    if (n == 0) asm volatile("cp.async.wait_group 0;\n");
    else if (n == 1) asm volatile("cp.async.wait_group 1;\n");
    else if (n == 2) asm volatile("cp.async.wait_group 2;\n");
}

template <int D, int THREADS>
DEV_INLINE void load_page(const __nv_bfloat16* __restrict__ kv,
                          const int* __restrict__ block_table,
                          int b, int max_pages, int pi, int kh,
                          int Hkv, int P, __nv_bfloat16* smem) {
    const int page = block_table[(size_t)b * max_pages + pi];
    const __nv_bfloat16* base = kv + (size_t)page * (P * Hkv * 2 * D) + (size_t)kh * (2 * D);
    const int cpt = (2 * D) / 8;
    const int nchunks = P * cpt;
    for (int c = threadIdx.x; c < nchunks; c += THREADS) {
        int t = c / cpt;
        int off = (c % cpt) * 8;
        cp_async16(smem + (size_t)t * (2 * D) + off, base + (size_t)t * (Hkv * 2 * D) + off);
    }
}

template <int D, int HPW, int THREADS>
__global__ void __launch_bounds__(THREADS) paged_attn128_kernel(
    const __nv_bfloat16* __restrict__ q,
    const __nv_bfloat16* __restrict__ kv,
    const int* __restrict__ block_table,
    const int* __restrict__ seq_lens,
    float* __restrict__ pm,
    float* __restrict__ pl,
    float* __restrict__ pacc,
    int* __restrict__ counters,
    __nv_bfloat16* __restrict__ out,
    int B, int H, int Hkv, int max_pages, int P, int SPLITS,
    float scale, int FUSE) {
    constexpr int VPT = D / 32;
    const int G = H / Hkv;
    const int NW = THREADS / 32;
    const int warp = threadIdx.x >> 5;
    const int lane = threadIdx.x & 31;
    const int g0 = warp * HPW;

    const int gid = blockIdx.x;
    const int split = gid % SPLITS;
    const int kv_id = gid / SPLITS;
    const int b = kv_id / Hkv;
    const int kh = kv_id % Hkv;
    const int L = seq_lens[b];
    const int n_pages = (L + P - 1) / P;
    int p_start = (split * n_pages) / SPLITS;
    int p_end = ((split + 1) * n_pages) / SPLITS;
    if (p_end > n_pages) p_end = n_pages;
    const int nchunk = p_end - p_start;

    __shared__ __nv_bfloat16 qsmem[8][128];
    __shared__ __nv_bfloat16 pages[2][16][256];

    for (int i = threadIdx.x; i < G * D; i += THREADS) {
        int g = i / D, d = i % D;
        qsmem[g][d] = q[(size_t)(b * H + kh * G + g) * D + d];
    }
    __syncthreads();

    const bool active = (g0 + HPW <= G);
    float qr[HPW][VPT];
    if (active) {
#pragma unroll
        for (int hh = 0; hh < HPW; ++hh)
#pragma unroll
            for (int i = 0; i < VPT; ++i)
                qr[hh][i] = __bfloat162float(qsmem[g0 + hh][i * 32 + lane]);
    }

    float m[HPW], l[HPW];
    float acc[HPW][VPT];
#pragma unroll
    for (int hh = 0; hh < HPW; ++hh) { m[hh] = -INFINITY; l[hh] = 0.f; }
#pragma unroll
    for (int hh = 0; hh < HPW; ++hh)
#pragma unroll
        for (int i = 0; i < VPT; ++i) acc[hh][i] = 0.f;

    int cur = 0;
    if (nchunk > 0) {
        load_page<D, THREADS>(kv, block_table, b, max_pages, p_start, kh, Hkv, P, &pages[0][0][0]);
        cp_commit();
    }
    for (int pi = 0; pi < nchunk; ++pi) {
        int nxt = cur ^ 1;
        if (pi + 1 < nchunk) {
            load_page<D, THREADS>(kv, block_table, b, max_pages, p_start + pi + 1, kh, Hkv, P, &pages[nxt][0][0]);
            cp_commit();
        }
        if (pi + 1 < nchunk) cp_wait_group(1); else cp_wait_group(0);
        __syncthreads();
        if (active) {
            const __nv_bfloat16* buf = &pages[cur][0][0];
            int page_global = p_start + pi;
            for (int t = 0; t < P; ++t) {
                int gtok = page_global * P + t;
                if (gtok >= L) continue;
#pragma unroll
                for (int hh = 0; hh < HPW; ++hh) {
                    float score = 0.f;
#pragma unroll
                    for (int i = 0; i < VPT; ++i) {
                        int d = i * 32 + lane;
                        score += qr[hh][i] * __bfloat162float(buf[(size_t)t * (2 * D) + d]);
                    }
#pragma unroll
                    for (int off = 16; off > 0; off >>= 1)
                        score += __shfl_xor_sync(0xffffffffu, score, off);
                    score *= scale;
                    float m_new = fmaxf(m[hh], score);
                    float alpha = __expf(m[hh] - m_new);
                    float beta = __expf(score - m_new);
                    l[hh] = l[hh] * alpha + beta;
#pragma unroll
                    for (int i = 0; i < VPT; ++i) {
                        int d = i * 32 + lane;
                        acc[hh][i] = acc[hh][i] * alpha + beta * __bfloat162float(buf[(size_t)t * (2 * D) + D + d]);
                    }
                    m[hh] = m_new;
                }
            }
        }
        __syncthreads();
        cur = nxt;
    }

    if (nchunk == 0) {
#pragma unroll
        for (int hh = 0; hh < HPW; ++hh) { m[hh] = -INFINITY; l[hh] = 0.f; }
#pragma unroll
        for (int hh = 0; hh < HPW; ++hh)
#pragma unroll
            for (int i = 0; i < VPT; ++i) acc[hh][i] = 0.f;
    }
    if (active) {
#pragma unroll
        for (int hh = 0; hh < HPW; ++hh) {
            int h = kh * G + g0 + hh;
            size_t poff = (size_t)split * B * H + (size_t)b * H + h;
            pm[poff] = m[hh];
            pl[poff] = l[hh];
            float* pacc_h = pacc + (size_t)poff * D;
#pragma unroll
            for (int i = 0; i < VPT; ++i) pacc_h[i * 32 + lane] = acc[hh][i];
        }
    }

    // Fused cross-split reduction: last block per (b, kh) combines and writes out.
    // (Skipped when FUSE==0; the host then launches a separate reduce kernel.)
    if (FUSE) {
    __shared__ int sm_old;
    __syncthreads();
    if (threadIdx.x == 0) {
        __threadfence();
        sm_old = atomicAdd(&counters[kv_id], 1);
    }
    __syncthreads();
    if (sm_old == SPLITS - 1) {
        const int total = G * D;
        for (int i = threadIdx.x; i < total; i += THREADS) {
            int hh = i / D;
            int d = i % D;
            int h = kh * G + hh;
            size_t bh = (size_t)b * H + h;
            float M = -INFINITY;
            for (int s = 0; s < SPLITS; ++s) {
                float mm = pm[(size_t)s * B * H + bh];
                M = fmaxf(M, mm);
            }
            float Lsum = 0.f, a = 0.f;
            for (int s = 0; s < SPLITS; ++s) {
                size_t pbase = (size_t)s * B * H + bh;
                float e = __expf(pm[pbase] - M);
                Lsum += pl[pbase] * e;
                a += pacc[pbase * D + d] * e;
            }
            out[bh * D + d] = __float2bfloat16(a / Lsum);
        }
        if (threadIdx.x == 0) counters[kv_id] = 0;
    }
    }  // end FUSE
}


template <int D>
__global__ void __launch_bounds__(D) paged_attn_reduce_kernel(
    const float* __restrict__ pm, const float* __restrict__ pl,
    const float* __restrict__ pacc, __nv_bfloat16* __restrict__ out,
    int B, int H, int SPLITS) {
    int gid = blockIdx.x;
    int b = gid / H, h = gid % H;
    int t = threadIdx.x;
    if (t >= D) return;
    size_t bh = (size_t)b * H + h;
    float M = -INFINITY;
    for (int s = 0; s < SPLITS; ++s) {
        float mm = pm[(size_t)s * B * H + bh];
        M = fmaxf(M, mm);
    }
    float Lsum = 0.f, acc = 0.f;
    for (int s = 0; s < SPLITS; ++s) {
        size_t base = ((size_t)s * B * H + bh) * D;
        float mm = pm[(size_t)s * B * H + bh];
        float e = __expf(mm - M);
        Lsum += pl[(size_t)s * B * H + bh] * e;
        acc += pacc[base + t] * e;
    }
    out[bh * D + t] = __float2bfloat16(acc / Lsum);
}

torch::Tensor paged_attn128(torch::Tensor query, torch::Tensor kv_cache,
                            torch::Tensor block_table, torch::Tensor seq_lens,
                            torch::Tensor pm, torch::Tensor pl, torch::Tensor pacc,
                            torch::Tensor counters, torch::Tensor out,
                            int64_t SPLITS, int64_t THREADS_ARG, int64_t HPW_ARG, int64_t FUSE) {
    int B = query.size(0), H = query.size(1), D = query.size(2);
    int Hkv = kv_cache.size(2);
    int P = kv_cache.size(1);
    int max_pages = block_table.size(1);
    float scale = 1.0f / sqrtf((float)D);
    int G = H / Hkv;
    cudaStream_t stream = at::cuda::getCurrentCUDAStream();
    int grid = B * Hkv * SPLITS;
    const __nv_bfloat16* qp = reinterpret_cast<const __nv_bfloat16*>(query.data_ptr());
    const __nv_bfloat16* kvp = reinterpret_cast<const __nv_bfloat16*>(kv_cache.data_ptr());
    const int* btp = reinterpret_cast<const int*>(block_table.data_ptr());
    const int* slp = reinterpret_cast<const int*>(seq_lens.data_ptr());
    float* pmp = pm.data_ptr<float>();
    float* plp = pl.data_ptr<float>();
    float* paccp = pacc.data_ptr<float>();
    int* ctp = counters.data_ptr<int>();
    __nv_bfloat16* outp = reinterpret_cast<__nv_bfloat16*>(out.data_ptr());
    auto launch = [&](auto Dt, auto HPWt, auto THt) {
        constexpr int DD = decltype(Dt)::value, HHP = decltype(HPWt)::value, TT = decltype(THt)::value;
        paged_attn128_kernel<DD, HHP, TT><<<grid, TT, 0, stream>>>(qp, kvp, btp, slp, pmp, plp, paccp, ctp, outp, B, H, Hkv, max_pages, P, SPLITS, scale, FUSE);
        if (!FUSE) {
            if (DD == 128) paged_attn_reduce_kernel<128><<<B*H, 128, 0, stream>>>(pmp, plp, paccp, outp, B, H, SPLITS);
            else paged_attn_reduce_kernel<64><<<B*H, 64, 0, stream>>>(pmp, plp, paccp, outp, B, H, SPLITS);
        }
    };
    int64_t threads = THREADS_ARG, hpw = HPW_ARG;
    if (D == 128 && threads == 256 && hpw == 1) launch(std::integral_constant<int,128>{}, std::integral_constant<int,1>{}, std::integral_constant<int,256>{});
    else if (D == 128 && threads == 256 && hpw == 2) launch(std::integral_constant<int,128>{}, std::integral_constant<int,2>{}, std::integral_constant<int,256>{});
    else if (D == 128 && threads == 128 && hpw == 1) launch(std::integral_constant<int,128>{}, std::integral_constant<int,1>{}, std::integral_constant<int,128>{});
    else if (D == 128 && threads == 128 && hpw == 2) launch(std::integral_constant<int,128>{}, std::integral_constant<int,2>{}, std::integral_constant<int,128>{});
    else if (D == 64 && threads == 256 && hpw == 1) launch(std::integral_constant<int,64>{}, std::integral_constant<int,1>{}, std::integral_constant<int,256>{});
    else if (D == 64 && threads == 128 && hpw == 1) launch(std::integral_constant<int,64>{}, std::integral_constant<int,1>{}, std::integral_constant<int,128>{});
    else TORCH_CHECK(false, "unsupported config");
    C10_CUDA_KERNEL_LAUNCH_CHECK();
    return out;
}
"""

_CPP_SRC = r"""#include <torch/extension.h>
torch::Tensor paged_attn128(torch::Tensor query, torch::Tensor kv_cache,
                            torch::Tensor block_table, torch::Tensor seq_lens,
                            torch::Tensor pm, torch::Tensor pl, torch::Tensor pacc,
                            torch::Tensor counters, torch::Tensor out,
                            int64_t SPLITS, int64_t THREADS_ARG, int64_t HPW_ARG, int64_t FUSE);
"""

_EXTRA_FLAGS = ["-O3"]
_arch = os.environ.get("PA_ARCH", "sm_120")
_EXTRA_FLAGS += ["-arch=" + _arch]

_ext = load_inline(
    name="paged_attn_kernel",
    cpp_sources=[_CPP_SRC],
    cuda_sources=[_CUDA_SRC],
    functions=["paged_attn128"],
    extra_cuda_cflags=_EXTRA_FLAGS,
    verbose=False,
)

# Tuning knobs (env-overridable).
_TARGET_BLOCKS = int(os.environ.get("PA_TARGET_BLOCKS", "0"))


def _pick_splits(batch, num_heads, num_kv_heads, head_dim, seq_len, page_size):
    n_pages = (seq_len + page_size - 1) // page_size
    n_kv = batch * num_kv_heads
    G = num_heads // num_kv_heads
    # Per-shape tuned grid target: G=8 (2 heads/warp) and D=64 want more,
    # smaller chunks; the G=4/D=128 shapes do best with ~700 blocks.
    target = _TARGET_BLOCKS
    if target <= 0:
        target = 900 if (G == 8 or head_dim == 64) else 700
    splits = max(1, (target + n_kv - 1) // n_kv)
    splits = max(1, min(splits, n_pages))
    return splits


class Model(nn.Module):
    """Single-query paged attention decode."""

    def __init__(
        self,
        batch: int,
        num_heads: int,
        num_kv_heads: int,
        head_dim: int,
        seq_len: int,
        page_size: int,
    ):
        super().__init__()
        assert num_heads % num_kv_heads == 0, "num_heads must be a multiple of num_kv_heads (GQA)"
        self.batch = batch
        self.num_heads = num_heads
        self.num_kv_heads = num_kv_heads
        self.head_dim = head_dim
        self.seq_len = seq_len
        self.page_size = page_size
        self.group_size = num_heads // num_kv_heads
        self.scale = 1.0 / math.sqrt(head_dim)
        self.splits = _pick_splits(batch, num_heads, num_kv_heads, head_dim, seq_len, page_size)
        self.fuse = 1 if head_dim == 64 else 0

        self.register_buffer("_dummy", torch.zeros(1, dtype=torch.bfloat16), persistent=False)
        self._out = None
        self._pm = None
        self._pl = None
        self._pacc = None
        self._counters = None
        self._graph = None
        self._graph_args = None

    def _launch(self, query, kv_cache, block_table, seq_lens):
        G = self.num_heads // self.num_kv_heads
        hpw = 2 if G == 8 else 1
        return _ext.paged_attn128(
            query, kv_cache, block_table, seq_lens,
            self._pm, self._pl, self._pacc, self._counters, self._out,
            self.splits, 128, hpw, self.fuse,
        )

    def __call__(self, *args, **kwargs):
        # Skip nn.Module's hook dispatch overhead for the hot path.
        return self.forward(*args, **kwargs)

    def forward(
        self,
        query: torch.Tensor,
        kv_cache: torch.Tensor,
        block_table: torch.Tensor,
        seq_lens: torch.Tensor,
    ) -> torch.Tensor:
        # Fast path: replay the captured graph when the exact same input
        # tensors are passed again (benchmark loops reuse them). Identity
        # checks are much cheaper than data_ptr() calls.
        g = self._graph
        if g is not None:
            a = self._graph_args
            if query is a[0] and kv_cache is a[1] and block_table is a[2] and seq_lens is a[3]:
                g.replay()
                return self._out

        B, H, D = query.shape
        dev = query.device
        if self._out is None or self._out.device != dev:
            self._out = torch.empty(B, H, D, dtype=query.dtype, device=dev)
            self._pm = torch.empty(self.splits, B, H, dtype=torch.float32, device=dev)
            self._pl = torch.empty(self.splits, B, H, dtype=torch.float32, device=dev)
            self._pacc = torch.empty(self.splits, B, H, D, dtype=torch.float32, device=dev)
            self._counters = torch.zeros(
                B * self.num_kv_heads, dtype=torch.int32, device=dev)

        try:
            graph = torch.cuda.CUDAGraph()
            side = torch.cuda.Stream()
            side.wait_stream(torch.cuda.current_stream())
            with torch.cuda.stream(side):
                for _ in range(3):
                    self._launch(query, kv_cache, block_table, seq_lens)
            torch.cuda.current_stream().wait_stream(side)
            with torch.cuda.graph(graph):
                self._launch(query, kv_cache, block_table, seq_lens)
            graph.replay()
            self._graph = graph
            self._graph_args = (query, kv_cache, block_table, seq_lens)
            return self._out
        except Exception:
            self._graph = None
            return self._launch(query, kv_cache, block_table, seq_lens)


def get_inputs():
    """Build random paged inputs for the current module-level shape knobs."""
    B = BATCH
    H = NUM_HEADS
    Hkv = NUM_KV_HEADS
    D = HEAD_DIM
    L = SEQ_LEN
    P = PAGE_SIZE

    pages_per_seq = (L + P - 1) // P
    total_pages = max(B * pages_per_seq + 8, 64)

    query = torch.randn(B, H, D, dtype=torch.bfloat16) * 0.1
    kv_cache = torch.randn(total_pages, P, Hkv, 2 * D, dtype=torch.bfloat16) * 0.1

    perm = torch.randperm(total_pages)[: B * pages_per_seq].reshape(B, pages_per_seq).int()
    block_table = perm.contiguous()
    seq_lens = torch.full((B,), L, dtype=torch.int32)

    return [query, kv_cache, block_table, seq_lens]


def get_init_inputs():
    return [BATCH, NUM_HEADS, NUM_KV_HEADS, HEAD_DIM, SEQ_LEN, PAGE_SIZE]


# Shape knobs (overridden by check.py / benchmark.py from shapes.py).
BATCH = 8
NUM_HEADS = 32
NUM_KV_HEADS = 8
HEAD_DIM = 128
SEQ_LEN = 1024
PAGE_SIZE = 16

20260801_214207_or-fable_deepseek_deepseek-v4-flash-0731_03_paged_attention