kernelbench.com

KernelBench hard · H100

Paged Attention MiniMax-M3

24.5%geomean peak fraction across shapes
harnessminimax-claudeagent session45mtotal wall46mcheck21sbenchmark43soutput tokensgpu-lock wait36sgpu-lock held28sregimememory

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

8×32×8×128×1024×160.097 ms17.0%0.35 TB/s · 17% of 2.0 TB/s HBM · also 1 TFLOPS (0% of compute)
32×32×8×128×2048×160.213 ms62.0%1.26 TB/s · 62% of 2.0 TB/s HBM · also 5 TFLOPS (1% of compute)
4×64×8×128×4096×160.120 ms27.5%0.56 TB/s · 27% of 2.0 TB/s HBM · also 4 TFLOPS (1% of compute)
16×32×8×128×1535×160.130 ms37.9%0.77 TB/s · 38% of 2.0 TB/s HBM · also 3 TFLOPS (0% of compute)
8×16×4×64×2000×160.092 ms8.7%0.18 TB/s · 9% of 2.0 TB/s HBM · also 1 TFLOPS (0% of compute)

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

geomean(17.0% · 62.0% · 27.5% · 37.9% · 8.7%) = 24.9% · published 24.5% (lower of repeated isolated re-benchmark passes)

Kernel source (redacted)
"""Paged attention decode kernel for H100 (Hopper SM90).

Single-query decode with grouped-query attention. The KV cache is laid out as
(num_blocks, page_size, num_kv_heads, head_dim * 2) with [K | V] packed on the
last dim.

Two-stage "flash decoding":
  1. `_paged_attn_split_kernel`: each program handles one (batch, kv_head,
     split) and computes a partial (acc, m, l) over a contiguous chunk of the
     sequence. The number of splits is chosen so the grid roughly fills
     the SMs.
  2. `_paged_attn_combine_kernel`: per (batch, kv_head), combines the
     per-split (acc, m, l) into the final output via a log-sum-exp reduction.
"""
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"]

LOG2E = 1.4426950408889634

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


# ============================================================================
# Stage 1a: single-pass attention (no split)
# ============================================================================
@triton.jit
def _paged_attn_kernel(
    Q_ptr,           # (B, H, D) bf16
    KV_ptr,          # (num_blocks, P, H_kv, 2*D) bf16, [K | V]
    BT_ptr,          # (B, max_blocks) int32
    SL_ptr,          # (B,) int32
    O_ptr,           # (B, H, D) bf16
    stride_qb, stride_qh, stride_qd,
    stride_kvn, stride_kvp, stride_kvh, stride_kvd,
    stride_btb, stride_btn,
    stride_ob, stride_oh, stride_od,
    scale_log2,
    G: tl.constexpr,
    D: tl.constexpr,
    P: tl.constexpr,
    BLOCK_N: tl.constexpr,
):
    pid_b = tl.program_id(0)
    pid_hkv = tl.program_id(1)

    seq_len = tl.load(SL_ptr + pid_b)

    h_start = pid_hkv * G
    q_head_offs = h_start + tl.arange(0, G)
    d_offs = tl.arange(0, D)

    q_ptrs = (
        Q_ptr
        + pid_b * stride_qb
        + q_head_offs[:, None] * stride_qh
        + d_offs[None, :] * stride_qd
    )
    q = tl.load(q_ptrs)  # (G, D)

    m_i = tl.full((G,), -1.0e30, dtype=tl.float32)
    l_i = tl.zeros((G,), dtype=tl.float32)
    acc = tl.zeros((G, D), dtype=tl.float32)

    n_offs = tl.arange(0, BLOCK_N)

    for start in tl.range(0, seq_len, BLOCK_N):
        token_offs = start + n_offs
        page_offs = token_offs // P
        offset_in_page = token_offs % P
        valid = token_offs < seq_len

        page_ids = tl.load(
            BT_ptr + pid_b * stride_btb + page_offs * stride_btn,
            mask=valid, other=0,
        )

        k_offs = (
            page_ids * stride_kvn
            + offset_in_page * stride_kvp
            + pid_hkv * stride_kvh
        )
        k_ptrs = KV_ptr + k_offs[:, None] + d_offs[None, :] * stride_kvd
        v_ptrs = KV_ptr + k_offs[:, None] + (d_offs[None, :] + D) * stride_kvd

        k = tl.load(k_ptrs, mask=valid[:, None], other=0.0)
        v = tl.load(v_ptrs, mask=valid[:, None], other=0.0)

        s = tl.dot(q, tl.trans(k)) * scale_log2
        s = tl.where(valid[None, :], s, -1.0e30)

        m_new = tl.maximum(m_i, tl.max(s, axis=1))
        alpha = tl.exp2(m_i - m_new)
        p = tl.exp2(s - m_new[:, None])

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

    acc = acc / l_i[:, None]

    o_ptrs = (
        O_ptr
        + pid_b * stride_ob
        + q_head_offs[:, None] * stride_oh
        + d_offs[None, :] * stride_od
    )
    tl.store(o_ptrs, acc.to(tl.bfloat16))


