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
+68
View File
@@ -0,0 +1,68 @@
import os
import ray
from ray.air.constants import COPY_DIRECTORY_CHECKPOINTS_INSTEAD_OF_MOVING_ENV
from ray.train.constants import (
ENABLE_V2_MIGRATION_WARNINGS_ENV_VAR,
RAY_CHDIR_TO_TRIAL_DIR,
)
from ray.train.v2._internal.constants import (
ENV_VARS_TO_PROPAGATE as TRAIN_ENV_VARS_TO_PROPAGATE,
)
DEFAULT_ENV_VARS = {
# https://github.com/ray-project/ray/issues/28197
"PL_DISABLE_FORK": "1"
}
ENV_VARS_TO_PROPAGATE = (
{
COPY_DIRECTORY_CHECKPOINTS_INSTEAD_OF_MOVING_ENV,
RAY_CHDIR_TO_TRIAL_DIR,
ENABLE_V2_MIGRATION_WARNINGS_ENV_VAR,
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"AWS_SECURITY_TOKEN",
"AWS_SESSION_TOKEN",
}
# Propagate the Ray Train environment variables from the driver process
# to the trainable process so that Tune + Train v2 can be used together.
| TRAIN_ENV_VARS_TO_PROPAGATE
)
class _ActorClassCache:
"""Caches actor classes.
ray.remote is a registration call. It sends the serialized object to the
key value store (redis), and will be fetched at an arbitrary worker
later. Registration does not use any Ray scheduling resources.
Later, class.remote() actually creates the remote actor. The
actor will be instantiated on some arbitrary machine,
according to the underlying Ray scheduler.
Without this cache, you would register the same serialized object
over and over again. Naturally, since redis doesnt spill to disk,
this can easily nuke the redis instance (and basically blow up Ray).
This cache instead allows us to register once and only once.
Note that we assume there can be multiple trainables in the
system at once.
"""
def __init__(self):
self._cache = {}
def get(self, trainable_cls):
"""Gets the wrapped trainable_cls, otherwise calls ray.remote."""
env_vars = DEFAULT_ENV_VARS.copy()
for env_var_to_propagate in ENV_VARS_TO_PROPAGATE:
if env_var_to_propagate in os.environ:
env_vars[env_var_to_propagate] = os.environ[env_var_to_propagate]
runtime_env = {"env_vars": env_vars}
if trainable_cls not in self._cache:
remote_cls = ray.remote(runtime_env=runtime_env)(trainable_cls)
self._cache[trainable_cls] = remote_cls
return self._cache[trainable_cls]
+12
View File
@@ -0,0 +1,12 @@
from functools import lru_cache
from pathlib import Path
@lru_cache()
def _is_ray_cluster():
"""Checks if the bootstrap config file exists.
This will always exist if using an autoscaling cluster/started
with the ray cluster launcher.
"""
return Path("~/ray_bootstrap_config.yaml").expanduser().exists()
@@ -0,0 +1,290 @@
import fnmatch
import logging
import os
import time
from collections import Counter
from pathlib import Path
from typing import Callable, Dict, Optional, Union
import pyarrow.fs
from ray.train._internal.storage import (
StorageContext,
_download_from_fs_path,
_list_at_fs_path,
get_fs_and_path,
)
from ray.tune.experiment.trial import Trial
from ray.tune.impl.out_of_band_serialize_dataset import out_of_band_serialize_dataset
logger = logging.getLogger(__name__)
_SLOW_SYNC_WARNING = (
"This could be due to a large number of trials, "
"large logfiles from lots of reported metrics, or throttling from the "
"remote storage if uploading too frequently.\n"
"You may want to consider switching the `RunConfig(storage_filesystem)`"
" to a more performant storage backend such as s3fs for a "
"S3 storage path.\n"
"You can suppress this error by setting the environment variable "
"TUNE_WARN_SLOW_EXPERIMENT_CHECKPOINT_SYNC_THRESHOLD_S to a higher "
"value than the current threshold ({threshold})."
)
def _find_newest_experiment_checkpoint(
experiment_path: str, fs: Optional[pyarrow.fs.FileSystem] = None
) -> Optional[str]:
"""Returns file name of most recently created experiment checkpoint.
Args:
experiment_path: Local or remote path to the experiment directory
containing at least one experiment checkpoint file.
fs: Optional custom ``pyarrow.fs.FileSystem`` corresponding to
``experiment_path``. If not provided, one is inferred from the
path.
Returns:
str: The local or remote path to the latest experiment checkpoint file
based on timestamp. None if no experiment checkpoints were found.
"""
from ray.tune.execution.tune_controller import TuneController
fs, experiment_fs_path = get_fs_and_path(experiment_path, storage_filesystem=fs)
filenames = _list_at_fs_path(fs=fs, fs_path=experiment_fs_path)
pattern = TuneController.CKPT_FILE_TMPL.format("*")
matching = fnmatch.filter(filenames, pattern)
if not matching:
return None
filename = max(matching)
return Path(experiment_fs_path, filename).as_posix()
class _ExperimentCheckpointManager:
"""Helper class for managing experiment-level checkpoints.
This class implements the ``checkpoint()`` method used to checkpoint
experiment state. When called, this will serialize and write to disk
the state of the trial runner, trial executor, and search algorithm, to
a specified checkpoint file.
The checkpoint period is automatically adjusted to
``max(10, time_per_checkpoint * 19)``. This means that at most 5% of the
time (1/20) will be used for writing checkpoints, while 95% of the time
(19/20) will be used to handle the rest of the training loop.
"""
def __init__(
self,
*,
storage: Optional[StorageContext],
checkpoint_period: Union[int, float, str],
sync_every_n_trial_checkpoints: Optional[int] = None,
):
self._storage = storage
self._last_save_time = float("-inf")
self._last_sync_time = None
# Dynamic checkpointing period
self._auto_checkpoint_enabled = checkpoint_period == "auto"
if self._auto_checkpoint_enabled:
self._checkpoint_period = 10.0 # Initial value
else:
self._checkpoint_period = float(checkpoint_period)
# TODO(justinvyu): This is a non-performant workaround to force sync
# every num_to_keep checkpoints in order to maintain consistency
# between the experiment state's view of the latest checkpoint,
# and the actual latest checkpoint that was uploaded.
self._sync_every_n_trial_checkpoints = sync_every_n_trial_checkpoints
self._trial_num_checkpoints_since_last_sync: Dict[Trial, int] = Counter()
self._should_force_sync_up: bool = False
self._excessive_sync_threshold = float(
os.environ.get(
"TUNE_WARN_EXCESSIVE_EXPERIMENT_CHECKPOINT_SYNC_THRESHOLD_S", "5"
)
)
self._slow_sync_threshold = float(
os.environ.get(
"TUNE_WARN_SLOW_EXPERIMENT_CHECKPOINT_SYNC_THRESHOLD_S", "30"
)
)
@property
def auto_checkpoint_enabled(self):
return self._auto_checkpoint_enabled
def _update_auto_checkpoint_time(self, time_taken: float):
if self._auto_checkpoint_enabled:
# Multiplying this time by 19 means we spend ~5% of the time
# writing global checkpoints and 95% of the time processing trials
self._checkpoint_period = max(10.0, time_taken * 19)
logger.debug(
f"Experiment state snapshotting took "
f"{time_taken:.2f} seconds. "
f"Adjusting snapshotting period to "
f"{self._checkpoint_period:.2f} seconds."
)
def sync_up_experiment_state(
self,
save_fn: Callable[[], None],
force: bool = False,
wait: bool = False,
) -> None:
"""Saves execution state to the experiment directory on the storage path.
This includes an experiment checkpoint file that contains trial statuses
and the searcher state.
Overwrites the current session checkpoint, which starts when self
is instantiated. Throttle depends on self._checkpoint_period.
Args:
save_fn: Function to call to actually save data to the driver
staging path. The files in the driver staging path will be
uploaded to the storage path.
force: Forces an experiment checkpoint and launches a sync to storage.
This happens regardless of checkpoint_period
wait: Waits for the sync up to complete before returning.
"""
driver_staging_path = self._storage.experiment_driver_staging_path
force = force or self._should_force_sync_up
now = time.monotonic()
if now - self._last_save_time < self._checkpoint_period and not force:
return
# Checkpoint
checkpoint_time_start = time.monotonic()
# NOTE: This context manager is for Datasets captured in a trial config.
# This is the case when *tuning over datasets*.
# If the datasets have already been full executed, then serializing
# block refs means that this checkpoint is not usable in a new Ray cluster.
# This context will serialize the dataset execution plan instead, if available.
with out_of_band_serialize_dataset():
save_fn()
def wait_for_sync():
try:
self._storage.syncer.wait()
except Exception:
logger.error(
"Saving experiment state to storage at "
f"'{self._storage.experiment_fs_path}' failed with exception: ",
exc_info=True,
)
if force:
start_time = time.monotonic()
wait_for_sync()
wait_time = time.monotonic() - start_time
if wait_time > self._slow_sync_threshold:
logger.warning(
"Saving the experiment state (which holds a global view "
"of trial statuses and is used to restore the experiment) "
f"took ~{wait_time:.2f} seconds, which may be a performance "
"bottleneck.\n"
f"{_SLOW_SYNC_WARNING.format(threshold=self._slow_sync_threshold)}"
)
time_since_last_sync = (
time.monotonic() - self._last_sync_time
if self._last_sync_time is not None
else None
)
launched_sync = self._storage.syncer.sync_up(
driver_staging_path, self._storage.experiment_fs_path
)
if launched_sync:
if (
time_since_last_sync is not None
and time_since_last_sync < self._excessive_sync_threshold
and self._should_force_sync_up
):
logger.warning(
"Experiment state snapshotting has been triggered multiple "
f"times in the last {self._excessive_sync_threshold} seconds "
"and may become a bottleneck. "
"A snapshot is forced if `CheckpointConfig(num_to_keep)` is set, "
"and a trial has checkpointed >= `num_to_keep` times "
"since the last snapshot.\n"
"You may want to consider increasing the "
"`CheckpointConfig(num_to_keep)` or decreasing the frequency of "
"saving checkpoints.\n"
"You can suppress this warning by setting the environment variable "
"TUNE_WARN_EXCESSIVE_EXPERIMENT_CHECKPOINT_SYNC_THRESHOLD_S "
"to a smaller value than the current threshold "
f"({self._excessive_sync_threshold}). "
"Set it to 0 to completely suppress this warning."
)
self._last_sync_time = time.monotonic()
# We just synced, so reset the force flag
self._trial_num_checkpoints_since_last_sync.clear()
self._should_force_sync_up = False
else:
if (
time_since_last_sync is not None
and time_since_last_sync > self._slow_sync_threshold
):
logger.warning(
"Saving the experiment state (which holds a global view "
"of trial statuses and is used to restore the experiment) "
f"has already taken {time_since_last_sync:.2f} seconds, "
"which may cause consistency issues upon restoration if your "
"driver script ungracefully exits.\n"
f"{_SLOW_SYNC_WARNING.format(threshold=self._slow_sync_threshold)}"
)
if wait:
wait_for_sync()
checkpoint_time_taken = time.monotonic() - checkpoint_time_start
# Adjust dynamic checkpointing
self._update_auto_checkpoint_time(time_taken=checkpoint_time_taken)
# Finish
self._last_save_time = time.monotonic()
def sync_down_experiment_state(self) -> None:
fs = self._storage.storage_filesystem
filepaths = _list_at_fs_path(fs=fs, fs_path=self._storage.experiment_fs_path)
# TODO(ekl) we should refactor our restore code to read the necessary data
# directly from the storage context. As a temporary hack, restore all the
# serialized files from the root dir where other modules expect them to be.
matches = [
path
for path in filepaths
if path.endswith(".json") or path.endswith(".pkl")
]
for relpath in matches:
fs_path = Path(self._storage.experiment_fs_path, relpath).as_posix()
local_path = Path(
self._storage.experiment_driver_staging_path, relpath
).as_posix()
_download_from_fs_path(fs=fs, fs_path=fs_path, local_path=local_path)
logger.debug(
f"Copied {matches} from:\n(fs, path) = "
f"({self._storage.storage_filesystem.type_name}, "
f"{self._storage.experiment_fs_path})\n"
f"-> {self._storage.experiment_driver_staging_path}"
)
def on_trial_checkpoint(self, trial: Trial):
if not self._sync_every_n_trial_checkpoints:
return
self._trial_num_checkpoints_since_last_sync[trial] += 1
if (
self._trial_num_checkpoints_since_last_sync[trial]
>= self._sync_every_n_trial_checkpoints
):
self._should_force_sync_up = True
@@ -0,0 +1,167 @@
import logging
import os
import time
from functools import lru_cache
from typing import Dict, Optional, Tuple
import ray
from ray.tune.execution.cluster_info import _is_ray_cluster
from ray.tune.experiment import Trial
logger = logging.getLogger(__name__)
# Ideally we want to use @cache; but it's only available for python 3.9.
# Caching is only helpful/correct for no autoscaler case.
@lru_cache()
def _get_cluster_resources_no_autoscaler() -> Dict:
return ray.cluster_resources()
def _get_trial_cpu_and_gpu(trial: Trial) -> Tuple[int, int]:
cpu = trial.placement_group_factory.required_resources.get("CPU", 0)
gpu = trial.placement_group_factory.required_resources.get("GPU", 0)
return cpu, gpu
def _can_fulfill_no_autoscaler(trial: Trial) -> bool:
"""Calculates if there is enough resources for a PENDING trial.
For no autoscaler case.
"""
assert trial.status == Trial.PENDING
asked_cpus, asked_gpus = _get_trial_cpu_and_gpu(trial)
return asked_cpus <= _get_cluster_resources_no_autoscaler().get(
"CPU", 0
) and asked_gpus <= _get_cluster_resources_no_autoscaler().get("GPU", 0)
@lru_cache()
def _get_insufficient_resources_warning_threshold() -> float:
if _is_ray_cluster():
return float(
os.environ.get(
"TUNE_WARN_INSUFFICENT_RESOURCE_THRESHOLD_S_AUTOSCALER", "60"
)
)
else:
# Set the default to 10s so that we don't prematurely determine that
# a cluster cannot fulfill the resources requirements.
# TODO(xwjiang): Change it back once #18608 is resolved.
return float(os.environ.get("TUNE_WARN_INSUFFICENT_RESOURCE_THRESHOLD_S", "60"))
MSG_TRAIN_START = (
"Training has not started in the last {wait_time:.0f} seconds. "
"This could be due to the cluster not having enough resources available. "
)
MSG_TRAIN_INSUFFICIENT = (
"You asked for {asked_cpus} CPUs and {asked_gpus} GPUs, but the cluster only "
"has {cluster_cpus} CPUs and {cluster_gpus} GPUs available. "
)
MSG_TRAIN_END = (
"Stop the training and adjust the required resources (e.g. via the "
"`ScalingConfig` or `resources_per_trial`, or `num_workers` for rllib), "
"or add more resources to your cluster."
)
MSG_TUNE_START = (
"No trial is running and no new trial has been started within "
"the last {wait_time:.0f} seconds. "
"This could be due to the cluster not having enough resources available. "
)
MSG_TUNE_INSUFFICIENT = (
"You asked for {asked_cpus} CPUs and {asked_gpus} GPUs per trial, "
"but the cluster only has {cluster_cpus} CPUs and {cluster_gpus} GPUs available. "
)
MSG_TUNE_END = (
"Stop the tuning and adjust the required resources (e.g. via the "
"`ScalingConfig` or `resources_per_trial`, or `num_workers` for rllib), "
"or add more resources to your cluster."
)
# TODO(xwjiang): Consider having a help page with more detailed instructions.
@lru_cache()
def _get_insufficient_resources_warning_msg(
for_train: bool = False, trial: Optional[Trial] = None
) -> str:
msg = "Ignore this message if the cluster is autoscaling. "
if for_train:
start = MSG_TRAIN_START
insufficient = MSG_TRAIN_INSUFFICIENT
end = MSG_TRAIN_END
else:
start = MSG_TUNE_START
insufficient = MSG_TUNE_INSUFFICIENT
end = MSG_TUNE_END
msg += start.format(wait_time=_get_insufficient_resources_warning_threshold())
if trial:
asked_cpus, asked_gpus = _get_trial_cpu_and_gpu(trial)
cluster_resources = _get_cluster_resources_no_autoscaler()
msg += insufficient.format(
asked_cpus=asked_cpus,
asked_gpus=asked_gpus,
cluster_cpus=cluster_resources.get("CPU", 0),
cluster_gpus=cluster_resources.get("GPU", 0),
)
msg += end
return msg
class _InsufficientResourcesManager:
"""Insufficient resources manager.
Makes best effort, conservative guesses about if Tune loop is stuck due to
infeasible resources. If so, outputs usability messages for users to
act upon.
"""
def __init__(self, for_train: bool = False):
# The information tracked across the life time of Tune loop.
self._no_running_trials_since = -1
self._last_trial_num = -1
self._for_train = for_train
def on_no_available_trials(self, all_trials):
"""Tracks information across the life of Tune loop and makes guesses
about if Tune loop is stuck due to infeasible resources.
If so, outputs certain warning messages.
The logic should be conservative, non-intrusive and informative.
For example, rate limiting is applied so that the message is not
spammy.
"""
# This is approximately saying we are not making progress.
if len(all_trials) == self._last_trial_num:
if self._no_running_trials_since == -1:
self._no_running_trials_since = time.monotonic()
elif (
time.monotonic() - self._no_running_trials_since
> _get_insufficient_resources_warning_threshold()
):
can_fulfill_any = any(
trial.status == Trial.PENDING and _can_fulfill_no_autoscaler(trial)
for trial in all_trials
)
if can_fulfill_any:
# If one trial can be fulfilled, it will be fulfilled eventually
self._no_running_trials_since = -1
return
# Otherwise, can fulfill none
msg = _get_insufficient_resources_warning_msg(
for_train=self._for_train, trial=all_trials[0]
)
logger.warning(msg)
self._no_running_trials_since = time.monotonic()
else:
self._no_running_trials_since = -1
self._last_trial_num = len(all_trials)
@@ -0,0 +1,131 @@
import warnings
from typing import Dict, Optional
from ray.air.execution.resources.request import ResourceRequest
from ray.util.annotations import DeveloperAPI, PublicAPI
from ray.util.placement_group import placement_group
@PublicAPI(stability="beta")
class PlacementGroupFactory(ResourceRequest):
"""Wrapper class that creates placement groups for trials.
This function should be used to define resource requests for Ray Tune
trials. It holds the parameters to create
:ref:`placement groups <ray-placement-group-doc-ref>`.
At a minimum, this will hold at least one bundle specifying the
resource requirements for each trial:
.. code-block:: python
from ray import tune
tuner = tune.Tuner(
tune.with_resources(
train,
resources=tune.PlacementGroupFactory([
{"CPU": 1, "GPU": 0.5, "custom_resource": 2}
])
)
)
tuner.fit()
If the trial itself schedules further remote workers, the resource
requirements should be specified in additional bundles. You can also
pass the placement strategy for these bundles, e.g. to enforce
co-located placement:
.. code-block:: python
from ray import tune
tuner = tune.Tuner(
tune.with_resources(
train,
resources=tune.PlacementGroupFactory([
{"CPU": 1, "GPU": 0.5, "custom_resource": 2},
{"CPU": 2},
{"CPU": 2},
], strategy="PACK")
)
)
tuner.fit()
The example above will reserve 1 CPU, 0.5 GPUs and 2 custom_resources
for the trainable itself, and reserve another 2 bundles of 2 CPUs each.
The trial will only start when all these resources are available. This
could be used e.g. if you had one learner running in the main trainable
that schedules two remote workers that need access to 2 CPUs each.
If the trainable itself doesn't require resources.
You can specify it as:
.. code-block:: python
from ray import tune
tuner = tune.Tuner(
tune.with_resources(
train,
resources=tune.PlacementGroupFactory([
{},
{"CPU": 2},
{"CPU": 2},
], strategy="PACK")
)
)
tuner.fit()
Args:
bundles: A list of bundles which
represent the resources requirements.
strategy: The strategy to create the placement group.
- "PACK": Packs Bundles into as few nodes as possible.
- "SPREAD": Places Bundles across distinct nodes as even as possible.
- "STRICT_PACK": Packs Bundles into one node. The group is
not allowed to span multiple nodes.
- "STRICT_SPREAD": Packs Bundles across distinct nodes.
*args: Passed to the call of ``placement_group()``
**kwargs: Passed to the call of ``placement_group()``
"""
def __call__(self, *args, **kwargs):
warnings.warn(
"Calling PlacementGroupFactory objects is deprecated. Use "
"`to_placement_group()` instead.",
DeprecationWarning,
)
kwargs.update(self._bound.kwargs)
# Call with bounded *args and **kwargs
return placement_group(*self._bound.args, **kwargs)
@DeveloperAPI
def resource_dict_to_pg_factory(spec: Optional[Dict[str, float]] = None):
"""Translates resource dict into PlacementGroupFactory."""
spec = spec or {"cpu": 1}
spec = spec.copy()
cpus = spec.pop("cpu", spec.pop("CPU", 0.0))
gpus = spec.pop("gpu", spec.pop("GPU", 0.0))
memory = spec.pop("memory", 0.0)
# If there is a custom_resources key, use as base for bundle
bundle = dict(spec.pop("custom_resources", {}))
# Otherwise, consider all other keys as custom resources
if not bundle:
bundle = spec
bundle.update(
{
"CPU": cpus,
"GPU": gpus,
"memory": memory,
}
)
return PlacementGroupFactory([bundle])
File diff suppressed because it is too large Load Diff