"""Train the paper-budget DeepONet with hybrid exponential vRBA.""" import csv import json import math import random import sys import time from pathlib import Path sys.path.insert(0,str(Path(__file__).resolve().parents[1])) import numpy as np import torch from model.vrba import VRBADeepONet, ExponentialVRBA from common import arguments, config, path, accelerator, load_data, write_json, save_checkpoint, dataset_manifest def run(cfg, resume=None, smoke=False): seed=cfg['training']['seed'] random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) torch.set_num_threads(1) device=accelerator(cfg['training']['device']) torch.cuda.manual_seed_all(seed) torch.backends.cuda.matmul.allow_tf32=False torch.backends.cudnn.allow_tf32=False data=load_data(cfg) results=path(cfg['paths']['results']) / 'smoke' if smoke else path(cfg['paths']['results']) results.mkdir(parents=True,exist_ok=True) if not smoke and (results/'history.csv').exists() and resume is None: raise RuntimeError('Existing formal history; use --resume or a new results directory.') write_json(results/'data_manifest.json',dataset_manifest(data,cfg)) write_json(results/'config_resolved.json',cfg) x=torch.as_tensor(data['x'],device=device) y=torch.as_tensor(data['y'],device=device) t=torch.as_tensor(data['time'],device=device) nt=cfg['data']['train']; bs=cfg['training']['batch_size'] steps=nt//bs if nt%bs: raise ValueError('Training count must be divisible by batch size.') epochs=1 if smoke else cfg['training']['epochs'] model=VRBADeepONet(**cfg['model']).to(device) nparam=sum(p.numel() for p in model.parameters()) if nparam != 101100: raise ValueError(f'Paper parameter count mismatch: {nparam}') v=cfg['vrba'] attention=ExponentialVRBA(nt,cfg['data']['output_points'],device, **{k:v[k] for k in ['eta','gamma','phi','c','initial_weight']}) optimizer=torch.optim.Adam(model.parameters(),lr=cfg['training']['learning_rate'], weight_decay=cfg['training']['weight_decay'],betas=tuple(cfg['training']['adam_betas']), eps=cfg['training']['adam_epsilon']) scheduler=torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,T_max=cfg['training']['epochs']*steps) best=float('inf'); best_epoch=0; start_epoch=0; step=0 if resume: ck=torch.load(path(resume),map_location=device,weights_only=False) if ck['model_config']!=cfg['model']: raise ValueError('Resume architecture mismatch.') if ck['normalization']!=data['stats']: raise ValueError('Resume normalization mismatch.') model.load_state_dict(ck['model_state_dict']); optimizer.load_state_dict(ck['optimizer_state_dict']) scheduler.load_state_dict(ck['scheduler_state_dict']); attention.weights.copy_(ck['attention_weights'].to(device)) start_epoch=ck['epoch']; step=ck['global_step']; best=ck['best_validation']; best_epoch=ck['best_epoch'] torch.set_rng_state(ck['torch_rng'].cpu()); torch.cuda.set_rng_state_all([r.cpu() for r in ck['cuda_rng']]) np.random.set_state(ck['numpy_rng']); random.setstate(ck['python_rng']) val=data['splits']['validation'] fields=['epoch','global_step','train_weighted_loss','train_sampled_mse_normalized', 'validation_mse_normalized','validation_relative_l2_normalized_batch_mean', 'validation_relative_l2_physical_global','learning_rate','best_epoch','elapsed_seconds'] mode='a' if resume else 'w' begin=time.perf_counter() environment=dict(python=sys.version,torch=torch.__version__,hip=torch.version.hip, device=torch.cuda.get_device_name(device),parameter_count=nparam, job_id=__import__('os').environ.get('SLURM_JOB_ID'),node=__import__('socket').gethostname()) write_json(results/'environment.json',environment) print('RUN_START '+json.dumps(dict(environment=environment,epochs=epochs,batch_size=bs,steps_per_epoch=steps,smoke=smoke)),flush=True) checks={} with (results/'history.csv').open(mode,newline='',buffering=1) as f: writer=csv.DictWriter(f,fieldnames=fields) if not resume: writer.writeheader() for epoch in range(start_epoch,epochs): model.train(); losses=torch.zeros(2,device=device) for _ in range(steps): # Eq. 44 is the function marginal; do not apply a second exponential. with torch.no_grad(): indices=torch.multinomial(attention.probabilities(),bs,replacement=v['replacement']) optimizer.zero_grad(set_to_none=True) prediction=model(x[indices],t) if smoke and not checks: assert prediction.shape==(bs,cfg['data']['output_points']) checks['forward_pass']='passed' residual=prediction-y[indices] weights=attention.update(indices,residual,step) loss=(weights*residual).square().mean() loss.backward() if smoke and 'backward_pass' not in checks: assert all(p.grad is not None and torch.isfinite(p.grad).all() for p in model.parameters()) checks['backward_pass']='passed' optimizer.step(); scheduler.step(); step+=1 losses+=torch.stack((loss.detach(),residual.detach().square().mean())) model.eval() with torch.inference_mode(): pv=model(x[val],t); rv=pv-y[val] val_mse=rv.square().mean().item() val_rel=torch.stack([torch.linalg.vector_norm(rv[i:i+bs])/torch.linalg.vector_norm(y[val][i:i+bs]) for i in range(0,cfg['data']['validation'],bs)]).mean().item() physical_true=y[val]*data['stats']['radius_std']+data['stats']['radius_mean'] physical_rel=(torch.linalg.vector_norm(rv*data['stats']['radius_std'])/torch.linalg.vector_norm(physical_true)).item() train_loss,train_mse=(losses/steps).tolist() if not all(math.isfinite(vv) for vv in [train_loss,train_mse,val_mse,val_rel,physical_rel]): raise FloatingPointError(f'Nonfinite values at epoch {epoch+1}') improved=val_rel0 and best<1e10 write_json(results/'checks.json',dict(attempt=1,checks=checks)) torch.cuda.synchronize() summary=dict(status='completed',completed_epochs=epochs,global_step=step,best_epoch=best_epoch, best_validation_relative_l2_normalized=best,early_stopping=False,smoke=smoke, duration_seconds=time.perf_counter()-begin) write_json(results/'training_summary.json',summary) print('TRAINING_COMPLETE '+json.dumps(summary),flush=True) if __name__=='__main__': parser=arguments(__doc__) parser.add_argument('--resume',default=None,help='Full training-state checkpoint; restores optimizer, scheduler, vRBA and RNG.') parser.add_argument('--smoke',action='store_true',help='Separate one-epoch pipeline check; no formal checkpoints.') args=parser.parse_args() run(config(args.config),args.resume,args.smoke)