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
+52
View File
@@ -0,0 +1,52 @@
import dataclasses
from typing import Iterable
def _validate_allowed_keys_exist(
dataclass_name: str, data_dict: dict, allowed_keys: set
):
keys_not_in_dict = allowed_keys.difference(data_dict)
if keys_not_in_dict:
raise ValueError(
f"Key(s) {sorted(keys_not_in_dict)} are not present in {dataclass_name}. "
"Remove them from `allowed_keys`. "
f"Valid keys: {sorted(data_dict.keys())}"
)
def ensure_only_allowed_dataclass_keys_updated(
dataclass: dataclasses.dataclass,
allowed_keys: Iterable[str],
):
"""
Validate dataclass by raising an exception if any key not included in
``allowed_keys`` differs from the default value.
A ``ValueError`` will also be raised if any of the ``allowed_keys``
is not present in ``dataclass.__dict__``.
Args:
dataclass: Dict or dataclass to check.
allowed_keys: dataclass attribute keys that can have a value different than
the default one.
"""
default_data = dataclass.__class__()
default_data_dict = default_data.__dict__
allowed_keys = set(allowed_keys)
_validate_allowed_keys_exist(
dataclass.__class__.__name__, default_data_dict, allowed_keys
)
# These keys should not have been updated in the `dataclass` object
prohibited_keys = set(default_data_dict) - allowed_keys
dataclass_dict = dataclass.__dict__
bad_keys = [
key for key in prohibited_keys if dataclass_dict[key] != default_data_dict[key]
]
if bad_keys:
raise ValueError(
f"Key(s) {bad_keys} are not allowed to be updated in the current context. "
"Remove them from the dataclass."
)
@@ -0,0 +1,92 @@
import logging
import threading
from typing import Optional
import ray
import ray._private.ray_constants as ray_constants
from ray.air._internal.device_manager.cpu import CPUTorchDeviceManager
from ray.air._internal.device_manager.hpu import HPUTorchDeviceManager
from ray.air._internal.device_manager.npu import NPUTorchDeviceManager
from ray.air._internal.device_manager.nvidia_gpu import CUDATorchDeviceManager
from ray.air._internal.device_manager.torch_device_manager import TorchDeviceManager
logger = logging.getLogger(__name__)
DEFAULT_TORCH_DEVICE_MANAGER_CLS = CPUTorchDeviceManager
SUPPORTED_ACCELERATOR_TORCH_DEVICE_MANAGER = {
ray_constants.GPU: CUDATorchDeviceManager,
ray_constants.HPU: HPUTorchDeviceManager,
ray_constants.NPU: NPUTorchDeviceManager,
}
def register_custom_torch_dist_backend(backend: Optional[str] = None) -> None:
if backend == "hccl":
# The name for the communication backend of Habana and torch-npu is the same.
HPUTorchDeviceManager.register_custom_torch_dist_backend()
NPUTorchDeviceManager.register_custom_torch_dist_backend()
_torch_device_manager = None
_torch_device_manager_lock = threading.Lock()
def get_torch_device_manager_by_context() -> TorchDeviceManager:
global _torch_device_manager
with _torch_device_manager_lock:
if not _torch_device_manager:
existing_device_manager_cls = None
resources = ray.get_runtime_context().get_accelerator_ids()
# select correct accelerator type from resources
for resource_type, resource_value in resources.items():
device_manager_cls = SUPPORTED_ACCELERATOR_TORCH_DEVICE_MANAGER.get(
resource_type, None
)
if resource_value and device_manager_cls:
# An error will raise when multiple accelerators are specified.
if existing_device_manager_cls:
raise RuntimeError(
"Unable to determine the appropriate DeviceManager "
f"for the specified resources {resources}."
)
else:
existing_device_manager_cls = device_manager_cls
device_manager_cls = (
existing_device_manager_cls or DEFAULT_TORCH_DEVICE_MANAGER_CLS
)
_torch_device_manager = device_manager_cls()
return _torch_device_manager
def get_torch_device_manager_by_device_type(device_type: str):
if device_type.lower() == ray_constants.GPU.lower() or device_type == "cuda":
return CUDATorchDeviceManager()
elif device_type.lower() == ray_constants.NPU.lower():
return NPUTorchDeviceManager()
elif device_type.lower() == ray_constants.HPU.lower():
return HPUTorchDeviceManager()
elif device_type.lower() == "cpu":
return CPUTorchDeviceManager()
raise RuntimeError(f"Device type {device_type} cannot be recognized.")
__all__ = [
TorchDeviceManager,
CPUTorchDeviceManager,
CUDATorchDeviceManager,
HPUTorchDeviceManager,
NPUTorchDeviceManager,
register_custom_torch_dist_backend,
get_torch_device_manager_by_context,
get_torch_device_manager_by_device_type,
]
@@ -0,0 +1,30 @@
from contextlib import contextmanager
from typing import List
import torch
from ray.air._internal.device_manager.torch_device_manager import TorchDeviceManager
class CPUTorchDeviceManager(TorchDeviceManager):
"""CPU device manager"""
def is_available(self) -> bool():
return True
def get_devices(self) -> List[torch.device]:
"""Gets the correct torch device list configured for this process."""
return [torch.device("cpu")]
def supports_stream(self) -> bool:
"""Validate if the device type support create a stream"""
return False
def get_stream_context(self, stream):
"""Return empty context mananger for CPU."""
@contextmanager
def default_context_manager():
yield
return default_context_manager()
@@ -0,0 +1,50 @@
from contextlib import contextmanager
from typing import List, Union
import torch
from ray._private.accelerators.hpu import HPU_PACKAGE_AVAILABLE
from ray.air._internal.device_manager.torch_device_manager import TorchDeviceManager
if HPU_PACKAGE_AVAILABLE:
import habana_frameworks.torch.hpu as torch_hpu
class HPUTorchDeviceManager(TorchDeviceManager):
"""HPU device manager"""
@staticmethod
def register_custom_torch_dist_backend():
if HPU_PACKAGE_AVAILABLE:
import habana_frameworks.torch.core # noqa: F401
import habana_frameworks.torch.distributed.hccl # noqa: F401
def is_available(self) -> bool():
if not HPU_PACKAGE_AVAILABLE:
return False
return torch_hpu.is_available()
def get_devices(self) -> List[torch.device]:
if not self.is_available():
raise RuntimeError(
"Using HPUTorchDeviceManager but torch hpu is not available."
)
return [torch.device("hpu")]
def set_device(self, device: Union[torch.device, int, str, None]):
torch_hpu.set_device(device)
def supports_stream(self) -> bool:
"""Validate if the device type support create a stream"""
return False
def get_stream_context(self, stream):
"""Get HPU stream context manager, empty so far."""
@contextmanager
def default_context_manager():
yield
return default_context_manager()
@@ -0,0 +1,104 @@
import os
from importlib.util import find_spec
from typing import List, Union
import torch
import ray
import ray._private.ray_constants as ray_constants
from ray._private.accelerators.npu import ASCEND_RT_VISIBLE_DEVICES_ENV_VAR
from ray.air._internal.device_manager.torch_device_manager import TorchDeviceManager
def is_package_present(package_name: str) -> bool:
try:
return find_spec(package_name) is not None
except ModuleNotFoundError:
return False
NPU_TORCH_PACKAGE_AVAILABLE = is_package_present("torch_npu")
if NPU_TORCH_PACKAGE_AVAILABLE:
import torch_npu # noqa: F401
class NPUTorchDeviceManager(TorchDeviceManager):
"""Ascend NPU device manager"""
@staticmethod
def register_custom_torch_dist_backend():
if NPU_TORCH_PACKAGE_AVAILABLE:
import torch_npu # noqa: F401, F811
def is_available(self) -> bool:
if not NPU_TORCH_PACKAGE_AVAILABLE:
return False
return torch.npu.is_available()
def get_devices(self) -> List[torch.device]:
"""Gets the correct torch device list configured for this process.
Returns a list of torch NPU devices allocated for the current worker.
If no NPUs are assigned, then it returns a list with a single CPU device.
"""
if NPU_TORCH_PACKAGE_AVAILABLE and torch.npu.is_available():
npu_ids = [
str(id)
for id in ray.get_runtime_context().get_accelerator_ids()[
ray_constants.NPU
]
]
device_ids = []
if len(npu_ids) > 0:
npu_visible_str = os.environ.get(ASCEND_RT_VISIBLE_DEVICES_ENV_VAR, "")
if npu_visible_str and npu_visible_str != "NoDevFiles":
npu_visible_list = npu_visible_str.split(",")
else:
npu_visible_list = []
for npu_id in npu_ids:
try:
device_ids.append(npu_visible_list.index(npu_id))
except IndexError:
raise RuntimeError(
"ASCEND_RT_VISIBLE_DEVICES set incorrectly. "
f"Got {npu_visible_str}, expected to include {npu_id}. "
"Did you override the `ASCEND_RT_VISIBLE_DEVICES` "
"environment variable?"
)
else:
# If called on the driver or outside of Ray Train, return the
# 0th device.
device_ids.append(0)
devices = [torch.device(f"npu:{device_id}") for device_id in device_ids]
else:
raise RuntimeError(
"Using NPUTorchDeviceManager but torch npu is not available."
)
return devices
def set_device(self, device: Union[torch.device, int]):
torch.npu.set_device(device)
def supports_stream(self) -> bool:
"""Validate if the device type support to create a stream"""
return True
def create_stream(self, device):
"""Create a stream on NPU device"""
return torch.npu.Stream(device)
def get_stream_context(self, stream):
"""Get a torch.stream context on NPU device"""
return torch.npu.stream(stream)
def get_current_stream(self):
"""Get current stream for NPU device"""
return torch.npu.current_stream()
@@ -0,0 +1,79 @@
import os
from typing import List, Union
import torch
import ray
from ray.air._internal.device_manager.torch_device_manager import TorchDeviceManager
class CUDATorchDeviceManager(TorchDeviceManager):
"""CUDA device manager"""
def is_available(self) -> bool():
return torch.cuda.is_available()
def get_devices(self) -> List[torch.device]:
"""Gets the correct torch device list configured for this process.
Returns a list of torch CUDA devices allocated for the current worker.
If no GPUs are assigned, then it returns a list with a single CPU device.
Assumes that `CUDA_VISIBLE_DEVICES` is set and is a
superset of the `ray.get_gpu_ids()`.
"""
# GPU IDs are assigned by Ray after you specify "use_gpu"
# GPU `ray.get_gpu_ids()` may return ints or may return strings.
# We should always convert to strings.
gpu_ids = [str(id) for id in ray.get_gpu_ids()]
device_ids = []
if len(gpu_ids) > 0:
cuda_visible_str = os.environ.get("CUDA_VISIBLE_DEVICES", "")
if cuda_visible_str and cuda_visible_str != "NoDevFiles":
cuda_visible_list = cuda_visible_str.split(",")
else:
cuda_visible_list = []
# By default, there should only be one GPU ID if `use_gpu=True`.
# If there are multiple GPUs, return a list of devices.
# If using fractional GPUs, these IDs are not guaranteed
# to be unique across different processes.
for gpu_id in gpu_ids:
try:
device_ids.append(cuda_visible_list.index(gpu_id))
except IndexError:
raise RuntimeError(
"CUDA_VISIBLE_DEVICES set incorrectly. "
f"Got {cuda_visible_str}, expected to include {gpu_id}. "
"Did you override the `CUDA_VISIBLE_DEVICES` environment"
" variable? If not, please help file an issue on Github."
)
else:
# If called on the driver or outside of Ray Train, return the
# 0th device.
device_ids.append(0)
return [torch.device(f"cuda:{device_id}") for device_id in device_ids]
def set_device(self, device: Union[torch.device, int, str, None]):
torch.cuda.set_device(device)
def supports_stream(self) -> bool:
"""Validate if the device type support create a stream"""
return True
def create_stream(self, device: torch.device) -> torch.cuda.Stream:
"""Create a stream on cuda device"""
return torch.cuda.Stream(device)
def get_stream_context(self, stream):
"""Get a stream context for cuda device"""
return torch.cuda.stream(stream)
def get_current_stream(self) -> torch.cuda.Stream:
"""Get current stream for cuda device"""
return torch.cuda.current_stream()
@@ -0,0 +1,40 @@
from abc import ABC
from typing import List, Union
import torch
class TorchDeviceManager(ABC):
"""This class contains the function needed for supporting
an acclerator family in Ray AI Library.
"""
def is_available(self) -> bool:
"""Validate if device is available."""
...
def get_devices(self) -> List[torch.device]:
"""Gets the correct torch device configured for this process"""
...
def set_device(self, device: Union[torch.device, int, str, None]):
"""Set the correct device for this process"""
...
def supports_stream(self) -> bool:
"""Validate if the device type support create a stream"""
...
def create_stream(self, device: torch.device):
"""Create a device stream"""
...
def get_stream_context(self, stream):
"""Get a stream context of device. If device didn't support stream,
this should return a empty context manager instead of None.
"""
...
def get_current_stream(self):
"""Get current stream on accelerators like torch.cuda.current_stream"""
...
+46
View File
@@ -0,0 +1,46 @@
import hashlib
import os
from pathlib import Path
from filelock import FileLock
import ray
RAY_LOCKFILE_DIR = "_ray_lockfiles"
class TempFileLock:
"""FileLock wrapper that uses temporary file locks.
The temporary directory that these locks are saved to can be configured via
the `RAY_TMPDIR` environment variable.
Args:
path: The file path that this temporary file lock is used for.
This will be used to generate the lockfile filename.
Ex: For concurrent writes to a file, this is the common filepath
that multiple processes are writing to.
**kwargs: Additional keyword arguments to pass to the underlying `FileLock`.
"""
def __init__(self, path: str, **kwargs):
self.path = path
temp_dir = Path(ray._common.utils.get_default_system_temp_dir()).resolve()
self._lock_dir = temp_dir / RAY_LOCKFILE_DIR
self._path_hash = hashlib.sha256(
str(Path(self.path).resolve()).encode("utf-8")
).hexdigest()
self._lock_path = self._lock_dir / f"{self._path_hash}.lock"
os.makedirs(str(self._lock_dir), exist_ok=True)
self._lock = FileLock(self._lock_path, **kwargs)
def __enter__(self):
self._lock.acquire()
return self
def __exit__(self, type, value, traceback):
self._lock.release()
def __getattr__(self, name):
return getattr(self._lock, name)
+31
View File
@@ -0,0 +1,31 @@
import json
import numbers
import numpy as np
class SafeFallbackEncoder(json.JSONEncoder):
def __init__(self, nan_str="null", **kwargs):
super(SafeFallbackEncoder, self).__init__(**kwargs)
self.nan_str = nan_str
def default(self, value):
try:
if type(value).__module__ == np.__name__ and isinstance(value, np.ndarray):
return value.tolist()
if isinstance(value, np.bool_):
return bool(value)
if np.isnan(value):
return self.nan_str
if issubclass(type(value), numbers.Integral):
return int(value)
if issubclass(type(value), numbers.Number):
return float(value)
return super(SafeFallbackEncoder, self).default(value)
except Exception:
return str(value) # give up, just stringify it (ok for logs)
+346
View File
@@ -0,0 +1,346 @@
import logging
import os
from copy import deepcopy
from typing import TYPE_CHECKING, Dict, Optional
from packaging import version
from ray._private.dict import flatten_dict
if TYPE_CHECKING:
from mlflow.entities import Run
from mlflow.tracking import MlflowClient
logger = logging.getLogger(__name__)
class _MLflowLoggerUtil:
"""Util class for setting up and logging to MLflow.
Use this util for any library that needs MLflow logging/tracking logic
such as Ray Tune or Ray Train.
"""
def __init__(self):
import mlflow
self._mlflow = mlflow
self.experiment_id = None
def __deepcopy__(self, memo=None):
# mlflow is a module, and thus cannot be copied
_mlflow = self._mlflow
self.__dict__.pop("_mlflow")
dict_copy = deepcopy(self.__dict__, memo)
copied_object = _MLflowLoggerUtil()
copied_object.__dict__.update(dict_copy)
self._mlflow = _mlflow
copied_object._mlflow = _mlflow
return copied_object
def setup_mlflow(
self,
tracking_uri: Optional[str] = None,
registry_uri: Optional[str] = None,
experiment_id: Optional[str] = None,
experiment_name: Optional[str] = None,
tracking_token: Optional[str] = None,
artifact_location: Optional[str] = None,
create_experiment_if_not_exists: bool = True,
) -> None:
"""
Sets up MLflow.
Sets the Mlflow tracking uri & token, and registry URI. Also sets
the MLflow experiment that the logger should use, and possibly
creates new experiment if it does not exist.
Args:
tracking_uri: The tracking URI for the MLflow tracking
server.
registry_uri: The registry URI for the MLflow model registry.
experiment_id: The id of an already existing MLflow
experiment to use for logging. If None is passed in
here and the MFLOW_EXPERIMENT_ID is not set, or the
experiment with this id does not exist,
``experiment_name`` will be used instead. This argument takes
precedence over ``experiment_name`` if both are passed in.
experiment_name: The experiment name to use for logging.
If None is passed in here, the MLFLOW_EXPERIMENT_NAME environment
variable is used to determine the experiment name.
If the experiment with the name already exists with MLflow,
it will be reused. If not, a new experiment will be created
with the provided name if
``create_experiment_if_not_exists`` is set to True.
tracking_token: Tracking token used to authenticate with MLflow.
artifact_location: The location to store run artifacts.
If not provided, MLFlow picks an appropriate default.
Ignored if experiment already exists.
create_experiment_if_not_exists: Whether to create an
experiment with the provided name if it does not already
exist. Defaults to True.
Raises:
ValueError: ``experiment_id`` and ``experiment_name`` are both ``None``.
"""
if tracking_token:
os.environ["MLFLOW_TRACKING_TOKEN"] = tracking_token
self._mlflow.set_tracking_uri(tracking_uri)
self._mlflow.set_registry_uri(registry_uri)
# First check experiment_id.
experiment_id = (
experiment_id
if experiment_id is not None
else os.environ.get("MLFLOW_EXPERIMENT_ID")
)
if experiment_id is not None:
from mlflow.exceptions import MlflowException
try:
self._mlflow.get_experiment(experiment_id=experiment_id)
logger.debug(
f"Experiment with provided id {experiment_id} "
"exists. Setting that as the experiment."
)
self.experiment_id = experiment_id
return
except MlflowException:
pass
# Then check experiment_name.
experiment_name = (
experiment_name
if experiment_name is not None
else os.environ.get("MLFLOW_EXPERIMENT_NAME")
)
if experiment_name is not None and self._mlflow.get_experiment_by_name(
name=experiment_name
):
logger.debug(
f"Experiment with provided name {experiment_name} "
"exists. Setting that as the experiment."
)
self.experiment_id = self._mlflow.get_experiment_by_name(
experiment_name
).experiment_id
return
# An experiment with the provided id or name does not exist.
# Create a new experiment if applicable.
if experiment_name and create_experiment_if_not_exists:
logger.debug(
"Existing experiment not found. Creating new "
f"experiment with name: {experiment_name}"
)
self.experiment_id = self._mlflow.create_experiment(
name=experiment_name, artifact_location=artifact_location
)
return
if create_experiment_if_not_exists:
raise ValueError(
f"Experiment with the provided experiment_id: "
f"{experiment_id} does not exist and no "
f"experiment_name provided. At least one of "
f"these has to be provided."
)
else:
raise ValueError(
f"Experiment with the provided experiment_id: "
f"{experiment_id} or experiment_name: "
f"{experiment_name} does not exist. Please "
f"create an MLflow experiment and provide "
f"either its id or name."
)
def _parse_dict(self, dict_to_log: Dict) -> Dict:
"""Parses provided dict to convert all values to float.
MLflow can only log metrics that are floats. This does not apply to
logging parameters or artifacts.
Args:
dict_to_log: The dictionary containing the metrics to log.
Returns:
A dictionary containing the metrics to log with all values being
converted to floats, or skipped if not able to be converted.
"""
new_dict = {}
for key, value in dict_to_log.items():
try:
value = float(value)
new_dict[key] = value
except (ValueError, TypeError):
logger.debug(
"Cannot log key {} with value {} since the "
"value cannot be converted to float.".format(key, value)
)
continue
return new_dict
def start_run(
self,
run_name: Optional[str] = None,
tags: Optional[Dict] = None,
set_active: bool = False,
) -> "Run":
"""Starts a new run and possibly sets it as the active run.
Args:
run_name: Name of the new MLflow run to create.
tags: Tags to set for the new run.
set_active: Whether to set the new run as the active run.
If an active run already exists, then that run is returned.
Returns:
The newly created MLflow run.
"""
import mlflow
from mlflow.utils.mlflow_tags import MLFLOW_RUN_NAME
if tags is None:
tags = {}
if set_active:
return self._start_active_run(run_name=run_name, tags=tags)
client = self._get_client()
# If `mlflow==1.30.0` and we don't use `run_name`, then MLflow might error. For
# more information, see #29749.
if version.parse(mlflow.__version__) >= version.parse("1.30.0"):
run = client.create_run(
run_name=run_name, experiment_id=self.experiment_id, tags=tags
)
else:
tags[MLFLOW_RUN_NAME] = run_name
run = client.create_run(experiment_id=self.experiment_id, tags=tags)
return run
def _start_active_run(
self, run_name: Optional[str] = None, tags: Optional[Dict] = None
) -> "Run":
"""Starts a run and sets it as the active run if one does not exist.
If an active run already exists, then returns it.
"""
active_run = self._mlflow.active_run()
if active_run:
return active_run
return self._mlflow.start_run(
run_name=run_name, experiment_id=self.experiment_id, tags=tags
)
def _run_exists(self, run_id: str) -> bool:
"""Check if run with the provided id exists."""
from mlflow.exceptions import MlflowException
try:
self._mlflow.get_run(run_id=run_id)
return True
except MlflowException:
return False
def _get_client(self) -> "MlflowClient":
"""Returns an ml.tracking.MlflowClient instance to use for logging."""
tracking_uri = self._mlflow.get_tracking_uri()
registry_uri = self._mlflow.get_registry_uri()
from mlflow.tracking import MlflowClient
return MlflowClient(tracking_uri=tracking_uri, registry_uri=registry_uri)
def log_params(self, params_to_log: Dict, run_id: Optional[str] = None):
"""Logs the provided parameters to the run specified by run_id.
If no ``run_id`` is passed in, then logs to the current active run.
If there is not active run, then creates a new run and sets it as
the active run.
Args:
params_to_log: Dictionary of parameters to log.
run_id: The ID of the run to log to.
"""
params_to_log = flatten_dict(params_to_log)
if run_id and self._run_exists(run_id):
client = self._get_client()
for key, value in params_to_log.items():
client.log_param(run_id=run_id, key=key, value=value)
else:
for key, value in params_to_log.items():
self._mlflow.log_param(key=key, value=value)
def log_metrics(
self, step: int, metrics_to_log: Dict, run_id: Optional[str] = None
):
"""Logs the provided metrics to the run specified by run_id.
If no ``run_id`` is passed in, then logs to the current active run.
If there is not active run, then creates a new run and sets it as
the active run.
Args:
step: Step at which the metrics are logged.
metrics_to_log: Dictionary of metrics to log.
run_id: The ID of the run to log to.
"""
metrics_to_log = flatten_dict(metrics_to_log)
metrics_to_log = self._parse_dict(metrics_to_log)
if run_id and self._run_exists(run_id):
client = self._get_client()
for key, value in metrics_to_log.items():
client.log_metric(run_id=run_id, key=key, value=value, step=step)
else:
for key, value in metrics_to_log.items():
self._mlflow.log_metric(key=key, value=value, step=step)
def save_artifacts(self, dir: str, run_id: Optional[str] = None):
"""Saves directory as artifact to the run specified by run_id.
If no ``run_id`` is passed in, then saves to the current active run.
If there is not active run, then creates a new run and sets it as
the active run.
Args:
dir: Path to directory containing the files to save.
run_id: The ID of the run to log to.
"""
if run_id and self._run_exists(run_id):
client = self._get_client()
client.log_artifacts(run_id=run_id, local_dir=dir)
else:
self._mlflow.log_artifacts(local_dir=dir)
def end_run(self, status: Optional[str] = None, run_id: Optional[str] = None):
"""Terminates the run specified by run_id.
If no ``run_id`` is passed in, then terminates the
active run if one exists.
Args:
status: The status to set when terminating the run.
run_id: The ID of the run to terminate.
"""
if (
run_id
and self._run_exists(run_id)
and not (
self._mlflow.active_run()
and self._mlflow.active_run().info.run_id == run_id
)
):
client = self._get_client()
client.set_terminated(run_id=run_id, status=status)
else:
self._mlflow.end_run(status=status)
@@ -0,0 +1,73 @@
from typing import Dict, Optional, Union
import numpy as np
import tensorflow as tf
from ray.air.util.data_batch_conversion import _unwrap_ndarray_object_type_if_needed
def convert_ndarray_to_tf_tensor(
ndarray: np.ndarray,
dtype: Optional[tf.dtypes.DType] = None,
type_spec: Optional[tf.TypeSpec] = None,
) -> tf.Tensor:
"""Convert a NumPy ndarray to a TensorFlow Tensor.
Args:
ndarray: A NumPy ndarray that we wish to convert to a TensorFlow Tensor.
dtype: A TensorFlow dtype for the created tensor; if None, the dtype will be
inferred from the NumPy ndarray data.
type_spec: A type spec that specifies the shape and dtype of the returned
tensor. If you specify ``dtype``, the dtype stored in the type spec is
ignored.
Returns:
A TensorFlow Tensor.
"""
if dtype is None and type_spec is not None:
dtype = type_spec.dtype
is_ragged = isinstance(type_spec, tf.RaggedTensorSpec)
ndarray = _unwrap_ndarray_object_type_if_needed(ndarray)
if is_ragged:
return tf.ragged.constant(ndarray, dtype=dtype)
else:
return tf.convert_to_tensor(ndarray, dtype=dtype)
def convert_ndarray_batch_to_tf_tensor_batch(
ndarrays: Union[np.ndarray, Dict[str, np.ndarray]],
dtypes: Optional[Union[tf.dtypes.DType, Dict[str, tf.dtypes.DType]]] = None,
) -> Union[tf.Tensor, Dict[str, tf.Tensor]]:
"""Convert a NumPy ndarray batch to a TensorFlow Tensor batch.
Args:
ndarrays: A (dict of) NumPy ndarray(s) that we wish to convert to a TensorFlow
Tensor.
dtypes: A (dict of) TensorFlow dtype(s) for the created tensor; if None, the
dtype will be inferred from the NumPy ndarray data.
Returns:
A (dict of) TensorFlow Tensor(s).
"""
if isinstance(ndarrays, np.ndarray):
# Single-tensor case.
if isinstance(dtypes, dict):
if len(dtypes) != 1:
raise ValueError(
"When constructing a single-tensor batch, only a single dtype "
f"should be given, instead got: {dtypes}"
)
dtypes = next(iter(dtypes.values()))
batch = convert_ndarray_to_tf_tensor(ndarrays, dtypes)
else:
# Multi-tensor case.
batch = {
col_name: convert_ndarray_to_tf_tensor(
col_ndarray,
dtype=dtypes[col_name] if isinstance(dtypes, dict) else dtypes,
)
for col_name, col_ndarray in ndarrays.items()
}
return batch
+105
View File
@@ -0,0 +1,105 @@
from typing import Any, Dict, List, Optional, Union
import torch
from ray.air._internal.device_manager import get_torch_device_manager_by_context
def get_devices() -> List[torch.device]:
"""Gets the correct torch device list configured for this process.
Returns a list of torch accelerator (GPU, HPU, NPU...) devices allocated for
the current worker.
If no accelerators are assigned, then it returns a list with a single CPU device.
"""
return get_torch_device_manager_by_context().get_devices()
def load_torch_model(
saved_model: Union[torch.nn.Module, Dict],
model_definition: Optional[torch.nn.Module] = None,
) -> torch.nn.Module:
"""Loads a PyTorch model from the provided ``saved_model``.
``model_definition`` is only used when ``saved_model`` is
a torch state dict, which will be loaded into ``model_definition``.
Otherwise, ``model_definition`` is discarded.
"""
if isinstance(saved_model, torch.nn.Module):
return saved_model
elif isinstance(saved_model, dict):
if not model_definition:
raise ValueError(
"Attempting to load torch model from a "
"state_dict, but no `model_definition` was "
"provided."
)
model_definition.load_state_dict(saved_model)
return model_definition
else:
raise ValueError(
f"Saved model is of type {type(saved_model)}. "
f"The model saved in the checkpoint is expected "
f"to be of type `torch.nn.Module`, or a model "
f"state dict of type dict."
)
def contains_tensor(obj):
if isinstance(obj, torch.Tensor):
return True
elif isinstance(obj, dict):
for k, v in obj.items():
if contains_tensor(k):
return True
if contains_tensor(v):
return True
elif isinstance(obj, (list, tuple)):
for v in obj:
if contains_tensor(v):
return True
return False
# Not present in torch<=1.7.0
# Adapted from https://github.com/pytorch/pytorch/blob/\
# c18da597e0bb1c1aecc97c77a73fed1849057fa4/torch/nn/modules/utils.py
def consume_prefix_in_state_dict_if_present_not_in_place(
state_dict: Dict[str, Any], prefix: str
) -> Dict[str, Any]:
"""Strip the prefix in state_dict, if any and return a new dict.
Adapted from https://github.com/pytorch/pytorch/blob/\
c18da597e0bb1c1aecc97c77a73fed1849057fa4/torch/nn/modules/utils.py
The original method modified the dict in-place.
Args:
state_dict: a state-dict to be loaded to the model.
prefix: prefix.
Returns:
A new state-dict with the prefix stripped from the keys.
"""
copied = False
for key in state_dict:
if key.startswith(prefix):
newkey = key[len(prefix) :]
if not copied:
# We are doing shallow copies here, so the performance
# impact should be negligible anyway, but this is
# a simple optimization.
state_dict = state_dict.copy()
copied = True
state_dict[newkey] = state_dict.pop(key)
if "_metadata" in state_dict:
state_dict["_metadata"] = state_dict["_metadata"].copy()
metadata = state_dict["_metadata"]
for key in metadata:
if len(key) == 0:
continue
newkey = key[len(prefix) :]
metadata[newkey] = metadata.pop(key)
return state_dict
+106
View File
@@ -0,0 +1,106 @@
import os
import urllib.parse
from pathlib import Path
from typing import Union
class URI:
"""Represents a URI, supporting path appending and retrieving parent URIs.
Example Usage:
>>> s3_uri = URI("s3://bucket/a?scheme=http&param=1")
>>> s3_uri
URI<s3://bucket/a?scheme=http&param=1>
>>> str(s3_uri / "b" / "c")
's3://bucket/a/b/c?scheme=http&param=1'
>>> str(s3_uri.parent)
's3://bucket?scheme=http&param=1'
>>> str(s3_uri)
's3://bucket/a?scheme=http&param=1'
>>> s3_uri.parent.name, s3_uri.name
('bucket', 'a')
>>> local_path = URI("/tmp/local")
>>> str(local_path)
'/tmp/local'
>>> str(local_path.parent)
'/tmp'
>>> str(local_path / "b" / "c")
'/tmp/local/b/c'
Args:
uri: The URI to represent.
Ex: s3://bucket?scheme=http&endpoint_override=localhost%3A900
Ex: file:///a/b/c/d
"""
def __init__(self, uri: str):
self._parsed = urllib.parse.urlparse(uri)
if not self._parsed.scheme:
# Just treat this as a regular path
self._path = Path(uri)
else:
self._path = Path(os.path.normpath(self._parsed.netloc + self._parsed.path))
def rstrip_subpath(self, subpath: Path) -> "URI":
"""Returns a new URI that strips the given subpath from the end of this URI.
Example:
>>> uri = URI("s3://bucket/a/b/c/?param=1")
>>> str(uri.rstrip_subpath(Path("b/c")))
's3://bucket/a?param=1'
>>> uri = URI("/tmp/a/b/c/")
>>> str(uri.rstrip_subpath(Path("/b/c/.//")))
'/tmp/a'
Args:
subpath: The subpath to strip from the end of this URI.
Returns:
A new URI with the subpath stripped from the end.
"""
assert str(self._path).endswith(str(subpath)), (self._path, subpath)
stripped_path = str(self._path).replace(str(subpath), "")
return URI(self._get_str_representation(self._parsed, stripped_path))
@property
def name(self) -> str:
return self._path.name
@property
def parent(self) -> "URI":
assert self._path.parent != ".", f"{str(self)} has no valid parent URI"
return URI(self._get_str_representation(self._parsed, self._path.parent))
@property
def scheme(self) -> str:
return self._parsed.scheme
@property
def path(self) -> str:
return str(self._path)
def __truediv__(self, path_to_append):
assert isinstance(path_to_append, str)
return URI(
self._get_str_representation(self._parsed, self._path / path_to_append)
)
@classmethod
def _get_str_representation(
cls, parsed_uri: urllib.parse.ParseResult, path: Union[str, Path]
) -> str:
if not parsed_uri.scheme:
return str(path)
return parsed_uri._replace(netloc=str(path), path="").geturl()
def __repr__(self):
return f"URI<{str(self)}>"
def __str__(self):
return self._get_str_representation(self._parsed, self._path)
def is_uri(path: str) -> bool:
return bool(urllib.parse.urlparse(path).scheme)
+277
View File
@@ -0,0 +1,277 @@
import collections
import json
import os
from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union
from ray._common.usage.usage_lib import TagKey, record_extra_usage_tag
if TYPE_CHECKING:
from ray.train._internal.storage import StorageContext
from ray.train.trainer import BaseTrainer
from ray.tune import Callback
from ray.tune.schedulers import TrialScheduler
from ray.tune.search import BasicVariantGenerator, Searcher
AIR_TRAINERS = {
"HorovodTrainer",
"LightGBMTrainer",
"TensorflowTrainer",
"TorchTrainer",
"XGBoostTrainer",
}
TRAIN_V2_TRAINERS = {
"DataParallelTrainer",
"JaxTrainer",
"LightGBMTrainer",
"TensorflowTrainer",
"TorchTrainer",
"XGBoostTrainer",
}
# searchers implemented by Ray Tune.
TUNE_SEARCHERS = {
"AxSearch",
"BayesOptSearch",
"TuneBOHB",
"HEBOSearch",
"HyperOptSearch",
"NevergradSearch",
"OptunaSearch",
"ZOOptSearch",
}
# These are just wrappers around real searchers.
# We don't want to double tag in this case, otherwise, the real tag
# will be overwritten.
TUNE_SEARCHER_WRAPPERS = {
"ConcurrencyLimiter",
"Repeater",
}
TUNE_SCHEDULERS = {
"FIFOScheduler",
"AsyncHyperBandScheduler",
"MedianStoppingRule",
"HyperBandScheduler",
"HyperBandForBOHB",
"PopulationBasedTraining",
"PopulationBasedTrainingReplay",
"PB2",
"ResourceChangingScheduler",
}
class AirEntrypoint(Enum):
TUNER = "Tuner.fit"
TRAINER = "Trainer.fit"
TUNE_RUN = "tune.run"
TUNE_RUN_EXPERIMENTS = "tune.run_experiments"
def _find_class_name(obj: object, allowed_module_path_prefix: str, whitelist: Set[str]):
"""Find the class name of the object. If the object is not
under `allowed_module_path_prefix` or if its class is not in the whitelist,
return "Custom".
Args:
obj: The object under inspection.
allowed_module_path_prefix: If the `obj`'s class is not under
the `allowed_module_path_prefix`, its class name will be anonymized.
whitelist: If the `obj`'s class is not in the `whitelist`,
it will be anonymized.
Returns:
The class name to be tagged with telemetry.
"""
module_path = obj.__module__
cls_name = obj.__class__.__name__
if module_path.startswith(allowed_module_path_prefix) and cls_name in whitelist:
return cls_name
else:
return "Custom"
def tag_air_trainer(trainer: "BaseTrainer"):
from ray.train.trainer import BaseTrainer
assert isinstance(trainer, BaseTrainer)
trainer_name = _find_class_name(trainer, "ray.train", AIR_TRAINERS)
record_extra_usage_tag(TagKey.AIR_TRAINER, trainer_name)
def tag_train_v2_trainer(trainer):
from ray.train.v2.api.data_parallel_trainer import DataParallelTrainer
assert isinstance(trainer, DataParallelTrainer)
trainer_name = _find_class_name(trainer, "ray.train", TRAIN_V2_TRAINERS)
record_extra_usage_tag(TagKey.TRAIN_TRAINER, trainer_name)
def tag_searcher(searcher: Union["BasicVariantGenerator", "Searcher"]):
from ray.tune.search import BasicVariantGenerator, Searcher
if isinstance(searcher, BasicVariantGenerator):
# Note this could be highly inflated as all train flows are treated
# as using BasicVariantGenerator.
record_extra_usage_tag(TagKey.TUNE_SEARCHER, "BasicVariantGenerator")
elif isinstance(searcher, Searcher):
searcher_name = _find_class_name(
searcher, "ray.tune.search", TUNE_SEARCHERS.union(TUNE_SEARCHER_WRAPPERS)
)
if searcher_name in TUNE_SEARCHER_WRAPPERS:
# ignore to avoid double tagging with wrapper name.
return
record_extra_usage_tag(TagKey.TUNE_SEARCHER, searcher_name)
else:
assert False, (
"Not expecting a non-BasicVariantGenerator, "
"non-Searcher type passed in for `tag_searcher`."
)
def tag_scheduler(scheduler: "TrialScheduler"):
from ray.tune.schedulers import TrialScheduler
assert isinstance(scheduler, TrialScheduler)
scheduler_name = _find_class_name(scheduler, "ray.tune.schedulers", TUNE_SCHEDULERS)
record_extra_usage_tag(TagKey.TUNE_SCHEDULER, scheduler_name)
def tag_setup_wandb():
record_extra_usage_tag(TagKey.AIR_SETUP_WANDB_INTEGRATION_USED, "1")
def tag_setup_mlflow():
record_extra_usage_tag(TagKey.AIR_SETUP_MLFLOW_INTEGRATION_USED, "1")
def _count_callbacks(callbacks: Optional[List["Callback"]]) -> Dict[str, int]:
"""Creates a map of callback class name -> count given a list of callbacks."""
from ray.air.integrations.comet import CometLoggerCallback
from ray.air.integrations.mlflow import MLflowLoggerCallback
from ray.air.integrations.wandb import WandbLoggerCallback
from ray.tune import Callback
from ray.tune.logger import LoggerCallback
from ray.tune.logger.aim import AimLoggerCallback
from ray.tune.utils.callback import DEFAULT_CALLBACK_CLASSES
built_in_callbacks = (
WandbLoggerCallback,
MLflowLoggerCallback,
CometLoggerCallback,
AimLoggerCallback,
) + DEFAULT_CALLBACK_CLASSES
callback_names = [callback_cls.__name__ for callback_cls in built_in_callbacks]
callback_counts = collections.defaultdict(int)
callbacks = callbacks or []
for callback in callbacks:
if not isinstance(callback, Callback):
# This will error later, but don't include this as custom usage.
continue
callback_name = callback.__class__.__name__
if callback_name in callback_names:
callback_counts[callback_name] += 1
elif isinstance(callback, LoggerCallback):
callback_counts["CustomLoggerCallback"] += 1
else:
callback_counts["CustomCallback"] += 1
return callback_counts
def tag_callbacks(callbacks: Optional[List["Callback"]]) -> bool:
"""Records built-in callback usage via a JSON str representing a
dictionary mapping callback class name -> counts.
User-defined callbacks will increment the count under the `CustomLoggerCallback`
or `CustomCallback` key depending on which of the provided interfaces they subclass.
NOTE: This will NOT track the name of the user-defined callback,
nor its implementation.
This will NOT report telemetry if no callbacks are provided by the user.
Args:
callbacks: List of callbacks supplied by the user. May be ``None``.
Returns:
bool: True if usage was recorded, False otherwise.
"""
if not callbacks:
# User didn't pass in any callbacks -> no usage recorded.
return False
callback_counts = _count_callbacks(callbacks)
if callback_counts:
callback_counts_str = json.dumps(callback_counts)
record_extra_usage_tag(TagKey.AIR_CALLBACKS, callback_counts_str)
def tag_storage_type(storage: "StorageContext"):
"""Records the storage configuration of an experiment.
The storage configuration is set by `RunConfig(storage_path, storage_filesystem)`.
The possible storage types (defined by `pyarrow.fs.FileSystem.type_name`) are:
- 'local' = pyarrow.fs.LocalFileSystem. This includes NFS usage.
- 'mock' = pyarrow.fs._MockFileSystem. This is used for testing.
- ('s3', 'gcs', 'abfs', 'hdfs'): Various remote storage schemes
with default implementations in pyarrow.
- 'custom' = All other storage schemes, which includes ALL cases where a
custom `storage_filesystem` is provided.
- 'other' = catches any other cases not explicitly handled above.
"""
whitelist = {"local", "mock", "s3", "gcs", "abfs", "hdfs"}
if storage.custom_fs_provided:
storage_config_tag = "custom"
elif storage.storage_filesystem.type_name in whitelist:
storage_config_tag = storage.storage_filesystem.type_name
else:
storage_config_tag = "other"
record_extra_usage_tag(TagKey.AIR_STORAGE_CONFIGURATION, storage_config_tag)
def tag_ray_air_env_vars() -> bool:
"""Records usage of environment variables exposed by the Ray AIR libraries.
NOTE: This does not track the values of the environment variables, nor
does this track environment variables not explicitly included in the
`all_ray_air_env_vars` allow-list.
Returns:
bool: True if at least one environment var is supplied by the user.
"""
from ray.air.constants import AIR_ENV_VARS
from ray.train.constants import TRAIN_ENV_VARS
from ray.tune.constants import TUNE_ENV_VARS
all_ray_air_env_vars = sorted(
set().union(AIR_ENV_VARS, TUNE_ENV_VARS, TRAIN_ENV_VARS)
)
user_supplied_env_vars = []
for env_var in all_ray_air_env_vars:
if env_var in os.environ:
user_supplied_env_vars.append(env_var)
if user_supplied_env_vars:
env_vars_str = json.dumps(user_supplied_env_vars)
record_extra_usage_tag(TagKey.AIR_ENV_VARS, env_vars_str)
return True
return False
def tag_air_entrypoint(entrypoint: AirEntrypoint) -> None:
"""Records the entrypoint to an AIR training run."""
assert entrypoint in AirEntrypoint
record_extra_usage_tag(TagKey.AIR_ENTRYPOINT, entrypoint.value)
+125
View File
@@ -0,0 +1,125 @@
import copy
import logging
import os
import queue
import threading
from typing import Optional
import numpy as np
from ray.air.constants import _ERROR_REPORT_TIMEOUT
logger = logging.getLogger(__name__)
def is_nan(value):
return np.isnan(value)
def is_nan_or_inf(value):
return is_nan(value) or np.isinf(value)
class StartTraceback(Exception):
"""These exceptions (and their tracebacks) can be skipped with `skip_exceptions`"""
pass
class StartTracebackWithWorkerRank(StartTraceback):
def __init__(self, worker_rank: int) -> None:
super().__init__()
self.worker_rank = worker_rank
def __reduce__(self):
return (self.__class__, (self.worker_rank,))
def skip_exceptions(exc: Optional[Exception]) -> Exception:
"""Skip all contained `StartTracebacks` to reduce traceback output.
Returns a shallow copy of the exception with all `StartTracebacks` removed.
If the RAY_AIR_FULL_TRACEBACKS environment variable is set,
the original exception (not a copy) is returned.
"""
should_not_shorten = bool(int(os.environ.get("RAY_AIR_FULL_TRACEBACKS", "0")))
if should_not_shorten:
return exc
if isinstance(exc, StartTraceback):
# If this is a StartTraceback, skip
return skip_exceptions(exc.__cause__)
# Perform a shallow copy to prevent recursive __cause__/__context__.
new_exc = copy.copy(exc).with_traceback(exc.__traceback__)
# Make sure nested exceptions are properly skipped.
cause = getattr(exc, "__cause__", None)
if cause:
new_exc.__cause__ = skip_exceptions(cause)
return new_exc
def exception_cause(exc: Optional[Exception]) -> Optional[Exception]:
if not exc:
return None
return getattr(exc, "__cause__", None)
class RunnerThread(threading.Thread):
"""Supervisor thread that runs your script."""
def __init__(self, *args, error_queue, **kwargs):
threading.Thread.__init__(self, *args, **kwargs)
self._error_queue = error_queue
self._ret = None
def _propagate_exception(self, e: BaseException):
try:
# report the error but avoid indefinite blocking which would
# prevent the exception from being propagated in the unlikely
# case that something went terribly wrong
self._error_queue.put(e, block=True, timeout=_ERROR_REPORT_TIMEOUT)
except queue.Full:
logger.critical(
(
"Runner Thread was unable to report error to main "
"function runner thread. This means a previous error "
"was not processed. This should never happen."
)
)
def run(self):
try:
self._ret = self._target(*self._args, **self._kwargs)
except StopIteration:
logger.debug(
(
"Thread runner raised StopIteration. Interpreting it as a "
"signal to terminate the thread without error."
)
)
except SystemExit as e:
# Do not propagate up for graceful termination.
if e.code == 0:
logger.debug(
(
"Thread runner raised SystemExit with error code 0. "
"Interpreting it as a signal to terminate the thread "
"without error."
)
)
else:
# If non-zero exit code, then raise exception to main thread.
self._propagate_exception(e)
except BaseException as e:
# Propagate all other exceptions to the main thread.
self._propagate_exception(e)
def join(self, timeout=None):
super(RunnerThread, self).join(timeout)
return self._ret