KernelBench hard · B200

Paged Attention GLM-5.2

passdid not score
harnesszai-claudeagent session1h 40mtotal wall1h 41mcheck24sbenchmark10soutput tokensgpu-lock wait0sgpu-lock held34sregimememory

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.066 ms51.2%4.10 TB/s · 51% of 8.0 TB/s HBM · also 16 TFLOPS (1% of compute)
4×64×8×128×4096×160.028 ms29.8%2.39 TB/s · 30% of 8.0 TB/s HBM · also 19 TFLOPS (1% of compute)
16×32×8×128×1535×160.038 ms33.4%2.67 TB/s · 33% of 8.0 TB/s HBM · also 11 TFLOPS (0% of compute)
8×16×4×64×2000×160.017 ms11.9%0.96 TB/s · 12% 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% · 51.2% · 29.8% · 33.4% · 11.9%) = 26.4%

Kernel source (redacted)
"""Custom paged-attention decode kernel for B200 (SM100, HBM3e).

Flash-decoding: one CTA per (batch, kv_head, seq_partition). Each KV element is
read exactly once (GQA query heads in a group share the load). Split-K along the
sequence gives enough parallelism to fill all 148 SMs even at batch=4. A second
small kernel does the flash reduction across partitions.

No SDPA / vLLM / FlashInfer calls -- pure Triton.
"""
import math

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

OP_TYPE = "attention"
SUPPORTED_PRECISIONS = ["bf16"]
HARDWARE_REQUIRED = ["RTX_PRO_6000", "H100", "B200"]

# --- 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

_NUM_SMS = 148  # B200


# ===========================================================================
# Decode kernel: produces per-partition partial (o, m, l) or final output.
# Grid: (B, Hkv, num_partitions)
# ===========================================================================
@triton.autotune(
    configs=[
        triton.Config({"BLOCK_N": 32},  num_warps=4, num_stages=4),
        triton.Config({"BLOCK_N": 32},  num_warps=8, num_stages=5),
        triton.Config({"BLOCK_N": 64},  num_warps=4, num_stages=4),
        triton.Config({"BLOCK_N": 64},  num_warps=8, num_stages=4),
        triton.Config({"BLOCK_N": 64},  num_warps=8, num_stages=5),
        triton.Config({"BLOCK_N": 64},  num_warps=8, num_stages=6),
        triton.Config({"BLOCK_N": 128}, num_warps=8, num_stages=3),
        triton.Config({"BLOCK_N": 128}, num_warps=8, num_stages=4),
        triton.Config({"BLOCK_N": 128}, num_warps=8, num_stages=5),
        triton.Config({"BLOCK_N": 128}, num_warps=16, num_stages=3),
        triton.Config({"BLOCK_N": 128}, num_warps=16, num_stages=4),
    ],
    key=["G", "D", "P", "WRITE_FINAL"],
)
@triton.jit
def _decode_kernel(
    Q_ptr, KV_ptr, BT_ptr, SL_ptr,
    OUT_ptr,
    PO_ptr, PM_ptr, PL_ptr,
    q_sb, q_sh,
    kv_sblk, kv_spg, kv_sh,
    bt_sb,
    out_sb, out_sh,
    po_sb, po_sh, po_sp,
    pm_sb, pm_sh, pm_sp,
    pl_sb, pl_sh, pl_sp,
    sm_scale,
    PART_SEQ,
    G: tl.constexpr,
    D: tl.constexpr,
    HKV: tl.constexpr,
    P: tl.constexpr,
    KVD: tl.constexpr,
    MQ: tl.constexpr,
    BLOCK_N: tl.constexpr,
    WRITE_FINAL: tl.constexpr,
):
    pid_b = tl.program_id(0)
    pid_h = tl.program_id(1)
    pid_p = tl.program_id(2)

    Lb = tl.load(SL_ptr + pid_b)
    start = pid_p * PART_SEQ
    # exclusive end of this partition (clamped to seq len); masks the inner loop
    # so interior partitions don't read into their neighbour's token range.
    part_end = tl.minimum((pid_p + 1) * PART_SEQ, Lb)

    # ---- Q tile (MQ, D) bf16; rows >= G are padded with 0 ----
    offs_q = tl.arange(0, MQ)
    g_mask = offs_q < G
    q_rows = pid_h * G + offs_q
    offs_d = tl.arange(0, D)
    q_ptrs = Q_ptr + pid_b * q_sb + q_rows[:, None] * q_sh + offs_d[None, :]
    Q = tl.load(q_ptrs, mask=g_mask[:, None], other=0.0)

    # ---- accumulators ----
    m_i = tl.full([MQ], float("-inf"), dtype=tl.float32)
    l_i = tl.full([MQ], 0.0, dtype=tl.float32)
    o_i = tl.zeros([MQ, D], dtype=tl.float32)

    if start < Lb:
        for start_n in range(0, PART_SEQ, BLOCK_N):
            n = start + start_n + tl.arange(0, BLOCK_N)
            mask_n = n < part_end
            page = n // P
            off_pg = n % P
            bid = tl.load(BT_ptr + pid_b * bt_sb + page, mask=mask_n, other=0)
            phys = bid * P + off_pg
            base = phys * (HKV * KVD) + pid_h * KVD
            kv_ptrs = KV_ptr + base[:, None] + offs_d[None, :]
            K = tl.load(kv_ptrs, mask=mask_n[:, None], other=0.0, eviction_policy="evict_first")
            qk = tl.dot(Q, tl.trans(K)) * sm_scale          # (MQ, BLOCK_N)
            qk = tl.where(mask_n[None, :], qk, float("-inf"))
            m_ij = tl.maximum(m_i, tl.max(qk, axis=1))
            qk = qk - m_ij[:, None]
            p = tl.exp(qk)                                   # (MQ, BLOCK_N)
            alpha = tl.exp(m_i - m_ij)
            l_ij = tl.sum(p, axis=1)
            V = tl.load(kv_ptrs + D, mask=mask_n[:, None], other=0.0, eviction_policy="evict_first")
            o_i = o_i * alpha[:, None] + tl.dot(p.to(tl.bfloat16), V)
            l_i = l_i * alpha + l_ij
            m_i = m_ij

    if WRITE_FINAL:
        o = o_i / l_i[:, None]
        out_ptrs = OUT_ptr + pid_b * out_sb + q_rows[:, None] * out_sh + offs_d[None, :]
        tl.store(out_ptrs, o.to(tl.bfloat16), mask=g_mask[:, None])
    else:
        po_ptrs = PO_ptr + pid_b * po_sb + pid_h * po_sh + pid_p * po_sp + offs_q[:, None] * D + offs_d[None, :]
        tl.store(po_ptrs, o_i, mask=g_mask[:, None])
        pm_ptrs = PM_ptr + pid_b * pm_sb + pid_h * pm_sh + pid_p * pm_sp + offs_q
        tl.store(pm_ptrs, m_i, mask=g_mask)
        pl_ptrs = PL_ptr + pid_b * pl_sb + pid_h * pl_sh + pid_p * pl_sp + offs_q
        tl.store(pl_ptrs, l_i, mask=g_mask)


