178 lines
5.9 KiB
Python
178 lines
5.9 KiB
Python
import os
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
from ray.train import CheckpointConfig, FailureConfig, RunConfig, ScalingConfig
|
|
from ray.train.lightning import (
|
|
RayDDPStrategy,
|
|
RayFSDPStrategy,
|
|
RayLightningEnvironment,
|
|
RayTrainReportCallback,
|
|
)
|
|
from ray.train.lightning._lightning_utils import import_lightning
|
|
from ray.train.tests.lightning_test_utils import DummyDataModule, LinearModule
|
|
from ray.train.torch import TorchTrainer
|
|
from ray.train.v2._internal.constants import HEALTH_CHECK_INTERVAL_S_ENV_VAR
|
|
from ray.train.v2.api.report_config import CheckpointUploadMode
|
|
from ray.train.v2.api.validation_config import ValidationConfig, ValidationTaskConfig
|
|
|
|
pl = import_lightning()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reduce_health_check_interval(monkeypatch):
|
|
monkeypatch.setenv(HEALTH_CHECK_INTERVAL_S_ENV_VAR, "0.2")
|
|
yield
|
|
|
|
|
|
@pytest.mark.parametrize("strategy_name", ["ddp", "fsdp"])
|
|
@pytest.mark.parametrize("accelerator", ["cpu"])
|
|
# @pytest.mark.parametrize("accelerator", ["cpu", "gpu"]) # TODO: Enable GPU test
|
|
@pytest.mark.parametrize("datasource", ["dataloader", "datamodule"])
|
|
def test_trainer_with_native_dataloader(
|
|
ray_start_4_cpus, strategy_name, accelerator, datasource
|
|
):
|
|
"""Test basic ddp and fsdp training with dataloader and datamodule."""
|
|
|
|
if accelerator == "cpu" and strategy_name == "fsdp":
|
|
return
|
|
|
|
num_workers = 2
|
|
num_epochs = 1
|
|
batch_size = 8
|
|
dataset_size = 256
|
|
|
|
strategy_map = {"ddp": RayDDPStrategy(), "fsdp": RayFSDPStrategy()}
|
|
|
|
def train_loop():
|
|
model = LinearModule(input_dim=32, output_dim=4, strategy=strategy_name)
|
|
|
|
strategy = strategy_map[strategy_name]
|
|
|
|
trainer = pl.Trainer(
|
|
max_epochs=num_epochs,
|
|
devices="auto",
|
|
accelerator=accelerator,
|
|
strategy=strategy,
|
|
plugins=[RayLightningEnvironment()],
|
|
callbacks=[RayTrainReportCallback()],
|
|
)
|
|
|
|
datamodule = DummyDataModule(batch_size, dataset_size)
|
|
|
|
if datasource == "dataloader":
|
|
trainer.fit(
|
|
model,
|
|
train_dataloaders=datamodule.train_dataloader(),
|
|
val_dataloaders=datamodule.val_dataloader(),
|
|
)
|
|
if datasource == "datamodule":
|
|
trainer.fit(model, datamodule=datamodule)
|
|
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=train_loop,
|
|
scaling_config=ScalingConfig(num_workers=2, use_gpu=(accelerator == "gpu")),
|
|
)
|
|
|
|
results = trainer.fit()
|
|
assert results.metrics["epoch"] == num_epochs - 1
|
|
assert (
|
|
results.metrics["step"] == num_epochs * dataset_size / num_workers / batch_size
|
|
)
|
|
assert "loss" in results.metrics
|
|
assert "val_loss" in results.metrics
|
|
|
|
|
|
def test_async_checkpointing_and_validation(ray_start_4_cpus, tmp_path):
|
|
"""Test lightning training with async checkpointing and validation."""
|
|
|
|
num_workers = 2
|
|
num_epochs = 2
|
|
batch_size = 8
|
|
dataset_size = 256
|
|
|
|
@ray.remote
|
|
class TmpdirPrefixActor:
|
|
def __init__(self):
|
|
self.tmpdir_prefixes = []
|
|
|
|
def set_tmpdir_prefix(self, tmpdir_prefix):
|
|
self.tmpdir_prefixes.append(tmpdir_prefix)
|
|
|
|
def get_tmpdir_prefixes(self):
|
|
return self.tmpdir_prefixes
|
|
|
|
tmpdir_prefix_actor = TmpdirPrefixActor.remote()
|
|
|
|
def validation_fn(checkpoint):
|
|
assert checkpoint.path is not None
|
|
checkpoint_file = checkpoint.path + "/checkpoint.ckpt"
|
|
assert os.path.exists(
|
|
checkpoint_file
|
|
), f"Checkpoint file not found: {checkpoint_file}"
|
|
return {"val_score": 1}
|
|
|
|
def train_loop():
|
|
checkpoint = ray.train.get_checkpoint()
|
|
model = LinearModule(input_dim=32, output_dim=4, strategy="ddp", fail_epoch=1)
|
|
callback = RayTrainReportCallback(
|
|
checkpoint_upload_mode=CheckpointUploadMode.ASYNC,
|
|
validation=ValidationTaskConfig(fn_kwargs={}),
|
|
)
|
|
# Only track tmpdirs from the post-resume attempt.
|
|
# TODO: fix bug where async checkpoint upload does not clean up tmpdir if worker fails.
|
|
if checkpoint is not None:
|
|
ray.get(
|
|
tmpdir_prefix_actor.set_tmpdir_prefix.remote(callback.tmpdir_prefix)
|
|
)
|
|
trainer = pl.Trainer(
|
|
max_epochs=num_epochs,
|
|
devices="auto",
|
|
accelerator="cpu",
|
|
strategy=RayDDPStrategy(),
|
|
plugins=[RayLightningEnvironment()],
|
|
callbacks=[callback],
|
|
)
|
|
|
|
datamodule = DummyDataModule(batch_size, dataset_size)
|
|
if checkpoint is not None:
|
|
with checkpoint.as_directory() as ckpt_dir:
|
|
ckpt_path = os.path.join(
|
|
ckpt_dir, RayTrainReportCallback.CHECKPOINT_NAME
|
|
)
|
|
trainer.fit(model, datamodule=datamodule, ckpt_path=ckpt_path)
|
|
else:
|
|
trainer.fit(model, datamodule=datamodule)
|
|
|
|
trainer = TorchTrainer(
|
|
train_loop_per_worker=train_loop,
|
|
scaling_config=ScalingConfig(num_workers=num_workers),
|
|
validation_config=ValidationConfig(fn=validation_fn),
|
|
run_config=RunConfig(
|
|
storage_path=str(tmp_path),
|
|
checkpoint_config=CheckpointConfig(
|
|
num_to_keep=1, checkpoint_score_attribute="val_score"
|
|
),
|
|
failure_config=FailureConfig(max_failures=1),
|
|
),
|
|
)
|
|
|
|
results = trainer.fit()
|
|
assert results.error is None
|
|
assert "loss" in results.metrics
|
|
assert results.best_checkpoints is not None
|
|
assert len(results.best_checkpoints) == 1
|
|
assert results.best_checkpoints[0][1]["val_score"] == 1
|
|
recorded_prefixes = ray.get(tmpdir_prefix_actor.get_tmpdir_prefixes.remote())
|
|
assert len(recorded_prefixes) == num_workers
|
|
# Seems pyarrow.fs.FileSystem's delete_dir can leave an empty dir behind.
|
|
for path in recorded_prefixes:
|
|
assert not os.path.exists(path) or not any(os.scandir(path))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", "-x", __file__]))
|