183 lines
6.3 KiB
Python
183 lines
6.3 KiB
Python
|
|
"""
|
|||
|
|
EngineX Attention operators.
|
|||
|
|
|
|||
|
|
Sub168 log shows three attention paths:
|
|||
|
|
1. CoreX FA2 packed prefill: B=2 Hq=4 Hkv=1 D=256 (full attention layers)
|
|||
|
|
2. CoreX paged FA2 chunked prefill: B=1 Hq=4 Hkv=1 D=256 cache_blocks=2
|
|||
|
|
3. CoreX GDN (handled in gdn.py, 4 of 36 layers)
|
|||
|
|
|
|||
|
|
Our image has:
|
|||
|
|
- libixattn.so (present but not wired)
|
|||
|
|
- ixformer.flash_attn_varlen_func (available)
|
|||
|
|
- xformers SDPA (current fallback, patched for head_dim=256)
|
|||
|
|
|
|||
|
|
CCCL parallel:
|
|||
|
|
paged_attention_v1 = dispatch_reduce (reduce over KV blocks)
|
|||
|
|
paged_attention_v2 = dispatch_reduce two-pass (partition-level reduce + final reduce)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import math
|
|||
|
|
from typing import List, Optional
|
|||
|
|
|
|||
|
|
import torch
|
|||
|
|
import torch.nn.functional as F
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fa2_xformers_fallback(
|
|||
|
|
query: torch.Tensor,
|
|||
|
|
key: torch.Tensor,
|
|||
|
|
value: torch.Tensor,
|
|||
|
|
cu_seqlens_q: Optional[torch.Tensor] = None,
|
|||
|
|
cu_seqlens_k: Optional[torch.Tensor] = None,
|
|||
|
|
max_seqlen_q: int = 0,
|
|||
|
|
max_seqlen_k: int = 0,
|
|||
|
|
dropout_p: float = 0.0,
|
|||
|
|
softmax_scale: Optional[float] = None,
|
|||
|
|
causal: bool = False,
|
|||
|
|
) -> torch.Tensor:
|
|||
|
|
"""
|
|||
|
|
xformers SDPA fallback for FA2.
|
|||
|
|
This is what we currently use — works but slower than native FA2.
|
|||
|
|
Head_dim=256 bypass already applied in patch_xformers_sdpa_*.py.
|
|||
|
|
"""
|
|||
|
|
if softmax_scale is None:
|
|||
|
|
softmax_scale = 1.0 / math.sqrt(query.shape[-1])
|
|||
|
|
|
|||
|
|
# Standard scaled dot product attention
|
|||
|
|
attn_weights = torch.matmul(query, key.transpose(-2, -1)) * softmax_scale
|
|||
|
|
|
|||
|
|
if causal and attn_weights.shape[-2] > 1:
|
|||
|
|
L = attn_weights.shape[-2]
|
|||
|
|
S = attn_weights.shape[-1]
|
|||
|
|
mask = torch.triu(
|
|||
|
|
torch.full((L, S), float('-inf'), device=query.device),
|
|||
|
|
diagonal=S - L + 1
|
|||
|
|
)
|
|||
|
|
attn_weights = attn_weights + mask
|
|||
|
|
|
|||
|
|
attn_weights = F.softmax(attn_weights, dim=-1)
|
|||
|
|
output = torch.matmul(attn_weights, value)
|
|||
|
|
return output
|
|||
|
|
|
|||
|
|
|
|||
|
|
def paged_attention_v1_pytorch(
|
|||
|
|
output: torch.Tensor, # [num_seqs, num_heads, head_size]
|
|||
|
|
query: torch.Tensor, # [num_seqs, num_heads, head_size]
|
|||
|
|
key_cache: torch.Tensor, # [num_blocks, num_kv_heads, block_size, head_size]
|
|||
|
|
value_cache: torch.Tensor, # [num_blocks, num_kv_heads, block_size, head_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] = None,
|
|||
|
|
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:
|
|||
|
|
"""
|
|||
|
|
Paged attention v1 — single-pass reduce over all KV blocks.
|
|||
|
|
|
|||
|
|
CCCL parallel: dispatch_reduce single-tile kernel.
|
|||
|
|
For short sequences (< 2 × sm_count × partition_size), v1 is faster
|
|||
|
|
because it avoids the two-pass overhead.
|
|||
|
|
|
|||
|
|
BI-V100 with 16 SMs: threshold ≈ 16 × 2 × 512 = 16384 tokens.
|
|||
|
|
"""
|
|||
|
|
num_seqs = query.shape[0]
|
|||
|
|
num_heads = query.shape[1]
|
|||
|
|
head_size = query.shape[2]
|
|||
|
|
num_queries_per_kv = num_heads // num_kv_heads
|
|||
|
|
|
|||
|
|
for seq_idx in range(num_seqs):
|
|||
|
|
seq_len = seq_lens[seq_idx].item()
|
|||
|
|
if seq_len == 0:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
q = query[seq_idx] # [num_heads, head_size]
|
|||
|
|
|
|||
|
|
num_blocks = (seq_len + block_size - 1) // block_size
|
|||
|
|
keys_list = []
|
|||
|
|
values_list = []
|
|||
|
|
|
|||
|
|
for block_idx in range(num_blocks):
|
|||
|
|
physical_block = block_tables[seq_idx, block_idx].item()
|
|||
|
|
if block_idx == num_blocks - 1:
|
|||
|
|
# Last block may be partial
|
|||
|
|
tokens_in_block = seq_len - block_idx * block_size
|
|||
|
|
else:
|
|||
|
|
tokens_in_block = block_size
|
|||
|
|
|
|||
|
|
k_block = key_cache[physical_block, :, :tokens_in_block, :]
|
|||
|
|
v_block = value_cache[physical_block, :, :tokens_in_block, :]
|
|||
|
|
keys_list.append(k_block)
|
|||
|
|
values_list.append(v_block)
|
|||
|
|
|
|||
|
|
# Concatenate all KV
|
|||
|
|
all_keys = torch.cat(keys_list, dim=1) # [num_kv_heads, seq_len, head_size]
|
|||
|
|
all_values = torch.cat(values_list, dim=1)
|
|||
|
|
|
|||
|
|
# GQA: repeat KV heads
|
|||
|
|
if num_queries_per_kv > 1:
|
|||
|
|
all_keys = all_keys.repeat_interleave(num_queries_per_kv, dim=0)
|
|||
|
|
all_values = all_values.repeat_interleave(num_queries_per_kv, dim=0)
|
|||
|
|
|
|||
|
|
# Attention: q @ k^T → softmax → @ v
|
|||
|
|
attn = torch.einsum('hd,hsd->hs', q, all_keys) * scale
|
|||
|
|
attn = F.softmax(attn, dim=-1)
|
|||
|
|
out = torch.einsum('hs,hsd->hd', attn, all_values)
|
|||
|
|
|
|||
|
|
output[seq_idx].copy_(out)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def paged_attention_v2_pytorch(
|
|||
|
|
output: torch.Tensor,
|
|||
|
|
exp_sums: torch.Tensor, # [num_seqs, num_heads, max_partitions]
|
|||
|
|
max_logits: torch.Tensor, # [num_seqs, num_heads, max_partitions]
|
|||
|
|
tmp_output: torch.Tensor, # [num_seqs, num_heads, max_partitions, head_size]
|
|||
|
|
query: torch.Tensor,
|
|||
|
|
key_cache: torch.Tensor,
|
|||
|
|
value_cache: torch.Tensor,
|
|||
|
|
num_kv_heads: int,
|
|||
|
|
scale: float,
|
|||
|
|
block_tables: torch.Tensor,
|
|||
|
|
seq_lens: torch.Tensor,
|
|||
|
|
block_size: int,
|
|||
|
|
max_seq_len: int,
|
|||
|
|
alibi_slopes: Optional[torch.Tensor] = None,
|
|||
|
|
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:
|
|||
|
|
"""
|
|||
|
|
Paged attention v2 — two-pass reduce with partitioning.
|
|||
|
|
|
|||
|
|
CCCL parallel: dispatch_reduce two-pass pattern.
|
|||
|
|
Pass 1: per-partition reduce (each partition = PARTITION_SIZE KV tokens)
|
|||
|
|
Pass 2: reduce across partitions (log-sum-exp correction)
|
|||
|
|
|
|||
|
|
For BI-V100 with 16 SMs, v2 is better when seq_len > 8192 (multiple
|
|||
|
|
waves of partitions keep all SMs busy).
|
|||
|
|
"""
|
|||
|
|
# For correctness, delegate to v1 — the two-pass optimization
|
|||
|
|
# only matters for perf on long sequences
|
|||
|
|
paged_attention_v1_pytorch(
|
|||
|
|
output, query, key_cache, value_cache,
|
|||
|
|
num_kv_heads, scale, block_tables, seq_lens,
|
|||
|
|
block_size, max_seq_len, alibi_slopes, kv_cache_dtype,
|
|||
|
|
k_scale, v_scale, tp_rank,
|
|||
|
|
blocksparse_local_blocks, blocksparse_vert_stride,
|
|||
|
|
blocksparse_block_size, blocksparse_head_sliding_step,
|
|||
|
|
)
|