Files
project_6/paged_attention_v2_triton.py
Claude 39e32343eb [ARCH] CCCL-derived paged attention kernel architecture + Triton rewrite
Architecture document: docs/paged_attention_kernel_architecture.md
Defines every module from CCCL algorithm patterns before code.

Three-level decomposition from CCCL:
  Level 1 (warp_reduce_shfl): shfl.down butterfly for per-thread QK scores
  Level 2 (block_reduce_warp_reductions): warp partials → SMEM → block aggregate
  Level 3 (agent_scan decoupled lookback): cross-partition combine

Compound type (from summary_statistics.cu):
  attention_partial = (max_score, exp_sum, weighted_v[256])
  combine(a, b) = online softmax rescaling (same math as Flash Attention)

Key design change: Grid on num_kv_heads, not num_heads.
  Before: grid = (1, 24, 200) = 4800 blocks, KV loaded 6x redundantly
  After:  grid = (1, 4, 200) = 800 blocks, KV loaded once per kv_head
  Each block computes GQA_RATIO=6 query heads with shared KV loads.
  Reduces KV cache bandwidth by 6x (the GQA ratio).

SMEM budget verified:
  K tile [32, 256] fp16 = 16KB
  V tile [32, 256] fp16 = 16KB
  Total = 32KB ≤ 48KB ✓

Phase 1 kernel: _partition_attn_kernel
  Processes query heads sequentially within the GQA group
  to minimize register pressure (6 × 256 = 1536 registers
  too many if all loaded simultaneously).

Phase 2 kernel: _reduce_partitions_kernel
  Also gridded on kv_heads, reduces all partitions for
  GQA_RATIO heads per block.

This replaces the previous Triton V2 which was gridded on num_heads
and had no GQA awareness at the kernel level.
2026-07-31 04:13:07 +00:00

