[ROCm][Perf] Optimize DeepSeek V4 C4A top-k with AITER (#52882)

Fangzhou Ai committed Aug 21, 2026 at 13:00 UTC fe76112ff2981e9373765d76687d53f94880f38e
5 files changed +612 -31
csrc/libtorch_stable/sampler.cu
+132 -11
@@ -2,6 +2,8 @@
2 #include "dispatch_utils.h"
3 #include "torch_utils.h"
4
5 +#include <cstring>
6 +
7 #ifndef USE_ROCM
8 #include <cub/cub.cuh>
9 #else
@@ -327,11 +329,14 @@ __device__ bool processHistogramStep(
329 }
330
331 // Follows half - 11 - 11 - 10 bit iterations
332 +// Keep the adaptive kernel's device instantiation separate from legacy calls.
333 template <int kNumThreadsPerBlock, int kNumBins, bool useRadixSort,
331 - bool multipleBlocksPerRow = false, bool mergeBlocks = false>
334 + bool multipleBlocksPerRow = false, bool mergeBlocks = false,
335 + bool deviceLengthAware = false>
336 static __device__ void topKPerRowJob(const int* indices, const float* logits,
337 int rowStart, int rowEnd, int* outIndices,
338 float* outLogits, int stride1, int topK) {
339 + static_assert(!deviceLengthAware || multipleBlocksPerRow != mergeBlocks);
340 // The number of slots for the final pass.
341 static constexpr int kNumFinalItems = 2048;
342 // The number of elements per thread for the final sort.
@@ -613,6 +618,57 @@ static __global__ __launch_bounds__(kNumThreadsPerBlock) void topKPerRowDecode(
618 indices, logits, rowStart, rowEnd, outIndices, outLogits, stride1, topK);
619 }
620
621 +// Measured native-kernel policy for gfx950, FP32, and k=1024. This
622 +// active-column split crossover is independent of the Python AITER/native
623 +// backend crossover; the two policies can be retuned separately. At exactly
624 +// 64K, an eager call may select AITER while a captured native replay uses two
625 +// splits.
626 +constexpr int kGfx950C4AShortRowNumBlocks = 3;
627 +constexpr int kGfx950C4ALongRowNumBlocks = 2;
628 +constexpr int kGfx950C4ATwoSplitMinRowLength = 64 * 1024;
629 +
630 +template <int kNumThreadsPerBlock, bool mergeBlocks = false>
631 +static __global__
632 +__launch_bounds__(kNumThreadsPerBlock) void topKPerRowDecodeDeviceLengthAware(
633 + const float* logits, const int* seqLens, int* outIndices, int stride0,
634 + int stride1, const int topK, int next_n, int seqLensIs2D = 0,
635 + float* outLogits = nullptr, const int* indices = nullptr) {
636 + static constexpr int kNumBins = 2048;
637 + int rowIdx = blockIdx.x;
638 + int batch_idx = rowIdx / next_n;
639 + int next_n_idx = rowIdx % next_n;
640 + int seq_len = seqLensIs2D ? seqLens[rowIdx] : seqLens[batch_idx];
641 + int rowEnd =
642 + seqLensIs2D ? max(0, seq_len) : max(0, seq_len - next_n + next_n_idx + 1);
643 + const int activeBlocks = rowEnd >= kGfx950C4ATwoSplitMinRowLength
644 + ? kGfx950C4ALongRowNumBlocks
645 + : kGfx950C4AShortRowNumBlocks;
646 + int rowStart = 0;
647 +
648 + if constexpr (!mergeBlocks) {
649 + if (blockIdx.y >= activeBlocks) return;
650 + const auto blockSize = rowEnd / activeBlocks;
651 + rowStart = blockSize * blockIdx.y;
652 + rowEnd = activeBlocks == blockIdx.y + 1 ? rowEnd : rowStart + blockSize;
653 + outIndices +=
654 + static_cast<int64_t>(rowIdx) * kGfx950C4AShortRowNumBlocks * topK +
655 + blockIdx.y * topK;
656 + outLogits +=
657 + static_cast<int64_t>(rowIdx) * kGfx950C4AShortRowNumBlocks * topK +
658 + blockIdx.y * topK;
659 + } else {
660 + rowEnd = activeBlocks * topK;
661 + indices +=
662 + static_cast<int64_t>(rowIdx) * kGfx950C4AShortRowNumBlocks * topK;
663 + outIndices += static_cast<int64_t>(rowIdx) * topK;
664 + }
665 + logits += static_cast<int64_t>(rowIdx) * stride0;
666 +
667 + topKPerRowJob<kNumThreadsPerBlock, kNumBins, true, !mergeBlocks, mergeBlocks,
668 + true>(indices, logits, rowStart, rowEnd, outIndices, outLogits,
669 + stride1, topK);
670 +}
671 +
672 } // namespace vllm
673
674 void apply_repetition_penalties_(
@@ -693,7 +749,60 @@ void top_k_per_row_decode(const torch::stable::Tensor& logits, int64_t next_n,
749 static_cast<int>(next_n), seqLensIs2D);
750 } else {
751 // Long sequences are run in two steps
696 - constexpr auto multipleBlocksPerRowConfig = 10;
752 +#ifdef USE_ROCM
753 + const bool useGfx950DeviceLengthAwareTopK1024 =
754 + topK == 1024 && numRows > 64 && numRows <= 256 &&
755 + std::strncmp(get_device_prop()->gcnArchName, "gfx950", 6) == 0;
756 + if (useGfx950DeviceLengthAwareTopK1024) {
757 + // Keep three blocks for short FULL-graph replays, but let each row use
758 + // two blocks once its live device length reaches the long-context
759 + // crossover. The grid and workspace shapes remain capture-stable.
760 + constexpr int multipleBlocksPerRowConfig =
761 + vllm::kGfx950C4AShortRowNumBlocks;
762 + const auto outIndicesAux = torch::stable::empty(
763 + {numRows, multipleBlocksPerRowConfig, topK},
764 + torch::headeronly::ScalarType::Int, std::nullopt, logits.device());
765 + const auto outLogitsAux = torch::stable::empty(
766 + {numRows, multipleBlocksPerRowConfig, topK},
767 + torch::headeronly::ScalarType::Float, std::nullopt, logits.device());
768 +
769 + constexpr int kNumThreadsPerSplitBlock = 1024;
770 + vllm::topKPerRowDecodeDeviceLengthAware<kNumThreadsPerSplitBlock>
771 + <<<dim3(numRows, multipleBlocksPerRowConfig),
772 + kNumThreadsPerSplitBlock, 2 * topK * sizeof(int32_t), stream>>>(
773 + logits.const_data_ptr<float>(), seqLens.const_data_ptr<int>(),
774 + outIndicesAux.mutable_data_ptr<int>(), static_cast<int>(stride0),
775 + static_cast<int>(stride1), static_cast<int>(topK),
776 + static_cast<int>(next_n), seqLensIs2D,
777 + outLogitsAux.mutable_data_ptr<float>());
778 + constexpr int kNumThreadsPerBlockMerge = 1024;
779 + vllm::topKPerRowDecodeDeviceLengthAware<kNumThreadsPerBlockMerge, true>
780 + <<<numRows, kNumThreadsPerBlockMerge, topK * sizeof(int32_t),
781 + stream>>>(
782 + outLogitsAux.const_data_ptr<float>(),
783 + seqLens.const_data_ptr<int>(), indices.mutable_data_ptr<int>(),
784 + multipleBlocksPerRowConfig * topK, 1, static_cast<int>(topK),
785 + static_cast<int>(next_n), seqLensIs2D, nullptr,
786 + outIndicesAux.const_data_ptr<int>());
787 + return;
788 + }
789 +#endif
790 +
791 +#ifdef USE_ROCM
792 + const bool useGfx950TopK1024 =
793 + topK == 1024 && numRows <= 64 &&
794 + std::strncmp(get_device_prop()->gcnArchName, "gfx950", 6) == 0;
795 +#else
796 + constexpr bool useGfx950TopK1024 = false;
797 +#endif
798 + int multipleBlocksPerRowConfig = 10;
799 + if (useGfx950TopK1024) {
800 + if (numRows <= 32) {
801 + multipleBlocksPerRowConfig = 8;
802 + } else {
803 + multipleBlocksPerRowConfig = 4;
804 + }
805 + }
806
807 const auto outIndicesAux = torch::stable::empty(
808 {numRows, multipleBlocksPerRowConfig, topK},
@@ -702,14 +811,26 @@ void top_k_per_row_decode(const torch::stable::Tensor& logits, int64_t next_n,
811 {numRows, multipleBlocksPerRowConfig, topK},
812 torch::headeronly::ScalarType::Float, std::nullopt, logits.device());
813
705 - vllm::topKPerRowDecode<kNumThreadsPerBlock, true, true>
706 - <<<dim3(numRows, multipleBlocksPerRowConfig), kNumThreadsPerBlock,
707 - 2 * topK * sizeof(int32_t), stream>>>(
708 - logits.const_data_ptr<float>(), seqLens.const_data_ptr<int>(),
709 - outIndicesAux.mutable_data_ptr<int>(), static_cast<int>(stride0),
710 - static_cast<int>(stride1), static_cast<int>(topK),
711 - static_cast<int>(next_n), seqLensIs2D,
712 - outLogitsAux.mutable_data_ptr<float>());
814 + if (useGfx950TopK1024) {
815 + constexpr int kNumThreadsPerSplitBlock = 1024;
816 + vllm::topKPerRowDecode<kNumThreadsPerSplitBlock, true, true>
817 + <<<dim3(numRows, multipleBlocksPerRowConfig),
818 + kNumThreadsPerSplitBlock, 2 * topK * sizeof(int32_t), stream>>>(
819 + logits.const_data_ptr<float>(), seqLens.const_data_ptr<int>(),
820 + outIndicesAux.mutable_data_ptr<int>(), static_cast<int>(stride0),
821 + static_cast<int>(stride1), static_cast<int>(topK),
822 + static_cast<int>(next_n), seqLensIs2D,
823 + outLogitsAux.mutable_data_ptr<float>());
824 + } else {
825 + vllm::topKPerRowDecode<kNumThreadsPerBlock, true, true>
826 + <<<dim3(numRows, multipleBlocksPerRowConfig), kNumThreadsPerBlock,
827 + 2 * topK * sizeof(int32_t), stream>>>(
828 + logits.const_data_ptr<float>(), seqLens.const_data_ptr<int>(),
829 + outIndicesAux.mutable_data_ptr<int>(), static_cast<int>(stride0),
830 + static_cast<int>(stride1), static_cast<int>(topK),
831 + static_cast<int>(next_n), seqLensIs2D,
832 + outLogitsAux.mutable_data_ptr<float>());
833 + }
834
835 constexpr int kNumThreadsPerBlockMerge = 1024;
836 vllm::topKPerRowDecode<kNumThreadsPerBlockMerge, true, false, true>
@@ -753,4 +874,4 @@ void top_k_per_row_prefill(const torch::stable::Tensor& logits,
874 static_cast<int>(stride0), static_cast<int>(stride1),
875 static_cast<int>(topK), kSortingAlgorithmThreshold);
876 }
756 -}
\ No newline at end of file
877 +}
tests/kernels/test_top_k_per_row.py
+293
@@ -374,6 +374,299 @@ def test_top_k_per_row_decode_large_vocab_size(clean_logits: bool) -> None:
374 )
375
376
377 +@pytest.mark.parametrize(
378 + "batch_size,num_speculative_tokens,min_seq_len,max_seq_len",
379 + [
380 + pytest.param(1, 2, 125_000, 250_000, id="eight-splits"),
381 + pytest.param(7, 4, 125_000, 250_000, id="four-splits"),
382 + pytest.param(16, 4, 2_500, 2_501, id="dynamic-short-rows"),
383 + pytest.param(13, 4, 25_000, 25_001, id="dynamic-three-splits"),
384 + pytest.param(16, 4, 65_535, 65_536, id="dynamic-boundary-mixed"),
385 + pytest.param(16, 4, 125_000, 125_001, id="dynamic-two-splits"),
386 + pytest.param(64, 3, 125_000, 125_001, id="dynamic-two-splits-max-rows"),
387 + pytest.param(64, 4, 125_000, 125_001, id="baseline-fallback"),
388 + ],
389 +)
390 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="This test requires ROCm")
391 +@torch.inference_mode()
392 +def test_top_k_per_row_decode_gfx950_long_c4a(
393 + batch_size: int,
394 + num_speculative_tokens: int,
395 + min_seq_len: int,
396 + max_seq_len: int,
397 +) -> None:
398 + properties = torch.cuda.get_device_properties(0)
399 + if not properties.gcnArchName.startswith("gfx950"):
400 + pytest.skip("This test exercises the gfx950 launch configuration")
401 +
402 + query_tokens = num_speculative_tokens + 1
403 + num_rows = batch_size * query_tokens
404 + stride = 262_144
405 + top_k = 1024
406 + offsets = torch.arange(num_rows, dtype=torch.int32, device="cuda")
407 + seq_lens = min_seq_len + offsets * (max_seq_len - min_seq_len) // max(
408 + num_rows - 1, 1
409 + )
410 + seq_lens = seq_lens.reshape(batch_size, query_tokens)
411 + logits = torch.randn(num_rows, stride, dtype=torch.float32, device="cuda")
412 + indices = torch.empty((num_rows, top_k), dtype=torch.int32, device="cuda")
413 +
414 + torch.ops._C.top_k_per_row_decode(
415 + logits,
416 + query_tokens,
417 + seq_lens,
418 + indices,
419 + num_rows,
420 + logits.stride(0),
421 + logits.stride(1),
422 + top_k,
423 + )
424 +
425 + row_starts = torch.zeros(num_rows, dtype=torch.int32, device="cuda")
426 + validate_topk_against_reference(
427 + logits,
428 + indices,
429 + row_starts,
430 + seq_lens.reshape(-1),
431 + top_k,
432 + "gfx950 long C4A top-k",
433 + )
434 +
435 +
436 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="This test requires ROCm")
437 +@torch.inference_mode()
438 +def test_top_k_per_row_decode_gfx950_long_c4a_1d_seq_lens() -> None:
439 + properties = torch.cuda.get_device_properties(0)
440 + if not properties.gcnArchName.startswith("gfx950"):
441 + pytest.skip("This test exercises the gfx950 launch configuration")
442 +
443 + batch_size = 20
444 + query_tokens = 4
445 + num_rows = batch_size * query_tokens
446 + stride = 262_144
447 + top_k = 1024
448 + seq_lens = torch.linspace(
449 + 125_003,
450 + 250_000,
451 + batch_size,
452 + dtype=torch.int32,
453 + device="cuda",
454 + )
455 + row_offsets = torch.arange(query_tokens, dtype=torch.int32, device="cuda")
456 + row_ends = (seq_lens[:, None] - query_tokens + row_offsets + 1).reshape(-1)
457 + logits = torch.randn(num_rows, stride, dtype=torch.float32, device="cuda")
458 + indices = torch.empty((num_rows, top_k), dtype=torch.int32, device="cuda")
459 +
460 + torch.ops._C.top_k_per_row_decode(
461 + logits,
462 + query_tokens,
463 + seq_lens,
464 + indices,
465 + num_rows,
466 + logits.stride(0),
467 + logits.stride(1),
468 + top_k,
469 + )
470 +
471 + row_starts = torch.zeros(num_rows, dtype=torch.int32, device="cuda")
472 + validate_topk_against_reference(
473 + logits,
474 + indices,
475 + row_starts,
476 + row_ends,
477 + top_k,
478 + "gfx950 long C4A top-k with 1-D sequence lengths",
479 + )
480 +
481 +
482 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="This test requires ROCm")
483 +@torch.inference_mode()
484 +def test_aiter_c4a_prefill_topk_returns_sequence_local_indices() -> None:
485 + from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
486 + _get_aiter_top_k_kernel,
487 + _launch_aiter_top_k_per_row_prefill,
488 + )
489 +
490 + row_starts = torch.tensor([1, 6, 20], dtype=torch.int32, device="cuda")
491 + row_ends = torch.tensor([3, 18, 23], dtype=torch.int32, device="cuda")
492 + logits = torch.arange(96, dtype=torch.float32, device="cuda").reshape(3, 32)
493 + top_k = 4
494 + indices = torch.empty((3, top_k), dtype=torch.int32, device="cuda")
495 +
496 + aiter_topk_kernel = _get_aiter_top_k_kernel(
497 + is_prefill=True,
498 + compress_ratio=4,
499 + num_rows=logits.shape[0],
500 + on_gfx950=True,
501 + )
502 + if aiter_topk_kernel is None:
503 + pytest.skip("AITER top-k is unavailable")
504 + assert (
505 + _launch_aiter_top_k_per_row_prefill(
506 + aiter_topk_kernel,
507 + logits,
508 + row_starts,
509 + row_ends,
510 + indices,
511 + top_k,
512 + )
513 + is None
514 + )
515 +
516 + for row_idx, (row_start, row_end) in enumerate(
517 + zip(row_starts.tolist(), row_ends.tolist())
518 + ):
519 + num_valid = min(top_k, row_end - row_start)
520 + expected = logits[row_idx, row_start:row_end].topk(num_valid).indices
521 + actual = indices[row_idx, :num_valid]
522 + assert set(actual.tolist()) == set(expected.tolist())
523 + assert torch.all(indices[row_idx, num_valid:] == -1)
524 +
525 +
526 +@pytest.mark.parametrize("num_speculative_tokens", [2, 3, 4])
527 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="This test requires ROCm")
528 +@torch.inference_mode()
529 +def test_aiter_c4a_decode_topk_uses_exact_mtp_lengths(
530 + num_speculative_tokens: int,
531 +) -> None:
532 + from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
533 + _get_aiter_top_k_kernel,
534 + _launch_aiter_top_k_per_row_decode,
535 + )
536 +
537 + query_tokens = num_speculative_tokens + 1
538 + offsets = torch.arange(query_tokens, dtype=torch.int32, device="cuda")
539 + final_uncompressed_lens = torch.tensor([43, 59], dtype=torch.int32, device="cuda")
540 + seq_lens = (final_uncompressed_lens[:, None] - query_tokens + offsets + 1) // 4
541 + seq_lens[0, 0] = 0
542 +
543 + num_rows = seq_lens.numel()
544 + logits = torch.arange(num_rows * 16, dtype=torch.float32, device="cuda").reshape(
545 + num_rows, 16
546 + )
547 + top_k = 4
548 + indices = torch.empty((num_rows, top_k), dtype=torch.int32, device="cuda")
549 +
550 + aiter_topk_kernel = _get_aiter_top_k_kernel(
551 + is_prefill=False,
552 + compress_ratio=4,
553 + num_rows=num_rows,
554 + max_valid_seq_len=seq_lens.max().item(),
555 + on_gfx950=True,
556 + )
557 + if aiter_topk_kernel is None:
558 + pytest.skip("AITER top-k is unavailable")
559 + assert (
560 + _launch_aiter_top_k_per_row_decode(
561 + aiter_topk_kernel,
562 + logits,
563 + seq_lens,
564 + indices,
565 + top_k,
566 + )
567 + is None
568 + )
569 +
570 + for row_idx, row_end in enumerate(seq_lens.reshape(-1).tolist()):
571 + num_valid = min(top_k, row_end)
572 + expected = logits[row_idx, :row_end].topk(num_valid).indices
573 + actual = indices[row_idx, :num_valid]
574 + assert set(actual.tolist()) == set(expected.tolist())
575 + assert torch.all(indices[row_idx, num_valid:] == -1)
576 +
577 +
578 +def test_aiter_c4a_topk_kernel_selection(monkeypatch) -> None:
579 + from vllm.v1.attention.ops import rocm_aiter_mla_sparse
580 +
581 + def prefill_kernel(*args, **kwargs) -> None:
582 + pass
583 +
584 + def decode_kernel(*args, **kwargs) -> None:
585 + pass
586 +
587 + monkeypatch.setattr(
588 + rocm_aiter_mla_sparse,
589 + "_get_aiter_topk_ops",
590 + lambda: (prefill_kernel, decode_kernel),
591 + )
592 + get_kernel = rocm_aiter_mla_sparse._get_aiter_top_k_kernel
593 +
594 + eligible_cases = [
595 + (
596 + dict(is_prefill=True, compress_ratio=4, num_rows=1, on_gfx950=True),
597 + prefill_kernel,
598 + ),
599 + (
600 + dict(
601 + is_prefill=False,
602 + compress_ratio=4,
603 + num_rows=1,
604 + max_valid_seq_len=65_536,
605 + on_gfx950=True,
606 + ),
607 + decode_kernel,
608 + ),
609 + (
610 + dict(
611 + is_prefill=False,
612 + compress_ratio=4,
613 + num_rows=257,
614 + max_valid_seq_len=250_000,
615 + on_gfx950=True,
616 + ),
617 + decode_kernel,
618 + ),
619 + ]
620 + for kwargs, expected_kernel in eligible_cases:
621 + assert get_kernel(**kwargs) is expected_kernel
622 +
623 + def fail_if_imported() -> None:
624 + pytest.fail("ineligible shapes must not import AITER top-k")
625 +
626 + monkeypatch.setattr(
627 + rocm_aiter_mla_sparse,
628 + "_get_aiter_topk_ops",
629 + fail_if_imported,
630 + )
631 + ineligible_cases = [
632 + dict(is_prefill=True, compress_ratio=1, num_rows=1, on_gfx950=True),
633 + dict(
634 + is_prefill=False,
635 + compress_ratio=4,
636 + num_rows=1,
637 + max_valid_seq_len=65_537,
638 + on_gfx950=True,
639 + ),
640 + dict(
641 + is_prefill=False,
642 + compress_ratio=4,
643 + num_rows=256,
644 + max_valid_seq_len=125_000,
645 + on_gfx950=True,
646 + ),
647 + dict(
648 + is_prefill=False,
649 + compress_ratio=4,
650 + num_rows=320,
651 + max_valid_seq_len=2_500,
652 + on_gfx950=False,
653 + ),
654 + ]
655 + for kwargs in ineligible_cases:
656 + assert get_kernel(**kwargs) is None
657 +
658 + monkeypatch.setattr(rocm_aiter_mla_sparse, "_get_aiter_topk_ops", lambda: None)
659 + assert (
660 + get_kernel(
661 + is_prefill=True,
662 + compress_ratio=4,
663 + num_rows=1,
664 + on_gfx950=True,
665 + )
666 + is None
667 + )
668 +
669 +
670 @pytest.mark.skipif(not current_platform.is_cuda(), reason="This test requires CUDA")
671 @pytest.mark.parametrize(
672 "seq_len_range,test_id",
vllm/model_executor/layers/sparse_attn_indexer.py
+3
@@ -750,6 +750,7 @@ class SparseAttnIndexer(CustomOp):
750 topk_indices_buffer: torch.Tensor,
751 skip_k_cache_insert: bool = False,
752 use_fp4_cache: bool = False,
753 + compress_ratio: int = 1,
754 ):
755 super().__init__()
756 self.k_cache = k_cache
@@ -762,6 +763,7 @@ class SparseAttnIndexer(CustomOp):
763 self.topk_indices_buffer = topk_indices_buffer
764 self.skip_k_cache_insert = skip_k_cache_insert
765 self.use_fp4_cache = use_fp4_cache
766 + self.compress_ratio = compress_ratio
767 self.dense_mha_metadata_layer_name = ""
768 # DCP scalars are constant for the run; resolve them here (config is set
769 # during model construction) and pass them into the custom op, rather
@@ -873,6 +875,7 @@ class SparseAttnIndexer(CustomOp):
875 self.max_total_seq_len,
876 self.topk_indices_buffer,
877 skip_k_cache_insert=self.skip_k_cache_insert,
878 + compress_ratio=self.compress_ratio,
879 )
880 raise RuntimeError(
881 "Sparse attention indexer ROCm path is only supported on AITER. "
vllm/models/deepseek_v4/attention.py
+1
@@ -854,6 +854,7 @@ class DeepseekV4Indexer(nn.Module):
854 self.topk_indices_buffer,
855 skip_k_cache_insert=True,
856 use_fp4_cache=self.use_fp4_kv,
857 + compress_ratio=self.compress_ratio,
858 )
859
860 # None on ROCm — maybe_execute_in_parallel falls back to sequential.
vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
+183 -20
@@ -3,6 +3,7 @@
3 import functools
4 import importlib
5 import math
6 +from collections.abc import Callable
7 from importlib.util import find_spec
8
9 import torch
@@ -10,6 +11,7 @@ import torch.nn.functional as F
11
12 import vllm.envs as envs
13 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
14 +from vllm.config import CUDAGraphMode
15 from vllm.forward_context import get_forward_context
16 from vllm.platforms import current_platform
17 from vllm.triton_utils import tl, triton
@@ -25,6 +27,127 @@ else:
27 _ON_GFX950 = False
28
29
30 +@functools.cache
31 +def _get_aiter_topk_ops() -> tuple[Callable[..., None], Callable[..., None]] | None:
32 + try:
33 + from aiter.ops.topk import (
34 + top_k_per_row_decode,
35 + top_k_per_row_prefill,
36 + )
37 + except ImportError:
38 + return None
39 + return top_k_per_row_prefill, top_k_per_row_decode
40 +
41 +
42 +_GFX950_C4A_AITER_MAX_COMPRESSED_SEQ_LEN = 64 * 1024
43 +_GFX950_C4A_NATIVE_MAX_ROWS = 256
44 +
45 +
46 +def _get_aiter_top_k_kernel(
47 + *,
48 + is_prefill: bool,
49 + compress_ratio: int,
50 + num_rows: int,
51 + max_valid_seq_len: int | None = None,
52 + on_gfx950: bool = _ON_GFX950,
53 +) -> Callable[..., None] | None:
54 + if compress_ratio <= 1 or not on_gfx950:
55 + return None
56 +
57 + if not is_prefill:
58 + assert max_valid_seq_len is not None
59 + # AITER v0.1.19 decode is one-block only. This measured gfx950
60 + # FP32/k=1024 compressed-row boundary is independent of the native
61 + # split-count boundary in sampler.cu.
62 + if (
63 + num_rows <= _GFX950_C4A_NATIVE_MAX_ROWS
64 + and max_valid_seq_len > _GFX950_C4A_AITER_MAX_COMPRESSED_SEQ_LEN
65 + ):
66 + return None
67 +
68 + topk_ops = _get_aiter_topk_ops()
69 + if topk_ops is None:
70 + return None
71 + return topk_ops[0] if is_prefill else topk_ops[1]
72 +
73 +
74 +@triton.jit
75 +def _localize_aiter_prefill_topk_kernel(
76 + indices_ptr,
77 + row_starts_ptr,
78 + indices_stride,
79 + num_topk,
80 + BLOCK_SIZE: tl.constexpr,
81 +):
82 + row_idx = tl.program_id(0)
83 + offsets = tl.program_id(1) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
84 + mask = offsets < num_topk
85 + indices = tl.load(indices_ptr + row_idx * indices_stride + offsets, mask=mask)
86 + row_start = tl.load(row_starts_ptr + row_idx)
87 + localized = tl.where(indices < 0, indices, indices - row_start)
88 + tl.store(
89 + indices_ptr + row_idx * indices_stride + offsets,
90 + localized,
91 + mask=mask,
92 + )
93 +
94 +
95 +def _localize_aiter_prefill_topk(
96 + indices: torch.Tensor,
97 + row_starts: torch.Tensor,
98 +) -> None:
99 + num_rows, num_topk = indices.shape
100 + block_size = 256
101 + _localize_aiter_prefill_topk_kernel[(num_rows, triton.cdiv(num_topk, block_size))](
102 + indices,
103 + row_starts,
104 + indices.stride(0),
105 + num_topk,
106 + BLOCK_SIZE=block_size,
107 + )
108 +
109 +
110 +def _launch_aiter_top_k_per_row_prefill(
111 + top_k_per_row_prefill: Callable[..., None],
112 + logits: torch.Tensor,
113 + row_starts: torch.Tensor,
114 + row_ends: torch.Tensor,
115 + indices: torch.Tensor,
116 + topk_tokens: int,
117 +) -> None:
118 + top_k_per_row_prefill(
119 + logits,
120 + row_starts,
121 + row_ends,
122 + indices,
123 + None,
124 + logits.shape[0],
125 + logits.stride(0),
126 + logits.stride(1),
127 + k=topk_tokens,
128 + )
129 + _localize_aiter_prefill_topk(indices, row_starts)
130 +
131 +
132 +def _launch_aiter_top_k_per_row_decode(
133 + top_k_per_row_decode: Callable[..., None],
134 + logits: torch.Tensor,
135 + seq_lens: torch.Tensor,
136 + indices: torch.Tensor,
137 + topk_tokens: int,
138 +) -> None:
139 + top_k_per_row_decode(
140 + logits,
141 + 1,
142 + seq_lens.reshape(-1),
143 + indices,
144 + logits.shape[0],
145 + logits.stride(0),
146 + logits.stride(1),
147 + k=topk_tokens,
148 + )
149 +
150 +
151 @triton.jit
152 def _indexer_k_quant_and_cache_kernel(
153 k_ptr, # [num_tokens, head_dim]
@@ -692,6 +815,7 @@ def rocm_aiter_sparse_attn_indexer_fake(
815 total_seq_lens: int,
816 topk_indices_buffer: torch.Tensor | None,
817 skip_k_cache_insert: bool = False,
818 + compress_ratio: int = 1,
819 ) -> torch.Tensor:
820 return topk_indices_buffer
821
@@ -712,9 +836,11 @@ def rocm_aiter_sparse_attn_indexer(
836 total_seq_lens: int,
837 topk_indices_buffer: torch.Tensor | None,
838 skip_k_cache_insert: bool = False,
839 + compress_ratio: int = 1,
840 ) -> torch.Tensor:
841 # careful! this will be None in dummy run
717 - attn_metadata = get_forward_context().attn_metadata
842 + forward_context = get_forward_context()
843 + attn_metadata = forward_context.attn_metadata
844 fp8_dtype = current_platform.fp8_dtype()
845 from vllm.utils.torch_utils import _resolve_layer_name
846
@@ -773,6 +899,7 @@ def rocm_aiter_sparse_attn_indexer(
899 total_seq_lens,
900 topk_indices_buffer,
901 skip_k_cache_insert,
902 + compress_ratio,
903 )
904 layer_attn_metadata = attn_metadata[k_cache_prefix]
905 assert isinstance(layer_attn_metadata, DeepseekV32IndexerMetadata)
@@ -833,16 +960,31 @@ def rocm_aiter_sparse_attn_indexer(
960
961 num_rows = logits.shape[0]
962
836 - torch.ops._C.top_k_per_row_prefill(
837 - logits,
838 - chunk.cu_seqlen_ks,
839 - chunk.cu_seqlen_ke,
840 - topk_indices,
841 - num_rows,
842 - logits.stride(0),
843 - logits.stride(1),
844 - topk_tokens,
963 + aiter_topk_kernel = _get_aiter_top_k_kernel(
964 + is_prefill=True,
965 + compress_ratio=compress_ratio,
966 + num_rows=num_rows,
967 )
968 + if aiter_topk_kernel is not None:
969 + _launch_aiter_top_k_per_row_prefill(
970 + aiter_topk_kernel,
971 + logits,
972 + chunk.cu_seqlen_ks,
973 + chunk.cu_seqlen_ke,
974 + topk_indices,
975 + topk_tokens,
976 + )
977 + else:
978 + torch.ops._C.top_k_per_row_prefill(
979 + logits,
980 + chunk.cu_seqlen_ks,
981 + chunk.cu_seqlen_ke,
982 + topk_indices,
983 + num_rows,
984 + logits.stride(0),
985 + logits.stride(1),
986 + topk_tokens,
987 + )
988
989 if has_decode:
990 decode_metadata = layer_attn_metadata.decode
@@ -882,16 +1024,37 @@ def rocm_aiter_sparse_attn_indexer(
1024 topk_indices = topk_indices_buffer[:num_padded_tokens, :topk_tokens]
1025 num_rows = logits.shape[0]
1026
885 - torch.ops._C.top_k_per_row_decode(
886 - logits,
887 - next_n,
888 - decode_metadata.seq_lens,
889 - topk_indices,
890 - num_rows,
891 - logits.stride(0),
892 - logits.stride(1),
893 - topk_tokens,
894 - )
1027 + # FULL graphs are not keyed by context length, so use a replay-safe
1028 + # upper bound when choosing the captured kernel.
1029 + if forward_context.cudagraph_runtime_mode == CUDAGraphMode.FULL:
1030 + max_compressed_seq_len = max_model_len
1031 + else:
1032 + max_compressed_seq_len = layer_attn_metadata.max_seq_len // compress_ratio
1033 + aiter_topk_kernel = _get_aiter_top_k_kernel(
1034 + is_prefill=False,
1035 + compress_ratio=compress_ratio,
1036 + num_rows=num_rows,
1037 + max_valid_seq_len=max_compressed_seq_len,
1038 + )
1039 + if aiter_topk_kernel is not None:
1040 + _launch_aiter_top_k_per_row_decode(
1041 + aiter_topk_kernel,
1042 + logits,
1043 + decode_metadata.seq_lens,
1044 + topk_indices,
1045 + topk_tokens,
1046 + )
1047 + else:
1048 + torch.ops._C.top_k_per_row_decode(
1049 + logits,
1050 + next_n,
1051 + decode_metadata.seq_lens,
1052 + topk_indices,
1053 + num_rows,
1054 + logits.stride(0),
1055 + logits.stride(1),
1056 + topk_tokens,
1057 + )
1058
1059 if decode_metadata.requires_padding:
1060 # if padded, we need to unpack