Files
2026-07-13 13:17:40 +08:00

312 lines
11 KiB
Python

# __validation_fn_simple_start__
import os
import torch
import ray.train
import ray.data
# Define Ray Data validation dataset outside validation function because it is not json serializable
validation_dataset = ...
def validation_fn(checkpoint: ray.train.Checkpoint) -> dict:
# Load the checkpoint
model = ...
with checkpoint.as_directory() as checkpoint_dir:
model_state_dict = torch.load(os.path.join(checkpoint_dir, "model.pt"))
model.load_state_dict(model_state_dict)
model.eval()
# Perform validation on the data
total_accuracy = 0
with torch.no_grad():
for batch in validation_dataset.iter_torch_batches(batch_size=128):
images, labels = batch["image"], batch["label"]
outputs = model(images)
total_accuracy += (outputs.argmax(1) == labels).sum().item()
return {"score": total_accuracy / len(validation_dataset)}
# __validation_fn_simple_end__
# __validation_fn_torch_trainer_start__
import torchmetrics
from torch.nn import CrossEntropyLoss
import ray.train.torch
from ray.data import ExecutionOptions
def eval_only_train_fn(config_dict: dict) -> dict:
# Load the checkpoint
model = ...
with config_dict["checkpoint"].as_directory() as checkpoint_dir:
model_state_dict = torch.load(os.path.join(checkpoint_dir, "model.pt"))
model.load_state_dict(model_state_dict)
model.cuda().eval()
# Set up metrics and data loaders
criterion = CrossEntropyLoss()
mean_valid_loss = torchmetrics.MeanMetric().cuda()
test_data_shard = ray.train.get_dataset_shard("validation")
test_dataloader = test_data_shard.iter_torch_batches(batch_size=128)
# Compute metric and return it directly from the train function
with torch.no_grad():
for batch in test_dataloader:
images, labels = batch["image"], batch["label"]
outputs = model(images)
loss = criterion(outputs, labels)
mean_valid_loss(loss)
return {"score": mean_valid_loss.compute().item()}
def validation_fn(checkpoint: ray.train.Checkpoint, train_run_name: str, epoch: int) -> dict:
trainer = ray.train.torch.TorchTrainer(
eval_only_train_fn,
train_loop_config={"checkpoint": checkpoint},
scaling_config=ray.train.ScalingConfig(
num_workers=2, use_gpu=True, accelerator_type="A10G"
),
# Give unique name to validation run so it does not attempt to load placeholder checkpoint.
# Also allows you to better associate training runs with validation runs.
run_config=ray.train.RunConfig(
name=f"{train_run_name}_validation_epoch_{epoch}"
),
# Use weaker GPUs for validation
datasets={"validation": validation_dataset},
# Pin to the "validation" subcluster so it doesn't compete with
# training. See https://docs.ray.io/en/latest/data/concurrent-dataset-execution.html.
dataset_config=ray.train.DataConfig(
execution_options={
"validation": ExecutionOptions(
label_selector={"ray-subcluster": "validation"}
),
},
),
)
result = trainer.fit()
# return_value holds the value returned by train function of worker 0
return result.return_value
# __validation_fn_torch_trainer_end__
# __validation_fn_map_batches_start__
import ray.data
class Predictor:
def __init__(self, checkpoint: ray.train.Checkpoint):
self.model = ...
with checkpoint.as_directory() as checkpoint_dir:
model_state_dict = torch.load(os.path.join(checkpoint_dir, "model.pt"))
self.model.load_state_dict(model_state_dict)
self.model.cuda().eval()
def __call__(self, batch: dict) -> dict:
image = torch.as_tensor(batch["image"], dtype=torch.float32, device="cuda")
label = torch.as_tensor(batch["label"], dtype=torch.float32, device="cuda")
pred = self.model(image)
return {"res": (pred.argmax(1) == label).cpu().numpy()}
# Construct ``validation_dataset`` under a DataContext copy pinned to the
# "validation" subcluster. ``Dataset.context`` is a deep copy of the
# current context taken at construction, so the selector is baked in and
# every downstream operator (including the ``map_batches`` below) inherits
# it — no in-function mutation needed. See
# https://docs.ray.io/en/latest/data/concurrent-dataset-execution.html.
ctx = ray.data.DataContext.get_current().copy()
ctx.execution_options.label_selector = {"ray-subcluster": "validation"}
with ray.data.DataContext.current(ctx):
validation_dataset = ray.data.read_parquet(...)
def validation_fn(checkpoint: ray.train.Checkpoint) -> dict:
# Set name to avoid confusion; default name is "Dataset"
validation_dataset.set_name("validation")
eval_res = validation_dataset.map_batches(
Predictor,
batch_size=128,
num_gpus=1,
fn_constructor_kwargs={"checkpoint": checkpoint},
concurrency=2,
)
mean = eval_res.mean(["res"])
return {
"score": mean,
}
# __validation_fn_map_batches_end__
# __validation_fn_report_start__
import tempfile
from ray.data import ExecutionOptions
from ray.train import ValidationConfig, ValidationTaskConfig
def train_func(config: dict) -> None:
...
epochs = ...
model = ...
rank = ray.train.get_context().get_world_rank()
for epoch in epochs:
... # training step
if rank == 0:
training_metrics = {"loss": ..., "epoch": epoch}
local_checkpoint_dir = tempfile.mkdtemp()
torch.save(
model.module.state_dict(),
os.path.join(local_checkpoint_dir, "model.pt"),
)
ray.train.report(
training_metrics,
checkpoint=ray.train.Checkpoint.from_directory(local_checkpoint_dir),
checkpoint_upload_mode=ray.train.CheckpointUploadMode.ASYNC,
validation=ValidationTaskConfig(fn_kwargs={
"train_run_name": ray.train.get_context().get_experiment_name(),
"epoch": epoch,
}),
)
else:
ray.train.report({}, None)
def run_trainer() -> ray.train.Result:
# 1) Construction-time tasks (parquet schema inference, file listing)
# read the current DataContext. Pin them to "training" with a copy of
# the DataContext applied via the DataContext.current() context
# manager — scoped to the `with` block so it doesn't leak. See
# https://docs.ray.io/en/latest/data/concurrent-dataset-execution.html.
ctx = ray.data.DataContext.get_current().copy()
ctx.execution_options.label_selector = {"ray-subcluster": "training"}
with ray.data.DataContext.current(ctx):
train_dataset = ray.data.read_parquet(...)
trainer = ray.train.torch.TorchTrainer(
train_func,
validation_config=ValidationConfig(fn=validation_fn),
# Pass training dataset in datasets arg to split it across training workers
datasets={"train": train_dataset},
# 2) DataConfig.execution_options REPLACES ds.context.execution_options
# wholesale at training start, dropping anything not re-specified
# (including label_selector). Restate the selector here so per-worker
# ingest stays pinned to "training".
dataset_config=ray.train.DataConfig(
datasets_to_split=["train"],
execution_options={
"train": ExecutionOptions(
label_selector={"ray-subcluster": "training"}
),
},
),
scaling_config=ray.train.ScalingConfig(
num_workers=2,
use_gpu=True,
# Use powerful GPUs for training
accelerator_type="A100",
),
)
return trainer.fit()
# __validation_fn_report_end__
# __exp_tracking_same_run_wandb_start__
import wandb
import ray.train
from ray.train import ValidationConfig, ValidationTaskConfig
entity = "my_entity"
project = "my_project"
num_epochs = ...
def validation_fn(checkpoint: ray.train.Checkpoint, wandb_run_id: str, val_step: int) -> dict:
wandb.init(
entity=entity,
project=project,
settings=wandb.Settings(mode="shared", x_primary=False),
id=wandb_run_id,
)
score = ...
wandb.log({"validation/loss": score, "val_step": val_step})
wandb.finish() # flush the metrics
return {"validation/loss": score}
def train_func():
if ray.train.get_context().get_world_rank() == 0:
run = wandb.init(
entity=entity,
project=project,
settings=wandb.Settings(mode="shared", x_primary=True,)
)
wandb.define_metric("val_step", hidden=True)
wandb.define_metric("train_step", hidden=True)
wandb.define_metric("validation/loss", step_metric="val_step")
wandb.define_metric("train/loss", step_metric="train_step")
for epoch in range(num_epochs):
loss = ...
if ray.train.get_context().get_world_rank() == 0:
wandb.log({"train/loss": loss, "train_step": epoch})
checkpoint = ...
ray.train.report(
{"train/loss": loss},
checkpoint=checkpoint,
validation=ValidationTaskConfig(
fn_kwargs={"wandb_run_id": run.id, "val_step": epoch}
),
)
else:
ray.train.report({}, None)
if ray.train.get_context().get_world_rank() == 0:
wandb.finish()
# __exp_tracking_same_run_wandb_end__
# __exp_tracking_same_run_mlflow_start__
import mlflow
from mlflow.tracking import MlflowClient
import ray.train
from ray.train import ValidationConfig, ValidationTaskConfig
tracking_uri = "my_uri"
experiment_name = "my_experiment"
num_epochs = ...
def validation_fn(
checkpoint: ray.train.Checkpoint, mlflow_run_id: str, val_step: int
) -> dict:
client = MlflowClient(tracking_uri=tracking_uri)
score = ...
client.log_metric(mlflow_run_id, "val_score", score, step=val_step)
return {"val_score": score}
def train_func():
if ray.train.get_context().get_world_rank() == 0:
client = MlflowClient(tracking_uri=tracking_uri)
experiment = client.get_experiment_by_name(experiment_name)
run = client.create_run(experiment_id=experiment.experiment_id)
for epoch in range(num_epochs):
loss = ...
if ray.train.get_context().get_world_rank() == 0:
client.log_metric(run.info.run_id, "train_loss", loss, step=epoch)
checkpoint = ...
ray.train.report(
{"train_loss": loss},
checkpoint=checkpoint,
validation=ValidationTaskConfig(
fn_kwargs={"mlflow_run_id": run.info.run_id, "val_step": epoch}
),
)
else:
ray.train.report({}, None)
if ray.train.get_context().get_world_rank() == 0:
client.set_terminated(run.info.run_id)
# __exp_tracking_same_run_mlflow_end__