main
py 861 lines 27.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from __future__ import annotations
5
6 import importlib
7 import types
8 from dataclasses import dataclass
9
10 import pytest
11 import torch
12
13 from vllm.model_executor.kernels.linear import (
14 _LINEAR_BACKEND_KERNEL_MAP,
15 _POSSIBLE_FP8_BLOCK_KERNELS,
16 _POSSIBLE_FP8_KERNELS,
17 _POSSIBLE_MXFP4_KERNELS,
18 _POSSIBLE_MXFP8_KERNELS,
19 _POSSIBLE_NVFP4_KERNELS,
20 B12xFp8BlockScaledMMKernel,
21 B12xMxFp4LinearKernel,
22 B12xMxfp8LinearKernel,
23 B12xNvFp4LinearKernel,
24 B12xTensorFP8ScaledMMLinearKernel,
25 FP8ScaledMMLinearLayerConfig,
26 Mxfp8LinearLayerConfig,
27 init_fp8_linear_kernel,
28 init_mxfp4_linear_kernel,
29 init_mxfp8_linear_kernel,
30 init_nvfp4_linear_kernel,
31 )
32 from vllm.model_executor.kernels.linear.nvfp4.marlin import (
33 MarlinNvFp4LinearKernel,
34 )
35 from vllm.model_executor.layers.quantization.utils.quant_utils import (
36 kFp8Dynamic128Sym,
37 kFp8Static128BlockSym,
38 kFp8StaticTensorSym,
39 kMxfp4Dynamic,
40 )
41 from vllm.platforms import PlatformEnum
42
43
44 @pytest.mark.parametrize(
45 ("kernel_cls", "kernels", "before", "after", "initializer", "kwargs"),
46 [
47 (
48 B12xMxFp4LinearKernel,
49 _POSSIBLE_MXFP4_KERNELS[PlatformEnum.CUDA],
50 "HummingMxFp4LinearKernel",
51 "EmulationMxfp4LinearKernel",
52 init_mxfp4_linear_kernel,
53 {"activation_quant_key": kMxfp4Dynamic},
54 ),
55 (
56 B12xNvFp4LinearKernel,
57 _POSSIBLE_NVFP4_KERNELS[PlatformEnum.CUDA],
58 "FbgemmNvFp4LinearKernel",
59 "EmulationNvFp4LinearKernel",
60 init_nvfp4_linear_kernel,
61 {},
62 ),
63 (
64 B12xMxfp8LinearKernel,
65 _POSSIBLE_MXFP8_KERNELS[PlatformEnum.CUDA],
66 "MarlinMxfp8LinearKernel",
67 "EmulationMxfp8LinearKernel",
68 init_mxfp8_linear_kernel,
69 {"weight_shape": (2048, 2048)},
70 ),
71 (
72 B12xTensorFP8ScaledMMLinearKernel,
73 _POSSIBLE_FP8_KERNELS[PlatformEnum.CUDA],
74 "CutlassFP8ScaledMMLinearKernel",
75 "PerTensorTorchFP8ScaledMMLinearKernel",
76 init_fp8_linear_kernel,
77 {
78 "activation_quant_key": kFp8StaticTensorSym,
79 "weight_quant_key": kFp8StaticTensorSym,
80 "input_dtype": torch.bfloat16,
81 "out_dtype": torch.bfloat16,
82 "weight_shape": (2048, 2048),
83 },
84 ),
85 (
86 B12xFp8BlockScaledMMKernel,
87 _POSSIBLE_FP8_BLOCK_KERNELS[PlatformEnum.CUDA],
88 "CutlassFp8BlockScaledMMKernel",
89 "MarlinFP8ScaledMMLinearKernel",
90 init_fp8_linear_kernel,
91 {
92 "activation_quant_key": kFp8Dynamic128Sym,
93 "weight_quant_key": kFp8Static128BlockSym,
94 "input_dtype": torch.bfloat16,
95 "out_dtype": torch.bfloat16,
96 "weight_shape": (2048, 2048),
97 },
98 ),
99 ],
100 )
101 def test_b12x_backend_registration_priority_and_selection(
102 monkeypatch,
103 default_vllm_config,
104 kernel_cls,
105 kernels,
106 before: str,
107 after: str,
108 initializer,
109 kwargs: dict,
110 ) -> None:
111 import vllm.model_executor.kernels.linear as linear_mod
112
113 assert kernel_cls in _LINEAR_BACKEND_KERNEL_MAP["b12x"]
114 names = [kernel.__name__ for kernel in kernels]
115 assert names.index(before) < names.index(kernel_cls.__name__) < names.index(after)
116
117 monkeypatch.setattr(linear_mod.current_platform, "_enum", PlatformEnum.CUDA)
118 monkeypatch.setattr(
119 linear_mod, "_get_linear_backend", lambda *, quantization: "b12x"
120 )
121 monkeypatch.setattr(
122 kernel_cls,
123 "is_supported",
124 classmethod(lambda cls, compute_capability=None: (True, None)),
125 )
126 monkeypatch.setattr(
127 kernel_cls,
128 "can_implement",
129 classmethod(lambda cls, config: (True, None)),
130 )
131
132 assert isinstance(initializer(**kwargs), kernel_cls)
133
134
135 def test_b12x_module_lookup_is_dynamo_safe(monkeypatch) -> None:
136 import vllm.utils.b12x as b12x_utils
137
138 module = types.ModuleType("b12x.gemm.blockscaled")
139 module.run = lambda x: x + 1 # type: ignore[attr-defined]
140 monkeypatch.setitem(
141 b12x_utils._B12X_SUBMODULES,
142 "b12x.gemm.blockscaled",
143 module,
144 )
145
146 @torch.compile(backend="eager", fullgraph=True)
147 def forward(x: torch.Tensor) -> torch.Tensor:
148 blockscaled = b12x_utils.get_b12x_blockscaled()
149 assert blockscaled is not None
150 return blockscaled.run(x) # type: ignore[attr-defined]
151
152 x = torch.ones(1)
153 torch.testing.assert_close(forward(x), x + 1)
154
155
156 def test_b12x_tensor_fp8_can_implement_supported_config() -> None:
157 config = FP8ScaledMMLinearLayerConfig(
158 activation_quant_key=kFp8StaticTensorSym,
159 weight_quant_key=kFp8StaticTensorSym,
160 weight_shape=(64, 128),
161 input_dtype=torch.bfloat16,
162 out_dtype=torch.bfloat16,
163 )
164
165 can_implement, reason = B12xTensorFP8ScaledMMLinearKernel.can_implement(config)
166
167 assert can_implement
168 assert reason is None
169
170
171 def test_b12x_block_fp8_checks_runtime_support(monkeypatch) -> None:
172 import vllm.model_executor.kernels.linear.scaled_mm.b12x as b12x_mod
173
174 platform = types.SimpleNamespace(
175 is_cuda=lambda: True,
176 is_device_capability_family=lambda family: family == 120,
177 )
178 monkeypatch.setattr(b12x_mod, "current_platform", platform)
179
180 monkeypatch.setattr(
181 b12x_mod,
182 "_import_b12x_blockscaled",
183 lambda: types.SimpleNamespace(is_supported=lambda: False),
184 )
185
186 supported, reason = B12xFp8BlockScaledMMKernel.is_supported()
187
188 assert not supported
189 assert reason == "B12X regular block-FP8 GEMM is not supported"
190
191
192 def test_b12x_block_fp8_requires_matching_supported_dtypes() -> None:
193 def config(input_dtype: torch.dtype, out_dtype: torch.dtype):
194 return FP8ScaledMMLinearLayerConfig(
195 activation_quant_key=kFp8Dynamic128Sym,
196 weight_quant_key=kFp8Static128BlockSym,
197 weight_shape=(256, 128),
198 input_dtype=input_dtype,
199 out_dtype=out_dtype,
200 )
201
202 can_implement, reason = B12xFp8BlockScaledMMKernel.can_implement(
203 config(torch.float32, torch.float32)
204 )
205 assert not can_implement
206 assert reason == "Supports only bf16/fp16 input dtype"
207
208 can_implement, reason = B12xFp8BlockScaledMMKernel.can_implement(
209 config(torch.bfloat16, torch.float16)
210 )
211 assert not can_implement
212 assert reason == "Input and output dtype must match"
213
214 can_implement, reason = B12xFp8BlockScaledMMKernel.can_implement(
215 config(torch.float16, torch.float16)
216 )
217 assert can_implement
218 assert reason is None
219
220
221 def test_b12x_block_fp8_requires_aligned_features() -> None:
222 def can_implement(weight_shape: tuple[int, int]):
223 config = FP8ScaledMMLinearLayerConfig(
224 activation_quant_key=kFp8Dynamic128Sym,
225 weight_quant_key=kFp8Static128BlockSym,
226 weight_shape=weight_shape,
227 input_dtype=torch.bfloat16,
228 out_dtype=torch.bfloat16,
229 )
230 return B12xFp8BlockScaledMMKernel.can_implement(config)
231
232 assert can_implement((256, 192)) == (
233 False,
234 "Input features must be a positive multiple of 128",
235 )
236 assert can_implement((192, 256)) == (
237 False,
238 "Output features must be a positive multiple of 128",
239 )
240
241
242 def test_b12x_tensor_fp8_process_weights_packs_modelopt_layout(
243 monkeypatch,
244 ) -> None:
245 import vllm.model_executor.kernels.linear.scaled_mm.b12x as b12x_mod
246
247 calls = []
248 packed = types.SimpleNamespace(out_features=64)
249
250 def pack(weight: torch.Tensor, output_scale: torch.Tensor):
251 calls.append((weight, output_scale))
252 return packed
253
254 monkeypatch.setattr(
255 b12x_mod,
256 "_import_b12x_tensor_fp8",
257 lambda: types.SimpleNamespace(pack_weight=pack),
258 )
259 layer = torch.nn.Module()
260 layer.prefix = "model.layers.0.self_attn.qkv_proj"
261 original_weight = (
262 torch.randn((128, 64), dtype=torch.float32).clamp(-4, 4).to(torch.float8_e4m3fn)
263 )
264 layer.weight = torch.nn.Parameter(original_weight, requires_grad=False)
265 layer.weight_scale = torch.nn.Parameter(torch.tensor(0.25), requires_grad=False)
266 layer.input_scale = torch.nn.Parameter(torch.tensor(0.5), requires_grad=False)
267 weight_loader = object()
268 scale_loader = object()
269 layer.weight.weight_loader = weight_loader
270 layer.weight_scale.weight_loader = scale_loader
271 kernel = object.__new__(B12xTensorFP8ScaledMMLinearKernel)
272 kernel.config = types.SimpleNamespace(weight_shape=(64, 128))
273 kernel.layer_param_names = (
274 "weight",
275 "weight_scale",
276 "input_scale",
277 "input_scale_ub",
278 )
279
280 kernel.process_weights_after_loading(layer)
281
282 assert layer.b12x_tensor_fp8_packed_weight is packed
283 assert layer.b12x_warmup_provider is kernel
284 assert len(calls) == 1
285 weight, output_scale = calls[0]
286 torch.testing.assert_close(weight, original_weight.T.contiguous())
287 torch.testing.assert_close(output_scale, torch.tensor([0.125]))
288 assert layer.weight.numel() == 0
289 assert layer.weight_scale.numel() == 0
290 assert layer.weight.weight_loader is weight_loader
291 assert layer.weight_scale.weight_loader is scale_loader
292 torch.testing.assert_close(layer.input_scale, torch.tensor(0.5))
293
294
295 def test_b12x_tensor_fp8_apply_quantizes_and_uses_packed_weight(
296 monkeypatch,
297 ) -> None:
298 import vllm.model_executor.kernels.linear.scaled_mm.b12x as b12x_mod
299
300 calls = []
301
302 def mm(
303 source: torch.Tensor,
304 packed_weight,
305 *,
306 bias: torch.Tensor | None = None,
307 out_dtype: torch.dtype,
308 expected_m: int,
309 stream: object = None,
310 ) -> torch.Tensor:
311 del stream
312 calls.append((source, packed_weight, bias, out_dtype, expected_m))
313 return torch.full(
314 (source.shape[0], packed_weight.out_features),
315 3.0,
316 dtype=out_dtype,
317 )
318
319 monkeypatch.setattr(
320 b12x_mod,
321 "_import_b12x_tensor_fp8",
322 lambda: types.SimpleNamespace(mm=mm),
323 )
324 monkeypatch.setattr(
325 b12x_mod,
326 "current_stream",
327 lambda: types.SimpleNamespace(cuda_stream=object()),
328 )
329 monkeypatch.setattr(torch.compiler, "is_compiling", lambda: False)
330
331 layer = torch.nn.Module()
332 packed = types.SimpleNamespace(out_features=48)
333 layer.b12x_tensor_fp8_packed_weight = packed
334 layer.weight = torch.nn.Parameter(
335 torch.empty((128, 48), dtype=torch.float8_e4m3fn),
336 requires_grad=False,
337 )
338 layer.weight_scale = torch.nn.Parameter(torch.tensor(0.25), requires_grad=False)
339 layer.input_scale = torch.nn.Parameter(torch.tensor(0.5), requires_grad=False)
340 x = torch.empty((2, 3, 128), dtype=torch.bfloat16)
341 x_q = torch.empty((6, 128), dtype=torch.float8_e4m3fn)
342 bias = torch.empty((48,), dtype=torch.bfloat16)
343 kernel = object.__new__(B12xTensorFP8ScaledMMLinearKernel)
344 kernel.config = types.SimpleNamespace(out_dtype=torch.bfloat16)
345 kernel.layer_param_names = (
346 "weight",
347 "weight_scale",
348 "input_scale",
349 "input_scale_ub",
350 )
351 kernel.quant_fp8 = lambda source, scale, scale_ub: (x_q, scale)
352
353 output = kernel.apply_weights(layer, x, bias)
354
355 assert output.shape == (2, 3, 48)
356 assert output.dtype == torch.bfloat16
357 assert len(calls) == 1
358 source, called_packed, called_bias, out_dtype, expected_m = calls[0]
359 assert source.data_ptr() == x_q.data_ptr()
360 assert called_packed is packed
361 assert called_bias is bias
362 assert out_dtype == torch.bfloat16
363 assert expected_m == 6
364
365
366 def test_b12x_mxfp8_can_implement_supported_config() -> None:
367 can_implement, reason = B12xMxfp8LinearKernel.can_implement(
368 Mxfp8LinearLayerConfig(weight_shape=(256, 512))
369 )
370
371 assert can_implement
372 assert reason is None
373
374
375 def test_b12x_mxfp8_support_check_reports_missing_import(monkeypatch) -> None:
376 import vllm.model_executor.kernels.linear.mxfp8.b12x as b12x_mod
377
378 monkeypatch.setattr(b12x_mod.current_platform, "is_cuda", lambda: True)
379 monkeypatch.setattr(
380 b12x_mod.current_platform,
381 "is_device_capability_family",
382 lambda family: family == 120,
383 )
384 monkeypatch.setattr(b12x_mod, "_import_b12x_mxfp8", lambda: None)
385
386 is_supported, reason = B12xMxfp8LinearKernel.is_supported()
387
388 assert not is_supported
389 assert reason == "Install the B12X backend with `pip install vllm[b12x]`"
390
391
392 def test_b12x_mxfp8_support_respects_runtime_probe(monkeypatch) -> None:
393 import vllm.model_executor.kernels.linear.mxfp8.b12x as b12x_mod
394
395 monkeypatch.setattr(b12x_mod.current_platform, "is_cuda", lambda: True)
396 monkeypatch.setattr(
397 b12x_mod.current_platform,
398 "is_device_capability_family",
399 lambda family: family == 120,
400 )
401 monkeypatch.setattr(
402 b12x_mod,
403 "_import_b12x_mxfp8",
404 lambda: types.SimpleNamespace(is_supported=lambda: False),
405 )
406
407 is_supported, reason = B12xMxfp8LinearKernel.is_supported()
408
409 assert not is_supported
410 assert reason == "b12x.gemm.mxfp8_linear is not supported"
411
412
413 def test_b12x_mxfp8_process_weights_packs_modelopt_layout(monkeypatch) -> None:
414 import vllm.model_executor.kernels.linear.mxfp8.b12x as b12x_mod
415
416 calls = []
417 packed = types.SimpleNamespace(out_features=48)
418
419 def pack(weight: torch.Tensor, weight_scale: torch.Tensor):
420 calls.append((weight, weight_scale))
421 return packed
422
423 monkeypatch.setattr(
424 b12x_mod,
425 "_import_b12x_mxfp8",
426 lambda: types.SimpleNamespace(pack_weight=pack),
427 )
428
429 layer = torch.nn.Module()
430 layer.prefix = "model.layers.0.self_attn.qkv_proj"
431 layer.weight = torch.nn.Parameter(
432 torch.empty((48, 128), dtype=torch.float8_e4m3fn),
433 requires_grad=False,
434 )
435 layer.weight_scale = torch.nn.Parameter(
436 torch.empty((64, 8), dtype=torch.uint8),
437 requires_grad=False,
438 )
439 weight_loader = object()
440 scale_loader = object()
441 layer.weight.weight_loader = weight_loader
442 layer.weight_scale.weight_loader = scale_loader
443 kernel = object.__new__(B12xMxfp8LinearKernel)
444
445 kernel.process_weights_after_loading(layer)
446
447 assert layer.b12x_mxfp8_packed_weight is packed
448 assert layer.b12x_warmup_provider is kernel
449 assert len(calls) == 1
450 weight, weight_scale = calls[0]
451 assert weight.shape == (48, 128)
452 assert weight_scale.shape == (48, 4)
453 assert weight.dtype == torch.float8_e4m3fn
454 assert weight_scale.dtype == torch.uint8
455 assert layer.weight.numel() == 0
456 assert layer.weight_scale.numel() == 0
457 assert layer.weight.weight_loader is weight_loader
458 assert layer.weight_scale.weight_loader is scale_loader
459
460
461 def test_b12x_mxfp8_reload_reuses_packed_tensor_addresses(monkeypatch) -> None:
462 import vllm.model_executor.kernels.linear.mxfp8.b12x as b12x_mod
463
464 @dataclass(frozen=True)
465 class PackedWeight:
466 values: torch.Tensor
467 scales: torch.Tensor
468 out_features: int
469
470 def pack(weight: torch.Tensor, weight_scale: torch.Tensor) -> PackedWeight:
471 return PackedWeight(
472 values=weight.clone(),
473 scales=weight_scale.clone(),
474 out_features=int(weight.shape[0]),
475 )
476
477 monkeypatch.setattr(
478 b12x_mod,
479 "_import_b12x_mxfp8",
480 lambda: types.SimpleNamespace(pack_weight=pack),
481 )
482 layer = torch.nn.Module()
483 layer.prefix = "model.layers.0.mlp.down_proj"
484 layer.weight = torch.nn.Parameter(
485 torch.zeros((48, 128), dtype=torch.float8_e4m3fn),
486 requires_grad=False,
487 )
488 layer.weight_scale = torch.nn.Parameter(
489 torch.zeros((48, 4), dtype=torch.uint8),
490 requires_grad=False,
491 )
492 kernel = object.__new__(B12xMxfp8LinearKernel)
493
494 kernel.process_weights_after_loading(layer)
495 packed = layer.b12x_mxfp8_packed_weight
496 values_ptr = packed.values.data_ptr()
497 scales_ptr = packed.scales.data_ptr()
498
499 layer.weight = torch.nn.Parameter(
500 torch.ones((48, 128), dtype=torch.float8_e4m3fn),
501 requires_grad=False,
502 )
503 layer.weight_scale = torch.nn.Parameter(
504 torch.full((48, 4), 3, dtype=torch.uint8),
505 requires_grad=False,
506 )
507 kernel.process_weights_after_loading(layer)
508
509 assert layer.b12x_mxfp8_packed_weight is packed
510 assert packed.values.data_ptr() == values_ptr
511 assert packed.scales.data_ptr() == scales_ptr
512 torch.testing.assert_close(
513 packed.values,
514 torch.ones((48, 128), dtype=torch.float8_e4m3fn),
515 )
516 torch.testing.assert_close(
517 packed.scales,
518 torch.full((48, 4), 3, dtype=torch.uint8),
519 )
520 assert layer.weight.numel() == 0
521 assert layer.weight_scale.numel() == 0
522
523
524 @pytest.fixture
525 def _mock_b12x_cuda_fp8_platform(monkeypatch: pytest.MonkeyPatch) -> None:
526 import vllm.model_executor.layers.quantization.utils.fp8_utils as fp8_utils
527
528 monkeypatch.setattr(
529 fp8_utils,
530 "current_platform",
531 types.SimpleNamespace(
532 is_fp8_fnuz=lambda: False,
533 is_rocm=lambda: False,
534 fp8_dtype=lambda: torch.float8_e4m3fn,
535 is_xpu=lambda: False,
536 is_cuda_alike=lambda: True,
537 ),
538 )
539
540
541 @pytest.mark.usefixtures("_mock_b12x_cuda_fp8_platform")
542 def test_b12x_block_fp8_process_weights_keeps_native_block_layout() -> None:
543 layer = torch.nn.Module()
544 layer.weight = torch.nn.Parameter(
545 torch.empty((128, 128), dtype=torch.float8_e4m3fn),
546 requires_grad=False,
547 )
548 layer.weight_scale_inv = torch.nn.Parameter(
549 torch.empty((1, 1), dtype=torch.float32),
550 requires_grad=False,
551 )
552 layer.weight_block_size = [128, 128]
553 weight_loader = object()
554 scale_loader = object()
555 layer.weight.weight_loader = weight_loader
556 layer.weight_scale_inv.weight_loader = scale_loader
557 kernel = object.__new__(B12xFp8BlockScaledMMKernel)
558
559 kernel.process_weights_after_loading(layer)
560
561 assert layer.b12x_warmup_provider is kernel
562 assert layer.weight.shape == (128, 128)
563 assert layer.weight.dtype == torch.float8_e4m3fn
564 assert layer.weight_scale_inv.shape == (1, 1)
565 assert layer.weight_scale_inv.dtype == torch.float32
566 assert layer.weight.weight_loader is weight_loader
567 assert layer.weight_scale_inv.weight_loader is scale_loader
568
569
570 @pytest.mark.parametrize("scale_dtype", [torch.float8_e8m0fnu, torch.uint8])
571 @pytest.mark.usefixtures("_mock_b12x_cuda_fp8_platform")
572 def test_b12x_block_fp8_upcasts_e8m0_weight_scales(scale_dtype) -> None:
573 layer = torch.nn.Module()
574 layer.weight = torch.nn.Parameter(
575 torch.empty((128, 128), dtype=torch.float8_e4m3fn),
576 requires_grad=False,
577 )
578 scale_bytes = torch.tensor([[125]], dtype=torch.uint8)
579 layer.weight_scale_inv = torch.nn.Parameter(
580 scale_bytes.view(scale_dtype),
581 requires_grad=False,
582 )
583 layer.weight_block_size = [128, 128]
584 kernel = object.__new__(B12xFp8BlockScaledMMKernel)
585
586 kernel.process_weights_after_loading(layer)
587
588 assert layer.weight_scale_inv.dtype == torch.float32
589 torch.testing.assert_close(
590 layer.weight_scale_inv,
591 torch.tensor([[0.25]], dtype=torch.float32),
592 )
593
594
595 def test_b12x_mxfp8_apply_uses_packed_weight(monkeypatch) -> None:
596 import vllm.model_executor.kernels.linear.mxfp8.b12x as b12x_mod
597
598 calls = []
599
600 def mxfp8_linear(
601 source: torch.Tensor,
602 packed_weight,
603 *,
604 bias: torch.Tensor | None = None,
605 expected_m: int | None = None,
606 stream: object = None,
607 ) -> torch.Tensor:
608 del stream
609 calls.append((source, packed_weight, bias, expected_m))
610 return source.new_full((source.shape[0], packed_weight.out_features), 3.0)
611
612 monkeypatch.setattr(
613 b12x_mod,
614 "_import_b12x_mxfp8",
615 lambda: types.SimpleNamespace(mm=mxfp8_linear),
616 )
617
618 layer = torch.nn.Module()
619 packed = types.SimpleNamespace(out_features=48)
620 layer.b12x_mxfp8_packed_weight = packed
621 x = torch.empty((2, 3, 128), dtype=torch.bfloat16)
622 bias = torch.empty((48,), dtype=torch.bfloat16)
623 kernel = object.__new__(B12xMxfp8LinearKernel)
624
625 output = kernel.apply_weights(layer, x, bias)
626
627 assert output.shape == (2, 3, 48)
628 assert output.dtype == x.dtype
629 assert len(calls) == 1
630 source, called_packed, called_bias, expected_m = calls[0]
631 assert source.shape == (6, 128)
632 assert called_packed is packed
633 assert called_bias is bias
634 assert expected_m == 6
635
636
637 def test_b12x_block_fp8_apply_uses_b12x_recipe_api(monkeypatch) -> None:
638 import vllm.model_executor.kernels.linear.scaled_mm.b12x as b12x_mod
639
640 calls = []
641
642 def mm_block_fp8(*args, **kwargs):
643 calls.append((args, kwargs))
644 return torch.full(
645 (args[0].shape[0], args[2].shape[0]),
646 13.0,
647 dtype=kwargs["out_dtype"],
648 )
649
650 monkeypatch.setattr(
651 b12x_mod,
652 "_import_b12x_blockscaled",
653 lambda: types.SimpleNamespace(mm_block_fp8=mm_block_fp8),
654 )
655
656 a = torch.empty((6, 128), dtype=torch.float8_e4m3fn)
657 weight = torch.empty((256, 128), dtype=torch.float8_e4m3fn)
658 a_scale = torch.empty((6, 1), dtype=torch.float32)
659 weight_scale = torch.empty((2, 1), dtype=torch.float32)
660 kernel = object.__new__(B12xFp8BlockScaledMMKernel)
661 kernel.config = types.SimpleNamespace(out_dtype=torch.bfloat16)
662
663 output = kernel.apply_block_scaled_mm(a, weight, a_scale, weight_scale)
664
665 assert output.shape == (6, 256)
666 assert output.dtype == torch.bfloat16
667 assert len(calls) == 1
668 assert calls[0] == (
669 (a, a_scale, weight, weight_scale),
670 {"out_dtype": torch.bfloat16},
671 )
672 torch.testing.assert_close(output, torch.full_like(output, 13.0))
673
674
675 def test_b12x_mxfp4_requires_dynamic_activations() -> None:
676 config = types.SimpleNamespace(activation_quant_key=kMxfp4Dynamic)
677 can_implement, reason = B12xMxFp4LinearKernel.can_implement(config)
678
679 assert can_implement
680 assert reason is None
681
682 config.activation_quant_key = None
683 can_implement, reason = B12xMxFp4LinearKernel.can_implement(config)
684
685 assert not can_implement
686 assert reason == "B12X MXFP4 GEMM requires dynamic MXFP4 activations"
687
688
689 @pytest.mark.parametrize(
690 ("kernel_cls", "module_name", "scale_dtype"),
691 [
692 (
693 B12xMxFp4LinearKernel,
694 "vllm.model_executor.kernels.linear.mxfp4.b12x",
695 torch.uint8,
696 ),
697 (
698 B12xNvFp4LinearKernel,
699 "vllm.model_executor.kernels.linear.nvfp4.b12x",
700 torch.float8_e4m3fn,
701 ),
702 ],
703 )
704 def test_b12x_fp4_processes_scale_and_preserves_loader(
705 monkeypatch,
706 kernel_cls,
707 module_name: str,
708 scale_dtype: torch.dtype,
709 ) -> None:
710 scale = torch.empty((48, 8), dtype=scale_dtype)
711 swizzled_scale = torch.empty((128, 8), dtype=scale_dtype)
712 intrinsics = types.SimpleNamespace(swizzle_block_scale=lambda value: swizzled_scale)
713 monkeypatch.setattr(
714 importlib.import_module(module_name),
715 "_import_b12x_intrinsics",
716 lambda: intrinsics,
717 )
718 layer = torch.nn.Module()
719 layer.prefix = "model.layers.0.mlp.shared_expert.down_proj"
720 layer.weight_scale = torch.nn.Parameter(scale, requires_grad=False)
721 weight_loader = object()
722 layer.weight_scale.weight_loader = weight_loader
723 kernel = object.__new__(kernel_cls)
724
725 kernel.process_weights_after_loading(layer)
726
727 assert layer.weight_scale.data_ptr() == swizzled_scale.data_ptr()
728 assert layer.weight_scale.weight_loader is weight_loader
729 assert layer.b12x_warmup_provider is kernel
730
731
732 def test_b12x_mxfp4_apply_calls_native_blockscaled_gemm(monkeypatch) -> None:
733 import vllm.model_executor.kernels.linear.mxfp4.b12x as b12x_mod
734 import vllm.utils.flashinfer as flashinfer_utils
735
736 calls: list[tuple] = []
737 x_packed = torch.empty((6, 64), dtype=torch.uint8)
738 x_scale_storage = torch.empty((128, 4), dtype=torch.uint8)
739
740 def mm_mxfp4(*args, **kwargs):
741 calls.append((args, kwargs))
742 return torch.full((6, 48), 3.0, dtype=torch.bfloat16)
743
744 monkeypatch.setattr(
745 flashinfer_utils,
746 "flashinfer_mxfp4_quantize",
747 lambda *args, **kwargs: (x_packed, x_scale_storage),
748 )
749 monkeypatch.setattr(
750 b12x_mod,
751 "_import_b12x_blockscaled",
752 lambda: types.SimpleNamespace(mm_mxfp4=mm_mxfp4),
753 )
754
755 layer = torch.nn.Module()
756 layer.output_size_per_partition = 48
757 layer.weight = torch.empty((48, 64), dtype=torch.uint8)
758 layer.weight_scale = torch.empty((128, 4), dtype=torch.uint8)
759 x = torch.empty((2, 3, 128), dtype=torch.bfloat16)
760 bias = torch.ones(48, dtype=torch.bfloat16)
761 kernel = object.__new__(B12xMxFp4LinearKernel)
762
763 output = kernel.apply_weights(layer, x, bias)
764
765 assert output.shape == (2, 3, 48)
766 torch.testing.assert_close(output, torch.full_like(output, 4.0))
767 assert len(calls) == 1
768 args, kwargs = calls[0]
769 assert args == (
770 x_packed,
771 x_scale_storage,
772 layer.weight,
773 layer.weight_scale,
774 )
775 assert kwargs == {"out_dtype": torch.bfloat16}
776
777
778 def test_b12x_nvfp4_can_implement_supported_config() -> None:
779 can_implement, reason = B12xNvFp4LinearKernel.can_implement(None)
780
781 assert can_implement
782 assert reason is None
783
784
785 def test_b12x_backend_preserves_w4a16_fallback(monkeypatch) -> None:
786 import vllm.model_executor.kernels.linear as linear_mod
787
788 monkeypatch.setattr(linear_mod.current_platform, "_enum", PlatformEnum.CUDA)
789 monkeypatch.setattr(
790 linear_mod, "_get_linear_backend", lambda *, quantization: "b12x"
791 )
792 monkeypatch.setitem(
793 linear_mod._POSSIBLE_NVFP4_KERNELS,
794 PlatformEnum.CUDA,
795 [B12xNvFp4LinearKernel, MarlinNvFp4LinearKernel],
796 )
797 monkeypatch.setattr(
798 MarlinNvFp4LinearKernel,
799 "is_supported",
800 classmethod(lambda cls, compute_capability=None: (True, None)),
801 )
802
803 kernel = init_nvfp4_linear_kernel(use_a16=True)
804
805 assert isinstance(kernel, MarlinNvFp4LinearKernel)
806
807
808 def test_b12x_nvfp4_apply_calls_native_blockscaled_gemm(monkeypatch) -> None:
809 import vllm.model_executor.kernels.linear.nvfp4.b12x as b12x_mod
810
811 calls: list[tuple] = []
812 quant_calls: list[tuple] = []
813 x_packed = torch.empty((6, 64), dtype=torch.uint8)
814 x_scale_storage = torch.empty((128, 8), dtype=torch.float8_e4m3fn)
815
816 def quant(*args, **kwargs):
817 quant_calls.append((args, kwargs))
818 return x_packed, x_scale_storage
819
820 def mm_nvfp4(*args, **kwargs):
821 calls.append((args, kwargs))
822 return torch.full((6, 48), 3.0, dtype=torch.bfloat16)
823
824 monkeypatch.setattr(b12x_mod, "scaled_fp4_quant", quant)
825 monkeypatch.setattr(
826 b12x_mod,
827 "_import_b12x_blockscaled",
828 lambda: types.SimpleNamespace(mm_nvfp4=mm_nvfp4),
829 )
830
831 layer = torch.nn.Module()
832 layer.output_size_per_partition = 48
833 layer.weight = torch.empty((48, 64), dtype=torch.uint8)
834 layer.weight_scale = torch.empty((128, 8), dtype=torch.float8_e4m3fn)
835 layer.input_global_scale_inv = torch.tensor(2.0)
836 layer.alpha = torch.tensor(0.25)
837 x = torch.empty((2, 3, 256), dtype=torch.bfloat16)[..., ::2]
838 bias = torch.ones(48, dtype=torch.bfloat16)
839 kernel = object.__new__(B12xNvFp4LinearKernel)
840
841 output = kernel.apply_weights(layer, x, bias)
842
843 assert output.shape == (2, 3, 48)
844 torch.testing.assert_close(output, torch.full_like(output, 4.0))
845 assert len(quant_calls) == 1
846 quant_args, quant_kwargs = quant_calls[0]
847 assert quant_args[0].shape == (6, 128)
848 assert quant_args[0].data_ptr() == x.data_ptr()
849 assert quant_args[1] is layer.input_global_scale_inv
850 assert not quant_args[0].is_contiguous()
851 assert quant_kwargs == {"is_sf_swizzled_layout": True}
852 assert len(calls) == 1
853 args, kwargs = calls[0]
854 assert args == (
855 x_packed,
856 x_scale_storage,
857 layer.weight,
858 layer.weight_scale,
859 layer.alpha,
860 )
861 assert kwargs == {"out_dtype": torch.bfloat16}