KernelBench hard · H100
Paged Attention GPT-5.6 Sol
37.3%geomean peak fraction across shapes
manually audited: clean
harnesscodexagent session38mtotal wall39mcheck53sbenchmark15soutput tokens57,132gpu-lock wait32sgpu-lock held37sregimememory
Per-shape vs governing ceilingeach shape graded against whichever binds — bf16 compute or HBM bandwidth
8×32×8×128×1024×160.053 ms31.4%0.64 TB/s · 31% of 2.0 TB/s HBM · also 3 TFLOPS (0% of compute)
32×32×8×128×2048×160.180 ms73.2%1.49 TB/s · 73% of 2.0 TB/s HBM · also 6 TFLOPS (1% of compute)
4×64×8×128×4096×160.086 ms38.3%0.78 TB/s · 38% of 2.0 TB/s HBM · also 6 TFLOPS (1% of compute)
16×32×8×128×1535×160.095 ms52.3%1.07 TB/s · 52% of 2.0 TB/s HBM · also 4 TFLOPS (1% of compute)
8×16×4×64×2000×160.051 ms15.7%0.32 TB/s · 16% 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(31.4% · 73.2% · 38.3% · 52.3% · 15.7%) = 37.3%
Kernel source (redacted)
"""Fused Triton paged-attention decode kernel.
The attention kernel assigns a program to a (batch, KV-head, sequence-split)
tuple. All query heads in a GQA group are evaluated together, so each packed
K/V cache line is fetched only once. Long, low-batch shapes use a few sequence
splits to expose enough CTAs; a small second kernel combines their online
softmax states.
"""
import math
import torch
import torch.nn as nn
import triton
import triton.language as tl
@triton.jit
def _paged_attention(
query,
kv_cache,
block_table,
seq_lens,
out,
partial_out,
partial_stats,
H: tl.constexpr,
HKV: tl.constexpr,
D: tl.constexpr,
G: tl.constexpr,
PAGE: tl.constexpr,
MAX_PAGES: tl.constexpr,
MAX_SEQ: tl.constexpr,
NUM_SPLITS: tl.constexpr,
BLOCK_M: tl.constexpr,
BLOCK_N: tl.constexpr,
LOOP_STAGES: tl.constexpr,
):
pid = tl.program_id(0)
split = pid % NUM_SPLITS
bh = pid // NUM_SPLITS
kv_head = bh % HKV
batch = bh // HKV
offs_m = tl.arange(0, BLOCK_M)
offs_d = tl.arange(0, D)
q_head = kv_head * G + offs_m
q_ptrs = query + (batch * H + q_head[:, None]) * D + offs_d[None, :]
q = tl.load(q_ptrs, mask=offs_m[:, None] < G, other=0.0)
# Contiguous, non-overlapping partitions. All benchmark sequence lengths
# are the model's MAX_SEQ, while the explicit seq_len predicate handles the
# non-power-of-two tail.
PART = tl.cdiv(MAX_SEQ, NUM_SPLITS)
seq_len = tl.load(seq_lens + batch)
start = split * PART
row_max = tl.full((BLOCK_M,), -float("inf"), tl.float32)
row_sum = tl.zeros((BLOCK_M,), tl.float32)
acc = tl.zeros((BLOCK_M, D), tl.float32)
for block_off in tl.range(0, PART, BLOCK_N, num_stages=LOOP_STAGES):
token = start + block_off + tl.arange(0, BLOCK_N)
valid_n = (token < seq_len) & (token < start + PART)
page_slot = token % PAGE
# Every configured token tile begins on a page boundary. Load each
# page-table entry once, then broadcast it to its PAGE token lanes.
page_in_tile = tl.arange(0, BLOCK_N // PAGE)
logical_page = (start + block_off) // PAGE + page_in_tile
valid_page = (start + block_off + page_in_tile * PAGE) < seq_len
physical_pages = tl.load(
block_table + batch * MAX_PAGES + logical_page,
mask=valid_page,
other=0,
)
physical_page = tl.reshape(
tl.broadcast_to(
physical_pages[:, None],
(BLOCK_N // PAGE, PAGE),
),
(BLOCK_N,),
)
# kv_cache: [page, PAGE, HKV, 2*D], with [K | V] packed in
# the innermost dimension.
token_base = (
physical_page * (PAGE * HKV * 2 * D)
+ page_slot * (HKV * 2 * D)
+ kv_head * (2 * D)
)
kv_ptrs = kv_cache + token_base[:, None] + offs_d[None, :]
k = tl.load(
kv_ptrs,
mask=valid_n[:, None],
other=0.0,
cache_modifier=".cg",
)
v = tl.load(
kv_ptrs + D,
mask=valid_n[:, None],
other=0.0,
cache_modifier=".cg",
)
scores = tl.dot(q, tl.trans(k)) * (1.0 / math.sqrt(D))
active = (offs_m[:, None] < G) & valid_n[None, :]
scores = tl.where(active, scores, -float("inf"))
block_max = tl.max(scores, axis=1)
next_max = tl.maximum(row_max, block_max)
alpha = tl.where(
next_max == -float("inf"),
0.0,
tl.exp2((row_max - next_max) * 1.4426950408889634074),
)
prob = tl.where(
active,
tl.exp2((scores - next_max[:, None]) * 1.4426950408889634074),
0.0,
)
acc = acc * alpha[:, None] + tl.dot(prob.to(tl.bfloat16), v)
row_sum = row_sum * alpha + tl.sum(prob, axis=1)
row_max = next_max
if NUM_SPLITS == 1:
result = acc / row_sum[:, None]
out_ptrs = out + (batch * H + q_head[:, None]) * D + offs_d[None, :]
tl.store(out_ptrs, result, mask=offs_m[:, None] < G)
else:
# partial_out layout: [B, HKV, S, G, D]
po_base = ((batch * HKV + kv_head) * NUM_SPLITS + split) * G * D
po_ptrs = partial_out + po_base + offs_m[:, None] * D + offs_d[None, :]
tl.store(po_ptrs, acc, mask=offs_m[:, None] < G)
# partial_stats layout: [B, HKV, S, G, 2] = [max, sum]
ps_base = ((batch * HKV + kv_head) * NUM_SPLITS + split) * G * 2
ps_ptrs = partial_stats + ps_base + offs_m * 2
tl.store(ps_ptrs, row_max, mask=offs_m < G)
tl.store(ps_ptrs + 1, row_sum, mask=offs_m < G)
@triton.jit
def _reduce_splits_by_head(
partial_out,
partial_stats,
out,
H: tl.constexpr,
HKV: tl.constexpr,
D: tl.constexpr,
G: tl.constexpr,
NUM_SPLITS: tl.constexpr,
):
"""Combine partitions with one program per query head for occupancy."""
bhq = tl.program_id(0)
batch = bhq // H
q_head = bhq % H
kv_head = q_head // G
group_head = q_head % G
bh = batch * HKV + kv_head
offs_d = tl.arange(0, D)
global_max = -float("inf")
for split in range(NUM_SPLITS):
stat = ((bh * NUM_SPLITS + split) * G + group_head) * 2
global_max = tl.maximum(global_max, tl.load(partial_stats + stat))
denom = 0.0
numerator = tl.zeros((D,), tl.float32)
for split in range(NUM_SPLITS):
stat = ((bh * NUM_SPLITS + split) * G + group_head) * 2
part_max = tl.load(partial_stats + stat)
part_sum = tl.load(partial_stats + stat + 1)
weight = tl.exp2((part_max - global_max) * 1.4426950408889634)
denom += part_sum * weight
part_base = ((bh * NUM_SPLITS + split) * G + group_head) * D
numerator += tl.load(partial_out + part_base + offs_d) * weight
tl.store(out + bhq * D + offs_d, numerator / denom)
@triton.jit
def _paged_attention_all_kv_small(
query,
kv_cache,
block_table,
seq_lens,
out,
partial_out,
partial_stats,
H: tl.constexpr,
HKV: tl.constexpr,
D: tl.constexpr,
G: tl.constexpr,
PAGE: tl.constexpr,
MAX_PAGES: tl.constexpr,
MAX_SEQ: tl.constexpr,
NUM_SPLITS: tl.constexpr,
BLOCK_N: tl.constexpr,
LOOP_STAGES: tl.constexpr,
):
"""D=64 path packing all independent KV heads into one MMA tile.
K/V columns from the four KV heads are concatenated. The score matrix is
block-diagonally masked so each group of four query rows only observes its
own KV head. This fills the native 16-row MMA tile without redundant KV
reads or padded query rows.
"""
pid = tl.program_id(0)
split = pid % NUM_SPLITS
batch = pid // NUM_SPLITS
offs_m = tl.arange(0, H)
offs_d = tl.arange(0, D)
q = tl.load(query + (batch * H + offs_m[:, None]) * D + offs_d[None, :])
PART = tl.cdiv(MAX_SEQ, NUM_SPLITS)
start = split * PART
seq_len = tl.load(seq_lens + batch)
row_max = tl.full((H,), -float("inf"), tl.float32)
row_sum = tl.zeros((H,), tl.float32)
acc = tl.zeros((H, D), tl.float32)
q_kv_head = offs_m // G
for block_off in tl.range(0, PART, BLOCK_N, num_stages=LOOP_STAGES):
packed_n = tl.arange(0, HKV * BLOCK_N)
col_kv_head = packed_n // BLOCK_N
local_n = tl.arange(0, BLOCK_N)
token = start + block_off + local_n
valid_local = (token < seq_len) & (token < start + PART)
physical_local = tl.load(
block_table + batch * MAX_PAGES + token // PAGE,
mask=valid_local,
other=0,
)
physical_page = tl.reshape(
tl.broadcast_to(physical_local[None, :], (HKV, BLOCK_N)),
(HKV * BLOCK_N,),
)
page_slot = tl.reshape(
tl.broadcast_to((token % PAGE)[None, :], (HKV, BLOCK_N)),
(HKV * BLOCK_N,),
)
valid_n = tl.reshape(
tl.broadcast_to(valid_local[None, :], (HKV, BLOCK_N)),
(HKV * BLOCK_N,),
)
token_base = (
physical_page * (PAGE * HKV * 2 * D)
+ page_slot * (HKV * 2 * D)
+ col_kv_head * (2 * D)
)
ptrs = kv_cache + token_base[:, None] + offs_d[None, :]
k = tl.load(ptrs, mask=valid_n[:, None], other=0.0, cache_modifier=".cg")
v = tl.load(ptrs + D, mask=valid_n[:, None], other=0.0, cache_modifier=".cg")
scores = tl.dot(q, tl.trans(k)) * (1.0 / math.sqrt(D))
own_head = q_kv_head[:, None] == col_kv_head[None, :]
active = own_head & valid_n[None, :]
scores = tl.where(active, scores, -float("inf"))
block_max = tl.max(scores, axis=1)
next_max = tl.maximum(row_max, block_max)
alpha = tl.where(
next_max == -float("inf"),
0.0,
tl.exp2((row_max - next_max) * 1.4426950408889634),
)
prob = tl.where(
active,
tl.exp2((scores - next_max[:, None]) * 1.4426950408889634),
0.0,
)
acc = acc * alpha[:, None] + tl.dot(prob.to(tl.bfloat16), v)
row_sum = row_sum * alpha + tl.sum(prob, axis=1)
row_max = next_max
if NUM_SPLITS == 1:
tl.store(
out + (batch * H + offs_m[:, None]) * D + offs_d[None, :],
acc / row_sum[:, None],
)
else:
kv_head = offs_m // G
group_head = offs_m % G
po_ptrs = partial_out + (
(((batch * HKV + kv_head[:, None]) * NUM_SPLITS + split) * G
+ group_head[:, None]) * D
+ offs_d[None, :]
)
tl.store(po_ptrs, acc)
ps_ptrs = partial_stats + (
((batch * HKV + kv_head) * NUM_SPLITS + split) * G + group_head
) * 2
tl.store(ps_ptrs, row_max)
tl.store(ps_ptrs + 1, row_sum)
class Model(nn.Module):
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
assert head_dim in (64, 128)
assert page_size == 16
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.max_pages = (seq_len + page_size - 1) // page_size
# Enough independent CTAs to fill H100's 114 SMs, without over-splitting
# the already-wide server-batch shapes.
base_programs = batch * num_kv_heads
if self.group_size == 8:
self.num_splits = 8
elif base_programs >= 192:
self.num_splits = 1
elif base_programs <= 32:
self.num_splits = 8
elif base_programs >= 128:
self.num_splits = 1
else:
self.num_splits = 1
self.block_m = triton.next_power_of_2(self.group_size)
self.block_n = 64
self.num_warps = 8 if self.group_size == 8 else 4
self.num_stages = 3 if self.group_size == 8 else 2
self.loop_stages = 4
self.maxnreg = 80 if self.group_size == 8 else None
self.reduce_warps = 4
if seq_len <= 1024:
self.block_n = 128
self.num_warps = 8
self.num_stages = 3
self.loop_stages = 5
self.maxnreg = 128
elif seq_len < 1800:
self.block_n = 128
if head_dim == 64:
self.block_n = 64
self.loop_stages = 3
self.pack_all_small_kv = head_dim == 64
if self.pack_all_small_kv:
self.num_splits = 16
self.block_n = 32
self.num_warps = 4
self.num_stages = 2
self.loop_stages = 5
self.maxnreg = None
if self.group_size == 8:
self.num_warps = 4
self.loop_stages = 2
self.register_buffer("_dummy", torch.zeros(1, dtype=torch.bfloat16), persistent=False)
def forward(self, query, kv_cache, block_table, seq_lens):
out = torch.empty_like(query)
if self.num_splits == 1:
# Dummy tensors are compile-time-dead in this specialization.
partial_out = out
partial_stats = out
else:
partial_out = torch.empty(
(self.batch, self.num_kv_heads, self.num_splits, self.group_size, self.head_dim),
device=query.device,
dtype=torch.bfloat16,
)
partial_stats = torch.empty(
(self.batch, self.num_kv_heads, self.num_splits, self.group_size, 2),
device=query.device,
dtype=torch.float32,
)
if self.pack_all_small_kv:
grid = (self.batch * self.num_splits,)
kernel = _paged_attention_all_kv_small
else:
grid = (self.batch * self.num_kv_heads * self.num_splits,)
kernel = _paged_attention
launch_kwargs = dict(
H=self.num_heads,
HKV=self.num_kv_heads,
D=self.head_dim,
G=self.group_size,
PAGE=self.page_size,
MAX_PAGES=self.max_pages,
MAX_SEQ=self.seq_len,
NUM_SPLITS=self.num_splits,
BLOCK_N=self.block_n,
LOOP_STAGES=self.loop_stages,
num_warps=self.num_warps,
num_stages=self.num_stages,
maxnreg=self.maxnreg,
)
if not self.pack_all_small_kv:
launch_kwargs["BLOCK_M"] = self.block_m
kernel[grid](
query,
kv_cache,
block_table,
seq_lens,
out,
partial_out,
partial_stats,
**launch_kwargs,
)
if self.num_splits > 1:
_reduce_splits_by_head[(self.batch * self.num_heads,)](
partial_out,
partial_stats,
out,
H=self.num_heads,
HKV=self.num_kv_heads,
D=self.head_dim,
G=self.group_size,
NUM_SPLITS=self.num_splits,
num_warps=self.reduce_warps,
num_stages=1,
)
return out
# Match reference.py's construction interface.
BATCH = 8
NUM_HEADS = 32
NUM_KV_HEADS = 8
HEAD_DIM = 128
SEQ_LEN = 1024
PAGE_SIZE = 16
def get_inputs():
pages_per_seq = (SEQ_LEN + PAGE_SIZE - 1) // PAGE_SIZE
total_pages = max(BATCH * pages_per_seq + 8, 64)
query = torch.randn(BATCH, NUM_HEADS, HEAD_DIM, dtype=torch.bfloat16) * 0.1
kv_cache = torch.randn(
total_pages,
PAGE_SIZE,
NUM_KV_HEADS,
2 * HEAD_DIM,
dtype=torch.bfloat16,
) * 0.1
block_table = torch.randperm(total_pages)[: BATCH * pages_per_seq]
block_table = block_table.reshape(BATCH, pages_per_seq).int().contiguous()
seq_lens = torch.full((BATCH,), SEQ_LEN, 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]
20260721_142512_codex_gpt-5.6-sol_03_paged_attention