main
py 1,288 lines 45.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import importlib.metadata
5 import os
6 import platform
7 from datetime import timedelta
8 from functools import cache, lru_cache, wraps
9 from typing import TYPE_CHECKING
10
11 import regex as re
12 import torch
13 from torch.distributed import PrefixStore, ProcessGroup
14 from torch.distributed.distributed_c10d import is_nccl_available
15
16 import vllm.envs as envs
17 from vllm.logger import init_logger
18 from vllm.v1.attention.backends.registry import AttentionBackendEnum
19
20 from .interface import DeviceCapability, Platform, PlatformEnum, in_wsl
21
22 if TYPE_CHECKING:
23 from vllm.config import VllmConfig
24 from vllm.config.kernel import IrOpPriorityConfig
25 from vllm.utils.argparse_utils import FlexibleArgumentParser
26 from vllm.v1.attention.backend import AttentionBackend
27 from vllm.v1.attention.selector import AttentionSelectorConfig
28
29 logger = init_logger(__name__)
30
31 _KV_CACHE_DTYPE_REASON = "kv_cache_dtype not supported"
32 _TURBOQUANT_LAYOUT_REASON = "no KV cache layout in common with TURBOQUANT"
33
34 try:
35 from amdsmi import (
36 AmdSmiException,
37 AmdSmiMemoryType,
38 amdsmi_get_gpu_asic_info,
39 amdsmi_get_gpu_device_uuid,
40 amdsmi_get_gpu_memory_total,
41 amdsmi_get_processor_handles,
42 amdsmi_init,
43 amdsmi_shut_down,
44 amdsmi_topo_get_link_type,
45 amdsmi_topo_get_numa_node_number,
46 )
47 except ImportError as e:
48 logger.warning("Failed to import from amdsmi with %r", e)
49
50 try:
51 import vllm._C # noqa: F401
52 except ImportError as e:
53 logger.warning("Failed to import from vllm._C with %r", e)
54
55 # import custom ops, trigger op registration
56 try:
57 import vllm._C_stable_libtorch # noqa: F401
58 except ImportError as e:
59 logger.warning("Failed to import from vllm._C_stable_libtorch with %r", e)
60
61 try:
62 import vllm._rocm_C # noqa: F401
63 except ImportError as e:
64 logger.warning("Failed to import from vllm._rocm_C with %r", e)
65
66 # Models not supported by ROCm.
67 _ROCM_UNSUPPORTED_MODELS: list[str] = []
68
69 # Models partially supported by ROCm.
70 # Architecture -> Reason.
71 _ROCM_PARTIALLY_SUPPORTED_MODELS: dict[str, str] = {}
72 _ROCM_DEVICE_ID_NAME_MAP: dict[str, str] = {
73 "0x74a0": "AMD_Instinct_MI300A",
74 "0x74a1": "AMD_Instinct_MI300X",
75 "0x74b5": "AMD_Instinct_MI300X", # MI300X VF
76 "0x74a2": "AMD_Instinct_MI308X",
77 "0x74a5": "AMD_Instinct_MI325X",
78 "0x74b9": "AMD_Instinct_MI325X", # MI325X VF
79 "0x74a9": "AMD_Instinct_MI300X_HF",
80 "0x74bd": "AMD_Instinct_MI300X_HF",
81 "0x744c": "AMD_Radeon_RX7900XTX",
82 # RDNA 3.5 APUs (Strix Point / Strix Halo)
83 "0x150e": "AMD_Radeon_890M", # gfx1150, Strix Point
84 "0x1586": "AMD_Radeon_8060S", # gfx1151, Strix Halo
85 # RDNA 4 discrete (Navi 48)
86 "0x7550": "AMD_Radeon_RX9070XT", # gfx1201
87 "0x7551": "AMD_Radeon_R9700", # gfx1201
88 }
89
90
91 @lru_cache(maxsize=8)
92 def _rocm_device_count_stateless(cuda_visible_devices: str | None = None) -> int:
93 """Get number of ROCm devices, caching based on the value of CUDA_VISIBLE_DEVICES
94 at the time of call.
95
96 This should be used instead of torch.accelerator.device_count() unless
97 CUDA_VISIBLE_DEVICES has already been set to the desired value.
98
99 # This can be removed and simply replaced with torch.cuda.get_device_count
100 # after https://github.com/pytorch/pytorch/pull/122815 is released."""
101 # Note: cuda_visible_devices is not used, but we keep it as an argument for
102 # LRU Cache purposes.
103
104 # Code below is based on
105 # https://github.com/pytorch/pytorch/blob/
106 # c1cd946818442aca8c7f812b16d187ce1586c3bc/
107 # torch/cuda/__init__.py#L831C1-L831C17
108 import torch.cuda
109
110 if not torch.cuda._is_compiled():
111 return 0
112 # ROCm uses amdsmi instead of nvml for stateless device count
113 # This requires a sufficiently modern version of Torch 2.4.0
114 raw_count = (
115 torch.cuda._device_count_amdsmi()
116 if (hasattr(torch.cuda, "_device_count_amdsmi"))
117 else -1
118 )
119 r = torch._C._cuda_getDeviceCount() if raw_count < 0 else raw_count
120 return r
121
122
123 @cache
124 def _get_wsl_kernel_version() -> tuple[int, ...] | None:
125 try:
126 release = platform.uname().release
127 parts = release.split("-")[0].split(".")
128 return tuple(int(part) for part in parts[:3])
129 except (TypeError, ValueError):
130 return None
131
132
133 def _sync_hip_cuda_env_vars():
134 """Ensure HIP_VISIBLE_DEVICES and CUDA_VISIBLE_DEVICES are consistent.
135 Treats empty string as unset. Raises on genuine conflicts."""
136 hip_val = os.environ.get("HIP_VISIBLE_DEVICES") or None
137 cuda_val = os.environ.get("CUDA_VISIBLE_DEVICES") or None
138
139 if hip_val is not None and cuda_val is not None:
140 if hip_val != cuda_val:
141 raise ValueError(
142 f"Inconsistent GPU visibility env vars: "
143 f"HIP_VISIBLE_DEVICES='{hip_val}' vs "
144 f"CUDA_VISIBLE_DEVICES='{cuda_val}'. "
145 f"Please set only one, or ensure they match."
146 )
147 elif hip_val is not None:
148 os.environ["CUDA_VISIBLE_DEVICES"] = hip_val
149
150
151 # Sync at import time - catches misconfigurations from process start.
152 _sync_hip_cuda_env_vars()
153
154
155 # AMDSMI utils
156 # Note that NVML is not affected by `{CUDA/HIP}_VISIBLE_DEVICES`,
157 # all the related functions work on real physical device ids.
158 # the major benefit of using AMDSMI is that it will not initialize CUDA
159
160
161 def with_amdsmi_context(fn):
162 @wraps(fn)
163 def wrapper(*args, **kwargs):
164 amdsmi_init()
165 try:
166 return fn(*args, **kwargs)
167 finally:
168 amdsmi_shut_down()
169
170 return wrapper
171
172
173 @with_amdsmi_context
174 def _query_gcn_arch_from_amdsmi() -> str:
175 """Query GCN arch from amdsmi. Raises if not available."""
176 handles = amdsmi_get_processor_handles()
177 if handles:
178 asic_info = amdsmi_get_gpu_asic_info(handles[0])
179 # Use target_graphics_version which contains the gfx name
180 # e.g., 'gfx942' for MI300X/MI325X
181 target_gfx = asic_info.get("target_graphics_version", "")
182 if target_gfx:
183 return target_gfx
184 raise RuntimeError("amdsmi did not return valid GCN arch")
185
186
187 @with_amdsmi_context
188 def _query_total_memory_from_amdsmi(physical_device_id: int) -> int:
189 """Query total VRAM (bytes) from amdsmi. Raises if not available."""
190 handles = amdsmi_get_processor_handles()
191 handle = handles[physical_device_id]
192 return amdsmi_get_gpu_memory_total(handle, AmdSmiMemoryType.VRAM)
193
194
195 def _get_gcn_arch() -> str:
196 """Get GCN arch via amdsmi (no CUDA init), fallback to torch.cuda.
197 Called once at module level; result stored in _GCN_ARCH.
198 """
199 try:
200 return _query_gcn_arch_from_amdsmi()
201 except Exception as e:
202 logger.debug("Failed to get GCN arch via amdsmi: %s", e)
203 # Ultimate fallback: use torch.cuda (will initialize CUDA)
204 return torch.cuda.get_device_properties("cuda").gcnArchName
205
206
207 # Resolve once at module load. Uses amdsmi (no CUDA init) so Ray workers
208 # can still set CUDA_VISIBLE_DEVICES after import.
209 # These are plain Python bools — fully torch.compile/Dynamo safe.
210 _GCN_ARCH = _get_gcn_arch()
211
212 _ON_GFX1X = any(arch in _GCN_ARCH for arch in ["gfx11", "gfx12"])
213 _ON_GFX11 = "gfx11" in _GCN_ARCH
214 _ON_GFX1100 = "gfx1100" in _GCN_ARCH
215 _ON_GFX1151 = "gfx1151" in _GCN_ARCH
216 _ON_GFX12X = any(arch in _GCN_ARCH for arch in ["gfx12"])
217 _ON_MI3XX = any(arch in _GCN_ARCH for arch in ["gfx942", "gfx950"])
218 _ON_GFX9 = any(arch in _GCN_ARCH for arch in ["gfx90a", "gfx942", "gfx950"])
219 _ON_GFX90A = "gfx90a" in _GCN_ARCH
220 _ON_GFX942 = "gfx942" in _GCN_ARCH
221 _ON_GFX950 = "gfx950" in _GCN_ARCH
222 _ON_GFX1250 = "gfx1250" in _GCN_ARCH
223
224 _ON_CDNA = any(arch in _GCN_ARCH for arch in ["gfx9", "gfx1250"])
225 # RDNA = gfx11/gfx12 minus the CDNA-classified gfx1250.
226 _ON_RDNA = _ON_GFX1X and not _ON_CDNA
227 _ON_RDNA4 = any(arch in _GCN_ARCH for arch in ["gfx1200", "gfx1201"])
228
229
230 def _capability_from_gcn_arch(gcn_arch: str) -> tuple[int, int] | None:
231 """Parse (major, minor) from a GCN arch string, mirroring how
232 HIP derives hipDeviceProp_t.major / .minor.
233
234 Format: gfx<MAJOR><MINOR><STEPPING>
235 - 1-digit major (gfx9xx): "gfx" + M + m + stepping
236 - 2-digit major (gfx1xxx): "gfx" + MM + m + stepping
237
238 Examples:
239 gfx90a -> (9, 0) gfx942 -> (9, 4) gfx950 -> (9, 5)
240 gfx1100 -> (11, 0) gfx1101 -> (11, 0) gfx1200 -> (12, 0)
241
242 Returns None only when the string is not gfx-prefixed at all
243 (i.e. not a ROCm arch string). Raises on any string that looks
244 like a GCN arch but does not match a known layout.
245
246 """
247 m = re.match(r"gfx(\d+)", gcn_arch)
248 if not m:
249 # Not a gfx string at all — caller should fall back to torch.cuda
250 return None
251
252 digits = m.group(1)
253 n = len(digits)
254
255 if n < 2:
256 raise ValueError(
257 f"GCN arch '{gcn_arch}' has too few digits ({n}) after 'gfx' "
258 f"to derive a (major, minor) capability. "
259 f"Please file a vLLM issue with your GPU model."
260 )
261
262 if n in (2, 3):
263 # 1-digit major: gfx9 family
264 # len 2: major + minor (e.g. gfx90 from gfx90a)
265 # len 3: major + minor + step (e.g. gfx942)
266 major = int(digits[0])
267 minor = int(digits[1])
268 elif n == 4:
269 # 2-digit major: gfx10xx, gfx11xx, gfx12xx
270 # major(2) + minor(1) + stepping(1)
271 major = int(digits[:2])
272 minor = int(digits[2])
273 elif n >= 5:
274 raise ValueError(
275 f"GCN arch '{gcn_arch}' has {n} digits after 'gfx', which "
276 f"exceeds the known 4-digit layout (MMms). Cannot determine "
277 f"major/minor split unambiguously. "
278 f"Please file a vLLM issue with your GPU model."
279 )
280
281 if major < 9:
282 raise ValueError(
283 f"Parsed unknown ROCm architecture from GCN arch '{gcn_arch}': "
284 f"major={major}, minor={minor}. "
285 f"Major version < 9 is not expected for any supported AMD GPU. "
286 f"Please file a vLLM issue with your GPU model."
287 )
288
289 if major > 12:
290 raise ValueError(
291 f"Parsed unknown ROCm architecture from GCN arch '{gcn_arch}': "
292 f"major={major}, minor={minor}. "
293 f"Major version > 12 is beyond currently known AMD generations. "
294 f"Please file a vLLM issue with your GPU model so support "
295 f"can be added."
296 )
297
298 return (major, minor)
299
300
301 def on_gfx1x() -> bool:
302 return _ON_GFX1X and not _ON_CDNA
303
304
305 def on_gfx11() -> bool:
306 return _ON_GFX11
307
308
309 def on_gfx1100() -> bool:
310 return _ON_GFX1100
311
312
313 def on_gfx1151() -> bool:
314 return _ON_GFX1151
315
316
317 def on_gfx12x() -> bool:
318 return _ON_GFX12X and not _ON_CDNA
319
320
321 def on_gfx1250() -> bool:
322 return _ON_GFX1250
323
324
325 def on_rdna4() -> bool:
326 return _ON_RDNA4
327
328
329 def on_mi3xx() -> bool:
330 return _ON_MI3XX
331
332
333 def on_gfx9() -> bool:
334 return _ON_GFX9
335
336
337 def on_gfx90a() -> bool:
338 return _ON_GFX90A
339
340
341 def on_gfx942() -> bool:
342 return _ON_GFX942
343
344
345 def on_gfx950() -> bool:
346 return _ON_GFX950
347
348
349 def on_cdna() -> bool:
350 return _ON_CDNA
351
352
353 def on_rdna() -> bool:
354 return _ON_RDNA
355
356
357 def get_cdna_version() -> int:
358 if on_gfx90a():
359 return 2
360 if on_gfx942():
361 return 3
362 if on_gfx950():
363 return 4
364 if on_gfx1250():
365 return 5
366 return 0
367
368
369 @cache
370 def get_rocm_version() -> tuple[int, ...] | None:
371 """Return the installed ROCm release as (major, minor, patch), or None."""
372 # ROCm 10+ ships as the `rocm` pip SDK; older releases install to /opt/rocm.
373 try:
374 version = importlib.metadata.version("rocm")
375 except importlib.metadata.PackageNotFoundError:
376 rocm_path = os.environ.get("ROCM_PATH", "/opt/rocm")
377 try:
378 with open(os.path.join(rocm_path, ".info", "version")) as f:
379 version = f.read()
380 except OSError:
381 return None
382 match = re.match(r"\s*(\d+)\.(\d+)(?:\.(\d+))?", version)
383 if match is None:
384 return None
385 return tuple(int(part) for part in match.groups() if part is not None)
386
387
388 # Enable HIP online tuning early, before hipBLASLt initializes.
389 # Turn on hipBLASLt online tuning if use AITER hipBLASLt GEMM.
390 if (
391 envs.VLLM_ROCM_USE_AITER
392 and envs.VLLM_ROCM_USE_AITER_LINEAR
393 and envs.VLLM_ROCM_USE_AITER_LINEAR_HIPBMM
394 and get_cdna_version() > 2
395 ):
396 os.environ["HIP_ONLINE_TUNING"] = "1"
397
398
399 @cache
400 def use_rocm_custom_paged_attention(
401 qtype: torch.dtype,
402 head_size: int,
403 block_size: int,
404 gqa_ratio: int,
405 max_seq_len: int,
406 sliding_window: int,
407 kv_cache_dtype: str,
408 alibi_slopes: torch.Tensor | None = None,
409 sinks: torch.Tensor | None = None,
410 ) -> bool:
411 # custom paged attn always supported on V0. On V1, requires sliding window
412 # disabled due to observed numerical discrepancy.
413 if on_cdna():
414 return (
415 (sliding_window == 0 or sliding_window == (-1, -1))
416 and (qtype == torch.half or qtype == torch.bfloat16)
417 and (head_size == 64 or head_size == 128)
418 and (block_size == 16 or block_size == 32)
419 and (gqa_ratio >= 1 and gqa_ratio <= 16)
420 and max_seq_len <= 128 * 1024
421 and sinks is None
422 )
423
424 else:
425 return (
426 _ON_GFX1X
427 and (sliding_window == 0 or sliding_window == (-1, -1))
428 and (qtype == torch.half or qtype == torch.bfloat16)
429 and head_size == 128
430 and block_size == 16
431 and (gqa_ratio >= 3 and gqa_ratio <= 16)
432 and max_seq_len <= 128 * 1024
433 and alibi_slopes is None
434 and kv_cache_dtype == "auto"
435 and sinks is None
436 )
437
438
439 @cache
440 def flash_attn_triton_available() -> bool:
441 if not on_gfx1x():
442 return False
443 try:
444 from importlib.util import find_spec
445
446 # Locate the Triton-AMD kernels. Older ROCm/flash-attention (pre
447 # 2026-03) shipped them as the flash_attn.flash_attn_triton_amd
448 # subpackage. The main_perf migration commit 3f94643 moved them
449 # into aiter at aiter.ops.triton._triton_kernels.flash_attn_triton_amd,
450 # so accept either location.
451 def _has_spec(name: str) -> bool:
452 try:
453 return find_spec(name) is not None
454 except (ImportError, ValueError):
455 return False
456
457 if not (
458 _has_spec("flash_attn.flash_attn_triton_amd")
459 or _has_spec("aiter.ops.triton._triton_kernels.flash_attn_triton_amd")
460 ):
461 return False
462 if os.environ.get("FLASH_ATTENTION_TRITON_AMD_ENABLE") != "TRUE":
463 logger.info_once(
464 "Set FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE to enable "
465 "Flash Attention Triton backend on RDNA."
466 )
467 return False
468 return True
469 except ImportError:
470 return False
471
472
473 def _get_backend_priorities(
474 use_mla: bool,
475 use_sparse: bool,
476 use_kv_connector: bool = False,
477 ) -> list[AttentionBackendEnum]:
478 from vllm._aiter_ops import is_aiter_found_and_supported, rocm_aiter_ops
479
480 if use_sparse:
481 return [AttentionBackendEnum.ROCM_AITER_MLA_SPARSE]
482
483 if use_mla:
484 if rocm_aiter_ops.is_mla_enabled():
485 return [
486 AttentionBackendEnum.ROCM_AITER_MLA,
487 AttentionBackendEnum.TRITON_MLA,
488 AttentionBackendEnum.ROCM_AITER_TRITON_MLA,
489 ]
490 else:
491 return [
492 AttentionBackendEnum.TRITON_MLA,
493 ]
494
495 backends = []
496 # Keep ROCM_ATTN disabled for KV connectors until connector transfer
497 # semantics are validated for its asymmetric native K/V cache views.
498 if not use_kv_connector:
499 backends.append(AttentionBackendEnum.ROCM_ATTN)
500 if rocm_aiter_ops.is_mha_enabled():
501 backends.append(AttentionBackendEnum.ROCM_AITER_FA)
502 if is_aiter_found_and_supported():
503 backends.append(AttentionBackendEnum.ROCM_AITER_UNIFIED_ATTN)
504 elif rocm_aiter_ops.is_rdna_aiter_enabled():
505 backends.insert(0, AttentionBackendEnum.ROCM_AITER_UNIFIED_ATTN)
506 backends.append(AttentionBackendEnum.TRITON_ATTN)
507 backends.append(AttentionBackendEnum.TURBOQUANT)
508
509 return backends
510
511
512 def _uses_turboquant(vllm_config: "VllmConfig | None") -> bool:
513 """Whether the run's KV cache dtype is one of the turboquant_* presets."""
514 cache_config = getattr(vllm_config, "cache_config", None)
515 return cache_config is not None and str(cache_config.cache_dtype).startswith(
516 "turboquant_"
517 )
518
519
520 def _shares_layout_with_turboquant(backend_class: type["AttentionBackend"]) -> bool:
521 """Whether this backend reads a KV cache layout TURBOQUANT reads too.
522
523 A turboquant_* run keeps its boundary layers at the native dtype (see
524 TurboQuantConfig.get_boundary_skip_layers), so those layers pick a backend of
525 their own while every other layer picks TURBOQUANT. One layout has to serve
526 the whole worker, and get_supported_kv_cache_layouts() turns an empty
527 intersection into a hard error at engine startup.
528 """
529 layouts = backend_class.supported_kv_cache_layouts()
530 turboquant_layouts = (
531 AttentionBackendEnum.TURBOQUANT.get_class().supported_kv_cache_layouts()
532 )
533 if layouts is None or turboquant_layouts is None:
534 return True
535 return not set(layouts).isdisjoint(turboquant_layouts)
536
537
538 def _get_invalid_reasons(
539 backend_class: type["AttentionBackend"],
540 device_capability: DeviceCapability,
541 attn_selector_config: "AttentionSelectorConfig",
542 *,
543 is_turboquant_run: bool,
544 ) -> list[str]:
545 """Why this backend cannot serve the layer, empty when it can."""
546 invalid_reasons = backend_class.validate_configuration(
547 device_capability=device_capability,
548 **attn_selector_config._asdict(),
549 )
550 if (
551 not invalid_reasons
552 and is_turboquant_run
553 and not _shares_layout_with_turboquant(backend_class)
554 ):
555 invalid_reasons = [_TURBOQUANT_LAYOUT_REASON]
556 return invalid_reasons
557
558
559 class RocmPlatform(Platform):
560 _enum = PlatformEnum.ROCM
561 device_name: str = "rocm"
562 device_type: str = "cuda"
563 dispatch_key: str = "CUDA"
564 ray_device_key: str = "GPU"
565 dist_backend: str = "nccl"
566 # rocm shares the same device control env var as CUDA
567 device_control_env_var: str = "CUDA_VISIBLE_DEVICES"
568 # Set in pre_register_and_update, so it exists only on the driver; Ray
569 # workers are separate processes and copy env vars by allowlist.
570 additional_env_vars: list[str] = ["GPU_PINNED_MIN_XFER_SIZE"]
571 ray_noset_device_env_vars: list[str] = [
572 "RAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICES",
573 "RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES",
574 "RAY_EXPERIMENTAL_NOSET_ROCR_VISIBLE_DEVICES",
575 ]
576
577 supported_quantization: list[str] = [
578 "awq",
579 "auto_awq",
580 "awq_marlin", # will be overwritten with awq
581 "gptq",
582 "auto_gptq",
583 "fp8",
584 "deepseek_v4_fp8",
585 "compressed-tensors",
586 "fbgemm_fp8",
587 "inc",
588 "quark",
589 "mxfp4",
590 "mxfp8",
591 "torchao",
592 "modelopt",
593 "modelopt_fp4",
594 "modelopt_mxfp8",
595 "modelopt_mixed",
596 "fp8_per_tensor",
597 "fp8_per_block",
598 "fp8_per_channel",
599 "online",
600 "gpt_oss_mxfp4",
601 ]
602
603 @classmethod
604 def import_kernels(cls) -> None:
605 """Import ROCm-specific kernels."""
606 super().import_kernels()
607
608 import contextlib
609
610 # Import ROCm-specific extension
611 with contextlib.suppress(ImportError):
612 import vllm._rocm_C # noqa: F401
613
614 @classmethod
615 def check_runner_kv_caches_multi_layer(cls) -> None:
616 pass
617
618 @classmethod
619 def is_pin_memory_available(cls) -> bool:
620 if in_wsl():
621 version = _get_wsl_kernel_version()
622 if version is None or version < (4, 19, 121):
623 # warning_once() causes a circular import on WSL, see #48397.
624 logger.warning(
625 "Using 'pin_memory=False' as WSL is detected and the "
626 "WSL2 kernel version is below 4.19.121. This may slow "
627 "down performance. Please run `wsl --update`."
628 )
629 return False
630
631 return True
632
633 @classmethod
634 def get_valid_backends(
635 cls,
636 device_capability: DeviceCapability,
637 attn_selector_config: "AttentionSelectorConfig",
638 num_heads: int | None = None,
639 ) -> tuple[
640 list[tuple["AttentionBackendEnum", int]],
641 dict["AttentionBackendEnum", list[str]],
642 ]:
643 valid_backends_priorities = []
644 invalid_reasons = {}
645
646 backend_priorities = _get_backend_priorities(
647 attn_selector_config.use_mla,
648 attn_selector_config.use_sparse,
649 attn_selector_config.use_kv_connector,
650 )
651 from vllm.config import get_current_vllm_config_or_none
652
653 vllm_config = get_current_vllm_config_or_none()
654 is_encoder_decoder = (
655 getattr(getattr(vllm_config, "model_config", None), "attn_type", None)
656 == "encoder_decoder"
657 )
658 # ROCM_ATTN still uses a legacy attention layout (KV is the outer
659 # dimension) that is incompatible with the encoder backend layouts. The
660 # encoder and decoder need the layouts to match. This is currently
661 # enforced implicitly.
662 # TODO: Make this explicit in the selector in a future PR.
663 if is_encoder_decoder and AttentionBackendEnum.ROCM_ATTN in backend_priorities:
664 backend_priorities.remove(AttentionBackendEnum.ROCM_ATTN)
665 is_turboquant_run = _uses_turboquant(vllm_config)
666 for priority, backend in enumerate(backend_priorities):
667 try:
668 invalid_reasons_i = _get_invalid_reasons(
669 backend.get_class(),
670 device_capability,
671 attn_selector_config,
672 is_turboquant_run=is_turboquant_run,
673 )
674 except ImportError:
675 invalid_reasons_i = ["ImportError"]
676 if invalid_reasons_i:
677 invalid_reasons[backend] = invalid_reasons_i
678 else:
679 valid_backends_priorities.append((backend, priority))
680
681 return valid_backends_priorities, invalid_reasons
682
683 @classmethod
684 def get_attn_backend_cls(
685 cls,
686 selected_backend: "AttentionBackendEnum",
687 attn_selector_config: "AttentionSelectorConfig",
688 num_heads: int | None = None,
689 ) -> str:
690 device_capability = cls.get_device_capability()
691 assert device_capability is not None
692
693 # First try checking just the selected backend, if there is one.
694 if selected_backend is not None:
695 # Keep lazy: vllm.config imports current_platform during initialization.
696 from vllm.config import get_current_vllm_config_or_none
697
698 is_turboquant_run = _uses_turboquant(get_current_vllm_config_or_none())
699 try:
700 sel_invalid_reasons = _get_invalid_reasons(
701 selected_backend.get_class(),
702 device_capability,
703 attn_selector_config,
704 is_turboquant_run=is_turboquant_run,
705 )
706 except ImportError:
707 sel_invalid_reasons = ["ImportError"]
708 if not sel_invalid_reasons:
709 logger.info_once(
710 "Using %s backend (selected via --attention-backend).",
711 selected_backend.name,
712 )
713 return selected_backend.get_path()
714 # Only tolerate the mismatch when turboquant is in play: boundary
715 # layers keep the native dtype while every other layer needs
716 # TURBOQUANT, so no single --attention-backend can serve every layer.
717 # For any other dtype the selection is genuinely invalid -> fail loud.
718 kv_dtype = attn_selector_config.kv_cache_dtype
719 layer_is_turboquant = kv_dtype is not None and str(kv_dtype).startswith(
720 "turboquant"
721 )
722 is_turboquant_fallback = (
723 is_turboquant_run or layer_is_turboquant
724 ) and sel_invalid_reasons in (
725 [_KV_CACHE_DTYPE_REASON],
726 [_TURBOQUANT_LAYOUT_REASON],
727 )
728 if not is_turboquant_fallback:
729 raise ValueError(
730 f"Selected backend {selected_backend} is not valid for "
731 f"this configuration. Reason: {sel_invalid_reasons}"
732 )
733 # NOTE: pass a str (not the list) -- info_once hashes its args.
734 logger.info_once(
735 "Selected backend %s is incompatible with this layer (%s) of "
736 "the turboquant run; using the auto-selected per-layer backend. "
737 "Reason: %s",
738 selected_backend.name,
739 attn_selector_config.attn_type,
740 str(sel_invalid_reasons),
741 )
742
743 # No selected backend or the selected backend is invalid,
744 # so we try finding a valid backend.
745 valid_backends_priorities, invalid_reasons = cls.get_valid_backends(
746 device_capability=device_capability,
747 attn_selector_config=attn_selector_config,
748 num_heads=num_heads,
749 )
750 reasons_str = (
751 "{"
752 + ", ".join(
753 f"{backend.name}: [{', '.join(reasons)}]"
754 for backend, reasons in invalid_reasons.items()
755 )
756 + "}"
757 )
758 config_str = attn_selector_config.__repr__()
759 logger.debug_once(
760 f"Some attention backends are not valid for {cls.device_name} with "
761 f"{config_str}. Reasons: {reasons_str}."
762 )
763 if len(valid_backends_priorities) == 0:
764 # If a backend rejected the requested kv-cache dtype, list the
765 # dtypes it does accept so the limitation is discoverable.
766 supported = sorted(
767 {
768 dt
769 for backend, reasons in invalid_reasons.items()
770 if any("kv_cache_dtype" in r for r in reasons)
771 for dt in backend.get_class().supported_kv_cache_dtypes
772 }
773 )
774 hint = (
775 f" Supported kv_cache_dtype values: {', '.join(supported)}."
776 if supported
777 else ""
778 )
779 raise ValueError(
780 f"No valid attention backend found for {cls.device_name} "
781 f"with {config_str}. Reasons: {reasons_str}.{hint}"
782 )
783
784 # We have found some valid backends. Select the one with the
785 # highest priority.
786 sorted_indices = sorted(
787 range(len(valid_backends_priorities)),
788 key=lambda i: valid_backends_priorities[i][1],
789 )
790 selected_index = sorted_indices[0]
791 selected_backend = valid_backends_priorities[selected_index][0]
792 valid_str = (
793 "[" + ", ".join(f"'{b[0].name}'" for b in valid_backends_priorities) + "]"
794 )
795 if invalid_reasons:
796 rejected_str = ", ".join(b.name for b in invalid_reasons)
797 logger.info(
798 "Found incompatible backend(s) [%s] with %s. "
799 "Overriding with %s out of potential backends: %s.",
800 rejected_str,
801 attn_selector_config.attn_type,
802 selected_backend.name,
803 valid_str,
804 )
805 else:
806 logger.info_once(
807 "Using %s backend out of potential backends: %s.",
808 selected_backend.name,
809 valid_str,
810 )
811
812 return selected_backend.get_path()
813
814 @classmethod
815 def get_supported_vit_attn_backends(cls) -> list["AttentionBackendEnum"]:
816 return [
817 AttentionBackendEnum.FLASH_ATTN,
818 AttentionBackendEnum.ROCM_AITER_FA,
819 AttentionBackendEnum.TRITON_ATTN,
820 AttentionBackendEnum.TORCH_SDPA,
821 ]
822
823 @classmethod
824 def get_vit_attn_backend(
825 cls,
826 head_size: int,
827 dtype: torch.dtype,
828 backend: "AttentionBackendEnum | None" = None,
829 ) -> "AttentionBackendEnum":
830 if backend is not None:
831 assert backend in cls.get_supported_vit_attn_backends(), (
832 f"Backend {backend} is not supported for vit attention. "
833 f"Supported backends are: {cls.get_supported_vit_attn_backends()}"
834 )
835 logger.info_once(f"Using backend {backend} for vit attention")
836 return backend
837
838 from importlib.util import find_spec
839
840 from vllm._aiter_ops import rocm_aiter_ops
841
842 if rocm_aiter_ops.is_mha_enabled() and on_cdna():
843 logger.info_once("Using AITER Flash Attention backend for ViT model.")
844 return AttentionBackendEnum.ROCM_AITER_FA
845
846 if (
847 on_cdna()
848 and find_spec("flash_attn") is not None
849 and (dtype == torch.float16 or dtype == torch.bfloat16)
850 ):
851 logger.info_once("Using Flash Attention backend for ViT model.")
852 return AttentionBackendEnum.FLASH_ATTN
853
854 # RDNA3/RDNA4 (gfx11xx/gfx12xx): Use Flash Attention Triton backend
855 if (
856 on_gfx1x()
857 and flash_attn_triton_available()
858 and (dtype == torch.float16 or dtype == torch.bfloat16)
859 ):
860 logger.info_once(
861 "Using Flash Attention (Triton backend) for ViT model on RDNA."
862 )
863 return AttentionBackendEnum.FLASH_ATTN
864
865 logger.info_once("Using Torch SDPA backend for ViT model.")
866 return AttentionBackendEnum.TORCH_SDPA
867
868 @classmethod
869 def set_device(cls, device: torch.device) -> None:
870 """Set the device for the current platform."""
871 torch.cuda.set_device(device)
872
873 @classmethod
874 def manual_seed_all(cls, seed: int) -> None:
875 torch.cuda.manual_seed_all(seed)
876
877 @classmethod
878 @lru_cache(maxsize=8)
879 def get_device_capability(cls, device_id: int = 0) -> DeviceCapability | None:
880 cap = _capability_from_gcn_arch(_GCN_ARCH)
881 if cap is not None:
882 return DeviceCapability(major=cap[0], minor=cap[1])
883
884 logger.warning_once(
885 "Could not derive device capability from GCN arch '%s', "
886 "falling back to torch.cuda (this will initialize CUDA).",
887 _GCN_ARCH,
888 )
889 major, minor = torch.cuda.get_device_capability(device_id)
890 return DeviceCapability(major=major, minor=minor)
891
892 @classmethod
893 @with_amdsmi_context
894 def is_fully_connected(cls, physical_device_ids: list[int]) -> bool:
895 """Query if the set of gpus are fully connected by xgmi (1 hop)."""
896 handles = [amdsmi_get_processor_handles()[i] for i in physical_device_ids]
897 for i, handle in enumerate(handles):
898 for j, peer_handle in enumerate(handles):
899 if i < j:
900 try:
901 link_type = amdsmi_topo_get_link_type(handle, peer_handle)
902 # type is 2 for XGMI
903 if link_type["hops"] != 1 or link_type["type"] != 2:
904 return False
905 except AmdSmiException as error:
906 logger.error("AMD 1 hop XGMI detection failed.", exc_info=error)
907 return False
908 return True
909
910 @classmethod
911 @with_amdsmi_context
912 @lru_cache(maxsize=8)
913 def get_device_name(cls, device_id: int = 0) -> str:
914 physical_device_id = cls.device_id_to_physical_device_id(device_id)
915 handle = amdsmi_get_processor_handles()[physical_device_id]
916 asic_info = amdsmi_get_gpu_asic_info(handle)
917 asic_info_device_id: str = asic_info["device_id"]
918 if asic_info_device_id in _ROCM_DEVICE_ID_NAME_MAP:
919 return _ROCM_DEVICE_ID_NAME_MAP[asic_info_device_id]
920 return asic_info["market_name"]
921
922 @classmethod
923 @with_amdsmi_context
924 def get_device_uuid(cls, device_id: int = 0) -> str:
925 try:
926 device = amdsmi_get_processor_handles()[device_id]
927 except AmdSmiException as error:
928 logger.error("amdsmi device query failed ", exc_info=error)
929 return ""
930 try:
931 device_uuid = amdsmi_get_gpu_device_uuid(device)
932 except AmdSmiException as error:
933 logger.error("amdsmi device uuid query failed ", exc_info=error)
934 return device_uuid
935
936 @classmethod
937 def get_device_total_memory(cls, device_id: int = 0) -> int:
938 # Query total VRAM via amdsmi so we don't initialize a HIP context in
939 # the calling process. torch.cuda.get_device_properties() creates a
940 # HIP context, which makes vLLM fall back from `fork` to `spawn` for
941 # worker processes. Keeping this query context-free preserves `fork`
942 # where it is otherwise valid (e.g. out-of-tree models registered in
943 # the parent process).
944 try:
945 physical_device_id = cls.device_id_to_physical_device_id(device_id)
946 return _query_total_memory_from_amdsmi(physical_device_id)
947 except Exception as e:
948 logger.debug("Failed to get total memory via amdsmi: %s", e)
949 logger.warning_once(
950 "Failed to get total memory via amdsmi, falling back to "
951 "torch.cuda. This will initialize CUDA."
952 )
953 return torch.cuda.get_device_properties(device_id).total_memory
954
955 @classmethod
956 def pre_register_and_update(
957 cls, parser: "FlexibleArgumentParser | None" = None
958 ) -> None:
959 # Keep mmap'd weight pages on the HIP staging path: above this
960 # threshold the runtime registers the pageable source instead, and each
961 # registration's MMU notifier makes KFD suspend our queues. In KB, so
962 # 4 GiB.
963 os.environ.setdefault("GPU_PINNED_MIN_XFER_SIZE", str(4 * 1024 * 1024))
964
965 @classmethod
966 def apply_config_platform_defaults(cls, vllm_config: "VllmConfig") -> None:
967 from vllm._aiter_ops import rocm_aiter_ops
968
969 compilation_config = vllm_config.compilation_config
970 use_aiter_fused_moe = rocm_aiter_ops.is_fused_moe_enabled()
971 use_aiter_fp8_linear = rocm_aiter_ops.is_linear_fp8_enabled()
972 use_aiter_fused_se = rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
973
974 if use_aiter_fp8_linear and "-quant_fp8" not in compilation_config.custom_ops:
975 compilation_config.custom_ops.append("+quant_fp8")
976
977 if use_aiter_fused_se and "-grouped_topk" in compilation_config.custom_ops:
978 logger.warning_once(
979 "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS is enabled, which "
980 "requires the 'grouped_topk' custom op. Overriding the "
981 "user-provided '-grouped_topk'."
982 )
983 compilation_config.custom_ops.remove("-grouped_topk")
984 # Ensure grouped_topk is always enabled when using AITER if
985 # its not disabled by user
986 if (
987 use_aiter_fused_moe
988 and "+grouped_topk" not in compilation_config.custom_ops
989 and "-grouped_topk" not in compilation_config.custom_ops
990 ):
991 compilation_config.custom_ops.append("+grouped_topk")
992
993 # Default dispatch to rocm's sparse_attn_indexer implementation
994 compilation_config.custom_ops.append("+sparse_attn_indexer")
995
996 @classmethod
997 def check_and_update_config(cls, vllm_config: "VllmConfig") -> None:
998 from vllm.config.compilation import CUDAGraphMode
999
1000 compilation_config = vllm_config.compilation_config
1001 parallel_config = vllm_config.parallel_config
1002
1003 if (
1004 compilation_config.cudagraph_mode.has_full_cudagraphs()
1005 and parallel_config.prefill_context_parallel_size > 1
1006 ):
1007 # prefill context parallel do not support full cudagraphs
1008 logger.warning_once(
1009 "Prefill context parallel (PCP) is enabled, which is "
1010 "incompatible with full CUDA graphs. "
1011 "Overriding cudagraph_mode to PIECEWISE."
1012 )
1013 compilation_config.cudagraph_mode = CUDAGraphMode.PIECEWISE
1014
1015 if parallel_config.worker_cls == "auto":
1016 parallel_config.worker_cls = "vllm.v1.worker.gpu_worker.Worker"
1017
1018 model_config = vllm_config.model_config
1019 scheduler_config = vllm_config.scheduler_config
1020 # Note: model_config may be None during testing
1021 if (
1022 model_config is not None
1023 and model_config.is_mm_prefix_lm
1024 and scheduler_config.is_multimodal_model
1025 and not scheduler_config.disable_chunked_mm_input
1026 ):
1027 logger.warning_once(
1028 "Forcing --disable_chunked_mm_input for models "
1029 "with multimodal-bidirectional attention."
1030 )
1031 scheduler_config.disable_chunked_mm_input = True
1032
1033 @classmethod
1034 def verify_model_arch(cls, model_arch: str) -> None:
1035 if model_arch in _ROCM_UNSUPPORTED_MODELS:
1036 raise ValueError(
1037 f"Model architecture '{model_arch}' is not supported by ROCm for now."
1038 )
1039
1040 if model_arch in _ROCM_PARTIALLY_SUPPORTED_MODELS:
1041 msg = _ROCM_PARTIALLY_SUPPORTED_MODELS[model_arch]
1042 logger.warning(
1043 "Model architecture '%s' is partially supported by ROCm: %s",
1044 model_arch,
1045 msg,
1046 )
1047
1048 @classmethod
1049 def verify_quantization(cls, quant: str) -> None:
1050 super().verify_quantization(quant)
1051 if quant == "awq" and not envs.VLLM_USE_TRITON_AWQ:
1052 logger.warning(
1053 "Using AWQ quantization with ROCm, but VLLM_USE_TRITON_AWQ"
1054 " is not set, enabling VLLM_USE_TRITON_AWQ."
1055 )
1056 os.environ["VLLM_USE_TRITON_AWQ"] = "1"
1057
1058 @classmethod
1059 def get_punica_wrapper(cls) -> str:
1060 return "vllm.lora.punica_wrapper.punica_gpu.PunicaWrapperGPU"
1061
1062 @classmethod
1063 def get_current_memory_usage(
1064 cls, device: torch.types.Device | None = None
1065 ) -> float:
1066 torch.cuda.empty_cache()
1067 torch.cuda.reset_peak_memory_stats(device)
1068 return torch.cuda.max_memory_allocated(device)
1069
1070 @classmethod
1071 def get_device_communicator_cls(cls) -> str:
1072 return (
1073 "vllm.distributed.device_communicators.cuda_communicator.CudaCommunicator" # noqa
1074 )
1075
1076 @classmethod
1077 def supports_mx(cls) -> bool:
1078 return any(gfx in _GCN_ARCH for gfx in ["gfx95", "gfx1250"])
1079
1080 @classmethod
1081 def supports_fp8(cls) -> bool:
1082 return on_cdna() or on_rdna4()
1083
1084 @classmethod
1085 def is_fp8_fnuz(cls) -> bool:
1086 # only device 0 is checked, this assumes MI300 platforms are homogeneous
1087 return "gfx94" in _GCN_ARCH
1088
1089 @classmethod
1090 def fp8_dtype(cls) -> torch.dtype:
1091 if cls.is_fp8_fnuz():
1092 return torch.float8_e4m3fnuz
1093 else:
1094 return torch.float8_e4m3fn
1095
1096 @classmethod
1097 def use_custom_allreduce(cls) -> bool:
1098 # We only enable custom allreduce for MI300 series
1099 return any(gfx in _GCN_ARCH for gfx in ["gfx94", "gfx95"])
1100
1101 @classmethod
1102 def opaque_attention_op(cls) -> bool:
1103 return True
1104
1105 @classmethod
1106 def is_navi(cls) -> bool:
1107 return "gfx1" in _GCN_ARCH
1108
1109 @classmethod
1110 def enable_multi_stream_overlap(
1111 cls,
1112 aux_stream_list: list[torch.cuda.Stream] | None,
1113 attn_metadata: object,
1114 ) -> bool:
1115 """ROCm multi-stream gates: streams and capture region.
1116
1117 Dict metadata marks piecewise cudagraph, whose eager breaks rebuild
1118 the attention inputs on the owning stream. Forking side streams
1119 there would rely on runtime HIP event sync, which is unreliable in
1120 this overlap on ROCm (event waits can hang), so multi-stream only
1121 runs where the fork/join becomes static graph edges: inside capture,
1122 or with non-dict metadata (full cudagraph or the profile run), which
1123 has no eager breaks.
1124 """
1125 return aux_stream_list is not None and (
1126 torch.cuda.is_current_stream_capturing()
1127 or not isinstance(attn_metadata, dict)
1128 )
1129
1130 @classmethod
1131 def get_static_graph_wrapper_cls(cls) -> str:
1132 return "vllm.compilation.cuda_graph.CUDAGraphWrapper"
1133
1134 @classmethod
1135 def stateless_init_device_torch_dist_pg(
1136 cls,
1137 backend: str,
1138 prefix_store: PrefixStore,
1139 group_rank: int,
1140 group_size: int,
1141 timeout: timedelta,
1142 ) -> ProcessGroup:
1143 assert is_nccl_available()
1144 pg: ProcessGroup = ProcessGroup(
1145 prefix_store,
1146 group_rank,
1147 group_size,
1148 )
1149 from torch.distributed.distributed_c10d import ProcessGroupNCCL
1150
1151 backend_options = ProcessGroupNCCL.Options()
1152 backend_options._timeout = timeout
1153
1154 backend_class = ProcessGroupNCCL(
1155 prefix_store, group_rank, group_size, backend_options
1156 )
1157 backend_type = ProcessGroup.BackendType.NCCL
1158 device = torch.device("cuda")
1159 pg._set_default_backend(backend_type)
1160 backend_class._set_sequence_number_for_group()
1161
1162 pg._register_backend(device, backend_type, backend_class)
1163 return pg
1164
1165 @classmethod
1166 def device_count(cls) -> int:
1167 return _rocm_device_count_stateless(getattr(envs, cls.device_control_env_var))
1168
1169 @classmethod
1170 def check_if_supports_dtype(cls, dtype: torch.dtype):
1171 if dtype == torch.bfloat16: # noqa: SIM102
1172 if not cls.has_device_capability(80):
1173 capability = cls.get_device_capability()
1174 gpu_name = cls.get_device_name()
1175
1176 if capability is None:
1177 compute_str = "does not have a compute capability"
1178 else:
1179 version_str = capability.as_version_str()
1180 compute_str = f"has compute capability {version_str}"
1181
1182 raise ValueError(
1183 "Bfloat16 is only supported on GPUs "
1184 "with compute capability of at least 8.0. "
1185 f"Your {gpu_name} GPU {compute_str}. "
1186 "You can use float16 instead by explicitly setting the "
1187 "`dtype` flag in CLI, for example: --dtype=half."
1188 )
1189
1190 @classmethod
1191 def insert_blocks_to_device(
1192 cls,
1193 src_cache: torch.Tensor,
1194 dst_cache: torch.Tensor,
1195 src_block_indices: torch.Tensor,
1196 dst_block_indices: torch.Tensor,
1197 ) -> None:
1198 """Copy blocks from src_cache to dst_cache on GPU."""
1199 _src_cache = src_cache[src_block_indices]
1200 dst_cache[dst_block_indices] = _src_cache.to(dst_cache.device)
1201
1202 @classmethod
1203 def swap_out_blocks_to_host(
1204 cls,
1205 src_cache: torch.Tensor,
1206 dst_cache: torch.Tensor,
1207 src_block_indices: torch.Tensor,
1208 dst_block_indices: torch.Tensor,
1209 ) -> None:
1210 """Copy blocks from GPU to host (CPU)."""
1211 _src_cache = src_cache[src_block_indices]
1212 dst_cache[dst_block_indices] = _src_cache.cpu()
1213
1214 @classmethod
1215 def support_hybrid_kv_cache(cls) -> bool:
1216 return True
1217
1218 @classmethod
1219 def support_static_graph_mode(cls) -> bool:
1220 return True
1221
1222 @classmethod
1223 def num_compute_units(cls, device_id: int = 0) -> int:
1224 return torch.cuda.get_device_properties(device_id).multi_processor_count
1225
1226 @classmethod
1227 def use_custom_op_collectives(cls) -> bool:
1228 return True
1229
1230 @classmethod
1231 def get_default_ir_op_priority(
1232 cls, vllm_config: "VllmConfig"
1233 ) -> "IrOpPriorityConfig":
1234 from vllm.config.compilation import CompilationMode, CUDAGraphMode
1235 from vllm.config.kernel import IrOpPriorityConfig
1236
1237 # Native used by default when compiling,
1238 # use vllm_c kernels where available when no codegen
1239 # TODO(luka/TJ) use aiter, vllm_c, native by default on ROCm
1240 cc = vllm_config.compilation_config
1241 using_inductor = cc.backend == "inductor" and cc.mode != CompilationMode.NONE
1242 default = ["native"] if using_inductor else ["vllm_c", "native"]
1243
1244 # Aiter rms norm perform best when CUDA Graph capture is enabled.
1245 # TODO(luka/TJ) remove env vars completely
1246 if (
1247 cc.cudagraph_mode != CUDAGraphMode.NONE
1248 and envs.VLLM_ROCM_USE_AITER
1249 and envs.VLLM_ROCM_USE_AITER_RMSNORM
1250 and not on_rdna4()
1251 ):
1252 rms_norm = ["aiter"] + default
1253 else:
1254 rms_norm = default
1255
1256 return IrOpPriorityConfig.with_default(
1257 default,
1258 rms_norm=rms_norm,
1259 fused_add_rms_norm=rms_norm,
1260 gelu_and_mul_sparse=["native"],
1261 )
1262
1263 @classmethod
1264 @with_amdsmi_context
1265 def get_all_device_numa_nodes(cls) -> list[int] | None:
1266 """Get NUMA nodes for all visible GPU devices."""
1267 try:
1268 handles = amdsmi_get_processor_handles()
1269 numa_nodes = []
1270 for device_id in range(cls.device_count()):
1271 physical_device_id = cls.device_id_to_physical_device_id(device_id)
1272 try:
1273 numa_node = amdsmi_topo_get_numa_node_number(
1274 handles[physical_device_id]
1275 )
1276 except AmdSmiException as e:
1277 logger.warning(
1278 "Could not detect NUMA node for GPU %d, "
1279 "disabling automatic NUMA binding: %s",
1280 device_id,
1281 e,
1282 )
1283 return None
1284 numa_nodes.append(numa_node)
1285 return numa_nodes
1286 except Exception as e:
1287 logger.warning("Failed to get NUMA nodes for GPUs: %s", e)
1288 return None