307 lines
9.8 KiB
Python
307 lines
9.8 KiB
Python
import contextlib
|
|
import os
|
|
import time
|
|
import uuid
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
import ray
|
|
import ray.train as train
|
|
from ray.cluster_utils import Cluster
|
|
from ray.train import RunConfig, ScalingConfig
|
|
from ray.train.examples.pytorch.torch_linear_example import (
|
|
train_func as linear_train_func,
|
|
)
|
|
from ray.train.torch import TorchCheckpoint, TorchConfig, TorchTrainer
|
|
from ray.train.trainer import TrainingFailedError
|
|
|
|
|
|
@pytest.fixture
|
|
def ray_start_4_cpus():
|
|
address_info = ray.init(num_cpus=4)
|
|
yield address_info
|
|
# The code after the yield will run as teardown code.
|
|
ray.shutdown()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def ray_start_2_node_cluster(num_cpus_per_node: int, num_gpus_per_node: int):
|
|
cluster = Cluster()
|
|
for _ in range(2):
|
|
cluster.add_node(num_cpus=num_cpus_per_node, num_gpus=num_gpus_per_node)
|
|
|
|
ray.init(address=cluster.address)
|
|
|
|
yield
|
|
|
|
ray.shutdown()
|
|
cluster.shutdown()
|
|
|
|
|
|
@pytest.mark.parametrize("num_workers", [1, 2])
|
|
def test_torch_linear(ray_start_4_cpus, num_workers):
|
|
def train_func(config):
|
|
result = linear_train_func(config)
|
|
assert len(result) == epochs
|
|
assert result[-1]["loss"] < result[0]["loss"]
|
|
|
|
num_workers = num_workers
|
|
epochs = 3
|
|
scaling_config = ScalingConfig(num_workers=num_workers)
|
|
config = {"lr": 1e-2, "hidden_size": 1, "batch_size": 4, "epochs": epochs}
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=train_func,
|
|
train_loop_config=config,
|
|
scaling_config=scaling_config,
|
|
)
|
|
trainer.fit()
|
|
|
|
|
|
@pytest.mark.parametrize("prepare_model", (True, False))
|
|
def test_torch_e2e(ray_start_4_cpus, prepare_model):
|
|
def train_func():
|
|
model = torch.nn.Linear(3, 1)
|
|
if prepare_model:
|
|
model = train.torch.prepare_model(model)
|
|
train.report({}, checkpoint=TorchCheckpoint.from_model(model))
|
|
|
|
scaling_config = ScalingConfig(num_workers=2)
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=train_func,
|
|
scaling_config=scaling_config,
|
|
)
|
|
trainer.fit()
|
|
|
|
|
|
@pytest.mark.parametrize("prepare_model", (True, False))
|
|
def test_torch_e2e_state_dict(ray_start_4_cpus, prepare_model):
|
|
def train_func():
|
|
model = torch.nn.Linear(3, 1)
|
|
if prepare_model:
|
|
model = train.torch.prepare_model(model)
|
|
train.report({}, checkpoint=TorchCheckpoint.from_state_dict(model.state_dict()))
|
|
|
|
scaling_config = ScalingConfig(num_workers=2)
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=train_func,
|
|
scaling_config=scaling_config,
|
|
)
|
|
result = trainer.fit()
|
|
|
|
# If loading from a state dict, a model definition must be passed in.
|
|
with pytest.raises(ValueError):
|
|
torch_checkpoint = TorchCheckpoint(
|
|
path=result.checkpoint.path, filesystem=result.checkpoint.filesystem
|
|
)
|
|
torch_checkpoint.get_model()
|
|
|
|
|
|
def test_checkpoint_freq(ray_start_4_cpus):
|
|
# checkpoint_freq is not supported so raise an error
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=lambda config: None,
|
|
scaling_config=train.ScalingConfig(num_workers=1),
|
|
run_config=train.RunConfig(
|
|
checkpoint_config=train.CheckpointConfig(
|
|
checkpoint_frequency=2,
|
|
),
|
|
),
|
|
)
|
|
with pytest.raises(ValueError):
|
|
trainer.fit()
|
|
|
|
|
|
def test_torch_session_errors(ray_start_4_cpus):
|
|
"""Test fail-fast behavior when reporting dicts with Torch tensors"""
|
|
|
|
def train_func():
|
|
model = torch.nn.Linear(1, 1).state_dict()
|
|
with pytest.raises(ValueError):
|
|
train.report(model)
|
|
|
|
scaling_config = ScalingConfig(num_workers=2)
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=train_func,
|
|
scaling_config=scaling_config,
|
|
)
|
|
trainer.fit()
|
|
|
|
|
|
def test_single_worker_failure(ray_start_4_cpus):
|
|
"""Tests if training fails upon any worker failure."""
|
|
|
|
def single_worker_fail():
|
|
if train.get_context().get_world_rank() == 0:
|
|
raise RuntimeError
|
|
else:
|
|
time.sleep(1000000)
|
|
|
|
scaling_config = ScalingConfig(num_workers=2)
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=single_worker_fail,
|
|
scaling_config=scaling_config,
|
|
)
|
|
with pytest.raises(TrainingFailedError) as exc_info:
|
|
trainer.fit()
|
|
assert isinstance(exc_info.value.__cause__, RuntimeError)
|
|
|
|
|
|
@pytest.mark.parametrize("num_gpus_per_worker", [0.5, 1, 2])
|
|
def test_tune_torch_get_device_gpu(num_gpus_per_worker):
|
|
"""Tests if GPU ids are set correctly when running train concurrently in nested actors
|
|
(for example when used with Tune).
|
|
"""
|
|
from ray.train import ScalingConfig
|
|
|
|
num_samples = 2
|
|
num_workers = 2
|
|
|
|
# We should have exactly enough resources in the cluster to run both samples
|
|
# concurrently.
|
|
total_gpus_required = num_workers * num_gpus_per_worker * num_samples
|
|
# Divide by two because of a 2 node cluster.
|
|
gpus_per_node = total_gpus_required // 2
|
|
|
|
exception = None
|
|
# Use the same number of cpus per node as gpus per node.
|
|
with ray_start_2_node_cluster(
|
|
num_cpus_per_node=gpus_per_node, num_gpus_per_node=gpus_per_node
|
|
):
|
|
|
|
@patch("torch.cuda.is_available", lambda: True)
|
|
def train_fn():
|
|
# We use STRICT_SPREAD strategy to force multiple samples on the same node.
|
|
# For single or fractional GPU case, each worker has only 1 visible device (
|
|
# the other is taken by the other sample) so device index should be 0.
|
|
# For the multiple GPU case, each worker has 2 visible devices so device
|
|
# index should be either 0 or 1. It doesn't matter which.
|
|
device_ids = sorted([device.index for device in train.torch.get_devices()])
|
|
assert device_ids in [[0], [0, 1]]
|
|
|
|
@ray.remote(num_cpus=0)
|
|
class TrialActor:
|
|
def __init__(self, warmup_steps):
|
|
self.trainer = TorchTrainer(
|
|
train_fn,
|
|
torch_config=TorchConfig(backend="gloo"),
|
|
run_config=RunConfig(
|
|
# Use a unique name to avoid using the same
|
|
# experiment directory
|
|
name=f"test_tune_torch_get_device_gpu_{uuid.uuid4()}"
|
|
),
|
|
scaling_config=ScalingConfig(
|
|
num_workers=num_workers,
|
|
use_gpu=True,
|
|
resources_per_worker={"CPU": 1, "GPU": num_gpus_per_worker},
|
|
# Need to specify 0 trainer resources so STRICT_SPREAD
|
|
# will work.
|
|
trainer_resources={"CPU": 0},
|
|
placement_strategy="STRICT_SPREAD",
|
|
# Each gpu worker will be spread onto separate nodes. This
|
|
# forces different samples to run concurrently on the same
|
|
# node.
|
|
),
|
|
)
|
|
|
|
def run(self):
|
|
return self.trainer.fit()
|
|
|
|
try:
|
|
actors = [TrialActor.remote(1) for _ in range(num_samples)]
|
|
ray.get([actor.run.remote() for actor in actors])
|
|
except Exception as exc:
|
|
exception = exc
|
|
|
|
# Raise exception after Ray cluster has been shutdown to avoid corrupted state
|
|
if exception:
|
|
raise exception
|
|
|
|
|
|
def test_torch_amp(ray_start_4_cpus):
|
|
def train_fn():
|
|
train.torch.accelerate(amp=True)
|
|
model = torch.nn.Linear(1, 1)
|
|
model = train.torch.prepare_model(model)
|
|
|
|
train.report({}, checkpoint=TorchCheckpoint.from_model(model))
|
|
|
|
trainer = TorchTrainer(
|
|
train_fn,
|
|
scaling_config=ScalingConfig(num_workers=2),
|
|
)
|
|
results = trainer.fit()
|
|
assert results.checkpoint
|
|
|
|
|
|
def test_torch_amp_with_custom_get_state(ray_start_4_cpus):
|
|
"""Tests amp with a model that has a custom __getstate__ method defined.
|
|
|
|
See https://discuss.ray.io/t/ray-train-hangs-for-long-time/6333/7
|
|
"""
|
|
|
|
def train_fn():
|
|
train.torch.accelerate(amp=True)
|
|
|
|
class CustomLinear(torch.nn.Linear):
|
|
def __getstate__(self):
|
|
return self.__dict__.copy()
|
|
|
|
model = CustomLinear(1, 1)
|
|
model = train.torch.prepare_model(model)
|
|
|
|
# TorchCheckpoint.from_model fails, so just save the state dict only.
|
|
train.report(
|
|
{}, checkpoint=TorchCheckpoint.from_state_dict(model.module.state_dict())
|
|
)
|
|
|
|
trainer = TorchTrainer(
|
|
train_fn,
|
|
scaling_config=ScalingConfig(num_workers=2),
|
|
)
|
|
results = trainer.fit()
|
|
assert results.checkpoint
|
|
|
|
|
|
def test_torch_env_vars(ray_start_4_cpus):
|
|
"""Check that env vars are set as expected."""
|
|
|
|
def train_func(config):
|
|
context = train.get_context()
|
|
assert os.environ["LOCAL_RANK"] == str(context.get_local_rank())
|
|
assert os.environ["RANK"] == str(context.get_world_rank())
|
|
assert os.environ["LOCAL_WORLD_SIZE"] == str(context.get_local_world_size())
|
|
assert os.environ["WORLD_SIZE"] == str(context.get_world_size())
|
|
assert os.environ["NODE_RANK"] == str(context.get_node_rank())
|
|
|
|
assert os.environ["ACCELERATE_TORCH_DEVICE"] == str(train.torch.get_device())
|
|
|
|
num_workers = 1
|
|
scaling_config = ScalingConfig(num_workers=num_workers)
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=train_func,
|
|
scaling_config=scaling_config,
|
|
)
|
|
trainer.fit()
|
|
|
|
|
|
def test_nonserializable_train_function(ray_start_4_cpus):
|
|
import threading
|
|
|
|
lock = threading.Lock()
|
|
|
|
def train_func():
|
|
print(lock)
|
|
|
|
trainer = TorchTrainer(train_func)
|
|
# Check that the `inspect_serializability` trace was printed
|
|
with pytest.raises(TypeError, match=r".*was found to be non-serializable.*"):
|
|
trainer.fit()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", "-x", __file__]))
|