# ===========================================================================
# Reduce kernel: flash-reduce per-partition partials -> final output.
# Grid: (B, H) -- one CTA per query head for high occupancy on small batches.
# ===========================================================================
@triton.jit
def _reduce_kernel(
    OUT_ptr, PO_ptr, PM_ptr, PL_ptr,
    out_sb, out_sh,
    po_sb, po_sh, po_sp,
    pm_sb, pm_sh, pm_sp,
    pl_sb, pl_sh, pl_sp,
    NP,
    G: tl.constexpr,
    D: tl.constexpr,
    BLOCK_P: tl.constexpr,
):
    pid_b = tl.program_id(0)
    pid_h = tl.program_id(1)          # query head index in [0, H)
    hkv = pid_h // G
    g = pid_h % G

    offs_d = tl.arange(0, D)
    offs_p = tl.arange(0, BLOCK_P)
    p_mask = offs_p < NP

    # partial max for this head: (BLOCK_P,)
    pm_ptrs = PM_ptr + pid_b * pm_sb + hkv * pm_sh + offs_p * pm_sp + g
    pm = tl.load(pm_ptrs, mask=p_mask, other=float("-inf"))
    m_g = tl.max(pm, axis=0)

    factor = tl.exp(pm - m_g)                                 # (BLOCK_P,)

    pl_ptrs = PL_ptr + pid_b * pl_sb + hkv * pl_sh + offs_p * pl_sp + g
    pl = tl.load(pl_ptrs, mask=p_mask, other=0.0)
    l_g = tl.sum(factor * pl, axis=0)

    # partial output for this head: (BLOCK_P, D)
    po_ptrs = (PO_ptr + pid_b * po_sb + hkv * po_sh
               + offs_p[:, None] * po_sp + g * D + offs_d[None, :])
    po = tl.load(po_ptrs, mask=p_mask[:, None], other=0.0)
    o_g = tl.sum(factor[:, None] * po, axis=0)               # (D,)

    out = o_g / l_g
    out_ptrs = OUT_ptr + pid_b * out_sb + pid_h * out_sh + offs_d
    tl.store(out_ptrs, out.to(tl.bfloat16))


