LittlePlyResearch notebook

prepare.py

python · 139 lines · Exact bytes

Line 1 ↗
Version & provenance · state-v2-cm7

Recorded SHA-256 5c6e09e662cb0f66e0506f628b952c6b51fe0026dc1a0c6f255422260c98779dRendered SHA-256 5c6e09e662cb0f66e0506f628b952c6b51fe0026dc1a0c6f255422260c98779d

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"""Bounded CPU preparation from immutable, already-tokenized game corpora.
2
3No source filters are added. Entire games are replay-validated before positions
4are committed. All historical validation/test positions are protected, including
5terminal positions. Training dedup ignores clocks/history as does protection.
6"""
7import argparse,concurrent.futures,fcntl,hashlib,json,os,sys,time
8from pathlib import Path
9import numpy as np
10import codec as c
11from records import Store,DTYPE,canonical,digest,sha,atomic_json
12sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'elite-continuation-v1'))
13from chessdata.reader import _manifest,_read_shard
14
15def aliases(meta):
16    ids=set(meta.get('game_ids',[]))
17    if meta.get('game_id'):ids.add(meta['game_id'])
18    return ['seq:'+meta['sequence_sha256']]+['id:'+v for v in sorted(ids)]
19
20def replay(task):
21    tokens,meta,mode,seed,source=task
22    if tokens[0]!=c.BOS or tokens[-1]!=c.EOS or (len(tokens)-2)%3:raise ValueError('game boundaries')
23    b=c.chess.Board();moves=[];keys=[];rows=[];terminal_at=None
24    for ply,start in enumerate(range(1,len(tokens)-1,3)):
25        move=c.decode(list(tokens[start:start+3]))
26        if move not in b.legal_moves:raise ValueError('illegal source game')
27        if terminal_at is None and b.is_game_over(claim_draw=False):terminal_at=ply
28        keys.append(c.position_key(b));moves.append(move.uci())
29        if mode!='protect' and terminal_at is None:
30            h=hashlib.sha256(f'{seed}:{meta["sequence_sha256"]}:{ply}'.encode()).digest()
31            history=0 if h[0]<64 else 16 # fixed 25% Bernoulli dropout, not exactly 25%.
32            e=c.example(b,move,history)
33            row=np.zeros((),dtype=DTYPE);row['x']=e['input_ids'];row['y']=e['labels'];row['attention']=e['attention_mask']
34            row['source']=source;row['split']=0 if mode=='train' else 1;row['ply']=ply;row['fullmove']=b.fullmove_number;row['key']=np.frombuffer(keys[-1],dtype=np.uint8)
35            rows.append(row)
36        b.push(move)
37    keys.append(c.position_key(b))
38    if meta['plies']!=len(moves) or hashlib.sha256(' '.join(moves).encode()).hexdigest()!=meta['sequence_sha256']:raise ValueError('move sequence identity')
39    # Existing converter uses ordinary FEN (legal en-passant normalization).
40    if b.fen()!=meta['final_fen']:raise ValueError('final board mismatch')
41    if terminal_at is not None and mode!='protect':
42        # Reject the WHOLE recorded game, not merely its post-terminal suffix.
43        # Protection still includes every supplied legal position conservatively.
44        meta={**meta,'state_v2_rejection':'continued_after_automatic_terminal','terminal_before_ply':terminal_at};rows=[]
45    return meta,keys,np.asarray(rows,dtype=DTYPE)
46
47def tasks(root,manifest,split,mode,seed,source,done,shuffle):
48    shards=[s for s in manifest['shards'] if s['split'] in split]
49    if shuffle:shards.sort(key=lambda s:digest([seed,sorted(s['files'])]))
50    ordinal=0
51    for shard in shards:
52        if ordinal+shard['games']<=done:ordinal+=shard['games'];continue
53        tokens,offsets,metas=_read_shard(Path(root),shard)
54        for a,b,meta in zip(offsets,offsets[1:],metas):
55            ordinal+=1
56            if ordinal<=done:continue
57            yield ordinal,(tokens[a:b],meta,mode,seed,source)
58
59def chunks(iterator,size):
60    chunk=[]
61    for item in iterator:
62        chunk.append(item)
63        if len(chunk)==size:yield chunk;chunk=[]
64    if chunk:yield chunk
65
66def process(store,pool,ref,manifest,source,mode,quota,config):
67    key=f'{mode}:{ref["manifest_sha256"]}:{source}'
68    cursor=store.get(key,{'ordinal':0,'rows':0,'finished':False})
69    if cursor['finished']:return
70    selected=('validation','test') if mode=='protect' else ('validation',) if mode=='validation' else ('train',)
71    iterator=tasks(ref['root'],manifest,selected,mode,config['seed'],source,cursor['ordinal'],mode=='train')
72    for chunk in chunks(iterator,config['chunk_games']):
73        # At most chunk_games tasks/results in flight, never unbounded executor.map.
74        outputs=list(pool.map(replay,[item[1] for item in chunk]))
75        accepted=[]
76        try:
77            store.db.execute('BEGIN')
78            for ordinal,(meta,keys,rows) in zip([item[0] for item in chunk],outputs):
79                store.count(mode+':games')
80                ids=aliases(meta)
81                if mode=='protect':
82                    store.db.executemany('INSERT OR IGNORE INTO protected VALUES(?)',((k,) for k in keys))
83                    store.db.executemany('INSERT OR IGNORE INTO protected_games VALUES(?)',((k,) for k in ids))
84                else:
85                    if meta.get('state_v2_rejection'):
86                        store.count(mode+':rejected:'+meta['state_v2_rejection']);cursor['ordinal']=ordinal;continue
87                    if mode=='train' and any(store.db.execute('SELECT 1 FROM protected_games WHERE k=?',(k,)).fetchone() for k in ids):
88                        store.count('train:heldout_game');cursor['ordinal']=ordinal;continue
89                    identity=meta['sequence_sha256']
90                    store.db.execute('INSERT OR IGNORE INTO games(identity,metadata) VALUES(?,?)',(identity,canonical(meta).decode()))
91                    game=store.db.execute('SELECT id FROM games WHERE identity=?',(identity,)).fetchone()[0]
92                    if game>=2**32:raise ValueError('game index overflow')
93                    for row in rows:
94                        if cursor['rows']>=quota:break
95                        k=row['key'].tobytes()
96                        if mode=='train' and store.db.execute('SELECT 1 FROM protected WHERE k=?',(k,)).fetchone():store.count('train:protected_position');continue
97                        # Validation dedup is separate from training dedup, but is
98                        # globally unique across both validation sources.
99                        sk=(b'v' if mode=='validation' else b't')+k
100                        if not store.db.execute('INSERT OR IGNORE INTO seen VALUES(?)',(sk,)).rowcount:store.count(mode+':duplicate_position');continue
101                        row['game']=game;accepted.append(row);cursor['rows']+=1
102                        store.count(f'{mode}:source{source}:rows');store.count(mode+':fen_only',int(row['x'][1]==c.HIST_NONE))
103                cursor['ordinal']=ordinal
104                if mode!='protect' and cursor['rows']>=quota:break
105            cursor['finished']=mode!='protect' and cursor['rows']>=quota
106            store.commit(accepted,source,0 if mode=='train' else 1,key,cursor)
107        except BaseException:store.db.rollback();raise
108        store.capacity()
109        print(json.dumps({'time':time.time(),'phase':mode,'source':source,**cursor,'allocated_bytes':store.capacity()}),flush=True)
110        if cursor['finished']:return
111    if mode!='protect' and cursor['rows']<quota:raise RuntimeError('source exhausted below configured position quota')
112    with store.db:cursor['finished']=True;store.set(key,cursor)
113
114def run(config):
115    root=Path(config['output']);root.mkdir(parents=True,exist_ok=True)
116    with (root/'owner.lock').open('a+b') as lock:
117        fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
118        code={p.name:sha(p) for p in [Path(__file__),Path(c.__file__),Path(__file__).with_name('records.py')]}
119        identity={'config':config,'code':code,'codec':c.CONTRACT}
120        store=Store(root,identity,config['storage_cap_bytes'])
121        refs={r['manifest_sha256']:r for r in config['references']+config['sources']}
122        manifests={}
123        for fp,ref in refs.items():
124            if sha(Path(ref['root'])/'manifest.json')!=fp:raise ValueError('source manifest mutated')
125            manifests[fp]=_manifest(Path(ref['root']))
126        with concurrent.futures.ProcessPoolExecutor(max_workers=config['workers']) as pool:
127            for ref in refs.values():process(store,pool,ref,manifests[ref['manifest_sha256']],0,'protect',0,config)
128            with store.db:store.set('protection_complete',True)
129            for source,ref in enumerate(config['sources']):process(store,pool,ref,manifests[ref['manifest_sha256']],source,'validation',config['validation_positions_per_source'],config)
130            for source,ref in enumerate(config['sources']):process(store,pool,ref,manifests[ref['manifest_sha256']],source,'train',config['train_positions_per_source'],config)
131        manifest=store.seal()
132        atomic_json(root/'completion.json',{'manifest_sha256':sha(root/'manifest.json'),'rows':manifest['rows'],'finished_unix':time.time(),'allocated_bytes':store.capacity(),
133            'protected_positions':store.db.execute('SELECT COUNT(*) FROM protected').fetchone()[0],
134            'protected_game_aliases':store.db.execute('SELECT COUNT(*) FROM protected_games').fetchone()[0]})
135        store.db.close()
136
137if __name__=='__main__':
138    p=argparse.ArgumentParser();p.add_argument('config');args=p.parse_args();run(json.loads(Path(args.config).read_text()))
139