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()