kernelbench.com

KernelBench hard · H100

Paged Attention Grok 4.5

43.7%geomean peak fraction across shapes
harnessgrokagent session42mtotal wall43mcheck30sbenchmark10soutput tokensgpu-lock wait0sgpu-lock held40sregimememory

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

8×32×8×128×1024×160.048 ms34.6%0.71 TB/s · 35% of 2.0 TB/s HBM · also 3 TFLOPS (0% of compute)
32×32×8×128×2048×160.175 ms75.5%1.54 TB/s · 75% of 2.0 TB/s HBM · also 6 TFLOPS (1% of compute)
4×64×8×128×4096×160.070 ms47.1%0.96 TB/s · 47% of 2.0 TB/s HBM · also 8 TFLOPS (1% of compute)
16×32×8×128×1535×160.088 ms55.9%1.14 TB/s · 56% of 2.0 TB/s HBM · also 5 TFLOPS (1% of compute)
8×16×4×64×2000×160.035 ms23.1%0.47 TB/s · 23% of 2.0 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(34.6% · 75.5% · 47.1% · 55.9% · 23.1%) = 43.7%

Kernel source (redacted)
"""High-performance paged-attention decode for H100 (SM90).

Triton GQA-shared kernel + CUDA-graph capture:
  - One CTA per (batch, kv_head, split) loads each K/V page once and attends
    all query heads that share that KV head (GQA reuse).
  - bf16 tensor-core MMA for QK / PV; fp32 online softmax.
  - Split-KV when B*Hkv underfills the GPU; reduce with online-softmax merge.
  - Model.forward captures a CUDA graph after the first call so subsequent
    replays (benchmark iters with fixed buffers) pay zero launch overhead.
"""
from __future__ import annotations

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"]

BATCH = 8
NUM_HEADS = 32
NUM_KV_HEADS = 8
HEAD_DIM = 128
SEQ_LEN = 1024
PAGE_SIZE = 16


