LittlePlyResearch notebook

train.py

python · 183 lines · Sanitized derivative

Line 1 ↗

Sanitized derivative: private paths or personal test identities use explicit placeholders. Original line count is preserved.

Version & provenance · theory-cm7

Recorded SHA-256 50d9653f5c4ce231a1d25b0c08ffbec9bcd13d698b8cd73f81552f6269bf5b27Rendered SHA-256 33f0d01a63785ebbb580f3703f8c9cd4509fe37793be98915f38c18bf9d60abe

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"""Reviewed finite-schedule completion with a separately authorized deadline and loss selection."""
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 stage_data import open_curriculum,Plan,start_prefetch,ExposureLedger,phase,tensors
11from checkpoint import capture_rng,restore_rng
12from model import StateModel,optimizer
13from resume_state import validate_source,restore_optimizer_scheduler,select_loss
14from optimizer_policy import lr_factor
15from evaluation import evaluate
16from deadline import Deadline,read_deadline
17from records import sha,digest,atomic_json,Records as SmallRecords
18from runtime_records import Records
19sys.path.append(str(Path(__file__).resolve().parent.parent/'elite-continuation-v1'))
20from training_core import atomic_save,launch_owned_workers
21from curriculum_loss import weighted_loss
22from exact_metrics import summarize
23
24def verify(cfg,args):
25    if cfg.get('draft_only'):raise ValueError('draft configuration is not launchable')
26    for path,expected in cfg['runtime_sha256'].items():
27        if sha(path)!=expected:raise ValueError('runtime changed: '+path)
28    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')
29    if sha(cfg['source_checkpoint'])!=cfg['source_checkpoint_sha256']:raise ValueError('resume source changed')
30    for path,expected in cfg['validation_sha256'].items():
31        if sha(path)!=expected:raise ValueError('validation changed')
32    if args.resume and (not args.resume_sha256 or sha(args.resume)!=args.resume_sha256):raise ValueError('resume hash')
33
34def capacity(cfg,extra=1_200_000_000):
35    out=Path(cfg['output']);used=sum(p.stat().st_size for p in Path(cfg['original_root']).rglob('*') if p.is_file())
36    if used+extra>cfg['maximum_output_bytes']:raise RuntimeError('checkpoint output storage cap')
37    if shutil.disk_usage(out).free<cfg['native_free_reserve_bytes']+extra:raise RuntimeError('native storage reserve')
38
39def resource_check(cfg):
40    capacity(cfg)
41    if shutil.disk_usage(cfg['output']).free<cfg['native_free_reserve_bytes']+cfg['maximum_output_bytes']:raise RuntimeError('full run storage reservation')
42    health=json.loads(Path('/work/littleply-serving/state/health.json').read_text())
43    if not health.get('ready') or time.time()-health['updated']>180:raise RuntimeError('live bot not freshly ready')
44    gpu=subprocess.run(['nvidia-smi','--query-gpu=index,memory.free','--format=csv,noheader,nounits'],capture_output=True,text=True,check=True,timeout=15).stdout
45    free={int(line.split(',')[0]):int(line.split(',')[1]) for line in gpu.splitlines()}
46    if set(free)!={0,1} or min(free.values())<16384:raise RuntimeError('both GPUs require16GiB before mandatory probe')
47    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})
48
49def probe(net,data,device,rank,clock):
50    reserve=[torch.empty_like(p) for p in net.parameters() for _ in range(2)];out=[]
51    for micro in (16,32,64):
52        if clock.remaining()<600:raise TimeoutError('insufficient completion-stage probe budget')
53        rows=data.take(np.arange(micro));x,y,m=[v.to(device) for v in tensors(rows)]
54        torch.cuda.reset_peak_memory_stats(device);began=time.monotonic()
55        for _ in range(2):
56            with torch.autocast('cuda',dtype=torch.bfloat16):loss=torch.nn.functional.cross_entropy(net(x,m).float().reshape(-1,512),y.reshape(-1))
57            loss.backward();net.zero_grad(set_to_none=True)
58        torch.cuda.synchronize();free,_=torch.cuda.mem_get_info(device)
59        good=torch.tensor(int(free>=4*1024**3),device=device);dist.all_reduce(good,op=dist.ReduceOp.MIN)
60        if not good.item():raise RuntimeError('probe headroom below4GiB; no fallback')
61        out.append(dict(rank=rank,micro=micro,seconds=time.monotonic()-began,free_bytes=free,peak_reserved_bytes=torch.cuda.max_memory_reserved(device)))
62    del reserve
63    return out
64
65def worker(cfg,args):
66    rank=int(os.environ['RANK']);world=int(os.environ['WORLD_SIZE'])
67    if world!=2:raise ValueError('two ranks required')
68    device=torch.device('cuda',rank);torch.cuda.set_device(device);torch.set_num_threads(4)
69    random.seed(cfg['seed']+rank);np.random.seed(cfg['seed']+rank);torch.manual_seed(cfg['seed']+rank);torch.cuda.manual_seed(cfg['seed']+rank)
70    deadline_value=read_deadline(os.environ['CHESS_JOB_DEADLINE']);clock=Deadline(deadline_value)
71    dist.init_process_group('nccl',init_method='file://'+os.environ['CHESS_DDP_INIT_FILE'],rank=rank,world_size=world,timeout=timedelta(seconds=180))
72    data=None;prefetch=None;baseline_ref=None
73    try:
74        data=open_curriculum(cfg['dataset'],cfg['adapter_directory']);ledger=ExposureLedger(data);plan=Plan(data,rank,micro=64)
75        saved=torch.load(cfg['source_checkpoint'],map_location='cpu',weights_only=True)
76        validate_source(saved,cfg,deadline_value,ledger,plan)
77        raw=StateModel(saved['model_config']);raw.load_state_dict(saved['model_state_dict'],strict=True);raw.to(device);net=DDP(raw,device_ids=[rank],gradient_as_bucket_view=True)
78        initialization={'source_checkpoint_sha256':cfg['source_checkpoint_sha256'],'source_updates':saved['updates'],'source_cursor':saved['exposures']['cursor'],'resume':'model/Adam/scheduler/sampler/both-rank RNG; separately authorized completion deadline'}
79        proofs=probe(net,data,device,rank,clock);both=[None,None];dist.all_gather_object(both,proofs)
80        out=Path(cfg['output']);opt=optimizer(raw,cfg['lr'],fused=True);accum=4;total_updates=math.ceil(plan.steps/accum)
81        def scale(step):return lr_factor(step,total_updates,cfg['warmup_updates'])
82        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((27,3),dtype=torch.float64,device=device)
83        attempt='completion'
84        opt.retention_proof=restore_optimizer_scheduler(opt,sched,saved)
85        cursor=saved['microstep'];updates=saved['updates'];latest=saved['latest_validation'];best=saved['best_nll'];best_validation=saved['best_validation'];initial_validation=saved['initial_validation']
86        group_totals.copy_(saved['rank_group_totals'][rank].to(device));restore_rng(saved['rank_rng'][rank],device)
87        del saved
88        if args.resume:raise ValueError('single-attempt stage; no resume')
89        guard_failed=[];baseline_ref=None;baseline_validation=None
90        if rank==0:atomic_json(out/('probe-resume.json' if args.resume else 'probe.json'),{'ranks':both,'initialization':initialization})
91        dummy,prefetch=start_prefetch(data,plan,cursor,True)
92        log=(out/'metrics.jsonl').open('a',buffering=1) if rank==0 else None
93        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')
94        def checkpoint(name,terminal=False,is_best=False):
95            rngs=[None,None];dist.all_gather_object(rngs,capture_rng(device));rank_totals=[None,None];dist.all_gather_object(rank_totals,group_totals.cpu())
96            if rank==0:
97                capacity(cfg)
98                atomic_save(out/name,{'format':'cm7-theory-completion-v1','identity':cfg,'identity_sha256':digest(cfg),'deadline':deadline_value,'terminal':terminal,
99                    '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(),
100                    'rank_rng':rngs,'rank_group_totals':rank_totals,'exposures':ledger.snapshot(plan.cursor(cursor)),'microstep':cursor,'updates':updates,'best_nll':best,'latest_validation':latest,
101                    'best_checkpoint':best_ref,'is_best_snapshot':is_best,'attempt':attempt,'best_validation':best_validation,'initial_validation':initial_validation,'retention_failures':guard_failed,'baseline_checkpoint':baseline_ref,'optimizer_retention':opt.retention_proof})
102            dist.barrier()
103        def validation():
104            nonlocal latest,best,best_ref,best_validation
105            improved=False
106            if rank==0:
107                began=time.time();latest=evaluate(raw,cfg,device);latest.update(unix=time.time(),updates=updates,seconds=time.time()-began)
108                improved=select_loss(latest,best);log.write(json.dumps(dict(event='validation',retention_failures=[],accuracy_diagnostic_only=True,**latest))+'\n')
109            state=[latest,improved];dist.broadcast_object_list(state,0);latest,improved=state
110            if improved:
111                best_validation=latest;best=latest['nll'];name=f'best-{updates}-{attempt}.pt';checkpoint(name,is_best=True)
112                ref=[{'path':str(out/name),'sha256':sha(out/name)} if rank==0 else None];dist.broadcast_object_list(ref,0);best_ref=ref[0]
113                if rank==0:atomic_json(out/'best.json',best_ref)
114        # Carry the previous loss-selected best independently of last-state resume.
115        if rank==0:
116            prior=torch.load(cfg['prior_selected']['path'],map_location='cpu',weights_only=True)
117            if sha(cfg['prior_selected']['path'])!=cfg['prior_selected']['sha256'] or prior['latest_validation']['nll']!=best:raise ValueError('prior selected loss/hash')
118            prior.update(format='cm7-theory-completion-v1',identity=cfg,identity_sha256=digest(cfg),deadline=deadline_value,is_best_snapshot=True,terminal=False,attempt='inherited-loss-best')
119            capacity(cfg);atomic_save(out/'best-inherited.pt',prior);del prior
120        dist.barrier()
121        ref=[{'path':str(out/'best-inherited.pt'),'sha256':sha(out/'best-inherited.pt')} if rank==0 else None];dist.broadcast_object_list(ref,0);best_ref=baseline_ref=ref[0]
122        if rank==0:
123            from finalization import atomic
124            atomic(out/'baseline.json',{'checkpoint':baseline_ref,'config_sha256':digest(cfg)})
125            atomic_json(out/'best.json',best_ref);atomic_json(out/'initial-validation.json',initial_validation)
126            atomic_json(out/'resume-receipt.json',{'updates':updates,'microstep':cursor,'cursor':plan.cursor(cursor),'optimizer':opt.retention_proof,'source_checkpoint_sha256':cfg['source_checkpoint_sha256'],'authorized_completion_deadline':deadline_value,'prior_selected':cfg['prior_selected']})
127        dist.barrier()
128        last_time=time.monotonic();last_cursor=plan.cursor(cursor)
129        while cursor<plan.steps and not guard_failed:
130            stop=torch.tensor(int(clock.soft_stop()),device=device);dist.all_reduce(stop,op=dist.ReduceOp.MAX)
131            if stop.item():break
132            start_cursor=cursor;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)
133            wait_seconds=load_seconds=0.;began=time.monotonic()
134            for step in range(cursor,end):
135                t=time.monotonic();batch,tags,load=prefetch.take(step);wait_seconds+=time.monotonic()-t;load_seconds+=load;empty=len(batch[0])==0
136                x,y,mask=[v.to(device,non_blocking=True) for v in (dummy if empty else batch)]
137                with net.no_sync() if step<end-1 else nullcontext():
138                    with torch.autocast('cuda',dtype=torch.bfloat16):logits=net(x,mask)
139                    loss=torch.nn.functional.cross_entropy(logits.float().reshape(-1,512),y.reshape(-1),reduction='none').reshape(-1,3)
140                    weighted_loss(loss,world,denom,empty).backward()
141                if not empty:
142                    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())
143            torch.nn.utils.clip_grad_norm_(raw.parameters(),.5,error_if_nonfinite=True);opt.step();sched.step();cursor=end;updates+=1
144            if updates%50==0:
145                sums=group_totals.clone();dist.all_reduce(sums);torch.cuda.synchronize();now=time.monotonic();free,_=torch.cuda.mem_get_info(device)
146                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)
147                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')
148                last_time=now;last_cursor=plan.cursor(cursor)
149            boundary=any(plan.cursor(start_cursor)<b<=plan.cursor(cursor) for b in cfg['pass_boundaries'])
150            if (updates in (25,50) or updates%cfg['validation_every_updates']==0 or boundary) and clock.remaining()>120:validation()
151            if updates%cfg['checkpoint_every_updates']==0 and clock.remaining()>300:
152                name='diagnostic-1500.pt' if updates==1500 else 'progress.pt'
153                checkpoint(name)
154                if updates==1500 and rank==0:
155                    atomic_json(out/'diagnostic-1500.json',{'updates':1500,'config_sha256':digest(cfg),'checkpoint':{'path':str(out/name),'sha256':sha(out/name)}})
156        if not guard_failed and clock.remaining()>120 and latest['updates']!=updates:validation()
157        checkpoint('last.pt',True)
158        if rank==0:
159            initial=initial_validation
160            atomic_json(out/'exact-validation-comparison.json',{'initial':initial['exact_cluster_accuracy'],'selected':best_validation['exact_cluster_accuracy'],
161                'paired_game_cluster_delta':summarize(best_validation['exact_outcomes'],initial['exact_outcomes']),'selected_checkpoint':best_ref,'selection':'minimum fixed pooled canonical-label NLL; all quality accuracy/loss thresholds removed; finite numerical safety remains'})
162            atomic_json(out/'terminal.json',{'status':'complete','reason':'retention_guard' if guard_failed else ('single_schedule_complete' if cursor==plan.steps else 'original_deadline_soft_stop'),'retention_failures':guard_failed,'baseline_checkpoint':baseline_ref,'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()
163    except BaseException as error:
164        # No collectives or new GPU work on an uncertain rank failure. The
165        # immutable prepass remains the selected fallback; guardian owns cleanup.
166        if rank==0 and baseline_ref is not None:
167            atomic_json(Path(cfg['output'])/'best.json',baseline_ref)
168            atomic_json(Path(cfg['output'])/'failure.json',{'error_type':type(error).__name__,'error':str(error),'selected_fallback':baseline_ref,'unix':time.time(),'uncommitted_weights_not_certified':True,'no_promotion':True})
169        raise
170    finally:
171        if prefetch:prefetch.close()
172        if data:
173            for source in data.sources.values():
174                if hasattr(source,'close'):source.close()
175        dist.destroy_process_group()
176if __name__=='__main__':
177    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())
178    if 'CHESS_JOB_DEADLINE' not in os.environ:raise ValueError('reviewed owner required')
179    if 'RANK' in os.environ:worker(cfg,args)
180    else:
181        verify(cfg,args);resource_check(cfg);extra=[] if not args.resume else ['--resume',args.resume,'--resume-sha256',args.resume_sha256]
182        raise SystemExit(launch_owned_workers(Path(__file__),[args.config,*extra]))
183