@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 7d94577138
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 15, 01:00 UTC

Add torch golden impl for moe_align_block_size kernel test (#20653)

Signed-off-by: Shixian Cui <shixian@amazon.com> Co-authored-by: Shixian Cui <shixian@amazon.com>

shixianc committed Jul 19, 2025 at 02:32 UTC 7d94577138e3d4c7bcfd781337ee1e5a2befa685
1 file changed +296 -71
tests/kernels/moe/test_moe_align_block_size.py
+296 -71
@@ -1,90 +1,315 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -import itertools
3 +"""Tests for the MOE align block size function.
4 +
5 +Run `pytest tests/kernels/moe/test_moe_align_block_size.py`.
6 +"""
7 +
8 +from typing import Optional
9
10 import pytest
11 import torch
12
8 -from vllm import _custom_ops as ops
13 from vllm.model_executor.layers.fused_moe.moe_align_block_size import (
10 - moe_align_block_size_triton)
11 -
12 -
13 -@pytest.mark.parametrize(
14 - "block_size,num_tokens,topk,num_experts",
15 - list(
16 - itertools.product(
17 - [32, 64, 128, 256], # block_size
18 - [
19 - 1,
20 - 3,
21 - 7,
22 - 16,
23 - 256,
24 - 2256,
25 - 4096,
26 - ], # num_tokens
27 - [1, 4, 16, 64], # topk
28 - [64, 160, 256, 257, 260, 264], # num_experts
29 - )),
30 -)
31 -def test_moe_align_block_size_compare_implementations(block_size, num_tokens,
32 - topk, num_experts):
33 - topk_ids = torch.stack([
34 - torch.randperm(num_experts, dtype=torch.int32, device="cuda")[:topk]
35 - for _ in range(num_tokens)
36 - ])
14 + moe_align_block_size)
15 +from vllm.platforms import current_platform
16 +from vllm.utils import round_up
17 +
18 +NUM_TOKENS = [1, 3, 7, 16, 256, 2256, 4096]
19 +NUM_EXPERTS = [32, 160, 256, 257, 512]
20 +TOP_KS = [1, 2, 16, 32]
21 +BLOCK_SIZES = [32, 64, 128, 256]
22 +current_platform.seed_everything(0)
23 +
24 +
25 +def _group_tokens_by_expert(
26 + sorted_ids: torch.Tensor,
27 + expert_ids: torch.Tensor,
28 + block_size: int,
29 + valid_length: int,
30 + total_tokens: int,
31 +) -> dict:
32 + num_blocks = valid_length // block_size
33 + expert_tokens: dict[int, list[int]] = {}
34 +
35 + for block_idx in range(num_blocks):
36 + expert_id = expert_ids[block_idx].item()
37 + block_start = block_idx * block_size
38 + block_end = min(block_start + block_size, valid_length)
39 +
40 + block_tokens = sorted_ids[block_start:block_end]
41 + valid_tokens = block_tokens[block_tokens < total_tokens]
42 +
43 + if expert_id not in expert_tokens:
44 + expert_tokens[expert_id] = []
45 + expert_tokens[expert_id].extend(valid_tokens.tolist())
46 + return expert_tokens
47 +
48
49 +def _verify_expert_level_sorting(
50 + actual_sorted_ids: torch.Tensor,
51 + golden_sorted_ids: torch.Tensor,
52 + expert_ids: torch.Tensor,
53 + block_size: int,
54 + valid_length: int,
55 + total_tokens: int,
56 +):
57 + """
58 + Verify that actual_sorted_ids follows the correct expert-level sorting.
59 + The kerne limplementation may or may not preserve original token order
60 + in topk_ids in the final sorted_ids however this does not impact quality.
61 + """
62 + # Group tokens by expert from the golden implementation
63 + golden_expert_tokens = _group_tokens_by_expert(golden_sorted_ids,
64 + expert_ids, block_size,
65 + valid_length, total_tokens)
66 +
67 + actual_expert_tokens = _group_tokens_by_expert(actual_sorted_ids,
68 + expert_ids, block_size,
69 + valid_length, total_tokens)
70 +
71 + assert set(golden_expert_tokens.keys()) == set(
72 + actual_expert_tokens.keys()), (
73 + f"Expert IDs mismatch: golden={set(golden_expert_tokens.keys())}, "
74 + f"actual={set(actual_expert_tokens.keys())}")
75 +
76 + for expert_id in golden_expert_tokens:
77 + golden_tokens = torch.tensor(golden_expert_tokens[expert_id],
78 + device=actual_sorted_ids.device)
79 + actual_tokens = torch.tensor(actual_expert_tokens[expert_id],
80 + device=actual_sorted_ids.device)
81 + assert torch.equal(
82 + torch.sort(golden_tokens)[0],
83 + torch.sort(actual_tokens)[0]), (
84 + f"Expert {expert_id} token mismatch: "
85 + f"golden={golden_expert_tokens[expert_id]}, "
86 + f"actual={actual_expert_tokens[expert_id]}")
87 +
88 +
89 +def torch_moe_align_block_size(
90 + topk_ids: torch.Tensor,
91 + block_size: int,
92 + num_experts: int,
93 + expert_map: Optional[torch.Tensor] = None,
94 + pad_sorted_ids: bool = False,
95 +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
96 + """
97 + Golden torch implementation of moe_align_block_size.
98 +
99 + This function aligns the token distribution across experts to be compatible
100 + with block size for matrix multiplication by sorting tokens by expert and
101 + padding to block boundaries.
102 + """
103 max_num_tokens_padded = topk_ids.numel() + num_experts * (block_size - 1)
104 + if pad_sorted_ids:
105 + max_num_tokens_padded = round_up(max_num_tokens_padded, block_size)
106 +
107 + flattened_token_indices = torch.arange(topk_ids.numel(),
108 + device=topk_ids.device,
109 + dtype=torch.int32)
110 + flattened_expert_ids = topk_ids.flatten()
111 + sorted_expert_ids, sort_indices = torch.sort(flattened_expert_ids,
112 + stable=True)
113 + sorted_token_indices = flattened_token_indices[sort_indices]
114 +
115 + expert_token_counts = torch.zeros(num_experts,
116 + dtype=torch.int64,
117 + device=topk_ids.device)
118 + for expert_id in range(num_experts):
119 + mask = sorted_expert_ids == expert_id
120 + expert_token_counts[expert_id] = mask.sum()
121 +
122 + expert_padded_counts = torch.zeros(num_experts,
123 + dtype=torch.int64,
124 + device=topk_ids.device)
125 + for expert_id in range(num_experts):
126 + original_count = expert_token_counts[expert_id]
127 + if original_count > 0:
128 + expert_padded_counts[expert_id] = (
129 + (original_count + block_size - 1) // block_size) * block_size
130
40 - sorted_ids_cuda = torch.empty((max_num_tokens_padded, ),
41 - dtype=torch.int32,
42 - device=topk_ids.device)
43 - sorted_ids_cuda.fill_(topk_ids.numel())
44 - max_num_m_blocks = max_num_tokens_padded // block_size
45 - expert_ids_cuda = torch.zeros((max_num_m_blocks, ),
46 - dtype=torch.int32,
47 - device=topk_ids.device)
48 - num_tokens_post_pad_cuda = torch.empty((1),
49 - dtype=torch.int32,
50 - device=topk_ids.device)
51 -
52 - sorted_ids_triton = torch.empty_like(sorted_ids_cuda)
53 - sorted_ids_triton.fill_(topk_ids.numel())
54 - expert_ids_triton = torch.zeros_like(expert_ids_cuda)
55 - num_tokens_post_pad_triton = torch.empty_like(num_tokens_post_pad_cuda)
56 -
57 - ops.moe_align_block_size(
58 - topk_ids,
59 - num_experts,
131 + sorted_token_ids = torch.full(
132 + (max_num_tokens_padded, ),
133 + topk_ids.numel(),
134 + dtype=torch.int32,
135 + device=topk_ids.device,
136 + )
137 + max_num_blocks = (max_num_tokens_padded + block_size - 1) // block_size
138 + expert_ids = torch.zeros(max_num_blocks,
139 + dtype=torch.int32,
140 + device=topk_ids.device)
141 +
142 + current_pos = 0
143 + current_block = 0
144 + for expert_id in range(num_experts):
145 + expert_mask = sorted_expert_ids == expert_id
146 + expert_tokens = sorted_token_indices[expert_mask]
147 + num_expert_tokens = expert_tokens.shape[0]
148 +
149 + if num_expert_tokens > 0:
150 + sorted_token_ids[current_pos:current_pos +
151 + num_expert_tokens] = (expert_tokens)
152 +
153 + expert_blocks_needed = expert_padded_counts[expert_id] // block_size
154 + expert_ids[current_block:current_block +
155 + expert_blocks_needed] = (expert_id)
156 +
157 + current_pos += expert_padded_counts[expert_id]
158 + current_block += expert_blocks_needed
159 +
160 + total_padded_tokens = expert_padded_counts.sum()
161 + num_tokens_post_pad = torch.tensor([total_padded_tokens],
162 + dtype=torch.int32,
163 + device=topk_ids.device)
164 +
165 + if expert_map is not None:
166 + expert_ids = expert_map[expert_ids]
167 + return sorted_token_ids, expert_ids, num_tokens_post_pad
168 +
169 +
170 +@pytest.mark.parametrize("m", NUM_TOKENS)
171 +@pytest.mark.parametrize("topk", TOP_KS)
172 +@pytest.mark.parametrize("num_experts", NUM_EXPERTS)
173 +@pytest.mark.parametrize("block_size", BLOCK_SIZES)
174 +@pytest.mark.parametrize("pad_sorted_ids", [False, True])
175 +@pytest.mark.skipif(current_platform.is_rocm(), reason="Skip for rocm")
176 +def test_moe_align_block_size(m: int, topk: int, num_experts: int,
177 + block_size: int, pad_sorted_ids: bool):
178 + """Test moe_align_block_size without expert mapping"""
179 + topk_ids = torch.zeros((m, topk), device="cuda", dtype=torch.int32)
180 + for i in range(m):
181 + experts = torch.randperm(num_experts, device="cuda")[:topk]
182 + topk_ids[i] = experts
183 +
184 + actual_sorted_ids, actual_expert_ids, actual_num_tokens = (
185 + moe_align_block_size(
186 + topk_ids=topk_ids,
187 + block_size=block_size,
188 + num_experts=num_experts,
189 + pad_sorted_ids=pad_sorted_ids,
190 + ))
191 + golden_sorted_ids, golden_expert_ids, golden_num_tokens = (
192 + torch_moe_align_block_size(
193 + topk_ids=topk_ids,
194 + block_size=block_size,
195 + num_experts=num_experts,
196 + pad_sorted_ids=pad_sorted_ids,
197 + ))
198 +
199 + torch.testing.assert_close(actual_num_tokens,
200 + golden_num_tokens,
201 + atol=0,
202 + rtol=0)
203 + torch.testing.assert_close(actual_expert_ids,
204 + golden_expert_ids,
205 + atol=0,
206 + rtol=0)
207 +
208 + # For sorted_token_ids, verify block-level correctness rather than exact
209 + # order Tokens within each expert's blocks can be in any order, but expert
210 + # regions must be correct
211 + _verify_expert_level_sorting(
212 + actual_sorted_ids,
213 + golden_sorted_ids,
214 + actual_expert_ids,
215 block_size,
61 - sorted_ids_cuda,
62 - expert_ids_cuda,
63 - num_tokens_post_pad_cuda,
216 + actual_num_tokens.item(),
217 + m * topk,
218 )
219
66 - moe_align_block_size_triton(
67 - topk_ids,
68 - num_experts,
220 + total_tokens = m * topk
221 + assert actual_num_tokens.item() % block_size == 0, (
222 + "num_tokens_post_pad should be divisible by block_size")
223 + assert actual_num_tokens.item() >= total_tokens, (
224 + "num_tokens_post_pad should be at least total_tokens")
225 + valid_tokens = actual_sorted_ids[actual_sorted_ids < total_tokens]
226 + assert len(valid_tokens) == total_tokens, (
227 + f"Should have exactly {total_tokens} valid tokens, "
228 + f"got {len(valid_tokens)}")
229 + assert (actual_expert_ids >= 0).all() and (
230 + actual_expert_ids
231 + < num_experts).all(), "expert_ids should contain valid expert indices"
232 +
233 +
234 +@pytest.mark.parametrize("m", [16, 32])
235 +@pytest.mark.parametrize("topk", [2, 4])
236 +@pytest.mark.parametrize("num_experts", [8])
237 +@pytest.mark.parametrize("block_size", [64])
238 +@pytest.mark.skipif(current_platform.is_rocm(), reason="Skip for rocm")
239 +def test_moe_align_block_size_with_expert_map(m: int, topk: int,
240 + num_experts: int,
241 + block_size: int):
242 + """Test moe_align_block_size with expert mapping (EP scenario)"""
243 + topk_ids = torch.zeros((m, topk), device="cuda", dtype=torch.int32)
244 + for i in range(m):
245 + experts = torch.randperm(num_experts, device="cuda")[:topk]
246 + topk_ids[i] = experts
247 +
248 + expert_map = torch.full((num_experts, ),
249 + -1,
250 + device="cuda",
251 + dtype=torch.int32)
252 + local_experts = list(range(0, num_experts, 2))
253 + for i, expert_id in enumerate(local_experts):
254 + expert_map[expert_id] = i
255 +
256 + actual_sorted_ids, actual_expert_ids, actual_num_tokens = (
257 + moe_align_block_size(
258 + topk_ids=topk_ids,
259 + block_size=block_size,
260 + num_experts=num_experts,
261 + expert_map=expert_map,
262 + ))
263 + golden_sorted_ids, golden_expert_ids, golden_num_tokens = (
264 + torch_moe_align_block_size(
265 + topk_ids=topk_ids,
266 + block_size=block_size,
267 + num_experts=num_experts,
268 + expert_map=expert_map,
269 + ))
270 +
271 + torch.testing.assert_close(actual_num_tokens,
272 + golden_num_tokens,
273 + atol=0,
274 + rtol=0)
275 + torch.testing.assert_close(actual_expert_ids,
276 + golden_expert_ids,
277 + atol=0,
278 + rtol=0)
279 + _verify_expert_level_sorting(
280 + actual_sorted_ids,
281 + golden_sorted_ids,
282 + actual_expert_ids,
283 block_size,
70 - sorted_ids_triton,
71 - expert_ids_triton,
72 - num_tokens_post_pad_triton,
284 + actual_num_tokens.item(),
285 + m * topk,
286 )
287
75 - assert torch.allclose(expert_ids_cuda, expert_ids_triton), (
76 - f"Expert IDs mismatch for block_size={block_size}, "
77 - f"num_tokens={num_tokens}, topk={topk}\n"
78 - f"CUDA expert_ids: {expert_ids_cuda}\n"
79 - f"Triton expert_ids: {expert_ids_triton}")
288
81 - assert torch.allclose(
82 - num_tokens_post_pad_cuda, num_tokens_post_pad_triton), (
83 - f"Num tokens post pad mismatch for block_size={block_size}, "
84 - f"num_tokens={num_tokens}, topk={topk}\n"
85 - f"CUDA num_tokens_post_pad: {num_tokens_post_pad_cuda}\n"
86 - f"Triton num_tokens_post_pad: {num_tokens_post_pad_triton}")
289 +def test_moe_align_block_size_deterministic():
290 + m, topk, num_experts, block_size = 128, 2, 32, 64
291 +
292 + torch.manual_seed(42)
293 + topk_ids = torch.randint(0,
294 + num_experts, (m, topk),
295 + device="cuda",
296 + dtype=torch.int32)
297
298 + # expect the results to be reproducible
299 + results = []
300 + for _ in range(5):
301 + sorted_ids, expert_ids, num_tokens = moe_align_block_size(
302 + topk_ids=topk_ids, block_size=block_size, num_experts=num_experts)
303 + results.append(
304 + (sorted_ids.clone(), expert_ids.clone(), num_tokens.clone()))
305
89 -if __name__ == "__main__":
90 - pytest.main([__file__])
306 + for i in range(1, len(results)):
307 + assert torch.equal(
308 + results[0][0],
309 + results[i][0]), ("sorted_ids should be deterministic")
310 + assert torch.equal(
311 + results[0][1],
312 + results[i][1]), ("expert_ids should be deterministic")
313 + assert torch.equal(
314 + results[0][2],
315 + results[i][2]), ("num_tokens should be deterministic")