chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,92 @@
from typing import Optional, Set
from ray._private.accelerators.accelerator import (
RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO_ENV_VAR,
AcceleratorManager,
)
from ray._private.accelerators.amd_gpu import AMDGPUAcceleratorManager
from ray._private.accelerators.furiosa import FuriosaAcceleratorManager
from ray._private.accelerators.hpu import HPUAcceleratorManager
from ray._private.accelerators.intel_gpu import IntelGPUAcceleratorManager
from ray._private.accelerators.metax_gpu import MetaxGPUAcceleratorManager
from ray._private.accelerators.neuron import NeuronAcceleratorManager
from ray._private.accelerators.npu import NPUAcceleratorManager
from ray._private.accelerators.nvidia_gpu import NvidiaGPUAcceleratorManager
from ray._private.accelerators.rbln import RBLNAcceleratorManager
from ray._private.accelerators.tpu import TPUAcceleratorManager
def get_all_accelerator_managers() -> Set[AcceleratorManager]:
"""Get all accelerator managers supported by Ray."""
return {
NvidiaGPUAcceleratorManager,
IntelGPUAcceleratorManager,
AMDGPUAcceleratorManager,
TPUAcceleratorManager,
NeuronAcceleratorManager,
HPUAcceleratorManager,
NPUAcceleratorManager,
RBLNAcceleratorManager,
MetaxGPUAcceleratorManager,
FuriosaAcceleratorManager,
}
def get_all_accelerator_resource_names() -> Set[str]:
"""Get all resource names for accelerators."""
return {
accelerator_manager.get_resource_name()
for accelerator_manager in get_all_accelerator_managers()
}
def get_accelerator_manager_for_resource(
resource_name: str,
) -> Optional[AcceleratorManager]:
"""Get the corresponding accelerator manager for the given
accelerator resource name
E.g., TPUAcceleratorManager is returned if resource name is "TPU"
"""
try:
return get_accelerator_manager_for_resource._resource_name_to_accelerator_manager.get( # noqa: E501
resource_name, None
)
except AttributeError:
# Lazy initialization.
resource_name_to_accelerator_manager = {
accelerator_manager.get_resource_name(): accelerator_manager
for accelerator_manager in get_all_accelerator_managers()
}
# Special handling for GPU resource name since multiple accelerator managers
# have the same GPU resource name.
if AMDGPUAcceleratorManager.get_current_node_num_accelerators() > 0:
resource_name_to_accelerator_manager["GPU"] = AMDGPUAcceleratorManager
elif IntelGPUAcceleratorManager.get_current_node_num_accelerators() > 0:
resource_name_to_accelerator_manager["GPU"] = IntelGPUAcceleratorManager
elif MetaxGPUAcceleratorManager.get_current_node_num_accelerators() > 0:
resource_name_to_accelerator_manager["GPU"] = MetaxGPUAcceleratorManager
else:
resource_name_to_accelerator_manager["GPU"] = NvidiaGPUAcceleratorManager
get_accelerator_manager_for_resource._resource_name_to_accelerator_manager = (
resource_name_to_accelerator_manager
)
return resource_name_to_accelerator_manager.get(resource_name, None)
__all__ = [
"NvidiaGPUAcceleratorManager",
"IntelGPUAcceleratorManager",
"AMDGPUAcceleratorManager",
"TPUAcceleratorManager",
"NeuronAcceleratorManager",
"HPUAcceleratorManager",
"NPUAcceleratorManager",
"RBLNAcceleratorManager",
"MetaxGPUAcceleratorManager",
"FuriosaAcceleratorManager",
"get_all_accelerator_managers",
"get_all_accelerator_resource_names",
"get_accelerator_manager_for_resource",
"RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO_ENV_VAR",
]
@@ -0,0 +1,158 @@
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Tuple
# https://github.com/ray-project/ray/issues/54868
# Ray no longer overrides accelerator ids environment variables when the number
# of accelerators is zero. For example, if a user sets `num_gpus=0` in
# `ray.init()`, the environment variable `CUDA_VISIBLE_DEVICES` will not be
# set to an empty string.
#
# Set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=1 to restore the old behavior where
# Ray would override accelerator env vars even when zero accelerators are assigned.
#
RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO_ENV_VAR = "RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO"
class AcceleratorManager(ABC):
"""This class contains all the functions needed for supporting
an accelerator family in Ray."""
@staticmethod
@abstractmethod
def get_resource_name() -> str:
"""Get the name of the resource representing this accelerator family.
Returns:
The resource name: e.g., the resource name for NVIDIA GPUs is "GPU"
"""
@staticmethod
@abstractmethod
def get_visible_accelerator_ids_env_var() -> str:
"""Get the env var that sets the ids of visible accelerators of this family.
Returns:
The env var for setting visible accelerator ids: e.g.,
CUDA_VISIBLE_DEVICES for NVIDIA GPUs.
"""
@staticmethod
@abstractmethod
def get_current_node_num_accelerators() -> int:
"""Get the total number of accelerators of this family on the current node.
Returns:
The detected total number of accelerators of this family.
Return 0 if the current node doesn't contain accelerators of this family.
"""
@staticmethod
@abstractmethod
def get_current_node_accelerator_type() -> Optional[str]:
"""Get the type of the accelerator of this family on the current node.
Currently Ray only supports single accelerator type of
an accelerator family on each node.
The result should only be used when get_current_node_num_accelerators() > 0.
Returns:
The detected accelerator type of this family: e.g., H100 for NVIDIA GPU.
Return None if it's unknown or the node doesn't have
accelerators of this family.
"""
@staticmethod
@abstractmethod
def get_current_node_additional_resources() -> Optional[Dict[str, float]]:
"""Get any additional resources required for the current node.
In case a particular accelerator type requires considerations for
additional resources (e.g. for TPUs, providing the TPU pod type and
TPU name), this function can be used to provide the
additional logical resources.
Returns:
A dictionary representing additional resources that may be
necessary for a particular accelerator type.
"""
@staticmethod
@abstractmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
"""Validate the resource request quantity of this accelerator resource.
Args:
quantity: The resource request quantity to be validated.
Returns:
(valid, error_message) tuple: the first element of the tuple
indicates whether the given quantity is valid or not,
the second element is the error message
if the given quantity is invalid.
"""
@staticmethod
@abstractmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
"""Get the ids of accelerators of this family that are visible to the current process.
Returns:
The list of visiable accelerator ids.
Return None if all accelerators are visible.
"""
@staticmethod
@abstractmethod
def set_current_process_visible_accelerator_ids(ids: List[str]) -> None:
"""Set the ids of accelerators of this family that are visible to the current process.
Args:
ids: The ids of visible accelerators of this family.
"""
@staticmethod
def get_ec2_instance_num_accelerators(
instance_type: str, instances: dict
) -> Optional[int]:
"""Get the number of accelerators of this family on ec2 instance with given type.
Args:
instance_type: The ec2 instance type.
instances: Map from ec2 instance type to instance metadata returned by
ec2 `describe-instance-types`.
Returns:
The number of accelerators of this family on the ec2 instance
with given type.
Return None if it's unknown.
"""
return None
@staticmethod
def get_ec2_instance_accelerator_type(
instance_type: str, instances: dict
) -> Optional[str]:
"""Get the accelerator type of this family on ec2 instance with given type.
Args:
instance_type: The ec2 instance type.
instances: Map from ec2 instance type to instance metadata returned by
ec2 `describe-instance-types`.
Returns:
The accelerator type of this family on the ec2 instance with given type.
Return None if it's unknown.
"""
return None
@staticmethod
def get_current_node_accelerator_labels() -> Optional[Dict[str, str]]:
"""Get accelerator related Ray node labels of the curent node.
Returns:
A dictionary mapping accelerator related label keys to values.
"""
return None
+153
View File
@@ -0,0 +1,153 @@
import logging
import os
from typing import List, Optional, Tuple
from ray._private.accelerators.accelerator import AcceleratorManager
from ray._private.ray_constants import env_bool
logger = logging.getLogger(__name__)
HIP_VISIBLE_DEVICES_ENV_VAR = "HIP_VISIBLE_DEVICES"
NOSET_HIP_VISIBLE_DEVICES_ENV_VAR = "RAY_EXPERIMENTAL_NOSET_HIP_VISIBLE_DEVICES"
amd_product_dict = {
"0x66a1": "AMD-Instinct-MI50",
"0x738c": "AMD-Instinct-MI100",
"0x7408": "AMD-Instinct-MI250X",
"0x740c": "AMD-Instinct-MI250X-MI250",
"0x740f": "AMD-Instinct-MI210",
"0x74a0": "AMD-Instinct-MI300A",
"0x74a1": "AMD-Instinct-MI300X-OAM",
"0x74a2": "AMD-Instinct-MI308X-OAM",
"0x74a9": "AMD-Instinct-MI300X-HF",
"0x74a5": "AMD-Instinct-MI325X-OAM",
"0x75a0": "AMD-Instinct-MI350X-OAM",
"0x75a3": "AMD-Instinct-MI355X-OAM",
"0x6798": "AMD-Radeon-R9-200-HD-7900",
"0x6799": "AMD-Radeon-HD-7900",
"0x679A": "AMD-Radeon-HD-7900",
"0x679B": "AMD-Radeon-HD-7900",
}
class AMDGPUAcceleratorManager(AcceleratorManager):
"""AMD GPU accelerators."""
@staticmethod
def get_resource_name() -> str:
return "GPU"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
if (
HIP_VISIBLE_DEVICES_ENV_VAR not in os.environ
and "ROCR_VISIBLE_DEVICES" in os.environ
):
raise RuntimeError(
f"Please use {HIP_VISIBLE_DEVICES_ENV_VAR} instead of ROCR_VISIBLE_DEVICES"
)
return HIP_VISIBLE_DEVICES_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
amd_visible_devices = os.environ.get(
AMDGPUAcceleratorManager.get_visible_accelerator_ids_env_var(), None
)
if amd_visible_devices is None:
return None
if amd_visible_devices == "":
return []
if amd_visible_devices == "NoDevFiles":
return []
return list(amd_visible_devices.split(","))
@staticmethod
def get_current_node_num_accelerators() -> int:
import ray._private.thirdparty.pyamdsmi as pyamdsmi
num_gpus = 0
try:
pyamdsmi.smi_initialize()
num_gpus = pyamdsmi.smi_get_device_count()
except Exception:
pass
finally:
try:
pyamdsmi.smi_shutdown()
except Exception:
pass
return num_gpus
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
try:
device_ids = AMDGPUAcceleratorManager._get_amd_device_ids()
if device_ids is None:
return None
return AMDGPUAcceleratorManager._gpu_name_to_accelerator_type(device_ids[0])
except Exception:
return None
@staticmethod
def _gpu_name_to_accelerator_type(name):
if name is None:
return None
try:
match = amd_product_dict[name]
return match
except Exception:
return None
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_amd_devices: List[str],
) -> None:
if env_bool(NOSET_HIP_VISIBLE_DEVICES_ENV_VAR, False):
return
os.environ[
AMDGPUAcceleratorManager.get_visible_accelerator_ids_env_var()
] = ",".join([str(i) for i in visible_amd_devices])
@staticmethod
def _get_amd_device_ids() -> List[str]:
"""Get the list of GPUs IDs
Example:
On a node with 2x MI210 GPUs
pyamdsmi library python bindings
return: ['0x740f', '0x740f']
Returns:
A list of strings containing GPU IDs
"""
import ray._private.thirdparty.pyamdsmi as pyamdsmi
device_ids = []
try:
pyamdsmi.smi_initialize()
num_devices = pyamdsmi.smi_get_device_count()
for i in range(num_devices):
did = pyamdsmi.smi_get_device_id(i)
if did >= 0:
device_ids.append(hex(did))
except Exception:
return None
finally:
try:
pyamdsmi.pyamdsmi_shutdown()
except Exception:
pass
return device_ids
+209
View File
@@ -0,0 +1,209 @@
import logging
import os
from functools import lru_cache
from typing import List, Optional, Tuple
from ray._private.accelerators.accelerator import AcceleratorManager
from ray._private.ray_constants import env_bool
logger = logging.getLogger(__name__)
# Ray uses ``FURIOSA_DEVICES`` to track which Furiosa NPUs are assigned to a
# worker/actor process. The value uses ``npu:<index>`` notation. Ray's
# scheduler operates at the device level, so the value Ray writes is always
# the device-level form (e.g. ``npu:0,npu:3``). Bare integer IDs are also
# accepted on read for convenience.
#
# Note that ``furiosa-llm``'s Python API does not honor ``FURIOSA_DEVICES``
# automatically; callers must pass ``devices=os.environ["FURIOSA_DEVICES"]``
# (or an equivalent list) explicitly to ``furiosa_llm.LLM(...)``. The
# ``furiosa-llm`` CLI does read the value but accepts a richer
# ``npu:X:Y`` (PE-level) form that Ray does not currently preserve through
# worker scheduling; see ``_strip_npu_prefix`` below.
FURIOSA_VISIBLE_DEVICES_ENV_VAR = "FURIOSA_DEVICES"
NOSET_FURIOSA_VISIBLE_DEVICES_ENV_VAR = "RAY_EXPERIMENTAL_NOSET_FURIOSA_DEVICES"
_FURIOSA_DEVICE_PREFIX = "npu:"
@lru_cache(maxsize=None)
def _ensure_furiosa_initialized() -> bool:
"""Run ``furiosa_smi_py.init()`` exactly once per process."""
from furiosa_smi_py import init
init()
return True
def _get_furiosa_list_devices():
"""Lazy import + one-shot init of ``furiosa_smi_py``.
Returns the current ``list_devices`` callable on success, or ``None`` if
the SDK is unavailable or initialization fails. ``list_devices`` is
re-imported on each call so test monkeypatches on the module attribute
take effect.
"""
try:
_ensure_furiosa_initialized()
from furiosa_smi_py import list_devices
except Exception as e:
logger.debug("furiosa_smi_py is unavailable: %s", e)
return None
return list_devices
def _strip_npu_prefix(token: str) -> str:
"""Return the numeric device index from an ``npu:<id>`` token.
Accepts bare integers (``"3"``) as well as the prefixed form
(``"npu:3"``) so that values written by other tooling round-trip
cleanly.
"""
token = token.strip()
if token.startswith(_FURIOSA_DEVICE_PREFIX):
token = token[len(_FURIOSA_DEVICE_PREFIX) :]
# ``furiosa-llm`` accepts both ``npu:X`` (whole NPU) and ``npu:X:Y``
# (PE-level, e.g. ``npu:0:0-3`` for fused PE 0-3 of NPU 0). Ray's
# scheduler currently operates at the device level only, so we keep
# the device index and drop any trailing PE selector. Round-tripping
# PE-level partitioning through worker scheduling is tracked as a
# follow-up enhancement.
return token.split(":", 1)[0]
class FuriosaAcceleratorManager(AcceleratorManager):
"""FuriosaAI NPU accelerators.
Resource name is ``FURIOSA``. The accelerator type is reported as
``FURIOSA_<ARCH>`` where ``<ARCH>`` is the architecture identifier
that the Furiosa SMI SDK exposes via its ``Arch`` enum. The current
SDK variants are ``Rngd``, ``RngdS``, ``RngdMax`` and ``RngdPlus``,
which surface here as ``FURIOSA_RNGD``, ``FURIOSA_RNGDS``,
``FURIOSA_RNGDMAX`` and ``FURIOSA_RNGDPLUS`` respectively.
Supporting any architecture the SDK reports keeps this manager
forward-compatible with new SKUs as Furiosa adds them to
``furiosa_smi_py``.
Device visibility is tracked through the ``FURIOSA_DEVICES``
environment variable, formatted as ``npu:<id>`` tokens. The value
can be passed to the ``furiosa-llm`` CLI (e.g.,
``furiosa-llm serve --devices "$FURIOSA_DEVICES" ...``). When
invoking the ``furiosa_llm.LLM`` Python API directly, the assigned
devices must be passed explicitly, e.g.
``LLM(model_path, devices=os.environ["FURIOSA_DEVICES"])``;
``LLM(devices=None)`` allocates all visible NPUs and would bypass
Ray's per-worker isolation.
"""
@staticmethod
def get_resource_name() -> str:
return "FURIOSA"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
return FURIOSA_VISIBLE_DEVICES_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
visible_devices = os.environ.get(
FuriosaAcceleratorManager.get_visible_accelerator_ids_env_var()
)
if visible_devices is None:
return None
if visible_devices == "":
return []
return [
_strip_npu_prefix(token)
for token in visible_devices.split(",")
if token.strip()
]
@staticmethod
def get_current_node_num_accelerators() -> int:
"""Detects the number of Furiosa NPU devices on the current machine."""
list_devices = _get_furiosa_list_devices()
if list_devices is None:
return 0
try:
return len(list_devices())
except Exception as e:
logger.debug("Could not list Furiosa NPU devices: %s", e)
return 0
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
"""Gets the architecture of the Furiosa NPU on the current node.
Returns a string like ``FURIOSA_RNGD``, ``FURIOSA_RNGDMAX``,
``FURIOSA_RNGDS``, or ``FURIOSA_RNGDPLUS``. Ray assumes a single
accelerator type per node, so the architecture of the first detected
device is used.
The architecture is read from
``device.device_info().arch()`` to mirror the upstream Furiosa SMI
interface. The Arch enum string is normalized into an
accelerator-type label: ``+`` is mapped to ``plus`` so distinct
SKUs do not collide (``rngd+`` becomes ``FURIOSA_RNGDPLUS``,
matching the PyO3 enum form ``RngdPlus``), and any remaining
non-alphanumeric characters are stripped.
"""
list_devices = _get_furiosa_list_devices()
if list_devices is None:
return None
try:
devices = list_devices()
if not devices:
return None
arch_obj = devices[0].device_info().arch()
if arch_obj is None:
return None
# PyO3 enums typically stringify as "<EnumName.Variant>",
# "EnumName.Variant", or just "Variant". Take the trailing
# component.
raw = str(arch_obj).split(".")[-1].strip()
if not raw:
return None
# Map special suffixes to their alphabetic equivalents so that the
# ``Arch::ToString`` form ("rngd+") and the PyO3 enum form
# ("RngdPlus") produce the same Ray accelerator type label.
# Without this, stripping "+" would collapse "rngd+" into
# "rngd", colliding with the distinct ``Rngd`` SKU.
raw = raw.replace("+", "plus")
normalized = "".join(ch for ch in raw if ch.isalnum()).upper()
if not normalized:
return None
return f"FURIOSA_{normalized}"
except Exception as e:
logger.debug("Failed to detect Furiosa NPU type: %s", e)
return None
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
if isinstance(quantity, float) and not quantity.is_integer():
return (
False,
f"{FuriosaAcceleratorManager.get_resource_name()} resource quantity"
" must be a whole number. Furiosa NPUs do not support"
" fractional resource sharing."
f" The specified quantity {quantity} is invalid.",
)
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_furiosa_devices: List[str],
) -> None:
if env_bool(NOSET_FURIOSA_VISIBLE_DEVICES_ENV_VAR, False):
return
formatted = ",".join(
f"{_FURIOSA_DEVICE_PREFIX}{_strip_npu_prefix(str(d))}"
for d in visible_furiosa_devices
)
os.environ[
FuriosaAcceleratorManager.get_visible_accelerator_ids_env_var()
] = formatted
+122
View File
@@ -0,0 +1,122 @@
import logging
import os
from functools import lru_cache
from importlib.util import find_spec
from typing import List, Optional, Tuple
from ray._private.accelerators.accelerator import AcceleratorManager
from ray._private.ray_constants import env_bool
logger = logging.getLogger(__name__)
HABANA_VISIBLE_DEVICES_ENV_VAR = "HABANA_VISIBLE_MODULES"
NOSET_HABANA_VISIBLE_MODULES_ENV_VAR = "RAY_EXPERIMENTAL_NOSET_HABANA_VISIBLE_MODULES"
@lru_cache()
def is_package_present(package_name: str) -> bool:
try:
return find_spec(package_name) is not None
except ModuleNotFoundError:
return False
HPU_PACKAGE_AVAILABLE = is_package_present("habana_frameworks")
class HPUAcceleratorManager(AcceleratorManager):
"""Intel Habana(HPU) accelerators."""
@staticmethod
def get_resource_name() -> str:
return "HPU"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
return HABANA_VISIBLE_DEVICES_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
hpu_visible_devices = os.environ.get(
HPUAcceleratorManager.get_visible_accelerator_ids_env_var(), None
)
if hpu_visible_devices is None:
return None
if hpu_visible_devices == "":
return []
return list(hpu_visible_devices.split(","))
@staticmethod
def get_current_node_num_accelerators() -> int:
"""Attempt to detect the number of HPUs on this machine.
Returns:
The number of HPUs if any were detected, otherwise 0.
"""
if HPU_PACKAGE_AVAILABLE:
import habana_frameworks.torch.hpu as torch_hpu
if torch_hpu.is_available():
return torch_hpu.device_count()
else:
logging.info("HPU devices not available")
return 0
else:
return 0
@staticmethod
def is_initialized() -> bool:
"""Attempt to check if HPU backend is initialized.
Returns:
True if backend initialized else False.
"""
if HPU_PACKAGE_AVAILABLE:
import habana_frameworks.torch.hpu as torch_hpu
if torch_hpu.is_available() and torch_hpu.is_initialized():
return True
else:
return False
else:
return False
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
"""Attempt to detect the HPU family type.
Returns:
The device name (GAUDI, GAUDI2) if detected else None.
"""
if HPUAcceleratorManager.is_initialized():
import habana_frameworks.torch.hpu as torch_hpu
return f"Intel-{torch_hpu.get_device_name()}"
else:
logging.info("HPU type cannot be detected")
return None
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
if isinstance(quantity, float) and not quantity.is_integer():
return (
False,
f"{HPUAcceleratorManager.get_resource_name()} resource quantity"
" must be whole numbers. "
f"The specified quantity {quantity} is invalid.",
)
else:
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_hpu_devices: List[str],
) -> None:
if env_bool(NOSET_HABANA_VISIBLE_MODULES_ENV_VAR, False):
return
os.environ[
HPUAcceleratorManager.get_visible_accelerator_ids_env_var()
] = ",".join([str(i) for i in visible_hpu_devices])
@@ -0,0 +1,104 @@
import logging
import os
from typing import List, Optional, Tuple
from ray._private.accelerators.accelerator import AcceleratorManager
from ray._private.ray_constants import env_bool
logger = logging.getLogger(__name__)
ONEAPI_DEVICE_SELECTOR_ENV_VAR = "ONEAPI_DEVICE_SELECTOR"
NOSET_ONEAPI_DEVICE_SELECTOR_ENV_VAR = "RAY_EXPERIMENTAL_NOSET_ONEAPI_DEVICE_SELECTOR"
ONEAPI_DEVICE_BACKEND_TYPE = "level_zero"
ONEAPI_DEVICE_TYPE = "gpu"
class IntelGPUAcceleratorManager(AcceleratorManager):
"""Intel GPU accelerators."""
@staticmethod
def get_resource_name() -> str:
return "GPU"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
return ONEAPI_DEVICE_SELECTOR_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
oneapi_visible_devices = os.environ.get(
IntelGPUAcceleratorManager.get_visible_accelerator_ids_env_var(), None
)
if oneapi_visible_devices is None:
return None
if oneapi_visible_devices == "":
return []
if oneapi_visible_devices == "NoDevFiles":
return []
prefix = ONEAPI_DEVICE_BACKEND_TYPE + ":"
return list(oneapi_visible_devices.split(prefix)[1].split(","))
@staticmethod
def get_current_node_num_accelerators() -> int:
try:
import dpctl
except ImportError:
dpctl = None
if dpctl is None:
return 0
num_gpus = 0
try:
dev_info = ONEAPI_DEVICE_BACKEND_TYPE + ":" + ONEAPI_DEVICE_TYPE
context = dpctl.SyclContext(dev_info)
num_gpus = context.device_count
except Exception:
num_gpus = 0
return num_gpus
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
"""Get the name of first Intel GPU. (supposed only one GPU type on a node)
Example:
name: 'Intel(R) Data Center GPU Max 1550'
return name: 'Intel-GPU-Max-1550'
Returns:
A string representing the name of Intel GPU type.
"""
try:
import dpctl
except ImportError:
dpctl = None
if dpctl is None:
return None
accelerator_type = None
try:
dev_info = ONEAPI_DEVICE_BACKEND_TYPE + ":" + ONEAPI_DEVICE_TYPE + ":0"
dev = dpctl.SyclDevice(dev_info)
accelerator_type = "Intel-GPU-" + "-".join(dev.name.split(" ")[-2:])
except Exception:
accelerator_type = None
return accelerator_type
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_xpu_devices: List[str],
) -> None:
if env_bool(NOSET_ONEAPI_DEVICE_SELECTOR_ENV_VAR, False):
return
prefix = ONEAPI_DEVICE_BACKEND_TYPE + ":"
os.environ[
IntelGPUAcceleratorManager.get_visible_accelerator_ids_env_var()
] = prefix + ",".join([str(i) for i in visible_xpu_devices])
@@ -0,0 +1,90 @@
import logging
import os
from typing import List, Optional, Tuple
from ray._private.accelerators.accelerator import AcceleratorManager
logger = logging.getLogger(__name__)
CUDA_VISIBLE_DEVICES_ENV_VAR = "CUDA_VISIBLE_DEVICES"
NOSET_CUDA_VISIBLE_DEVICES_ENV_VAR = "RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES"
class MetaxGPUAcceleratorManager(AcceleratorManager):
"""Metax GPU accelerators."""
@staticmethod
def get_resource_name() -> str:
return "GPU"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
return CUDA_VISIBLE_DEVICES_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
cuda_visible_devices = os.environ.get(
MetaxGPUAcceleratorManager.get_visible_accelerator_ids_env_var(), None
)
if cuda_visible_devices is None:
return None
if cuda_visible_devices == "":
return []
return list(cuda_visible_devices.split(","))
@staticmethod
def get_current_node_num_accelerators() -> int:
try:
import pymxsml.mxsml_extension as pymxsml
try:
pymxsml.mxSmlExInit()
except pymxsml.MXSMLEXError:
return 0
device_count = pymxsml.mxSmlExDeviceGetCount()
pymxsml.mxSmlExShutdown()
return device_count
except Exception as e:
logger.debug("Could not import pymxsml: %s", e)
return 0
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
try:
import pymxsml.mxsml_extension as pymxsml
try:
pymxsml.mxSmlExInit()
except pymxsml.MXSMLEXError:
return None
device_name = None
device_count = pymxsml.mxSmlExDeviceGetCount()
if device_count > 0:
handle = pymxsml.mxSmlExDeviceGetHandleByIndex(0)
device_name = pymxsml.mxSmlExDeviceGetName(handle)
if isinstance(device_name, bytes):
device_name = device_name.decode("utf-8")
pymxsml.mxSmlExShutdown()
return device_name
except Exception:
logger.warning("Failed to detect GPU type.", exc_info=True)
return None
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_cuda_devices: List[str],
) -> None:
if os.environ.get(NOSET_CUDA_VISIBLE_DEVICES_ENV_VAR):
return
os.environ[
MetaxGPUAcceleratorManager.get_visible_accelerator_ids_env_var()
] = ",".join(visible_cuda_devices)
+133
View File
@@ -0,0 +1,133 @@
import json
import logging
import os
import subprocess
import sys
from typing import List, Optional, Tuple
from ray._private.accelerators.accelerator import AcceleratorManager
from ray._private.ray_constants import env_bool
logger = logging.getLogger(__name__)
NEURON_RT_VISIBLE_CORES_ENV_VAR = "NEURON_RT_VISIBLE_CORES"
NOSET_AWS_NEURON_RT_VISIBLE_CORES_ENV_VAR = (
"RAY_EXPERIMENTAL_NOSET_NEURON_RT_VISIBLE_CORES"
)
# https://awsdocs-neuron.readthedocs-hosted.com/en/latest/general/arch/neuron-hardware/inf2-arch.html#aws-inf2-arch
# https://awsdocs-neuron.readthedocs-hosted.com/en/latest/general/arch/neuron-hardware/trn1-arch.html#aws-trn1-arch
# Subject to removal after the information is available via public API
AWS_NEURON_INSTANCE_MAP = {
"trn1.2xlarge": 2,
"trn1.32xlarge": 32,
"trn1n.32xlarge": 32,
"inf2.xlarge": 2,
"inf2.8xlarge": 2,
"inf2.24xlarge": 12,
"inf2.48xlarge": 24,
}
class NeuronAcceleratorManager(AcceleratorManager):
"""AWS Inferentia and Trainium accelerators."""
@staticmethod
def get_resource_name() -> str:
return "neuron_cores"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
return NEURON_RT_VISIBLE_CORES_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
neuron_visible_cores = os.environ.get(
NeuronAcceleratorManager.get_visible_accelerator_ids_env_var(), None
)
if neuron_visible_cores is None:
return None
if neuron_visible_cores == "":
return []
return list(neuron_visible_cores.split(","))
@staticmethod
def get_current_node_num_accelerators() -> int:
"""
Attempt to detect the number of Neuron cores on this machine.
Returns:
The number of Neuron cores if any were detected, otherwise 0.
"""
nc_count: int = 0
neuron_path = "/opt/aws/neuron/bin/"
if sys.platform.startswith("linux") and os.path.isdir(neuron_path):
result = subprocess.run(
[os.path.join(neuron_path, "neuron-ls"), "--json-output"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if result.returncode == 0 and result.stdout:
neuron_devices = json.loads(result.stdout)
for neuron_device in neuron_devices:
nc_count += neuron_device.get("nc_count", 0)
return nc_count
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
from ray.util.accelerators import AWS_NEURON_CORE
return AWS_NEURON_CORE
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
if isinstance(quantity, float) and not quantity.is_integer():
return (
False,
f"{NeuronAcceleratorManager.get_resource_name()} resource quantity"
" must be whole numbers. "
f"The specified quantity {quantity} is invalid.",
)
else:
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_neuron_core_ids: List[str],
) -> None:
"""Set the NEURON_RT_VISIBLE_CORES environment variable based on
given visible_neuron_core_ids.
Args:
visible_neuron_core_ids: List of str representing core IDs.
"""
if env_bool(NOSET_AWS_NEURON_RT_VISIBLE_CORES_ENV_VAR, False):
return
os.environ[
NeuronAcceleratorManager.get_visible_accelerator_ids_env_var()
] = ",".join([str(i) for i in visible_neuron_core_ids])
@staticmethod
def get_ec2_instance_num_accelerators(
instance_type: str, instances: dict
) -> Optional[int]:
# TODO: AWS SDK (public API) doesn't yet expose the NeuronCore
# information. It will be available (work-in-progress)
# as xxAcceleratorInfo in InstanceTypeInfo.
# https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_InstanceTypeInfo.html
# See https://github.com/ray-project/ray/issues/38473
return AWS_NEURON_INSTANCE_MAP.get(instance_type.lower(), None)
@staticmethod
def get_ec2_instance_accelerator_type(
instance_type: str, instances: dict
) -> Optional[str]:
from ray.util.accelerators import AWS_NEURON_CORE
return AWS_NEURON_CORE
+100
View File
@@ -0,0 +1,100 @@
import glob
import logging
import os
from typing import List, Optional, Tuple
from ray._private.accelerators.accelerator import AcceleratorManager
from ray._private.ray_constants import env_bool
logger = logging.getLogger(__name__)
ASCEND_RT_VISIBLE_DEVICES_ENV_VAR = "ASCEND_RT_VISIBLE_DEVICES"
NOSET_ASCEND_RT_VISIBLE_DEVICES_ENV_VAR = (
"RAY_EXPERIMENTAL_NOSET_ASCEND_RT_VISIBLE_DEVICES"
)
class NPUAcceleratorManager(AcceleratorManager):
"""Ascend NPU accelerators."""
@staticmethod
def get_resource_name() -> str:
return "NPU"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
return ASCEND_RT_VISIBLE_DEVICES_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
ascend_visible_devices = os.environ.get(
NPUAcceleratorManager.get_visible_accelerator_ids_env_var(), None
)
if ascend_visible_devices is None:
return None
if ascend_visible_devices == "":
return []
if ascend_visible_devices == "NoDevFiles":
return []
return list(ascend_visible_devices.split(","))
@staticmethod
def get_current_node_num_accelerators() -> int:
"""Attempt to detect the number of NPUs on this machine.
NPU chips are represented as devices within `/dev/`, either as `/dev/davinci?`.
Returns:
The number of NPUs if any were detected, otherwise 0.
"""
try:
import acl
device_count, ret = acl.rt.get_device_count()
if ret == 0:
return device_count
except Exception as e:
logger.debug("Could not import AscendCL: %s", e)
try:
npu_files = glob.glob("/dev/davinci[0-9]*")
return len(npu_files)
except Exception as e:
logger.debug("Failed to detect number of NPUs: %s", e)
return 0
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
"""Get the type of the Ascend NPU on the current node.
Returns:
A string of the type, such as "Ascend910A", "Ascend910B", "Ascend310P1".
"""
try:
import acl
return acl.get_soc_name()
except Exception:
logger.exception("Failed to detect NPU type.")
return None
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_npu_devices: List[str],
) -> None:
if env_bool(NOSET_ASCEND_RT_VISIBLE_DEVICES_ENV_VAR, False):
return
os.environ[
NPUAcceleratorManager.get_visible_accelerator_ids_env_var()
] = ",".join([str(i) for i in visible_npu_devices])
@@ -0,0 +1,145 @@
import logging
import os
import re
from typing import List, Optional, Tuple
from ray._private.accelerators.accelerator import AcceleratorManager
from ray._private.ray_constants import env_bool
logger = logging.getLogger(__name__)
CUDA_VISIBLE_DEVICES_ENV_VAR = "CUDA_VISIBLE_DEVICES"
NOSET_CUDA_VISIBLE_DEVICES_ENV_VAR = "RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES"
# Capture the accelerator model from the NVML device name: the run of leading
# all-caps tokens (e.g. "RTX", "PRO") up to and including the first token that
# contains a digit. This keeps datacenter cards stable ("Tesla V100-SXM2-16GB"
# -> "V100", "NVIDIA A100-SXM4-40GB" -> "A100") while disambiguating the RTX
# line, whose first token is only a brand prefix ("NVIDIA RTX PRO 6000 Blackwell
# Server Edition" -> "RTX PRO 6000"). A trailing SKU suffix after a hyphen is
# dropped. Mixed-case consumer names ("NVIDIA GeForce RTX 5090") don't match and
# fall back to a hyphen-joined product name in _gpu_name_to_accelerator_type.
NVIDIA_GPU_NAME_PATTERN = re.compile(r"\w+\s+((?:[A-Z]+\s+)*[A-Z0-9]*\d[A-Z0-9]*)")
class NvidiaGPUAcceleratorManager(AcceleratorManager):
"""NVIDIA GPU accelerators."""
@staticmethod
def get_resource_name() -> str:
return "GPU"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
return CUDA_VISIBLE_DEVICES_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
cuda_visible_devices = os.environ.get(
NvidiaGPUAcceleratorManager.get_visible_accelerator_ids_env_var(), None
)
if cuda_visible_devices is None:
return None
if cuda_visible_devices == "":
return []
if cuda_visible_devices == "NoDevFiles":
return []
return list(cuda_visible_devices.split(","))
@staticmethod
def get_current_node_num_accelerators() -> int:
import ray._private.thirdparty.pynvml as pynvml
try:
pynvml.nvmlInit()
except pynvml.NVMLError:
return 0 # pynvml init failed
device_count = pynvml.nvmlDeviceGetCount()
pynvml.nvmlShutdown()
return device_count
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
import ray._private.thirdparty.pynvml as pynvml
try:
pynvml.nvmlInit()
except pynvml.NVMLError:
return None # pynvml init failed
device_count = pynvml.nvmlDeviceGetCount()
cuda_device_type = None
if device_count > 0:
handle = pynvml.nvmlDeviceGetHandleByIndex(0)
device_name = pynvml.nvmlDeviceGetName(handle)
if isinstance(device_name, bytes):
device_name = device_name.decode("utf-8")
cuda_device_type = (
NvidiaGPUAcceleratorManager._gpu_name_to_accelerator_type(device_name)
)
pynvml.nvmlShutdown()
return cuda_device_type
@staticmethod
def _gpu_name_to_accelerator_type(name):
if name is None:
return None
match = NVIDIA_GPU_NAME_PATTERN.match(name)
result = match.group(1).replace(" ", "-") if match else None
if result and len(result) > 1:
return result
# The pattern above requires an all-uppercase/numeric model token, which
# works for datacenter cards ("Tesla V100-SXM2-16GB" -> "V100",
# "NVIDIA RTX PRO 6000 ..." -> "RTX-PRO-6000") but not for consumer
# cards whose product line is mixed case ("NVIDIA GeForce RTX 5090").
# Fall back to a hyphen-joined product name so callers get a useful
# accelerator_type label like "GeForce-RTX-5090".
cleaned = re.sub(r"^NVIDIA\s+", "", name).strip()
return cleaned.replace(" ", "-") if cleaned else None
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_cuda_devices: List[str],
) -> None:
if env_bool(NOSET_CUDA_VISIBLE_DEVICES_ENV_VAR, False):
return
os.environ[
NvidiaGPUAcceleratorManager.get_visible_accelerator_ids_env_var()
] = ",".join([str(i) for i in visible_cuda_devices])
@staticmethod
def get_ec2_instance_num_accelerators(
instance_type: str, instances: dict
) -> Optional[int]:
if instance_type not in instances:
return None
gpus = instances[instance_type].get("GpuInfo", {}).get("Gpus")
if gpus is not None:
# TODO(ameer): currently we support one gpu type per node.
assert len(gpus) == 1
return gpus[0]["Count"]
return None
@staticmethod
def get_ec2_instance_accelerator_type(
instance_type: str, instances: dict
) -> Optional[str]:
if instance_type not in instances:
return None
gpus = instances[instance_type].get("GpuInfo", {}).get("Gpus")
if gpus is not None:
# TODO(ameer): currently we support one gpu type per node.
assert len(gpus) == 1
return gpus[0]["Name"]
return None
+81
View File
@@ -0,0 +1,81 @@
import logging
import os
from typing import List, Optional, Tuple
from ray._private.accelerators.accelerator import AcceleratorManager
from ray._private.ray_constants import env_bool
logger = logging.getLogger(__name__)
RBLN_RT_VISIBLE_DEVICES_ENV_VAR = "RBLN_DEVICES"
NOSET_RBLN_RT_VISIBLE_DEVICES_ENV_VAR = "RAY_EXPERIMENTAL_NOSET_RBLN_RT_VISIBLE_DEVICES"
class RBLNAcceleratorManager(AcceleratorManager):
"""Rebellions RBLN accelerators."""
@staticmethod
def get_resource_name() -> str:
return "RBLN"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
return RBLN_RT_VISIBLE_DEVICES_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
visible_devices = os.environ.get(
RBLNAcceleratorManager.get_visible_accelerator_ids_env_var()
)
if visible_devices is None:
return None
if visible_devices == "":
return []
return visible_devices.split(",")
@staticmethod
def get_current_node_num_accelerators() -> int:
"""Detects the number of RBLN devices on the current machine."""
try:
from rebel import device_count
return device_count()
except Exception as e:
logger.debug("Could not detect RBLN devices: %s", e)
return 0
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
"""Gets the type of RBLN NPU on the current node."""
try:
from rebel import get_npu_name
return get_npu_name()
except Exception as e:
logger.exception("Failed to detect RBLN NPU type: %s", e)
return None
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
if isinstance(quantity, float) and not quantity.is_integer():
return (
False,
f"{RBLNAcceleratorManager.get_resource_name()} resource quantity"
" must be whole numbers. "
f"The specified quantity {quantity} is invalid.",
)
else:
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_rbln_devices: List[str],
) -> None:
if env_bool(NOSET_RBLN_RT_VISIBLE_DEVICES_ENV_VAR, False):
return
os.environ[
RBLNAcceleratorManager.get_visible_accelerator_ids_env_var()
] = ",".join(map(str, visible_rbln_devices))
+766
View File
@@ -0,0 +1,766 @@
import glob
import logging
import os
import re
from functools import lru_cache
from typing import Dict, List, Optional, Set, Tuple
import requests
import ray
from ray._private.accelerators.accelerator import AcceleratorManager
from ray._private.ray_constants import env_bool
from ray.util.placement_group import (
PlacementGroup,
placement_group,
remove_placement_group,
)
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
logger = logging.getLogger(__name__)
TPU_VALID_CHIP_OPTIONS = (1, 2, 4, 8)
GKE_TPU_ACCELERATOR_TYPE_ENV_VAR = "TPU_ACCELERATOR_TYPE"
GKE_TPU_TOPOLOGY_ENV_VAR = "TPU_TOPOLOGY"
GKE_TPU_WORKER_ID_ENV_VAR = "TPU_WORKER_ID"
GKE_TPU_NAME_ENV_VAR = "TPU_NAME"
# Constants for accessing the `accelerator-type` from TPU VM
# instance metadata.
# See https://cloud.google.com/compute/docs/metadata/overview
# for more details about VM instance metadata.
GCE_TPU_ACCELERATOR_ENDPOINT = (
"http://metadata.google.internal/computeMetadata/v1/instance/attributes/"
)
GCE_TPU_HEADERS = {"Metadata-Flavor": "Google"}
GCE_TPU_ACCELERATOR_KEY = "accelerator-type"
GCE_TPU_ENV_KEY = "tpu-env"
GCE_TPU_INSTANCE_ID_KEY = "instance-id"
GCE_TPU_WORKER_ID_KEY = "agent-worker-number"
TPU_VISIBLE_CHIPS_ENV_VAR = "TPU_VISIBLE_CHIPS"
NOSET_TPU_VISIBLE_CHIPS_ENV_VAR = "RAY_EXPERIMENTAL_NOSET_TPU_VISIBLE_CHIPS"
# The following defines environment variables that allow
# us to access a subset of TPU visible chips.
#
# See: https://github.com/google/jax/issues/14977 for an example/more details.
TPU_CHIPS_PER_HOST_BOUNDS_ENV_VAR = "TPU_CHIPS_PER_HOST_BOUNDS"
TPU_CHIPS_PER_HOST_BOUNDS_1_CHIP_CONFIG = "1,1,1"
TPU_CHIPS_PER_HOST_BOUNDS_2_CHIP_CONFIG = "1,2,1"
TPU_HOST_BOUNDS_ENV_VAR = "TPU_HOST_BOUNDS"
TPU_SINGLE_HOST_BOUNDS = "1,1,1"
# By default TPU VMs come with 4 chips per host and 2 tensorcores per chip.
# For more details: https://cloud.google.com/tpu/docs/system-architecture-tpu-vm
DEFAULT_TPU_NUM_CHIPS_PER_HOST = 4
DEFAULT_TPU_NUM_CORES_PER_CHIP = 2
# Accelerators that support up to 8 chips per host for single-host topologies: v5e, v6e
TPU_8_CHIPS_PER_HOST_TYPES = ("v5litepod", "v6e")
# Topologies that are always sub-host or single-host
TPU_SINGLE_HOST_TOPOLOGIES = ("1x1", "2x2", "2x4")
# Accelerators that are 2 cores per chip: v2, v3, v4, v5p, v7x
# Accelerators that are 1 core per chip: v5e, v6e
SINGLE_CORE_TPU_TYPES = ("v5litepod", "v6e")
# The valid TPU types.
VALID_TPU_TYPES = ("v2", "v3", "v4", "v5p", "v5litepod", "v6e", "v7x")
# This is only used to construct TPU 3D topologies
def _get_larger_3d_topologies(max_x: int, max_y: int, max_z: int) -> Set[str]:
"""Returns a set of larger 3D TPU topologies given the max x,y,z value. Using DEFAULT_TPU_NUM_CHIPS_PER_HOST as increment"""
topologies = set()
for x in range(
DEFAULT_TPU_NUM_CHIPS_PER_HOST, max_x + 1, DEFAULT_TPU_NUM_CHIPS_PER_HOST
):
for y in range(
DEFAULT_TPU_NUM_CHIPS_PER_HOST, max_y + 1, DEFAULT_TPU_NUM_CHIPS_PER_HOST
):
for z in range(
DEFAULT_TPU_NUM_CHIPS_PER_HOST,
max_z + 1,
DEFAULT_TPU_NUM_CHIPS_PER_HOST,
):
topologies.add(f"{x}x{y}x{z}")
return topologies
# The valid TPU topologies for each of the TPU types.
VALID_TPU_TOPOLOGY = {
"v2": {"4x4", "4x8", "8x8", "8x16", "16x16"},
"v3": {"4x4", "4x8", "8x8", "8x16", "16x16", "16x32", "32x32"},
"v4": {"2x2x1", "2x2x2", "2x2x4", "2x4x4"}.union(
_get_larger_3d_topologies(12, 12, 16)
),
"v5p": {
"2x2x1",
"2x2x2",
"2x2x4",
"2x4x4",
}.union(_get_larger_3d_topologies(16, 16, 24)),
"v5litepod": {"1x1", "2x2", "2x4", "2x8", "4x4", "4x8", "8x8", "8x16", "16x16"},
"v6e": {"1x1", "2x2", "2x4", "2x8", "4x4", "4x8", "8x8", "8x16", "16x16"},
"v7x": {
"2x2x1",
"2x2x2",
"2x2x4",
"2x4x4",
"4x4x4",
"4x4x8",
"4x8x8",
"8x8x8",
"8x8x16",
"8x16x16",
},
}
def _get_tpu_metadata(key: str) -> Optional[str]:
"""Poll and get TPU metadata."""
try:
accelerator_type_request = requests.get(
os.path.join(GCE_TPU_ACCELERATOR_ENDPOINT, key),
headers=GCE_TPU_HEADERS,
)
if (
accelerator_type_request.status_code == 200
and accelerator_type_request.text
):
return accelerator_type_request.text
else:
logging.debug(
"Unable to poll TPU GCE Metadata. Got "
f"status code: {accelerator_type_request.status_code} and "
f"content: {accelerator_type_request.text}"
)
except requests.RequestException as e:
logging.debug("Unable to poll the TPU GCE Metadata: %s", e)
return None
def _accelerator_type_check(accelerator_type: str):
if not accelerator_type.startswith(VALID_TPU_TYPES):
raise ValueError(
f"Invalid accelerator type: {accelerator_type}. Must start with one of: {VALID_TPU_TYPES}"
)
def get_total_chips_from_accelerator_type(accelerator_type: str) -> int:
"""Calculates total chips from a GCP accelerator ("pod") type string (e.g. "v6e-16")."""
_accelerator_type_check(accelerator_type)
parts = accelerator_type.split("-")
if len(parts) < 2:
raise ValueError(
f"Accelerator type must include size (e.g. 'v6e-8'), got: {accelerator_type}"
)
num_cores = int(parts[1])
cores_per_chip = get_tpu_cores_per_chip(accelerator_type)
return num_cores // cores_per_chip
def get_num_tpu_visible_chips_per_host(accelerator_type: str) -> int:
_accelerator_type_check(accelerator_type)
if accelerator_type.startswith(TPU_8_CHIPS_PER_HOST_TYPES):
total_chips = get_total_chips_from_accelerator_type(accelerator_type)
# Sub/single-host topologies return their exact chip count
if total_chips <= 8:
return total_chips
# Multi-host topologies default to 4 visible chips per host
return DEFAULT_TPU_NUM_CHIPS_PER_HOST
def get_tpu_cores_per_chip(accelerator_type: str) -> int:
_accelerator_type_check(accelerator_type)
if accelerator_type.startswith(SINGLE_CORE_TPU_TYPES):
return 1
return DEFAULT_TPU_NUM_CORES_PER_CHIP
def get_num_chips_from_topology(topology: str) -> int:
"""
Calculates the total number of chips in a TPU topology.
Ex: "2x2x2" -> 8
"""
total_chips = 1
for dim in topology.strip().lower().split("x"):
total_chips *= int(dim)
return total_chips
def infer_tpu_pod_type_from_topology(
topology: str, accelerator_type: str
) -> Optional[str]:
"""Infer the TPU pod type (e.g. v4-32) from topology and accelerator type."""
if not topology or not accelerator_type:
return None
try:
num_chips = get_num_chips_from_topology(topology)
generation = accelerator_type.lower().replace("tpu-", "")
num_cores = num_chips * get_tpu_cores_per_chip(generation)
return f"{generation}-{num_cores}"
except Exception as e:
raise ValueError(
f"Failed to infer pod type from topology '{topology}' "
f"and type '{accelerator_type}'"
) from e
def fetch_tpu_slice_name_from_pg(pg):
@ray.remote(num_cpus=0)
def _get_tpu_slice_name():
return TPUAcceleratorManager.get_current_node_tpu_name()
tpu_name_ref = _get_tpu_slice_name.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg, placement_group_bundle_index=0
)
).remote()
return ray.get(tpu_name_ref)
def get_chips_per_host(topology: str, accelerator_version: str) -> int:
"""Get the number of chips per host based on topology and accelerator version.
Rules for determining the default number of chips per host:
- Default for most TPU generations (v4, v5p, v7x, etc.) is 4 chips per host.
- For v5e and v6e:
- Topologies with <= 8 chips use the exact chip count (e.g. 1x1 -> 1).
These topologies are always sub or single-host.
- Multi-host topologies (> 8 chips) default to 4-chip hosts.
Args:
topology: The TPU topology string (e.g. "2x2x2", "2x4").
accelerator_version: The accelerator version string (e.g. "v4", "v6e").
Returns:
The default number of chips per host for the given configuration.
"""
total_chips = get_num_chips_from_topology(topology)
# Check for 8-chip host types (v5litepod, v6e) for single host setups
if (
accelerator_version.strip().lower() in TPU_8_CHIPS_PER_HOST_TYPES
and topology.strip().lower() in TPU_SINGLE_HOST_TOPOLOGIES
):
return total_chips
return DEFAULT_TPU_NUM_CHIPS_PER_HOST
DEFAULT_TPU_HEAD_RESERVATION_TIMEOUT_S: float = 100.0
def reserve_tpu_slice(
topology: str,
accelerator_type: str,
timeout_s: Optional[float] = DEFAULT_TPU_HEAD_RESERVATION_TIMEOUT_S,
) -> Optional[Tuple[str, PlacementGroup]]:
"""Reserves a TPU slice using its head resource and returns the slice name.
This enables gang scheduling of training workers with multi-host TPUs.
This is used by JaxTrainer with TPUs in Ray Train.
Args:
topology: The TPU topology string (e.g. "2x2x2").
accelerator_type: The accelerator type of the node (e.g. "TPU-V4").
timeout_s: The maximum time in seconds to wait for the TPU head
placement group to become ready. The head reservation must succeed
before the slice name can be retrieved, so this call is necessarily
blocking. Defaults to ``DEFAULT_TPU_HEAD_RESERVATION_TIMEOUT_S``.
Pass ``None`` to wait indefinitely.
Returns:
A tuple of a string representing a unique TPU slice name and the placement
group handle reserving the TPU head.
Raises:
TimeoutError: If the TPU head placement group does not become ready
within ``timeout_s`` seconds.
"""
pod_type = infer_tpu_pod_type_from_topology(topology, accelerator_type)
if pod_type is None:
return None
# Reserve a slice by creating a placement group on the TPU head.
head_label_selector = {
"ray.io/tpu-worker-id": "0",
"ray.io/tpu-pod-type": pod_type,
}
head_placement_group = placement_group(
bundles=[{f"TPU-{pod_type}-head": 1}],
bundle_label_selector=[head_label_selector],
)
logger.debug(
"Waiting up to %s seconds to reserve multi-host slice head.", timeout_s
)
ready, _ = ray.wait([head_placement_group.ready()], timeout=timeout_s)
if not ready:
# Clean up the pending head reservation so that resources are not
# held while the caller decides whether to retry.
try:
remove_placement_group(head_placement_group)
except Exception:
logger.exception(
"Failed to clean up pending TPU head placement group after timeout."
)
raise TimeoutError(
"Failed to reserve TPU head for slice with shape: {} after {} "
"seconds. Ensure your cluster has sufficient resources. Requesting "
"TPU head node with labels: {}. Current resources: {}".format(
pod_type,
timeout_s,
head_label_selector,
ray.available_resources(),
)
)
# Retrieve the unique slice ID.
slice_name = fetch_tpu_slice_name_from_pg(head_placement_group)
if slice_name is None:
raise RuntimeError(
"Failed to retrieve TPU slice name after reserving head placement group. "
"Ensure that TPU slice metadata is available and correctly configured on multi-host nodes."
)
return (slice_name, head_placement_group)
class TPUAcceleratorManager(AcceleratorManager):
"""Google TPU accelerators."""
@staticmethod
def get_resource_name() -> str:
return "TPU"
@staticmethod
def get_visible_accelerator_ids_env_var() -> str:
return TPU_VISIBLE_CHIPS_ENV_VAR
@staticmethod
def get_current_process_visible_accelerator_ids() -> Optional[List[str]]:
tpu_visible_chips = os.environ.get(
TPUAcceleratorManager.get_visible_accelerator_ids_env_var(), None
)
if tpu_visible_chips is None:
return None
if tpu_visible_chips == "":
return []
return list(tpu_visible_chips.split(","))
@staticmethod
@lru_cache()
def get_current_node_num_accelerators() -> int:
"""Attempt to detect the number of TPUs on this machine.
TPU chips are represented as devices within `/dev/`, either as
`/dev/accel*` or `/dev/vfio/*`.
Returns:
The number of TPUs if any were detected, otherwise 0.
"""
# Real TPU chips are exposed as character devices at /dev/accel0,
# /dev/accel1, etc. NVIDIA drivers 570.x and later (Blackwell-class
# GPUs such as the RTX 5090) instead create /dev/accel as a *directory*
# containing /dev/accel/accel0, which the non-recursive glob below
# would otherwise miscount as a TPU chip. Filter directory entries out
# so both GKE and GCE TPU detection keep working while rejecting the
# NVIDIA false positive.
accel_chips = [p for p in glob.glob("/dev/accel*") if not os.path.isdir(p)]
if accel_chips:
return len(accel_chips)
try:
vfio_entries = os.listdir("/dev/vfio")
numeric_entries = [int(entry) for entry in vfio_entries if entry.isdigit()]
return len(numeric_entries)
except FileNotFoundError as e:
logger.debug("Failed to detect number of TPUs: %s", e)
return 0
@staticmethod
def is_valid_tpu_accelerator_type(tpu_accelerator_type: str) -> bool:
"""Check whether the tpu accelerator_type is formatted correctly.
The accelerator_type field typically follows a form of v{generation}-{cores/chips},
but newer generations like 7x may follow tpu{generation}-{cores/chips}.
See the following for more information:
https://cloud.google.com/sdk/gcloud/reference/compute/tpus/tpu-vm/accelerator-types/describe
Args:
tpu_accelerator_type: The string representation of the accelerator type
to be checked for validity.
Returns:
True if it's valid, false otherwise.
"""
# 1. Legacy format: v2-8, v3-32.
# 2. Newer format with letters in generation: v5litepod-16, v6e-4.
# 3. Ironwood TPU format which contains a tpu prefix: tpu7x-16.
expected_pattern = re.compile(r"^(v|tpu)\d+[a-zA-Z]*-\d+$")
if not expected_pattern.match(tpu_accelerator_type):
return False
return True
@staticmethod
def is_valid_tpu_accelerator_topology(
tpu_accelerator_version: str, tpu_topology: str
) -> bool:
"""Check whether the tpu topology is valid.
The accelerator_type field follows a form of v{generation}.
The accelerator_topology field follows either the form {A}x{B} or {A}x{B}x{C} depending on the v{generation}
Args:
tpu_accelerator_version: The string representation of the accelerator version. (e.g. v6e, V5P)
tpu_topology: The string representation of the accelerator topology
to be checked for validity
Returns:
True if it's a valid topology, False otherwise.
"""
tpu_version_formatted = tpu_accelerator_version.strip().lower().split("-")[0]
if tpu_version_formatted.startswith("tpu"):
tpu_version_formatted = "v" + tpu_version_formatted[3:]
if (
tpu_version_formatted.lower() not in VALID_TPU_TOPOLOGY
or tpu_topology.strip().lower()
not in VALID_TPU_TOPOLOGY[tpu_version_formatted]
):
return False
return True
@staticmethod
def validate_resource_request_quantity(
quantity: float,
) -> Tuple[bool, Optional[str]]:
if quantity not in TPU_VALID_CHIP_OPTIONS:
return (
False,
f"The number of requested 'TPU' was set to {quantity} which "
"is not a supported chip configuration. Supported configs: "
f"{TPU_VALID_CHIP_OPTIONS}",
)
else:
return (True, None)
@staticmethod
def set_current_process_visible_accelerator_ids(
visible_tpu_chips: List[str],
) -> None:
"""Set TPU environment variables based on the provided visible_tpu_chips.
To access a subset of the TPU visible chips, we must use a combination of
environment variables that tells the compiler (via ML framework) the:
- Visible chips
- The physical bounds of chips per host
- The host bounds within the context of a TPU pod.
See: https://github.com/google/jax/issues/14977 for an example/more details.
Args:
visible_tpu_chips: List of str representing TPU chips.
"""
if env_bool(NOSET_TPU_VISIBLE_CHIPS_ENV_VAR, False):
return
num_visible_tpu_chips = len(visible_tpu_chips)
num_accelerators_on_node = (
TPUAcceleratorManager.get_current_node_num_accelerators()
)
if num_visible_tpu_chips == num_accelerators_on_node:
# Let the ML framework use the defaults
os.environ.pop(TPU_CHIPS_PER_HOST_BOUNDS_ENV_VAR, None)
os.environ.pop(TPU_HOST_BOUNDS_ENV_VAR, None)
return
os.environ[
TPUAcceleratorManager.get_visible_accelerator_ids_env_var()
] = ",".join([str(i) for i in visible_tpu_chips])
if num_visible_tpu_chips == 1:
os.environ[
TPU_CHIPS_PER_HOST_BOUNDS_ENV_VAR
] = TPU_CHIPS_PER_HOST_BOUNDS_1_CHIP_CONFIG
os.environ[TPU_HOST_BOUNDS_ENV_VAR] = TPU_SINGLE_HOST_BOUNDS
elif num_visible_tpu_chips == 2:
os.environ[
TPU_CHIPS_PER_HOST_BOUNDS_ENV_VAR
] = TPU_CHIPS_PER_HOST_BOUNDS_2_CHIP_CONFIG
os.environ[TPU_HOST_BOUNDS_ENV_VAR] = TPU_SINGLE_HOST_BOUNDS
@staticmethod
def get_current_node_tpu_pod_type() -> Optional[str]:
"""Get the TPU pod type of the current node if applicable.
Individual TPU VMs within a TPU pod must know what type
of pod it is a part of. This is necessary for the
ML framework to work properly.
The logic is different if the TPU was provisioned via:
```
gcloud tpus tpu-vm create ...
```
(i.e. a GCE VM), vs through GKE:
- GCE VMs will always have a metadata server to poll this info
- GKE VMS will have environment variables preset.
Returns:
A string representing the current TPU pod type, e.g.
v4-16.
"""
# Start with GKE-based check
accelerator_type = os.getenv(GKE_TPU_ACCELERATOR_TYPE_ENV_VAR, "")
if not accelerator_type:
# GCE-based VM check
accelerator_type = _get_tpu_metadata(key=GCE_TPU_ACCELERATOR_KEY)
if accelerator_type and TPUAcceleratorManager.is_valid_tpu_accelerator_type(
tpu_accelerator_type=accelerator_type
):
if accelerator_type.lower().startswith("tpu"):
return "v" + accelerator_type.lower()[3:]
return accelerator_type
logging.debug("Failed to get a valid accelerator type.")
return None
@staticmethod
def get_current_node_tpu_name() -> Optional[str]:
"""Return the name of the TPU pod that this worker node is a part of.
For instance, if the TPU was created with name "my-tpu", this function
will return "my-tpu".
If created through the Ray cluster launcher, the
name will typically be something like "ray-my-tpu-cluster-worker-aa946781-tpu".
In case the TPU was created through KubeRay, we currently expect that the
environment variable TPU_NAME is set per TPU pod slice, in which case
this function will return the value of that environment variable.
"""
try:
# Start with GKE-based check
tpu_name = os.getenv(GKE_TPU_NAME_ENV_VAR, None)
if not tpu_name:
# GCE-based VM check
tpu_name = _get_tpu_metadata(key=GCE_TPU_INSTANCE_ID_KEY)
return tpu_name
except ValueError as e:
logging.debug("Could not get TPU name: %s", e)
return None
@staticmethod
def get_current_node_tpu_worker_id() -> Optional[int]:
"""Return the worker index of the TPU pod."""
try:
# Start with GKE-based check
worker_id = os.getenv(GKE_TPU_WORKER_ID_ENV_VAR, None)
if not worker_id:
# GCE-based VM check
worker_id = _get_tpu_metadata(key=GCE_TPU_WORKER_ID_KEY)
if worker_id:
return int(worker_id)
else:
return None
except ValueError as e:
logging.debug("Could not get TPU worker id: %s", e)
return None
@staticmethod
def get_num_workers_in_current_tpu_pod() -> Optional[int]:
"""Return the total number of workers in a TPU pod."""
tpu_pod_type = TPUAcceleratorManager.get_current_node_tpu_pod_type()
chips_per_host = TPUAcceleratorManager.get_current_node_num_accelerators()
cores_per_chip = get_tpu_cores_per_chip(tpu_pod_type) # Hard-coded map.
cores_per_host = chips_per_host * cores_per_chip
if tpu_pod_type and cores_per_host > 0:
num_cores = int(tpu_pod_type.split("-")[1])
num_workers = num_cores // cores_per_host
# If the chip count doesn't fill a full host, a sub-host is still treated as a host.
if num_cores % cores_per_host != 0:
num_workers += 1
return num_workers
else:
logging.debug("Could not get num workers in TPU pod.")
return None
@staticmethod
def get_current_node_tpu_topology() -> Optional[str]:
try:
# Attempt GKE based lookup first
if topology := os.environ.get(GKE_TPU_TOPOLOGY_ENV_VAR):
return topology.strip().lower()
# GCE-based VM check using TPU env string.
tpu_env = _get_tpu_metadata(key=GCE_TPU_ENV_KEY)
if tpu_env:
topology = re.search(r"TOPOLOGY:\s*'([^']+)'", tpu_env)
if topology:
return topology.group(1).strip().lower()
except ValueError as e:
logging.debug("Could not get TPU topology: %s", e)
return None
@staticmethod
def get_current_node_accelerator_type() -> Optional[str]:
"""Attempt to detect the TPU accelerator type.
The output of this function will return the "ray accelerator type"
resource (e.g. TPU-V4) that indicates the TPU version.
We also expect that our TPU nodes contain a "TPU pod type"
resource, which indicates information about the topology of
the TPU pod slice.
We expect that the "TPU pod type" resource to be used when
running multi host workers, i.e. when TPU units are pod slices.
We expect that the "ray accelerator type" resource to be used when
running single host workers, i.e. when TPU units are single hosts.
Returns:
A string representing the TPU accelerator type,
e.g. "TPU-V2", "TPU-V3", "TPU-V4" if applicable, else None.
"""
def tpu_pod_type_to_ray_accelerator_type(
tpu_pod_type: str,
) -> Optional[str]:
return "TPU-" + str(tpu_pod_type.split("-")[0].upper())
ray_accelerator_type = None
tpu_pod_type = TPUAcceleratorManager.get_current_node_tpu_pod_type()
if tpu_pod_type is not None:
ray_accelerator_type = tpu_pod_type_to_ray_accelerator_type(
tpu_pod_type=tpu_pod_type
)
if ray_accelerator_type is None:
logger.info(
"While trying to autodetect a TPU type, "
f"received malformed accelerator_type: {tpu_pod_type}"
)
if ray_accelerator_type is None:
logging.info("Failed to auto-detect TPU type.")
return ray_accelerator_type
@staticmethod
def get_current_node_additional_resources() -> Optional[Dict[str, float]]:
"""Get additional resources required for TPU nodes.
This will populate the TPU pod type and the TPU name which
is used for TPU pod execution.
When running workloads on a TPU pod, we need a way to run
the same binary on every worker in the TPU pod.
See https://jax.readthedocs.io/en/latest/multi_process.html
for more information.
To do this in ray, we take advantage of custom resources. We
mark worker 0 of the TPU pod as a "coordinator" that identifies
the other workers in the TPU pod. We therefore need:
- worker 0 to be targetable.
- all workers in the TPU pod to have a unique identifier consistent
within a TPU pod.
So assuming we want to run the following workload:
@ray.remote
def my_jax_fn():
import jax
return jax.device_count()
We could broadcast this on a TPU pod (e.g. a v4-16) as follows:
@ray.remote(resources={"TPU-v4-16-head"})
def run_jax_fn(executable):
# Note this will execute on worker 0
tpu_name = ray.util.tpu.get_current_pod_name()
num_hosts = ray.util.tpu.get_current_pod_worker_count()
tpu_executable = executable.options(resources={"TPU": 4, tpu_name: 1})
return [tpu_executable.remote() for _ in range(num_hosts)]
Returns:
A dictionary representing additional resources that may be
necessary for a particular accelerator type.
"""
resources = {}
tpu_name = TPUAcceleratorManager.get_current_node_tpu_name()
worker_id = TPUAcceleratorManager.get_current_node_tpu_worker_id()
tpu_pod_type = TPUAcceleratorManager.get_current_node_tpu_pod_type()
if tpu_name and worker_id is not None and tpu_pod_type:
pod_head_resource_name = f"TPU-{tpu_pod_type}-head"
# Add the name of the TPU to the resource.
resources[tpu_name] = 1
# Only add in the TPU pod type resource to worker 0.
if worker_id == 0:
resources[pod_head_resource_name] = 1
else:
logging.info(
"Failed to configure TPU pod. Got: "
"tpu_name: %s, worker_id: %s, accelerator_type: %s",
tpu_name,
worker_id,
tpu_pod_type,
)
if resources:
return resources
return None
@staticmethod
def get_current_node_accelerator_labels() -> Dict[str, str]:
"""Get default TPU-specific Ray node labels for the current node.
For TPUs, these labels include:
- ray.io/tpu-slice-name: the name of the TPU Pod or slice
- ray.io/tpu-worker-id: the integer worker ID within the slice
- ray.io/tpu-topology: the TPU topology (e.g. 4x4)
- ray.io/tpu-pod-type: the TPU pod type (e.g. v4-8)
Returns:
A dictionary of TPU label keys and resolved values.
"""
tpu_labels = {}
tpu_name = TPUAcceleratorManager.get_current_node_tpu_name()
if tpu_name:
tpu_labels[ray._raylet.RAY_NODE_TPU_SLICE_NAME_KEY] = tpu_name
worker_id = TPUAcceleratorManager.get_current_node_tpu_worker_id()
if worker_id is not None:
tpu_labels[ray._raylet.RAY_NODE_TPU_WORKER_ID_KEY] = str(worker_id)
tpu_topology = TPUAcceleratorManager.get_current_node_tpu_topology()
if tpu_topology:
tpu_labels[ray._raylet.RAY_NODE_TPU_TOPOLOGY_KEY] = tpu_topology
pod_type = TPUAcceleratorManager.get_current_node_tpu_pod_type()
if pod_type:
tpu_labels[ray._raylet.RAY_NODE_TPU_POD_TYPE_KEY] = pod_type
return tpu_labels