LittlePlyResearch notebook

month_twenty.py

python · 177 lines · Sanitized derivative

Line 1 ↗

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

Version & provenance · match-20261028

Recorded SHA-256 28245689f1b09742216bb411997476166973c0bea850c8ecb3f7e423b44d19e3Rendered SHA-256 c1d3fd8cfddf7fa8b979ce2a7361ffeb14b74da9d5b8ece6ef0a687bbc505262

Apache-2.0 for original LittlePly code, declared by the project owner on 2026-10-28. Historical source bytes and hashes are unchanged. Third-party components retain their own licenses; Lichess data remain CC0-1.0. License notes. Line links are stable within this snapshot. SHA-256 compared to persisted run identity/manifest. Unavailable files are never replaced with newer versions. Rendered derivatives preserve line numbers and redact private paths; original and rendered hashes are distinct.

1"""Authorized 20-game full-month checkpoint match. No training or data evaluation."""
2import os,sys,json,subprocess,time,re,statistics,io
3from pathlib import Path
4os.environ.update(HF_HUB_OFFLINE='1',TRANSFORMERS_OFFLINE='1',CUBLAS_WORKSPACE_CONFIG=':4096:8',PYTHONDONTWRITEBYTECODE='1')
5sys.path.insert(0,'<PRIVATE_PATH>')
6import training_core as tc
7import torch,transformers,chess,chess.pgn
8import harness as h
9
10OUT=h.ROOT/'month-best-20-games-seed20261028'
11CK=Path(tc.__file__).parent/'training-v1/month-2h/best.pt'
12EXPECTED='6133648b2926b38542ee3ee1251a1ba111d61da9a8ea69d1c5f0559b09933643'
13SEED=20261028
14# Hand-authored standard opening sequences; no engine selection, database,
15# training/test lookup or result-dependent selection. Each appears in both colours.
16OPENINGS=[
17 ('Ruy Lopez','e2e4 e7e5 g1f3 b8c6 f1b5 a7a6'),
18 ('Sicilian','e2e4 c7c5 g1f3 d7d6 d2d4 c5d4'),
19 ('French','e2e4 e7e6 d2d4 d7d5 b1c3 g8f6'),
20 ('Caro-Kann','e2e4 c7c6 d2d4 d7d5 b1c3 d5e4'),
21 ('Queens Gambit Declined','d2d4 d7d5 c2c4 e7e6 b1c3 g8f6'),
22 ('Kings Indian','d2d4 g8f6 c2c4 g7g6 b1c3 f8g7'),
23 ('Queens Indian','d2d4 g8f6 c2c4 e7e6 g1f3 b7b6'),
24 ('English','c2c4 e7e5 b1c3 g8f6 g2g3 d7d5'),
25 ('Reti','g1f3 d7d5 g2g3 g8f6 f1g2 e7e6'),
26 ('Italian','e2e4 e7e5 f1c4 g8f6 d2d3 f8c5'),
27]
28
29def write_json(path,value):
30    tmp=path.with_suffix(path.suffix+'.tmp')
31    tmp.write_text(json.dumps(value,indent=2));tmp.replace(path)
32
33def verify_proof(board,proof,attacker,depth=0):
34    assert depth<=3 and board.fen()==proof['fen']
35    if proof['kind']=='checkmate':
36        assert board.is_checkmate() and board.turn!=attacker;return
37    assert h.outcome(board) is None and depth<3
38    branches=proof['branches']
39    if board.turn!=attacker:
40        assert [x['move'] for x in branches]==sorted(m.uci() for m in board.legal_moves)
41    else:assert len(branches)==1
42    for branch in branches:
43        m=chess.Move.from_uci(branch['move']);assert m in board.legal_moves
44        board.push(m)
45        try:verify_proof(board,branch['child'],attacker,depth+1)
46        finally:board.pop()
47
48def audit(folder,report,opening,color):
49    with (folder/'game.pgn').open() as f:g=chess.pgn.read_game(f)
50    assert g and not g.errors
51    moves=list(g.mainline_moves());assert [m.uci() for m in moves[:6]]==opening
52    board=chess.Board();by_ply={r['ply']:r for r in report['moves']}
53    for m in moves:
54        assert m in board.legal_moves
55        row=by_ply.get(board.ply()+1)
56        if row and row.get('decision_source')=='forced_mate_override':
57            assert row['model_query']=='not_queried' and 'raw_greedy_legal' not in row
58            assert row['mate_search']['move']==m.uci()
59            verify_proof(board,row['mate_search']['proof'],color)
60        board.push(m)
61    assert board.fen()==report['final_fen']
62    end=board.outcome(claim_draw=True)
63    if report['result']!='*':assert end and end.result()==report['result']
64    queried=[r for r in report['moves'] if r.get('model_query')=='queried']
65    nodes=[]
66    log=folder/'uci.log'
67    if log.exists():
68        for part in log.read_text().split('> go nodes 1000')[1:]:
69            found=re.findall(r' nodes (\d+)',part)
70            nodes.append(int(found[-1]) if found else None)
71    return g,{'plies':report['plies'],'result':report['result'],'termination':report['termination'],
72      'error':report['error'],'cleanup_error':report['cleanup_error'],'seconds':report['seconds'],
73      'model_queries':len(queried),'raw_greedy_legal':sum(r['raw_greedy_legal'] for r in queried),
74      'raw_legal_probability_sum':sum(r['legal_joint_probability'] for r in queried),
75      'overrides':sum(r.get('decision_source')=='forced_mate_override' for r in report['moves']),
76      'tactical_unknown':sum(r.get('mate_search',{}).get('status')=='unknown' for r in report['moves']),
77      'sunfish_reported_nodes':nodes,'all_moves_legal':True,'pgn_roundtrip_verified':True,
78      'proofs_replayed':True}
79
80def publish(results,games,started):
81    summary={'completed_games':len(results),'seconds_since_start':time.monotonic()-started,'by_colour':{}}
82    for colour in ['white','black']:
83        rows=[r for r in results if r['model_colour']==colour]
84        win='1-0' if colour=='white' else '0-1';loss='0-1' if colour=='white' else '1-0'
85        summary['by_colour'][colour]={'wins':sum(r['result']==win for r in rows),
86            'draws':sum(r['result']=='1/2-1/2' for r in rows),'losses':sum(r['result']==loss for r in rows),
87            'unfinished':sum(r['result']=='*' for r in rows)}
88    for key in ['overrides','model_queries','raw_greedy_legal','tactical_unknown']:
89        summary[key]=sum(r[key] for r in results)
90    summary['mean_raw_legal_probability']=sum(r['raw_legal_probability_sum'] for r in results)/max(1,summary['model_queries'])
91    observed=[n for r in results for n in r['sunfish_reported_nodes'] if n is not None]
92    summary['sunfish_requested_nodes']=1000
93    summary['sunfish_reported_nodes_range']=[min(observed),max(observed)] if observed else None
94    summary['terminal_games']=sum(r['result']!='*' for r in results)
95    summary['capped_games']=sum(r['termination']=='ply_cap_unfinished' for r in results)
96    summary['timeout_games']=sum(r['termination']=='timeout_unfinished' for r in results)
97    summary['failed_games']=sum(r['termination']=='error_unfinished' for r in results)
98    summary['total_game_seconds']=sum(r['seconds'] for r in results)
99    write_json(OUT/'results.json',results);write_json(OUT/'summary.json',summary)
100    text='\n\n'.join(str(g) for g in games)+'\n'
101    stream=io.StringIO(text)
102    for original in games:
103        replay=chess.pgn.read_game(stream)
104        assert replay and not replay.errors and list(replay.mainline_moves())==list(original.mainline_moves())
105        assert replay.headers['Result']==original.headers['Result']
106    assert chess.pgn.read_game(stream) is None
107    tmp=OUT/'full-month-vs-sunfish-20-games.pgn.tmp';tmp.write_text(text)
108    tmp.replace(OUT/'full-month-vs-sunfish-20-games.pgn')
109    return summary
110
111def run():
112    assert h.sha(CK)==EXPECTED,'checkpoint hash mismatch'
113    saved=tc.load_checkpoint(CK)
114    cfg=saved['model_config']
115    assert (cfg['n_layer'],cfg['n_embd'],cfg['n_head'],cfg['vocab_size'],cfg['n_positions'])==(12,768,12,128,1024)
116    assert saved['identity']['tokenizer']['sha256']==h.sha(h.tokenizer.__file__)
117    openings=[]
118    for name,line in OPENINGS:
119        b=chess.Board()
120        for uci in line.split():
121            assert h.outcome(b) is None;b.push_uci(uci)
122        assert h.outcome(b) is None
123        openings.append({'name':name,'uci':line.split(),'final_fen':b.fen()})
124    assert len(set(o['final_fen'] for o in openings))==10
125    git=['git','-c','core.autocrlf=true','-c','core.filemode=false','-C',str(h.ROOT/'sunfish')]
126    assert subprocess.check_output(git+['rev-parse','HEAD'],text=True).strip()==h.PIN
127    assert not subprocess.check_output(git+['status','--porcelain'],text=True).strip()
128    occupancy=subprocess.check_output(['nvidia-smi','--query-compute-apps=pid,process_name','--format=csv,noheader'],text=True)
129    assert not occupancy.strip(),'GPU busy: '+occupancy
130    OUT.mkdir(exist_ok=False)
131    (OUT/'gpu-preflight.txt').write_text(subprocess.check_output(['nvidia-smi'],text=True))
132    manifest={'label':'FULL-MONTH TRAINED MODEL + THREE-PLY FORCED-MATE OVERRIDE',
133      'checkpoint':str(CK),'checkpoint_sha256':EXPECTED,'checkpoint_identity':saved['identity'],
134      'checkpoint_cursor':saved['cursor'],'model_config':cfg,'seed':SEED,
135      'opening_provenance':'Ten hand-authored common opening sequences selected before play; no database/engine/test-set selection',
136      'openings':openings,'games':20,'sunfish_commit':h.PIN,'sunfish_runtime':sys.version,
137      'sunfish_requested_nodes':1000,'engine_timeout_seconds':10,'model_timeout_seconds':60,
138      'game_walltime_seconds':180,'ply_cap_including_opening':256,
139      'mate_max_plies':3,'mate_nodes':50000,'mate_seconds':1.,
140      'draw_policy':'immediate symmetric python-chess outcome(claim_draw=True)',
141      'decoding':'full legal-move joint argmax; raw probabilities separately logged; tactical override skips model',
142      'torch':torch.__version__,'transformers':transformers.__version__,'python_chess':chess.__version__,
143      'device':'cuda:1','dtype':'float32','executable':sys.executable,
144      'source_hashes':{p.name:h.sha(p) for p in [Path(__file__),Path(h.__file__),h.ROOT/'mate_override.py',Path(tc.__file__),Path(tc.__file__).with_name('training_adapter.py')]}}
145    write_json(OUT/'manifest.json',manifest)
146    del saved
147    os.environ['CHESS_INFERENCE_DEVICE']='cuda:1'
148    results=[];games=[];started=time.monotonic();worker=None
149    try:
150        for index,opening in enumerate(openings,1):
151            for color in [True,False]:
152                name=f'{index:02d}-model-'+('white' if color else 'black')
153                if worker is None or not worker.p.is_alive():
154                    if worker is not None:worker.close()
155                    worker=h.ModelWorker('training_adapter:load',CK,SEED,60)
156                worker.timeout=60
157                print('START '+name+' '+opening['name'],flush=True)
158                report=h.game(worker,lambda:h.UCI(h.engine_command(),10),color,OUT/name,
159                    cap=256,nodes=1000,opening=opening['uci'],walltime=180,
160                    label='FULL-MONTH TRAINED; '+opening['name']+'; no Elo claim',
161                    mate_override=True,mate_nodes=50000,mate_seconds=1.)
162                g,row=audit(OUT/name,report,opening['uci'],color)
163                g.headers.update(Round=str(len(results)+1),Opening=opening['name'],
164                    White='Full-month model + mate override' if color else 'Sunfish classic',
165                    Black='Sunfish classic' if color else 'Full-month model + mate override',
166                    CheckpointSHA256=EXPECTED,SunfishCommit=h.PIN)
167                (OUT/name/'game.pgn').write_text(str(g)+'\n')
168                row.update(game=name,opening=opening['name'],model_colour='white' if color else 'black',seed=SEED,
169                    pgn_sha256=h.sha(OUT/name/'game.pgn'))
170                results.append(row);games.append(g);summary=publish(results,games,started)
171                print(json.dumps({k:v for k,v in row.items() if k!='sunfish_reported_nodes'}),flush=True)
172        print('FINAL '+json.dumps(summary),flush=True)
173    finally:
174        if worker is not None:worker.close()
175
176if __name__=='__main__':run()
177