[ROCm][Perf][GLM-5.3-Flash] Add AITER topk backend for decodes (#58167)

Signed-off-by: simondanielsson <simon.danielsson99@hotmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Simon Danielsson committed Oct 2, 2026 at 12:47 UTC 98b29aa99aee8fe58a21b168b45a5dcb910f63c0
8 files changed +705 -240
benchmarks/kernels/benchmark_top_k_per_row_decode.py new
+158
@@ -0,0 +1,158 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Benchmark the ROCm sparse-indexer decode top-k in isolation.
4 +
5 +Example usage:
6 +python3 benchmarks/kernels/benchmark_top_k_per_row_decode.py \
7 + --rows 4 16 32 --lens 2048 8192 16384 65536 --capacity 262144
8 +"""
9 +
10 +import argparse
11 +import itertools
12 +import json
13 +
14 +import torch
15 +
16 +import vllm._custom_ops # noqa: F401
17 +from vllm._aiter_ops import rocm_aiter_ops
18 +from vllm.triton_utils import triton
19 +
20 +DISTRIBUTIONS = ("indexer", "fp8", "randn")
21 +
22 +
23 +def make_logits(rows: int, capacity: int, dist: str, spread: float):
24 + normal = torch.randn(rows, capacity, device="cuda", dtype=torch.float32)
25 + if dist == "randn":
26 + return normal
27 + if dist == "fp8":
28 + return normal.to(torch.float8_e4m3fnuz).to(torch.float32)
29 + return normal * spread + 3.0
30 +
31 +
32 +def make_inputs(rows: int, live: int, capacity: int, k: int, args):
33 + torch.manual_seed(args.seed)
34 + logits = make_logits(rows, capacity, args.dist, args.spread)
35 + seq_lens = torch.full((rows,), live, device="cuda", dtype=torch.int32)
36 + invalid = torch.arange(capacity, device="cuda")[None] >= seq_lens[:, None]
37 + logits.masked_fill_(invalid, -float("inf"))
38 + indices = torch.empty((rows, k), device="cuda", dtype=torch.int32)
39 + return logits, seq_lens, indices
40 +
41 +
42 +def run_native(logits, seq_lens, indices, k):
43 + torch.ops._C.top_k_per_row_decode(
44 + logits,
45 + 1,
46 + seq_lens,
47 + indices,
48 + logits.shape[0],
49 + logits.stride(0),
50 + logits.stride(1),
51 + k,
52 + )
53 +
54 +
55 +def run_aiter(logits, seq_lens, indices, k):
56 + rocm_aiter_ops.indexer_top_k_decode(logits, 1, seq_lens, indices, k)
57 +
58 +
59 +BACKENDS = {"native": run_native, "aiter": run_aiter}
60 +
61 +
62 +def check(logits, indices, k):
63 + selected = logits.gather(1, indices.long()).sort(dim=1, descending=True).values
64 + expected = logits.topk(k, dim=1).values
65 + torch.testing.assert_close(selected, expected, atol=0, rtol=0)
66 +
67 +
68 +def capture(run):
69 + side = torch.cuda.Stream()
70 + side.wait_stream(torch.cuda.current_stream())
71 + with torch.cuda.stream(side):
72 + for _ in range(3):
73 + run()
74 + torch.cuda.current_stream().wait_stream(side)
75 + graph = torch.cuda.CUDAGraph()
76 + with torch.cuda.graph(graph):
77 + run()
78 + return graph
79 +
80 +
81 +def benchmark(backend: str, rows: int, live: int, args) -> dict:
82 + logits, seq_lens, indices = make_inputs(rows, live, args.capacity, args.top_k, args)
83 + fn = BACKENDS[backend]
84 +
85 + def run():
86 + fn(logits, seq_lens, indices, args.top_k)
87 +
88 + run()
89 + torch.accelerator.synchronize()
90 + check(logits, indices, args.top_k)
91 + timed = run
92 + if args.graph:
93 + graph = capture(run)
94 + indices.zero_()
95 + graph.replay()
96 + torch.accelerator.synchronize()
97 + check(logits, indices, args.top_k)
98 + timed = graph.replay
99 + ms = triton.testing.do_bench(timed, warmup=args.warmup, rep=args.rep)
100 + return dict(
101 + backend=backend,
102 + mode="graph" if args.graph else "eager",
103 + dist=args.dist,
104 + rows=rows,
105 + live=live,
106 + capacity=args.capacity,
107 + k=args.top_k,
108 + us=ms * 1000,
109 + live_gbps=rows * live * 4 / (ms * 1e6),
110 + )
111 +
112 +
113 +def main():
114 + parser = argparse.ArgumentParser(description=__doc__)
115 + parser.add_argument("--rows", type=int, nargs="+", default=[1, 2, 4, 8])
116 + parser.add_argument(
117 + "--lens",
118 + type=int,
119 + nargs="+",
120 + default=[32768, 65536, 98304, 131072],
121 + help="Valid columns per row; must be at least --top-k",
122 + )
123 + parser.add_argument("--capacity", type=int, default=262144)
124 + parser.add_argument("--top-k", type=int, default=512)
125 + parser.add_argument(
126 + "--backends", nargs="+", choices=list(BACKENDS), default=["native", "aiter"]
127 + )
128 + parser.add_argument(
129 + "--graph",
130 + action="store_true",
131 + help="Time a captured graph replay, matching how decode actually runs",
132 + )
133 + parser.add_argument(
134 + "--dist",
135 + choices=DISTRIBUTIONS,
136 + default="indexer",
137 + help="Logit value distribution; see the module docstring",
138 + )
139 + parser.add_argument(
140 + "--spread",
141 + type=float,
142 + default=0.01,
143 + help="Relative width of the --dist indexer cluster",
144 + )
145 + parser.add_argument("--warmup", type=int, default=25)
146 + parser.add_argument("--rep", type=int, default=200)
147 + parser.add_argument("--seed", type=int, default=123)
148 + args = parser.parse_args()
149 +
150 + print(json.dumps(dict(gpu=torch.cuda.get_device_name(0), args=vars(args))))
151 + for backend, rows, live in itertools.product(args.backends, args.rows, args.lens):
152 + if live < args.top_k:
153 + continue
154 + print(json.dumps(benchmark(backend, rows, live, args)), flush=True)
155 +
156 +
157 +if __name__ == "__main__":
158 + main()
tests/kernels/test_top_k_per_row.py
+225 -118
@@ -636,8 +636,8 @@ def test_top_k_per_row_decode_gfx950_k512_masked_graph_replay() -> None:
636 @pytest.mark.skipif(not current_platform.is_rocm(), reason="This test requires ROCm")
637 @torch.inference_mode()
638 def test_aiter_c4a_prefill_topk_returns_sequence_local_indices() -> None:
639 + from vllm._aiter_ops import rocm_aiter_ops
640 from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
640 - _get_aiter_top_k_kernel,
641 _launch_aiter_top_k_per_row_prefill,
642 )
643
@@ -647,17 +647,14 @@ def test_aiter_c4a_prefill_topk_returns_sequence_local_indices() -> None:
647 top_k = 4
648 indices = torch.empty((3, top_k), dtype=torch.int32, device="cuda")
649
650 - aiter_topk_kernel = _get_aiter_top_k_kernel(
650 + if not rocm_aiter_ops.is_indexer_top_k_supported(
651 is_prefill=True,
652 compress_ratio=4,
653 num_rows=logits.shape[0],
654 - on_gfx950=True,
655 - )
656 - if aiter_topk_kernel is None:
654 + ):
655 pytest.skip("AITER top-k is unavailable")
656 assert (
657 _launch_aiter_top_k_per_row_prefill(
660 - aiter_topk_kernel,
658 logits,
659 row_starts,
660 row_ends,
@@ -683,10 +680,7 @@ def test_aiter_c4a_prefill_topk_returns_sequence_local_indices() -> None:
680 def test_aiter_c4a_decode_topk_uses_exact_mtp_lengths(
681 num_speculative_tokens: int,
682 ) -> None:
686 - from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
687 - _get_aiter_top_k_kernel,
688 - _launch_aiter_top_k_per_row_decode,
689 - )
683 + from vllm._aiter_ops import rocm_aiter_ops
684
685 query_tokens = num_speculative_tokens + 1
686 offsets = torch.arange(query_tokens, dtype=torch.int32, device="cuda")
@@ -701,20 +695,18 @@ def test_aiter_c4a_decode_topk_uses_exact_mtp_lengths(
695 top_k = 4
696 indices = torch.empty((num_rows, top_k), dtype=torch.int32, device="cuda")
697
704 - aiter_topk_kernel = _get_aiter_top_k_kernel(
698 + if not rocm_aiter_ops.is_indexer_top_k_supported(
699 is_prefill=False,
700 compress_ratio=4,
701 num_rows=num_rows,
702 max_valid_seq_len=seq_lens.max().item(),
709 - on_gfx950=True,
710 - )
711 - if aiter_topk_kernel is None:
703 + ):
704 pytest.skip("AITER top-k is unavailable")
705 assert (
714 - _launch_aiter_top_k_per_row_decode(
715 - aiter_topk_kernel,
706 + rocm_aiter_ops.indexer_top_k_decode(
707 logits,
717 - seq_lens,
708 + 1,
709 + seq_lens.reshape(-1),
710 indices,
711 top_k,
712 )
@@ -729,130 +721,107 @@ def test_aiter_c4a_decode_topk_uses_exact_mtp_lengths(
721 assert torch.all(indices[row_idx, num_valid:] == -1)
722
723
732 -def test_aiter_c4a_topk_kernel_selection(monkeypatch) -> None:
733 - from vllm.v1.attention.ops import rocm_aiter_mla_sparse
724 +@requires_gfx950
725 +@torch.inference_mode()
726 +def test_aiter_decode_topk_matches_per_row_under_mtp() -> None:
727 + """The aiter backend expands (B, 1) seq_lens into per-row ends before
728 + launching, so next_n > 1 must agree with the in-tree kernel, including
729 + the rows that clamp to a zero-length prefix.
730 + """
731 + from vllm.model_executor.layers.indexer_topk import get_indexer_topk
732
735 - def prefill_kernel(*args, **kwargs) -> None:
736 - pass
733 + next_n = 4
734 + top_k = 512
735 + num_columns = 1024
736 + seq_lens = torch.tensor([[2], [513], [700]], dtype=torch.int32, device="cuda")
737 + num_rows = seq_lens.shape[0] * next_n
738 +
739 + torch.manual_seed(0)
740 + logits = torch.randn(num_rows, num_columns, dtype=torch.float32, device="cuda")
741 +
742 + outputs = {}
743 + for backend in ("aiter", "per_row"):
744 + indices = torch.empty((num_rows, top_k), dtype=torch.int32, device="cuda")
745 + get_indexer_topk(backend)(logits, seq_lens, next_n, indices, top_k, num_columns)
746 + outputs[backend] = indices
747 +
748 + row_ends = (
749 + (seq_lens - next_n + 1 + torch.arange(next_n, device="cuda"))
750 + .clamp_(min=0)
751 + .reshape(-1)
752 + .tolist()
753 + )
754 + assert row_ends[:next_n] == [0, 0, 1, 2]
755 +
756 + for row_idx, row_end in enumerate(row_ends):
757 + num_valid = min(top_k, row_end)
758 + aiter_row = outputs["aiter"][row_idx]
759 + assert set(aiter_row[:num_valid].tolist()) == set(
760 + outputs["per_row"][row_idx][:num_valid].tolist()
761 + )
762 + assert torch.all(aiter_row[num_valid:] == -1)
763
738 - def decode_kernel(*args, **kwargs) -> None:
739 - pass
764 +
765 +def _enable_aiter_topk(monkeypatch, enabled: bool = True) -> None:
766 + from vllm._aiter_ops import rocm_aiter_ops
767
768 monkeypatch.setattr(
742 - rocm_aiter_mla_sparse,
743 - "_get_aiter_topk_ops",
744 - lambda: (prefill_kernel, decode_kernel),
769 + rocm_aiter_ops, "is_indexer_top_k_enabled", lambda: enabled, raising=True
770 )
746 - get_kernel = rocm_aiter_mla_sparse._get_aiter_top_k_kernel
771
748 - eligible_cases = [
749 - (
750 - dict(is_prefill=True, compress_ratio=4, num_rows=1, on_gfx950=True),
751 - prefill_kernel,
752 - ),
753 - (
754 - dict(
755 - is_prefill=False,
756 - compress_ratio=4,
757 - num_rows=1,
758 - max_valid_seq_len=65_536,
759 - on_gfx950=True,
760 - ),
761 - decode_kernel,
762 - ),
763 - (
764 - dict(
765 - is_prefill=False,
766 - compress_ratio=4,
767 - num_rows=257,
768 - max_valid_seq_len=250_000,
769 - num_columns=524_288,
770 - on_gfx950=True,
771 - ),
772 - decode_kernel,
773 - ),
774 - (
775 - dict(
776 - is_prefill=False,
777 - compress_ratio=2,
778 - num_rows=385,
779 - max_valid_seq_len=250_000,
780 - num_columns=524_288,
781 - topk_tokens=512,
782 - on_gfx950=True,
783 - ),
784 - decode_kernel,
785 - ),
786 - (
787 - dict(
788 - is_prefill=False,
789 - compress_ratio=2,
790 - num_rows=384,
791 - max_valid_seq_len=50_000,
792 - num_columns=1_048_577,
793 - topk_tokens=512,
794 - on_gfx950=True,
795 - ),
796 - decode_kernel,
797 - ),
798 - ]
799 - for kwargs, expected_kernel in eligible_cases:
800 - assert get_kernel(**kwargs) is expected_kernel
772
802 - def fail_if_imported() -> None:
803 - pytest.fail("ineligible shapes must not import AITER top-k")
773 +def test_aiter_c4a_topk_kernel_selection(monkeypatch) -> None:
774 + from vllm._aiter_ops import rocm_aiter_ops
775
805 - monkeypatch.setattr(
806 - rocm_aiter_mla_sparse,
807 - "_get_aiter_topk_ops",
808 - fail_if_imported,
809 - )
810 - ineligible_cases = [
811 - dict(is_prefill=True, compress_ratio=1, num_rows=1, on_gfx950=True),
776 + _enable_aiter_topk(monkeypatch)
777 + is_supported = rocm_aiter_ops.is_indexer_top_k_supported
778 +
779 + eligible_cases = [
780 + dict(is_prefill=True, compress_ratio=4, num_rows=1),
781 + dict(is_prefill=False, compress_ratio=4, num_rows=1, max_valid_seq_len=65_536),
782 dict(
783 is_prefill=False,
814 - compress_ratio=2,
815 - num_rows=384,
816 - max_valid_seq_len=1_048_576,
817 - num_columns=1_048_576,
818 - topk_tokens=512,
819 - on_gfx950=True,
784 + compress_ratio=4,
785 + num_rows=257,
786 + max_valid_seq_len=250_000,
787 ),
788 dict(
789 is_prefill=False,
823 - compress_ratio=4,
824 - num_rows=1,
825 - max_valid_seq_len=65_537,
826 - on_gfx950=True,
790 + compress_ratio=2,
791 + num_rows=384,
792 + max_valid_seq_len=1_048_576,
793 ),
794 + ]
795 + for kwargs in eligible_cases:
796 + assert is_supported(**kwargs) is True
797 +
798 + ineligible_cases = [
799 + dict(is_prefill=True, compress_ratio=1, num_rows=1),
800 + dict(is_prefill=False, compress_ratio=4, num_rows=1, max_valid_seq_len=65_537),
801 dict(
802 is_prefill=False,
803 compress_ratio=4,
804 num_rows=256,
805 max_valid_seq_len=125_000,
833 - on_gfx950=True,
834 - ),
835 - dict(
836 - is_prefill=False,
837 - compress_ratio=4,
838 - num_rows=320,
839 - max_valid_seq_len=2_500,
840 - on_gfx950=False,
806 ),
807 ]
808 for kwargs in ineligible_cases:
844 - assert get_kernel(**kwargs) is None
809 + assert is_supported(**kwargs) is False
810 +
811 + _enable_aiter_topk(monkeypatch, enabled=False)
812 + assert is_supported(is_prefill=True, compress_ratio=4, num_rows=1) is False
813
846 - monkeypatch.setattr(rocm_aiter_mla_sparse, "_get_aiter_topk_ops", lambda: None)
847 - assert (
848 - get_kernel(
849 - is_prefill=True,
850 - compress_ratio=4,
851 - num_rows=1,
852 - on_gfx950=True,
853 - )
854 - is None
855 - )
814 +
815 +def test_dsv4_native_top_k_window() -> None:
816 + """The in-tree decode kernel's measured advantage band over AITER, which
817 + applies to the DSV4 indexer only."""
818 + from vllm._aiter_ops import rocm_aiter_ops
819 +
820 + prefers_native = rocm_aiter_ops.dsv4_indexer_prefers_native_top_k
821 + assert prefers_native(num_rows=128, num_columns=524_288, topk_tokens=512) is True
822 + assert prefers_native(num_rows=385, num_columns=524_288, topk_tokens=512) is False
823 + assert prefers_native(num_rows=128, num_columns=1_048_577, topk_tokens=512) is False
824 + assert prefers_native(num_rows=128, num_columns=524_288, topk_tokens=1024) is False
825
826
827 @pytest.mark.skipif(not current_platform.is_cuda(), reason="This test requires CUDA")
@@ -1927,3 +1896,141 @@ def test_sparse_indexer_topk_backend_resolution() -> None:
1896 if not is_sm100:
1897 with pytest.raises(RuntimeError, match="SM100a/SM103a"):
1898 resolve("deep_select")
1899 +
1900 +
1901 +def _patch_aiter_topk(monkeypatch, decode_kernel, enabled: bool = True) -> None:
1902 + from vllm._aiter_ops import rocm_aiter_ops
1903 +
1904 + _enable_aiter_topk(monkeypatch, enabled=enabled)
1905 + monkeypatch.setattr(rocm_aiter_ops, "is_enabled", lambda: enabled, raising=True)
1906 + monkeypatch.setattr(rocm_aiter_ops, "indexer_top_k_decode", decode_kernel)
1907 +
1908 +
1909 +def _make_topk(backend: str):
1910 + from vllm.config import VllmConfig, set_current_vllm_config
1911 + from vllm.model_executor.layers.indexer_topk import SparseIndexerTopk
1912 +
1913 + cfg = VllmConfig(kernel_config={"sparse_indexer_topk_backend": backend})
1914 + with set_current_vllm_config(cfg):
1915 + return SparseIndexerTopk()
1916 +
1917 +
1918 +@pytest.mark.skipif(not current_platform.is_cuda(), reason="requires CUDA")
1919 +def test_sparse_indexer_topk_aiter_requires_rocm() -> None:
1920 + """Explicitly requesting the ROCm-only backend must fail fast elsewhere."""
1921 + logits = torch.randn(8, 4096, dtype=torch.float32, device="cuda")
1922 + with pytest.raises(RuntimeError, match="ROCm"):
1923 + _make_topk("aiter").resolve_backend(logits, 2048, 8)
1924 +
1925 +
1926 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="requires ROCm")
1927 +def test_sparse_indexer_topk_backend_resolution_aiter(monkeypatch) -> None:
1928 + """Aiter is opt-in only: "auto" never selects it (the shape-dependent perf
1929 + gate lives at the call site), and an explicit request validates hard
1930 + capability only."""
1931 +
1932 + def decode_kernel(*args, **kwargs) -> None:
1933 + pass
1934 +
1935 + _patch_aiter_topk(monkeypatch, decode_kernel)
1936 + logits = torch.randn(8, 4096, dtype=torch.float32, device="cuda")
1937 +
1938 + assert _make_topk("auto").resolve_backend(logits, 2048, 8) == "per_row"
1939 + assert _make_topk("aiter").resolve_backend(logits, 2048, 8) == "aiter"
1940 + # Shapes the call-site perf gate would decline still resolve.
1941 + assert _make_topk("aiter").resolve_backend(logits, 512, 8) == "aiter"
1942 + with pytest.raises(RuntimeError, match="topk_tokens must be in"):
1943 + _make_topk("aiter").resolve_backend(logits, 3000, 8)
1944 +
1945 + _enable_aiter_topk(monkeypatch, enabled=False)
1946 + with pytest.raises(RuntimeError, match="no tuned indexer top-k kernels"):
1947 + _make_topk("aiter").resolve_backend(logits, 2048, 8)
1948 +
1949 + _patch_aiter_topk(monkeypatch, decode_kernel, enabled=False)
1950 + assert _make_topk("auto").resolve_backend(logits, 2048, 8) == "per_row"
1951 + with pytest.raises(RuntimeError, match="VLLM_ROCM_USE_AITER"):
1952 + _make_topk("aiter").resolve_backend(logits, 2048, 8)
1953 +
1954 +
1955 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="requires ROCm")
1956 +@torch.inference_mode()
1957 +def test_sparse_indexer_topk_aiter_applies_no_shape_gate(monkeypatch) -> None:
1958 + """Shape-dependent perf gating belongs to the call site: once "aiter" is
1959 + requested the dispatcher must run it for every supported k, including the
1960 + k=512 band the in-tree kernel wins on DSV4's compressed-KV logits."""
1961 + from vllm.model_executor.layers import indexer_topk
1962 +
1963 + calls: list[tuple] = []
1964 + _patch_aiter_topk(monkeypatch, lambda *args: calls.append(args))
1965 + monkeypatch.setattr(
1966 + indexer_topk.ops,
1967 + "top_k_per_row_decode",
1968 + lambda *args: pytest.fail("an explicit aiter request must not fall back"),
1969 + )
1970 +
1971 + num_rows = 8
1972 + logits = torch.randn(num_rows, 4096, dtype=torch.float32, device="cuda")
1973 + seq_lens = torch.full((num_rows, 1), 4096, dtype=torch.int32, device="cuda")
1974 + indices = torch.empty((num_rows, 512), dtype=torch.int32, device="cuda")
1975 + op = _make_topk("aiter")
1976 +
1977 + op(logits, seq_lens, 1, indices, 512, 4096)
1978 + # Past the compressed-context boundary the call-site gate declines on.
1979 + op(logits, seq_lens, 1, indices, 512, 4 * (64 * 1024 + 1))
1980 + assert len(calls) == 2
1981 +
1982 +
1983 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="requires ROCm")
1984 +@torch.inference_mode()
1985 +def test_sparse_indexer_topk_auto_stays_on_per_row(monkeypatch) -> None:
1986 + """The "auto" chain must not reach AITER on its own: the decode top-k runs
1987 + through top_k_per_row_decode unless the caller asks for the aiter backend."""
1988 + from vllm.model_executor.layers import indexer_topk
1989 +
1990 + def decode_kernel(*args, **kwargs) -> None:
1991 + pytest.fail("AITER is opt-in only")
1992 +
1993 + _patch_aiter_topk(monkeypatch, decode_kernel)
1994 + calls: list[tuple] = []
1995 + monkeypatch.setattr(
1996 + indexer_topk.ops, "top_k_per_row_decode", lambda *args: calls.append(args)
1997 + )
1998 +
1999 + num_rows = 8
2000 + logits = torch.randn(num_rows, 4096, dtype=torch.float32, device="cuda")
2001 + seq_lens = torch.full((num_rows, 1), 4096, dtype=torch.int32, device="cuda")
2002 + indices = torch.empty((num_rows, 1024), dtype=torch.int32, device="cuda")
2003 +
2004 + _make_topk("auto")(logits, seq_lens, 1, indices, 1024, 4096)
2005 + assert len(calls) == 1
2006 +
2007 +
2008 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="requires ROCm")
2009 +@torch.inference_mode()
2010 +def test_sparse_indexer_topk_aiter_expands_mtp_row_ends(monkeypatch) -> None:
2011 + """AITER's decode kernel only implements the next_n == 1 form, so spec
2012 + decode batches must be handed the equivalent expanded per-row ends."""
2013 + from vllm.model_executor.layers import indexer_topk
2014 +
2015 + recorded: dict = {}
2016 +
2017 + def decode_kernel(logits, next_n, seq_lens, *args, **kwargs) -> None:
2018 + recorded["next_n"] = next_n
2019 + recorded["row_ends"] = seq_lens.tolist()
2020 +
2021 + _patch_aiter_topk(monkeypatch, decode_kernel)
2022 + monkeypatch.setattr(
2023 + indexer_topk.ops,
2024 + "top_k_per_row_decode",
2025 + lambda *args: pytest.fail("an explicit aiter request must not fall back"),
2026 + )
2027 +
2028 + next_n = 2
2029 + logits = torch.randn(4, 4096, dtype=torch.float32, device="cuda")
2030 + seq_lens = torch.tensor([[100], [200]], dtype=torch.int32, device="cuda")
2031 + indices = torch.empty((4, 1024), dtype=torch.int32, device="cuda")
2032 +
2033 + _make_topk("aiter")(logits, seq_lens, next_n, indices, 1024, 800)
2034 +
2035 + assert recorded["next_n"] == 1
2036 + assert recorded["row_ends"] == [99, 100, 199, 200]
tests/models/glm5next/test_sparse_indexer_topk_dispatch.py
+72 -14
@@ -8,10 +8,6 @@ import torch
8 from vllm.config import VllmConfig, set_current_vllm_config
9 from vllm.platforms import current_platform
10
11 -pytestmark = pytest.mark.skipif(
12 - not current_platform.is_cuda(), reason="CUDA-only dispatch"
13 -)
14 -
11
12 def _require_deep_gemm() -> None:
13 from vllm.utils.deep_gemm import has_deep_gemm
@@ -20,17 +16,10 @@ def _require_deep_gemm() -> None:
16 pytest.skip("kpool indexer requires DeepGEMM")
17
18
23 -@pytest.mark.parametrize("backend", ["auto", "persistent", "cooperative", "torch"])
24 -def test_kpool_indexer_dispatches_through_shared_topk_backend(backend: str) -> None:
25 - """The kpool indexer must read kernel_config.sparse_indexer_topk_backend
26 - and hand it to the shared SparseIndexerTopk dispatcher, rather than
27 - hard-coding its own cooperative/persistent/per_row choice."""
28 - _require_deep_gemm()
29 - from vllm.models.glm5next.nvidia.sparse_indexer import SparseAttnIndexerKpool
30 -
19 +def _build(indexer_cls, backend: str):
20 cfg = VllmConfig(kernel_config={"sparse_indexer_topk_backend": backend})
21 with set_current_vllm_config(cfg):
33 - op = SparseAttnIndexerKpool(
22 + return indexer_cls(
23 k_cache=None,
24 quant_block_size=128,
25 scale_fmt="ue8m0",
@@ -40,4 +29,73 @@ def test_kpool_indexer_dispatches_through_shared_topk_backend(backend: str) -> N
29 max_total_seq_len=8192,
30 topk_indices_buffer=torch.empty(8, 2176, dtype=torch.int32, device="cuda"),
31 )
43 - assert op.topk_backend == backend
32 +
33 +
34 +@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only dispatch")
35 +@pytest.mark.parametrize("backend", ["auto", "persistent", "cooperative", "torch"])
36 +def test_kpool_indexer_dispatches_through_shared_topk_backend(backend: str) -> None:
37 + """The kpool indexer must read kernel_config.sparse_indexer_topk_backend
38 + and hand it to the shared SparseIndexerTopk dispatcher, rather than
39 + hard-coding its own cooperative/persistent/per_row choice."""
40 + _require_deep_gemm()
41 + from vllm.models.glm5next.nvidia.sparse_indexer import SparseAttnIndexerKpool
42 +
43 + assert _build(SparseAttnIndexerKpool, backend).topk_backend == backend
44 +
45 +
46 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="ROCm-only dispatch")
47 +@pytest.mark.parametrize("backend", ["auto", "aiter", "per_row", "torch"])
48 +def test_kpool_indexer_dispatches_through_shared_topk_backend_rocm(
49 + backend: str,
50 +) -> None:
51 + """The AMD kpool indexer must go through the same dispatcher, so the
52 + AITER decode top-k is reachable from kernel_config."""
53 + from vllm.models.glm5next.amd.sparse_indexer import SparseAttnIndexerKpool
54 +
55 + assert _build(SparseAttnIndexerKpool, backend).topk_backend == backend
56 +
57 +
58 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="ROCm-only dispatch")
59 +@pytest.mark.parametrize(
60 + "configured,full_cudagraph,expected",
61 + [
62 + ("auto", False, "aiter"),
63 + ("auto", True, "auto"),
64 + ("aiter", True, "aiter"),
65 + ("per_row", False, "per_row"),
66 + ],
67 +)
68 +def test_kpool_decode_topk_backend_never_auto_selects_aiter_under_full_cudagraph(
69 + monkeypatch: pytest.MonkeyPatch,
70 + configured: str,
71 + full_cudagraph: bool,
72 + expected: str,
73 +) -> None:
74 + """A FULL cudagraph bakes in one kernel for every replay, so "auto" cannot
75 + use the context length to pick AITER and must stay on the in-tree kernel.
76 + Explicit backends keep working, and outside FULL the long-context heuristic
77 + still narrows to AITER."""
78 + from vllm._aiter_ops import rocm_aiter_ops
79 + from vllm.models.glm5next.amd import sparse_indexer
80 +
81 + monkeypatch.setattr(
82 + rocm_aiter_ops, "is_indexer_top_k_enabled", lambda: True, raising=False
83 + )
84 + monkeypatch.setattr(
85 + rocm_aiter_ops,
86 + "is_indexer_top_k_supported",
87 + lambda **kwargs: True,
88 + raising=False,
89 + )
90 +
91 + assert (
92 + sparse_indexer._kpool_decode_topk_backend(
93 + configured,
94 + num_rows=8,
95 + max_valid_seq_len=64 * 1024,
96 + select_k=512,
97 + index_kpool=4,
98 + full_cudagraph=full_cudagraph,
99 + )
100 + == expected
101 + )
vllm/_aiter_ops.py
+99
@@ -1913,6 +1913,11 @@ def _sync_aiter_situv2_moe_env() -> None:
1913 os.environ.pop(name, None)
1914
1915
1916 +_GFX950_C4A_AITER_MAX_COMPRESSED_SEQ_LEN = 64 * 1024
1917 +_GFX950_C4A_NATIVE_MAX_ROWS = 256
1918 +_GFX950_DSV4_NATIVE_MAX_COLUMNS = 1024 * 1024
1919 +
1920 +
1921 class rocm_aiter_ops:
1922 """ROCm AITER operations wrapper for AMD GPU acceleration in vLLM.
1923
@@ -2546,6 +2551,100 @@ class rocm_aiter_ops:
2551 """Whether AITER has DSV4 native-I384 configs through the given M."""
2552 return cls._probe_dsv4_i384_fhmoe_capability(num_tokens)
2553
2554 + @classmethod
2555 + @if_aiter_supported
2556 + def is_indexer_top_k_enabled(cls) -> bool:
2557 + """gfx950 is the only arch with tuned AITER indexer top-k kernels."""
2558 + from vllm.platforms.rocm import on_gfx950
2559 +
2560 + return cls._AITER_ENABLED and on_gfx950()
2561 +
2562 + @classmethod
2563 + def is_indexer_top_k_supported(
2564 + cls,
2565 + *,
2566 + is_prefill: bool,
2567 + compress_ratio: int,
2568 + num_rows: int,
2569 + max_valid_seq_len: int | None = None,
2570 + ) -> bool:
2571 + """Whether AITER's sparse indexer top-k beats the in-tree kernel for
2572 + this shape."""
2573 + if compress_ratio <= 1 or not cls.is_indexer_top_k_enabled():
2574 + return False
2575 +
2576 + if not is_prefill:
2577 + assert max_valid_seq_len is not None
2578 + # AITER v0.1.19 decode is one-block only. This measured gfx950
2579 + # FP32/k=1024 compressed-row boundary is independent of the native
2580 + # split-count boundary in sampler.cu.
2581 + if (
2582 + num_rows <= _GFX950_C4A_NATIVE_MAX_ROWS
2583 + and max_valid_seq_len > _GFX950_C4A_AITER_MAX_COMPRESSED_SEQ_LEN
2584 + ):
2585 + return False
2586 +
2587 + return True
2588 +
2589 + @staticmethod
2590 + def dsv4_indexer_prefers_native_top_k(
2591 + *,
2592 + num_rows: int,
2593 + num_columns: int,
2594 + topk_tokens: int,
2595 + ) -> bool:
2596 + """The in-tree decode kernel's measured advantage window over AITER.
2597 + Tuned on DSV4's compressed-KV logits, so it applies to that indexer
2598 + only."""
2599 + return (
2600 + topk_tokens == 512
2601 + and 0 < num_rows <= 384
2602 + and num_columns <= _GFX950_DSV4_NATIVE_MAX_COLUMNS
2603 + )
2604 +
2605 + @staticmethod
2606 + def indexer_top_k_decode(
2607 + logits: torch.Tensor,
2608 + next_n: int,
2609 + seq_lens: torch.Tensor,
2610 + topk_indices: torch.Tensor,
2611 + topk_tokens: int,
2612 + ) -> None:
2613 + from aiter.ops.topk import top_k_per_row_decode
2614 +
2615 + top_k_per_row_decode(
2616 + logits,
2617 + next_n,
2618 + seq_lens,
2619 + topk_indices,
2620 + logits.shape[0],
2621 + logits.stride(0),
2622 + logits.stride(1),
2623 + k=topk_tokens,
2624 + )
2625 +
2626 + @staticmethod
2627 + def indexer_top_k_prefill(
2628 + logits: torch.Tensor,
2629 + row_starts: torch.Tensor,
2630 + row_ends: torch.Tensor,
2631 + indices: torch.Tensor,
2632 + topk_tokens: int,
2633 + ) -> None:
2634 + from aiter.ops.topk import top_k_per_row_prefill
2635 +
2636 + top_k_per_row_prefill(
2637 + logits,
2638 + row_starts,
2639 + row_ends,
2640 + indices,
2641 + None,
2642 + logits.shape[0],
2643 + logits.stride(0),
2644 + logits.stride(1),
2645 + k=topk_tokens,
2646 + )
2647 +
2648 @staticmethod
2649 def register_ops_once() -> None:
2650 global _OPS_REGISTERED
vllm/config/kernel.py
+3
@@ -167,6 +167,7 @@ SparseIndexerTopkBackend = Literal[
167 "per_row",
168 "flashinfer",
169 "torch",
170 + "aiter",
171 ]
172
173 # Architectures whose model code wires up the flashinfer moe_ep experts. MTP
@@ -289,6 +290,8 @@ class KernelConfig:
290 - "per_row": Use vLLM's top_k_per_row_decode kernel
291 - "flashinfer": Use FlashInfer's top_k_ragged_transform kernel
292 - "torch": Use a plain torch.topk implementation (debug reference)
293 + - "aiter": Use AITER's top_k_per_row_decode kernel (ROCm gfx950 only,
294 + requires VLLM_ROCM_USE_AITER)
295
296 Explicit values raise RuntimeError when their constraints are not met.
297 """
vllm/model_executor/layers/indexer_topk.py
+57 -10
@@ -163,6 +163,14 @@ class SparseIndexerTopk(torch.nn.Module):
163 current_platform.has_device_capability(90)
164 and not current_platform.is_device_capability_family(120)
165 )
166 + self._is_rocm = current_platform.is_rocm()
167 + self._aiter_enabled = False
168 + self._aiter_capable = False
169 + if self._is_rocm:
170 + from vllm._aiter_ops import rocm_aiter_ops
171 +
172 + self._aiter_enabled = bool(rocm_aiter_ops.is_enabled())
173 + self._aiter_capable = bool(rocm_aiter_ops.is_indexer_top_k_enabled())
174
175 def resolve_backend(
176 self, logits: torch.Tensor, topk_tokens: int, num_rows: int
@@ -193,6 +201,19 @@ class SparseIndexerTopk(torch.nn.Module):
201 f"inputs violate DeepSelect's constraints: dtype={logits.dtype},"
202 f" stride={logits.stride()}, topk={topk_tokens}"
203 )
204 + elif self._backend == "aiter":
205 + if not self._is_rocm:
206 + failures.append("requires a ROCm platform")
207 + elif not self._aiter_enabled:
208 + failures.append("requires AITER (VLLM_ROCM_USE_AITER=1)")
209 + elif not self._aiter_capable:
210 + failures.append(
211 + "AITER has no tuned indexer top-k kernels for this GPU arch"
212 + )
213 + if topk_tokens not in (512, 1024, 2048, 4096):
214 + failures.append(
215 + f"topk_tokens must be in (512, 1024, 2048, 4096), got {topk_tokens}"
216 + )
217 elif self._backend == "flashinfer":
218 if not self._is_cuda:
219 failures.append("requires a CUDA platform")
@@ -216,11 +237,14 @@ class SparseIndexerTopk(torch.nn.Module):
237 self, logits: torch.Tensor, topk_tokens: int, num_rows: int
238 ) -> str:
239 """The priority chain: cooperative -> persistent -> per_row.
219 - deep_select/flashinfer/torch are opt-in only.
240 + aiter/deep_select/flashinfer/torch are opt-in only.
241
242 cooperative_topk is preferred whenever it is applicable, i.e. within
243 its AUTO_COOPERATIVE_MAX_ROWS row limit; larger batches go to
244 persistent_topk.
245 +
246 + aiter not auto-resolved as it does not outperform the per-row on every shape,
247 + hence we delegate to the caller to decide when to use it.
248 """
249 if not self._cooperative_constraints(logits, topk_tokens, num_rows):
250 return "cooperative"
@@ -285,7 +309,8 @@ class SparseIndexerTopk(torch.nn.Module):
309 max_seq_len: int,
310 ) -> None:
311 """Run the resolved decode top-k implementation, writing into
288 - topk_indices (int32, -1 fill for rows shorter than topk_tokens)."""
312 + topk_indices (int32, -1 fill for rows shorter than topk_tokens).
313 + """
314 backend = self.resolve_backend(logits, topk_tokens, logits.shape[0])
315 if backend == "deep_select":
316 row_ends = self._row_ends(seq_lens, next_n, logits.shape[0])
@@ -344,15 +369,37 @@ class SparseIndexerTopk(torch.nn.Module):
369 ) < row_ends.unsqueeze(1)
370 indices = torch.where(in_range, indices, -1)
371 topk_indices.copy_(indices)
347 - else:
348 - assert backend == "per_row", f"unknown topk backend: {backend}"
349 - ops.top_k_per_row_decode(
372 + elif backend == "aiter":
373 + from vllm._aiter_ops import rocm_aiter_ops
374 +
375 + # AITER's decode kernel only implements the 1D/next_n == 1 form,
376 + # which is equivalent once the ragged ends are expanded.
377 + rocm_aiter_ops.indexer_top_k_decode(
378 logits,
351 - next_n,
352 - seq_lens,
379 + 1,
380 + self._row_ends(seq_lens, next_n, logits.shape[0]),
381 topk_indices,
354 - logits.shape[0],
355 - logits.stride(0),
356 - logits.stride(1),
382 topk_tokens,
383 )
384 + else:
385 + assert backend == "per_row", f"unknown topk backend: {backend}"
386 + self._per_row(logits, seq_lens, next_n, topk_indices, topk_tokens)
387 +
388 + @staticmethod
389 + def _per_row(
390 + logits: torch.Tensor,
391 + seq_lens: torch.Tensor,
392 + next_n: int,
393 + topk_indices: torch.Tensor,
394 + topk_tokens: int,
395 + ) -> None:
396 + ops.top_k_per_row_decode(
397 + logits,
398 + next_n,
399 + seq_lens,
400 + topk_indices,
401 + logits.shape[0],
402 + logits.stride(0),
403 + logits.stride(1),
404 + topk_tokens,
405 + )
vllm/models/glm5next/amd/sparse_indexer.py
+76 -6
@@ -8,10 +8,11 @@ import vllm.envs as envs
8 from vllm import _custom_ops # noqa: F401 # registers the torch.ops._C kernels
9 from vllm._aiter_ops import rocm_aiter_ops
10 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
11 -from vllm.config import get_current_vllm_config_or_none
11 +from vllm.config import CUDAGraphMode, get_current_vllm_config_or_none
12 from vllm.forward_context import get_forward_context
13 from vllm.logger import init_logger
14 from vllm.model_executor.custom_op import CustomOp
15 +from vllm.model_executor.layers.indexer_topk import get_indexer_topk
16 from vllm.models.glm5next.amd.ops import kpool_compress as kpool_ops
17 from vllm.models.glm5next.common.sparse_indexer import (
18 RADIX_TOPK_WORKSPACE_SIZE,
@@ -24,6 +25,7 @@ from vllm.models.glm5next.common.sparse_indexer import (
25 kv_cache_as_quant_view,
26 )
27 from vllm.platforms import current_platform
28 +from vllm.utils.math_utils import cdiv
29 from vllm.utils.torch_utils import (
30 LayerNameType,
31 _encode_layer_name,
@@ -37,6 +39,9 @@ from vllm.v1.worker.workspace import current_workspace_manager
39
40 logger = init_logger(__name__)
41
42 +_GFX950_KPOOL_AITER_MAX_POOLS = 256 * 1024 # (1M ctx)
43 +_GFX950_KPOOL_AITER_MIN_POOLS = 16 * 1024
44 +
45 # kpool write helper: form pools from the current token batch and compress them
46 # into the index K cache via the fused Triton kernel.
47
@@ -87,6 +92,51 @@ def _kpool_compress_insert(
92 )
93
94
95 +def _kpool_decode_topk_backend(
96 + configured: str,
97 + *,
98 + num_rows: int,
99 + max_valid_seq_len: int,
100 + select_k: int,
101 + index_kpool: int,
102 + full_cudagraph: bool,
103 +) -> str:
104 + """Select the topk backend for decodes.
105 +
106 + Heuristic based on ctx lengths:
107 + - <16k pools: in-tree hip kernel
108 + - 16-256k pools: use aiter
109 + - >256k pools: in-tree because not measured on >1m ctx
110 +
111 + Since under FULL cudagraphs we cannot access the context length,
112 + and the aiter kernel does not always outperform the in-tree kernel,
113 + we default to in-tree kernel under FULL cudagraphs. Users can opt in
114 + if they know their context length is long enough to benefit from aiter.
115 + """
116 + if (
117 + configured != "auto"
118 + or full_cudagraph
119 + or not rocm_aiter_ops.is_indexer_top_k_enabled()
120 + ):
121 + return configured
122 + if rocm_aiter_ops.is_indexer_top_k_supported(
123 + is_prefill=False,
124 + compress_ratio=index_kpool,
125 + num_rows=num_rows,
126 + max_valid_seq_len=max_valid_seq_len,
127 + ):
128 + return "aiter"
129 + if (
130 + index_kpool > 1
131 + and select_k == 512
132 + and _GFX950_KPOOL_AITER_MIN_POOLS
133 + <= max_valid_seq_len
134 + <= _GFX950_KPOOL_AITER_MAX_POOLS
135 + ):
136 + return "aiter"
137 + return configured
138 +
139 +
140 @eager_break_during_capture
141 def sparse_attn_indexer_kpool(
142 hidden_states: torch.Tensor,
@@ -117,6 +167,7 @@ def sparse_attn_indexer_kpool(
167 # path and when the tail cache is disabled.
168 tail_kv_cache: torch.Tensor | None = None,
169 tail_prefix: str | None = None,
170 + topk_backend: str = "auto",
171 ) -> torch.Tensor:
172 # careful! this will be None in dummy run
173 attn_metadata = get_forward_context().attn_metadata
@@ -580,15 +631,29 @@ def sparse_attn_indexer_kpool(
631 else:
632 topk_dst = topk_indices_buffer[:num_padded_tokens, :topk_tokens]
633
583 - torch.ops._C.top_k_per_row_decode(
634 + full_cudagraph = (
635 + get_forward_context().cudagraph_runtime_mode == CUDAGraphMode.FULL
636 + )
637 + if full_cudagraph:
638 + topk_max_seq_len = max_pool_len * index_kpool
639 + else:
640 + topk_max_seq_len = attn_metadata_narrowed.max_seq_len
641 +
642 + resolved_backend = _kpool_decode_topk_backend(
643 + topk_backend,
644 + num_rows=num_rows,
645 + max_valid_seq_len=cdiv(topk_max_seq_len, index_kpool),
646 + select_k=select_k,
647 + index_kpool=index_kpool,
648 + full_cudagraph=full_cudagraph,
649 + )
650 + get_indexer_topk(resolved_backend)(
651 logits,
585 - next_n,
652 seq_lens,
653 + next_n,
654 topk_dst,
588 - num_rows,
589 - logits.stride(0),
590 - logits.stride(1),
655 select_k,
656 + topk_max_seq_len,
657 )
658
659 # Resolve to token-level indices in the output buffer.
@@ -664,6 +729,10 @@ class SparseAttnIndexerKpool(CustomOp):
729 self.topk_indices_buffer = topk_indices_buffer
730 self.skip_k_cache_insert = skip_k_cache_insert
731 self.use_fp4_cache = use_fp4_cache
732 + cfg = get_current_vllm_config_or_none()
733 + self.topk_backend = (
734 + cfg.kernel_config.sparse_indexer_topk_backend if cfg is not None else "auto"
735 + )
736
737 def forward_hip(
738 self,
@@ -726,6 +795,7 @@ class SparseAttnIndexerKpool(CustomOp):
795 positions,
796 self.tail_cache.kv_cache if self.tail_cache is not None else None,
797 self.tail_cache.prefix if self.tail_cache is not None else None,
798 + self.topk_backend,
799 )
800
801 def forward_native(
vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
+15 -92
@@ -32,18 +32,6 @@ logger = init_logger(__name__)
32 FP8_DTYPE = current_platform.fp8_dtype()
33
34
35 -@functools.cache
36 -def _get_aiter_topk_ops() -> tuple[Callable[..., None], Callable[..., None]] | None:
37 - try:
38 - from aiter.ops.topk import (
39 - top_k_per_row_decode,
40 - top_k_per_row_prefill,
41 - )
42 - except ImportError:
43 - return None
44 - return top_k_per_row_prefill, top_k_per_row_decode
45 -
46 -
35 @functools.cache
36 def _get_aiter_sparse_prefill_opus() -> Callable[..., torch.Tensor] | None:
37 from vllm._aiter_ops import rocm_aiter_ops
@@ -58,9 +46,6 @@ def _get_aiter_sparse_prefill_opus() -> Callable[..., torch.Tensor] | None:
46 return pa_sparse_prefill_opus
47
48
61 -_GFX950_C4A_AITER_MAX_COMPRESSED_SEQ_LEN = 64 * 1024
62 -_GFX950_C4A_NATIVE_MAX_ROWS = 256
63 -_GFX950_DSV4_NATIVE_MAX_COLUMNS = 1024 * 1024
49 # Conservative perf gate, not a correctness bound: OPUS is correct for any query
50 # count, but Triton stays faster below this measured crossover.
51 _GFX950_AITER_SPARSE_PREFILL_OPUS_MIN_QUERIES = 1024
@@ -71,43 +56,6 @@ def _indexer_k_is_c4a_block_flat(compress_ratio: int) -> bool:
56 return compress_ratio == 4
57
58
74 -def _get_aiter_top_k_kernel(
75 - *,
76 - is_prefill: bool,
77 - compress_ratio: int,
78 - num_rows: int,
79 - max_valid_seq_len: int | None = None,
80 - num_columns: int | None = None,
81 - topk_tokens: int = 1024,
82 - on_gfx950: bool = _ON_GFX950,
83 -) -> Callable[..., None] | None:
84 - if compress_ratio <= 1 or not on_gfx950:
85 - return None
86 -
87 - if not is_prefill:
88 - assert max_valid_seq_len is not None
89 - if (
90 - topk_tokens == 512
91 - and 0 < num_rows <= 384
92 - and num_columns is not None
93 - and num_columns <= _GFX950_DSV4_NATIVE_MAX_COLUMNS
94 - ):
95 - return None
96 - # AITER v0.1.19 decode is one-block only. This measured gfx950
97 - # FP32/k=1024 compressed-row boundary is independent of the native
98 - # split-count boundary in sampler.cu.
99 - if (
100 - num_rows <= _GFX950_C4A_NATIVE_MAX_ROWS
101 - and max_valid_seq_len > _GFX950_C4A_AITER_MAX_COMPRESSED_SEQ_LEN
102 - ):
103 - return None
104 -
105 - topk_ops = _get_aiter_topk_ops()
106 - if topk_ops is None:
107 - return None
108 - return topk_ops[0] if is_prefill else topk_ops[1]
109 -
110 -
59 @triton.jit
60 def _localize_aiter_prefill_topk_kernel(
61 indices_ptr,
@@ -145,44 +93,18 @@ def _localize_aiter_prefill_topk(
93
94
95 def _launch_aiter_top_k_per_row_prefill(
148 - top_k_per_row_prefill: Callable[..., None],
96 logits: torch.Tensor,
97 row_starts: torch.Tensor,
98 row_ends: torch.Tensor,
99 indices: torch.Tensor,
100 topk_tokens: int,
101 ) -> None:
155 - top_k_per_row_prefill(
156 - logits,
157 - row_starts,
158 - row_ends,
159 - indices,
160 - None,
161 - logits.shape[0],
162 - logits.stride(0),
163 - logits.stride(1),
164 - k=topk_tokens,
165 - )
166 - _localize_aiter_prefill_topk(indices, row_starts)
167 -
102 + from vllm._aiter_ops import rocm_aiter_ops
103
169 -def _launch_aiter_top_k_per_row_decode(
170 - top_k_per_row_decode: Callable[..., None],
171 - logits: torch.Tensor,
172 - seq_lens: torch.Tensor,
173 - indices: torch.Tensor,
174 - topk_tokens: int,
175 -) -> None:
176 - top_k_per_row_decode(
177 - logits,
178 - 1,
179 - seq_lens.reshape(-1),
180 - indices,
181 - logits.shape[0],
182 - logits.stride(0),
183 - logits.stride(1),
184 - k=topk_tokens,
104 + rocm_aiter_ops.indexer_top_k_prefill(
105 + logits, row_starts, row_ends, indices, topk_tokens
106 )
107 + _localize_aiter_prefill_topk(indices, row_starts)
108
109
110 @triton.jit
@@ -1195,6 +1117,8 @@ def rocm_aiter_sparse_attn_indexer(
1117 candidate_block_size: int = 0,
1118 candidate_write: bool = False,
1119 ) -> torch.Tensor:
1120 + from vllm._aiter_ops import rocm_aiter_ops
1121 +
1122 # careful! this will be None in dummy run
1123 forward_context = get_forward_context()
1124 attn_metadata = forward_context.attn_metadata
@@ -1347,14 +1271,12 @@ def rocm_aiter_sparse_attn_indexer(
1271
1272 num_rows = logits.shape[0]
1273
1350 - aiter_topk_kernel = _get_aiter_top_k_kernel(
1274 + if rocm_aiter_ops.is_indexer_top_k_supported(
1275 is_prefill=True,
1276 compress_ratio=compress_ratio,
1277 num_rows=num_rows,
1354 - )
1355 - if aiter_topk_kernel is not None:
1278 + ):
1279 _launch_aiter_top_k_per_row_prefill(
1357 - aiter_topk_kernel,
1280 logits,
1281 chunk.cu_seqlen_ks,
1282 chunk.cu_seqlen_ke,
@@ -1448,19 +1370,20 @@ def rocm_aiter_sparse_attn_indexer(
1370 max_compressed_seq_len = max_model_len
1371 else:
1372 max_compressed_seq_len = layer_attn_metadata.max_seq_len // compress_ratio
1451 - aiter_topk_kernel = _get_aiter_top_k_kernel(
1373 + if rocm_aiter_ops.is_indexer_top_k_supported(
1374 is_prefill=False,
1375 compress_ratio=compress_ratio,
1376 num_rows=num_rows,
1377 max_valid_seq_len=max_compressed_seq_len,
1378 + ) and not rocm_aiter_ops.dsv4_indexer_prefers_native_top_k(
1379 + num_rows=num_rows,
1380 num_columns=logits.shape[1],
1381 topk_tokens=topk_tokens,
1458 - )
1459 - if aiter_topk_kernel is not None:
1460 - _launch_aiter_top_k_per_row_decode(
1461 - aiter_topk_kernel,
1382 + ):
1383 + rocm_aiter_ops.indexer_top_k_decode(
1384 logits,
1463 - decode_metadata.seq_lens,
1385 + 1,
1386 + decode_metadata.seq_lens.reshape(-1),
1387 topk_indices,
1388 topk_tokens,
1389 )