train.py
python · 201 lines · Sanitized derivative
Sanitized derivative: private paths or personal test identities use explicit placeholders. Original line count is preserved.
Version & provenance · curriculum-cm7
Recorded SHA-256 a8360c18dcdf9c2e4234f67b9d8bfe4263024ef1b24c845887545bbd85414684Rendered SHA-256 31f107fcf8a1cfbbe0cff526c473ce6bfdb385fd4255bf259c5a81a6d4843681
Apache-2.0 original project code; dependencies retain their own licenses. License notes. Line links are stable within this snapshot. CM-7 completed implementation source; original and public rendered hashes recorded. Host-specific paths use /work placeholders; inspect reproduction instructions before running. Separate Rust helper retains its GPL license.
1"""One sealed curriculum pass, two ranks, original eight-hour deadline on resume."""
2import argparse,json,math,os,sys,time,random,shutil,subprocess
3from pathlib import Path
4from contextlib import nullcontext
5from datetime import timedelta
6import numpy as np
7import torch
8import torch.distributed as dist
9from torch.nn.parallel import DistributedDataParallel as DDP
10from composite import open_curriculum,Plan,start_prefetch,ExposureLedger,phase,tensors
11from checkpoint import capture_rng,restore_rng
12from training_components import initialize,optimizer
13from deadline import Deadline,read_deadline
14from records import sha,digest,atomic_json,Records as SmallRecords
15from runtime_records import Records
16sys.path.insert(0,str(Path(__file__).resolve().parent.parent/'elite-continuation-v1'))
17from training_core import atomic_save,launch_owned_workers
18from curriculum_loss import weighted_loss
19from exact_metrics import summarize
20
21def verify(cfg,args):
22 for path,expected in cfg['runtime_sha256'].items():
23 if sha(path)!=expected:raise ValueError('runtime changed: '+path)
24 if sha(Path(cfg['dataset'])/'manifest.json')!=cfg['sealed_dataset_manifest_sha256'] or sha(Path(cfg['dataset'])/'schedule.bin')!=cfg['schedule_sha256']:raise ValueError('sealed curriculum changed')
25 if sha(cfg['parent'])!=cfg['parent_sha256']:raise ValueError('parent changed')
26 for path,expected in cfg['validation_sha256'].items():
27 if sha(path)!=expected:raise ValueError('validation changed')
28 if args.resume and (not args.resume_sha256 or sha(args.resume)!=args.resume_sha256):raise ValueError('resume hash')
29
30def capacity(cfg,extra=1_200_000_000):
31 out=Path(cfg['output']);used=sum(p.stat().st_size for p in out.iterdir() if p.is_file())
32 if used+extra>cfg['maximum_output_bytes']:raise RuntimeError('checkpoint output storage cap')
33 if shutil.disk_usage(out).free<cfg['native_free_reserve_bytes']+extra:raise RuntimeError('native storage reserve')
34
35def resource_check(cfg):
36 capacity(cfg)
37 if shutil.disk_usage(cfg['output']).free<cfg['native_free_reserve_bytes']+cfg['maximum_output_bytes']:raise RuntimeError('full run storage reservation')
38 health=json.loads(Path('/work/littleply-serving/state/health.json').read_text())
39 if not health.get('ready') or time.time()-health['updated']>180:raise RuntimeError('live bot not freshly ready')
40 gpu=subprocess.run(['nvidia-smi','--query-gpu=index,memory.free','--format=csv,noheader,nounits'],capture_output=True,text=True,check=True,timeout=15).stdout
41 free={int(line.split(',')[0]):int(line.split(',')[1]) for line in gpu.splitlines()}
42 if set(free)!={0,1} or min(free.values())<16384:raise RuntimeError('both GPUs require16GiB before mandatory probe')
43 atomic_json(Path(cfg['output'])/'resource-preflight.json',{'unix':time.time(),'free_mib':free,'bot_worker':health.get('worker_pid'),'bot_checkpoint':health.get('checkpoint_sha256'),'bot_ready':True,'native_free_bytes':shutil.disk_usage(cfg['output']).free})
44
45def probe(net,data,device,rank,clock):
46 reserve=[torch.empty_like(p) for p in net.parameters() for _ in range(2)];out=[]
47 for micro in (16,32,64):
48 if clock.remaining()<600:raise TimeoutError('insufficient original probe budget')
49 rows=data.take(np.arange(micro));x,y,m=[v.to(device) for v in tensors(rows)]
50 torch.cuda.reset_peak_memory_stats(device);began=time.monotonic()
51 for _ in range(2):
52 with torch.autocast('cuda',dtype=torch.bfloat16):loss=torch.nn.functional.cross_entropy(net(x,m).float().reshape(-1,512),y.reshape(-1))
53 loss.backward();net.zero_grad(set_to_none=True)
54 torch.cuda.synchronize();free,_=torch.cuda.mem_get_info(device)
55 good=torch.tensor(int(free>=4*1024**3),device=device);dist.all_reduce(good,op=dist.ReduceOp.MIN)
56 if not good.item():raise RuntimeError('probe headroom below4GiB; no fallback')
57 out.append(dict(rank=rank,micro=micro,seconds=time.monotonic()-began,free_bytes=free,peak_reserved_bytes=torch.cuda.max_memory_reserved(device)))
58 del reserve
59 return out
60
61def evaluate(net,cfg,device):
62 net.eval();groups={};total_loss=0.;total_n=0;exact_outcomes=[]
63 with torch.inference_mode():
64 for spec in cfg['validation_groups']:
65 cls=Records if spec['kind']==0 else SmallRecords
66 source=cls(spec['root'],spec['source'],spec['split'],verify=True)
67 metadata=None
68 if spec['kind']==1:
69 metadata=[json.loads(x) for x in Path(spec['tie_metadata']).read_text().splitlines()]
70 for start in range(0,len(source),64):
71 rows=source.take(range(start,min(start+64,len(source))));x,y,m=[v.to(device) for v in tensors(rows)]
72 with torch.autocast('cuda',dtype=torch.bfloat16,enabled=device.type=='cuda'):logits=net(x,m)
73 losses=torch.nn.functional.cross_entropy(logits.float().reshape(-1,512),y.reshape(-1),reduction='none').reshape(-1,3).sum(-1).cpu()
74 correct=(logits.argmax(-1)==y).sum(0).cpu().tolist()
75 a=groups.setdefault(spec['name'],dict(decisions=0,loss_sum=0.,correct=[0,0,0],kind=spec['kind'],source=spec['source'],phase_counts=[0,0,0],phase_loss_sums=[0.,0.,0.],raw_tie_preserving=0))
76 a['decisions']+=len(rows);a['loss_sum']+=float(losses.sum());a['correct']=[v+w for v,w in zip(a['correct'],correct)]
77 a['phase_counts']=[v+int(np.sum(phase(rows)==i)) for i,v in enumerate(a['phase_counts'])]
78 a['phase_loss_sums']=[v+float(losses[torch.from_numpy(phase(rows)==i)].sum()) for i,v in enumerate(a['phase_loss_sums'])]
79 total_loss+=float(losses.sum());total_n+=len(rows)
80 if metadata is not None:
81 prefix=x[:,:120];mask=m[:,:120];generated=[]
82 for _ in range(3):
83 with torch.autocast('cuda',dtype=torch.bfloat16,enabled=device.type=='cuda'):token=net.next_logits(prefix,mask).argmax(-1)
84 generated.append(token.cpu().tolist());prefix=torch.cat((prefix,token[:,None]),1);mask=torch.cat((mask,torch.ones_like(token[:,None],dtype=mask.dtype)),1)
85 for i,triple in enumerate(zip(*generated)):
86 item=metadata[int(rows[i]['game'])];hit=int(list(triple) in item['tied_target_triples']);a['raw_tie_preserving']+=hit
87 exact_outcomes.append({'key':item['position_key'],'game':item['source_sequence_sha256'],'correct':hit})
88 if hasattr(source,'close'):source.close()
89 if not total_n:raise ValueError('empty frozen validation')
90 for a in groups.values():
91 a['nll']=a['loss_sum']/(3*a['decisions']);a['phase_accuracy']=[v/a['decisions'] for v in a['correct']]
92 a['raw_tie_preserving_fraction']=a['raw_tie_preserving']/a['decisions'] if a['kind']==1 else None
93 a['phase_nll']=[s/(3*n) if n else None for s,n in zip(a['phase_loss_sums'],a['phase_counts'])]
94 net.train();return dict(nll=total_loss/(3*total_n),decisions=total_n,groups=groups,exact_outcomes=exact_outcomes,exact_cluster_accuracy=summarize(exact_outcomes),nll_definition='canonical-label CE; exact theory additionally reports autoregressive raw tie-set preservation')
95
96def worker(cfg,args):
97 rank=int(os.environ['RANK']);world=int(os.environ['WORLD_SIZE'])
98 if world!=2:raise ValueError('two ranks required')
99 device=torch.device('cuda',rank);torch.cuda.set_device(device);torch.set_num_threads(4)
100 random.seed(cfg['seed']+rank);np.random.seed(cfg['seed']+rank);torch.manual_seed(cfg['seed']+rank);torch.cuda.manual_seed(cfg['seed']+rank)
101 deadline_value=read_deadline(os.environ['CHESS_JOB_DEADLINE']);clock=Deadline(deadline_value)
102 dist.init_process_group('nccl',init_method='file://'+os.environ['CHESS_DDP_INIT_FILE'],rank=rank,world_size=world,timeout=timedelta(seconds=180))
103 data=None;prefetch=None
104 try:
105 data=open_curriculum(cfg['dataset'],cfg['adapter_directory']);ledger=ExposureLedger(data);plan=Plan(data,rank,micro=64)
106 raw,initialization=initialize(cfg['parent']);raw.to(device);net=DDP(raw,device_ids=[rank],gradient_as_bucket_view=True)
107 proofs=probe(net,data,device,rank,clock);both=[None,None];dist.all_gather_object(both,proofs)
108 out=Path(cfg['output']);opt=optimizer(raw,cfg['lr'],fused=True);accum=4;total_updates=math.ceil(plan.steps/accum)
109 def scale(step):
110 if step<cfg['warmup_updates']:return (step+1)/cfg['warmup_updates']
111 f=min(1.,(step-cfg['warmup_updates'])/max(1,total_updates-cfg['warmup_updates']))
112 return .1+.9*.5*(1+math.cos(math.pi*f))
113 sched=torch.optim.lr_scheduler.LambdaLR(opt,scale);cursor=updates=0;best=float('inf');best_ref=None;best_validation=None;initial_validation=None;latest=None;group_totals=torch.zeros((12,3),dtype=torch.float64,device=device)
114 attempt='initial' if not args.resume else args.resume_sha256[:16]
115 if args.resume:
116 saved=torch.load(args.resume,map_location='cpu',weights_only=True)
117 if saved['format']!='cm6-curriculum-checkpoint-v1' or saved['identity']!=cfg or saved['deadline']!=deadline_value or saved['terminal']:raise ValueError('incompatible/terminal resume')
118 if len(saved['rank_rng'])!=2:raise ValueError('missing rank RNG')
119 consumed=ledger.verify(saved['exposures']);cursor=saved['microstep'];updates=saved['updates']
120 if plan.cursor(cursor)!=consumed or updates!=math.ceil(cursor/accum):raise ValueError('cursor/update mismatch')
121 raw.load_state_dict(saved['model_state_dict']);opt.load_state_dict(saved['optimizer_state_dict']);sched.load_state_dict(saved['scheduler_state_dict'])
122 best=saved['best_nll'];latest=saved['latest_validation'];group_totals.copy_(saved['rank_group_totals'][rank].to(device));restore_rng(saved['rank_rng'][rank],device)
123 best_validation=saved['best_validation']
124 initial_validation=saved['initial_validation']
125 best_ref=({'path':str(Path(args.resume).resolve()),'sha256':args.resume_sha256} if saved['is_best_snapshot'] else saved['best_checkpoint'])
126 if best_ref is None or sha(best_ref['path'])!=best_ref['sha256']:raise ValueError('missing or changed immutable best checkpoint')
127 del saved
128 if rank==0:atomic_json(out/('probe-resume.json' if args.resume else 'probe.json'),{'ranks':both,'initialization':initialization})
129 dummy,prefetch=start_prefetch(data,plan,cursor,True)
130 log=(out/'metrics.jsonl').open('a',buffering=1) if rank==0 else None
131 if rank==0:log.write(json.dumps({'event':'attempt_start','attempt':attempt,'unix':time.time(),'committed_schedule_cursor':plan.cursor(cursor),'resume_sha256':args.resume_sha256,'accounting':'cursor is restored committed exposure count; superseded attempt logs retain possibly replayed uncommitted work'})+'\n')
132 def checkpoint(name,terminal=False,is_best=False):
133 rngs=[None,None];dist.all_gather_object(rngs,capture_rng(device));rank_totals=[None,None];dist.all_gather_object(rank_totals,group_totals.cpu())
134 if rank==0:
135 capacity(cfg)
136 atomic_save(out/name,{'format':'cm6-curriculum-checkpoint-v1','identity':cfg,'identity_sha256':digest(cfg),'deadline':deadline_value,'terminal':terminal,
137 'model_config':raw.net.config.to_dict(),'model_state_dict':raw.state_dict(),'optimizer_state_dict':opt.state_dict(),'scheduler_state_dict':sched.state_dict(),
138 'rank_rng':rngs,'rank_group_totals':rank_totals,'exposures':ledger.snapshot(plan.cursor(cursor)),'microstep':cursor,'updates':updates,'best_nll':best,'latest_validation':latest,
139 'best_checkpoint':best_ref,'is_best_snapshot':is_best,'attempt':attempt,'best_validation':best_validation,'initial_validation':initial_validation})
140 dist.barrier()
141 def validation():
142 nonlocal latest,best,best_ref,best_validation,initial_validation
143 improved=False
144 if rank==0:
145 began=time.time();latest=evaluate(raw,cfg,device);latest.update(unix=time.time(),updates=updates,seconds=time.time()-began)
146 improved=latest['nll']<best;best=min(best,latest['nll']);log.write(json.dumps(dict(event='validation',**latest))+'\n')
147 if updates==0:atomic_json(out/'initial-validation.json',latest)
148 flag=torch.tensor(int(improved),device=device);dist.broadcast(flag,0)
149 state=[latest,best];dist.broadcast_object_list(state,0);latest,best=state
150 if updates==0:initial_validation=latest
151 if flag.item():
152 best_validation=latest
153 name=f'best-{updates}-{attempt}.pt'
154 checkpoint(name,is_best=True)
155 ref=[{'path':str(out/name),'sha256':sha(out/name)} if rank==0 else None];dist.broadcast_object_list(ref,0);best_ref=ref[0]
156 if rank==0:atomic_json(out/'best.json',best_ref)
157 if not args.resume:validation()
158 last_time=time.monotonic();last_cursor=plan.cursor(cursor)
159 while cursor<plan.steps:
160 stop=torch.tensor(int(clock.soft_stop()),device=device);dist.all_reduce(stop,op=dist.ReduceOp.MAX)
161 if stop.item():break
162 end=min(cursor+accum,plan.steps);denom=3*sum(plan.global_decisions(i) for i in range(cursor,end));opt.zero_grad(set_to_none=True)
163 wait_seconds=load_seconds=0.;began=time.monotonic()
164 for step in range(cursor,end):
165 t=time.monotonic();batch,tags,load=prefetch.take(step);wait_seconds+=time.monotonic()-t;load_seconds+=load;empty=len(batch[0])==0
166 x,y,mask=[v.to(device,non_blocking=True) for v in (dummy if empty else batch)]
167 with net.no_sync() if step<end-1 else nullcontext():
168 with torch.autocast('cuda',dtype=torch.bfloat16):logits=net(x,mask)
169 loss=torch.nn.functional.cross_entropy(logits.float().reshape(-1,512),y.reshape(-1),reduction='none').reshape(-1,3)
170 weighted_loss(loss,world,denom,empty).backward()
171 if not empty:
172 tag=tags.to(device);group_totals[:,0].scatter_add_(0,tag,torch.ones(len(tag),dtype=torch.float64,device=device));group_totals[:,1].scatter_add_(0,tag,loss.detach().sum(-1).double());group_totals[:,2].scatter_add_(0,tag,(logits.argmax(-1)==y).sum(-1).double())
173 torch.nn.utils.clip_grad_norm_(raw.parameters(),1.,error_if_nonfinite=True);opt.step();sched.step();cursor=end;updates+=1
174 if updates%50==0:
175 sums=group_totals.clone();dist.all_reduce(sums);torch.cuda.synchronize();now=time.monotonic();free,_=torch.cuda.mem_get_info(device)
176 mem=torch.tensor([free,torch.cuda.max_memory_reserved(device)],dtype=torch.int64,device=device);mems=[torch.empty_like(mem) for _ in range(2)];dist.all_gather(mems,mem)
177 if rank==0:log.write(json.dumps({'event':'training','attempt':attempt,'unix':time.time(),'updates':updates,'schedule_cursor':plan.cursor(cursor),'prediction_targets':3*plan.cursor(cursor),'window_decisions_per_second':(plan.cursor(cursor)-last_cursor)/(now-last_time),'last_update_wall_seconds':now-began,'cpu_wait_seconds':wait_seconds,'cpu_load_seconds':load_seconds,'lr':sched.get_last_lr()[0],'remaining_seconds':clock.remaining(),'source_phase_kind_totals':sums.cpu().tolist(),'rank_free_peak_bytes':[v.cpu().tolist() for v in mems]})+'\n')
178 last_time=now;last_cursor=plan.cursor(cursor)
179 if updates%cfg['validation_every_updates']==0 and clock.remaining()>600:validation()
180 if updates%cfg['checkpoint_every_updates']==0 and clock.remaining()>300:checkpoint('progress.pt')
181 if clock.remaining()>360:validation()
182 checkpoint('last.pt',True)
183 if rank==0:
184 initial=initial_validation
185 atomic_json(out/'exact-validation-comparison.json',{'initial':initial['exact_cluster_accuracy'],'selected':best_validation['exact_cluster_accuracy'],
186 'paired_game_cluster_delta':summarize(best_validation['exact_outcomes'],initial['exact_outcomes']),'selected_checkpoint':best_ref,'selection':'pooled frozen canonical-label validation NLL, including step zero'})
187 atomic_json(out/'terminal.json',{'status':'complete','reason':'single_schedule_complete' if cursor==plan.steps else 'original_deadline_soft_stop','updates':updates,'schedule_cursor':plan.cursor(cursor),'remaining_schedule_entries':len(data.schedule)-plan.cursor(cursor),'prediction_targets':3*plan.cursor(cursor),'best_nll':best,'latest_validation':latest,'best_checkpoint':best_ref,'last_sha256':sha(out/'last.pt'),'finished_unix':time.time(),'no_promotion':True});log.close()
188 finally:
189 if prefetch:prefetch.close()
190 if data:
191 for source in data.sources.values():
192 if hasattr(source,'close'):source.close()
193 dist.destroy_process_group()
194if __name__=='__main__':
195 p=argparse.ArgumentParser();p.add_argument('config');p.add_argument('--resume');p.add_argument('--resume-sha256');args=p.parse_args();cfg=json.loads(Path(args.config).read_text())
196 if 'CHESS_JOB_DEADLINE' not in os.environ:raise ValueError('reviewed owner required')
197 if 'RANK' in os.environ:worker(cfg,args)
198 else:
199 verify(cfg,args);resource_check(cfg);extra=[] if not args.resume else ['--resume',args.resume,'--resume-sha256',args.resume_sha256]
200 raise SystemExit(launch_owned_workers(Path(__file__),[args.config,*extra]))
201