Skip to content

Commit 0598e29

Browse files
committed
[ROCm][Perf] Split MiniMax-M3 prefill index-score K loop
Signed-off-by: Aakif Nawaz <aakif.nawaz@amd.com>
1 parent c205726 commit 0598e29

2 files changed

Lines changed: 150 additions & 17 deletions

File tree

tests/kernels/attention/test_minimax_m3.py

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,46 @@ def test_sparse_kernels_recognize_fp8_dtypes(dtype: torch.dtype):
124124

125125

126126
# Index top-k kernels.
127+
def _assert_prefill_index_scores(
128+
actual: torch.Tensor,
129+
idx_q: torch.Tensor,
130+
index_kv_cache: torch.Tensor,
131+
block_table: torch.Tensor,
132+
q_lens: torch.Tensor,
133+
seq_lens: torch.Tensor,
134+
prefix_lens: torch.Tensor,
135+
block_size_q: int,
136+
) -> None:
137+
q_start = 0
138+
for req_id, (q_len, seq_len, prefix_len) in enumerate(
139+
zip(q_lens.tolist(), seq_lens.tolist(), prefix_lens.tolist())
140+
):
141+
q = idx_q[q_start : q_start + q_len]
142+
num_blocks = (seq_len + BLOCK_SIZE - 1) // BLOCK_SIZE
143+
pages = block_table[req_id, :num_blocks]
144+
k = index_kv_cache[pages].reshape(num_blocks * BLOCK_SIZE, -1)
145+
expected = torch.einsum("qhd,kd->hqk", q.float(), k.float())
146+
147+
q_pos = prefix_len + torch.arange(q_len, device=idx_q.device)
148+
k_pos = torch.arange(k.shape[0], device=idx_q.device)
149+
expected.masked_fill_(k_pos[None, :] > q_pos[:, None], -float("inf"))
150+
expected = (
151+
expected.reshape(idx_q.shape[1], q_len, num_blocks, BLOCK_SIZE)
152+
.max(dim=3)
153+
.values
154+
)
155+
156+
for local_q in range(q_len):
157+
q_block_end = min(q_len, (local_q // block_size_q + 1) * block_size_q)
158+
hi = min(seq_len, prefix_len + q_block_end)
159+
written_blocks = (hi + BLOCK_SIZE - 1) // BLOCK_SIZE
160+
torch.testing.assert_close(
161+
actual[:, q_start + local_q, :written_blocks],
162+
expected[:, local_q, :written_blocks],
163+
)
164+
q_start += q_len
165+
166+
127167
def _reference_index_topk(
128168
idx_q: torch.Tensor,
129169
index_kv_cache: torch.Tensor,
@@ -222,14 +262,34 @@ def _reference_decode_index_score(
222262
return out
223263

224264

225-
def test_prefill_index_topk_correctness():
265+
@pytest.mark.parametrize("long_context", [False, True])
266+
def test_prefill_index_topk_correctness(long_context: bool):
267+
if current_platform.is_rocm():
268+
from vllm.models.minimax_m3.amd.ops.index_topk import (
269+
minimax_m3_index_score as amd_index_score,
270+
)
271+
272+
index_score = amd_index_score
273+
else:
274+
index_score = minimax_m3_index_score
275+
276+
if long_context:
277+
if not current_platform.is_rocm():
278+
pytest.skip("The split-K index-score path is ROCm-specific.")
279+
from vllm.platforms.rocm import on_gfx942
280+
281+
if not on_gfx942():
282+
pytest.skip("The split-K index-score path is enabled on gfx942.")
283+
226284
topk = 6
227285
init_blocks = 0
228286
local_blocks = 1
229287
num_idx_heads = 2
230288
head_dim = 16
231-
q_lens = torch.tensor((4, 3), device="cuda", dtype=torch.int32)
232-
prefix_lens = torch.tensor((0, 1024), device="cuda", dtype=torch.int32)
289+
q_lens_values = (128, 129) if long_context else (4, 3)
290+
prefix_lens_values = (8192, 16384) if long_context else (0, 1024)
291+
q_lens = torch.tensor(q_lens_values, device="cuda", dtype=torch.int32)
292+
prefix_lens = torch.tensor(prefix_lens_values, device="cuda", dtype=torch.int32)
233293
seq_lens = prefix_lens + q_lens
234294
batch = q_lens.numel()
235295
max_seq_len = seq_lens.max().item()
@@ -242,13 +302,15 @@ def test_prefill_index_topk_correctness():
242302
batch, max_blocks
243303
)
244304
idx_q = torch.ones(q_lens.sum().item(), num_idx_heads, head_dim, device="cuda")
245-
index_kv_cache = torch.empty(num_pages, BLOCK_SIZE, head_dim, device="cuda")
246-
for req_id in range(batch):
247-
for block_id in range(max_blocks):
248-
page = block_table[req_id, block_id]
249-
index_kv_cache[page].fill_(block_id + 1)
305+
block_values = torch.empty(num_pages, device="cuda")
306+
block_values[block_table] = torch.arange(
307+
1, max_blocks + 1, device="cuda", dtype=torch.float32
308+
).expand(batch, -1)
309+
index_kv_cache = (
310+
block_values[:, None, None].expand(-1, BLOCK_SIZE, head_dim).contiguous()
311+
)
250312

251-
score = minimax_m3_index_score(
313+
score = index_score(
252314
idx_q,
253315
index_kv_cache,
254316
block_table,
@@ -259,6 +321,16 @@ def test_prefill_index_topk_correctness():
259321
max_seq_len=max_seq_len,
260322
num_kv_heads=num_idx_heads,
261323
)
324+
_assert_prefill_index_scores(
325+
score,
326+
idx_q,
327+
index_kv_cache,
328+
block_table,
329+
q_lens,
330+
seq_lens,
331+
prefix_lens,
332+
block_size_q=128 if long_context else 64,
333+
)
262334
actual = minimax_m3_index_topk(
263335
score,
264336
cu_seqlens,

vllm/models/minimax_m3/amd/ops/index_topk.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,20 @@
1717
import torch
1818

1919
from vllm.platforms import current_platform
20+
from vllm.platforms.rocm import on_gfx942
2021
from vllm.triton_utils import tl, triton
2122
from vllm.utils.math_utils import round_up
23+
from vllm.utils.platform_utils import num_compute_units
2224

2325
# One sparse block == one KV page.
2426
SPARSE_BLOCK_SIZE = 128
2527

28+
# gfx942 split-K prefill tuning. Target bounded K-loop lengths while retaining
29+
# enough work per split to amortize launch overhead.
30+
_INDEX_SCORE_TARGET_K_TILES_PER_SPLIT = 64
31+
_INDEX_SCORE_MAX_SPLITS = 16
32+
_INDEX_SCORE_MIN_SEQ_LEN = _INDEX_SCORE_TARGET_K_TILES_PER_SPLIT * SPARSE_BLOCK_SIZE
33+
2634

2735
# ---------------------------------------------------------------------------
2836
# Bitonic top-k helpers (layout-agnostic).
@@ -101,8 +109,10 @@ def _index_block_score_kernel(
101109
stride_bt_b,
102110
BLOCK_SIZE_Q: tl.constexpr,
103111
BLOCK_SIZE_K: tl.constexpr, # == SPARSE_BLOCK_SIZE (128)
112+
NUM_K_SPLITS: tl.constexpr,
104113
):
105-
pid_q = tl.program_id(0)
114+
pid_q = tl.program_id(0) // NUM_K_SPLITS
115+
pid_split = tl.program_id(0) % NUM_K_SPLITS
106116
pid_bh = tl.program_id(1)
107117
pid_b = pid_bh // num_idx_heads
108118
pid_h = pid_bh % num_idx_heads
@@ -114,6 +124,16 @@ def _index_block_score_kernel(
114124
if BLOCK_SIZE_Q * pid_q >= q_len:
115125
return
116126

127+
# Split the state-free K loop across programs. Every score element still
128+
# has exactly one writer, so no reduction or atomic operation is needed.
129+
hi = min(seq_len, prefix_len + (pid_q + 1) * BLOCK_SIZE_Q)
130+
num_k_tiles = tl.cdiv(hi, BLOCK_SIZE_K)
131+
tiles_per_split = tl.cdiv(num_k_tiles, NUM_K_SPLITS)
132+
tile_lo = pid_split * tiles_per_split
133+
tile_hi = min(num_k_tiles, tile_lo + tiles_per_split)
134+
if tile_lo >= tile_hi:
135+
return
136+
117137
q_ptrs = tl.make_block_ptr(
118138
base=q_ptr + seq_start * stride_q_n + pid_h * stride_q_h,
119139
shape=(q_len, head_dim),
@@ -130,10 +150,8 @@ def _index_block_score_kernel(
130150
off_d = tl.arange(0, head_dim)
131151
# Block table row for this request.
132152
bt_row = block_table_ptr + pid_b * stride_bt_b
133-
# Causal window: only blocks up to the last query token's position.
134-
hi = min(seq_len, prefix_len + (pid_q + 1) * BLOCK_SIZE_Q)
135-
for i in tl.range(0, hi, BLOCK_SIZE_K):
136-
blk = i // BLOCK_SIZE_K
153+
for blk in tl.range(tile_lo, tile_hi):
154+
i = blk * BLOCK_SIZE_K
137155
page = tl.load(bt_row + blk).to(tl.int64)
138156
pos = i + off_k
139157
# index-K for this page: [BLOCK_SIZE_D, BLOCK_SIZE_K] (transposed)
@@ -651,6 +669,39 @@ def _topk_index_merge_kernel(
651669
# ---------------------------------------------------------------------------
652670
# Python wrappers
653671
# ---------------------------------------------------------------------------
672+
def _index_score_launch_config(
673+
max_query_len: int,
674+
max_seq_len: int,
675+
batch: int,
676+
num_idx_heads: int,
677+
) -> tuple[int, int, dict]:
678+
"""Select a split-K launch only for shapes with useful parallel work."""
679+
if not on_gfx942() or max_query_len < 128 or max_seq_len < _INDEX_SCORE_MIN_SEQ_LEN:
680+
return 64, 1, {}
681+
682+
block_size_q = 128
683+
num_k_tiles = triton.cdiv(max_seq_len, SPARSE_BLOCK_SIZE)
684+
q_programs = triton.cdiv(max_query_len, block_size_q) * batch * num_idx_heads
685+
686+
# Aim for two programs per CU when the split cap and K work permit, and
687+
# avoid leaving very long K loops. Power-of-two splits limit compilations.
688+
occupancy_splits = triton.cdiv(2 * num_compute_units(), q_programs)
689+
work_splits = triton.cdiv(num_k_tiles, _INDEX_SCORE_TARGET_K_TILES_PER_SPLIT)
690+
num_splits = triton.next_power_of_2(max(occupancy_splits, work_splits))
691+
num_splits = min(_INDEX_SCORE_MAX_SPLITS, num_k_tiles, num_splits)
692+
if num_splits == 1:
693+
return 64, 1, {}
694+
695+
return (
696+
block_size_q,
697+
num_splits,
698+
{
699+
"num_warps": 4,
700+
"num_stages": 1,
701+
},
702+
)
703+
704+
654705
@torch.no_grad()
655706
def minimax_m3_index_score(
656707
idx_q: torch.Tensor, # [total_q, num_idx_heads, head_dim]
@@ -682,8 +733,16 @@ def minimax_m3_index_score(
682733
dtype=torch.float32,
683734
device=idx_q.device,
684735
)
685-
BLOCK_SIZE_Q = 64
686-
grid_score = (triton.cdiv(max_query_len, BLOCK_SIZE_Q), batch * num_idx_heads)
736+
block_size_q, num_k_splits, launch_kwargs = _index_score_launch_config(
737+
max_query_len,
738+
max_seq_len,
739+
batch,
740+
num_idx_heads,
741+
)
742+
grid_score = (
743+
triton.cdiv(max_query_len, block_size_q) * num_k_splits,
744+
batch * num_idx_heads,
745+
)
687746
_index_block_score_kernel[grid_score](
688747
idx_q,
689748
index_kv_cache,
@@ -704,8 +763,10 @@ def minimax_m3_index_score(
704763
score.stride(1),
705764
score.stride(2),
706765
block_table.stride(0),
707-
BLOCK_SIZE_Q=BLOCK_SIZE_Q,
766+
BLOCK_SIZE_Q=block_size_q,
708767
BLOCK_SIZE_K=SPARSE_BLOCK_SIZE,
768+
NUM_K_SPLITS=num_k_splits,
769+
**launch_kwargs,
709770
)
710771
return score
711772

0 commit comments

Comments
 (0)