rem3di-scratch-pcqm-cosmo / source /scratch_train.py
Queimo's picture
Verify random initialization, real-data gradients and exact next-step resume before scratch training
04a3ade verified
Raw History Blame Contribute Delete
17.4 kB
"""Train one fifth-epoch, save an exact-resume checkpoint, then release the GPU."""
from pathlib import Path
import sys
sys.path.insert(0,str(Path(__file__).resolve().parent))
from scratch_model import *
import random,math,gc,fcntl,shutil,traceback,argparse,importlib.metadata,platform
from huggingface_hub import HfApi,CommitOperationAdd
def status(phase,**extra):
write(OUT/'status.json',dict(phase=phase,pid=os.getpid(),updated_unix=time.time(),**extra))
print('SCRATCH_TRAIN',phase,json.dumps(extra),flush=True)
def rng_state():
return dict(torch_rng=torch.get_rng_state(),cuda_rng=torch.cuda.get_rng_state_all(),
numpy_rng=np.random.get_state(),python_rng=random.getstate())
def restore_rng(state):
torch.set_rng_state(state['torch_rng']);torch.cuda.set_rng_state_all(state['cuda_rng'])
np.random.set_state(state['numpy_rng']);random.setstate(state['python_rng'])
def seed():
random.seed(CONFIG['seed']);np.random.seed(CONFIG['seed']);torch.manual_seed(CONFIG['seed']);torch.cuda.manual_seed_all(CONFIG['seed'])
torch.use_deterministic_algorithms(True)
class TrainingData:
def __init__(self):
label_columns=[f'p_{i:03d}' for i in range(153)]+['area_A2','volume_A3']
self.pcqm=pd.read_parquet(DATA/'pcqm.parquet',columns=['split'])
self.cosmo=pd.read_parquet(DATA/'cosmo.parquet',columns=['split',*label_columns])
self.p_cache=RawCache('pcqm');self.c_cache=RawCache('cosmo')
self.pt=np.flatnonzero(self.pcqm.split.to_numpy()=='train');self.pv=np.flatnonzero(self.pcqm.split.to_numpy()=='validation')
self.ct=np.flatnonzero(self.cosmo.split.to_numpy()=='train');self.cv=np.flatnonzero(self.cosmo.split.to_numpy()=='validation')
transform=json.loads((DATA/'cosmo_label_transform.json').read_text())
y=self.cosmo[[f'p_{i:03d}' for i in range(153)]+['area_A2','volume_A3']].to_numpy(dtype=np.float64)
y=np.c_[np.log1p(y[:,:153]),np.log(y[:,153:])]
self.labels=torch.tensor((y-np.asarray(transform['mean']))/np.asarray(transform['scale']),dtype=torch.float32,device='cuda')
assert torch.isfinite(self.labels).all()
self.epoch_plans={e:self.plan(e) for e in range(1,CONFIG['epochs']+1)}
self.total_steps=sum(v[2] for v in self.epoch_plans.values())
schedule=[];offset=0
for epoch,(pp,cp,n) in self.epoch_plans.items():
for fifth in range(1,6):
end=(fifth*n+4)//5;start=((fifth-1)*n+4)//5
schedule.append(dict(point=len(schedule)+1,epoch=epoch,fifth=fifth,batch_start=start,batch_end=end,
batches=n,epoch_position=(epoch-1)+end/n,nominal_epoch=(epoch-1)+fifth/5,global_step=offset+end))
offset+=n
assert len(schedule)==50 and schedule[-1]['global_step']==self.total_steps
write(OUT/'probe_schedule.json',dict(total_steps=self.total_steps,probes=50,points=schedule))
self.schedule=schedule
def plan(self,epoch):
rng=np.random.default_rng(CONFIG['seed']+epoch)
pp=list(batches(self.pt,self.p_cache.lengths,rng,max_size=CONFIG['pcqm_batch_size'],atom_cap=CONFIG['pcqm_atom_cap']))
cp=list(batches(self.ct,self.c_cache.lengths,rng,max_size=CONFIG['cosmo_batch_size'],atom_cap=CONFIG['cosmo_atom_cap']))
assert np.array_equal(np.sort(np.concatenate(pp)),self.pt)
assert np.array_equal(np.sort(np.concatenate(cp)),self.ct)
return pp,cp,max(len(pp),len(cp))
def update(model,optimizer,data,pi,ci):
optimizer.zero_grad(set_to_none=True)
raw,z,target=model.denoising(data.p_cache.sample(pi))
first=uncertainty(raw[None],model.log_variance[:1]);first.backward()
log=dict(denoising_raw=float(raw.detach()),denoising_per_channel=float(raw.detach())/1088,
descriptor_std_pcqm=float(z.detach().std(dim=0,unbiased=False).mean()),weighted_denoising=float(first.detach()))
del raw,z,target,first
loss,z,pred,parts=model.cosmo(data.c_cache.sample(ci),data.labels[ci])
second=uncertainty(loss[None],model.log_variance[1:]);second.backward()
log.update(cosmo=float(loss.detach()),descriptor_std_cosmo=float(z.detach().std(dim=0,unbiased=False).mean()),
weighted_cosmo=float(second.detach()),**{'cosmo_'+k:v for k,v in parts.items()})
norm=nn.utils.clip_grad_norm_(model.parameters(),CONFIG['gradient_clip'],error_if_nonfinite=True)
assert all(math.isfinite(v) for v in log.values()),log
optimizer.step()
log.update(gradient_norm=float(norm),total=log['weighted_denoising']+log['weighted_cosmo'],
log_variance_denoising=float(model.log_variance[0].detach()),log_variance_cosmo=float(model.log_variance[1].detach()))
return log
@torch.no_grad()
def validate(model,data):
old=rng_state();torch.manual_seed(83026);torch.cuda.manual_seed_all(83026);model.eval()
result={}
try:
for name,ids,cache in [('pcqm',data.pv,data.p_cache),('cosmo',data.cv,data.c_cache)]:
total=0.;count=0;zs=np.zeros(640);zss=zs.copy();std=0.;atoms=0
for group in batches(ids,cache.lengths,max_size=64,atom_cap=3600):
if name=='pcqm':
loss,z,target=model.denoising(cache.sample(group))
valid=target[~(torch.arange(target.shape[1],device='cuda')[None]>=torch.tensor(cache.lengths[group],device='cuda')[:,None])]
total+=float(loss)*len(valid);atoms+=len(valid)
std+=float(valid.std(dim=0).mean())*len(valid)
else:
loss,z,_,_=model.cosmo(cache.sample(group),data.labels[group]);total+=float(loss)*len(group)
values=z.double().cpu().numpy();zs+=values.sum(0);zss+=np.square(values).sum(0);count+=len(group)
assert count==len(ids)
result[name+'_rows']=count
result[name+'_loss']=total/(atoms if name=='pcqm' else count)
result[name+'_descriptor_channel_std']=float(np.sqrt(np.maximum(zss/count-(zs/count)**2,0)).mean())
if name=='pcqm':result['pcqm_target_batch_std']=std/atoms;result['pcqm_loss_per_channel']=result['pcqm_loss']/1088
assert all(math.isfinite(v) for v in result.values()),result
return result
finally:restore_rng(old)
def checkpoint(point,model,optimizer,scheduler,global_step,metrics):
path=OUT/'checkpoints'/f'probe_{point:03d}';pending=path.with_name(path.name+'.partial')
if path.exists():raise RuntimeError(f'Checkpoint already exists: {path}')
pending.mkdir(parents=True,exist_ok=True)
for name,module in model.parts.items():torch.save(module.state_dict(),pending/name)
torch.save(dict(log_variance=model.log_variance.detach().cpu(),task_order=CONFIG['task_order']),pending/'task_weighting.pth')
torch.save(dict(point=point,global_step=global_step,optimizer=optimizer.state_dict(),scheduler=scheduler.state_dict(),
config=CONFIG,**rng_state()),pending/'training_state.pt')
write(pending/'metrics.json',metrics);write(pending/'config.json',CONFIG)
for source,name in [(OUT/'architecture.yaml','post_training_architecture_config.yaml'),
(DATA/'normalization.pt','normalization.pt'),(DATA/'cosmo_label_transform.json','cosmo_label_transform.json'),
(OUT/'preparation_complete.json','data_manifest.json')]:shutil.copyfile(source,pending/name)
write(pending/'manifest.json',dict(point=point,global_step=global_step,
files={p.name:dict(bytes=p.stat().st_size,sha256=sha(p)) for p in pending.iterdir() if p.is_file() and p.name!='manifest.json'}))
pending.rename(path);return path
def upload(path):
if (path/'hub_verified.json').exists():return json.loads((path/'hub_verified.json').read_text())
api=HfApi();api.create_repo(MODEL_REPO,exist_ok=True)
files=[p for p in path.iterdir() if p.is_file() and p.name!='hub_verified.json']
for attempt in range(5):
try:
commit=api.create_commit(repo_id=MODEL_REPO,operations=[CommitOperationAdd(path_in_repo=path.name+'/'+p.name,path_or_fileobj=str(p)) for p in files],
commit_message=f'Save exact-resume scratch checkpoint {path.name} before its BigSolDB probe')
entries={p.rfilename:p for p in api.model_info(MODEL_REPO,revision=commit.oid,files_metadata=True).siblings}
for p in files:
if entries[path.name+'/'+p.name].lfs:assert entries[path.name+'/'+p.name].lfs.sha256==sha(p)
result=dict(verified=True,repository=MODEL_REPO,revision=commit.oid,point=int(path.name.split('_')[-1]))
write(path/'hub_verified.json',result);return result
except Exception:
if attempt==4:raise
time.sleep(min(10*2**attempt,50))
def scheduler_for(optimizer,data):
return torch.optim.lr_scheduler.OneCycleLR(optimizer,max_lr=[CONFIG['learning_rate_max'],CONFIG['learning_rate_uncertainty']],total_steps=data.total_steps)
def restore(path,model,optimizer,scheduler):
manifest=json.loads((path/'manifest.json').read_text())
assert sha(path/'training_state.pt')==manifest['files']['training_state.pt']['sha256']
state=torch.load(path/'training_state.pt',map_location='cpu',weights_only=False)
assert state['config']==CONFIG
optimizer.load_state_dict(state['optimizer']);scheduler.load_state_dict(state['scheduler']);restore_rng(state)
return state
def smoke(data):
import copy
seed();loads=[];original_load=torch.load
def record_load(path,*args,**kwargs):loads.append(str(path));return original_load(path,*args,**kwargs)
torch.load=record_load
try:model=ScratchJoint()
finally:torch.load=original_load
assert loads==[str(DATA/'normalization.pt')],loads
opt=optimizer_for(model);scheduler=scheduler_for(opt,data)
state=copy.deepcopy(model.state_dict());seeded_rng=rng_state()
pp,cp,_=data.epoch_plans[1];model.train()
update(model,opt,data,pp[0],cp[0]);scheduler.step()
first_gradients={name:any(p.grad is not None and p.grad.abs().sum()>0 for p in module.parameters()) for name,module in model.parts.items()}
# Author pair2bias weights start at zero: geometry receives its first gradient
# after the first optimizer update has opened this attention path.
gates=[v for k,v in state.items() if k.endswith('pair2bias.weight')]
assert gates and all(torch.count_nonzero(v)==0 for v in gates)
assert all(value for name,value in first_gradients.items() if name!='geometric_preprocessor.pth')
assert not torch.equal(model.encoder.state_dict()[next(iter(model.encoder.state_dict()))],state['encoder.'+next(iter(model.encoder.state_dict()))])
# Save after one step, then require an exact next-step replay including optimizer and RNG.
path=OUT/'checkpoints/probe_999';path.parent.mkdir(exist_ok=True)
if path.exists():shutil.rmtree(path)
saved=checkpoint(999,model,opt,scheduler,1,dict(smoke=True))
expected=update(model,opt,data,pp[1],cp[1]);scheduler.step()
gradients={name:any(p.grad is not None and p.grad.abs().sum()>0 for p in module.parameters()) for name,module in model.parts.items()}
assert all(gradients.values()),gradients
after={k:v.detach().cpu().clone() for k,v in model.state_dict().items()}
del model,opt,scheduler;gc.collect();torch.cuda.empty_cache()
replay=ScratchJoint(saved);opt=optimizer_for(replay);scheduler=scheduler_for(opt,data);restore(saved,replay,opt,scheduler)
replay.train();actual=update(replay,opt,data,pp[1],cp[1]);scheduler.step()
assert expected==actual and all(torch.equal(v.cpu(),after[k]) for k,v in replay.state_dict().items())
del replay,opt,scheduler,state,after;gc.collect();torch.cuda.empty_cache();shutil.rmtree(saved)
seed();model=ScratchJoint();opt=optimizer_for(model);scheduler=scheduler_for(opt,data)
initial=validate(model,data)
path=OUT/'checkpoints/probe_000'
if path.exists():
existing=ScratchJoint(path)
assert all(torch.equal(v,existing.state_dict()[k]) for k,v in model.state_dict().items())
del existing;gc.collect();torch.cuda.empty_cache()
else:path=checkpoint(0,model,opt,scheduler,0,dict(point=0,epoch_position=0,validation=initial))
upload(path)
write(OUT/'smoke_validation.json',dict(passed=True,all_trainable_head_gradients=gradients,
first_step_gradients=first_gradients,author_zero_initialized_pair_bias_preserved=True,
initialization_loaded_only_training_statistics=loads,no_pretrained_rem3di_weights_loaded=True,
exact_optimizer_rng_next_step_replay=True,initial_validation=initial))
source_names=['scratch_prepare.py','scratch_data.py','scratch_model.py','scratch_train.py','scratch_probe.py','scratch_driver.py','scratch_cache_initial.py','scratch_fast_cache.py',
'eval_common.py','train_cosmo.py','multitask_data.py','bigsoldb_evaluate.py']
write(OUT/'provenance.json',dict(initial_rem3di_checkpoint=None,initialization='Constructor initialization of all learned components',
architecture_sha256=sha(OUT/'architecture.yaml'),
foundation_sha256=sha(Path('/marimo/rem3di_training/MACE-POLAR-1-M.model')),
python=platform.python_version(),cuda=torch.version.cuda,gpu=torch.cuda.get_device_name(0),
versions={n:importlib.metadata.version(n) for n in ['torch','numpy','pandas','scikit-learn','rdkit','structured-data-models','mace-torch','e3nn']},
source_sha256={n:sha(Path(__file__).with_name(n)) for n in source_names},
legacy_source_sha256={str(p.relative_to(AUDIT/'author_legacy_source')):sha(p)
for p in sorted((AUDIT/'author_legacy_source/threedscriptors').rglob('*.py'))}))
ops=[CommitOperationAdd(path_in_repo='source/'+n,path_or_fileobj=str(Path(__file__).with_name(n))) for n in source_names]
ops += [CommitOperationAdd(path_in_repo=n,path_or_fileobj=str(OUT/n)) for n in ['smoke_validation.json','config.json','preparation_complete.json','probe_schedule.json','provenance.json']]
HfApi().create_commit(repo_id=MODEL_REPO,operations=ops,commit_message='Verify random initialization, real-data gradients and exact next-step resume before scratch training')
def train_point(point,data):
import wandb
path=OUT/'checkpoints'/f'probe_{point:03d}'
if path.exists():upload(path);return
previous=OUT/'checkpoints'/f'probe_{point-1:03d}'
seed();model=ScratchJoint(previous);opt=optimizer_for(model);scheduler=scheduler_for(opt,data)
state=restore(previous,model,opt,scheduler);global_step=state['global_step']
info=data.schedule[point-1];pp,cp,n=data.epoch_plans[info['epoch']]
assert state['point']==point-1
assert global_step==info['global_step']-(info['batch_end']-info['batch_start'])
wb_path=OUT/'wandb.json';wb=json.loads(wb_path.read_text()) if wb_path.exists() else dict(run_id=wandb.util.generate_id())
write(wb_path,wb)
run=wandb.init(entity='queimo',project='rem3di-reproduction',id=wb['run_id'],resume='allow',name='scratch-pcqm-cosmo-ten-epochs',
group='rem3di-scratch-pcqm-cosmo',job_type='scratch-pretraining',config=CONFIG,dir=str(OUT),settings=wandb.Settings(init_timeout=90))
write(wb_path,dict(run_id=run.id,url=run.url));restore_rng(state)
model.train();logs=[];start=time.perf_counter()
try:
for step in range(info['batch_start'],info['batch_end']):
log=update(model,opt,data,pp[step%len(pp)],cp[step%len(cp)]);scheduler.step();global_step+=1;logs.append(log)
if (step+1)%20==0 or step+1==info['batch_end']:
status('training',point=point,epoch=info['epoch'],batch=step+1,batches=n,wandb_url=run.url)
run.log(dict(optimizer_step=global_step,epoch_position=info['epoch']-1+(step+1)/n,**{'train/'+k:v for k,v in log.items()},lr=scheduler.get_last_lr()[0]))
assert global_step==info['global_step']
validation=validate(model,data)
metrics={**info,'global_step':global_step,'train':{k:float(np.mean([r[k] for r in logs])) for k in logs[0]},
'validation':validation,'seconds':time.perf_counter()-start,
'epoch_coverage':dict(pcqm_unique=len(data.pt),cosmo_unique=len(data.ct)) if info['fifth']==5 else None}
saved=checkpoint(point,model,opt,scheduler,global_step,metrics)
status('uploading_checkpoint',point=point,epoch=info['epoch'])
hub=upload(saved);write(OUT/'latest_point.json',dict(**metrics,hub=hub))
run.log(dict(point=point,epoch_position=info['epoch_position'],**{'validation/'+k:v for k,v in validation.items()},hub_revision=hub['revision']))
run.finish()
except Exception:run.finish(exit_code=1);raise
def main():
parser=argparse.ArgumentParser();parser.add_argument('--point',type=int);parser.add_argument('--smoke',action='store_true');args=parser.parse_args()
lock=(OUT/'training.lock').open('a');fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
assert json.loads((OUT/'preparation_complete.json').read_text())['verified']
seed();data=TrainingData();write(OUT/'config.json',CONFIG)
if args.smoke:
if not (OUT/'smoke_validation.json').exists():smoke(data)
else:
assert json.loads((OUT/'smoke_validation.json').read_text())['passed']
assert 1<=args.point<=50;train_point(args.point,data)
if __name__=='__main__':
try:main()
except Exception:status('failed_training',error=traceback.format_exc());raise