main
py 414 lines 12.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """WARNING: This test runs in both single-node (4 GPUs) and multi-node
4 (2 node with 2 GPUs each) modes. If the test only uses 2 GPUs, it is
5 important to set the distributed backend to "mp" to avoid Ray scheduling
6 all workers in a node other than the head node, which can cause the test
7 to fail.
8 """
9
10 import json
11 import os
12 from dataclasses import dataclass
13 from typing import Literal, NamedTuple
14
15 import pytest
16
17 from vllm.config.compilation import CompilationMode
18 from vllm.config.model import RunnerOption
19 from vllm.logger import init_logger
20 from vllm.platforms import current_platform
21 from vllm.utils.torch_utils import is_torch_equal_or_newer
22
23 from ...models.registry import HF_EXAMPLE_MODELS, _HfExamplesInfo
24 from ...utils import compare_all_settings, create_new_process_for_each_test
25
26 logger = init_logger("test_sequence_parallel")
27
28 VLLM_MULTI_NODE = os.getenv("VLLM_MULTI_NODE", "0") == "1"
29 NVFP4_MODEL_ID = "nvidia/Llama-3.1-8B-Instruct-NVFP4"
30 NVFP4_MODEL_INFO = _HfExamplesInfo(NVFP4_MODEL_ID)
31
32
33 class ParallelSetup(NamedTuple):
34 tp_size: int
35 pp_size: int
36 fuse_norm_quant: bool
37 fuse_act_quant: bool
38 eager_mode: bool
39
40
41 class SPTestOptions(NamedTuple):
42 multi_node_only: bool
43 load_format: str | None = None
44 model_info: _HfExamplesInfo | None = None
45
46
47 @dataclass
48 class SPTestSettings:
49 parallel_setups: list[ParallelSetup]
50 distributed_backends: list[str]
51 runner: RunnerOption
52 test_options: SPTestOptions
53
54 @staticmethod
55 def fast(
56 *,
57 tp_base: int = 2,
58 pp_base: int = 1,
59 runner: RunnerOption = "auto",
60 multi_node_only: bool = False,
61 load_format: str | None = None,
62 ):
63 parallel_setups = [
64 ParallelSetup(
65 tp_size=tp_base,
66 pp_size=pp_multiplier * pp_base,
67 fuse_norm_quant=False,
68 fuse_act_quant=False,
69 eager_mode=False,
70 )
71 for pp_multiplier in [1, 2]
72 ]
73 return SPTestSettings(
74 parallel_setups=parallel_setups,
75 distributed_backends=["mp", "ray"],
76 runner=runner,
77 test_options=SPTestOptions(
78 multi_node_only=multi_node_only, load_format=load_format
79 ),
80 )
81
82 @staticmethod
83 def fp8_quant(
84 *,
85 tp_base: int = 2,
86 pp_base: int = 1,
87 runner: RunnerOption = "auto",
88 multi_node_only: bool = False,
89 load_format: str | None = None,
90 ):
91 parallel_setups = []
92 for fusion_val in [False, True]:
93 parallel_setups.append(
94 ParallelSetup(
95 tp_size=tp_base,
96 pp_size=pp_base,
97 fuse_norm_quant=fusion_val,
98 fuse_act_quant=fusion_val,
99 eager_mode=True,
100 )
101 )
102 return SPTestSettings(
103 parallel_setups=parallel_setups,
104 distributed_backends=["mp", "ray"],
105 runner=runner,
106 test_options=SPTestOptions(
107 multi_node_only=multi_node_only, load_format=load_format
108 ),
109 )
110
111 def iter_params(self, model_id: str):
112 opts = self.test_options
113
114 for parallel_setup in self.parallel_setups:
115 for backend in self.distributed_backends:
116 yield (
117 model_id,
118 parallel_setup,
119 backend,
120 self.runner,
121 opts,
122 )
123
124
125 def _build_sp_args(
126 model_id: str,
127 parallel_setup: ParallelSetup,
128 distributed_backend: str,
129 runner: RunnerOption,
130 test_options: SPTestOptions,
131 num_gpus_available: int,
132 use_inductor_graph_partition: bool,
133 fuse_gemm_comms: bool,
134 enable_prompt_embeds: bool,
135 *,
136 is_multimodal: bool,
137 ) -> tuple[list[str], list[str]] | None:
138 (
139 tp_size,
140 pp_size,
141 fuse_norm_quant,
142 fuse_act_quant,
143 eager_mode,
144 ) = parallel_setup
145
146 multi_node_only = test_options.multi_node_only
147 load_format = test_options.load_format
148
149 model_info = test_options.model_info or HF_EXAMPLE_MODELS.find_hf_info(model_id)
150 model_info.check_transformers_version(on_fail="skip")
151
152 trust_remote_code = model_info.trust_remote_code
153 tokenizer_mode = model_info.tokenizer_mode
154 hf_overrides = dict(model_info.hf_overrides)
155 require_embed_inputs = model_info.require_embed_inputs
156
157 if load_format == "dummy":
158 # Avoid OOM
159 text_overrides = {
160 "num_hidden_layers": 4,
161 "hidden_size": 512,
162 "intermediate_size": 800,
163 "num_attention_heads": 4,
164 "num_key_value_heads": 1,
165 }
166
167 if is_multimodal:
168 hf_overrides.update({"text_config": text_overrides})
169 else:
170 hf_overrides.update(text_overrides)
171 else:
172 model_info.check_available_online(on_fail="skip")
173
174 if num_gpus_available < tp_size * pp_size:
175 return None
176 if VLLM_MULTI_NODE and distributed_backend == "mp":
177 return None
178 if multi_node_only and not VLLM_MULTI_NODE:
179 return None
180
181 common_args = [
182 "--dtype",
183 "bfloat16",
184 "--max-model-len",
185 "2048",
186 "--max-num-seqs",
187 "8",
188 ]
189 if eager_mode:
190 common_args.append("-cc.cudagraph_mode=none")
191 if runner != "auto":
192 common_args.extend(["--runner", runner])
193 if trust_remote_code:
194 common_args.append("--trust-remote-code")
195 if tokenizer_mode:
196 common_args.extend(["--tokenizer-mode", tokenizer_mode])
197 if load_format:
198 common_args.extend(["--load-format", load_format])
199 if hf_overrides:
200 common_args.extend(["--hf-overrides", json.dumps(hf_overrides)])
201 if require_embed_inputs:
202 common_args.extend(
203 [
204 "--skip-tokenizer-init",
205 "--enable-prompt-embeds",
206 "--enable-mm-embeds",
207 ]
208 )
209 elif enable_prompt_embeds:
210 common_args.append("--enable-prompt-embeds")
211
212 compilation_config = {
213 "mode": CompilationMode.VLLM_COMPILE,
214 "compile_sizes": [4, 8],
215 "pass_config": {
216 "enable_sp": True,
217 "fuse_gemm_comms": fuse_gemm_comms,
218 "fuse_norm_quant": fuse_norm_quant,
219 "fuse_act_quant": fuse_act_quant,
220 "fuse_allreduce_rms": False,
221 "eliminate_noops": True,
222 "sp_min_token_num": 0,
223 },
224 "use_inductor_graph_partition": use_inductor_graph_partition,
225 }
226 if not use_inductor_graph_partition:
227 compilation_config["splitting_ops"] = []
228
229 tp_sp_args = [
230 *common_args,
231 "--tensor-parallel-size",
232 str(tp_size),
233 "--pipeline-parallel-size",
234 str(pp_size),
235 "--distributed-executor-backend",
236 distributed_backend,
237 "--compilation_config",
238 json.dumps(compilation_config),
239 ]
240
241 tp_args = [
242 *common_args,
243 "--tensor-parallel-size",
244 str(tp_size),
245 "--distributed-executor-backend",
246 "mp",
247 ]
248
249 return tp_args, tp_sp_args
250
251
252 def _compare_sp_settings(
253 model_id: str,
254 settings: list[tuple[list[str], list[str]]],
255 *,
256 method: Literal["generate", "encode"],
257 ) -> None:
258 if not settings:
259 pytest.skip("No supported sequence-parallel configurations")
260
261 settings_by_tp_args: dict[tuple[str, ...], list[list[str]]] = {}
262 for tp_args, tp_sp_args in settings:
263 settings_by_tp_args.setdefault(tuple(tp_args), []).append(tp_sp_args)
264
265 for tp_args_key, grouped_tp_sp_args in settings_by_tp_args.items():
266 all_args = [list(tp_args_key), *grouped_tp_sp_args]
267 compare_all_settings(
268 model_id,
269 all_args,
270 [None] * len(all_args),
271 method=method,
272 force_v1_runner=True,
273 )
274
275
276 SP_TEXT_GENERATION_MODELS = {
277 # [Decoder-only]
278 "hmellor/tiny-random-LlamaForCausalLM": SPTestSettings.fast(),
279 "RedHatAI/Llama-3.2-1B-Instruct-FP8": SPTestSettings.fp8_quant(),
280 }
281
282
283 @pytest.mark.parametrize(
284 ("model_id", "settings"),
285 [
286 pytest.param(model_id, settings, id=model_id)
287 for model_id, settings in SP_TEXT_GENERATION_MODELS.items()
288 ],
289 )
290 @create_new_process_for_each_test()
291 def test_tp_sp_generation(
292 model_id: str,
293 settings: SPTestSettings,
294 num_gpus_available,
295 ):
296 # Skip FP8 SP-only test on sm89 (compute capability 8.9).
297 # An unknown capability (None) is left to fail in the test body rather than
298 # being silently skipped here.
299 capability = current_platform.get_device_capability()
300 if "fp8" in model_id.lower() and capability is not None and capability < (9, 0):
301 pytest.skip("FP8 reduction support begins with sm90 capable devices.")
302
303 graph_partition_options = [False]
304 if is_torch_equal_or_newer("2.9.0.dev"):
305 graph_partition_options.append(True)
306
307 comparisons = []
308 for _, parallel_setup, backend, runner, test_options in settings.iter_params(
309 model_id
310 ):
311 for use_inductor_graph_partition in graph_partition_options:
312 comparison = _build_sp_args(
313 model_id,
314 parallel_setup,
315 backend,
316 runner,
317 test_options,
318 num_gpus_available,
319 use_inductor_graph_partition,
320 fuse_gemm_comms=False, # TODO: enable async TP
321 enable_prompt_embeds=False,
322 is_multimodal=False,
323 )
324 if comparison is not None:
325 comparisons.append(comparison)
326
327 _compare_sp_settings(model_id, comparisons, method="generate")
328
329
330 # Focused regression test for the SP + prompt_embeds graph-rewrite path.
331 # Covers pp_size=1 (SP only) and pp_size=2 (SP + PP); kept small on purpose so
332 # we don't double the matrix of `test_tp_sp_generation` above.
333 SP_PROMPT_EMBEDS_PARALLEL_SETUPS = [
334 ParallelSetup(
335 tp_size=2,
336 pp_size=pp_size,
337 fuse_norm_quant=False,
338 fuse_act_quant=False,
339 eager_mode=False,
340 )
341 for pp_size in [1, 2]
342 ]
343
344
345 @create_new_process_for_each_test()
346 def test_tp_sp_generation_prompt_embeds(
347 num_gpus_available,
348 ):
349 model_id = "hmellor/tiny-random-LlamaForCausalLM"
350 graph_partition_options = [False]
351 if is_torch_equal_or_newer("2.9.0.dev"):
352 graph_partition_options.append(True)
353
354 comparisons = []
355 for parallel_setup in SP_PROMPT_EMBEDS_PARALLEL_SETUPS:
356 for use_inductor_graph_partition in graph_partition_options:
357 comparison = _build_sp_args(
358 model_id,
359 parallel_setup,
360 distributed_backend="mp",
361 runner="auto",
362 test_options=SPTestOptions(multi_node_only=False, load_format=None),
363 num_gpus_available=num_gpus_available,
364 use_inductor_graph_partition=use_inductor_graph_partition,
365 fuse_gemm_comms=False,
366 enable_prompt_embeds=True,
367 is_multimodal=False,
368 )
369 if comparison is not None:
370 comparisons.append(comparison)
371
372 _compare_sp_settings(model_id, comparisons, method="generate")
373
374
375 @create_new_process_for_each_test()
376 def test_tp_sp_nvfp4_generation(num_gpus_available: int):
377 is_blackwell = current_platform.is_cuda() and (
378 current_platform.is_device_capability_family(100)
379 )
380 is_rocm_cdna = False
381 if current_platform.is_rocm():
382 from vllm.platforms.rocm import on_cdna
383
384 is_rocm_cdna = on_cdna()
385 if not (is_blackwell or is_rocm_cdna):
386 pytest.skip("NVFP4 sequence parallelism requires Blackwell or ROCm CDNA")
387
388 comparison = _build_sp_args(
389 NVFP4_MODEL_ID,
390 ParallelSetup(
391 tp_size=2,
392 pp_size=1,
393 fuse_norm_quant=True,
394 fuse_act_quant=True,
395 eager_mode=True,
396 ),
397 "mp",
398 "auto",
399 SPTestOptions(
400 multi_node_only=False,
401 load_format="dummy",
402 model_info=NVFP4_MODEL_INFO,
403 ),
404 num_gpus_available,
405 use_inductor_graph_partition=False,
406 fuse_gemm_comms=False,
407 enable_prompt_embeds=False,
408 is_multimodal=False,
409 )
410 _compare_sp_settings(
411 NVFP4_MODEL_ID,
412 [] if comparison is None else [comparison],
413 method="generate",
414 )