Organize complete research documentation and recovery inventory
Browse files- scripts/run_review26_e4b_supervisor.py +41 -4
- scripts/run_scale_grpo.py +99 -19
- scripts/stage_review30_test_eval.py +47 -0
- scripts/tracegrpo-e4b.service +1 -1
- scripts/verify_complete_hf_archives.py +38 -0
- src/baselines/__init__.py +1 -0
- src/baselines/classical_pipeline.py +132 -0
- src/rl/live_adapter.py +41 -16
scripts/run_review26_e4b_supervisor.py
CHANGED
|
@@ -5,6 +5,11 @@ from datetime import datetime, timezone
|
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
ROOT=Path(__file__).resolve().parents[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
DEFAULT_RUN=ROOT/'results/phase6/review26/e4b_balanced_reward'
|
| 9 |
|
| 10 |
def utc(): return datetime.now(timezone.utc).isoformat()
|
|
@@ -108,18 +113,49 @@ def start_pending_archive(run_dir,state,log,popen=subprocess.Popen):
|
|
| 108 |
state['archive_worker_pid']=child.pid;state['archive_worker_step']='stop'
|
| 109 |
_write_log(log,f'started HF stopped-run archive pid={child.pid}')
|
| 110 |
|
| 111 |
-
def supervise(run_dir, *, poll_seconds=5, free_check=gpu_is_free, popen=subprocess.Popen
|
|
|
|
| 112 |
run_dir=Path(run_dir).resolve();run_dir.mkdir(parents=True,exist_ok=True)
|
| 113 |
manifest=json.loads((run_dir/'run_manifest.json').read_text())
|
| 114 |
if manifest.get('launch_authority')!='D-GRPO-17': raise RuntimeError('wrong launch authority')
|
| 115 |
state_path=run_dir/'e4b_supervisor_state.json';terminal=run_dir/'e4b_terminal_stop.json'
|
| 116 |
log=run_dir/'e4b_supervisor.log'
|
| 117 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
if (run_dir/'e4b_supervisor_tripwire.json').exists():
|
| 119 |
atomic_json(terminal,dict(status='stopped',disposition='tripwire_terminal',
|
| 120 |
marker='e4b_supervisor_tripwire.json',at_utc=utc()))
|
| 121 |
_write_log(log,'persisted tripwire marker found at startup; refusing auto-resume');return 0
|
| 122 |
-
state=json.loads(state_path.read_text()) if state_path.exists() else dict(gpu_seconds_used=0.,restarts=0)
|
| 123 |
abandoned_start=state.pop('active_child_started_at',None)
|
| 124 |
had_interrupted_child=abandoned_start is not None
|
| 125 |
if abandoned_start is not None:
|
|
@@ -267,8 +303,9 @@ def shutil_disk_fraction(root):
|
|
| 267 |
|
| 268 |
def main():
|
| 269 |
ap=argparse.ArgumentParser();ap.add_argument('--run-dir',type=Path,default=DEFAULT_RUN)
|
|
|
|
| 270 |
args=ap.parse_args()
|
| 271 |
-
try:return supervise(args.run_dir)
|
| 272 |
except BaseException as exc:
|
| 273 |
run_dir=Path(args.run_dir)
|
| 274 |
try:
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
ROOT=Path(__file__).resolve().parents[1]
|
| 8 |
+
# systemd invokes this file by absolute path, which puts ``scripts/`` rather
|
| 9 |
+
# than the repository root at sys.path[0]. Runtime helpers use the ``scripts``
|
| 10 |
+
# namespace, so make imports independent of the launch form and working dir.
|
| 11 |
+
if str(ROOT) not in sys.path:
|
| 12 |
+
sys.path.insert(0, str(ROOT))
|
| 13 |
DEFAULT_RUN=ROOT/'results/phase6/review26/e4b_balanced_reward'
|
| 14 |
|
| 15 |
def utc(): return datetime.now(timezone.utc).isoformat()
|
|
|
|
| 113 |
state['archive_worker_pid']=child.pid;state['archive_worker_step']='stop'
|
| 114 |
_write_log(log,f'started HF stopped-run archive pid={child.pid}')
|
| 115 |
|
| 116 |
+
def supervise(run_dir, *, poll_seconds=5, free_check=gpu_is_free, popen=subprocess.Popen,
|
| 117 |
+
review28_authorized_resume=False):
|
| 118 |
run_dir=Path(run_dir).resolve();run_dir.mkdir(parents=True,exist_ok=True)
|
| 119 |
manifest=json.loads((run_dir/'run_manifest.json').read_text())
|
| 120 |
if manifest.get('launch_authority')!='D-GRPO-17': raise RuntimeError('wrong launch authority')
|
| 121 |
state_path=run_dir/'e4b_supervisor_state.json';terminal=run_dir/'e4b_terminal_stop.json'
|
| 122 |
log=run_dir/'e4b_supervisor.log'
|
| 123 |
+
state=json.loads(state_path.read_text()) if state_path.exists() else dict(gpu_seconds_used=0.,restarts=0)
|
| 124 |
+
if terminal.exists():
|
| 125 |
+
if not review28_authorized_resume:
|
| 126 |
+
_write_log(log,'terminal marker already exists; refusing launch');return 0
|
| 127 |
+
try:
|
| 128 |
+
old_terminal=json.loads(terminal.read_text())
|
| 129 |
+
prior_status=old_terminal['worker_status']
|
| 130 |
+
eval_status_path=run_dir/'e4b_step100_eval_worker_status.json'
|
| 131 |
+
eval_status=json.loads(eval_status_path.read_text())
|
| 132 |
+
eval_metrics_path=Path(eval_status['metrics_path'])
|
| 133 |
+
if not eval_metrics_path.is_absolute(): eval_metrics_path=ROOT/eval_metrics_path
|
| 134 |
+
except (OSError,KeyError,ValueError,json.JSONDecodeError):
|
| 135 |
+
_write_log(log,'Review #28 resume refused: missing preserved failure/eval evidence');return 0
|
| 136 |
+
if (old_terminal.get('disposition')!='software_or_runtime_failure_hold' or
|
| 137 |
+
prior_status.get('status')!='failed' or
|
| 138 |
+
'FileExistsError' not in str(prior_status.get('stop_reason','')) or
|
| 139 |
+
eval_status.get('status')!='eval_only_complete' or
|
| 140 |
+
not eval_metrics_path.is_file() or
|
| 141 |
+
(run_dir/'e4b_supervisor_tripwire.json').exists()):
|
| 142 |
+
_write_log(log,'Review #28 resume refused: failure cause/eval result/tripwire precondition mismatch');return 0
|
| 143 |
+
if not state.get('review28_step100_eval_charged'):
|
| 144 |
+
state['gpu_seconds_used']=float(state.get('gpu_seconds_used',0.))+float(eval_status['gpu_wall_hours'])*3600
|
| 145 |
+
state['review28_step100_eval_charged']=True
|
| 146 |
+
state['review28_eval_gpu_hours']=float(eval_status['gpu_wall_hours'])
|
| 147 |
+
atomic_json(state_path,state)
|
| 148 |
+
eval_status['budget_charged_to_e4b'] = True
|
| 149 |
+
atomic_json(eval_status_path,eval_status)
|
| 150 |
+
for source,name in ((run_dir/'scale_final_status.json','scale_final_status_pre_review28.json'),
|
| 151 |
+
(terminal,'e4b_terminal_stop_pre_review28.json')):
|
| 152 |
+
archived=run_dir/name
|
| 153 |
+
if source.exists() and not archived.exists(): os.replace(source,archived)
|
| 154 |
+
_write_log(log,'Review #28 authorized software-failure recovery: archived prior terminal markers; step-100 eval GPU time charged')
|
| 155 |
if (run_dir/'e4b_supervisor_tripwire.json').exists():
|
| 156 |
atomic_json(terminal,dict(status='stopped',disposition='tripwire_terminal',
|
| 157 |
marker='e4b_supervisor_tripwire.json',at_utc=utc()))
|
| 158 |
_write_log(log,'persisted tripwire marker found at startup; refusing auto-resume');return 0
|
|
|
|
| 159 |
abandoned_start=state.pop('active_child_started_at',None)
|
| 160 |
had_interrupted_child=abandoned_start is not None
|
| 161 |
if abandoned_start is not None:
|
|
|
|
| 303 |
|
| 304 |
def main():
|
| 305 |
ap=argparse.ArgumentParser();ap.add_argument('--run-dir',type=Path,default=DEFAULT_RUN)
|
| 306 |
+
ap.add_argument('--review28-authorized-resume',action='store_true')
|
| 307 |
args=ap.parse_args()
|
| 308 |
+
try:return supervise(args.run_dir,review28_authorized_resume=args.review28_authorized_resume)
|
| 309 |
except BaseException as exc:
|
| 310 |
run_dir=Path(args.run_dir)
|
| 311 |
try:
|
scripts/run_scale_grpo.py
CHANGED
|
@@ -39,6 +39,33 @@ def select_rollout(provenance):
|
|
| 39 |
return int.from_bytes(hashlib.sha256(key.encode()).digest()[:8], 'big') % 20 == 0
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
class ScaleRuntime(Runtime):
|
| 43 |
sample_weight_fn=staticmethod(bounded_zero_correct_sample_difficulty_weight)
|
| 44 |
|
|
@@ -151,7 +178,6 @@ class ScaleRuntime(Runtime):
|
|
| 151 |
ids=[r['prompt_id'] for r in self.manifest['valid_selection']]
|
| 152 |
labels={r['prompt_id']:int(r['target']) for r in self.manifest['valid_selection']}
|
| 153 |
prompts=[self.rows[pid]['prompt'] for pid in ids]
|
| 154 |
-
self.trainer._move_model_to_vllm()
|
| 155 |
sampled=self.trainer.llm.generate(prompts,sampling_params=SamplingParams(
|
| 156 |
temperature=.8,max_tokens=1536,n=8,seed=42),use_tqdm=True)
|
| 157 |
records=[];correct_vuln_r2=[]
|
|
@@ -198,6 +224,7 @@ class ScaleRuntime(Runtime):
|
|
| 198 |
if len(self.eval_entropies)>=2 and all(x<.3 for x in self.eval_entropies[-2:]):
|
| 199 |
raise SmokeStop('two_consecutive_VALID_entropies_below_0.3 [D-GRPO-9]')
|
| 200 |
metrics=dict(optimizer_step=step,checkpoint=str(checkpoint),adapter_label='E4 balanced reward',
|
|
|
|
| 201 |
inference_protocol='E2 exact: 8 generations, temperature 0.8, max_tokens 1536, seed 42',
|
| 202 |
n_rows=len(records),n_pairs=len({r['pair_id'] for r in records}),
|
| 203 |
parse_rate=sampled_parse,row_auc=score['auc'],
|
|
@@ -206,6 +233,7 @@ class ScaleRuntime(Runtime):
|
|
| 206 |
greedy_label_accuracy=correct/valid_n,greedy_per_class_accuracy=per_class,
|
| 207 |
greedy_predicted_vulnerable_fraction=predicted_vulnerable,
|
| 208 |
greedy_completion_entropy_nats=greedy_diagnostics['entropy_nats'],
|
|
|
|
| 209 |
correct_vulnerable_r2_mean=float(np.mean(correct_vuln_r2)) if correct_vuln_r2 else None,
|
| 210 |
correct_vulnerable_r2_n=len(correct_vuln_r2),test_touched=False)
|
| 211 |
path=self.run_dir/f'e4_valid_step_{step:06d}.json'
|
|
@@ -362,10 +390,26 @@ def write_progress_report(run_dir,status):
|
|
| 362 |
axes[1,0].plot(steps,[x['completion_tokens']['mean'] for x in curves],label='mean')
|
| 363 |
axes[1,0].plot(steps,[x['completion_tokens']['p95'] for x in curves],label='step p95')
|
| 364 |
axes[1,0].legend();axes[1,0].set_title('Completion tokens')
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
for ax in axes.flat:
|
| 370 |
ax.set_xlabel('Optimizer step')
|
| 371 |
if manifest.get('continuation'):
|
|
@@ -375,7 +419,7 @@ def write_progress_report(run_dir,status):
|
|
| 375 |
fig.tight_layout();fig.savefig(run_dir/'training_curves.png',dpi=140);plt.close(fig)
|
| 376 |
|
| 377 |
|
| 378 |
-
def worker(run_dir):
|
| 379 |
import pandas as pd
|
| 380 |
import torch
|
| 381 |
from datasets import Dataset
|
|
@@ -388,7 +432,7 @@ def worker(run_dir):
|
|
| 388 |
from scripts.verify_grpo_reward_equivalence import sha256
|
| 389 |
|
| 390 |
runtime=None;trainer=None;e4=False
|
| 391 |
-
start=float(os.environ
|
| 392 |
status=dict(status='initializing',started_at=now(),authorization='Review #20 + D-GRPO-14',
|
| 393 |
test_touched=False,scale_budget_ceiling_gpu_hours=250,
|
| 394 |
launch_segment_budget_gpu_hours=40,resume_requires_user=True)
|
|
@@ -403,7 +447,22 @@ def worker(run_dir):
|
|
| 403 |
e4b=manifest.get('launch_authority')=='D-GRPO-17'
|
| 404 |
if e4b:status['authorization']='D-GRPO-17'
|
| 405 |
resume_file=run_dir/'e4b_resume.json'
|
| 406 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
continuation=json.loads(resume_file.read_text())
|
| 408 |
if continuation.get('source_rule')!='D-GRPO-17' or continuation.get('lr_factor')!=1.0:
|
| 409 |
raise SmokeStop('E4b_resume_pointer_invalid')
|
|
@@ -457,8 +516,13 @@ def worker(run_dir):
|
|
| 457 |
runtime.best_p_pass=resume_extra['best_p_pass']
|
| 458 |
runtime.best_checkpoint=resume_extra['best_checkpoint']
|
| 459 |
runtime.last_safe_step=continuation['optimizer_step']
|
| 460 |
-
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
runtime.steps_per_epoch=(len(runtime.train_ids)+63)//64
|
| 463 |
runtime.progress('loading_fixed_judge')
|
| 464 |
runtime.judge=FixedJudge(run_dir);runtime.judge.should_log=select_rollout
|
|
@@ -565,11 +629,22 @@ def worker(run_dir):
|
|
| 565 |
out=trainer.llm.generate([rows[runtime.train_ids[0]]['prompt']],
|
| 566 |
sampling_params=SamplingParams(temperature=1.,max_tokens=1536,n=1),use_tqdm=False)
|
| 567 |
record_integration_canary(runtime,trainer,out[0].outputs[0].text)
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
except SmokeStop as error:
|
| 574 |
status.update(status='stopped',stop_reason=str(error),requires_user_review=True)
|
| 575 |
print('TRIPWIRE_OR_BUDGET_STOP',error,flush=True)
|
|
@@ -589,8 +664,11 @@ def worker(run_dir):
|
|
| 589 |
if runtime and runtime.curve:
|
| 590 |
status['aggregate_zero_variance_fraction']=sum(s['zero_groups'] for s in runtime.curve)/(64*len(runtime.curve))
|
| 591 |
status['aggregate_rollout_accounting']=rollout_accounting(runtime.curve,canary=runtime.canary)
|
| 592 |
-
|
| 593 |
-
|
|
|
|
|
|
|
|
|
|
| 594 |
print('SCALE_FINAL_STATUS',json.dumps(status),flush=True)
|
| 595 |
if status['status']=='failed':raise SystemExit(2)
|
| 596 |
|
|
@@ -645,10 +723,12 @@ def supervise(run_dir):
|
|
| 645 |
|
| 646 |
def main():
|
| 647 |
parser=argparse.ArgumentParser();parser.add_argument('--worker',action='store_true')
|
| 648 |
-
parser.add_argument('--run-dir',type=Path,required=True)
|
|
|
|
| 649 |
run_dir=args.run_dir.resolve()
|
| 650 |
if not (run_dir/'run_manifest.json').is_file():raise SystemExit('frozen preflight manifest missing')
|
| 651 |
-
if args.
|
|
|
|
| 652 |
else:supervise(run_dir)
|
| 653 |
|
| 654 |
if __name__=='__main__':main()
|
|
|
|
| 39 |
return int.from_bytes(hashlib.sha256(key.encode()).digest()[:8], 'big') % 20 == 0
|
| 40 |
|
| 41 |
|
| 42 |
+
def restore_e4b_eval_history(runtime, run_dir):
|
| 43 |
+
"""Carry completed VALID endpoints forward when resuming a full-state run."""
|
| 44 |
+
run_dir=Path(run_dir)
|
| 45 |
+
curve_path=run_dir/'valid_curve.jsonl'
|
| 46 |
+
history=([json.loads(line) for line in curve_path.read_text().splitlines()]
|
| 47 |
+
if curve_path.is_file() else [])
|
| 48 |
+
retry_path=run_dir/'step100_matched_eval_retry/e4_valid_step_000100.json'
|
| 49 |
+
if retry_path.is_file() and not any(int(x.get('optimizer_step',-1))==100 for x in history):
|
| 50 |
+
retry=json.loads(retry_path.read_text())
|
| 51 |
+
history.append(retry)
|
| 52 |
+
append_json(curve_path,retry)
|
| 53 |
+
write_json(run_dir/'valid_step_000100.json',retry)
|
| 54 |
+
runtime.eval_entropies=[value for row in history
|
| 55 |
+
if (value:=row.get('entropy_nats',row.get('greedy_completion_entropy_nats'))) is not None]
|
| 56 |
+
for row in history:
|
| 57 |
+
score=row.get('row_auc')
|
| 58 |
+
if score is not None and score>runtime.best_p_pass:
|
| 59 |
+
runtime.best_p_pass=score
|
| 60 |
+
runtime.best_checkpoint=row.get('checkpoint')
|
| 61 |
+
if runtime.best_checkpoint is not None:
|
| 62 |
+
best_step=max((int(x.get('optimizer_step',0)) for x in history
|
| 63 |
+
if x.get('checkpoint')==runtime.best_checkpoint),default=0)
|
| 64 |
+
write_json(run_dir/'best_checkpoint.json',dict(step=best_step,
|
| 65 |
+
valid_row_auc=runtime.best_p_pass,path=runtime.best_checkpoint,
|
| 66 |
+
selection='primary E4 endpoint row ROC-AUC on chronological VALID'))
|
| 67 |
+
|
| 68 |
+
|
| 69 |
class ScaleRuntime(Runtime):
|
| 70 |
sample_weight_fn=staticmethod(bounded_zero_correct_sample_difficulty_weight)
|
| 71 |
|
|
|
|
| 178 |
ids=[r['prompt_id'] for r in self.manifest['valid_selection']]
|
| 179 |
labels={r['prompt_id']:int(r['target']) for r in self.manifest['valid_selection']}
|
| 180 |
prompts=[self.rows[pid]['prompt'] for pid in ids]
|
|
|
|
| 181 |
sampled=self.trainer.llm.generate(prompts,sampling_params=SamplingParams(
|
| 182 |
temperature=.8,max_tokens=1536,n=8,seed=42),use_tqdm=True)
|
| 183 |
records=[];correct_vuln_r2=[]
|
|
|
|
| 224 |
if len(self.eval_entropies)>=2 and all(x<.3 for x in self.eval_entropies[-2:]):
|
| 225 |
raise SmokeStop('two_consecutive_VALID_entropies_below_0.3 [D-GRPO-9]')
|
| 226 |
metrics=dict(optimizer_step=step,checkpoint=str(checkpoint),adapter_label='E4 balanced reward',
|
| 227 |
+
measurement_label='E4b step-100 matched eval, post-fix retry of the interrupted measurement',
|
| 228 |
inference_protocol='E2 exact: 8 generations, temperature 0.8, max_tokens 1536, seed 42',
|
| 229 |
n_rows=len(records),n_pairs=len({r['pair_id'] for r in records}),
|
| 230 |
parse_rate=sampled_parse,row_auc=score['auc'],
|
|
|
|
| 233 |
greedy_label_accuracy=correct/valid_n,greedy_per_class_accuracy=per_class,
|
| 234 |
greedy_predicted_vulnerable_fraction=predicted_vulnerable,
|
| 235 |
greedy_completion_entropy_nats=greedy_diagnostics['entropy_nats'],
|
| 236 |
+
entropy_nats=greedy_diagnostics['entropy_nats'],
|
| 237 |
correct_vulnerable_r2_mean=float(np.mean(correct_vuln_r2)) if correct_vuln_r2 else None,
|
| 238 |
correct_vulnerable_r2_n=len(correct_vuln_r2),test_touched=False)
|
| 239 |
path=self.run_dir/f'e4_valid_step_{step:06d}.json'
|
|
|
|
| 390 |
axes[1,0].plot(steps,[x['completion_tokens']['mean'] for x in curves],label='mean')
|
| 391 |
axes[1,0].plot(steps,[x['completion_tokens']['p95'] for x in curves],label='step p95')
|
| 392 |
axes[1,0].legend();axes[1,0].set_title('Completion tokens')
|
| 393 |
+
if is_e4:
|
| 394 |
+
# E4 uses the E2-matched sampled protocol. Its schema intentionally
|
| 395 |
+
# reports row_auc and greedy_label_accuracy, not the older
|
| 396 |
+
# label_accuracy / val_p_pass_at_1 pair.
|
| 397 |
+
for key, label in (('row_auc','row AUC'),
|
| 398 |
+
('greedy_label_accuracy','greedy accuracy')):
|
| 399 |
+
points=[x for x in validations if x.get(key) is not None]
|
| 400 |
+
if points:
|
| 401 |
+
axes[1,1].plot([x['optimizer_step'] for x in points],
|
| 402 |
+
[x[key] for x in points],'o-',label=label)
|
| 403 |
+
axes[1,1].set_title('E2-matched VALID AUC; greedy accuracy is secondary')
|
| 404 |
+
else:
|
| 405 |
+
for key in ('label_accuracy','val_p_pass_at_1'):
|
| 406 |
+
points=[x for x in validations if x.get(key) is not None]
|
| 407 |
+
if points:
|
| 408 |
+
axes[1,1].plot([x['optimizer_step'] for x in points],
|
| 409 |
+
[x[key] for x in points],'o-',label=key)
|
| 410 |
+
axes[1,1].set_title('Scheduled full VALID; matched diagnostic is separate')
|
| 411 |
+
if axes[1,1].lines:
|
| 412 |
+
axes[1,1].legend()
|
| 413 |
for ax in axes.flat:
|
| 414 |
ax.set_xlabel('Optimizer step')
|
| 415 |
if manifest.get('continuation'):
|
|
|
|
| 419 |
fig.tight_layout();fig.savefig(run_dir/'training_curves.png',dpi=140);plt.close(fig)
|
| 420 |
|
| 421 |
|
| 422 |
+
def worker(run_dir, eval_only_step=None):
|
| 423 |
import pandas as pd
|
| 424 |
import torch
|
| 425 |
from datasets import Dataset
|
|
|
|
| 432 |
from scripts.verify_grpo_reward_equivalence import sha256
|
| 433 |
|
| 434 |
runtime=None;trainer=None;e4=False
|
| 435 |
+
start=float(os.environ.get('SCALE_START_UNIX',time.time()))
|
| 436 |
status=dict(status='initializing',started_at=now(),authorization='Review #20 + D-GRPO-14',
|
| 437 |
test_touched=False,scale_budget_ceiling_gpu_hours=250,
|
| 438 |
launch_segment_budget_gpu_hours=40,resume_requires_user=True)
|
|
|
|
| 447 |
e4b=manifest.get('launch_authority')=='D-GRPO-17'
|
| 448 |
if e4b:status['authorization']='D-GRPO-17'
|
| 449 |
resume_file=run_dir/'e4b_resume.json'
|
| 450 |
+
if eval_only_step is not None:
|
| 451 |
+
if not e4b or eval_only_step != 100:
|
| 452 |
+
raise SmokeStop('only_the_authorized_E4b_step100_eval_retry_is_supported')
|
| 453 |
+
from scripts.verify_grpo_reward_equivalence import sha256
|
| 454 |
+
from src.rl.full_state_checkpoint import verify_full_state
|
| 455 |
+
checkpoint=run_dir/'checkpoints'/f'step-{eval_only_step:06d}'
|
| 456 |
+
if not verify_full_state(checkpoint,expected_base_revision=manifest['policy']['base_revision']):
|
| 457 |
+
raise SmokeStop('step100_eval_checkpoint_integrity_failure')
|
| 458 |
+
adapter_hash=sha256(checkpoint/'adapter/adapter_model.safetensors')
|
| 459 |
+
continuation=dict(source_rule='D-GRPO-17',source_run=run_dir.relative_to(ROOT).as_posix(),
|
| 460 |
+
checkpoint=(checkpoint.relative_to(ROOT).as_posix()),optimizer_step=eval_only_step,
|
| 461 |
+
lr_factor=1.0,adapter_sha256=adapter_hash,adam_reset=False,scheduler_reset=False)
|
| 462 |
+
manifest['continuation']=continuation
|
| 463 |
+
status.update(authorization='Review #28 step-100 standalone matched-eval retry',
|
| 464 |
+
eval_only=True,eval_checkpoint=str(checkpoint),resume_optimizer_step=eval_only_step)
|
| 465 |
+
elif e4b and resume_file.is_file():
|
| 466 |
continuation=json.loads(resume_file.read_text())
|
| 467 |
if continuation.get('source_rule')!='D-GRPO-17' or continuation.get('lr_factor')!=1.0:
|
| 468 |
raise SmokeStop('E4b_resume_pointer_invalid')
|
|
|
|
| 516 |
runtime.best_p_pass=resume_extra['best_p_pass']
|
| 517 |
runtime.best_checkpoint=resume_extra['best_checkpoint']
|
| 518 |
runtime.last_safe_step=continuation['optimizer_step']
|
| 519 |
+
if e4b:
|
| 520 |
+
restore_e4b_eval_history(runtime,run_dir)
|
| 521 |
+
else:
|
| 522 |
+
valid_curve_path=resume_source/'valid_curve.jsonl'
|
| 523 |
+
prior_valid=([json.loads(x) for x in valid_curve_path.read_text().splitlines()]
|
| 524 |
+
if valid_curve_path.is_file() else [])
|
| 525 |
+
runtime.eval_entropies=[x['entropy_nats'] for x in prior_valid]
|
| 526 |
runtime.steps_per_epoch=(len(runtime.train_ids)+63)//64
|
| 527 |
runtime.progress('loading_fixed_judge')
|
| 528 |
runtime.judge=FixedJudge(run_dir);runtime.judge.should_log=select_rollout
|
|
|
|
| 629 |
out=trainer.llm.generate([rows[runtime.train_ids[0]]['prompt']],
|
| 630 |
sampling_params=SamplingParams(temperature=1.,max_tokens=1536,n=1),use_tqdm=False)
|
| 631 |
record_integration_canary(runtime,trainer,out[0].outputs[0].text)
|
| 632 |
+
if eval_only_step is not None:
|
| 633 |
+
trainer.state.global_step=eval_only_step
|
| 634 |
+
runtime.run_dir=run_dir/'step100_matched_eval_retry'
|
| 635 |
+
runtime.run_dir.mkdir(parents=True,exist_ok=True)
|
| 636 |
+
runtime.progress('e4b_step100_matched_eval_retry',checkpoint=str(resume_checkpoint),
|
| 637 |
+
protocol='E2 exact: n=8, temperature=0.8, max_tokens=1536, seed=42')
|
| 638 |
+
metrics=runtime.evaluate_e4_matched(eval_only_step,resume_checkpoint)
|
| 639 |
+
status.update(status='eval_only_complete',metrics_path=str(runtime.run_dir/'e4_valid_step_000100.json'),
|
| 640 |
+
row_auc=metrics['row_auc'],auc_delta_vs_sft500=metrics['auc_delta_vs_sft500'],
|
| 641 |
+
sampled_parse_rate=metrics['parse_rate'],gpu_wall_hours=(time.time()-start)/3600)
|
| 642 |
+
else:
|
| 643 |
+
runtime.progress('training_scale',config_hash=config_hash,train_rows=len(runtime.train_ids),
|
| 644 |
+
valid_rows=len(manifest['valid_selection']),wall_cap_hours=40)
|
| 645 |
+
trainer.train(resume_from_checkpoint=str(bridge) if bridge else None)
|
| 646 |
+
status.update(status='stopped',stop_reason='trainer_reached_max_steps_without_review',
|
| 647 |
+
requires_user_review=True)
|
| 648 |
except SmokeStop as error:
|
| 649 |
status.update(status='stopped',stop_reason=str(error),requires_user_review=True)
|
| 650 |
print('TRIPWIRE_OR_BUDGET_STOP',error,flush=True)
|
|
|
|
| 664 |
if runtime and runtime.curve:
|
| 665 |
status['aggregate_zero_variance_fraction']=sum(s['zero_groups'] for s in runtime.curve)/(64*len(runtime.curve))
|
| 666 |
status['aggregate_rollout_accounting']=rollout_accounting(runtime.curve,canary=runtime.canary)
|
| 667 |
+
status_path=(run_dir/'e4b_step100_eval_worker_status.json' if eval_only_step is not None
|
| 668 |
+
else run_dir/'scale_final_status.json')
|
| 669 |
+
write_json(status_path,status)
|
| 670 |
+
if eval_only_step is None:
|
| 671 |
+
write_progress_report(run_dir,status)
|
| 672 |
print('SCALE_FINAL_STATUS',json.dumps(status),flush=True)
|
| 673 |
if status['status']=='failed':raise SystemExit(2)
|
| 674 |
|
|
|
|
| 723 |
|
| 724 |
def main():
|
| 725 |
parser=argparse.ArgumentParser();parser.add_argument('--worker',action='store_true')
|
| 726 |
+
parser.add_argument('--run-dir',type=Path,required=True)
|
| 727 |
+
parser.add_argument('--eval-only-step',type=int);args=parser.parse_args()
|
| 728 |
run_dir=args.run_dir.resolve()
|
| 729 |
if not (run_dir/'run_manifest.json').is_file():raise SystemExit('frozen preflight manifest missing')
|
| 730 |
+
if args.eval_only_step is not None:worker(run_dir,eval_only_step=args.eval_only_step)
|
| 731 |
+
elif args.worker:worker(run_dir)
|
| 732 |
else:supervise(run_dir)
|
| 733 |
|
| 734 |
if __name__=='__main__':main()
|
scripts/stage_review30_test_eval.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage the one-time final TEST evaluation without opening TEST data.
|
| 2 |
+
|
| 3 |
+
This dry run validates the cached E4b VALID result schema and records the frozen
|
| 4 |
+
four-system evaluation plan. It intentionally has no code path that reads a split
|
| 5 |
+
file or invokes a model.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
VALID_FIXTURE = ROOT / "results/phase6/review26/e4b_balanced_reward/e4_valid_step_000200.json"
|
| 14 |
+
OUTPUT = ROOT / "results/phase10/test_staging/dry_run.json"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def main() -> dict:
|
| 18 |
+
record = json.loads(VALID_FIXTURE.read_text())
|
| 19 |
+
required = {"optimizer_step", "row_auc", "row_auc_pair_cluster_95_ci", "parse_rate",
|
| 20 |
+
"greedy_label_accuracy", "greedy_per_class_accuracy", "test_touched"}
|
| 21 |
+
missing = sorted(required - record.keys())
|
| 22 |
+
if missing:
|
| 23 |
+
raise ValueError(f"VALID fixture missing required metric fields: {missing}")
|
| 24 |
+
if record["test_touched"] is not False:
|
| 25 |
+
raise ValueError("VALID fixture indicates TEST was touched")
|
| 26 |
+
result = {
|
| 27 |
+
"status": "valid_fixture_schema_pass",
|
| 28 |
+
"fixture": str(VALID_FIXTURE.relative_to(ROOT)),
|
| 29 |
+
"fixture_step": record["optimizer_step"],
|
| 30 |
+
"fixture_auc": record["row_auc"],
|
| 31 |
+
"fixture_parse_rate": record["parse_rate"],
|
| 32 |
+
"metrics_schema": sorted(required),
|
| 33 |
+
"final_test_systems": ["classical pipeline", "SFT-500", "GRPO-324", "E4b step-200"],
|
| 34 |
+
"final_test_protocol": "E2-matched; accuracy + Wilson; per-class accuracy; row AUC + pair-cluster CI; IoU; P-Pass@1; trace validity",
|
| 35 |
+
"final_test_execution_authorized": False,
|
| 36 |
+
"test_split_opened": False,
|
| 37 |
+
"model_inference_run": False,
|
| 38 |
+
"note": "Dry run uses a cached chronological VALID result only; TEST path was not enumerated or read.",
|
| 39 |
+
}
|
| 40 |
+
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
| 41 |
+
OUTPUT.write_text(json.dumps(result, indent=2) + "\n")
|
| 42 |
+
print(json.dumps(result, indent=2))
|
| 43 |
+
return result
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
main()
|
scripts/tracegrpo-e4b.service
CHANGED
|
@@ -11,7 +11,7 @@ Environment=HF_HUB_OFFLINE=1
|
|
| 11 |
Environment=TOKENIZERS_PARALLELISM=false
|
| 12 |
Environment=PYTHONUNBUFFERED=1
|
| 13 |
ExecStartPre=+/usr/bin/systemctl stop qwen3-32b.service
|
| 14 |
-
ExecStart=/home/ubuntu/ib_connect_research/project/tracegrpo/.venv/bin/python -u
|
| 15 |
Restart=on-failure
|
| 16 |
RestartSec=30
|
| 17 |
KillMode=control-group
|
|
|
|
| 11 |
Environment=TOKENIZERS_PARALLELISM=false
|
| 12 |
Environment=PYTHONUNBUFFERED=1
|
| 13 |
ExecStartPre=+/usr/bin/systemctl stop qwen3-32b.service
|
| 14 |
+
ExecStart=/home/ubuntu/ib_connect_research/project/tracegrpo/.venv/bin/python -u -m scripts.run_review26_e4b_supervisor
|
| 15 |
Restart=on-failure
|
| 16 |
RestartSec=30
|
| 17 |
KillMode=control-group
|
scripts/verify_complete_hf_archives.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Stream every recovery member and compare with the authoritative file inventory."""
|
| 3 |
+
import argparse
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import tarfile
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
def verify(root):
|
| 10 |
+
m=json.loads((root/'SNAPSHOT_MANIFEST.json').read_text());reports=[]
|
| 11 |
+
for archive in m['archives']:
|
| 12 |
+
expected={r['path']:r for r in m['files'] if r['archive']==archive['path']};actual={}
|
| 13 |
+
h=hashlib.sha256()
|
| 14 |
+
with (root/archive['path']).open('rb') as stream:
|
| 15 |
+
for block in iter(lambda:stream.read(8*1024*1024),b''):h.update(block)
|
| 16 |
+
if h.hexdigest()!=archive['sha256']:raise ValueError('Archive SHA mismatch: '+archive['path'])
|
| 17 |
+
with tarfile.open(root/archive['path'],'r|gz') as tar:
|
| 18 |
+
for member in tar:
|
| 19 |
+
if member.name=='BUNDLE_MANIFEST.json':
|
| 20 |
+
inner=json.loads(tar.extractfile(member).read())
|
| 21 |
+
if {r['path'] for r in inner['files']}!=set(expected):raise ValueError('Bundle inventory mismatch')
|
| 22 |
+
continue
|
| 23 |
+
if member.islnk():
|
| 24 |
+
if member.linkname not in actual:raise ValueError('Missing hard-link target')
|
| 25 |
+
actual[member.name]=actual[member.linkname];continue
|
| 26 |
+
if not member.isfile():raise ValueError('Unexpected non-file archive member')
|
| 27 |
+
h=hashlib.sha256();size=0;stream=tar.extractfile(member)
|
| 28 |
+
for block in iter(lambda:stream.read(8*1024*1024),b''):h.update(block);size+=len(block)
|
| 29 |
+
actual[member.name]={'sha256':h.hexdigest(),'bytes':size}
|
| 30 |
+
if set(actual)!=set(expected):raise ValueError('Member set mismatch: '+archive['path'])
|
| 31 |
+
for path,row in expected.items():
|
| 32 |
+
if actual[path]['sha256']!=row['sha256'] or actual[path]['bytes']!=row['bytes']:raise ValueError('File content mismatch: '+path)
|
| 33 |
+
rec={'archive':archive['path'],'verified_files':len(actual),'archive_sha256':archive['sha256'],'all_member_hashes_match':True};reports.append(rec);print(json.dumps(rec),flush=True)
|
| 34 |
+
status={'status':'verified','archives':reports,'total_files':sum(r['verified_files'] for r in reports),'test_evaluated':False,'models_loaded':False}
|
| 35 |
+
(root/'ARCHIVE_VERIFICATION.json').write_text(json.dumps(status,indent=2)+'\n');return status
|
| 36 |
+
|
| 37 |
+
if __name__=='__main__':
|
| 38 |
+
p=argparse.ArgumentParser();p.add_argument('release',type=Path);a=p.parse_args();verify(a.release)
|
src/baselines/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Frozen classical baselines and artifact loaders."""
|
src/baselines/classical_pipeline.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Persisted Phase 7 character detector and line-localizer components."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import joblib
|
| 10 |
+
import numpy as np
|
| 11 |
+
import pandas as pd
|
| 12 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 13 |
+
from sklearn.linear_model import SGDClassifier
|
| 14 |
+
|
| 15 |
+
from scripts.run_line_localization_baseline import materialize
|
| 16 |
+
|
| 17 |
+
LABEL_CONFIG = dict(analyzer="char", ngram_range=(3, 5), min_df=2,
|
| 18 |
+
max_features=300_000, sublinear_tf=True, dtype=np.float32)
|
| 19 |
+
LABEL_MODEL_CONFIG = dict(loss="log_loss", alpha=1e-5, max_iter=200, tol=1e-3,
|
| 20 |
+
random_state=42, average=True)
|
| 21 |
+
LINE_CONFIG = dict(analyzer="char", ngram_range=(2, 5), min_df=2,
|
| 22 |
+
max_features=250_000, sublinear_tf=True, dtype=np.float32)
|
| 23 |
+
LINE_MODEL_CONFIG = dict(loss="log_loss", alpha=1e-5, max_iter=30, tol=1e-3,
|
| 24 |
+
random_state=42, average=True, class_weight="balanced")
|
| 25 |
+
ARTIFACT_FILES = {
|
| 26 |
+
"label_vectorizer": "label_vectorizer.joblib",
|
| 27 |
+
"label_classifier": "label_classifier.joblib",
|
| 28 |
+
"line_vectorizer": "line_vectorizer.joblib",
|
| 29 |
+
"line_localizer": "line_localizer.joblib",
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def sha256_file(path: Path) -> str:
|
| 34 |
+
digest = hashlib.sha256()
|
| 35 |
+
with path.open("rb") as stream:
|
| 36 |
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
| 37 |
+
digest.update(chunk)
|
| 38 |
+
return digest.hexdigest()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def train_pair_texts(frame: pd.DataFrame, pair_ids: set[str]) -> tuple[list[str], np.ndarray]:
|
| 42 |
+
texts: list[str] = []
|
| 43 |
+
labels: list[int] = []
|
| 44 |
+
subset = frame[frame["pair_id"].astype(str).isin(pair_ids)]
|
| 45 |
+
for pair_id, group in subset.groupby("pair_id", sort=True):
|
| 46 |
+
if len(group) != 2 or set(group["target"].astype(int)) != {0, 1}:
|
| 47 |
+
raise ValueError(f"label pair integrity failure: {pair_id}")
|
| 48 |
+
for target in (0, 1):
|
| 49 |
+
row = group[group["target"].astype(int) == target].iloc[0]
|
| 50 |
+
texts.append(str(row["func_vuln"] if target else row["func_patch"]))
|
| 51 |
+
labels.append(target)
|
| 52 |
+
return texts, np.asarray(labels, dtype=np.int8)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def fit_and_save(corpus: Path, train_split: Path, annotations: Path,
|
| 56 |
+
artifact_dir: Path) -> dict[str, Any]:
|
| 57 |
+
train_ids = set(map(str, json.loads(train_split.read_text())))
|
| 58 |
+
frame = pd.read_parquet(corpus)
|
| 59 |
+
texts, labels = train_pair_texts(frame, train_ids)
|
| 60 |
+
label_vectorizer = TfidfVectorizer(**LABEL_CONFIG)
|
| 61 |
+
x_label = label_vectorizer.fit_transform(texts)
|
| 62 |
+
label_classifier = SGDClassifier(**LABEL_MODEL_CONFIG).fit(x_label, labels)
|
| 63 |
+
|
| 64 |
+
train_examples, missing, out_of_range = materialize(corpus, train_split, annotations)
|
| 65 |
+
line_docs = [candidate for ex in train_examples for candidate in ex["candidates"]]
|
| 66 |
+
line_labels = np.asarray([int(i + 1 in ex["gold"])
|
| 67 |
+
for ex in train_examples for i in range(ex["n_lines"])], dtype=np.int8)
|
| 68 |
+
if missing or out_of_range or not line_labels.any():
|
| 69 |
+
raise ValueError(f"TRAIN localization targets invalid: missing={missing}, out_of_range={out_of_range}")
|
| 70 |
+
line_vectorizer = TfidfVectorizer(**LINE_CONFIG)
|
| 71 |
+
x_line = line_vectorizer.fit_transform(line_docs)
|
| 72 |
+
line_localizer = SGDClassifier(**LINE_MODEL_CONFIG).fit(x_line, line_labels)
|
| 73 |
+
|
| 74 |
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
| 75 |
+
objects = locals()
|
| 76 |
+
for key, filename in ARTIFACT_FILES.items():
|
| 77 |
+
joblib.dump(objects[key], artifact_dir / filename, compress=3)
|
| 78 |
+
manifest = {
|
| 79 |
+
"artifact_version": "phase7_classical_v1",
|
| 80 |
+
"fit_scope": "chronological TRAIN only",
|
| 81 |
+
"train_pairs": len(train_ids),
|
| 82 |
+
"train_rows": len(texts),
|
| 83 |
+
"train_line_candidates": len(line_labels),
|
| 84 |
+
"train_positive_lines": int(line_labels.sum()),
|
| 85 |
+
"seed": 42,
|
| 86 |
+
"label_vectorizer_config": {**LABEL_CONFIG, "dtype": "float32"},
|
| 87 |
+
"label_classifier_config": LABEL_MODEL_CONFIG,
|
| 88 |
+
"line_vectorizer_config": {**LINE_CONFIG, "dtype": "float32"},
|
| 89 |
+
"line_localizer_config": LINE_MODEL_CONFIG,
|
| 90 |
+
"versions": {"sklearn": __import__("sklearn").__version__, "numpy": np.__version__,
|
| 91 |
+
"pandas": pd.__version__, "joblib": joblib.__version__},
|
| 92 |
+
"inputs_sha256": {p.name: sha256_file(p) for p in (corpus, train_split, annotations)},
|
| 93 |
+
"test_read": False,
|
| 94 |
+
"artifacts": {key: {"path": filename, "sha256": sha256_file(artifact_dir / filename)}
|
| 95 |
+
for key, filename in ARTIFACT_FILES.items()},
|
| 96 |
+
}
|
| 97 |
+
(artifact_dir / "manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
|
| 98 |
+
return manifest
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def load_pipeline(artifact_dir: Path, *, verify_hashes: bool = True) -> dict[str, Any]:
|
| 102 |
+
manifest = json.loads((artifact_dir / "manifest.json").read_text())
|
| 103 |
+
if manifest.get("artifact_version") != "phase7_classical_v1":
|
| 104 |
+
raise ValueError("Unsupported classical artifact version")
|
| 105 |
+
loaded = {}
|
| 106 |
+
for key, filename in ARTIFACT_FILES.items():
|
| 107 |
+
path = artifact_dir / filename
|
| 108 |
+
if verify_hashes and sha256_file(path) != manifest["artifacts"][key]["sha256"]:
|
| 109 |
+
raise ValueError(f"Artifact hash mismatch: {filename}")
|
| 110 |
+
loaded[key] = joblib.load(path)
|
| 111 |
+
loaded["manifest"] = manifest
|
| 112 |
+
return loaded
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def predict_labels(pipeline: dict[str, Any], frame: pd.DataFrame,
|
| 116 |
+
pair_ids: set[str]) -> list[dict[str, Any]]:
|
| 117 |
+
texts, labels = train_pair_texts(frame, pair_ids)
|
| 118 |
+
subset = frame[frame["pair_id"].astype(str).isin(pair_ids)]
|
| 119 |
+
rows = []
|
| 120 |
+
for pair_id, group in subset.groupby("pair_id", sort=True):
|
| 121 |
+
for target in (0, 1):
|
| 122 |
+
row = group[group["target"].astype(int) == target].iloc[0]
|
| 123 |
+
rows.append({"sample_id": str(row["sample_id"]), "pair_id": str(pair_id), "target": target})
|
| 124 |
+
# train_pair_texts guarantees the same pair/label ordering as rows.
|
| 125 |
+
scores = pipeline["label_classifier"].decision_function(
|
| 126 |
+
pipeline["label_vectorizer"].transform(texts))
|
| 127 |
+
predictions = pipeline["label_classifier"].predict(
|
| 128 |
+
pipeline["label_vectorizer"].transform(texts)).astype(int)
|
| 129 |
+
if len(rows) != len(labels):
|
| 130 |
+
raise AssertionError("Label metadata/prediction alignment failure")
|
| 131 |
+
return [row | {"decision_score": float(score), "pred_target": int(pred)}
|
| 132 |
+
for row, score, pred in zip(rows, scores, predictions, strict=True)]
|
src/rl/live_adapter.py
CHANGED
|
@@ -8,7 +8,9 @@ from __future__ import annotations
|
|
| 8 |
from dataclasses import dataclass
|
| 9 |
import hashlib
|
| 10 |
import json
|
|
|
|
| 11 |
import shutil
|
|
|
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
|
|
@@ -25,7 +27,7 @@ class AdapterSnapshot:
|
|
| 25 |
|
| 26 |
|
| 27 |
def publish_live_adapter(model, root: Path, step: int, base_revision: str) -> AdapterSnapshot:
|
| 28 |
-
"""Publish an immutable adapter
|
| 29 |
configs = model.peft_config
|
| 30 |
if len(configs) != 1:
|
| 31 |
raise ValueError("D-GRPO-3 requires exactly one shared adapter")
|
|
@@ -38,21 +40,44 @@ def publish_live_adapter(model, root: Path, step: int, base_revision: str) -> Ad
|
|
| 38 |
raise ValueError("adapter step and pinned base revision are required")
|
| 39 |
if any(getattr(module, "merged", False) for module in model.modules()):
|
| 40 |
raise ValueError("D-GRPO-1 forbids merged policy layers")
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
class NativeAdapterLLM:
|
|
|
|
| 8 |
from dataclasses import dataclass
|
| 9 |
import hashlib
|
| 10 |
import json
|
| 11 |
+
import os
|
| 12 |
import shutil
|
| 13 |
+
import tempfile
|
| 14 |
from pathlib import Path
|
| 15 |
|
| 16 |
|
|
|
|
| 27 |
|
| 28 |
|
| 29 |
def publish_live_adapter(model, root: Path, step: int, base_revision: str) -> AdapterSnapshot:
|
| 30 |
+
"""Publish an immutable adapter copy; reuse an identical same-step publication."""
|
| 31 |
configs = model.peft_config
|
| 32 |
if len(configs) != 1:
|
| 33 |
raise ValueError("D-GRPO-3 requires exactly one shared adapter")
|
|
|
|
| 40 |
raise ValueError("adapter step and pinned base revision are required")
|
| 41 |
if any(getattr(module, "merged", False) for module in model.modules()):
|
| 42 |
raise ValueError("D-GRPO-1 forbids merged policy layers")
|
| 43 |
+
root = root.resolve()
|
| 44 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 45 |
+
destination = root / f"adapter-step-{step:06d}"
|
| 46 |
+
# Serialize to a temporary directory first. If this step was already
|
| 47 |
+
# published, compare bytes and provenance before reusing it; a same-step
|
| 48 |
+
# but changed policy is a hard error, never a silent overwrite.
|
| 49 |
+
with tempfile.TemporaryDirectory(prefix=f".adapter-step-{step:06d}-", dir=root) as temp:
|
| 50 |
+
staged = Path(temp)
|
| 51 |
+
model.save_pretrained(staged, safe_serialization=True, selected_adapters=[name])
|
| 52 |
+
staged_weights = staged / "adapter_model.safetensors"
|
| 53 |
+
if not staged_weights.is_file():
|
| 54 |
+
raise ValueError("adapter-only publication has no adapter weights")
|
| 55 |
+
if any(staged.glob("model*.safetensors")):
|
| 56 |
+
raise ValueError("unexpected base/merged weights in adapter publication")
|
| 57 |
+
weights_sha = hashlib.sha256(staged_weights.read_bytes()).hexdigest()
|
| 58 |
+
provenance = {
|
| 59 |
+
"adapter_step": step, "pinned_base_revision": base_revision,
|
| 60 |
+
"adapter_weights_sha256": weights_sha, "merged": False,
|
| 61 |
+
}
|
| 62 |
+
if destination.exists():
|
| 63 |
+
weight_file = destination / "adapter_model.safetensors"
|
| 64 |
+
provenance_file = destination / "policy_provenance.json"
|
| 65 |
+
try:
|
| 66 |
+
existing = json.loads(provenance_file.read_text())
|
| 67 |
+
existing_sha = hashlib.sha256(weight_file.read_bytes()).hexdigest()
|
| 68 |
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
| 69 |
+
raise ValueError("existing same-step adapter publication is incomplete") from exc
|
| 70 |
+
if existing != provenance or existing_sha != weights_sha:
|
| 71 |
+
raise ValueError("same-step adapter publication differs from current policy")
|
| 72 |
+
return AdapterSnapshot(step, destination, weights_sha, base_revision)
|
| 73 |
+
(staged / "policy_provenance.json").write_text(json.dumps(provenance, indent=2) + "\n")
|
| 74 |
+
try:
|
| 75 |
+
os.rename(staged, destination)
|
| 76 |
+
except FileExistsError:
|
| 77 |
+
# A concurrent publisher may have won the rename. Validate its
|
| 78 |
+
# immutable contents on the next invocation rather than overwrite.
|
| 79 |
+
raise
|
| 80 |
+
return AdapterSnapshot(step, destination, weights_sha, base_revision)
|
| 81 |
|
| 82 |
|
| 83 |
class NativeAdapterLLM:
|