Skip to content

Commit e46db60

Browse files
authored
[Kernel][PA] Support BF16 vectorized KV with asymmetric K/V head dim (#1065)
* feat(pa): support BF16 KV and asymmetric value dimensions Signed-off-by: Xiake Sun <xiake.sun@amd.com> * test(pa): Add BF16 and FP8 Q/K192 with asymmetric values V128/V192 in test_pa.py Signed-off-by: Xiake Sun <xiake.sun@amd.com> * fix format with black Signed-off-by: Xiake Sun <xiake.sun@amd.com> * fix(pa): clarify workspace shapes and validate GQA heads - Add docs for partition workspaces using the flattened total-row dimension that matches host allocation and kernel indexing. - Reject Q-head counts that are not divisible by the number of KV heads before deriving the query group size. Signed-off-by: Xiake Sun <xiake.sun@amd.com> * refactor(pa): select tile decode path by KV dtype Signed-off-by: Xiake Sun <xiake.sun@amd.com> * feat(pa): route page-16/page-64 tile decode via pa_decode_ps_launch Signed-off-by: Xiake Sun <xiake.sun@amd.com> --------- Signed-off-by: Xiake Sun <xiake.sun@amd.com>
1 parent 612c715 commit e46db60

3 files changed

Lines changed: 620 additions & 247 deletions

File tree

kernels/attention/pa_decode_fp8.py

Lines changed: 76 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -296,11 +296,83 @@ def pa_decode_ps_launch(
296296
"""
297297
num_query_heads = query.shape[1]
298298
num_kv_heads = key_cache.shape[1]
299-
trans_v = len(value_cache.shape) == 5
300-
query_input_dtype = _get_query_input_dtype(query)
301-
299+
batch_size = context_lengths.shape[0]
300+
if query.shape[0] % batch_size != 0:
301+
raise ValueError(f"query.shape[0] ({query.shape[0]}) must be divisible by " f"batch_size ({batch_size})")
302+
if num_query_heads % num_kv_heads != 0:
303+
raise ValueError(f"num_query_heads ({num_query_heads}) must be divisible by " f"num_kv_heads ({num_kv_heads})")
304+
query_length = query.shape[0] // batch_size
305+
query_group_size = num_query_heads // num_kv_heads
306+
head_size = query.shape[-1]
307+
value_head_size = output.shape[-1]
308+
block_size = key_cache.shape[-2]
302309
dev = query.device
303310
is_graph_capturing = _is_current_stream_capturing()
311+
s = stream or torch.cuda.current_stream()
312+
313+
if max_context_partition_num <= 0:
314+
raise ValueError("max_context_partition_num must be positive.")
315+
316+
# Small physical pages use the standalone tile kernel. Dispatch before the
317+
# FP8 metadata/SW setup so BF16 KV stays unscaled and asymmetric V uses the
318+
# tile wrapper's value-sized workspace allocation and validation.
319+
if block_size in _PA_DECODE_PS_SMALL_BLOCK_SIZES and sliding_window == 0:
320+
if block_tables is None:
321+
raise ValueError(
322+
f"pa_decode_ps_launch: block_size={block_size} requires `block_tables` "
323+
"(per-sequence physical block index table)."
324+
)
325+
326+
tile_key_scale = key_scale
327+
tile_value_scale = value_scale
328+
if key_cache.dtype != torch.bfloat16:
329+
tile_key_scale = _prepare_scale_tensor(
330+
"key_scale",
331+
key_scale,
332+
device=dev,
333+
is_graph_capturing=is_graph_capturing,
334+
)
335+
tile_value_scale = _prepare_scale_tensor(
336+
"value_scale",
337+
value_scale,
338+
device=dev,
339+
is_graph_capturing=is_graph_capturing,
340+
)
341+
if tile_key_scale.ndim > 1:
342+
num_blocks = key_cache.shape[0]
343+
tile_key_scale = tile_key_scale.reshape(num_blocks, num_kv_heads, block_size)
344+
tile_value_scale = tile_value_scale.reshape(num_blocks, num_kv_heads, block_size)
345+
346+
pa_decode_tile(
347+
output,
348+
query,
349+
key_cache,
350+
value_cache,
351+
block_tables,
352+
context_lengths,
353+
tile_key_scale,
354+
tile_value_scale,
355+
softmax_scale=softmax_scale,
356+
stream=s,
357+
num_partitions=max_context_partition_num,
358+
pmax=max_logits,
359+
psum=exp_sums,
360+
pout=temporary_output,
361+
)
362+
return "ps_small_block"
363+
364+
is_bf16_kv = key_cache.dtype == torch.bfloat16
365+
is_asymmetric = value_head_size != head_size
366+
if is_bf16_kv or is_asymmetric:
367+
unsupported_path = "sliding-window" if sliding_window > 0 else "page-1024 metadata"
368+
raise ValueError(
369+
"BF16 KV and asymmetric value dimensions currently require "
370+
"block_size 16 or 64 with sliding_window=0; "
371+
f"the {unsupported_path} path is not supported."
372+
)
373+
374+
trans_v = len(value_cache.shape) == 5
375+
query_input_dtype = _get_query_input_dtype(query)
304376

305377
key_scale = _prepare_scale_tensor(
306378
"key_scale",
@@ -319,28 +391,16 @@ def pa_decode_ps_launch(
319391
# token), which enables the per-token K/V path in the metadata kernel.
320392
per_token_kv = key_scale.ndim > 1
321393

322-
query_length = query.shape[0] // context_lengths.shape[0]
323-
query_group_size = num_query_heads // num_kv_heads
324-
batch_size = context_lengths.shape[0]
325-
head_size = query.shape[-1]
326-
327394
# Strides for key_scale/value_scale
328395
stride_ks_block = key_scale.stride(0) if per_token_kv else 0
329396
stride_ks_head = key_scale.stride(1) if per_token_kv else 0
330397

331-
s = stream or torch.cuda.current_stream()
332-
333-
if max_context_partition_num <= 0:
334-
raise ValueError("max_context_partition_num must be positive for sliding-window decode.")
335398
if is_graph_capturing and (exp_sums is None or max_logits is None or temporary_output is None):
336399
raise ValueError(
337400
"CUDA graph capture requires preallocated `exp_sums`, `max_logits`, "
338401
"and `temporary_output` for the sliding-window path."
339402
)
340-
# ── small-block (block_size 16/64) → tile kernel ──
341-
# Key cache shape is [num_blocks, num_kv_heads, head_size // 16, block_size, 16].
342-
block_size = key_cache.shape[-2]
343-
if block_size in _PA_DECODE_PS_SMALL_BLOCK_SIZES or sliding_window > 0:
403+
if sliding_window > 0:
344404
eqgs = query_length * query_group_size
345405
if exp_sums is None:
346406
exp_sums = torch.zeros(
@@ -454,48 +514,6 @@ def pa_decode_ps_launch(
454514
)
455515
return "ps_sw_partitioned"
456516

457-
if block_size in _PA_DECODE_PS_SMALL_BLOCK_SIZES:
458-
if block_tables is None:
459-
raise ValueError(
460-
f"pa_decode_ps_launch: block_size={block_size} requires `block_tables` "
461-
"(per-sequence physical block index table)."
462-
)
463-
if is_graph_capturing:
464-
# Buffer sizes must be fixed ahead of capture and stay identical
465-
# across every replay, so require the caller to have preallocated
466-
# exp_sums/max_logits/temporary_output, exactly as the other PS
467-
# paths already require.
468-
if exp_sums is None or max_logits is None or temporary_output is None:
469-
raise ValueError(
470-
"CUDA graph capture requires preallocated `exp_sums`, `max_logits`, "
471-
"and `temporary_output` for the tile-backed small-block PS path."
472-
)
473-
# pa_decode_tile requires an exact [num_blocks, num_kv_heads,
474-
# block_size] per-token scale shape; callers here may pass an extra
475-
# trailing singleton dim (e.g. from a pertoken-quant helper), which
476-
# reshape away without changing the strides.
477-
if per_token_kv:
478-
num_blocks = key_cache.shape[0]
479-
key_scale = key_scale.reshape(num_blocks, num_kv_heads, block_size)
480-
value_scale = value_scale.reshape(num_blocks, num_kv_heads, block_size)
481-
pa_decode_tile(
482-
output,
483-
query,
484-
key_cache,
485-
value_cache,
486-
block_tables,
487-
context_lengths,
488-
key_scale,
489-
value_scale,
490-
softmax_scale=softmax_scale,
491-
stream=s,
492-
num_partitions=max_context_partition_num,
493-
pmax=max_logits,
494-
psum=exp_sums,
495-
pout=temporary_output,
496-
)
497-
return "ps_small_block"
498-
499517
if metadata is None:
500518
if is_graph_capturing:
501519
raise ValueError(

0 commit comments

Comments
 (0)