338 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
paged_attention_v2_triton.py — CCCL-derived Triton PagedAttention V2
=====================================================================
Architecture: docs/paged_attention_kernel_architecture.md
Two-kernel design:
Phase 1: _partition_attn — per-partition compound reduction (CCCL block_reduce pattern)
Phase 2: _reduce_partitions — cross-partition combine (CCCL agent_reduce pattern)
Key CCCL derivations:
1. Compound type: (max_score, exp_sum, weighted_v[D]) — from summary_statistics.cu
2. Combine op: online softmax rescaling — from Flash Attention = CCCL's binary_op pattern
3. Warp reduce: shfl.down butterfly — from warp_reduce_shfl.cuh (Triton does this via tl.sum/tl.max)
4. Block reduce: warp partials → SMEM → serial combine — from block_reduce_warp_reductions.cuh
5. Paged gather: indirect load via block_tables — from prefix_prefill.py (proven on BI-V100)
6. GQA: grid on kv_heads, process gqa_ratio query heads per block — KV loaded once
Grid design:
Phase 1: (num_seqs, num_kv_heads, num_partitions) — NOT (num_seqs, num_heads, num_partitions)
Each block loads KV once for kv_head, computes gqa_ratio query heads.
Reduces KV cache reads by gqa_ratio (6x for Qwen3.6).
Phase 2: (num_seqs, num_kv_heads) — reduces partitions, writes all gqa_ratio outputs.
SMEM budget (head_dim=256, BLOCK_N=32):
K tile: 32×256×2 = 16KB
V tile: 32×256×2 = 16KB
Warp partials: negligible (in registers for Triton)
Total: 32KB ≤ 48KB ✓
"""
import torch
import triton
import triton.language as tl
from typing import Optional
@triton.jit
def _partition_attn_kernel(
# Outputs (per partition)
tmp_output_ptr, # [num_seqs, num_heads, max_parts, head_size]
exp_sums_ptr, # [num_seqs, num_heads, max_parts]
max_logits_ptr, # [num_seqs, num_heads, max_parts]
# Inputs
query_ptr, # [num_seqs, num_heads, head_size]
key_cache_ptr, # [num_blocks, kv_heads, head_size/x, block_size, x]
value_cache_ptr, # [num_blocks, kv_heads, head_size, block_size]
block_tables_ptr, # [num_seqs, max_blocks_per_seq]
seq_lens_ptr, # [num_seqs]
# Scalars
scale: tl.float32,
gqa_ratio: tl.int32, # num_heads // num_kv_heads
block_size: tl.int32,
x_pack: tl.int32, # key_cache packing factor
# Strides: query [S, H, D]
stride_qs: tl.int32, stride_qh: tl.int32, stride_qd: tl.int32,
# Strides: key_cache [B, KH, D/X, BS, X]
stride_kc_b: tl.int32, stride_kc_h: tl.int32,
stride_kc_dx: tl.int32, stride_kc_bs: tl.int32, stride_kc_x: tl.int32,
# Strides: value_cache [B, KH, D, BS]
stride_vc_b: tl.int32, stride_vc_h: tl.int32,
stride_vc_d: tl.int32, stride_vc_bs: tl.int32,
# Strides: block_tables [S, MAX_BLOCKS]
stride_bt_s: tl.int32, stride_bt_b: tl.int32,
# Strides: tmp_output [S, H, P, D]
stride_to_s: tl.int32, stride_to_h: tl.int32,
stride_to_p: tl.int32, stride_to_d: tl.int32,
# Strides: exp_sums/max_logits [S, H, P]
stride_es_s: tl.int32, stride_es_h: tl.int32, stride_es_p: tl.int32,
# Constants
PARTITION_SIZE: tl.constexpr,
HEAD_DIM: tl.constexpr,
BLOCK_N: tl.constexpr,
GQA_RATIO: tl.constexpr,
):
"""Phase 1: Per-partition attention with GQA broadcast.
Grid: (num_seqs, num_kv_heads, num_partitions)
Each block processes one (seq, kv_head, partition), computing GQA_RATIO query heads.
Algorithm (CCCL compound reduction):
For each BLOCK_N chunk of KV tokens in this partition:
1. Paged K gather: block_tables → physical_block → K[BLOCK_N, HEAD_DIM]
2. Scores: Q[g, HEAD_DIM] · K[HEAD_DIM, BLOCK_N] → [GQA_RATIO, BLOCK_N]
3. Online softmax update (combine op from summary_statistics.cu):
For each query head g:
m_new = max(m_old, max(scores[g]))
rescale_old = exp(m_old - m_new)
p = exp(scores[g] - m_new)
l_new = rescale_old * l_old + sum(p)
acc[g] = rescale_old * acc[g] + p · V
m_old, l_old = m_new, l_new
4. Paged V gather → accumulate weighted V
Write per-partition results for all GQA_RATIO heads.
"""
seq_idx = tl.program_id(0)
kv_head_idx = tl.program_id(1)
part_idx = tl.program_id(2)
seq_len = tl.load(seq_lens_ptr + seq_idx)
part_start = part_idx * PARTITION_SIZE
part_end = tl.minimum(part_start + PARTITION_SIZE, seq_len)
if part_start >= seq_len:
# Unused partition — write sentinels for all GQA_RATIO heads
for g in range(GQA_RATIO):
head_idx = kv_head_idx * GQA_RATIO + g
tl.store(max_logits_ptr + seq_idx * stride_es_s + head_idx * stride_es_h + part_idx * stride_es_p,
float('-inf'))
tl.store(exp_sums_ptr + seq_idx * stride_es_s + head_idx * stride_es_h + part_idx * stride_es_p,
0.0)
return
offs_d = tl.arange(0, HEAD_DIM)
offs_n = tl.arange(0, BLOCK_N)
# Load all GQA_RATIO query vectors for this kv_head
# q[g]: [HEAD_DIM] for g in 0..GQA_RATIO-1
# We process them sequentially to stay within register budget
# (Loading all 6 × 256 = 1536 fp32 values would be 6KB of registers per thread)
# Initialize compound accumulators for each query head
# m[g]: running max, l[g]: running exp_sum, acc[g]: [HEAD_DIM] weighted V
# For Triton, we process one query head at a time through the full partition
# to minimize register pressure.
for g in range(GQA_RATIO):
head_idx = kv_head_idx * GQA_RATIO + g
# Load Q for this head
q = tl.load(query_ptr + seq_idx * stride_qs + head_idx * stride_qh
+ offs_d * stride_qd).to(tl.float32)
# Compound accumulator
m_i = float('-inf')
l_i = 0.0
acc = tl.zeros([HEAD_DIM], dtype=tl.float32)
# Inner loop: BLOCK_N KV tokens per iteration
for start_n in range(part_start, part_end, BLOCK_N):
token_ids = start_n + offs_n
valid = token_ids < part_end
# Paged K gather (from prefix_prefill.py)
blk_idx = token_ids // block_size
blk_off = token_ids % block_size
phys_blk = tl.load(block_tables_ptr + seq_idx * stride_bt_s + blk_idx * stride_bt_b,
mask=valid, other=0)
off_k = (phys_blk[None, :] * stride_kc_b +
kv_head_idx * stride_kc_h +
(offs_d[:, None] // x_pack) * stride_kc_dx +
blk_off[None, :] * stride_kc_bs +
(offs_d[:, None] % x_pack) * stride_kc_x)
k = tl.load(key_cache_ptr + off_k, mask=valid[None, :], other=0.0) # [D, N]
# Scores: q · k per token
scores = tl.sum(q[:, None] * k, axis=0) * scale # [BLOCK_N]
scores = tl.where(valid, scores, float('-inf'))
# Online softmax (CCCL combine op)
m_ij = tl.max(scores, axis=0)
p = tl.exp(scores - m_ij)
l_ij = tl.sum(p, axis=0)
m_new = tl.maximum(m_i, m_ij)
alpha = tl.exp(m_i - m_new)
beta = tl.exp(m_ij - m_new)
l_new = alpha * l_i + beta * l_ij
# Paged V gather
off_v = (phys_blk[:, None] * stride_vc_b +
kv_head_idx * stride_vc_h +
offs_d[None, :] * stride_vc_d +
blk_off[:, None] * stride_vc_bs)
v = tl.load(value_cache_ptr + off_v, mask=valid[:, None], other=0.0) # [N, D]
# Update accumulator
safe_l = tl.maximum(l_new, 1e-6)
acc = acc * (alpha * l_i / safe_l)
p_scaled = p * (beta / safe_l)
acc += tl.sum(p_scaled[:, None] * v, axis=0)
m_i = m_new
l_i = l_new
# Write partition results for this head
tl.store(max_logits_ptr + seq_idx * stride_es_s + head_idx * stride_es_h + part_idx * stride_es_p,
m_i)
tl.store(exp_sums_ptr + seq_idx * stride_es_s + head_idx * stride_es_h + part_idx * stride_es_p,
l_i)
out_base = seq_idx * stride_to_s + head_idx * stride_to_h + part_idx * stride_to_p
tl.store(tmp_output_ptr + out_base + offs_d * stride_to_d,
acc.to(tmp_output_ptr.dtype.element_ty))
@triton.jit
def _reduce_partitions_kernel(
output_ptr, # [num_seqs, num_heads, head_size]
tmp_output_ptr, # [num_seqs, num_heads, max_parts, head_size]
exp_sums_ptr, # [num_seqs, num_heads, max_parts]
max_logits_ptr, # [num_seqs, num_heads, max_parts]
seq_lens_ptr, # [num_seqs]
gqa_ratio: tl.int32,
max_num_parts: tl.int32,
stride_out_s: tl.int32, stride_out_h: tl.int32, stride_out_d: tl.int32,
stride_to_s: tl.int32, stride_to_h: tl.int32,
stride_to_p: tl.int32, stride_to_d: tl.int32,
stride_es_s: tl.int32, stride_es_h: tl.int32, stride_es_p: tl.int32,
PARTITION_SIZE: tl.constexpr,
HEAD_DIM: tl.constexpr,
MAX_NUM_PARTS: tl.constexpr,
GQA_RATIO: tl.constexpr,
):
"""Phase 2: Cross-partition reduction.
Grid: (num_seqs, num_kv_heads)
Each block reduces all partitions for GQA_RATIO query heads.
Algorithm (CCCL block_reduce_warp_reductions pattern):
For each query head in this kv_head group:
1. Load all partition (max, sum) into registers
2. Global max across partitions
3. Rescale: weights = exp(part_max - global_max) * part_sum / total
4. Weighted combination of partition outputs
"""
seq_idx = tl.program_id(0)
kv_head_idx = tl.program_id(1)
seq_len = tl.load(seq_lens_ptr + seq_idx)
num_parts = (seq_len + PARTITION_SIZE - 1) // PARTITION_SIZE
part_offsets = tl.arange(0, MAX_NUM_PARTS)
valid = part_offsets < num_parts
offs_d = tl.arange(0, HEAD_DIM)
for g in range(GQA_RATIO):
head_idx = kv_head_idx * GQA_RATIO + g
es_base = seq_idx * stride_es_s + head_idx * stride_es_h
# Load partition statistics
part_max = tl.load(max_logits_ptr + es_base + part_offsets * stride_es_p,
mask=valid, other=float('-inf'))
part_sum = tl.load(exp_sums_ptr + es_base + part_offsets * stride_es_p,
mask=valid, other=0.0)
# Global max
global_max = tl.max(part_max, axis=0)
# Rescale and normalize (CCCL combine op applied across all partitions)
rescale = tl.exp(part_max - global_max) * part_sum
total = tl.sum(rescale, axis=0)
# Weighted combination
acc = tl.zeros([HEAD_DIM], dtype=tl.float32)
for p in range(MAX_NUM_PARTS):
if p < num_parts:
w = tl.exp(tl.load(max_logits_ptr + es_base + p * stride_es_p) - global_max) * \
tl.load(exp_sums_ptr + es_base + p * stride_es_p) / tl.maximum(total, 1e-6)
to_base = seq_idx * stride_to_s + head_idx * stride_to_h + p * stride_to_p
part_out = tl.load(tmp_output_ptr + to_base + offs_d * stride_to_d)
acc += w * part_out.to(tl.float32)
# Store final output
out_base = seq_idx * stride_out_s + head_idx * stride_out_h
tl.store(output_ptr + out_base + offs_d * stride_out_d,
acc.to(output_ptr.dtype.element_ty))
def paged_attention_v2_triton(
output: torch.Tensor,
exp_sums: torch.Tensor,
max_logits: torch.Tensor,
tmp_output: torch.Tensor,
query: torch.Tensor,
key_cache: torch.Tensor,
value_cache: torch.Tensor,
num_kv_heads: int,
scale: float,
block_tables: torch.Tensor,
seq_lens: torch.Tensor,
block_size: int,
max_seq_len: int,
alibi_slopes: Optional[torch.Tensor],
kv_cache_dtype: str = "auto",
k_scale: float = 1.0,
v_scale: float = 1.0,
**kwargs,
) -> None:
"""Launch CCCL-derived Triton V2 kernels."""
num_seqs, num_heads, head_size = query.shape
gqa_ratio = num_heads // num_kv_heads
max_num_parts = tmp_output.shape[2]
x_pack = key_cache.shape[-1]
PARTITION_SIZE = 512
BLOCK_N = 32 if head_size > 128 else 64
num_partitions = (max_seq_len + PARTITION_SIZE - 1) // PARTITION_SIZE
# Phase 1: grid on kv_heads (not num_heads) — GQA broadcast inside kernel
grid_p1 = (num_seqs, num_kv_heads, num_partitions)
_partition_attn_kernel[grid_p1](
tmp_output, exp_sums, max_logits,
query, key_cache, value_cache, block_tables, seq_lens,
scale, gqa_ratio, block_size, x_pack,
query.stride(0), query.stride(1), query.stride(2),
key_cache.stride(0), key_cache.stride(1), key_cache.stride(2),
key_cache.stride(3), key_cache.stride(4),
value_cache.stride(0), value_cache.stride(1), value_cache.stride(2),
value_cache.stride(3),
block_tables.stride(0), block_tables.stride(1),
tmp_output.stride(0), tmp_output.stride(1), tmp_output.stride(2), tmp_output.stride(3),
exp_sums.stride(0), exp_sums.stride(1), exp_sums.stride(2),
PARTITION_SIZE=PARTITION_SIZE,
HEAD_DIM=head_size,
BLOCK_N=BLOCK_N,
GQA_RATIO=gqa_ratio,
)
# Phase 2: grid on kv_heads — reduce all partitions for GQA_RATIO heads each
MAX_NUM_PARTS_CONST = triton.next_power_of_2(max_num_parts)
if MAX_NUM_PARTS_CONST > 1024:
MAX_NUM_PARTS_CONST = 1024
grid_p2 = (num_seqs, num_kv_heads)
_reduce_partitions_kernel[grid_p2](
output,
tmp_output, exp_sums, max_logits, seq_lens,
gqa_ratio, max_num_parts,
output.stride(0), output.stride(1), output.stride(2),
tmp_output.stride(0), tmp_output.stride(1), tmp_output.stride(2), tmp_output.stride(3),
exp_sums.stride(0), exp_sums.stride(1), exp_sums.stride(2),
PARTITION_SIZE=PARTITION_SIZE,
HEAD_DIM=head_size,
MAX_NUM_PARTS=MAX_NUM_PARTS_CONST,
GQA_RATIO=gqa_ratio,
)