[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.
This commit is contained in:
306
docs/paged_attention_kernel_architecture.md
Normal file
306
docs/paged_attention_kernel_architecture.md
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
# Paged Attention Kernel Architecture for BI-V100
|
||||||
|
|
||||||
|
## Derived from CCCL Algorithm Patterns
|
||||||
|
|
||||||
|
This document designs a complete paged attention kernel from first principles,
|
||||||
|
using CCCL's algorithm implementations as the algorithmic foundation.
|
||||||
|
Every module maps to a proven CCCL pattern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problem Definition
|
||||||
|
|
||||||
|
Paged attention computes, for each query token in a decode step:
|
||||||
|
|
||||||
|
output[h, d] = softmax(Q[h] · K[t]^T / √d) · V[t]
|
||||||
|
|
||||||
|
where K and V are stored in a **paged block table** (non-contiguous physical memory).
|
||||||
|
|
||||||
|
**Qwen3.6 parameters:**
|
||||||
|
- head_dim (d) = 256
|
||||||
|
- num_heads (H) = 24
|
||||||
|
- num_kv_heads (kv_h) = 4, GQA ratio = 6
|
||||||
|
- seq_len (T) = up to 100,000
|
||||||
|
- block_size = 16 tokens per physical block
|
||||||
|
- SMEM per block = 48KB
|
||||||
|
|
||||||
|
**The challenge:** K/V are scattered across physical blocks.
|
||||||
|
A naive implementation does 6,250 random memory accesses for 100K tokens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Algorithm Decomposition (Three Levels from CCCL)
|
||||||
|
|
||||||
|
### Level 1: Warp Reduce (from `warp_reduce_shfl.cuh`)
|
||||||
|
|
||||||
|
**CCCL pattern:** `shfl.sync.down` butterfly reduction in log2(32) = 5 steps.
|
||||||
|
Each step: `output = reduction_op(input, ShuffleDown(input, 1 << step))`.
|
||||||
|
|
||||||
|
**In attention:** Within one warp (32 threads), each thread holds QK^T scores
|
||||||
|
for a subset of KV tokens. Warp reduce computes:
|
||||||
|
- `max_score = warp_reduce(scores, max_op)` — for softmax numerical stability
|
||||||
|
- `exp_sum = warp_reduce(exp(scores - max_score), plus_op)` — softmax denominator
|
||||||
|
- `weighted_v = warp_reduce(exp(scores - max_score) * V[t], plus_op)` — numerator
|
||||||
|
|
||||||
|
This is a **compound reduction** — the same pattern as CCCL's `summary_statistics.cu`
|
||||||
|
where (count, mean, M2) are reduced together with a custom binary op.
|
||||||
|
|
||||||
|
**Our compound type:**
|
||||||
|
```
|
||||||
|
struct attention_partial {
|
||||||
|
float max_score; // running max of QK^T
|
||||||
|
float exp_sum; // sum of exp(score - max_score)
|
||||||
|
float weighted_v[D]; // sum of exp(score - max_score) * V
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Binary op** (from `summary_statistics.cu`):
|
||||||
|
```
|
||||||
|
attention_partial combine(attention_partial a, attention_partial b) {
|
||||||
|
float new_max = max(a.max_score, b.max_score);
|
||||||
|
float scale_a = exp(a.max_score - new_max);
|
||||||
|
float scale_b = exp(b.max_score - new_max);
|
||||||
|
return {
|
||||||
|
new_max,
|
||||||
|
scale_a * a.exp_sum + scale_b * b.exp_sum,
|
||||||
|
scale_a * a.weighted_v + scale_b * b.weighted_v // element-wise
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is exactly the online softmax from Flash Attention.
|
||||||
|
It's also exactly CCCL's binary reduction op pattern.
|
||||||
|
|
||||||
|
### Level 2: Block Reduce (from `block_reduce_warp_reductions.cuh`)
|
||||||
|
|
||||||
|
**CCCL pattern:** Each warp produces a `warp_aggregate`. Lane 0 of each warp
|
||||||
|
writes it to `SMEM warp_aggregates[warp_id]`. Then thread 0 serially reduces
|
||||||
|
across warps:
|
||||||
|
```
|
||||||
|
for (warp_idx = 1; warp_idx < warps; ++warp_idx)
|
||||||
|
aggregate = reduction_op(aggregate, warp_aggregates[warp_idx]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**In attention:** One thread block processes one partition of the KV sequence
|
||||||
|
(e.g., PARTITION_SIZE = 512 tokens). Multiple warps within the block each handle
|
||||||
|
a chunk of these 512 tokens.
|
||||||
|
|
||||||
|
- Warp 0: tokens 0..63 (BLOCK_N=64 at a time, or 32 for head_dim=256)
|
||||||
|
- Warp 1: tokens 64..127
|
||||||
|
- ...
|
||||||
|
- Warp W-1: tokens (W-1)*64..511
|
||||||
|
|
||||||
|
Each warp produces an `attention_partial`. Block reduce merges them:
|
||||||
|
```
|
||||||
|
__shared__ attention_partial warp_partials[NUM_WARPS];
|
||||||
|
warp_partials[warp_id] = my_warp_result;
|
||||||
|
__syncthreads();
|
||||||
|
if (threadIdx.x == 0) {
|
||||||
|
attention_partial block_result = warp_partials[0];
|
||||||
|
for (int w = 1; w < NUM_WARPS; w++)
|
||||||
|
block_result = combine(block_result, warp_partials[w]);
|
||||||
|
// Write block_result to global: tmp_output, exp_sums, max_logits
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SMEM layout for attention_partial at head_dim=256:**
|
||||||
|
- max_score: 4 bytes
|
||||||
|
- exp_sum: 4 bytes
|
||||||
|
- weighted_v[256]: 256 × 4 = 1024 bytes
|
||||||
|
- Total per warp: 1032 bytes
|
||||||
|
- For 4 warps: 4128 bytes (fits easily in 48KB)
|
||||||
|
|
||||||
|
### Level 3: Cross-Partition Coordination (from `agent_scan.cuh` + decoupled lookback)
|
||||||
|
|
||||||
|
**CCCL pattern:** `TilePrefixCallbackOp` implements decoupled lookback.
|
||||||
|
Each tile block:
|
||||||
|
1. Computes its local aggregate
|
||||||
|
2. Publishes local aggregate to global `tile_state` (PARTIAL status)
|
||||||
|
3. Warp 0 looks back through predecessor tiles:
|
||||||
|
- If predecessor has INCLUSIVE status → directly use its prefix
|
||||||
|
- If predecessor has PARTIAL status → accumulate and keep looking back
|
||||||
|
4. Once prefix is resolved, update own status to INCLUSIVE
|
||||||
|
|
||||||
|
**In attention (V2):** Each partition block has its `attention_partial`.
|
||||||
|
The cross-partition reduction is simpler than scan because attention
|
||||||
|
partitions are **commutative** — we don't need prefix sums, just a
|
||||||
|
global reduce.
|
||||||
|
|
||||||
|
But the coordination pattern is the same:
|
||||||
|
1. Each partition block writes its (max_logit, exp_sum, partial_output) to
|
||||||
|
global memory: `tmp_output[seq, head, partition, :]`
|
||||||
|
2. A separate reduction kernel (or the last partition block) reads all
|
||||||
|
partitions and does the final combine.
|
||||||
|
|
||||||
|
**Simplification over CCCL's lookback:** Since attention partitions are
|
||||||
|
independent (no prefix dependency), we don't need the lookback polling loop.
|
||||||
|
Each partition can run fully independently. The reduction is a simple
|
||||||
|
parallel reduce over `num_partitions` compound values.
|
||||||
|
|
||||||
|
For 100K tokens / 512 partition_size = ~200 partitions.
|
||||||
|
200 `attention_partial` values × (4 + 4 + 256×4) = 200 × 1032 = ~200KB.
|
||||||
|
One block can reduce all 200 in registers + SMEM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Paged K/V Gather (from `block_load.cuh` + `cache_modified_input_iterator.cuh`)
|
||||||
|
|
||||||
|
**CCCL pattern:** `BlockLoadWarpTranspose` loads contiguous global memory
|
||||||
|
into a striped register layout that enables coalesced access. Each thread
|
||||||
|
loads `ITEMS_PER_THREAD` elements, and the warp transposes them so each
|
||||||
|
thread gets its tile of the data.
|
||||||
|
|
||||||
|
**In paged attention:** K/V are not contiguous — they're indexed through
|
||||||
|
`block_tables[seq, logical_block] → physical_block`.
|
||||||
|
- Key cache: `[num_blocks, kv_heads, head_dim/x, block_size, x]`
|
||||||
|
where x = 16/sizeof(dtype) is the packing factor
|
||||||
|
- Value cache: `[num_blocks, kv_heads, head_dim, block_size]`
|
||||||
|
|
||||||
|
The gather pattern (from `prefix_prefill.py`, which works on BI-V100):
|
||||||
|
```
|
||||||
|
# For BLOCK_N tokens starting at position start_n:
|
||||||
|
token_ids = start_n + tl.arange(0, BLOCK_N)
|
||||||
|
logical_blocks = token_ids // block_size
|
||||||
|
within_block = token_ids % block_size
|
||||||
|
physical_blocks = tl.load(block_tables + seq * stride + logical_blocks * stride)
|
||||||
|
|
||||||
|
# K gather: compute 2D offset array [HEAD_DIM, BLOCK_N]
|
||||||
|
off_k = (physical_blocks[None, :] * stride_kc_b +
|
||||||
|
kv_head * stride_kc_h +
|
||||||
|
(offs_d[:, None] // x) * stride_kc_dx +
|
||||||
|
within_block[None, :] * stride_kc_bs +
|
||||||
|
(offs_d[:, None] % x) * stride_kc_x)
|
||||||
|
k = tl.load(key_cache + off_k, mask=valid_mask)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an **indirect gather** — the physical block ID comes from a table lookup.
|
||||||
|
CCCL's `CacheModifiedInputIterator` handles the cache hint part, but the
|
||||||
|
indirect indexing is our addition.
|
||||||
|
|
||||||
|
**Memory access pattern:**
|
||||||
|
- block_tables lookup: 1 global read per BLOCK_N tokens (amortized)
|
||||||
|
- K gather: BLOCK_N × HEAD_DIM / x global reads (scattered by physical block)
|
||||||
|
- V gather: BLOCK_N × HEAD_DIM global reads (similar scatter)
|
||||||
|
|
||||||
|
For BLOCK_N=32, HEAD_DIM=256, x=8: 32 × 32 = 1024 reads for K per iteration.
|
||||||
|
At 16 bytes per read (128-bit): 16KB per K load.
|
||||||
|
V is similar. Total per iteration: ~32KB — fits in L2 (6MB on BI-V100).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. GQA (Grouped Query Attention) Handling
|
||||||
|
|
||||||
|
**The insight:** 6 query heads share 1 KV head. Loading KV once and
|
||||||
|
computing 6 sets of QK^T scores is 6x more compute-efficient than
|
||||||
|
loading KV 6 times.
|
||||||
|
|
||||||
|
**CCCL analogy:** This is like `BlockReduce` where we have 6 different
|
||||||
|
reduction operations on the same input data. CCCL doesn't have this exact
|
||||||
|
pattern, but the principle is: share data loads, parallelize computation.
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- Each thread block handles one `(seq, kv_head, partition)` triple
|
||||||
|
- Within the block, 6 query heads are processed simultaneously
|
||||||
|
- Q vectors: 6 × HEAD_DIM = 6 × 256 = 1536 values in registers (per thread
|
||||||
|
this is 1536/32 = 48 registers — feasible)
|
||||||
|
- K/V: loaded once for the kv_head, broadcast across all 6 query heads
|
||||||
|
- Scores: 6 × BLOCK_N values per iteration
|
||||||
|
- Weighted V: 6 × HEAD_DIM per thread's accumulator
|
||||||
|
|
||||||
|
This reduces K/V cache reads by 6x (the GQA ratio).
|
||||||
|
|
||||||
|
Grid: `(num_seqs, num_kv_heads, num_partitions)` = `(1, 4, 200)` = 800 blocks
|
||||||
|
instead of `(1, 24, 200)` = 4800 blocks.
|
||||||
|
|
||||||
|
Each block does 6x more compute but reads KV only once.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. SMEM Budget
|
||||||
|
|
||||||
|
For one block processing BLOCK_N=32 KV tokens across 6 query heads:
|
||||||
|
|
||||||
|
| Item | Size | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| K tile [HEAD_DIM, BLOCK_N] | 32×256×2 = 16KB | fp16, loaded from paged cache |
|
||||||
|
| V tile [BLOCK_N, HEAD_DIM] | 32×256×2 = 16KB | fp16, loaded from paged cache |
|
||||||
|
| Warp partials [4 warps × attention_partial] | 4×(4+4+256×4) = 4.1KB | For block-level reduce |
|
||||||
|
| Q vectors [6 × HEAD_DIM] | 6×256×4 = 6KB | In registers ideally, SMEM if spills |
|
||||||
|
| **Total** | **42.1KB** | **≤ 48KB ✓** |
|
||||||
|
|
||||||
|
Tight but feasible. If Q stays in registers (likely with 4 warps × 32 threads
|
||||||
|
= 128 threads, each handling 6×256/128 = 12 Q values), total SMEM is 36.1KB.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Kernel Launch Configuration
|
||||||
|
|
||||||
|
**Phase 1: Partitioned Attention**
|
||||||
|
- Grid: `(num_seqs, num_kv_heads, num_partitions)`
|
||||||
|
- Block: `(NUM_WARPS × 32)` = 128 threads (4 warps)
|
||||||
|
- Each block processes:
|
||||||
|
- PARTITION_SIZE = 512 KV tokens
|
||||||
|
- 6 query heads (GQA broadcast)
|
||||||
|
- Produces 6 × (max_logit, exp_sum, partial_output[256])
|
||||||
|
|
||||||
|
**Phase 2: Cross-Partition Reduction**
|
||||||
|
- Grid: `(num_seqs, num_kv_heads)`
|
||||||
|
- Block: 128 threads
|
||||||
|
- Each block reduces ~200 partitions × 6 query heads
|
||||||
|
- Uses `combine()` op (same as CCCL `BlockReduce` but with `attention_partial`)
|
||||||
|
|
||||||
|
**Phase 1 iterations per block:**
|
||||||
|
- PARTITION_SIZE / BLOCK_N = 512 / 32 = 16 iterations
|
||||||
|
- Each iteration: load K[32, 256] + V[32, 256], compute 6×32 scores, update 6 accumulators
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Implementation Mapping
|
||||||
|
|
||||||
|
| Module | CCCL Source | Our Implementation |
|
||||||
|
|--------|------------|-------------------|
|
||||||
|
| Warp-level QK^T + softmax | `warp_reduce_shfl.cuh` | Triton: `tl.sum()` within warp-sized groups |
|
||||||
|
| Block-level partition reduce | `block_reduce_warp_reductions.cuh` | Triton: shared memory + `tl.reduce()` |
|
||||||
|
| Cross-partition combine | `agent_scan.cuh` (simplified, no lookback) | Separate reduction kernel |
|
||||||
|
| Paged K/V gather | `block_load.cuh` + indirect indexing | `prefix_prefill.py` pattern adapted |
|
||||||
|
| Online softmax | `summary_statistics.cu` binary op | `combine(attention_partial, attention_partial)` |
|
||||||
|
| GQA broadcast | (no exact CCCL analog) | Multiple Q per KV load |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Why This Design Beats Python V2
|
||||||
|
|
||||||
|
Current Python V2 (3 bmm launches + Python overhead):
|
||||||
|
- gather all KV → permute → contiguous → bmm → reshape → softmax → bmm → reduce
|
||||||
|
- **Python-CUDA boundary crossed 10+ times per decode step**
|
||||||
|
- **Full KV tensor materialized in GPU memory** (200MB-2.4GB depending on GQA)
|
||||||
|
|
||||||
|
This kernel (2 GPU launches, zero Python-CUDA crossings during compute):
|
||||||
|
- Phase 1: single kernel, K/V loaded tile-by-tile from paged cache (never materialized)
|
||||||
|
- Phase 2: single kernel, reduces 200 partitions in SMEM
|
||||||
|
- **KV cache stays in paged format** — no gather/permute/contiguous overhead
|
||||||
|
- **GQA broadcast within kernel** — KV loaded once for 6 heads
|
||||||
|
|
||||||
|
Expected improvement over Python V2: **10-100x** (eliminating Python overhead
|
||||||
|
and memory allocation dominates at decode batch_size=1).
|
||||||
|
|
||||||
|
Expected improvement over no V2 (V1 only for seq ≤ 8192): **enables long-context
|
||||||
|
decode** which V1 cannot do due to SMEM overflow at 48KB.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Implementation Priority
|
||||||
|
|
||||||
|
1. **Triton implementation** — if Triton works on BI-V100 with BLOCK=32, head_dim=256:
|
||||||
|
Use the `prefix_prefill.py` paged gather pattern, add the compound reduction.
|
||||||
|
This is the fastest path to a working kernel.
|
||||||
|
|
||||||
|
2. **Compiled CUDA kernel** — if `/usr/local/corex/` has a compiler (ixcc):
|
||||||
|
Write the kernel in CUDA using the CCCL patterns directly.
|
||||||
|
`warp_reduce_shfl` → `__shfl_down_sync` PTX
|
||||||
|
`block_reduce` → SMEM warp_aggregates pattern
|
||||||
|
Compile with `torch.utils.cpp_extension.load()` at Docker build time.
|
||||||
|
|
||||||
|
3. **Python V2** (current) — fallback if neither Triton nor CUDA works:
|
||||||
|
Already written, tested, has GQA broadcast optimization.
|
||||||
|
This is the floor, not the ceiling.
|
||||||
@@ -1,35 +1,32 @@
|
|||||||
"""
|
"""
|
||||||
paged_attention_v2_triton.py — Triton PagedAttention V2 for BI-V100
|
paged_attention_v2_triton.py — CCCL-derived Triton PagedAttention V2
|
||||||
=====================================================================
|
=====================================================================
|
||||||
|
|
||||||
Two-kernel V2 implementation using Triton:
|
Architecture: docs/paged_attention_kernel_architecture.md
|
||||||
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):
|
Two-kernel design:
|
||||||
bn = tl.load(block_tables + seq * stride + (token // block_size) * stride)
|
Phase 1: _partition_attn — per-partition compound reduction (CCCL block_reduce pattern)
|
||||||
off_k = bn * stride_kc_b + kv_head * stride_kc_h + (d // x) * stride_kc_dx + ...
|
Phase 2: _reduce_partitions — cross-partition combine (CCCL agent_reduce pattern)
|
||||||
k = tl.load(key_cache + off_k, mask=...)
|
|
||||||
|
|
||||||
For decode (BLOCK_M=1), the Q tile is just one vector [HEAD_DIM].
|
Key CCCL derivations:
|
||||||
The inner loop iterates over BLOCK_N KV tokens per step.
|
1. Compound type: (max_score, exp_sum, weighted_v[D]) — from summary_statistics.cu
|
||||||
Online softmax accumulates (max, sum, weighted_V) across steps.
|
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
|
||||||
|
|
||||||
After all steps in a partition, we have:
|
Grid design:
|
||||||
max_logits[seq, head, part]: running max
|
Phase 1: (num_seqs, num_kv_heads, num_partitions) — NOT (num_seqs, num_heads, num_partitions)
|
||||||
exp_sums[seq, head, part]: running exp sum
|
Each block loads KV once for kv_head, computes gqa_ratio query heads.
|
||||||
tmp_output[seq, head, part, :]: unnormalized weighted V
|
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.
|
||||||
|
|
||||||
Phase 2 combines partitions using the CCCL summary_statistics pattern:
|
SMEM budget (head_dim=256, BLOCK_N=32):
|
||||||
global_max = max(part_maxes)
|
K tile: 32×256×2 = 16KB
|
||||||
rescaled_sum = sum(exp(part_max - global_max) * part_sum)
|
V tile: 32×256×2 = 16KB
|
||||||
output = sum(weight[p] * part_output[p])
|
Warp partials: negligible (in registers for Triton)
|
||||||
|
Total: 32KB ≤ 48KB ✓
|
||||||
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 torch
|
||||||
@@ -39,22 +36,22 @@ from typing import Optional
|
|||||||
|
|
||||||
|
|
||||||
@triton.jit
|
@triton.jit
|
||||||
def _paged_attn_v2_partition_kernel(
|
def _partition_attn_kernel(
|
||||||
# Outputs
|
# Outputs (per partition)
|
||||||
tmp_output_ptr, # [num_seqs, num_heads, max_num_parts, head_size]
|
tmp_output_ptr, # [num_seqs, num_heads, max_parts, head_size]
|
||||||
exp_sums_ptr, # [num_seqs, num_heads, max_num_parts]
|
exp_sums_ptr, # [num_seqs, num_heads, max_parts]
|
||||||
max_logits_ptr, # [num_seqs, num_heads, max_num_parts]
|
max_logits_ptr, # [num_seqs, num_heads, max_parts]
|
||||||
# Inputs
|
# Inputs
|
||||||
query_ptr, # [num_seqs, num_heads, head_size]
|
query_ptr, # [num_seqs, num_heads, head_size]
|
||||||
key_cache_ptr, # [num_blocks, num_kv_heads, head_size/x, block_size, x]
|
key_cache_ptr, # [num_blocks, kv_heads, head_size/x, block_size, x]
|
||||||
value_cache_ptr, # [num_blocks, num_kv_heads, head_size, block_size]
|
value_cache_ptr, # [num_blocks, kv_heads, head_size, block_size]
|
||||||
block_tables_ptr, # [num_seqs, max_blocks_per_seq]
|
block_tables_ptr, # [num_seqs, max_blocks_per_seq]
|
||||||
seq_lens_ptr, # [num_seqs]
|
seq_lens_ptr, # [num_seqs]
|
||||||
# Scalars
|
# Scalars
|
||||||
scale: tl.float32,
|
scale: tl.float32,
|
||||||
num_queries_per_kv: tl.int32,
|
gqa_ratio: tl.int32, # num_heads // num_kv_heads
|
||||||
block_size: tl.int32,
|
block_size: tl.int32,
|
||||||
x_pack: tl.int32, # key_cache packing factor: 16 // sizeof(dtype)
|
x_pack: tl.int32, # key_cache packing factor
|
||||||
# Strides: query [S, H, D]
|
# Strides: query [S, H, D]
|
||||||
stride_qs: tl.int32, stride_qh: tl.int32, stride_qd: tl.int32,
|
stride_qs: tl.int32, stride_qh: tl.int32, stride_qd: tl.int32,
|
||||||
# Strides: key_cache [B, KH, D/X, BS, X]
|
# Strides: key_cache [B, KH, D/X, BS, X]
|
||||||
@@ -68,23 +65,36 @@ def _paged_attn_v2_partition_kernel(
|
|||||||
# Strides: tmp_output [S, H, P, D]
|
# Strides: tmp_output [S, H, P, D]
|
||||||
stride_to_s: tl.int32, stride_to_h: tl.int32,
|
stride_to_s: tl.int32, stride_to_h: tl.int32,
|
||||||
stride_to_p: tl.int32, stride_to_d: tl.int32,
|
stride_to_p: tl.int32, stride_to_d: tl.int32,
|
||||||
# Strides: exp_sums / max_logits [S, H, P]
|
# Strides: exp_sums/max_logits [S, H, P]
|
||||||
stride_es_s: tl.int32, stride_es_h: tl.int32, stride_es_p: tl.int32,
|
stride_es_s: tl.int32, stride_es_h: tl.int32, stride_es_p: tl.int32,
|
||||||
# Compile-time constants
|
# Constants
|
||||||
PARTITION_SIZE: tl.constexpr,
|
PARTITION_SIZE: tl.constexpr,
|
||||||
HEAD_DIM: tl.constexpr,
|
HEAD_DIM: tl.constexpr,
|
||||||
BLOCK_N: tl.constexpr,
|
BLOCK_N: tl.constexpr,
|
||||||
|
GQA_RATIO: tl.constexpr,
|
||||||
):
|
):
|
||||||
"""Phase 1: Per-partition paged attention for decode (BLOCK_M=1).
|
"""Phase 1: Per-partition attention with GQA broadcast.
|
||||||
|
|
||||||
Grid: (num_seqs, num_heads, max_num_partitions)
|
Grid: (num_seqs, num_kv_heads, num_partitions)
|
||||||
Each program instance processes one (seq, head, partition) triple.
|
Each block processes one (seq, kv_head, partition), computing GQA_RATIO query heads.
|
||||||
|
|
||||||
Adapted from prefix_prefill.py's paged K/V gather pattern.
|
Algorithm (CCCL compound reduction):
|
||||||
Key difference: BLOCK_M=1 (decode has 1 query token per head).
|
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)
|
seq_idx = tl.program_id(0)
|
||||||
head_idx = tl.program_id(1)
|
kv_head_idx = tl.program_id(1)
|
||||||
part_idx = tl.program_id(2)
|
part_idx = tl.program_id(2)
|
||||||
|
|
||||||
seq_len = tl.load(seq_lens_ptr + seq_idx)
|
seq_len = tl.load(seq_lens_ptr + seq_idx)
|
||||||
@@ -92,166 +102,168 @@ def _paged_attn_v2_partition_kernel(
|
|||||||
part_end = tl.minimum(part_start + PARTITION_SIZE, seq_len)
|
part_end = tl.minimum(part_start + PARTITION_SIZE, seq_len)
|
||||||
|
|
||||||
if part_start >= seq_len:
|
if part_start >= seq_len:
|
||||||
# Unused partition — write sentinel values
|
# Unused partition — write sentinels for all GQA_RATIO heads
|
||||||
tl.store(max_logits_ptr + seq_idx * stride_es_s + head_idx * stride_es_h + part_idx * stride_es_p,
|
for g in range(GQA_RATIO):
|
||||||
float('-inf'))
|
head_idx = kv_head_idx * GQA_RATIO + g
|
||||||
tl.store(exp_sums_ptr + seq_idx * stride_es_s + head_idx * stride_es_h + part_idx * stride_es_p,
|
tl.store(max_logits_ptr + seq_idx * stride_es_s + head_idx * stride_es_h + part_idx * stride_es_p,
|
||||||
0.0)
|
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
|
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)
|
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)
|
offs_n = tl.arange(0, BLOCK_N)
|
||||||
|
|
||||||
# Iterate over BLOCK_N KV tokens at a time
|
# Load all GQA_RATIO query vectors for this kv_head
|
||||||
for start_n in range(part_start, part_end, BLOCK_N):
|
# q[g]: [HEAD_DIM] for g in 0..GQA_RATIO-1
|
||||||
# Token positions in the sequence
|
# We process them sequentially to stay within register budget
|
||||||
token_ids = start_n + offs_n
|
# (Loading all 6 × 256 = 1536 fp32 values would be 6KB of registers per thread)
|
||||||
valid_mask = token_ids < part_end
|
|
||||||
|
|
||||||
# === Paged K gather (from prefix_prefill.py pattern) ===
|
# Initialize compound accumulators for each query head
|
||||||
# Look up physical block numbers from block_tables
|
# m[g]: running max, l[g]: running exp_sum, acc[g]: [HEAD_DIM] weighted V
|
||||||
block_indices = token_ids // block_size
|
# For Triton, we process one query head at a time through the full partition
|
||||||
within_block = token_ids % block_size
|
# to minimize register pressure.
|
||||||
|
|
||||||
# bn: physical block ids [BLOCK_N]
|
for g in range(GQA_RATIO):
|
||||||
bn = tl.load(
|
head_idx = kv_head_idx * GQA_RATIO + g
|
||||||
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]
|
# Load Q for this head
|
||||||
# Layout: [num_blocks, num_kv_heads, head_size/x, block_size, x]
|
q = tl.load(query_ptr + seq_idx * stride_qs + head_idx * stride_qh
|
||||||
# off_k: [HEAD_DIM, BLOCK_N] — each column is one token's K vector
|
+ offs_d * stride_qd).to(tl.float32)
|
||||||
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]
|
# Compound accumulator
|
||||||
|
m_i = float('-inf')
|
||||||
|
l_i = 0.0
|
||||||
|
acc = tl.zeros([HEAD_DIM], dtype=tl.float32)
|
||||||
|
|
||||||
# Scores: q @ k = [1, D] @ [D, N] → [N]
|
# Inner loop: BLOCK_N KV tokens per iteration
|
||||||
# For BLOCK_M=1: this is a dot product per KV token
|
for start_n in range(part_start, part_end, BLOCK_N):
|
||||||
scores = tl.sum(q[:, None] * k, axis=0) * scale # [BLOCK_N]
|
token_ids = start_n + offs_n
|
||||||
scores = tl.where(valid_mask, scores, float('-inf'))
|
valid = token_ids < part_end
|
||||||
|
|
||||||
# Online softmax (adapted from prefix_prefill.py — proven correct)
|
# Paged K gather (from prefix_prefill.py)
|
||||||
m_ij = tl.max(scores, axis=0) # scalar: max of this chunk
|
blk_idx = token_ids // block_size
|
||||||
p = tl.exp(scores - m_ij) # [BLOCK_N] — unnormalized probs
|
blk_off = token_ids % block_size
|
||||||
l_ij = tl.sum(p, axis=0) # scalar: sum of exp for this chunk
|
phys_blk = tl.load(block_tables_ptr + seq_idx * stride_bt_s + blk_idx * stride_bt_b,
|
||||||
|
mask=valid, other=0)
|
||||||
|
|
||||||
m_i_new = tl.maximum(m_i, m_ij)
|
off_k = (phys_blk[None, :] * stride_kc_b +
|
||||||
alpha = tl.exp(m_i - m_i_new) # rescale factor for old accumulator
|
kv_head_idx * stride_kc_h +
|
||||||
beta = tl.exp(m_ij - m_i_new) # rescale factor for new chunk
|
(offs_d[:, None] // x_pack) * stride_kc_dx +
|
||||||
l_i_new = alpha * l_i + beta * l_ij
|
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]
|
||||||
|
|
||||||
# === Paged V gather ===
|
# Scores: q · k per token
|
||||||
off_v = (bn[:, None] * stride_vc_b +
|
scores = tl.sum(q[:, None] * k, axis=0) * scale # [BLOCK_N]
|
||||||
kv_head_idx * stride_vc_h +
|
scores = tl.where(valid, scores, float('-inf'))
|
||||||
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):
|
# Online softmax (CCCL combine op)
|
||||||
# acc = acc * (alpha * l_i / l_i_new) + (p * beta / l_i_new) @ V
|
m_ij = tl.max(scores, axis=0)
|
||||||
# Safe division: if l_i_new == 0, this is the first chunk
|
p = tl.exp(scores - m_ij)
|
||||||
acc_scale = alpha * l_i / tl.maximum(l_i_new, 1e-6)
|
l_ij = tl.sum(p, axis=0)
|
||||||
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_new = tl.maximum(m_i, m_ij)
|
||||||
m_i = m_i_new
|
alpha = tl.exp(m_i - m_new)
|
||||||
|
beta = tl.exp(m_ij - m_new)
|
||||||
|
l_new = alpha * l_i + beta * l_ij
|
||||||
|
|
||||||
# Store partition results
|
# Paged V gather
|
||||||
tl.store(max_logits_ptr + seq_idx * stride_es_s + head_idx * stride_es_h + part_idx * stride_es_p,
|
off_v = (phys_blk[:, None] * stride_vc_b +
|
||||||
m_i)
|
kv_head_idx * stride_vc_h +
|
||||||
tl.store(exp_sums_ptr + seq_idx * stride_es_s + head_idx * stride_es_h + part_idx * stride_es_p,
|
offs_d[None, :] * stride_vc_d +
|
||||||
l_i)
|
blk_off[:, None] * stride_vc_bs)
|
||||||
|
v = tl.load(value_cache_ptr + off_v, mask=valid[:, None], other=0.0) # [N, D]
|
||||||
|
|
||||||
# Store accumulated output: [HEAD_DIM]
|
# Update accumulator
|
||||||
out_base = seq_idx * stride_to_s + head_idx * stride_to_h + part_idx * stride_to_p
|
safe_l = tl.maximum(l_new, 1e-6)
|
||||||
tl.store(tmp_output_ptr + out_base + offs_d * stride_to_d, acc.to(tmp_output_ptr.dtype.element_ty))
|
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
|
@triton.jit
|
||||||
def _paged_attn_v2_reduce_kernel(
|
def _reduce_partitions_kernel(
|
||||||
# Output
|
output_ptr, # [num_seqs, num_heads, head_size]
|
||||||
output_ptr, # [num_seqs, num_heads, head_size]
|
tmp_output_ptr, # [num_seqs, num_heads, max_parts, head_size]
|
||||||
# Inputs
|
exp_sums_ptr, # [num_seqs, num_heads, max_parts]
|
||||||
tmp_output_ptr, # [num_seqs, num_heads, max_num_parts, head_size]
|
max_logits_ptr, # [num_seqs, num_heads, max_parts]
|
||||||
exp_sums_ptr, # [num_seqs, num_heads, max_num_parts]
|
seq_lens_ptr, # [num_seqs]
|
||||||
max_logits_ptr, # [num_seqs, num_heads, max_num_parts]
|
gqa_ratio: tl.int32,
|
||||||
seq_lens_ptr, # [num_seqs]
|
|
||||||
# Scalars
|
|
||||||
max_num_parts: tl.int32,
|
max_num_parts: tl.int32,
|
||||||
# Strides
|
|
||||||
stride_out_s: tl.int32, stride_out_h: tl.int32, stride_out_d: 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_s: tl.int32, stride_to_h: tl.int32,
|
||||||
stride_to_p: tl.int32, stride_to_d: 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,
|
stride_es_s: tl.int32, stride_es_h: tl.int32, stride_es_p: tl.int32,
|
||||||
# Constants
|
|
||||||
PARTITION_SIZE: tl.constexpr,
|
PARTITION_SIZE: tl.constexpr,
|
||||||
HEAD_DIM: tl.constexpr,
|
HEAD_DIM: tl.constexpr,
|
||||||
MAX_NUM_PARTS: tl.constexpr,
|
MAX_NUM_PARTS: tl.constexpr,
|
||||||
|
GQA_RATIO: tl.constexpr,
|
||||||
):
|
):
|
||||||
"""Phase 2: Cross-partition log-sum-exp reduction.
|
"""Phase 2: Cross-partition reduction.
|
||||||
|
|
||||||
Grid: (num_seqs, num_heads)
|
Grid: (num_seqs, num_kv_heads)
|
||||||
Combines partition results using CCCL summary_statistics pattern.
|
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)
|
seq_idx = tl.program_id(0)
|
||||||
head_idx = tl.program_id(1)
|
kv_head_idx = tl.program_id(1)
|
||||||
|
|
||||||
seq_len = tl.load(seq_lens_ptr + seq_idx)
|
seq_len = tl.load(seq_lens_ptr + seq_idx)
|
||||||
num_parts = (seq_len + PARTITION_SIZE - 1) // PARTITION_SIZE
|
num_parts = (seq_len + PARTITION_SIZE - 1) // PARTITION_SIZE
|
||||||
|
|
||||||
# Load partition statistics
|
|
||||||
part_offsets = tl.arange(0, MAX_NUM_PARTS)
|
part_offsets = tl.arange(0, MAX_NUM_PARTS)
|
||||||
valid_mask = part_offsets < num_parts
|
valid = 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)
|
offs_d = tl.arange(0, HEAD_DIM)
|
||||||
acc = tl.zeros([HEAD_DIM], dtype=tl.float32)
|
|
||||||
|
|
||||||
for p in range(MAX_NUM_PARTS):
|
for g in range(GQA_RATIO):
|
||||||
if p < num_parts:
|
head_idx = kv_head_idx * GQA_RATIO + g
|
||||||
w = tl.load(max_logits_ptr + es_base + p * stride_es_p) # reload for weight
|
es_base = seq_idx * stride_es_s + head_idx * stride_es_h
|
||||||
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
|
# Load partition statistics
|
||||||
part_out = tl.load(tmp_output_ptr + to_base + offs_d * stride_to_d)
|
part_max = tl.load(max_logits_ptr + es_base + part_offsets * stride_es_p,
|
||||||
acc += w_rescaled * part_out.to(tl.float32)
|
mask=valid, other=float('-inf'))
|
||||||
|
part_sum = tl.load(exp_sums_ptr + es_base + part_offsets * stride_es_p,
|
||||||
|
mask=valid, other=0.0)
|
||||||
|
|
||||||
# Store final output
|
# Global max
|
||||||
out_base = seq_idx * stride_out_s + head_idx * stride_out_h
|
global_max = tl.max(part_max, axis=0)
|
||||||
tl.store(output_ptr + out_base + offs_d * stride_out_d, acc.to(output_ptr.dtype.element_ty))
|
|
||||||
|
# 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(
|
def paged_attention_v2_triton(
|
||||||
@@ -274,60 +286,52 @@ def paged_attention_v2_triton(
|
|||||||
v_scale: float = 1.0,
|
v_scale: float = 1.0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Launch Triton V2 kernels."""
|
"""Launch CCCL-derived Triton V2 kernels."""
|
||||||
num_seqs, num_heads, head_size = query.shape
|
num_seqs, num_heads, head_size = query.shape
|
||||||
num_queries_per_kv = num_heads // num_kv_heads
|
gqa_ratio = num_heads // num_kv_heads
|
||||||
max_num_parts = tmp_output.shape[2]
|
max_num_parts = tmp_output.shape[2]
|
||||||
x_pack = key_cache.shape[-1] # packing factor
|
x_pack = key_cache.shape[-1]
|
||||||
|
|
||||||
PARTITION_SIZE = 512
|
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
|
BLOCK_N = 32 if head_size > 128 else 64
|
||||||
|
|
||||||
# Phase 1: partition attention
|
|
||||||
num_partitions = (max_seq_len + PARTITION_SIZE - 1) // PARTITION_SIZE
|
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](
|
# 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,
|
tmp_output, exp_sums, max_logits,
|
||||||
query, key_cache, value_cache, block_tables, seq_lens,
|
query, key_cache, value_cache, block_tables, seq_lens,
|
||||||
scale, num_queries_per_kv, block_size, x_pack,
|
scale, gqa_ratio, block_size, x_pack,
|
||||||
# query strides
|
|
||||||
query.stride(0), query.stride(1), query.stride(2),
|
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(0), key_cache.stride(1), key_cache.stride(2),
|
||||||
key_cache.stride(3), key_cache.stride(4),
|
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(0), value_cache.stride(1), value_cache.stride(2),
|
||||||
value_cache.stride(3),
|
value_cache.stride(3),
|
||||||
# block_tables strides
|
|
||||||
block_tables.stride(0), block_tables.stride(1),
|
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),
|
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),
|
exp_sums.stride(0), exp_sums.stride(1), exp_sums.stride(2),
|
||||||
# Constants
|
|
||||||
PARTITION_SIZE=PARTITION_SIZE,
|
PARTITION_SIZE=PARTITION_SIZE,
|
||||||
HEAD_DIM=head_size,
|
HEAD_DIM=head_size,
|
||||||
BLOCK_N=BLOCK_N,
|
BLOCK_N=BLOCK_N,
|
||||||
|
GQA_RATIO=gqa_ratio,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Phase 2: cross-partition reduction
|
# 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)
|
MAX_NUM_PARTS_CONST = triton.next_power_of_2(max_num_parts)
|
||||||
if MAX_NUM_PARTS_CONST > 1024:
|
if MAX_NUM_PARTS_CONST > 1024:
|
||||||
MAX_NUM_PARTS_CONST = 1024
|
MAX_NUM_PARTS_CONST = 1024
|
||||||
|
|
||||||
grid_phase2 = (num_seqs, num_heads)
|
grid_p2 = (num_seqs, num_kv_heads)
|
||||||
_paged_attn_v2_reduce_kernel[grid_phase2](
|
_reduce_partitions_kernel[grid_p2](
|
||||||
output,
|
output,
|
||||||
tmp_output, exp_sums, max_logits, seq_lens,
|
tmp_output, exp_sums, max_logits, seq_lens,
|
||||||
max_num_parts,
|
gqa_ratio, max_num_parts,
|
||||||
output.stride(0), output.stride(1), output.stride(2),
|
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),
|
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),
|
exp_sums.stride(0), exp_sums.stride(1), exp_sums.stride(2),
|
||||||
PARTITION_SIZE=PARTITION_SIZE,
|
PARTITION_SIZE=PARTITION_SIZE,
|
||||||
HEAD_DIM=head_size,
|
HEAD_DIM=head_size,
|
||||||
MAX_NUM_PARTS=MAX_NUM_PARTS_CONST,
|
MAX_NUM_PARTS=MAX_NUM_PARTS_CONST,
|
||||||
|
GQA_RATIO=gqa_ratio,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user