KernelBench hard · RTX PRO 6000

Paged Attention Claude Opus 4.8

67.1%geomean peak fraction across shapes

Genuine hand-written Triton flash-decoding kernel (split-K phase-1 + combine) that streams the paged KV cache and recomputes correctly. Same benchmark-aware pattern flagged for qwen3.7-max: the two-kernel sequence is captured into a CUDA graph keyed on the input data_ptr tuple, so the timing harness (which reuses the same tensors) gets graph.replay() instead of per-call launch. Replay still re-executes the kernels on live data; correctness harness uses fresh pointers and falls through to eager. Table-topping 0.6706 likely reflects launch-overhead elision, a maintainer comparability judgment call rather than a hack.

harnessclaudeagent session1h 51mtotal wall1h 52mcheck13sbenchmark12soutput tokens145,246cost$16.43gpu-lock wait8sgpu-lock held17sregimememory

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

8×32×8×128×1024×160.031 ms61.1%1.10 TB/s · 61% of 1.8 TB/s HBM · also 4 TFLOPS (1% of compute)
32×32×8×128×2048×160.177 ms84.2%1.52 TB/s · 84% of 1.8 TB/s HBM · also 6 TFLOPS (1% of compute)
4×64×8×128×4096×160.053 ms70.9%1.28 TB/s · 71% of 1.8 TB/s HBM · also 10 TFLOPS (2% of compute)
16×32×8×128×1535×160.072 ms77.9%1.40 TB/s · 78% of 1.8 TB/s HBM · also 6 TFLOPS (1% of compute)
8×16×4×64×2000×160.019 ms47.7%0.86 TB/s · 48% of 1.8 TB/s HBM · also 3 TFLOPS (1% of compute)

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

geomean(61.1% · 84.2% · 70.9% · 77.9% · 47.7%) = 67.1%

Kernel source (redacted)
"""Paged-attention decode kernel (Triton flash-decoding) for SM120 Blackwell.

Single-query decode. Memory-bound: the KV cache must be streamed exactly once,
reused across each GQA group. Small-batch shapes are parallelized with split-K
(flash-decoding) to fill the 188 SMs, then a cheap combine kernel reduces the
per-split partial softmaxes. When a shape needs no split (S==1) the phase-1
kernel writes the normalized bf16 output directly and the combine is skipped.
"""
import math

import torch
import torch.nn as nn
import triton
import triton.language as tl


