LittlePlyResearch notebook

train.py

python · 181 lines · Sanitized derivative

Line 1 ↗

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

Version & provenance · cm8-training-v1

Recorded SHA-256 42922b989fcffafe95f63cfa8e0a28ad97ae60134dc87d7de1f70cdefd134b6fRendered SHA-256 378dc454fc5ca63147bab620aa1065874bd23a1afd87e14656ac53bf03e984eb

Original LittlePly code: Apache-2.0. External Rust helper remains a separate GPL component. License notes. Line links are stable within this snapshot. Frozen research implementation; host paths redacted. Original and rendered digests identify the derivative; not a portable launcher or complete training payload.

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