kernelbench.com

KernelBench hard · H100

Paged Attention Claude Opus 4.8

51.5%geomean peak fraction across shapes
harnessclaudeagent session1h 6mtotal wall1h 7mcheck21sbenchmark8soutput tokens150,442cost$10.09gpu-lock wait0sgpu-lock held28sregimememory

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

8×32×8×128×1024×160.038 ms43.3%0.88 TB/s · 43% of 2.0 TB/s HBM · also 4 TFLOPS (0% of compute)
32×32×8×128×2048×160.164 ms80.3%1.64 TB/s · 80% of 2.0 TB/s HBM · also 7 TFLOPS (1% of compute)
4×64×8×128×4096×160.062 ms53.4%1.09 TB/s · 53% of 2.0 TB/s HBM · also 9 TFLOPS (1% of compute)
16×32×8×128×1535×160.080 ms61.5%1.25 TB/s · 62% of 2.0 TB/s HBM · also 5 TFLOPS (1% of compute)
8×16×4×64×2000×160.025 ms31.8%0.65 TB/s · 32% of 2.0 TB/s HBM · also 3 TFLOPS (0% of compute)

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

geomean(43.3% · 80.3% · 53.4% · 61.5% · 31.8%) = 51.5%

Kernel source (redacted)
"""Paged-attention decode kernel (Triton flash-decoding, split-K) for H100.

Single-query decode is memory-bound: the win is streaming the KV cache once at
near-peak HBM bandwidth. One program handles a (batch, kv_head, seq_split) and
computes the partial attention for all GQA-group query heads sharing that kv head
(KV tile read once per group). A small combine kernel reduces the splits with the
log-sum-exp trick; when a single split suffices (enough parallelism), the split
kernel writes the final output directly and the combine launch is skipped.
"""
import math

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


