File size: 8,555 Bytes
600bbdc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | import logging
import time
from pathlib import Path
import hydra
import torch
from omegaconf import OmegaConf
from torch.amp import GradScaler, autocast
from torch.nn.parallel import DistributedDataParallel
from common import load_config
from onescience.datapipes.cfd import DeepMindLagrangianDatapipe
from onescience.distributed.manager import DistributedManager
from onescience.launch.utils import load_checkpoint, save_checkpoint
def setup_logging(rank: int):
level = logging.INFO if rank == 0 else logging.WARNING
logging.basicConfig(level=level, format="%(asctime)s - %(levelname)s - %(message)s")
return logging.getLogger("lagrangian_mgn.train")
def resolve_device(requested: str, manager: DistributedManager):
if manager.distributed:
return manager.device
if requested == "cpu":
return torch.device("cpu")
if requested in ("cuda", "gpu"):
if not torch.cuda.is_available():
raise RuntimeError("Config requested CUDA, but torch.cuda.is_available() is false.")
return torch.device("cuda:0")
return torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
def train_step(model, graph, dataset, criterion, optimizer, scaler, scheduler, device, amp_enabled):
graph = graph.to(device)
optimizer.zero_grad(set_to_none=True)
with autocast(device_type=device.type, enabled=amp_enabled):
gt_pos, gt_vel, gt_acc = dataset.unpack_targets(graph)
pred_acc = model(graph.ndata["x"], graph.edata["x"], graph)
mask = graph.ndata["mask"].unsqueeze(-1).to(pred_acc.dtype)
num_nz = torch.clamp(mask.sum() * dataset.dim, min=1.0)
loss_acc_norm = (mask * criterion(pred_acc, gt_acc)).sum() / num_nz
with torch.no_grad():
pos, vel, _ = dataset.unpack_inputs(graph)
pred_pos, pred_vel = dataset.time_integrator(
position=pos,
velocity=vel[-1],
acceleration=pred_acc,
dt=dataset.dt,
denormalize=True,
)
loss_pos = (mask * criterion(pred_pos, gt_pos)).sum() / num_nz
loss_vel = (
mask * criterion(pred_vel, dataset.denormalize_velocity(gt_vel))
).sum() / num_nz
loss_acc = (
mask
* criterion(
dataset.denormalize_acceleration(pred_acc),
dataset.denormalize_acceleration(gt_acc),
)
).sum() / num_nz
scaler.scale(loss_acc_norm).backward()
scaler.step(optimizer)
scaler.update()
scheduler.step()
return {
"loss": loss_acc_norm.item() + loss_pos.item() + loss_vel.item(),
"loss_acc_norm": loss_acc_norm.item(),
"loss_pos": loss_pos.item(),
"loss_vel": loss_vel.item(),
"loss_acc": loss_acc.item(),
}
def main():
cfg = load_config()
Path(cfg.output).mkdir(parents=True, exist_ok=True)
Path(cfg.resume_dir).mkdir(parents=True, exist_ok=True)
DistributedManager.initialize()
manager = DistributedManager()
logger = setup_logging(manager.rank)
if manager.rank == 0:
logger.info("Config:\n%s", OmegaConf.to_yaml(cfg))
datapipe = DeepMindLagrangianDatapipe(cfg, distributed=manager.distributed)
train_loader = datapipe.train_dataloader()
val_loader = datapipe.val_dataloader()
dataset = datapipe.train_dataset
if cfg.model.recompute_activation and str(cfg.model.mlp_activation_fn).lower() != "silu":
raise ValueError("recompute_activation only supports SiLU.")
device = resolve_device(str(cfg.train.device), manager)
model = hydra.utils.instantiate(cfg.model).to(device)
if cfg.compile.enabled:
model = torch.compile(model, **cfg.compile.args)
if manager.distributed:
model = DistributedDataParallel(
model,
device_ids=[manager.local_rank],
output_device=manager.local_rank,
find_unused_parameters=False,
)
criterion = hydra.utils.instantiate(cfg.loss)
optimizer = hydra.utils.instantiate(cfg.optimizer, model.parameters())
num_iterations = max(int(cfg.train.epochs) * len(train_loader), 1)
if cfg.lr_scheduler._target_ == "torch.optim.lr_scheduler.CosineAnnealingLR":
if cfg.lr_scheduler.T_max is None:
cfg.lr_scheduler.T_max = num_iterations
elif cfg.lr_scheduler._target_ == "torch.optim.lr_scheduler.OneCycleLR":
if cfg.lr_scheduler.total_steps is None:
cfg.lr_scheduler.total_steps = num_iterations
scheduler = hydra.utils.instantiate(cfg.lr_scheduler, optimizer)
amp_enabled = bool(cfg.amp.enabled and device.type == "cuda")
scaler = GradScaler(enabled=amp_enabled)
epoch_init = load_checkpoint(
cfg.resume_dir,
models=model,
optimizer=optimizer,
scheduler=scheduler,
scaler=scaler,
device=device,
)
best_valid_loss = float("inf")
best_loss_epoch = epoch_init
logger.info("Starting training on %s", device)
for epoch in range(epoch_init, int(cfg.train.epochs)):
if manager.distributed and hasattr(train_loader.sampler, "set_epoch"):
train_loader.sampler.set_epoch(epoch)
start = time.time()
model.train()
epoch_losses = {}
for step, graph in enumerate(train_loader, start=1):
losses = train_step(
model,
graph,
dataset,
criterion,
optimizer,
scaler,
scheduler,
device,
amp_enabled,
)
for key, value in losses.items():
epoch_losses.setdefault(key, []).append(value)
if manager.rank == 0 and step % int(cfg.train.log_interval) == 0:
logger.info(
"Epoch %s/%s step %s/%s loss %.4e acc_norm %.4e",
epoch + 1,
cfg.train.epochs,
step,
len(train_loader),
losses["loss"],
losses["loss_acc_norm"],
)
model.eval()
valid_loss = 0.0
with torch.no_grad():
for graph in val_loader:
graph = graph.to(device)
gt_pos, _, gt_acc = datapipe.valid_dataset.unpack_targets(graph)
pred_acc = model(graph.ndata["x"], graph.edata["x"], graph)
mask = graph.ndata["mask"].unsqueeze(-1).to(pred_acc.dtype)
num_nz = torch.clamp(mask.sum() * datapipe.valid_dataset.dim, min=1.0)
loss_acc = (mask * criterion(pred_acc, gt_acc)).sum() / num_nz
pos, vel, _ = datapipe.valid_dataset.unpack_inputs(graph)
pred_pos, _ = datapipe.valid_dataset.time_integrator(
position=pos,
velocity=vel[-1],
acceleration=pred_acc,
dt=datapipe.valid_dataset.dt,
denormalize=True,
)
loss_pos = (mask * criterion(pred_pos, gt_pos)).sum() / num_nz
valid_loss += (loss_acc + loss_pos).item()
valid_loss /= max(len(val_loader), 1)
if manager.rank == 0:
mean_losses = {k: sum(v) / len(v) for k, v in epoch_losses.items()}
logger.info(
"Epoch %s finished in %.2fs train_loss %.4e valid_loss %.4e lr %.2e",
epoch + 1,
time.time() - start,
mean_losses.get("loss", 0.0),
valid_loss,
scheduler.get_last_lr()[0],
)
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
best_loss_epoch = epoch
save_checkpoint(
cfg.resume_dir,
models=model,
optimizer=optimizer,
scheduler=scheduler,
scaler=scaler,
epoch=epoch + 1,
)
logger.info("Checkpoint saved to %s", cfg.resume_dir)
if epoch - best_loss_epoch >= int(cfg.train.patience):
logger.warning("Early stopping after %s stale epochs", cfg.train.patience)
break
if manager.distributed:
torch.distributed.barrier()
manager.cleanup()
logger.info("Training finished")
if __name__ == "__main__":
main()
|