KernelBench hard · H100
Paged Attention LongCat 2.0
24.2%geomean peak fraction across shapes
manually audited: clean
harnesslongcat-claudeagent session3h 34mtotal wall3h 35mcheck29sbenchmark32soutput tokens327,942cost$44.22gpu-lock wait31sgpu-lock held30sregimememory
Per-shape vs governing ceilingeach shape graded against whichever binds — bf16 compute or HBM bandwidth
8×32×8×128×1024×160.094 ms17.6%0.36 TB/s · 18% of 2.0 TB/s HBM · also 1 TFLOPS (0% of compute)
32×32×8×128×2048×160.226 ms58.4%1.19 TB/s · 58% of 2.0 TB/s HBM · also 5 TFLOPS (1% of compute)
4×64×8×128×4096×160.128 ms25.8%0.53 TB/s · 26% of 2.0 TB/s HBM · also 4 TFLOPS (1% of compute)
16×32×8×128×1535×160.141 ms35.2%0.72 TB/s · 35% of 2.0 TB/s HBM · also 3 TFLOPS (0% of compute)
8×16×4×64×2000×160.091 ms8.9%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.6% · 58.4% · 25.8% · 35.2% · 8.9%) = 24.2%
Kernel source (redacted)
"""Triton paged-attention decode kernel for SM90 Hopper (H100 PCIe).
Single-query decode with GQA, memory-bound. Two kernels, launched as a group:
phase1 grid = (batch, num_kv_heads, K_SPLIT)
Each block owns one kv_head and the GROUP_SIZE = num_heads // num_kv_heads
query heads that share it, and processes a *contiguous chunk* of the paged
KV sequence. KV for the kv_head is streamed page-by-page and reused across
the whole group, so every KV byte is loaded exactly once. Online softmax
(FlashAttention-style running m/l) folds the softmax into the stream, and a
per-token mask handles the predicated tail page. The partial (m, l, acc)
state is written to a workspace.
phase2 grid = (batch, num_kv_heads)
Reduces the K_SPLIT partial states into the final output with the standard
online-softmax merge.
Splitting the sequence (instead of launching one block that loops over all pages
serially) raises the thread-block count from batch*num_kv_heads to
batch*num_kv_heads*K_SPLIT, which is what saturates the 132 SMs of the H100 for
the small-batch shapes. K_SPLIT is chosen per-shape to land the grid in the
~256-512 block range without making the reduction expensive.
The KV cache packs [K | V] on its last dim; each page loads K and V as separate
(page_size, head_dim) tiles with a constant offset.
"""
import math
import torch
import torch.nn as nn
import triton
import triton.language as tl
# --- Shape knobs (overridden by check.py / benchmark.py from shapes.py) ----
BATCH = 8
NUM_HEADS = 32
NUM_KV_HEADS = 8
HEAD_DIM = 128
SEQ_LEN = 1024
PAGE_SIZE = 16
@triton.jit
def _phase1_kernel(
q_ptr, kv_ptr, block_table_ptr, seq_lens_ptr,
wm_ptr, wl_ptr, wa_ptr, # workspace: m, l, acc
stride_q_b, stride_q_h, stride_q_d,
stride_kv_b, stride_kv_p, stride_kv_h, stride_kv_d,
stride_bt_b, stride_bt_m,
GROUP_SIZE: tl.constexpr,
HEAD_DIM: tl.constexpr,
PAGE_SIZE: tl.constexpr,
MAX_BLOCKS: tl.constexpr,
NUM_KV_HEADS: tl.constexpr,
K_SPLIT: tl.constexpr,
scale,
num_warps: tl.constexpr = 4,
):
pid_b = tl.program_id(0)
pid_kv = tl.program_id(1)
pid_s = tl.program_id(2)
seq_len = tl.load(seq_lens_ptr + pid_b)
num_pages = (seq_len + PAGE_SIZE - 1) // PAGE_SIZE
pages_per_split = (num_pages + K_SPLIT - 1) // K_SPLIT
page_start = pid_s * pages_per_split
page_end = tl.minimum(page_start + pages_per_split, num_pages)
q_head_start = pid_kv * GROUP_SIZE
# Load the group's queries: (G, D), contiguous along the head dim.
offs_g = tl.arange(0, GROUP_SIZE)[:, None] # (G, 1)
offs_d = tl.arange(0, HEAD_DIM)[None, :] # (1, D)
q_mask = (q_head_start + offs_g) < (pid_kv * GROUP_SIZE + GROUP_SIZE)
q = tl.load(
q_ptr + pid_b * stride_q_b + (q_head_start + offs_g) * stride_q_h + offs_d,
mask=q_mask,
other=0.0,
).to(tl.float32) # (G, D)
# Running online-softmax state, per query head in the group.
m = tl.full((GROUP_SIZE,), -1e30, dtype=tl.float32)
l = tl.zeros((GROUP_SIZE,), dtype=tl.float32)
acc = tl.zeros((GROUP_SIZE, HEAD_DIM), dtype=tl.float32)
offs_p = tl.arange(0, PAGE_SIZE)[:, None] # (P, 1)
offs_dp = tl.arange(0, HEAD_DIM)[None, :] # (1, D)
offs_p1d = tl.arange(0, PAGE_SIZE) # (P,)
for page_idx in range(page_start, page_end):
valid_page = page_idx < num_pages
page = tl.load(block_table_ptr + pid_b * stride_bt_b + page_idx * stride_bt_m)
page = tl.where(valid_page, page, 0)
base = page * stride_kv_b + pid_kv * stride_kv_h
k = tl.load(
kv_ptr + base + offs_p * stride_kv_p + offs_dp,
mask=valid_page,
other=0.0,
).to(tl.float32) # (P, D)
v = tl.load(
kv_ptr + base + offs_p * stride_kv_p + offs_dp + HEAD_DIM,
mask=valid_page,
other=0.0,
).to(tl.float32) # (P, D)
scores = tl.dot(q, k.T) * scale # (G, P)
token = page_idx * PAGE_SIZE + offs_p1d # (P,)
mask = token < seq_len
scores = tl.where(mask, scores, -1e30)
new_m = tl.maximum(m, tl.max(scores, axis=1)) # (G,)
exp_scores = tl.exp(scores - new_m[:, None]) # (G, P)
exp_scores = tl.where(mask, exp_scores, 0.0)
new_l = l * tl.exp(m - new_m) + tl.sum(exp_scores, axis=1) # (G,)
acc = acc * tl.exp(m - new_m)[:, None] + tl.dot(exp_scores, v) # (G, D)
m = new_m
l = new_l
# Write partial state to workspace. Layout: (B, Hkv, K_SPLIT, G) and
# (B, Hkv, K_SPLIT, G, D), flattened.
split_idx = (pid_b * NUM_KV_HEADS + pid_kv) * K_SPLIT + pid_s
base_m = split_idx * GROUP_SIZE
offs_g1d = tl.arange(0, GROUP_SIZE)
gmask = offs_g1d < GROUP_SIZE
tl.store(wm_ptr + base_m + offs_g1d, m, mask=gmask)
tl.store(wl_ptr + base_m + offs_g1d, l, mask=gmask)
base_a = split_idx * (GROUP_SIZE * HEAD_DIM)
offs_gd = tl.arange(0, GROUP_SIZE * HEAD_DIM)
tl.store(wa_ptr + base_a + offs_gd, acc.reshape(GROUP_SIZE * HEAD_DIM),
mask=offs_gd < GROUP_SIZE * HEAD_DIM)
@triton.jit
def _phase2_kernel(
wm_ptr, wl_ptr, wa_ptr, out_ptr,
stride_o_b, stride_o_h, stride_o_d,
GROUP_SIZE: tl.constexpr,
HEAD_DIM: tl.constexpr,
NUM_KV_HEADS: tl.constexpr,
K_SPLIT: tl.constexpr,
num_warps: tl.constexpr = 4,
):
pid_b = tl.program_id(0)
pid_kv = tl.program_id(1)
q_head_start = pid_kv * GROUP_SIZE
offs_g = tl.arange(0, GROUP_SIZE)[:, None] # (G, 1)
offs_d = tl.arange(0, HEAD_DIM)[None, :] # (1, D)
q_mask = (q_head_start + offs_g) < (pid_kv * GROUP_SIZE + GROUP_SIZE)
# Merge the K_SPLIT partial (m, l, acc) states with online-softmax reduction.
m = tl.full((GROUP_SIZE,), -1e30, dtype=tl.float32)
l = tl.zeros((GROUP_SIZE,), dtype=tl.float32)
acc = tl.zeros((GROUP_SIZE, HEAD_DIM), dtype=tl.float32)
offs_g1d = tl.arange(0, GROUP_SIZE)
offs_gd = tl.arange(0, GROUP_SIZE * HEAD_DIM)
gmask = offs_g1d < GROUP_SIZE
gdmask = offs_gd < GROUP_SIZE * HEAD_DIM
for s in range(K_SPLIT):
split_idx = (pid_b * NUM_KV_HEADS + pid_kv) * K_SPLIT + s
base_m = split_idx * GROUP_SIZE
ms = tl.load(wm_ptr + base_m + offs_g1d, mask=gmask, other=-1e30)
ls = tl.load(wl_ptr + base_m + offs_g1d, mask=gmask, other=0.0)
base_a = split_idx * (GROUP_SIZE * HEAD_DIM)
accs = tl.load(wa_ptr + base_a + offs_gd, mask=gdmask, other=0.0).reshape(GROUP_SIZE, HEAD_DIM)
new_m = tl.maximum(m, ms)
acc = acc * tl.exp(m - new_m)[:, None] + accs * tl.exp(ms - new_m)[:, None]
l = l * tl.exp(m - new_m) + ls * tl.exp(ms - new_m)
m = new_m
out = (acc / l[:, None]).to(tl.bfloat16)
tl.store(
out_ptr + pid_b * stride_o_b + (q_head_start + offs_g) * stride_o_h + offs_d,
out,
mask=q_mask,
)
class Model(nn.Module):
"""Single-query paged attention decode (Triton split-K kernel)."""
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 = 1.0 / 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
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)
max_blocks = block_table.shape[1]
# Choose K_SPLIT to land the phase1 grid in the ~256-512 block range so the
# GPU stays full for small batches, without oversplitting large ones.
base = B * Hkv
num_pages = (int(seq_lens.max().item()) + P - 1) // P
# Target a grid large enough to saturate the 132 SMs of the H100.
# The optimum scales with how register-heavy each block is (G*D), so a
# single desired-count heuristic lands near the per-shape optimum across
# the whole sweep; the rest of the shapes sit on a flat plateau.
desired = 640
k_split = max(1, min(num_pages, (desired + base - 1) // base))
# Keep the reduction cheap.
k_split = min(k_split, 20)
wm = torch.empty(B, Hkv, k_split, G, dtype=torch.float32, device=query.device)
wl = torch.empty(B, Hkv, k_split, G, dtype=torch.float32, device=query.device)
wa = torch.empty(B, Hkv, k_split, G, D, dtype=torch.float32, device=query.device)
grid1 = (B, Hkv, k_split)
_phase1_kernel[grid1](
query, kv_cache, block_table, seq_lens,
wm, wl, wa,
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),
GROUP_SIZE=G, HEAD_DIM=D, PAGE_SIZE=P, MAX_BLOCKS=max_blocks,
NUM_KV_HEADS=Hkv, K_SPLIT=k_split, scale=self.scale,
)
grid2 = (B, Hkv)
_phase2_kernel[grid2](
wm, wl, wa, out,
out.stride(0), out.stride(1), out.stride(2),
GROUP_SIZE=G, HEAD_DIM=D, NUM_KV_HEADS=Hkv, K_SPLIT=k_split,
)
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]
20260707_075525_longcat-claude_LongCat-2.0_03_paged_attention