main
py 1,343 lines 50.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import contextlib
4 import enum
5 import functools
6 import itertools
7 import math
8 import os
9 import platform
10 import sys
11 from datetime import timedelta
12 from typing import TYPE_CHECKING, Any, NamedTuple
13
14 import torch
15
16 from vllm.logger import init_logger
17 from vllm.v1.attention.backends.registry import AttentionBackendEnum
18
19 if TYPE_CHECKING:
20 from torch.distributed import PrefixStore, ProcessGroup
21
22 from vllm.config import VllmConfig
23 from vllm.config.kernel import IrOpPriorityConfig
24 from vllm.inputs import EngineInput
25 from vllm.pooling_params import PoolingParams
26 from vllm.sampling_params import SamplingParams
27 from vllm.utils.argparse_utils import FlexibleArgumentParser
28 from vllm.v1.attention.backend import AttentionBackend
29 from vllm.v1.attention.selector import AttentionSelectorConfig
30 else:
31 FlexibleArgumentParser = object
32
33 logger = init_logger(__name__)
34
35 _assigned_physical_gpu_ids: list[int] | None = None
36
37
38 def set_assigned_physical_gpu_ids(ids: list[int]) -> None:
39 """Set the physical GPU IDs assigned to this worker process.
40 Called during worker init so that device_id_to_physical_device_id()
41 can map local_rank to the correct physical device without relying
42 on CUDA_VISIBLE_DEVICES.
43
44 Idempotent: a second call with the same value is a no-op.
45 Raises RuntimeError if called again with a different value.
46
47 This is expected to run during single-threaded worker initialization."""
48 global _assigned_physical_gpu_ids
49 if _assigned_physical_gpu_ids is not None:
50 if _assigned_physical_gpu_ids != ids:
51 raise RuntimeError(
52 f"set_assigned_physical_gpu_ids called with conflicting values: "
53 f"existing={_assigned_physical_gpu_ids}, new={ids}"
54 )
55 return
56 _assigned_physical_gpu_ids = ids
57
58
59 def get_assigned_physical_gpu_ids() -> list[int] | None:
60 return _assigned_physical_gpu_ids
61
62
63 @functools.cache
64 def in_wsl() -> bool:
65 # Reference: https://github.com/microsoft/WSL/issues/4071
66 return "microsoft" in " ".join(platform.uname()).lower()
67
68
69 class PlatformEnum(enum.Enum):
70 """Enumeration of supported hardware platforms."""
71
72 CUDA = enum.auto()
73 ROCM = enum.auto()
74 TPU = enum.auto()
75 XPU = enum.auto()
76 CPU = enum.auto()
77 OOT = enum.auto()
78 UNSPECIFIED = enum.auto()
79
80
81 class CpuArchEnum(enum.Enum):
82 X86 = enum.auto()
83 ARM = enum.auto()
84 POWERPC = enum.auto()
85 S390X = enum.auto()
86 RISCV = enum.auto()
87 OTHER = enum.auto()
88 UNKNOWN = enum.auto()
89
90
91 class DeviceCapability(NamedTuple):
92 major: int
93 minor: int
94
95 def __lt__(self, other: Any) -> bool:
96 if not isinstance(other, DeviceCapability):
97 return NotImplemented
98 return (self.major, self.minor) < (other.major, other.minor)
99
100 def __le__(self, other: Any) -> bool:
101 if not isinstance(other, DeviceCapability):
102 return NotImplemented
103 return (self.major, self.minor) <= (other.major, other.minor)
104
105 def __eq__(self, other: Any) -> bool:
106 if not isinstance(other, DeviceCapability):
107 return NotImplemented
108 return (self.major, self.minor) == (other.major, other.minor)
109
110 def __ge__(self, other: Any) -> bool:
111 if not isinstance(other, DeviceCapability):
112 return NotImplemented
113 return (self.major, self.minor) >= (other.major, other.minor)
114
115 def __gt__(self, other: Any) -> bool:
116 if not isinstance(other, DeviceCapability):
117 return NotImplemented
118 return (self.major, self.minor) > (other.major, other.minor)
119
120 def __hash__(self) -> int:
121 return hash((self.major, self.minor))
122
123 def as_version_str(self) -> str:
124 return f"{self.major}.{self.minor}"
125
126 def to_int(self) -> int:
127 """Express device capability as an integer `<major><minor>`.
128
129 It is assumed that the minor version is always a single digit.
130 """
131 assert 0 <= self.minor < 10
132 return self.major * 10 + self.minor
133
134
135 class Platform:
136 _enum: PlatformEnum
137 device_name: str
138 device_type: str
139
140 # available dispatch keys:
141 # check https://github.com/pytorch/pytorch/blob/313dac6c1ca0fa0cde32477509cce32089f8532a/torchgen/model.py#L134 # noqa
142 # use "CPU" as a fallback for platforms not registered in PyTorch
143 dispatch_key: str = "CPU"
144
145 # available ray device keys:
146 # https://github.com/ray-project/ray/blob/10ba5adadcc49c60af2c358a33bb943fb491a171/python/ray/_private/ray_constants.py#L438 # noqa
147 # empty string means the device does not support ray
148 ray_device_key: str = ""
149
150 @classmethod
151 def log_warnings(cls) -> None:
152 """Log platform-specific diagnostics after logging is configured."""
153
154 # platform-agnostic way to specify the device control environment variable,
155 # .e.g. CUDA_VISIBLE_DEVICES for CUDA.
156 # hint: search for "get_visible_accelerator_ids_env_var" in
157 # https://github.com/ray-project/ray/tree/master/python/ray/_private/accelerators # noqa
158 device_control_env_var: str = "VLLM_DEVICE_CONTROL_ENV_VAR_PLACEHOLDER"
159
160 # environment variables that need to be set to 1 to prevent ray from
161 # setting the visible devices e.g.
162 # RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES
163 ray_noset_device_env_vars: list[str] = []
164
165 # The torch.compile backend for compiling simple and
166 # standalone functions. The default value is "inductor" to keep
167 # the same behavior as PyTorch.
168 # NOTE: for the forward part of the model, vLLM has another separate
169 # compilation strategy.
170 simple_compile_backend: str = "inductor"
171
172 # The backend used for distributed communication.
173 dist_backend: str = ""
174
175 supported_quantization: list[str] = []
176
177 additional_env_vars: list[str] = []
178
179 _global_graph_pool: Any | None = None
180
181 @property
182 def pass_key(self) -> str:
183 """Inductor config key for the PassManager custom pass."""
184 return "post_grad_custom_post_pass"
185
186 @property
187 def supported_dtypes(self) -> list[torch.dtype]:
188 """Returns the supported dtypes for the current platform."""
189 # Be careful with the order of the dtypes. The first dtype will
190 # be used as the default dtype fallback for the current platform,
191 # when encountering unsupported dtypes in "auto" dtype.
192 return [torch.bfloat16, torch.float16, torch.float32]
193
194 def is_cuda(self) -> bool:
195 return self._enum == PlatformEnum.CUDA
196
197 def is_rocm(self) -> bool:
198 return self._enum == PlatformEnum.ROCM
199
200 def is_tpu(self) -> bool:
201 return self._enum == PlatformEnum.TPU
202
203 def is_xpu(self) -> bool:
204 return self._enum == PlatformEnum.XPU
205
206 def is_cpu(self) -> bool:
207 return self._enum == PlatformEnum.CPU
208
209 def uses_host_device_handling(self) -> bool:
210 """Whether vLLM should leave DeviceConfig.device unset."""
211 return self.is_tpu()
212
213 def is_zen_cpu(self) -> bool:
214 return False
215
216 def is_out_of_tree(self) -> bool:
217 return self._enum == PlatformEnum.OOT
218
219 def is_unspecified(self) -> bool:
220 return self._enum == PlatformEnum.UNSPECIFIED
221
222 def get_max_output_tokens(self, prompt_len: int) -> int:
223 return sys.maxsize
224
225 def is_cuda_alike(self) -> bool:
226 """Stateless version of [torch.cuda.is_available][]."""
227 return self._enum in (PlatformEnum.CUDA, PlatformEnum.ROCM)
228
229 def is_sleep_mode_available(self) -> bool:
230 # TODO: Actually only mi3xx has the sleep mode support now
231 # for ROCm, but currently we don't have a way to detect the
232 # exact GPU model statelessly here. So we return True for
233 # all ROCm platforms for now.
234 return self._enum in (PlatformEnum.CUDA, PlatformEnum.ROCM, PlatformEnum.XPU)
235
236 def is_cumem_allocator_available(self) -> bool:
237 try:
238 from vllm.device_allocator.cumem import cumem_available
239 except ImportError:
240 return False
241
242 return cumem_available
243
244 @classmethod
245 def get_pass_manager_cls(cls) -> str:
246 """Get the pass manager class for this platform.
247 It will be registered as a custom pass under the current_platform.pass_key.
248 """
249 return "vllm.compilation.passes.pass_manager.PostGradPassManager"
250
251 @classmethod
252 def get_compile_backend(cls) -> str:
253 """Get the custom compile backend for current platform."""
254 return cls.simple_compile_backend
255
256 @classmethod
257 def import_ir_kernels(cls) -> None:
258 """The default implementation imports ``vllm.kernels``, which registers
259 the built-in IR op implementations. Out-of-tree (OOT) platforms should
260 override this method to import their own kernel modules.
261 """
262 import vllm.kernels # noqa: F401
263
264 @classmethod
265 def device_control_id_to_physical_device_id(cls, device_id: str) -> int:
266 """Map one device-control env entry to an integer physical device ID."""
267 try:
268 return int(device_id)
269 except ValueError as e:
270 raise ValueError(
271 f"Non-integer device ID {device_id!r} is not supported by "
272 f"{cls.device_name}."
273 ) from e
274
275 # GPU device IDs can refer to three distinct namespaces:
276 # - logical: vLLM-local IDs such as local ranks. These index
277 # assigned_physical_gpu_ids when it is set.
278 # - visible: torch/CUDA ordinals in the current process after applying
279 # the device-control env var, e.g. CUDA_VISIBLE_DEVICES.
280 # - physical: global GPU IDs used by topology and management APIs such as
281 # NVML, which are not remapped by CUDA_VISIBLE_DEVICES.
282 # Keep conversions explicit. In particular, torch device indices are
283 # visible IDs, not vLLM logical IDs.
284
285 @classmethod
286 def device_id_to_physical_device_id(cls, device_id: int):
287 """Map a vLLM-local logical device ID to a physical device ID.
288
289 The input is a logical local ID (e.g. a local rank), NOT a visible
290 device ordinal; for the latter use
291 visible_device_id_to_physical_device_id(). The two coincide only
292 when no logical-to-physical mapping is in effect.
293 """
294 if _assigned_physical_gpu_ids is not None:
295 if device_id >= len(_assigned_physical_gpu_ids):
296 raise IndexError(
297 f"device_id {device_id} is out of range for "
298 f"assigned_physical_gpu_ids {_assigned_physical_gpu_ids} "
299 f"({len(_assigned_physical_gpu_ids)} devices assigned)"
300 )
301 return _assigned_physical_gpu_ids[device_id]
302 # Treat empty device control env var as unset. This is a valid
303 # configuration in Ray setups where the engine is launched in
304 # a CPU-only placement group located on a GPU node.
305 if (
306 cls.device_control_env_var in os.environ
307 and os.environ[cls.device_control_env_var] != ""
308 ):
309 device_ids = os.environ[cls.device_control_env_var].split(",")
310 physical_device_id = device_ids[device_id]
311 return cls.device_control_id_to_physical_device_id(physical_device_id)
312 else:
313 return device_id
314
315 @classmethod
316 def logical_device_id_to_visible_device_id(cls, device_id: int) -> int:
317 """Map a vLLM-local logical device ID to the current process's
318 visible accelerator ordinal.
319
320 vLLM internals use logical local IDs. Physical IDs are used only
321 at platform/topology boundaries. This helper performs the final
322 translation needed by APIs such as ``torch.device("cuda:N")``.
323 """
324 physical_device_id = cls.device_id_to_physical_device_id(device_id)
325 device_control_env = os.environ.get(cls.device_control_env_var, "")
326 if not device_control_env:
327 return physical_device_id
328
329 visible_physical_device_ids = [
330 cls.device_control_id_to_physical_device_id(physical_id)
331 for physical_id in device_control_env.split(",")
332 ]
333 if physical_device_id not in visible_physical_device_ids:
334 raise RuntimeError(
335 f"Physical device {physical_device_id} for logical device "
336 f"{device_id} is not visible in {cls.device_control_env_var}="
337 f"{device_control_env}"
338 )
339 return visible_physical_device_ids.index(physical_device_id)
340
341 @classmethod
342 def visible_device_id_to_physical_device_id(cls, device_id: int) -> int:
343 """Map a visible accelerator ordinal (e.g. ``torch.device.index``)
344 to a physical device ID.
345
346 This is the inverse of the env-var translation performed by
347 logical_device_id_to_visible_device_id() and is independent of any
348 logical-to-physical mapping set via set_assigned_physical_gpu_ids().
349 """
350 device_control_env = os.environ.get(cls.device_control_env_var, "")
351 if not device_control_env:
352 return device_id
353 visible_device_ids = device_control_env.split(",")
354 if device_id >= len(visible_device_ids):
355 raise IndexError(
356 f"visible device ordinal {device_id} is out of range for "
357 f"{cls.device_control_env_var}={device_control_env}"
358 )
359 return cls.device_control_id_to_physical_device_id(
360 visible_device_ids[device_id]
361 )
362
363 @classmethod
364 def import_kernels(cls) -> None:
365 """Import any platform-specific C kernels."""
366 try:
367 import vllm._C # noqa: F401
368 except ImportError as e:
369 logger.warning_once("Failed to import from vllm._C: %s", repr(e))
370 with contextlib.suppress(ImportError):
371 import vllm._moe_C_stable_libtorch # noqa: F401
372
373 @classmethod
374 def register_triton_kernel_overrides(cls) -> None:
375 """Override core Triton kernels with platform implementations.
376
377 Platforms whose Triton kernels need replacement implementations
378 (e.g. CPU fallbacks when the Triton-CPU backend is unavailable)
379 override this method and call
380 :func:`vllm.triton_utils.dispatcher.register_kernels`
381 with their overrides. It runs during model runner init, before any
382 kernel launch. The default implementation does nothing.
383 """
384 return
385
386 @classmethod
387 def get_attn_backend_cls(
388 cls,
389 selected_backend: "AttentionBackendEnum",
390 attn_selector_config: "AttentionSelectorConfig",
391 num_heads: int | None = None,
392 ) -> str:
393 """Get the attention backend class of a device."""
394 return ""
395
396 @classmethod
397 def get_supported_vit_attn_backends(cls) -> list["AttentionBackendEnum"]:
398 return [
399 AttentionBackendEnum.TORCH_SDPA,
400 ]
401
402 @classmethod
403 def get_vit_attn_backend(
404 cls,
405 head_size: int,
406 dtype: torch.dtype,
407 backend: "AttentionBackendEnum | None" = None,
408 ) -> "AttentionBackendEnum":
409 """Get the vision attention backend class of a device.
410
411 NOTE: ViT Attention should be checked and override in the platform-specific
412 implementation. we should not override this in any other places, like
413 the model_executor/models/<model_name>.py.
414
415 We check if the backend is None or not:
416 1. If not, check if the backend is supported by the platform.
417 2. If None, continue to the default selection logic.
418 """
419 if backend is not None:
420 assert backend in cls.get_supported_vit_attn_backends(), (
421 f"Backend {backend} is not supported for vit attention"
422 f"Supported backends are: {cls.get_supported_vit_attn_backends()}"
423 )
424 logger.info_once(f"Using backend {backend} for vit attention")
425 return backend
426
427 logger.info_once(
428 f"Using default backend {AttentionBackendEnum.TORCH_SDPA} for vit attention"
429 )
430 return AttentionBackendEnum.TORCH_SDPA
431
432 @classmethod
433 def get_device_capability(
434 cls,
435 device_id: int = 0,
436 ) -> DeviceCapability | None:
437 """Stateless version of [torch.cuda.get_device_capability][].
438
439 Args:
440 device_id: Device index in the visible device namespace, matching
441 the argument accepted by torch.cuda.
442
443 """
444 return None
445
446 @classmethod
447 def has_device_capability(
448 cls,
449 capability: tuple[int, int] | int,
450 device_id: int = 0,
451 ) -> bool:
452 """Test whether this platform is compatible with a device capability.
453
454 The `capability` argument can either be:
455
456 - A tuple `(major, minor)`.
457 - An integer `<major><minor>`. (See
458 [`DeviceCapability.to_int`][vllm.platforms.interface.DeviceCapability.to_int])
459 """
460 current_capability = cls.get_device_capability(device_id=device_id)
461 if current_capability is None:
462 return False
463
464 if isinstance(capability, tuple):
465 return current_capability >= capability
466
467 return current_capability.to_int() >= capability
468
469 @classmethod
470 def is_device_capability(
471 cls,
472 capability: tuple[int, int] | int,
473 device_id: int = 0,
474 ) -> bool:
475 """Test whether this platform has exactly the specified device capability.
476
477 The `capability` argument can either be:
478
479 - A tuple `(major, minor)`.
480 - An integer `<major><minor>`. (See
481 [`DeviceCapability.to_int`][vllm.platforms.interface.DeviceCapability.to_int])
482 """
483 current_capability = cls.get_device_capability(device_id=device_id)
484 if current_capability is None:
485 return False
486
487 if isinstance(capability, tuple):
488 return current_capability == capability
489
490 return current_capability.to_int() == capability
491
492 @classmethod
493 def is_device_capability_family(
494 cls,
495 capability: int,
496 device_id: int = 0,
497 ) -> bool:
498 """Returns True if the device capability is any <major>.x.
499 Mirrors CUDA 13 'family' architecture semantics (e.g. 10.x, 11.x, 12.x).
500 """
501 current_capability = cls.get_device_capability(device_id=device_id)
502 if current_capability is None:
503 return False
504 return (current_capability.to_int() // 10) == (capability // 10)
505
506 @classmethod
507 def get_device_name(cls, device_id: int = 0) -> str:
508 """Get the name of a device."""
509 raise NotImplementedError
510
511 @classmethod
512 def get_device_uuid(cls, device_id: int = 0) -> str:
513 """Get the uuid of a device, e.g. the PCI bus ID."""
514 raise NotImplementedError
515
516 @classmethod
517 def get_device_total_memory(cls, device_id: int = 0) -> int:
518 """Get the total memory of a device in bytes."""
519 raise NotImplementedError
520
521 @classmethod
522 def get_all_gpu_pci_bus_ids(cls) -> dict[int, str]:
523 """Return a mapping of device index to PCI bus ID string.
524
525 Used by ``VLLM_GPU_NIC_PCIE_MAPPING`` for RDMA NIC selection.
526 Subclasses should override with platform-specific discovery
527 (e.g. pynvml for CUDA).
528 """
529 raise NotImplementedError(
530 "VLLM_GPU_NIC_PCIE_MAPPING is not supported on the "
531 f"current platform ({cls.device_name})"
532 )
533
534 @classmethod
535 def inference_mode(cls):
536 """A device-specific wrapper of `torch.inference_mode`.
537
538 This wrapper is recommended because some hardware backends such as TPU
539 do not support `torch.inference_mode`. In such a case, they will fall
540 back to `torch.no_grad` by overriding this method.
541 """
542 return torch.inference_mode(mode=True)
543
544 @classmethod
545 def set_device(cls, device: torch.device) -> None:
546 """Set the device for the current platform."""
547 raise NotImplementedError
548
549 @classmethod
550 def manual_seed_all(cls, seed: int) -> None:
551 """Set RNG seed across all devices for the current platform."""
552 raise NotImplementedError
553
554 @classmethod
555 def pre_register_and_update(
556 cls, parser: FlexibleArgumentParser | None = None
557 ) -> None:
558 """Do some pre-registration or update action for the current platform.
559
560 This function is called before global VllmConfig is initialized or cli
561 arguments are parsed. It's used for out-of-tree platforms to register or
562 update the configuration.
563
564 For example, the out-of-tree quantization config can be imported and
565 registered here dynamically.
566 """
567 pass
568
569 @classmethod
570 def apply_config_platform_defaults(cls, vllm_config: "VllmConfig") -> None:
571 """Apply the platform-specific default values to the config.
572
573 This function is called during the initialization of global VllmConfig, after
574 parsing cli arguments.
575 It can modify the defaults of the config according to the platform. For example,
576 it can enable custom_ops based on the enabled features.
577
578 The config is passed by reference, so it can be modified in place.
579 """
580 pass
581
582 @classmethod
583 def check_and_update_config(cls, vllm_config: "VllmConfig") -> None:
584 """Check and update the configuration for the current platform.
585
586 It can raise an exception if the configuration is not compatible with
587 the current platform, or it can update the configuration to make it
588 compatible with the current platform.
589
590 The config is passed by reference, so it can be modified in place.
591 """
592 pass
593
594 @classmethod
595 def _find_non_ssm_backends(
596 cls, vllm_config: "VllmConfig"
597 ) -> "list[type[AttentionBackend]]":
598 """Distinct non-SSM attention backends, in layer order."""
599 from vllm.config.vllm import get_layers_from_vllm_config
600 from vllm.model_executor.layers.attention_layer_base import (
601 AttentionLayerBase,
602 )
603
604 attn_layers = get_layers_from_vllm_config(
605 vllm_config,
606 AttentionLayerBase, # type: ignore[type-abstract]
607 )
608 backends: list[type[AttentionBackend]] = []
609 for layer in attn_layers.values():
610 b = layer.get_attn_backend()
611 if not b.is_ssm() and b not in backends:
612 backends.append(b)
613 return backends
614
615 @classmethod
616 def _preferred_block_size_for_backends(
617 cls,
618 backend_classes: "list[type[AttentionBackend]]",
619 default_block_size: int,
620 vllm_config: "VllmConfig",
621 ) -> int:
622 """Smallest block size every backend accepts.
623
624 ``supports_block_size`` may be overridden to accept exact sizes only
625 (CPU_MLA takes 16 and no multiple of it), so candidates are the LCMs of
626 one supported size per backend, each checked against every backend.
627
628 Raises:
629 ValueError: If the backends share no supported block size.
630
631 """
632 from vllm.config.vllm import set_current_vllm_config
633 from vllm.v1.attention.backend import MultipleOf
634
635 # Backends may read the current config to decide a preference.
636 with set_current_vllm_config(vllm_config):
637 if len(backend_classes) == 1:
638 return backend_classes[0].get_preferred_block_size(default_block_size)
639 if all(b.supports_block_size(default_block_size) for b in backend_classes):
640 return default_block_size
641 # A backend declaring no sizes accepts any, so it contributes 1.
642 per_backend_sizes = [
643 [
644 s.base if isinstance(s, MultipleOf) else s
645 for s in b.get_supported_kernel_block_sizes()
646 ]
647 or [1]
648 for b in backend_classes
649 ]
650 candidates = sorted(
651 {math.lcm(*sizes) for sizes in itertools.product(*per_backend_sizes)}
652 )
653 for candidate in candidates:
654 if all(b.supports_block_size(candidate) for b in backend_classes):
655 return candidate
656 raise ValueError(
657 "The attention backends share no supported KV cache block size ("
658 + "; ".join(
659 f"{b.get_name()}: {b.get_supported_kernel_block_sizes()}"
660 for b in backend_classes
661 )
662 + ")."
663 )
664
665 @classmethod
666 def update_block_size_for_backend(cls, vllm_config: "VllmConfig") -> None:
667 """Ensure block_size is compatible with the attention backend.
668 For hybrid models, also aligns block_size with mamba page sizes.
669 """
670 from vllm.config.cache import CacheConfig
671
672 cache_config = vllm_config.cache_config
673 model_config = vllm_config.model_config
674
675 # model_config may be None during testing.
676 if not model_config:
677 return
678
679 backend_classes = cls._find_non_ssm_backends(vllm_config)
680 if not backend_classes:
681 return
682
683 # Phase 1: Pick a block size every attention backend supports (skip if
684 # user set --block-size). Models can mix backends with disjoint
685 # preferences, and a size from the first alone later fails
686 # select_common_block_size().
687 if not cache_config.user_specified_block_size:
688 preferred = cls._preferred_block_size_for_backends(
689 backend_classes, CacheConfig.DEFAULT_BLOCK_SIZE, vllm_config
690 )
691 if preferred != CacheConfig.DEFAULT_BLOCK_SIZE:
692 logger.info(
693 "Setting kv cache block size to %d for %s backend(s).",
694 preferred,
695 "/".join(b.get_name() for b in backend_classes),
696 )
697 cache_config.block_size = preferred
698
699 # Phase 2: Align block/mamba sizes for hybrid models
700 # (may override user settings).
701 if model_config.is_hybrid:
702 cls._align_hybrid_block_size(vllm_config, backend_classes[0])
703
704 # Phase 3: Align block/page sizes when multiple KV dtypes share the
705 # block pool (e.g. nvfp4 primary + unquantized skip layers).
706 # May override the user's --block-size.
707 if cache_config.kv_cache_dtype_skip_layers:
708 cls._align_heterogeneous_kv_block_size(vllm_config, backend_classes[0])
709
710 @classmethod
711 def _align_heterogeneous_kv_block_size(
712 cls,
713 vllm_config: "VllmConfig",
714 backend_cls: "type[AttentionBackend]",
715 ) -> None:
716 """Align block size when several KV dtypes share one block pool.
717
718 A quantized primary (e.g. nvfp4) shares the block pool with one or more
719 higher-precision "padded specs" (skip layers today; the first/last-N
720 sibling in the future). A padded spec's per-token page is larger than
721 the primary's *and not an integer multiple of it*, so the trivial
722 ``unify_kv_cache_spec_page_size`` cannot reconcile them. We do it here
723 instead, before the specs are built:
724
725 1. Bump the primary ``block_size`` (kernel-aligned) until the primary
726 page is large enough to cover the largest padded-spec page.
727 2. Record that shared page in each padded spec's ``*_page_size_padded``
728 hint, so it pads up to the shared page.
729
730 ``unify`` then sees equal pages and stays trivial.
731
732 To add a padded-spec type: append its per-token page to ``padded_pages``
733 and set its ``*_page_size_padded`` hint below.
734 """
735 from vllm.config.vllm import set_current_vllm_config
736 from vllm.utils.math_utils import cdiv
737 from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE
738 from vllm.v1.attention.backend import MultipleOf
739 from vllm.v1.kv_cache_interface import FullAttentionSpec, get_kv_quant_mode
740
741 cache_config = vllm_config.cache_config
742 model_config = vllm_config.model_config
743 parallel_config = vllm_config.parallel_config
744 if not model_config:
745 return
746
747 def per_token_page_bytes(dtype: "torch.dtype", cache_dtype: str) -> int:
748 """Bytes one token occupies in one layer, for the given dtype."""
749 spec = FullAttentionSpec(
750 block_size=1,
751 num_kv_heads=model_config.get_num_kv_heads(parallel_config),
752 head_size=model_config.get_head_size(),
753 dtype=dtype,
754 kv_quant_mode=get_kv_quant_mode(cache_dtype),
755 )
756 # The backend owns its packing
757 return backend_cls.customize_spec(spec).page_size_bytes
758
759 primary_dtype = (
760 STR_DTYPE_TO_TORCH_DTYPE[cache_config.cache_dtype]
761 if cache_config.cache_dtype != "auto"
762 else model_config.dtype
763 )
764 primary_page = per_token_page_bytes(primary_dtype, cache_config.cache_dtype)
765
766 # Per-token page of every higher-precision padded spec sharing the pool.
767 padded_pages: list[int] = []
768 if cache_config.kv_cache_dtype_skip_layers:
769 padded_pages.append(per_token_page_bytes(model_config.dtype, "auto"))
770 # To add the first/last-N sibling:
771 # padded_pages.append(per_token_page_bytes(<sibling_dtype>, "auto"))
772 if not padded_pages:
773 return
774
775 largest_padded_page = max(padded_pages)
776 assert largest_padded_page >= primary_page, (
777 f"padded-spec per-token page ({largest_padded_page}B) < primary "
778 f"({primary_page}B); a higher-precision padded spec must not be "
779 "smaller than the quantized primary."
780 )
781 if largest_padded_page == primary_page:
782 # Pages already match per token; ``unify`` reconciles the differing
783 # block sizes by integer scaling, so no bump or padding is needed.
784 return
785
786 # Smallest block the kernel supports, and the granularity the primary
787 # block is rounded up to (never below the already-chosen block_size).
788 with set_current_vllm_config(vllm_config):
789 supported = backend_cls.get_supported_kernel_block_sizes()
790 smallest_kernel_block = min(
791 s.base if isinstance(s, MultipleOf) else s for s in supported
792 )
793 block_alignment = max(smallest_kernel_block, cache_config.block_size)
794
795 # Bytes one padded-spec page spans at its own smallest kernel block;
796 # also cover any mamba page a hybrid model already padded.
797 required_page = max(
798 largest_padded_page * smallest_kernel_block,
799 cache_config.mamba_page_size_padded or 0,
800 )
801
802 # Smallest kernel-aligned primary block whose page covers required_page.
803 primary_block_size = block_alignment * cdiv(
804 required_page, block_alignment * primary_page
805 )
806 if cache_config.block_size < primary_block_size:
807 cache_config.block_size = primary_block_size
808 logger.info(
809 "Setting attention block size to %d tokens so the quantized "
810 "primary KV page covers the higher-precision padded-spec page.",
811 primary_block_size,
812 )
813
814 # The shared page that every padded spec (and mamba) pads up to.
815 shared_page = cache_config.block_size * primary_page
816 if cache_config.kv_cache_dtype_skip_layers:
817 cache_config.skip_page_size_padded = shared_page
818 # To add the first/last-N sibling:
819 # cache_config.sibling_page_size_padded = shared_page
820 if cache_config.mamba_page_size_padded is not None:
821 cache_config.mamba_page_size_padded = shared_page
822
823 @classmethod
824 def _get_indexer_block_alignment(cls, vllm_config: "VllmConfig") -> int | None:
825 """Extra ``block_size`` multiple a sparse indexer needs, else ``None``.
826
827 The CUDA kpool paged-MQA indexer virtually splits each storage block
828 into pool pages, so ``block_size`` must be a multiple of
829 ``index_kpool * min(PAGED_MQA_PAGE_SIZES)`` — implemented in the CUDA
830 platform override. Other platforms impose no extra constraint.
831 """
832 return None
833
834 @classmethod
835 def _align_hybrid_block_size(
836 cls,
837 vllm_config: "VllmConfig",
838 backend_cls: "type[AttentionBackend]",
839 ) -> None:
840 """For hybrid attention/mamba models, ensure that the attention page
841 size is >= the mamba page size, and pad the mamba page size to match.
842 """
843 from math import lcm
844
845 from vllm.config.vllm import set_current_vllm_config
846 from vllm.model_executor.models import ModelRegistry
847 from vllm.utils.math_utils import cdiv
848 from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE
849 from vllm.v1.attention.backend import MultipleOf
850 from vllm.v1.kv_cache_interface import (
851 FullAttentionSpec,
852 MambaSpec,
853 MLAAttentionSpec,
854 get_kv_quant_mode,
855 )
856
857 cache_config = vllm_config.cache_config
858 model_config = vllm_config.model_config
859 parallel_config = vllm_config.parallel_config
860
861 if cache_config.cache_dtype == "auto":
862 kv_cache_dtype = model_config.dtype
863 else:
864 kv_cache_dtype = STR_DTYPE_TO_TORCH_DTYPE[cache_config.cache_dtype]
865
866 kv_quant_mode = get_kv_quant_mode(cache_config.cache_dtype)
867
868 # Compute attention page size for 1 token
869 if model_config.use_mla:
870 attn_page_size_1_token = MLAAttentionSpec(
871 block_size=1,
872 num_kv_heads=model_config.get_num_kv_heads(parallel_config),
873 head_size=model_config.get_head_size(),
874 dtype=kv_cache_dtype,
875 cache_dtype_str=cache_config.cache_dtype,
876 kv_quant_mode=kv_quant_mode,
877 ).page_size_bytes
878 elif cache_config.cache_dtype.startswith("turboquant_"):
879 # TQ has a packed K|V layout; the standard FullAttentionSpec
880 # formula over-sizes it and trips unify_kv_cache_spec_page_size
881 # when all attention layers are TQ. With mixed skip+TQ the skip
882 # layers still use the standard layout — take max so mamba
883 # padding covers the largest actual page.
884 from vllm.v1.attention.backends.turboquant_attn import (
885 TurboQuantAttentionBackend,
886 )
887
888 tq_spec = FullAttentionSpec(
889 block_size=1,
890 num_kv_heads=model_config.get_num_kv_heads(parallel_config),
891 head_size=model_config.get_head_size(),
892 dtype=kv_cache_dtype,
893 kv_quant_mode=kv_quant_mode,
894 )
895 tq_page = TurboQuantAttentionBackend.customize_spec(tq_spec).page_size_bytes
896 if cache_config.kv_cache_dtype_skip_layers:
897 skip_page = FullAttentionSpec(
898 block_size=1,
899 num_kv_heads=model_config.get_num_kv_heads(parallel_config),
900 head_size=model_config.get_head_size(),
901 dtype=model_config.dtype,
902 ).page_size_bytes
903 # lcm, not max: skip_page is often not a multiple of
904 # tq_page, so max would leave per-layer page sizes
905 # un-unifiable downstream.
906 attn_page_size_1_token = lcm(tq_page, skip_page)
907 else:
908 attn_page_size_1_token = tq_page
909 else:
910 attn_spec = FullAttentionSpec(
911 block_size=1,
912 num_kv_heads=model_config.get_num_kv_heads(parallel_config),
913 head_size=model_config.get_head_size(),
914 dtype=kv_cache_dtype,
915 kv_quant_mode=kv_quant_mode,
916 )
917 attn_page_size_1_token = backend_cls.customize_spec(
918 attn_spec
919 ).page_size_bytes
920
921 # Compute mamba page size
922 model_cls, _ = ModelRegistry.resolve_model_cls(
923 model_config.architecture,
924 model_config=model_config,
925 )
926 # Qwen4Exp has multiple Mamba state layouts with different sizes.
927 if hasattr(model_cls, "get_mamba_specs_from_config"):
928 mamba_page_size = max(
929 spec.page_size_bytes
930 for spec in model_cls.get_mamba_specs_from_config(vllm_config)
931 )
932 else:
933 mamba_page_size = MambaSpec(
934 shapes=model_cls.get_mamba_state_shape_from_config(vllm_config),
935 dtypes=model_cls.get_mamba_state_dtype_from_config(vllm_config),
936 block_size=-1,
937 ).page_size_bytes
938
939 if mamba_page_size == 0:
940 return
941
942 # Get kernel block alignment from the backend's supported sizes
943 with set_current_vllm_config(vllm_config):
944 kernel_block_alignment_size = max(
945 min(
946 s.base if isinstance(s, MultipleOf) else s
947 for s in backend_cls.get_supported_kernel_block_sizes()
948 ),
949 cache_config.block_size,
950 )
951 if model_config.use_mla:
952 # TRTLLM/FlashInfer MLA decode kernels require the physical
953 # number of kernel blocks to be aligned to 128 / kernel_block_size.
954 # For hybrid MLA/Mamba models, make the manager block size a
955 # multiple of 128 so split kernel blocks keep that invariant.
956 kernel_block_alignment_size = max(kernel_block_alignment_size, 128)
957
958 # Use minimum block size that satisfies both backend alignment and
959 # mamba page size compatibility
960 attn_block_size = kernel_block_alignment_size * cdiv(
961 mamba_page_size,
962 kernel_block_alignment_size * attn_page_size_1_token,
963 )
964 indexer_align = cls._get_indexer_block_alignment(vllm_config)
965 if indexer_align:
966 attn_block_size = indexer_align * cdiv(attn_block_size, indexer_align)
967
968 if cache_config.block_size < attn_block_size:
969 cache_config.block_size = attn_block_size
970 logger.info(
971 "Setting attention block size to %d tokens "
972 "to ensure that attention page size is >= mamba page size.",
973 attn_block_size,
974 )
975
976 if cache_config.mamba_cache_mode == "align":
977 cache_config.mamba_block_size = cache_config.block_size
978
979 # Pad mamba page size to exactly match attention page size
980 attn_page_size = cache_config.block_size * attn_page_size_1_token
981 assert attn_page_size >= mamba_page_size
982
983 if attn_page_size == mamba_page_size:
984 return
985
986 if (
987 cache_config.mamba_page_size_padded is None
988 or cache_config.mamba_page_size_padded != attn_page_size
989 ):
990 cache_config.mamba_page_size_padded = attn_page_size
991 mamba_padding_pct = (
992 100 * (attn_page_size - mamba_page_size) / mamba_page_size
993 )
994 logger.info(
995 "Padding mamba page size by %.2f%% to ensure "
996 "that mamba page size and attention page size are "
997 "exactly equal.",
998 mamba_padding_pct,
999 )
1000
1001 @classmethod
1002 def register_custom_kv_cache_specs(cls, vllm_config: "VllmConfig") -> None:
1003 """Register custom KVCacheSpec class on current platform."""
1004 pass
1005
1006 @classmethod
1007 def verify_model_arch(cls, model_arch: str) -> None:
1008 """Verify whether the current platform supports the specified model
1009 architecture.
1010
1011 - This will raise an Error or Warning based on the model support on
1012 the current platform.
1013 - By default all models are considered supported.
1014 """
1015 pass
1016
1017 @classmethod
1018 def verify_quantization(cls, quant: str) -> None:
1019 """Verify whether the quantization is supported by the current platform."""
1020 if cls.supported_quantization and quant not in cls.supported_quantization:
1021 raise ValueError(
1022 f"{quant} quantization is currently not supported in {cls.device_name}."
1023 )
1024
1025 @classmethod
1026 def get_cpu_architecture(cls) -> CpuArchEnum:
1027 """Determine the CPU architecture of the current system.
1028 Returns CpuArchEnum indicating the architecture type.
1029 """
1030 machine = platform.machine().lower()
1031
1032 if machine in ("x86_64", "amd64", "i386", "i686"):
1033 return CpuArchEnum.X86
1034 elif machine.startswith("arm") or machine.startswith("aarch"):
1035 return CpuArchEnum.ARM
1036 elif machine.startswith("ppc"):
1037 return CpuArchEnum.POWERPC
1038 elif machine == "s390x":
1039 return CpuArchEnum.S390X
1040 elif machine.startswith("riscv"):
1041 return CpuArchEnum.RISCV
1042
1043 return CpuArchEnum.OTHER if machine else CpuArchEnum.UNKNOWN
1044
1045 @classmethod
1046 def is_pin_memory_available(cls) -> bool:
1047 """Checks whether pin memory is available on the current platform."""
1048 if in_wsl():
1049 # https://docs.nvidia.com/cuda/wsl-user-guide/index.html#known-limitations-for-linux-cuda-applications
1050 # Pinned memory support under WSL depends on the vendor and driver
1051 # version. Conservative default: return False. Platform subclasses
1052 # that can verify support (e.g. CudaPlatformBase) override this.
1053 # warning_once() causes a circular import on WSL, see #48397.
1054 logger.warning(
1055 "Using 'pin_memory=False' as WSL is detected. "
1056 "This may slow down performance."
1057 )
1058 return False
1059 return True
1060
1061 @classmethod
1062 def get_current_memory_usage(
1063 cls, device: torch.types.Device | None = None
1064 ) -> float:
1065 """Return the memory usage in bytes."""
1066 raise NotImplementedError
1067
1068 @classmethod
1069 def get_punica_wrapper(cls) -> str:
1070 """Return the punica wrapper for current platform."""
1071 raise NotImplementedError
1072
1073 @classmethod
1074 def get_infinity_values(cls, dtype: torch.dtype) -> tuple[float, float]:
1075 """Return the platform specific values for (-inf, inf)."""
1076 return float("-inf"), float("inf")
1077
1078 @classmethod
1079 def can_update_inplace(cls) -> bool:
1080 """Checks if the platform allows inplace memory updates."""
1081 return True
1082
1083 @classmethod
1084 def get_lora_vocab_padding_size(cls) -> int:
1085 """Returns how much padding the LoRA logits need for kernels."""
1086 return 256
1087
1088 @classmethod
1089 def get_device_communicator_cls(cls) -> str:
1090 """Get device specific communicator class for distributed communication."""
1091 return "vllm.distributed.device_communicators.base_device_communicator.DeviceCommunicatorBase" # noqa
1092
1093 @classmethod
1094 def is_integrated_gpu(cls, device_id: int = 0) -> bool:
1095 """Returns whether the GPU is an integrated (UMA) device that shares
1096 system memory with the CPU.
1097
1098 On UMA systems (e.g. NVIDIA GH200, DGX Spark, Jetson Orin),
1099 cudaMemGetInfo may underreport free memory because it does not
1100 account for reclaimable OS memory (page cache, buffers).
1101 """
1102 return False
1103
1104 @classmethod
1105 def enable_multi_stream_overlap(
1106 cls,
1107 aux_stream_list: list[torch.cuda.Stream] | None,
1108 attn_metadata: object,
1109 ) -> bool:
1110 """Whether the current platform should enable multi-stream overlap
1111 for the given aux streams and attention metadata."""
1112 return False
1113
1114 @classmethod
1115 def supports_mx(cls) -> bool:
1116 """Returns whether the current platform supports MX types."""
1117 return False
1118
1119 @classmethod
1120 def supports_fp8(cls) -> bool:
1121 """Returns whether the current platform supports FP8 types."""
1122 return False
1123
1124 @classmethod
1125 def is_fp8_fnuz(cls) -> bool:
1126 """Returns whether the preferred FP8 type is FNUZ on the current platform.
1127
1128 There are two representations of FP8, OCP FP8 and FNUZ FP8.
1129 The OCP specification can be found at https://tinyurl.com/b7jvwpft.
1130 The FNUZ specification can be found at https://tinyurl.com/5n6hwwu5.
1131
1132 AMD's MI300 and MI325 have native hardware support for FNUZ. All other
1133 hardware has converged on the OCP FP8 standard.
1134 """
1135 return False
1136
1137 @classmethod
1138 def fp8_dtype(cls) -> torch.dtype:
1139 """Returns the preferred FP8 type on the current platform.
1140
1141 See the documentation for is_fp8_fnuz for details.
1142 """
1143 return torch.float8_e4m3fn
1144
1145 @classmethod
1146 def use_all_gather(cls) -> bool:
1147 """Whether to use allgather in LogitsProcessor to gather the logits."""
1148 return True
1149
1150 @classmethod
1151 def use_custom_allreduce(cls) -> bool:
1152 """Returns if custom allreduce is supported on the current platform."""
1153 return False
1154
1155 @classmethod
1156 def opaque_attention_op(cls) -> bool:
1157 """Returns True if we register attention as one giant opaque custom op
1158 on the current platform
1159 """
1160 return False
1161
1162 @classmethod
1163 def validate_request(
1164 cls,
1165 processed_inputs: "EngineInput",
1166 params: "SamplingParams | PoolingParams",
1167 ) -> None:
1168 """Raises if this request is unsupported on this platform."""
1169
1170 def __getattr__(self, key: str):
1171 # Pickle checks dunder methods like __getstate__. If we return None
1172 # for them, pickle treats it like a real value and tries to call it.
1173 if key.startswith("__") and key.endswith("__"):
1174 raise AttributeError(key)
1175
1176 device = getattr(torch, self.device_type, None)
1177 if device is not None and hasattr(device, key):
1178 attr = getattr(device, key)
1179 # NOTE: `hasattr(device, key)=True` can only avoid AttributeError,
1180 # but the value of this attr could be `None`.
1181 if attr is not None:
1182 return attr
1183
1184 logger.warning_once(
1185 "Current platform %s does not have '%s' attribute.",
1186 self.device_type,
1187 key,
1188 )
1189 return None
1190
1191 def get_global_graph_pool(self) -> Any:
1192 """Return the global graph pool for this platform."""
1193 cls = self.__class__
1194 if cls._global_graph_pool is None:
1195 cls._global_graph_pool = self.graph_pool_handle()
1196 return cls._global_graph_pool
1197
1198 @classmethod
1199 def get_static_graph_wrapper_cls(cls) -> str:
1200 """Get static graph wrapper class for static graph."""
1201 return "vllm.compilation.base_static_graph.AbstractStaticGraphWrapper"
1202
1203 @classmethod
1204 def stateless_init_device_torch_dist_pg(
1205 cls,
1206 backend: str,
1207 prefix_store: "PrefixStore",
1208 group_rank: int,
1209 group_size: int,
1210 timeout: timedelta,
1211 ) -> "ProcessGroup":
1212 """Init platform-specific torch distributed process group."""
1213 raise NotImplementedError
1214
1215 @classmethod
1216 def check_if_supports_dtype(cls, dtype: torch.dtype):
1217 """Check if the dtype is supported by the current platform."""
1218 raise NotImplementedError
1219
1220 @classmethod
1221 def support_hybrid_kv_cache(cls) -> bool:
1222 """Returns if the hybrid kv cache is supported by the current platform."""
1223 return False
1224
1225 @classmethod
1226 def support_static_graph_mode(cls) -> bool:
1227 """Returns if the graph mode is supported by the current platform."""
1228 return False
1229
1230 @classmethod
1231 def check_runner_kv_caches_multi_layer(cls) -> None:
1232 """Check whether the platform's ModelRunner can handle multiple attention
1233 layers that share the same layer index (e.g. cross attention and self
1234 attention in the same decoder block of an encoder-decoder model such as
1235 BART).
1236
1237 Platforms that have verified that their ``runner_kv_caches`` is not
1238 impacted by this case should override this to a no-op. Otherwise the
1239 default implementation raises ``NotImplementedError``.
1240 """
1241 raise NotImplementedError(
1242 "Multiple attention layers with the same layer index are not "
1243 "supported on the current platform."
1244 )
1245
1246 @classmethod
1247 def support_deep_gemm(cls) -> bool:
1248 """Returns if DeepGEMM is supported by the current platform."""
1249 return False
1250
1251 @classmethod
1252 def use_custom_op_collectives(cls) -> bool:
1253 """Whether this platform should use torch.ops.vllm.* custom ops for collectives.
1254
1255 Returns False by default - platforms must explicitly opt-in.
1256 """
1257 return False
1258
1259 @classmethod
1260 def use_sync_weight_loader(cls) -> bool:
1261 """Returns if the current platform needs to sync weight loader."""
1262 return False
1263
1264 @classmethod
1265 def make_synced_weight_loader(cls, original_weight_loader):
1266 """Wrap the original weight loader to make it synced."""
1267 if not cls.use_sync_weight_loader():
1268 return original_weight_loader
1269
1270 def _synced_weight_loader(param, *args, **kwargs):
1271 out = original_weight_loader(param, *args, **kwargs)
1272 if param.device != torch.device("cpu"):
1273 torch._sync(param)
1274 return out
1275
1276 return _synced_weight_loader
1277
1278 @classmethod
1279 def get_nixl_supported_devices(cls) -> dict[str, tuple[str, ...]]:
1280 """Returns a mapping from device_type to a tuple of supported
1281 kv_buffer_device for nixl.
1282 """
1283 return {}
1284
1285 @classmethod
1286 def get_nixl_memory_type(cls) -> str | None:
1287 """Returns the nixl memory type for the current platform."""
1288 return None
1289
1290 @classmethod
1291 def check_max_model_len(cls, max_model_len: int) -> int:
1292 """Check max_model_len for the current platform."""
1293 return max_model_len
1294
1295 @classmethod
1296 def set_additional_forward_context(cls, *args, **kwargs) -> dict[str, Any]:
1297 """Set some additional forward context for the current platform if needs."""
1298 return {}
1299
1300 @classmethod
1301 def num_compute_units(cls, device_id: int = 0) -> int:
1302 """Get the number of compute units for the current platform.
1303 (NVIDIA SM / AMD CU / Intel EU)
1304 """
1305 raise NotImplementedError(
1306 "num_compute_units is not implemented for the current platform."
1307 )
1308
1309 @classmethod
1310 def get_default_ir_op_priority(
1311 cls, vllm_config: "VllmConfig"
1312 ) -> "IrOpPriorityConfig":
1313 """Get the default IR op priority for the current platform."""
1314 from vllm.config.kernel import IrOpPriorityConfig
1315
1316 # Native always used by default. Platforms can override this behavior.
1317 return IrOpPriorityConfig.with_default(["native"])
1318
1319 @classmethod
1320 def is_arch_support_pdl(cls) -> bool:
1321 """Does the current platform support PDL (Programmatic Dependent Launch)?"""
1322 return False
1323
1324 @classmethod
1325 def validate_environ(cls, hard_fail: bool) -> None:
1326 """Validate environment variables for the current platform."""
1327 from vllm import envs
1328
1329 for env in os.environ:
1330 if env.startswith("VLLM_") and env not in envs.environment_variables:
1331 if hard_fail:
1332 raise ValueError(
1333 f"Unknown vLLM environment variable detected: {env}"
1334 )
1335 else:
1336 logger.warning(
1337 "Unknown vLLM environment variable detected: %s", env
1338 )
1339
1340
1341 class UnspecifiedPlatform(Platform):
1342 _enum = PlatformEnum.UNSPECIFIED
1343 device_type = ""