# ============================================================================
# Stage 1b: split-K attention
# ============================================================================
@triton.jit
def _paged_attn_split_kernel(
    Q_ptr,           # (B, H, D) bf16
    KV_ptr,          # (num_blocks, P, H_kv, 2*D) bf16, [K | V]
    BT_ptr,          # (B, max_blocks) int32
    SL_ptr,          # (B,) int32
    O_partial_ptr,   # (B, H_kv, S, G, D) fp32
    M_partial_ptr,   # (B, H_kv, S, G) fp32  -- running max in log2 space
    L_partial_ptr,   # (B, H_kv, S, G) fp32  -- running unnormalized sum
    # Q strides
    stride_qb, stride_qh, stride_qd,
    # KV strides (in elements)
    stride_kvn, stride_kvp, stride_kvh, stride_kvd,
    # block_table strides
    stride_btb, stride_btn,
    # partial output strides
    stride_op_b, stride_op_h, stride_op_s, stride_op_g, stride_op_d,
    # partial m, l strides
    stride_mp_b, stride_mp_h, stride_mp_s, stride_mp_g,
    stride_lp_b, stride_lp_h, stride_lp_s, stride_lp_g,
    scale_log2,
    NUM_SPLITS: tl.constexpr,
    G: tl.constexpr,
    D: tl.constexpr,
    P: tl.constexpr,
    BLOCK_N: tl.constexpr,
):
    pid_b = tl.program_id(0)
    pid_hkv = tl.program_id(1)
    pid_split = tl.program_id(2)

    seq_len = tl.load(SL_ptr + pid_b)

    chunk = (seq_len + NUM_SPLITS - 1) // NUM_SPLITS
    n_start = pid_split * chunk
    n_end = tl.minimum(n_start + chunk, seq_len)

    h_start = pid_hkv * G
    q_head_offs = h_start + tl.arange(0, G)        # absolute head index, for Q load
    d_offs = tl.arange(0, D)
    g_offs_l = tl.arange(0, G)                     # group index (0..G-1), for partial buffers

    if n_start >= seq_len:
        # write -inf m, 0 l so combine ignores this split
        m_ptrs = (
            M_partial_ptr
            + pid_b * stride_mp_b
            + pid_hkv * stride_mp_h
            + pid_split * stride_mp_s
            + g_offs_l * stride_mp_g
        )
        l_ptrs = (
            L_partial_ptr
            + pid_b * stride_lp_b
            + pid_hkv * stride_lp_h
            + pid_split * stride_lp_s
            + g_offs_l * stride_lp_g
        )
        tl.store(m_ptrs, tl.full((G,), -1.0e30, dtype=tl.float32))
        tl.store(l_ptrs, tl.zeros((G,), dtype=tl.float32))
        return

    # ----- load Q (G, D) -----
    q_ptrs = (
        Q_ptr
        + pid_b * stride_qb
        + q_head_offs[:, None] * stride_qh
        + d_offs[None, :] * stride_qd
    )
    q = tl.load(q_ptrs)  # (G, D)

    # ----- online-softmax accumulators -----
    m_i = tl.full((G,), -1.0e30, dtype=tl.float32)
    l_i = tl.zeros((G,), dtype=tl.float32)
    acc = tl.zeros((G, D), dtype=tl.float32)

    n_offs = tl.arange(0, BLOCK_N)

    for start in tl.range(n_start, n_end, BLOCK_N):
        token_offs = start + n_offs
        page_offs = token_offs // P
        offset_in_page = token_offs % P
        valid = token_offs < n_end

        page_ids = tl.load(
            BT_ptr + pid_b * stride_btb + page_offs * stride_btn,
            mask=valid, other=0,
        )

        k_offs = (
            page_ids * stride_kvn
            + offset_in_page * stride_kvp
            + pid_hkv * stride_kvh
        )
        k_ptrs = KV_ptr + k_offs[:, None] + d_offs[None, :] * stride_kvd
        v_ptrs = KV_ptr + k_offs[:, None] + (d_offs[None, :] + D) * stride_kvd

        k = tl.load(k_ptrs, mask=valid[:, None], other=0.0)  # (BLOCK_N, D)
        v = tl.load(v_ptrs, mask=valid[:, None], other=0.0)  # (BLOCK_N, D)

        s = tl.dot(q, tl.trans(k)) * scale_log2
        s = tl.where(valid[None, :], s, -1.0e30)

        m_new = tl.maximum(m_i, tl.max(s, axis=1))
        alpha = tl.exp2(m_i - m_new)
        p = tl.exp2(s - m_new[:, None])

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

    # write partial output and (m, l)
    o_ptrs = (
        O_partial_ptr
        + pid_b * stride_op_b
        + pid_hkv * stride_op_h
        + pid_split * stride_op_s
        + g_offs_l[:, None] * stride_op_g
        + d_offs[None, :] * stride_op_d
    )
    tl.store(o_ptrs, acc)

    m_ptrs = (
        M_partial_ptr
        + pid_b * stride_mp_b
        + pid_hkv * stride_mp_h
        + pid_split * stride_mp_s
        + g_offs_l * stride_mp_g
    )
    l_ptrs = (
        L_partial_ptr
        + pid_b * stride_lp_b
        + pid_hkv * stride_lp_h
        + pid_split * stride_lp_s
        + g_offs_l * stride_lp_g
    )
    tl.store(m_ptrs, m_i)
    tl.store(l_ptrs, l_i)