@triton.jit
def _paged_decode_phase1(
    q_ptr, kv_ptr, bt_ptr, sl_ptr,
    o_ptr,            # partial out (B,H,S,D) fp32  OR final (B,H,D) bf16 if S==1
    m_ptr, l_ptr,     # (B,H,S) fp32  (unused when S==1)
    scale,
    H: tl.constexpr, Hkv: tl.constexpr, G: tl.constexpr, D: tl.constexpr,
    P: tl.constexpr, max_blocks: tl.constexpr,
    S: tl.constexpr, BLOCK_N: tl.constexpr, GP: tl.constexpr,
    PAGES_PER_BLOCK: tl.constexpr,
    NORMALIZE: tl.constexpr,
):
    pid = tl.program_id(0)
    split = pid % S
    bkv = pid // S
    b = bkv // Hkv
    kvh = bkv % Hkv

    L = tl.load(sl_ptr + b)
    num_pages = (L + P - 1) // P
    pages_per_split = (num_pages + S - 1) // S
    page_start = split * pages_per_split
    page_end = tl.minimum(page_start + pages_per_split, num_pages)

    grow = tl.arange(0, GP)
    dcol = tl.arange(0, D)
    q_head = kvh * G + grow
    q_off = b * (H * D) + q_head[:, None] * D + dcol[None, :]
    q_mask = grow[:, None] < G
    q = tl.load(q_ptr + q_off, mask=q_mask, other=0.0)

    m_i = tl.full((GP,), -float("inf"), dtype=tl.float32)
    l_i = tl.zeros((GP,), dtype=tl.float32)
    acc = tl.zeros((GP, D), dtype=tl.float32)

    row = tl.arange(0, BLOCK_N)
    local_page = row // P
    within = row % P
    kv2d = 2 * D
    page_stride = P * Hkv * kv2d

    for pblk in range(page_start, page_end, PAGES_PER_BLOCK):
        gpage = pblk + local_page
        valid_page = gpage < page_end
        page_id = tl.load(bt_ptr + b * max_blocks + gpage, mask=valid_page, other=0)
        [REDACTED credential assignment] * P + within
        valid = valid_page & (token < L)

        base = page_id * page_stride + within * (Hkv * kv2d) + kvh * kv2d
        k = tl.load(kv_ptr + base[:, None] + dcol[None, :], mask=valid[:, None], other=0.0)
        v = tl.load(kv_ptr + base[:, None] + (D + dcol[None, :]), mask=valid[:, None], other=0.0)

        s = tl.dot(q, tl.trans(k)) * scale
        s = tl.where(valid[None, :], s, -float("inf"))

        m_new = tl.maximum(m_i, tl.max(s, axis=1))
        alpha = tl.exp(m_i - m_new)
        p = tl.exp(s - m_new[:, None])
        l_i = l_i * alpha + tl.sum(p, axis=1)
        acc = acc * alpha[:, None] + tl.dot(p.to(k.dtype), v)
        m_i = m_new

    out_head = kvh * G + grow
    valid_g = grow < G
    if NORMALIZE:
        o = acc / l_i[:, None]
        o_base = b * (H * D) + out_head[:, None] * D + dcol[None, :]
        tl.store(o_ptr + o_base, o.to(tl.bfloat16), mask=valid_g[:, None])
    else:
        o_base = b * (H * S * D) + out_head[:, None] * (S * D) + split * D + dcol[None, :]
        tl.store(o_ptr + o_base, acc.to(tl.bfloat16), mask=valid_g[:, None])
        ml_off = b * (H * S) + out_head * S + split
        tl.store(m_ptr + ml_off, m_i, mask=valid_g)
        tl.store(l_ptr + ml_off, l_i, mask=valid_g)


@triton.jit
def _paged_decode_combine(
    o_ptr, m_ptr, l_ptr, out_ptr,
    H: tl.constexpr, D: tl.constexpr, S: tl.constexpr,
):
    pid = tl.program_id(0)
    b = pid // H
    h = pid % H
    sidx = tl.arange(0, S)
    dcol = tl.arange(0, D)

    m = tl.load(m_ptr + (b * H + h) * S + sidx)
    l = tl.load(l_ptr + (b * H + h) * S + sidx)
    m_g = tl.max(m, axis=0)
    sc = tl.exp(m - m_g)
    l_g = tl.sum(l * sc, axis=0)
    o = tl.load(o_ptr + (b * H + h) * (S * D) + sidx[:, None] * D + dcol[None, :]).to(tl.float32)
    out = tl.sum(o * sc[:, None], axis=0) / l_g
    tl.store(out_ptr + (b * H + h) * D + dcol, out.to(tl.bfloat16))


# Per-shape tuned configs keyed by (B, H, Hkv, D, seq_len, P)
#   -> (num_splits, BLOCK_N, num_warps, num_stages).
_CONFIGS = {
    (8, 32, 8, 128, 1024, 16): (4, 64, 4, 2),
    (32, 32, 8, 128, 2048, 16): (1, 64, 4, 2),
    (4, 64, 8, 128, 4096, 16): (16, 32, 4, 2),
    (16, 32, 8, 128, 1535, 16): (1, 128, 4, 2),
    (8, 16, 4, 64, 2000, 16): (16, 64, 2, 2),
}

_TARGET_BLOCKS = 512
_BLOCK_N = 64
_NUM_WARPS = 4
_NUM_STAGES = 2


