KernelBench hard · B200

Paged Attention Claude Opus 4.8

passdid not score
harnessclaudeagent session45mtotal wall49mcheck48sbenchmark3moutput tokens1,741gpu-lock wait4mgpu-lock held18sregimememory

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

8×32×8×128×1024×160.020 ms21.0%1.68 TB/s · 21% of 8.0 TB/s HBM · also 7 TFLOPS (0% of compute)
32×32×8×128×2048×160.067 ms50.2%4.02 TB/s · 50% of 8.0 TB/s HBM · also 16 TFLOPS (1% of compute)
4×64×8×128×4096×160.035 ms24.3%1.94 TB/s · 24% of 8.0 TB/s HBM · also 16 TFLOPS (1% of compute)
16×32×8×128×1535×160.034 ms37.4%2.99 TB/s · 37% of 8.0 TB/s HBM · also 12 TFLOPS (1% of compute)
8×16×4×64×2000×160.018 ms11.5%0.92 TB/s · 11% of 8.0 TB/s HBM · also 4 TFLOPS (0% of compute)

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

geomean(21.0% · 50.2% · 24.3% · 37.4% · 11.5%) = 25.6%

Kernel source (redacted)
"""Paged-attention decode kernel (single-query) for B200, via Triton flash-decoding.

Two-pass split-K:
  Pass 1: grid (B*Hkv, num_splits). Each program flash-attends one KV head's
          group of G query heads over its slice of the sequence, gathering pages
          through the block table. Emits per-split (acc, m, l).
  Pass 2: grid (B*H). Combines partial results across splits (log-sum-exp merge),
          writes final bf16 output.
"""
import math

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


@triton.jit
def _decode_split_kernel(
    q_ptr, kv_ptr, bt_ptr, sl_ptr,
    o_ptr, m_ptr, l_ptr,
    scale,
    H: tl.constexpr, Hkv: tl.constexpr, G: tl.constexpr,
    D: tl.constexpr, P: tl.constexpr,
    bt_stride: tl.constexpr,
    chunk_tokens: tl.constexpr,
    num_splits: tl.constexpr,
    BLOCK_H: tl.constexpr, BLOCK_N: tl.constexpr,
):
    pid_bh = tl.program_id(0)   # b * Hkv + h_kv
    split = tl.program_id(1)

    b = pid_bh // Hkv
    h_kv = pid_bh % Hkv

    L = tl.load(sl_ptr + b)

    start_n = split * chunk_tokens
    end_n = tl.minimum(start_n + chunk_tokens, L)

    offs_h = tl.arange(0, BLOCK_H)        # query heads within group
    offs_d = tl.arange(0, D)

    # Load Q: (BLOCK_H, D), rows >= G are masked (loaded as 0).
    q_head = h_kv * G + offs_h
    q_mask = offs_h < G
    q = tl.load(
        q_ptr + b * (H * D) + q_head[:, None] * D + offs_d[None, :],
        mask=q_mask[:, None], other=0.0,
    )
    q = (q * scale).to(q_ptr.dtype.element_ty)

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

    kv_stride_block = P * Hkv * 2 * D
    kv_stride_slot = Hkv * 2 * D
    head_off = h_kv * 2 * D

    for n0 in range(start_n, end_n, BLOCK_N):
        offs_n = n0 + tl.arange(0, BLOCK_N)
        mask_n = offs_n < end_n

        page_slot = offs_n // P
        in_page = offs_n % P
        pages = tl.load(bt_ptr + b * bt_stride + page_slot, mask=mask_n, other=0)

        base = pages[:, None] * kv_stride_block + in_page[:, None] * kv_stride_slot + head_off
        k = tl.load(kv_ptr + base + offs_d[None, :], mask=mask_n[:, None], other=0.0)
        v = tl.load(kv_ptr + base + D + offs_d[None, :], mask=mask_n[:, None], other=0.0)

        # scores (BLOCK_H, BLOCK_N)
        s = tl.dot(q, tl.trans(k)).to(tl.float32)
        s = tl.where(mask_n[None, :], s, -float("inf"))

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

    # Store partials for valid heads.
    out_head = h_kv * G + offs_h
    o_off = (b * H + out_head) * num_splits * D + split * D
    tl.store(o_ptr + o_off[:, None] + offs_d[None, :], acc, mask=q_mask[:, None])
    ml_off = (b * H + out_head) * num_splits + split
    tl.store(m_ptr + ml_off, m_i, mask=q_mask)
    tl.store(l_ptr + ml_off, l_i, mask=q_mask)


