| """ |
| TD Self-Improvement Loop v1 — The Full Autonomous Pipeline |
| |
| Ties everything together into an endless self-improvement loop: |
| |
| Step 1: Test (benchmark + weight health) |
| Step 2: Diagnose (weakness finder) |
| Step 3: Find data (dataset finder) |
| Step 4: Train + Repair (selfimprove.py) |
| Step 5: Verify (re-benchmark) |
| → Loop |
| |
| This is the script you start and walk away from. |
| |
| Usage: |
| python td_loop.py # Start from latest checkpoint |
| python td_loop.py --model path/to/model # Start from specific model |
| python td_loop.py --max-cycles 10 # Stop after 10 cycles |
| python td_loop.py --dry-run # Just test+diagnose, don't train |
| """ |
|
|
| import json |
| import time |
| import shutil |
| import gc |
| import sys |
| import argparse |
| from pathlib import Path |
| from typing import Dict, List, Optional, Tuple |
| from dataclasses import dataclass |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class LoopConfig: |
| """Configuration for the self-improvement loop.""" |
|
|
| |
| model_path: str = "" |
| reference_model: str = "Qwen/Qwen3-VL-8B-Instruct" |
|
|
| |
| output_dir: str = "td_fuse_outputs/self_improve" |
| benchmark_dir: str = "td_fuse_outputs/benchmarks" |
| weakness_dir: str = "td_fuse_outputs/weakness_reports" |
| data_dir: str = "td_fuse_outputs/training_data" |
|
|
| |
| max_cycles: int = 0 |
| dry_run: bool = False |
| training_budget: int = 8000 |
|
|
| |
| targets: Dict = None |
|
|
| |
| hf_token: str = "" |
|
|
| |
| max_regression_pct: float = 0.05 |
| max_consecutive_regressions: int = 2 |
|
|
| |
| quick_test: bool = False |
|
|
| |
| max_training_hours: float = 4.0 |
|
|
| def __post_init__(self): |
| if self.targets is None: |
| self.targets = { |
| "math": 0.80, |
| "code": 0.70, |
| "reasoning": 0.75, |
| "creativity": 0.65, |
| "knowledge": 0.80, |
| "instruction_following": 0.85, |
| } |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class CycleResult: |
| """Results from one improvement cycle.""" |
| cycle_num: int |
| benchmark_scores: Dict[str, float] |
| weight_health_summary: Dict |
| weaknesses_found: int |
| data_collected: int |
| training_loss: float |
| improved: bool |
| regressions: List[str] |
| duration_seconds: float |
| model_path: str |
|
|
|
|
| def load_cycle_history(output_dir: str) -> List[CycleResult]: |
| """Load previous cycle results. Returns empty list if file is corrupt.""" |
| history_file = Path(output_dir) / "loop_history.json" |
| if not history_file.exists(): |
| return [] |
|
|
| try: |
| with open(history_file) as f: |
| data = json.load(f) |
| return [CycleResult(**c) for c in data] |
| except (json.JSONDecodeError, TypeError, KeyError) as e: |
| print(f" WARNING: History file corrupt ({e}) — starting fresh") |
| |
| backup = history_file.with_suffix(".json.bak") |
| try: |
| shutil.copy2(str(history_file), str(backup)) |
| print(f" Backed up corrupt history to {backup}") |
| except Exception: |
| pass |
| return [] |
|
|
|
|
| def save_cycle_history(history: List[CycleResult], output_dir: str): |
| """Save cycle history to disk. Atomic write to prevent corruption.""" |
| history_file = Path(output_dir) / "loop_history.json" |
| tmp_file = Path(output_dir) / "loop_history.json.tmp" |
| Path(output_dir).mkdir(parents=True, exist_ok=True) |
|
|
| data = [] |
| for c in history: |
| d = { |
| "cycle_num": c.cycle_num, |
| "benchmark_scores": c.benchmark_scores, |
| "weight_health_summary": c.weight_health_summary, |
| "weaknesses_found": c.weaknesses_found, |
| "data_collected": c.data_collected, |
| "training_loss": c.training_loss, |
| "improved": c.improved, |
| "regressions": c.regressions, |
| "duration_seconds": c.duration_seconds, |
| "model_path": c.model_path, |
| } |
| data.append(d) |
|
|
| try: |
| |
| with open(tmp_file, "w") as f: |
| json.dump(data, f, indent=2) |
| |
| tmp_file.replace(history_file) |
| except OSError as e: |
| print(f" WARNING: Failed to save history ({e}). Disk may be full!") |
| |
| try: |
| tmp_file.unlink(missing_ok=True) |
| except Exception: |
| pass |
|
|
|
|
| |
| |
| |
|
|
| def _setup_logging(output_dir: str): |
| """ |
| Tee all stdout/stderr to a log file so diagnostics survive terminal disconnects. |
| Returns the log file path. |
| """ |
| import sys as _sys |
|
|
| Path(output_dir).mkdir(parents=True, exist_ok=True) |
| log_path = Path(output_dir) / f"loop_{time.strftime('%Y%m%d_%H%M%S')}.log" |
|
|
| class Tee: |
| def __init__(self, original, log_file): |
| self.original = original |
| self.log_file = log_file |
| def write(self, data): |
| self.original.write(data) |
| try: |
| self.log_file.write(data) |
| self.log_file.flush() |
| except Exception: |
| pass |
| def flush(self): |
| self.original.flush() |
| try: |
| self.log_file.flush() |
| except Exception: |
| pass |
|
|
| log_fh = open(log_path, "a") |
| _sys.stdout = Tee(_sys.stdout, log_fh) |
| _sys.stderr = Tee(_sys.stderr, log_fh) |
| return str(log_path) |
|
|
|
|
| def _acquire_lockfile(output_dir: str) -> Optional[str]: |
| """ |
| Prevent two td_loop instances from running simultaneously. |
| Returns lockfile path if acquired, None if another instance is running. |
| """ |
| import os |
| lock_path = Path(output_dir) / "td_loop.lock" |
| Path(output_dir).mkdir(parents=True, exist_ok=True) |
|
|
| if lock_path.exists(): |
| |
| try: |
| with open(lock_path) as f: |
| old_pid = int(f.read().strip()) |
| os.kill(old_pid, 0) |
| |
| return None |
| except (ValueError, ProcessLookupError, PermissionError, OSError): |
| |
| print(f" Removing stale lockfile (PID {lock_path.read_text().strip()} is dead)") |
|
|
| with open(lock_path, "w") as f: |
| f.write(str(os.getpid())) |
| return str(lock_path) |
|
|
|
|
| def _release_lockfile(output_dir: str): |
| """Release the lockfile.""" |
| lock_path = Path(output_dir) / "td_loop.lock" |
| try: |
| lock_path.unlink(missing_ok=True) |
| except Exception: |
| pass |
|
|
|
|
| def run_loop(cfg: LoopConfig): |
| """ |
| The main self-improvement loop. |
| |
| Start it and walk away. It will: |
| 1. Test the model (benchmark + weight health) |
| 2. Find weaknesses |
| 3. Search for training data |
| 4. Train + repair |
| 5. Verify improvement |
| 6. Repeat |
| """ |
| |
| lock = _acquire_lockfile(cfg.output_dir) |
| if lock is None: |
| print("ERROR: Another td_loop instance is already running!") |
| print(f" Lock file: {Path(cfg.output_dir) / 'td_loop.lock'}") |
| print(" If this is wrong (crashed process), delete the lock file and retry.") |
| return |
|
|
| |
| log_path = _setup_logging(cfg.output_dir) |
|
|
| |
| import signal |
| shutdown_requested = [False] |
|
|
| def _signal_handler(signum, frame): |
| if shutdown_requested[0]: |
| print("\n FORCED SHUTDOWN — exiting immediately!") |
| _release_lockfile(cfg.output_dir) |
| raise SystemExit(1) |
| shutdown_requested[0] = True |
| print(f"\n SHUTDOWN REQUESTED (Ctrl+C) — will stop after current step completes.") |
| print(f" Press Ctrl+C again to force quit.") |
|
|
| signal.signal(signal.SIGINT, _signal_handler) |
| signal.signal(signal.SIGTERM, _signal_handler) |
|
|
| print("\n" + "=" * 60) |
| print("TD SELF-IMPROVEMENT LOOP v1") |
| print("=" * 60) |
| print(f" Max cycles: {'infinite' if cfg.max_cycles == 0 else cfg.max_cycles}") |
| print(f" Training budget: {cfg.training_budget} samples/cycle") |
| print(f" Dry run: {cfg.dry_run}") |
| print(f" Started: {time.strftime('%Y-%m-%d %H:%M:%S')}") |
| print(f" Log file: {log_path}") |
| print("=" * 60) |
|
|
| try: |
| _run_loop_inner(cfg, shutdown_requested) |
| finally: |
| _release_lockfile(cfg.output_dir) |
| print(f"\n Lock released. Log saved to: {log_path}") |
|
|
|
|
| def _run_loop_inner(cfg: LoopConfig, shutdown_requested: list): |
| """Inner loop logic, separated so run_loop can handle cleanup.""" |
|
|
| |
| try: |
| import torch |
| if torch.cuda.is_available(): |
| gpu_name = torch.cuda.get_device_name(0) |
| gpu_mem = torch.cuda.get_device_properties(0).total_memory / 1e9 |
| print(f"\n GPU: {gpu_name} ({gpu_mem:.1f} GB)") |
| if gpu_mem < 20: |
| print(f" WARNING: GPU has only {gpu_mem:.1f} GB — Qwen3-VL-8B needs ~24 GB for training!") |
| |
| test = torch.empty(256, 1024, 1024, dtype=torch.bfloat16, device="cuda") |
| del test |
| torch.cuda.empty_cache() |
| print(f" GPU health: OK (allocation test passed)") |
| else: |
| print("\n WARNING: No CUDA GPU detected! Training will be extremely slow or fail.") |
| except Exception as e: |
| print(f"\n WARNING: GPU check failed: {e}") |
|
|
| |
| try: |
| disk = shutil.disk_usage(cfg.output_dir if Path(cfg.output_dir).exists() else ".") |
| free_gb = disk.free / 1e9 |
| print(f" Disk: {free_gb:.1f} GB free") |
| if free_gb < 50: |
| print(f" WARNING: Low disk space! Need ~25 GB per cycle for checkpoints.") |
| except Exception: |
| pass |
|
|
| |
| history = load_cycle_history(cfg.output_dir) |
| cycle_num = len(history) + 1 |
| cycles_run_this_session = 0 |
| consecutive_regressions = 0 |
|
|
| |
| if not cfg.model_path: |
| cfg.model_path = _auto_detect_model() |
|
|
| print(f"\n Starting model: {cfg.model_path}") |
| print(f" Starting at cycle: {cycle_num}") |
|
|
| |
| while True: |
| |
| if shutdown_requested[0]: |
| print(f"\n Shutdown requested — stopping before cycle {cycle_num}") |
| _print_final_summary(history) |
| return |
| if cfg.max_cycles > 0 and cycles_run_this_session >= cfg.max_cycles: |
| print(f"\n Reached max cycles ({cfg.max_cycles}). Stopping.") |
| break |
|
|
| cycle_start = time.time() |
| print(f"\n{'#' * 60}") |
| print(f"# CYCLE {cycle_num}") |
| print(f"# {time.strftime('%Y-%m-%d %H:%M:%S')}") |
| print(f"{'#' * 60}") |
|
|
| |
| |
| |
| print(f"\n{'='*50}") |
| print(f"STEP 1/{5 if not cfg.dry_run else 2}: TEST MODEL") |
| print(f"{'='*50}") |
|
|
| |
| try: |
| benchmark_scores = _run_benchmark(cfg.model_path, cfg.benchmark_dir, cycle_num) |
| except Exception as e: |
| print(f"\n CRITICAL: Benchmark crashed: {e}") |
| import traceback |
| traceback.print_exc() |
| |
| gc.collect() |
| try: |
| import torch |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| except ImportError: |
| pass |
| try: |
| print(" Retrying benchmark after GPU cleanup...") |
| benchmark_scores = _run_benchmark(cfg.model_path, cfg.benchmark_dir, cycle_num) |
| except Exception as e2: |
| print(f" Benchmark failed twice: {e2}") |
| print(f" Skipping cycle {cycle_num}") |
| consecutive_regressions += 1 |
| if consecutive_regressions >= cfg.max_consecutive_regressions: |
| print(f"\n {consecutive_regressions} consecutive failures — STOPPING LOOP") |
| break |
| cycle_num += 1 |
| continue |
|
|
| |
| health_data = _analyze_weights(cfg.model_path, cfg.reference_model, cfg.output_dir) |
|
|
| |
| |
| |
| print(f"\n{'='*50}") |
| print(f"STEP 2/{5 if not cfg.dry_run else 2}: DIAGNOSE WEAKNESSES") |
| print(f"{'='*50}") |
|
|
| try: |
| weakness_report = _find_weaknesses( |
| benchmark_path=str(Path(cfg.benchmark_dir) / f"cycle{cycle_num}" / "results.json"), |
| health_data=health_data, |
| targets=cfg.targets, |
| budget=cfg.training_budget, |
| output_dir=cfg.weakness_dir, |
| cycle_num=cycle_num, |
| ) |
| except Exception as e: |
| print(f"\n WARNING: Weakness analysis crashed: {e}") |
| print(f" Using fallback: train all categories equally") |
| weakness_report = { |
| "search_plan": [{"category": c, "budget": cfg.training_budget // 6, |
| "keywords": [f"{c} training dataset"], |
| "hf_tags": [], "priority": 0.5, "score": 0.5, |
| "target": 0.75, "gap": 0.25} |
| for c in ["math", "code", "reasoning", "creativity", |
| "knowledge", "instruction_following"]], |
| "data_allocation": {c: cfg.training_budget // 6 |
| for c in ["math", "code", "reasoning", "creativity", |
| "knowledge", "instruction_following"]}, |
| "lora_target_layers": list(range(36)), |
| "norms_to_repair": [], |
| "total_weaknesses": 6, |
| } |
|
|
| if cfg.dry_run: |
| print("\n DRY RUN — stopping after diagnosis.") |
| result = CycleResult( |
| cycle_num=cycle_num, |
| benchmark_scores=benchmark_scores, |
| weight_health_summary=health_data.get("summary", {}), |
| weaknesses_found=weakness_report.get("total_weaknesses", 0), |
| data_collected=0, |
| training_loss=0.0, |
| improved=False, |
| regressions=[], |
| duration_seconds=time.time() - cycle_start, |
| model_path=cfg.model_path, |
| ) |
| history.append(result) |
| save_cycle_history(history, cfg.output_dir) |
| break |
|
|
| |
| |
| |
| print(f"\n{'='*50}") |
| print(f"STEP 3/5: FIND TRAINING DATA") |
| print(f"{'='*50}") |
|
|
| |
| if not weakness_report.get("search_plan"): |
| print("\n No weaknesses found — all targets met! Skipping training.") |
| print(" (This shouldn't happen unless the model is already great)") |
| training_data = {"total_samples": 0, "categories_covered": [], "categories_missing": []} |
| else: |
| try: |
| training_data = _find_training_data( |
| weakness_report=weakness_report, |
| output_dir=cfg.data_dir, |
| hf_token=cfg.hf_token, |
| cycle_num=cycle_num, |
| ) |
| except Exception as e: |
| print(f"\n WARNING: Data finder crashed: {e}") |
| print(f" Continuing with generic datasets (selfimprove.py fallback)") |
| training_data = {"total_samples": 0, "categories_covered": [], "categories_missing": []} |
|
|
| |
| |
| |
| print(f"\n{'='*50}") |
| print(f"STEP 4/5: TRAIN + REPAIR") |
| print(f"{'='*50}") |
|
|
| |
| try: |
| disk = shutil.disk_usage(cfg.output_dir if Path(cfg.output_dir).exists() else ".") |
| free_gb = disk.free / 1e9 |
| if free_gb < 10: |
| print(f"\n CRITICAL: Only {free_gb:.1f} GB disk free — skipping training!") |
| print(f" Need at least 10 GB to safely save a checkpoint.") |
| consecutive_regressions += 1 |
| cycle_num += 1 |
| continue |
| except Exception: |
| pass |
|
|
| try: |
| new_model_path, training_loss = _train_model( |
| model_path=cfg.model_path, |
| training_data_path=str(Path(cfg.data_dir) / f"cycle{cycle_num}" / "training_data.jsonl"), |
| lora_targets=weakness_report.get("lora_target_layers", list(range(36))), |
| norms_to_repair=weakness_report.get("norms_to_repair", []), |
| reference_model=cfg.reference_model, |
| output_dir=cfg.output_dir, |
| cycle_num=cycle_num, |
| quick_test=cfg.quick_test, |
| ) |
| except Exception as e: |
| print(f"\n CRITICAL: Training crashed: {e}") |
| print(f" Skipping this cycle and retrying with current model...") |
| import traceback |
| traceback.print_exc() |
| |
| cycle_duration = time.time() - cycle_start |
| result = CycleResult( |
| cycle_num=cycle_num, |
| benchmark_scores=benchmark_scores, |
| weight_health_summary=health_data.get("summary", {}), |
| weaknesses_found=weakness_report.get("total_weaknesses", 0), |
| data_collected=training_data.get("total_samples", 0), |
| training_loss=0.0, |
| improved=False, |
| regressions=["TRAINING_CRASH"], |
| duration_seconds=cycle_duration, |
| model_path=cfg.model_path, |
| ) |
| history.append(result) |
| save_cycle_history(history, cfg.output_dir) |
| consecutive_regressions += 1 |
| if consecutive_regressions >= cfg.max_consecutive_regressions: |
| print(f"\n {consecutive_regressions} consecutive failures — STOPPING LOOP") |
| break |
| cycle_num += 1 |
| |
| gc.collect() |
| try: |
| import torch |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| except ImportError: |
| pass |
| continue |
|
|
| |
| train_elapsed_h = (time.time() - cycle_start) / 3600 |
| if train_elapsed_h > cfg.max_training_hours: |
| print(f"\n WARNING: Training took {train_elapsed_h:.1f}h (limit: {cfg.max_training_hours}h)") |
| print(f" This may indicate a stuck process or oversized dataset.") |
|
|
| |
| |
| |
| print(f"\n{'='*50}") |
| print(f"STEP 5/5: VERIFY IMPROVEMENT") |
| print(f"{'='*50}") |
|
|
| try: |
| new_scores = _run_benchmark(new_model_path, cfg.benchmark_dir, cycle_num, suffix="_post") |
| except Exception as e: |
| print(f"\n CRITICAL: Post-training benchmark crashed: {e}") |
| print(f" Cannot verify improvement — treating as regression for safety.") |
| import traceback |
| traceback.print_exc() |
| new_scores = {} |
|
|
| |
| improved, regressions = _compare_scores(benchmark_scores, new_scores, cfg.max_regression_pct) |
|
|
| if improved: |
| print(f"\n ✓ IMPROVEMENT CONFIRMED — cycle {cycle_num} successful!") |
| consecutive_regressions = 0 |
| cfg.model_path = new_model_path |
| else: |
| print(f"\n ✗ REGRESSION DETECTED in: {', '.join(regressions)}") |
| consecutive_regressions += 1 |
|
|
| if consecutive_regressions >= cfg.max_consecutive_regressions: |
| print(f"\n {consecutive_regressions} consecutive regressions — STOPPING LOOP") |
| print(f" The model may have hit its improvement ceiling.") |
| break |
|
|
| |
| cfg.model_path = new_model_path |
|
|
| |
| cycle_duration = time.time() - cycle_start |
| result = CycleResult( |
| cycle_num=cycle_num, |
| benchmark_scores=new_scores, |
| weight_health_summary=health_data.get("summary", {}), |
| weaknesses_found=weakness_report.get("total_weaknesses", 0), |
| data_collected=training_data.get("total_samples", 0), |
| training_loss=training_loss, |
| improved=improved, |
| regressions=regressions, |
| duration_seconds=cycle_duration, |
| model_path=new_model_path, |
| ) |
| history.append(result) |
| save_cycle_history(history, cfg.output_dir) |
|
|
| |
| _print_cycle_summary(result, history) |
|
|
| |
| if _all_targets_met(new_scores, cfg.targets): |
| print(f"\n{'='*60}") |
| print(f"TARGET PERFORMANCE REACHED AFTER {cycle_num} CYCLES!") |
| print(f"{'='*60}") |
| break |
|
|
| |
| |
| |
| gc.collect() |
| try: |
| import torch |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| |
| allocated = torch.cuda.memory_allocated() / 1e9 |
| reserved = torch.cuda.memory_reserved() / 1e9 |
| print(f"\n GPU memory: {allocated:.1f} GB allocated, {reserved:.1f} GB reserved") |
| except ImportError: |
| pass |
|
|
| |
| cycles_run_this_session += 1 |
| if shutdown_requested[0]: |
| print(f"\n Shutdown requested — stopping after cycle {cycle_num}") |
| break |
|
|
| cycle_num += 1 |
|
|
| |
| _print_final_summary(history) |
|
|
|
|
| |
| |
| |
|
|
| def _auto_detect_model() -> str: |
| """Find the best starting model.""" |
| base = Path("td_fuse_outputs/self_improve") |
|
|
| |
| if base.exists(): |
| for n in range(50, 0, -1): |
| cycle_dir = base / f"improved_cycle{n}" |
| if cycle_dir.exists() and list(cycle_dir.glob("*.safetensors")): |
| print(f" Found cycle {n} output at {cycle_dir}") |
| return str(cycle_dir) |
|
|
| |
| old_improved = base / "improved" |
| if old_improved.exists() and list(old_improved.glob("*.safetensors")): |
| return str(old_improved) |
|
|
| |
| healed = Path("td_fuse_outputs/reasoning_healed") |
| if healed.exists() and list(healed.glob("*.safetensors")): |
| return str(healed) |
|
|
| raise FileNotFoundError("No model found! Need a starting model checkpoint.") |
|
|
|
|
| def _run_benchmark( |
| model_path: str, |
| benchmark_dir: str, |
| cycle_num: int, |
| suffix: str = "", |
| ) -> Dict[str, float]: |
| """Run td_benchmark.py and return scores per category.""" |
| from td_benchmark import BenchmarkConfig, run_benchmark |
|
|
| output_dir = str(Path(benchmark_dir) / f"cycle{cycle_num}{suffix}") |
| results_path = str(Path(output_dir) / "results.json") |
| Path(output_dir).mkdir(parents=True, exist_ok=True) |
|
|
| |
| mp = Path(model_path) |
| if mp.is_dir() and not (mp / "config.json").exists(): |
| raise FileNotFoundError(f"No config.json in {model_path} — stale or corrupt checkpoint") |
|
|
| config = BenchmarkConfig( |
| model_path=model_path, |
| output_dir=output_dir, |
| output_path=results_path, |
| disable_thinking=True, |
| ) |
|
|
| print(f" Running benchmark on {model_path}...") |
| results = run_benchmark(config) |
|
|
| |
| if not isinstance(results, dict): |
| print(f" WARNING: Benchmark returned {type(results)} instead of dict — treating as empty") |
| results = {} |
|
|
| |
| scores = {} |
| for category, data in results.get("categories", {}).items(): |
| if isinstance(data, dict): |
| scores[category] = data.get("score", 0.0) |
| else: |
| scores[category] = 0.0 |
|
|
| print(f" Benchmark scores:") |
| for cat, score in sorted(scores.items()): |
| print(f" {cat}: {score:.0%}") |
|
|
| |
| gc.collect() |
| try: |
| import torch |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| except ImportError: |
| pass |
|
|
| return scores |
|
|
|
|
| def _analyze_weights( |
| model_path: str, |
| reference_model: str, |
| output_dir: str, |
| ) -> Dict: |
| """Run weight health analysis from selfimprove.py.""" |
| try: |
| from selfimprove import ( |
| analyze_model_weights, |
| compute_health_scores, |
| compute_damage_scores, |
| compute_coherence_scores, |
| detect_clogged_norms, |
| ) |
|
|
| print(f" Analyzing model weights...") |
| model_analysis = analyze_model_weights(model_path, "TD model") |
| health_scores = compute_health_scores(model_analysis) |
| coherence_scores = compute_coherence_scores(model_analysis) |
| clogged_norms = detect_clogged_norms(health_scores) |
|
|
| |
| |
| damage_scores = {} |
| ref_path = Path(output_dir) / "reference_weights" |
| if ref_path.exists() and list(ref_path.glob("*.safetensors")): |
| print(f" Found cached reference at {ref_path}, comparing...") |
| ref_analysis = analyze_model_weights(str(ref_path), "Reference") |
| damage_scores = compute_damage_scores(model_analysis, ref_analysis) |
| else: |
| print(f" No cached reference — skipping damage comparison (saves time)") |
|
|
| |
| unhealthy = sum(1 for h in health_scores.values() if h["health_score"] > 0.2) |
| summary = { |
| "total_tensors": len(health_scores), |
| "unhealthy_tensors": unhealthy, |
| "clogged_norms": len(clogged_norms), |
| "coherence_issues": sum(1 for s in coherence_scores.values() if s > 0.3), |
| } |
|
|
| print(f" Weight health: {unhealthy}/{len(health_scores)} tensors with issues") |
| print(f" Clogged norms: {len(clogged_norms)}") |
|
|
| |
| gc.collect() |
|
|
| return { |
| "health_scores": health_scores, |
| "damage_scores": damage_scores, |
| "coherence_scores": coherence_scores, |
| "clogged_norms": clogged_norms, |
| "summary": summary, |
| } |
|
|
| except Exception as e: |
| print(f" Warning: Weight analysis failed: {e}") |
| return {"summary": {}, "health_scores": {}, "clogged_norms": []} |
|
|
|
|
| def _find_weaknesses( |
| benchmark_path: str, |
| health_data: Dict, |
| targets: Dict, |
| budget: int, |
| output_dir: str, |
| cycle_num: int, |
| ) -> Dict: |
| """Run td_weakness.py analysis.""" |
| from td_weakness import run_weakness_analysis |
|
|
| output_path = str(Path(output_dir) / f"cycle{cycle_num}" / "weakness_report.json") |
| Path(output_path).parent.mkdir(parents=True, exist_ok=True) |
|
|
| report = run_weakness_analysis( |
| benchmark_path=benchmark_path, |
| health_scores=health_data.get("health_scores") or {}, |
| damage_scores=health_data.get("damage_scores") or {}, |
| coherence_scores=health_data.get("coherence_scores") or {}, |
| clogged_norms=health_data.get("clogged_norms") or [], |
| targets=targets, |
| total_budget=budget, |
| output_path=output_path, |
| ) |
|
|
| return { |
| "search_plan": report.search_plan, |
| "data_allocation": report.data_allocation, |
| "lora_target_layers": report.lora_target_layers, |
| "norms_to_repair": report.norms_to_repair, |
| "total_weaknesses": len(report.answer_weaknesses) + len(report.weight_weaknesses), |
| } |
|
|
|
|
| def _find_training_data( |
| weakness_report: Dict, |
| output_dir: str, |
| hf_token: str, |
| cycle_num: int, |
| ) -> Dict: |
| """Run td_data_finder.py to search for and download data.""" |
| from td_data_finder import find_training_data |
|
|
| cycle_data_dir = str(Path(output_dir) / f"cycle{cycle_num}") |
|
|
| result = find_training_data( |
| search_plan=weakness_report["search_plan"], |
| data_allocation=weakness_report["data_allocation"], |
| output_dir=cycle_data_dir, |
| hf_token=hf_token if hf_token else None, |
| ) |
|
|
| return { |
| "total_samples": result.total_samples, |
| "categories_covered": result.categories_covered, |
| "categories_missing": result.categories_missing, |
| } |
|
|
|
|
| def _train_model( |
| model_path: str, |
| training_data_path: str, |
| lora_targets: List[int], |
| norms_to_repair: List[str], |
| reference_model: str, |
| output_dir: str, |
| cycle_num: int, |
| quick_test: bool = False, |
| ) -> Tuple[str, float]: |
| """ |
| Run training using selfimprove.py's training pipeline. |
| |
| Returns: (new_model_path, final_training_loss) |
| """ |
| from selfimprove import SelfImproveConfig, run_cycle |
|
|
| cfg = SelfImproveConfig() |
| |
| |
| cfg.model_path = model_path |
| cfg.reference_model = reference_model |
| cfg.output_dir = output_dir |
|
|
| if quick_test: |
| |
| |
| print(" *** QUICK TEST: Compressed training settings ***") |
| cfg.mega_mode = True |
| cfg.top_n_layers = 8 |
| cfg.min_improvement_score = 0.0 |
| cfg.lora_r = 32 |
| cfg.lora_alpha = 64 |
| cfg.train_batch = 4 |
| cfg.train_grad_accum = 2 |
| cfg.num_train_samples = 500 |
| cfg.train_epochs = 1 |
| cfg.learning_rate = 1e-5 |
| cfg.norm_learning_rate = 1e-3 |
| cfg.save_steps = 9999 |
| else: |
| |
| cfg.mega_mode = True |
| cfg.top_n_layers = 36 |
| cfg.min_improvement_score = 0.0 |
| cfg.lora_r = 128 |
| cfg.lora_alpha = 256 |
| cfg.train_batch = 4 |
| cfg.train_grad_accum = 4 |
| cfg.num_train_samples = 8000 |
| cfg.learning_rate = 1e-5 |
| cfg.norm_learning_rate = 1e-3 |
| cfg.save_steps = 200 |
|
|
| |
| |
| training_data_file = Path(training_data_path) |
| if training_data_file.exists() and training_data_file.stat().st_size > 0: |
| cfg.custom_data_path = str(training_data_file) |
| print(f" Using targeted training data: {training_data_file}") |
| else: |
| print(f" No targeted data found — selfimprove.py will use generic datasets") |
|
|
| |
| |
| import re as _re |
| si_match = _re.search(r'improved_cycle(\d+)', model_path) |
| if si_match: |
| si_cycle_num = int(si_match.group(1)) + 1 |
| elif "improve" in model_path: |
| si_cycle_num = 2 |
| else: |
| si_cycle_num = 1 |
|
|
| print(f" Starting training (loop cycle {cycle_num}, selfimprove cycle {si_cycle_num})...") |
| print(f" Model: {model_path}") |
| print(f" Output will be: {output_dir}/improved_cycle{si_cycle_num}") |
| run_cycle(cfg) |
|
|
| |
| new_model_path = str(Path(output_dir) / f"improved_cycle{si_cycle_num}") |
| if not Path(new_model_path).exists(): |
| |
| alt_path = str(Path(output_dir) / f"improved_cycle{cycle_num}") |
| if Path(alt_path).exists(): |
| new_model_path = alt_path |
| else: |
| |
| base = Path(output_dir) |
| found = False |
| for n in range(50, 0, -1): |
| check = base / f"improved_cycle{n}" |
| if check.exists() and list(check.glob("*.safetensors")): |
| new_model_path = str(check) |
| print(f" Output path mismatch — found model at {check}") |
| found = True |
| break |
| if not found: |
| old_improved = base / "improved" |
| if old_improved.exists(): |
| new_model_path = str(old_improved) |
| else: |
| |
| print(f" ERROR: No output model found! Training may have failed silently.") |
| print(f" Returning original model: {model_path}") |
| return model_path, 0.0 |
|
|
| |
| out_p = Path(new_model_path) |
| if out_p.is_dir(): |
| st_files = list(out_p.glob("*.safetensors")) |
| if not st_files: |
| print(f" ERROR: Output model has no safetensors files! Returning original.") |
| return model_path, 0.0 |
| total_sz = sum(f.stat().st_size for f in st_files) / 1e9 |
| if total_sz < 1.0: |
| print(f" ERROR: Output model only {total_sz:.2f} GB — likely corrupt. Returning original.") |
| return model_path, 0.0 |
|
|
| |
| training_loss = 0.0 |
| log_file = Path(new_model_path) / "training_log.json" |
| if log_file.exists(): |
| try: |
| with open(log_file) as f: |
| log = json.load(f) |
| training_loss = log.get("final_loss", 0.0) |
| except Exception: |
| pass |
|
|
| return new_model_path, training_loss |
|
|
|
|
| def _compare_scores( |
| old_scores: Dict[str, float], |
| new_scores: Dict[str, float], |
| max_regression: float, |
| ) -> Tuple[bool, List[str]]: |
| """ |
| Compare benchmark scores before and after training. |
| Returns: (overall_improved, list_of_regressed_categories) |
| """ |
| regressions = [] |
| improvements = 0 |
|
|
| for category in old_scores: |
| if category not in new_scores: |
| print(f" ? {category}: MISSING from new scores — treating as regression!") |
| regressions.append(category) |
| continue |
|
|
| old = old_scores[category] |
| new = new_scores[category] |
| diff = new - old |
|
|
| if diff > 0.01: |
| improvements += 1 |
| print(f" ↑ {category}: {old:.1%} → {new:.1%} (+{diff:.1%})") |
| elif diff < -max_regression: |
| regressions.append(category) |
| print(f" ↓ {category}: {old:.1%} → {new:.1%} ({diff:.1%}) REGRESSION!") |
| else: |
| print(f" = {category}: {old:.1%} → {new:.1%} (stable)") |
|
|
| |
| |
| |
| |
| overall_improved = improvements > len(regressions) |
| return overall_improved, regressions |
|
|
|
|
| def _all_targets_met(scores: Dict[str, float], targets: Dict[str, float]) -> bool: |
| """Check if all scores meet or exceed targets.""" |
| for category, target in targets.items(): |
| if category not in scores: |
| return False |
| if scores[category] < target: |
| return False |
| return True |
|
|
|
|
| |
| |
| |
|
|
| def _print_cycle_summary(result: CycleResult, history: List[CycleResult]): |
| """Print a summary of the completed cycle.""" |
| try: |
| hours = result.duration_seconds / 3600 |
| print(f"\n{'='*50}") |
| print(f"CYCLE {result.cycle_num} SUMMARY") |
| print(f"{'='*50}") |
| print(f" Duration: {hours:.1f} hours") |
| print(f" Weaknesses found: {result.weaknesses_found}") |
| print(f" Data collected: {result.data_collected}") |
| print(f" Training loss: {result.training_loss:.4f}") |
| print(f" Improved: {'Yes' if result.improved else 'No'}") |
| if result.regressions: |
| print(f" Regressions: {', '.join(result.regressions)}") |
| print(f" Model saved: {result.model_path}") |
|
|
| |
| if len(history) > 1: |
| print(f"\n Score progression:") |
| for h in history[-5:]: |
| scores = h.benchmark_scores or {} |
| avg = sum(scores.values()) / max(len(scores), 1) if scores else 0.0 |
| print(f" Cycle {h.cycle_num}: avg {avg:.1%}") |
| except Exception as e: |
| print(f" (Could not print cycle summary: {e})") |
|
|
|
|
| def _print_final_summary(history: List[CycleResult]): |
| """Print final summary after loop ends.""" |
| if not history: |
| return |
|
|
| try: |
| total_hours = sum(r.duration_seconds for r in history) / 3600 |
| improved_cycles = sum(1 for r in history if r.improved) |
|
|
| print(f"\n{'#' * 60}") |
| print(f"# LOOP COMPLETE") |
| print(f"{'#' * 60}") |
| print(f" Total cycles: {len(history)}") |
| print(f" Improved cycles: {improved_cycles}/{len(history)}") |
| print(f" Total time: {total_hours:.1f} hours") |
|
|
| if len(history) >= 2: |
| first = history[0] |
| last = history[-1] |
| first_scores = first.benchmark_scores or {} |
| last_scores = last.benchmark_scores or {} |
| if first_scores and last_scores: |
| print(f"\n Score changes (first → last):") |
| for cat in sorted(first_scores.keys()): |
| if cat in last_scores: |
| old = first_scores[cat] |
| new = last_scores[cat] |
| diff = new - old |
| arrow = "↑" if diff > 0 else "↓" if diff < 0 else "=" |
| print(f" {arrow} {cat}: {old:.1%} → {new:.1%} ({diff:+.1%})") |
|
|
| print(f"\n Final model: {history[-1].model_path}") |
| except Exception as e: |
| print(f" (Could not print final summary: {e})") |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="TD Self-Improvement Loop") |
| parser.add_argument("--model", default="", help="Starting model path") |
| parser.add_argument("--max-cycles", type=int, default=0, help="Max cycles (0=infinite)") |
| parser.add_argument("--budget", type=int, default=8000, help="Training samples per cycle") |
| parser.add_argument("--dry-run", action="store_true", help="Test + diagnose only") |
| parser.add_argument("--quick-test", action="store_true", |
| help="Compressed test run (~30-45 min): 500 samples, 1 epoch, LoRA r=32, 8 layers. " |
| "Tests every code path but finishes fast.") |
| parser.add_argument("--hf-token", default="", help="HuggingFace token") |
| args = parser.parse_args() |
|
|
| cfg = LoopConfig( |
| model_path=args.model, |
| max_cycles=args.max_cycles, |
| training_budget=args.budget, |
| dry_run=args.dry_run, |
| hf_token=args.hf_token, |
| ) |
|
|
| |
| if args.quick_test: |
| cfg.quick_test = True |
| cfg.max_cycles = 1 |
| cfg.training_budget = 500 |
| print("\n *** QUICK TEST MODE ***") |
| print(" 500 samples, 1 epoch, LoRA r=32, 8 layers") |
| print(" Expected time: ~30-45 minutes") |
| print(" Purpose: verify every code path works before real run\n") |
|
|
| run_loop(cfg) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|