main
py 1,064 lines 39.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Code inside this file can safely assume cuda platform, e.g. importing
4 pynvml. However, it should not initialize cuda context.
5 """
6
7 from __future__ import annotations
8
9 import contextlib
10 import os
11 import platform
12 from collections.abc import Callable
13 from datetime import timedelta
14 from functools import cache, lru_cache, wraps
15 from typing import TYPE_CHECKING, NamedTuple, TypeVar
16
17 import torch
18 from torch.distributed import PrefixStore, ProcessGroup
19 from torch.distributed.distributed_c10d import is_nccl_available
20 from typing_extensions import ParamSpec
21
22 # import custom ops, trigger op registration
23 import vllm._C_stable_libtorch # noqa
24
25 with contextlib.suppress(ImportError):
26 import vllm._qutlass_C # noqa
27 import vllm.envs as envs
28 from vllm.logger import init_logger
29 from vllm.utils.import_utils import import_pynvml
30 from vllm.v1.attention.backends.registry import AttentionBackendEnum
31
32 from .interface import DeviceCapability, Platform, PlatformEnum, in_wsl
33
34 if TYPE_CHECKING:
35 from vllm.config import VllmConfig
36 from vllm.config.cache import CacheDType
37 from vllm.config.kernel import IrOpPriorityConfig
38 from vllm.v1.attention.backend import AttentionBackend
39 from vllm.v1.attention.selector import AttentionSelectorConfig
40 else:
41 VllmConfig = None
42 CacheDType = None
43
44 logger = init_logger(__name__)
45
46 _P = ParamSpec("_P")
47 _R = TypeVar("_R")
48
49 pynvml = import_pynvml()
50
51 # pytorch 2.5 uses cudnn sdpa by default, which will cause crash on some models
52 # see https://github.com/huggingface/diffusers/issues/9704 for details
53 torch.backends.cuda.enable_cudnn_sdp(False)
54
55
56 @lru_cache(maxsize=8)
57 def _cuda_device_count_stateless(cuda_visible_devices: str | None = None) -> int:
58 """Get number of CUDA devices, caching based on the value of CUDA_VISIBLE_DEVICES
59 at the time of call.
60
61 This should be used instead of torch.accelerator.device_count() unless
62 CUDA_VISIBLE_DEVICES has already been set to the desired value.
63
64 # This can be removed and simply replaced with torch.cuda.get_device_count
65 # after https://github.com/pytorch/pytorch/pull/122815 is released."""
66 # Note: cuda_visible_devices is not used, but we keep it as an argument for
67 # LRU Cache purposes.
68
69 # Code below is based on
70 # https://github.com/pytorch/pytorch/blob/
71 # c1cd946818442aca8c7f812b16d187ce1586c3bc/
72 # torch/cuda/__init__.py#L831C1-L831C17
73 import torch.cuda
74
75 if not torch.cuda._is_compiled():
76 return 0
77 raw_count = torch.cuda._device_count_nvml()
78 r = torch._C._cuda_getDeviceCount() if raw_count < 0 else raw_count
79 return r
80
81
82 @cache
83 def _get_backend_priorities(
84 use_mla: bool,
85 device_capability: DeviceCapability,
86 num_heads: int | None = None,
87 kv_cache_dtype: CacheDType | None = None,
88 use_non_causal: bool = False,
89 head_size: int | None = None,
90 use_mm_prefix: bool = False,
91 ) -> list[AttentionBackendEnum]:
92 """Get backend priorities with lazy import to avoid circular dependency."""
93 from vllm.utils.torch_utils import is_quantized_kv_cache
94
95 if use_mla:
96 if device_capability.major == 10:
97 # Sparse MLA backend priorities
98 # See https://github.com/vllm-project/vllm/issues/35807 for
99 # benchmark results
100 if kv_cache_dtype is not None and is_quantized_kv_cache(kv_cache_dtype):
101 # Prefer FlashInfer for fp8 kv cache
102 sparse_backends = [
103 AttentionBackendEnum.FLASHINFER_MLA_SPARSE,
104 AttentionBackendEnum.FLASHMLA_SPARSE,
105 ]
106 else:
107 # BF16 KV Cache
108 # Prefer FlashInfer at low head counts (FlashMLA uses padding)
109 if num_heads is not None and num_heads <= 16:
110 sparse_backends = [
111 AttentionBackendEnum.FLASHINFER_MLA_SPARSE,
112 AttentionBackendEnum.FLASHMLA_SPARSE,
113 ]
114 else:
115 sparse_backends = [
116 AttentionBackendEnum.FLASHMLA_SPARSE,
117 AttentionBackendEnum.FLASHINFER_MLA_SPARSE,
118 ]
119
120 return [
121 AttentionBackendEnum.FLASHINFER_MLA,
122 # R1 dims + FP8 KV only; rejected by supports_combination
123 # otherwise. Behind FLASHINFER_MLA: wins past bs≈8, regresses
124 # at bs≤2.
125 AttentionBackendEnum.TOKENSPEED_MLA,
126 AttentionBackendEnum.CUTLASS_MLA,
127 AttentionBackendEnum.FLASH_ATTN_MLA,
128 AttentionBackendEnum.FLASHMLA,
129 AttentionBackendEnum.TRITON_MLA,
130 *sparse_backends,
131 ]
132 elif device_capability.major == 12:
133 return [
134 AttentionBackendEnum.TRITON_MLA,
135 AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM120,
136 ]
137 else:
138 sparse_tail = [
139 AttentionBackendEnum.FLASH_ATTN_MLA_SPARSE,
140 AttentionBackendEnum.FLASHMLA_SPARSE,
141 ]
142 flashinfer_sparse = AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM90
143 if head_size == 512:
144 sparse_tail.insert(0, flashinfer_sparse)
145 else:
146 sparse_tail.append(flashinfer_sparse)
147 return [
148 AttentionBackendEnum.FLASH_ATTN_MLA,
149 AttentionBackendEnum.FLASHMLA,
150 AttentionBackendEnum.FLASHINFER_MLA,
151 AttentionBackendEnum.TRITON_MLA,
152 *sparse_tail,
153 ]
154 else:
155 # SM100f defaults to FlashInfer for TRTLLM causal attention, but its non-causal
156 # cutlass path (used for dflash attention) is known to have problems.
157 # So prefer FlashAttention when non-causal on SM100f.
158 if device_capability.major == 10 and not use_non_causal:
159 return [
160 *([AttentionBackendEnum.TRITON_FLASHINFER] if use_mm_prefix else []),
161 AttentionBackendEnum.FLASHINFER,
162 AttentionBackendEnum.FLASH_ATTN,
163 AttentionBackendEnum.TRITON_ATTN,
164 AttentionBackendEnum.FLEX_ATTENTION,
165 AttentionBackendEnum.TURBOQUANT,
166 ]
167 else:
168 return [
169 *(
170 [AttentionBackendEnum.TRITON_FLASH_ATTN]
171 if device_capability.major == 9 and use_mm_prefix
172 else []
173 ),
174 AttentionBackendEnum.FLASH_ATTN,
175 AttentionBackendEnum.FLASHINFER,
176 AttentionBackendEnum.TRITON_ATTN,
177 AttentionBackendEnum.FLEX_ATTENTION,
178 AttentionBackendEnum.TURBOQUANT,
179 ]
180
181
182 def _backend_cls_path(backend_cls: type[AttentionBackend]) -> str:
183 module, qualname = backend_cls.full_cls_name()
184 return f"{module}.{qualname}"
185
186
187 def _get_attn_backend_class(backend: AttentionBackendEnum) -> type[AttentionBackend]:
188 return backend.get_class()
189
190
191 class _BackendCandidate(NamedTuple):
192 backend_class: type[AttentionBackend]
193 backend: AttentionBackendEnum
194 priority: int
195
196
197 def with_nvml_context(fn: Callable[_P, _R]) -> Callable[_P, _R]:
198 @wraps(fn)
199 def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
200 pynvml.nvmlInit()
201 try:
202 return fn(*args, **kwargs)
203 finally:
204 pynvml.nvmlShutdown()
205
206 return wrapper
207
208
209 @cache
210 def _get_wsl_kernel_version() -> tuple[int, ...] | None:
211 """Return the WSL2 kernel version as a tuple, or None on parse failure.
212
213 platform.uname().release on WSL2 looks like
214 "5.15.167.4-microsoft-standard-WSL2"; we take the numeric prefix.
215 """
216 try:
217 release = platform.uname().release
218 parts = release.split("-")[0].split(".")
219 return tuple(int(x) for x in parts[:3])
220 except Exception:
221 return None
222
223
224 class CudaPlatformBase(Platform):
225 _enum = PlatformEnum.CUDA
226 device_name: str = "cuda"
227 device_type: str = "cuda"
228 dispatch_key: str = "CUDA"
229 ray_device_key: str = "GPU"
230 dist_backend: str = "nccl"
231 device_control_env_var: str = "CUDA_VISIBLE_DEVICES"
232 ray_noset_device_env_vars: list[str] = [
233 "RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES",
234 ]
235
236 @classmethod
237 def import_kernels(cls) -> None:
238 """Import CUDA kernel extensions (_C_stable_libtorch, optional _qutlass_C)."""
239 try:
240 import vllm._C_stable_libtorch # noqa: F401
241 except ImportError as e:
242 logger.warning_once(
243 "Failed to import from vllm._C_stable_libtorch: %s", repr(e)
244 )
245 with contextlib.suppress(ImportError):
246 import vllm._moe_C_stable_libtorch # noqa: F401
247 with contextlib.suppress(ImportError):
248 import vllm._qutlass_C # noqa: F401
249
250 @classmethod
251 def check_runner_kv_caches_multi_layer(cls) -> None:
252 pass
253
254 @property
255 def supported_dtypes(self) -> list[torch.dtype]:
256 if self.has_device_capability(80):
257 # Ampere and Hopper or later NVIDIA GPUs.
258 return [torch.bfloat16, torch.float16, torch.float32]
259 if self.has_device_capability(60):
260 # Pascal, Volta and Turing NVIDIA GPUs, BF16 is not supported
261 return [torch.float16, torch.float32]
262 # Kepler and Maxwell NVIDIA GPUs, only FP32 is supported,
263 # though vLLM doesn't support these GPUs.
264 return [torch.float32]
265
266 @classmethod
267 def set_device(cls, device: torch.device) -> None:
268 """Set the device for the current platform."""
269 torch.cuda.set_device(device)
270 # With this trick we can force the device to be set eagerly
271 # see https://github.com/pytorch/pytorch/issues/155668
272 # for why and when it is needed
273 _ = torch.zeros(1, device=device)
274
275 @classmethod
276 def manual_seed_all(cls, seed: int) -> None:
277 torch.cuda.manual_seed_all(seed)
278
279 @classmethod
280 def get_device_capability(cls, device_id: int = 0) -> DeviceCapability | None:
281 raise NotImplementedError
282
283 @classmethod
284 def get_cuda_runtime_major(cls) -> int:
285 """Major ``torch.version.cuda`` version, or ``0`` if undetermined."""
286 major = (torch.version.cuda or "0").split(".", 1)[0]
287 return int(major) if major.isdigit() else 0
288
289 @classmethod
290 def get_device_name(cls, device_id: int = 0) -> str:
291 raise NotImplementedError
292
293 @classmethod
294 def get_device_total_memory(cls, device_id: int = 0) -> int:
295 raise NotImplementedError
296
297 @classmethod
298 def is_fully_connected(cls, device_ids: list[int]) -> bool:
299 raise NotImplementedError
300
301 @classmethod
302 def is_pin_memory_available(cls) -> bool:
303 if in_wsl():
304 # WSL1 has no CUDA support, so being on the CUDA platform under
305 # WSL implies WSL2. Gate on kernel >= 4.19.121, the first WSL2
306 # kernel with limited pinned memory support for CUDA.
307 version = _get_wsl_kernel_version()
308 if version is None or version < (4, 19, 121):
309 # warning_once() causes a circular import on WSL, see #48397.
310 logger.warning(
311 "Using 'pin_memory=False' as WSL is detected and the "
312 "WSL2 kernel version is below 4.19.121. This may slow "
313 "down performance. Please run `wsl --update`."
314 )
315 return False
316 # On compatible WSL2 kernels, pinned memory is supported but
317 # disabled by default. Enable it via VLLM_WSL2_ENABLE_PIN_MEMORY=1.
318 import vllm.envs as envs
319
320 return envs.VLLM_WSL2_ENABLE_PIN_MEMORY
321 return True
322
323 @classmethod
324 def check_and_update_config(cls, vllm_config: VllmConfig) -> None:
325 parallel_config = vllm_config.parallel_config
326 model_config = vllm_config.model_config
327
328 if parallel_config.worker_cls == "auto":
329 parallel_config.worker_cls = "vllm.v1.worker.gpu_worker.Worker"
330
331 scheduler_config = vllm_config.scheduler_config
332 # Note: model_config may be None during testing
333 if (
334 model_config is not None
335 and model_config.is_mm_prefix_lm
336 and scheduler_config.is_multimodal_model
337 and not scheduler_config.disable_chunked_mm_input
338 ):
339 logger.warning_once(
340 "Forcing --disable_chunked_mm_input for models "
341 "with multimodal-bidirectional attention."
342 )
343 scheduler_config.disable_chunked_mm_input = True
344
345 if (
346 in_wsl()
347 and vllm_config.offload_config.uva.cpu_offload_gb > 0
348 and bool(vllm_config.compilation_config.cudagraph_mode)
349 ):
350 logger.warning_once(
351 "--cpu-offload-gb is enabled with CUDA graphs on WSL2. "
352 "This combination requires pinned (page-locked) memory "
353 "allocations. WARNING: Windows (WDDM) enforces a hard "
354 "system-wide cap of roughly 50%% of physical RAM on pinned "
355 "memory shared across ALL processes by default (limit can "
356 "changed via %%USERPROFILE%%\\.wslconfig). "
357 "Excessive use of page-locked memory can prevent Windows "
358 "from reclaiming memory under load, which can cause the "
359 "entire host OS to become unresponsive and may require a "
360 "hard reboot to recover. Proceed at your own risk. "
361 "To raise the WSL2 VM memory ceiling, increase the `memory` "
362 "setting in %%USERPROFILE%%\\.wslconfig and run "
363 "`wsl --shutdown`."
364 )
365
366 @classmethod
367 def get_current_memory_usage(
368 cls, device: torch.types.Device | None = None
369 ) -> float:
370 torch.cuda.empty_cache()
371 torch.cuda.reset_peak_memory_stats(device)
372 return torch.cuda.max_memory_allocated(device)
373
374 @classmethod
375 def get_valid_backends(
376 cls,
377 device_capability: DeviceCapability,
378 attn_selector_config: AttentionSelectorConfig,
379 num_heads: int | None = None,
380 ) -> tuple[
381 list[_BackendCandidate],
382 dict[AttentionBackendEnum, tuple[int, list[str]]],
383 ]:
384 valid_backends_priorities = []
385 invalid_reasons: dict[AttentionBackendEnum, tuple[int, list[str]]] = {}
386
387 backend_priorities = _get_backend_priorities(
388 use_mla=attn_selector_config.use_mla,
389 device_capability=device_capability,
390 num_heads=num_heads,
391 kv_cache_dtype=attn_selector_config.kv_cache_dtype,
392 use_non_causal=attn_selector_config.use_non_causal,
393 head_size=attn_selector_config.head_size,
394 use_mm_prefix=attn_selector_config.use_mm_prefix,
395 )
396 for priority, backend in enumerate(backend_priorities):
397 try:
398 backend_class = _get_attn_backend_class(backend)
399 invalid_reasons_i = backend_class.validate_configuration(
400 device_capability=device_capability,
401 **attn_selector_config._asdict(),
402 )
403 except (ImportError, OSError) as e:
404 logger.debug(
405 "Attention backend %s is unavailable", backend.name, exc_info=True
406 )
407 invalid_reasons_i = [f"{type(e).__name__}: {e}"]
408 if invalid_reasons_i:
409 invalid_reasons[backend] = (priority, invalid_reasons_i)
410 else:
411 valid_backends_priorities.append(
412 _BackendCandidate(backend_class, backend, priority)
413 )
414
415 return valid_backends_priorities, invalid_reasons
416
417 @classmethod
418 def _get_indexer_block_alignment(cls, vllm_config: VllmConfig) -> int | None:
419 index_kpool = getattr(
420 vllm_config.model_config.hf_text_config, "index_kpool", None
421 )
422 if not index_kpool or index_kpool <= 1:
423 return None
424 from vllm.utils.deep_gemm import PAGED_MQA_PAGE_SIZES
425
426 # kpool paged-MQA indexer: the storage block (block_size /
427 # index_kpool) is virtually split into pool pages, so block_size
428 # must be a multiple of index_kpool times a legal pool page.
429 page = min(PAGED_MQA_PAGE_SIZES)
430 if cls.is_device_capability_family(120):
431 # On sm120 the DeepGEMM paged-MQA kernel only accepts block_kv
432 # 64 for the fp8 indexer cache, so align to the largest pool
433 # page here to make the page split land on 64 not the min 32.
434 page = max(PAGED_MQA_PAGE_SIZES)
435 return index_kpool * page
436
437 @classmethod
438 def get_attn_backend_cls(
439 cls,
440 selected_backend: AttentionBackendEnum | None,
441 attn_selector_config: AttentionSelectorConfig,
442 num_heads: int | None = None,
443 ) -> str:
444 device_capability = cls.get_device_capability()
445 assert device_capability is not None
446
447 # First try checking just the selected backend, if there is one.
448 if selected_backend is not None:
449 try:
450 backend_class = _get_attn_backend_class(selected_backend)
451 invalid_reasons = backend_class.validate_configuration(
452 device_capability=device_capability,
453 **attn_selector_config._asdict(),
454 )
455 except (ImportError, OSError) as e:
456 raise ValueError(
457 f"Selected backend {selected_backend} is not valid for "
458 f"this configuration. Reason: [{type(e).__name__}: {e}]"
459 ) from e
460 if invalid_reasons:
461 raise ValueError(
462 f"Selected backend {selected_backend} is not valid for "
463 f"this configuration. Reason: {invalid_reasons}"
464 )
465 else:
466 logger.info("Using %s backend.", selected_backend)
467 return _backend_cls_path(backend_class)
468
469 # No selected backend or the selected backend is invalid,
470 # so we try finding a valid backend.
471 valid_backends_priorities, all_invalid_reasons = cls.get_valid_backends(
472 device_capability=device_capability,
473 attn_selector_config=attn_selector_config,
474 num_heads=num_heads,
475 )
476 reasons_str = (
477 "{"
478 + ", ".join(
479 f"{backend.name}: [{', '.join(reasons)}]"
480 for backend, (_, reasons) in all_invalid_reasons.items()
481 )
482 + "}"
483 )
484 config_str = attn_selector_config.__repr__()
485 logger.debug_once(
486 f"Some attention backends are not valid for {cls.device_name} with "
487 f"{config_str}. Reasons: {reasons_str}."
488 )
489 if len(valid_backends_priorities) == 0:
490 raise ValueError(
491 f"No valid attention backend found for {cls.device_name} "
492 f"with {config_str}. Reasons: {reasons_str}."
493 )
494
495 # We have found some valid backends. Select the one with the
496 # highest priority.
497 selected_candidate = min(
498 valid_backends_priorities,
499 key=lambda candidate: candidate.priority,
500 )
501 selected_backend_class = selected_candidate.backend_class
502 selected_backend = selected_candidate.backend
503 selected_priority = selected_candidate.priority
504
505 # If the user specified --block-size (but not --attention-backend),
506 # check whether that constraint precluded any higher-priority backends.
507 if attn_selector_config.block_size is not None:
508 excluded = [
509 backend
510 for backend, (priority, reasons) in all_invalid_reasons.items()
511 if priority < selected_priority
512 and reasons == ["block_size not supported"]
513 ]
514 if excluded:
515 names = ", ".join(b.name for b in excluded)
516 logger.warning(
517 "--block-size %d precluded higher-priority backend(s) "
518 "%s. Using %s instead, which may result in reduced "
519 "performance. Consider removing --block-size to "
520 "auto-select the optimal block size.",
521 attn_selector_config.block_size,
522 names,
523 selected_backend.name,
524 )
525
526 logger.info_once(
527 "Using %s attention backend out of potential backends: %s.",
528 selected_backend.name,
529 "["
530 + ", ".join(
531 f"'{candidate.backend.name}'" for candidate in valid_backends_priorities
532 )
533 + "]",
534 )
535
536 return _backend_cls_path(selected_backend_class)
537
538 @classmethod
539 def get_supported_vit_attn_backends(cls) -> list[AttentionBackendEnum]:
540 if cls.has_device_capability(80):
541 return [
542 AttentionBackendEnum.FLASH_ATTN,
543 AttentionBackendEnum.TRITON_ATTN,
544 AttentionBackendEnum.TORCH_SDPA,
545 AttentionBackendEnum.FLASHINFER,
546 ]
547 else:
548 return [
549 AttentionBackendEnum.FLASH_ATTN,
550 AttentionBackendEnum.TORCH_SDPA,
551 AttentionBackendEnum.TRITON_ATTN,
552 AttentionBackendEnum.FLASHINFER,
553 ]
554
555 @classmethod
556 def get_vit_attn_backend(
557 cls,
558 head_size: int,
559 dtype: torch.dtype,
560 backend: AttentionBackendEnum | None = None,
561 ) -> AttentionBackendEnum:
562 if backend is not None:
563 assert backend in cls.get_supported_vit_attn_backends(), (
564 f"Backend {backend} is not supported for vit attention. "
565 f"Supported backends are: {cls.get_supported_vit_attn_backends()}"
566 )
567 logger.info_once(f"Using backend {backend} for vit attention")
568 return backend
569
570 cc = cls.get_device_capability()
571 for vit_attn_backend in cls.get_supported_vit_attn_backends():
572 if vit_attn_backend == AttentionBackendEnum.TORCH_SDPA:
573 return vit_attn_backend
574 try:
575 backend_class = vit_attn_backend.get_class()
576 is_backend_supported = backend_class.supports_head_size(
577 head_size
578 ) and backend_class.supports_dtype(dtype)
579 if cc is not None:
580 is_backend_supported = (
581 is_backend_supported
582 and backend_class.supports_compute_capability(cc)
583 )
584 if is_backend_supported:
585 logger.info_once(
586 f"Using backend {vit_attn_backend} for vit attention",
587 )
588 return vit_attn_backend
589 except ImportError:
590 pass
591
592 return AttentionBackendEnum.TORCH_SDPA
593
594 @classmethod
595 def get_punica_wrapper(cls) -> str:
596 return "vllm.lora.punica_wrapper.punica_gpu.PunicaWrapperGPU"
597
598 @classmethod
599 def get_device_communicator_cls(cls) -> str:
600 return (
601 "vllm.distributed.device_communicators.cuda_communicator.CudaCommunicator" # noqa
602 )
603
604 @classmethod
605 def supports_fp8(cls) -> bool:
606 return cls.has_device_capability(89)
607
608 @classmethod
609 def use_custom_allreduce(cls) -> bool:
610 return True
611
612 @classmethod
613 def opaque_attention_op(cls) -> bool:
614 return True
615
616 @classmethod
617 def get_static_graph_wrapper_cls(cls) -> str:
618 return "vllm.compilation.cuda_graph.CUDAGraphWrapper"
619
620 @classmethod
621 def stateless_init_device_torch_dist_pg(
622 cls,
623 backend: str,
624 prefix_store: PrefixStore,
625 group_rank: int,
626 group_size: int,
627 timeout: timedelta,
628 ) -> ProcessGroup:
629 assert is_nccl_available()
630 pg: ProcessGroup = ProcessGroup(
631 prefix_store,
632 group_rank,
633 group_size,
634 )
635 from torch.distributed.distributed_c10d import ProcessGroupNCCL
636
637 backend_options = ProcessGroupNCCL.Options()
638 backend_options._timeout = timeout
639
640 backend_class = ProcessGroupNCCL(
641 prefix_store, group_rank, group_size, backend_options
642 )
643 backend_type = ProcessGroup.BackendType.NCCL
644 device = torch.device("cuda")
645 pg._set_default_backend(backend_type)
646 backend_class._set_sequence_number_for_group()
647
648 pg._register_backend(device, backend_type, backend_class)
649 return pg
650
651 @classmethod
652 def device_count(cls) -> int:
653 return _cuda_device_count_stateless(envs.CUDA_VISIBLE_DEVICES)
654
655 @classmethod
656 def check_if_supports_dtype(cls, dtype: torch.dtype):
657 if dtype == torch.bfloat16: # noqa: SIM102
658 if not cls.has_device_capability(80):
659 capability = cls.get_device_capability()
660 gpu_name = cls.get_device_name()
661
662 if capability is None:
663 compute_str = "does not have a compute capability"
664 else:
665 version_str = capability.as_version_str()
666 compute_str = f"has compute capability {version_str}"
667
668 raise ValueError(
669 "Bfloat16 is only supported on GPUs "
670 "with compute capability of at least 8.0. "
671 f"Your {gpu_name} GPU {compute_str}. "
672 "You can use float16 instead by explicitly setting the "
673 "`dtype` flag in CLI, for example: --dtype=half."
674 )
675
676 @classmethod
677 def insert_blocks_to_device(
678 cls,
679 src_cache: torch.Tensor,
680 dst_cache: torch.Tensor,
681 src_block_indices: torch.Tensor,
682 dst_block_indices: torch.Tensor,
683 ) -> None:
684 """Copy blocks from src_cache to dst_cache on GPU."""
685 _src_cache = src_cache[src_block_indices]
686 dst_cache[dst_block_indices] = _src_cache.to(dst_cache.device)
687
688 @classmethod
689 def swap_out_blocks_to_host(
690 cls,
691 src_cache: torch.Tensor,
692 dst_cache: torch.Tensor,
693 src_block_indices: torch.Tensor,
694 dst_block_indices: torch.Tensor,
695 ) -> None:
696 """Copy blocks from GPU to host (CPU)."""
697 _src_cache = src_cache[src_block_indices]
698 dst_cache[dst_block_indices] = _src_cache.cpu()
699
700 @classmethod
701 def support_hybrid_kv_cache(cls) -> bool:
702 return True
703
704 @classmethod
705 def support_static_graph_mode(cls) -> bool:
706 return True
707
708 @classmethod
709 def support_deep_gemm(cls) -> bool:
710 """Currently, only Hopper and Blackwell GPUs are supported."""
711 return (
712 cls.is_device_capability(90)
713 or cls.is_device_capability_family(100)
714 or cls.is_device_capability_family(120)
715 )
716
717 @classmethod
718 def is_integrated_gpu(cls, device_id: int = 0) -> bool:
719 return bool(torch.cuda.get_device_properties(device_id).is_integrated)
720
721 @classmethod
722 def num_compute_units(cls, device_id: int = 0) -> int:
723 return torch.cuda.get_device_properties(device_id).multi_processor_count
724
725 @classmethod
726 def use_custom_op_collectives(cls) -> bool:
727 return True
728
729 @classmethod
730 def get_default_ir_op_priority(cls, vllm_config: VllmConfig) -> IrOpPriorityConfig:
731 from vllm.config.compilation import CompilationMode
732 from vllm.config.kernel import IrOpPriorityConfig
733
734 # Native used by default when compiling,
735 # use vllm_c kernels where available when no codegen
736 cc = vllm_config.compilation_config
737 using_inductor = cc.backend == "inductor" and cc.mode != CompilationMode.NONE
738 default = ["native"] if using_inductor else ["vllm_c", "native"]
739
740 # Use oink if enabled for rms_norm
741 # TODO(Laurawly/luka): remove this env var,
742 # users can just use IR op priority directly
743 rms_norm = default
744 if envs.VLLM_USE_OINK_OPS:
745 rms_norm = ["oink"] + default
746
747 return IrOpPriorityConfig.with_default(
748 default,
749 rms_norm=rms_norm,
750 fused_add_rms_norm=rms_norm,
751 gelu_and_mul_sparse=["triton", "native"],
752 )
753
754 @classmethod
755 def is_arch_support_pdl(cls) -> bool:
756 try:
757 device = torch.cuda.current_device()
758 major, _ = torch.cuda.get_device_capability(device)
759 except Exception:
760 return False
761 return major >= 9
762
763
764 # NVML utils
765 # Note that NVML is not affected by `CUDA_VISIBLE_DEVICES`,
766 # all the related functions work on real physical device ids.
767 # the major benefit of using NVML is that it will not initialize CUDA
768 class NvmlCudaPlatform(CudaPlatformBase):
769 @classmethod
770 @with_nvml_context
771 def device_control_id_to_physical_device_id(cls, device_id: str) -> int:
772 try:
773 return int(device_id)
774 except ValueError:
775 handle = pynvml.nvmlDeviceGetHandleByUUID(device_id)
776 return pynvml.nvmlDeviceGetIndex(handle)
777
778 @classmethod
779 @cache
780 @with_nvml_context
781 def get_device_capability(cls, device_id: int = 0) -> DeviceCapability | None:
782 try:
783 physical_device_id = cls.visible_device_id_to_physical_device_id(device_id)
784 handle = pynvml.nvmlDeviceGetHandleByIndex(physical_device_id)
785 major, minor = pynvml.nvmlDeviceGetCudaComputeCapability(handle)
786 return DeviceCapability(major=major, minor=minor)
787 except RuntimeError:
788 return None
789
790 @classmethod
791 def has_device_capability(
792 cls,
793 capability: tuple[int, int] | int,
794 device_id: int = 0,
795 ) -> bool:
796 # No @with_nvml_context here: the base implementation only reads
797 # get_device_capability(), which is cached and brings its own NVML
798 # context. Wrapping this method as well cost an nvmlInit()/
799 # nvmlShutdown() pair on every call, including calls made per attention
800 # layer per step from the Triton reshape-and-cache path.
801 try:
802 return super().has_device_capability(capability, device_id)
803 except RuntimeError:
804 return False
805
806 @classmethod
807 @with_nvml_context
808 def get_device_name(cls, device_id: int = 0) -> str:
809 physical_device_id = cls.device_id_to_physical_device_id(device_id)
810 return cls._get_physical_device_name(physical_device_id)
811
812 @classmethod
813 @with_nvml_context
814 def get_device_uuid(cls, device_id: int = 0) -> str:
815 physical_device_id = cls.device_id_to_physical_device_id(device_id)
816 handle = pynvml.nvmlDeviceGetHandleByIndex(physical_device_id)
817 return pynvml.nvmlDeviceGetUUID(handle)
818
819 @classmethod
820 @with_nvml_context
821 def get_device_total_memory(cls, device_id: int = 0) -> int:
822 physical_device_id = cls.device_id_to_physical_device_id(device_id)
823 handle = pynvml.nvmlDeviceGetHandleByIndex(physical_device_id)
824 return int(pynvml.nvmlDeviceGetMemoryInfo(handle).total)
825
826 @classmethod
827 @with_nvml_context
828 def is_fully_connected(cls, physical_device_ids: list[int]) -> bool:
829 """Query if the set of gpus are fully connected by nvlink (1 hop)."""
830 handles = [pynvml.nvmlDeviceGetHandleByIndex(i) for i in physical_device_ids]
831 for i, handle in enumerate(handles):
832 for j, peer_handle in enumerate(handles):
833 if i < j:
834 try:
835 p2p_status = pynvml.nvmlDeviceGetP2PStatus(
836 handle,
837 peer_handle,
838 pynvml.NVML_P2P_CAPS_INDEX_NVLINK,
839 )
840 if p2p_status != pynvml.NVML_P2P_STATUS_OK:
841 return False
842 except pynvml.NVMLError:
843 logger.exception(
844 "NVLink detection failed. This is normal if"
845 " your machine has no NVLink equipped."
846 )
847 return False
848 return True
849
850 @classmethod
851 def _get_physical_device_name(cls, device_id: int = 0) -> str:
852 handle = pynvml.nvmlDeviceGetHandleByIndex(device_id)
853 return pynvml.nvmlDeviceGetName(handle)
854
855 @classmethod
856 @with_nvml_context
857 def get_device_numa_node(cls, device_id: int = 0) -> int | None:
858 """Get the NUMA node ID for a GPU device."""
859 physical_device_id = cls.device_id_to_physical_device_id(device_id)
860 handle = pynvml.nvmlDeviceGetHandleByIndex(physical_device_id)
861
862 try:
863 numa_node = pynvml.nvmlDeviceGetNumaNodeId(handle)
864 if cls._numa_node_has_cpus(numa_node):
865 return numa_node
866 # On non-CDMM Grace-Blackwell systems (e.g. GB200), each GPU's HBM
867 # is a separate NUMA node with no CPUs. Fall through to
868 # CPU-affinity-based detection to find the nearest CPU node.
869 logger.debug(
870 "NUMA node %d for GPU %d has no CPUs (non-CDMM topology), "
871 "falling back to CPU-affinity-based detection",
872 numa_node,
873 device_id,
874 )
875 except Exception:
876 pass
877
878 try:
879 cpu_ids = cls._get_device_cpu_affinity(handle)
880 if cpu_ids:
881 numa_node = cls._get_numa_node_for_cpu(cpu_ids[0])
882 if numa_node is not None:
883 logger.debug(
884 "Determined NUMA node %d for GPU %d via CPU affinity",
885 numa_node,
886 device_id,
887 )
888 return numa_node
889 except Exception as e:
890 logger.warning("Failed to get NUMA node for GPU %d: %s", device_id, e)
891
892 return None
893
894 @classmethod
895 def _numa_node_has_cpus(cls, node_id: int) -> bool:
896 """Check whether a NUMA node has any CPUs assigned to it."""
897 from pathlib import Path
898
899 cpulist_file = Path(f"/sys/devices/system/node/node{node_id}/cpulist")
900 try:
901 return cpulist_file.read_text().strip() != ""
902 except (OSError, ValueError):
903 return False
904
905 @classmethod
906 def _get_device_cpu_affinity(cls, handle) -> list[int]:
907 """Get the list of CPU IDs associated with a GPU via NVML."""
908 cpu_count = os.cpu_count()
909 if cpu_count is None:
910 return []
911
912 cpu_set_size = (cpu_count + 63) // 64
913 cpu_affinity_mask = pynvml.nvmlDeviceGetCpuAffinity(handle, cpu_set_size)
914
915 cpu_ids = []
916 for i, mask in enumerate(cpu_affinity_mask):
917 for bit in range(64):
918 cpu_id = i * 64 + bit
919 if cpu_id >= cpu_count:
920 break
921 if mask & (1 << bit):
922 cpu_ids.append(cpu_id)
923 return cpu_ids
924
925 @classmethod
926 def _get_numa_node_for_cpu(cls, cpu_id: int) -> int | None:
927 """Determine which NUMA node a CPU belongs to."""
928 from pathlib import Path
929
930 node_path = Path("/sys/devices/system/node")
931 if not node_path.exists():
932 return None
933
934 for node_dir in node_path.iterdir():
935 if not node_dir.name.startswith("node"):
936 continue
937 try:
938 node_id = int(node_dir.name[4:])
939 cpulist_file = node_dir / "cpulist"
940 if cpulist_file.exists():
941 cpulist = cpulist_file.read_text().strip()
942 if cls._cpu_in_cpulist(cpu_id, cpulist):
943 return node_id
944 except (ValueError, OSError):
945 continue
946 return None
947
948 @classmethod
949 def _cpu_in_cpulist(cls, cpu_id: int, cpulist: str) -> bool:
950 """Check if a CPU ID is in a cpulist string such as '0-3,8-11'."""
951 for part in cpulist.split(","):
952 part = part.strip()
953 if "-" in part:
954 start, end = part.split("-", 1)
955 if int(start) <= cpu_id <= int(end):
956 return True
957 elif part.isdigit() and int(part) == cpu_id:
958 return True
959 return False
960
961 @classmethod
962 @with_nvml_context
963 def get_all_device_numa_nodes(cls) -> list[int] | None:
964 """Get NUMA nodes for all visible GPU devices."""
965 try:
966 numa_nodes = []
967 for device_id in range(cls.device_count()):
968 numa_node = cls.get_device_numa_node(device_id)
969 if numa_node is None:
970 logger.warning(
971 "Could not detect NUMA node for GPU %d, "
972 "disabling automatic NUMA binding",
973 device_id,
974 )
975 return None
976 numa_nodes.append(numa_node)
977 return numa_nodes
978 except Exception as e:
979 logger.warning("Failed to get NUMA nodes for GPUs: %s", e)
980 return None
981
982 @classmethod
983 @with_nvml_context
984 def get_all_gpu_pci_bus_ids(cls) -> dict[int, str]:
985 """Query NVML for GPU index -> PCI bus ID mapping."""
986 out: dict[int, str] = {}
987 for idx in range(pynvml.nvmlDeviceGetCount()):
988 handle = pynvml.nvmlDeviceGetHandleByIndex(idx)
989 pci_info = pynvml.nvmlDeviceGetPciInfo(handle)
990 bus_id = pci_info.busId
991 if isinstance(bus_id, bytes):
992 bus_id = bus_id.decode("utf-8")
993 out[idx] = bus_id.rstrip("\x00")
994 if not out:
995 raise RuntimeError("NVML returned no GPU PCI bus ID rows")
996 return out
997
998 @classmethod
999 @with_nvml_context
1000 def log_warnings(cls):
1001 device_ids: int = pynvml.nvmlDeviceGetCount()
1002 if device_ids > 1:
1003 device_names = [cls._get_physical_device_name(i) for i in range(device_ids)]
1004 if (
1005 len(set(device_names)) > 1
1006 and os.environ.get("CUDA_DEVICE_ORDER") != "PCI_BUS_ID"
1007 ):
1008 logger.warning_once(
1009 "Detected different devices in the system: %s. Please"
1010 " make sure to set `CUDA_DEVICE_ORDER=PCI_BUS_ID` to "
1011 "avoid unexpected behavior.",
1012 ", ".join(device_names),
1013 scope="process",
1014 )
1015
1016
1017 class NonNvmlCudaPlatform(CudaPlatformBase):
1018 @classmethod
1019 @cache
1020 def get_device_capability(cls, device_id: int = 0) -> DeviceCapability:
1021 major, minor = torch.cuda.get_device_capability(device_id)
1022 return DeviceCapability(major=major, minor=minor)
1023
1024 @classmethod
1025 def get_device_name(cls, device_id: int = 0) -> str:
1026 return torch.cuda.get_device_name(device_id)
1027
1028 @classmethod
1029 def get_device_total_memory(cls, device_id: int = 0) -> int:
1030 device_props = torch.cuda.get_device_properties(device_id)
1031 return device_props.total_memory
1032
1033 @classmethod
1034 def is_fully_connected(cls, physical_device_ids: list[int]) -> bool:
1035 logger.exception(
1036 "NVLink detection not possible, as context support was"
1037 " not found. Assuming no NVLink available."
1038 )
1039 return False
1040
1041 @classmethod
1042 def get_device_numa_node(cls, device_id: int = 0) -> int | None:
1043 return None
1044
1045 @classmethod
1046 def get_all_device_numa_nodes(cls) -> list[int] | None:
1047 return None
1048
1049
1050 # Autodetect either NVML-enabled or non-NVML platform
1051 # based on whether NVML is available.
1052 nvml_available = False
1053 try:
1054 try:
1055 pynvml.nvmlInit()
1056 nvml_available = True
1057 except Exception:
1058 # On Jetson, NVML is not supported.
1059 nvml_available = False
1060 finally:
1061 if nvml_available:
1062 pynvml.nvmlShutdown()
1063
1064 CudaPlatform = NvmlCudaPlatform if nvml_available else NonNvmlCudaPlatform