chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
# isort: off
try:
import torch # noqa: F401
except ModuleNotFoundError:
raise ModuleNotFoundError(
"PyTorch isn't installed. To install PyTorch, run 'pip install torch'"
)
# isort: on
from ray.train.torch.config import TorchConfig
from ray.train.torch.torch_checkpoint import TorchCheckpoint
from ray.train.torch.torch_trainer import TorchTrainer
from ray.train.torch.train_loop_utils import (
accelerate,
backward,
enable_reproducibility,
get_device,
get_devices,
prepare_data_loader,
prepare_model,
prepare_optimizer,
)
from ray.train.v2._internal.constants import is_v2_enabled
if is_v2_enabled():
from ray.train.v2.torch.torch_trainer import TorchTrainer # noqa: F811
from ray.train.v2.torch.train_loop_utils import ( # noqa: F811
accelerate,
backward,
enable_reproducibility,
get_device,
get_devices,
prepare_data_loader,
prepare_model,
prepare_optimizer,
)
__all__ = [
"TorchTrainer",
"TorchCheckpoint",
"TorchConfig",
"accelerate",
"get_device",
"get_devices",
"prepare_model",
"prepare_optimizer",
"prepare_data_loader",
"backward",
"enable_reproducibility",
]
# DO NOT ADD ANYTHING AFTER THIS LINE.
+256
View File
@@ -0,0 +1,256 @@
import logging
import os
from dataclasses import dataclass
from datetime import timedelta
from typing import Any, Dict, Optional
import torch
import torch.distributed as dist
from packaging.version import Version
import ray
from ray._common.network_utils import build_address
from ray._private import ray_constants
from ray.air._internal.device_manager import register_custom_torch_dist_backend
from ray.exceptions import GetTimeoutError
from ray.train._internal.base_worker_group import BaseWorkerGroup
from ray.train._internal.utils import get_address_and_port
from ray.train.backend import Backend, BackendConfig
from ray.train.constants import (
DEFAULT_TORCH_PROCESS_GROUP_SHUTDOWN_TIMEOUT_S,
TORCH_PROCESS_GROUP_SHUTDOWN_TIMEOUT_S,
)
from ray.train.v2._internal.util import TrainingFramework
from ray.util import PublicAPI
logger = logging.getLogger(__name__)
class TorchConfigContextManager:
def __enter__(self):
# Set default cuda device
if torch.cuda.is_available():
device = ray.train.torch.get_device()
if device.type == "cuda":
torch.cuda.set_device(device)
def __exit__(self, type, value, traceback):
# Propagate exceptions if any
return False
@PublicAPI(stability="stable")
@dataclass
class TorchConfig(BackendConfig):
"""Configuration for torch process group setup.
See https://pytorch.org/docs/stable/distributed.html for more info.
Args:
backend: The backend to use for training.
See ``torch.distributed.init_process_group`` for more info and
valid values.
If set to None, nccl will be used if GPUs are requested, else gloo
will be used.
init_method: The initialization method to use. Either "env"
for environment variable initialization or "tcp" for TCP
initialization. Defaults to "env".
timeout_s: Seconds for process group operations to timeout.
"""
backend: Optional[str] = None
init_method: str = "env"
timeout_s: int = 1800
@property
def backend_cls(self):
return _TorchBackend
@property
def train_func_context(self):
return TorchConfigContextManager
@property
def framework(self):
return TrainingFramework.TORCH
def to_dict(self) -> Dict[str, Any]:
config_dict = {
"backend": self.backend,
"init_method": self.init_method,
"timeout_s": self.timeout_s,
}
return config_dict
def _is_backend_nccl(backend: str) -> bool:
# Check containment because comma separated lists of backends like cpu:gloo,cuda:nccl are supported.
return backend == "nccl" or any(
item.split(":")[1] == "nccl"
for item in backend.split(",")
if item.startswith("cuda:")
)
def _setup_torch_process_group(
backend: str,
world_rank: int,
world_size: int,
init_method: str,
timeout_s: int = 1800,
):
"""Connects the distributed PyTorch backend.
Args:
backend: The backend (nccl, gloo, etc.) to use for training.
world_rank: Rank of the current worker.
world_size: Number of workers participating in the job.
init_method: URL specifying how to initialize the process group.
timeout_s: Seconds for process group operations to timeout.
"""
if world_rank == 0:
logger.info(
f"Setting up process group for: {init_method} [rank={world_rank}, "
f"world_size={world_size}]"
)
else:
logger.debug(
f"Setting up process group for: {init_method} [rank={world_rank}, "
f"world_size={world_size}]"
)
logger.debug(f"using {backend}")
if _is_backend_nccl(backend):
# See https://github.com/pytorch/pytorch/blob/c263bd43e8e8502d4726643bc6fd046f0130ac0e/torch/distributed/distributed_c10d.py#L803-L823 # noqa: E501
# We do not use TORCH_NCCL_BLOCKING_WAIT due to performance overhead.
if Version(torch.__version__) < Version("2.2.0"):
TORCH_NCCL_ASYNC_ERROR_HANDLING_ENV_VAR = "NCCL_ASYNC_ERROR_HANDLING"
TORCH_NCCL_BLOCKING_WAIT_ENV_VAR = "NCCL_BLOCKING_WAIT"
else:
TORCH_NCCL_ASYNC_ERROR_HANDLING_ENV_VAR = "TORCH_NCCL_ASYNC_ERROR_HANDLING"
TORCH_NCCL_BLOCKING_WAIT_ENV_VAR = "TORCH_NCCL_BLOCKING_WAIT"
if (
TORCH_NCCL_ASYNC_ERROR_HANDLING_ENV_VAR not in os.environ
and TORCH_NCCL_BLOCKING_WAIT_ENV_VAR not in os.environ
):
logger.debug(
f"Setting {TORCH_NCCL_ASYNC_ERROR_HANDLING_ENV_VAR}=1 to fail if NCCL collective communication operations are timing out. " # noqa: E501
f"To override this behavior, you can set {TORCH_NCCL_ASYNC_ERROR_HANDLING_ENV_VAR}=0." # noqa: E501
)
os.environ[TORCH_NCCL_ASYNC_ERROR_HANDLING_ENV_VAR] = "1"
elif backend == "hccl":
register_custom_torch_dist_backend(backend)
dist.init_process_group(
backend=backend,
init_method=init_method,
rank=world_rank,
world_size=world_size,
timeout=timedelta(seconds=timeout_s),
)
def _shutdown_torch(destroy_process_group=False):
from ray.air._internal.torch_utils import get_devices
devices = get_devices()
if destroy_process_group and dist.is_initialized():
dist.destroy_process_group()
if torch.cuda.is_available():
for device in devices:
if device.type == "cuda":
with torch.cuda.device(device):
torch.cuda.empty_cache()
def _set_torch_distributed_env_vars():
# Same env vars as in
# https://pytorch.org/docs/stable/elastic/run.html#environment-variables
from ray.train.torch import get_device
context = ray.train.get_context()
os.environ["LOCAL_RANK"] = str(context.get_local_rank())
os.environ["LOCAL_WORLD_SIZE"] = str(context.get_local_world_size())
os.environ["NODE_RANK"] = str(context.get_node_rank())
os.environ["RANK"] = str(context.get_world_rank())
os.environ["WORLD_SIZE"] = str(context.get_world_size())
# Makes sure Hugging Face Accelerate uses the correct device
device = get_device()
os.environ["ACCELERATE_TORCH_DEVICE"] = str(device)
class _TorchBackend(Backend):
share_cuda_visible_devices: bool = True
def on_start(self, worker_group: BaseWorkerGroup, backend_config: TorchConfig):
if dist.is_available():
# Set the appropriate training backend.
if backend_config.backend is None:
resources = worker_group.get_resources_per_worker()
num_gpus_per_worker = resources.get("GPU", 0)
if num_gpus_per_worker > 0:
backend = "nccl"
else:
backend = "gloo"
else:
backend = backend_config.backend
master_addr, master_port = worker_group.execute_single(
0, get_address_and_port
)
if backend_config.init_method == "env":
def set_env_vars(addr, port):
os.environ["MASTER_ADDR"] = addr
os.environ["MASTER_PORT"] = str(port)
worker_group.execute(set_env_vars, addr=master_addr, port=master_port)
url = "env://"
elif backend_config.init_method == "tcp":
url = f"tcp://{build_address(master_addr, master_port)}"
else:
raise ValueError(
f"The provided init_method ("
f"{backend_config.init_method}) is not supported. Must "
f"be either 'env' or 'tcp'."
)
setup_futures = []
for i in range(len(worker_group)):
setup_futures.append(
worker_group.execute_single_async(
i,
_setup_torch_process_group,
backend=backend,
world_rank=i,
world_size=len(worker_group),
init_method=url,
timeout_s=backend_config.timeout_s,
)
)
ray.get(setup_futures)
else:
raise RuntimeError("Distributed torch is not available.")
def on_shutdown(self, worker_group: BaseWorkerGroup, backend_config):
futures = worker_group.execute_async(
_shutdown_torch,
destroy_process_group=len(worker_group) > 1,
)
timeout_s = ray_constants.env_integer(
TORCH_PROCESS_GROUP_SHUTDOWN_TIMEOUT_S,
DEFAULT_TORCH_PROCESS_GROUP_SHUTDOWN_TIMEOUT_S,
)
try:
ray.get(futures, timeout=timeout_s)
except GetTimeoutError:
logger.warning(
f"Torch process group shutdown timed out after {timeout_s} seconds"
)
def on_training_start(
self, worker_group: BaseWorkerGroup, backend_config: BackendConfig
):
worker_group.execute(_set_torch_distributed_env_vars)
+196
View File
@@ -0,0 +1,196 @@
import os
import tempfile
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional
import torch
from ray.air._internal.torch_utils import (
consume_prefix_in_state_dict_if_present_not_in_place,
load_torch_model,
)
from ray.train._internal.framework_checkpoint import FrameworkCheckpoint
from ray.util.annotations import PublicAPI
if TYPE_CHECKING:
from ray.data.preprocessor import Preprocessor
ENCODED_DATA_KEY = "torch_encoded_data"
@PublicAPI(stability="beta")
class TorchCheckpoint(FrameworkCheckpoint):
"""A :class:`~ray.train.Checkpoint` with Torch-specific functionality."""
MODEL_FILENAME = "model.pt"
@classmethod
def from_state_dict(
cls,
state_dict: Dict[str, Any],
*,
preprocessor: Optional["Preprocessor"] = None,
) -> "TorchCheckpoint":
"""Create a :class:`~ray.train.Checkpoint` that stores a model state dictionary.
.. tip::
This is the recommended method for creating
:class:`TorchCheckpoints<TorchCheckpoint>`.
Args:
state_dict: The model state dictionary to store in the checkpoint.
preprocessor: A fitted preprocessor to be applied before inference.
Returns:
A :class:`TorchCheckpoint` containing the specified state dictionary.
Examples:
.. testcode::
import torch
import torch.nn as nn
from ray.train.torch import TorchCheckpoint
# Set manual seed
torch.manual_seed(42)
# Function to create a NN model
def create_model() -> nn.Module:
model = nn.Sequential(nn.Linear(1, 10),
nn.ReLU(),
nn.Linear(10,1))
return model
# Create a TorchCheckpoint from our model's state_dict
model = create_model()
checkpoint = TorchCheckpoint.from_state_dict(model.state_dict())
# Now load the model from the TorchCheckpoint by providing the
# model architecture
model_from_chkpt = checkpoint.get_model(create_model())
# Assert they have the same state dict
assert str(model.state_dict()) == str(model_from_chkpt.state_dict())
print("worked")
.. testoutput::
:hide:
...
"""
tempdir = tempfile.mkdtemp()
model_path = Path(tempdir, cls.MODEL_FILENAME).as_posix()
stripped_state_dict = consume_prefix_in_state_dict_if_present_not_in_place(
state_dict, "module."
)
torch.save(stripped_state_dict, model_path)
checkpoint = cls.from_directory(tempdir)
if preprocessor:
checkpoint.set_preprocessor(preprocessor)
return checkpoint
@classmethod
def from_model(
cls,
model: torch.nn.Module,
*,
preprocessor: Optional["Preprocessor"] = None,
) -> "TorchCheckpoint":
"""Create a :class:`~ray.train.Checkpoint` that stores a Torch model.
.. note::
PyTorch recommends storing state dictionaries. To create a
:class:`TorchCheckpoint` from a state dictionary, call
:meth:`~ray.train.torch.TorchCheckpoint.from_state_dict`. To learn more
about state dictionaries, read
`Saving and Loading Models <https://pytorch.org/tutorials/beginner/saving_loading_models.html#what-is-a-state-dict>`_. # noqa: E501
Args:
model: The Torch model to store in the checkpoint.
preprocessor: A fitted preprocessor to be applied before inference.
Returns:
A :class:`TorchCheckpoint` containing the specified model.
Examples:
.. testcode::
from ray.train.torch import TorchCheckpoint
import torch
# Create model identity and send a random tensor to it
model = torch.nn.Identity()
input = torch.randn(2, 2)
output = model(input)
# Create a checkpoint
checkpoint = TorchCheckpoint.from_model(model)
print(checkpoint)
.. testoutput::
:hide:
...
"""
tempdir = tempfile.mkdtemp()
model_path = Path(tempdir, cls.MODEL_FILENAME).as_posix()
torch.save(model, model_path)
checkpoint = cls.from_directory(tempdir)
if preprocessor:
checkpoint.set_preprocessor(preprocessor)
return checkpoint
def get_model(self, model: Optional[torch.nn.Module] = None) -> torch.nn.Module:
"""Retrieve the model stored in this checkpoint.
.. warning::
The checkpoint path must point to a **trusted** source.
Checkpoints created with
:meth:`~ray.train.torch.TorchCheckpoint.from_model` store the entire
``nn.Module`` via pickle serialization. Loading such a checkpoint from an
untrusted path (shared storage, downloaded artifact, checkpoint produced by
a different party) is equivalent to executing arbitrary Python code. Prefer
checkpoints created with
:meth:`~ray.train.torch.TorchCheckpoint.from_state_dict`, which stores
only model weights and is safe to load from untrusted sources.
Args:
model: If the checkpoint contains a model state dict, and not
the model itself, then the state dict will be loaded to this
``model``. Otherwise, the model will be discarded.
Returns:
The loaded ``torch.nn.Module``.
"""
with self.as_directory() as tempdir:
model_path = Path(tempdir, self.MODEL_FILENAME).as_posix()
if not os.path.exists(model_path):
raise RuntimeError(
"`model.pt` not found within this checkpoint. Make sure you "
"created this `TorchCheckpoint` from one of its public "
"constructors (`from_state_dict` or `from_model`)."
)
model_or_state_dict = torch.load(
model_path, map_location="cpu", weights_only=False
)
if isinstance(model_or_state_dict, torch.nn.Module):
if model:
warnings.warn(
"TorchCheckpoint already contains all information needed. "
"Discarding provided `model` argument."
)
model = load_torch_model(
saved_model=model_or_state_dict, model_definition=model
)
return model
+200
View File
@@ -0,0 +1,200 @@
from typing import Any, Callable, Dict, Optional, Union
from ray.train import Checkpoint, DataConfig, RunConfig, ScalingConfig
from ray.train.data_parallel_trainer import DataParallelTrainer
from ray.train.torch.config import TorchConfig
from ray.train.trainer import GenDataset
from ray.util import PublicAPI
@PublicAPI(stability="stable")
class TorchTrainer(DataParallelTrainer):
"""A Trainer for data parallel PyTorch training.
At a high level, this Trainer does the following:
1. Launches multiple workers as defined by the ``scaling_config``.
2. Sets up a distributed PyTorch environment
on these workers as defined by the ``torch_config``.
3. Ingests the input ``datasets`` based on the ``dataset_config``.
4. Runs the input ``train_loop_per_worker(train_loop_config)``
on all workers.
For more details, see:
* :ref:`PyTorch Guide <train-pytorch>`
* :ref:`PyTorch Lightning Guide <train-pytorch-lightning>`
* :ref:`Hugging Face Transformers Guide <train-pytorch-transformers>`
Example:
.. testcode::
:skipif: True
import os
import tempfile
import torch
from torch import nn
from torch.nn.parallel import DistributedDataParallel
import ray
from ray.train import Checkpoint, CheckpointConfig, RunConfig, ScalingConfig
from ray.train.torch import TorchTrainer
# If using GPUs, set this to True.
use_gpu = False
# Number of processes to run training on.
num_workers = 4
# Define your network structure.
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__init__()
self.layer1 = nn.Linear(1, 32)
self.relu = nn.ReLU()
self.layer2 = nn.Linear(32, 1)
def forward(self, input):
return self.layer2(self.relu(self.layer1(input)))
# Training loop.
def train_loop_per_worker(config):
# Read configurations.
lr = config["lr"]
batch_size = config["batch_size"]
num_epochs = config["num_epochs"]
# Fetch training dataset.
train_dataset_shard = ray.train.get_dataset_shard("train")
# Instantiate and prepare model for training.
model = NeuralNetwork()
model = ray.train.torch.prepare_model(model)
# Define loss and optimizer.
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=lr)
# Create data loader.
dataloader = train_dataset_shard.iter_torch_batches(
batch_size=batch_size, dtypes=torch.float
)
# Train multiple epochs.
for epoch in range(num_epochs):
# Train epoch.
for batch in dataloader:
output = model(batch["input"])
loss = loss_fn(output, batch["label"])
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Create checkpoint.
base_model = (model.module
if isinstance(model, DistributedDataParallel) else model)
checkpoint_dir = tempfile.mkdtemp()
torch.save(
{"model_state_dict": base_model.state_dict()},
os.path.join(checkpoint_dir, "model.pt"),
)
checkpoint = Checkpoint.from_directory(checkpoint_dir)
# Report metrics and checkpoint.
ray.train.report({"loss": loss.item()}, checkpoint=checkpoint)
# Define configurations.
train_loop_config = {"num_epochs": 20, "lr": 0.01, "batch_size": 32}
scaling_config = ScalingConfig(num_workers=num_workers, use_gpu=use_gpu)
run_config = RunConfig(checkpoint_config=CheckpointConfig(num_to_keep=1))
# Define datasets.
train_dataset = ray.data.from_items(
[{"input": [x], "label": [2 * x + 1]} for x in range(2000)]
)
datasets = {"train": train_dataset}
# Initialize the Trainer.
trainer = TorchTrainer(
train_loop_per_worker=train_loop_per_worker,
train_loop_config=train_loop_config,
scaling_config=scaling_config,
run_config=run_config,
datasets=datasets
)
# Train the model.
result = trainer.fit()
# Inspect the results.
final_loss = result.metrics["loss"]
Args:
train_loop_per_worker: The training function to execute on each worker.
This function can either take in zero arguments or a single ``Dict``
argument which is set by defining ``train_loop_config``.
Within this function you can use any of the
:ref:`Ray Train Loop utilities <train-loop-api>`.
train_loop_config: A configuration ``Dict`` to pass in as an argument to
``train_loop_per_worker``.
This is typically used for specifying hyperparameters. Passing large
datasets via `train_loop_config` is not recommended and may introduce
large overhead and unknown issues with serialization and deserialization.
torch_config: The configuration for setting up the PyTorch Distributed backend.
If set to None, a default configuration will be used in which
GPU training uses NCCL and CPU training uses Gloo.
scaling_config: The configuration for how to scale data parallel training.
``num_workers`` determines how many Python processes are used for training,
and ``use_gpu`` determines whether or not each process should use GPUs.
See :class:`~ray.train.ScalingConfig` for more info.
run_config: The configuration for the execution of the training run.
See :class:`~ray.train.RunConfig` for more info.
datasets: The Ray Datasets to ingest for training.
Datasets are keyed by name (``{name: dataset}``).
Each dataset can be accessed from within the ``train_loop_per_worker``
by calling ``ray.train.get_dataset_shard(name)``.
Sharding and additional configuration can be done by
passing in a ``dataset_config``.
dataset_config: The configuration for ingesting the input ``datasets``.
By default, all the Ray Dataset are split equally across workers.
See :class:`~ray.train.DataConfig` for more details.
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.
This checkpoint can be accessed from within ``train_loop_per_worker``
by calling ``ray.train.get_checkpoint()``.
"""
def __init__(
self,
train_loop_per_worker: Union[Callable[[], None], Callable[[Dict], None]],
*,
train_loop_config: Optional[Dict] = None,
torch_config: Optional[TorchConfig] = None,
scaling_config: Optional[ScalingConfig] = None,
run_config: Optional[RunConfig] = None,
datasets: Optional[Dict[str, GenDataset]] = None,
dataset_config: Optional[DataConfig] = None,
metadata: Optional[Dict[str, Any]] = None,
resume_from_checkpoint: Optional[Checkpoint] = None,
):
if not torch_config:
torch_config = TorchConfig()
super(TorchTrainer, self).__init__(
train_loop_per_worker=train_loop_per_worker,
train_loop_config=train_loop_config,
backend_config=torch_config,
scaling_config=scaling_config,
dataset_config=dataset_config,
run_config=run_config,
datasets=datasets,
resume_from_checkpoint=resume_from_checkpoint,
metadata=metadata,
)
+793
View File
@@ -0,0 +1,793 @@
import collections
import logging
import os
import random
import types
from typing import Any, Callable, Dict, List, Optional, Union
import numpy as np
import torch
from packaging.version import Version
from torch.cuda.amp import GradScaler, autocast
from torch.nn.parallel import DistributedDataParallel
from torch.optim import Optimizer
from torch.utils.data import (
DataLoader,
DistributedSampler,
IterableDataset,
RandomSampler,
SequentialSampler,
)
from ray._common.usage.usage_lib import TagKey, record_extra_usage_tag
from ray.air._internal.device_manager import (
get_torch_device_manager_by_context,
get_torch_device_manager_by_device_type,
)
from ray.train._internal import session
from ray.train._internal.accelerator import Accelerator
from ray.train._internal.session import get_accelerator, set_accelerator
from ray.train.utils import _log_deprecation_warning
from ray.util.annotations import Deprecated, PublicAPI
logger = logging.getLogger(__name__)
@PublicAPI(stability="stable")
def get_device() -> torch.device:
"""Gets the correct torch device configured for this process.
Returns the torch device for the current worker. If more than 1 GPU is
requested per worker, returns the device with the minimal device index.
.. note::
If you requested multiple GPUs per worker, and want to get
the full list of torch devices, please use
:meth:`~ray.train.torch.get_devices`.
Assumes that `CUDA_VISIBLE_DEVICES` is set and is a
superset of the `ray.get_gpu_ids()`.
Examples:
Example: Launched 2 workers on the current node, each with 1 GPU
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] = "2,3"
ray.get_gpu_ids() == [2]
torch.cuda.is_available() == True
get_device() == torch.device("cuda:0")
Example: Launched 4 workers on the current node, each with 1 GPU
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3"
ray.get_gpu_ids() == [2]
torch.cuda.is_available() == True
get_device() == torch.device("cuda:2")
Example: Launched 2 workers on the current node, each with 2 GPUs
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3"
ray.get_gpu_ids() == [2,3]
torch.cuda.is_available() == True
get_device() == torch.device("cuda:2")
You can move a model to device by:
.. testcode::
:skipif: True
model.to(ray.train.torch.get_device())
Instead of manually checking the device type:
.. testcode::
:skipif: True
model.to("cuda" if torch.cuda.is_available() else "cpu")
Returns:
The torch device for the current worker.
"""
from ray.air._internal import torch_utils
record_extra_usage_tag(TagKey.TRAIN_TORCH_GET_DEVICE, "1")
return torch_utils.get_devices()[0]
@PublicAPI(stability="beta")
def get_devices() -> List[torch.device]:
"""Gets the correct torch device list configured for this process.
Assumes that `CUDA_VISIBLE_DEVICES` is set and is a
superset of the `ray.get_gpu_ids()`.
Examples:
Example: Launched 2 workers on the current node, each with 1 GPU
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] == "2,3"
ray.get_gpu_ids() == [2]
torch.cuda.is_available() == True
get_devices() == [torch.device("cuda:0")]
Example: Launched 4 workers on the current node, each with 1 GPU
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] == "0,1,2,3"
ray.get_gpu_ids() == [2]
torch.cuda.is_available() == True
get_devices() == [torch.device("cuda:2")]
Example: Launched 2 workers on the current node, each with 2 GPUs
.. testcode::
:skipif: True
os.environ["CUDA_VISIBLE_DEVICES"] == "0,1,2,3"
ray.get_gpu_ids() == [2,3]
torch.cuda.is_available() == True
get_devices() == [torch.device("cuda:2"), torch.device("cuda:3")]
Returns:
A list of torch devices for the current worker.
"""
from ray.air._internal import torch_utils
record_extra_usage_tag(TagKey.TRAIN_TORCH_GET_DEVICES, "1")
return torch_utils.get_devices()
@PublicAPI(stability="stable")
def prepare_model(
model: torch.nn.Module,
move_to_device: Union[bool, torch.device] = True,
parallel_strategy: Optional[str] = "ddp",
parallel_strategy_kwargs: Optional[Dict[str, Any]] = None,
) -> torch.nn.Module:
"""Prepares the model for distributed execution.
This allows you to use the same exact code regardless of number of
workers or the device type being used (CPU, GPU).
Args:
model: A torch model to prepare.
move_to_device: Either a boolean indiciating whether to move
the model to the correct device or an actual device to
move the model to. If set to False, the model needs
to manually be moved to the correct device.
parallel_strategy: Whether to wrap models in
``DistributedDataParallel``, ``FullyShardedDataParallel``,
or neither. Must be one of ``"ddp"``, ``"fsdp"``, or ``None``.
parallel_strategy_kwargs: Args to pass into
``DistributedDataParallel`` or ``FullyShardedDataParallel``
initialization if ``parallel_strategy`` is set to "ddp"
or "fsdp", respectively.
Returns:
The prepared model, wrapped according to ``parallel_strategy``.
"""
if parallel_strategy == "fsdp" and Version(torch.__version__) < Version("1.11.0"):
raise ImportError(
"FullyShardedDataParallel requires torch>=1.11.0. "
"Run `pip install 'torch>=1.11.0'` to use FullyShardedDataParallel."
)
record_extra_usage_tag(TagKey.TRAIN_TORCH_PREPARE_MODEL, "1")
return get_accelerator(_TorchAccelerator).prepare_model(
model,
move_to_device=move_to_device,
parallel_strategy=parallel_strategy,
parallel_strategy_kwargs=parallel_strategy_kwargs,
)
@PublicAPI(stability="stable")
def prepare_data_loader(
data_loader: torch.utils.data.DataLoader,
add_dist_sampler: bool = True,
move_to_device: bool = True,
auto_transfer: bool = True,
) -> torch.utils.data.DataLoader:
"""Prepares :class:`~torch.utils.data.DataLoader` for distributed execution.
This allows you to use the same exact code regardless of number of
workers or the device type being used (CPU, GPU).
.. note::
This method adds a `DistributedSampler` to the `DataLoader` if the
number of training workers is greater than 1. If shuffling is
enabled on the original `DataLoader`, then `shuffle=True` will also
be passed into the `DistributedSampler` constructor. `shuffle=False`
on the original `DataLoader` also means that shuffling is disabled
on the sampler.
With more than 1 worker, calling the `DistributedSampler.set_epoch` method
at the beginning of each epoch before creating the DataLoader iterator
is necessary to make shuffling work properly across multiple epochs.
Otherwise, the same ordering will be always used.
See: https://pytorch.org/docs/stable/data.html#torch.utils.data.distributed.DistributedSampler # noqa: E501
Example:
.. testcode:
:skipif: True
import torch
import ray.train.torch
train_dataloader = torch.utils.data.DataLoader(
..., batch_size=..., shuffle=True
)
train_dataloader = ray.train.torch.prepare_data_loader(train_loader)
for epoch in range(10):
if ray.train.get_context().get_world_size() > 1:
# Required for the distributed sampler to shuffle properly across epochs
train_dataloader.sampler.set_epoch(epoch)
for X, y in train_loader:
# No need to move data to GPU, this is done by `prepare_data_loader`!
# X, y = X.to("cuda"), y.to("cuda")
...
Args:
data_loader: The DataLoader to prepare.
add_dist_sampler: Whether to add a DistributedSampler to
the provided DataLoader.
move_to_device: If set, automatically move the data
returned by the data loader to the correct device.
auto_transfer: If set and device is GPU, another CUDA stream
is created to automatically copy data from host (CPU) memory
to device (GPU) memory (the default CUDA stream still runs the
training procedure). If device is CPU, it will be disabled
regardless of the setting. This configuration will be ignored
if ``move_to_device`` is False.
Returns:
The prepared DataLoader.
"""
record_extra_usage_tag(TagKey.TRAIN_TORCH_PREPARE_DATALOADER, "1")
return get_accelerator(_TorchAccelerator).prepare_data_loader(
data_loader,
add_dist_sampler=add_dist_sampler,
move_to_device=move_to_device,
auto_transfer=auto_transfer,
)
def _log_amp_deprecation_warning():
# Keep V2 imports out of top-level V1 imports.
from ray.train.v2.torch.train_loop_utils import _TORCH_AMP_DEPRECATION_MESSAGE
_log_deprecation_warning(_TORCH_AMP_DEPRECATION_MESSAGE)
@Deprecated
def accelerate(amp: bool = False) -> None:
"""[Deprecated] Enables training optimizations.
Arguments:
amp: If true, perform training with automatic mixed precision.
Otherwise, use full precision.
.. warning:: ``train.torch.accelerate`` cannot be called more than once, and it
must be called before any other ``train.torch`` utility function.
"""
_log_amp_deprecation_warning()
try:
set_accelerator(_TorchAccelerator(amp=amp))
except RuntimeError:
raise RuntimeError(
"An accelerator has already been set. Make sure "
"`train.torch.accelerate()` is not called multiple times, and is called "
"before any of the prepare methods."
)
@Deprecated
def prepare_optimizer(optimizer: torch.optim.Optimizer) -> torch.optim.Optimizer:
"""[Deprecated] Wraps optimizer to support automatic mixed precision.
Args:
optimizer: The DataLoader to prepare.
Returns:
A wrapped optimizer.
"""
_log_amp_deprecation_warning()
return get_accelerator(_TorchAccelerator).prepare_optimizer(optimizer)
@Deprecated
def backward(tensor: torch.Tensor) -> None:
"""[Deprecated] Computes the gradient of the specified tensor w.r.t. graph leaves.
Args:
tensor: Tensor of which the derivative will be computed.
"""
_log_amp_deprecation_warning()
get_accelerator(_TorchAccelerator).backward(tensor)
@PublicAPI(stability="stable")
def enable_reproducibility(seed: int = 0) -> None:
"""Limits sources of nondeterministic behavior.
This function:
* Seeds PyTorch, Python, and NumPy.
* Disables CUDA convolution benchmarking.
* Configures PyTorch to use determinstic algorithms.
* Seeds workers spawned for multi-process data loading.
Args:
seed: The number to seed libraries and data workers with.
.. warning:: ``train.torch.enable_reproducibility()`` can't guarantee
completely reproducible results across executions. To learn more, read
the `PyTorch notes on randomness
<https://pytorch.org/docs/stable/notes/randomness.html>`_.
"""
get_accelerator(_TorchAccelerator).enable_reproducibility(seed)
@Deprecated
class TorchWorkerProfiler:
"""Utility class for running PyTorch Profiler on a Train worker.
Args:
trace_dir: The directory to store traces on the worker node.
If ``None``, this will use a default temporary dir.
"""
WORKER_TRACE_DIR_NAME = "pytorch_profiler_worker_traces"
def __init__(self, trace_dir: Optional[str] = None):
raise DeprecationWarning(
"The `ray.train.torch.TorchWorkerProfiler` API is deprecated in Ray 2.0.",
)
class _TorchAccelerator(Accelerator):
"""A utility that implements methods to accelerate PyTorch training.
Arguments:
amp: If true, perform training with automatic mixed precision.
Otherwise, use full precision.
"""
def __init__(self, amp: bool = False):
self.amp_is_enabled = amp
self.scaler = GradScaler() if amp else None
self._seed = None
self.device_manager = get_torch_device_manager_by_context()
def prepare_model(
self,
model: torch.nn.Module,
move_to_device: bool = True,
parallel_strategy: Optional[str] = "ddp",
parallel_strategy_kwargs: Optional[Dict[str, Any]] = None,
) -> torch.nn.Module:
"""Prepares the model for distributed execution.
This allows you to use the same exact code regardless of number of
workers or the device type being used (CPU, GPU).
Args:
model: A torch model to prepare.
move_to_device: Whether to move the model to the correct
device. If set to False, the model needs to manually be moved
to the correct device.
parallel_strategy: Whether to wrap models in
``DistributedDataParallel``, ``FullyShardedDataParallel`` (
Experimental), or neither. Must be one of ``"ddp"``, ``"fsdp"``,
or ``None``.
parallel_strategy_kwargs: Args to pass into
``DistributedDataParallel`` or ``FullyShardedDataParallel``
initialization if ``parallel_strategy`` is set to "ddp"
or "fsdp", respectively.
Returns:
The prepared model, wrapped according to ``parallel_strategy``.
"""
parallel_strategy_kwargs = parallel_strategy_kwargs or {}
rank = session.get_local_rank()
if isinstance(move_to_device, torch.device):
device = move_to_device
else:
device = get_device()
if isinstance(device, list):
device = device[0]
if self.device_manager.is_available():
self.device_manager.set_device(device)
if move_to_device:
if rank == 0:
logger.info(f"Moving model to device: {device}")
else:
logger.debug(f"Moving model to device: {device}")
model = model.to(device)
def model_get_state(self):
# `__getstate__` is an special method that informs pickle which attributes
# to serialize. This custom implementation ensures that the wrapped forward
# method and custom `__getstate__` method aren't serialized.
if hasattr(self, "_original_get_state"):
state = self._original_get_state()
state["__getstate__"] = state["_original_get_state"]
del state["_original_get_state"]
else:
# If model does not have a `__getstate__` already defined, use default
# implementation.
state = self.__dict__.copy()
del state["__getstate__"]
state["forward"] = state["_unwrapped_forward"]
del state["_unwrapped_forward"]
return state
if self.amp_is_enabled:
# Pickle cannot serialize the wrapped forward method. As a workaround,
# define a custom `__getstate__` method that unwraps the forward method.
model._unwrapped_forward = model.forward
model.forward = autocast()(model.forward)
# TODO(amogkam): Replace below logic with a generic "unpack model" method.
# Replacing the `model.forward` method makes the model no longer
# serializable. When serializing the model, we have to override the
# `__getstate__` method to set back the original forward method.
if hasattr(model, "__getstate__"):
model._original_get_state = model.__getstate__
# `__getstate__` must be a bound method rather than an callable attribute.
# See https://stackoverflow.com/questions/972/adding-a-method-to-an-existing-object-instance. # noqa: E501
model.__getstate__ = types.MethodType(model_get_state, model)
world_size = session.get_world_size()
if parallel_strategy and world_size > 1:
if parallel_strategy == "ddp":
DataParallel = DistributedDataParallel
if self.device_manager.is_available() and device.type != "cpu":
parallel_strategy_kwargs = {
"device_ids": [device],
"output_device": device,
**parallel_strategy_kwargs,
}
else:
if not torch.cuda.is_available():
raise RuntimeError(
"FSDP is only available with GPU-enabled "
"training. Set "
"`use_gpu=True` in your Trainer to train with "
"GPUs."
)
from torch.distributed.fsdp import FullyShardedDataParallel
DataParallel = FullyShardedDataParallel
if rank == 0:
logger.info(f"Wrapping provided model in {DataParallel.__name__}.")
else:
logger.debug(f"Wrapping provided model in {DataParallel.__name__}.")
model = DataParallel(model, **parallel_strategy_kwargs)
return model
def prepare_data_loader(
self,
data_loader: torch.utils.data.DataLoader,
add_dist_sampler: bool = True,
move_to_device: bool = True,
auto_transfer: bool = False,
) -> torch.utils.data.DataLoader:
"""Prepares DataLoader for distributed execution.
This allows you to use the same exact code regardless of number of
workers or the device type being used (CPU, GPU).
Args:
data_loader: The DataLoader to prepare.
add_dist_sampler: Whether to add a DistributedSampler to
the provided DataLoader.
move_to_device: If set, automatically move the data
returned by the data loader to the correct device.
auto_transfer: (Experimental) If set and device is GPU, another CUDA stream
is created to automatically copy data from host (CPU) memory
to device (GPU) memory (the default CUDA stream still runs the
training procedure). If device is CPU, it will be disabled
regardless of the setting. This configuration will be ignored
if ``move_to_device`` is False.
Returns:
The prepared DataLoader.
"""
world_size = session.get_world_size()
world_rank = session.get_world_rank()
# Only add Distributed Sampler if the following conditions hold:
# 1. More than one training worker is being used.
# 2. A DistributedSampler has not already been added by the user.
# 3. The dataset is not an IterableDataset. Samplers do not worker with
# IterableDatasets.
if (
world_size > 1
and not isinstance(data_loader.sampler, DistributedSampler)
and not (
hasattr(data_loader, "dataset")
and isinstance(data_loader.dataset, IterableDataset)
)
and add_dist_sampler
):
def with_sampler(loader):
# Automatically set the DistributedSampler
# If you're using a sampler, the DataLoader shuffle flag must be set to
# False. Shuffling is instead determined by the shuffle argument passed
# to the DistributedSampler constructor.
# If no sampler is passed to the DataLoader constructor, Torch
# constructs a default sampler. The default sampler is a RandomSampler
# if shuffling is enabled and a SequentialSampler otherwise. DataLoader
# does not have a shuffle attribute, so we instead identify whether
# shuffling is enabled by checking the default sampler type.
shuffle = not isinstance(loader.sampler, SequentialSampler)
def seeded_worker_init_fn(
worker_init_fn: Optional[Callable[[int], None]],
):
def wrapper(worker_id: int):
worker_seed = torch.initial_seed() % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
if worker_init_fn:
worker_init_fn(worker_id)
return wrapper
worker_init_fn: Optional[Callable[[int], None]] = loader.worker_init_fn
generator: Optional[torch.Generator] = loader.generator
if self._seed is not None:
worker_init_fn = seeded_worker_init_fn(worker_init_fn)
generator = torch.Generator()
generator.manual_seed(self._seed)
using_default_sampler = isinstance(
loader.sampler, (SequentialSampler, RandomSampler)
)
if not using_default_sampler and world_rank == 0:
logger.warning(
f"The {loader.sampler.__class__.__name__} will be overwritten "
"with a DistributedSampler. You can disable this by setting "
"`with_sampler` to False in `prepare_data_loader`."
)
data_loader_args = {
"dataset": loader.dataset,
"batch_size": loader.batch_size,
"shuffle": False,
"num_workers": loader.num_workers,
"collate_fn": loader.collate_fn,
"pin_memory": loader.pin_memory,
"drop_last": loader.drop_last,
"timeout": loader.timeout,
"worker_init_fn": worker_init_fn,
"generator": generator,
"sampler": DistributedSampler(loader.dataset, shuffle=shuffle),
}
return DataLoader(**data_loader_args)
data_loader = with_sampler(data_loader)
if move_to_device:
device = get_device()
data_loader = _WrappedDataLoader(data_loader, device, auto_transfer)
return data_loader
def prepare_optimizer(self, optimizer: Optimizer) -> Optimizer:
"""Wraps optimizer to support automatic mixed precision.
Args:
optimizer: The DataLoader to prepare.
Returns:
A wrapped optimizer.
"""
return _WrappedOptimizer(optimizer, scaler=self.scaler)
def backward(self, tensor: torch.Tensor) -> None:
"""Computes the gradient of the specified tensor w.r.t. graph leaves.
Args:
tensor: Tensor of which the derivative will be computed.
"""
if self.amp_is_enabled:
self.scaler.scale(tensor).backward()
else:
tensor.backward()
def enable_reproducibility(self, seed: int = 0) -> None:
"""Limits sources of nondeterministic behavior."""
self._seed = seed
torch.manual_seed(seed)
random.seed(seed)
np.random.seed(seed)
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.benchmark = False
# If you want to use deterministic algorithms with CUDA, then you need to set
# the CUBLAS_WORKSPACE_CONFIG environment variable; otherwise, Torch errors.
# See https://docs.nvidia.com/cuda/cublas/index.html#cublasApi_reproducibility.
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
class _WrappedDataLoader(DataLoader):
def __init__(
self, base_dataloader: DataLoader, device: torch.device, auto_transfer: bool
):
self.__dict__.update(getattr(base_dataloader, "__dict__", {}))
self._dataloader = base_dataloader
self.dataloader_iter = None
self.device = device
self.device_manager = get_torch_device_manager_by_device_type(device.type)
# disable auto transfer (host->device) if cpu is used
if device.type != "cpu" and self.device_manager.supports_stream():
self._auto_transfer = auto_transfer
else:
self._auto_transfer = False
# create a new device stream to move data from host to device concurrently
self._memcpy_stream = (
self.device_manager.create_stream(device)
if device.type != "cpu" and self._auto_transfer
else None
)
self.next_batch = None
def _move_to_device(self, item):
if item is None:
return None
def try_move_device(i):
try:
i = i.to(self.device, non_blocking=self._auto_transfer)
except AttributeError:
logger.debug(f"Item {i} cannot be moved to device " f"{self.device}.")
return i
with self.device_manager.get_stream_context(self._memcpy_stream):
if isinstance(item, collections.abc.Mapping):
item_on_device = {k: self._move_to_device(v) for k, v in item.items()}
elif isinstance(item, tuple):
item_on_device = tuple(self._move_to_device(i) for i in item)
elif isinstance(item, list):
item_on_device = [self._move_to_device(i) for i in item]
elif isinstance(item, torch.Tensor):
item_on_device = try_move_device(item)
else:
logger.debug(
f"Data type {type(item)} doesn't support being moved to device."
)
item_on_device = item
return item_on_device
def _wait_for_batch(self, item):
if self._memcpy_stream is None:
return
# Reference:
# https://pytorch.org/docs/stable/generated/torch.Tensor.record_stream.html
# The training stream (current) needs to wait until
# the memory copy stream finishes.
curr_stream = self.device_manager.get_current_stream()
curr_stream.wait_stream(self._memcpy_stream)
# When a tensor is used by CUDA streams different from
# its original allocator, we need to call ``record_stream``
# to inform the allocator of all these streams. Otherwise,
# the tensor might be freed once it is no longer used by
# the creator stream.
for i in item:
# The Pytorch DataLoader has no restrictions on what is outputted for
# each batch. We should only ``record_stream`` if the item has the
# ability to do so.
try:
i.record_stream(curr_stream)
except AttributeError:
pass
def __len__(self):
return len(self._dataloader)
def _prefetch_next_batch(self):
next_batch = next(self.dataloader_iter, None)
self.next_batch = self._move_to_device(next_batch)
def __iter__(self):
self.dataloader_iter = iter(self._dataloader)
self._prefetch_next_batch()
return self
def __next__(self):
next_batch = self.next_batch
if next_batch is None:
raise StopIteration
self._wait_for_batch(next_batch)
self._prefetch_next_batch()
return next_batch
class _WrappedOptimizer(Optimizer):
def __init__(self, optimizer: Optimizer, scaler: Optional[GradScaler] = None):
self.optimizer = optimizer
self.scaler = scaler
@property
def state(self):
return self.optimizer.state
@state.setter
def state(self, state):
self.optimizer.state = state
@property
def param_groups(self):
return self.optimizer.param_groups
@param_groups.setter
def param_groups(self, param_groups):
self.optimizer.param_groups = param_groups
@property
def defaults(self):
return self.optimizer.defaults
@defaults.setter
def defaults(self, defaults):
self.optimizer.defaults = defaults
def add_param_group(self, param_group):
self.optimizer.add_param_group(param_group)
def load_state_dict(self, state_dict):
self.optimizer.load_state_dict(state_dict)
def state_dict(self):
return self.optimizer.state_dict()
def zero_grad(self):
self.optimizer.zero_grad()
def step(self, closure=None):
if self.scaler is not None:
self.scaler.step(self.optimizer, closure)
self.scaler.update()
else:
self.optimizer.step(closure)
+5
View File
@@ -0,0 +1,5 @@
from ray.train.torch.xla.config import TorchXLAConfig
__all__ = [
"TorchXLAConfig",
]
+171
View File
@@ -0,0 +1,171 @@
import logging
import os
import re
import shutil
import uuid
from dataclasses import dataclass
import ray
from ray.train._internal.base_worker_group import BaseWorkerGroup
from ray.train._internal.utils import get_address_and_port
from ray.train.backend import Backend
from ray.train.torch import TorchConfig
from ray.util import PublicAPI
logger = logging.getLogger(__name__)
@PublicAPI(stability="alpha")
@dataclass
class TorchXLAConfig(TorchConfig):
"""
Configuration for torch XLA setup.
See https://pytorch.org/xla/release/1.13/index.html for more info.
Currently, only "neuron_cores" accelerator (AwsNeuronXLABackend)
is supported with xrt runtime.
"""
neuron_parallel_compile: bool = False
@property
def backend_cls(self):
return _TorchAwsNeuronXLABackend
def _kill_xrt_server():
import subprocess
subprocess.call(["pkill", "-f", "xrt_run_server"])
def _set_xla_env_vars():
# https://pytorch.org/docs/1.13/elastic/run.html#environment-variables
context = ray.train.get_context()
os.environ["LOCAL_RANK"] = str(context.get_local_rank())
os.environ["RANK"] = str(context.get_world_rank())
os.environ["LOCAL_WORLD_SIZE"] = str(context.get_local_world_size())
os.environ["WORLD_SIZE"] = str(context.get_world_size())
os.environ["GROUP_RANK"] = str(context.get_node_rank())
os.environ["GROUP_WORLD_SIZE"] = str(
context.get_world_size() / context.get_local_world_size()
)
os.environ["ROLE_RANK"] = str(context.get_world_rank())
os.environ["ROLE_WORLD_RANK"] = str(context.get_world_rank())
os.environ["ROLE_WORLD_SIZE"] = str(context.get_world_size())
# EFA and XLA setup
# https://github.com/aws/libfabric/blob/master/prov/efa/src/rxr/rxr_init.c
# https://github.com/aws-neuron/aws-neuron-samples/blob/master/torch-neuronx/training/dp_bert_hf_pretrain/run_dp_bert_large_hf_pretrain_bf16_s128.sh # noqa
os.environ["FI_PROVIDER"] = "efa"
os.environ["FI_EFA_USE_DEVICE_RDMA"] = "1"
os.environ["FI_EFA_FORK_SAFE"] = "1"
os.environ["XLA_TRANSFER_SEED_ASYNC"] = "1"
os.environ["NCCL_ASYNC_ERROR_HANDLING"] = "1"
def _setup_xla_torch_process_group():
try:
import torch.distributed as dist
import torch_xla.core.xla_model as xm # noqa F401
import torch_xla.distributed.xla_backend # noqa F401
dist.init_process_group("xla")
except ImportError:
raise ImportError("torch_xla must be installed to use torch_xla backend.")
# The following env vars enable Neuron graph extraction for parallel compilation
# Note: model outputs are invalid and should be ignored while these env vars are set
def _set_neuron_parallel_compile_env_vars():
os.environ["NEURON_PARALLEL_COMPILE"] = "1"
os.environ["NEURON_EXTRACT_GRAPHS_ONLY"] = "1"
os.environ["NEURON_FALL_BACK_TO_NULL_NEFF"] = "1"
# Compile previously extracted Neuron graphs
def _neuron_compile_extracted_graphs():
try:
from libneuronxla.neuron_cc_cache import CacheUrl
from libneuronxla.neuron_parallel_compile import parallel_compile
except ImportError:
raise ImportError(
"libneuronxla must be installed to use Neuron parallel compilation."
)
# Only 1 worker per node should run parallel_compile()
if os.environ.get("LOCAL_RANK") == "0":
logger.info("Compiling extracted graphs on local rank0 worker")
parallel_compile_workdir = (
f"/tmp/{os.environ.get('USER','no-user')}/parallel_compile_workdir/"
)
if os.path.exists(parallel_compile_workdir):
shutil.rmtree(parallel_compile_workdir)
os.makedirs(parallel_compile_workdir, exist_ok=True)
# Users can set the cache directory using --cache_dir in NEURON_CC_FLAGS or by
# using NEURON_COMPILE_CACHE_URL. --cache_dir takes precedence.
explicit_cache_dir = None
if neuron_cc_flags := os.environ.get("NEURON_CC_FLAGS"):
if s := re.search(r"--cache_dir[= ](\S+)", neuron_cc_flags):
explicit_cache_dir = s.group(1)
parallel_compile(
parallel_compile_workdir,
CacheUrl.get_cache_url(explicit_cache_dir),
)
class _TorchAwsNeuronXLABackend(Backend):
unique_run_id: str = str(uuid.uuid4())
def on_start(self, worker_group: BaseWorkerGroup, backend_config: TorchXLAConfig):
"""Logic ran right before training is started."""
# On previous worker failure, we don't run graceful shutdown on workers.
# This would leak any running xrt server.
worker_group.execute(_kill_xrt_server)
# Get master address and port from the first worker.
master_addr, master_port = worker_group.execute_single(0, get_address_and_port)
def set_env_vars(addr, port):
os.environ["MASTER_ADDR"] = addr
os.environ["MASTER_PORT"] = str(port)
# To trigger the xrt server
os.environ["TORCHELASTIC_RUN_ID"] = self.unique_run_id
# Set the env vars on all workers.
worker_group.execute(set_env_vars, addr=master_addr, port=master_port)
# Set up env vars for neuron parallel compilation graph extraction
if backend_config.neuron_parallel_compile:
logger.info("Extracting graphs for Neuron parallel compilation")
worker_group.execute(_set_neuron_parallel_compile_env_vars)
def on_training_start(
self, worker_group: BaseWorkerGroup, backend_config: TorchXLAConfig
):
"""
Configure the environment variables for the worker group.
And initialize the xla distributed process group.
TODO: Current setup only supports homogenous cluster with
neuron_cores accelerator and xrt runtime.
"""
worker_group.execute(_set_xla_env_vars)
worker_group.execute(_setup_xla_torch_process_group)
def on_shutdown(
self, worker_group: BaseWorkerGroup, backend_config: TorchXLAConfig
):
"""
Logic ran right after training is finished.
This is a sanity cleanup to kill xrt server, and to optionally
run neuron parallel graph compilation
"""
worker_group.execute(_kill_xrt_server)
# Compile the extracted graphs. This must run at end of training.
if backend_config.neuron_parallel_compile:
worker_group.execute(_neuron_compile_extracted_graphs)