@triton.jit
def _gqa_kernel(
    Q_ptr, KV_ptr, BT_ptr, SL_ptr, Out_ptr,
    PartialO_ptr, PartialM_ptr, PartialL_ptr,
    stride_qb, stride_qh, stride_qd,
    stride_kv_block, stride_kv_tok, stride_kv_h, stride_kv_d,
    stride_bt_b, stride_bt_p,
    stride_ob, stride_oh, stride_od,
    stride_po_s, stride_po_b, stride_po_h, stride_po_d,
    stride_pm_s, stride_pm_b, stride_pm_h,
    scale, num_heads, num_kv_heads,
    PAGE_SIZE: tl.constexpr,
    HEAD_DIM: tl.constexpr,
    BLOCK_M: tl.constexpr,
    BLOCK_N: tl.constexpr,
    NUM_SPLITS: tl.constexpr,
    HAS_SPLITS: tl.constexpr,
):
    batch_id = tl.program_id(0)
    kv_head_id = tl.program_id(1)
    split_id = tl.program_id(2)

    group_size = num_heads // num_kv_heads
    q_head0 = kv_head_id * group_size

    seq_len = tl.load(SL_ptr + batch_id)
    tps = (seq_len + NUM_SPLITS - 1) // NUM_SPLITS
    start_tok = split_id * tps
    end_tok = tl.minimum(start_tok + tps, seq_len)

    offs_g = tl.arange(0, BLOCK_M)
    offs_d = tl.arange(0, HEAD_DIM)
    offs_n = tl.arange(0, BLOCK_N)
    mask_g = offs_g < group_size
    h_ids = q_head0 + offs_g

    if start_tok >= end_tok:
        if HAS_SPLITS:
            po = (
                PartialO_ptr
                + split_id * stride_po_s
                + batch_id * stride_po_b
                + h_ids[:, None] * stride_po_h
                + offs_d[None, :] * stride_po_d
            )
            tl.store(po, tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32), mask=mask_g[:, None])
            pm = PartialM_ptr + split_id * stride_pm_s + batch_id * stride_pm_b + h_ids * stride_pm_h
            tl.store(pm, tl.full([BLOCK_M], float("-inf"), dtype=tl.float32), mask=mask_g)
            pl = PartialL_ptr + split_id * stride_pm_s + batch_id * stride_pm_b + h_ids * stride_pm_h
            tl.store(pl, tl.zeros([BLOCK_M], dtype=tl.float32), mask=mask_g)
        return

    q = tl.load(
        Q_ptr + batch_id * stride_qb + h_ids[:, None] * stride_qh + offs_d[None, :] * stride_qd,
        mask=mask_g[:, None],
        other=0.0,
    )
    q_scaled = (q.to(tl.float32) * scale).to(tl.bfloat16)

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

    n_tiles = (end_tok - start_tok + BLOCK_N - 1) // BLOCK_N
    for tile_id in tl.range(0, n_tiles):
        tok = start_tok + tile_id * BLOCK_N
        chunk = tl.minimum(BLOCK_N, end_tok - tok)
        abs_tok = tok + offs_n
        mask_n = offs_n < chunk

        page_idx = abs_tok // PAGE_SIZE
        page_off = abs_tok % PAGE_SIZE
        phys = tl.load(
            BT_ptr + batch_id * stride_bt_b + page_idx * stride_bt_p,
            mask=mask_n,
            other=0,
        ).to(tl.int64)

        tok_base = (
            phys * stride_kv_block
            + page_off.to(tl.int64) * stride_kv_tok
            + kv_head_id * stride_kv_h
        )
        k = tl.load(
            KV_ptr + tok_base[:, None] + offs_d[None, :] * stride_kv_d,
            mask=mask_n[:, None],
            other=0.0,
        )
        v = tl.load(
            KV_ptr + tok_base[:, None] + (HEAD_DIM + offs_d[None, :]) * stride_kv_d,
            mask=mask_n[:, None],
            other=0.0,
        )

        qk = tl.dot(q_scaled, tl.trans(k))
        qk = tl.where(mask_n[None, :], qk, float("-inf"))

        m_ij = tl.max(qk, axis=1)
        m_new = tl.maximum(m_i, m_ij)
        alpha = tl.where(m_i == float("-inf"), 0.0, tl.exp(m_i - m_new))
        p = tl.exp(qk - m_new[:, None])
        p = tl.where(mask_n[None, :], p, 0.0)
        l_ij = tl.sum(p, axis=1)

        acc = acc * alpha[:, None]
        acc = tl.dot(p.to(tl.bfloat16), v, acc)
        l_i = l_i * alpha + l_ij
        m_i = m_new

    if HAS_SPLITS:
        po = (
            PartialO_ptr
            + split_id * stride_po_s
            + batch_id * stride_po_b
            + h_ids[:, None] * stride_po_h
            + offs_d[None, :] * stride_po_d
        )
        tl.store(po, acc, mask=mask_g[:, None])
        pm = PartialM_ptr + split_id * stride_pm_s + batch_id * stride_pm_b + h_ids * stride_pm_h
        tl.store(pm, m_i, mask=mask_g)
        pl = PartialL_ptr + split_id * stride_pm_s + batch_id * stride_pm_b + h_ids * stride_pm_h
        tl.store(pl, l_i, mask=mask_g)
    else:
        l_safe = tl.where(l_i > 0.0, l_i, 1.0)
        tl.store(
            Out_ptr
            + batch_id * stride_ob
            + h_ids[:, None] * stride_oh
            + offs_d[None, :] * stride_od,
            (acc / l_safe[:, None]).to(tl.bfloat16),
            mask=mask_g[:, None],
        )


