"""V4.5 ensemble distillation: a V4.2-recipe 15M student trained on a four-seed teacher ensemble. Each student is the exact twin of one CE-trained V4.2-recipe seed (`TWINS`): same configuration, seed, initialization and frozen 20-epoch mixture, so the only change is the target. The loss per supervised token is (1 - weight) * CE(label) + weight * CE(teacher distribution), where the teacher distribution pools the four frozen epoch-20 teachers either as a normalized geometric mean ('geo', the token-level form of the summed-logP ranking ensemble) or as an arithmetic mixture ('mix'). Temperature is 1. """ import copy import hashlib import json import math from pathlib import Path import random import signal import time import tomllib import torch import torch.nn.functional as F from vimeml.training.data import SentenceWindowDataset,file_sha,write_json from vimeml.training.data_mixture import mixture_dataset from vimeml.training.model_v4 import TinyGPTV4,GPTV4Config from vimeml.training.train import (optimizer_for,learning_rate,atomic_checkpoint,rng_state, restore_rng,resolve_precision,amp_context) from vimeml.posttraining.streams import CommittedStream,loader_for,microbatches from vimeml.posttraining.train import validate from vimeml.posttraining.data import TOKENIZER_SHA ROOT=Path(__file__).resolve().parents[3] STAGE='v4.5' MIXTURE_EPOCHS=20 DISTILLATION_WEIGHT=0.5 POOLS=('geo','mix') # Frozen teachers: epoch 20 of the four V4.2-recipe seeds, fetched from the private HF # training-output repos at fixed revisions by prepare_v45_teachers.py. TEACHERS={ 'v42':{'repo_id':'Voltline/vimeml-v42-training-output','revision':'99d914882ae7410bbd99d08090b7d59b3becbded', 'path':'checkpoints/best-lm.pt','step':1081942, 'sha256':'0e83ea7417b9816b37183044468343b151f4e610da0764c31f1f780c5fa5796d'}, 's1':{'repo_id':'Voltline/vimeml-v44-training-output','revision':'a77fd281907b24919ab3e1fa0a6feabc896096fc', 'path':'s1/checkpoints/epoch-20.pt','step':1081944, 'sha256':'31cc81207de132b9c8c51f975ef8a550d50f35196401c69b24f70f46f4502338'}, 's2':{'repo_id':'Voltline/vimeml-v44-training-output','revision':'a77fd281907b24919ab3e1fa0a6feabc896096fc', 'path':'s2/checkpoints/epoch-20.pt','step':1081940, 'sha256':'dd8a33acee77981cdb3b07b602bbdd665f6db058eed505e8bcb06a54859040cb'}, 's3':{'repo_id':'Voltline/vimeml-v44-training-output','revision':'a77fd281907b24919ab3e1fa0a6feabc896096fc', 'path':'s3/checkpoints/epoch-20.pt','step':1081937, 'sha256':'bac6f0fe8d20ad643b25ff8291599f74b4274efa79965dbe366972dbb6b5786e'}, } TEACHER_DIR='artifacts/teachers/v45' # Twin configuration of each student seed; the twin is one of the teachers. TWINS={'v42':'configs/train-v42.toml','s1':'configs/train-v44-s1.toml', 's2':'configs/train-v44-s2.toml','s3':'configs/train-v44-s3.toml'} ARMS={f'{pool}-{seed}':(pool,seed) for pool in POOLS for seed in TWINS} # Fields a student may differ from its twin in; everything else must be identical. STUDENT_KEYS=('stage','arm','output_dir','log_dir','tracking','distillation') TASK_CONTRACT={ 'objective':'ensemble_distillation_token_normalized', 'tasks':['old_web_lm','chat_lm'], 'normalization':'total supervised tokens per update', 'lm':'(1-w)*CE(label)+w*CE(pooled teacher distribution) on all valid targets', 'distillation_weight':DISTILLATION_WEIGHT,'temperature':1.0, 'teachers':{k:v['sha256'] for k,v in TEACHERS.items()}, 'teacher_precision':'BF16 autocast, frozen eval mode; pooled in FP32', 'conversion':'none; V4.1 conversion pool removed', 'ime_validation':'none_in_training; deferred post-training over saved epoch checkpoints', 'context_length':128,'tokenizer_sha256':TOKENIZER_SHA, } def teacher_file(name,root=ROOT): return Path(root)/TEACHER_DIR/f'{name}.pt' def twin_config(seed,root=ROOT): return tomllib.loads((Path(root)/TWINS[seed]).read_text('utf-8')) def strip_student(config): config={k:copy.deepcopy(v) for k,v in config.items() if k not in STUDENT_KEYS} config['data_mixture'].pop('chat_fraction',None) config['training'].pop('lm_microbatch',None) config['training'].pop('smoke_updates',None) return config def read(path): return json.loads(Path(path).read_text('utf-8')) def build_plan(config,root=ROOT,verify_files=True): root=Path(root) if config.get('stage')!=STAGE or config.get('architecture')!='tiny_gpt_v4' or config.get('initialization')!={'mode':'random'}: raise ValueError('V4.5 students start from random initialization (their twin\'s)') GPTV4Config(**config['model']) s=dict(config['training']) arm=config.get('arm') if arm not in ARMS: raise ValueError('Unknown V4.5 student arm') pool,seed=ARMS[arm] if config.get('distillation')!={'pool':pool,'twin':seed,'weight':DISTILLATION_WEIGHT, 'teachers':sorted(TEACHERS)}: raise ValueError(f'V4.5 {arm} distillation block differs from its registered arm') twin=twin_config(seed,root) if strip_student(config)!=strip_student(twin): raise ValueError(f'V4.5 {arm} differs from its twin {TWINS[seed]} outside the student fields') if not 1<=s['epochs']<=MIXTURE_EPOCHS: raise ValueError('V4.5 uses at most the 20-epoch mixture plan') for key in ('lm_batch','lm_microbatch','epochs','num_workers','cpu_threads','log_every', 'eval_every','checkpoint_every','validation_batch','bucket_multiplier','warmup_steps'): if type(s[key]) is not int or s[key]<1: raise ValueError(f'Invalid training integer: {key}') if type(s.get('smoke_updates')) is not int or s['smoke_updates']<0: raise ValueError('Invalid smoke update budget') if type(s['seed']) is not int or not 0<=s['seed']<2**63: raise ValueError('Invalid random seed') for key in ('learning_rate','min_learning_rate','beta1','beta2','weight_decay','grad_clip'): if not math.isfinite(s[key]) or s[key]<=0: raise ValueError(f'Invalid training scalar: {key}') compiled=config['runtime']['compile_backbone'] if (s['device']=='cuda' and s['precision']=='bf16' and compiled): pass elif (s['device']=='cpu' and s['precision']=='fp32' and not compiled and s['smoke_updates']>0): pass else: raise ValueError('V4.5 formal training is CUDA/BF16/compiled; only smoke runs are CPU/FP32') if (not 0=1 or s['beta2']>=1 or not 0<=s['prefix_crop_probability']<1 or s['lm_microbatch']>s['lm_batch']): raise ValueError('V4.5 optimizer/microbatch contract mismatch') mix_dir=root/config['data_mixture']['directory'] mix=read(mix_dir/'manifest.json') request=mix['request'] # The twin's own frozen mixture (V4.2's, or the V4.4 seed arm's), pinned by its manifest SHA. if (mix['status']!='complete' or len(mix['epochs'])!=request['epochs'] or request['epochs']updates[e]): raise ValueError('Invalid committed LM epoch cursor') if state['step']!=sum(updates[:e])+b: raise ValueError('Step does not match committed epoch updates') if state['totals']['lm']['examples']!=consumed_examples(state,plan): raise ValueError('LM exposure does not match committed updates') if state['lm_cursor']!={'epoch':e,'batch':b,'offset':0}: raise ValueError('LM stream cursor mismatch') epochs=[r['epoch'] for r in state['epoch_results']] allowed=list(range(1,e+1)) if e=s['smoke_updates']: break if s['smoke_updates'] and state['step']>=s['smoke_updates']: break if requested: break if not any(r['epoch']==e+1 for r in state['epoch_results']): save() write_json(output/'progress.json',progress(state,plan,'validating')) print(f'[epoch {e+1}] full old LM and chat validation',flush=True) before=rng_state() with fp32_evaluation(model): old_stats=read(root/config['token_dir']/'manifest.json')['splits']['validation'] chat_stats=read(root/chat_bank['token_dir']/'manifest.json')['splits']['validation'] lv=validate(model,old_validation,device,old_stats['prediction_pairs'],old_stats['characters']) chat=validate(model,chat_validation,device,chat_stats['prediction_pairs'],chat_stats['characters']) restore_rng(before) report={'epoch':e+1,'step':state['step'],'lm':lv,'chat':chat} roles=[] if state['best_lm'] is None or lv['bpc']=s['smoke_updates']: save() terminal='smoke_complete' else: terminal='complete' if state['epoch']==s['epochs'] else 'interrupted' save() result=progress(state,plan,terminal) result['checkpoints']={n:str(output/f'{n}.pt') for n in ('best-lm','last') if (output/f'{n}.pt').exists()} result['epoch_checkpoints']=sorted(p.name for p in output.glob('epoch-*.pt')) if device.type=='cuda': result['peak_allocated_MiB']=torch.cuda.max_memory_allocated(device)/2**20 result['peak_reserved_MiB']=torch.cuda.max_memory_reserved(device)/2**20 write_json(output/'summary.json',result) write_json(output/'progress.json',result) return result except BaseException as error: write_json(output/'failure.json',{'status':'failed','active':True,'error_type':type(error).__name__, 'error':str(error),'last_committed_in_memory_step':state['step'],'safe_resume_checkpoint':str(output/'last.pt')}) raise finally: writer.close();metrics.close() for sig,handler in handlers.items(): signal.signal(sig,handler) lm_train.close() for data in (lm_val,chat_val): data.close()