KernelBench hard · H100
Paged Attention GLM-5.2
31.4%geomean peak fraction across shapes
harnesszai-claudeagent session45mtotal wall51mcheck45sbenchmark6moutput tokens—gpu-lock wait6mgpu-lock held30sregimememory
Per-shape vs governing ceilingeach shape graded against whichever binds — bf16 compute or HBM bandwidth
8×32×8×128×1024×160.069 ms24.0%0.49 TB/s · 24% of 2.0 TB/s HBM · also 2 TFLOPS (0% of compute)
32×32×8×128×2048×160.217 ms60.9%1.24 TB/s · 61% of 2.0 TB/s HBM · also 5 TFLOPS (1% of compute)
4×64×8×128×4096×160.094 ms35.2%0.72 TB/s · 35% of 2.0 TB/s HBM · also 6 TFLOPS (1% of compute)
16×32×8×128×1535×160.118 ms41.8%0.85 TB/s · 42% of 2.0 TB/s HBM · also 3 TFLOPS (0% of compute)
8×16×4×64×2000×160.057 ms14.2%0.29 TB/s · 14% 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(24.0% · 60.9% · 35.2% · 41.8% · 14.2%) = 31.4%
Kernel source (redacted)
"""Custom paged-attention decode kernel for H100 PCIe (SM90 Hopper).
Memory-bound single-query decode. Strategy: grouped flash-decoding.
* One CTA per (seq_split, batch, kv_head) -> processes ALL G query heads in
that GQA group so the KV cache for that kv_head is streamed exactly once
(the dominant cost). G = num_heads // num_kv_heads.
* Adaptive sequence splitting so the grid fills the 114 SMs even at small
batch*kv_head (e.g. B=4,Hkv=8 => 32 pairs => split seq 4096 into chunks).
* Two-kernel flash-decode: split kernel writes fp32 partial (O, lse); a tiny
reduce kernel merges the splits via logsumexp. When num_splits==1 the split
kernel normalises and writes the final bf16 output directly (no reduce).
* KV cache layout (num_blocks, page_size, num_kv_heads, head_dim*2) packs
[K|V] on the last dim, so one gather per token pulls both. We iterate in
page-sized tiles so each page is a single contiguous load.
"""
import math
import torch
import torch.nn as nn
import triton
import triton.language as tl
OP_TYPE = "attention"
SUPPORTED_PRECISIONS = ["bf16"]
HARDWARE_REQUIRED = ["H100"]
BATCH = 8
NUM_HEADS = 32
NUM_KV_HEADS = 8
HEAD_DIM = 128
SEQ_LEN = 1024
PAGE_SIZE = 16
@triton.jit
def _attn_split_kernel(
Q_ptr, KV_ptr, BT_ptr, SL_ptr,
OPART_ptr, LSE_ptr, O_ptr,
# strides
qsb, qsh,
kvsb, kvsp, kvsh,
btsb,
opsb, opbb, ophb,
lssb, lsbb,
osb, ohb,
# runtime ints
pages_per_split,
pages_per_seq,
sm_scale,
# constexprs
G_C: tl.constexpr,
D_C: tl.constexpr,
P_C: tl.constexpr,
N_C: tl.constexpr, # pages per inner-loop tile
BLOCK_SEQ: tl.constexpr, # N_C * P_C
NUM_SPLITS: tl.constexpr,
):
split_idx = tl.program_id(0)
b = tl.program_id(1)
hkv = tl.program_id(2)
seq_len = tl.load(SL_ptr + b)
g_ar = tl.arange(0, G_C)
d_ar = tl.arange(0, D_C)
n_ar = tl.arange(0, N_C)
p_ar = tl.arange(0, P_C)
# Load Q for all G heads of this kv_head group: [G_C, D_C]
q_ptrs = Q_ptr + b * qsb + (hkv * G_C + g_ar[:, None]) * qsh + d_ar[None, :]
q = tl.load(q_ptrs)
first_page = split_idx * pages_per_split
last_page = first_page + pages_per_split
last_page = tl.minimum(last_page, pages_per_seq)
m_i = tl.full([G_C], -float("inf"), tl.float32)
l_i = tl.zeros([G_C], tl.float32)
acc = tl.zeros([G_C, D_C], tl.float32)
for p0 in range(first_page, last_page, N_C):
page_idx = p0 + n_ar # [N_C]
page_valid = page_idx < pages_per_seq
phys = tl.load(BT_ptr + b * btsb + page_idx,
mask=page_valid, other=0) # [N_C] int32
# token index for each (page, in-page-offset): [N_C, P_C]
tidx = page_idx[:, None] * P_C + p_ar[None, :]
tok_valid = tidx < seq_len
base = (KV_ptr
+ phys[:, None, None] * kvsb
+ p_ar[None, :, None] * kvsp
+ hkv * kvsh)
K = tl.reshape(tl.load(base + d_ar[None, None, :],
mask=tok_valid[:, :, None], other=0.0),
(BLOCK_SEQ, D_C))
V = tl.reshape(tl.load(base + D_C + d_ar[None, None, :],
mask=tok_valid[:, :, None], other=0.0),
(BLOCK_SEQ, D_C))
s = tl.dot(q, tl.trans(K)) * sm_scale # [G_C, BLOCK_SEQ]
tok_valid_1d = tl.reshape(tok_valid, (BLOCK_SEQ,))
s = tl.where(tok_valid_1d[None, :], s, -float("inf"))
m_n = tl.maximum(m_i, tl.max(s, 1))
alpha = tl.exp(m_i - m_n)
p = tl.exp(s - m_n[:, None])
acc = acc * alpha[:, None]
acc = tl.dot(p.to(tl.bfloat16), V, acc=acc)
l_i = l_i * alpha + tl.sum(p, 1)
m_i = m_n
if NUM_SPLITS == 1:
o = acc / l_i[:, None]
o_ptrs = O_ptr + b * osb + (hkv * G_C + g_ar[:, None]) * ohb + d_ar[None, :]
tl.store(o_ptrs, o.to(tl.bfloat16))
else:
lse = m_i + tl.log(l_i)
opart = acc / l_i[:, None] # normalised partial
op_ptrs = (OPART_ptr + split_idx * opsb + b * opbb
+ (hkv * G_C + g_ar[:, None]) * ophb + d_ar[None, :])
tl.store(op_ptrs, opart) # fp32
lse_ptrs = LSE_ptr + split_idx * lssb + b * lsbb + (hkv * G_C + g_ar)
tl.store(lse_ptrs, lse)
@triton.jit
def _reduce_kernel(
OPART_ptr, LSE_ptr, O_ptr,
opsb, opbb, ophb,
lssb, lsbb,
osb, ohb,
NUM_SPLITS,
G_C: tl.constexpr,
D_C: tl.constexpr,
BLOCK_S: tl.constexpr,
):
b = tl.program_id(0)
hkv = tl.program_id(1)
g_ar = tl.arange(0, G_C)
d_ar = tl.arange(0, D_C)
s_ar = tl.arange(0, BLOCK_S)
s_valid = s_ar < NUM_SPLITS
lse_ptrs = (LSE_ptr + s_ar[:, None] * lssb + b * lsbb
+ (hkv * G_C + g_ar[None, :]))
lse = tl.load(lse_ptrs, mask=s_valid[:, None], other=-float("inf")) # [BLOCK_S, G_C]
m_g = tl.max(lse, 0)
alpha = tl.exp(lse - m_g[None, :])
l_g = tl.sum(alpha, 0)
op_ptrs = (OPART_ptr + s_ar[:, None, None] * opsb + b * opbb
+ (hkv * G_C + g_ar[None, :, None]) * ophb + d_ar[None, None, :])
opart = tl.load(op_ptrs, mask=s_valid[:, None, None], other=0.0) # [BLOCK_S,G_C,D_C]
o = tl.sum(alpha[:, :, None] * opart, 0) # [G_C, D_C]
o = o / l_g[:, None]
o_ptrs = O_ptr + b * osb + (hkv * G_C + g_ar[:, None]) * ohb + d_ar[None, :]
tl.store(o_ptrs, o.to(tl.bfloat16))
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
)
def forward(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
out = torch.empty(B, H, D, dtype=query.dtype, device=query.device)
total_pages = block_table.shape[1]
TARGET_BLOCKS = 512
bhkv = B * Hkv
ns = max(1, (TARGET_BLOCKS + bhkv - 1) // bhkv)
ns = min(ns, total_pages)
pages_per_split = (total_pages + ns - 1) // ns
num_splits = (total_pages + pages_per_split - 1) // pages_per_split
qsb, qsh = query.stride(0), query.stride(1)
kvsb, kvsp, kvsh = kv_cache.stride(0), kv_cache.stride(1), kv_cache.stride(2)
btsb = block_table.stride(0)
osb, ohb = out.stride(0), out.stride(1)
N_C = 4
BLOCK_SEQ = N_C * P
grid = (num_splits, B, Hkv)
if num_splits == 1:
_attn_split_kernel[grid](
query, kv_cache, block_table, seq_lens,
None, None, out,
qsb, qsh, kvsb, kvsp, kvsh, btsb,
0, 0, 0, 0, 0, osb, ohb,
pages_per_split, total_pages, self.scale,
G_C=G, D_C=D, P_C=P, N_C=N_C, BLOCK_SEQ=BLOCK_SEQ,
NUM_SPLITS=1, num_warps=4, num_stages=2,
)
else:
opart = torch.empty(num_splits, B, H, D, dtype=torch.float32,
device=query.device)
lse = torch.empty(num_splits, B, H, dtype=torch.float32,
device=query.device)
opsb, opbb, ophb = opart.stride(0), opart.stride(1), opart.stride(2)
lssb, lsbb = lse.stride(0), lse.stride(1)
_attn_split_kernel[grid](
query, kv_cache, block_table, seq_lens,
opart, lse, out,
qsb, qsh, kvsb, kvsp, kvsh, btsb,
opsb, opbb, ophb, lssb, lsbb, osb, ohb,
pages_per_split, total_pages, self.scale,
G_C=G, D_C=D, P_C=P, N_C=N_C, BLOCK_SEQ=BLOCK_SEQ,
NUM_SPLITS=num_splits, num_warps=4, num_stages=2,
)
BLOCK_S = triton.next_power_of_2(num_splits)
_reduce_kernel[(B, Hkv)](
opart, lse, out,
opsb, opbb, ophb, lssb, lsbb, osb, ohb,
num_splits, G_C=G, D_C=D, BLOCK_S=BLOCK_S, num_warps=4,
)
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_170849_zai-claude_glm-5.2_03_paged_attention