def _next_pow2(x):
    p = 1
    while p < x:
        p <<= 1
    return p


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)
        import os as _os
        _mq_override = _os.environ.get("KBH_MQ")
        if _mq_override:
            self.MQ = int(_mq_override)
        else:
            self.MQ = _next_pow2(max(self.group_size, 1))

        # ---- choose split-K parallelism ----
        # Decode prefers a modest number of fairly large CTAs (per-CTA softmax
        # overhead + split-K reduce cost penalize very high partition counts).
        # Empirically ~1 wave (B*Hkv*npart ~= 128) is the sweet spot across the
        # shape sweep. Going higher only helps the pure-read ceiling, not decode.
        import os
        base = batch * num_kv_heads
        target_ctas = 128
        npart = (target_ctas + base - 1) // base
        npart = max(npart, 1)
        min_tokens_per_part = 64
        max_npart = max(1, seq_len // min_tokens_per_part)
        npart = min(npart, max_npart)
        npart = max(npart, 1)
        env_np = os.environ.get("KBH_NPART")
        if env_np:
            npart = int(env_np)
        self.npart = npart
        self.part_seq = (seq_len + npart - 1) // npart

        self.register_buffer("_dummy", torch.zeros(1, dtype=torch.bfloat16), persistent=False)

        # Preallocate output and partial buffers (static addresses for CUDA graphs).
        self._out = torch.empty(batch, num_heads, head_dim, dtype=torch.bfloat16, device="cuda")
        if npart > 1:
            self._po = torch.empty(batch, num_kv_heads, npart, self.MQ, head_dim, dtype=torch.float32, device="cuda")
            self._pm = torch.empty(batch, num_kv_heads, npart, self.MQ, dtype=torch.float32, device="cuda")
            self._pl = torch.empty(batch, num_kv_heads, npart, self.MQ, dtype=torch.float32, device="cuda")
        else:
            self._po = self._pm = self._pl = None

        self._graph = None
        self._graph_key = None

    def _launch(self, out, 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
        KVD = 2 * D
        MQ = self.MQ
        npart = self.npart
        part_seq = self.part_seq

        grid = (B, Hkv, npart)
        if npart == 1:
            _decode_kernel[grid](
                query, kv_cache, block_table, seq_lens,
                out,
                None, None, None,
                query.stride(0), query.stride(1),
                kv_cache.stride(0), kv_cache.stride(1), kv_cache.stride(2),
                block_table.stride(0),
                out.stride(0), out.stride(1),
                0, 0, 0, 0, 0, 0, 0, 0, 0,
                self.scale, part_seq,
                G=G, D=D, HKV=Hkv, P=P, KVD=KVD, MQ=MQ,
                WRITE_FINAL=True,
            )
        else:
            po, pm, pl = self._po, self._pm, self._pl
            _decode_kernel[grid](
                query, kv_cache, block_table, seq_lens,
                out,
                po, pm, pl,
                query.stride(0), query.stride(1),
                kv_cache.stride(0), kv_cache.stride(1), kv_cache.stride(2),
                block_table.stride(0),
                out.stride(0), out.stride(1),
                po.stride(0), po.stride(1), po.stride(2),
                pm.stride(0), pm.stride(1), pm.stride(2),
                pl.stride(0), pl.stride(1), pl.stride(2),
                self.scale, part_seq,
                G=G, D=D, HKV=Hkv, P=P, KVD=KVD, MQ=MQ,
                WRITE_FINAL=False,
            )
            _reduce_kernel[(B, self.num_heads)](
                out, po, pm, pl,
                out.stride(0), out.stride(1),
                po.stride(0), po.stride(1), po.stride(2),
                pm.stride(0), pm.stride(1), pm.stride(2),
                pl.stride(0), pl.stride(1), pl.stride(2),
                npart,
                G=G, D=D, BLOCK_P=_next_pow2(npart),
            )

    def forward(self, query, kv_cache, block_table, seq_lens):
        out = self._out
        key = (query.data_ptr(), kv_cache.data_ptr(),
               block_table.data_ptr(), seq_lens.data_ptr())

        # If we're already inside an external CUDA-graph capture, just launch
        # eagerly so the kernel launches get captured by the outer graph.
        capturing = torch.cuda.is_current_stream_capturing()

        if not capturing and self._graph is not None and key == self._graph_key:
            self._graph.replay()
            return out

        # Eager launch (also resolves Triton autotune before capture).
        self._launch(out, query, kv_cache, block_table, seq_lens)

        # Try to capture a graph for this set of input addresses so subsequent
        # calls replay with near-zero launch overhead. Falls back to eager.
        if not capturing and self._graph is None:
            try:
                torch.cuda.synchronize()
                g = torch.cuda.CUDAGraph()
                with torch.cuda.graph(g):
                    self._launch(out, query, kv_cache, block_table, seq_lens)
                self._graph = g
                self._graph_key = key
            except Exception:
                self._graph = None
        return out


def get_inputs():
    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]

20260620_111951_zai-claude_glm-5.2_03_paged_attention