specialists: switch training to PyTorch, add capacity sweep infrastructure

- train_specialist_pt.py: pure PyTorch trainer replacing Fabricate binary.
  No SQLite/WAL — saves checkpoint.pt + history.json only. Fabricate was
  causing D-state IO blocking on the HDD due to continuous WAL writes.
- eval_specialist_pt.py: PyTorch eval with confusion matrix, threshold
  analysis, guardrail assessment, and deployment verdict.
- capacity_sweep.py: updated to call PyTorch scripts; TIERS now include
  heads field (4/6/8) since PyTorch supports multi-head unlike Fabricate.
- prereq_op_binary_split.py: evaluate_combined rewritten in PyTorch.
- Performance fix in train_specialist_pt.py: load training data onto GPU
  once and sample via torch.randint instead of DataLoader (eliminates
  Python/CPU overhead on tiny datasets). Fix applied, not yet timed.

Next session: time the fix, then run sweep_all_gates.sh --pilot-only.
See HANDOFF-2026-03-31.md for full context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-03-31 22:55:51 -06:00
parent 410bb2d78c
commit 3fc9deac5b
61 changed files with 9168 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""
gen_confidence_tier_data.py
Generate synthetic training data for the confidence_tier specialist.
The Whetstone confidence formula (from TaskitemConfidenceAmbiguity.h):
base = 85
- 25 if not queueReady
- 10 per ambiguous requirement (up to 3)
- 8 per conflict detected
- 5 if prereq_count > 2
- 4 if has_dependencies
clamped to [0, 100]
Tier labels:
0 = high (confidence >= 80)
1 = medium (confidence 6079)
2 = low (confidence < 60)
The specialist encodes features as a short token sequence:
"conflicts=N ambiguity=M deps=D prereqs=P queueready=B"
Usage:
python3 specialists/scripts/gen_confidence_tier_data.py \
--train-out specialists/data/generated/confidence_tier_train.tsv \
--eval-out specialists/data/generated/confidence_tier_eval.tsv
"""
import random
import argparse
from pathlib import Path
from collections import Counter
LABEL_ORDER = ["high", "medium", "low"]
LABEL_INDEX = {lbl: i for i, lbl in enumerate(LABEL_ORDER)}
def compute_confidence(conflicts, ambiguity, deps, prereqs, queue_ready):
conf = 85
if not queue_ready:
conf -= 25
conf -= min(ambiguity, 3) * 10
conf -= conflicts * 8
if prereqs > 2:
conf -= 5
if deps > 0:
conf -= 4
return max(0, min(100, conf))
def tier(conf):
if conf >= 80:
return "high"
elif conf >= 60:
return "medium"
else:
return "low"
def encode_input(conflicts, ambiguity, deps, prereqs, queue_ready):
qr = "yes" if queue_ready else "no"
return f"conflicts={conflicts} ambiguity={ambiguity} deps={deps} prereqs={prereqs} queueready={qr}"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--train-out", default="specialists/data/generated/confidence_tier_train.tsv")
ap.add_argument("--eval-out", default="specialists/data/generated/confidence_tier_eval.tsv")
ap.add_argument("--n-per-class", type=int, default=400)
ap.add_argument("--seed", type=int, default=42)
args = ap.parse_args()
rng = random.Random(args.seed)
# Generate all unique input combinations and label them
seen = set()
by_tier: dict[str, list[str]] = {"high": [], "medium": [], "low": []}
for conflicts in range(0, 10):
for ambiguity in range(0, 4):
for deps in range(0, 3):
for prereqs in range(1, 5):
for queue_ready in (True, False):
key = (conflicts, ambiguity, deps, prereqs, queue_ready)
if key in seen:
continue
seen.add(key)
conf = compute_confidence(*key)
t = tier(conf)
text = encode_input(*key)
by_tier[t].append(text)
print("Raw tier distribution:", {k: len(v) for k, v in by_tier.items()})
# Balance to n_per_class (oversample if needed)
target = args.n_per_class
balanced = []
for lbl, examples in by_tier.items():
rng.shuffle(examples)
chosen = examples[:]
while len(chosen) < target:
chosen.extend(examples)
chosen = chosen[:target]
balanced.extend((lbl, ex) for ex in chosen)
rng.shuffle(balanced)
n_eval = max(1, int(len(balanced) * 0.15))
eval_set = balanced[:n_eval]
train_set = balanced[n_eval:]
Path(args.train_out).parent.mkdir(parents=True, exist_ok=True)
Path(args.eval_out).parent.mkdir(parents=True, exist_ok=True)
def write_tsv(path, rows):
with open(path, "w") as f:
for lbl, text in rows:
f.write(f"{LABEL_INDEX[lbl]}\t0\t{text}\n")
print(f"Wrote {len(rows)} rows → {path}")
write_tsv(args.train_out, train_set)
write_tsv(args.eval_out, eval_set)
for split_name, split in [("train", train_set), ("eval", eval_set)]:
c = Counter(lbl for lbl, _ in split)
print(f" {split_name}: {dict(sorted(c.items()))}")
if __name__ == "__main__":
main()