Files
project_6/paged_attention_v2_triton.py
Claude cd0d9e1a91 [OPT] Fix online softmax bug in Triton V2 Phase 1
Bug: if l_i > 0 branch in Triton is invalid (compiled as constexpr).
Also: p = exp(scores - m_i_new) computed after m_i_new update was
using the wrong reference max (should subtract m_ij first, then rescale).

Fix: Adapted exactly from prefix_prefill.py's proven-correct pattern:
  p = exp(scores - m_ij)          # probs relative to chunk max
  l_ij = sum(p)                   # chunk sum
  m_i_new = max(m_i, m_ij)       # new running max
  alpha = exp(m_i - m_i_new)     # old accumulator rescale
  beta = exp(m_ij - m_i_new)     # new chunk rescale
  l_i_new = alpha*l_i + beta*l_ij
  acc = acc*(alpha*l_i/l_i_new) + (p*beta/l_i_new) @ V

This is the Flash Attention online softmax tiling algorithm.
Same math as CCCL's parallel_reduce with compound accumulators.
2026-07-30 16:16:56 +00:00

334 lines
13 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 — Triton PagedAttention V2 for BI-V100
=====================================================================
Two-kernel V2 implementation using Triton:
Phase 1: _paged_attn_v2_partition — per-partition attention (paged K/V gather)
Phase 2: _paged_attn_v2_reduce — cross-partition log-sum-exp reduction
The K/V gather pattern is adapted from prefix_prefill.py (lines 100-170):
bn = tl.load(block_tables + seq * stride + (token // block_size) * stride)
off_k = bn * stride_kc_b + kv_head * stride_kc_h + (d // x) * stride_kc_dx + ...
k = tl.load(key_cache + off_k, mask=...)
For decode (BLOCK_M=1), the Q tile is just one vector [HEAD_DIM].
The inner loop iterates over BLOCK_N KV tokens per step.
Online softmax accumulates (max, sum, weighted_V) across steps.
After all steps in a partition, we have:
max_logits[seq, head, part]: running max
exp_sums[seq, head, part]: running exp sum
tmp_output[seq, head, part, :]: unnormalized weighted V
Phase 2 combines partitions using the CCCL summary_statistics pattern:
global_max = max(part_maxes)
rescaled_sum = sum(exp(part_max - global_max) * part_sum)
output = sum(weight[p] * part_output[p])
SMEM analysis:
Phase 1: K tile [BLOCK_N, HEAD_DIM] loaded via gather (no explicit SMEM tile)
Triton manages register allocation for tl.load + tl.dot
At BLOCK_N=32, HEAD_DIM=256: 32×256 fp16 values in registers = 16KB
Phase 2: No SMEM needed (partitions ≈ 200, all in registers)
"""
import torch
import triton
import triton.language as tl
from typing import Optional
@triton.jit
def _paged_attn_v2_partition_kernel(
# Outputs
tmp_output_ptr, # [num_seqs, num_heads, max_num_parts, head_size]
exp_sums_ptr, # [num_seqs, num_heads, max_num_parts]
max_logits_ptr, # [num_seqs, num_heads, max_num_parts]
# Inputs
query_ptr, # [num_seqs, num_heads, head_size]
key_cache_ptr, # [num_blocks, num_kv_heads, head_size/x, block_size, x]
value_cache_ptr, # [num_blocks, num_kv_heads, head_size, block_size]
block_tables_ptr, # [num_seqs, max_blocks_per_seq]
seq_lens_ptr, # [num_seqs]
# Scalars
scale: tl.float32,
num_queries_per_kv: tl.int32,
block_size: tl.int32,
x_pack: tl.int32, # key_cache packing factor: 16 // sizeof(dtype)
# 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,
# Compile-time constants
PARTITION_SIZE: tl.constexpr,
HEAD_DIM: tl.constexpr,
BLOCK_N: tl.constexpr,
):
"""Phase 1: Per-partition paged attention for decode (BLOCK_M=1).
Grid: (num_seqs, num_heads, max_num_partitions)
Each program instance processes one (seq, head, partition) triple.
Adapted from prefix_prefill.py's paged K/V gather pattern.
Key difference: BLOCK_M=1 (decode has 1 query token per head).
"""
seq_idx = tl.program_id(0)
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 sentinel values
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
# GQA: map query head → KV head
kv_head_idx = head_idx // num_queries_per_kv
# Load query vector: [HEAD_DIM]
offs_d = tl.arange(0, HEAD_DIM)
q = tl.load(query_ptr + seq_idx * stride_qs + head_idx * stride_qh + offs_d * stride_qd).to(tl.float32)
# Online softmax state
m_i = float('-inf') # running max
l_i = 0.0 # running exp sum
acc = tl.zeros([HEAD_DIM], dtype=tl.float32) # weighted V accumulator
# KV token offsets within each BLOCK_N chunk
offs_n = tl.arange(0, BLOCK_N)
# Iterate over BLOCK_N KV tokens at a time
for start_n in range(part_start, part_end, BLOCK_N):
# Token positions in the sequence
token_ids = start_n + offs_n
valid_mask = token_ids < part_end
# === Paged K gather (from prefix_prefill.py pattern) ===
# Look up physical block numbers from block_tables
block_indices = token_ids // block_size
within_block = token_ids % block_size
# bn: physical block ids [BLOCK_N]
bn = tl.load(
block_tables_ptr + seq_idx * stride_bt_s + block_indices * stride_bt_b,
mask=valid_mask, other=0)
# K offsets: key_cache[bn, kv_head, d//x, within_block, d%x]
# Layout: [num_blocks, num_kv_heads, head_size/x, block_size, x]
# off_k: [HEAD_DIM, BLOCK_N] — each column is one token's K vector
off_k = (bn[None, :] * stride_kc_b +
kv_head_idx * stride_kc_h +
(offs_d[:, None] // x_pack) * stride_kc_dx +
within_block[None, :] * stride_kc_bs +
(offs_d[:, None] % x_pack) * stride_kc_x)
k = tl.load(key_cache_ptr + off_k, mask=valid_mask[None, :], other=0.0) # [D, N]
# Scores: q @ k = [1, D] @ [D, N] → [N]
# For BLOCK_M=1: this is a dot product per KV token
scores = tl.sum(q[:, None] * k, axis=0) * scale # [BLOCK_N]
scores = tl.where(valid_mask, scores, float('-inf'))
# Online softmax (adapted from prefix_prefill.py — proven correct)
m_ij = tl.max(scores, axis=0) # scalar: max of this chunk
p = tl.exp(scores - m_ij) # [BLOCK_N] — unnormalized probs
l_ij = tl.sum(p, axis=0) # scalar: sum of exp for this chunk
m_i_new = tl.maximum(m_i, m_ij)
alpha = tl.exp(m_i - m_i_new) # rescale factor for old accumulator
beta = tl.exp(m_ij - m_i_new) # rescale factor for new chunk
l_i_new = alpha * l_i + beta * l_ij
# === Paged V gather ===
off_v = (bn[:, None] * stride_vc_b +
kv_head_idx * stride_vc_h +
offs_d[None, :] * stride_vc_d +
within_block[:, None] * stride_vc_bs)
v = tl.load(value_cache_ptr + off_v, mask=valid_mask[:, None], other=0.0) # [N, D]
# Update accumulator (Flash Attention online softmax pattern):
# acc = acc * (alpha * l_i / l_i_new) + (p * beta / l_i_new) @ V
# Safe division: if l_i_new == 0, this is the first chunk
acc_scale = alpha * l_i / tl.maximum(l_i_new, 1e-6)
acc = acc * acc_scale
p_scale = beta / tl.maximum(l_i_new, 1e-6)
p_scaled = p * p_scale # [BLOCK_N]
acc += tl.sum(p_scaled[:, None] * v, axis=0) # [HEAD_DIM]
l_i = l_i_new
m_i = m_i_new
# Store partition results
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)
# Store accumulated output: [HEAD_DIM]
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 _paged_attn_v2_reduce_kernel(
# Output
output_ptr, # [num_seqs, num_heads, head_size]
# Inputs
tmp_output_ptr, # [num_seqs, num_heads, max_num_parts, head_size]
exp_sums_ptr, # [num_seqs, num_heads, max_num_parts]
max_logits_ptr, # [num_seqs, num_heads, max_num_parts]
seq_lens_ptr, # [num_seqs]
# Scalars
max_num_parts: tl.int32,
# Strides
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,
# Constants
PARTITION_SIZE: tl.constexpr,
HEAD_DIM: tl.constexpr,
MAX_NUM_PARTS: tl.constexpr,
):
"""Phase 2: Cross-partition log-sum-exp reduction.
Grid: (num_seqs, num_heads)
Combines partition results using CCCL summary_statistics pattern.
"""
seq_idx = tl.program_id(0)
head_idx = tl.program_id(1)
seq_len = tl.load(seq_lens_ptr + seq_idx)
num_parts = (seq_len + PARTITION_SIZE - 1) // PARTITION_SIZE
# Load partition statistics
part_offsets = tl.arange(0, MAX_NUM_PARTS)
valid_mask = part_offsets < num_parts
es_base = seq_idx * stride_es_s + head_idx * stride_es_h
part_max = tl.load(max_logits_ptr + es_base + part_offsets * stride_es_p,
mask=valid_mask, other=float('-inf'))
part_sum = tl.load(exp_sums_ptr + es_base + part_offsets * stride_es_p,
mask=valid_mask, other=0.0)
# Global max
global_max = tl.max(part_max, axis=0)
# Rescale and normalize
rescale = tl.exp(part_max - global_max) * part_sum
total = tl.sum(rescale, axis=0)
weights = rescale / total # [MAX_NUM_PARTS]
# Weighted sum of partition outputs
offs_d = tl.arange(0, HEAD_DIM)
acc = tl.zeros([HEAD_DIM], dtype=tl.float32)
for p in range(MAX_NUM_PARTS):
if p < num_parts:
w = tl.load(max_logits_ptr + es_base + p * stride_es_p) # reload for weight
w_rescaled = tl.exp(w - global_max) * tl.load(exp_sums_ptr + es_base + p * stride_es_p) / total
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_rescaled * 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 Triton V2 kernels."""
num_seqs, num_heads, head_size = query.shape
num_queries_per_kv = num_heads // num_kv_heads
max_num_parts = tmp_output.shape[2]
x_pack = key_cache.shape[-1] # packing factor
PARTITION_SIZE = 512
# BLOCK_N: must fit in SMEM. For decode (BLOCK_M=1), SMEM is dominated by K/V gather.
# head_dim=256: BLOCK_N=32 → 32×256×2 = 16KB per tile (K or V)
# head_dim=128: BLOCK_N=64 → 64×128×2 = 16KB per tile
BLOCK_N = 32 if head_size > 128 else 64
# Phase 1: partition attention
num_partitions = (max_seq_len + PARTITION_SIZE - 1) // PARTITION_SIZE
grid_phase1 = (num_seqs, num_heads, num_partitions)
_paged_attn_v2_partition_kernel[grid_phase1](
tmp_output, exp_sums, max_logits,
query, key_cache, value_cache, block_tables, seq_lens,
scale, num_queries_per_kv, block_size, x_pack,
# query strides
query.stride(0), query.stride(1), query.stride(2),
# key_cache strides
key_cache.stride(0), key_cache.stride(1), key_cache.stride(2),
key_cache.stride(3), key_cache.stride(4),
# value_cache strides
value_cache.stride(0), value_cache.stride(1), value_cache.stride(2),
value_cache.stride(3),
# block_tables strides
block_tables.stride(0), block_tables.stride(1),
# tmp_output strides
tmp_output.stride(0), tmp_output.stride(1), tmp_output.stride(2), tmp_output.stride(3),
# exp_sums strides
exp_sums.stride(0), exp_sums.stride(1), exp_sums.stride(2),
# Constants
PARTITION_SIZE=PARTITION_SIZE,
HEAD_DIM=head_size,
BLOCK_N=BLOCK_N,
)
# Phase 2: cross-partition reduction
MAX_NUM_PARTS_CONST = triton.next_power_of_2(max_num_parts)
if MAX_NUM_PARTS_CONST > 1024:
MAX_NUM_PARTS_CONST = 1024
grid_phase2 = (num_seqs, num_heads)
_paged_attn_v2_reduce_kernel[grid_phase2](
output,
tmp_output, exp_sums, max_logits, seq_lens,
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,
)