[OPT] PagedAttention V2 implementation — fill the NotImplementedError hole
The single biggest performance bottleneck in the baseline:
paged_attention_v2 = raise NotImplementedError()
paged_attn.py: use_v1 = True (hardcoded to avoid calling V2)
V1 limitation: processes entire KV sequence in one kernel launch.
For seq_len=100K, this is a single massive attention computation.
V2: splits into PARTITION_SIZE=512 chunks, runs them in parallel,
then reduces with log-sum-exp. 195 parallel partitions vs 1.
Implementation (paged_attention_v2_pytorch.py):
Phase 1: Per-partition attention
- For each (seq, head, partition): compute QK^T, softmax, weighted V sum
- Store partial: tmp_output, exp_sums, max_logits (per partition)
Phase 2: Cross-partition reduction (log-sum-exp)
- global_max = max(max_logits across partitions)
- rescale = exp(partition_max - global_max) × partition_exp_sum
- output = Σ (rescale / total_sum) × partition_output
This is the same algorithm as vllm's paged_attention_v2_kernel.cu:
- The reduction pattern is identical to CCCL's block_reduce_warp_reductions
(combine partial statistics from independent segments)
- The online softmax tiling is the same as Flash Attention's partitioning
Integration:
- patch_paged_attention_v2.py patches _custom_ops.py and paged_attn.py
- Removes use_v1=True hardcode → V2 used for seq_len > 8192
- Dockerfile adds the patch step
This is a PyTorch implementation (no CUDA compilation needed).
Next step: if /usr/local/corex/ has ixcc or nvcc-compatible compiler,
replace with compiled CUDA kernel for further speedup.
2026-07-30 15:40:14 +00:00
|
|
|
|
"""
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
paged_attention_v2_pytorch.py — BI-V100 PagedAttention V2 (CCCL-informed)
|
|
|
|
|
|
===========================================================================
|
[OPT] PagedAttention V2 implementation — fill the NotImplementedError hole
The single biggest performance bottleneck in the baseline:
paged_attention_v2 = raise NotImplementedError()
paged_attn.py: use_v1 = True (hardcoded to avoid calling V2)
V1 limitation: processes entire KV sequence in one kernel launch.
For seq_len=100K, this is a single massive attention computation.
V2: splits into PARTITION_SIZE=512 chunks, runs them in parallel,
then reduces with log-sum-exp. 195 parallel partitions vs 1.
Implementation (paged_attention_v2_pytorch.py):
Phase 1: Per-partition attention
- For each (seq, head, partition): compute QK^T, softmax, weighted V sum
- Store partial: tmp_output, exp_sums, max_logits (per partition)
Phase 2: Cross-partition reduction (log-sum-exp)
- global_max = max(max_logits across partitions)
- rescale = exp(partition_max - global_max) × partition_exp_sum
- output = Σ (rescale / total_sum) × partition_output
This is the same algorithm as vllm's paged_attention_v2_kernel.cu:
- The reduction pattern is identical to CCCL's block_reduce_warp_reductions
(combine partial statistics from independent segments)
- The online softmax tiling is the same as Flash Attention's partitioning
Integration:
- patch_paged_attention_v2.py patches _custom_ops.py and paged_attn.py
- Removes use_v1=True hardcode → V2 used for seq_len > 8192
- Dockerfile adds the patch step
This is a PyTorch implementation (no CUDA compilation needed).
Next step: if /usr/local/corex/ has ixcc or nvcc-compatible compiler,
replace with compiled CUDA kernel for further speedup.
2026-07-30 15:40:14 +00:00
|
|
|
|
|
|
|
|
|
|
Fills the `raise NotImplementedError()` hole in vllm/_custom_ops.py.
|
|
|
|
|
|
|
|
|
|
|
|
Algorithm: Partitioned attention with log-sum-exp reduction.
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
Architecture informed by CCCL patterns:
|
|
|
|
|
|
- summary_statistics.cu: fuse multiple statistics in a single reduction pass
|
|
|
|
|
|
- warp_reduce_shfl.cuh: accumulate (max, sum, weighted_output) as one compound type
|
|
|
|
|
|
- block_reduce_warp_reductions.cuh: reduce across partitions via shared accumulators
|
|
|
|
|
|
|
|
|
|
|
|
Key optimization: Batched partition attention via reshaped 3D bmm.
|
|
|
|
|
|
Instead of looping over P partitions with P × torch.bmm calls,
|
|
|
|
|
|
reshape KV into [H, P*part_len, d] and Q into [H, 1, d], then
|
|
|
|
|
|
slice scores into [H, P, part_len] for partition-wise softmax.
|
|
|
|
|
|
This gives ONE bmm launch for all partitions.
|
|
|
|
|
|
|
|
|
|
|
|
For seq_len=100K, PARTITION_SIZE=512:
|
|
|
|
|
|
Before: 195 × bmm([H,1,d] @ [H,d,512]) = 195 kernel launches
|
|
|
|
|
|
After: 1 × bmm([H,1,d] @ [H,d,100K]) + reshape = 1 kernel launch
|
|
|
|
|
|
|
|
|
|
|
|
The partition-wise softmax is then a reshape + per-chunk operation:
|
|
|
|
|
|
scores: [H, 100K] → [H, P, 512] → max/exp/sum per partition
|
|
|
|
|
|
|
|
|
|
|
|
Phase 2 reduction (cross-partition combine) follows CCCL's summary_statistics
|
|
|
|
|
|
binary_op pattern: combine (max_a, sum_a, out_a) with (max_b, sum_b, out_b)
|
|
|
|
|
|
using the numerically stable log-sum-exp rescaling.
|
[OPT] PagedAttention V2 implementation — fill the NotImplementedError hole
The single biggest performance bottleneck in the baseline:
paged_attention_v2 = raise NotImplementedError()
paged_attn.py: use_v1 = True (hardcoded to avoid calling V2)
V1 limitation: processes entire KV sequence in one kernel launch.
For seq_len=100K, this is a single massive attention computation.
V2: splits into PARTITION_SIZE=512 chunks, runs them in parallel,
then reduces with log-sum-exp. 195 parallel partitions vs 1.
Implementation (paged_attention_v2_pytorch.py):
Phase 1: Per-partition attention
- For each (seq, head, partition): compute QK^T, softmax, weighted V sum
- Store partial: tmp_output, exp_sums, max_logits (per partition)
Phase 2: Cross-partition reduction (log-sum-exp)
- global_max = max(max_logits across partitions)
- rescale = exp(partition_max - global_max) × partition_exp_sum
- output = Σ (rescale / total_sum) × partition_output
This is the same algorithm as vllm's paged_attention_v2_kernel.cu:
- The reduction pattern is identical to CCCL's block_reduce_warp_reductions
(combine partial statistics from independent segments)
- The online softmax tiling is the same as Flash Attention's partitioning
Integration:
- patch_paged_attention_v2.py patches _custom_ops.py and paged_attn.py
- Removes use_v1=True hardcode → V2 used for seq_len > 8192
- Dockerfile adds the patch step
This is a PyTorch implementation (no CUDA compilation needed).
Next step: if /usr/local/corex/ has ixcc or nvcc-compatible compiler,
replace with compiled CUDA kernel for further speedup.
2026-07-30 15:40:14 +00:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import torch
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
_PARTITION_SIZE = 512
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def paged_attention_v2_pytorch(
|
|
|
|
|
|
output: torch.Tensor, # [num_seqs, num_heads, head_size]
|
|
|
|
|
|
exp_sums: torch.Tensor, # [num_seqs, num_heads, max_num_partitions]
|
|
|
|
|
|
max_logits: torch.Tensor, # [num_seqs, num_heads, max_num_partitions]
|
|
|
|
|
|
tmp_output: torch.Tensor, # [num_seqs, num_heads, max_num_partitions, head_size]
|
|
|
|
|
|
query: torch.Tensor, # [num_seqs, num_heads, head_size]
|
|
|
|
|
|
key_cache: torch.Tensor, # [num_blocks, num_kv_heads, head_size/x, block_size, x]
|
|
|
|
|
|
value_cache: torch.Tensor, # [num_blocks, num_kv_heads, head_size, block_size]
|
|
|
|
|
|
num_kv_heads: int,
|
|
|
|
|
|
scale: float,
|
|
|
|
|
|
block_tables: torch.Tensor, # [num_seqs, max_blocks_per_seq]
|
|
|
|
|
|
seq_lens: torch.Tensor, # [num_seqs]
|
|
|
|
|
|
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,
|
|
|
|
|
|
tp_rank: int = 0,
|
|
|
|
|
|
blocksparse_local_blocks: int = 0,
|
|
|
|
|
|
blocksparse_vert_stride: int = 0,
|
|
|
|
|
|
blocksparse_block_size: int = 64,
|
|
|
|
|
|
blocksparse_head_sliding_step: int = 0,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
num_seqs, num_heads, head_size = query.shape
|
2026-07-30 15:44:46 +00:00
|
|
|
|
gqa_ratio = num_heads // num_kv_heads
|
[OPT] PagedAttention V2 implementation — fill the NotImplementedError hole
The single biggest performance bottleneck in the baseline:
paged_attention_v2 = raise NotImplementedError()
paged_attn.py: use_v1 = True (hardcoded to avoid calling V2)
V1 limitation: processes entire KV sequence in one kernel launch.
For seq_len=100K, this is a single massive attention computation.
V2: splits into PARTITION_SIZE=512 chunks, runs them in parallel,
then reduces with log-sum-exp. 195 parallel partitions vs 1.
Implementation (paged_attention_v2_pytorch.py):
Phase 1: Per-partition attention
- For each (seq, head, partition): compute QK^T, softmax, weighted V sum
- Store partial: tmp_output, exp_sums, max_logits (per partition)
Phase 2: Cross-partition reduction (log-sum-exp)
- global_max = max(max_logits across partitions)
- rescale = exp(partition_max - global_max) × partition_exp_sum
- output = Σ (rescale / total_sum) × partition_output
This is the same algorithm as vllm's paged_attention_v2_kernel.cu:
- The reduction pattern is identical to CCCL's block_reduce_warp_reductions
(combine partial statistics from independent segments)
- The online softmax tiling is the same as Flash Attention's partitioning
Integration:
- patch_paged_attention_v2.py patches _custom_ops.py and paged_attn.py
- Removes use_v1=True hardcode → V2 used for seq_len > 8192
- Dockerfile adds the patch step
This is a PyTorch implementation (no CUDA compilation needed).
Next step: if /usr/local/corex/ has ixcc or nvcc-compatible compiler,
replace with compiled CUDA kernel for further speedup.
2026-07-30 15:40:14 +00:00
|
|
|
|
max_num_partitions = tmp_output.shape[2]
|
2026-07-30 15:44:46 +00:00
|
|
|
|
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
# Initialize unused slots
|
2026-07-30 15:44:46 +00:00
|
|
|
|
max_logits.fill_(float('-inf'))
|
|
|
|
|
|
exp_sums.zero_()
|
|
|
|
|
|
tmp_output.zero_()
|
|
|
|
|
|
|
[OPT] PagedAttention V2 implementation — fill the NotImplementedError hole
The single biggest performance bottleneck in the baseline:
paged_attention_v2 = raise NotImplementedError()
paged_attn.py: use_v1 = True (hardcoded to avoid calling V2)
V1 limitation: processes entire KV sequence in one kernel launch.
For seq_len=100K, this is a single massive attention computation.
V2: splits into PARTITION_SIZE=512 chunks, runs them in parallel,
then reduces with log-sum-exp. 195 parallel partitions vs 1.
Implementation (paged_attention_v2_pytorch.py):
Phase 1: Per-partition attention
- For each (seq, head, partition): compute QK^T, softmax, weighted V sum
- Store partial: tmp_output, exp_sums, max_logits (per partition)
Phase 2: Cross-partition reduction (log-sum-exp)
- global_max = max(max_logits across partitions)
- rescale = exp(partition_max - global_max) × partition_exp_sum
- output = Σ (rescale / total_sum) × partition_output
This is the same algorithm as vllm's paged_attention_v2_kernel.cu:
- The reduction pattern is identical to CCCL's block_reduce_warp_reductions
(combine partial statistics from independent segments)
- The online softmax tiling is the same as Flash Attention's partitioning
Integration:
- patch_paged_attention_v2.py patches _custom_ops.py and paged_attn.py
- Removes use_v1=True hardcode → V2 used for seq_len > 8192
- Dockerfile adds the patch step
This is a PyTorch implementation (no CUDA compilation needed).
Next step: if /usr/local/corex/ has ixcc or nvcc-compatible compiler,
replace with compiled CUDA kernel for further speedup.
2026-07-30 15:40:14 +00:00
|
|
|
|
for seq_idx in range(num_seqs):
|
2026-07-30 15:44:46 +00:00
|
|
|
|
seq_len = int(seq_lens[seq_idx].item())
|
|
|
|
|
|
if seq_len == 0:
|
|
|
|
|
|
output[seq_idx].zero_()
|
[OPT] PagedAttention V2 implementation — fill the NotImplementedError hole
The single biggest performance bottleneck in the baseline:
paged_attention_v2 = raise NotImplementedError()
paged_attn.py: use_v1 = True (hardcoded to avoid calling V2)
V1 limitation: processes entire KV sequence in one kernel launch.
For seq_len=100K, this is a single massive attention computation.
V2: splits into PARTITION_SIZE=512 chunks, runs them in parallel,
then reduces with log-sum-exp. 195 parallel partitions vs 1.
Implementation (paged_attention_v2_pytorch.py):
Phase 1: Per-partition attention
- For each (seq, head, partition): compute QK^T, softmax, weighted V sum
- Store partial: tmp_output, exp_sums, max_logits (per partition)
Phase 2: Cross-partition reduction (log-sum-exp)
- global_max = max(max_logits across partitions)
- rescale = exp(partition_max - global_max) × partition_exp_sum
- output = Σ (rescale / total_sum) × partition_output
This is the same algorithm as vllm's paged_attention_v2_kernel.cu:
- The reduction pattern is identical to CCCL's block_reduce_warp_reductions
(combine partial statistics from independent segments)
- The online softmax tiling is the same as Flash Attention's partitioning
Integration:
- patch_paged_attention_v2.py patches _custom_ops.py and paged_attn.py
- Removes use_v1=True hardcode → V2 used for seq_len > 8192
- Dockerfile adds the patch step
This is a PyTorch implementation (no CUDA compilation needed).
Next step: if /usr/local/corex/ has ixcc or nvcc-compatible compiler,
replace with compiled CUDA kernel for further speedup.
2026-07-30 15:40:14 +00:00
|
|
|
|
continue
|
2026-07-30 15:44:46 +00:00
|
|
|
|
|
|
|
|
|
|
num_blocks_seq = (seq_len + block_size - 1) // block_size
|
|
|
|
|
|
num_partitions = (seq_len + _PARTITION_SIZE - 1) // _PARTITION_SIZE
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
# Batched KV gather: ONE index_select, ONE reshape
|
|
|
|
|
|
# Pattern: avoid per-block Python loop (CCCL does this via
|
|
|
|
|
|
# block-cooperative load, we do it via batched indexing)
|
2026-07-30 15:44:46 +00:00
|
|
|
|
# =============================================================
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
blk_ids = block_tables[seq_idx, :num_blocks_seq]
|
|
|
|
|
|
|
|
|
|
|
|
# Key: [nblk, kv_h, d/x, blk_sz, x] → [nblk*blk_sz, kv_h, d]
|
|
|
|
|
|
k_gathered = key_cache[blk_ids]
|
|
|
|
|
|
k_flat = (k_gathered
|
|
|
|
|
|
.permute(0, 3, 1, 2, 4)
|
|
|
|
|
|
.reshape(-1, num_kv_heads, head_size))[:seq_len]
|
|
|
|
|
|
|
|
|
|
|
|
# Value: [nblk, kv_h, d, blk_sz] → [nblk*blk_sz, kv_h, d]
|
|
|
|
|
|
v_flat = (value_cache[blk_ids]
|
|
|
|
|
|
.permute(0, 3, 1, 2)
|
|
|
|
|
|
.reshape(-1, num_kv_heads, head_size))[:seq_len]
|
|
|
|
|
|
|
[OPT] PagedAttention V2 implementation — fill the NotImplementedError hole
The single biggest performance bottleneck in the baseline:
paged_attention_v2 = raise NotImplementedError()
paged_attn.py: use_v1 = True (hardcoded to avoid calling V2)
V1 limitation: processes entire KV sequence in one kernel launch.
For seq_len=100K, this is a single massive attention computation.
V2: splits into PARTITION_SIZE=512 chunks, runs them in parallel,
then reduces with log-sum-exp. 195 parallel partitions vs 1.
Implementation (paged_attention_v2_pytorch.py):
Phase 1: Per-partition attention
- For each (seq, head, partition): compute QK^T, softmax, weighted V sum
- Store partial: tmp_output, exp_sums, max_logits (per partition)
Phase 2: Cross-partition reduction (log-sum-exp)
- global_max = max(max_logits across partitions)
- rescale = exp(partition_max - global_max) × partition_exp_sum
- output = Σ (rescale / total_sum) × partition_output
This is the same algorithm as vllm's paged_attention_v2_kernel.cu:
- The reduction pattern is identical to CCCL's block_reduce_warp_reductions
(combine partial statistics from independent segments)
- The online softmax tiling is the same as Flash Attention's partitioning
Integration:
- patch_paged_attention_v2.py patches _custom_ops.py and paged_attn.py
- Removes use_v1=True hardcode → V2 used for seq_len > 8192
- Dockerfile adds the patch step
This is a PyTorch implementation (no CUDA compilation needed).
Next step: if /usr/local/corex/ has ixcc or nvcc-compatible compiler,
replace with compiled CUDA kernel for further speedup.
2026-07-30 15:40:14 +00:00
|
|
|
|
if k_scale != 1.0:
|
2026-07-30 15:44:46 +00:00
|
|
|
|
k_flat = k_flat.float().mul_(k_scale)
|
[OPT] PagedAttention V2 implementation — fill the NotImplementedError hole
The single biggest performance bottleneck in the baseline:
paged_attention_v2 = raise NotImplementedError()
paged_attn.py: use_v1 = True (hardcoded to avoid calling V2)
V1 limitation: processes entire KV sequence in one kernel launch.
For seq_len=100K, this is a single massive attention computation.
V2: splits into PARTITION_SIZE=512 chunks, runs them in parallel,
then reduces with log-sum-exp. 195 parallel partitions vs 1.
Implementation (paged_attention_v2_pytorch.py):
Phase 1: Per-partition attention
- For each (seq, head, partition): compute QK^T, softmax, weighted V sum
- Store partial: tmp_output, exp_sums, max_logits (per partition)
Phase 2: Cross-partition reduction (log-sum-exp)
- global_max = max(max_logits across partitions)
- rescale = exp(partition_max - global_max) × partition_exp_sum
- output = Σ (rescale / total_sum) × partition_output
This is the same algorithm as vllm's paged_attention_v2_kernel.cu:
- The reduction pattern is identical to CCCL's block_reduce_warp_reductions
(combine partial statistics from independent segments)
- The online softmax tiling is the same as Flash Attention's partitioning
Integration:
- patch_paged_attention_v2.py patches _custom_ops.py and paged_attn.py
- Removes use_v1=True hardcode → V2 used for seq_len > 8192
- Dockerfile adds the patch step
This is a PyTorch implementation (no CUDA compilation needed).
Next step: if /usr/local/corex/ has ixcc or nvcc-compatible compiler,
replace with compiled CUDA kernel for further speedup.
2026-07-30 15:40:14 +00:00
|
|
|
|
if v_scale != 1.0:
|
2026-07-30 15:44:46 +00:00
|
|
|
|
v_flat = v_flat.float().mul_(v_scale)
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================
|
[OPT] GQA broadcast in V2 — eliminate 1GB/step memory allocation
Qwen3.6: num_heads=24, num_kv_heads=4, gqa_ratio=6, head_dim=256
Before (expand GQA then bmm):
k_flat: [100K, 4, 256] → expand to [100K, 24, 256] → contiguous
Memory: 100K × 24 × 256 × 2B = 1.2GB allocated per decode step
Then: [24, 256, 100K] @ [24, 1, 256]^T → scores
After (broadcast without materializing):
k_kv: [100K, 4, 256] → [4, 256, 100K] (no expansion)
q: [24, 256] → [4, 6, 1, 256]
scores: matmul([4, 6, 1, 256], [4, 1, 256, 100K]) → [4, 6, 100K]
Broadcasting handles GQA — K stays at kv_heads size.
Memory: 100K × 4 × 256 × 2B = 200MB (6x reduction)
For 100K context generating 1000 tokens:
Old: 1000 × 1.2GB = 1.2TB total memory traffic for GQA expansion alone
New: 1000 × 200MB = 200GB total (saved 1TB of unnecessary data movement)
V weighted sum still needs GQA expansion (V @ scores requires matching dims),
but the dominant cost (Q @ K^T) is now broadcast.
2026-07-30 16:13:54 +00:00
|
|
|
|
# GQA broadcast: avoid materializing the expanded KV tensor
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
#
|
[OPT] GQA broadcast in V2 — eliminate 1GB/step memory allocation
Qwen3.6: num_heads=24, num_kv_heads=4, gqa_ratio=6, head_dim=256
Before (expand GQA then bmm):
k_flat: [100K, 4, 256] → expand to [100K, 24, 256] → contiguous
Memory: 100K × 24 × 256 × 2B = 1.2GB allocated per decode step
Then: [24, 256, 100K] @ [24, 1, 256]^T → scores
After (broadcast without materializing):
k_kv: [100K, 4, 256] → [4, 256, 100K] (no expansion)
q: [24, 256] → [4, 6, 1, 256]
scores: matmul([4, 6, 1, 256], [4, 1, 256, 100K]) → [4, 6, 100K]
Broadcasting handles GQA — K stays at kv_heads size.
Memory: 100K × 4 × 256 × 2B = 200MB (6x reduction)
For 100K context generating 1000 tokens:
Old: 1000 × 1.2GB = 1.2TB total memory traffic for GQA expansion alone
New: 1000 × 200MB = 200GB total (saved 1TB of unnecessary data movement)
V weighted sum still needs GQA expansion (V @ scores requires matching dims),
but the dominant cost (Q @ K^T) is now broadcast.
2026-07-30 16:13:54 +00:00
|
|
|
|
# Qwen3.6: H=24, kv_h=4, gqa_ratio=6, head_dim=256
|
|
|
|
|
|
# Old: expand kv_h→H then contiguous → allocates seq_len×H×d (1.2GB at 100K)
|
|
|
|
|
|
# New: reshape Q as [kv_h, gqa, 1, d], K as [kv_h, 1, d, seq_len]
|
|
|
|
|
|
# → bmm with broadcasting → [kv_h, gqa, 1, seq_len]
|
|
|
|
|
|
# → reshape to [H, seq_len]
|
|
|
|
|
|
# Saves: gqa_ratio × memory (6x for Qwen3.6 = 1GB per decode step)
|
2026-07-30 15:44:46 +00:00
|
|
|
|
# =============================================================
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
q = query[seq_idx].float() # [H, d]
|
2026-07-30 15:44:46 +00:00
|
|
|
|
|
[OPT] GQA broadcast in V2 — eliminate 1GB/step memory allocation
Qwen3.6: num_heads=24, num_kv_heads=4, gqa_ratio=6, head_dim=256
Before (expand GQA then bmm):
k_flat: [100K, 4, 256] → expand to [100K, 24, 256] → contiguous
Memory: 100K × 24 × 256 × 2B = 1.2GB allocated per decode step
Then: [24, 256, 100K] @ [24, 1, 256]^T → scores
After (broadcast without materializing):
k_kv: [100K, 4, 256] → [4, 256, 100K] (no expansion)
q: [24, 256] → [4, 6, 1, 256]
scores: matmul([4, 6, 1, 256], [4, 1, 256, 100K]) → [4, 6, 100K]
Broadcasting handles GQA — K stays at kv_heads size.
Memory: 100K × 4 × 256 × 2B = 200MB (6x reduction)
For 100K context generating 1000 tokens:
Old: 1000 × 1.2GB = 1.2TB total memory traffic for GQA expansion alone
New: 1000 × 200MB = 200GB total (saved 1TB of unnecessary data movement)
V weighted sum still needs GQA expansion (V @ scores requires matching dims),
but the dominant cost (Q @ K^T) is now broadcast.
2026-07-30 16:13:54 +00:00
|
|
|
|
if gqa_ratio > 1:
|
|
|
|
|
|
# K: [seq_len, kv_h, d] → [kv_h, d, seq_len] (no GQA expansion)
|
|
|
|
|
|
k_kv = k_flat.permute(1, 2, 0).float().contiguous() # [kv_h, d, seq_len]
|
|
|
|
|
|
v_kv = v_flat.permute(1, 0, 2).float().contiguous() # [kv_h, seq_len, d]
|
|
|
|
|
|
|
|
|
|
|
|
# Q: [H, d] → [kv_h, gqa, 1, d]
|
|
|
|
|
|
q_grouped = q.view(num_kv_heads, gqa_ratio, 1, head_size)
|
|
|
|
|
|
|
|
|
|
|
|
# Scores: [kv_h, gqa, 1, d] @ [kv_h, 1, d, seq_len] → [kv_h, gqa, 1, seq_len]
|
|
|
|
|
|
scores_all = torch.matmul(q_grouped, k_kv.unsqueeze(1)).squeeze(2) # [kv_h, gqa, seq_len]
|
|
|
|
|
|
scores_all = scores_all.reshape(num_heads, seq_len) * scale # [H, seq_len]
|
|
|
|
|
|
else:
|
|
|
|
|
|
k_t = k_flat.permute(1, 2, 0).float().contiguous() # [H, d, seq_len]
|
|
|
|
|
|
scores_all = torch.bmm(q.unsqueeze(1), k_t).squeeze(1) * scale # [H, seq_len]
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
|
|
|
|
|
|
# Alibi bias (if needed)
|
|
|
|
|
|
if alibi_slopes is not None:
|
|
|
|
|
|
positions = torch.arange(seq_len, device=query.device, dtype=torch.float32)
|
|
|
|
|
|
scores_all = scores_all + alibi_slopes.unsqueeze(1) * positions.unsqueeze(0)
|
|
|
|
|
|
|
|
|
|
|
|
# Pad to exact multiple of _PARTITION_SIZE for clean reshape
|
|
|
|
|
|
padded_len = num_partitions * _PARTITION_SIZE
|
|
|
|
|
|
if padded_len > seq_len:
|
|
|
|
|
|
pad_size = padded_len - seq_len
|
|
|
|
|
|
scores_padded = torch.full(
|
|
|
|
|
|
(num_heads, padded_len), float('-inf'),
|
|
|
|
|
|
dtype=scores_all.dtype, device=scores_all.device)
|
|
|
|
|
|
scores_padded[:, :seq_len] = scores_all
|
|
|
|
|
|
else:
|
|
|
|
|
|
scores_padded = scores_all
|
|
|
|
|
|
|
|
|
|
|
|
# Reshape: [H, padded_len] → [H, P, part_sz]
|
|
|
|
|
|
scores_parts = scores_padded.view(num_heads, num_partitions, _PARTITION_SIZE)
|
|
|
|
|
|
|
|
|
|
|
|
# Per-partition online softmax (vectorized over H and P simultaneously)
|
|
|
|
|
|
# Pattern from CCCL summary_statistics: compute (max, sum) in one pass
|
|
|
|
|
|
part_max = scores_parts.max(dim=-1).values # [H, P]
|
|
|
|
|
|
scores_exp = torch.exp(scores_parts - part_max.unsqueeze(-1)) # [H, P, part_sz]
|
|
|
|
|
|
part_sum = scores_exp.sum(dim=-1) # [H, P]
|
|
|
|
|
|
|
|
|
|
|
|
# Weighted values per partition: need V reshaped the same way
|
|
|
|
|
|
# V: [seq_len, H, d] → pad → [padded_len, H, d] → [H, P, part_sz, d]
|
[OPT] GQA broadcast in V2 — eliminate 1GB/step memory allocation
Qwen3.6: num_heads=24, num_kv_heads=4, gqa_ratio=6, head_dim=256
Before (expand GQA then bmm):
k_flat: [100K, 4, 256] → expand to [100K, 24, 256] → contiguous
Memory: 100K × 24 × 256 × 2B = 1.2GB allocated per decode step
Then: [24, 256, 100K] @ [24, 1, 256]^T → scores
After (broadcast without materializing):
k_kv: [100K, 4, 256] → [4, 256, 100K] (no expansion)
q: [24, 256] → [4, 6, 1, 256]
scores: matmul([4, 6, 1, 256], [4, 1, 256, 100K]) → [4, 6, 100K]
Broadcasting handles GQA — K stays at kv_heads size.
Memory: 100K × 4 × 256 × 2B = 200MB (6x reduction)
For 100K context generating 1000 tokens:
Old: 1000 × 1.2GB = 1.2TB total memory traffic for GQA expansion alone
New: 1000 × 200MB = 200GB total (saved 1TB of unnecessary data movement)
V weighted sum still needs GQA expansion (V @ scores requires matching dims),
but the dominant cost (Q @ K^T) is now broadcast.
2026-07-30 16:13:54 +00:00
|
|
|
|
if gqa_ratio > 1:
|
|
|
|
|
|
v_perm = v_kv # already [kv_h, seq_len, d], no GQA expansion needed
|
|
|
|
|
|
# Will handle GQA in the bmm below via broadcast
|
|
|
|
|
|
else:
|
|
|
|
|
|
v_perm = v_flat.permute(1, 0, 2).float().contiguous() # [H, seq_len, d]
|
[FIX] V2 shape mismatch bug — v_padded used num_heads for kv_h tensor
Bug: After GQA broadcast optimization, v_perm was [kv_h, seq_len, d]
in the GQA path, but unconditional v_padded allocation used num_heads:
v_padded = torch.zeros((num_heads, padded_len, head_size))
v_padded[:, :seq_len, :] = v_perm # [24, padded, d] vs [4, seq, d] → CRASH
Fix: v_padded/v_parts allocation is now inside the non-GQA else branch.
GQA branch uses its own v_padded_kv with correct [kv_h, padded, d] shape.
This was a real runtime bug — V2 would have crashed on first call
for any GQA model (Qwen3.6, Llama, etc.).
2026-07-31 03:52:23 +00:00
|
|
|
|
# Weighted V sum per partition
|
|
|
|
|
|
# NOTE: v_perm shape differs by GQA mode:
|
|
|
|
|
|
# GQA: v_perm = v_kv = [kv_h, seq_len, d]
|
|
|
|
|
|
# No GQA: v_perm = [H, seq_len, d]
|
[OPT] Complete GQA broadcast — V weighted sum also avoids expansion
Previous commit broadcast Q@K^T (saved 1GB/step).
This commit broadcasts scores@V too (saves 2GB/step).
Before: V expanded from [kv_h, padded_len, d] to [H, padded_len, d]
4×100K×256×4B → 24×100K×256×4B = 400MB → 2.4GB allocation
After: broadcast matmul at kv_h level
se: [kv_h, gqa, P, 1, part_sz] @ V: [kv_h, 1, P, part_sz, d]
→ [kv_h, gqa, P, 1, d] → reshape to [H, P, d]
V stays at kv_h size: 400MB (no 2.4GB allocation)
Total per-decode-step memory for 100K context:
Before all GQA opts: 3.6GB (K expansion + V expansion)
After: 600MB (6x total reduction from GQA ratio=6)
This is the CCCL insight applied: transform_reduce with a compound type.
Instead of expanding to full head count then reducing, keep the reduction
at the minimal group size and broadcast the grouping dimension.
2026-07-30 16:15:34 +00:00
|
|
|
|
# scores_exp: [H, P, part_sz] → [kv_h, gqa, P, part_sz]
|
|
|
|
|
|
# v_perm: [kv_h, seq_len, d] → [kv_h, P, part_sz, d]
|
[OPT] GQA broadcast in V2 — eliminate 1GB/step memory allocation
Qwen3.6: num_heads=24, num_kv_heads=4, gqa_ratio=6, head_dim=256
Before (expand GQA then bmm):
k_flat: [100K, 4, 256] → expand to [100K, 24, 256] → contiguous
Memory: 100K × 24 × 256 × 2B = 1.2GB allocated per decode step
Then: [24, 256, 100K] @ [24, 1, 256]^T → scores
After (broadcast without materializing):
k_kv: [100K, 4, 256] → [4, 256, 100K] (no expansion)
q: [24, 256] → [4, 6, 1, 256]
scores: matmul([4, 6, 1, 256], [4, 1, 256, 100K]) → [4, 6, 100K]
Broadcasting handles GQA — K stays at kv_heads size.
Memory: 100K × 4 × 256 × 2B = 200MB (6x reduction)
For 100K context generating 1000 tokens:
Old: 1000 × 1.2GB = 1.2TB total memory traffic for GQA expansion alone
New: 1000 × 200MB = 200GB total (saved 1TB of unnecessary data movement)
V weighted sum still needs GQA expansion (V @ scores requires matching dims),
but the dominant cost (Q @ K^T) is now broadcast.
2026-07-30 16:13:54 +00:00
|
|
|
|
if gqa_ratio > 1:
|
|
|
|
|
|
se_grouped = scores_exp.view(num_kv_heads, gqa_ratio, num_partitions, _PARTITION_SIZE)
|
[OPT] Complete GQA broadcast — V weighted sum also avoids expansion
Previous commit broadcast Q@K^T (saved 1GB/step).
This commit broadcasts scores@V too (saves 2GB/step).
Before: V expanded from [kv_h, padded_len, d] to [H, padded_len, d]
4×100K×256×4B → 24×100K×256×4B = 400MB → 2.4GB allocation
After: broadcast matmul at kv_h level
se: [kv_h, gqa, P, 1, part_sz] @ V: [kv_h, 1, P, part_sz, d]
→ [kv_h, gqa, P, 1, d] → reshape to [H, P, d]
V stays at kv_h size: 400MB (no 2.4GB allocation)
Total per-decode-step memory for 100K context:
Before all GQA opts: 3.6GB (K expansion + V expansion)
After: 600MB (6x total reduction from GQA ratio=6)
This is the CCCL insight applied: transform_reduce with a compound type.
Instead of expanding to full head count then reducing, keep the reduction
at the minimal group size and broadcast the grouping dimension.
2026-07-30 16:15:34 +00:00
|
|
|
|
# V: pad and reshape to [kv_h, P, part_sz, d]
|
[OPT] GQA broadcast in V2 — eliminate 1GB/step memory allocation
Qwen3.6: num_heads=24, num_kv_heads=4, gqa_ratio=6, head_dim=256
Before (expand GQA then bmm):
k_flat: [100K, 4, 256] → expand to [100K, 24, 256] → contiguous
Memory: 100K × 24 × 256 × 2B = 1.2GB allocated per decode step
Then: [24, 256, 100K] @ [24, 1, 256]^T → scores
After (broadcast without materializing):
k_kv: [100K, 4, 256] → [4, 256, 100K] (no expansion)
q: [24, 256] → [4, 6, 1, 256]
scores: matmul([4, 6, 1, 256], [4, 1, 256, 100K]) → [4, 6, 100K]
Broadcasting handles GQA — K stays at kv_heads size.
Memory: 100K × 4 × 256 × 2B = 200MB (6x reduction)
For 100K context generating 1000 tokens:
Old: 1000 × 1.2GB = 1.2TB total memory traffic for GQA expansion alone
New: 1000 × 200MB = 200GB total (saved 1TB of unnecessary data movement)
V weighted sum still needs GQA expansion (V @ scores requires matching dims),
but the dominant cost (Q @ K^T) is now broadcast.
2026-07-30 16:13:54 +00:00
|
|
|
|
if padded_len > seq_len:
|
[OPT] Complete GQA broadcast — V weighted sum also avoids expansion
Previous commit broadcast Q@K^T (saved 1GB/step).
This commit broadcasts scores@V too (saves 2GB/step).
Before: V expanded from [kv_h, padded_len, d] to [H, padded_len, d]
4×100K×256×4B → 24×100K×256×4B = 400MB → 2.4GB allocation
After: broadcast matmul at kv_h level
se: [kv_h, gqa, P, 1, part_sz] @ V: [kv_h, 1, P, part_sz, d]
→ [kv_h, gqa, P, 1, d] → reshape to [H, P, d]
V stays at kv_h size: 400MB (no 2.4GB allocation)
Total per-decode-step memory for 100K context:
Before all GQA opts: 3.6GB (K expansion + V expansion)
After: 600MB (6x total reduction from GQA ratio=6)
This is the CCCL insight applied: transform_reduce with a compound type.
Instead of expanding to full head count then reducing, keep the reduction
at the minimal group size and broadcast the grouping dimension.
2026-07-30 16:15:34 +00:00
|
|
|
|
v_padded_kv = torch.zeros(
|
|
|
|
|
|
(num_kv_heads, padded_len, head_size),
|
|
|
|
|
|
dtype=v_kv.dtype, device=v_kv.device)
|
|
|
|
|
|
v_padded_kv[:, :seq_len, :] = v_kv
|
[OPT] GQA broadcast in V2 — eliminate 1GB/step memory allocation
Qwen3.6: num_heads=24, num_kv_heads=4, gqa_ratio=6, head_dim=256
Before (expand GQA then bmm):
k_flat: [100K, 4, 256] → expand to [100K, 24, 256] → contiguous
Memory: 100K × 24 × 256 × 2B = 1.2GB allocated per decode step
Then: [24, 256, 100K] @ [24, 1, 256]^T → scores
After (broadcast without materializing):
k_kv: [100K, 4, 256] → [4, 256, 100K] (no expansion)
q: [24, 256] → [4, 6, 1, 256]
scores: matmul([4, 6, 1, 256], [4, 1, 256, 100K]) → [4, 6, 100K]
Broadcasting handles GQA — K stays at kv_heads size.
Memory: 100K × 4 × 256 × 2B = 200MB (6x reduction)
For 100K context generating 1000 tokens:
Old: 1000 × 1.2GB = 1.2TB total memory traffic for GQA expansion alone
New: 1000 × 200MB = 200GB total (saved 1TB of unnecessary data movement)
V weighted sum still needs GQA expansion (V @ scores requires matching dims),
but the dominant cost (Q @ K^T) is now broadcast.
2026-07-30 16:13:54 +00:00
|
|
|
|
else:
|
[OPT] Complete GQA broadcast — V weighted sum also avoids expansion
Previous commit broadcast Q@K^T (saved 1GB/step).
This commit broadcasts scores@V too (saves 2GB/step).
Before: V expanded from [kv_h, padded_len, d] to [H, padded_len, d]
4×100K×256×4B → 24×100K×256×4B = 400MB → 2.4GB allocation
After: broadcast matmul at kv_h level
se: [kv_h, gqa, P, 1, part_sz] @ V: [kv_h, 1, P, part_sz, d]
→ [kv_h, gqa, P, 1, d] → reshape to [H, P, d]
V stays at kv_h size: 400MB (no 2.4GB allocation)
Total per-decode-step memory for 100K context:
Before all GQA opts: 3.6GB (K expansion + V expansion)
After: 600MB (6x total reduction from GQA ratio=6)
This is the CCCL insight applied: transform_reduce with a compound type.
Instead of expanding to full head count then reducing, keep the reduction
at the minimal group size and broadcast the grouping dimension.
2026-07-30 16:15:34 +00:00
|
|
|
|
v_padded_kv = v_kv
|
|
|
|
|
|
v_parts_kv = v_padded_kv.view(num_kv_heads, num_partitions, _PARTITION_SIZE, head_size)
|
|
|
|
|
|
# Broadcast: [kv_h, gqa, P, 1, part_sz] @ [kv_h, 1, P, part_sz, d]
|
|
|
|
|
|
# → [kv_h, gqa, P, 1, d]
|
|
|
|
|
|
part_out_grouped = torch.matmul(
|
|
|
|
|
|
se_grouped.unsqueeze(3), # [kv_h, gqa, P, 1, part_sz]
|
|
|
|
|
|
v_parts_kv.unsqueeze(1) # [kv_h, 1, P, part_sz, d]
|
|
|
|
|
|
).squeeze(3) # [kv_h, gqa, P, d]
|
|
|
|
|
|
part_out = part_out_grouped.reshape(num_heads, num_partitions, head_size)
|
[OPT] GQA broadcast in V2 — eliminate 1GB/step memory allocation
Qwen3.6: num_heads=24, num_kv_heads=4, gqa_ratio=6, head_dim=256
Before (expand GQA then bmm):
k_flat: [100K, 4, 256] → expand to [100K, 24, 256] → contiguous
Memory: 100K × 24 × 256 × 2B = 1.2GB allocated per decode step
Then: [24, 256, 100K] @ [24, 1, 256]^T → scores
After (broadcast without materializing):
k_kv: [100K, 4, 256] → [4, 256, 100K] (no expansion)
q: [24, 256] → [4, 6, 1, 256]
scores: matmul([4, 6, 1, 256], [4, 1, 256, 100K]) → [4, 6, 100K]
Broadcasting handles GQA — K stays at kv_heads size.
Memory: 100K × 4 × 256 × 2B = 200MB (6x reduction)
For 100K context generating 1000 tokens:
Old: 1000 × 1.2GB = 1.2TB total memory traffic for GQA expansion alone
New: 1000 × 200MB = 200GB total (saved 1TB of unnecessary data movement)
V weighted sum still needs GQA expansion (V @ scores requires matching dims),
but the dominant cost (Q @ K^T) is now broadcast.
2026-07-30 16:13:54 +00:00
|
|
|
|
else:
|
[FIX] V2 shape mismatch bug — v_padded used num_heads for kv_h tensor
Bug: After GQA broadcast optimization, v_perm was [kv_h, seq_len, d]
in the GQA path, but unconditional v_padded allocation used num_heads:
v_padded = torch.zeros((num_heads, padded_len, head_size))
v_padded[:, :seq_len, :] = v_perm # [24, padded, d] vs [4, seq, d] → CRASH
Fix: v_padded/v_parts allocation is now inside the non-GQA else branch.
GQA branch uses its own v_padded_kv with correct [kv_h, padded, d] shape.
This was a real runtime bug — V2 would have crashed on first call
for any GQA model (Qwen3.6, Llama, etc.).
2026-07-31 03:52:23 +00:00
|
|
|
|
# Non-GQA: v_perm is [H, seq_len, d], pad and reshape normally
|
|
|
|
|
|
if padded_len > seq_len:
|
|
|
|
|
|
v_padded = torch.zeros(
|
|
|
|
|
|
(num_heads, padded_len, head_size),
|
|
|
|
|
|
dtype=v_perm.dtype, device=v_perm.device)
|
|
|
|
|
|
v_padded[:, :seq_len, :] = v_perm
|
|
|
|
|
|
else:
|
|
|
|
|
|
v_padded = v_perm
|
|
|
|
|
|
v_parts = v_padded.view(num_heads, num_partitions, _PARTITION_SIZE, head_size)
|
[OPT] GQA broadcast in V2 — eliminate 1GB/step memory allocation
Qwen3.6: num_heads=24, num_kv_heads=4, gqa_ratio=6, head_dim=256
Before (expand GQA then bmm):
k_flat: [100K, 4, 256] → expand to [100K, 24, 256] → contiguous
Memory: 100K × 24 × 256 × 2B = 1.2GB allocated per decode step
Then: [24, 256, 100K] @ [24, 1, 256]^T → scores
After (broadcast without materializing):
k_kv: [100K, 4, 256] → [4, 256, 100K] (no expansion)
q: [24, 256] → [4, 6, 1, 256]
scores: matmul([4, 6, 1, 256], [4, 1, 256, 100K]) → [4, 6, 100K]
Broadcasting handles GQA — K stays at kv_heads size.
Memory: 100K × 4 × 256 × 2B = 200MB (6x reduction)
For 100K context generating 1000 tokens:
Old: 1000 × 1.2GB = 1.2TB total memory traffic for GQA expansion alone
New: 1000 × 200MB = 200GB total (saved 1TB of unnecessary data movement)
V weighted sum still needs GQA expansion (V @ scores requires matching dims),
but the dominant cost (Q @ K^T) is now broadcast.
2026-07-30 16:13:54 +00:00
|
|
|
|
HP = num_heads * num_partitions
|
|
|
|
|
|
scores_exp_flat = scores_exp.reshape(HP, 1, _PARTITION_SIZE)
|
|
|
|
|
|
v_parts_flat = v_parts.reshape(HP, _PARTITION_SIZE, head_size)
|
|
|
|
|
|
part_out_flat = torch.bmm(scores_exp_flat, v_parts_flat) # [HP, 1, d]
|
|
|
|
|
|
part_out = part_out_flat.view(num_heads, num_partitions, head_size) # [H, P, d]
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
|
|
|
|
|
|
# Store partition results
|
|
|
|
|
|
max_logits[seq_idx, :, :num_partitions] = part_max
|
|
|
|
|
|
exp_sums[seq_idx, :, :num_partitions] = part_sum
|
|
|
|
|
|
tmp_output[seq_idx, :, :num_partitions, :] = part_out.to(tmp_output.dtype)
|
2026-07-30 15:44:46 +00:00
|
|
|
|
|
|
|
|
|
|
# =============================================================
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
# Phase 2: Cross-partition reduction (CCCL binary_op pattern)
|
|
|
|
|
|
#
|
|
|
|
|
|
# This is the summary_statistics.binary_op pattern:
|
|
|
|
|
|
# Combine (max_a, sum_a, out_a) ⊕ (max_b, sum_b, out_b)
|
|
|
|
|
|
# using numerically stable log-sum-exp rescaling.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Fully vectorized — no loop over partitions.
|
2026-07-30 15:44:46 +00:00
|
|
|
|
# =============================================================
|
|
|
|
|
|
pm = max_logits[seq_idx, :, :num_partitions] # [H, P]
|
|
|
|
|
|
ps = exp_sums[seq_idx, :, :num_partitions] # [H, P]
|
|
|
|
|
|
po = tmp_output[seq_idx, :, :num_partitions, :] # [H, P, d]
|
|
|
|
|
|
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
global_max = pm.max(dim=-1).values # [H]
|
|
|
|
|
|
rescale = torch.exp(pm - global_max.unsqueeze(-1)) * ps # [H, P]
|
|
|
|
|
|
total = rescale.sum(dim=-1, keepdim=True) # [H, 1]
|
|
|
|
|
|
weights = rescale / total # [H, P]
|
2026-07-30 15:44:46 +00:00
|
|
|
|
|
[OPT] V2 single-bmm: 195 kernel launches → 3 (CCCL transform_reduce pattern)
Phase 1 rewrite:
Before: for p in range(195): torch.bmm(Q, K_partition_p)
After: scores = torch.bmm(Q, K_all) # ONE launch for all 100K tokens
scores_parts = scores.view(H, P, part_sz) # reshape, no copy
part_out = torch.bmm(scores_exp_flat, v_parts_flat) # ONE launch
195 Python→CUDA round-trips → 2 round-trips.
Architecture informed by CCCL:
- summary_statistics.cu: fuse (max, exp_sum, weighted_output) computation
into a single reduction pass over the data. We do this by computing
Q@K^T over the ENTIRE sequence in one bmm, then reshaping to partitions
for the softmax statistics — the data is only read once from HBM.
- block_reduce_warp_reductions.cuh: Phase 2 reduction combines partition
statistics using the same (rescale, accumulate) pattern as CUB's
cross-warp aggregate merging.
Phase 2 (unchanged, already vectorized):
global_max + rescale + torch.bmm(weights, partition_outputs)
Total GPU kernel launches per decode step:
Before: 1 (gather) + 195 (Q@K) + 195 (scores@V) + 1 (reduce) = 392
After: 1 (gather) + 1 (Q@K_all) + 1 (scores_exp@V) + 1 (reduce) = 4
KV gather also stays batched: key_cache[blk_ids] is one index_select.
2026-07-30 15:58:26 +00:00
|
|
|
|
# [H, 1, P] @ [H, P, d] → [H, 1, d] → [H, d]
|
|
|
|
|
|
final = torch.bmm(weights.unsqueeze(1), po.float()).squeeze(1) # [H, d]
|
2026-07-30 15:44:46 +00:00
|
|
|
|
output[seq_idx] = final.to(output.dtype)
|