main
py 836 lines 30.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests whether FP8 computation is enabled correctly.
4
5 Run `pytest tests/quantization/test_fp8.py --forked`.
6 """
7
8 from types import SimpleNamespace
9
10 import pytest
11 import torch
12
13 from tests.quantization.utils import (
14 is_quant_method_supported,
15 load_model_without_vllm_runner,
16 )
17 from vllm import _custom_ops as ops
18 from vllm.config import set_current_vllm_config
19 from vllm.config.cache import CacheConfig
20 from vllm.config.kernel import KernelConfig
21 from vllm.config.model import ModelConfig
22 from vllm.forward_context import set_forward_context
23 from vllm.model_executor.layers.attention import Attention
24 from vllm.model_executor.layers.attention.attention import (
25 set_default_quant_scales,
26 )
27 from vllm.model_executor.layers.fused_moe import FusedMoEFactory
28 from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
29 from vllm.model_executor.layers.quantization.fp8 import (
30 Fp8Config,
31 Fp8LinearMethod,
32 Fp8MoEMethod,
33 )
34 from vllm.model_executor.layers.quantization.kv_cache import BaseKVCacheMethod
35 from vllm.model_executor.layers.quantization.modelopt import ModelOptLinearMethod
36 from vllm.model_executor.layers.quantization.utils import flashinfer_utils
37 from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
38 prepare_fp8_moe_layer_for_fi,
39 )
40 from vllm.model_executor.layers.quantization.utils.fp8_utils import (
41 process_fp8_input_tensor_strategy_moe,
42 )
43 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
44 from vllm.platforms import current_platform
45
46 DEVICE_TYPE = current_platform.device_type
47
48 MODELS = [
49 "neuralmagic/Meta-Llama-3-8B-Instruct-FP8-KV",
50 # The checkpoint below was removed from the HF.
51 # TODO: add a small replacement checkpoint.
52 pytest.param(
53 "nm-testing/Qwen2-0.5B-Instruct-FP8-SkipQKV",
54 marks=pytest.mark.skip(reason="Checkpoint removed from HF."),
55 ),
56 ]
57
58
59 @pytest.mark.skipif(not current_platform.is_cuda(), reason="DSV4.1 requires CUDA")
60 @pytest.mark.parametrize("tp_rank", [0, 1])
61 @pytest.mark.parametrize("scale_dtype", [torch.uint8, torch.float8_e8m0fnu])
62 def test_deepseek_v41_mxfp8_scale_loading(
63 dist_init, default_vllm_config, monkeypatch, tp_rank, scale_dtype
64 ):
65 """Expand linear scales before TP slicing, including padded shared experts."""
66 from vllm.model_executor.layers import linear as linear_module
67 from vllm.model_executor.layers.linear import (
68 ColumnParallelLinear,
69 MergedColumnParallelLinear,
70 RowParallelLinear,
71 )
72 from vllm.models.deepseek_v41 import quant_config as quant_module
73 from vllm.models.deepseek_v41.nvidia import model as model_module
74
75 default_vllm_config.model_config = SimpleNamespace(dtype=torch.bfloat16)
76 quant_config = quant_module.DeepseekV4FP8Config(
77 is_checkpoint_fp8_serialized=True, weight_block_size=[32, 32]
78 )
79 quant_config._resolved_expert_dtype = "fp4"
80 for module in (model_module, linear_module):
81 monkeypatch.setattr(module, "get_tensor_model_parallel_world_size", lambda: 2)
82 monkeypatch.setattr(module, "get_tensor_model_parallel_rank", lambda: tp_rank)
83 block = torch.nn.Module()
84 block.attn = torch.nn.Module()
85 block.attn.wq_b = ColumnParallelLinear(
86 128, 128, quant_config=quant_config, bias=False
87 )
88 block.attn.fused_wqa_wkv = MergedColumnParallelLinear(
89 128, [128, 64], quant_config=quant_config, bias=False
90 )
91 block.ffn = torch.nn.Module()
92 block.ffn.shared_experts = torch.nn.Module()
93 block.ffn.shared_experts.gate_up_proj = MergedColumnParallelLinear(
94 128, [128, 128], quant_config=quant_config, bias=False
95 )
96 block.ffn.shared_experts.down_proj = RowParallelLinear(
97 128, 128, quant_config=quant_config, bias=False
98 )
99 block.ffn.experts = torch.nn.Module()
100 block.ffn.experts.register_parameter(
101 "weight_scale", torch.nn.Parameter(torch.empty(3, 4, dtype=torch.uint8), False)
102 )
103 block.engram = torch.nn.Module()
104 block.engram.register_parameter(
105 "weight_scale_inv",
106 torch.nn.Parameter(torch.empty(3, 4, dtype=scale_dtype), False),
107 )
108
109 def load_expert_scale(param, weight, *args, **kwargs):
110 param.data.copy_(weight)
111 return True
112
113 block.ffn.experts.weight_scale.weight_loader = load_expert_scale
114 model = model_module.DeepseekV4Model.__new__(model_module.DeepseekV4Model)
115 torch.nn.Module.__init__(model)
116 model.layers = torch.nn.ModuleList([block])
117 model.config = SimpleNamespace(num_attention_heads=2)
118 model.quant_config = quant_config
119 model.use_sequence_parallel = False
120 monkeypatch.setattr(
121 model,
122 "get_expert_mapping",
123 lambda: [("experts.weight_scale", "experts.0.w1.weight_scale", 0, "w1")],
124 )
125
126 checkpoints = [
127 ("attn.wq_b", "attn.wq_b", 128, 128, 0, None),
128 ("attn.wq_a", "attn.fused_wqa_wkv", 128, 128, 0, slice(0, 64)),
129 ("attn.wkv", "attn.fused_wqa_wkv", 64, 128, 0, slice(64, 96)),
130 (
131 "ffn.shared_experts.w1",
132 "ffn.shared_experts.gate_up_proj",
133 96,
134 128,
135 0,
136 slice(0, 64),
137 ),
138 (
139 "ffn.shared_experts.w3",
140 "ffn.shared_experts.gate_up_proj",
141 96,
142 128,
143 0,
144 slice(64, 128),
145 ),
146 (
147 "ffn.shared_experts.down_proj",
148 "ffn.shared_experts.down_proj",
149 128,
150 96,
151 1,
152 None,
153 ),
154 ]
155 weights = []
156 expected = []
157 for source, target, n, k, axis, shard in checkpoints:
158 weight = torch.randint(-4, 5, (n, k)).to(torch.float8_e4m3fn)
159 scale_bytes = torch.randint(124, 131, (n // 32, k // 32), dtype=torch.uint8)
160 weights.extend(
161 [
162 (f"layers.0.{source}.weight", weight),
163 (f"layers.0.{source}.scale", scale_bytes.view(scale_dtype)),
164 ]
165 )
166 dequant = (
167 weight.float().reshape(n // 32, 32, k // 32, 32)
168 * torch.exp2(scale_bytes.float() - 127)[:, None, :, None]
169 ).reshape(n, k)
170 if "shared_experts" in source:
171 padded = torch.zeros(128, 128)
172 padded[:n, :k] = dequant
173 dequant = padded
174 expected.append((target, shard, dequant.chunk(2, dim=axis)[tp_rank]))
175
176 expert_scale = torch.full((3, 4), 125, dtype=torch.uint8)
177 weights.append(
178 ("layers.0.ffn.experts.0.w1.weight_scale", expert_scale.view(scale_dtype))
179 )
180 weights.append(("layers.0.engram.weight_scale_inv", expert_scale.view(scale_dtype)))
181 mapper = model_module._make_deepseek_v4_weights_mapper("fp4", "weight_scale")
182 loaded = model.load_weights(
183 (name.removeprefix("model."), weight) for name, weight in mapper.apply(weights)
184 )
185 assert "layers.0.ffn.experts.weight_scale" in loaded
186 assert torch.equal(block.ffn.experts.weight_scale, expert_scale)
187 assert torch.equal(block.engram.weight_scale_inv.view(torch.uint8), expert_scale)
188 for target, shard, reference in expected:
189 linear = block.get_submodule(target)
190 assert isinstance(linear.quant_method, ModelOptLinearMethod)
191 assert f"layers.0.{target}.weight_scale" in loaded
192 weight = linear.weight if shard is None else linear.weight[shard]
193 scale = linear.weight_scale if shard is None else linear.weight_scale[shard]
194 actual = (
195 weight.float().unflatten(-1, (-1, 32))
196 * torch.exp2(scale.float() - 127).unsqueeze(-1)
197 ).flatten(-2)
198 torch.testing.assert_close(actual, reference, rtol=0, atol=0)
199
200
201 @pytest.mark.skipif(not current_platform.is_cuda(), reason="DSV4.1 requires CUDA")
202 @pytest.mark.parametrize(
203 "weight_block_size,expert_dtype,scale_name",
204 [
205 ([32, 32], "fp4", "weight_scale"),
206 ([128, 128], "fp4", "weight_scale_inv"),
207 ([32, 32], "fp8", "weight_scale_inv"),
208 ],
209 )
210 def test_deepseek_v41_vl_mapper_routes_linear_scales(
211 weight_block_size, expert_dtype, scale_name
212 ):
213 """The VL wrapper must map ``.scale`` keys to the parameter the linear
214 quant method registers, as the text model does. A hardcoded
215 ``weight_scale_inv`` raised KeyError for native MXFP8 checkpoints."""
216 from vllm.models.deepseek_v41.nvidia import model as model_module
217 from vllm.models.deepseek_v41.nvidia import vl_model as vl_module
218
219 vllm_config = SimpleNamespace(
220 quant_config=SimpleNamespace(weight_block_size=weight_block_size)
221 )
222 resolved = model_module._linear_scale_param_name(
223 vllm_config,
224 expert_dtype,
225 )
226 assert resolved == scale_name
227
228 mapper = vl_module._make_deepseek_v4_vl_weights_mapper(expert_dtype, resolved)
229 weight = torch.empty(0)
230 mapped = [
231 name
232 for name, _ in mapper.apply(
233 [("layers.0.attn.wq_a.scale", weight), ("layers.0.attn.wkv.weight", weight)]
234 )
235 ]
236 assert mapped == [
237 f"language_model.model.layers.0.attn.wq_a.{scale_name}",
238 "language_model.model.layers.0.attn.wkv.weight",
239 ]
240
241
242 def test_deepseek_v41_vl_exposes_quant_mappings_on_class():
243 """``configure_quant_config`` reads both mappings off the class, before
244 ``__init__`` builds the instance mapper. Without them a Quark config's
245 per-layer keys never match this wrapper's ``language_model.``-rooted
246 prefixes and every attention shard falls back to the global spec."""
247 from vllm.models.deepseek_v41.amd.vl_model import DeepseekV41ForCausalLM
248
249 assert "fused_wqa_wkv" in DeepseekV41ForCausalLM.packed_modules_mapping
250 mapper = DeepseekV41ForCausalLM.hf_to_vllm_mapper.get_rename_mapper()
251 assert mapper.apply_list(["layers.0.attn.wq_a"]) == [
252 "language_model.model.layers.0.attn.wq_a"
253 ]
254
255
256 def test_deepseek_v41_engram_scale_accepts_quark_name():
257 """Quark exports name the engram scale ``embed.weight_scale``; the
258 ``\\.scale$`` rules only match a literal ``.scale`` suffix, so without an
259 explicit rule the tensor is never routed and loading fails."""
260 from vllm.models.deepseek_v41.amd import model as model_module
261
262 mapper = model_module._make_deepseek_v4_weights_mapper("fp4", "weight_scale")
263 weight = torch.empty(0)
264 mapped = [
265 name
266 for name, _ in mapper.apply(
267 [
268 ("layers.1.engram.embed.weight_scale", weight),
269 ("layers.1.engram.embed.scale", weight),
270 ]
271 )
272 ]
273 assert mapped == [
274 "model.layers.1.engram.embed_tokens.weight_scale_inv",
275 "model.layers.1.engram.embed_tokens.weight_scale_inv",
276 ]
277
278
279 def test_deepseek_v41_declines_quark_configs():
280 """``from_config`` rewrites a Quark config into a single global FP8 scheme,
281 which would discard the per-layer specs of a mixed-precision export (MXFP4
282 experts + 2-D block MXFP8 attention in DeepSeek-V4.1-Flash). QuarkConfig
283 knows how to dispatch each scheme, so it must handle every Quark export."""
284 from vllm.models.deepseek_v41.quant_config import DeepseekV4FP8Config
285
286 hf_config = SimpleNamespace(model_type="deepseek_v41")
287 mxfp4_global = {
288 "global_quant_config": {
289 "weight": {"dtype": "fp4", "qscheme": "per_group", "group_size": 32}
290 },
291 "quant_method": "quark",
292 }
293
294 assert (
295 DeepseekV4FP8Config.override_quantization_method(
296 mxfp4_global, None, hf_config=hf_config
297 )
298 is None
299 )
300
301 mixed = {
302 **mxfp4_global,
303 "layer_quant_config": {
304 "layers.0.attn.wkv": {
305 "weight": {
306 "dtype": "fp8_e4m3",
307 "qscheme": "per_block",
308 "block_size": [32, 32],
309 }
310 }
311 },
312 }
313 assert (
314 DeepseekV4FP8Config.override_quantization_method(
315 mixed, None, hf_config=hf_config
316 )
317 is None
318 )
319
320
321 @pytest.mark.skipif(not current_platform.is_cuda(), reason="DeepGEMM requires CUDA")
322 @pytest.mark.parametrize("scale_dtype", [torch.uint8, torch.float8_e8m0fnu])
323 @pytest.mark.parametrize(
324 "weight_shape,is_bmm",
325 [
326 ((129, 160), False),
327 ((387, 160), True),
328 ((3, 129, 160), False),
329 ((3, 128, 512), False),
330 ],
331 )
332 def test_deepgemm_mxfp8_preserves_weight_and_scale_values(
333 scale_dtype, weight_shape, is_bmm
334 ):
335 """DeepGEMM layout conversion preserves native weights and E8M0 scales."""
336 from vllm.model_executor.layers.quantization.utils import fp8_utils
337 from vllm.utils.deep_gemm import is_deep_gemm_supported
338
339 if not is_deep_gemm_supported() or not current_platform.is_device_capability_family(
340 100
341 ):
342 pytest.skip("DeepGEMM MXFP8 requires Blackwell")
343
344 weight = torch.randn(weight_shape, device="cuda").to(torch.float8_e4m3fn)
345 scales = (
346 torch.randint(
347 1,
348 255,
349 (weight.numel() // 32 + 1,),
350 device="cuda",
351 dtype=torch.uint8,
352 )[1:]
353 .view(*weight_shape[:-1], weight_shape[-1] // 32)
354 .view(scale_dtype)
355 )
356 original_weight = weight.view(torch.uint8).clone()
357 original_scales = scales.view(torch.uint8).clone()
358 processed_weight, packed = fp8_utils.deepgemm_post_process_fp8_weight_block(
359 weight,
360 scales,
361 quant_block_shape=(1, 32),
362 use_e8m0=True,
363 is_bmm=is_bmm,
364 bmm_batch_size=3 if is_bmm else 0,
365 )
366 assert packed.dtype == torch.int32
367 unpacked = torch.stack(
368 [(packed >> (8 * i)) & 0xFF for i in range(4)], dim=-1
369 ).flatten(-2)
370 unpacked = unpacked[..., : scales.shape[-1]].to(torch.uint8)
371 torch.testing.assert_close(
372 unpacked.reshape_as(original_scales), original_scales, rtol=0, atol=0
373 )
374 torch.testing.assert_close(
375 processed_weight.view(torch.uint8).reshape_as(original_weight),
376 original_weight,
377 rtol=0,
378 atol=0,
379 )
380
381
382 @pytest.mark.skipif(not current_platform.is_cuda(), reason="DeepGEMM requires CUDA")
383 @pytest.mark.parametrize("prequantized", [False, True])
384 @pytest.mark.parametrize("config_source", ["deepseek", "mxfp8"])
385 @pytest.mark.parametrize("num_tokens", [1, 7, 128])
386 def test_mxfp8_bmm_loads_and_projects_grouped_weights(
387 dist_init, default_vllm_config, monkeypatch, prequantized, config_source, num_tokens
388 ):
389 """BMM metadata set after construction selects grouped weight processing."""
390 from vllm.model_executor.kernels.linear.mxfp8.deep_gemm import (
391 DeepGemmMxfp8BmmLinearKernel,
392 )
393 from vllm.model_executor.layers.linear import ColumnParallelLinear
394 from vllm.model_executor.layers.quantization import get_quantization_config
395 from vllm.models.deepseek_v4.common.ops.fused_inv_rope_fp8_quant import (
396 fused_inv_rope_fp8_quant,
397 )
398 from vllm.models.deepseek_v4.nvidia.ops.o_proj import (
399 compute_fp8_einsum_recipe,
400 deep_gemm_fp8_o_proj,
401 )
402 from vllm.models.deepseek_v41.nvidia.model import DeepseekV4Model
403 from vllm.models.deepseek_v41.quant_config import DeepseekV4FP8Config
404 from vllm.utils.deep_gemm import is_deep_gemm_supported
405
406 if not is_deep_gemm_supported() or not current_platform.is_device_capability_family(
407 100
408 ):
409 pytest.skip("DeepGEMM MXFP8 BMM requires Blackwell")
410
411 default_vllm_config.model_config = SimpleNamespace(dtype=torch.bfloat16)
412 deepseek_config = DeepseekV4FP8Config(
413 is_checkpoint_fp8_serialized=True, weight_block_size=[32, 32]
414 )
415 deepseek_config._resolved_expert_dtype = "fp4"
416 quant_config: QuantizationConfig = deepseek_config
417 if config_source == "mxfp8":
418 quant_config = get_quantization_config("mxfp8").from_config(
419 {"quant_method": "mxfp8"}
420 )
421 with torch.device("cuda"):
422 linear = ColumnParallelLinear(
423 512,
424 256,
425 bias=False,
426 quant_config=quant_config,
427 return_bias=False,
428 )
429 linear.is_bmm = True
430 linear.bmm_batch_size = 2
431 weight = torch.randn(256, 512, device="cuda").to(torch.float8_e4m3fn)
432 scales = torch.randint(124, 131, (8, 16), device="cuda", dtype=torch.uint8)
433 checkpoint_scales = (
434 scales.view(torch.float8_e8m0fnu)
435 if config_source == "deepseek"
436 else scales.repeat_interleave(32, dim=0)
437 )
438 model = DeepseekV4Model.__new__(DeepseekV4Model)
439 torch.nn.Module.__init__(model)
440 block = torch.nn.Module()
441 block.attn = torch.nn.Module()
442 block.attn.wo_a = linear
443 model.layers = torch.nn.ModuleList([block])
444 model.config = SimpleNamespace(num_attention_heads=2)
445 model.quant_config = quant_config
446 model.use_sequence_parallel = False
447 monkeypatch.setattr(model, "get_expert_mapping", lambda: [])
448 model.load_weights(
449 [
450 ("layers.0.attn.wo_a.weight", weight),
451 ("layers.0.attn.wo_a.weight_scale", checkpoint_scales),
452 ]
453 )
454 reference_weight = (
455 weight.float().reshape(8, 32, 16, 32)
456 * torch.exp2(scales.float() - 127)[:, None, :, None]
457 ).reshape(2, 128, 512)
458 assert isinstance(linear.quant_method, ModelOptLinearMethod)
459 linear.quant_method.process_weights_after_loading(linear)
460 linear.quant_method.process_weights_after_loading(linear)
461 assert isinstance(linear.quant_method.kernel, DeepGemmMxfp8BmmLinearKernel)
462 assert linear.weight.shape == (2, 128, 512)
463 torch.testing.assert_close(
464 linear.weight.view(torch.uint8).flatten(),
465 weight.view(torch.uint8).flatten(),
466 rtol=0,
467 atol=0,
468 )
469 unpacked_scales = torch.stack(
470 [(linear.weight_scale >> (8 * i)) & 0xFF for i in range(4)], dim=-1
471 ).flatten(-2)
472 torch.testing.assert_close(
473 unpacked_scales.to(torch.uint8).reshape(256, 16),
474 scales.repeat_interleave(32, dim=0),
475 rtol=0,
476 atol=0,
477 )
478
479 x = torch.randn(2, num_tokens, 512, device="cuda", dtype=torch.bfloat16)
480 reference = torch.einsum("gmk,gnk->mgn", x.float(), reference_weight)
481 inputs = x.transpose(0, 1)
482 if prequantized:
483 cache = torch.cat(
484 (
485 torch.ones(num_tokens, 32, device="cuda"),
486 torch.zeros(num_tokens, 32, device="cuda"),
487 ),
488 dim=1,
489 )
490 inputs = fused_inv_rope_fp8_quant(
491 x.permute(1, 0, 2),
492 torch.arange(num_tokens, device="cuda"),
493 cache,
494 n_groups=2,
495 heads_per_group=1,
496 nope_dim=448,
497 rope_dim=64,
498 quant_group_size=32,
499 tma_aligned_scales=current_platform.has_device_capability(100),
500 )
501 with torch.no_grad():
502 output = linear(inputs)
503 assert output.shape == reference.shape
504 assert (output.float() - reference).norm() / reference.norm() < 0.06
505 if prequantized:
506 recipe, tma_aligned_scales = compute_fp8_einsum_recipe(block_size=32)
507 projected = deep_gemm_fp8_o_proj(
508 x.permute(1, 0, 2),
509 torch.arange(num_tokens, device="cuda"),
510 cache,
511 linear,
512 torch.nn.Identity(),
513 n_groups=2,
514 heads_per_group=1,
515 nope_dim=448,
516 rope_dim=64,
517 o_lora_rank=128,
518 einsum_recipe=recipe,
519 tma_aligned_scales=tma_aligned_scales,
520 )
521 torch.testing.assert_close(projected, output.flatten(1), rtol=0, atol=0)
522 compiled = torch.compile(linear, backend="eager", fullgraph=True)
523 with torch.no_grad():
524 torch.testing.assert_close(compiled(inputs), output, rtol=0, atol=0)
525
526
527 def test_prepare_gated_trtllm_fp8_moe_weights_pads_each_projection(monkeypatch):
528 monkeypatch.setattr(
529 flashinfer_utils,
530 "rotate_weights_for_fi_trtllm_fp8_per_tensor_moe",
531 lambda *args: None,
532 )
533 intermediate = 17
534 padded_intermediate = 32
535 hidden_size = 4
536 gate = torch.ones((1, intermediate, hidden_size), dtype=torch.float8_e4m3fn)
537 up = torch.full_like(gate, 2)
538 w13 = torch.cat((gate, up), dim=1)
539 w2 = torch.ones((1, hidden_size, intermediate), dtype=torch.float8_e4m3fn)
540 layer = SimpleNamespace(
541 activation=SimpleNamespace(is_gated=True),
542 moe_config=SimpleNamespace(
543 is_act_and_mul=True,
544 intermediate_size_per_partition=intermediate,
545 ),
546 )
547
548 padded_w31, _, _, _ = prepare_fp8_moe_layer_for_fi(
549 layer,
550 w13,
551 w2,
552 w13_scale=torch.ones(1),
553 w13_input_scale=torch.ones(1),
554 w2_scale=torch.ones(1),
555 w2_input_scale=torch.ones(1),
556 is_trtllm=True,
557 )
558
559 expected = w13.new_zeros((1, 2 * padded_intermediate, hidden_size))
560 expected[:, :intermediate] = up
561 expected[:, padded_intermediate : padded_intermediate + intermediate] = gate
562 assert layer.moe_config.intermediate_size_per_partition == padded_intermediate
563 assert torch.equal(padded_w31, expected)
564
565
566 def test_static_fp8_moe_input_scales_remain_scalar() -> None:
567 a1_scale, a2_scale = process_fp8_input_tensor_strategy_moe(
568 torch.tensor([0.25, 0.5]),
569 torch.tensor([0.75, 0.6]),
570 enable_eplb=False,
571 )
572
573 assert a1_scale.ndim == a2_scale.ndim == 0
574
575
576 @pytest.mark.skipif(
577 not is_quant_method_supported("fp8"),
578 reason="FP8 is not supported on this GPU type.",
579 )
580 @pytest.mark.parametrize("model_id", MODELS)
581 @pytest.mark.parametrize(
582 "force_marlin", [True, False] if current_platform.is_cuda() else [False]
583 )
584 @pytest.mark.parametrize(
585 "use_rocm_aiter", [True, False] if current_platform.is_rocm() else [False]
586 )
587 def test_model_load_and_run(
588 model_id: str,
589 force_marlin: bool,
590 use_rocm_aiter: bool,
591 monkeypatch,
592 dist_init,
593 workspace_init,
594 ) -> None:
595 if use_rocm_aiter:
596 monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1")
597
598 kernel_config = KernelConfig(
599 linear_backend="marlin" if force_marlin else "auto",
600 moe_backend="marlin" if force_marlin else "auto",
601 )
602 model, vllm_config = load_model_without_vllm_runner(
603 model_id,
604 model_config_kwargs={"hf_overrides": {"num_hidden_layers": 3}},
605 vllm_config_kwargs={"kernel_config": kernel_config},
606 )
607 monkeypatch.setattr(Attention, "forward", lambda _, q, k, v: q.contiguous())
608 input_ids = torch.tensor([1, 2, 3, 4], device=DEVICE_TYPE)
609 positions = torch.arange(input_ids.numel(), device=DEVICE_TYPE)
610 with (
611 set_current_vllm_config(vllm_config),
612 set_forward_context(None, vllm_config, num_tokens=input_ids.numel()),
613 ):
614 model(input_ids, positions, None)
615
616
617 @pytest.mark.skipif(
618 not is_quant_method_supported("fp8"),
619 reason="FP8 is not supported on this GPU type.",
620 )
621 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
622 def test_scaled_fp8_quant(dtype) -> None:
623 def quantize_ref(tensor, inv_scale):
624 # The reference implementation that fully aligns to
625 # the kernel being tested.
626 finfo = torch.finfo(current_platform.fp8_dtype())
627 scale = inv_scale.reciprocal()
628 qweight = (tensor.to(torch.float32) * scale).clamp(min=finfo.min, max=finfo.max)
629 qweight = qweight.to(current_platform.fp8_dtype())
630 return qweight
631
632 def per_tensor_dequantize(tensor, inv_scale, dtype):
633 fake_qweight = tensor.to(dtype)
634 dq_weight = fake_qweight * inv_scale
635 return dq_weight
636
637 # Note that we use a shape % 4 != 0 to cover edge cases,
638 # because scaled_fp8_quant is vectorized by 4.
639 x = (torch.randn(size=(11, 11), device=DEVICE_TYPE) * 13).to(dtype)
640
641 # Dynamic quantization
642 ref_y, inv_scale = ops.scaled_fp8_quant(x, None)
643 ref_y = per_tensor_dequantize(ref_y, inv_scale, dtype)
644
645 # Reference dynamic quantization
646 y = quantize_ref(x, inv_scale)
647 torch.testing.assert_close(ref_y, per_tensor_dequantize(y, inv_scale, dtype))
648
649 # Static quantization
650 y, _ = ops.scaled_fp8_quant(x, inv_scale)
651 torch.testing.assert_close(ref_y, per_tensor_dequantize(y, inv_scale, dtype))
652
653 # Padding
654 y, _ = ops.scaled_fp8_quant(x, inv_scale, num_token_padding=17)
655 assert y.shape[0] == 17
656 torch.testing.assert_close(
657 ref_y,
658 per_tensor_dequantize(torch.narrow(y, 0, 0, x.shape[0]), inv_scale, dtype),
659 )
660
661 # non-contiguous input with padding
662 m, n, padded_stride = 975, 512, 576
663 padded_tensor = (torch.randn(size=(m, padded_stride), device=DEVICE_TYPE) * 13).to(
664 dtype
665 )
666 x_nc = padded_tensor[:, :n] # shape (m, n) with stride (padded_stride, 1)
667
668 assert not x_nc.is_contiguous()
669 assert x_nc.stride(0) == padded_stride
670
671 # dynamic quantization
672 ref_y_nc, inv_scale_nc = ops.scaled_fp8_quant(x_nc, None)
673 ref_y_nc = per_tensor_dequantize(ref_y_nc, inv_scale_nc, dtype)
674
675 # reference dynamic quantization
676 y_nc = quantize_ref(x_nc, inv_scale_nc)
677 torch.testing.assert_close(
678 ref_y_nc, per_tensor_dequantize(y_nc, inv_scale_nc, dtype)
679 )
680
681 # static quantization
682 y_nc, _ = ops.scaled_fp8_quant(x_nc, inv_scale_nc)
683 torch.testing.assert_close(
684 ref_y_nc, per_tensor_dequantize(y_nc, inv_scale_nc, dtype)
685 )
686
687 # padding after non-contiguous input quantization
688 y_nc_pad, _ = ops.scaled_fp8_quant(x_nc, inv_scale_nc, num_token_padding=m + 10)
689 assert y_nc_pad.shape[0] == m + 10
690 torch.testing.assert_close(
691 ref_y_nc,
692 per_tensor_dequantize(
693 torch.narrow(y_nc_pad, 0, 0, x_nc.shape[0]), inv_scale_nc, dtype
694 ),
695 )
696
697
698 @pytest.mark.skipif(
699 current_platform.is_fp8_fnuz(),
700 reason="FP8 e4m3fn weight reloading is not supported on e4m3fnuz platforms",
701 )
702 @pytest.mark.parametrize("method_cls", [Fp8LinearMethod, Fp8MoEMethod])
703 # FP8 weight reloading does not support online quantization
704 @pytest.mark.parametrize("weight_block_size", [None, [128, 128]])
705 # any postprocessing that is applied to the weights such as padding and repacking
706 # (excluding device sharding) must also be applied to the reloaded weights
707 #
708 # this is the case for marlin as well as per-tensor Fp8MoEMethod
709 @pytest.mark.parametrize("use_marlin", [False]) # skip True
710 def test_fp8_reloading(
711 default_vllm_config,
712 method_cls,
713 weight_block_size,
714 use_marlin,
715 dist_init,
716 monkeypatch,
717 ):
718 # NOTE(rob): this test fails when using DeepGEMM because the
719 # shapes are invalid. Previously the test was passing because
720 # we set fp8_backend to None, which sidestepped the issue.
721 monkeypatch.setenv("VLLM_USE_DEEP_GEMM", "0")
722
723 if method_cls is Fp8MoEMethod and weight_block_size is None:
724 pytest.skip(
725 "FP8 Tensor weight reloading does not support fusing w13_weight_scale. "
726 "If this is your use case, consider using a restore function like #26327"
727 )
728
729 # Set model config as model_config.dtype is required in Fp8LinearMethod.
730 default_vllm_config.model_config = ModelConfig()
731 default_vllm_config.kernel_config.moe_backend = "triton"
732 layer_size = 128 if weight_block_size is not None else 1
733 with torch.device(f"{DEVICE_TYPE}:0"):
734 config = Fp8Config(
735 weight_block_size=weight_block_size,
736 )
737
738 if method_cls is Fp8LinearMethod:
739 layer = torch.nn.Linear(layer_size, layer_size)
740 method = method_cls(config)
741 method.create_weights(
742 layer=layer,
743 input_size_per_partition=layer_size,
744 output_partition_sizes=[layer_size],
745 input_size=layer_size,
746 output_size=layer_size,
747 params_dtype=torch.bfloat16,
748 weight_loader=default_weight_loader,
749 )
750 method.use_marlin = use_marlin
751
752 else:
753 layer = FusedMoEFactory(
754 num_experts=1,
755 top_k=1,
756 hidden_size=layer_size,
757 intermediate_size=layer_size,
758 )
759 layer = layer.routed_experts
760 method = method_cls(config, layer)
761 method.create_weights(
762 layer=layer,
763 num_experts=1,
764 hidden_size=layer_size,
765 intermediate_size_per_partition=layer_size,
766 params_dtype=torch.bfloat16,
767 weight_loader=default_weight_loader,
768 )
769
770 # capture weights format during loading
771 original_metadata = [
772 (name, param.shape, getattr(param, "weight_loader", default_weight_loader))
773 for name, param in layer.named_parameters()
774 ]
775
776 # test loading
777 for name, shape, _ in original_metadata:
778 param = getattr(layer, name)
779 weight_loader = getattr(param, "weight_loader", default_weight_loader)
780 weight_loader(param, torch.zeros(shape)) # cannot use empty
781
782 method.process_weights_after_loading(layer)
783
784 # test reloading works after loading
785 for name, shape, _ in original_metadata:
786 param = getattr(layer, name)
787 weight_loader = getattr(param, "weight_loader", default_weight_loader)
788 weight_loader(param, torch.zeros(shape)) # cannot use empty
789
790 method.process_weights_after_loading(layer)
791
792
793 def test_kv_cache_scale_sync_to_host_copies():
794 """Test device-to-host sync of the k/v quantization scales, for both the
795 checkpoint-load and runtime-calc paths that produce them.
796 """
797 layer = torch.nn.Module()
798 set_default_quant_scales(layer, register_buffer=True)
799 layer.kv_cache_dtype = "fp8"
800
801 method = BaseKVCacheMethod(quant_config=None)
802 method.create_weights(layer)
803 # 0.3 stays != 1.0 even after the fp8_fnuz x2 rescale.
804 checkpoint_scale = torch.tensor(0.3, dtype=torch.float32)
805 layer.k_scale.weight_loader(layer.k_scale, checkpoint_scale)
806 layer.v_scale.weight_loader(layer.v_scale, checkpoint_scale)
807 method.process_weights_after_loading(layer)
808
809 assert layer._k_scale_float != 1.0
810 assert layer._v_scale_float != 1.0
811 # Host copy must mirror both the float and the device scale tensor.
812 assert layer._k_scale_cpu.item() == pytest.approx(layer._k_scale_float)
813 assert layer._v_scale_cpu.item() == pytest.approx(layer._v_scale_float)
814 assert layer._k_scale_cpu.item() == pytest.approx(layer._k_scale.item())
815 assert layer._v_scale_cpu.item() == pytest.approx(layer._v_scale.item())
816
817
818 @pytest.mark.skipif(
819 not is_quant_method_supported("fp8"),
820 reason="FP8 is not supported on this GPU type.",
821 )
822 def test_kv_cache_dtype_skip_layers(monkeypatch, dist_init, workspace_init):
823 """Test that kv_cache_dtype_skip_layers skips quantization for specified layers."""
824 monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")
825
826 model, _ = load_model_without_vllm_runner(
827 "facebook/opt-125m",
828 vllm_config_kwargs={
829 "cache_config": CacheConfig(
830 cache_dtype="fp8", kv_cache_dtype_skip_layers=["0", "2"]
831 )
832 },
833 )
834 for i, layer in enumerate(model.model.decoder.layers):
835 expected = "auto" if str(i) in ["0", "2"] else "fp8"
836 assert layer.self_attn.attn.kv_cache_dtype == expected