| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Kernel tests for ROCm AITER fused MoE. |
| 4 | |
| 5 | This file owns the ROCm-specific fused-MoE custom-op path: |
| 6 | - custom-op registration and fake-tensor support |
| 7 | - enablement gating for ``VLLM_ROCM_USE_AITER_MOE`` and shared experts |
| 8 | - BF16 accuracy for the fused MoE kernel on representative shapes |
| 9 | - gfx950-only AITER MXFP4 W4A16 MoE support, accuracy, and determinism |
| 10 | - MoE-facing FP8 group-quant activation quality |
| 11 | - deterministic routing and representative gfx942 / gfx950 coverage |
| 12 | |
| 13 | Generic fused-MoE backend selection and non-ROCm kernel coverage live in the |
| 14 | generic MoE test files under ``tests/kernels/moe``. |
| 15 | |
| 16 | Raw ROCm AITER helper-op coverage such as ``group_fp8_quant`` and |
| 17 | ``per_tensor_quant`` lives in ``tests/kernels/core/test_rocm_aiter_ops.py``. |
| 18 | This file only keeps the MoE-shaped integration angle for those helpers. |
| 19 | """ |
| 20 | |
| 21 | import importlib |
| 22 | import math |
| 23 | import warnings |
| 24 | from typing import Any, NamedTuple |
| 25 | from unittest import mock |
| 26 | |
| 27 | import pytest |
| 28 | import torch |
| 29 | import torch.nn.functional as F |
| 30 | |
| 31 | from tests.kernels.utils import _assert_deterministic |
| 32 | from vllm.platforms import current_platform |
| 33 | from vllm.platforms.rocm import on_cdna, on_gfx942, on_gfx950 |
| 34 | from vllm.utils.torch_utils import set_random_seed |
| 35 | |
| 36 | pytestmark = pytest.mark.skipif( |
| 37 | not current_platform.is_rocm(), reason="ROCm-specific tests" |
| 38 | ) |
| 39 | |
| 40 | |
| 41 | # Helpers ----------------------------------------------------------------- |
| 42 | |
| 43 | |
| 44 | def _reload_envs(): |
| 45 | import vllm.envs as envs |
| 46 | |
| 47 | return importlib.reload(envs) |
| 48 | |
| 49 | |
| 50 | @pytest.fixture(autouse=True) |
| 51 | def _restore_rocm_env_state(): |
| 52 | """Restore global env + AITER flag state after every test.""" |
| 53 | yield |
| 54 | _reload_envs() |
| 55 | from vllm._aiter_ops import rocm_aiter_ops |
| 56 | |
| 57 | rocm_aiter_ops.refresh_env_variables() |
| 58 | |
| 59 | |
| 60 | def _assert_aiter_supported() -> None: |
| 61 | from vllm._aiter_ops import is_aiter_found_and_supported |
| 62 | |
| 63 | assert is_aiter_found_and_supported(), ( |
| 64 | "aiter is required on supported ROCm hardware for this test" |
| 65 | ) |
| 66 | |
| 67 | |
| 68 | def _format_observed_rate(count: int, total: int) -> str: |
| 69 | return f"{count / total:.4%} ({count}/{total})" |
| 70 | |
| 71 | |
| 72 | def _format_allowed_rate(rate: float, total: int) -> str: |
| 73 | allowed_count = int(rate * total) |
| 74 | return f"{rate:.4%} (<= {allowed_count}/{total})" |
| 75 | |
| 76 | |
| 77 | def _quantile(values: torch.Tensor, q: float) -> float: |
| 78 | if values.numel() == 0: |
| 79 | return 0.0 |
| 80 | return torch.quantile(values, q).item() |
| 81 | |
| 82 | |
| 83 | def _assert_close_budget( |
| 84 | actual: torch.Tensor, |
| 85 | expected: torch.Tensor, |
| 86 | *, |
| 87 | label: str, |
| 88 | atol: float, |
| 89 | rtol: float = 0.0, |
| 90 | pass_rate: float = 0.99999, |
| 91 | max_violation_factor: float = 3.0, |
| 92 | ) -> None: |
| 93 | actual_f = actual.detach().float().flatten() |
| 94 | expected_f = expected.detach().float().flatten() |
| 95 | abs_diff = (actual_f - expected_f).abs() |
| 96 | allowed = atol + rtol * expected_f.abs() |
| 97 | |
| 98 | total = abs_diff.numel() |
| 99 | within = abs_diff <= allowed |
| 100 | passed = int(within.sum().item()) |
| 101 | failed = total - passed |
| 102 | allowed_fail_rate = 1.0 - pass_rate |
| 103 | |
| 104 | max_abs = abs_diff.max().item() |
| 105 | mean_abs = abs_diff.mean().item() |
| 106 | p99_abs = _quantile(abs_diff, 0.99) |
| 107 | p999_abs = _quantile(abs_diff, 0.999) |
| 108 | worst_ratio = (abs_diff / allowed.clamp_min(1e-12)).max().item() |
| 109 | max_atol = max_violation_factor * atol |
| 110 | above_max_count = int((abs_diff > max_atol).sum().item()) |
| 111 | |
| 112 | msg = ( |
| 113 | "[rocm_aiter_moe] " |
| 114 | f"{label}: " |
| 115 | f"pass={passed / total:.4%} ({passed}/{total}) " |
| 116 | f"fail={_format_observed_rate(failed, total)} " |
| 117 | f"allowed_fail={_format_allowed_rate(allowed_fail_rate, total)} " |
| 118 | f"atol={atol:g} " |
| 119 | f"rtol={rtol:g} " |
| 120 | f"abs>{max_atol:g}={_format_observed_rate(above_max_count, total)} " |
| 121 | f"allowed_above_max={_format_allowed_rate(0.0, total)} " |
| 122 | f"max_abs={max_abs:.6g} " |
| 123 | f"mean_abs={mean_abs:.6g} " |
| 124 | f"p99_abs={p99_abs:.6g} " |
| 125 | f"p999_abs={p999_abs:.6g} " |
| 126 | f"worst_ratio={worst_ratio:.6g}" |
| 127 | ) |
| 128 | print(msg) |
| 129 | if failed > 0: |
| 130 | warnings.warn(msg, stacklevel=2) |
| 131 | |
| 132 | assert passed / total >= pass_rate, msg |
| 133 | assert max_abs <= max_atol, msg |
| 134 | assert mean_abs <= atol * 0.25, msg |
| 135 | |
| 136 | |
| 137 | def _assert_group_quant_quality( |
| 138 | actual: torch.Tensor, |
| 139 | expected: torch.Tensor, |
| 140 | *, |
| 141 | label: str, |
| 142 | preferred_rel: float = 0.06, |
| 143 | pass_rate: float = 0.9999, |
| 144 | max_rel: float = 0.55, |
| 145 | max_fail_rate: float = 0.000005, |
| 146 | mean_limit: float = 0.03, |
| 147 | ) -> None: |
| 148 | rel = ( |
| 149 | (actual.float() - expected.float()).abs() |
| 150 | / expected.float().abs().clamp_min(1e-5) |
| 151 | ).flatten() |
| 152 | total = rel.numel() |
| 153 | within_preferred_count = int((rel <= preferred_rel).sum().item()) |
| 154 | fail_count = total - within_preferred_count |
| 155 | allowed_fail_rate = 1.0 - pass_rate |
| 156 | above_max_count = int((rel > max_rel).sum().item()) |
| 157 | mean_rel = rel.mean().item() |
| 158 | max_rel_err = rel.max().item() |
| 159 | p99 = _quantile(rel, 0.99) |
| 160 | p999 = _quantile(rel, 0.999) |
| 161 | |
| 162 | msg = ( |
| 163 | "[rocm_aiter_moe] " |
| 164 | f"{label}: " |
| 165 | f"rel<={preferred_rel:g} pass={within_preferred_count / total:.4%} " |
| 166 | f"({within_preferred_count}/{total}) " |
| 167 | f"fail={_format_observed_rate(fail_count, total)} " |
| 168 | f"allowed_fail={_format_allowed_rate(allowed_fail_rate, total)} " |
| 169 | f"rel>{max_rel:g}={_format_observed_rate(above_max_count, total)} " |
| 170 | f"allowed_above_max={_format_allowed_rate(max_fail_rate, total)} " |
| 171 | f"mean_rel={mean_rel:.6g} " |
| 172 | f"max_rel={max_rel_err:.6g} " |
| 173 | f"p99={p99:.6g} " |
| 174 | f"p999={p999:.6g}" |
| 175 | ) |
| 176 | print(msg) |
| 177 | if fail_count > 0: |
| 178 | warnings.warn(msg, stacklevel=2) |
| 179 | |
| 180 | assert within_preferred_count / total >= pass_rate, msg |
| 181 | assert above_max_count / total <= max_fail_rate, msg |
| 182 | assert mean_rel < mean_limit, msg |
| 183 | |
| 184 | |
| 185 | def ref_moe_forward( |
| 186 | hidden_states: torch.Tensor, |
| 187 | w1: torch.Tensor, |
| 188 | w2: torch.Tensor, |
| 189 | topk_weights: torch.Tensor, |
| 190 | topk_ids: torch.Tensor, |
| 191 | activation: str = "silu", |
| 192 | ) -> torch.Tensor: |
| 193 | """Float32 mask-based MoE reference for the ROCm fused-MoE kernel.""" |
| 194 | num_tokens, hidden_dim = hidden_states.shape |
| 195 | num_experts = w1.shape[0] |
| 196 | intermediate_dim = w1.shape[1] // 2 |
| 197 | topk = topk_ids.shape[1] |
| 198 | device = hidden_states.device |
| 199 | |
| 200 | hidden_states_f = hidden_states.float() |
| 201 | w1_f = w1.float() |
| 202 | w2_f = w2.float() |
| 203 | |
| 204 | expanded = hidden_states_f.view(num_tokens, 1, hidden_dim).expand( |
| 205 | num_tokens, topk, hidden_dim |
| 206 | ) |
| 207 | expanded = expanded.reshape(num_tokens * topk, hidden_dim) |
| 208 | output = torch.zeros( |
| 209 | num_tokens * topk, hidden_dim, dtype=torch.float32, device=device |
| 210 | ) |
| 211 | flat_topk_ids = topk_ids.view(-1).long() |
| 212 | |
| 213 | for expert_idx in range(num_experts): |
| 214 | expert_mask = flat_topk_ids == expert_idx |
| 215 | if expert_mask.sum() == 0: |
| 216 | continue |
| 217 | gate_up = expanded[expert_mask] @ w1_f[expert_idx].T |
| 218 | gate = gate_up[:, :intermediate_dim] |
| 219 | up = gate_up[:, intermediate_dim:] |
| 220 | if activation == "silu": |
| 221 | act = F.silu(gate) * up |
| 222 | elif activation == "gelu": |
| 223 | act = F.gelu(gate) * up |
| 224 | else: |
| 225 | raise ValueError(f"Unknown activation: {activation}") |
| 226 | output[expert_mask] = act @ w2_f[expert_idx].T |
| 227 | |
| 228 | output = output.view(num_tokens, topk, hidden_dim) |
| 229 | weights = topk_weights.float().view(num_tokens, topk, 1) |
| 230 | return (output * weights).sum(dim=1) |
| 231 | |
| 232 | |
| 233 | class AiterMxfp4MoeCase(NamedTuple): |
| 234 | hidden_states: torch.Tensor |
| 235 | w1_kernel: torch.Tensor |
| 236 | w2_kernel: torch.Tensor |
| 237 | w1_ref: torch.Tensor |
| 238 | w2_ref: torch.Tensor |
| 239 | topk_weights: torch.Tensor |
| 240 | topk_ids: torch.Tensor |
| 241 | moe_config: Any |
| 242 | quant_config: Any |
| 243 | |
| 244 | |
| 245 | def _make_topk_ids( |
| 246 | num_tokens: int, |
| 247 | num_experts: int, |
| 248 | topk: int, |
| 249 | *, |
| 250 | device: str = "cuda", |
| 251 | ) -> torch.Tensor: |
| 252 | """Generate distinct expert IDs per token with the same top-k shape as |
| 253 | production routing.""" |
| 254 | router_logits = torch.randn(num_tokens, num_experts, device=device) |
| 255 | _, topk_ids = torch.topk(torch.softmax(router_logits, dim=-1), k=topk, dim=-1) |
| 256 | return topk_ids.to(torch.int32) |
| 257 | |
| 258 | |
| 259 | def _shuffle_moe_weights( |
| 260 | w1: torch.Tensor, |
| 261 | w2: torch.Tensor, |
| 262 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 263 | """Shuffle MoE weights into the AITER CK layout used in production.""" |
| 264 | from vllm._aiter_ops import rocm_aiter_ops |
| 265 | |
| 266 | w1_shuffled, w2_shuffled = rocm_aiter_ops.shuffle_weights(w1, w2) |
| 267 | w1_shuffled.is_shuffled = True |
| 268 | w2_shuffled.is_shuffled = True |
| 269 | return w1_shuffled, w2_shuffled |
| 270 | |
| 271 | |
| 272 | def _make_moe_case( |
| 273 | *, |
| 274 | num_tokens: int, |
| 275 | hidden_dim: int, |
| 276 | intermediate_dim: int, |
| 277 | num_experts: int, |
| 278 | topk: int, |
| 279 | seed: int, |
| 280 | ) -> dict[str, torch.Tensor]: |
| 281 | torch.set_default_device("cuda") |
| 282 | set_random_seed(seed) |
| 283 | |
| 284 | hidden_states = torch.randn(num_tokens, hidden_dim, dtype=torch.bfloat16) |
| 285 | w1 = torch.randn( |
| 286 | num_experts, |
| 287 | intermediate_dim * 2, |
| 288 | hidden_dim, |
| 289 | dtype=torch.bfloat16, |
| 290 | ) / math.sqrt(hidden_dim) |
| 291 | w2 = torch.randn( |
| 292 | num_experts, |
| 293 | hidden_dim, |
| 294 | intermediate_dim, |
| 295 | dtype=torch.bfloat16, |
| 296 | ) / math.sqrt(intermediate_dim) |
| 297 | topk_weights = torch.rand(num_tokens, topk, dtype=torch.float32) |
| 298 | topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True) |
| 299 | topk_ids = _make_topk_ids(num_tokens, num_experts, topk) |
| 300 | return { |
| 301 | "hidden_states": hidden_states, |
| 302 | "w1": w1, |
| 303 | "w2": w2, |
| 304 | "topk_weights": topk_weights, |
| 305 | "topk_ids": topk_ids, |
| 306 | } |
| 307 | |
| 308 | |
| 309 | def _interleave_gate_up_rows(t: torch.Tensor) -> torch.Tensor: |
| 310 | """Reorder contiguous ``[gate; up]`` rows into gpt-oss interleaved order.""" |
| 311 | e, two_i = t.shape[0], t.shape[1] |
| 312 | i, rest = two_i // 2, t.shape[2:] |
| 313 | perm = (0, 2, 1, *range(3, 3 + len(rest))) |
| 314 | return t.view(e, 2, i, *rest).permute(*perm).contiguous().view(e, two_i, *rest) |
| 315 | |
| 316 | |
| 317 | def _make_aiter_mxfp4_moe_case( |
| 318 | *, |
| 319 | num_tokens: int, |
| 320 | hidden_dim: int, |
| 321 | intermediate_dim: int, |
| 322 | num_experts: int, |
| 323 | topk: int, |
| 324 | seed: int, |
| 325 | ) -> AiterMxfp4MoeCase: |
| 326 | from triton_kernels.numerics_details.mxfp import ( |
| 327 | downcast_to_mxfp, |
| 328 | upcast_from_mxfp, |
| 329 | ) |
| 330 | |
| 331 | from tests.kernels.moe.utils import make_dummy_moe_config |
| 332 | from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import ( |
| 333 | Mxfp4MoeBackend, |
| 334 | convert_gpt_oss_weight_to_mxfp4_moe_kernel_format, |
| 335 | make_mxfp4_moe_quant_config, |
| 336 | ) |
| 337 | |
| 338 | torch.set_default_device("cuda") |
| 339 | set_random_seed(seed) |
| 340 | |
| 341 | hidden_states = torch.randn(num_tokens, hidden_dim, dtype=torch.bfloat16) |
| 342 | w1 = torch.randn( |
| 343 | num_experts, |
| 344 | intermediate_dim * 2, |
| 345 | hidden_dim, |
| 346 | dtype=torch.bfloat16, |
| 347 | ) / math.sqrt(hidden_dim) |
| 348 | w2 = torch.randn( |
| 349 | num_experts, |
| 350 | hidden_dim, |
| 351 | intermediate_dim, |
| 352 | dtype=torch.bfloat16, |
| 353 | ) / math.sqrt(intermediate_dim) |
| 354 | topk_weights = torch.rand(num_tokens, topk, dtype=torch.float32) |
| 355 | topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True) |
| 356 | topk_ids = _make_topk_ids(num_tokens, num_experts, topk) |
| 357 | |
| 358 | w1_q, w1_scale = downcast_to_mxfp(w1, torch.uint8, axis=-1) |
| 359 | w2_q, w2_scale = downcast_to_mxfp(w2, torch.uint8, axis=-1) |
| 360 | |
| 361 | w1_ref = upcast_from_mxfp(w1_q, w1_scale, torch.bfloat16, axis=-1) |
| 362 | w2_ref = upcast_from_mxfp(w2_q, w2_scale, torch.bfloat16, axis=-1) |
| 363 | |
| 364 | ( |
| 365 | w1_kernel, |
| 366 | w2_kernel, |
| 367 | w1_scale_kernel, |
| 368 | w2_scale_kernel, |
| 369 | _, |
| 370 | _, |
| 371 | ) = convert_gpt_oss_weight_to_mxfp4_moe_kernel_format( |
| 372 | mxfp4_backend=Mxfp4MoeBackend.AITER, |
| 373 | layer=torch.nn.Module(), |
| 374 | # w13 rows must be gpt-oss-interleaved; the converter de-interleaves them |
| 375 | # back to the contiguous gate/up blocks that ``w1_ref`` / ``ref_moe_forward`` |
| 376 | # use. w2 has no gate/up split, so it stays as-is. |
| 377 | w13_weight=_interleave_gate_up_rows(w1_q), |
| 378 | w2_weight=w2_q.clone(), |
| 379 | w13_weight_scale=_interleave_gate_up_rows(w1_scale), |
| 380 | w2_weight_scale=w2_scale.clone(), |
| 381 | ) |
| 382 | |
| 383 | moe_config = make_dummy_moe_config( |
| 384 | num_experts=num_experts, |
| 385 | experts_per_token=topk, |
| 386 | hidden_dim=hidden_dim, |
| 387 | intermediate_size=intermediate_dim, |
| 388 | in_dtype=torch.bfloat16, |
| 389 | ) |
| 390 | quant_config = make_mxfp4_moe_quant_config( |
| 391 | Mxfp4MoeBackend.AITER, |
| 392 | w1_scale=w1_scale_kernel, |
| 393 | w2_scale=w2_scale_kernel, |
| 394 | ) |
| 395 | |
| 396 | return AiterMxfp4MoeCase( |
| 397 | hidden_states=hidden_states, |
| 398 | w1_kernel=w1_kernel, |
| 399 | w2_kernel=w2_kernel, |
| 400 | w1_ref=w1_ref, |
| 401 | w2_ref=w2_ref, |
| 402 | topk_weights=topk_weights, |
| 403 | topk_ids=topk_ids, |
| 404 | moe_config=moe_config, |
| 405 | quant_config=quant_config, |
| 406 | ) |
| 407 | |
| 408 | |
| 409 | def _run_fused_moe( |
| 410 | hidden_states: torch.Tensor, |
| 411 | w1: torch.Tensor, |
| 412 | w2: torch.Tensor, |
| 413 | topk_weights: torch.Tensor, |
| 414 | topk_ids: torch.Tensor, |
| 415 | *, |
| 416 | activation_method: int, |
| 417 | quant_method: int, |
| 418 | ) -> torch.Tensor: |
| 419 | w1_shuffled, w2_shuffled = _shuffle_moe_weights(w1, w2) |
| 420 | return torch.ops.vllm.rocm_aiter_fused_moe( |
| 421 | hidden_states, |
| 422 | w1_shuffled, |
| 423 | w2_shuffled, |
| 424 | topk_weights, |
| 425 | topk_ids, |
| 426 | expert_mask=None, |
| 427 | activation_method=activation_method, |
| 428 | quant_method=quant_method, |
| 429 | doweight_stage1=False, |
| 430 | ) |
| 431 | |
| 432 | |
| 433 | # Custom op tests --------------------------------------------------------- |
| 434 | |
| 435 | |
| 436 | def test_aiter_fused_moe_custom_op_registered(): |
| 437 | """The main fused-MoE custom op should stay registered for runtime use.""" |
| 438 | _assert_aiter_supported() |
| 439 | import vllm._aiter_ops as aiter_ops # noqa: F401 |
| 440 | |
| 441 | assert hasattr(torch.ops.vllm, "rocm_aiter_fused_moe") |
| 442 | assert callable(torch.ops.vllm.rocm_aiter_fused_moe) |
| 443 | |
| 444 | |
| 445 | def test_aiter_asm_moe_tkw1_custom_op_registered(): |
| 446 | """The tkw1 custom op should stay registered for FP8 apply-router-weight |
| 447 | paths.""" |
| 448 | _assert_aiter_supported() |
| 449 | import vllm._aiter_ops as aiter_ops # noqa: F401 |
| 450 | |
| 451 | assert hasattr(torch.ops.vllm, "rocm_aiter_asm_moe_tkw1") |
| 452 | assert callable(torch.ops.vllm.rocm_aiter_asm_moe_tkw1) |
| 453 | |
| 454 | |
| 455 | def test_aiter_fused_moe_fake_tensor_support(): |
| 456 | """The fused-MoE op should preserve fake-tensor compatibility for |
| 457 | torch.compile-style tracing.""" |
| 458 | _assert_aiter_supported() |
| 459 | import vllm._aiter_ops # noqa: F401 |
| 460 | |
| 461 | num_tokens = 16 |
| 462 | hidden_dim = 1024 |
| 463 | intermediate_dim = 2048 |
| 464 | num_experts = 8 |
| 465 | topk = 2 |
| 466 | |
| 467 | hidden_states = torch.randn( |
| 468 | num_tokens, hidden_dim, dtype=torch.bfloat16, device="cuda" |
| 469 | ) |
| 470 | w1 = torch.randn( |
| 471 | num_experts, |
| 472 | intermediate_dim * 2, |
| 473 | hidden_dim, |
| 474 | dtype=torch.bfloat16, |
| 475 | device="cuda", |
| 476 | ) |
| 477 | w2 = torch.randn( |
| 478 | num_experts, |
| 479 | hidden_dim, |
| 480 | intermediate_dim, |
| 481 | dtype=torch.bfloat16, |
| 482 | device="cuda", |
| 483 | ) |
| 484 | topk_weights = torch.rand(num_tokens, topk, dtype=torch.float32, device="cuda") |
| 485 | topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True) |
| 486 | topk_ids = _make_topk_ids(num_tokens, num_experts, topk, device="cuda") |
| 487 | |
| 488 | torch.library.opcheck( |
| 489 | torch.ops.vllm.rocm_aiter_fused_moe, |
| 490 | (hidden_states, w1, w2, topk_weights, topk_ids), |
| 491 | kwargs={ |
| 492 | "expert_mask": None, |
| 493 | "activation_method": 0, |
| 494 | "quant_method": 0, |
| 495 | "doweight_stage1": False, |
| 496 | }, |
| 497 | test_utils=("test_faketensor",), |
| 498 | ) |
| 499 | |
| 500 | |
| 501 | # Env gating tests -------------------------------------------------------- |
| 502 | |
| 503 | |
| 504 | @pytest.mark.parametrize( |
| 505 | ("use_aiter", "use_moe", "expected"), |
| 506 | [ |
| 507 | (True, True, True), |
| 508 | (True, False, False), |
| 509 | (False, True, False), |
| 510 | (False, False, False), |
| 511 | ], |
| 512 | ) |
| 513 | def test_aiter_moe_enablement_follows_env( |
| 514 | use_aiter: bool, |
| 515 | use_moe: bool, |
| 516 | expected: bool, |
| 517 | monkeypatch: pytest.MonkeyPatch, |
| 518 | ): |
| 519 | """The fused-MoE gate should depend only on the main AITER toggle and the |
| 520 | MoE-specific toggle.""" |
| 521 | from vllm._aiter_ops import rocm_aiter_ops |
| 522 | |
| 523 | _assert_aiter_supported() |
| 524 | |
| 525 | with monkeypatch.context() as mp: |
| 526 | mp.setenv("VLLM_ROCM_USE_AITER", "1" if use_aiter else "0") |
| 527 | mp.setenv("VLLM_ROCM_USE_AITER_MOE", "1" if use_moe else "0") |
| 528 | _reload_envs() |
| 529 | rocm_aiter_ops.refresh_env_variables() |
| 530 | |
| 531 | assert rocm_aiter_ops.is_fused_moe_enabled() is expected |
| 532 | |
| 533 | |
| 534 | @pytest.mark.parametrize( |
| 535 | ("use_aiter", "use_moe", "use_shared", "expected"), |
| 536 | [ |
| 537 | (True, True, True, True), |
| 538 | (True, True, False, False), |
| 539 | (True, False, True, False), |
| 540 | (False, True, True, False), |
| 541 | ], |
| 542 | ) |
| 543 | def test_aiter_moe_shared_experts_enablement_follows_env( |
| 544 | use_aiter: bool, |
| 545 | use_moe: bool, |
| 546 | use_shared: bool, |
| 547 | expected: bool, |
| 548 | monkeypatch: pytest.MonkeyPatch, |
| 549 | ): |
| 550 | """Shared-expert fusion should only be enabled when the fused-MoE path is |
| 551 | enabled too.""" |
| 552 | from vllm._aiter_ops import rocm_aiter_ops |
| 553 | |
| 554 | _assert_aiter_supported() |
| 555 | |
| 556 | with monkeypatch.context() as mp: |
| 557 | mp.setenv("VLLM_ROCM_USE_AITER", "1" if use_aiter else "0") |
| 558 | mp.setenv("VLLM_ROCM_USE_AITER_MOE", "1" if use_moe else "0") |
| 559 | mp.setenv( |
| 560 | "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS", |
| 561 | "1" if use_shared else "0", |
| 562 | ) |
| 563 | _reload_envs() |
| 564 | rocm_aiter_ops.refresh_env_variables() |
| 565 | |
| 566 | assert rocm_aiter_ops.is_fusion_moe_shared_experts_enabled() is expected |
| 567 | |
| 568 | |
| 569 | @pytest.mark.parametrize( |
| 570 | "value,expected_act,expected_env", |
| 571 | [ |
| 572 | # Unset / auto / legacy "1" all mean the a4w4 default. |
| 573 | (None, "a4w4", {"AITER_SITUV2_A4W4": "1"}), |
| 574 | ("auto", "a4w4", {"AITER_SITUV2_A4W4": "1"}), |
| 575 | ("1", "a4w4", {"AITER_SITUV2_A4W4": "1"}), |
| 576 | ("a4w4", "a4w4", {"AITER_SITUV2_A4W4": "1"}), |
| 577 | ("0", "a16w4", {}), |
| 578 | ("a16w4", "a16w4", {}), |
| 579 | ("A8W4", "a8w4", {"AITER_SITUV2_A8W4": "1"}), |
| 580 | ], |
| 581 | ) |
| 582 | def test_aiter_moe_situv2_activation_syncs_aiter_env( |
| 583 | value: str | None, |
| 584 | expected_act: str, |
| 585 | expected_env: dict[str, str], |
| 586 | monkeypatch: pytest.MonkeyPatch, |
| 587 | ): |
| 588 | """Each SiTUv2 activation choice sets exactly its AITER dispatch env.""" |
| 589 | import os |
| 590 | |
| 591 | from vllm._aiter_ops import rocm_aiter_ops |
| 592 | |
| 593 | _assert_aiter_supported() |
| 594 | |
| 595 | with monkeypatch.context() as mp: |
| 596 | # Stale AITER overrides must be replaced by the vLLM selection. |
| 597 | mp.setenv("AITER_SITUV2_A8W4", "1") |
| 598 | mp.setenv("AITER_SITUV2_A4W4", "1") |
| 599 | mp.setenv("VLLM_ROCM_USE_AITER", "1") |
| 600 | mp.setenv("VLLM_ROCM_USE_AITER_MOE", "1") |
| 601 | if value is None: |
| 602 | mp.delenv("VLLM_ROCM_USE_AITER_MOE_SITUV2", raising=False) |
| 603 | else: |
| 604 | mp.setenv("VLLM_ROCM_USE_AITER_MOE_SITUV2", value) |
| 605 | _reload_envs() |
| 606 | rocm_aiter_ops.refresh_env_variables() |
| 607 | |
| 608 | assert rocm_aiter_ops.get_fused_moe_situv2_activation() == expected_act |
| 609 | assert rocm_aiter_ops.is_fused_moe_situv2_gate_up_interleaved() is ( |
| 610 | expected_act == "a8w4" |
| 611 | ) |
| 612 | assert rocm_aiter_ops.is_fused_moe_situv2_enabled() is (expected_act != "a16w4") |
| 613 | for name in ("AITER_SITUV2_A8W4", "AITER_SITUV2_A4W4"): |
| 614 | assert os.environ.get(name) == expected_env.get(name) |
| 615 | |
| 616 | |
| 617 | def test_aiter_moe_situv2_rejects_unknown_activation( |
| 618 | monkeypatch: pytest.MonkeyPatch, |
| 619 | ): |
| 620 | with monkeypatch.context() as mp: |
| 621 | mp.setenv("VLLM_ROCM_USE_AITER_MOE_SITUV2", "a8w8") |
| 622 | _reload_envs() |
| 623 | import vllm.envs as envs |
| 624 | |
| 625 | with pytest.raises(ValueError, match="VLLM_ROCM_USE_AITER_MOE_SITUV2"): |
| 626 | _ = envs.VLLM_ROCM_USE_AITER_MOE_SITUV2 |
| 627 | |
| 628 | |
| 629 | @pytest.mark.parametrize("moe_padding", [True, False]) |
| 630 | def test_aiter_moe_padding_env_var( |
| 631 | moe_padding: bool, |
| 632 | monkeypatch: pytest.MonkeyPatch, |
| 633 | ): |
| 634 | """The ROCm MoE padding env var should keep its exact parse contract.""" |
| 635 | with monkeypatch.context() as mp: |
| 636 | mp.setenv("VLLM_ROCM_MOE_PADDING", "1" if moe_padding else "0") |
| 637 | envs = _reload_envs() |
| 638 | assert envs.VLLM_ROCM_MOE_PADDING is moe_padding |
| 639 | |
| 640 | |
| 641 | # a4w4 (FP4 activation) opt-in gating test --------------------------------- |
| 642 | |
| 643 | |
| 644 | @pytest.mark.parametrize( |
| 645 | ("env_value", "model_type", "expected"), |
| 646 | [ |
| 647 | # Unset/"0" is opt-in-only: a4w4 never turns on by itself, even on |
| 648 | # a validated model type. |
| 649 | (None, "deepseek_v41", False), |
| 650 | ("0", "deepseek_v41", False), |
| 651 | ("1", "deepseek_v41", True), |
| 652 | ("1", "deepseek_v41_text", True), |
| 653 | ], |
| 654 | ) |
| 655 | def test_aiter_moe_a4w4_dsv4_is_opt_in( |
| 656 | env_value: str | None, |
| 657 | model_type: str, |
| 658 | expected: bool, |
| 659 | monkeypatch: pytest.MonkeyPatch, |
| 660 | ): |
| 661 | """vllm-project/vllm#58819's a4w4 MoE activation path must stay a plain |
| 662 | explicit opt-in (VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4=1), with no |
| 663 | default-on behavior, but is still only permitted on the model types |
| 664 | this flag has been validated for.""" |
| 665 | import types |
| 666 | |
| 667 | from tests.kernels.moe.utils import make_dummy_moe_config |
| 668 | from vllm._aiter_ops import rocm_aiter_ops |
| 669 | |
| 670 | _assert_aiter_supported() |
| 671 | |
| 672 | fake_vllm_config = types.SimpleNamespace( |
| 673 | model_config=types.SimpleNamespace( |
| 674 | hf_config=types.SimpleNamespace(model_type=model_type) |
| 675 | ) |
| 676 | ) |
| 677 | |
| 678 | with monkeypatch.context() as mp: |
| 679 | mp.setenv("VLLM_ROCM_USE_AITER", "1") |
| 680 | mp.setenv("VLLM_ROCM_USE_AITER_MOE", "1") |
| 681 | if env_value is None: |
| 682 | mp.delenv("VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4", raising=False) |
| 683 | else: |
| 684 | mp.setenv("VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4", env_value) |
| 685 | _reload_envs() |
| 686 | rocm_aiter_ops.refresh_env_variables() |
| 687 | |
| 688 | with mock.patch( |
| 689 | "vllm.config.get_current_vllm_config_or_none", |
| 690 | return_value=fake_vllm_config, |
| 691 | ): |
| 692 | moe_config = make_dummy_moe_config( |
| 693 | num_experts=4, |
| 694 | experts_per_token=2, |
| 695 | hidden_dim=256, |
| 696 | intermediate_size=512, |
| 697 | in_dtype=torch.bfloat16, |
| 698 | ) |
| 699 | |
| 700 | assert moe_config.use_mxfp4_w4a4_dsv4 is expected |
| 701 | |
| 702 | |
| 703 | @pytest.mark.parametrize("model_type", ["deepseek_v4", "gpt_oss", None]) |
| 704 | def test_aiter_moe_a4w4_dsv4_rejects_unvalidated_model_type( |
| 705 | model_type: str | None, |
| 706 | monkeypatch: pytest.MonkeyPatch, |
| 707 | ): |
| 708 | """Forcing a4w4 on a model type this flag hasn't been GSM8K/AgentX |
| 709 | validated for must raise: it also flips the MXFP4 weight-shuffle layout |
| 710 | (ATOM SEPARATED vs INTERLEAVE), which silently produces garbled output |
| 711 | on the wrong model rather than just being a slower path — the same bug |
| 712 | class vllm-project/vllm#58819's own second commit fixed for DeepSeek |
| 713 | V4.1 itself.""" |
| 714 | import types |
| 715 | |
| 716 | from tests.kernels.moe.utils import make_dummy_moe_config |
| 717 | from vllm._aiter_ops import rocm_aiter_ops |
| 718 | |
| 719 | _assert_aiter_supported() |
| 720 | |
| 721 | fake_vllm_config = types.SimpleNamespace( |
| 722 | model_config=types.SimpleNamespace( |
| 723 | hf_config=types.SimpleNamespace(model_type=model_type) |
| 724 | ) |
| 725 | ) |
| 726 | |
| 727 | with monkeypatch.context() as mp: |
| 728 | mp.setenv("VLLM_ROCM_USE_AITER", "1") |
| 729 | mp.setenv("VLLM_ROCM_USE_AITER_MOE", "1") |
| 730 | mp.setenv("VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4", "1") |
| 731 | _reload_envs() |
| 732 | rocm_aiter_ops.refresh_env_variables() |
| 733 | |
| 734 | with ( |
| 735 | mock.patch( |
| 736 | "vllm.config.get_current_vllm_config_or_none", |
| 737 | return_value=fake_vllm_config, |
| 738 | ), |
| 739 | pytest.raises(ValueError, match="VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4"), |
| 740 | ): |
| 741 | make_dummy_moe_config( |
| 742 | num_experts=4, |
| 743 | experts_per_token=2, |
| 744 | hidden_dim=256, |
| 745 | intermediate_size=512, |
| 746 | in_dtype=torch.bfloat16, |
| 747 | ) |
| 748 | |
| 749 | |
| 750 | # Dispatch-policy forwarding test ------------------------------------------ |
| 751 | |
| 752 | |
| 753 | @pytest.mark.parametrize("dispatch_policy", [0, 1, 2]) |
| 754 | def test_aiter_moe_dispatch_policy_forwarded_to_fused_moe( |
| 755 | dispatch_policy: int, |
| 756 | monkeypatch: pytest.MonkeyPatch, |
| 757 | ): |
| 758 | """VLLM_ROCM_AITER_MOE_DISPATCH_POLICY should reach rocm_aiter_ops.fused_moe |
| 759 | unchanged, the same forwarding AiterExperts.apply does via |
| 760 | rocm_aiter_fused_experts, for every documented policy value (0=auto, |
| 761 | 1=always single-pass, 2=always multi-pass). See vllm-project/vllm#54966. |
| 762 | """ |
| 763 | from tests.kernels.moe.utils import make_dummy_moe_config |
| 764 | from vllm._aiter_ops import rocm_aiter_ops |
| 765 | from vllm.model_executor.layers.fused_moe.activation import MoEActivation |
| 766 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 767 | rocm_aiter_fused_experts, |
| 768 | ) |
| 769 | |
| 770 | _assert_aiter_supported() |
| 771 | case = _make_moe_case( |
| 772 | num_tokens=8, |
| 773 | hidden_dim=256, |
| 774 | intermediate_dim=512, |
| 775 | num_experts=4, |
| 776 | topk=2, |
| 777 | seed=5, |
| 778 | ) |
| 779 | w1_shuffled, w2_shuffled = _shuffle_moe_weights(case["w1"], case["w2"]) |
| 780 | moe_config = make_dummy_moe_config( |
| 781 | num_experts=4, |
| 782 | experts_per_token=2, |
| 783 | hidden_dim=256, |
| 784 | intermediate_size=512, |
| 785 | in_dtype=torch.bfloat16, |
| 786 | ) |
| 787 | |
| 788 | with monkeypatch.context() as mp: |
| 789 | mp.setenv("VLLM_ROCM_AITER_MOE_DISPATCH_POLICY", str(dispatch_policy)) |
| 790 | _reload_envs() |
| 791 | rocm_aiter_ops.refresh_env_variables() |
| 792 | |
| 793 | assert rocm_aiter_ops.get_moe_dispatch_policy() == dispatch_policy, ( |
| 794 | "rocm_aiter_ops cached a stale dispatch policy after the env var changed." |
| 795 | ) |
| 796 | |
| 797 | with mock.patch.object( |
| 798 | rocm_aiter_ops, "fused_moe", wraps=rocm_aiter_ops.fused_moe |
| 799 | ) as fused_moe_mock: |
| 800 | rocm_aiter_fused_experts( |
| 801 | hidden_states=case["hidden_states"], |
| 802 | w1=w1_shuffled, |
| 803 | w2=w2_shuffled, |
| 804 | topk_weights=case["topk_weights"], |
| 805 | topk_ids=case["topk_ids"], |
| 806 | moe_config=moe_config, |
| 807 | activation=MoEActivation.SILU, |
| 808 | moe_sorting_dispatch_policy=rocm_aiter_ops.get_moe_dispatch_policy(), |
| 809 | ) |
| 810 | |
| 811 | assert ( |
| 812 | fused_moe_mock.call_args.kwargs["moe_sorting_dispatch_policy"] |
| 813 | == dispatch_policy |
| 814 | ), ( |
| 815 | "VLLM_ROCM_AITER_MOE_DISPATCH_POLICY was not forwarded to " |
| 816 | "rocm_aiter_ops.fused_moe." |
| 817 | ) |
| 818 | |
| 819 | |
| 820 | # Enum tests -------------------------------------------------------------- |
| 821 | |
| 822 | |
| 823 | def test_quant_method_enum_values(): |
| 824 | """The AITER quant-method bridge enum should keep its wire values.""" |
| 825 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import QuantMethod |
| 826 | |
| 827 | assert QuantMethod.NO == 0 |
| 828 | assert QuantMethod.PER_TENSOR == 1 |
| 829 | assert QuantMethod.PER_TOKEN == 2 |
| 830 | assert QuantMethod.BLOCK_1X32 == 3 |
| 831 | assert QuantMethod.BLOCK_1X128 == 4 |
| 832 | assert QuantMethod.BLOCK_128x128 == 5 |
| 833 | |
| 834 | |
| 835 | def test_activation_method_enum_values(): |
| 836 | """The AITER activation-method bridge enum should keep its wire values.""" |
| 837 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 838 | ActivationMethod, |
| 839 | ) |
| 840 | |
| 841 | assert ActivationMethod.SILU == 0 |
| 842 | assert ActivationMethod.GELU == 1 |
| 843 | |
| 844 | |
| 845 | # MXFP4 kernel tests ------------------------------------------------------ |
| 846 | |
| 847 | |
| 848 | def test_aiter_mxfp4_quant_scheme_support_matches_gfx950(): |
| 849 | """AITER MXFP4 MoE support should stay gfx950-only.""" |
| 850 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 851 | AiterExperts, |
| 852 | ) |
| 853 | from vllm.model_executor.layers.quantization.utils.quant_utils import ( |
| 854 | kMxfp4Static, |
| 855 | ) |
| 856 | |
| 857 | assert AiterExperts._supports_quant_scheme(kMxfp4Static, None) is on_gfx950() |
| 858 | |
| 859 | |
| 860 | @pytest.mark.skipif(not on_gfx950(), reason="gfx950 ROCm only") |
| 861 | def test_aiter_fused_moe_mi350_mxfp4_w4a16_accuracy(): |
| 862 | """The gfx950 AITER MXFP4 W4A16 MoE path should match the dequantized |
| 863 | MXFP4 reference.""" |
| 864 | from vllm.model_executor.layers.fused_moe.activation import MoEActivation |
| 865 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 866 | rocm_aiter_fused_experts, |
| 867 | ) |
| 868 | |
| 869 | _assert_aiter_supported() |
| 870 | case = _make_aiter_mxfp4_moe_case( |
| 871 | num_tokens=32, |
| 872 | hidden_dim=512, |
| 873 | intermediate_dim=1024, |
| 874 | num_experts=4, |
| 875 | topk=2, |
| 876 | seed=11, |
| 877 | ) |
| 878 | ref_out = ref_moe_forward( |
| 879 | case.hidden_states, |
| 880 | case.w1_ref, |
| 881 | case.w2_ref, |
| 882 | case.topk_weights, |
| 883 | case.topk_ids, |
| 884 | ) |
| 885 | out = rocm_aiter_fused_experts( |
| 886 | hidden_states=case.hidden_states, |
| 887 | w1=case.w1_kernel, |
| 888 | w2=case.w2_kernel, |
| 889 | topk_weights=case.topk_weights, |
| 890 | topk_ids=case.topk_ids, |
| 891 | activation=MoEActivation.SWIGLUOAI, |
| 892 | quant_config=case.quant_config, |
| 893 | moe_config=case.moe_config, |
| 894 | expert_mask=None, |
| 895 | ) |
| 896 | |
| 897 | assert out.shape == case.hidden_states.shape |
| 898 | _assert_close_budget( |
| 899 | out.float(), |
| 900 | ref_out.float(), |
| 901 | label="mi350_mxfp4_w4a16_accuracy", |
| 902 | atol=0.1, |
| 903 | rtol=0.1, |
| 904 | pass_rate=0.99, |
| 905 | max_violation_factor=2.0, |
| 906 | ) |
| 907 | |
| 908 | |
| 909 | @pytest.mark.skipif(not on_gfx950(), reason="gfx950 ROCm only") |
| 910 | def test_aiter_fused_moe_mi350_mxfp4_w4a16_determinism(): |
| 911 | """The gfx950 AITER MXFP4 W4A16 MoE path should stay bitwise |
| 912 | deterministic.""" |
| 913 | from vllm.model_executor.layers.fused_moe.activation import MoEActivation |
| 914 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 915 | rocm_aiter_fused_experts, |
| 916 | ) |
| 917 | |
| 918 | _assert_aiter_supported() |
| 919 | case = _make_aiter_mxfp4_moe_case( |
| 920 | num_tokens=8, |
| 921 | hidden_dim=512, |
| 922 | intermediate_dim=1024, |
| 923 | num_experts=4, |
| 924 | topk=2, |
| 925 | seed=13, |
| 926 | ) |
| 927 | |
| 928 | def run_mxfp4_moe(): |
| 929 | return rocm_aiter_fused_experts( |
| 930 | hidden_states=case.hidden_states, |
| 931 | w1=case.w1_kernel, |
| 932 | w2=case.w2_kernel, |
| 933 | topk_weights=case.topk_weights, |
| 934 | topk_ids=case.topk_ids, |
| 935 | activation=MoEActivation.SWIGLUOAI, |
| 936 | quant_config=case.quant_config, |
| 937 | moe_config=case.moe_config, |
| 938 | expert_mask=None, |
| 939 | ) |
| 940 | |
| 941 | _assert_deterministic(run_mxfp4_moe, n_runs=4) |
| 942 | |
| 943 | |
| 944 | # FP8 group-quant tests --------------------------------------------------- |
| 945 | |
| 946 | |
| 947 | @pytest.mark.skipif( |
| 948 | not (on_gfx942() or on_gfx950()), |
| 949 | reason="gfx942/gfx950 ROCm only", |
| 950 | ) |
| 951 | @pytest.mark.parametrize("num_tokens,hidden_dim", [(16, 2048), (64, 4096), (128, 8192)]) |
| 952 | def test_aiter_moe_group_fp8_quant_reconstructs_hidden_states( |
| 953 | num_tokens: int, |
| 954 | hidden_dim: int, |
| 955 | ): |
| 956 | """The MoE-facing FP8 group-quant helper should reconstruct hidden states |
| 957 | within the expected FP8 error budget. |
| 958 | |
| 959 | The raw op's shape and standalone roundtrip contracts are covered in |
| 960 | ``tests/kernels/core/test_rocm_aiter_ops.py``. This test keeps the |
| 961 | representative hidden-state sizes that the fused-MoE path actually cares |
| 962 | about. |
| 963 | """ |
| 964 | from vllm._aiter_ops import rocm_aiter_ops |
| 965 | |
| 966 | _assert_aiter_supported() |
| 967 | torch.set_default_device("cuda") |
| 968 | set_random_seed(1) |
| 969 | |
| 970 | group_size = 128 |
| 971 | hidden_states = torch.randn(num_tokens, hidden_dim, dtype=torch.bfloat16) |
| 972 | x_fp8, scales = rocm_aiter_ops.group_fp8_quant(hidden_states, group_size) |
| 973 | |
| 974 | assert x_fp8.shape == hidden_states.shape |
| 975 | assert scales.shape == (num_tokens, (hidden_dim + group_size - 1) // group_size) |
| 976 | assert scales.dtype == torch.float32 |
| 977 | |
| 978 | scales_expanded = scales.repeat_interleave(group_size, dim=1)[:, :hidden_dim] |
| 979 | dequantized = x_fp8.float() * scales_expanded |
| 980 | |
| 981 | _assert_group_quant_quality( |
| 982 | dequantized, |
| 983 | hidden_states, |
| 984 | label=f"group_fp8_quant shape=({num_tokens}, {hidden_dim})", |
| 985 | ) |
| 986 | |
| 987 | |
| 988 | # Kernel accuracy tests --------------------------------------------------- |
| 989 | |
| 990 | |
| 991 | @pytest.mark.parametrize( |
| 992 | "num_tokens,hidden_dim,intermediate_dim", |
| 993 | [ |
| 994 | (16, 512, 1024), |
| 995 | (128, 2048, 4096), |
| 996 | (2048, 4096, 11008), |
| 997 | ], |
| 998 | ) |
| 999 | def test_aiter_fused_moe_bf16_accuracy( |
| 1000 | num_tokens: int, |
| 1001 | hidden_dim: int, |
| 1002 | intermediate_dim: int, |
| 1003 | ): |
| 1004 | """The ROCm AITER fused-MoE BF16 path should match the float32 reference |
| 1005 | on representative shapes.""" |
| 1006 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 1007 | ActivationMethod, |
| 1008 | QuantMethod, |
| 1009 | ) |
| 1010 | |
| 1011 | _assert_aiter_supported() |
| 1012 | case = _make_moe_case( |
| 1013 | num_tokens=num_tokens, |
| 1014 | hidden_dim=hidden_dim, |
| 1015 | intermediate_dim=intermediate_dim, |
| 1016 | num_experts=8, |
| 1017 | topk=2, |
| 1018 | seed=0, |
| 1019 | ) |
| 1020 | ref_out = ref_moe_forward( |
| 1021 | case["hidden_states"], |
| 1022 | case["w1"], |
| 1023 | case["w2"], |
| 1024 | case["topk_weights"], |
| 1025 | case["topk_ids"], |
| 1026 | activation="silu", |
| 1027 | ) |
| 1028 | out = _run_fused_moe( |
| 1029 | case["hidden_states"], |
| 1030 | case["w1"], |
| 1031 | case["w2"], |
| 1032 | case["topk_weights"], |
| 1033 | case["topk_ids"], |
| 1034 | activation_method=int(ActivationMethod.SILU), |
| 1035 | quant_method=int(QuantMethod.NO), |
| 1036 | ) |
| 1037 | |
| 1038 | assert out.shape == (num_tokens, hidden_dim) |
| 1039 | _assert_close_budget( |
| 1040 | out.float(), |
| 1041 | ref_out, |
| 1042 | label=f"bf16_accuracy shape=({num_tokens}, {hidden_dim}, {intermediate_dim})", |
| 1043 | atol=0.05, |
| 1044 | rtol=0.0, |
| 1045 | ) |
| 1046 | |
| 1047 | |
| 1048 | def test_aiter_fused_moe_gelu_accuracy(): |
| 1049 | """The GELU activation variant should stay aligned with the float32 |
| 1050 | reference.""" |
| 1051 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 1052 | ActivationMethod, |
| 1053 | QuantMethod, |
| 1054 | ) |
| 1055 | |
| 1056 | _assert_aiter_supported() |
| 1057 | case = _make_moe_case( |
| 1058 | num_tokens=32, |
| 1059 | hidden_dim=512, |
| 1060 | intermediate_dim=1024, |
| 1061 | num_experts=4, |
| 1062 | topk=2, |
| 1063 | seed=42, |
| 1064 | ) |
| 1065 | ref_out = ref_moe_forward( |
| 1066 | case["hidden_states"], |
| 1067 | case["w1"], |
| 1068 | case["w2"], |
| 1069 | case["topk_weights"], |
| 1070 | case["topk_ids"], |
| 1071 | activation="gelu", |
| 1072 | ) |
| 1073 | out = _run_fused_moe( |
| 1074 | case["hidden_states"], |
| 1075 | case["w1"], |
| 1076 | case["w2"], |
| 1077 | case["topk_weights"], |
| 1078 | case["topk_ids"], |
| 1079 | activation_method=int(ActivationMethod.GELU), |
| 1080 | quant_method=int(QuantMethod.NO), |
| 1081 | ) |
| 1082 | |
| 1083 | assert out.shape == case["hidden_states"].shape |
| 1084 | _assert_close_budget( |
| 1085 | out.float(), |
| 1086 | ref_out, |
| 1087 | label="gelu_accuracy", |
| 1088 | atol=0.05, |
| 1089 | rtol=0.0, |
| 1090 | ) |
| 1091 | |
| 1092 | |
| 1093 | def test_aiter_fused_moe_determinism(): |
| 1094 | """The BF16 fused-MoE kernel should stay bitwise deterministic for the |
| 1095 | same inputs.""" |
| 1096 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 1097 | ActivationMethod, |
| 1098 | QuantMethod, |
| 1099 | ) |
| 1100 | |
| 1101 | _assert_aiter_supported() |
| 1102 | case = _make_moe_case( |
| 1103 | num_tokens=8, |
| 1104 | hidden_dim=256, |
| 1105 | intermediate_dim=512, |
| 1106 | num_experts=4, |
| 1107 | topk=2, |
| 1108 | seed=2, |
| 1109 | ) |
| 1110 | |
| 1111 | def run_moe(): |
| 1112 | return _run_fused_moe( |
| 1113 | case["hidden_states"], |
| 1114 | case["w1"], |
| 1115 | case["w2"], |
| 1116 | case["topk_weights"], |
| 1117 | case["topk_ids"], |
| 1118 | activation_method=int(ActivationMethod.SILU), |
| 1119 | quant_method=int(QuantMethod.NO), |
| 1120 | ) |
| 1121 | |
| 1122 | _assert_deterministic(run_moe, n_runs=4) |
| 1123 | |
| 1124 | |
| 1125 | # Routed end-to-end tests ------------------------------------------------- |
| 1126 | |
| 1127 | |
| 1128 | @pytest.mark.parametrize( |
| 1129 | "num_tokens,num_experts,topk", |
| 1130 | [ |
| 1131 | (1, 8, 2), |
| 1132 | (16, 8, 2), |
| 1133 | (64, 16, 4), |
| 1134 | ], |
| 1135 | ) |
| 1136 | def test_aiter_fused_moe_end_to_end( |
| 1137 | num_tokens: int, |
| 1138 | num_experts: int, |
| 1139 | topk: int, |
| 1140 | ): |
| 1141 | """The full router-logits to top-k to fused-MoE path should stay accurate.""" |
| 1142 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 1143 | ActivationMethod, |
| 1144 | QuantMethod, |
| 1145 | ) |
| 1146 | |
| 1147 | _assert_aiter_supported() |
| 1148 | torch.set_default_device("cuda") |
| 1149 | set_random_seed(7) |
| 1150 | |
| 1151 | hidden_dim = 512 |
| 1152 | intermediate_dim = 1024 |
| 1153 | hidden_states = torch.randn(num_tokens, hidden_dim, dtype=torch.bfloat16) |
| 1154 | w1 = torch.randn( |
| 1155 | num_experts, |
| 1156 | intermediate_dim * 2, |
| 1157 | hidden_dim, |
| 1158 | dtype=torch.bfloat16, |
| 1159 | ) / math.sqrt(hidden_dim) |
| 1160 | w2 = torch.randn( |
| 1161 | num_experts, |
| 1162 | hidden_dim, |
| 1163 | intermediate_dim, |
| 1164 | dtype=torch.bfloat16, |
| 1165 | ) / math.sqrt(intermediate_dim) |
| 1166 | |
| 1167 | router_logits = torch.randn(num_tokens, num_experts, device="cuda") |
| 1168 | router_probs = torch.softmax(router_logits, dim=-1) |
| 1169 | topk_weights, topk_ids = torch.topk(router_probs, k=topk, dim=-1) |
| 1170 | topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True) |
| 1171 | topk_weights = topk_weights.float() |
| 1172 | topk_ids = topk_ids.to(torch.int32) |
| 1173 | |
| 1174 | ref_out = ref_moe_forward(hidden_states, w1, w2, topk_weights, topk_ids) |
| 1175 | out = _run_fused_moe( |
| 1176 | hidden_states, |
| 1177 | w1, |
| 1178 | w2, |
| 1179 | topk_weights, |
| 1180 | topk_ids, |
| 1181 | activation_method=int(ActivationMethod.SILU), |
| 1182 | quant_method=int(QuantMethod.NO), |
| 1183 | ) |
| 1184 | |
| 1185 | assert out.shape == (num_tokens, hidden_dim) |
| 1186 | assert out.dtype == torch.bfloat16 |
| 1187 | _assert_close_budget( |
| 1188 | out.float(), |
| 1189 | ref_out, |
| 1190 | label=f"end_to_end tokens={num_tokens} experts={num_experts} topk={topk}", |
| 1191 | atol=0.05, |
| 1192 | rtol=0.0, |
| 1193 | ) |
| 1194 | |
| 1195 | |
| 1196 | # Arch-specific tests ----------------------------------------------------- |
| 1197 | |
| 1198 | |
| 1199 | @pytest.mark.skipif( |
| 1200 | not (on_gfx942() or on_gfx950()), |
| 1201 | reason="gfx942/gfx950 ROCm only", |
| 1202 | ) |
| 1203 | def test_aiter_fused_moe_mi3xx_bf16_accuracy(): |
| 1204 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 1205 | ActivationMethod, |
| 1206 | QuantMethod, |
| 1207 | ) |
| 1208 | |
| 1209 | _assert_aiter_supported() |
| 1210 | case = _make_moe_case( |
| 1211 | num_tokens=64, |
| 1212 | hidden_dim=4096, |
| 1213 | intermediate_dim=11008, |
| 1214 | num_experts=8, |
| 1215 | topk=2, |
| 1216 | seed=42, |
| 1217 | ) |
| 1218 | ref_out = ref_moe_forward( |
| 1219 | case["hidden_states"], |
| 1220 | case["w1"], |
| 1221 | case["w2"], |
| 1222 | case["topk_weights"], |
| 1223 | case["topk_ids"], |
| 1224 | ) |
| 1225 | out = _run_fused_moe( |
| 1226 | case["hidden_states"], |
| 1227 | case["w1"], |
| 1228 | case["w2"], |
| 1229 | case["topk_weights"], |
| 1230 | case["topk_ids"], |
| 1231 | activation_method=int(ActivationMethod.SILU), |
| 1232 | quant_method=int(QuantMethod.NO), |
| 1233 | ) |
| 1234 | |
| 1235 | _assert_close_budget( |
| 1236 | out.float(), |
| 1237 | ref_out, |
| 1238 | label=(f"mi3xx_bf16_accuracy arch={'gfx942' if on_gfx942() else 'gfx950'}"), |
| 1239 | atol=0.05, |
| 1240 | rtol=0.0, |
| 1241 | ) |
| 1242 | |
| 1243 | |
| 1244 | @pytest.mark.skipif( |
| 1245 | not (on_gfx942() or on_gfx950()), |
| 1246 | reason="gfx942/gfx950 ROCm only", |
| 1247 | ) |
| 1248 | def test_aiter_fused_moe_mi3xx_fp8_accuracy(): |
| 1249 | """The MI3xx FP8 per-tensor MoE path should stay within the measured FP8 |
| 1250 | error budget.""" |
| 1251 | from tests.kernels.moe.utils import make_test_weights |
| 1252 | from vllm._aiter_ops import rocm_aiter_ops |
| 1253 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 1254 | ActivationMethod, |
| 1255 | QuantMethod, |
| 1256 | ) |
| 1257 | |
| 1258 | _assert_aiter_supported() |
| 1259 | torch.set_default_device("cuda") |
| 1260 | set_random_seed(99) |
| 1261 | |
| 1262 | num_tokens = 32 |
| 1263 | hidden_dim = 512 |
| 1264 | intermediate_dim = 1024 |
| 1265 | num_experts = 4 |
| 1266 | topk = 2 |
| 1267 | |
| 1268 | hidden_states = torch.randn(num_tokens, hidden_dim, dtype=torch.bfloat16) / 10 |
| 1269 | fp8_dtype = current_platform.fp8_dtype() |
| 1270 | (w1_bf16, w1_fp8, w1_scale, _), (w2_bf16, w2_fp8, w2_scale, _) = make_test_weights( |
| 1271 | num_experts, |
| 1272 | intermediate_dim * 2, |
| 1273 | hidden_dim, |
| 1274 | torch.bfloat16, |
| 1275 | torch.float8_e4m3fn, |
| 1276 | per_out_ch_quant=False, |
| 1277 | ) |
| 1278 | _, a1_scale = rocm_aiter_ops.per_tensor_quant(hidden_states, fp8_dtype) |
| 1279 | router_logits = torch.randn(num_tokens, num_experts, device="cuda") |
| 1280 | router_probs = torch.softmax(router_logits, dim=-1) |
| 1281 | topk_weights, topk_ids = torch.topk(router_probs, k=topk, dim=-1) |
| 1282 | topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True) |
| 1283 | topk_weights = topk_weights.float() |
| 1284 | topk_ids = topk_ids.to(torch.int32) |
| 1285 | |
| 1286 | ref_out = ref_moe_forward( |
| 1287 | hidden_states, |
| 1288 | w1_bf16, |
| 1289 | w2_bf16, |
| 1290 | topk_weights, |
| 1291 | topk_ids, |
| 1292 | ) |
| 1293 | |
| 1294 | w1_shuffled, w2_shuffled = _shuffle_moe_weights(w1_fp8, w2_fp8) |
| 1295 | out = torch.ops.vllm.rocm_aiter_fused_moe( |
| 1296 | hidden_states, |
| 1297 | w1_shuffled, |
| 1298 | w2_shuffled, |
| 1299 | topk_weights, |
| 1300 | topk_ids, |
| 1301 | expert_mask=None, |
| 1302 | activation_method=int(ActivationMethod.SILU), |
| 1303 | quant_method=int(QuantMethod.PER_TENSOR), |
| 1304 | doweight_stage1=False, |
| 1305 | w1_scale=w1_scale, |
| 1306 | w2_scale=w2_scale, |
| 1307 | a1_scale=a1_scale, |
| 1308 | ) |
| 1309 | |
| 1310 | assert out.shape == hidden_states.shape |
| 1311 | _assert_close_budget( |
| 1312 | out.float(), |
| 1313 | ref_out, |
| 1314 | label=(f"mi3xx_fp8_accuracy arch={'gfx942' if on_gfx942() else 'gfx950'}"), |
| 1315 | atol=0.02, |
| 1316 | rtol=0.0, |
| 1317 | pass_rate=1.0, |
| 1318 | max_violation_factor=1.5, |
| 1319 | ) |
| 1320 | |
| 1321 | |
| 1322 | # Weight alignment tests -------------------------------------------------- |
| 1323 | # |
| 1324 | # AITER's CK 2stages MoE kernel rejects an intermediate size not divisible by |
| 1325 | # its tile width (64 at or below inter_dim 192, 128 above). Some model + TP |
| 1326 | # splits land on an unaligned size (e.g. 1792 / TP=8 = 224), so the AITER path |
| 1327 | # rounds the intermediate dim up in ``maybe_roundup_sizes`` and allocates the |
| 1328 | # weights at the padded size. |
| 1329 | |
| 1330 | ALIGNMENT_HIDDEN = 64 |
| 1331 | ALIGNMENT_NUM_EXPERTS = 2 |
| 1332 | |
| 1333 | |
| 1334 | def _make_alignment_moe_config(intermediate: int): |
| 1335 | from tests.kernels.moe.utils import make_dummy_moe_config |
| 1336 | |
| 1337 | return make_dummy_moe_config( |
| 1338 | num_experts=ALIGNMENT_NUM_EXPERTS, |
| 1339 | hidden_dim=ALIGNMENT_HIDDEN, |
| 1340 | intermediate_size=intermediate, |
| 1341 | ) |
| 1342 | |
| 1343 | |
| 1344 | def _make_aiter_method(moe_config): |
| 1345 | """Build the unquantized method with the backend pinned to AITER. |
| 1346 | |
| 1347 | ``select_unquantized_moe_backend`` needs a real ROCm + AITER runtime, so |
| 1348 | stub it out and set the backend directly. |
| 1349 | """ |
| 1350 | from unittest.mock import patch |
| 1351 | |
| 1352 | from vllm.model_executor.layers.fused_moe.oracle.unquantized import ( |
| 1353 | UnquantizedMoeBackend, |
| 1354 | ) |
| 1355 | from vllm.model_executor.layers.fused_moe.unquantized_fused_moe_method import ( |
| 1356 | UnquantizedFusedMoEMethod, |
| 1357 | ) |
| 1358 | |
| 1359 | with patch( |
| 1360 | "vllm.model_executor.layers.fused_moe.unquantized_fused_moe_method" |
| 1361 | ".select_unquantized_moe_backend", |
| 1362 | return_value=(UnquantizedMoeBackend.AITER, None), |
| 1363 | ): |
| 1364 | return UnquantizedFusedMoEMethod(moe_config) |
| 1365 | |
| 1366 | |
| 1367 | def _roundup(method, moe_config, intermediate): |
| 1368 | from vllm.model_executor.layers.fused_moe.config import FusedMoEParallelConfig |
| 1369 | |
| 1370 | return method.maybe_roundup_sizes( |
| 1371 | hidden_size=ALIGNMENT_HIDDEN, |
| 1372 | intermediate_size_per_partition=intermediate, |
| 1373 | act_dtype=moe_config.in_dtype, |
| 1374 | moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(), |
| 1375 | ) |
| 1376 | |
| 1377 | |
| 1378 | @pytest.mark.parametrize( |
| 1379 | ("intermediate", "expected_padded"), |
| 1380 | [ |
| 1381 | (64, 64), # already aligned, untouched |
| 1382 | (96, 128), |
| 1383 | (160, 192), |
| 1384 | (192, 192), # must stay 192: it is valid and has tuned configs |
| 1385 | (224, 256), # K2-Horizon-375B at TP=8 |
| 1386 | (256, 256), |
| 1387 | (448, 512), # K2-Horizon-375B at TP=4 |
| 1388 | (4096, 4096), |
| 1389 | ], |
| 1390 | ) |
| 1391 | def test_aiter_moe_roundup_pads_intermediate( |
| 1392 | intermediate, |
| 1393 | expected_padded, |
| 1394 | default_vllm_config, |
| 1395 | ): |
| 1396 | from vllm.model_executor.layers.fused_moe.oracle.unquantized import ( |
| 1397 | aiter_moe_intermediate_alignment, |
| 1398 | ) |
| 1399 | |
| 1400 | moe_config = _make_alignment_moe_config(intermediate) |
| 1401 | method = _make_aiter_method(moe_config) |
| 1402 | |
| 1403 | hidden, padded = _roundup(method, moe_config, intermediate) |
| 1404 | |
| 1405 | assert padded == expected_padded |
| 1406 | assert hidden == ALIGNMENT_HIDDEN |
| 1407 | # Padding must not move a size across the <= 192 threshold, or the |
| 1408 | # alignment we picked is not the one the kernel dispatches the padded |
| 1409 | # shape to. |
| 1410 | assert aiter_moe_intermediate_alignment(padded) == ( |
| 1411 | aiter_moe_intermediate_alignment(intermediate) |
| 1412 | ) |
| 1413 | |
| 1414 | |
| 1415 | @pytest.mark.parametrize("backend_name", ["TRITON", "FLASHINFER_CUTLASS"]) |
| 1416 | def test_aiter_moe_roundup_is_not_applied_to_other_backends( |
| 1417 | backend_name, |
| 1418 | default_vllm_config, |
| 1419 | ): |
| 1420 | """Backends with no alignment requirement keep the unaligned size.""" |
| 1421 | from vllm.model_executor.layers.fused_moe.oracle.unquantized import ( |
| 1422 | UnquantizedMoeBackend, |
| 1423 | ) |
| 1424 | |
| 1425 | intermediate = 224 |
| 1426 | moe_config = _make_alignment_moe_config(intermediate) |
| 1427 | method = _make_aiter_method(moe_config) |
| 1428 | method.unquantized_backend = UnquantizedMoeBackend[backend_name] |
| 1429 | |
| 1430 | _, padded = _roundup(method, moe_config, intermediate) |
| 1431 | |
| 1432 | assert padded == intermediate |
| 1433 | |
| 1434 | |
| 1435 | @pytest.mark.parametrize("intermediate", [224, 448]) |
| 1436 | def test_aiter_moe_reload_zeroes_intermediate_padding( |
| 1437 | intermediate, |
| 1438 | monkeypatch, |
| 1439 | default_vllm_config, |
| 1440 | ): |
| 1441 | """A reload must re-zero pad lanes left dirty in the weight storage. |
| 1442 | |
| 1443 | The reload writes back only the logical slices, so the pad lanes hold |
| 1444 | garbage (NaN here) that the AITER conversion must zero. Two passes also |
| 1445 | cover reloading into storage that already holds a previous conversion. |
| 1446 | """ |
| 1447 | from unittest.mock import MagicMock |
| 1448 | |
| 1449 | from vllm.model_executor.layers.fused_moe.oracle.unquantized import ( |
| 1450 | aiter_moe_intermediate_alignment, |
| 1451 | ) |
| 1452 | |
| 1453 | alignment = aiter_moe_intermediate_alignment(intermediate) |
| 1454 | padded = -(-intermediate // alignment) * alignment |
| 1455 | assert padded != intermediate |
| 1456 | |
| 1457 | moe_config = _make_alignment_moe_config(intermediate) |
| 1458 | assert moe_config.is_act_and_mul |
| 1459 | method = _make_aiter_method(moe_config) |
| 1460 | # RoutedExperts records the rounded-up size after maybe_roundup_sizes. |
| 1461 | moe_config.intermediate_size_per_partition = padded |
| 1462 | layer = torch.nn.Module() |
| 1463 | layer.moe_config = moe_config |
| 1464 | |
| 1465 | method.create_weights( |
| 1466 | layer=layer, |
| 1467 | num_experts=ALIGNMENT_NUM_EXPERTS, |
| 1468 | hidden_size=ALIGNMENT_HIDDEN, |
| 1469 | intermediate_size_per_partition=padded, |
| 1470 | params_dtype=torch.float32, |
| 1471 | ) |
| 1472 | |
| 1473 | # Identity shuffle keeps pad lanes sliceable; both stubs need real ROCm. |
| 1474 | monkeypatch.setattr( |
| 1475 | "vllm.model_executor.layers.fused_moe.oracle.unquantized" |
| 1476 | ".rocm_aiter_ops.shuffle_weights", |
| 1477 | lambda w13, w2: (w13, w2), |
| 1478 | ) |
| 1479 | monkeypatch.setattr( |
| 1480 | method, |
| 1481 | "_init_moe_kernel", |
| 1482 | lambda _layer: setattr(method, "moe_kernel", MagicMock()), |
| 1483 | ) |
| 1484 | |
| 1485 | up = padded # up projection starts one padded block into the fused rows |
| 1486 | |
| 1487 | for _ in range(2): |
| 1488 | gate = torch.randn(ALIGNMENT_NUM_EXPERTS, intermediate, ALIGNMENT_HIDDEN) |
| 1489 | up_proj = torch.randn(ALIGNMENT_NUM_EXPERTS, intermediate, ALIGNMENT_HIDDEN) |
| 1490 | down = torch.randn(ALIGNMENT_NUM_EXPERTS, ALIGNMENT_HIDDEN, intermediate) |
| 1491 | |
| 1492 | # NaN the pads, then write only the logical slices like the loader. |
| 1493 | layer.w13_weight.data.fill_(float("nan")) |
| 1494 | layer.w2_weight.data.fill_(float("nan")) |
| 1495 | layer.w13_weight.data[:, :intermediate].copy_(gate) |
| 1496 | layer.w13_weight.data[:, up : up + intermediate].copy_(up_proj) |
| 1497 | layer.w2_weight.data[:, :, :intermediate].copy_(down) |
| 1498 | |
| 1499 | method.process_weights_after_loading(layer) |
| 1500 | |
| 1501 | w13 = layer.w13_weight |
| 1502 | w2 = layer.w2_weight |
| 1503 | assert torch.equal(w13[:, :intermediate], gate) |
| 1504 | assert torch.equal(w13[:, up : up + intermediate], up_proj) |
| 1505 | assert torch.equal(w2[:, :, :intermediate], down) |
| 1506 | assert torch.all(w13[:, intermediate:up] == 0) |
| 1507 | assert torch.all(w13[:, up + intermediate :] == 0) |
| 1508 | assert torch.all(w2[:, :, intermediate:] == 0) |
| 1509 | |
| 1510 | |
| 1511 | def _aiter_accepts_intermediate(intermediate: int, num_tokens: int) -> bool: |
| 1512 | """Run the real AITER CK MoE kernel and report whether it dispatched.""" |
| 1513 | from vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe import ( |
| 1514 | ActivationMethod, |
| 1515 | QuantMethod, |
| 1516 | ) |
| 1517 | |
| 1518 | case = _make_moe_case( |
| 1519 | num_tokens=num_tokens, |
| 1520 | hidden_dim=1024, |
| 1521 | intermediate_dim=intermediate, |
| 1522 | num_experts=8, |
| 1523 | topk=2, |
| 1524 | seed=0, |
| 1525 | ) |
| 1526 | try: |
| 1527 | _run_fused_moe( |
| 1528 | case["hidden_states"], |
| 1529 | case["w1"], |
| 1530 | case["w2"], |
| 1531 | case["topk_weights"], |
| 1532 | case["topk_ids"], |
| 1533 | activation_method=int(ActivationMethod.SILU), |
| 1534 | quant_method=int(QuantMethod.NO), |
| 1535 | ) |
| 1536 | except RuntimeError: |
| 1537 | return False |
| 1538 | return True |
| 1539 | |
| 1540 | |
| 1541 | @pytest.mark.skipif( |
| 1542 | not on_cdna(), |
| 1543 | reason="CDNA ROCm only", |
| 1544 | ) |
| 1545 | @pytest.mark.parametrize("intermediate", [192, 224, 256]) |
| 1546 | # With topk=2 and 8 experts, these token counts select block_m 32 / 64 / 128 |
| 1547 | # respectively (AITER's get_block_size_M, on both 256- and 304-CU parts). |
| 1548 | @pytest.mark.parametrize("num_tokens", [1024, 2560, 5120]) |
| 1549 | def test_aiter_moe_alignment_rule_holds_across_block_m(intermediate, num_tokens): |
| 1550 | """The alignment rule must hold at every reachable ``block_m``. |
| 1551 | |
| 1552 | Stage 1 checks ``NPerBlock`` and stage 2 ``KPerBlock``; both vary with |
| 1553 | ``block_m``, so a size validated at one tile size says nothing about the |
| 1554 | others. |
| 1555 | """ |
| 1556 | from vllm.model_executor.layers.fused_moe.oracle.unquantized import ( |
| 1557 | aiter_moe_intermediate_alignment, |
| 1558 | ) |
| 1559 | |
| 1560 | _assert_aiter_supported() |
| 1561 | |
| 1562 | alignment = aiter_moe_intermediate_alignment(intermediate) |
| 1563 | padded = -(-intermediate // alignment) * alignment |
| 1564 | |
| 1565 | assert _aiter_accepts_intermediate(intermediate, num_tokens) == ( |
| 1566 | intermediate % alignment == 0 |
| 1567 | ) |
| 1568 | assert _aiter_accepts_intermediate(padded, num_tokens) |