@triton.jit
def _reduce_kernel(
    PartialO_ptr, PartialM_ptr, PartialL_ptr, Out_ptr,
    stride_po_s, stride_po_b, stride_po_h, stride_po_d,
    stride_pm_s, stride_pm_b, stride_pm_h,
    stride_ob, stride_oh, stride_od,
    NUM_SPLITS: tl.constexpr,
    HEAD_DIM: tl.constexpr,
):
    batch_id = tl.program_id(0)
    head_id = tl.program_id(1)
    offs_d = tl.arange(0, HEAD_DIM)
    m = tl.full([], float("-inf"), dtype=tl.float32)
    l = tl.full([], 0.0, dtype=tl.float32)
    acc = tl.zeros([HEAD_DIM], dtype=tl.float32)
    for s in tl.static_range(NUM_SPLITS):
        m_s = tl.load(
            PartialM_ptr + s * stride_pm_s + batch_id * stride_pm_b + head_id * stride_pm_h
        )
        l_s = tl.load(
            PartialL_ptr + s * stride_pm_s + batch_id * stride_pm_b + head_id * stride_pm_h
        )
        o_s = tl.load(
            PartialO_ptr
            + s * stride_po_s
            + batch_id * stride_po_b
            + head_id * stride_po_h
            + offs_d * stride_po_d
        )
        m_new = tl.maximum(m, m_s)
        alpha = tl.where(m == float("-inf"), 0.0, tl.exp(m - m_new))
        alpha_s = tl.where(m_s == float("-inf"), 0.0, tl.exp(m_s - m_new))
        acc = acc * alpha + o_s * alpha_s
        l = l * alpha + l_s * alpha_s
        m = m_new
    tl.store(
        Out_ptr + batch_id * stride_ob + head_id * stride_oh + offs_d * stride_od,
        (acc / tl.where(l > 0.0, l, 1.0)).to(tl.bfloat16),
    )


class _Workspace:
    def __init__(self):
        self.key = None
        self.po = self.pm = self.pl = None

    def get(self, ns, B, H, D, device):
        key = (ns, B, H, D, str(device))
        if key != self.key:
            self.po = torch.empty(ns, B, H, D, dtype=torch.float32, device=device)
            self.pm = torch.empty(ns, B, H, dtype=torch.float32, device=device)
            self.pl = torch.empty(ns, B, H, dtype=torch.float32, device=device)
            self.key = key
        return self.po, self.pm, self.pl


_WS = _Workspace()


