File size: 11,493 Bytes
b296ad4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | """Deadline-aware single-GPU trainer. One resumable checkpoint, atomic replacement."""
import argparse
import contextlib
import json
import math
import os
import random
import shutil
import signal
import time
from datetime import datetime,timezone
from pathlib import Path
import numpy as np
import torch
from tinyquery.model import Config,TinyQuery
class Data:
def __init__(self,base,split,device):
self.ids=np.load(base/(split+'-ids.npy'),mmap_mode='r')
m=np.load(base/(split+'-meta.npz'))
self.lengths=m['lengths']; self.boundaries=m['boundaries']; self.actions=m['actions']; self.device=device
self.sample_weights=m['sample_weights'] if 'sample_weights' in m else np.ones(len(self.lengths))
assert np.isfinite(self.sample_weights).all() and (self.sample_weights>0).all()
self.buckets={}
# Narrower length groups reduce padding while preserving each row's sampling weight.
for limit in [128,192,256,320,384,448,512,640,768,1024,1536,2049]:
low=max([x for x in self.buckets]+[0])
indices=np.flatnonzero((self.lengths>low)&(self.lengths<=limit))
if len(indices): self.buckets[limit]=indices
self.keys=list(self.buckets); self.probs=np.array([self.sample_weights[self.buckets[k]].sum() for k in self.keys],dtype=float)
self.probs/=self.probs.sum()
self.within={k:self.sample_weights[v].astype(float)/self.sample_weights[v].sum() for k,v in self.buckets.items()}
def batch(self,size,rng):
limit=rng.choice(self.keys,p=self.probs); indices=rng.choice(self.buckets[limit],size=size,p=self.within[limit])
return self.from_indices(indices)
def from_indices(self,indices):
width=min(self.ids.shape[1],int(self.lengths[indices].max()))
raw=torch.tensor(np.array(self.ids[indices,:width],dtype=np.int64),device=self.device)
lengths=torch.tensor(self.lengths[indices],device=self.device)
boundaries=torch.tensor(self.boundaries[indices],device=self.device,dtype=torch.long)
actions=torch.tensor(self.actions[indices],device=self.device,dtype=torch.long)
x=raw[:,:-1]; y=raw[:,1:].clone()
positions=torch.arange(y.shape[1],device=self.device)[None,:]
y.masked_fill_(positions>=lengths[:,None]-1,-100)
weights=positions>=boundaries[:,None]
return x,y,weights,boundaries,actions,int((lengths-1).sum()),int((lengths-boundaries-1).sum())
def main():
p=argparse.ArgumentParser(); p.add_argument('--data',required=True); p.add_argument('--out',required=True)
p.add_argument('--minutes',type=float,default=150); p.add_argument('--deadline',help='Optional hard UTC deadline in ISO 8601 format')
p.add_argument('--batch',type=int,default=16); p.add_argument('--accum',type=int,default=2)
p.add_argument('--lr',type=float,default=0.0006); p.add_argument('--resume',action='store_true')
p.add_argument('--copy-dim',type=int,default=0); p.add_argument('--init-from')
p.add_argument('--steps',type=int,default=0); p.add_argument('--compile',action='store_true')
p.add_argument('--save-seconds',type=float,default=120)
p.add_argument('--width',type=int,default=1024); p.add_argument('--layers',type=int,default=12)
p.add_argument('--heads',type=int,default=16); p.add_argument('--kv-heads',type=int,default=4)
p.add_argument('--hidden',type=int,default=2816); p.add_argument('--prompt-weight',type=float,default=.15)
args=p.parse_args(); base=Path(args.data); out=Path(args.out); out.mkdir(parents=True,exist_ok=True)
torch.manual_seed(20260910); np.random.seed(20260910); random.seed(20260910)
device='cuda' if torch.cuda.is_available() else ('mps' if torch.backends.mps.is_available() else 'cpu')
if device=='cuda': torch.backends.cuda.matmul.allow_tf32=True
torch.set_num_threads(8)
token_info=json.loads((base/'tokenization.json').read_text())
config=Config(vocab_size=token_info['vocab_size'],width=args.width,layers=args.layers,heads=args.heads,
kv_heads=args.kv_heads,hidden=args.hidden,context=token_info['context'],copy_dim=args.copy_dim)
model=TinyQuery(config).to(device)
params=sum(p.numel() for p in model.parameters()); assert params<500_000_000
optimizer=torch.optim.AdamW(model.parameters(),lr=args.lr,betas=(.9,.95),weight_decay=.1,fused=device=='cuda')
step=0; processed=0; response_tokens=0; prior_seconds=0
rng=np.random.default_rng(42)
if args.resume:
checkpoint=torch.load(out/'last.pt',map_location=device,weights_only=False)
assert Config(**checkpoint['config']).to_dict()==config.to_dict()
model.load_state_dict(checkpoint['model']); optimizer.load_state_dict(checkpoint['optimizer'])
step=checkpoint['step']; processed=checkpoint['processed_tokens']; response_tokens=checkpoint['response_tokens']
prior_seconds=checkpoint.get('training_seconds',0); rng.bit_generator.state=checkpoint['rng']
del checkpoint
elif args.init_from:
checkpoint=torch.load(args.init_from,map_location=device,weights_only=False)
previous=Config(**checkpoint['config']).to_dict(); current=config.to_dict()
assert {k:v for k,v in previous.items() if k!='copy_dim'}=={k:v for k,v in current.items() if k!='copy_dim'}
missing,unexpected=model.load_state_dict(checkpoint['model'],strict=False)
assert not unexpected and all(name.startswith('copy_') for name in missing)
processed=checkpoint['processed_tokens']; response_tokens=checkpoint['response_tokens']
prior_seconds=checkpoint.get('training_seconds',0)
print(json.dumps({'event':'initialize_from_own_checkpoint','parent_step':checkpoint['step'],'new_parameters':missing}),flush=True)
del checkpoint
train=Data(base,'train',device); val=Data(base,'validation',device)
(out/'config.json').write_text(json.dumps(config.to_dict(),indent=2))
(out/'run-args.json').write_text(json.dumps(vars(args),indent=2))
runner=torch.compile(model,dynamic=True) if args.compile else model
start=time.time(); stop=min(start+args.minutes*60,datetime.fromisoformat(args.deadline).timestamp() if args.deadline else float('inf'))
if stop<=start: raise ValueError('Training deadline has already passed')
last_save=start; last_log=start; initial_step=step; initial_tokens=processed
stopping=False
def request_stop(signum,frame):
nonlocal stopping
stopping=True
signal.signal(signal.SIGTERM,request_stop)
signal.signal(signal.SIGINT,request_stop)
best_path=out/'best-info.json'
best_loss=json.loads(best_path.read_text())['response_loss'] if best_path.exists() else float('inf')
autocast=lambda: torch.autocast('cuda',dtype=torch.bfloat16) if device=='cuda' else contextlib.nullcontext()
def save(final=False):
expected=params*12+200_000_000
free=shutil.disk_usage(out).free
if free<expected: raise RuntimeError(f'Checkpoint needs about {expected} free bytes; only {free} available')
checkpoint={'config':config.to_dict(),'model':model.state_dict(),'optimizer':optimizer.state_dict(),
'step':step,'processed_tokens':processed,'response_tokens':response_tokens,
'training_seconds':prior_seconds+time.time()-start,'rng':rng.bit_generator.state,
'random_initialization':True}
temp=out/'last.tmp.pt'; torch.save(checkpoint,temp); os.replace(temp,out/'last.pt')
print(json.dumps({'event':'checkpoint','step':step,'final':final,'free_gb':shutil.disk_usage(out).free/1e9}),flush=True)
def validate():
model.eval(); numerator=torch.zeros(3);denominator=torch.zeros(3)
with torch.no_grad(),autocast():
for offset in range(0,len(val.lengths),32):
indices=np.arange(offset,min(offset+32,len(val.lengths)))
x,y,w,b,a,nt,nr=val.from_indices(indices)
loss,parts=model(x,y,w,b,a,prompt_weight=args.prompt_weight)
counts=torch.tensor([(nt-nr)*args.prompt_weight+nr,nr,len(indices)])
numerator+=parts.float().cpu()*counts;denominator+=counts
model.train(); values=(numerator/denominator).tolist()
print(json.dumps({'event':'validation','step':step,'loss':values,'examples':len(val.lengths),'method':'all_records_token_weighted'}),flush=True)
return values
def save_best(metrics):
nonlocal best_loss
if metrics[1]>=best_loss: return
from safetensors.torch import save_file
state={k:v.detach().cpu().to(torch.bfloat16).contiguous() for k,v in model.state_dict().items()}
temp=out/'best.tmp.safetensors'; save_file(state,str(temp),metadata={'step':str(step),'validation_response_loss':str(metrics[1]),'random_initialization':'true'}); os.replace(temp,out/'best.safetensors')
best_loss=metrics[1]
best_path.write_text(json.dumps({'step':step,'response_loss':best_loss,'validation':metrics},indent=2))
print(json.dumps({'event':'best','step':step,'response_loss':best_loss}),flush=True)
print(json.dumps({'event':'start','parameters':params,'device':device,'config':config.to_dict(),
'deadline':datetime.fromtimestamp(stop,timezone.utc).isoformat(),'validation':validate()}),flush=True)
with (out/'metrics.jsonl').open('a') as log:
model.train()
while time.time()<stop-35 and not stopping:
if args.steps and step-initial_step>=args.steps: break
progress=min(1,(time.time()-start)/(stop-start))
warm=min(1,(step+1)/100)
lr=args.lr*warm*(.1+.9*.5*(1+math.cos(math.pi*progress)))
for group in optimizer.param_groups: group['lr']=lr
optimizer.zero_grad(set_to_none=True); sums=torch.zeros(3,device=device)
for _ in range(args.accum):
x,y,w,b,a,nt,nr=train.batch(args.batch,rng)
with autocast(): loss,parts=runner(x,y,w,b,a,prompt_weight=args.prompt_weight)
(loss/args.accum).backward(); sums+=parts
processed+=nt; response_tokens+=nr
norm=torch.nn.utils.clip_grad_norm_(model.parameters(),1.0)
if not torch.isfinite(norm): raise RuntimeError('Non-finite gradient; refusing corrupt checkpoint')
optimizer.step(); step+=1
now=time.time()
if now-last_log>20 or step<=3:
entry={'step':step,'elapsed_seconds':prior_seconds+now-start,'processed_tokens':processed,
'response_tokens':response_tokens,'tokens_per_second':(processed-initial_tokens)/(now-start),
'loss':(sums/args.accum).tolist(),'lr':lr,'gradient_norm':float(norm)}
log.write(json.dumps(entry)+'\n'); log.flush(); print(json.dumps(entry),flush=True); last_log=now
if now-last_save>args.save_seconds:
val_metrics=validate(); save_best(val_metrics); save(); last_save=time.time()
final_val=validate(); save_best(final_val); save(final=True)
summary={'parameters':params,'step':step,'processed_tokens':processed,'response_tokens':response_tokens,
'training_seconds':prior_seconds+time.time()-start,'validation':final_val,
'random_initialization':True,'device':device}
(out/'summary.json').write_text(json.dumps(summary,indent=2)); print(json.dumps(summary),flush=True)
if __name__=='__main__': main()
|