Skip to content

Commit f0d20a1

Browse files
authored
Merge branch 'main' into main
2 parents f4a6cbb + 84e5f3e commit f0d20a1

6 files changed

Lines changed: 159 additions & 15 deletions

File tree

‎tests/kernels/test_minimax_m3_amd_ops.py‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,69 @@ def test_mxfp8_linear_emulation_bf16_at_load(
424424
assert _relerr(out.float(), out_ref.float()) < 2e-2
425425

426426

427+
# --------------------------------------------------------------------------- #
428+
# Native MXFP8 linear: the K % 128 != 0 weight is dequantized once at load
429+
# --------------------------------------------------------------------------- #
430+
@requires_gfx950
431+
# (5120, 576) and (5120, 288): DSv4.1 shared-expert down_proj at TP4 and TP8.
432+
@pytest.mark.parametrize("shape", [(5120, 576), (5120, 288), (256, 2048)])
433+
@torch.inference_mode()
434+
def test_mxfp8_rocm_native_unaligned_k_dequantizes_at_load(shape):
435+
"""``dot_scaled`` tiles K by 128, so an unaligned weight must not reach it.
436+
437+
``process_weights_after_loading`` converts those to BF16 once; a weight that
438+
never went through it has to fall back per step rather than take the
439+
invalid path, which is what keying the dispatch on the weight dtype alone
440+
would do.
441+
"""
442+
from vllm.model_executor.kernels.linear.mxfp8 import rocm_native
443+
from vllm.model_executor.kernels.linear.mxfp8.Mxfp8LinearKernel import (
444+
Mxfp8LinearLayerConfig,
445+
)
446+
447+
N, K = shape
448+
aligned = K % 128 == 0
449+
torch.manual_seed(0)
450+
w_bf16 = torch.randn(N, K, device=DEVICE, dtype=torch.bfloat16) * 0.1
451+
w_fp8, w_scale = _mxfp8_e4m3_quantize_torch(w_bf16, is_sf_swizzled_layout=False)
452+
x = torch.randn(7, K, device=DEVICE, dtype=torch.bfloat16) * 0.5
453+
out_ref = torch.nn.functional.linear(x, dequant_mxfp8_to_bf16(w_fp8, w_scale))
454+
# dot_scaled re-quantizes the activation, the dequant paths do not.
455+
tol = 5e-2 if aligned else 2e-3
456+
457+
def make_layer():
458+
layer = torch.nn.Module()
459+
layer.weight = torch.nn.Parameter(w_fp8.clone(), requires_grad=False)
460+
layer.weight_scale = torch.nn.Parameter(w_scale.clone(), requires_grad=False)
461+
return layer
462+
463+
kernel = rocm_native.RocmDotScaledMxfp8LinearKernel(Mxfp8LinearLayerConfig())
464+
layer = make_layer()
465+
kernel.process_weights_after_loading(layer)
466+
assert (layer.weight.element_size() >= 2) is not aligned
467+
assert _relerr(kernel.apply_weights(layer, x).float(), out_ref.float()) < tol
468+
469+
# An unaligned weight that is still FP8 must not reach dot_scaled.
470+
unprocessed = make_layer()
471+
unprocessed.weight_scale = torch.nn.Parameter(
472+
w_scale[:N, : K // 32].contiguous(), requires_grad=False
473+
)
474+
calls = []
475+
real = rocm_native._mxfp8_dot_scaled_linear
476+
477+
def _spy(*a, **k):
478+
calls.append(1)
479+
return real(*a, **k)
480+
481+
rocm_native._mxfp8_dot_scaled_linear = _spy
482+
try:
483+
out = kernel.apply_weights(unprocessed, x)
484+
finally:
485+
rocm_native._mxfp8_dot_scaled_linear = real
486+
assert bool(calls) is aligned
487+
assert _relerr(out.float(), out_ref.float()) < tol
488+
489+
427490
# ── EP expert_mask handling for the FlyDSL (AITER_MXFP8) MoE ────────────────
428491
# The map/mask choice lives in ``RoutedExperts.expert_map``: it hands AITER
429492
# experts (``consumes_expert_mask``) the precomputed 0/1 ``expert_mask`` and

‎tests/test_request_input_bounds.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import sys
1313
from collections.abc import Callable
1414
from typing import Protocol
15+
from unittest.mock import Mock
1516

1617
import pytest
1718
from pydantic import ValidationError
@@ -27,10 +28,39 @@
2728
from vllm.exceptions import VLLMValidationError
2829
from vllm.sampling_params import BeamSearchParams
2930
from vllm.tokenizers import deepseek_v4_encoding, deepseek_v32_encoding
31+
from vllm.v1.engine.input_processor import InputProcessor
3032

3133
pytestmark = [pytest.mark.cpu_test, pytest.mark.skip_global_cleanup]
3234

3335

36+
@pytest.mark.parametrize("offset", [-1, 0, 2, 3, 4, 2**127])
37+
def test_routed_experts_offset_validated_before_engine_submission(offset):
38+
"""Reject invalid offsets even when routed-expert output is disabled."""
39+
processor = Mock(spec=InputProcessor)
40+
processor.tokenizer = None
41+
processor.generation_config_fields = {}
42+
processor.renderer = Mock()
43+
processor.renderer.get_eos_token_id.return_value = None
44+
processor.vllm_config = Mock()
45+
params = SamplingParams(routed_experts_prompt_start=offset)
46+
prompt = {"type": "token", "prompt_token_ids": [1, 2, 3]}
47+
48+
if not 0 <= offset <= 3:
49+
with pytest.raises(VLLMValidationError, match="routed_experts_prompt_start"):
50+
InputProcessor.process_inputs(
51+
processor, "invalid", prompt, params, ("generate",)
52+
)
53+
params.routed_experts_prompt_start = 0
54+
55+
request = InputProcessor.process_inputs(
56+
processor, "valid", prompt, params, ("generate",)
57+
)
58+
assert (
59+
request.sampling_params.routed_experts_prompt_start
60+
== params.routed_experts_prompt_start
61+
)
62+
63+
3464
# --- Stop strings: public requests cap the number of stop strings ---------
3565

3666

‎tests/v1/core/test_scheduler.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
ECConnectorOutput,
5252
KVConnectorOutput,
5353
ModelRunnerOutput,
54+
RoutedExpertsLists,
5455
SamplingMaskLists,
5556
make_empty_encoder_model_runner_output,
5657
)
@@ -131,6 +132,44 @@ def test_add_requests():
131132
assert len(scheduler.waiting) == i + 1
132133

133134

135+
def test_routed_experts_prompt_start_at_prompt_end():
136+
"""A prompt-end offset should return empty routing data, not crash."""
137+
scheduler = create_scheduler()
138+
(request,) = create_requests(num_requests=1)
139+
request.sampling_params.routed_experts_prompt_start = request.num_prompt_tokens
140+
scheduler.add_request(request)
141+
scheduler_output = scheduler.schedule()
142+
num_tokens = scheduler_output.num_scheduled_tokens[request.request_id]
143+
144+
manager = Mock()
145+
manager.routed_experts_by_slot = np.empty((0, 1, 1), dtype=np.uint8)
146+
manager.get.return_value = np.empty((0, 1, 1), dtype=np.uint8)
147+
scheduler.enable_return_routed_experts = True
148+
scheduler.routed_experts_mgr = manager
149+
scheduler._re_block_ids = {request.request_id: []}
150+
151+
outputs = scheduler.update_from_output(
152+
scheduler_output,
153+
ModelRunnerOutput(
154+
req_ids=[request.request_id],
155+
req_id_to_index={request.request_id: 0},
156+
sampled_token_ids=[[0]],
157+
logprobs=None,
158+
prompt_logprobs_dict={},
159+
pooler_output=[],
160+
routed_experts=RoutedExpertsLists(
161+
routing_data=np.zeros((num_tokens, 1, 1), dtype=np.uint8),
162+
slot_mapping=np.arange(num_tokens),
163+
),
164+
),
165+
)
166+
167+
manager.get.assert_called_once_with(
168+
[], request.num_prompt_tokens, token_start=request.num_prompt_tokens
169+
)
170+
assert outputs[request.client_index].outputs[0].routed_experts.shape == (0, 1, 1)
171+
172+
134173
def test_finish_request():
135174
scheduler = create_scheduler()
136175
requests = create_requests(num_requests=10)

‎vllm/model_executor/kernels/linear/mxfp8/rocm_native.py‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
55
Consumes the FP8 E4M3 weights + E8M0 block scales directly (no dequant-to-BF16);
66
activations are MXFP8-quantized per token. Uses the CDNA4 hardware microscaling
7-
matrix cores. Falls back (via the kernel selector) to the BF16
8-
``EmulationMxfp8LinearKernel`` on archs without native MX or for shapes with
9-
``K % 128 != 0``.
7+
matrix cores. ``dot_scaled`` tiles K by 128; a weight whose K is not a multiple
8+
of that is dequantized to BF16 once at load and served by a plain linear
9+
instead, since ``can_implement`` does not filter on K.
1010
"""
1111

1212
import torch
@@ -194,6 +194,9 @@ def _select_cfg(M, N, K):
194194
return (128, 128, 256, 8, 2) if K % 256 == 0 else (128, 256, 128, 8, 3)
195195

196196

197+
_DOT_SCALED_K_ALIGN = 128
198+
199+
197200
class RocmDotScaledMxfp8LinearKernel(Mxfp8LinearKernel):
198201
"""Native CDNA4 (gfx950) MXFP8 linear via Triton ``tl.dot_scaled``."""
199202

@@ -219,6 +222,8 @@ def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
219222
N, K = weight.shape
220223
scale_k = K // MXFP8_BLOCK_SIZE
221224
weight_scale = layer.weight_scale.data[:N, :scale_k].contiguous()
225+
if K % _DOT_SCALED_K_ALIGN != 0:
226+
weight = dequant_mxfp8_to_bf16(weight.contiguous(), weight_scale)
222227
layer.weight = Parameter(weight.contiguous(), requires_grad=False)
223228
layer.weight_scale = Parameter(weight_scale, requires_grad=False)
224229

@@ -235,10 +240,13 @@ def apply_weights(
235240
)
236241
out_shape = (*x.shape[:-1], layer.weight.shape[0])
237242
x2d = x.reshape(-1, x.shape[-1])
238-
if x2d.shape[-1] % 128 == 0:
243+
if layer.weight.element_size() >= 2:
244+
out = torch.nn.functional.linear(x2d, layer.weight.to(x.dtype))
245+
elif x2d.shape[-1] % _DOT_SCALED_K_ALIGN == 0:
239246
out = _mxfp8_dot_scaled_linear(x2d, layer.weight, layer.weight_scale)
240247
else:
241-
# dot_scaled tiling needs K % 128 == 0; dequantize fallback otherwise.
248+
# process_weights_after_loading dequantizes these weights once, so
249+
# this runs only if it did not; dot_scaled would be invalid here.
242250
w_bf16 = dequant_mxfp8_to_bf16(layer.weight, layer.weight_scale)
243251
out = torch.nn.functional.linear(x2d, w_bf16).to(x.dtype)
244252
out = out.reshape(out_shape)

‎vllm/v1/core/sched/scheduler.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2101,7 +2101,7 @@ def update_from_output(
21012101
prompt_start = (
21022102
request.sampling_params.routed_experts_prompt_start
21032103
)
2104-
assert prompt_start < request.num_prompt_tokens
2104+
assert prompt_start <= request.num_prompt_tokens
21052105
else:
21062106
prompt_start = 0
21072107
routed_experts = self.routed_experts_mgr.get(

‎vllm/v1/engine/input_processor.py‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -354,12 +354,21 @@ def process_inputs(
354354
if isinstance(params, SamplingParams):
355355
# TODO: can we avoid cloning here in multiproc case?
356356
sampling_params = params.clone()
357+
prompt_len = length_from_prompt_token_ids_or_embeds(
358+
prompt_token_ids, prompt_embeds
359+
)
360+
if not 0 <= sampling_params.routed_experts_prompt_start <= prompt_len:
361+
raise VLLMValidationError(
362+
f"routed_experts_prompt_start must be between 0 and "
363+
f"the prompt length ({prompt_len}), inclusive.",
364+
parameter="routed_experts_prompt_start",
365+
value=sampling_params.routed_experts_prompt_start,
366+
)
357367
# If unset max tokens, then generate up to the max_model_len.
358368
if sampling_params.max_tokens is None:
359-
seq_len = length_from_prompt_token_ids_or_embeds(
360-
prompt_token_ids, prompt_embeds
369+
sampling_params.max_tokens = (
370+
self.model_config.max_model_len - prompt_len
361371
)
362-
sampling_params.max_tokens = self.model_config.max_model_len - seq_len
363372

364373
sampling_params.update_from_generation_config(
365374
self.generation_config_fields,
@@ -368,12 +377,7 @@ def process_inputs(
368377
if self.tokenizer is not None:
369378
sampling_params.update_from_tokenizer(self.tokenizer)
370379
if sampling_params.trace_decode_token_ids:
371-
self._normalize_trace_replay_params(
372-
sampling_params,
373-
length_from_prompt_token_ids_or_embeds(
374-
prompt_token_ids, prompt_embeds
375-
),
376-
)
380+
self._normalize_trace_replay_params(sampling_params, prompt_len)
377381
else:
378382
pooling_params = params.clone()
379383

0 commit comments

Comments
 (0)