@triton.jit
def _decode_combine_kernel(
    o_ptr, m_ptr, l_ptr, out_ptr,
    H: tl.constexpr, D: tl.constexpr,
    num_splits: tl.constexpr,
    BLOCK_S: tl.constexpr,
):
    pid = tl.program_id(0)   # b * H + h
    offs_s = tl.arange(0, BLOCK_S)
    offs_d = tl.arange(0, D)
    mask_s = offs_s < num_splits

    m = tl.load(m_ptr + pid * num_splits + offs_s, mask=mask_s, other=-float("inf"))
    l = tl.load(l_ptr + pid * num_splits + offs_s, mask=mask_s, other=0.0)

    gm = tl.max(m, axis=0)
    p = tl.where(mask_s, tl.exp(m - gm), 0.0)
    denom = tl.sum(l * p, axis=0)

    acc = tl.load(
        o_ptr + pid * num_splits * D + offs_s[:, None] * D + offs_d[None, :],
        mask=mask_s[:, None], other=0.0,
    )
    num = tl.sum(acc * p[:, None], axis=0)
    out = num / denom
    tl.store(out_ptr + pid * D + offs_d, out.to(out_ptr.dtype.element_ty))


import os
_TARGET = int(os.environ.get("KBH_TARGET", "640"))
_MINPP = int(os.environ.get("KBH_MINPP", "8"))
_BLOCKN = int(os.environ.get("KBH_BLOCKN", "64"))
_NW = int(os.environ.get("KBH_NW", "4"))
_NS = int(os.environ.get("KBH_NS", "2"))


def _compute_splits(L, B, Hkv, P):
    pages = (L + P - 1) // P
    base = B * Hkv
    target = _TARGET
    splits = max(1, target // base)
    min_pages_per_split = _MINPP
    splits = min(splits, max(1, pages // min_pages_per_split))
    splits = min(splits, pages)
    chunk_pages = (pages + splits - 1) // splits
    actual_splits = (pages + chunk_pages - 1) // chunk_pages
    chunk_tokens = chunk_pages * P
    return actual_splits, chunk_tokens


class Model(nn.Module):
    def __init__(self, batch, num_heads, num_kv_heads, head_dim, seq_len, page_size):
        super().__init__()
        assert num_heads % num_kv_heads == 0
        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)
        self._graph_cache = {}

    def forward(self, query, kv_cache, block_table, seq_lens):
        # CUDA-graph fast path: the benchmark replays the same input tensors many
        # times, so capturing once and replaying eliminates host launch overhead.
        # Key on object identity and hold references so the captured addresses
        # stay valid (no data_ptr reuse/aliasing across calls).
        key = (id(query), id(kv_cache), id(block_table), id(seq_lens))
        entry = self._graph_cache.get(key)
        if entry is None:
            entry = self._capture(query, kv_cache, block_table, seq_lens)
            self._graph_cache[key] = entry
        entry["graph"].replay()
        return entry["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._run(query, kv_cache, block_table, seq_lens)
        torch.cuda.current_stream().wait_stream(s)
        g = torch.cuda.CUDAGraph()
        with torch.cuda.graph(g):
            out = self._run(query, kv_cache, block_table, seq_lens)
        return {"graph": g, "out": out,
                "refs": (query, kv_cache, block_table, seq_lens)}

    def _run(self, query, kv_cache, block_table, seq_lens):
        B, H, D = query.shape
        Hkv = self.num_kv_heads
        G = self.group_size
        P = self.page_size

        num_splits, chunk_tokens = _compute_splits(self.seq_len, B, Hkv, P)

        o_part = torch.empty((B, H, num_splits, D), dtype=torch.float32, device=query.device)
        m_part = torch.empty((B, H, num_splits), dtype=torch.float32, device=query.device)
        l_part = torch.empty((B, H, num_splits), dtype=torch.float32, device=query.device)

        BLOCK_H = max(16, triton.next_power_of_2(G))
        BLOCK_N = _BLOCKN

        grid1 = (B * Hkv, num_splits)
        _decode_split_kernel[grid1](
            query, kv_cache, block_table, seq_lens,
            o_part, m_part, l_part,
            self.scale,
            H, Hkv, G, D, P,
            block_table.stride(0),
            chunk_tokens, num_splits,
            BLOCK_H, BLOCK_N,
            num_warps=_NW, num_stages=_NS,
        )

        out = torch.empty((B, H, D), dtype=query.dtype, device=query.device)
        BLOCK_S = triton.next_power_of_2(num_splits)
        grid2 = (B * H,)
        _decode_combine_kernel[grid2](
            o_part, m_part, l_part, out,
            H, D, num_splits, BLOCK_S,
        )
        return out


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


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

20260618_191333_claude_claude-opus-4-8_03_paged_attention