Approach: make training deterministic where practical, avoid resource leaks and unhandled exceptions, prevent non-atomic checkpoint overwrite, protect against OOM during accumulation, and optionally use AMP + loss scaling and gradient clipping.python
import os
import torch
from torch.cuda.amp import GradScaler, autocast
def set_seed(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
import random, numpy as np
random.seed(seed)
np.random.seed(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
def save_checkpoint_atomic(state, path):
tmp = f"{path}.tmp"
torch.save(state, tmp)
os.replace(tmp, path) # atomic on POSIX and Windows since Python 3.3
def train(model, dataloader, optimizer, loss_fn, epochs,
device='cuda', seed=42, clip_norm=None,
accumulate_steps=1, use_amp=True, ckpt_path='checkpoint.pt'):
set_seed(seed)
model.to(device)
scaler = GradScaler(enabled=use_amp and device=='cuda')
try:
for epoch in range(epochs):
model.train()
optimizer.zero_grad()
for step, batch in enumerate(dataloader, 1):
inputs = batch['x'].to(device, non_blocking=True)
targets = batch['y'].to(device, non_blocking=True)
with autocast(enabled=scaler.is_enabled()):
outputs = model(inputs)
loss = loss_fn(outputs, targets) / accumulate_steps
scaler.scale(loss).backward()
if step % accumulate_steps == 0:
if clip_norm:
# unscale before clipping
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), clip_norm)
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
# safe atomic checkpointing and persist optimizer/scaler state
checkpoint = {
'epoch': epoch,
'model_state': model.state_dict(),
'optim_state': optimizer.state_dict(),
'scaler_state': scaler.state_dict() if scaler is not None else None,
'seed': seed
}
save_checkpoint_atomic(checkpoint, ckpt_path)
except Exception:
# optionally log exception, ensure checkpointing of progress before re-raising
try:
save_checkpoint_atomic({
'model_state': model.state_dict(),
'optim_state': optimizer.state_dict(),
'scaler_state': scaler.state_dict() if scaler is not None else None,
'note': 'exception_checkpoint'
}, ckpt_path + ".failed")
except Exception:
pass
raise
finally:
# best-effort cleanup of GPU memory
try:
del inputs, targets, outputs, loss
except NameError:
pass
torch.cuda.empty_cache()
Key changes and why:- Deterministic seeding: set_seed sets PyTorch, CUDA, random, numpy seeds and disables cuDNN nondeterminism to improve reproducibility.- non_blocking=True and .to(device): improves throughput and avoids accidental host/GPU copies blocking.- Mixed precision (autocast + GradScaler): reduces memory and speeds up FP16-capable GPUs; scaler handles loss scaling to avoid underflow.- Gradient accumulation: divide loss by accumulate_steps and only step optimizer after accumulated steps to control effective batch size and OOM risk.- Unscale before gradient clipping: scaler.unscale_ then clip_grad_norm_ to correctly clip true gradients.- Atomic checkpointing: save to a tmp file then os.replace to avoid corrupt/partial checkpoints if crash occurs during save.- Save optimizer and scaler states: necessary to resume correctly.- try/except/finally: captures exceptions, attempts to save a final checkpoint, re-raises for caller to handle, and performs cleanup to free GPU memory.- torch.cuda.empty_cache() and explicit del: best-effort resource cleanup to reduce memory leaks.- Minimal logging omitted—add structured logging in real code.Edge cases: full determinism on CUDA is not always guaranteed across drivers/hardware; atomic replace semantics rely on filesystem semantics (network filesystems may differ).