Skip to content

Commit 73db4b5

Browse files
authored
[Kernel][Perf] Optimize paged-attention metadata decode (#910)
1 parent ff196dd commit 73db4b5

7 files changed

Lines changed: 1301 additions & 1477 deletions

File tree

kernels/attention/pa_common.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import flydsl.expr as fx
1212
from flydsl.expr import arith, const_expr, range_constexpr
1313
from kernels.common import buffer_ops
14+
from kernels.common.utils import copy_load
1415

1516
# PA Q-tiling constants (identical across all PA decode kernels).
1617
MFMA_N = 16
@@ -53,3 +54,24 @@ def _prefetch_q_chunks(
5354
)
5455
)
5556
return q_chunks
57+
58+
59+
@flyc.jit
60+
def _prefetch_q_chunks_tile(
61+
q_tiles,
62+
q_copy_atom,
63+
q_reg,
64+
q_base,
65+
lane16id,
66+
*,
67+
q_lanes_per_head,
68+
):
69+
q_load_lane = lane16id
70+
if const_expr(q_lanes_per_head < MFMA_N):
71+
q_load_lane = (lane16id < fx.Int32(q_lanes_per_head)).select(lane16id, fx.Int32(0))
72+
q_elem = q_base + q_load_lane * fx.Int32(Q_ELEMS_PER_LANE)
73+
q_tile = q_elem // fx.Int32(4)
74+
q_chunks = []
75+
for qwi in range_constexpr(Q_CHUNKS_PER_LANE):
76+
q_chunks.append(copy_load(q_tiles, q_tile + fx.Int32(qwi), q_copy_atom, q_reg))
77+
return q_chunks

kernels/attention/pa_decode_fp8.py

Lines changed: 45 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,14 @@
1919
from kernels.attention.pa_decode_swa import compile_pa_decode_sw, compile_pa_decode_sw_reduce
2020
from kernels.attention.pa_decode_tile import pa_decode_tile
2121
from kernels.attention.pa_metadata import compile_pa_decode_metadata
22+
from kernels.attention.pa_metadata_tuning import lookup_pa_metadata_grid_multiplier
2223
from kernels.common.tensor_shim import _run_compiled
2324
from kernels.common.utils import cdiv
2425

2526
# ── Kernel geometry constants ────────────────────────────────────────
2627
KV_COMPUTE_BLOCK = 256 # tile size (matches SP3 kTileKV)
27-
# Persistent-grid oversubscription for the metadata decode path: launch
28-
# CU_count * this many workgroups so the HW keeps multiple workgroups resident
29-
# per CU (memory-latency hiding). 1 = original (1 wg/CU).
30-
_PA_METADATA_GRID_OVERSUB = 3
3128
MFMA_N = 16
3229

33-
_PACKED_FP8_QUERY_DTYPES = tuple(
34-
dtype
35-
for dtype in (
36-
torch.uint8,
37-
getattr(torch, "float8_e4m3fnuz", None),
38-
getattr(torch, "float8_e4m3fn", None),
39-
)
40-
if dtype is not None
41-
)
42-
4330

