mirror of
https://github.com/wassname/ray.git
synced 2026-09-11 12:43:20 +08:00
[tune] placement group support (#13370)
This commit is contained in:
@@ -688,6 +688,8 @@ These are the environment variables Ray Tune currently considers:
|
|||||||
experiment state is checkpointed. If not set this will default to ``10``.
|
experiment state is checkpointed. If not set this will default to ``10``.
|
||||||
* **TUNE_MAX_LEN_IDENTIFIER**: Maximum length of trial subdirectory names (those
|
* **TUNE_MAX_LEN_IDENTIFIER**: Maximum length of trial subdirectory names (those
|
||||||
with the parameter values in them)
|
with the parameter values in them)
|
||||||
|
* **TUNE_MAX_PENDING_TRIALS_PG**: Maximum number of pending trials when placement groups are used. Defaults
|
||||||
|
to ``1000``.
|
||||||
* **TUNE_RESULT_DIR**: Directory where Ray Tune trial results are stored. If this
|
* **TUNE_RESULT_DIR**: Directory where Ray Tune trial results are stored. If this
|
||||||
is not set, ``~/ray_results`` will be used.
|
is not set, ``~/ray_results`` will be used.
|
||||||
* **TUNE_RESULT_BUFFER_LENGTH**: Ray Tune can buffer results from trainables before they are passed
|
* **TUNE_RESULT_BUFFER_LENGTH**: Ray Tune can buffer results from trainables before they are passed
|
||||||
@@ -697,6 +699,9 @@ These are the environment variables Ray Tune currently considers:
|
|||||||
but never longer than this value. Defaults to 100 (seconds).
|
but never longer than this value. Defaults to 100 (seconds).
|
||||||
* **TUNE_RESULT_BUFFER_MIN_TIME_S**: Additionally, you can specify a minimum time to buffer results. Defaults to 0.
|
* **TUNE_RESULT_BUFFER_MIN_TIME_S**: Additionally, you can specify a minimum time to buffer results. Defaults to 0.
|
||||||
* **TUNE_SYNCER_VERBOSITY**: Amount of command output when using Tune with Docker Syncer. Defaults to 0.
|
* **TUNE_SYNCER_VERBOSITY**: Amount of command output when using Tune with Docker Syncer. Defaults to 0.
|
||||||
|
* **TUNE_TRIAL_STARTUP_GRACE_PERIOD**: Amount of time after starting a trial that Ray Tune checks for successful
|
||||||
|
trial startups. After the grace period, Tune will block until a result from a running trial is received. Can
|
||||||
|
be disabled by setting this to lower or equal to 0.
|
||||||
* **TUNE_WARN_THRESHOLD_S**: Threshold for logging if an Tune event loop operation takes too long. Defaults to 0.5 (seconds).
|
* **TUNE_WARN_THRESHOLD_S**: Threshold for logging if an Tune event loop operation takes too long. Defaults to 0.5 (seconds).
|
||||||
* **TUNE_STATE_REFRESH_PERIOD**: Frequency of updating the resource tracking from Ray. Defaults to 10 (seconds).
|
* **TUNE_STATE_REFRESH_PERIOD**: Frequency of updating the resource tracking from Ray. Defaults to 10 (seconds).
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import json
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
# For compatibility under py2 to consider unicode as str
|
# For compatibility under py2 to consider unicode as str
|
||||||
|
from ray.tune.utils.serialization import TuneFunctionEncoder
|
||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
||||||
from ray.tune import TuneError
|
from ray.tune import TuneError
|
||||||
from ray.tune.trial import Trial
|
from ray.tune.trial import Trial
|
||||||
from ray.tune.resources import json_to_resources
|
from ray.tune.resources import PlacementGroupFactory, json_to_resources
|
||||||
from ray.tune.utils.util import SafeFallbackEncoder
|
from ray.tune.utils.util import SafeFallbackEncoder
|
||||||
|
|
||||||
|
|
||||||
@@ -142,11 +143,16 @@ def to_argv(config):
|
|||||||
argv.append(v)
|
argv.append(v)
|
||||||
elif isinstance(v, bool):
|
elif isinstance(v, bool):
|
||||||
pass
|
pass
|
||||||
|
elif callable(v):
|
||||||
|
argv.append(json.dumps(v, cls=TuneFunctionEncoder))
|
||||||
else:
|
else:
|
||||||
argv.append(json.dumps(v, cls=SafeFallbackEncoder))
|
argv.append(json.dumps(v, cls=SafeFallbackEncoder))
|
||||||
return argv
|
return argv
|
||||||
|
|
||||||
|
|
||||||
|
_cached_pgf = {}
|
||||||
|
|
||||||
|
|
||||||
def create_trial_from_spec(spec, output_path, parser, **trial_kwargs):
|
def create_trial_from_spec(spec, output_path, parser, **trial_kwargs):
|
||||||
"""Creates a Trial object from parsing the spec.
|
"""Creates a Trial object from parsing the spec.
|
||||||
|
|
||||||
@@ -163,13 +169,34 @@ def create_trial_from_spec(spec, output_path, parser, **trial_kwargs):
|
|||||||
Returns:
|
Returns:
|
||||||
A trial object with corresponding parameters to the specification.
|
A trial object with corresponding parameters to the specification.
|
||||||
"""
|
"""
|
||||||
|
global _cached_pgf
|
||||||
|
|
||||||
|
spec = spec.copy()
|
||||||
|
resources = spec.pop("resources_per_trial", None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args, _ = parser.parse_known_args(to_argv(spec))
|
args, _ = parser.parse_known_args(to_argv(spec))
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
raise TuneError("Error parsing args, see above message", spec)
|
raise TuneError("Error parsing args, see above message", spec)
|
||||||
if "resources_per_trial" in spec:
|
|
||||||
trial_kwargs["resources"] = json_to_resources(
|
if resources:
|
||||||
spec["resources_per_trial"])
|
if isinstance(resources, PlacementGroupFactory):
|
||||||
|
trial_kwargs["placement_group_factory"] = resources
|
||||||
|
elif callable(resources):
|
||||||
|
if resources in _cached_pgf:
|
||||||
|
trial_kwargs["placement_group_factory"] = _cached_pgf[
|
||||||
|
resources]
|
||||||
|
else:
|
||||||
|
pgf = PlacementGroupFactory(resources)
|
||||||
|
_cached_pgf[resources] = pgf
|
||||||
|
trial_kwargs["placement_group_factory"] = pgf
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
trial_kwargs["resources"] = json_to_resources(resources)
|
||||||
|
except (TuneError, ValueError) as exc:
|
||||||
|
raise TuneError("Error parsing resources_per_trial",
|
||||||
|
resources) from exc
|
||||||
|
|
||||||
return Trial(
|
return Trial(
|
||||||
# Submitting trial via server in py2.7 creates Unicode, which does not
|
# Submitting trial via server in py2.7 creates Unicode, which does not
|
||||||
# convert to string in a straightforward manner.
|
# convert to string in a straightforward manner.
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ def NamespacedKubernetesSyncer(namespace):
|
|||||||
Args:
|
Args:
|
||||||
namespace (str): Kubernetes namespace.
|
namespace (str): Kubernetes namespace.
|
||||||
|
|
||||||
Returns: A ``KubernetesSyncer`` class to be passed to ``tune.run()``.
|
Returns:
|
||||||
|
A ``KubernetesSyncer`` class to be passed to ``tune.run()``.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import random
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
import ray
|
import ray
|
||||||
|
from ray.actor import ActorHandle
|
||||||
from ray.exceptions import GetTimeoutError
|
from ray.exceptions import GetTimeoutError
|
||||||
from ray import ray_constants
|
from ray import ray_constants
|
||||||
from ray.resource_spec import ResourceSpec
|
from ray.resource_spec import ResourceSpec
|
||||||
@@ -18,10 +20,12 @@ from ray.tune.function_runner import FunctionRunner
|
|||||||
from ray.tune.logger import NoopLogger
|
from ray.tune.logger import NoopLogger
|
||||||
from ray.tune.result import TRIAL_INFO, STDOUT_FILE, STDERR_FILE
|
from ray.tune.result import TRIAL_INFO, STDOUT_FILE, STDERR_FILE
|
||||||
from ray.tune.resources import Resources
|
from ray.tune.resources import Resources
|
||||||
|
from ray.tune.utils.placement_groups import PlacementGroupManager
|
||||||
from ray.tune.utils.trainable import TrainableUtil
|
from ray.tune.utils.trainable import TrainableUtil
|
||||||
from ray.tune.trial import Trial, Checkpoint, Location, TrialInfo
|
from ray.tune.trial import Trial, Checkpoint, Location, TrialInfo
|
||||||
from ray.tune.trial_executor import TrialExecutor
|
from ray.tune.trial_executor import TrialExecutor
|
||||||
from ray.tune.utils import warn_if_slow
|
from ray.tune.utils import warn_if_slow
|
||||||
|
from ray.util.placement_group import PlacementGroup, remove_placement_group
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -88,11 +92,14 @@ class _TrialCleanup:
|
|||||||
is passed, cleanup will kick in and remove futures.
|
is passed, cleanup will kick in and remove futures.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, threshold=TRIAL_CLEANUP_THRESHOLD):
|
def __init__(self, threshold: int = TRIAL_CLEANUP_THRESHOLD):
|
||||||
self.threshold = threshold
|
self.threshold = threshold
|
||||||
self._cleanup_map = {}
|
self._cleanup_map = {}
|
||||||
|
|
||||||
def add(self, trial, actor):
|
def add(self,
|
||||||
|
trial: Trial,
|
||||||
|
actor: ActorHandle,
|
||||||
|
placement_group: Optional[PlacementGroup] = None):
|
||||||
"""Adds a trial actor to be stopped.
|
"""Adds a trial actor to be stopped.
|
||||||
|
|
||||||
If the number of futures exceeds the threshold, the cleanup mechanism
|
If the number of futures exceeds the threshold, the cleanup mechanism
|
||||||
@@ -101,15 +108,20 @@ class _TrialCleanup:
|
|||||||
Args:
|
Args:
|
||||||
trial (Trial): The trial corresponding to the future.
|
trial (Trial): The trial corresponding to the future.
|
||||||
actor (ActorHandle): Handle to the trainable to be stopped.
|
actor (ActorHandle): Handle to the trainable to be stopped.
|
||||||
|
placement_group (PlacementGroup): Placement group to stop.
|
||||||
"""
|
"""
|
||||||
future = actor.stop.remote()
|
future = actor.stop.remote()
|
||||||
actor.__ray_terminate__.remote()
|
|
||||||
|
if placement_group:
|
||||||
|
remove_placement_group(placement_group)
|
||||||
|
else:
|
||||||
|
actor.__ray_terminate__.remote()
|
||||||
|
|
||||||
self._cleanup_map[future] = trial
|
self._cleanup_map[future] = trial
|
||||||
if len(self._cleanup_map) > self.threshold:
|
if len(self._cleanup_map) > self.threshold:
|
||||||
self.cleanup(partial=True)
|
self.cleanup(partial=True)
|
||||||
|
|
||||||
def cleanup(self, partial=True):
|
def cleanup(self, partial: bool = True):
|
||||||
"""Waits for cleanup to finish.
|
"""Waits for cleanup to finish.
|
||||||
|
|
||||||
If partial=False, all futures are expected to return. If a future
|
If partial=False, all futures are expected to return. If a future
|
||||||
@@ -141,10 +153,10 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
"""An implementation of TrialExecutor based on Ray."""
|
"""An implementation of TrialExecutor based on Ray."""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
queue_trials=False,
|
queue_trials: bool = False,
|
||||||
reuse_actors=False,
|
reuse_actors: bool = False,
|
||||||
ray_auto_init=None,
|
ray_auto_init: Optional[bool] = None,
|
||||||
refresh_period=None):
|
refresh_period: Optional[float] = None):
|
||||||
if ray_auto_init is None:
|
if ray_auto_init is None:
|
||||||
if os.environ.get("TUNE_DISABLE_AUTO_INIT") == "1":
|
if os.environ.get("TUNE_DISABLE_AUTO_INIT") == "1":
|
||||||
logger.info("'TUNE_DISABLE_AUTO_INIT=1' detected.")
|
logger.info("'TUNE_DISABLE_AUTO_INIT=1' detected.")
|
||||||
@@ -168,6 +180,9 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
|
|
||||||
self._avail_resources = Resources(cpu=0, gpu=0)
|
self._avail_resources = Resources(cpu=0, gpu=0)
|
||||||
self._committed_resources = Resources(cpu=0, gpu=0)
|
self._committed_resources = Resources(cpu=0, gpu=0)
|
||||||
|
self._pg_manager = PlacementGroupManager()
|
||||||
|
self._staged_trials = set()
|
||||||
|
|
||||||
self._resources_initialized = False
|
self._resources_initialized = False
|
||||||
|
|
||||||
if refresh_period is None:
|
if refresh_period is None:
|
||||||
@@ -188,6 +203,49 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
if ray.is_initialized():
|
if ray.is_initialized():
|
||||||
self._update_avail_resources()
|
self._update_avail_resources()
|
||||||
|
|
||||||
|
def in_staging_grace_period(self) -> bool:
|
||||||
|
"""Returns True if trials have recently been staged."""
|
||||||
|
return self._pg_manager.in_staging_grace_period()
|
||||||
|
|
||||||
|
def stage_and_update_status(self, trials: List[Trial]):
|
||||||
|
"""Check and update statuses of scheduled placement groups.
|
||||||
|
|
||||||
|
Stages placement groups of all trials.
|
||||||
|
"""
|
||||||
|
for trial in trials:
|
||||||
|
if trial.status != Trial.PENDING:
|
||||||
|
continue
|
||||||
|
if not trial.uses_placement_groups:
|
||||||
|
continue
|
||||||
|
if trial in self._staged_trials:
|
||||||
|
continue
|
||||||
|
if self._pg_manager.trial_in_use(trial):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self._pg_manager.stage_trial_pg(
|
||||||
|
trial.placement_group_factory):
|
||||||
|
# Break if we reached the limit of pending placement groups.
|
||||||
|
break
|
||||||
|
|
||||||
|
self._staged_trials.add(trial)
|
||||||
|
|
||||||
|
self._pg_manager.update_status()
|
||||||
|
|
||||||
|
def get_staged_trial(self):
|
||||||
|
"""Get a trial whose placement group was successfully staged.
|
||||||
|
|
||||||
|
Can also return None if no trial is available.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Trial object or None.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for trial in self._staged_trials:
|
||||||
|
if self._pg_manager.has_ready(trial.placement_group_factory):
|
||||||
|
return trial
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def _setup_remote_runner(self, trial, reuse_allowed):
|
def _setup_remote_runner(self, trial, reuse_allowed):
|
||||||
trial.init_logdir()
|
trial.init_logdir()
|
||||||
# We checkpoint metadata here to try mitigating logdir duplication
|
# We checkpoint metadata here to try mitigating logdir duplication
|
||||||
@@ -212,16 +270,31 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
logger.debug("Cannot reuse cached runner {} for new trial".format(
|
logger.debug("Cannot reuse cached runner {} for new trial".format(
|
||||||
self._cached_actor))
|
self._cached_actor))
|
||||||
with self._change_working_directory(trial):
|
with self._change_working_directory(trial):
|
||||||
self._trial_cleanup.add(trial, actor=self._cached_actor)
|
pg = self._pg_manager.clean_trial_placement_group(trial)
|
||||||
|
|
||||||
|
self._trial_cleanup.add(
|
||||||
|
trial, actor=self._cached_actor, placement_group=pg)
|
||||||
self._cached_actor = None
|
self._cached_actor = None
|
||||||
|
|
||||||
_actor_cls = _class_cache.get(trial.get_trainable_cls())
|
_actor_cls = _class_cache.get(trial.get_trainable_cls())
|
||||||
full_actor_class = _actor_cls.options(
|
if trial.uses_placement_groups:
|
||||||
num_cpus=trial.resources.cpu,
|
if not self._pg_manager.has_ready(trial.placement_group_factory):
|
||||||
num_gpus=trial.resources.gpu,
|
if trial not in self._staged_trials:
|
||||||
memory=trial.resources.memory or None,
|
if self._pg_manager.stage_trial_pg(
|
||||||
object_store_memory=trial.resources.object_store_memory or None,
|
trial.placement_group_factory):
|
||||||
resources=trial.resources.custom_resources)
|
self._staged_trials.add(trial)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
full_actor_class = self._pg_manager.get_full_actor_cls(
|
||||||
|
trial, _actor_cls)
|
||||||
|
else:
|
||||||
|
full_actor_class = _actor_cls.options(
|
||||||
|
num_cpus=trial.resources.cpu,
|
||||||
|
num_gpus=trial.resources.gpu,
|
||||||
|
memory=trial.resources.memory or None,
|
||||||
|
object_store_memory=trial.resources.object_store_memory
|
||||||
|
or None,
|
||||||
|
resources=trial.resources.custom_resources)
|
||||||
# Clear the Trial's location (to be updated later on result)
|
# Clear the Trial's location (to be updated later on result)
|
||||||
# since we don't know where the remote runner is placed.
|
# since we don't know where the remote runner is placed.
|
||||||
trial.set_location(Location())
|
trial.set_location(Location())
|
||||||
@@ -285,7 +358,8 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
trial_item = self._find_item(self._running, trial)
|
trial_item = self._find_item(self._running, trial)
|
||||||
assert len(trial_item) < 2, trial_item
|
assert len(trial_item) < 2, trial_item
|
||||||
|
|
||||||
def _start_trial(self, trial, checkpoint=None, runner=None, train=True):
|
def _start_trial(self, trial, checkpoint=None, runner=None,
|
||||||
|
train=True) -> bool:
|
||||||
"""Starts trial and restores last result if trial was paused.
|
"""Starts trial and restores last result if trial was paused.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -297,6 +371,9 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
cached actor. If None, a new runner is created.
|
cached actor. If None, a new runner is created.
|
||||||
train (bool): Whether or not to start training.
|
train (bool): Whether or not to start training.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if trial was started successfully, False otherwise.
|
||||||
|
|
||||||
See `RayTrialExecutor.restore` for possible errors raised.
|
See `RayTrialExecutor.restore` for possible errors raised.
|
||||||
"""
|
"""
|
||||||
prior_status = trial.status
|
prior_status = trial.status
|
||||||
@@ -309,6 +386,8 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
or issubclass(trial.get_trainable_cls(),
|
or issubclass(trial.get_trainable_cls(),
|
||||||
FunctionRunner)
|
FunctionRunner)
|
||||||
runner = self._setup_remote_runner(trial, reuse_allowed)
|
runner = self._setup_remote_runner(trial, reuse_allowed)
|
||||||
|
if not runner:
|
||||||
|
return False
|
||||||
trial.set_runner(runner)
|
trial.set_runner(runner)
|
||||||
self.restore(trial, checkpoint)
|
self.restore(trial, checkpoint)
|
||||||
self.set_status(trial, Trial.RUNNING)
|
self.set_status(trial, Trial.RUNNING)
|
||||||
@@ -320,6 +399,7 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
self._running[previous_run[0]] = trial
|
self._running[previous_run[0]] = trial
|
||||||
elif train and not trial.is_restoring:
|
elif train and not trial.is_restoring:
|
||||||
self._train(trial)
|
self._train(trial)
|
||||||
|
return True
|
||||||
|
|
||||||
def _stop_trial(self, trial, error=False, error_msg=None):
|
def _stop_trial(self, trial, error=False, error_msg=None):
|
||||||
"""Stops this trial.
|
"""Stops this trial.
|
||||||
@@ -344,15 +424,17 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
self._cached_actor = trial.runner
|
self._cached_actor = trial.runner
|
||||||
else:
|
else:
|
||||||
logger.debug("Trial %s: Destroying actor.", trial)
|
logger.debug("Trial %s: Destroying actor.", trial)
|
||||||
|
pg = self._pg_manager.clean_trial_placement_group(trial)
|
||||||
with self._change_working_directory(trial):
|
with self._change_working_directory(trial):
|
||||||
self._trial_cleanup.add(trial, actor=trial.runner)
|
self._trial_cleanup.add(
|
||||||
|
trial, actor=trial.runner, placement_group=pg)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Trial %s: Error stopping runner.", trial)
|
logger.exception("Trial %s: Error stopping runner.", trial)
|
||||||
self.set_status(trial, Trial.ERROR)
|
self.set_status(trial, Trial.ERROR)
|
||||||
finally:
|
finally:
|
||||||
trial.set_runner(None)
|
trial.set_runner(None)
|
||||||
|
|
||||||
def start_trial(self, trial, checkpoint=None, train=True):
|
def start_trial(self, trial, checkpoint=None, train=True) -> bool:
|
||||||
"""Starts the trial.
|
"""Starts the trial.
|
||||||
|
|
||||||
Will not return resources if trial repeatedly fails on start.
|
Will not return resources if trial repeatedly fails on start.
|
||||||
@@ -362,16 +444,21 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
checkpoint (Checkpoint): A Python object or path storing the state
|
checkpoint (Checkpoint): A Python object or path storing the state
|
||||||
of trial.
|
of trial.
|
||||||
train (bool): Whether or not to start training.
|
train (bool): Whether or not to start training.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if trial was started successfully, False otherwise.
|
||||||
"""
|
"""
|
||||||
self._commit_resources(trial.resources)
|
if not trial.uses_placement_groups:
|
||||||
|
self._commit_resources(trial.resources)
|
||||||
try:
|
try:
|
||||||
self._start_trial(trial, checkpoint, train=train)
|
return self._start_trial(trial, checkpoint, train=train)
|
||||||
except AbortTrialExecution:
|
except AbortTrialExecution:
|
||||||
logger.exception("Trial %s: Error starting runner, aborting!",
|
logger.exception("Trial %s: Error starting runner, aborting!",
|
||||||
trial)
|
trial)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
error_msg = traceback.format_exc()
|
error_msg = traceback.format_exc()
|
||||||
self._stop_trial(trial, error=True, error_msg=error_msg)
|
self._stop_trial(trial, error=True, error_msg=error_msg)
|
||||||
|
return False
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Trial %s: Unexpected error starting runner.",
|
logger.exception("Trial %s: Unexpected error starting runner.",
|
||||||
trial)
|
trial)
|
||||||
@@ -380,6 +467,7 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
self._stop_trial(trial, error=True, error_msg=error_msg)
|
self._stop_trial(trial, error=True, error_msg=error_msg)
|
||||||
# Note that we don't return the resources, since they may
|
# Note that we don't return the resources, since they may
|
||||||
# have been lost. TODO(ujvl): is this the right thing to do?
|
# have been lost. TODO(ujvl): is this the right thing to do?
|
||||||
|
return False
|
||||||
|
|
||||||
def _find_item(self, dictionary, item):
|
def _find_item(self, dictionary, item):
|
||||||
out = [rid for rid, t in dictionary.items() if t is item]
|
out = [rid for rid, t in dictionary.items() if t is item]
|
||||||
@@ -391,7 +479,8 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
self._stop_trial(trial, error=error, error_msg=error_msg)
|
self._stop_trial(trial, error=error, error_msg=error_msg)
|
||||||
if prior_status == Trial.RUNNING:
|
if prior_status == Trial.RUNNING:
|
||||||
logger.debug("Trial %s: Returning resources.", trial)
|
logger.debug("Trial %s: Returning resources.", trial)
|
||||||
self._return_resources(trial.resources)
|
if not trial.uses_placement_groups:
|
||||||
|
self._return_resources(trial.resources)
|
||||||
out = self._find_item(self._running, trial)
|
out = self._find_item(self._running, trial)
|
||||||
for result_id in out:
|
for result_id in out:
|
||||||
self._running.pop(result_id)
|
self._running.pop(result_id)
|
||||||
@@ -478,7 +567,9 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
return trial
|
return trial
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_next_available_trial(self):
|
def get_next_available_trial(self, timeout: Optional[float] = None):
|
||||||
|
if not self._running:
|
||||||
|
return None
|
||||||
shuffled_results = list(self._running.keys())
|
shuffled_results = list(self._running.keys())
|
||||||
random.shuffle(shuffled_results)
|
random.shuffle(shuffled_results)
|
||||||
# Note: We shuffle the results because `ray.wait` by default returns
|
# Note: We shuffle the results because `ray.wait` by default returns
|
||||||
@@ -486,7 +577,10 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
# trials (i.e. trials that run remotely) also get fairly reported.
|
# trials (i.e. trials that run remotely) also get fairly reported.
|
||||||
# See https://github.com/ray-project/ray/issues/4211 for details.
|
# See https://github.com/ray-project/ray/issues/4211 for details.
|
||||||
start = time.time()
|
start = time.time()
|
||||||
[result_id], _ = ray.wait(shuffled_results)
|
ready, _ = ray.wait(shuffled_results, timeout=timeout)
|
||||||
|
if not ready:
|
||||||
|
return None
|
||||||
|
result_id = ready[0]
|
||||||
wait_time = time.time() - start
|
wait_time = time.time() - start
|
||||||
if wait_time > NONTRIVIAL_WAIT_TIME_THRESHOLD_S:
|
if wait_time > NONTRIVIAL_WAIT_TIME_THRESHOLD_S:
|
||||||
self._last_nontrivial_wait = time.time()
|
self._last_nontrivial_wait = time.time()
|
||||||
@@ -540,6 +634,9 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
custom_resources=custom_resources)
|
custom_resources=custom_resources)
|
||||||
|
|
||||||
def _return_resources(self, resources):
|
def _return_resources(self, resources):
|
||||||
|
if resources.has_placement_group:
|
||||||
|
return
|
||||||
|
|
||||||
committed = self._committed_resources
|
committed = self._committed_resources
|
||||||
|
|
||||||
all_keys = set(resources.custom_resources).union(
|
all_keys = set(resources.custom_resources).union(
|
||||||
@@ -611,6 +708,9 @@ class RayTrialExecutor(TrialExecutor):
|
|||||||
has exceeded self._refresh_period. This also assumes that the
|
has exceeded self._refresh_period. This also assumes that the
|
||||||
cluster is not resizing very frequently.
|
cluster is not resizing very frequently.
|
||||||
"""
|
"""
|
||||||
|
if resources.has_placement_group:
|
||||||
|
return self._pg_manager.can_stage()
|
||||||
|
|
||||||
self._update_avail_resources()
|
self._update_avail_resources()
|
||||||
currently_available = Resources.subtract(self._avail_resources,
|
currently_available = Resources.subtract(self._avail_resources,
|
||||||
self._committed_resources)
|
self._committed_resources)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import logging
|
|||||||
import json
|
import json
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
# For compatibility under py2 to consider unicode as str
|
# For compatibility under py2 to consider unicode as str
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
||||||
import ray
|
import ray
|
||||||
@@ -15,7 +17,7 @@ class Resources(
|
|||||||
namedtuple("Resources", [
|
namedtuple("Resources", [
|
||||||
"cpu", "gpu", "memory", "object_store_memory", "extra_cpu",
|
"cpu", "gpu", "memory", "object_store_memory", "extra_cpu",
|
||||||
"extra_gpu", "extra_memory", "extra_object_store_memory",
|
"extra_gpu", "extra_memory", "extra_object_store_memory",
|
||||||
"custom_resources", "extra_custom_resources"
|
"custom_resources", "extra_custom_resources", "has_placement_group"
|
||||||
])):
|
])):
|
||||||
"""Ray resources required to schedule a trial.
|
"""Ray resources required to schedule a trial.
|
||||||
|
|
||||||
@@ -38,6 +40,8 @@ class Resources(
|
|||||||
extra_custom_resources (dict): Extra custom resources to reserve in
|
extra_custom_resources (dict): Extra custom resources to reserve in
|
||||||
case the trial needs to launch additional Ray actors that use
|
case the trial needs to launch additional Ray actors that use
|
||||||
any of these custom resources.
|
any of these custom resources.
|
||||||
|
has_placement_group (bool): Bool indicating if the trial also
|
||||||
|
has an associated placement group.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -53,7 +57,8 @@ class Resources(
|
|||||||
extra_memory=0,
|
extra_memory=0,
|
||||||
extra_object_store_memory=0,
|
extra_object_store_memory=0,
|
||||||
custom_resources=None,
|
custom_resources=None,
|
||||||
extra_custom_resources=None):
|
extra_custom_resources=None,
|
||||||
|
has_placement_group=False):
|
||||||
custom_resources = custom_resources or {}
|
custom_resources = custom_resources or {}
|
||||||
extra_custom_resources = extra_custom_resources or {}
|
extra_custom_resources = extra_custom_resources or {}
|
||||||
leftovers = set(custom_resources) ^ set(extra_custom_resources)
|
leftovers = set(custom_resources) ^ set(extra_custom_resources)
|
||||||
@@ -92,7 +97,7 @@ class Resources(
|
|||||||
return super(Resources, cls).__new__(
|
return super(Resources, cls).__new__(
|
||||||
cls, cpu, gpu, memory, object_store_memory, extra_cpu, extra_gpu,
|
cls, cpu, gpu, memory, object_store_memory, extra_cpu, extra_gpu,
|
||||||
extra_memory, extra_object_store_memory, custom_resources,
|
extra_memory, extra_object_store_memory, custom_resources,
|
||||||
extra_custom_resources)
|
extra_custom_resources, has_placement_group)
|
||||||
|
|
||||||
def summary_string(self):
|
def summary_string(self):
|
||||||
summary = "{} CPUs, {} GPUs".format(self.cpu + self.extra_cpu,
|
summary = "{} CPUs, {} GPUs".format(self.cpu + self.extra_cpu,
|
||||||
@@ -171,11 +176,22 @@ class Resources(
|
|||||||
return resources_to_json(self)
|
return resources_to_json(self)
|
||||||
|
|
||||||
|
|
||||||
def json_to_resources(data):
|
class PlacementGroupFactory:
|
||||||
|
"""Wrapper class to identify placement group factory methods."""
|
||||||
|
|
||||||
|
def __init__(self, factory):
|
||||||
|
self._factory = factory
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return self._factory(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def json_to_resources(data: Optional[str]):
|
||||||
if data is None or data == "null":
|
if data is None or data == "null":
|
||||||
return None
|
return None
|
||||||
if isinstance(data, string_types):
|
if isinstance(data, string_types):
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
|
|
||||||
for k in data:
|
for k in data:
|
||||||
if k in ["driver_cpu_limit", "driver_gpu_limit"]:
|
if k in ["driver_cpu_limit", "driver_gpu_limit"]:
|
||||||
raise TuneError(
|
raise TuneError(
|
||||||
@@ -193,7 +209,7 @@ def json_to_resources(data):
|
|||||||
data.get("extra_custom_resources"))
|
data.get("extra_custom_resources"))
|
||||||
|
|
||||||
|
|
||||||
def resources_to_json(resources):
|
def resources_to_json(resources: Optional[Resources]):
|
||||||
if resources is None:
|
if resources is None:
|
||||||
return None
|
return None
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import unittest
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import ray
|
import ray
|
||||||
|
from ray import tune
|
||||||
from ray.rllib import _register_all
|
from ray.rllib import _register_all
|
||||||
from ray.tune import Trainable
|
from ray.tune import Trainable
|
||||||
from ray.tune.ray_trial_executor import RayTrialExecutor
|
from ray.tune.ray_trial_executor import RayTrialExecutor
|
||||||
@@ -12,6 +13,7 @@ from ray.tune.suggest import BasicVariantGenerator
|
|||||||
from ray.tune.trial import Trial, Checkpoint
|
from ray.tune.trial import Trial, Checkpoint
|
||||||
from ray.tune.resources import Resources
|
from ray.tune.resources import Resources
|
||||||
from ray.cluster_utils import Cluster
|
from ray.cluster_utils import Cluster
|
||||||
|
from ray.util import placement_group
|
||||||
|
|
||||||
|
|
||||||
class RayTrialExecutorTest(unittest.TestCase):
|
class RayTrialExecutorTest(unittest.TestCase):
|
||||||
@@ -270,6 +272,87 @@ class RayExecutorQueueTest(unittest.TestCase):
|
|||||||
self.trial_executor.has_resources(cpu_only_trial3.resources))
|
self.trial_executor.has_resources(cpu_only_trial3.resources))
|
||||||
|
|
||||||
|
|
||||||
|
class RayExecutorPlacementGroupTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.head_cpus = 8
|
||||||
|
self.head_gpus = 4
|
||||||
|
self.head_custom = 16
|
||||||
|
|
||||||
|
self.cluster = Cluster(
|
||||||
|
initialize_head=True,
|
||||||
|
connect=True,
|
||||||
|
head_node_args={
|
||||||
|
"num_cpus": self.head_cpus,
|
||||||
|
"num_gpus": self.head_gpus,
|
||||||
|
"resources": {
|
||||||
|
"custom": self.head_custom
|
||||||
|
},
|
||||||
|
"_system_config": {
|
||||||
|
"num_heartbeats_timeout": 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
# Pytest doesn't play nicely with imports
|
||||||
|
_register_all()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
ray.shutdown()
|
||||||
|
self.cluster.shutdown()
|
||||||
|
_register_all() # re-register the evicted objects
|
||||||
|
|
||||||
|
def testResourcesAvailableNoPlacementGroup(self):
|
||||||
|
def train(config):
|
||||||
|
tune.report(metric=0, resources=ray.available_resources())
|
||||||
|
|
||||||
|
out = tune.run(
|
||||||
|
train,
|
||||||
|
resources_per_trial={
|
||||||
|
"cpu": 1,
|
||||||
|
"gpu": 1,
|
||||||
|
"custom_resources": {
|
||||||
|
"custom": 3
|
||||||
|
},
|
||||||
|
"extra_cpu": 3,
|
||||||
|
"extra_gpu": 1,
|
||||||
|
"extra_custom_resources": {
|
||||||
|
"custom": 4
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Only `cpu`, `gpu`, and `custom_resources` will be "really" reserved,
|
||||||
|
# the extra_* will just be internally reserved by Tune.
|
||||||
|
self.assertDictEqual({
|
||||||
|
key: val
|
||||||
|
for key, val in out.trials[0].last_result["resources"].items()
|
||||||
|
if key in ["CPU", "GPU", "custom"]
|
||||||
|
}, {
|
||||||
|
"CPU": self.head_cpus - 1.0,
|
||||||
|
"GPU": self.head_gpus - 1.0,
|
||||||
|
"custom": self.head_custom - 3.0
|
||||||
|
})
|
||||||
|
|
||||||
|
def testResourcesAvailableWithPlacementGroup(self):
|
||||||
|
def train(config):
|
||||||
|
tune.report(metric=0, resources=ray.available_resources())
|
||||||
|
|
||||||
|
def placement_group_factory():
|
||||||
|
head_bundle = {"CPU": 1, "GPU": 0, "custom": 4}
|
||||||
|
child_bundle = {"CPU": 2, "GPU": 1, "custom": 3}
|
||||||
|
|
||||||
|
return placement_group([head_bundle, child_bundle, child_bundle])
|
||||||
|
|
||||||
|
out = tune.run(train, resources_per_trial=placement_group_factory)
|
||||||
|
|
||||||
|
self.assertDictEqual({
|
||||||
|
key: val
|
||||||
|
for key, val in out.trials[0].last_result["resources"].items()
|
||||||
|
if key in ["CPU", "GPU", "custom"]
|
||||||
|
}, {
|
||||||
|
"CPU": self.head_cpus - 5.0,
|
||||||
|
"GPU": self.head_gpus - 2.0,
|
||||||
|
"custom": self.head_custom - 10.0
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class LocalModeExecutorTest(RayTrialExecutorTest):
|
class LocalModeExecutorTest(RayTrialExecutorTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ray.init(local_mode=True)
|
ray.init(local_mode=True)
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ class TrialRunnerTest(unittest.TestCase):
|
|||||||
if result["training_iteration"] == 1:
|
if result["training_iteration"] == 1:
|
||||||
executor = trial_runner.trial_executor
|
executor = trial_runner.trial_executor
|
||||||
executor.stop_trial(trial)
|
executor.stop_trial(trial)
|
||||||
trial.update_resources(2, 0)
|
trial.update_resources(dict(cpu=2, gpu=0))
|
||||||
executor.start_trial(trial)
|
executor.start_trial(trial)
|
||||||
return TrialScheduler.CONTINUE
|
return TrialScheduler.CONTINUE
|
||||||
|
|
||||||
@@ -282,7 +282,8 @@ class TrialRunnerTest(unittest.TestCase):
|
|||||||
runner.step()
|
runner.step()
|
||||||
self.assertEqual(trials[0].status, Trial.RUNNING)
|
self.assertEqual(trials[0].status, Trial.RUNNING)
|
||||||
self.assertEqual(runner.trial_executor._committed_resources.cpu, 1)
|
self.assertEqual(runner.trial_executor._committed_resources.cpu, 1)
|
||||||
self.assertRaises(ValueError, lambda: trials[0].update_resources(2, 0))
|
self.assertRaises(
|
||||||
|
ValueError, lambda: trials[0].update_resources(dict(cpu=2, gpu=0)))
|
||||||
|
|
||||||
runner.step()
|
runner.step()
|
||||||
self.assertEqual(trials[0].status, Trial.RUNNING)
|
self.assertEqual(trials[0].status, Trial.RUNNING)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import time
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
@@ -6,11 +7,15 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
import ray
|
import ray
|
||||||
|
from ray.cluster_utils import Cluster
|
||||||
from ray.rllib import _register_all
|
from ray.rllib import _register_all
|
||||||
|
|
||||||
from ray.tune import TuneError
|
from ray import tune
|
||||||
|
from ray.tune import Callback, TuneError
|
||||||
|
from ray.tune.ray_trial_executor import RayTrialExecutor
|
||||||
from ray.tune.result import TRAINING_ITERATION
|
from ray.tune.result import TRAINING_ITERATION
|
||||||
from ray.tune.schedulers import TrialScheduler, FIFOScheduler
|
from ray.tune.schedulers import TrialScheduler, FIFOScheduler
|
||||||
from ray.tune.experiment import Experiment
|
from ray.tune.experiment import Experiment
|
||||||
@@ -21,6 +26,7 @@ from ray.tune.suggest.repeater import Repeater
|
|||||||
from ray.tune.suggest._mock import _MockSuggestionAlgorithm
|
from ray.tune.suggest._mock import _MockSuggestionAlgorithm
|
||||||
from ray.tune.suggest.suggestion import Searcher, ConcurrencyLimiter
|
from ray.tune.suggest.suggestion import Searcher, ConcurrencyLimiter
|
||||||
from ray.tune.suggest.search_generator import SearchGenerator
|
from ray.tune.suggest.search_generator import SearchGenerator
|
||||||
|
from ray.util import placement_group
|
||||||
|
|
||||||
|
|
||||||
class TrialRunnerTest3(unittest.TestCase):
|
class TrialRunnerTest3(unittest.TestCase):
|
||||||
@@ -921,6 +927,165 @@ class ResourcesTest(unittest.TestCase):
|
|||||||
self.assertEqual(original, new_resource)
|
self.assertEqual(original, new_resource)
|
||||||
|
|
||||||
|
|
||||||
|
class TrialRunnerPlacementGroupTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
os.environ["TUNE_GLOBAL_CHECKPOINT_S"] = "10000"
|
||||||
|
self.head_cpus = 8
|
||||||
|
self.head_gpus = 4
|
||||||
|
self.head_custom = 16
|
||||||
|
|
||||||
|
self.cluster = Cluster(
|
||||||
|
initialize_head=True,
|
||||||
|
connect=True,
|
||||||
|
head_node_args={
|
||||||
|
"num_cpus": self.head_cpus,
|
||||||
|
"num_gpus": self.head_gpus,
|
||||||
|
"resources": {
|
||||||
|
"custom": self.head_custom
|
||||||
|
},
|
||||||
|
"_system_config": {
|
||||||
|
"num_heartbeats_timeout": 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
# Pytest doesn't play nicely with imports
|
||||||
|
_register_all()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
ray.shutdown()
|
||||||
|
self.cluster.shutdown()
|
||||||
|
_register_all() # re-register the evicted objects
|
||||||
|
|
||||||
|
def testPlacementGroupRequests(self, scheduled=10):
|
||||||
|
"""In this test we try to start 10 trials but only have resources
|
||||||
|
for 2. Placement groups should still be created and PENDING.
|
||||||
|
|
||||||
|
Eventually they should be scheduled sequentially (i.e. in pairs
|
||||||
|
of two)."""
|
||||||
|
|
||||||
|
def train(config):
|
||||||
|
time.sleep(1)
|
||||||
|
now = time.time()
|
||||||
|
tune.report(end=now - config["start_time"])
|
||||||
|
|
||||||
|
def placement_group_factory():
|
||||||
|
head_bundle = {"CPU": 4, "GPU": 0, "custom": 0}
|
||||||
|
child_bundle = {"custom": 1}
|
||||||
|
|
||||||
|
return placement_group([head_bundle, child_bundle, child_bundle])
|
||||||
|
|
||||||
|
trial_executor = RayTrialExecutor()
|
||||||
|
|
||||||
|
this = self
|
||||||
|
|
||||||
|
class _TestCallback(Callback):
|
||||||
|
def on_step_end(self, iteration, trials, **info):
|
||||||
|
if iteration == 1:
|
||||||
|
this.assertEqual(scheduled, len(trials))
|
||||||
|
this.assertEqual(
|
||||||
|
scheduled,
|
||||||
|
sum(
|
||||||
|
len(s) for s in
|
||||||
|
trial_executor._pg_manager._staging.values()) +
|
||||||
|
sum(
|
||||||
|
len(s)
|
||||||
|
for s in trial_executor._pg_manager._ready.values(
|
||||||
|
)) + len(trial_executor._pg_manager._in_use_pgs))
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
out = tune.run(
|
||||||
|
train,
|
||||||
|
config={"start_time": start},
|
||||||
|
resources_per_trial=placement_group_factory,
|
||||||
|
num_samples=10,
|
||||||
|
trial_executor=trial_executor,
|
||||||
|
callbacks=[_TestCallback()])
|
||||||
|
|
||||||
|
trial_end_times = sorted(t.last_result["end"] for t in out.trials)
|
||||||
|
print("Trial end times:", trial_end_times)
|
||||||
|
max_diff = trial_end_times[-1] - trial_end_times[0]
|
||||||
|
|
||||||
|
# Not all trials have been run in parallel
|
||||||
|
self.assertGreater(max_diff, 5)
|
||||||
|
|
||||||
|
# Some trials should have run in parallel
|
||||||
|
self.assertLess(max_diff, 10)
|
||||||
|
|
||||||
|
@patch("ray.tune.trial_runner.TUNE_MAX_PENDING_TRIALS_PG", 6)
|
||||||
|
@patch("ray.tune.utils.placement_groups.TUNE_MAX_PENDING_TRIALS_PG", 6)
|
||||||
|
def testPlacementGroupLimitedRequests(self):
|
||||||
|
"""Assert that maximum number of placement groups is enforced."""
|
||||||
|
self.testPlacementGroupRequests(scheduled=6)
|
||||||
|
|
||||||
|
def testPlacementGroupDistributedTraining(self):
|
||||||
|
"""Run distributed training using placement groups.
|
||||||
|
|
||||||
|
Each trial requests 4 CPUs and starts 4 remote training workers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def placement_group_factory():
|
||||||
|
head_bundle = {"CPU": 1, "GPU": 0, "custom": 0}
|
||||||
|
child_bundle = {"CPU": 1}
|
||||||
|
|
||||||
|
return placement_group(
|
||||||
|
[head_bundle, child_bundle, child_bundle, child_bundle])
|
||||||
|
|
||||||
|
@ray.remote
|
||||||
|
class TrainingActor:
|
||||||
|
def train(self, val):
|
||||||
|
time.sleep(1)
|
||||||
|
return val
|
||||||
|
|
||||||
|
def train(config):
|
||||||
|
base = config["base"]
|
||||||
|
actors = [TrainingActor.remote() for _ in range(4)]
|
||||||
|
futures = [
|
||||||
|
actor.train.remote(base + 2 * i)
|
||||||
|
for i, actor in enumerate(actors)
|
||||||
|
]
|
||||||
|
results = ray.get(futures)
|
||||||
|
|
||||||
|
end = time.time() - config["start_time"]
|
||||||
|
tune.report(avg=np.mean(results), end=end)
|
||||||
|
|
||||||
|
trial_executor = RayTrialExecutor()
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
out = tune.run(
|
||||||
|
train,
|
||||||
|
config={
|
||||||
|
"start_time": start,
|
||||||
|
"base": tune.grid_search(list(range(0, 100, 10)))
|
||||||
|
},
|
||||||
|
resources_per_trial=placement_group_factory,
|
||||||
|
num_samples=1,
|
||||||
|
trial_executor=trial_executor)
|
||||||
|
|
||||||
|
avgs = sorted(t.last_result["avg"] for t in out.trials)
|
||||||
|
self.assertSequenceEqual(avgs, list(range(3, 103, 10)))
|
||||||
|
|
||||||
|
trial_end_times = sorted(t.last_result["end"] for t in out.trials)
|
||||||
|
print("Trial end times:", trial_end_times)
|
||||||
|
max_diff = trial_end_times[-1] - trial_end_times[0]
|
||||||
|
|
||||||
|
# Not all trials have been run in parallel
|
||||||
|
self.assertGreater(max_diff, 5)
|
||||||
|
|
||||||
|
# Some trials should have run in parallel
|
||||||
|
# Todo: Re-enable when using buildkite
|
||||||
|
# self.assertLess(max_diff, 10)
|
||||||
|
|
||||||
|
# Assert proper cleanup
|
||||||
|
pg_manager = trial_executor._pg_manager
|
||||||
|
self.assertFalse(pg_manager._in_use_trials)
|
||||||
|
self.assertFalse(pg_manager._in_use_pgs)
|
||||||
|
self.assertFalse(pg_manager._staging_futures)
|
||||||
|
for pgf in pg_manager._staging:
|
||||||
|
self.assertFalse(pg_manager._staging[pgf])
|
||||||
|
for pgf in pg_manager._ready:
|
||||||
|
self.assertFalse(pg_manager._ready[pgf])
|
||||||
|
self.assertTrue(pg_manager._latest_staging_start_time)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import pytest
|
import pytest
|
||||||
sys.exit(pytest.main(["-v", __file__]))
|
sys.exit(pytest.main(["-v", __file__]))
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class _MockTrialExecutor(RayTrialExecutor):
|
|||||||
def fetch_result(self, trial):
|
def fetch_result(self, trial):
|
||||||
return [self.results.get(trial, {})]
|
return [self.results.get(trial, {})]
|
||||||
|
|
||||||
def get_next_available_trial(self):
|
def get_next_available_trial(self, timeout=None):
|
||||||
return self.next_trial or super().get_next_available_trial()
|
return self.next_trial or super().get_next_available_trial()
|
||||||
|
|
||||||
def get_next_failed_trial(self):
|
def get_next_failed_trial(self):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
from typing import Callable, Dict, Sequence, Union
|
||||||
import json
|
import json
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
import ray.cloudpickle as cloudpickle
|
import ray.cloudpickle as cloudpickle
|
||||||
from collections import deque
|
from collections import deque
|
||||||
@@ -18,7 +18,8 @@ from ray.tune.checkpoint_manager import Checkpoint, CheckpointManager
|
|||||||
# have been defined yet. See https://github.com/ray-project/ray/issues/1716.
|
# have been defined yet. See https://github.com/ray-project/ray/issues/1716.
|
||||||
from ray.tune.registry import get_trainable_cls, validate_trainable
|
from ray.tune.registry import get_trainable_cls, validate_trainable
|
||||||
from ray.tune.result import DEFAULT_RESULTS_DIR, DONE, TRAINING_ITERATION
|
from ray.tune.result import DEFAULT_RESULTS_DIR, DONE, TRAINING_ITERATION
|
||||||
from ray.tune.resources import Resources, json_to_resources, resources_to_json
|
from ray.tune.resources import PlacementGroupFactory, Resources, \
|
||||||
|
json_to_resources, resources_to_json
|
||||||
from ray.tune.utils.serialization import TuneFunctionEncoder
|
from ray.tune.utils.serialization import TuneFunctionEncoder
|
||||||
from ray.tune.utils.trainable import TrainableUtil
|
from ray.tune.utils.trainable import TrainableUtil
|
||||||
from ray.tune.utils import date_str, flatten_dict
|
from ray.tune.utils import date_str, flatten_dict
|
||||||
@@ -179,6 +180,7 @@ class Trial:
|
|||||||
evaluated_params=None,
|
evaluated_params=None,
|
||||||
experiment_tag="",
|
experiment_tag="",
|
||||||
resources=None,
|
resources=None,
|
||||||
|
placement_group_factory=None,
|
||||||
stopping_criterion=None,
|
stopping_criterion=None,
|
||||||
remote_checkpoint_dir=None,
|
remote_checkpoint_dir=None,
|
||||||
checkpoint_freq=0,
|
checkpoint_freq=0,
|
||||||
@@ -221,6 +223,12 @@ class Trial:
|
|||||||
resources = default_resources
|
resources = default_resources
|
||||||
self.location = Location()
|
self.location = Location()
|
||||||
self.resources = resources or Resources(cpu=1, gpu=0)
|
self.resources = resources or Resources(cpu=1, gpu=0)
|
||||||
|
self.placement_group_factory = placement_group_factory
|
||||||
|
if self.placement_group_factory:
|
||||||
|
resource_kwargs = self.resources._asdict()
|
||||||
|
resource_kwargs["has_placement_group"] = True
|
||||||
|
self.resources = Resources(**resource_kwargs)
|
||||||
|
|
||||||
self.stopping_criterion = stopping_criterion or {}
|
self.stopping_criterion = stopping_criterion or {}
|
||||||
|
|
||||||
self.log_to_file = log_to_file
|
self.log_to_file = log_to_file
|
||||||
@@ -330,6 +338,10 @@ class Trial:
|
|||||||
logdir_name = os.path.basename(self.logdir)
|
logdir_name = os.path.basename(self.logdir)
|
||||||
return os.path.join(self.remote_checkpoint_dir_prefix, logdir_name)
|
return os.path.join(self.remote_checkpoint_dir_prefix, logdir_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uses_placement_groups(self):
|
||||||
|
return bool(self.placement_group_factory)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
return Trial(
|
return Trial(
|
||||||
self.trainable_name,
|
self.trainable_name,
|
||||||
@@ -339,6 +351,7 @@ class Trial:
|
|||||||
evaluated_params=self.evaluated_params,
|
evaluated_params=self.evaluated_params,
|
||||||
experiment_tag=self.experiment_tag,
|
experiment_tag=self.experiment_tag,
|
||||||
resources=self.resources,
|
resources=self.resources,
|
||||||
|
placement_group_factory=self.placement_group_factory,
|
||||||
stopping_criterion=self.stopping_criterion,
|
stopping_criterion=self.stopping_criterion,
|
||||||
remote_checkpoint_dir=self.remote_checkpoint_dir,
|
remote_checkpoint_dir=self.remote_checkpoint_dir,
|
||||||
checkpoint_freq=self.checkpoint_freq,
|
checkpoint_freq=self.checkpoint_freq,
|
||||||
@@ -362,7 +375,8 @@ class Trial:
|
|||||||
os.makedirs(self.logdir, exist_ok=True)
|
os.makedirs(self.logdir, exist_ok=True)
|
||||||
self.invalidate_json_state()
|
self.invalidate_json_state()
|
||||||
|
|
||||||
def update_resources(self, cpu, gpu, **kwargs):
|
def update_resources(
|
||||||
|
self, resources: Union[Dict, Callable, PlacementGroupFactory]):
|
||||||
"""EXPERIMENTAL: Updates the resource requirements.
|
"""EXPERIMENTAL: Updates the resource requirements.
|
||||||
|
|
||||||
Should only be called when the trial is not running.
|
Should only be called when the trial is not running.
|
||||||
@@ -372,7 +386,20 @@ class Trial:
|
|||||||
"""
|
"""
|
||||||
if self.status is Trial.RUNNING:
|
if self.status is Trial.RUNNING:
|
||||||
raise ValueError("Cannot update resources while Trial is running.")
|
raise ValueError("Cannot update resources while Trial is running.")
|
||||||
self.resources = Resources(cpu, gpu, **kwargs)
|
if isinstance(resources, PlacementGroupFactory):
|
||||||
|
self.placement_group_factory = resources
|
||||||
|
elif callable(resources):
|
||||||
|
self.placement_group_factory = PlacementGroupFactory(resources)
|
||||||
|
else:
|
||||||
|
self.resources = Resources(**resources)
|
||||||
|
self.placement_group_factory = None
|
||||||
|
|
||||||
|
if self.placement_group_factory and \
|
||||||
|
not self.resources.has_placement_group:
|
||||||
|
resource_kwargs = self.resources._asdict()
|
||||||
|
resource_kwargs["has_placement_group"] = True
|
||||||
|
self.resources = Resources(**resource_kwargs)
|
||||||
|
|
||||||
self.invalidate_json_state()
|
self.invalidate_json_state()
|
||||||
|
|
||||||
def set_runner(self, runner):
|
def set_runner(self, runner):
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class TrialExecutor:
|
|||||||
and starting/stopping trials.
|
and starting/stopping trials.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, queue_trials=False):
|
def __init__(self, queue_trials: bool = False):
|
||||||
"""Initializes a new TrialExecutor.
|
"""Initializes a new TrialExecutor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -78,7 +78,7 @@ class TrialExecutor:
|
|||||||
raise NotImplementedError("Subclasses of TrialExecutor must provide "
|
raise NotImplementedError("Subclasses of TrialExecutor must provide "
|
||||||
"has_resources() method")
|
"has_resources() method")
|
||||||
|
|
||||||
def start_trial(self, trial, checkpoint=None, train=True):
|
def start_trial(self, trial, checkpoint=None, train=True) -> bool:
|
||||||
"""Starts the trial restoring from checkpoint if checkpoint is provided.
|
"""Starts the trial restoring from checkpoint if checkpoint is provided.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -86,6 +86,9 @@ class TrialExecutor:
|
|||||||
checkpoint (Checkpoint): A Python object or path storing the state
|
checkpoint (Checkpoint): A Python object or path storing the state
|
||||||
of trial.
|
of trial.
|
||||||
train (bool): Whether or not to start training.
|
train (bool): Whether or not to start training.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if trial started successfully, False otherwise.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Subclasses of TrialExecutor must provide "
|
raise NotImplementedError("Subclasses of TrialExecutor must provide "
|
||||||
"start_trial() method")
|
"start_trial() method")
|
||||||
@@ -165,6 +168,8 @@ class TrialExecutor:
|
|||||||
if self._queue_trials:
|
if self._queue_trials:
|
||||||
return
|
return
|
||||||
for trial in trial_runner.get_trials():
|
for trial in trial_runner.get_trials():
|
||||||
|
if trial.uses_placement_groups:
|
||||||
|
return
|
||||||
if trial.status == Trial.PENDING:
|
if trial.status == Trial.PENDING:
|
||||||
if not self.has_resources(trial.resources):
|
if not self.has_resources(trial.resources):
|
||||||
resource_string = trial.resources.summary_string()
|
resource_string = trial.resources.summary_string()
|
||||||
@@ -275,3 +280,7 @@ class TrialExecutor:
|
|||||||
def cleanup(self, trial):
|
def cleanup(self, trial):
|
||||||
"""Ensures that trials are cleaned up after stopping."""
|
"""Ensures that trials are cleaned up after stopping."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def in_staging_grace_period(self) -> bool:
|
||||||
|
"""Returns True if trials have recently been staged."""
|
||||||
|
return False
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
@@ -20,6 +22,7 @@ from ray.tune.schedulers import FIFOScheduler, TrialScheduler
|
|||||||
from ray.tune.suggest import BasicVariantGenerator
|
from ray.tune.suggest import BasicVariantGenerator
|
||||||
from ray.tune.utils import warn_if_slow, flatten_dict, env_integer
|
from ray.tune.utils import warn_if_slow, flatten_dict, env_integer
|
||||||
from ray.tune.utils.log import Verbosity, has_verbosity
|
from ray.tune.utils.log import Verbosity, has_verbosity
|
||||||
|
from ray.tune.utils.placement_groups import TUNE_MAX_PENDING_TRIALS_PG
|
||||||
from ray.tune.utils.serialization import TuneFunctionDecoder, \
|
from ray.tune.utils.serialization import TuneFunctionDecoder, \
|
||||||
TuneFunctionEncoder
|
TuneFunctionEncoder
|
||||||
from ray.tune.web_server import TuneServer
|
from ray.tune.web_server import TuneServer
|
||||||
@@ -108,6 +111,11 @@ class TrialRunner:
|
|||||||
self._search_alg = search_alg or BasicVariantGenerator()
|
self._search_alg = search_alg or BasicVariantGenerator()
|
||||||
self._scheduler_alg = scheduler or FIFOScheduler()
|
self._scheduler_alg = scheduler or FIFOScheduler()
|
||||||
self.trial_executor = trial_executor or RayTrialExecutor()
|
self.trial_executor = trial_executor or RayTrialExecutor()
|
||||||
|
self._pending_trial_queue_times = {}
|
||||||
|
|
||||||
|
# Setting this to 0 still allows adding one new (pending) trial,
|
||||||
|
# but it will prevent us from trying to fill the trial list
|
||||||
|
self._max_pending_trials = 0 # Can be updated in `self.add_trial()`
|
||||||
|
|
||||||
self._metric = metric
|
self._metric = metric
|
||||||
|
|
||||||
@@ -142,6 +150,7 @@ class TrialRunner:
|
|||||||
self._trials = []
|
self._trials = []
|
||||||
self._cached_trial_decisions = {}
|
self._cached_trial_decisions = {}
|
||||||
self._queued_trial_decisions = {}
|
self._queued_trial_decisions = {}
|
||||||
|
|
||||||
self._stop_queue = []
|
self._stop_queue = []
|
||||||
self._should_stop_experiment = False # used by TuneServer
|
self._should_stop_experiment = False # used by TuneServer
|
||||||
self._local_checkpoint_dir = local_checkpoint_dir
|
self._local_checkpoint_dir = local_checkpoint_dir
|
||||||
@@ -349,18 +358,50 @@ class TrialRunner:
|
|||||||
with warn_if_slow("callbacks.on_step_begin"):
|
with warn_if_slow("callbacks.on_step_begin"):
|
||||||
self._callbacks.on_step_begin(
|
self._callbacks.on_step_begin(
|
||||||
iteration=self._iteration, trials=self._trials)
|
iteration=self._iteration, trials=self._trials)
|
||||||
|
|
||||||
|
# This will contain the next trial to start
|
||||||
next_trial = self._get_next_trial() # blocking
|
next_trial = self._get_next_trial() # blocking
|
||||||
if next_trial is not None:
|
|
||||||
|
# Create pending trials
|
||||||
|
num_pending_trials = len(
|
||||||
|
[t for t in self._trials if t.status == Trial.PENDING])
|
||||||
|
while num_pending_trials < self._max_pending_trials:
|
||||||
|
if not self._update_trial_queue(blocking=False):
|
||||||
|
break
|
||||||
|
num_pending_trials += 1
|
||||||
|
|
||||||
|
# Update status of staged placement groups
|
||||||
|
self.trial_executor.stage_and_update_status(self._trials)
|
||||||
|
|
||||||
|
def _start_trial(trial: Trial) -> bool:
|
||||||
|
"""Helper function to start trial and call callbacks"""
|
||||||
with warn_if_slow("start_trial"):
|
with warn_if_slow("start_trial"):
|
||||||
self.trial_executor.start_trial(next_trial)
|
if self.trial_executor.start_trial(trial):
|
||||||
self._callbacks.on_trial_start(
|
self._callbacks.on_trial_start(
|
||||||
iteration=self._iteration,
|
iteration=self._iteration,
|
||||||
trials=self._trials,
|
trials=self._trials,
|
||||||
trial=next_trial)
|
trial=trial)
|
||||||
elif self.trial_executor.get_running_trials():
|
return True
|
||||||
self._process_events() # blocking
|
return False
|
||||||
else:
|
|
||||||
self.trial_executor.on_no_available_trials(self)
|
may_handle_events = True
|
||||||
|
if next_trial is not None:
|
||||||
|
if _start_trial(next_trial):
|
||||||
|
may_handle_events = False
|
||||||
|
else:
|
||||||
|
next_trial = self.trial_executor.get_staged_trial()
|
||||||
|
if next_trial is not None:
|
||||||
|
if _start_trial(next_trial):
|
||||||
|
may_handle_events = False
|
||||||
|
|
||||||
|
if may_handle_events:
|
||||||
|
if self.trial_executor.get_running_trials():
|
||||||
|
timeout = None
|
||||||
|
if self.trial_executor.in_staging_grace_period():
|
||||||
|
timeout = 0.1
|
||||||
|
self._process_events(timeout=timeout) # blocking
|
||||||
|
else:
|
||||||
|
self.trial_executor.on_no_available_trials(self)
|
||||||
|
|
||||||
self._stop_experiment_if_needed()
|
self._stop_experiment_if_needed()
|
||||||
|
|
||||||
@@ -410,6 +451,9 @@ class TrialRunner:
|
|||||||
Args:
|
Args:
|
||||||
trial (Trial): Trial to queue.
|
trial (Trial): Trial to queue.
|
||||||
"""
|
"""
|
||||||
|
if trial.uses_placement_groups:
|
||||||
|
self._max_pending_trials = TUNE_MAX_PENDING_TRIALS_PG
|
||||||
|
|
||||||
self._trials.append(trial)
|
self._trials.append(trial)
|
||||||
with warn_if_slow("scheduler.on_trial_add"):
|
with warn_if_slow("scheduler.on_trial_add"):
|
||||||
self._scheduler_alg.on_trial_add(self, trial)
|
self._scheduler_alg.on_trial_add(self, trial)
|
||||||
@@ -462,7 +506,7 @@ class TrialRunner:
|
|||||||
logger.debug("Running trial {}".format(trial))
|
logger.debug("Running trial {}".format(trial))
|
||||||
return trial
|
return trial
|
||||||
|
|
||||||
def _process_events(self):
|
def _process_events(self, timeout: Optional[float] = None):
|
||||||
with warn_if_slow("get_next_failed_trial"):
|
with warn_if_slow("get_next_failed_trial"):
|
||||||
failed_trial = self.trial_executor.get_next_failed_trial()
|
failed_trial = self.trial_executor.get_next_failed_trial()
|
||||||
if failed_trial:
|
if failed_trial:
|
||||||
@@ -475,8 +519,10 @@ class TrialRunner:
|
|||||||
else:
|
else:
|
||||||
# TODO(ujvl): Consider combining get_next_available_trial and
|
# TODO(ujvl): Consider combining get_next_available_trial and
|
||||||
# fetch_result functionality so that we don't timeout on fetch.
|
# fetch_result functionality so that we don't timeout on fetch.
|
||||||
trial = self.trial_executor.get_next_available_trial() # blocking
|
trial = self.trial_executor.get_next_available_trial(
|
||||||
|
timeout=timeout) # blocking
|
||||||
|
if not trial:
|
||||||
|
return
|
||||||
if trial.is_restoring:
|
if trial.is_restoring:
|
||||||
with warn_if_slow("process_trial_restore"):
|
with warn_if_slow("process_trial_restore"):
|
||||||
self._process_trial_restore(trial)
|
self._process_trial_restore(trial)
|
||||||
@@ -882,7 +928,8 @@ class TrialRunner:
|
|||||||
with warn_if_slow("scheduler.on_trial_add"):
|
with warn_if_slow("scheduler.on_trial_add"):
|
||||||
self._scheduler_alg.on_trial_add(self, trial)
|
self._scheduler_alg.on_trial_add(self, trial)
|
||||||
|
|
||||||
def _update_trial_queue(self, blocking=False, timeout=600):
|
def _update_trial_queue(self, blocking: bool = False,
|
||||||
|
timeout: int = 600) -> bool:
|
||||||
"""Adds next trials to queue if possible.
|
"""Adds next trials to queue if possible.
|
||||||
|
|
||||||
Note that the timeout is currently unexposed to the user.
|
Note that the timeout is currently unexposed to the user.
|
||||||
@@ -891,6 +938,9 @@ class TrialRunner:
|
|||||||
blocking (bool): Blocks until either a trial is available
|
blocking (bool): Blocks until either a trial is available
|
||||||
or is_finished (timeout or search algorithm finishes).
|
or is_finished (timeout or search algorithm finishes).
|
||||||
timeout (int): Seconds before blocking times out.
|
timeout (int): Seconds before blocking times out.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean indicating if a new trial was created or not.
|
||||||
"""
|
"""
|
||||||
trial = self._search_alg.next_trial()
|
trial = self._search_alg.next_trial()
|
||||||
if blocking and not trial:
|
if blocking and not trial:
|
||||||
@@ -906,6 +956,9 @@ class TrialRunner:
|
|||||||
|
|
||||||
if trial:
|
if trial:
|
||||||
self.add_trial(trial)
|
self.add_trial(trial)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def request_stop_trial(self, trial):
|
def request_stop_trial(self, trial):
|
||||||
self._stop_queue.append(trial)
|
self._stop_queue.append(trial)
|
||||||
@@ -974,7 +1027,8 @@ class TrialRunner:
|
|||||||
state = self.__dict__.copy()
|
state = self.__dict__.copy()
|
||||||
for k in [
|
for k in [
|
||||||
"_trials", "_stop_queue", "_server", "_search_alg",
|
"_trials", "_stop_queue", "_server", "_search_alg",
|
||||||
"_scheduler_alg", "trial_executor", "_syncer", "_callbacks"
|
"_scheduler_alg", "_pending_trial_queue_times",
|
||||||
|
"trial_executor", "_syncer", "_callbacks"
|
||||||
]:
|
]:
|
||||||
del state[k]
|
del state[k]
|
||||||
state["launch_web_server"] = bool(self._server)
|
state["launch_web_server"] = bool(self._server)
|
||||||
|
|||||||
@@ -160,10 +160,11 @@ def run(
|
|||||||
config (dict): Algorithm-specific configuration for Tune variant
|
config (dict): Algorithm-specific configuration for Tune variant
|
||||||
generation (e.g. env, hyperparams). Defaults to empty dict.
|
generation (e.g. env, hyperparams). Defaults to empty dict.
|
||||||
Custom search algorithms may ignore this.
|
Custom search algorithms may ignore this.
|
||||||
resources_per_trial (dict): Machine resources to allocate per trial,
|
resources_per_trial (dict|Callable): Machine resources to allocate per
|
||||||
e.g. ``{"cpu": 64, "gpu": 8}``. Note that GPUs will not be
|
trial, e.g. ``{"cpu": 64, "gpu": 8}``. Note that GPUs will not be
|
||||||
assigned unless you specify them here. Defaults to 1 CPU and 0
|
assigned unless you specify them here. Defaults to 1 CPU and 0
|
||||||
GPUs in ``Trainable.default_resource_request()``.
|
GPUs in ``Trainable.default_resource_request()``. This can also
|
||||||
|
be a function returning a placement group.
|
||||||
num_samples (int): Number of times to sample from the
|
num_samples (int): Number of times to sample from the
|
||||||
hyperparameter space. Defaults to 1. If `grid_search` is
|
hyperparameter space. Defaults to 1. If `grid_search` is
|
||||||
provided as an argument, the grid will be repeated
|
provided as an argument, the grid will be repeated
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from typing import Dict, Optional, Set, Tuple
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import ray
|
||||||
|
from ray import ObjectRef
|
||||||
|
from ray.actor import ActorClass
|
||||||
|
from ray.tune.resources import PlacementGroupFactory
|
||||||
|
from ray.tune.trial import Trial
|
||||||
|
from ray.util.placement_group import PlacementGroup
|
||||||
|
|
||||||
|
TUNE_MAX_PENDING_TRIALS_PG = int(os.getenv("TUNE_MAX_PENDING_TRIALS_PG", 1000))
|
||||||
|
# Seconds we wait for a trial to come up before we make blocking calls
|
||||||
|
# to process events
|
||||||
|
TUNE_TRIAL_STARTUP_GRACE_PERIOD = float(
|
||||||
|
os.getenv("TUNE_TRIAL_STARTUP_GRACE_PERIOD", 10.))
|
||||||
|
|
||||||
|
|
||||||
|
class PlacementGroupManager:
|
||||||
|
"""PlacementGroupManager to stage and manage placement groups.
|
||||||
|
|
||||||
|
This class schedules placement groups for trials, keeps track of
|
||||||
|
their state, and can return a fully configured actor class using
|
||||||
|
this placement group.
|
||||||
|
|
||||||
|
If two trials share the same placement group factory, both could use
|
||||||
|
resulting placement groups from it. Thus this manager associates
|
||||||
|
placement groups with their factory methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Sets of staged placement groups by factory
|
||||||
|
self._staging: Dict[PlacementGroupFactory, Set[
|
||||||
|
PlacementGroup]] = defaultdict(set)
|
||||||
|
# Sets of ready and unused placement groups by factory
|
||||||
|
self._ready: Dict[PlacementGroupFactory, Set[
|
||||||
|
PlacementGroup]] = defaultdict(set)
|
||||||
|
# Ray futures to check if a placement group is ready
|
||||||
|
self._staging_futures: Dict[ObjectRef, Tuple[PlacementGroupFactory,
|
||||||
|
PlacementGroup]] = {}
|
||||||
|
|
||||||
|
# Placement groups used by trials
|
||||||
|
self._in_use_pgs: Dict[PlacementGroup, Trial] = {}
|
||||||
|
self._in_use_trials: Dict[Trial, PlacementGroup] = {}
|
||||||
|
|
||||||
|
# Latest PG staging time to check if still in grace period.
|
||||||
|
self._latest_staging_start_time = time.time()
|
||||||
|
|
||||||
|
def stage_trial_pg(self, pgf: PlacementGroupFactory):
|
||||||
|
"""Stage a trial placement group.
|
||||||
|
|
||||||
|
Create the trial placement group if maximum number of pending
|
||||||
|
placement groups is not exhausted.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pgf (PlacementGroupFactory): Placement group factory to stage.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
False if placement group has not been staged, True otherwise.
|
||||||
|
|
||||||
|
Creates placement group and moves it to `self._staging`.
|
||||||
|
"""
|
||||||
|
if not self.can_stage():
|
||||||
|
return False
|
||||||
|
|
||||||
|
pg = pgf() # This creates the placement group
|
||||||
|
|
||||||
|
self._staging[pgf].add(pg)
|
||||||
|
self._staging_futures[pg.ready()] = (pgf, pg)
|
||||||
|
|
||||||
|
self._latest_staging_start_time = time.time()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_stage(self):
|
||||||
|
"""Return True if we can stage another placement group."""
|
||||||
|
return len(self._staging) < TUNE_MAX_PENDING_TRIALS_PG
|
||||||
|
|
||||||
|
def update_status(self):
|
||||||
|
"""Update placement group status.
|
||||||
|
|
||||||
|
Moves ready placement groups from `self._staging` to
|
||||||
|
`self._ready`.
|
||||||
|
"""
|
||||||
|
ready = True
|
||||||
|
while ready:
|
||||||
|
# Use a loop as `ready` might return futures one by one
|
||||||
|
ready, _ = ray.wait(list(self._staging_futures.keys()), timeout=0)
|
||||||
|
|
||||||
|
for ready_fut in ready:
|
||||||
|
ready_pgf, ready_pg = self._staging_futures.pop(ready_fut)
|
||||||
|
|
||||||
|
self._staging[ready_pgf].remove(ready_pg)
|
||||||
|
self._ready[ready_pgf].add(ready_pg)
|
||||||
|
|
||||||
|
def get_full_actor_cls(self, trial: Trial,
|
||||||
|
actor_cls: ActorClass) -> Optional[ActorClass]:
|
||||||
|
"""Get a fully configured actor class.
|
||||||
|
|
||||||
|
Returns the actor handle if the placement group is ready. In this case,
|
||||||
|
the placement group is moved to `self._in_use_pgs` and removed from
|
||||||
|
`self._ready`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trial (Trial): Trial object to start
|
||||||
|
actor_cls: Ray actor class.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured ActorClass or None
|
||||||
|
|
||||||
|
"""
|
||||||
|
pgf = trial.placement_group_factory
|
||||||
|
|
||||||
|
if not self._ready[pgf]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
pg = self._ready[pgf].pop()
|
||||||
|
self._in_use_pgs[pg] = trial
|
||||||
|
self._in_use_trials[trial] = pg
|
||||||
|
|
||||||
|
# We still have to pass resource specs
|
||||||
|
# Pass the full resource specs of the first bundle per default
|
||||||
|
first_bundle = pg.bundle_specs[0].copy()
|
||||||
|
num_cpus = first_bundle.pop("CPU", None)
|
||||||
|
num_gpus = first_bundle.get("GPU", None)
|
||||||
|
|
||||||
|
# Only custom resources remain in `first_bundle`
|
||||||
|
resources = first_bundle or None
|
||||||
|
|
||||||
|
return actor_cls.options(
|
||||||
|
placement_group=pg,
|
||||||
|
placement_group_bundle_index=0,
|
||||||
|
num_cpus=num_cpus,
|
||||||
|
num_gpus=num_gpus,
|
||||||
|
resources=resources)
|
||||||
|
|
||||||
|
def has_ready(self, pgf: PlacementGroupFactory) -> bool:
|
||||||
|
"""Return True if placement group is ready.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pgf (PlacementGroupFactory): PlacementGroupFactory object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return bool(self._ready[pgf])
|
||||||
|
|
||||||
|
def trial_in_use(self, trial: Trial):
|
||||||
|
return trial in self._in_use_trials
|
||||||
|
|
||||||
|
def clean_trial_placement_group(self,
|
||||||
|
trial: Trial) -> Optional[PlacementGroup]:
|
||||||
|
"""Remove reference to placement groups associated with a trial.
|
||||||
|
|
||||||
|
Returns an associated placement group. If the trial was scheduled, this
|
||||||
|
is the placement group it was scheduled on. If the trial was not
|
||||||
|
scheduled, it will first try to return a staging placement group. If
|
||||||
|
there is no staging placement group, it will return a ready placement
|
||||||
|
group that is not yet being used by another trial.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trial (Trial): Trial object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PlacementGroup or None.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pgf = trial.placement_group_factory
|
||||||
|
|
||||||
|
trial_pg = None
|
||||||
|
|
||||||
|
if trial in self._in_use_trials:
|
||||||
|
# Trial was in use. Just return its placement group.
|
||||||
|
trial_pg = self._in_use_trials.pop(trial)
|
||||||
|
self._in_use_pgs.pop(trial_pg)
|
||||||
|
else:
|
||||||
|
# Trial was not in use. If there are pending placement groups
|
||||||
|
# in staging, pop a random one.
|
||||||
|
if self._staging[pgf]:
|
||||||
|
trial_pg = self._staging[pgf].pop()
|
||||||
|
|
||||||
|
# For staging placement groups, we will also need to
|
||||||
|
# remove the future.
|
||||||
|
trial_future = None
|
||||||
|
for future, (pgf, pg) in self._staging_futures.items():
|
||||||
|
if pg == trial_pg:
|
||||||
|
trial_future = future
|
||||||
|
break
|
||||||
|
del self._staging_futures[trial_future]
|
||||||
|
|
||||||
|
elif self._ready[pgf]:
|
||||||
|
# Otherwise, return an unused ready placement group.
|
||||||
|
trial_pg = self._ready[pgf].pop()
|
||||||
|
|
||||||
|
return trial_pg
|
||||||
|
|
||||||
|
def in_staging_grace_period(self):
|
||||||
|
return self._staging_futures and time.time(
|
||||||
|
) <= self._latest_staging_start_time + TUNE_TRIAL_STARTUP_GRACE_PERIOD
|
||||||
@@ -563,7 +563,8 @@ def create_logdir(dirname: str, local_dir: str):
|
|||||||
dirname (str): Dirname to create in `local_dir`
|
dirname (str): Dirname to create in `local_dir`
|
||||||
local_dir (str): Root directory for the log dir
|
local_dir (str): Root directory for the log dir
|
||||||
|
|
||||||
Returns: full path to the newly created logdir.
|
Returns:
|
||||||
|
full path to the newly created logdir.
|
||||||
"""
|
"""
|
||||||
local_dir = os.path.expanduser(local_dir)
|
local_dir = os.path.expanduser(local_dir)
|
||||||
logdir = os.path.join(local_dir, dirname)
|
logdir = os.path.join(local_dir, dirname)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from typing import (List, Dict, Optional)
|
from typing import (List, Dict, Optional, Union)
|
||||||
|
|
||||||
import ray
|
import ray
|
||||||
from ray._raylet import PlacementGroupID, ObjectRef
|
from ray._raylet import PlacementGroupID, ObjectRef
|
||||||
@@ -83,10 +83,10 @@ class PlacementGroup:
|
|||||||
placement_group_bundle_index=bundle_index,
|
placement_group_bundle_index=bundle_index,
|
||||||
resources=resources).remote(self)
|
resources=resources).remote(self)
|
||||||
|
|
||||||
def wait(self, timeout_seconds: int) -> bool:
|
def wait(self, timeout_seconds: Union[float, int]) -> bool:
|
||||||
"""Wait for the placement group to be ready within the specified time.
|
"""Wait for the placement group to be ready within the specified time.
|
||||||
Args:
|
Args:
|
||||||
timeout_seconds(str): Timeout in seconds.
|
timeout_seconds(float|int): Timeout in seconds.
|
||||||
Return:
|
Return:
|
||||||
True if the placement group is created. False otherwise.
|
True if the placement group is created. False otherwise.
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user