class Trainer: """A class for enabling seamless distributed deep learning. Directory structure: - A logdir is created during instantiation. This will hold all the results/checkpoints for the lifetime of the Trainer. By default, it will be of the form ``~/ray_results/train_<datestring>``. - A run_dir is created for each ``run`` call. This will hold the checkpoints and results for a single ``trainer.run()`` or ``trainer.run_iterator()`` call. It will be of the form ``run_<run_id>``. Args: backend (Union[str, BackendConfig]): The backend used for distributed communication. If configurations are needed, a subclass of ``BackendConfig`` can be passed in. Supported ``str`` values: {"torch", "tensorflow", "horovod"}. num_workers: The number of workers (Ray actors) to launch. Each worker will reserve 1 CPU by default. The number of CPUs reserved by each worker can be overridden with the ``resources_per_worker`` argument. use_gpu: If True, training will be done on GPUs (1 per worker). Defaults to False. The number of GPUs reserved by each worker can be overridden with the ``resources_per_worker`` argument. resources_per_worker (Optional[Dict]): If specified, the resources defined in this Dict will be reserved for each worker. The ``CPU`` and ``GPU`` keys (case-sensitive) can be defined to override the number of CPU/GPUs used by each worker. logdir (Optional[str]): Path to the file directory where logs should be persisted. If this is not specified, one will be generated. max_retries: Number of retries when Ray actors fail. Defaults to 3. Set to -1 for unlimited retries. """ def __init__( self, backend: Union[str, BackendConfig], num_workers: int, use_gpu: bool = False, resources_per_worker: Optional[Dict[str, float]] = None, logdir: Optional[str] = None, max_retries: int = 3, ): warnings.warn( "The `ray.train.Trainer` API is deprecated in Ray " "2.0, and is replaced by Ray AI Runtime (Ray AIR). Ray AIR (" "https://docs.ray.io/en/latest/ray-air/getting-started.html) will " "provide greater functionality than `ray.train.Trainer`, " "and with a more flexible and easy-to-use API.", DeprecationWarning, stacklevel=2, ) if num_workers <= 0: raise ValueError("`num_workers` must be a positive integer.") if not ray.is_initialized(): ray.init() if "GPU" in ray.available_resources() and not use_gpu: logger.info( "GPUs are detected in your Ray cluster, but GPU " "training is not enabled for Ray Train. To enable " "GPU training, make sure to set `use_gpu` to True " "when instantiating your Trainer." ) if resources_per_worker is not None: # Copy this parameter to avoid mutating the user input resources_per_worker = copy.deepcopy(resources_per_worker) self._num_workers = num_workers self._use_gpu = use_gpu self._resources_per_worker = resources_per_worker # Incremental unique run ID. self._run_id = 0 self.logdir = self.create_logdir(logdir) # Setup executor. self._backend_config = self._get_backend_config(backend) num_cpus = 1 num_gpus = int(use_gpu) if resources_per_worker: # Override CPU and GPU resources and remove from dict. num_cpus = resources_per_worker.pop("CPU", num_cpus) num_gpus = resources_per_worker.pop("GPU", num_gpus) if not use_gpu and num_gpus > 0: raise ValueError( "`use_gpu` is False but `GPU` was found in " "`resources_per_worker`. Either set `use_gpu` to True or " "remove `GPU` from `resources_per_worker." ) if use_gpu and num_gpus == 0: raise ValueError( "`use_gpu` is True but `GPU` is set to 0 in " "`resources_per_worker`. Either set `use_gpu` to False or " "request a positive number of `GPU` in " "`resources_per_worker." ) runtime_env = { "env_vars": { var_name: os.environ[var_name] for var_name in BACKEND_ENV_VARS if var_name in os.environ } } remote_executor = ray.remote(num_cpus=0)(BackendExecutor) backend_executor_actor = remote_executor.options( runtime_env=runtime_env ).remote( backend_config=self._backend_config, num_workers=num_workers, num_cpus_per_worker=num_cpus, num_gpus_per_worker=num_gpus, additional_resources_per_worker=resources_per_worker, max_retries=max_retries, ) self._backend_executor = ActorWrapper(backend_executor_actor) # Todo (krfricke): Initialize checkpoint manager here with final values # rather than in `on_training_start` if self._is_tune_enabled(): self.checkpoint_manager = TuneCheckpointManager( checkpoint_strategy=None, run_dir=None ) else: self.checkpoint_manager = CheckpointManager( checkpoint_strategy=None, run_dir=None ) def create_logdir(self, log_dir: Optional[Union[str, Path]]) -> Path: """Create logdir for the Trainer.""" # Create directory for logs. log_dir = Path(log_dir) if log_dir else None if not log_dir: # Initialize timestamp for identifying this Train execution. timestr = datetime.today().strftime("%Y-%m-%d_%H-%M-%S") log_dir = Path(f"train_{timestr}") log_dir = construct_path(log_dir, DEFAULT_RESULTS_DIR) log_dir.mkdir(parents=True, exist_ok=True) logger.info(f"Trainer logs will be logged in: {log_dir}") return log_dir def create_run_dir(self): """Create rundir for the particular training run.""" self.latest_run_dir.mkdir(parents=True, exist_ok=True) logger.info(f"Run results will be logged in: {self.latest_run_dir}") def _get_backend_config(self, backend: Union[str, BackendConfig]) -> BackendConfig: """Gets the ``BackendConfig`` to use for training. Args: backend (Union[str, BackendConfig]): If a ``BackendConfig`` is passed in, then it will also be returned. If a ``str`` is passed in, then the default config for that backend will be returned. Returns: The ``BackendConfig`` that will be used to set up the ``BackendExecutor``. """ if isinstance(backend, BackendConfig): return backend elif isinstance(backend, str): return _get_backend_config_cls(backend)() else: raise TypeError(f"Invalid type for backend: {type(backend)}.") def _is_tune_enabled(self): """Whether or not this Trainer is part of a Tune session.""" return TUNE_INSTALLED and tune.is_session_enabled() def start(self, initialization_hook: Optional[Callable[[], None]] = None): """Starts the training execution service. Args: initialization_hook (Optional[Callable]): The function to call on each worker when it is instantiated. """ self._backend_executor.start(initialization_hook) def run( self, train_func: Union[Callable[[], T], Callable[[Dict[str, Any]], T]], config: Optional[Dict[str, Any]] = None, callbacks: Optional[List[TrainingCallback]] = None, dataset: Optional[Union[RayDataset, Dict[str, RayDataset]]] = None, checkpoint: Optional[Union[Dict, str, Path]] = None, checkpoint_strategy: Optional[CheckpointConfig] = None, ) -> List[T]: """Runs a training function in a distributed manner. Args: train_func: The training function to execute. This can either take in no arguments or a ``config`` dict. config (Optional[Dict]): Configurations to pass into ``train_func``. If None then an empty Dict will be created. callbacks (Optional[List[TrainingCallback]]): A list of Callbacks which will be executed during training. If this is not set, currently there are NO default Callbacks. dataset (Optional[Union[RayDataset, Dict[str, RayDataset]]]): Distributed Ray :ref:`Dataset <dataset-api>` or :ref:`DatasetPipeline <dataset-pipeline-api>` to pass into the workers, which can be accessed from the training function via ``train.get_dataset_shard()``. Sharding will automatically be handled by the Trainer. Multiple Datasets can be passed in as a ``Dict`` that maps each name key to a Dataset value, and each Dataset can be accessed from the training function by passing in a `dataset_name` argument to ``train.get_dataset_shard()``. checkpoint (Optional[Dict|str|Path]): The checkpoint data that should be loaded onto each worker and accessed by the training function via ``train.load_checkpoint()``. If this is a ``str`` or ``Path`` then the value is expected to be a path to a file that contains a serialized checkpoint dict. If this is ``None`` then no checkpoint will be loaded. checkpoint_strategy (Optional[CheckpointConfig]): The configurations for saving checkpoints. Returns: A list of results from the training function. Each value in the list corresponds to the output of the training function from each worker. """ # Create new log directory for this run. self._run_id += 1 self.create_run_dir() # TODO(matt): Set default callbacks. callbacks = [] if callbacks is None else callbacks finished_with_errors = False for callback in callbacks: callback.start_training( logdir=str(self.latest_run_dir), config=config or {} ) train_func = construct_train_func(train_func, config) dataset_spec = RayDatasetSpec(dataset_or_dict=dataset) try: iterator = TrainingIterator( backend_executor=self._backend_executor, backend_config=self._backend_config, train_func=train_func, dataset_spec=dataset_spec, checkpoint_manager=self.checkpoint_manager, checkpoint=checkpoint, checkpoint_strategy=checkpoint_strategy, run_dir=self.latest_run_dir, ) for intermediate_result in iterator: for callback in callbacks: callback.process_results(intermediate_result) assert iterator.is_finished() return iterator.get_final_results() finally: for callback in callbacks: callback.finish_training(error=finished_with_errors) def run_iterator( self, train_func: Union[Callable[[], T], Callable[[Dict[str, Any]], T]], config: Optional[Dict[str, Any]] = None, dataset: Optional[Union[RayDataset, Dict[str, RayDataset]]] = None, checkpoint: Optional[Union[Dict, str, Path]] = None, checkpoint_strategy: Optional[CheckpointConfig] = None, ) -> "TrainingIterator": """Same as ``run`` except returns an iterator over the results. This is useful if you want to have more customization of what to do with the intermediate results or how to use the ``Trainer`` with Ray Tune. .. code-block:: python def train_func(config): ... for _ in config["epochs"]: metrics = train() metrics = validate(...) ray.train.report(**metrics) return model iterator = trainer.run_iterator(train_func, config=config) for result in iterator: do_stuff(result) latest_ckpt = trainer.get_latest_checkpoint() assert iterator.is_finished() model = iterator.get_fin()[0] Args: train_func: The training function to execute. This can either take in no arguments or a ``config`` dict. config (Optional[Dict]): Configurations to pass into ``train_func``. If None then an empty Dict will be created. checkpoint (Optional[Dict|Path|str]): The checkpoint data that should be loaded onto each worker and accessed by the training function via ``train.load_checkpoint()``. If this is a ``str`` or ``Path`` then the value is expected to be a path to a file that contains a serialized checkpoint dict. If this is ``None`` then no checkpoint will be loaded. checkpoint_strategy (Optional[CheckpointConfig]): The configurations for saving checkpoints. Returns: An Iterator over the intermediate results from ``train.report()``. """ # Create new log directory for this run. self._run_id += 1 self.create_run_dir() train_func = construct_train_func(train_func, config) dataset_spec = RayDatasetSpec(dataset_or_dict=dataset) return TrainingIterator( backend_executor=self._backend_executor, backend_config=self._backend_config, train_func=train_func, run_dir=self.latest_run_dir, dataset_spec=dataset_spec, checkpoint_manager=self.checkpoint_manager, checkpoint=checkpoint, checkpoint_strategy=checkpoint_strategy, ) @property def latest_run_dir(self) -> Optional[Path]: """Path to the log directory for the latest call to ``run()``. Returns ``None`` if ``run()`` has not been called. """ if self._run_id > 0: run_dir = Path(f"run_{self._run_id:03d}") return construct_path(run_dir, self.logdir) else: return None @property def latest_checkpoint_dir(self) -> Optional[Path]: """Path to the checkpoint directory. Returns ``None`` if ``run()`` has not been called or if ``train.checkpoint()`` has not been called from ``train_func``within the most recent call to ``run``. """ return self.checkpoint_manager.latest_checkpoint_dir @property def best_checkpoint_path(self) -> Optional[Path]: """Path to the best persisted checkpoint from the latest run. "Best" is defined by the input ``CheckpointConfig``. Default behavior is to return the most recent checkpoint. Returns ``None`` if ``run()`` has not been called or if ``train.save_checkpoint()`` has not been called from ``train_func`` within the most recent call to ``run``. """ return self.checkpoint_manager.best_checkpoint_path @property def latest_checkpoint(self) -> Optional[Dict]: """The latest saved checkpoint. This checkpoint may not be saved to disk. Returns ``None`` if ``run()`` has not been called or if ``train.checkpoint()`` has not been called from ``train_func``. """ return self.checkpoint_manager.latest_checkpoint @property def best_checkpoint(self) -> Optional[Dict]: """Best saved checkpoint from the latest run. "Best" is defined by the input ``CheckpointConfig``. Default behavior is to return the most recent checkpoint. Returns ``None`` if ``run()`` has not been called or if ``train.save_checkpoint()`` has not been called from ``train_func`` within the most recent call to ``run``. """ best_checkpoint_path = self.best_checkpoint_path if best_checkpoint_path is None: return None else: return load_checkpoint_from_path(best_checkpoint_path) @staticmethod def load_checkpoint_from_path(checkpoint_file_path: Union[str, Path]) -> Dict: """Convenience method to load a checkpoint from path. An error will be raised if the provided path does not exist. Args: checkpoint_file_path (Union[str, Path]): The path to the checkpoint to load. If the checkpoint saved in this path has not been created by Ray Train, there is no guarantee that it can be loaded in successfully. """ return load_checkpoint_from_path(checkpoint_file_path) def shutdown(self): """Shuts down the training execution service.""" self._backend_executor.shutdown() def to_tune_trainable( self, train_func: Callable[[Dict[str, Any]], T], dataset: Optional[Union[RayDataset, Dict[str, RayDataset]]] = None, ) -> Type[Trainable]: """Creates a Tune ``Trainable`` from the input training function. Args: train_func: The function that should be executed on each training worker. dataset (Optional[Union[RayDataset, Dict[str, RayDataset]]]): Distributed Ray p:ref:`Dataset <dataset-api>` or :ref:`DatasetPipeline <dataset-pipeline-api>` to pass into the workers, which can be accessed from the training function via ``train.get_dataset_shard()``. Sharding will automatically be handled by the Trainer. Multiple Datasets can be passed in as a ``Dict`` that maps each name key to a Dataset value, and each Dataset can be accessed from the training function by passing in a `dataset_name` argument to ``train.get_dataset_shard()``. Returns: A Trainable that can directly be passed into ``tune.run()``. """ if not TUNE_INSTALLED: raise ValueError( "Tune is not installed. Please install ray[" "tune] to use the Tune integration." ) if self._backend_executor.is_started(): raise RuntimeError( "The Trainer must not be active to use " "`to_tune_trainable`. Either shutdown the " "Trainer or don't start it in the first place." ) return _create_tune_trainable( train_func, dataset, self._backend_config, self._num_workers, self._use_gpu, self._resources_per_worker, ) def to_worker_group(self, train_cls: Type, *args, **kwargs) -> "TrainWorkerGroup": """Returns Ray actors with the provided class and the backend started. This is useful if you want to provide your own class for training and have more control over execution, but still want to use Ray Train to setup the appropriate backend configurations (torch, tf, etc.). .. code-block:: python class Trainer: def __init__(self, config): self.config = config def train_epoch(self): ... return 1 config = {"lr": 0.1} trainer = Trainer(num_workers=2, backend="torch") workers = trainer.to_worker_group(train_cls=Trainer, config=config) futures = [w.train_epoch.remote() for w in workers] assert ray.get(futures) == [1, 1] assert ray.get(workers[0].train_epoch.remote()) == 1 workers.shutdown() Args: train_cls: The class definition to use for the Ray actors/workers. args, kwargs: Arguments to pass into the ``__init__`` of the provided ``train_cls``. """ if self._backend_executor.is_started(): raise RuntimeError( "The Trainer must not be active to use " "`to_worker_group`. Either shutdown the " "Trainer or don't start it in the first place." ) self._backend_executor.start( train_cls=train_cls, train_cls_args=args, train_cls_kwargs=kwargs ) worker_group = self._backend_executor.get_worker_group() return TrainWorkerGroup(worker_group)
def __init__( self, backend: Union[str, BackendConfig], num_workers: int, use_gpu: bool = False, resources_per_worker: Optional[Dict[str, float]] = None, logdir: Optional[str] = None, max_retries: int = 3, ): warnings.warn( "The `ray.train.Trainer` API is deprecated in Ray " "2.0, and is replaced by Ray AI Runtime (Ray AIR). Ray AIR (" "https://docs.ray.io/en/latest/ray-air/getting-started.html) will " "provide greater functionality than `ray.train.Trainer`, " "and with a more flexible and easy-to-use API.", DeprecationWarning, stacklevel=2, ) if num_workers <= 0: raise ValueError("`num_workers` must be a positive integer.") if not ray.is_initialized(): ray.init() if "GPU" in ray.available_resources() and not use_gpu: logger.info( "GPUs are detected in your Ray cluster, but GPU " "training is not enabled for Ray Train. To enable " "GPU training, make sure to set `use_gpu` to True " "when instantiating your Trainer." ) if resources_per_worker is not None: # Copy this parameter to avoid mutating the user input resources_per_worker = copy.deepcopy(resources_per_worker) self._num_workers = num_workers self._use_gpu = use_gpu self._resources_per_worker = resources_per_worker # Incremental unique run ID. self._run_id = 0 self.logdir = self.create_logdir(logdir) # Setup executor. self._backend_config = self._get_backend_config(backend) num_cpus = 1 num_gpus = int(use_gpu) if resources_per_worker: # Override CPU and GPU resources and remove from dict. num_cpus = resources_per_worker.pop("CPU", num_cpus) num_gpus = resources_per_worker.pop("GPU", num_gpus) if not use_gpu and num_gpus > 0: raise ValueError( "`use_gpu` is False but `GPU` was found in " "`resources_per_worker`. Either set `use_gpu` to True or " "remove `GPU` from `resources_per_worker." ) if use_gpu and num_gpus == 0: raise ValueError( "`use_gpu` is True but `GPU` is set to 0 in " "`resources_per_worker`. Either set `use_gpu` to False or " "request a positive number of `GPU` in " "`resources_per_worker." ) runtime_env = { "env_vars": { var_name: os.environ[var_name] for var_name in BACKEND_ENV_VARS if var_name in os.environ } } remote_executor = ray.remote(num_cpus=0)(BackendExecutor) backend_executor_actor = remote_executor.options( runtime_env=runtime_env ).remote( backend_config=self._backend_config, num_workers=num_workers, num_cpus_per_worker=num_cpus, num_gpus_per_worker=num_gpus, additional_resources_per_worker=resources_per_worker, max_retries=max_retries, ) self._backend_executor = ActorWrapper(backend_executor_actor) # Todo (krfricke): Initialize checkpoint manager here with final values # rather than in `on_training_start` if self._is_tune_enabled(): self.checkpoint_manager = TuneCheckpointManager( checkpoint_strategy=None, run_dir=None ) else: self.checkpoint_manager = CheckpointManager( checkpoint_strategy=None, run_dir=None )