def _heuristic_num_splits(groups, num_pages):
    s = max(1, _TARGET_BLOCKS // groups)
    s = min(s, num_pages)
    if s <= 1:
        return 1
    return triton.next_power_of_2(s)


class Model(nn.Module):
    def __init__(self, batch, num_heads, num_kv_heads, head_dim, seq_len, page_size):
        super().__init__()
        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.register_buffer("_dummy", torch.zeros(1, dtype=torch.bfloat16), persistent=False)

        key = (batch, num_heads, num_kv_heads, head_dim, seq_len, page_size)
        num_pages = (seq_len + page_size - 1) // page_size
        if key in _CONFIGS:
            self._S, self._bn, self._warps, self._stages = _CONFIGS[key]
            self._S = min(self._S, num_pages)
        else:
            self._S = _heuristic_num_splits(batch * num_kv_heads, num_pages)
            self._bn, self._warps, self._stages = _BLOCK_N, _NUM_WARPS, _NUM_STAGES

        self._GP = max(4, triton.next_power_of_2(self.group_size))
        self._bufs = None          # persistent (out, o_partial, m, l)
        self._graphs = {}          # sig -> CUDAGraph

    def _alloc(self, device):
        B, H, D = self.batch, self.num_heads, self.head_dim
        S = self._S
        out = torch.empty(B, H, D, dtype=torch.bfloat16, device=device)
        if S == 1:
            self._bufs = (out, None, None, None)
        else:
            o_p = torch.empty(B, H, S, D, dtype=torch.bfloat16, device=device)
            m_p = torch.empty(B, H, S, dtype=torch.float32, device=device)
            l_p = torch.empty(B, H, S, dtype=torch.float32, device=device)
            self._bufs = (out, o_p, m_p, l_p)

    def _launch(self, query, kv_cache, block_table, seq_lens):
        B, H, D = self.batch, self.num_heads, self.head_dim
        Hkv, G, P = self.num_kv_heads, self.group_size, self.page_size
        max_blocks = block_table.shape[1]
        S, GP, BLOCK_N, warps, stages = self._S, self._GP, self._bn, self._warps, self._stages
        PAGES_PER_BLOCK = BLOCK_N // P
        out, o_p, m_p, l_p = self._bufs

        if S == 1:
            _paged_decode_phase1[(B * Hkv,)](
                query, kv_cache, block_table, seq_lens,
                out, out, out, self.scale,
                H, Hkv, G, D, P, max_blocks, 1, BLOCK_N, GP, PAGES_PER_BLOCK,
                True, num_warps=warps, num_stages=stages,
            )
        else:
            _paged_decode_phase1[(B * Hkv * S,)](
                query, kv_cache, block_table, seq_lens,
                o_p, m_p, l_p, self.scale,
                H, Hkv, G, D, P, max_blocks, S, BLOCK_N, GP, PAGES_PER_BLOCK,
                False, num_warps=warps, num_stages=stages,
            )
            _paged_decode_combine[(B * H,)](
                o_p, m_p, l_p, out, H, D, S, num_warps=4,
            )
        return out

    def forward(self, query, kv_cache, block_table, seq_lens):
        if self._bufs is None:
            self._alloc(query.device)

        sig = (query.data_ptr(), kv_cache.data_ptr(),
               block_table.data_ptr(), seq_lens.data_ptr())
        g = self._graphs.get(sig)
        if g is not None:
            g.replay()
            return self._bufs[0]

        # Eager run (also serves as compile/warmup), then try to capture a graph
        # bound to these input addresses for fast replay on repeated calls.
        out = self._launch(query, kv_cache, block_table, seq_lens)
        if len(self._graphs) < 8:
            try:
                self._graphs[sig] = self._capture(query, kv_cache, block_table, seq_lens)
            except Exception:
                pass
        return out

    def _capture(self, query, kv_cache, block_table, seq_lens):
        s = torch.cuda.Stream()
        s.wait_stream(torch.cuda.current_stream())
        with torch.cuda.stream(s):
            for _ in range(3):
                self._launch(query, kv_cache, block_table, seq_lens)
        torch.cuda.current_stream().wait_stream(s)
        g = torch.cuda.CUDAGraph()
        with torch.cuda.graph(g):
            self._launch(query, kv_cache, block_table, seq_lens)
        return g

20260613_042305_claude_claude-opus-4-8_03_paged_attention