# ============================================================================
# Stage 2: combine partial outputs across splits
# ============================================================================
@triton.jit
def _paged_attn_combine_kernel(
    O_partial_ptr,   # (B, H_kv, S, G, D) fp32
    M_partial_ptr,   # (B, H_kv, S, G) fp32
    L_partial_ptr,   # (B, H_kv, S, G) fp32
    O_ptr,           # (B, H, D) bf16
    stride_op_b, stride_op_h, stride_op_s, stride_op_g, stride_op_d,
    stride_mp_b, stride_mp_h, stride_mp_s, stride_mp_g,
    stride_lp_b, stride_lp_h, stride_lp_s, stride_lp_g,
    stride_ob, stride_oh, stride_od,
    G: tl.constexpr,
    D: tl.constexpr,
    NUM_SPLITS: tl.constexpr,
    BLOCK_S: tl.constexpr,
):
    pid_b = tl.program_id(0)
    pid_hkv = tl.program_id(1)

    h_start = pid_hkv * G
    g_offs = h_start + tl.arange(0, G)        # absolute head index, for output
    g_offs_l = tl.arange(0, G)                # group index, for partial buffers
    d_offs = tl.arange(0, D)
    s_offs = tl.arange(0, BLOCK_S)
    s_mask = s_offs < NUM_SPLITS

    # Load m, l (G, S) — m in log2 space, l unnormalized in normal space
    m_ptrs = (
        M_partial_ptr
        + pid_b * stride_mp_b
        + pid_hkv * stride_mp_h
        + s_offs[None, :] * stride_mp_s
        + g_offs_l[:, None] * stride_mp_g
    )
    l_ptrs = (
        L_partial_ptr
        + pid_b * stride_lp_b
        + pid_hkv * stride_lp_h
        + s_offs[None, :] * stride_lp_s
        + g_offs_l[:, None] * stride_lp_g
    )
    m = tl.load(m_ptrs, mask=s_mask[None, :], other=-1.0e30)
    l = tl.load(l_ptrs, mask=s_mask[None, :], other=0.0)

    m_max = tl.max(m, axis=1)                          # (G,)
    alpha = tl.exp2(m - m_max[:, None])                # (G, S)
    alpha_l = alpha * l                                # (G, S)
    l_global = tl.sum(alpha_l, axis=1)                 # (G,)

    # Load O_partial (G, S, D)
    op_ptrs = (
        O_partial_ptr
        + pid_b * stride_op_b
        + pid_hkv * stride_op_h
        + s_offs[None, :, None] * stride_op_s
        + g_offs_l[:, None, None] * stride_op_g
        + d_offs[None, None, :] * stride_op_d
    )
    op = tl.load(op_ptrs, mask=s_mask[None, :, None], other=0.0)  # (G, S, D)

    acc = tl.sum(alpha[:, :, None] * op, axis=1)       # (G, D)
    out = acc / l_global[:, None]

    o_ptrs = (
        O_ptr
        + pid_b * stride_ob
        + g_offs[:, None] * stride_oh
        + d_offs[None, :] * stride_od
    )
    tl.store(o_ptrs, out.to(tl.bfloat16))


# ============================================================================
# Driver
# ============================================================================
def _pick_block_n(D: int) -> int:
    if D >= 128:
        return 64
    return 128


