mirror of
https://github.com/wassname/ray.git
synced 2026-08-02 13:01:01 +08:00
[tune] Fault tolerance improvements (#5877)
* Precede ray.get with ray.wait. * Trigger checkpoint deletes locally in Trainable * Clean-up code. * Minor changes. * Track best checkpoint so far again * Pulled checkpoint GC out of Trainable. * Added comments, error logging. * Immediate pull after checkpoint taken; rsync source delete on pull * Minor doc fixes * Fix checkpoint manager bug * Fix bugs, tests, formatting * Fix bugs, feature flag for force sync. * Fix test. * Fix minor bugs: clear proc and less verbose sync_on_checkpoint warnings. * Fix bug: update IP of last_result. * Fixed message. * Added a lot of logging. * Changes to ray trial executor. * More bug fixes (logging after failure), better logging. * Fix richards bug and logging * Add comments. * try-except * Fix heapq bug. * . * Move handling of no available trials to ray_trial_executor (#1) * Fix formatting bug, lint. * Addressed Richard's comments * Revert tests. * fix rebase * Fix trial location reporting. * Fix test * Fix lint * Rebase, use ray.get w/ timeout, lint. * lint * fix rebase * Address richard's comments
This commit is contained in:
committed by
Richard Liaw
parent
66edebce3a
commit
2965dc1b72
@@ -4,18 +4,18 @@ from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import ray
|
||||
from ray.exceptions import RayTimeoutError
|
||||
from ray import ray_constants
|
||||
from ray.resource_spec import ResourceSpec
|
||||
from ray.tune.error import AbortTrialExecution
|
||||
from ray.tune.logger import NoopLogger
|
||||
from ray.tune.trial import Trial, Checkpoint
|
||||
from ray.tune.trial import Trial, Checkpoint, Location
|
||||
from ray.tune.resources import Resources
|
||||
from ray.tune.trial_executor import TrialExecutor
|
||||
from ray.tune.util import warn_if_slow
|
||||
@@ -25,6 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
RESOURCE_REFRESH_PERIOD = 0.5 # Refresh resources every 500 ms
|
||||
BOTTLENECK_WARN_PERIOD_S = 60
|
||||
NONTRIVIAL_WAIT_TIME_THRESHOLD_S = 1e-3
|
||||
DEFAULT_GET_TIMEOUT = 30.0 # seconds
|
||||
|
||||
|
||||
class _LocalWrapper(object):
|
||||
@@ -37,7 +38,7 @@ class _LocalWrapper(object):
|
||||
|
||||
|
||||
class RayTrialExecutor(TrialExecutor):
|
||||
"""An implemention of TrialExecutor based on Ray."""
|
||||
"""An implementation of TrialExecutor based on Ray."""
|
||||
|
||||
def __init__(self,
|
||||
queue_trials=False,
|
||||
@@ -71,36 +72,18 @@ class RayTrialExecutor(TrialExecutor):
|
||||
if ray.is_initialized():
|
||||
self._update_avail_resources()
|
||||
|
||||
def _setup_runner(self, trial, reuse_allowed):
|
||||
def _setup_remote_runner(self, trial, reuse_allowed):
|
||||
trial.init_logger()
|
||||
# We checkpoint metadata here to try mitigating logdir duplication
|
||||
self.try_checkpoint_metadata(trial)
|
||||
remote_logdir = trial.logdir
|
||||
|
||||
if (self._reuse_actors and reuse_allowed
|
||||
and self._cached_actor is not None):
|
||||
logger.debug("Reusing cached runner {} for {}".format(
|
||||
self._cached_actor, trial.trial_id))
|
||||
existing_runner = self._cached_actor
|
||||
self._cached_actor = None
|
||||
else:
|
||||
if self._cached_actor:
|
||||
logger.debug(
|
||||
"Cannot reuse cached runner {} for new trial".format(
|
||||
self._cached_actor))
|
||||
self._cached_actor.stop.remote()
|
||||
self._cached_actor.__ray_terminate__.remote()
|
||||
self._cached_actor = None
|
||||
existing_runner = None
|
||||
cls = ray.remote(
|
||||
num_cpus=trial.resources.cpu,
|
||||
num_gpus=trial.resources.gpu,
|
||||
memory=trial.resources.memory,
|
||||
object_store_memory=trial.resources.object_store_memory,
|
||||
resources=trial.resources.custom_resources)(
|
||||
trial.get_trainable_cls())
|
||||
|
||||
trial.init_logger()
|
||||
# We checkpoint metadata here to try mitigating logdir duplication
|
||||
self.try_checkpoint_metadata(trial)
|
||||
remote_logdir = trial.logdir
|
||||
|
||||
if existing_runner:
|
||||
trial.runner = existing_runner
|
||||
if not self.reset_trial(trial, trial.config, trial.experiment_tag):
|
||||
raise AbortTrialExecution(
|
||||
@@ -108,6 +91,21 @@ class RayTrialExecutor(TrialExecutor):
|
||||
"implemented and return True.")
|
||||
return existing_runner
|
||||
|
||||
if self._cached_actor:
|
||||
logger.debug("Cannot reuse cached runner {} for new trial".format(
|
||||
self._cached_actor))
|
||||
self._cached_actor.stop.remote()
|
||||
self._cached_actor.__ray_terminate__.remote()
|
||||
self._cached_actor = None
|
||||
|
||||
cls = ray.remote(
|
||||
num_cpus=trial.resources.cpu,
|
||||
num_gpus=trial.resources.gpu,
|
||||
memory=trial.resources.memory,
|
||||
object_store_memory=trial.resources.object_store_memory,
|
||||
resources=trial.resources.custom_resources)(
|
||||
trial.get_trainable_cls())
|
||||
|
||||
def logger_creator(config):
|
||||
# Set the working dir in the remote process, for user file writes
|
||||
if not os.path.exists(remote_logdir):
|
||||
@@ -116,6 +114,10 @@ class RayTrialExecutor(TrialExecutor):
|
||||
os.chdir(remote_logdir)
|
||||
return NoopLogger(config, remote_logdir)
|
||||
|
||||
# Clear the Trial's location (to be updated later on result)
|
||||
# since we don't know where the remote runner is placed.
|
||||
trial.set_location(Location())
|
||||
logger.info("Trial %s: Setting up new remote runner.", trial)
|
||||
# Logging for trials is handled centrally by TrialRunner, so
|
||||
# configure the remote runner to use a noop-logger.
|
||||
return cls.remote(config=trial.config, logger_creator=logger_creator)
|
||||
@@ -136,22 +138,20 @@ class RayTrialExecutor(TrialExecutor):
|
||||
"""Starts trial and restores last result if trial was paused.
|
||||
|
||||
Raises:
|
||||
ValueError if restoring from checkpoint fails.
|
||||
RuntimeError if restoring from checkpoint fails.
|
||||
"""
|
||||
prior_status = trial.status
|
||||
self.set_status(trial, Trial.RUNNING)
|
||||
trial.runner = self._setup_runner(
|
||||
trial.runner = self._setup_remote_runner(
|
||||
trial,
|
||||
reuse_allowed=checkpoint is not None
|
||||
or trial._checkpoint.value is not None)
|
||||
reuse_allowed=checkpoint is not None or trial.has_checkpoint())
|
||||
if not self.restore(trial, checkpoint):
|
||||
if trial.status == Trial.ERROR:
|
||||
raise RuntimeError(
|
||||
"Restore from checkpoint failed for Trial {}.".format(
|
||||
str(trial)))
|
||||
"Trial {}: Restore from checkpoint failed.".format(trial))
|
||||
|
||||
previous_run = self._find_item(self._paused, trial)
|
||||
if (prior_status == Trial.PAUSED and previous_run):
|
||||
if prior_status == Trial.PAUSED and previous_run:
|
||||
# If Trial was in flight when paused, self._paused stores result.
|
||||
self._paused.pop(previous_run[0])
|
||||
self._running[previous_run[0]] = trial
|
||||
@@ -175,10 +175,8 @@ class RayTrialExecutor(TrialExecutor):
|
||||
if stop_logger:
|
||||
trial.close_logger()
|
||||
|
||||
if error:
|
||||
self.set_status(trial, Trial.ERROR)
|
||||
else:
|
||||
self.set_status(trial, Trial.TERMINATED)
|
||||
self.set_status(trial, Trial.ERROR if error else Trial.TERMINATED)
|
||||
trial.set_location(Location())
|
||||
|
||||
try:
|
||||
trial.write_error_log(error_msg)
|
||||
@@ -188,12 +186,11 @@ class RayTrialExecutor(TrialExecutor):
|
||||
logger.debug("Reusing actor for {}".format(trial.runner))
|
||||
self._cached_actor = trial.runner
|
||||
else:
|
||||
logger.debug(
|
||||
"Destroying actor for trial {}.".format(trial))
|
||||
logger.debug("Trial %s: Destroying actor.", trial)
|
||||
trial.runner.stop.remote()
|
||||
trial.runner.__ray_terminate__.remote()
|
||||
except Exception:
|
||||
logger.exception("Error stopping runner for Trial %s", str(trial))
|
||||
logger.exception("Trial %s: Error stopping runner.", trial)
|
||||
self.set_status(trial, Trial.ERROR)
|
||||
finally:
|
||||
trial.runner = None
|
||||
@@ -208,32 +205,35 @@ class RayTrialExecutor(TrialExecutor):
|
||||
checkpoint (Checkpoint): A Python object or path storing the state
|
||||
of trial.
|
||||
"""
|
||||
|
||||
self._commit_resources(trial.resources)
|
||||
try:
|
||||
self._start_trial(trial, checkpoint)
|
||||
except Exception as e:
|
||||
logger.exception("Error starting runner for Trial %s", str(trial))
|
||||
error_msg = traceback.format_exc()
|
||||
except AbortTrialExecution:
|
||||
logger.exception("Trial %s: Error starting runner, aborting!",
|
||||
trial)
|
||||
time.sleep(2)
|
||||
error_msg = traceback.format_exc()
|
||||
self._stop_trial(trial, error=True, error_msg=error_msg)
|
||||
return # don't retry fatal Tune errors
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Trial %s: Error starting runner. Attempting "
|
||||
"restart without checkpoint.", trial)
|
||||
time.sleep(2)
|
||||
error_msg = traceback.format_exc()
|
||||
self._stop_trial(trial, error=True, error_msg=error_msg)
|
||||
if isinstance(e, AbortTrialExecution):
|
||||
return # don't retry fatal Tune errors
|
||||
try:
|
||||
# This forces the trial to not start from checkpoint.
|
||||
trial.clear_checkpoint()
|
||||
logger.info(
|
||||
"Trying to start runner for Trial %s without checkpoint.",
|
||||
str(trial))
|
||||
self._start_trial(trial)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Error starting runner for Trial %s, aborting!",
|
||||
str(trial))
|
||||
"Trial %s: Error starting runner on second "
|
||||
"attempt, aborting!", trial)
|
||||
error_msg = traceback.format_exc()
|
||||
self._stop_trial(trial, error=True, error_msg=error_msg)
|
||||
# note that we don't return the resources, since they may
|
||||
# have been lost
|
||||
# Note that we don't return the resources, since they may
|
||||
# have been lost. TODO(ujvl): is this the right thing to do?
|
||||
|
||||
def _find_item(self, dictionary, item):
|
||||
out = [rid for rid, t in dictionary.items() if t is item]
|
||||
@@ -245,7 +245,7 @@ class RayTrialExecutor(TrialExecutor):
|
||||
self._stop_trial(
|
||||
trial, error=error, error_msg=error_msg, stop_logger=stop_logger)
|
||||
if prior_status == Trial.RUNNING:
|
||||
logger.debug("Returning resources for Trial %s.", str(trial))
|
||||
logger.debug("Trial %s: Returning resources.", trial)
|
||||
self._return_resources(trial.resources)
|
||||
out = self._find_item(self._running, trial)
|
||||
for result_id in out:
|
||||
@@ -253,7 +253,6 @@ class RayTrialExecutor(TrialExecutor):
|
||||
|
||||
def continue_training(self, trial):
|
||||
"""Continues the training of this trial."""
|
||||
|
||||
self._train(trial)
|
||||
|
||||
def pause_trial(self, trial):
|
||||
@@ -262,7 +261,6 @@ class RayTrialExecutor(TrialExecutor):
|
||||
If trial is in-flight, preserves return value in separate queue
|
||||
before pausing, which is restored when Trial is resumed.
|
||||
"""
|
||||
|
||||
trial_future = self._find_item(self._running, trial)
|
||||
if trial_future:
|
||||
self._paused[trial_future[0]] = trial
|
||||
@@ -285,7 +283,13 @@ class RayTrialExecutor(TrialExecutor):
|
||||
trial.config = new_config
|
||||
trainable = trial.runner
|
||||
with warn_if_slow("reset_config"):
|
||||
reset_val = ray.get(trainable.reset_config.remote(new_config))
|
||||
try:
|
||||
reset_val = ray.get(
|
||||
trainable.reset_config.remote(new_config),
|
||||
DEFAULT_GET_TIMEOUT)
|
||||
except RayTimeoutError:
|
||||
logger.exception("Trial %s: reset_config timed out.")
|
||||
return False
|
||||
return reset_val
|
||||
|
||||
def get_running_trials(self):
|
||||
@@ -351,7 +355,7 @@ class RayTrialExecutor(TrialExecutor):
|
||||
raise ValueError("Trial was not running.")
|
||||
self._running.pop(trial_future[0])
|
||||
with warn_if_slow("fetch_result"):
|
||||
result = ray.get(trial_future[0])
|
||||
result = ray.get(trial_future[0], DEFAULT_GET_TIMEOUT)
|
||||
|
||||
# For local mode
|
||||
if isinstance(result, _LocalWrapper):
|
||||
@@ -530,48 +534,28 @@ class RayTrialExecutor(TrialExecutor):
|
||||
|
||||
def save(self, trial, storage=Checkpoint.DISK):
|
||||
"""Saves the trial's state to a checkpoint."""
|
||||
trial._checkpoint.storage = storage
|
||||
trial._checkpoint.last_result = trial.last_result
|
||||
if storage == Checkpoint.MEMORY:
|
||||
trial._checkpoint.value = trial.runner.save_to_object.remote()
|
||||
value = trial.runner.save_to_object.remote()
|
||||
checkpoint = Checkpoint(storage, value, trial.last_result)
|
||||
else:
|
||||
# Keeps only highest performing checkpoints if enabled
|
||||
if trial.keep_checkpoints_num:
|
||||
try:
|
||||
last_attr_val = trial.last_result[
|
||||
trial.checkpoint_score_attr]
|
||||
if (trial.compare_checkpoints(last_attr_val)
|
||||
and not math.isnan(last_attr_val)):
|
||||
trial.best_checkpoint_attr_value = last_attr_val
|
||||
self._checkpoint_and_erase(trial)
|
||||
except KeyError:
|
||||
logger.warning(
|
||||
"Result dict has no key: {}. keep"
|
||||
"_checkpoints_num flag will not work".format(
|
||||
trial.checkpoint_score_attr))
|
||||
else:
|
||||
with warn_if_slow("save_to_disk"):
|
||||
trial._checkpoint.value = ray.get(
|
||||
trial.runner.save.remote())
|
||||
with warn_if_slow("save_checkpoint_to_disk"):
|
||||
value = ray.get(trial.runner.save.remote())
|
||||
checkpoint = Checkpoint(storage, value, trial.last_result)
|
||||
|
||||
return trial._checkpoint.value
|
||||
|
||||
def _checkpoint_and_erase(self, trial):
|
||||
"""Checkpoints the model and erases old checkpoints
|
||||
if needed.
|
||||
Parameters
|
||||
----------
|
||||
trial : trial to save
|
||||
"""
|
||||
|
||||
with warn_if_slow("save_to_disk"):
|
||||
trial._checkpoint.value = ray.get(trial.runner.save.remote())
|
||||
|
||||
if len(trial.history) >= trial.keep_checkpoints_num:
|
||||
ray.get(trial.runner.delete_checkpoint.remote(trial.history[-1]))
|
||||
trial.history.pop()
|
||||
|
||||
trial.history.insert(0, trial._checkpoint.value)
|
||||
with warn_if_slow("on_checkpoint", DEFAULT_GET_TIMEOUT) as profile:
|
||||
try:
|
||||
trial.on_checkpoint(checkpoint)
|
||||
except Exception:
|
||||
logger.exception("Trial %s: Error handling checkpoint %s",
|
||||
trial, checkpoint.value)
|
||||
return None
|
||||
if profile.too_slow and trial.sync_on_checkpoint:
|
||||
logger.warning(
|
||||
"Consider turning off forced head-worker trial checkpoint "
|
||||
"syncs by setting sync_on_checkpoint=False. Note that this "
|
||||
"might result in faulty trial restoration for some worker "
|
||||
"failure modes.")
|
||||
return checkpoint.value
|
||||
|
||||
def restore(self, trial, checkpoint=None):
|
||||
"""Restores training state from a given model checkpoint.
|
||||
@@ -580,11 +564,13 @@ class RayTrialExecutor(TrialExecutor):
|
||||
if restoring on a different node.
|
||||
"""
|
||||
if checkpoint is None or checkpoint.value is None:
|
||||
checkpoint = trial._checkpoint
|
||||
checkpoint = trial.checkpoint
|
||||
if checkpoint is None or checkpoint.value is None:
|
||||
return True
|
||||
if trial.runner is None:
|
||||
logger.error("Unable to restore - no runner.")
|
||||
logger.error(
|
||||
"Trial %s: Unable to restore - no runner. "
|
||||
"Setting status to ERROR.", trial)
|
||||
self.set_status(trial, Trial.ERROR)
|
||||
return False
|
||||
try:
|
||||
@@ -593,23 +579,31 @@ class RayTrialExecutor(TrialExecutor):
|
||||
assert type(value) != Checkpoint, type(value)
|
||||
trial.runner.restore_from_object.remote(value)
|
||||
else:
|
||||
# TODO: Somehow, the call to get the current IP on the
|
||||
# remote actor can be very slow - a better fix would
|
||||
# be to use an actor table to detect the IP of the Trainable
|
||||
# and rsync the files there.
|
||||
# See https://github.com/ray-project/ray/issues/5168
|
||||
logger.info("Trial %s: Attempting restoration from %s", trial,
|
||||
checkpoint.value)
|
||||
with warn_if_slow("get_current_ip"):
|
||||
worker_ip = ray.get(trial.runner.current_ip.remote())
|
||||
worker_ip = ray.get(trial.runner.current_ip.remote(),
|
||||
DEFAULT_GET_TIMEOUT)
|
||||
with warn_if_slow("sync_to_new_location"):
|
||||
trial.sync_logger_to_new_location(worker_ip)
|
||||
with warn_if_slow("restore_from_disk"):
|
||||
ray.get(trial.runner.restore.remote(value))
|
||||
trial.last_result = checkpoint.last_result
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception("Error restoring runner for Trial %s.", trial)
|
||||
ray.get(
|
||||
trial.runner.restore.remote(value),
|
||||
DEFAULT_GET_TIMEOUT)
|
||||
except RayTimeoutError:
|
||||
logger.exception(
|
||||
"Trial %s: Unable to restore - runner task timed "
|
||||
"out. Setting status to ERROR", trial)
|
||||
self.set_status(trial, Trial.ERROR)
|
||||
return False
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Trial %s: Unable to restore. Setting status to ERROR", trial)
|
||||
self.set_status(trial, Trial.ERROR)
|
||||
return False
|
||||
|
||||
trial.last_result = checkpoint.result
|
||||
return True
|
||||
|
||||
def export_trial_if_needed(self, trial):
|
||||
"""Exports model of this trial based on trial.export_formats.
|
||||
@@ -619,7 +613,8 @@ class RayTrialExecutor(TrialExecutor):
|
||||
"""
|
||||
if trial.export_formats and len(trial.export_formats) > 0:
|
||||
return ray.get(
|
||||
trial.runner.export_model.remote(trial.export_formats))
|
||||
trial.runner.export_model.remote(trial.export_formats),
|
||||
DEFAULT_GET_TIMEOUT)
|
||||
return {}
|
||||
|
||||
def has_gpus(self):
|
||||
|
||||
Reference in New Issue
Block a user