mirror of
https://github.com/wassname/ray.git
synced 2026-09-10 12:38:43 +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
+217
-147
@@ -28,26 +28,142 @@ SYNC_PERIOD = 300
|
||||
_syncers = {}
|
||||
|
||||
|
||||
def validate_sync_string(sync_string):
|
||||
if "{source}" not in sync_string:
|
||||
raise ValueError("Sync template missing '{source}'.")
|
||||
if "{target}" not in sync_string:
|
||||
raise ValueError("Sync template missing '{target}'.")
|
||||
|
||||
|
||||
def wait_for_sync():
|
||||
for syncer in _syncers.values():
|
||||
syncer.wait()
|
||||
|
||||
|
||||
class BaseSyncer(object):
|
||||
def __init__(self, local_dir, remote_dir, sync_function=None):
|
||||
class SyncClient(object):
|
||||
def sync_up(self, source, target):
|
||||
"""Sync up from source to target.
|
||||
|
||||
Args:
|
||||
source (str): Source path.
|
||||
target (str): Target path.
|
||||
|
||||
Returns:
|
||||
True if sync initiation successful, False otherwise.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def sync_down(self, source, target):
|
||||
"""Sync down from source to target.
|
||||
|
||||
Args:
|
||||
source (str): Source path.
|
||||
target (str): Target path.
|
||||
|
||||
Returns:
|
||||
True if sync initiation successful, False otherwise.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def wait(self):
|
||||
"""Wait for current sync to complete, if asynchronously started."""
|
||||
pass
|
||||
|
||||
def reset(self):
|
||||
"""Resets state."""
|
||||
pass
|
||||
|
||||
|
||||
class FunctionBasedClient(SyncClient):
|
||||
def __init__(self, sync_up_func, sync_down_func):
|
||||
self.sync_up_func = sync_up_func
|
||||
self.sync_down_func = sync_down_func
|
||||
|
||||
def sync_up(self, source, target):
|
||||
self.sync_up_func(source, target)
|
||||
return True
|
||||
|
||||
def sync_down(self, source, target):
|
||||
self.sync_down_func(source, target)
|
||||
return True
|
||||
|
||||
|
||||
class CommandBasedClient(SyncClient):
|
||||
def __init__(self, sync_up_template, sync_down_template):
|
||||
"""Syncs between two directories with the given command.
|
||||
|
||||
Arguments:
|
||||
sync_up_template (str): A runnable string template; needs to
|
||||
include replacement fields '{source}' and '{target}'.
|
||||
sync_down_template (str): A runnable string template; needs to
|
||||
include replacement fields '{source}' and '{target}'.
|
||||
"""
|
||||
if not isinstance(sync_up_template, str):
|
||||
raise ValueError("{} is not a string.".format(sync_up_template))
|
||||
if not isinstance(sync_down_template, str):
|
||||
raise ValueError("{} is not a string.".format(sync_down_template))
|
||||
self._validate_sync_string(sync_up_template)
|
||||
self._validate_sync_string(sync_down_template)
|
||||
self.sync_up_template = sync_up_template
|
||||
self.sync_down_template = sync_down_template
|
||||
self.logfile = None
|
||||
self.sync_process = None
|
||||
|
||||
def set_logdir(self, logdir):
|
||||
"""Sets the directory to log sync execution output in.
|
||||
|
||||
Args:
|
||||
logdir: Log directory.
|
||||
"""
|
||||
self.logfile = tempfile.NamedTemporaryFile(
|
||||
prefix="log_sync", dir=logdir, suffix=".log", delete=False)
|
||||
|
||||
def sync_up(self, source, target):
|
||||
return self.execute(self.sync_up_template, source, target)
|
||||
|
||||
def sync_down(self, source, target):
|
||||
return self.execute(self.sync_down_template, source, target)
|
||||
|
||||
def execute(self, sync_template, source, target):
|
||||
"""Executes sync_template on source and target."""
|
||||
if self.sync_process:
|
||||
self.sync_process.poll()
|
||||
if self.sync_process.returncode is None:
|
||||
logger.warning("Last sync is still in progress, skipping.")
|
||||
return False
|
||||
final_cmd = sync_template.format(
|
||||
source=quote(source), target=quote(target))
|
||||
logger.debug("Running sync: {}".format(final_cmd))
|
||||
self.sync_process = subprocess.Popen(
|
||||
final_cmd, shell=True, stderr=subprocess.PIPE, stdout=self.logfile)
|
||||
return True
|
||||
|
||||
def wait(self):
|
||||
if self.sync_process:
|
||||
_, error_msg = self.sync_process.communicate()
|
||||
error_msg = error_msg.decode("ascii")
|
||||
code = self.sync_process.returncode
|
||||
self.sync_process = None
|
||||
if code != 0:
|
||||
raise TuneError("Sync error ({}): {}".format(code, error_msg))
|
||||
|
||||
def reset(self):
|
||||
if self.sync_process:
|
||||
logger.warning("Sync process still running but resetting anyways.")
|
||||
self.sync_process = None
|
||||
|
||||
@staticmethod
|
||||
def _validate_sync_string(sync_string):
|
||||
if "{source}" not in sync_string:
|
||||
raise ValueError("Sync template missing '{source}'.")
|
||||
if "{target}" not in sync_string:
|
||||
raise ValueError("Sync template missing '{target}'.")
|
||||
|
||||
|
||||
NOOP = FunctionBasedClient(lambda s, t: None, lambda s, t: None)
|
||||
|
||||
|
||||
class Syncer(object):
|
||||
def __init__(self, local_dir, remote_dir, sync_client=NOOP):
|
||||
"""Syncs between two directories with the sync_function.
|
||||
|
||||
Arguments:
|
||||
local_dir (str): Directory to sync. Uniquely identifies the syncer.
|
||||
remote_dir (str): Remote directory to sync with.
|
||||
sync_function (func): Function for syncing the local_dir to
|
||||
sync_client (SyncClient): Client for syncing between local_dir and
|
||||
remote_dir. Defaults to a Noop.
|
||||
"""
|
||||
self._local_dir = (os.path.join(local_dir, "")
|
||||
@@ -55,32 +171,7 @@ class BaseSyncer(object):
|
||||
self._remote_dir = remote_dir
|
||||
self.last_sync_up_time = float("-inf")
|
||||
self.last_sync_down_time = float("-inf")
|
||||
self._sync_function = sync_function or (lambda source, target: None)
|
||||
|
||||
def sync_function(self, source, target):
|
||||
"""Executes sync between source and target.
|
||||
|
||||
Can be overwritten by subclasses for custom sync procedures.
|
||||
|
||||
Args:
|
||||
source: Path to source file(s).
|
||||
target: Path to target file(s).
|
||||
"""
|
||||
if self._sync_function:
|
||||
return self._sync_function(source, target)
|
||||
|
||||
def sync(self, source, target):
|
||||
if not (source and target):
|
||||
logger.debug(
|
||||
"Source or target is empty, skipping log sync for {}".format(
|
||||
self._local_dir))
|
||||
return
|
||||
|
||||
try:
|
||||
self.sync_function(source, target)
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception("Sync function failed.")
|
||||
self.sync_client = sync_client
|
||||
|
||||
def sync_up_if_needed(self):
|
||||
if time.time() - self.last_sync_up_time > SYNC_PERIOD:
|
||||
@@ -90,120 +181,86 @@ class BaseSyncer(object):
|
||||
if time.time() - self.last_sync_down_time > SYNC_PERIOD:
|
||||
self.sync_down()
|
||||
|
||||
def sync_down(self, *args, **kwargs):
|
||||
self.sync(self._remote_path, self._local_dir, *args, **kwargs)
|
||||
self.last_sync_down_time = time.time()
|
||||
def sync_up(self):
|
||||
"""Attempts to start the sync-up to the remote path.
|
||||
|
||||
def sync_up(self, *args, **kwargs):
|
||||
self.sync(self._local_dir, self._remote_path, *args, **kwargs)
|
||||
self.last_sync_up_time = time.time()
|
||||
Returns:
|
||||
Whether the sync (if feasible) was successfully started.
|
||||
"""
|
||||
result = False
|
||||
if self.validate_hosts(self._local_dir, self._remote_path):
|
||||
try:
|
||||
result = self.sync_client.sync_up(self._local_dir,
|
||||
self._remote_path)
|
||||
self.last_sync_up_time = time.time()
|
||||
except Exception:
|
||||
logger.exception("Sync execution failed.")
|
||||
return result
|
||||
|
||||
def sync_down(self):
|
||||
"""Attempts to start the sync-down from the remote path.
|
||||
|
||||
Returns:
|
||||
Whether the sync (if feasible) was successfully started.
|
||||
"""
|
||||
result = False
|
||||
if self.validate_hosts(self._local_dir, self._remote_path):
|
||||
try:
|
||||
result = self.sync_client.sync_down(self._remote_path,
|
||||
self._local_dir)
|
||||
self.last_sync_down_time = time.time()
|
||||
except Exception:
|
||||
logger.exception("Sync execution failed.")
|
||||
return result
|
||||
|
||||
def validate_hosts(self, source, target):
|
||||
if not (source and target):
|
||||
logger.debug("Source or target is empty, skipping log sync for "
|
||||
"{}".format(self._local_dir))
|
||||
return False
|
||||
return True
|
||||
|
||||
def wait(self):
|
||||
"""Waits for the sync client to complete the current sync."""
|
||||
self.sync_client.wait()
|
||||
|
||||
def reset(self):
|
||||
self.last_sync_up_time = float("-inf")
|
||||
self.last_sync_down_time = float("-inf")
|
||||
|
||||
def wait(self):
|
||||
pass
|
||||
self.sync_client.reset()
|
||||
|
||||
@property
|
||||
def _remote_path(self):
|
||||
"""Protected method for accessing remote_dir.
|
||||
|
||||
Can be overridden in subclass for custom path.
|
||||
"""
|
||||
return self._remote_dir
|
||||
|
||||
|
||||
class CommandSyncer(BaseSyncer):
|
||||
def __init__(self, local_dir, remote_dir, sync_template):
|
||||
"""Syncs between two directories with the given command.
|
||||
|
||||
Arguments:
|
||||
local_dir (str): Directory to sync.
|
||||
remote_dir (str): Remote directory to sync with.
|
||||
sync_template (str): A string template
|
||||
for syncer to run and needs to include replacement fields
|
||||
'{source}' and '{target}'. Returned when using
|
||||
`CommandSyncer.sync_template`, which can be overridden
|
||||
by subclass.
|
||||
"""
|
||||
super(CommandSyncer, self).__init__(local_dir, remote_dir)
|
||||
if not isinstance(sync_template, str):
|
||||
raise ValueError("{} is not a string.".format(sync_template))
|
||||
validate_sync_string(sync_template)
|
||||
self._sync_template = sync_template
|
||||
self.logfile = tempfile.NamedTemporaryFile(
|
||||
prefix="log_sync",
|
||||
dir=self._local_dir,
|
||||
suffix=".log",
|
||||
delete=False)
|
||||
|
||||
self.sync_process = None
|
||||
|
||||
def sync_function(self, source, target):
|
||||
self.last_sync_time = time.time()
|
||||
if self.sync_process:
|
||||
self.sync_process.poll()
|
||||
if self.sync_process.returncode is None:
|
||||
logger.warning("Last sync is still in progress, skipping.")
|
||||
return
|
||||
final_cmd = self._sync_template.format(
|
||||
source=quote(source), target=quote(target))
|
||||
logger.debug("Running sync: {}".format(final_cmd))
|
||||
self.sync_process = subprocess.Popen(
|
||||
final_cmd, shell=True, stdout=self.logfile)
|
||||
return True
|
||||
|
||||
def reset(self):
|
||||
if self.sync_process:
|
||||
logger.warning("Sync process still running but resetting anyways.")
|
||||
self.sync_process = None
|
||||
super(CommandSyncer, self).reset()
|
||||
|
||||
def wait(self):
|
||||
if self.sync_process:
|
||||
self.sync_process.wait()
|
||||
|
||||
|
||||
def _get_sync_cls(sync_function):
|
||||
if not sync_function:
|
||||
return
|
||||
if isinstance(sync_function, types.FunctionType):
|
||||
return BaseSyncer
|
||||
elif isinstance(sync_function, str):
|
||||
return CommandSyncer
|
||||
else:
|
||||
raise ValueError("Sync function {} must be string or function".format(
|
||||
sync_function))
|
||||
|
||||
|
||||
def get_syncer(local_dir, remote_dir=None, sync_function=None):
|
||||
"""Returns a Syncer depending on given args.
|
||||
def get_cloud_syncer(local_dir, remote_dir=None, sync_function=None):
|
||||
"""Returns a Syncer.
|
||||
|
||||
This syncer is in charge of syncing the local_dir with upload_dir.
|
||||
|
||||
Args:
|
||||
local_dir: Source directory for syncing.
|
||||
remote_dir: Target directory for syncing. If None,
|
||||
returns BaseSyncer with a noop.
|
||||
local_dir (str): Source directory for syncing.
|
||||
remote_dir (str): Target directory for syncing. If not provided, a
|
||||
no-op Syncer is returned.
|
||||
sync_function (func | str): Function for syncing the local_dir to
|
||||
remote_dir. If string, then it must be a string template for
|
||||
syncer to run. If not provided, it defaults
|
||||
to standard S3 or gsutil sync commands.
|
||||
"""
|
||||
"""
|
||||
key = (local_dir, remote_dir)
|
||||
|
||||
if key in _syncers:
|
||||
return _syncers[key]
|
||||
|
||||
if not remote_dir:
|
||||
_syncers[key] = BaseSyncer(local_dir, remote_dir)
|
||||
_syncers[key] = Syncer(local_dir, remote_dir, NOOP)
|
||||
return _syncers[key]
|
||||
|
||||
sync_cls = _get_sync_cls(sync_function)
|
||||
client = _get_sync_client(sync_function)
|
||||
|
||||
if sync_cls:
|
||||
_syncers[key] = sync_cls(local_dir, remote_dir, sync_function)
|
||||
if client:
|
||||
_syncers[key] = Syncer(local_dir, remote_dir, client)
|
||||
return _syncers[key]
|
||||
|
||||
if remote_dir.startswith(S3_PREFIX):
|
||||
@@ -211,15 +268,17 @@ def get_syncer(local_dir, remote_dir=None, sync_function=None):
|
||||
raise TuneError(
|
||||
"Upload uri starting with '{}' requires awscli tool"
|
||||
" to be installed".format(S3_PREFIX))
|
||||
_syncers[key] = CommandSyncer(local_dir, remote_dir,
|
||||
"aws s3 sync {source} {target}")
|
||||
template = "aws s3 sync {source} {target}"
|
||||
s3_client = CommandBasedClient(template, template)
|
||||
_syncers[key] = Syncer(local_dir, remote_dir, s3_client)
|
||||
elif remote_dir.startswith(GS_PREFIX):
|
||||
if not distutils.spawn.find_executable("gsutil"):
|
||||
raise TuneError(
|
||||
"Upload uri starting with '{}' requires gsutil tool"
|
||||
" to be installed".format(GS_PREFIX))
|
||||
_syncers[key] = CommandSyncer(local_dir, remote_dir,
|
||||
"gsutil rsync -r {source} {target}")
|
||||
template = "gsutil rsync -r {source} {target}"
|
||||
gs_client = CommandBasedClient(template, template)
|
||||
_syncers[key] = Syncer(local_dir, remote_dir, gs_client)
|
||||
else:
|
||||
raise TuneError("Upload uri must start with one of: {}"
|
||||
"".format(ALLOWED_REMOTE_PREFIXES))
|
||||
@@ -228,37 +287,48 @@ def get_syncer(local_dir, remote_dir=None, sync_function=None):
|
||||
|
||||
|
||||
def get_log_syncer(local_dir, remote_dir=None, sync_function=None):
|
||||
"""Returns a Syncer depending on given args.
|
||||
|
||||
This syncer is in charge of syncing the local_dir with remote local_dir.
|
||||
"""Returns a log Syncer.
|
||||
|
||||
Args:
|
||||
local_dir: Source directory for syncing.
|
||||
remote_dir: Target directory for syncing. If None,
|
||||
returns BaseSyncer with noop.
|
||||
sync_function (func | str): Function for syncing the local_dir to
|
||||
local_dir (str): Source directory for syncing.
|
||||
remote_dir (str): Target directory for syncing. If not provided, a
|
||||
no-op Syncer is returned.
|
||||
sync_function (func|str): Function for syncing the local_dir to
|
||||
remote_dir. If string, then it must be a string template for
|
||||
syncer to run. If not provided, it defaults rsync.
|
||||
"""
|
||||
"""
|
||||
key = (local_dir, remote_dir)
|
||||
|
||||
if key in _syncers:
|
||||
return _syncers[key]
|
||||
|
||||
sync_cls = None
|
||||
if sync_function:
|
||||
sync_cls = _get_sync_cls(sync_function)
|
||||
elif not remote_dir:
|
||||
sync_client = NOOP
|
||||
elif sync_function:
|
||||
sync_client = _get_sync_client(sync_function)
|
||||
else:
|
||||
sync_cls = CommandSyncer
|
||||
sync_function = log_sync_template()
|
||||
sync_up = log_sync_template()
|
||||
sync_down = log_sync_template(options="--remove-source-files")
|
||||
if sync_up and sync_down:
|
||||
sync_client = CommandBasedClient(sync_up, sync_down)
|
||||
sync_client.set_logdir(local_dir)
|
||||
else:
|
||||
sync_client = NOOP
|
||||
|
||||
if not remote_dir or sync_function is None:
|
||||
sync_cls = BaseSyncer
|
||||
|
||||
class MixedSyncer(NodeSyncMixin, sync_cls):
|
||||
class MixedSyncer(NodeSyncMixin, Syncer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
sync_cls.__init__(self, *args, **kwargs)
|
||||
Syncer.__init__(self, *args, **kwargs)
|
||||
NodeSyncMixin.__init__(self)
|
||||
|
||||
_syncers[key] = MixedSyncer(local_dir, remote_dir, sync_function)
|
||||
_syncers[key] = MixedSyncer(local_dir, remote_dir, sync_client)
|
||||
return _syncers[key]
|
||||
|
||||
|
||||
def _get_sync_client(sync_function):
|
||||
if not sync_function:
|
||||
return None
|
||||
if isinstance(sync_function, types.FunctionType):
|
||||
return FunctionBasedClient(sync_function, sync_function)
|
||||
elif isinstance(sync_function, str):
|
||||
return CommandBasedClient(sync_function, sync_function)
|
||||
else:
|
||||
raise ValueError("Sync function {} must be string or function".format(
|
||||
sync_function))
|
||||
|
||||
Reference in New Issue
Block a user