Download source/scratch_train.py from Queimo/rem3di-scratch-pcqm-cosmo: direct link, hf CLI and curl.
- Browser
- Download file 17.4 kB
-
https://huggingface.co/Queimo/rem3di-scratch-pcqm-cosmo/resolve/main/source/scratch_train.py
- Command line
-
hf download hf://Queimo/rem3di-scratch-pcqm-cosmo/source/scratch_train.py
-
curl -L -o scratch_train.py https://huggingface.co/Queimo/rem3di-scratch-pcqm-cosmo/resolve/main/source/scratch_train.py
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 | |
| 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 | |