main
py 228 lines 8.62 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """XPU-only batch-invariance tests for norm, collectives, and sampler behavior."""
4
5 import pytest
6 import torch
7
8 from vllm import envs
9 from vllm.config import CacheConfig, LoadConfig, ModelConfig, VllmConfig
10 from vllm.distributed.device_communicators.xpu_communicator import XpuCommunicator
11 from vllm.platforms import current_platform
12
13 pytestmark = pytest.mark.skipif(
14 not current_platform.is_xpu(), reason="XPU batch-invariance tests"
15 )
16
17
18 @pytest.fixture
19 def tiny_qwen3_moe_path(tmp_path):
20 """Create a local Qwen3-MoE config for dummy-loading tests."""
21 from transformers import Qwen3MoeConfig
22
23 config = Qwen3MoeConfig(
24 architectures=["Qwen3MoeForCausalLM"],
25 vocab_size=256,
26 hidden_size=512,
27 intermediate_size=1024,
28 moe_intermediate_size=512,
29 num_hidden_layers=2,
30 num_attention_heads=4,
31 num_key_value_heads=2,
32 head_dim=128,
33 num_experts=4,
34 num_experts_per_tok=2,
35 max_position_embeddings=512,
36 bos_token_id=1,
37 eos_token_id=2,
38 tie_word_embeddings=False,
39 )
40 config.save_pretrained(tmp_path)
41 return tmp_path
42
43
44 @pytest.mark.parametrize("source", ["argument", "checkpoint"])
45 def test_rejects_weight_quantization(tiny_qwen3_moe_path, source):
46 """Reject both explicit and checkpoint-inferred quantization before loading."""
47 from transformers import Qwen3MoeConfig
48
49 if source == "checkpoint":
50 config = Qwen3MoeConfig.from_pretrained(tiny_qwen3_moe_path)
51 config.quantization_config = {
52 "quant_method": "fp8",
53 "activation_scheme": "dynamic",
54 }
55 config.save_pretrained(tiny_qwen3_moe_path)
56
57 model_config = ModelConfig(
58 model=str(tiny_qwen3_moe_path),
59 skip_tokenizer_init=True,
60 dtype="bfloat16",
61 quantization="fp8" if source == "argument" else None,
62 )
63 assert model_config.quantization == "fp8"
64 with pytest.raises(ValueError, match="supports only unquantized models"):
65 VllmConfig(
66 model_config=model_config,
67 load_config=LoadConfig(load_format="dummy"),
68 )
69
70
71 @pytest.mark.parametrize("cache_dtype", ["fp8", "int8_per_token_head", "nvfp4"])
72 def test_rejects_quantized_kv_cache(cache_dtype):
73 with pytest.raises(ValueError, match="does not support quantized KV caches"):
74 VllmConfig(cache_config=CacheConfig(cache_dtype=cache_dtype))
75
76
77 @pytest.mark.parametrize("cache_dtype", ["auto", "float16", "bfloat16"])
78 def test_accepts_unquantized_kv_cache(cache_dtype):
79 config = VllmConfig(cache_config=CacheConfig(cache_dtype=cache_dtype))
80 assert config.cache_config.cache_dtype == cache_dtype
81
82
83 def test_quantized_kv_cache_allowed_without_batch_invariance(monkeypatch):
84 monkeypatch.setattr(envs, "VLLM_BATCH_INVARIANT", False)
85 config = VllmConfig(cache_config=CacheConfig(cache_dtype="fp8"))
86 assert config.cache_config.cache_dtype == "fp8"
87
88
89 # These dimensions and seeds locally reproduce non-invariant norm mismatches.
90 # They make the invariant equality assertion exercise a discriminating case.
91 @pytest.mark.parametrize(
92 (
93 "norm_name",
94 "batch_size",
95 "hidden_size",
96 "position",
97 "weight_seed",
98 "input_seed",
99 "input_pattern",
100 ),
101 [
102 ("rms", 1024, 16384, 512, 7, 42, "normal"),
103 ("gemma", 257, 8192, 255, 1, 2, "normal"),
104 ("rms", 5, 8192, 3, 7, 46, "scaled"),
105 ("gemma", 5, 8192, 3, 1, 46, "scaled"),
106 ],
107 )
108 def test_residual_norm_preserves_batch_invariance(
109 norm_name,
110 batch_size,
111 hidden_size,
112 position,
113 weight_seed,
114 input_seed,
115 input_pattern,
116 monkeypatch,
117 default_vllm_config,
118 ):
119 """Residual norm output must not change when its row changes batch position."""
120 from vllm.model_executor.determinism.batch_invariant import init_batch_invariance
121 from vllm.model_executor.layers.layernorm import GemmaRMSNorm, RMSNorm
122
123 monkeypatch.setattr(envs, "VLLM_BATCH_INVARIANT", True)
124 init_batch_invariance()
125 norm_cls = RMSNorm if norm_name == "rms" else GemmaRMSNorm
126 norm = norm_cls(hidden_size, eps=1e-6).to(device="xpu", dtype=torch.bfloat16)
127 with torch.no_grad():
128 torch.manual_seed(weight_seed)
129 norm.weight.uniform_(-0.5, 0.5)
130
131 if input_pattern == "scaled":
132 # Heterogeneous columns expose native mean's batch-dependent rounding.
133 generator = torch.Generator(device="cpu").manual_seed(input_seed)
134 scale = torch.randn(hidden_size, generator=generator).exp()
135 x = (torch.randn(batch_size, hidden_size, generator=generator) * scale).to(
136 device="xpu", dtype=torch.bfloat16
137 )
138 residual = (
139 torch.randn(batch_size, hidden_size, generator=generator) * scale
140 ).to(device="xpu", dtype=torch.bfloat16)
141 else:
142 torch.manual_seed(input_seed)
143 x = torch.randn(batch_size, hidden_size, device="xpu", dtype=torch.bfloat16)
144 residual = torch.randn_like(x)
145
146 single_output, single_residual = norm(
147 x[position : position + 1].clone(),
148 residual[position : position + 1].clone(),
149 )
150 batch_output, batch_residual = norm(x.clone(), residual.clone())
151
152 assert torch.equal(single_output, batch_output[position : position + 1])
153 assert torch.equal(single_residual, batch_residual[position : position + 1])
154
155
156 @pytest.mark.parametrize(
157 "operation", ["all_reduce", "reduce_scatter", "reduce_scatterv"]
158 )
159 @pytest.mark.parametrize("rank", range(4))
160 def test_collectives_preserve_rank_order_and_partition(monkeypatch, operation, rank):
161 """Cancellation exposes reordered sums; mock ranks require no extra XPUs."""
162 from vllm.distributed.device_communicators import xpu_communicator
163
164 inputs = [
165 torch.full((8, 3), value, dtype=torch.float32, device="xpu")
166 for value in (1e20, -1e20, 3.0, 0.0)
167 ]
168 inputs[2].add_(torch.arange(24, device="xpu").reshape(8, 3))
169
170 def all_gather_single(output, input_, group):
171 torch.testing.assert_close(input_, inputs[rank].reshape(-1), rtol=0, atol=0)
172 output.copy_(torch.stack(inputs).reshape(-1))
173
174 monkeypatch.setattr(xpu_communicator.dist, "all_gather_single", all_gather_single)
175 communicator = XpuCommunicator.__new__(XpuCommunicator)
176 communicator.world_size = 4
177 communicator.rank_in_group = rank
178 communicator.device_group = object()
179 expected = inputs[0].clone()
180 for value in inputs[1:]:
181 expected.add_(value)
182 assert torch.equal(expected, inputs[2])
183
184 if operation == "all_reduce":
185 actual = communicator.all_reduce(inputs[rank])
186 elif operation == "reduce_scatter":
187 actual = communicator.reduce_scatter(inputs[rank], dim=0)
188 expected = expected.chunk(4)[rank]
189 else:
190 sizes = [1, 3, 2, 2]
191 actual = communicator.reduce_scatterv(inputs[rank], dim=0, sizes=sizes)
192 expected = expected.split(sizes)[rank]
193 torch.testing.assert_close(actual, expected, rtol=0, atol=0)
194
195
196 # This batch size, vocabulary size, and positions locally reproduce an
197 # unseeded non-invariant XPU sampler mismatch.
198 @pytest.mark.parametrize("position", [0, 128, 255])
199 def test_seeded_sampler_preserves_full_distribution_and_rng(position, monkeypatch):
200 """Moving a request must preserve its distribution and per-request RNG."""
201 from vllm.triton_utils import HAS_TRITON
202 from vllm.v1.sample.ops.topk_topp_sampler import TopKTopPSampler
203
204 assert HAS_TRITON, "XPU sampling must exercise the Triton top-k/top-p path"
205 monkeypatch.setattr(envs, "VLLM_XPU_USE_SAMPLER_KERNEL", True)
206 sampler = TopKTopPSampler(logprobs_mode="processed_logprobs")
207 torch.manual_seed(42)
208 logits = torch.randn(256, 8192, device="xpu", dtype=torch.float32)
209 single_rng = torch.Generator(device="xpu").manual_seed(123)
210 batch_rng = torch.Generator(device="xpu").manual_seed(123)
211 k = torch.full((256,), 32, dtype=torch.int32, device="xpu")
212 p = torch.full((256,), 0.9, device="xpu")
213 for _ in range(3):
214 single_token, single_logprobs = sampler(
215 logits[position : position + 1].clone(),
216 {0: single_rng},
217 k[position : position + 1],
218 p[position : position + 1],
219 )
220 batch_tokens, batch_logprobs = sampler(
221 logits.clone(), {position: batch_rng}, k, p
222 )
223 assert single_logprobs is not None and batch_logprobs is not None
224 torch.testing.assert_close(
225 single_logprobs[0], batch_logprobs[position], rtol=0, atol=0
226 )
227 assert single_token.item() == batch_tokens[position].item()
228 assert torch.equal(single_rng.get_state(), batch_rng.get_state())