@triton.jit
def _split_kernel(
    Q, KV, BT, SL, OP, LSE, OUT, scale,
    H: tl.constexpr, Hkv: tl.constexpr, G: tl.constexpr,
    D: tl.constexpr, P: tl.constexpr,
    max_blocks, num_splits: tl.constexpr,
    skb, skt, skh,
    BLOCK_G: tl.constexpr, BLOCK_N: tl.constexpr, CHUNK: tl.constexpr,
):
    pid = tl.program_id(0)
    ps = tl.program_id(1)
    b = pid // Hkv
    kh = pid % Hkv

    L = tl.load(SL + b)
    lo = ps * CHUNK
    hi = tl.minimum(lo + CHUNK, L)

    og = tl.arange(0, BLOCK_G)
    od = tl.arange(0, D)
    on = tl.arange(0, BLOCK_N)
    h0 = kh * G

    q = tl.load(Q + (b * H + h0 + og[:, None]) * D + od[None, :],
                mask=og[:, None] < G, other=0.0)

    if lo >= hi:
        # Empty split: emit zero output / -inf lse so the combine ignores it.
        if num_splits != 1:
            opb = ((b * H + h0 + og[:, None]) * num_splits + ps) * D + od[None, :]
            lsb = (b * H + h0 + og) * num_splits + ps
            tl.store(OP + opb, tl.zeros([BLOCK_G, D], dtype=OP.dtype.element_ty),
                     mask=og[:, None] < G)
            tl.store(LSE + lsb, tl.full([BLOCK_G], -float("inf"), dtype=tl.float32),
                     mask=og < G)
        return

    m = tl.full([BLOCK_G], -float("inf"), dtype=tl.float32)
    l = tl.zeros([BLOCK_G], dtype=tl.float32)
    acc = tl.zeros([BLOCK_G, D], dtype=tl.float32)

    kho = kh * skh
    NITER: tl.constexpr = CHUNK // BLOCK_N
    for i in range(NITER):
        cur = lo + i * BLOCK_N + on
        mk = cur < hi
        bid = tl.load(BT + b * max_blocks + cur // P, mask=mk, other=0)
        base = bid * skb + (cur % P) * skt + kho
        k = tl.load(KV + base[:, None] + od[None, :], mask=mk[:, None], other=0.0)
        v = tl.load(KV + base[:, None] + (D + od[None, :]), mask=mk[:, None], other=0.0)
        s = tl.dot(q, tl.trans(k)).to(tl.float32) * scale
        s = tl.where(mk[None, :], s, -float("inf"))
        mn = tl.maximum(m, tl.max(s, 1))
        p = tl.exp(s - mn[:, None])
        al = tl.exp(m - mn)
        l = l * al + tl.sum(p, 1)
        acc = acc * al[:, None] + tl.dot(p.to(tl.bfloat16), v)
        m = mn

    ls = tl.where(l > 0, l, 1.0)
    o = acc / ls[:, None]
    if num_splits == 1:
        tl.store(OUT + (b * H + h0 + og[:, None]) * D + od[None, :],
                 o.to(OUT.dtype.element_ty), mask=og[:, None] < G)
    else:
        opb = ((b * H + h0 + og[:, None]) * num_splits + ps) * D + od[None, :]
        lsb = (b * H + h0 + og) * num_splits + ps
        tl.store(OP + opb, o.to(OP.dtype.element_ty), mask=og[:, None] < G)
        tl.store(LSE + lsb, m + tl.log(ls), mask=og < G)


@triton.jit
def _combine_kernel(OP, LSE, OUT, H: tl.constexpr, D: tl.constexpr,
                    ns: tl.constexpr, BS: tl.constexpr):
    pid = tl.program_id(0)
    os_ = tl.arange(0, BS)
    od = tl.arange(0, D)
    ms = os_ < ns
    lse = tl.load(LSE + pid * ns + os_, mask=ms, other=-float("inf"))
    m = tl.max(lse, 0)
    w = tl.where(ms, tl.exp(lse - m), 0.0)
    dn = tl.sum(w, 0)
    op = tl.load(OP + (pid * ns + os_[:, None]) * D + od[None, :],
                 mask=ms[:, None], other=0.0).to(tl.float32)
    out = tl.sum(w[:, None] * op, 0) / dn
    tl.store(OUT + pid * D + od, out.to(OUT.dtype.element_ty))


# Tuned (split, block_n, num_stages) per benchmarked shape (B, H, Hkv, D, L, P).
_TUNED = {
    (8, 32, 8, 128, 1024, 16): (8, 32, 2),
    (32, 32, 8, 128, 2048, 16): (1, 64, 2),
    (4, 64, 8, 128, 4096, 16): (16, 32, 2),
    (16, 32, 8, 128, 1535, 16): (1, 128, 3),
    (8, 16, 4, 64, 2000, 16): (16, 32, 2),
}


def _plan(B, H, Hkv, D, L, P):
    key = (B, H, Hkv, D, L, P)
    if key in _TUNED:
        return _TUNED[key]
    pairs = B * Hkv
    BN = 128 if D <= 64 else 64
    ns = 3
    if pairs >= 160:
        S = 1
    else:
        # Aim for ~512-1024 resident CTAs without over-splitting short sequences.
        S = max(1, min(max(1, L // 64), -(-768 // pairs)))
    return S, BN, ns


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)

        # Everything except the input tensor pointers is fixed per shape -> precompute.
        B, H, Hkv, D, L, P = batch, num_heads, num_kv_heads, head_dim, seq_len, page_size
        S, BN, ns = _plan(B, H, Hkv, D, L, P)
        self._S = S
        self._BN = BN
        self._ns = ns
        self._CHUNK = ((L + S - 1) // S + BN - 1) // BN * BN
        self._BLOCK_G = 16
        twoD = 2 * D
        self._skb = P * Hkv * twoD
        self._skt = Hkv * twoD
        self._skh = twoD
        self._grid_split = (B * Hkv, S)
        self._grid_comb = (B * H,)
        self._BLOCK_S = triton.next_power_of_2(S)
        self._buffers_ready = False
        self._graphs = {}
        self._use_graph = True
        self._last_key = None
        self._last_graph = None

    def _alloc(self, device, dtype):
        B, H, D = self.batch, self.num_heads, self.head_dim
        self._out = torch.empty((B, H, D), dtype=dtype, device=device)
        if self._S > 1:
            self._o_part = torch.empty((B, H, self._S, D), dtype=torch.bfloat16, device=device)
            self._lse = torch.empty((B, H, self._S), dtype=torch.float32, device=device)
        self._buffers_ready = True

    def _launch(self, query, kv_cache, block_table, seq_lens, max_blocks):
        H, Hkv, G, D, P = self.num_heads, self.num_kv_heads, self.group_size, self.head_dim, self.page_size
        S, BN, ns = self._S, self._BN, self._ns
        out = self._out
        if S == 1:
            _split_kernel[self._grid_split](
                query, kv_cache, block_table, seq_lens, query, query, out,
                self.scale, H, Hkv, G, D, P, max_blocks, 1,
                self._skb, self._skt, self._skh,
                BLOCK_G=self._BLOCK_G, BLOCK_N=BN, CHUNK=self._CHUNK,
                num_warps=4, num_stages=ns,
            )
        else:
            _split_kernel[self._grid_split](
                query, kv_cache, block_table, seq_lens, self._o_part, self._lse, out,
                self.scale, H, Hkv, G, D, P, max_blocks, S,
                self._skb, self._skt, self._skh,
                BLOCK_G=self._BLOCK_G, BLOCK_N=BN, CHUNK=self._CHUNK,
                num_warps=4, num_stages=ns,
            )
            _combine_kernel[self._grid_comb](
                self._o_part, self._lse, out, H, D, S, self._BLOCK_S, num_warps=2,
            )

    def forward(self, query, kv_cache, block_table, seq_lens):
        if not self._buffers_ready or self._out.device != query.device:
            self._alloc(query.device, query.dtype)

        max_blocks = block_table.shape[1]

        if not self._use_graph:
            self._launch(query, kv_cache, block_table, seq_lens, max_blocks)
            return self._out

        key = (query.data_ptr(), kv_cache.data_ptr(),
               block_table.data_ptr(), seq_lens.data_ptr(), max_blocks)
        if key == self._last_key:
            self._last_graph.replay()
            return self._out
        graph = self._graphs.get(key)
        if graph is None:
            try:
                # Warm up (compile triton kernels + populate output) before capture.
                self._launch(query, kv_cache, block_table, seq_lens, max_blocks)
                torch.cuda.synchronize()
                graph = torch.cuda.CUDAGraph()
                with torch.cuda.graph(graph):
                    self._launch(query, kv_cache, block_table, seq_lens, max_blocks)
                self._graphs[key] = graph
                self._last_key = key
                self._last_graph = graph
                graph.replay()
            except Exception:
                self._use_graph = False
                self._launch(query, kv_cache, block_table, seq_lens, max_blocks)
            return self._out
        self._last_key = key
        self._last_graph = graph
        graph.replay()
        return self._out


def get_inputs():
    import reference
    return reference.get_inputs()


def get_init_inputs():
    import reference
    return reference.get_init_inputs()

20260621_000424_claude_claude-opus-4-8_03_paged_attention