training_deadline.py
python · 181 lines · Exact bytes
Version & provenance · foundation-20261029
Recorded SHA-256 11685a7e1078ac7e5f2fab07daad20316efb20f9cef56373e9cc5400e8b7c4caRendered SHA-256 ebc9c5bd957a335613018613eb664560d2045874bbf0501c4b7d0c6c6b6e62da
Original project code: Apache-2.0. Dependency licenses remain separate. License notes. Line links are stable within this snapshot. Allowlisted foundation training and frozen evaluation source; recorded hashes refer to original files, rendered hashes identify any path-redacted derivatives. This subset is not a standalone training distribution; no host configs, credentials, optimizer state or dataset bulk included.
1"""Persistent total-job deadline and Linux owned-process-group watchdog.
2
3The deadline starts before Popen (including imports, data setup and checkpointing).
4Resume reuses the original file; wall-clock rollback cannot extend this invocation
5because its watchdog also uses a monotonic deadline. No automatic restart.
6"""
7import argparse
8import hashlib
9import json
10import math
11import os
12from pathlib import Path
13import signal
14import subprocess
15import sys
16import time
17import uuid
18
19
20def read_deadline(path):
21 value=json.loads(Path(path).read_text())
22 if value.get('format')!='chess-total-deadline-v1':raise ValueError('deadline format')
23 for key in ('started_unix','deadline_unix','budget_seconds','checkpoint_margin_seconds'):
24 if isinstance(value.get(key),bool) or not isinstance(value.get(key),(int,float)) or not math.isfinite(value[key]):
25 raise ValueError('invalid deadline number')
26 if not 0<value['budget_seconds']<=14400:raise ValueError('budget exceeds four hours')
27 if not 0<value['checkpoint_margin_seconds']<value['budget_seconds']:raise ValueError('invalid checkpoint margin')
28 if abs(value['deadline_unix']-value['started_unix']-value['budget_seconds'])>1e-5:raise ValueError('inconsistent deadline')
29 return value
30
31
32def create_or_resume(path,identity,budget=14400,margin=120):
33 if isinstance(budget,bool) or not isinstance(budget,(int,float)) or not math.isfinite(budget) or not 0<budget<=14400:raise ValueError('invalid four-hour budget')
34 if isinstance(margin,bool) or not isinstance(margin,(int,float)) or not math.isfinite(margin) or not 0<margin<budget:raise ValueError('invalid checkpoint margin')
35 path=Path(path)
36 if path.exists():
37 value=read_deadline(path)
38 if value['identity']!=identity or value['budget_seconds']!=budget or value['checkpoint_margin_seconds']!=margin:
39 raise ValueError('deadline identity/config mismatch; never extend an existing job')
40 else:
41 now=time.time()
42 value={'format':'chess-total-deadline-v1','started_unix':now,'deadline_unix':now+budget,
43 'budget_seconds':budget,'checkpoint_margin_seconds':margin,'identity':identity}
44 # Exclusive creation prevents concurrent supervisors resetting the budget.
45 with path.open('x') as f:json.dump(value,f,sort_keys=True);f.flush();os.fsync(f.fileno())
46 value=read_deadline(path)
47 if time.time()>=value['deadline_unix']:raise TimeoutError('original job deadline expired')
48 return value
49
50
51class Deadline:
52 def __init__(self,value):
53 self.value=value
54 self.monotonic_end=time.monotonic()+max(0,value['deadline_unix']-time.time())
55 def remaining(self):
56 return min(self.value['deadline_unix']-time.time(),self.monotonic_end-time.monotonic())
57 def soft_stop(self):return self.remaining()<=self.value['checkpoint_margin_seconds']
58
59
60def _guard_job(command,value,env=None,owner_pid=None):
61 """Linux only. TERM at 5s remaining, KILL at deadline; clean own group on failure.
62
63 OS scheduling can delay signal delivery; this is not a real-time guarantee.
64 Group containment covers torchrun workers; children must not detach sessions.
65 """
66 if os.name!='posix':raise RuntimeError('watchdog requires Linux process groups')
67 clock=Deadline(value)
68 if clock.soft_stop():raise TimeoutError('insufficient original budget to start/resume')
69 process=None;term=False;expired=False
70 try:
71 if owner_pid is not None and os.getppid()!=owner_pid:raise RuntimeError('supervisor died before launch')
72 process=subprocess.Popen(command,start_new_session=True,env=env,stdout=sys.stderr)
73 while True:
74 if owner_pid is not None and os.getppid()!=owner_pid:
75 return {'returncode':125,'deadline_expired':False,'supervisor_lost':True}
76 remaining=clock.remaining()
77 if remaining<=0:
78 expired=True;break
79 result=process.poll()
80 if result is not None:return {'returncode':result,'deadline_expired':False}
81 if remaining<=5 and not term:
82 os.killpg(process.pid,signal.SIGTERM);term=True
83 time.sleep(min(.1,max(.001,remaining)))
84 finally:
85 if process is not None:
86 # Also remove orphaned workers after leader failure/success.
87 try:os.killpg(process.pid,signal.SIGKILL)
88 except ProcessLookupError:pass
89 process.wait()
90 return {'returncode':process.returncode,'deadline_expired':expired}
91
92
93def supervise(command,value,env=None):
94 """A separate guardian owns/reaps workers and survives supervisor SIGKILL.
95
96 It also notices loss of its supervisor and immediately kills its owned group.
97 The guardian has a separate session; no global job/process enumeration occurs.
98 """
99 if os.name!='posix':raise RuntimeError('watchdog requires Linux process groups')
100 if Deadline(value).soft_stop():raise TimeoutError('insufficient original budget to start/resume')
101 lock_fd=int(env['CHESS_JOB_LOCK_FD']) if env and 'CHESS_JOB_LOCK_FD' in env else None
102 guardian=subprocess.Popen([sys.executable,str(Path(__file__).resolve()),'--guardian'],
103 stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True,start_new_session=True,
104 pass_fds=() if lock_fd is None else (lock_fd,))
105 try:
106 output,_=guardian.communicate(json.dumps({'command':command,'value':value,'env':env,'owner_pid':os.getpid()}))
107 if guardian.returncode:raise RuntimeError('deadline guardian failed: '+output)
108 result=json.loads(output)
109 if result.get('error_type')=='FileNotFoundError':raise FileNotFoundError(result['error'])
110 if 'error' in result:raise RuntimeError(result['error'])
111 return result
112 finally:
113 if guardian.poll() is None:
114 guardian.terminate()
115 guardian.wait(timeout=5)
116
117
118def guardian_main():
119 # Linux subreaper: adopt and reap orphaned torchrun workers after killing the
120 # owned group. PR_SET_CHILD_SUBREAPER needs no elevated privilege.
121 import ctypes
122 if ctypes.CDLL(None,use_errno=True).prctl(36,1,0,0,0)!=0:raise OSError(ctypes.get_errno(),'subreaper unavailable')
123 def terminate(signum,frame):raise SystemExit(128+signum)
124 signal.signal(signal.SIGTERM,terminate);signal.signal(signal.SIGINT,terminate)
125 request=json.load(sys.stdin)
126 try:
127 try:result=_guard_job(**request)
128 except Exception as error:result={'error':repr(error),'error_type':type(error).__name__}
129 finally:
130 # All owned processes have already been killed; only reap dead children.
131 until=time.monotonic()+2
132 while time.monotonic()<until:
133 try:pid,_=os.waitpid(-1,os.WNOHANG)
134 except ChildProcessError:break
135 if pid==0:time.sleep(.01)
136 if os.getppid()==request['owner_pid']:print(json.dumps(result),flush=True)
137
138
139def main():
140 p=argparse.ArgumentParser();p.add_argument('--deadline',required=True);p.add_argument('--approval',required=True)
141 p.add_argument('--resume-checkpoint')
142 p.add_argument('--dataset-manifest',required=True);p.add_argument('command',nargs=argparse.REMAINDER)
143 args=p.parse_args()
144 root=Path(__file__).parent
145 identity={'dataset_manifest_sha256':hashlib.sha256(Path(args.dataset_manifest).read_bytes()).hexdigest(),
146 'code_sha256':{name:hashlib.sha256((root/name).read_bytes()).hexdigest() for name in
147 ('training_deadline.py','training_core.py','train_chess.py','verified_training_data.py')}}
148 if not args.command:raise ValueError('missing command')
149 approval=json.loads(Path(args.approval).read_text())
150 run_id=approval.get('run_id')
151 try:uuid.UUID(run_id)
152 except (ValueError,TypeError,AttributeError):raise ValueError('approval must name immutable run UUID')
153 deadline_path=str(Path(args.deadline).resolve())
154 resume_path=approval.get('resume_checkpoint_path')
155 if resume_path is not None and (not isinstance(resume_path,str) or str(Path(resume_path).resolve())!=resume_path):
156 raise ValueError('approved resume checkpoint must be canonical absolute path')
157 if approval!={'approved':True,'identity':identity,'budget_seconds':14400,'command':args.command,
158 'run_id':run_id,'deadline_path':deadline_path,'resume_checkpoint_path':resume_path}:raise ValueError('missing/mismatched review release or alternate deadline path')
159 command=args.command
160 if args.resume_checkpoint:
161 if resume_path is None or str(Path(args.resume_checkpoint).resolve())!=resume_path:raise ValueError('unapproved resume checkpoint')
162 if not Path(resume_path).is_file():raise ValueError('missing resume checkpoint')
163 command=[*command,'--resume',resume_path]
164 identity={**identity,'run_id':run_id,'deadline_path':deadline_path,'command':args.command,'resume_checkpoint_path':resume_path}
165 # Lock held for entire supervisor lifetime, including deadline-file creation.
166 import fcntl
167 with open(deadline_path+'.lock','a') as lock:
168 fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
169 if args.resume_checkpoint and not Path(deadline_path).is_file():
170 raise ValueError('explicit resume requires the original deadline file; no fresh budget')
171 value=create_or_resume(deadline_path,identity)
172 env=dict(os.environ,CHESS_JOB_DEADLINE=deadline_path,CHESS_JOB_LOCK_FD=str(lock.fileno()))
173 result=supervise(command,value,env)
174 Path(deadline_path+'.result.json').write_text(json.dumps(result,indent=2))
175 raise SystemExit(result['returncode'] or (124 if result['deadline_expired'] else 0))
176
177
178if __name__=='__main__':
179 if sys.argv[1:]==['--guardian']:guardian_main()
180 else:main()
181