Approach: run a per-step (or per-checkpoint) diagnostic that iterates model.parameters() and their .grad to compute L2 norms, check for NaN/Inf, compute simple distribution stats, inspect optimizer learning rates and scheduler state, and print/emit alerts when heuristics trigger. Save results to JSON/CSV for trend analysis.python
import math, json, torch
from collections import defaultdict
import numpy as np
def tensor_stats(t):
t = t.detach().cpu()
a = t.view(-1).numpy()
return {
"mean": float(np.mean(a)),
"std": float(np.std(a)),
"p0": float(np.min(a)),
"p1": float(np.percentile(a,1)),
"p50": float(np.percentile(a,50)),
"p99": float(np.percentile(a,99)),
"p100": float(np.max(a)),
"zeros_frac": float((a==0).sum()/a.size)
}
def diagnose(model, optimizer, scheduler=None):
report = {"layers":{}, "optimizer":{}, "alerts":[]}
total_param_norm = 0.0
total_grad_norm = 0.0
zero_grad_layers = 0
for name, p in model.named_parameters():
stats = {}
p_norm = p.data.norm(2).item() if p.data.numel()>0 else 0.0
stats['param_l2'] = p_norm
total_param_norm += p_norm**2
if p.grad is None:
stats['has_grad'] = False
zero_grad_layers += 1
stats['grad_l2'] = None
else:
g = p.grad
grads_nan = torch.isnan(g).any().item()
grads_inf = torch.isinf(g).any().item()
g_norm = g.norm(2).item()
stats.update({"has_grad":True, "grad_l2":g_norm,
"grad_has_nan":bool(grads_nan),
"grad_has_inf":bool(grads_inf)})
total_grad_norm += g_norm**2
# weight & grad distributions (sample if large)
stats['param_stats'] = tensor_stats(p.data if p.data.numel()<=1e6 else p.data.view(-1)[:100000])
stats['grad_stats'] = tensor_stats(g if g.numel()<=1e6 else g.view(-1)[:100000])
# heuristics
if grads_nan or grads_inf:
report['alerts'].append(f"{name}: grad NaN/Inf")
if g_norm < 1e-6:
report['alerts'].append(f"{name}: vanishing grad (L2 {g_norm:.2e})")
if g_norm > 1e3:
report['alerts'].append(f"{name}: exploding grad (L2 {g_norm:.2e})")
if stats['param_stats']['zeros_frac'] > 0.99:
report['alerts'].append(f"{name}: >99% weights zeroed")
report['layers'][name]=stats
report['total_param_l2'] = math.sqrt(total_param_norm)
report['total_grad_l2'] = math.sqrt(total_grad_norm)
report['zero_grad_layers'] = zero_grad_layers
# optimizer / scheduler
lrs = set()
for i, g in enumerate(optimizer.param_groups):
lr = g.get("lr", None)
lrs.add(lr)
report['optimizer']['lrs'] = list(lrs)
if scheduler is not None:
# try common attributes
report['scheduler'] = {"last_lr": getattr(scheduler, "get_last_lr", lambda: None)() if hasattr(scheduler,"get_last_lr") else None}
# heuristics summary
if report['total_grad_l2'] < 1e-5:
report['alerts'].append(f"Global vanishing grads (total L2 {report['total_grad_l2']:.2e})")
if report['total_grad_l2'] > 1e4:
report['alerts'].append(f"Global exploding grads (total L2 {report['total_grad_l2']:.2e})")
if len(report['optimizer']['lrs'])==1 and list(report['optimizer']['lrs'])[0]==0:
report['alerts'].append("Learning rate is zero")
return report
# Example usage:
# report = diagnose(model, optimizer, scheduler)
# print(json.dumps(report, indent=2))
Thresholds & heuristics explained:- Per-layer grad L2: - <1e-6: suspect vanishing (no signal); 1e-6 chosen as small relative to typical float32 training where useful grads are >=1e-4–1e-3 for many layers. - >1e3–1e4: suspect exploding; depends on scale—flag for investigation.- Param L2 & zeros_frac: - zeros_frac >99%: likely dead/reinitialized or masked layer. - Extremely small param L2 (near 0) for many layers may indicate faulty initialization or aggressive weight decay.- NaN/Inf: immediate alert — stop training and check loss scaler/grad clipping/inputs.- Zero or extremely small global grad norm: check learning rate, optimizer state (momentum near 1), or gradient flow (wrong loss, detached graph).- Learning rate checks: - lr == 0: misconfigured scheduler. - sudden drops/steps: compare current LR to recent history; if LR dropped drastically coinciding with plateau, confirm intended schedule.- Additional heuristics: - Count layers with grad None or zeros: >20% unexpected in dense nets is suspicious unless intentional (frozen layers). - High zeros_frac in grads: indicates sparse updates (e.g., ReLU dead units, gradient masking).Actions when flagged:- For NaNs: enable AMP anomaly detection, reduce LR, disable grad scaler, check batch, clamp inputs.- For vanishing grads: inspect activation functions, initialization (Xavier/He), remove too-strong weight decay, verify loss backward is called.- For exploding grads: add/adjust grad clipping, reduce LR, check for runaway activations.Integrate this script into training loop to log trends (JSON/CSV) and trigger alerts for automated stop/retry.