def _pick_num_warps(D: int) -> int:
    if D >= 64:
        return 4
    return 2


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


# H100 PCIe: 114 SMs. Aim for ~2-3x oversubscription of work-units.
def _pick_num_splits(B: int, H_kv: int, seq_len: int) -> int:
    base = B * H_kv
    if base >= 64:
        return 1
    # aim for ~256 program-units
    splits = max(1, 256 // base)
    # cap so each split has at least 128 tokens of work
    splits = min(splits, max(1, seq_len // 128))
    return splits


_WORKSPACE = {}


def _get_workspace(B: int, H_kv: int, G: int, D: int, S: int, device):
    key = (B, H_kv, G, D, S, str(device))
    ws = _WORKSPACE.get(key)
    if ws is None:
        op = torch.empty(B, H_kv, S, G, D, dtype=torch.float32, device=device)
        mp = torch.empty(B, H_kv, S, G, dtype=torch.float32, device=device)
        lp = torch.empty(B, H_kv, S, G, dtype=torch.float32, device=device)
        _WORKSPACE[key] = (op, mp, lp)
        return op, mp, lp
    return ws


class Model(nn.Module):
    """Single-query paged attention decode (Triton, split-K)."""

    def __init__(
        self,
        batch: int,
        num_heads: int,
        num_kv_heads: int,
        head_dim: int,
        seq_len: int,
        page_size: int,
    ):
        super().__init__()
        assert num_heads % num_kv_heads == 0, "num_heads must be a multiple of num_kv_heads (GQA)"
        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_log2 = LOG2E / math.sqrt(head_dim)
        self.register_buffer(
            "_dummy", torch.zeros(1, dtype=torch.bfloat16), persistent=False
        )

    def forward(
        self,
        query: torch.Tensor,
        kv_cache: torch.Tensor,
        block_table: torch.Tensor,
        seq_lens: torch.Tensor,
    ) -> torch.Tensor:
        B, H, D = query.shape
        H_kv = self.num_kv_heads
        G = self.group_size
        P = self.page_size

        out = torch.empty(B, H, D, dtype=query.dtype, device=query.device)

        BLOCK_N = _pick_block_n(D)
        num_warps = _pick_num_warps(D)

        num_splits = _pick_num_splits(B, H_kv, int(seq_lens.max().item()))

        if num_splits == 1:
            grid = (B, H_kv)
            _paged_attn_kernel[grid](
                query, kv_cache, block_table, seq_lens, out,
                query.stride(0), query.stride(1), query.stride(2),
                kv_cache.stride(0), kv_cache.stride(1),
                kv_cache.stride(2), kv_cache.stride(3),
                block_table.stride(0), block_table.stride(1),
                out.stride(0), out.stride(1), out.stride(2),
                self.scale_log2,
                G=G, D=D, P=P, BLOCK_N=BLOCK_N,
                num_warps=num_warps, num_stages=2,
            )
            return out

        # split path
        num_splits_pow2 = max(1, _next_pow2(num_splits))
        op, mp, lp = _get_workspace(B, H_kv, G, D, num_splits, query.device)

        grid_split = (B, H_kv, num_splits)
        _paged_attn_split_kernel[grid_split](
            query, kv_cache, block_table, seq_lens, op, mp, lp,
            query.stride(0), query.stride(1), query.stride(2),
            kv_cache.stride(0), kv_cache.stride(1),
            kv_cache.stride(2), kv_cache.stride(3),
            block_table.stride(0), block_table.stride(1),
            op.stride(0), op.stride(1), op.stride(2), op.stride(3), op.stride(4),
            mp.stride(0), mp.stride(1), mp.stride(2), mp.stride(3),
            lp.stride(0), lp.stride(1), lp.stride(2), lp.stride(3),
            self.scale_log2,
            NUM_SPLITS=num_splits,
            G=G, D=D, P=P, BLOCK_N=BLOCK_N,
            num_warps=num_warps, num_stages=2,
        )

        grid_combine = (B, H_kv)
        _paged_attn_combine_kernel[grid_combine](
            op, mp, lp, out,
            op.stride(0), op.stride(1), op.stride(2), op.stride(3), op.stride(4),
            mp.stride(0), mp.stride(1), mp.stride(2), mp.stride(3),
            lp.stride(0), lp.stride(1), lp.stride(2), lp.stride(3),
            out.stride(0), out.stride(1), out.stride(2),
            G=G, D=D, NUM_SPLITS=num_splits, BLOCK_S=num_splits_pow2,
            num_warps=2, num_stages=1,
        )
        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]

20260618_065118_minimax-claude_MiniMax-M3_03_paged_attention