chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user