def _pick_splits(batch: int, hkv: int, seq_len: int) -> int:
    """Split-KV heuristic for H100 (CUDA-graph friendly)."""
    base = max(1, batch * hkv)
    if base >= 192:
        return 1
    target = 384
    max_splits = max(1, seq_len // 64)
    desired = (target + base - 1) // base
    return min(max(1, desired), max_splits, 16)


def paged_attention_decode(
    query: torch.Tensor,
    kv_cache: torch.Tensor,
    block_table: torch.Tensor,
    seq_lens: torch.Tensor,
    num_kv_heads: int,
    page_size: int,
    scale: float,
    out: torch.Tensor | None = None,
    max_seq: int | None = None,
) -> torch.Tensor:
    B, H, D = query.shape
    Hkv = num_kv_heads
    group_size = H // Hkv

    if out is None:
        out = torch.empty(B, H, D, dtype=query.dtype, device=query.device)

    if max_seq is None:
        max_seq = int(seq_lens.max().item()) if B > 0 else 0

    num_splits = _pick_splits(B, Hkv, max_seq)
    BLOCK_M = max(16, triton.next_power_of_2(group_size))
    n_ctas = B * Hkv * num_splits

    if D <= 64:
        BLOCK_N, num_warps, num_stages = 128, 4, 3
    elif n_ctas >= 200 or max_seq >= 2048:
        BLOCK_N, num_warps, num_stages = 128, 4, 4
    else:
        BLOCK_N, num_warps, num_stages = 64, 4, 4

    kwargs = dict(
        stride_qb=query.stride(0),
        stride_qh=query.stride(1),
        stride_qd=query.stride(2),
        stride_kv_block=kv_cache.stride(0),
        stride_kv_tok=kv_cache.stride(1),
        stride_kv_h=kv_cache.stride(2),
        stride_kv_d=kv_cache.stride(3),
        stride_bt_b=block_table.stride(0),
        stride_bt_p=block_table.stride(1),
        stride_ob=out.stride(0),
        stride_oh=out.stride(1),
        stride_od=out.stride(2),
        scale=scale,
        num_heads=H,
        num_kv_heads=Hkv,
        PAGE_SIZE=page_size,
        HEAD_DIM=D,
        BLOCK_M=BLOCK_M,
        BLOCK_N=BLOCK_N,
        NUM_SPLITS=num_splits,
        num_warps=num_warps,
        num_stages=num_stages,
    )

    if num_splits == 1:
        _gqa_kernel[(B, Hkv, 1)](
            query, kv_cache, block_table, seq_lens, out,
            None, None, None,
            stride_po_s=0, stride_po_b=0, stride_po_h=0, stride_po_d=0,
            stride_pm_s=0, stride_pm_b=0, stride_pm_h=0,
            HAS_SPLITS=False,
            **kwargs,
        )
    else:
        po, pm, pl = _WS.get(num_splits, B, H, D, query.device)
        _gqa_kernel[(B, Hkv, num_splits)](
            query, kv_cache, block_table, seq_lens, out, po, pm, pl,
            stride_po_s=po.stride(0),
            stride_po_b=po.stride(1),
            stride_po_h=po.stride(2),
            stride_po_d=po.stride(3),
            stride_pm_s=pm.stride(0),
            stride_pm_b=pm.stride(1),
            stride_pm_h=pm.stride(2),
            HAS_SPLITS=True,
            **kwargs,
        )
        _reduce_kernel[(B, H)](
            po, pm, pl, out,
            po.stride(0), po.stride(1), po.stride(2), po.stride(3),
            pm.stride(0), pm.stride(1), pm.stride(2),
            out.stride(0), out.stride(1), out.stride(2),
            NUM_SPLITS=num_splits,
            HEAD_DIM=D,
            num_warps=4,
        )
    return out


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 = None
        self._graph_keys = None
        self._static_out = None

    def forward(self, query, kv_cache, block_table, seq_lens):
        keys = (
            query.data_ptr(),
            kv_cache.data_ptr(),
            block_table.data_ptr(),
            seq_lens.data_ptr(),
            tuple(query.shape),
            tuple(kv_cache.shape),
        )
        if (
            self._graph is not None
            and self._graph_keys == keys
            and self._static_out is not None
        ):
            self._graph.replay()
            return self._static_out

        kw = dict(
            num_kv_heads=self.num_kv_heads,
            page_size=self.page_size,
            scale=self.scale,
            max_seq=self.seq_len,
        )
        out = paged_attention_decode(query, kv_cache, block_table, seq_lens, **kw)

        if not query.is_cuda:
            return out

        try:
            if self._static_out is None or self._static_out.shape != out.shape:
                self._static_out = torch.empty_like(out)
            s = torch.cuda.Stream()
            s.wait_stream(torch.cuda.current_stream())
            with torch.cuda.stream(s):
                paged_attention_decode(
                    query, kv_cache, block_table, seq_lens,
                    out=self._static_out, **kw,
                )
            torch.cuda.current_stream().wait_stream(s)

            g = torch.cuda.CUDAGraph()
            with torch.cuda.graph(g):
                paged_attention_decode(
                    query, kv_cache, block_table, seq_lens,
                    out=self._static_out, **kw,
                )
            self._graph = g
            self._graph_keys = keys
            g.replay()
            return self._static_out
        except Exception:
            self._graph = None
            self._graph_keys = None
            return out


def get_inputs():
    B, H, Hkv, D, L, P = BATCH, NUM_HEADS, NUM_KV_HEADS, HEAD_DIM, SEQ_LEN, 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]

20260709_040836_grok_grok-4.5_03_paged_attention