chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# isort: off
|
||||
try:
|
||||
import horovod # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
raise ModuleNotFoundError(
|
||||
"Horovod isn't installed. To install Horovod with PyTorch support, run 'pip "
|
||||
"install 'horovod[pytorch]''. To install Horovod with TensorFlow support, "
|
||||
"run 'pip install 'horovod[tensorflow]''."
|
||||
)
|
||||
# isort: on
|
||||
|
||||
from ray.train.horovod.config import HorovodConfig
|
||||
from ray.train.horovod.horovod_trainer import HorovodTrainer
|
||||
from ray.train.v2._internal.constants import is_v2_enabled
|
||||
|
||||
if is_v2_enabled():
|
||||
from ray.train.v2.horovod.horovod_trainer import HorovodTrainer # noqa: F811
|
||||
|
||||
__all__ = ["HorovodConfig", "HorovodTrainer"]
|
||||
|
||||
|
||||
# DO NOT ADD ANYTHING AFTER THIS LINE.
|
||||
@@ -0,0 +1,163 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Set
|
||||
|
||||
from horovod.ray.runner import Coordinator
|
||||
from horovod.ray.utils import detect_nics, nics_to_env_var
|
||||
from horovod.runner.common.util import secret, timeout
|
||||
|
||||
import ray
|
||||
from ray.train._internal.utils import update_env_vars
|
||||
from ray.train._internal.worker_group import Worker, WorkerGroup
|
||||
from ray.train.backend import Backend, BackendConfig
|
||||
from ray.util import PublicAPI
|
||||
|
||||
|
||||
@PublicAPI(stability="beta")
|
||||
@dataclass
|
||||
class HorovodConfig(BackendConfig):
|
||||
"""Configurations for Horovod setup.
|
||||
|
||||
See https://github.com/horovod/horovod/blob/master/horovod/runner/common/util/settings.py # noqa: E501
|
||||
|
||||
Args:
|
||||
nics (Optional[Set[str]): Network interfaces that can be used for
|
||||
communication.
|
||||
verbose: Horovod logging verbosity.
|
||||
key (Optional[str]): Secret used for communication between workers.
|
||||
ssh_port (Optional[int]): Port for SSH server running on worker nodes.
|
||||
ssh_identity_file (Optional[str]): Path to the identity file to
|
||||
ssh into different hosts on the cluster.
|
||||
ssh_str (Optional[str]): CAUTION WHEN USING THIS. Private key
|
||||
file contents. Writes the private key to ssh_identity_file.
|
||||
timeout_s: Timeout parameter for Gloo rendezvous.
|
||||
placement_group_timeout_s: Timeout parameter for Ray
|
||||
Placement Group creation. Currently unused.
|
||||
"""
|
||||
|
||||
nics: Optional[Set[str]] = None
|
||||
verbose: int = 1
|
||||
key: Optional[str] = None
|
||||
ssh_port: Optional[int] = None
|
||||
ssh_identity_file: Optional[str] = None
|
||||
ssh_str: Optional[str] = None
|
||||
timeout_s: int = 300
|
||||
placement_group_timeout_s: int = 100
|
||||
|
||||
@property
|
||||
def start_timeout(self):
|
||||
return timeout.Timeout(
|
||||
self.timeout_s,
|
||||
message="Timed out waiting for {activity}. Please "
|
||||
"check connectivity between servers. You "
|
||||
"may need to increase the --start-timeout "
|
||||
"parameter if you have too many servers.",
|
||||
)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.ssh_str and not os.path.exists(self.ssh_identity_file):
|
||||
with open(self.ssh_identity_file, "w") as f:
|
||||
os.chmod(self.ssh_identity_file, 0o600)
|
||||
f.write(self.ssh_str)
|
||||
|
||||
if self.key is None:
|
||||
self.key = secret.make_secret_key()
|
||||
|
||||
@property
|
||||
def backend_cls(self):
|
||||
return _HorovodBackend
|
||||
|
||||
|
||||
class _HorovodBackend(Backend):
|
||||
share_cuda_visible_devices: bool = True
|
||||
|
||||
def on_start(self, worker_group: WorkerGroup, backend_config: HorovodConfig):
|
||||
# NOTE: Horovod backend uses V1 WorkerGroup directly instead of BaseWorkerGroup
|
||||
# because it requires direct access to worker metadata (node_id, hostname) that is
|
||||
# specific to the V1 implementation. Horovod does not support V2 WorkerGroup.
|
||||
|
||||
# TODO(matt): Implement placement group strategies in BackendExecutor.
|
||||
|
||||
# Initialize workers with Horovod environment variables
|
||||
setup_futures = []
|
||||
for rank in range(len(worker_group)):
|
||||
worker_node_id = worker_group.workers[rank].metadata.node_id
|
||||
setup_futures.append(
|
||||
worker_group.execute_single_async(
|
||||
rank,
|
||||
_init_env_vars,
|
||||
rank,
|
||||
len(worker_group),
|
||||
worker_node_id,
|
||||
)
|
||||
)
|
||||
ray.get(setup_futures)
|
||||
|
||||
# Use Horovod Ray Coordinator
|
||||
# backend_config as settings
|
||||
self.coordinator = Coordinator(backend_config)
|
||||
|
||||
# Get all the hostnames of all workers
|
||||
node_ids = [w.metadata.node_id for w in worker_group.workers]
|
||||
hostnames = [w.metadata.hostname for w in worker_group.workers]
|
||||
# Register each hostname to the coordinator. assumes the hostname
|
||||
# ordering is the same.
|
||||
for rank, (hostname, node_id) in enumerate(zip(hostnames, node_ids)):
|
||||
self.coordinator.register(hostname, node_id, rank)
|
||||
all_info = self.coordinator.finalize_registration()
|
||||
|
||||
setup_futures = []
|
||||
for rank, local_cross_env_var in all_info.items():
|
||||
setup_futures.append(
|
||||
worker_group.execute_single_async(
|
||||
rank, update_env_vars, local_cross_env_var
|
||||
)
|
||||
)
|
||||
ray.get(setup_futures)
|
||||
|
||||
coordinator_envs = self.coordinator.establish_rendezvous()
|
||||
|
||||
# Get one worker from each host/node.
|
||||
node_worker_indexes = [node_ids.index(node_id) for node_id in set(node_ids)]
|
||||
node_workers = [
|
||||
_HorovodWorkerWrapper(worker_group.workers[worker_index])
|
||||
for worker_index in node_worker_indexes
|
||||
]
|
||||
assert len(node_workers) == len(self.coordinator.hostnames)
|
||||
|
||||
nics = detect_nics(
|
||||
backend_config,
|
||||
all_host_names=list(self.coordinator.hostnames),
|
||||
node_workers=node_workers,
|
||||
)
|
||||
coordinator_envs.update(nics_to_env_var(nics))
|
||||
|
||||
worker_group.execute(update_env_vars, coordinator_envs)
|
||||
|
||||
|
||||
def _init_env_vars(world_rank: int, world_size: int, node_id: str):
|
||||
"""Initialize Horovod environment variables."""
|
||||
os.environ["HOROVOD_HOSTNAME"] = node_id
|
||||
os.environ["HOROVOD_RANK"] = str(world_rank)
|
||||
os.environ["HOROVOD_SIZE"] = str(world_size)
|
||||
|
||||
|
||||
# TODO(tgaddair): temporary workaround for Horovod's worker discovery logic,
|
||||
# which requires passing in an extra parameter as part of the RayExecutor
|
||||
# API. This will be removed in the future as we migrate more of the
|
||||
# RayExecutor utils into Ray Train.
|
||||
# See: https://github.com/horovod/horovod/blob/v0.23.0/horovod/ray/driver_service.py#L9 # noqa: E501
|
||||
@dataclass
|
||||
class _HorovodWorkerWrapper:
|
||||
w: Worker
|
||||
|
||||
@property
|
||||
def execute(self):
|
||||
w = self.w
|
||||
|
||||
class ExecuteHandle:
|
||||
def remote(self, func, *args, **kwargs):
|
||||
_ = None
|
||||
return w.actor._RayTrainWorker__execute.remote(func, _, *args, **kwargs)
|
||||
|
||||
return ExecuteHandle()
|
||||
@@ -0,0 +1,197 @@
|
||||
from typing import Any, Callable, Dict, Optional, Union
|
||||
|
||||
from ray.air.config import RunConfig, ScalingConfig
|
||||
from ray.train import Checkpoint, DataConfig
|
||||
from ray.train.data_parallel_trainer import DataParallelTrainer
|
||||
from ray.train.horovod.config import HorovodConfig
|
||||
from ray.train.trainer import GenDataset
|
||||
from ray.util.annotations import PublicAPI
|
||||
|
||||
|
||||
@PublicAPI(stability="beta")
|
||||
class HorovodTrainer(DataParallelTrainer):
|
||||
"""A Trainer for data parallel Horovod training.
|
||||
|
||||
This Trainer runs the function ``train_loop_per_worker`` on multiple Ray
|
||||
Actors. These actors already have the necessary Horovod setup already
|
||||
configured for distributed Horovod training.
|
||||
|
||||
The ``train_loop_per_worker`` function is expected to take in either 0 or 1
|
||||
arguments:
|
||||
|
||||
.. testcode::
|
||||
|
||||
def train_loop_per_worker():
|
||||
...
|
||||
|
||||
.. testcode::
|
||||
|
||||
def train_loop_per_worker(config: Dict):
|
||||
...
|
||||
|
||||
If ``train_loop_per_worker`` accepts an argument, then
|
||||
``train_loop_config`` will be passed in as the argument. This is useful if you
|
||||
want to tune the values in ``train_loop_config`` as hyperparameters.
|
||||
|
||||
If the ``datasets`` dict contains a training dataset (denoted by
|
||||
the "train" key), then it will be split into multiple dataset
|
||||
shards that can then be accessed by ``ray.train.get_dataset_shard("train")`` inside
|
||||
``train_loop_per_worker``. All the other datasets will not be split and
|
||||
``ray.train.get_dataset_shard(...)`` will return the entire Dataset.
|
||||
|
||||
Inside the ``train_loop_per_worker`` function, you can use any of the
|
||||
:ref:`Ray Train loop methods <train-loop-api>`.
|
||||
|
||||
.. testcode::
|
||||
|
||||
from ray import train
|
||||
|
||||
def train_loop_per_worker():
|
||||
# Report intermediate results for callbacks or logging and
|
||||
# checkpoint data.
|
||||
train.report(...)
|
||||
|
||||
# Returns dict of last saved checkpoint.
|
||||
train.get_checkpoint()
|
||||
|
||||
# Returns the Dataset shard for the given key.
|
||||
train.get_dataset_shard("my_dataset")
|
||||
|
||||
# Returns the total number of workers executing training.
|
||||
train.get_context().get_world_size()
|
||||
|
||||
# Returns the rank of this worker.
|
||||
train.get_context().get_world_rank()
|
||||
|
||||
# Returns the rank of the worker on the current node.
|
||||
train.get_context().get_local_rank()
|
||||
|
||||
Any returns from the ``train_loop_per_worker`` will be discarded and not
|
||||
used or persisted anywhere.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
.. testcode::
|
||||
:skipif: True
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import ray
|
||||
import horovod.torch as hvd
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from ray import train
|
||||
import ray.train.torch # Need this to use `train.torch.get_device()`
|
||||
from ray.train import Checkpoint, ScalingConfig
|
||||
from ray.train.horovod import HorovodTrainer
|
||||
|
||||
# If using GPUs, set this to True.
|
||||
use_gpu = False
|
||||
|
||||
input_size = 1
|
||||
layer_size = 15
|
||||
output_size = 1
|
||||
num_epochs = 3
|
||||
|
||||
class NeuralNetwork(nn.Module):
|
||||
def __init__(self):
|
||||
super(NeuralNetwork, self).__init__()
|
||||
self.layer1 = nn.Linear(input_size, layer_size)
|
||||
self.relu = nn.ReLU()
|
||||
self.layer2 = nn.Linear(layer_size, output_size)
|
||||
def forward(self, input):
|
||||
return self.layer2(self.relu(self.layer1(input)))
|
||||
|
||||
def train_loop_per_worker():
|
||||
hvd.init()
|
||||
dataset_shard = train.get_dataset_shard("train")
|
||||
model = NeuralNetwork()
|
||||
device = train.torch.get_device()
|
||||
model.to(device)
|
||||
loss_fn = nn.MSELoss()
|
||||
lr_scaler = 1
|
||||
optimizer = torch.optim.SGD(model.parameters(), lr=0.1 * lr_scaler)
|
||||
# Horovod: wrap optimizer with DistributedOptimizer.
|
||||
optimizer = hvd.DistributedOptimizer(
|
||||
optimizer,
|
||||
named_parameters=model.named_parameters(),
|
||||
op=hvd.Average,
|
||||
)
|
||||
for epoch in range(num_epochs):
|
||||
model.train()
|
||||
for batch in dataset_shard.iter_torch_batches(
|
||||
batch_size=32, dtypes=torch.float
|
||||
):
|
||||
inputs, labels = torch.unsqueeze(batch["x"], 1), batch["y"]
|
||||
outputs = model(inputs)
|
||||
loss = loss_fn(outputs, labels)
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
print(f"epoch: {epoch}, loss: {loss.item()}")
|
||||
|
||||
# Save a model checkpoint at the end of each epoch
|
||||
with tempfile.TemporaryDirectory() as temp_checkpoint_dir:
|
||||
ckpt_path = os.path.join(temp_checkpoint_dir, "model.pt")
|
||||
torch.save(model.state_dict(), ckpt_path)
|
||||
train.report(
|
||||
{"loss": loss.item(), "epoch": epoch},
|
||||
checkpoint=Checkpoint.from_directory(temp_checkpoint_dir),
|
||||
)
|
||||
|
||||
train_dataset = ray.data.from_items([{"x": x, "y": x + 1} for x in range(32)])
|
||||
scaling_config = ScalingConfig(num_workers=3, use_gpu=use_gpu)
|
||||
trainer = HorovodTrainer(
|
||||
train_loop_per_worker=train_loop_per_worker,
|
||||
scaling_config=scaling_config,
|
||||
datasets={"train": train_dataset},
|
||||
)
|
||||
result = trainer.fit()
|
||||
|
||||
Args:
|
||||
train_loop_per_worker: The training function to execute.
|
||||
This can either take in no arguments or a ``config`` dict.
|
||||
train_loop_config: Configurations to pass into
|
||||
``train_loop_per_worker`` if it accepts an argument.
|
||||
horovod_config: Configuration for setting up the Horovod backend.
|
||||
If set to None, use the default configuration. This replaces the
|
||||
``backend_config`` arg of ``DataParallelTrainer``.
|
||||
scaling_config: Configuration for how to scale data parallel training.
|
||||
dataset_config: Configuration for dataset ingest.
|
||||
run_config: Configuration for the execution of the training run.
|
||||
datasets: Any Datasets to use for training. Use
|
||||
the key "train" to denote which dataset is the training
|
||||
dataset.
|
||||
metadata: Dict that should be made available via
|
||||
`ray.train.get_context().get_metadata()` and in `checkpoint.get_metadata()`
|
||||
for checkpoints saved from this Trainer. Must be JSON-serializable.
|
||||
resume_from_checkpoint: A checkpoint to resume training from.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
train_loop_per_worker: Union[Callable[[], None], Callable[[Dict], None]],
|
||||
*,
|
||||
train_loop_config: Optional[Dict] = None,
|
||||
horovod_config: Optional[HorovodConfig] = None,
|
||||
scaling_config: Optional[ScalingConfig] = None,
|
||||
dataset_config: Optional[DataConfig] = None,
|
||||
run_config: Optional[RunConfig] = None,
|
||||
datasets: Optional[Dict[str, GenDataset]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
resume_from_checkpoint: Optional[Checkpoint] = None,
|
||||
):
|
||||
super().__init__(
|
||||
train_loop_per_worker=train_loop_per_worker,
|
||||
train_loop_config=train_loop_config,
|
||||
backend_config=horovod_config or HorovodConfig(),
|
||||
scaling_config=scaling_config,
|
||||
dataset_config=dataset_config,
|
||||
run_config=run_config,
|
||||
datasets=datasets,
|
||||
resume_from_checkpoint=resume_from_checkpoint,
|
||||
metadata=metadata,
|
||||
)
|
||||
Reference in New Issue
Block a user