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

142 lines
4.6 KiB
Python

import logging
import gc
import os
import torch
import torch.nn
from torchrec.distributed.train_pipeline import StagedTrainPipeline, SparseDataDistUtil
from torchrec.distributed.train_pipeline.utils import PipelineStage
from torchrec.optim.keyed import CombinedOptimizer, KeyedOptimizerWrapper
from torchrec.optim.optimizers import in_backward_optimizer_filter
import ray.train
import ray.train.torch
from runner import TrainLoopRunner
logger = logging.getLogger(__name__)
class TorchRecRunner(TrainLoopRunner):
def _setup(self):
if self.factory.benchmark_config.mock_gpu:
raise ValueError("Mock GPU is not supported for running TorchRec.")
self.model = self.factory.get_model()
# TODO: This code depends on the model having a fused_optimizer,
# which is hidden in the `get_model` method of the factory.
dense_optimizer = KeyedOptimizerWrapper(
dict(in_backward_optimizer_filter(self.model.named_parameters())),
lambda params: torch.optim.Adagrad(params, lr=15.0, eps=1e-8),
)
self.optimizer = CombinedOptimizer(
[self.model.fused_optimizer, dense_optimizer]
)
self._data_dist_stream = torch.cuda.Stream()
self._h2d_stream = torch.cuda.Stream()
def _wrap_dataloader(self, dataloader, train: bool = True):
dataloader_iter = iter(dataloader)
device = ray.train.torch.get_device()
sdd = SparseDataDistUtil(
model=self.model,
data_dist_stream=self._data_dist_stream,
# prefetch_stream=torch.cuda.Stream(),
)
pipeline = [
PipelineStage(
name="data_copy",
runnable=lambda batch: batch.to(device, non_blocking=True),
stream=self._h2d_stream,
),
PipelineStage(
name="start_sparse_data_dist",
runnable=sdd.start_sparse_data_dist,
stream=sdd.data_dist_stream,
fill_callback=sdd.wait_sparse_data_dist,
),
# PipelineStage(
# name="prefetch",
# runnable=sdd.prefetch,
# stream=sdd.prefetch_stream,
# fill_callback=sdd.load_prefetch,
# ),
]
pipeline = StagedTrainPipeline(pipeline_stages=pipeline)
def dataloader_with_torchrec_pipeline():
while batch := pipeline.progress(dataloader_iter):
yield batch
pipeline.flush_end()
return super()._wrap_dataloader(
dataloader_with_torchrec_pipeline(), train=train
)
def _train_step(self, batch):
self.model.train()
self.optimizer.zero_grad()
loss, out = self.model(batch)
loss.backward()
self.optimizer.step()
def _validate_step(self, batch):
self.model.eval()
with torch.no_grad():
loss, out = self.model(batch)
return loss
def _get_model_and_optim_filenames(self):
rank = ray.train.get_context().get_world_rank()
return f"model_shard_{rank=}.pt", f"optimizer_shard_{rank=}.pt"
def _save_training_state(self, local_dir: str):
# NOTE: Embedding table shards are on different GPUs,
# so we need to do distributed checkpointing.
# This checkpoint format must be loaded on the same number
# of workers and GPU types, since it was sharded with a compute-specific plan.
model_filename, optimizer_filename = self._get_model_and_optim_filenames()
torch.save(self.model.state_dict(), os.path.join(local_dir, model_filename))
torch.save(
self.optimizer.state_dict(), os.path.join(local_dir, optimizer_filename)
)
def _load_training_state(self, local_dir: str):
model_filename, optimizer_filename = self._get_model_and_optim_filenames()
self.model.load_state_dict(
torch.load(
os.path.join(local_dir, model_filename),
map_location=self.model.device,
)
)
self.optimizer.load_state_dict(
torch.load(
os.path.join(local_dir, optimizer_filename),
map_location=self.model.device,
)
)
def _cleanup(self):
# NOTE: This cleanup is needed to avoid zombie Train worker processes
# that hang on gc collect on python teardown.
del self.model
del self.optimizer
del self._data_dist_stream
del self._h2d_stream
torch.cuda.synchronize()
torch.cuda.empty_cache()
gc.collect()