KernelBench hard · H100
Paged Attention MiniMax-M3
24.5%geomean peak fraction across shapes
harnessminimax-claudeagent session45mtotal wall46mcheck21sbenchmark43soutput tokens—gpu-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