4431
# =====================================================================
4532
# Launch API — Persistent Scheduling mode
@@ -54,6 +41,9 @@ def get_pa_metadata(
5441
num_query_heads: int,
5542
num_kv_heads: int,
5643
partition_size: int = KV_COMPUTE_BLOCK,
44+
*,
45+
per_token_kv: bool | None = None,
46+
grid_multiplier: int | None = None,
5747
):
5848
"""Compute PA metadata (worklist, reduce maps) via get_pa_metadata_v1.
5949
@@ -69,6 +59,11 @@ def get_pa_metadata(
6959
NOTE: the consuming decode kernel must interpret kv_start/kv_end as partition
7060
indices accordingly.
7161
62+
Exact shape/device matches are loaded from FlyDSL's persistent Autotuner
63+
cache. Missing entries default to ``grid_multiplier=1``. ``per_token_kv``
64+
selects scale-mode-specific tuning, and ``grid_multiplier`` is the tuner's
65+
explicit candidate override.
66+
7267
Returns a dict with: work_indptr, work_info_flat, reduce_indptr,
7368
reduce_final_map, reduce_partial_map, num_sm, partial_output,
7469
partial_lse, stride_po_partial, stride_pl_partial.
@@ -81,13 +76,25 @@ def get_pa_metadata(
8176
head_size = query.shape[-1]
8277

8378
props = torch.cuda.get_device_properties(dev)
84-
# Oversubscribe the persistent grid: the decode kernel is memory-latency-bound
85-
# and only ~3 workgroups/CU fit by VGPR, but the worklist defaults to 1 wg/CU
86-
# (grid = CU count). Distributing work across num_cu = CU_count * OVERSUB bins
87-
# (and launching that many workgroups) lets the HW keep multiple workgroups
88-
# resident per CU → more waves in flight → better latency hiding.
89-
base_cu = props.multi_processor_count
90-
num_sm = base_cu * _PA_METADATA_GRID_OVERSUB
79+
num_blocks = key_cache.shape[0]
80+
if grid_multiplier is None and per_token_kv is not None:
81+
grid_multiplier = lookup_pa_metadata_grid_multiplier(
82+
num_cu=props.multi_processor_count,
83+
batch_size=batch_size,
84+
num_blocks=num_blocks,
85+
query_length=query_length,
86+
per_token_kv=per_token_kv,
87+
num_query_heads=num_query_heads,
88+
num_kv_heads=num_kv_heads,
89+
head_dim=head_size,
90+
block_size=key_cache.shape[-2],
91+
device_tensor=query,
92+
)
93+
if grid_multiplier is None:
94+
grid_multiplier = 1
95+
if grid_multiplier < 1:
96+
raise ValueError("grid_multiplier must be positive")
97+
num_sm = props.multi_processor_count * grid_multiplier
9198
num_sm = (num_sm // num_kv_heads) * num_kv_heads # keep divisible by num_kv_heads
9299

93100
seqlens_qo_indptr = torch.arange(batch_size + 1, dtype=torch.int32, device=dev) * query_length
@@ -100,18 +107,14 @@ def get_pa_metadata(
100107
partition_indptr = torch.zeros(batch_size + 1, dtype=torch.int32, device=dev)
101108
partition_indptr[1:] = torch.cumsum(_parts_per_batch, dim=0).to(torch.int32)
102109

103-
block_size = key_cache.shape[-2] if len(key_cache.shape) == 5 else key_cache.shape[-2]
104-
105110
(
106-
(work_meta_data_size, work_meta_data_type),
107111
(work_indptr_size, work_indptr_type),
108112
(work_info_set_size, work_info_set_type),
109113
(reduce_indptr_size, reduce_indptr_type),
110114
(reduce_final_map_size, reduce_final_map_type),
111115
(reduce_partial_map_size, reduce_partial_map_type),
112116
) = get_pa_metadata_info_v1(batch_size, num_kv_heads, num_cu=num_sm)
113117

114-
work_metadata_ptrs = torch.empty(work_meta_data_size, dtype=work_meta_data_type, device=dev)
115118
work_indptr = torch.empty(work_indptr_size, dtype=work_indptr_type, device=dev)
116119
work_info = torch.empty(work_info_set_size, dtype=work_info_set_type, device=dev)
117120
reduce_indptr = torch.empty(reduce_indptr_size, dtype=reduce_indptr_type, device=dev)
@@ -120,24 +123,18 @@ def get_pa_metadata(
120123

121124
get_pa_metadata_v1(
122125
seqlens_qo_indptr,
123-
kv_indptr,
124126
context_lengths,
125-
num_query_heads // num_kv_heads,
126-
num_kv_heads,
127-
True,
128-
work_metadata_ptrs,
129127
work_indptr,
130128
work_info,
131129
reduce_indptr,
132130
reduce_final_map,
133131
reduce_partial_map,
132+
query_group_size=num_query_heads // num_kv_heads,
133+
num_kv_heads=num_kv_heads,
134134
kv_granularity=partition_size,
135-
block_size=block_size,
136-
max_seqlen_qo=query_length,
137-
uni_seqlen_qo=query_length,
138-
fast_mode=True,
139-
max_split_per_batch=-1,
135+
query_length=query_length,
140136
num_cu=num_sm,
137+
stream=torch.cuda.current_stream(dev),
141138
)
142139

143140
# The FlyDSL get_pa_metadata_v1 produces the reduce_* maps natively
@@ -216,15 +213,11 @@ def _prepare_scale_tensor(
216213

217214

218215
def _get_query_input_dtype(query: torch.Tensor) -> str:
219-
if query.dtype in _PACKED_FP8_QUERY_DTYPES:
220-
return "packed_fp8"
221216
if query.dtype == torch.bfloat16:
222217
return "bf16"
223218
if query.dtype == torch.float16:
224219
return "f16"
225-
raise ValueError(
226-
f"Unsupported query dtype for pa_decode_ps_launch: {query.dtype}. " "Expected packed FP8/uint8, bf16, or f16."
227-
)
220+
raise ValueError(f"Unsupported query dtype for pa_decode_ps_launch: {query.dtype}. Expected bf16 or f16.")
228221

229222

230223
def _get_output_dtype_str(output: torch.Tensor) -> str:
@@ -269,9 +262,8 @@ def get_recommended_splits(
269262
return max(4, min(n, 8))
270263

271264

272-
# Small block_size (16/64) is routed through the load-balanced worklist
273-
# (metadata) path: `compile_pa_decode_metadata` gathers 256//block_size physical
274-
# pages per 256-token partition, for both per-tensor and per-token KV quant.
265+
# Small block sizes use the standalone tile kernel; the metadata decode path
266+
# below is reserved for 1024-token physical pages.
275267
_PA_DECODE_PS_SMALL_BLOCK_SIZES = (16, 64)
276268

277269

@@ -322,12 +314,6 @@ def pa_decode_ps_launch(
322314
device=dev,
323315
is_graph_capturing=is_graph_capturing,
324316
)
325-
if query_input_dtype == "packed_fp8":
326-
raise ValueError(
327-
"`pa_decode_ps_launch` no longer accepts host query_scale and only supports "
328-
"bf16/f16 query inputs with kernel-internal query scale computation."
329-
)
330-
331317
# Detect per-token vs per-tensor quantization from scale tensor
332318
# dimensionality: a >1-D scale tensor carries one scale per (block, head,
333319
# token), which enables the per-token K/V path in the metadata kernel.
@@ -516,7 +502,15 @@ def pa_decode_ps_launch(
516502
"CUDA graph capture requires precomputed `metadata`; "
517503
"call `get_pa_metadata()` before capture and pass it via `metadata=`."
518504
)
519-
metadata = get_pa_metadata(query, key_cache, context_lengths, kv_indptr, num_query_heads, num_kv_heads)
505+
metadata = get_pa_metadata(
506+
query,
507+
key_cache,
508+
context_lengths,
509+
kv_indptr,
510+
num_query_heads,
511+
num_kv_heads,
512+
per_token_kv=per_token_kv,
513+
)
520514

521515
work_indptr = metadata["work_indptr"]
522516
work_info_flat = metadata["work_info_flat"]
@@ -590,7 +584,7 @@ def pa_decode_ps_launch(
590584
max_seqlen_q=query_length,
591585
final_output=output,
592586
num_query_heads=num_query_heads,
593-
head_size=int(query.shape[-1]),
587+
head_dim=int(query.shape[-1]),
594588
stream=s,
595589
)
596590

0 commit comments

Comments
 (0)