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