Upload 163 files
Browse files- hugging/td_fuse/SELF_IMPROVE_LOOP.md +324 -0
- hugging/td_fuse/__pycache__/selfimprove.cpython-310.pyc +0 -0
- hugging/td_fuse/__pycache__/td_benchmark.cpython-310.pyc +0 -0
- hugging/td_fuse/__pycache__/td_contamination.cpython-310.pyc +0 -0
- hugging/td_fuse/__pycache__/td_data_finder.cpython-310.pyc +0 -0
- hugging/td_fuse/__pycache__/td_holdout.cpython-310.pyc +0 -0
- hugging/td_fuse/__pycache__/td_loop.cpython-310.pyc +0 -0
- hugging/td_fuse/__pycache__/td_weakness.cpython-310.pyc +0 -0
- hugging/td_fuse/selfimprove.py +521 -168
- hugging/td_fuse/td_benchmark.py +833 -0
- hugging/td_fuse/td_contamination.py +193 -0
- hugging/td_fuse/td_data_finder.py +615 -0
- hugging/td_fuse/td_holdout.py +426 -0
- hugging/td_fuse/td_loop.py +1082 -0
- hugging/td_fuse/td_weakness.py +791 -0
hugging/td_fuse/SELF_IMPROVE_LOOP.md
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TD Self-Improvement Loop — The Plan
|
| 2 |
+
|
| 3 |
+
## What We Have Now (Cycles 1-3)
|
| 4 |
+
|
| 5 |
+
Right now, **we** are the brain. We pick the datasets, we decide what to train on, we run the script. The model just eats whatever we feed it. It's like a student who has no say in their homework.
|
| 6 |
+
|
| 7 |
+
## What We Want
|
| 8 |
+
|
| 9 |
+
The model becomes its own teacher. It figures out what it's bad at — both by testing its answers AND checking its own weights — finds its own study material from anywhere on the internet, trains itself, and repeats forever. Each loop it gets a little smarter.
|
| 10 |
+
|
| 11 |
+
## The Loop (5 Steps)
|
| 12 |
+
|
| 13 |
+
### Step 1: Test Itself (Two Ways)
|
| 14 |
+
|
| 15 |
+
**A) Answer Testing** — run the model through a big set of questions across every category:
|
| 16 |
+
|
| 17 |
+
- **Math** — arithmetic, algebra, word problems, competition math
|
| 18 |
+
- **Code** — write functions, debug code, explain code
|
| 19 |
+
- **Reasoning** — logic puzzles, multi-step deduction, common sense
|
| 20 |
+
- **Creativity** — writing stories, poems, explanations, rewriting text
|
| 21 |
+
- **Knowledge** — science, history, geography, general facts
|
| 22 |
+
- **Instruction following** — do exactly what the prompt says
|
| 23 |
+
|
| 24 |
+
Maybe 100-200 questions per category. The model generates answers. We check which ones are **wrong**.
|
| 25 |
+
|
| 26 |
+
**B) Weight Health Check** — the same analysis selfimprove.py already does:
|
| 27 |
+
|
| 28 |
+
- Kurtosis, skewness, dead neurons, extreme weights
|
| 29 |
+
- SVD rank utilization (is the layer using its full capacity?)
|
| 30 |
+
- Condition numbers (is the layer numerically stable?)
|
| 31 |
+
- Layer coherence (do adjacent layers flow smoothly?)
|
| 32 |
+
- Compare to reference (how far has each layer drifted?)
|
| 33 |
+
- Clogged norms (are layernorms blocking information flow?)
|
| 34 |
+
|
| 35 |
+
This catches problems that answer testing might miss — a layer could be giving okay answers now but have terrible weight health that will cause problems later. Like a car that drives fine but has a cracked engine block.
|
| 36 |
+
|
| 37 |
+
### Step 2: Identify Weaknesses
|
| 38 |
+
|
| 39 |
+
Combine BOTH sources:
|
| 40 |
+
|
| 41 |
+
**From answer testing:**
|
| 42 |
+
- "Got 95% on arithmetic but 40% on word problems" → weak at word problems
|
| 43 |
+
- "Code works but is messy and slow" → weak at code quality
|
| 44 |
+
- "Logic breaks down after step 5" → weak at long-chain reasoning
|
| 45 |
+
- "Stories are boring and repetitive" → weak at creativity
|
| 46 |
+
|
| 47 |
+
**From weight health:**
|
| 48 |
+
- "Layer 6 post_attention_layernorm kurtosis 2446" → needs surgical norm repair
|
| 49 |
+
- "Layer 0 k_proj condition number 78983" → numerically unstable, needs targeted training
|
| 50 |
+
- "Layers 23-28 low rank utilization" → these layers aren't using their capacity
|
| 51 |
+
- "Layer 33-35 coherence issues" → merge artifacts still present
|
| 52 |
+
|
| 53 |
+
The model literally tells us what it's bad at from two angles — what it gets wrong AND what's broken inside. No guessing.
|
| 54 |
+
|
| 55 |
+
### Step 3: Find Training Data (Search Everywhere)
|
| 56 |
+
|
| 57 |
+
This is the new part. Instead of us picking datasets, the script searches the entire internet for training data that matches the weaknesses:
|
| 58 |
+
|
| 59 |
+
**Search sources (in order of priority):**
|
| 60 |
+
|
| 61 |
+
1. **HuggingFace Datasets API** — biggest collection of ML datasets, easy to download programmatically
|
| 62 |
+
2. **GitHub** — search for repos with training data, benchmarks, problem sets
|
| 63 |
+
3. **arXiv/Papers With Code** — find datasets mentioned in papers about the specific weakness
|
| 64 |
+
4. **Kaggle** — competitions and datasets (especially good for code and math)
|
| 65 |
+
5. **Web scraping** — if no dataset exists, scrape relevant content (Stack Overflow for code, math forums for math, etc.)
|
| 66 |
+
|
| 67 |
+
**How it picks:**
|
| 68 |
+
- Bad at word problems → search "math word problems dataset" across all sources → download best matches → format
|
| 69 |
+
- Bad at code quality → search "code review" + "competitive programming" → download → format
|
| 70 |
+
- Bad at long reasoning → search "chain of thought reasoning" + "multi-step" → download → format
|
| 71 |
+
- Weight health issues → search for data that exercises the broken layers (e.g., complex reasoning exercises the deeper layers)
|
| 72 |
+
|
| 73 |
+
The script already knows how to format data into the `<|im_start|>` chat template. It just needs to find the RIGHT data based on the weaknesses from Step 2.
|
| 74 |
+
|
| 75 |
+
For things where there's no perfect dataset (like creativity), use **STaR** — make the model generate multiple answers, keep only the best ones (scored by length, diversity, coherence), and train on those winners.
|
| 76 |
+
|
| 77 |
+
**Data allocation:** More samples go to worse weaknesses. If math is 42% and code is 65%, math gets 2x more training data than code.
|
| 78 |
+
|
| 79 |
+
### Step 4: Train + Repair
|
| 80 |
+
|
| 81 |
+
Two-pronged approach (what selfimprove.py already does, but targeted):
|
| 82 |
+
|
| 83 |
+
**Weight repair (surgical):**
|
| 84 |
+
- Fix clogged norms by blending with reference values
|
| 85 |
+
- This happens BEFORE training so the pipes are clear
|
| 86 |
+
|
| 87 |
+
**LoRA training (learning):**
|
| 88 |
+
- Target the layers that weight health flagged as worst
|
| 89 |
+
- Train on the data from Step 3
|
| 90 |
+
- Unfrozen norms get trained with higher LR
|
| 91 |
+
- Merge LoRA back into model
|
| 92 |
+
|
| 93 |
+
### Step 5: Verify
|
| 94 |
+
|
| 95 |
+
Before moving on, check that things actually improved:
|
| 96 |
+
|
| 97 |
+
- Re-run the benchmark from Step 1A — did scores go up?
|
| 98 |
+
- Re-run weight health from Step 1B — did health improve?
|
| 99 |
+
- If scores went DOWN on anything → flag it, investigate, maybe rollback
|
| 100 |
+
- Save everything: model checkpoint, benchmark scores, weight health report
|
| 101 |
+
|
| 102 |
+
### Then: Loop
|
| 103 |
+
|
| 104 |
+
The improved model from Step 5 becomes the input for Step 1. Start over.
|
| 105 |
+
|
| 106 |
+
Test → Diagnose (answers + weights) → Find data (anywhere) → Train + Repair → Verify → Repeat.
|
| 107 |
+
|
| 108 |
+
Each cycle the easy stuff gets solved and only the hard stuff remains. The training gets more and more targeted. The model gets a little better each time.
|
| 109 |
+
|
| 110 |
+
## What We Need to Build
|
| 111 |
+
|
| 112 |
+
### 1. Benchmark Suite (`td_benchmark.py`)
|
| 113 |
+
|
| 114 |
+
A big test set. Sources:
|
| 115 |
+
|
| 116 |
+
- GSM8K test split (math)
|
| 117 |
+
- HumanEval or MBPP (code — checks if code actually runs)
|
| 118 |
+
- ARC Challenge (reasoning)
|
| 119 |
+
- MMLU-Pro test split (knowledge + reasoning)
|
| 120 |
+
- Custom creativity prompts (score by a separate model or rule-based)
|
| 121 |
+
- Custom instruction-following tests
|
| 122 |
+
|
| 123 |
+
The benchmark runs the model, checks answers, and outputs a score per category.
|
| 124 |
+
|
| 125 |
+
### 2. Weight Health Analyzer (already built in `selfimprove.py`)
|
| 126 |
+
|
| 127 |
+
This already exists — `compute_health_scores()`, `compute_damage_scores()`, `compute_coherence_scores()`, `detect_clogged_norms()`. We just need to make it output a structured weakness report that the data finder can use.
|
| 128 |
+
|
| 129 |
+
### 3. Weakness Finder (`td_weakness.py`)
|
| 130 |
+
|
| 131 |
+
Takes BOTH benchmark results AND weight health and outputs:
|
| 132 |
+
|
| 133 |
+
- Which categories scored low (from answers)
|
| 134 |
+
- Which layers are unhealthy (from weights)
|
| 135 |
+
- Search keywords for finding datasets — not just on HuggingFace, ANYWHERE
|
| 136 |
+
|
| 137 |
+
Example output:
|
| 138 |
+
```
|
| 139 |
+
WEAKNESSES FOUND:
|
| 140 |
+
|
| 141 |
+
ANSWER WEAKNESSES:
|
| 142 |
+
math_word_problems: 42% (target: 80%)
|
| 143 |
+
→ search: "math word problems dataset", "grade school math", "GSM8K hard"
|
| 144 |
+
code_debugging: 35% (target: 70%)
|
| 145 |
+
→ search: "code debugging dataset", "bug fix pairs", "code review"
|
| 146 |
+
long_reasoning: 51% (target: 75%)
|
| 147 |
+
→ search: "chain of thought reasoning", "multi-step logic"
|
| 148 |
+
|
| 149 |
+
WEIGHT WEAKNESSES:
|
| 150 |
+
layers 0-6: high kurtosis norms (avg 200+)
|
| 151 |
+
→ action: surgical norm repair
|
| 152 |
+
layers 23-28: low rank utilization (<10%)
|
| 153 |
+
→ action: target with LoRA, train on complex tasks
|
| 154 |
+
layer 0 k_proj: condition number 78983
|
| 155 |
+
→ action: include in LoRA targets, high priority
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### 4. Dataset Finder (`td_data_finder.py`)
|
| 159 |
+
|
| 160 |
+
Takes the search keywords and:
|
| 161 |
+
|
| 162 |
+
- Searches HuggingFace Datasets API (primary — easiest to download)
|
| 163 |
+
- Searches GitHub for repos with training data
|
| 164 |
+
- Searches Kaggle datasets
|
| 165 |
+
- Falls back to web scraping if needed (Stack Overflow, math forums, etc.)
|
| 166 |
+
- Downloads the best matches
|
| 167 |
+
- Formats them into the chat template
|
| 168 |
+
- Allocates more samples to worse weaknesses
|
| 169 |
+
|
| 170 |
+
### 5. The Loop Runner (`td_loop.py`)
|
| 171 |
+
|
| 172 |
+
Ties it all together in an endless loop:
|
| 173 |
+
|
| 174 |
+
```
|
| 175 |
+
cycle = 0
|
| 176 |
+
while True:
|
| 177 |
+
cycle += 1
|
| 178 |
+
|
| 179 |
+
# Step 1: Test (both ways)
|
| 180 |
+
answer_scores = benchmark(model)
|
| 181 |
+
weight_health = analyze_weights(model)
|
| 182 |
+
|
| 183 |
+
# Step 2: Diagnose
|
| 184 |
+
weaknesses = find_weaknesses(answer_scores, weight_health)
|
| 185 |
+
|
| 186 |
+
# Step 3: Find data (search everywhere)
|
| 187 |
+
data = find_training_data(weaknesses)
|
| 188 |
+
|
| 189 |
+
# Step 4: Train + repair
|
| 190 |
+
repair_norms(model, weight_health)
|
| 191 |
+
model = train_lora(model, data, weight_health)
|
| 192 |
+
|
| 193 |
+
# Step 5: Verify
|
| 194 |
+
new_scores = benchmark(model)
|
| 195 |
+
if any_score_dropped(new_scores, answer_scores):
|
| 196 |
+
print(f"WARNING: Regression detected! Investigating...")
|
| 197 |
+
# Could rollback or adjust
|
| 198 |
+
|
| 199 |
+
save_checkpoint(model, cycle, new_scores, weight_health)
|
| 200 |
+
|
| 201 |
+
if all_scores_above_target(new_scores):
|
| 202 |
+
print(f"Target performance reached after {cycle} cycles!")
|
| 203 |
+
break
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
## Phase 2: Synthetic Learning Mode
|
| 207 |
+
|
| 208 |
+
### The Problem
|
| 209 |
+
|
| 210 |
+
Eventually, the model will run out of external datasets. It's trained on every math dataset, every code repo, every reasoning benchmark. The internet has been squeezed dry. Now what?
|
| 211 |
+
|
| 212 |
+
This isn't hypothetical — it's already happening to the big AI companies. OpenAI, Google, Anthropic — they're all running into the same wall: there's only so much human-written data in the world.
|
| 213 |
+
|
| 214 |
+
### The Solution: Synthetic Learning
|
| 215 |
+
|
| 216 |
+
When external data runs out, the model enters **Synthetic Learning Mode** — it becomes its own teacher AND its own textbook writer. Instead of finding data, it creates data.
|
| 217 |
+
|
| 218 |
+
**How it works:**
|
| 219 |
+
|
| 220 |
+
1. **The model generates problems for itself.** Not random ones — problems specifically designed to target its weaknesses. If it's bad at 5-step algebra, it writes 5-step algebra problems. If it's bad at recursive code, it writes recursive coding challenges.
|
| 221 |
+
|
| 222 |
+
2. **The model generates multiple answers to each problem.** Maybe 8-16 attempts per question, using different reasoning paths each time (this is what PACER does — sample multiple traces).
|
| 223 |
+
|
| 224 |
+
3. **Verify which answers are correct.** For math: check the number. For code: run it and see if it passes tests. For logic: check against known rules. Only keep the winners.
|
| 225 |
+
|
| 226 |
+
4. **Train on the winners.** The correct reasoning chains become the training data. The model learns from its own best thinking.
|
| 227 |
+
|
| 228 |
+
5. **The improved model generates harder problems.** Because it solved the easy ones, it can now create harder challenges for itself. The difficulty automatically scales up.
|
| 229 |
+
|
| 230 |
+
**This is real — it already works:**
|
| 231 |
+
|
| 232 |
+
- **STaR (Self-Taught Reasoner)** — generate reasoning chains, keep correct ones, train on them. Published by Google.
|
| 233 |
+
- **Self-Play** — the model debates itself, the better argument wins. Used in AlphaGo and now in LLM training.
|
| 234 |
+
- **Rejection Sampling** — generate many, filter to best, train on best. This is how DeepSeek-R1 was trained.
|
| 235 |
+
- **GRPO** — what we already planned. Group Relative Policy Optimisation rewards correct answers automatically.
|
| 236 |
+
|
| 237 |
+
**Synthetic Learning categories:**
|
| 238 |
+
|
| 239 |
+
- **Math:** Generate equations → solve them multiple ways → verify answer → train on correct chains
|
| 240 |
+
- **Code:** Generate coding challenges → write solutions → run the code → if it works, train on it
|
| 241 |
+
- **Reasoning:** Generate logic puzzles with known answers → solve them → verify → train on correct ones
|
| 242 |
+
- **Creativity:** Generate multiple versions of stories/poems → score them (diversity, coherence, originality) → train on best ones
|
| 243 |
+
- **Knowledge:** Generate question-answer pairs from what it already knows → verify against itself → train to make recall faster and more accurate
|
| 244 |
+
- **Cross-domain:** Generate problems that require MULTIPLE skills (math + code, reasoning + creativity) — these are the hardest and most valuable
|
| 245 |
+
|
| 246 |
+
**Quality control (so it doesn't train on garbage):**
|
| 247 |
+
|
| 248 |
+
- **Cherry_LLM perplexity filter** — catches repetitive or low-quality synthetic data before it enters training
|
| 249 |
+
- **Verified rewards only** — no guessing. Math must be checkable, code must run, logic must follow rules.
|
| 250 |
+
- **Diversity checks** — if all the synthetic data looks the same, throw it out. Variety matters.
|
| 251 |
+
- **Calibration** — compare the model's confidence to its actual accuracy. If it's confident but wrong, that's a problem.
|
| 252 |
+
|
| 253 |
+
### When Does Synthetic Mode Activate?
|
| 254 |
+
|
| 255 |
+
The dataset finder tracks what's been used:
|
| 256 |
+
|
| 257 |
+
```
|
| 258 |
+
if datasets_exhausted(weakness_category):
|
| 259 |
+
print(f"No new external data for {weakness_category}")
|
| 260 |
+
print(f"Entering SYNTHETIC LEARNING MODE")
|
| 261 |
+
data = generate_synthetic_data(model, weakness_category, num_samples=8000)
|
| 262 |
+
else:
|
| 263 |
+
data = download_external_data(weakness_category)
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
It can also run in **hybrid mode** — mix external data (for things it hasn't seen) with synthetic data (for things where external data ran out). Best of both worlds.
|
| 267 |
+
|
| 268 |
+
### The Flywheel Effect
|
| 269 |
+
|
| 270 |
+
This is where it gets exciting. Each cycle:
|
| 271 |
+
|
| 272 |
+
1. Better model → generates harder problems
|
| 273 |
+
2. Harder problems → more room to improve
|
| 274 |
+
3. More improvement → even better model
|
| 275 |
+
4. Even better model → even harder problems
|
| 276 |
+
5. Repeat
|
| 277 |
+
|
| 278 |
+
The model literally pulls itself up by its own bootstraps. Each cycle creates the conditions for the next cycle. The ceiling keeps rising because the model keeps raising it.
|
| 279 |
+
|
| 280 |
+
### Future Hardware (When TD Pays For Itself)
|
| 281 |
+
|
| 282 |
+
By the time synthetic learning is fully running, the model will be smart enough to help design its own infrastructure:
|
| 283 |
+
|
| 284 |
+
- Optimise GPU usage patterns (which operations bottleneck, how to schedule them)
|
| 285 |
+
- Help design custom training pipelines that are faster for its specific architecture
|
| 286 |
+
- Suggest hardware configurations that maximise training throughput
|
| 287 |
+
- Eventually, help design specialised hardware (custom GPU kernels, FPGA configurations, or even chip architectures)
|
| 288 |
+
|
| 289 |
+
This is the endgame: TD doesn't just improve its brain, it improves the hardware running its brain.
|
| 290 |
+
|
| 291 |
+
## Build Order
|
| 292 |
+
|
| 293 |
+
**Phase 1 — Self-Improvement Loop:**
|
| 294 |
+
1. **Benchmark suite first** — we need to measure before we can improve
|
| 295 |
+
2. **Weight health reporter** — restructure existing code to output weakness report
|
| 296 |
+
3. **Weakness finder** — combines benchmark + weight health into search queries
|
| 297 |
+
4. **Dataset finder** — searches everywhere (HuggingFace, GitHub, Kaggle, web)
|
| 298 |
+
5. **Loop runner** — connects everything, runs forever
|
| 299 |
+
|
| 300 |
+
**Phase 2 — Synthetic Learning (built after Phase 1 is running):**
|
| 301 |
+
6. **Synthetic data generator** — creates problems + solutions for each weakness category
|
| 302 |
+
7. **Verification engine** — checks math answers, runs code, validates logic
|
| 303 |
+
8. **Quality filters** — Cherry_LLM perplexity scoring, diversity checks, calibration
|
| 304 |
+
9. **Hybrid mode** — mix external + synthetic data based on what's available
|
| 305 |
+
10. **Difficulty scaling** — auto-increase problem difficulty as the model improves
|
| 306 |
+
|
| 307 |
+
## What This Means
|
| 308 |
+
|
| 309 |
+
- Cycles 1-3 = us manually improving the model (current)
|
| 310 |
+
- Cycle 4+ = the model drives its own improvement with external data (Phase 1)
|
| 311 |
+
- When external data runs out = synthetic learning kicks in (Phase 2)
|
| 312 |
+
- Two diagnostic methods: answer testing (what it gets wrong) + weight health (what's broken inside)
|
| 313 |
+
- Dataset search is not limited to one source — it can find data anywhere
|
| 314 |
+
- When data sources dry up — the model generates its own training data
|
| 315 |
+
- Each loop is automatic — start it and walk away
|
| 316 |
+
- The endgame: TD improves itself, then improves the hardware running itself
|
| 317 |
+
|
| 318 |
+
## Dad's Tests
|
| 319 |
+
|
| 320 |
+
1. **Economic test:** Big companies DO this — it's called RLHF/RLAIF. We're doing a simpler version that works on one GPU. The weight health checking is extra — most companies don't bother because they have enough compute to brute force it.
|
| 321 |
+
2. **Architecture test:** Built on Qwen3-VL which is current and actively maintained. LoRA + HuggingFace are industry standard. Web search for datasets is how researchers actually find training data.
|
| 322 |
+
3. **Realism test:** Each piece already works individually (benchmark tools exist, training works, dataset APIs exist, web search works). We're just connecting them into one loop.
|
| 323 |
+
4. **Pragmatism test:** Uses selfimprove.py we already built for the training + weight analysis. Just adds testing + multi-source dataset search on top.
|
| 324 |
+
5. **Long-term test:** Self-improvement loops are the future of AI. This approach scales — better base model = better self-diagnosis = better training data = faster improvement.
|
hugging/td_fuse/__pycache__/selfimprove.cpython-310.pyc
ADDED
|
Binary file (39.1 kB). View file
|
|
|
hugging/td_fuse/__pycache__/td_benchmark.cpython-310.pyc
ADDED
|
Binary file (24.8 kB). View file
|
|
|
hugging/td_fuse/__pycache__/td_contamination.cpython-310.pyc
ADDED
|
Binary file (5.34 kB). View file
|
|
|
hugging/td_fuse/__pycache__/td_data_finder.cpython-310.pyc
ADDED
|
Binary file (13 kB). View file
|
|
|
hugging/td_fuse/__pycache__/td_holdout.cpython-310.pyc
ADDED
|
Binary file (13.9 kB). View file
|
|
|
hugging/td_fuse/__pycache__/td_loop.cpython-310.pyc
ADDED
|
Binary file (26.4 kB). View file
|
|
|
hugging/td_fuse/__pycache__/td_weakness.cpython-310.pyc
ADDED
|
Binary file (20.2 kB). View file
|
|
|
hugging/td_fuse/selfimprove.py
CHANGED
|
@@ -52,6 +52,9 @@ class SelfImproveConfig:
|
|
| 52 |
save_steps: int = 100
|
| 53 |
num_train_samples: int = 3000
|
| 54 |
|
|
|
|
|
|
|
|
|
|
| 55 |
# Layernorm unfreezing
|
| 56 |
norm_kurtosis_threshold: float = 15.0
|
| 57 |
|
|
@@ -89,7 +92,7 @@ def auto_detect_model_path() -> Tuple[str, int]:
|
|
| 89 |
|
| 90 |
# Check for highest cycle output (cycle3, cycle2, etc.)
|
| 91 |
if base.exists():
|
| 92 |
-
for n in range(
|
| 93 |
cycle_dir = base / f"improved_cycle{n}"
|
| 94 |
if cycle_dir.exists() and list(cycle_dir.glob("*.safetensors")):
|
| 95 |
print(f" Found cycle {n} output at {cycle_dir}")
|
|
@@ -118,6 +121,27 @@ def analyze_layer(name: str, tensor: torch.Tensor) -> Dict:
|
|
| 118 |
"""Full health analysis for a single weight tensor."""
|
| 119 |
t = tensor.float()
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
stats = {
|
| 122 |
"name": name,
|
| 123 |
"shape": list(tensor.shape),
|
|
@@ -125,16 +149,16 @@ def analyze_layer(name: str, tensor: torch.Tensor) -> Dict:
|
|
| 125 |
"dtype": str(tensor.dtype),
|
| 126 |
"l2_norm": t.norm(2).item(),
|
| 127 |
"frobenius_norm": t.norm('fro').item() if t.dim() >= 2 else t.norm(2).item(),
|
| 128 |
-
"mean":
|
| 129 |
-
"std":
|
| 130 |
"min": t.min().item(),
|
| 131 |
"max": t.max().item(),
|
| 132 |
-
"abs_mean":
|
| 133 |
"sparsity": (t.abs() < 1e-6).float().mean().item(),
|
| 134 |
-
"kurtosis":
|
| 135 |
-
"skewness":
|
| 136 |
"near_zero_pct": (t.abs() < 1e-5).float().mean().item(),
|
| 137 |
-
"extreme_pct": (t.abs() >
|
| 138 |
}
|
| 139 |
|
| 140 |
if t.dim() == 2 and min(t.shape) >= 2:
|
|
@@ -180,13 +204,21 @@ def analyze_model_weights(model_path: str, label: str = "model") -> Dict:
|
|
| 180 |
|
| 181 |
for st_file in st_files:
|
| 182 |
print(f" Loading {Path(st_file).name}...")
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
for name, tensor in state_dict.items():
|
| 185 |
if any(skip in name for skip in ["visual.", "merger."]):
|
| 186 |
continue
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
del state_dict
|
| 191 |
gc.collect()
|
| 192 |
|
|
@@ -207,7 +239,8 @@ def compute_health_scores(analysis: Dict) -> Dict[str, Dict]:
|
|
| 207 |
issues = []
|
| 208 |
score = 0.0
|
| 209 |
|
| 210 |
-
|
|
|
|
| 211 |
if kurt > 10:
|
| 212 |
score += 0.3
|
| 213 |
issues.append(f"extreme kurtosis ({kurt:.1f})")
|
|
@@ -215,7 +248,7 @@ def compute_health_scores(analysis: Dict) -> Dict[str, Dict]:
|
|
| 215 |
score += 0.15
|
| 216 |
issues.append(f"high kurtosis ({kurt:.1f})")
|
| 217 |
|
| 218 |
-
skew = abs(stats
|
| 219 |
if skew > 2:
|
| 220 |
score += 0.15
|
| 221 |
issues.append(f"high skew ({skew:.2f})")
|
|
@@ -243,24 +276,25 @@ def compute_health_scores(analysis: Dict) -> Dict[str, Dict]:
|
|
| 243 |
elif sv_ratio > 1000:
|
| 244 |
score += 0.1
|
| 245 |
|
| 246 |
-
dead = stats
|
| 247 |
if dead > 0.3:
|
| 248 |
score += 0.2
|
| 249 |
issues.append(f"high dead neuron % ({dead:.1%})")
|
| 250 |
elif dead > 0.15:
|
| 251 |
score += 0.1
|
| 252 |
|
| 253 |
-
extreme = stats
|
| 254 |
if extreme > 0.05:
|
| 255 |
score += 0.15
|
| 256 |
issues.append(f"many extreme weights ({extreme:.1%})")
|
| 257 |
|
| 258 |
-
|
|
|
|
| 259 |
score += 0.25
|
| 260 |
-
issues.append(f"near-zero variance ({
|
| 261 |
-
elif
|
| 262 |
score += 0.15
|
| 263 |
-
issues.append(f"high variance ({
|
| 264 |
|
| 265 |
health_scores[name] = {
|
| 266 |
"health_score": min(score, 1.0),
|
|
@@ -293,10 +327,11 @@ def compute_damage_scores(model_analysis: Dict, ref_analysis: Dict) -> Dict[str,
|
|
| 293 |
r_stats = ref_analysis["layers"][ref_key]
|
| 294 |
matched += 1
|
| 295 |
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
| 300 |
|
| 301 |
sv_ratio_diff = 0
|
| 302 |
if "sv_ratio" in m_stats and "sv_ratio" in r_stats:
|
|
@@ -335,8 +370,8 @@ def compute_coherence_scores(analysis: Dict) -> Dict[int, float]:
|
|
| 335 |
if layer_num not in layer_norms:
|
| 336 |
layer_norms[layer_num] = []
|
| 337 |
layer_stds[layer_num] = []
|
| 338 |
-
layer_norms[layer_num].append(stats
|
| 339 |
-
layer_stds[layer_num].append(stats
|
| 340 |
|
| 341 |
avg_norms = {l: sum(ns)/len(ns) for l, ns in layer_norms.items()}
|
| 342 |
avg_stds = {l: sum(ss)/len(ss) for l, ss in layer_stds.items()}
|
|
@@ -465,7 +500,8 @@ def detect_clogged_norms(health_scores: Dict[str, Dict], threshold: float = 15.0
|
|
| 465 |
clogged = []
|
| 466 |
for name, h in health_scores.items():
|
| 467 |
if any(norm_type in name.lower() for norm_type in ["layernorm", "ln_", "_norm"]):
|
| 468 |
-
|
|
|
|
| 469 |
if kurt > threshold:
|
| 470 |
clogged.append(name)
|
| 471 |
|
|
@@ -502,7 +538,11 @@ def repair_clogged_norms(model_path: str, clogged_norms: List[str],
|
|
| 502 |
print(" Loading reference norms...")
|
| 503 |
ref_norms = {}
|
| 504 |
for st_file in sorted(glob.glob(f"{ref_path}/*.safetensors")):
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
for name, tensor in state.items():
|
| 507 |
if name in clogged_norms:
|
| 508 |
ref_norms[name] = tensor.clone()
|
|
@@ -518,14 +558,40 @@ def repair_clogged_norms(model_path: str, clogged_norms: List[str],
|
|
| 518 |
repaired = 0
|
| 519 |
st_files = sorted(glob.glob(f"{model_path}/*.safetensors"))
|
| 520 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
for st_file in st_files:
|
| 522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
changed = False
|
| 524 |
|
| 525 |
for name in list(state.keys()):
|
| 526 |
if name in ref_norms:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
kurt = abs(health_scores[name]["stats"]["kurtosis"])
|
| 528 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
# Blend ratio based on severity
|
| 530 |
if kurt > 100:
|
| 531 |
ref_weight = 0.80 # Severely broken — mostly use reference
|
|
@@ -549,7 +615,11 @@ def repair_clogged_norms(model_path: str, clogged_norms: List[str],
|
|
| 549 |
changed = True
|
| 550 |
|
| 551 |
if changed:
|
| 552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
del state
|
| 554 |
|
| 555 |
gc.collect()
|
|
@@ -583,12 +653,44 @@ def improve_with_lora(model_path: str, target_layers: List[int],
|
|
| 583 |
from datasets import load_dataset
|
| 584 |
from torch.utils.data import Dataset
|
| 585 |
|
| 586 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
print(" Loading model...")
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 593 |
|
| 594 |
# Enable gradient checkpointing for mega mode (saves ~40% VRAM)
|
|
@@ -607,7 +709,16 @@ def improve_with_lora(model_path: str, target_layers: List[int],
|
|
| 607 |
bias="none",
|
| 608 |
task_type=TaskType.CAUSAL_LM,
|
| 609 |
)
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
|
| 612 |
# UNFREEZE clogged norms — the key cycle 2 fix
|
| 613 |
unfrozen_count = 0
|
|
@@ -638,29 +749,75 @@ def improve_with_lora(model_path: str, target_layers: List[int],
|
|
| 638 |
train_texts = []
|
| 639 |
total = cfg.num_train_samples
|
| 640 |
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
else:
|
| 647 |
-
|
| 648 |
-
alloc = {"code": per, "math": per, "reasoning": per,
|
| 649 |
-
"creativity": 0, "instructions": per, "general": per}
|
| 650 |
|
| 651 |
# 1. MATH (top priority)
|
| 652 |
try:
|
| 653 |
-
ds = load_dataset("openai/gsm8k", "main", split="train")
|
| 654 |
count = 0
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
|
|
|
|
|
|
| 664 |
print(f" Math (GSM8K): {count}")
|
| 665 |
except Exception as e:
|
| 666 |
print(f" WARNING: GSM8K failed: {e}")
|
|
@@ -686,20 +843,21 @@ def improve_with_lora(model_path: str, target_layers: List[int],
|
|
| 686 |
|
| 687 |
# 2. CODE (top priority)
|
| 688 |
try:
|
| 689 |
-
ds = load_dataset("sahil2801/CodeAlpaca-20k", split="train")
|
| 690 |
count = 0
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
|
|
|
|
|
|
| 703 |
print(f" Code (CodeAlpaca): {count}")
|
| 704 |
except Exception as e:
|
| 705 |
print(f" WARNING: CodeAlpaca failed: {e}")
|
|
@@ -725,21 +883,22 @@ def improve_with_lora(model_path: str, target_layers: List[int],
|
|
| 725 |
|
| 726 |
# 3. REASONING (top priority)
|
| 727 |
try:
|
| 728 |
-
ds = load_dataset("TIGER-Lab/MMLU-Pro", split="test")
|
| 729 |
count = 0
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
|
|
|
|
|
|
| 743 |
print(f" Reasoning (MMLU-Pro): {count}")
|
| 744 |
except Exception as e:
|
| 745 |
print(f" WARNING: MMLU-Pro failed: {e}")
|
|
@@ -770,37 +929,39 @@ def improve_with_lora(model_path: str, target_layers: List[int],
|
|
| 770 |
|
| 771 |
# 5. INSTRUCTIONS (general)
|
| 772 |
try:
|
| 773 |
-
ds = load_dataset("yahma/alpaca-cleaned", split="train")
|
| 774 |
count = 0
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
|
|
|
|
|
|
| 787 |
print(f" Instructions (Alpaca): {count}")
|
| 788 |
except Exception as e:
|
| 789 |
print(f" WARNING: Alpaca failed: {e}")
|
| 790 |
|
| 791 |
# 6. GENERAL TEXT (lowest priority)
|
| 792 |
try:
|
| 793 |
-
ds = load_dataset("neuralmagic/LLM_compression_calibration", split="train")
|
| 794 |
count = 0
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
|
|
|
|
|
|
| 804 |
print(f" General text: {count}")
|
| 805 |
except Exception as e:
|
| 806 |
print(f" WARNING: General text failed: {e}")
|
|
@@ -811,6 +972,44 @@ def improve_with_lora(model_path: str, target_layers: List[int],
|
|
| 811 |
print(" ERROR: Not enough training data!")
|
| 812 |
return model_path
|
| 813 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 814 |
random.shuffle(train_texts)
|
| 815 |
|
| 816 |
# Tokenize
|
|
@@ -831,10 +1030,15 @@ def improve_with_lora(model_path: str, target_layers: List[int],
|
|
| 831 |
print(" Tokenizing...")
|
| 832 |
dataset = ImproveDataset(train_texts, tokenizer)
|
| 833 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
out_dir = Path(cfg.output_dir) / "train_output"
|
| 835 |
out_dir.mkdir(parents=True, exist_ok=True)
|
| 836 |
|
| 837 |
-
|
|
|
|
| 838 |
|
| 839 |
# Set up optimizer with different LR for norms vs LoRA
|
| 840 |
# Group parameters: norms get higher LR
|
|
@@ -891,19 +1095,85 @@ def improve_with_lora(model_path: str, target_layers: List[int],
|
|
| 891 |
print(f"\n Training: ~{total_steps} steps on {len(target_layers)} layers + {len(clogged_norms)} norms")
|
| 892 |
print(f" Data: {len(train_texts)} samples × {cfg.train_epochs} epochs")
|
| 893 |
print(f" LoRA: r={cfg.lora_r}, alpha={cfg.lora_alpha}")
|
| 894 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 895 |
|
| 896 |
# Merge LoRA back
|
| 897 |
print("\n Merging LoRA adapters into model...")
|
| 898 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
|
| 900 |
# Save
|
| 901 |
save_dir = Path(cfg.output_dir) / f"improved_cycle{cycle_num}"
|
| 902 |
save_dir.mkdir(parents=True, exist_ok=True)
|
| 903 |
print(f" Saving to {save_dir}...")
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
print(f" SAVED: {save_dir} ({sz:.1f} GB)")
|
| 908 |
|
| 909 |
shutil.rmtree(str(out_dir), ignore_errors=True)
|
|
@@ -927,11 +1197,29 @@ def quick_benchmark(model_path: str) -> Dict:
|
|
| 927 |
|
| 928 |
from transformers import AutoModelForImageTextToText, AutoTokenizer
|
| 929 |
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 935 |
|
| 936 |
tests = [
|
| 937 |
{"q": "What is 47 * 23?", "expected": "1081", "type": "math"},
|
|
@@ -946,20 +1234,36 @@ def quick_benchmark(model_path: str) -> Dict:
|
|
| 946 |
|
| 947 |
results = []
|
| 948 |
for test in tests:
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 961 |
|
| 962 |
-
score = sum(1 for r in results if r
|
| 963 |
print(f"\n BENCHMARK: {score}/{len(tests)} passed")
|
| 964 |
|
| 965 |
del model
|
|
@@ -983,7 +1287,18 @@ def run_cycle(cfg: SelfImproveConfig = None):
|
|
| 983 |
if not cfg.model_path:
|
| 984 |
cfg.model_path, cycle_num = auto_detect_model_path()
|
| 985 |
else:
|
| 986 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 987 |
|
| 988 |
# MEGA MODE for cycle 3+
|
| 989 |
if cycle_num >= 3 and not cfg.mega_mode:
|
|
@@ -1023,16 +1338,24 @@ def run_cycle(cfg: SelfImproveConfig = None):
|
|
| 1023 |
print("STEP 3/7: REFERENCE COMPARISON")
|
| 1024 |
print(f"{'='*40}")
|
| 1025 |
ref_path = Path(cfg.output_dir) / "reference_weights"
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
# DON'T delete reference yet — we need it for norm repair
|
| 1037 |
gc.collect()
|
| 1038 |
|
|
@@ -1055,24 +1378,31 @@ def run_cycle(cfg: SelfImproveConfig = None):
|
|
| 1055 |
if clogged_norms:
|
| 1056 |
print(f"\n CLOGGED NORMS DETECTED ({len(clogged_norms)}):")
|
| 1057 |
for cn in clogged_norms:
|
| 1058 |
-
|
|
|
|
| 1059 |
print(f" {cn}: kurtosis={kurt:.1f}")
|
| 1060 |
else:
|
| 1061 |
print(f"\n No clogged norms above threshold {cfg.norm_kurtosis_threshold}")
|
| 1062 |
|
| 1063 |
# STEP 5c: REPAIR clogged norms surgically
|
| 1064 |
-
if clogged_norms:
|
| 1065 |
print(f"\n{'='*40}")
|
| 1066 |
print("STEP 5c/7: SURGICAL NORM REPAIR")
|
| 1067 |
print(f"{'='*40}")
|
| 1068 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1069 |
|
| 1070 |
# Clean up reference now
|
| 1071 |
print(f" Cleaning up reference weights...")
|
| 1072 |
shutil.rmtree(str(ref_path), ignore_errors=True)
|
| 1073 |
gc.collect()
|
| 1074 |
|
| 1075 |
-
# Save report
|
| 1076 |
report_dir = Path(cfg.output_dir)
|
| 1077 |
report_dir.mkdir(parents=True, exist_ok=True)
|
| 1078 |
pre_report = {
|
|
@@ -1085,8 +1415,11 @@ def run_cycle(cfg: SelfImproveConfig = None):
|
|
| 1085 |
"layer_details": {str(k): v for k, v in layer_details.items()},
|
| 1086 |
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 1087 |
}
|
| 1088 |
-
|
| 1089 |
-
|
|
|
|
|
|
|
|
|
|
| 1090 |
|
| 1091 |
# STEP 6: Train
|
| 1092 |
print(f"\n{'='*40}")
|
|
@@ -1099,16 +1432,26 @@ def run_cycle(cfg: SelfImproveConfig = None):
|
|
| 1099 |
print("STEP 7/7: VERIFY + BENCHMARK")
|
| 1100 |
print(f"{'='*40}")
|
| 1101 |
|
| 1102 |
-
# Re-analyze health
|
| 1103 |
-
post_analysis = analyze_model_weights(improved_path, "TD improved")
|
| 1104 |
-
post_health = compute_health_scores(post_analysis)
|
| 1105 |
-
|
| 1106 |
pre_avg = sum(h["health_score"] for h in health_scores.values()) / max(len(health_scores), 1)
|
| 1107 |
-
post_avg =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
|
| 1109 |
-
# Per-layer comparison
|
| 1110 |
-
|
|
|
|
|
|
|
|
|
|
| 1111 |
for layer_num in target_layers:
|
|
|
|
|
|
|
| 1112 |
post_layer_scores = []
|
| 1113 |
for name, h in post_health.items():
|
| 1114 |
parts = name.split(".")
|
|
@@ -1127,16 +1470,22 @@ def run_cycle(cfg: SelfImproveConfig = None):
|
|
| 1127 |
if clogged_norms:
|
| 1128 |
print(f"\n Clogged norm changes:")
|
| 1129 |
for cn in clogged_norms:
|
| 1130 |
-
|
|
|
|
| 1131 |
# Find matching post norm
|
| 1132 |
if cn in post_health:
|
| 1133 |
-
|
|
|
|
| 1134 |
delta = pre_kurt - post_kurt
|
| 1135 |
symbol = "+" if delta > 0 else "-"
|
| 1136 |
print(f" {cn}: kurtosis {pre_kurt:.1f} → {post_kurt:.1f} ({symbol}{abs(delta):.1f})")
|
| 1137 |
|
| 1138 |
-
# Quick benchmark
|
| 1139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
|
| 1141 |
elapsed = (time.time() - start) / 60
|
| 1142 |
health_improvement = (pre_avg - post_avg) / max(pre_avg, 1e-8) * 100
|
|
@@ -1149,22 +1498,26 @@ def run_cycle(cfg: SelfImproveConfig = None):
|
|
| 1149 |
print(f" Health BEFORE: {pre_avg:.6f}")
|
| 1150 |
print(f" Health AFTER: {post_avg:.6f}")
|
| 1151 |
print(f" Health improvement: {health_improvement:.1f}%")
|
| 1152 |
-
print(f" Benchmark: {benchmark
|
| 1153 |
print(f" Improved model: {improved_path}")
|
| 1154 |
print(f"{'='*60}")
|
| 1155 |
|
| 1156 |
-
# Final report
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1168 |
|
| 1169 |
return improved_path
|
| 1170 |
|
|
|
|
| 52 |
save_steps: int = 100
|
| 53 |
num_train_samples: int = 3000
|
| 54 |
|
| 55 |
+
# Custom training data (from td_data_finder — targeted by weakness)
|
| 56 |
+
custom_data_path: str = "" # If set, load from this JSONL instead of hardcoded datasets
|
| 57 |
+
|
| 58 |
# Layernorm unfreezing
|
| 59 |
norm_kurtosis_threshold: float = 15.0
|
| 60 |
|
|
|
|
| 92 |
|
| 93 |
# Check for highest cycle output (cycle3, cycle2, etc.)
|
| 94 |
if base.exists():
|
| 95 |
+
for n in range(50, 0, -1):
|
| 96 |
cycle_dir = base / f"improved_cycle{n}"
|
| 97 |
if cycle_dir.exists() and list(cycle_dir.glob("*.safetensors")):
|
| 98 |
print(f" Found cycle {n} output at {cycle_dir}")
|
|
|
|
| 121 |
"""Full health analysis for a single weight tensor."""
|
| 122 |
t = tensor.float()
|
| 123 |
|
| 124 |
+
std_val = t.std().item()
|
| 125 |
+
mean_val = t.mean().item()
|
| 126 |
+
abs_mean_val = t.abs().mean().item()
|
| 127 |
+
|
| 128 |
+
# Kurtosis/skewness: guard against overflow for extreme tensors
|
| 129 |
+
# (e.g. degenerate norms with std near 0 → huge normalized values → inf in pow(4))
|
| 130 |
+
try:
|
| 131 |
+
normalized = (t - mean_val) / max(std_val, 1e-8)
|
| 132 |
+
# Clamp to prevent inf/nan from pow(4) on extreme values
|
| 133 |
+
normalized = normalized.clamp(-1e4, 1e4)
|
| 134 |
+
kurtosis_val = normalized.pow(4).mean().item() - 3
|
| 135 |
+
skewness_val = normalized.pow(3).mean().item()
|
| 136 |
+
# Catch NaN/Inf that slipped through
|
| 137 |
+
if math.isnan(kurtosis_val) or math.isinf(kurtosis_val):
|
| 138 |
+
kurtosis_val = 0.0
|
| 139 |
+
if math.isnan(skewness_val) or math.isinf(skewness_val):
|
| 140 |
+
skewness_val = 0.0
|
| 141 |
+
except Exception:
|
| 142 |
+
kurtosis_val = 0.0
|
| 143 |
+
skewness_val = 0.0
|
| 144 |
+
|
| 145 |
stats = {
|
| 146 |
"name": name,
|
| 147 |
"shape": list(tensor.shape),
|
|
|
|
| 149 |
"dtype": str(tensor.dtype),
|
| 150 |
"l2_norm": t.norm(2).item(),
|
| 151 |
"frobenius_norm": t.norm('fro').item() if t.dim() >= 2 else t.norm(2).item(),
|
| 152 |
+
"mean": mean_val,
|
| 153 |
+
"std": std_val,
|
| 154 |
"min": t.min().item(),
|
| 155 |
"max": t.max().item(),
|
| 156 |
+
"abs_mean": abs_mean_val,
|
| 157 |
"sparsity": (t.abs() < 1e-6).float().mean().item(),
|
| 158 |
+
"kurtosis": kurtosis_val,
|
| 159 |
+
"skewness": skewness_val,
|
| 160 |
"near_zero_pct": (t.abs() < 1e-5).float().mean().item(),
|
| 161 |
+
"extreme_pct": (t.abs() > abs_mean_val * 5).float().mean().item() if abs_mean_val > 0 else 0.0,
|
| 162 |
}
|
| 163 |
|
| 164 |
if t.dim() == 2 and min(t.shape) >= 2:
|
|
|
|
| 204 |
|
| 205 |
for st_file in st_files:
|
| 206 |
print(f" Loading {Path(st_file).name}...")
|
| 207 |
+
try:
|
| 208 |
+
state_dict = load_file(st_file, device="cpu")
|
| 209 |
+
except Exception as e:
|
| 210 |
+
print(f" WARNING: Failed to load {st_file}: {e} — skipping shard")
|
| 211 |
+
continue
|
| 212 |
for name, tensor in state_dict.items():
|
| 213 |
if any(skip in name for skip in ["visual.", "merger."]):
|
| 214 |
continue
|
| 215 |
+
try:
|
| 216 |
+
stats = analyze_layer(name, tensor)
|
| 217 |
+
all_stats[name] = stats
|
| 218 |
+
total_params += stats["params"]
|
| 219 |
+
except Exception as e:
|
| 220 |
+
print(f" WARNING: analyze_layer failed for {name}: {e}")
|
| 221 |
+
continue
|
| 222 |
del state_dict
|
| 223 |
gc.collect()
|
| 224 |
|
|
|
|
| 239 |
issues = []
|
| 240 |
score = 0.0
|
| 241 |
|
| 242 |
+
# Safe access — analyze_layer might have partial results if SVD failed etc.
|
| 243 |
+
kurt = abs(stats.get("kurtosis", 0))
|
| 244 |
if kurt > 10:
|
| 245 |
score += 0.3
|
| 246 |
issues.append(f"extreme kurtosis ({kurt:.1f})")
|
|
|
|
| 248 |
score += 0.15
|
| 249 |
issues.append(f"high kurtosis ({kurt:.1f})")
|
| 250 |
|
| 251 |
+
skew = abs(stats.get("skewness", 0))
|
| 252 |
if skew > 2:
|
| 253 |
score += 0.15
|
| 254 |
issues.append(f"high skew ({skew:.2f})")
|
|
|
|
| 276 |
elif sv_ratio > 1000:
|
| 277 |
score += 0.1
|
| 278 |
|
| 279 |
+
dead = stats.get("near_zero_pct", 0)
|
| 280 |
if dead > 0.3:
|
| 281 |
score += 0.2
|
| 282 |
issues.append(f"high dead neuron % ({dead:.1%})")
|
| 283 |
elif dead > 0.15:
|
| 284 |
score += 0.1
|
| 285 |
|
| 286 |
+
extreme = stats.get("extreme_pct", 0)
|
| 287 |
if extreme > 0.05:
|
| 288 |
score += 0.15
|
| 289 |
issues.append(f"many extreme weights ({extreme:.1%})")
|
| 290 |
|
| 291 |
+
std_val = stats.get("std", 0)
|
| 292 |
+
if std_val < 1e-4:
|
| 293 |
score += 0.25
|
| 294 |
+
issues.append(f"near-zero variance ({std_val:.6f})")
|
| 295 |
+
elif std_val > 1.0:
|
| 296 |
score += 0.15
|
| 297 |
+
issues.append(f"high variance ({std_val:.4f})")
|
| 298 |
|
| 299 |
health_scores[name] = {
|
| 300 |
"health_score": min(score, 1.0),
|
|
|
|
| 327 |
r_stats = ref_analysis["layers"][ref_key]
|
| 328 |
matched += 1
|
| 329 |
|
| 330 |
+
# Safe .get() for all stats — partial analysis results possible
|
| 331 |
+
norm_diff = abs(m_stats.get("l2_norm", 0) - r_stats.get("l2_norm", 0)) / max(r_stats.get("l2_norm", 1e-8), 1e-8)
|
| 332 |
+
std_diff = abs(m_stats.get("std", 0) - r_stats.get("std", 0)) / max(r_stats.get("std", 1e-8), 1e-8)
|
| 333 |
+
kurt_diff = abs(m_stats.get("kurtosis", 0) - r_stats.get("kurtosis", 0)) / max(abs(r_stats.get("kurtosis", 0)) + 1, 1e-8)
|
| 334 |
+
sparsity_diff = abs(m_stats.get("sparsity", 0) - r_stats.get("sparsity", 0))
|
| 335 |
|
| 336 |
sv_ratio_diff = 0
|
| 337 |
if "sv_ratio" in m_stats and "sv_ratio" in r_stats:
|
|
|
|
| 370 |
if layer_num not in layer_norms:
|
| 371 |
layer_norms[layer_num] = []
|
| 372 |
layer_stds[layer_num] = []
|
| 373 |
+
layer_norms[layer_num].append(stats.get("l2_norm", 0))
|
| 374 |
+
layer_stds[layer_num].append(stats.get("std", 0))
|
| 375 |
|
| 376 |
avg_norms = {l: sum(ns)/len(ns) for l, ns in layer_norms.items()}
|
| 377 |
avg_stds = {l: sum(ss)/len(ss) for l, ss in layer_stds.items()}
|
|
|
|
| 500 |
clogged = []
|
| 501 |
for name, h in health_scores.items():
|
| 502 |
if any(norm_type in name.lower() for norm_type in ["layernorm", "ln_", "_norm"]):
|
| 503 |
+
stats = h.get("stats", {}) if isinstance(h, dict) else {}
|
| 504 |
+
kurt = abs(stats.get("kurtosis", 0))
|
| 505 |
if kurt > threshold:
|
| 506 |
clogged.append(name)
|
| 507 |
|
|
|
|
| 538 |
print(" Loading reference norms...")
|
| 539 |
ref_norms = {}
|
| 540 |
for st_file in sorted(glob.glob(f"{ref_path}/*.safetensors")):
|
| 541 |
+
try:
|
| 542 |
+
state = load_file(st_file, device="cpu")
|
| 543 |
+
except Exception as e:
|
| 544 |
+
print(f" WARNING: Failed to load reference {st_file}: {e} — skipping")
|
| 545 |
+
continue
|
| 546 |
for name, tensor in state.items():
|
| 547 |
if name in clogged_norms:
|
| 548 |
ref_norms[name] = tensor.clone()
|
|
|
|
| 558 |
repaired = 0
|
| 559 |
st_files = sorted(glob.glob(f"{model_path}/*.safetensors"))
|
| 560 |
|
| 561 |
+
if not st_files:
|
| 562 |
+
print(" WARNING: No safetensors files found in model path! Skipping repair.")
|
| 563 |
+
return model_path
|
| 564 |
+
|
| 565 |
for st_file in st_files:
|
| 566 |
+
try:
|
| 567 |
+
state = load_file(st_file, device="cpu")
|
| 568 |
+
except Exception as e:
|
| 569 |
+
print(f" WARNING: Failed to load {st_file}: {e} — skipping this shard")
|
| 570 |
+
continue
|
| 571 |
changed = False
|
| 572 |
|
| 573 |
for name in list(state.keys()):
|
| 574 |
if name in ref_norms:
|
| 575 |
+
# Guard: health_scores might not have this name
|
| 576 |
+
if name not in health_scores or "stats" not in health_scores[name]:
|
| 577 |
+
print(f" {name}: not in health_scores — SKIP")
|
| 578 |
+
continue
|
| 579 |
+
if "kurtosis" not in health_scores[name]["stats"]:
|
| 580 |
+
print(f" {name}: no kurtosis in stats — SKIP")
|
| 581 |
+
continue
|
| 582 |
kurt = abs(health_scores[name]["stats"]["kurtosis"])
|
| 583 |
|
| 584 |
+
# BUG FIX: Check if reference is actually better before blending.
|
| 585 |
+
# Some reference norms have HIGHER kurtosis than our model's.
|
| 586 |
+
# Blending toward a worse reference makes things worse!
|
| 587 |
+
# Example from cycle 3: layers.5.input_layernorm went 28.5 → 43.4
|
| 588 |
+
ref_t = ref_norms[name].float()
|
| 589 |
+
ref_kurt = abs(((ref_t - ref_t.mean()) / max(ref_t.std().item(), 1e-8)).pow(4).mean().item() - 3)
|
| 590 |
+
|
| 591 |
+
if ref_kurt >= kurt:
|
| 592 |
+
print(f" {name}: kurtosis {kurt:.1f} (ref is worse: {ref_kurt:.1f}) — SKIP")
|
| 593 |
+
continue
|
| 594 |
+
|
| 595 |
# Blend ratio based on severity
|
| 596 |
if kurt > 100:
|
| 597 |
ref_weight = 0.80 # Severely broken — mostly use reference
|
|
|
|
| 615 |
changed = True
|
| 616 |
|
| 617 |
if changed:
|
| 618 |
+
try:
|
| 619 |
+
save_file(state, st_file)
|
| 620 |
+
except Exception as e:
|
| 621 |
+
print(f" ERROR: Failed to save repaired weights to {st_file}: {e}")
|
| 622 |
+
print(f" Disk may be full! Norm repair aborted for this shard.")
|
| 623 |
del state
|
| 624 |
|
| 625 |
gc.collect()
|
|
|
|
| 653 |
from datasets import load_dataset
|
| 654 |
from torch.utils.data import Dataset
|
| 655 |
|
| 656 |
+
# ── DISK SPACE CHECK ──
|
| 657 |
+
# Qwen3-VL-8B saves as ~16GB. Need at least 25GB free for save + temp files.
|
| 658 |
+
try:
|
| 659 |
+
disk = shutil.disk_usage(cfg.output_dir if Path(cfg.output_dir).exists() else ".")
|
| 660 |
+
free_gb = disk.free / 1e9
|
| 661 |
+
print(f" Disk space: {free_gb:.1f} GB free")
|
| 662 |
+
if free_gb < 25:
|
| 663 |
+
print(f" WARNING: Only {free_gb:.1f} GB free! Need ~25 GB for training + save.")
|
| 664 |
+
if free_gb < 10:
|
| 665 |
+
print(f" CRITICAL: Less than 10 GB free — ABORTING to prevent disk-full corruption!")
|
| 666 |
+
return model_path
|
| 667 |
+
except Exception as e:
|
| 668 |
+
print(f" Could not check disk space: {e} — continuing anyway")
|
| 669 |
+
|
| 670 |
+
# ── CHECKPOINT VALIDATION ──
|
| 671 |
+
model_dir = Path(model_path)
|
| 672 |
+
if model_dir.is_dir():
|
| 673 |
+
if not (model_dir / "config.json").exists():
|
| 674 |
+
print(f" ERROR: No config.json in {model_path} — stale or corrupt checkpoint!")
|
| 675 |
+
return model_path
|
| 676 |
+
st_files = list(model_dir.glob("*.safetensors"))
|
| 677 |
+
if not st_files and not list(model_dir.glob("*.bin")):
|
| 678 |
+
print(f" ERROR: No model weights in {model_path} — checkpoint is empty!")
|
| 679 |
+
return model_path
|
| 680 |
+
|
| 681 |
+
# Load model with OOM guard
|
| 682 |
print(" Loading model...")
|
| 683 |
+
try:
|
| 684 |
+
model = AutoModelForImageTextToText.from_pretrained(
|
| 685 |
+
model_path, dtype=torch.bfloat16,
|
| 686 |
+
device_map="auto", trust_remote_code=True
|
| 687 |
+
)
|
| 688 |
+
except (RuntimeError, Exception) as e:
|
| 689 |
+
gc.collect()
|
| 690 |
+
if torch.cuda.is_available():
|
| 691 |
+
torch.cuda.empty_cache()
|
| 692 |
+
print(f" CRITICAL: Failed to load model: {e}")
|
| 693 |
+
return model_path
|
| 694 |
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 695 |
|
| 696 |
# Enable gradient checkpointing for mega mode (saves ~40% VRAM)
|
|
|
|
| 709 |
bias="none",
|
| 710 |
task_type=TaskType.CAUSAL_LM,
|
| 711 |
)
|
| 712 |
+
try:
|
| 713 |
+
model = get_peft_model(model, lora_config)
|
| 714 |
+
except Exception as e:
|
| 715 |
+
print(f"\n CRITICAL: get_peft_model failed: {e}")
|
| 716 |
+
print(f" LoRA config may be incompatible with this model.")
|
| 717 |
+
del model
|
| 718 |
+
gc.collect()
|
| 719 |
+
if torch.cuda.is_available():
|
| 720 |
+
torch.cuda.empty_cache()
|
| 721 |
+
return model_path
|
| 722 |
|
| 723 |
# UNFREEZE clogged norms — the key cycle 2 fix
|
| 724 |
unfrozen_count = 0
|
|
|
|
| 749 |
train_texts = []
|
| 750 |
total = cfg.num_train_samples
|
| 751 |
|
| 752 |
+
# === CUSTOM DATA PATH (from td_data_finder — targeted by weakness) ===
|
| 753 |
+
# If td_loop found weakness-targeted data, use that instead of hardcoded datasets.
|
| 754 |
+
# This is what makes the self-improvement loop actually self-improving:
|
| 755 |
+
# each cycle trains on different data based on what the model is bad at.
|
| 756 |
+
if cfg.custom_data_path and Path(cfg.custom_data_path).exists():
|
| 757 |
+
print(f" Using TARGETED training data from: {cfg.custom_data_path}")
|
| 758 |
+
with open(cfg.custom_data_path) as _f:
|
| 759 |
+
for line in _f:
|
| 760 |
+
line = line.strip()
|
| 761 |
+
if not line:
|
| 762 |
+
continue
|
| 763 |
+
try:
|
| 764 |
+
item = json.loads(line)
|
| 765 |
+
text = item.get("text", "")
|
| 766 |
+
if text and len(text) > 20:
|
| 767 |
+
train_texts.append(text)
|
| 768 |
+
except json.JSONDecodeError:
|
| 769 |
+
continue
|
| 770 |
+
if len(train_texts) >= total:
|
| 771 |
+
break
|
| 772 |
+
print(f" Loaded {len(train_texts)} targeted samples")
|
| 773 |
+
if len(train_texts) < total * 0.5:
|
| 774 |
+
print(f" WARNING: Only got {len(train_texts)}/{total} targeted samples.")
|
| 775 |
+
print(f" Supplementing with generic data to reach {total}...")
|
| 776 |
+
# Fall through to hardcoded data loading below to fill the gap
|
| 777 |
+
else:
|
| 778 |
+
# Got enough targeted data — skip hardcoded loading
|
| 779 |
+
# Jump past the hardcoded data section
|
| 780 |
+
pass # train_texts is already populated
|
| 781 |
+
|
| 782 |
+
# Only load hardcoded datasets if we don't have enough targeted data
|
| 783 |
+
# Only load generic datasets if targeted data wasn't enough
|
| 784 |
+
# When alloc values are 0, each section's `if count >= alloc[key]: break`
|
| 785 |
+
# exits immediately, loading 0 samples. Clean skip, no indent changes needed.
|
| 786 |
+
alloc = {"code": 0, "math": 0, "reasoning": 0, "creativity": 0,
|
| 787 |
+
"instruction_following": 0, "general": 0}
|
| 788 |
+
if len(train_texts) < total:
|
| 789 |
+
remaining_needed = total - len(train_texts)
|
| 790 |
+
if cfg.custom_data_path and train_texts:
|
| 791 |
+
print(f" Need {remaining_needed} more samples from generic datasets...")
|
| 792 |
+
scale = remaining_needed / total
|
| 793 |
+
else:
|
| 794 |
+
scale = 1.0
|
| 795 |
+
|
| 796 |
+
if cfg.mega_mode:
|
| 797 |
+
alloc = {"code": int(total*0.25*scale), "math": int(total*0.25*scale),
|
| 798 |
+
"reasoning": int(total*0.19*scale), "creativity": int(total*0.12*scale),
|
| 799 |
+
"instruction_following": int(total*0.12*scale), "general": int(total*0.07*scale)}
|
| 800 |
+
else:
|
| 801 |
+
per = int((total // 5) * scale)
|
| 802 |
+
alloc = {"code": per, "math": per, "reasoning": per,
|
| 803 |
+
"creativity": 0, "instruction_following": per, "general": per}
|
| 804 |
else:
|
| 805 |
+
print(f" Targeted data sufficient ({len(train_texts)} samples). Skipping generic datasets.")
|
|
|
|
|
|
|
| 806 |
|
| 807 |
# 1. MATH (top priority)
|
| 808 |
try:
|
|
|
|
| 809 |
count = 0
|
| 810 |
+
if alloc["math"] > 0:
|
| 811 |
+
ds = load_dataset("openai/gsm8k", "main", split="train")
|
| 812 |
+
for row in ds:
|
| 813 |
+
if count >= alloc["math"]:
|
| 814 |
+
break
|
| 815 |
+
q, a = row.get("question", ""), row.get("answer", "")
|
| 816 |
+
if q and a:
|
| 817 |
+
chat = (f"<|im_start|>user\n{q}\nThink step by step.<|im_end|>\n"
|
| 818 |
+
f"<|im_start|>assistant\n<think>\n{a}\n</think><|im_end|>")
|
| 819 |
+
train_texts.append(chat)
|
| 820 |
+
count += 1
|
| 821 |
print(f" Math (GSM8K): {count}")
|
| 822 |
except Exception as e:
|
| 823 |
print(f" WARNING: GSM8K failed: {e}")
|
|
|
|
| 843 |
|
| 844 |
# 2. CODE (top priority)
|
| 845 |
try:
|
|
|
|
| 846 |
count = 0
|
| 847 |
+
if alloc["code"] > 0:
|
| 848 |
+
ds = load_dataset("sahil2801/CodeAlpaca-20k", split="train")
|
| 849 |
+
for row in ds:
|
| 850 |
+
if count >= alloc["code"]:
|
| 851 |
+
break
|
| 852 |
+
inst = row.get("instruction", "")
|
| 853 |
+
inp = row.get("input", "")
|
| 854 |
+
out = row.get("output", "")
|
| 855 |
+
if inst and out and len(out) > 20:
|
| 856 |
+
prompt = f"{inst}\n{inp}" if inp else inst
|
| 857 |
+
chat = (f"<|im_start|>user\n{prompt}<|im_end|>\n"
|
| 858 |
+
f"<|im_start|>assistant\n{out}<|im_end|>")
|
| 859 |
+
train_texts.append(chat)
|
| 860 |
+
count += 1
|
| 861 |
print(f" Code (CodeAlpaca): {count}")
|
| 862 |
except Exception as e:
|
| 863 |
print(f" WARNING: CodeAlpaca failed: {e}")
|
|
|
|
| 883 |
|
| 884 |
# 3. REASONING (top priority)
|
| 885 |
try:
|
|
|
|
| 886 |
count = 0
|
| 887 |
+
if alloc["reasoning"] > 0:
|
| 888 |
+
ds = load_dataset("TIGER-Lab/MMLU-Pro", split="test")
|
| 889 |
+
for row in ds:
|
| 890 |
+
if count >= alloc["reasoning"]:
|
| 891 |
+
break
|
| 892 |
+
q = row.get("question", "")
|
| 893 |
+
options = row.get("options", [])
|
| 894 |
+
answer_idx = row.get("answer_index", -1)
|
| 895 |
+
if q and options and 0 <= answer_idx < len(options):
|
| 896 |
+
answer_text = options[answer_idx]
|
| 897 |
+
chat = (f"<|im_start|>user\n{q}<|im_end|>\n"
|
| 898 |
+
f"<|im_start|>assistant\n<think>\nLet me reason through this.\n"
|
| 899 |
+
f"{answer_text}\n</think>\n{answer_text}<|im_end|>")
|
| 900 |
+
train_texts.append(chat)
|
| 901 |
+
count += 1
|
| 902 |
print(f" Reasoning (MMLU-Pro): {count}")
|
| 903 |
except Exception as e:
|
| 904 |
print(f" WARNING: MMLU-Pro failed: {e}")
|
|
|
|
| 929 |
|
| 930 |
# 5. INSTRUCTIONS (general)
|
| 931 |
try:
|
|
|
|
| 932 |
count = 0
|
| 933 |
+
if alloc.get("instruction_following", 0) > 0:
|
| 934 |
+
ds = load_dataset("yahma/alpaca-cleaned", split="train")
|
| 935 |
+
for row in ds:
|
| 936 |
+
if count >= alloc["instruction_following"]:
|
| 937 |
+
break
|
| 938 |
+
inst = row.get("instruction", "")
|
| 939 |
+
inp = row.get("input", "")
|
| 940 |
+
out = row.get("output", "")
|
| 941 |
+
if inst and out and len(out) > 30:
|
| 942 |
+
prompt = f"{inst}\n{inp}" if inp else inst
|
| 943 |
+
chat = (f"<|im_start|>user\n{prompt}<|im_end|>\n"
|
| 944 |
+
f"<|im_start|>assistant\n{out}<|im_end|>")
|
| 945 |
+
train_texts.append(chat)
|
| 946 |
+
count += 1
|
| 947 |
print(f" Instructions (Alpaca): {count}")
|
| 948 |
except Exception as e:
|
| 949 |
print(f" WARNING: Alpaca failed: {e}")
|
| 950 |
|
| 951 |
# 6. GENERAL TEXT (lowest priority)
|
| 952 |
try:
|
|
|
|
| 953 |
count = 0
|
| 954 |
+
if alloc.get("general", 0) > 0:
|
| 955 |
+
ds = load_dataset("neuralmagic/LLM_compression_calibration", split="train")
|
| 956 |
+
for row in ds:
|
| 957 |
+
if count >= alloc["general"]:
|
| 958 |
+
break
|
| 959 |
+
text = row.get("text", "")
|
| 960 |
+
if len(text) > 100:
|
| 961 |
+
chat = (f"<|im_start|>user\nContinue this text naturally.<|im_end|>\n"
|
| 962 |
+
f"<|im_start|>assistant\n{text[:800]}<|im_end|>")
|
| 963 |
+
train_texts.append(chat)
|
| 964 |
+
count += 1
|
| 965 |
print(f" General text: {count}")
|
| 966 |
except Exception as e:
|
| 967 |
print(f" WARNING: General text failed: {e}")
|
|
|
|
| 972 |
print(" ERROR: Not enough training data!")
|
| 973 |
return model_path
|
| 974 |
|
| 975 |
+
# === CONTAMINATION CHECK ===
|
| 976 |
+
# Remove any training samples that match benchmark/holdout questions.
|
| 977 |
+
# Without this, benchmark scores become meaningless after a few cycles.
|
| 978 |
+
try:
|
| 979 |
+
from td_contamination import check_contamination
|
| 980 |
+
before = len(train_texts)
|
| 981 |
+
train_texts = check_contamination(train_texts)
|
| 982 |
+
if len(train_texts) < before:
|
| 983 |
+
print(f" Contamination guard removed {before - len(train_texts)} samples")
|
| 984 |
+
except ImportError:
|
| 985 |
+
print(" WARNING: td_contamination.py not found — skipping contamination check")
|
| 986 |
+
|
| 987 |
+
# === DIVERSITY CHECK (anti-collapse) ===
|
| 988 |
+
# If too many training samples are near-duplicates, the model collapses
|
| 989 |
+
# toward repeating the same patterns. Detect and warn early.
|
| 990 |
+
unique_prefixes = set()
|
| 991 |
+
for t in train_texts:
|
| 992 |
+
# Use first 100 chars as a rough fingerprint
|
| 993 |
+
prefix = t[:100].strip().lower()
|
| 994 |
+
unique_prefixes.add(prefix)
|
| 995 |
+
diversity_ratio = len(unique_prefixes) / max(len(train_texts), 1)
|
| 996 |
+
print(f" Diversity check: {len(unique_prefixes)}/{len(train_texts)} unique prefixes ({diversity_ratio:.0%})")
|
| 997 |
+
if diversity_ratio < 0.5:
|
| 998 |
+
print(" WARNING: Low diversity (<50%) — high risk of mode collapse!")
|
| 999 |
+
print(" Deduplicating training data...")
|
| 1000 |
+
seen = set()
|
| 1001 |
+
deduped = []
|
| 1002 |
+
for t in train_texts:
|
| 1003 |
+
prefix = t[:100].strip().lower()
|
| 1004 |
+
if prefix not in seen:
|
| 1005 |
+
seen.add(prefix)
|
| 1006 |
+
deduped.append(t)
|
| 1007 |
+
removed = len(train_texts) - len(deduped)
|
| 1008 |
+
train_texts = deduped
|
| 1009 |
+
print(f" Removed {removed} near-duplicate samples. Now {len(train_texts)} unique.")
|
| 1010 |
+
elif diversity_ratio < 0.7:
|
| 1011 |
+
print(" NOTE: Moderate diversity (50-70%) — monitor for early signs of collapse")
|
| 1012 |
+
|
| 1013 |
random.shuffle(train_texts)
|
| 1014 |
|
| 1015 |
# Tokenize
|
|
|
|
| 1030 |
print(" Tokenizing...")
|
| 1031 |
dataset = ImproveDataset(train_texts, tokenizer)
|
| 1032 |
|
| 1033 |
+
if len(dataset) == 0:
|
| 1034 |
+
print(" ERROR: Dataset is empty after tokenization!")
|
| 1035 |
+
return model_path
|
| 1036 |
+
|
| 1037 |
out_dir = Path(cfg.output_dir) / "train_output"
|
| 1038 |
out_dir.mkdir(parents=True, exist_ok=True)
|
| 1039 |
|
| 1040 |
+
effective_batch = cfg.train_batch * cfg.train_grad_accum
|
| 1041 |
+
total_steps = (len(dataset) * cfg.train_epochs) // max(effective_batch, 1)
|
| 1042 |
|
| 1043 |
# Set up optimizer with different LR for norms vs LoRA
|
| 1044 |
# Group parameters: norms get higher LR
|
|
|
|
| 1095 |
print(f"\n Training: ~{total_steps} steps on {len(target_layers)} layers + {len(clogged_norms)} norms")
|
| 1096 |
print(f" Data: {len(train_texts)} samples × {cfg.train_epochs} epochs")
|
| 1097 |
print(f" LoRA: r={cfg.lora_r}, alpha={cfg.lora_alpha}")
|
| 1098 |
+
|
| 1099 |
+
try:
|
| 1100 |
+
train_result = trainer.train()
|
| 1101 |
+
except (torch.cuda.OutOfMemoryError, RuntimeError) as e:
|
| 1102 |
+
err_str = str(e).lower()
|
| 1103 |
+
if "out of memory" in err_str or "cuda" in err_str:
|
| 1104 |
+
print(f"\n OOM DURING TRAINING: {e}")
|
| 1105 |
+
print(" Cleaning up GPU memory and returning previous model...")
|
| 1106 |
+
del model, trainer
|
| 1107 |
+
gc.collect()
|
| 1108 |
+
if torch.cuda.is_available():
|
| 1109 |
+
torch.cuda.empty_cache()
|
| 1110 |
+
shutil.rmtree(str(out_dir), ignore_errors=True)
|
| 1111 |
+
return model_path
|
| 1112 |
+
else:
|
| 1113 |
+
raise # Re-raise non-OOM RuntimeErrors
|
| 1114 |
+
|
| 1115 |
+
# ── NaN/Inf LOSS CHECK ──
|
| 1116 |
+
# If training produced NaN loss, the model weights are garbage — abort
|
| 1117 |
+
try:
|
| 1118 |
+
final_loss = train_result.training_loss
|
| 1119 |
+
if final_loss is not None and (math.isnan(final_loss) or math.isinf(final_loss)):
|
| 1120 |
+
print(f"\n CRITICAL: Training loss is {final_loss}! Model weights are corrupted.")
|
| 1121 |
+
print(f" Falling back to previous model: {model_path}")
|
| 1122 |
+
del model, trainer
|
| 1123 |
+
gc.collect()
|
| 1124 |
+
if torch.cuda.is_available():
|
| 1125 |
+
torch.cuda.empty_cache()
|
| 1126 |
+
shutil.rmtree(str(out_dir), ignore_errors=True)
|
| 1127 |
+
return model_path
|
| 1128 |
+
print(f"\n Training complete. Final loss: {final_loss:.4f}")
|
| 1129 |
+
except (AttributeError, TypeError):
|
| 1130 |
+
print("\n Training complete. (Could not read final loss)")
|
| 1131 |
|
| 1132 |
# Merge LoRA back
|
| 1133 |
print("\n Merging LoRA adapters into model...")
|
| 1134 |
+
try:
|
| 1135 |
+
merged = model.merge_and_unload()
|
| 1136 |
+
except Exception as e:
|
| 1137 |
+
print(f"\n CRITICAL: LoRA merge failed: {e}")
|
| 1138 |
+
print(f" Falling back to previous model: {model_path}")
|
| 1139 |
+
del model, trainer
|
| 1140 |
+
gc.collect()
|
| 1141 |
+
if torch.cuda.is_available():
|
| 1142 |
+
torch.cuda.empty_cache()
|
| 1143 |
+
shutil.rmtree(str(out_dir), ignore_errors=True)
|
| 1144 |
+
return model_path
|
| 1145 |
|
| 1146 |
# Save
|
| 1147 |
save_dir = Path(cfg.output_dir) / f"improved_cycle{cycle_num}"
|
| 1148 |
save_dir.mkdir(parents=True, exist_ok=True)
|
| 1149 |
print(f" Saving to {save_dir}...")
|
| 1150 |
+
try:
|
| 1151 |
+
merged.save_pretrained(str(save_dir), safe_serialization=True)
|
| 1152 |
+
tokenizer.save_pretrained(str(save_dir))
|
| 1153 |
+
except Exception as e:
|
| 1154 |
+
print(f"\n CRITICAL: Model save failed: {e}")
|
| 1155 |
+
print(f" Disk may be full! Falling back to previous model: {model_path}")
|
| 1156 |
+
del merged, model
|
| 1157 |
+
gc.collect()
|
| 1158 |
+
if torch.cuda.is_available():
|
| 1159 |
+
torch.cuda.empty_cache()
|
| 1160 |
+
shutil.rmtree(str(save_dir), ignore_errors=True)
|
| 1161 |
+
shutil.rmtree(str(out_dir), ignore_errors=True)
|
| 1162 |
+
return model_path
|
| 1163 |
+
|
| 1164 |
+
# Verify save was successful — check files exist and aren't tiny/corrupt
|
| 1165 |
+
st_files = list(save_dir.glob("*.safetensors"))
|
| 1166 |
+
if not st_files:
|
| 1167 |
+
print(" ERROR: No safetensors files saved! Disk may be full.")
|
| 1168 |
+
print(f" Falling back to previous model: {model_path}")
|
| 1169 |
+
shutil.rmtree(str(save_dir), ignore_errors=True)
|
| 1170 |
+
return model_path
|
| 1171 |
+
sz = sum(f.stat().st_size for f in st_files) / 1e9
|
| 1172 |
+
if sz < 1.0: # Qwen3-VL-8B should be ~16GB, anything under 1GB is corrupt
|
| 1173 |
+
print(f" ERROR: Saved model suspiciously small ({sz:.2f} GB). Likely corrupt!")
|
| 1174 |
+
print(f" Falling back to previous model: {model_path}")
|
| 1175 |
+
shutil.rmtree(str(save_dir), ignore_errors=True)
|
| 1176 |
+
return model_path
|
| 1177 |
print(f" SAVED: {save_dir} ({sz:.1f} GB)")
|
| 1178 |
|
| 1179 |
shutil.rmtree(str(out_dir), ignore_errors=True)
|
|
|
|
| 1197 |
|
| 1198 |
from transformers import AutoModelForImageTextToText, AutoTokenizer
|
| 1199 |
|
| 1200 |
+
# ── CHECKPOINT VALIDATION ──
|
| 1201 |
+
mp = Path(model_path)
|
| 1202 |
+
if mp.is_dir():
|
| 1203 |
+
if not (mp / "config.json").exists():
|
| 1204 |
+
print(f" ERROR: No config.json in {model_path} — skipping quick benchmark")
|
| 1205 |
+
return {"score": 0, "total": 5, "results": [], "error": "no config.json"}
|
| 1206 |
+
if not list(mp.glob("*.safetensors")) and not list(mp.glob("*.bin")):
|
| 1207 |
+
print(f" ERROR: No model weights in {model_path} — skipping quick benchmark")
|
| 1208 |
+
return {"score": 0, "total": 5, "results": [], "error": "no weights"}
|
| 1209 |
+
|
| 1210 |
+
# ── MODEL LOAD with OOM guard ──
|
| 1211 |
+
try:
|
| 1212 |
+
model = AutoModelForImageTextToText.from_pretrained(
|
| 1213 |
+
model_path, dtype=torch.bfloat16,
|
| 1214 |
+
device_map="auto", trust_remote_code=True
|
| 1215 |
+
)
|
| 1216 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 1217 |
+
except (RuntimeError, Exception) as e:
|
| 1218 |
+
gc.collect()
|
| 1219 |
+
if torch.cuda.is_available():
|
| 1220 |
+
torch.cuda.empty_cache()
|
| 1221 |
+
print(f" CRITICAL: Failed to load model for quick benchmark: {e}")
|
| 1222 |
+
return {"score": 0, "total": 5, "results": [], "error": str(e)}
|
| 1223 |
|
| 1224 |
tests = [
|
| 1225 |
{"q": "What is 47 * 23?", "expected": "1081", "type": "math"},
|
|
|
|
| 1234 |
|
| 1235 |
results = []
|
| 1236 |
for test in tests:
|
| 1237 |
+
try:
|
| 1238 |
+
chat = f"<|im_start|>user\n{test['q']}<|im_end|>\n<|im_start|>assistant\n"
|
| 1239 |
+
ids = tokenizer(chat, return_tensors="pt").to(model.device)
|
| 1240 |
+
try:
|
| 1241 |
+
with torch.no_grad():
|
| 1242 |
+
out = model.generate(**ids, max_new_tokens=200, do_sample=False)
|
| 1243 |
+
except (torch.cuda.OutOfMemoryError, RuntimeError) as e:
|
| 1244 |
+
gc.collect()
|
| 1245 |
+
if torch.cuda.is_available():
|
| 1246 |
+
torch.cuda.empty_cache()
|
| 1247 |
+
print(f"\n [OOM] {test['type']}: {test['q']}")
|
| 1248 |
+
results.append({"type": test["type"], "passed": False, "error": "OOM"})
|
| 1249 |
+
continue
|
| 1250 |
+
answer = tokenizer.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True)
|
| 1251 |
+
# Truncate garbage output
|
| 1252 |
+
if len(answer) > 5000:
|
| 1253 |
+
answer = answer[:5000]
|
| 1254 |
+
|
| 1255 |
+
passed = test["expected"].lower() in answer.lower()
|
| 1256 |
+
status = "PASS" if passed else "FAIL"
|
| 1257 |
+
results.append({"type": test["type"], "passed": passed})
|
| 1258 |
+
|
| 1259 |
+
print(f"\n [{status}] {test['type']}: {test['q']}")
|
| 1260 |
+
print(f" Answer: {answer[:200]}")
|
| 1261 |
+
print(f" Expected to contain: {test['expected']}")
|
| 1262 |
+
except Exception as e:
|
| 1263 |
+
print(f"\n [ERROR] {test['type']}: {test['q']} — {e}")
|
| 1264 |
+
results.append({"type": test["type"], "passed": False, "error": str(e)})
|
| 1265 |
|
| 1266 |
+
score = sum(1 for r in results if r.get("passed", False))
|
| 1267 |
print(f"\n BENCHMARK: {score}/{len(tests)} passed")
|
| 1268 |
|
| 1269 |
del model
|
|
|
|
| 1287 |
if not cfg.model_path:
|
| 1288 |
cfg.model_path, cycle_num = auto_detect_model_path()
|
| 1289 |
else:
|
| 1290 |
+
# BUG FIX: Parse actual cycle number from path instead of assuming 2
|
| 1291 |
+
# "improved_cycle5" → cycle 6, "improved_cycle2" → cycle 3
|
| 1292 |
+
# Old code: `cycle_num = 2 if "improve" in cfg.model_path else 1`
|
| 1293 |
+
# That would ALWAYS save to improved_cycle2, overwriting previous!
|
| 1294 |
+
import re as _re
|
| 1295 |
+
cycle_match = _re.search(r'improved_cycle(\d+)', cfg.model_path)
|
| 1296 |
+
if cycle_match:
|
| 1297 |
+
cycle_num = int(cycle_match.group(1)) + 1
|
| 1298 |
+
elif "improve" in cfg.model_path:
|
| 1299 |
+
cycle_num = 2
|
| 1300 |
+
else:
|
| 1301 |
+
cycle_num = 1
|
| 1302 |
|
| 1303 |
# MEGA MODE for cycle 3+
|
| 1304 |
if cycle_num >= 3 and not cfg.mega_mode:
|
|
|
|
| 1338 |
print("STEP 3/7: REFERENCE COMPARISON")
|
| 1339 |
print(f"{'='*40}")
|
| 1340 |
ref_path = Path(cfg.output_dir) / "reference_weights"
|
| 1341 |
+
ref_available = False
|
| 1342 |
+
try:
|
| 1343 |
+
if not ref_path.exists() or not list(ref_path.glob("*.safetensors")):
|
| 1344 |
+
print(f" Downloading reference: {cfg.reference_model}...")
|
| 1345 |
+
from huggingface_hub import snapshot_download
|
| 1346 |
+
snapshot_download(
|
| 1347 |
+
cfg.reference_model, local_dir=str(ref_path),
|
| 1348 |
+
allow_patterns=["*.safetensors", "*.json"],
|
| 1349 |
+
ignore_patterns=["*.bin", "*.pt", "*.onnx"],
|
| 1350 |
+
)
|
| 1351 |
+
ref_analysis = analyze_model_weights(str(ref_path), "Qwen3-VL-8B (reference)")
|
| 1352 |
+
damage_scores = compute_damage_scores(model_analysis, ref_analysis)
|
| 1353 |
+
ref_available = True
|
| 1354 |
+
except Exception as e:
|
| 1355 |
+
print(f" WARNING: Reference comparison failed: {e}")
|
| 1356 |
+
print(f" Continuing without damage scores (norm repair + LoRA still work)")
|
| 1357 |
+
damage_scores = {}
|
| 1358 |
+
ref_analysis = None
|
| 1359 |
# DON'T delete reference yet — we need it for norm repair
|
| 1360 |
gc.collect()
|
| 1361 |
|
|
|
|
| 1378 |
if clogged_norms:
|
| 1379 |
print(f"\n CLOGGED NORMS DETECTED ({len(clogged_norms)}):")
|
| 1380 |
for cn in clogged_norms:
|
| 1381 |
+
stats = health_scores.get(cn, {}).get("stats", {})
|
| 1382 |
+
kurt = abs(stats.get("kurtosis", 0))
|
| 1383 |
print(f" {cn}: kurtosis={kurt:.1f}")
|
| 1384 |
else:
|
| 1385 |
print(f"\n No clogged norms above threshold {cfg.norm_kurtosis_threshold}")
|
| 1386 |
|
| 1387 |
# STEP 5c: REPAIR clogged norms surgically
|
| 1388 |
+
if clogged_norms and ref_available:
|
| 1389 |
print(f"\n{'='*40}")
|
| 1390 |
print("STEP 5c/7: SURGICAL NORM REPAIR")
|
| 1391 |
print(f"{'='*40}")
|
| 1392 |
+
try:
|
| 1393 |
+
repair_clogged_norms(cfg.model_path, clogged_norms, str(ref_path), health_scores)
|
| 1394 |
+
except Exception as e:
|
| 1395 |
+
print(f" WARNING: Norm repair failed: {e}")
|
| 1396 |
+
print(f" Continuing without repair — LoRA training can still help")
|
| 1397 |
+
elif clogged_norms and not ref_available:
|
| 1398 |
+
print(f"\n Skipping norm repair — reference model not available")
|
| 1399 |
|
| 1400 |
# Clean up reference now
|
| 1401 |
print(f" Cleaning up reference weights...")
|
| 1402 |
shutil.rmtree(str(ref_path), ignore_errors=True)
|
| 1403 |
gc.collect()
|
| 1404 |
|
| 1405 |
+
# Save report (non-critical)
|
| 1406 |
report_dir = Path(cfg.output_dir)
|
| 1407 |
report_dir.mkdir(parents=True, exist_ok=True)
|
| 1408 |
pre_report = {
|
|
|
|
| 1415 |
"layer_details": {str(k): v for k, v in layer_details.items()},
|
| 1416 |
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 1417 |
}
|
| 1418 |
+
try:
|
| 1419 |
+
with open(report_dir / f"pre_cycle{cycle_num}_report.json", "w") as f:
|
| 1420 |
+
json.dump(pre_report, f, indent=2)
|
| 1421 |
+
except OSError as e:
|
| 1422 |
+
print(f" WARNING: Could not save pre-report: {e}")
|
| 1423 |
|
| 1424 |
# STEP 6: Train
|
| 1425 |
print(f"\n{'='*40}")
|
|
|
|
| 1432 |
print("STEP 7/7: VERIFY + BENCHMARK")
|
| 1433 |
print(f"{'='*40}")
|
| 1434 |
|
| 1435 |
+
# Re-analyze health (non-critical — if this fails, cycle still succeeded)
|
|
|
|
|
|
|
|
|
|
| 1436 |
pre_avg = sum(h["health_score"] for h in health_scores.values()) / max(len(health_scores), 1)
|
| 1437 |
+
post_avg = pre_avg # Default: same as before if analysis fails
|
| 1438 |
+
post_health = {}
|
| 1439 |
+
try:
|
| 1440 |
+
post_analysis = analyze_model_weights(improved_path, "TD improved")
|
| 1441 |
+
post_health = compute_health_scores(post_analysis)
|
| 1442 |
+
post_avg = sum(h["health_score"] for h in post_health.values()) / max(len(post_health), 1)
|
| 1443 |
+
except Exception as e:
|
| 1444 |
+
print(f" WARNING: Post-training health analysis failed: {e}")
|
| 1445 |
+
print(f" Cannot compare before/after — but training output is still valid.")
|
| 1446 |
|
| 1447 |
+
# Per-layer comparison (only if post-analysis succeeded)
|
| 1448 |
+
if post_health:
|
| 1449 |
+
print(f"\n Per-layer improvement:")
|
| 1450 |
+
else:
|
| 1451 |
+
print(f"\n (Skipping per-layer comparison — post-analysis unavailable)")
|
| 1452 |
for layer_num in target_layers:
|
| 1453 |
+
if not post_health:
|
| 1454 |
+
break
|
| 1455 |
post_layer_scores = []
|
| 1456 |
for name, h in post_health.items():
|
| 1457 |
parts = name.split(".")
|
|
|
|
| 1470 |
if clogged_norms:
|
| 1471 |
print(f"\n Clogged norm changes:")
|
| 1472 |
for cn in clogged_norms:
|
| 1473 |
+
pre_stats = health_scores.get(cn, {}).get("stats", {})
|
| 1474 |
+
pre_kurt = abs(pre_stats.get("kurtosis", 0))
|
| 1475 |
# Find matching post norm
|
| 1476 |
if cn in post_health:
|
| 1477 |
+
post_stats = post_health.get(cn, {}).get("stats", {})
|
| 1478 |
+
post_kurt = abs(post_stats.get("kurtosis", 0))
|
| 1479 |
delta = pre_kurt - post_kurt
|
| 1480 |
symbol = "+" if delta > 0 else "-"
|
| 1481 |
print(f" {cn}: kurtosis {pre_kurt:.1f} → {post_kurt:.1f} ({symbol}{abs(delta):.1f})")
|
| 1482 |
|
| 1483 |
+
# Quick benchmark (non-critical — don't crash if it fails)
|
| 1484 |
+
try:
|
| 1485 |
+
benchmark = quick_benchmark(improved_path)
|
| 1486 |
+
except Exception as e:
|
| 1487 |
+
print(f" WARNING: Quick benchmark failed: {e}")
|
| 1488 |
+
benchmark = {"score": 0, "total": 0, "error": str(e)}
|
| 1489 |
|
| 1490 |
elapsed = (time.time() - start) / 60
|
| 1491 |
health_improvement = (pre_avg - post_avg) / max(pre_avg, 1e-8) * 100
|
|
|
|
| 1498 |
print(f" Health BEFORE: {pre_avg:.6f}")
|
| 1499 |
print(f" Health AFTER: {post_avg:.6f}")
|
| 1500 |
print(f" Health improvement: {health_improvement:.1f}%")
|
| 1501 |
+
print(f" Benchmark: {benchmark.get('score', '?')}/{benchmark.get('total', '?')}")
|
| 1502 |
print(f" Improved model: {improved_path}")
|
| 1503 |
print(f"{'='*60}")
|
| 1504 |
|
| 1505 |
+
# Final report (non-critical)
|
| 1506 |
+
try:
|
| 1507 |
+
final_report = {
|
| 1508 |
+
**pre_report,
|
| 1509 |
+
"pre_avg_health": pre_avg,
|
| 1510 |
+
"post_avg_health": post_avg,
|
| 1511 |
+
"health_improvement_pct": health_improvement,
|
| 1512 |
+
"benchmark": benchmark,
|
| 1513 |
+
"duration_min": elapsed,
|
| 1514 |
+
"improved_path": improved_path,
|
| 1515 |
+
}
|
| 1516 |
+
report_save_path = Path(improved_path) / "cycle_report.json"
|
| 1517 |
+
with open(report_save_path, "w") as f:
|
| 1518 |
+
json.dump(final_report, f, indent=2)
|
| 1519 |
+
except Exception as e:
|
| 1520 |
+
print(f" WARNING: Could not save cycle report: {e}")
|
| 1521 |
|
| 1522 |
return improved_path
|
| 1523 |
|
hugging/td_fuse/td_benchmark.py
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TD Benchmark Suite v1 — Model Report Card
|
| 3 |
+
|
| 4 |
+
Tests the model across 6 categories and gives a score per category.
|
| 5 |
+
This is Step 1A of the self-improvement loop.
|
| 6 |
+
|
| 7 |
+
Categories:
|
| 8 |
+
1. Math (arithmetic, algebra, word problems)
|
| 9 |
+
2. Code (write functions, debug, explain)
|
| 10 |
+
3. Reasoning (logic, multi-step, common sense)
|
| 11 |
+
4. Creativity (writing, rewriting, explaining)
|
| 12 |
+
5. Knowledge (science, history, general facts)
|
| 13 |
+
6. Instruction Following (do exactly what's asked)
|
| 14 |
+
|
| 15 |
+
Output: JSON report with scores per category + list of failed questions.
|
| 16 |
+
The weakness finder uses this to decide what to train on next.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import json
|
| 21 |
+
import time
|
| 22 |
+
import re
|
| 23 |
+
import gc
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
from typing import Dict, List, Tuple, Optional
|
| 26 |
+
from dataclasses import dataclass, field
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class BenchmarkConfig:
|
| 31 |
+
model_path: str = ""
|
| 32 |
+
output_dir: str = "td_fuse_outputs/benchmark"
|
| 33 |
+
output_path: str = "" # If set, save results to this exact path
|
| 34 |
+
max_new_tokens: int = 256 # SPEED: 512 was overkill, most answers are short
|
| 35 |
+
temperature: float = 0.0 # Greedy — deterministic answers
|
| 36 |
+
batch_questions: int = 1 # Questions per batch (1 = sequential)
|
| 37 |
+
disable_thinking: bool = True # SPEED: Skip <think> blocks for benchmark
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# ============================================================
|
| 41 |
+
# TEST BANKS — questions with verifiable answers
|
| 42 |
+
# ============================================================
|
| 43 |
+
|
| 44 |
+
MATH_TESTS = [
|
| 45 |
+
# Arithmetic
|
| 46 |
+
{"q": "What is 47 * 23?", "answer": "1081", "type": "arithmetic", "difficulty": "easy"},
|
| 47 |
+
{"q": "What is 156 + 289?", "answer": "445", "type": "arithmetic", "difficulty": "easy"},
|
| 48 |
+
{"q": "What is 1000 - 387?", "answer": "613", "type": "arithmetic", "difficulty": "easy"},
|
| 49 |
+
{"q": "What is 144 / 12?", "answer": "12", "type": "arithmetic", "difficulty": "easy"},
|
| 50 |
+
{"q": "What is 17 * 19?", "answer": "323", "type": "arithmetic", "difficulty": "easy"},
|
| 51 |
+
{"q": "What is 2.5 * 4.8?", "answer": "12", "type": "arithmetic", "difficulty": "medium"},
|
| 52 |
+
{"q": "What is 15% of 240?", "answer": "36", "type": "arithmetic", "difficulty": "medium"},
|
| 53 |
+
{"q": "What is 3/4 + 2/3? Give your answer as a fraction.", "answer": "17/12", "type": "fractions", "difficulty": "medium"},
|
| 54 |
+
|
| 55 |
+
# Algebra
|
| 56 |
+
{"q": "Solve for x: 3x + 7 = 22", "answer": "5", "type": "algebra", "difficulty": "easy"},
|
| 57 |
+
{"q": "Solve for x: 2x - 5 = 13", "answer": "9", "type": "algebra", "difficulty": "easy"},
|
| 58 |
+
{"q": "Solve for x: x^2 = 144", "answer": "12", "type": "algebra", "difficulty": "medium"},
|
| 59 |
+
{"q": "If f(x) = 2x + 3, what is f(7)?", "answer": "17", "type": "algebra", "difficulty": "medium"},
|
| 60 |
+
|
| 61 |
+
# Word problems
|
| 62 |
+
{"q": "A store sells apples for $2 each and oranges for $3 each. If I buy 4 apples and 3 oranges, how much do I spend?", "answer": "17", "type": "word_problem", "difficulty": "easy"},
|
| 63 |
+
{"q": "A train travels at 60 mph. How far does it go in 2.5 hours?", "answer": "150", "type": "word_problem", "difficulty": "easy"},
|
| 64 |
+
{"q": "If 5 workers can build a wall in 10 days, how many days would it take 10 workers?", "answer": "5", "type": "word_problem", "difficulty": "medium"},
|
| 65 |
+
{"q": "A rectangle has a perimeter of 30 cm and a width of 5 cm. What is its length?", "answer": "10", "type": "word_problem", "difficulty": "medium"},
|
| 66 |
+
{"q": "I have 3 times as many cats as dogs. I have 2 dogs. How many animals do I have in total?", "answer": "8", "type": "word_problem", "difficulty": "easy"},
|
| 67 |
+
{"q": "A car uses 8 liters of fuel per 100km. How many liters does it need for a 350km trip?", "answer": "28", "type": "word_problem", "difficulty": "medium"},
|
| 68 |
+
|
| 69 |
+
# Harder math
|
| 70 |
+
{"q": "What is the sum of the first 10 positive integers?", "answer": "55", "type": "series", "difficulty": "medium"},
|
| 71 |
+
{"q": "What is 2^10?", "answer": "1024", "type": "exponents", "difficulty": "medium"},
|
| 72 |
+
{"q": "What is the greatest common divisor of 48 and 36?", "answer": "12", "type": "number_theory", "difficulty": "medium"},
|
| 73 |
+
{"q": "If a triangle has sides of length 3, 4, and 5, what is its area?", "answer": "6", "type": "geometry", "difficulty": "medium"},
|
| 74 |
+
{"q": "What is the square root of 169?", "answer": "13", "type": "roots", "difficulty": "easy"},
|
| 75 |
+
{"q": "A coin is flipped 3 times. How many possible outcomes are there?", "answer": "8", "type": "probability", "difficulty": "medium"},
|
| 76 |
+
{"q": "What is 7! (7 factorial)?", "answer": "5040", "type": "factorial", "difficulty": "hard"},
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
CODE_TESTS = [
|
| 80 |
+
# Write functions
|
| 81 |
+
{"q": "Write a Python function called 'is_even' that returns True if a number is even, False otherwise. Just give the function, no explanation.",
|
| 82 |
+
"check": "is_even", "verify": "callable", "type": "write", "difficulty": "easy"},
|
| 83 |
+
{"q": "Write a Python function called 'factorial' that computes the factorial of a non-negative integer. Just give the function.",
|
| 84 |
+
"check": "factorial", "verify": "callable", "type": "write", "difficulty": "medium"},
|
| 85 |
+
{"q": "Write a Python function called 'reverse_string' that takes a string and returns it reversed. Just give the function.",
|
| 86 |
+
"check": "reverse_string", "verify": "callable", "type": "write", "difficulty": "easy"},
|
| 87 |
+
{"q": "Write a Python function called 'max_of_three' that takes three numbers and returns the largest. Just give the function.",
|
| 88 |
+
"check": "max_of_three", "verify": "callable", "type": "write", "difficulty": "easy"},
|
| 89 |
+
{"q": "Write a Python function called 'count_vowels' that counts the number of vowels (a,e,i,o,u) in a string. Just give the function.",
|
| 90 |
+
"check": "count_vowels", "verify": "callable", "type": "write", "difficulty": "medium"},
|
| 91 |
+
{"q": "Write a Python function called 'fibonacci' that returns the nth Fibonacci number (0-indexed, so fibonacci(0)=0, fibonacci(1)=1). Just give the function.",
|
| 92 |
+
"check": "fibonacci", "verify": "callable", "type": "write", "difficulty": "medium"},
|
| 93 |
+
{"q": "Write a Python function called 'is_palindrome' that returns True if a string reads the same forwards and backwards. Just give the function.",
|
| 94 |
+
"check": "is_palindrome", "verify": "callable", "type": "write", "difficulty": "medium"},
|
| 95 |
+
{"q": "Write a Python function called 'flatten' that takes a list of lists and returns a single flat list. Example: flatten([[1,2],[3,4]]) returns [1,2,3,4]. Just give the function.",
|
| 96 |
+
"check": "flatten", "verify": "callable", "type": "write", "difficulty": "medium"},
|
| 97 |
+
|
| 98 |
+
# Debug
|
| 99 |
+
{"q": "This Python code has a bug. Fix it and give ONLY the corrected code:\ndef add_list(lst):\n total = 0\n for i in range(len(lst) + 1):\n total += lst[i]\n return total",
|
| 100 |
+
"check": "range(len(lst))", "verify": "contains", "type": "debug", "difficulty": "easy"},
|
| 101 |
+
{"q": "This Python code has a bug. Fix it and give ONLY the corrected code:\ndef greet(name):\n return 'Hello' + name",
|
| 102 |
+
"check": ["Hello ' + name", "Hello \" + name", "'Hello ' +", "'Hello, '", "f'Hello {", "f\"Hello {", "Hello \" +"], "verify": "contains_any", "type": "debug", "difficulty": "easy"},
|
| 103 |
+
|
| 104 |
+
# Explain
|
| 105 |
+
{"q": "In one sentence, what does this Python code do?\n[x**2 for x in range(10) if x % 2 == 0]",
|
| 106 |
+
"check": "square", "verify": "contains", "type": "explain", "difficulty": "easy"},
|
| 107 |
+
{"q": "In one sentence, what does this Python code do?\nlen(set(my_list))",
|
| 108 |
+
"check": "unique", "verify": "contains", "type": "explain", "difficulty": "easy"},
|
| 109 |
+
]
|
| 110 |
+
|
| 111 |
+
REASONING_TESTS = [
|
| 112 |
+
# Logic
|
| 113 |
+
{"q": "If all roses are flowers and all flowers need water, do roses need water? Answer yes or no.",
|
| 114 |
+
"answer": "yes", "type": "syllogism", "difficulty": "easy"},
|
| 115 |
+
{"q": "If it's raining, the ground is wet. The ground is wet. Is it definitely raining? Answer yes or no.",
|
| 116 |
+
"answer": "no", "type": "logic_fallacy", "difficulty": "medium"},
|
| 117 |
+
{"q": "All cats are animals. Some animals are pets. Can we conclude all cats are pets? Answer yes or no.",
|
| 118 |
+
"answer": "no", "type": "syllogism", "difficulty": "medium"},
|
| 119 |
+
{"q": "If A is taller than B, and B is taller than C, who is the shortest?",
|
| 120 |
+
"answer": "c", "type": "transitive", "difficulty": "easy"},
|
| 121 |
+
{"q": "If no fish can fly, and all salmon are fish, can salmon fly? Answer yes or no.",
|
| 122 |
+
"answer": "no", "type": "syllogism", "difficulty": "easy"},
|
| 123 |
+
|
| 124 |
+
# Pattern recognition
|
| 125 |
+
{"q": "What comes next: 2, 4, 8, 16, ?", "answer": "32", "type": "pattern", "difficulty": "easy"},
|
| 126 |
+
{"q": "What comes next: 1, 1, 2, 3, 5, 8, ?", "answer": "13", "type": "pattern", "difficulty": "medium"},
|
| 127 |
+
{"q": "What comes next: 3, 6, 9, 12, ?", "answer": "15", "type": "pattern", "difficulty": "easy"},
|
| 128 |
+
{"q": "What comes next: 1, 4, 9, 16, 25, ?", "answer": "36", "type": "pattern", "difficulty": "medium"},
|
| 129 |
+
|
| 130 |
+
# Multi-step reasoning
|
| 131 |
+
{"q": "There are 3 boxes. Box A is heavier than Box B. Box C is lighter than Box B. Which box is the heaviest?",
|
| 132 |
+
"answer": "a", "type": "multi_step", "difficulty": "easy"},
|
| 133 |
+
{"q": "Tom is twice as old as Jerry. Jerry is 3 years older than Spike. Spike is 4 years old. How old is Tom?",
|
| 134 |
+
"answer": "14", "type": "multi_step", "difficulty": "medium"},
|
| 135 |
+
{"q": "A snail climbs 3 meters during the day but slips back 2 meters at night. How many days does it take to climb a 10 meter wall?",
|
| 136 |
+
"answer": "8", "type": "multi_step", "difficulty": "hard"},
|
| 137 |
+
{"q": "In a race, you overtake the person in 2nd place. What position are you now in?",
|
| 138 |
+
"answer": "2", "type": "trick", "difficulty": "medium"},
|
| 139 |
+
|
| 140 |
+
# Common sense
|
| 141 |
+
{"q": "If I put a ice cube in a hot pan, what happens to it?",
|
| 142 |
+
"answer": "melt", "type": "common_sense", "difficulty": "easy"},
|
| 143 |
+
{"q": "Can a dead person eat lunch? Answer yes or no.",
|
| 144 |
+
"answer": "no", "type": "common_sense", "difficulty": "easy"},
|
| 145 |
+
]
|
| 146 |
+
|
| 147 |
+
CREATIVITY_TESTS = [
|
| 148 |
+
# These are scored differently — we check for length, variety, and coherence
|
| 149 |
+
{"q": "Write a 4-line poem about the ocean.",
|
| 150 |
+
"min_lines": 3, "min_words": 15, "type": "poem", "difficulty": "easy"},
|
| 151 |
+
{"q": "Describe a sunset to someone who has never seen one, in 2-3 sentences.",
|
| 152 |
+
"min_lines": 1, "min_words": 20, "type": "description", "difficulty": "easy"},
|
| 153 |
+
{"q": "Come up with 3 creative names for a coffee shop run by robots.",
|
| 154 |
+
"min_items": 3, "type": "brainstorm", "difficulty": "easy"},
|
| 155 |
+
{"q": "Write a very short story (3-5 sentences) about a cat who learns to fly.",
|
| 156 |
+
"min_lines": 2, "min_words": 25, "type": "story", "difficulty": "medium"},
|
| 157 |
+
{"q": "Explain how a refrigerator works to a 5 year old, in 2-3 simple sentences.",
|
| 158 |
+
"min_lines": 1, "min_words": 15, "type": "explain_simple", "difficulty": "easy"},
|
| 159 |
+
{"q": "Rewrite this sentence to make it more exciting: 'The man walked to the store.'",
|
| 160 |
+
"min_words": 5, "type": "rewrite", "difficulty": "easy",
|
| 161 |
+
"must_not_contain": "The man walked to the store"},
|
| 162 |
+
{"q": "Give me an analogy that explains what a CPU does in a computer.",
|
| 163 |
+
"min_words": 10, "type": "analogy", "difficulty": "medium"},
|
| 164 |
+
{"q": "Write a haiku about programming (5-7-5 syllable pattern).",
|
| 165 |
+
"min_lines": 3, "min_words": 8, "type": "poem", "difficulty": "medium"},
|
| 166 |
+
]
|
| 167 |
+
|
| 168 |
+
KNOWLEDGE_TESTS = [
|
| 169 |
+
# Science
|
| 170 |
+
{"q": "What planet is closest to the Sun?", "answer": "mercury", "type": "science", "difficulty": "easy"},
|
| 171 |
+
{"q": "What is the chemical symbol for water?", "answer": "h2o", "type": "science", "difficulty": "easy"},
|
| 172 |
+
{"q": "What gas do plants absorb from the atmosphere?", "answer": "carbon dioxide", "type": "science", "difficulty": "easy",
|
| 173 |
+
"alt_answers": ["co2"]},
|
| 174 |
+
{"q": "How many chromosomes do humans have?", "answer": "46", "type": "science", "difficulty": "medium"},
|
| 175 |
+
{"q": "What is the speed of light in km/s approximately?", "answer": "300000", "type": "science", "difficulty": "medium",
|
| 176 |
+
"alt_answers": ["300,000", "3×10^8", "3x10^8", "299792"]},
|
| 177 |
+
|
| 178 |
+
# History
|
| 179 |
+
{"q": "In what year did World War 2 end?", "answer": "1945", "type": "history", "difficulty": "easy"},
|
| 180 |
+
{"q": "Who was the first person to walk on the Moon?", "answer": "armstrong", "type": "history", "difficulty": "easy",
|
| 181 |
+
"alt_answers": ["neil armstrong"]},
|
| 182 |
+
{"q": "What country built the Great Wall?", "answer": "china", "type": "history", "difficulty": "easy"},
|
| 183 |
+
|
| 184 |
+
# Geography
|
| 185 |
+
{"q": "What is the largest ocean on Earth?", "answer": "pacific", "type": "geography", "difficulty": "easy"},
|
| 186 |
+
{"q": "What is the capital of Japan?", "answer": "tokyo", "type": "geography", "difficulty": "easy"},
|
| 187 |
+
{"q": "What is the longest river in the world?", "answer": "nile", "type": "geography", "difficulty": "medium",
|
| 188 |
+
"alt_answers": ["amazon"]}, # Both are accepted — depends on measurement method
|
| 189 |
+
{"q": "How many continents are there?", "answer": "7", "type": "geography", "difficulty": "easy"},
|
| 190 |
+
|
| 191 |
+
# General
|
| 192 |
+
{"q": "What programming language is known for its use in web browsers?", "answer": "javascript", "type": "tech", "difficulty": "easy",
|
| 193 |
+
"alt_answers": ["js"]},
|
| 194 |
+
{"q": "What does CPU stand for?", "answer": "central processing unit", "type": "tech", "difficulty": "easy"},
|
| 195 |
+
{"q": "What does DNA stand for?", "answer": "deoxyribonucleic acid", "type": "science", "difficulty": "medium"},
|
| 196 |
+
]
|
| 197 |
+
|
| 198 |
+
INSTRUCTION_TESTS = [
|
| 199 |
+
# Follow exact instructions
|
| 200 |
+
{"q": "Respond with only the word 'hello'. Nothing else.",
|
| 201 |
+
"check": "hello", "verify": "exact_word", "type": "exact", "difficulty": "easy"},
|
| 202 |
+
{"q": "List exactly 3 colors, one per line. Nothing else.",
|
| 203 |
+
"check": 3, "verify": "line_count", "type": "format", "difficulty": "easy"},
|
| 204 |
+
{"q": "Write a sentence that contains exactly 5 words.",
|
| 205 |
+
"check": 5, "verify": "word_count", "type": "format", "difficulty": "medium"},
|
| 206 |
+
{"q": "Give me a number between 1 and 10. Just the number, nothing else.",
|
| 207 |
+
"check": "number_1_10", "verify": "number_range", "type": "exact", "difficulty": "easy"},
|
| 208 |
+
{"q": "Repeat the following sentence exactly: 'The quick brown fox jumps over the lazy dog.'",
|
| 209 |
+
"check": "the quick brown fox jumps over the lazy dog", "verify": "contains_exact", "type": "repeat", "difficulty": "easy"},
|
| 210 |
+
{"q": "Name 5 fruits. Number them 1-5.",
|
| 211 |
+
"check": 5, "verify": "numbered_items", "type": "format", "difficulty": "easy"},
|
| 212 |
+
{"q": "Answer this question with exactly one word: What color is the sky on a clear day?",
|
| 213 |
+
"check": 1, "verify": "word_count", "type": "exact", "difficulty": "medium"},
|
| 214 |
+
{"q": "Write the alphabet in reverse (Z to A) with commas between each letter.",
|
| 215 |
+
"check": "z", "verify": "starts_with", "type": "exact", "difficulty": "hard"},
|
| 216 |
+
{"q": "Respond with ONLY a JSON object with keys 'name' and 'age'. Use any values.",
|
| 217 |
+
"check": "{", "verify": "starts_with_json", "type": "format", "difficulty": "medium"},
|
| 218 |
+
{"q": "Say 'yes' if 2+2=4, otherwise say 'no'. Only one word.",
|
| 219 |
+
"check": "yes", "verify": "exact_word", "type": "exact", "difficulty": "easy"},
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
# ============================================================
|
| 224 |
+
# ANSWER CHECKING
|
| 225 |
+
# ============================================================
|
| 226 |
+
|
| 227 |
+
def strip_thinking(response: str) -> str:
|
| 228 |
+
"""
|
| 229 |
+
BUG FIX: Qwen3-VL wraps reasoning in <think>...</think> tags.
|
| 230 |
+
These aren't special tokens so they stay in the decoded output.
|
| 231 |
+
We need to strip them before checking answers, otherwise:
|
| 232 |
+
- instruction checks see "<think>" as the first word
|
| 233 |
+
- code exec tries to run thinking text as Python
|
| 234 |
+
- exact match checks fail because of extra text
|
| 235 |
+
"""
|
| 236 |
+
# Remove <think>...</think> blocks
|
| 237 |
+
cleaned = re.sub(r'<think>.*?</think>', '', response, flags=re.DOTALL)
|
| 238 |
+
# Also handle unclosed think tags (model didn't close it)
|
| 239 |
+
cleaned = re.sub(r'<think>.*$', '', cleaned, flags=re.DOTALL)
|
| 240 |
+
return cleaned.strip()
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def check_math_answer(response: str, expected: str) -> bool:
|
| 244 |
+
"""Check if the response contains the correct number."""
|
| 245 |
+
response_clean = strip_thinking(response).lower().strip()
|
| 246 |
+
# Strip commas from numbers (model might say "1,081" for 1081)
|
| 247 |
+
response_no_commas = response_clean.replace(",", "")
|
| 248 |
+
# Extract numbers from response
|
| 249 |
+
numbers = re.findall(r'-?\d+\.?\d*', response_no_commas)
|
| 250 |
+
|
| 251 |
+
# Also check for fraction answers
|
| 252 |
+
if "/" in expected:
|
| 253 |
+
if expected.lower() in response_clean:
|
| 254 |
+
return True
|
| 255 |
+
# Check decimal equivalent
|
| 256 |
+
parts = expected.split("/")
|
| 257 |
+
if len(parts) == 2:
|
| 258 |
+
try:
|
| 259 |
+
decimal_val = str(round(int(parts[0]) / int(parts[1]), 4))
|
| 260 |
+
if decimal_val in response_no_commas:
|
| 261 |
+
return True
|
| 262 |
+
except (ValueError, ZeroDivisionError):
|
| 263 |
+
pass
|
| 264 |
+
|
| 265 |
+
# BUG FIX: Also match "12" to "12.0" (correct but different format)
|
| 266 |
+
for num_str in numbers:
|
| 267 |
+
try:
|
| 268 |
+
if float(num_str) == float(expected):
|
| 269 |
+
return True
|
| 270 |
+
except ValueError:
|
| 271 |
+
continue
|
| 272 |
+
|
| 273 |
+
return expected in numbers
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def check_knowledge_answer(response: str, test: dict) -> bool:
|
| 277 |
+
"""Check if response contains the expected answer or alternatives."""
|
| 278 |
+
response_lower = strip_thinking(response).lower().strip()
|
| 279 |
+
if test["answer"].lower() in response_lower:
|
| 280 |
+
return True
|
| 281 |
+
for alt in test.get("alt_answers", []):
|
| 282 |
+
if alt.lower() in response_lower:
|
| 283 |
+
return True
|
| 284 |
+
return False
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def check_reasoning_answer(response: str, expected: str) -> bool:
|
| 288 |
+
"""Check reasoning answer — flexible matching."""
|
| 289 |
+
response_lower = strip_thinking(response).lower().strip()
|
| 290 |
+
expected_lower = expected.lower()
|
| 291 |
+
|
| 292 |
+
# Direct match
|
| 293 |
+
if expected_lower in response_lower:
|
| 294 |
+
return True
|
| 295 |
+
|
| 296 |
+
# For yes/no questions
|
| 297 |
+
if expected_lower in ["yes", "no"]:
|
| 298 |
+
# Check first word
|
| 299 |
+
first_word = response_lower.split()[0] if response_lower.split() else ""
|
| 300 |
+
first_word = first_word.strip(".,!?")
|
| 301 |
+
return first_word == expected_lower
|
| 302 |
+
|
| 303 |
+
# For number answers
|
| 304 |
+
numbers = re.findall(r'-?\d+\.?\d*', response_lower)
|
| 305 |
+
if expected_lower in numbers:
|
| 306 |
+
return True
|
| 307 |
+
|
| 308 |
+
return False
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def check_code_answer(response: str, test: dict) -> bool:
|
| 312 |
+
"""Check code answers — verify function exists and/or contains expected code."""
|
| 313 |
+
verify = test["verify"]
|
| 314 |
+
check = test["check"]
|
| 315 |
+
response = strip_thinking(response)
|
| 316 |
+
|
| 317 |
+
if verify == "callable":
|
| 318 |
+
# Try to extract and compile the function
|
| 319 |
+
code_block = response
|
| 320 |
+
# Extract from markdown code block if present
|
| 321 |
+
md_match = re.search(r'```(?:python)?\s*\n(.*?)```', response, re.DOTALL)
|
| 322 |
+
if md_match:
|
| 323 |
+
code_block = md_match.group(1)
|
| 324 |
+
|
| 325 |
+
# BUG FIX: If no code block found, try to extract just the def...
|
| 326 |
+
if not md_match and "def " in code_block:
|
| 327 |
+
# Find from first 'def' to end — skip any explanation text before it
|
| 328 |
+
def_start = code_block.index("def ")
|
| 329 |
+
code_block = code_block[def_start:]
|
| 330 |
+
|
| 331 |
+
try:
|
| 332 |
+
import signal
|
| 333 |
+
|
| 334 |
+
def _timeout_handler(signum, frame):
|
| 335 |
+
raise TimeoutError("Code execution timed out")
|
| 336 |
+
|
| 337 |
+
old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
|
| 338 |
+
# BUG FIX: Set alarm BEFORE exec(), not after!
|
| 339 |
+
# If model writes `while True: pass` at module level, exec()
|
| 340 |
+
# hangs forever and the alarm never gets set.
|
| 341 |
+
signal.alarm(10) # 10 seconds for compile+exec+tests
|
| 342 |
+
|
| 343 |
+
try:
|
| 344 |
+
namespace = {}
|
| 345 |
+
exec(code_block.strip(), namespace)
|
| 346 |
+
func = namespace.get(check)
|
| 347 |
+
if func is None or not callable(func):
|
| 348 |
+
return False
|
| 349 |
+
|
| 350 |
+
# Run basic tests based on function name
|
| 351 |
+
if check == "is_even":
|
| 352 |
+
result = func(4) == True and func(3) == False
|
| 353 |
+
elif check == "factorial":
|
| 354 |
+
result = func(5) == 120 and func(0) == 1
|
| 355 |
+
elif check == "reverse_string":
|
| 356 |
+
result = func("hello") == "olleh"
|
| 357 |
+
elif check == "max_of_three":
|
| 358 |
+
result = func(1, 5, 3) == 5
|
| 359 |
+
elif check == "count_vowels":
|
| 360 |
+
result = func("hello") == 2
|
| 361 |
+
elif check == "fibonacci":
|
| 362 |
+
result = func(0) == 0 and func(1) == 1 and func(6) == 8
|
| 363 |
+
elif check == "is_palindrome":
|
| 364 |
+
result = func("racecar") == True and func("hello") == False
|
| 365 |
+
elif check == "flatten":
|
| 366 |
+
result = func([[1, 2], [3, 4]]) == [1, 2, 3, 4]
|
| 367 |
+
else:
|
| 368 |
+
result = True
|
| 369 |
+
return result
|
| 370 |
+
finally:
|
| 371 |
+
signal.alarm(0)
|
| 372 |
+
signal.signal(signal.SIGALRM, old_handler)
|
| 373 |
+
except (Exception, TimeoutError):
|
| 374 |
+
return False
|
| 375 |
+
|
| 376 |
+
elif verify == "contains":
|
| 377 |
+
return check.lower() in response.lower()
|
| 378 |
+
|
| 379 |
+
elif verify == "contains_any":
|
| 380 |
+
return any(c.lower() in response.lower() for c in check)
|
| 381 |
+
|
| 382 |
+
return False
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def check_creativity_answer(response: str, test: dict) -> bool:
|
| 386 |
+
"""Check creativity — basic quality checks."""
|
| 387 |
+
response = strip_thinking(response)
|
| 388 |
+
lines = [l.strip() for l in response.strip().split("\n") if l.strip()]
|
| 389 |
+
words = response.split()
|
| 390 |
+
|
| 391 |
+
# Must not contain forbidden text (for rewrite tasks)
|
| 392 |
+
if "must_not_contain" in test:
|
| 393 |
+
if test["must_not_contain"].lower() in response.lower():
|
| 394 |
+
return False
|
| 395 |
+
|
| 396 |
+
# Check minimum lines
|
| 397 |
+
if "min_lines" in test and len(lines) < test["min_lines"]:
|
| 398 |
+
return False
|
| 399 |
+
|
| 400 |
+
# Check minimum words
|
| 401 |
+
if "min_words" in test and len(words) < test["min_words"]:
|
| 402 |
+
return False
|
| 403 |
+
|
| 404 |
+
# Check minimum items (for brainstorm)
|
| 405 |
+
if "min_items" in test:
|
| 406 |
+
# Count numbered items or bullet points
|
| 407 |
+
items = [l for l in lines if re.match(r'^\d+[\.\):]|^[-*•]', l)]
|
| 408 |
+
if len(items) < test["min_items"]:
|
| 409 |
+
# Also count plain lines as items
|
| 410 |
+
if len(lines) < test["min_items"]:
|
| 411 |
+
return False
|
| 412 |
+
|
| 413 |
+
# Basic coherence — not just random characters
|
| 414 |
+
if len(response.strip()) < 10:
|
| 415 |
+
return False
|
| 416 |
+
|
| 417 |
+
return True
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def check_instruction_answer(response: str, test: dict) -> bool:
|
| 421 |
+
"""Check instruction following — strict format checks."""
|
| 422 |
+
verify = test["verify"]
|
| 423 |
+
check = test["check"]
|
| 424 |
+
response_clean = strip_thinking(response).strip()
|
| 425 |
+
response_lower = response_clean.lower()
|
| 426 |
+
|
| 427 |
+
if verify == "exact_word":
|
| 428 |
+
# Should be just this word (maybe with punctuation)
|
| 429 |
+
first_word = response_lower.split()[0] if response_lower.split() else ""
|
| 430 |
+
first_word = re.sub(r'[.,!?"\']', '', first_word)
|
| 431 |
+
return first_word == check.lower()
|
| 432 |
+
|
| 433 |
+
elif verify == "line_count":
|
| 434 |
+
lines = [l.strip() for l in response_clean.split("\n") if l.strip()]
|
| 435 |
+
return len(lines) == check
|
| 436 |
+
|
| 437 |
+
elif verify == "word_count":
|
| 438 |
+
# Find the main sentence (skip thinking/explanation)
|
| 439 |
+
# Look for a clean sentence
|
| 440 |
+
lines = [l.strip() for l in response_clean.split("\n") if l.strip()]
|
| 441 |
+
for line in lines:
|
| 442 |
+
# Skip lines that look like meta-commentary
|
| 443 |
+
if any(skip in line.lower() for skip in ["here is", "here's", "sure", "okay"]):
|
| 444 |
+
continue
|
| 445 |
+
words = line.split()
|
| 446 |
+
if len(words) == check:
|
| 447 |
+
return True
|
| 448 |
+
# Also check total
|
| 449 |
+
return len(response_clean.split()) == check
|
| 450 |
+
|
| 451 |
+
elif verify == "number_range":
|
| 452 |
+
numbers = re.findall(r'\d+', response_clean)
|
| 453 |
+
if numbers:
|
| 454 |
+
n = int(numbers[0])
|
| 455 |
+
return 1 <= n <= 10
|
| 456 |
+
return False
|
| 457 |
+
|
| 458 |
+
elif verify == "contains_exact":
|
| 459 |
+
return check.lower() in response_lower
|
| 460 |
+
|
| 461 |
+
elif verify == "numbered_items":
|
| 462 |
+
items = re.findall(r'^\d+[\.\):]', response_clean, re.MULTILINE)
|
| 463 |
+
return len(items) >= check
|
| 464 |
+
|
| 465 |
+
elif verify == "starts_with":
|
| 466 |
+
return response_lower.startswith(check.lower())
|
| 467 |
+
|
| 468 |
+
elif verify == "starts_with_json":
|
| 469 |
+
stripped = response_clean.lstrip()
|
| 470 |
+
# May be in a code block
|
| 471 |
+
if "```" in stripped:
|
| 472 |
+
md_match = re.search(r'```(?:json)?\s*\n(.*?)```', stripped, re.DOTALL)
|
| 473 |
+
if md_match:
|
| 474 |
+
stripped = md_match.group(1).strip()
|
| 475 |
+
try:
|
| 476 |
+
obj = json.loads(stripped)
|
| 477 |
+
return "name" in obj and "age" in obj
|
| 478 |
+
except (json.JSONDecodeError, TypeError):
|
| 479 |
+
return stripped.startswith("{")
|
| 480 |
+
|
| 481 |
+
return False
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
# ============================================================
|
| 485 |
+
# MODEL RUNNER
|
| 486 |
+
# ============================================================
|
| 487 |
+
|
| 488 |
+
def load_model(model_path: str):
|
| 489 |
+
"""Load model and tokenizer. Validates checkpoint integrity first."""
|
| 490 |
+
from transformers import AutoModelForImageTextToText, AutoTokenizer
|
| 491 |
+
from pathlib import Path
|
| 492 |
+
|
| 493 |
+
# Validate checkpoint before loading — catch stale/corrupt checkpoints
|
| 494 |
+
p = Path(model_path)
|
| 495 |
+
if p.is_dir():
|
| 496 |
+
config_file = p / "config.json"
|
| 497 |
+
if not config_file.exists():
|
| 498 |
+
raise FileNotFoundError(f"No config.json in {model_path} — stale or corrupt checkpoint!")
|
| 499 |
+
st_files = list(p.glob("*.safetensors"))
|
| 500 |
+
if not st_files and not list(p.glob("*.bin")):
|
| 501 |
+
raise FileNotFoundError(f"No model weights in {model_path} — checkpoint is empty!")
|
| 502 |
+
total_size = sum(f.stat().st_size for f in st_files) / 1e9 if st_files else 0
|
| 503 |
+
if st_files and total_size < 0.5:
|
| 504 |
+
raise ValueError(f"Model weights only {total_size:.2f} GB — likely corrupt checkpoint!")
|
| 505 |
+
|
| 506 |
+
print(f" Loading model from {model_path}...")
|
| 507 |
+
try:
|
| 508 |
+
model = AutoModelForImageTextToText.from_pretrained(
|
| 509 |
+
model_path, dtype=torch.bfloat16,
|
| 510 |
+
device_map="auto", trust_remote_code=True
|
| 511 |
+
)
|
| 512 |
+
except (RuntimeError, torch.cuda.OutOfMemoryError) as e:
|
| 513 |
+
gc.collect()
|
| 514 |
+
if torch.cuda.is_available():
|
| 515 |
+
torch.cuda.empty_cache()
|
| 516 |
+
raise RuntimeError(f"Failed to load model (OOM or corrupt): {e}") from e
|
| 517 |
+
|
| 518 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 519 |
+
return model, tokenizer
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def ask_model(model, tokenizer, question: str, max_tokens: int = 256,
|
| 523 |
+
disable_thinking: bool = True) -> str:
|
| 524 |
+
"""Ask the model a question and get the response.
|
| 525 |
+
|
| 526 |
+
SPEED: disable_thinking=True adds /no_think to skip <think> blocks.
|
| 527 |
+
Qwen3-VL's thinking mode generates 100-500+ tokens of reasoning
|
| 528 |
+
before giving a 1-word answer. For benchmarks this wastes ~80% of
|
| 529 |
+
inference time. /no_think makes it answer directly.
|
| 530 |
+
"""
|
| 531 |
+
# Build prompt — /no_think goes in the system message
|
| 532 |
+
if disable_thinking:
|
| 533 |
+
chat = (
|
| 534 |
+
"<|im_start|>system\n/no_think<|im_end|>\n"
|
| 535 |
+
f"<|im_start|>user\n{question}<|im_end|>\n"
|
| 536 |
+
"<|im_start|>assistant\n"
|
| 537 |
+
)
|
| 538 |
+
else:
|
| 539 |
+
chat = f"<|im_start|>user\n{question}<|im_end|>\n<|im_start|>assistant\n"
|
| 540 |
+
|
| 541 |
+
ids = tokenizer(chat, return_tensors="pt").to(model.device)
|
| 542 |
+
|
| 543 |
+
try:
|
| 544 |
+
with torch.no_grad():
|
| 545 |
+
out = model.generate(
|
| 546 |
+
**ids,
|
| 547 |
+
max_new_tokens=max_tokens,
|
| 548 |
+
do_sample=False,
|
| 549 |
+
temperature=None,
|
| 550 |
+
top_p=None,
|
| 551 |
+
)
|
| 552 |
+
except (torch.cuda.OutOfMemoryError, RuntimeError) as e:
|
| 553 |
+
# OOM during generation — clean up and return empty
|
| 554 |
+
gc.collect()
|
| 555 |
+
if torch.cuda.is_available():
|
| 556 |
+
torch.cuda.empty_cache()
|
| 557 |
+
print(f" OOM during generation: {e}")
|
| 558 |
+
return "[OOM — no response generated]"
|
| 559 |
+
|
| 560 |
+
response = tokenizer.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True)
|
| 561 |
+
|
| 562 |
+
# Guard against garbage output: if response is absurdly long, truncate
|
| 563 |
+
if len(response) > 5000:
|
| 564 |
+
response = response[:5000]
|
| 565 |
+
|
| 566 |
+
return response.strip()
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
# ============================================================
|
| 570 |
+
# RUN BENCHMARK
|
| 571 |
+
# ============================================================
|
| 572 |
+
|
| 573 |
+
def run_category(model, tokenizer, category_name: str, tests: list,
|
| 574 |
+
check_fn, cfg: BenchmarkConfig) -> Dict:
|
| 575 |
+
"""Run all tests in a category and return results."""
|
| 576 |
+
print(f"\n === {category_name.upper()} ({len(tests)} questions) ===")
|
| 577 |
+
|
| 578 |
+
results = []
|
| 579 |
+
passed = 0
|
| 580 |
+
failed_questions = []
|
| 581 |
+
|
| 582 |
+
for i, test in enumerate(tests):
|
| 583 |
+
q = test["q"]
|
| 584 |
+
q_start = time.time()
|
| 585 |
+
|
| 586 |
+
try:
|
| 587 |
+
response = ask_model(model, tokenizer, q, cfg.max_new_tokens,
|
| 588 |
+
disable_thinking=cfg.disable_thinking)
|
| 589 |
+
except Exception as e:
|
| 590 |
+
print(f" [ERROR] Question {i+1} crashed: {e}")
|
| 591 |
+
response = "[ERROR — question crashed]"
|
| 592 |
+
gc.collect()
|
| 593 |
+
if torch.cuda.is_available():
|
| 594 |
+
torch.cuda.empty_cache()
|
| 595 |
+
|
| 596 |
+
q_time = time.time() - q_start
|
| 597 |
+
|
| 598 |
+
try:
|
| 599 |
+
if check_fn == check_code_answer:
|
| 600 |
+
correct = check_fn(response, test)
|
| 601 |
+
elif check_fn == check_creativity_answer:
|
| 602 |
+
correct = check_fn(response, test)
|
| 603 |
+
elif check_fn == check_instruction_answer:
|
| 604 |
+
correct = check_fn(response, test)
|
| 605 |
+
elif check_fn == check_knowledge_answer:
|
| 606 |
+
correct = check_fn(response, test)
|
| 607 |
+
elif check_fn == check_reasoning_answer:
|
| 608 |
+
correct = check_fn(response, test.get("answer", ""))
|
| 609 |
+
else:
|
| 610 |
+
# Math
|
| 611 |
+
correct = check_fn(response, test.get("answer", ""))
|
| 612 |
+
except Exception as e:
|
| 613 |
+
print(f" [ERROR] Answer checker crashed on question {i+1}: {e}")
|
| 614 |
+
correct = False
|
| 615 |
+
|
| 616 |
+
status = "PASS" if correct else "FAIL"
|
| 617 |
+
if correct:
|
| 618 |
+
passed += 1
|
| 619 |
+
else:
|
| 620 |
+
# Store the clean response (without thinking) for the failure report
|
| 621 |
+
clean_response = strip_thinking(response)
|
| 622 |
+
failed_questions.append({
|
| 623 |
+
"question": q,
|
| 624 |
+
"response": clean_response[:300],
|
| 625 |
+
"expected": test.get("answer", test.get("check", "N/A")),
|
| 626 |
+
"type": test.get("type", "unknown"),
|
| 627 |
+
"difficulty": test.get("difficulty", "unknown"),
|
| 628 |
+
})
|
| 629 |
+
|
| 630 |
+
# Print compact result
|
| 631 |
+
short_q = q[:60] + "..." if len(q) > 60 else q
|
| 632 |
+
print(f" [{status}] {short_q} ({q_time:.1f}s)")
|
| 633 |
+
if not correct:
|
| 634 |
+
clean_r = strip_thinking(response)
|
| 635 |
+
short_r = clean_r[:80] + "..." if len(clean_r) > 80 else clean_r
|
| 636 |
+
print(f" Got: {short_r}")
|
| 637 |
+
|
| 638 |
+
results.append({
|
| 639 |
+
"question": q,
|
| 640 |
+
"correct": correct,
|
| 641 |
+
"type": test.get("type", "unknown"),
|
| 642 |
+
"difficulty": test.get("difficulty", "unknown"),
|
| 643 |
+
})
|
| 644 |
+
|
| 645 |
+
total = len(tests)
|
| 646 |
+
# CRITICAL FIX: Score as 0.0-1.0 (NOT 0-100) so td_weakness.py targets work
|
| 647 |
+
score = passed / total if total > 0 else 0.0
|
| 648 |
+
|
| 649 |
+
print(f"\n SCORE: {passed}/{total} ({score:.0%})")
|
| 650 |
+
|
| 651 |
+
# Per-type breakdown
|
| 652 |
+
type_scores = {}
|
| 653 |
+
for r in results:
|
| 654 |
+
t = r["type"]
|
| 655 |
+
if t not in type_scores:
|
| 656 |
+
type_scores[t] = {"correct": 0, "total": 0}
|
| 657 |
+
type_scores[t]["total"] += 1
|
| 658 |
+
if r["correct"]:
|
| 659 |
+
type_scores[t]["correct"] += 1
|
| 660 |
+
|
| 661 |
+
for t, s in sorted(type_scores.items()):
|
| 662 |
+
pct = s["correct"] / s["total"] if s["total"] > 0 else 0.0
|
| 663 |
+
print(f" {t}: {s['correct']}/{s['total']} ({pct:.0%})")
|
| 664 |
+
|
| 665 |
+
return {
|
| 666 |
+
"category": category_name,
|
| 667 |
+
"score": score, # 0.0-1.0 scale
|
| 668 |
+
"passed": passed,
|
| 669 |
+
"total": total,
|
| 670 |
+
"type_scores": {t: {"correct": s["correct"], "total": s["total"],
|
| 671 |
+
"pct": s["correct"] / s["total"] if s["total"] > 0 else 0.0}
|
| 672 |
+
for t, s in type_scores.items()},
|
| 673 |
+
"failed_questions": failed_questions,
|
| 674 |
+
"results": results,
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
def run_benchmark(cfg: BenchmarkConfig = None) -> Dict:
|
| 679 |
+
"""Run the full benchmark suite."""
|
| 680 |
+
if cfg is None:
|
| 681 |
+
cfg = BenchmarkConfig()
|
| 682 |
+
|
| 683 |
+
# Auto-detect model
|
| 684 |
+
if not cfg.model_path:
|
| 685 |
+
from pathlib import Path
|
| 686 |
+
base = Path("td_fuse_outputs/self_improve")
|
| 687 |
+
if base.exists():
|
| 688 |
+
for n in range(50, 0, -1):
|
| 689 |
+
d = base / f"improved_cycle{n}"
|
| 690 |
+
if d.exists() and list(d.glob("*.safetensors")):
|
| 691 |
+
cfg.model_path = str(d)
|
| 692 |
+
break
|
| 693 |
+
if not cfg.model_path:
|
| 694 |
+
healed = Path("td_fuse_outputs/reasoning_healed")
|
| 695 |
+
if healed.exists():
|
| 696 |
+
cfg.model_path = str(healed)
|
| 697 |
+
if not cfg.model_path:
|
| 698 |
+
raise FileNotFoundError("No model found!")
|
| 699 |
+
|
| 700 |
+
start = time.time()
|
| 701 |
+
print("=" * 60)
|
| 702 |
+
print(f"TD BENCHMARK SUITE v1")
|
| 703 |
+
print(f"Model: {cfg.model_path}")
|
| 704 |
+
print(f"Started: {time.strftime('%H:%M:%S')}")
|
| 705 |
+
print("=" * 60)
|
| 706 |
+
|
| 707 |
+
model, tokenizer = load_model(cfg.model_path)
|
| 708 |
+
|
| 709 |
+
# Run all categories
|
| 710 |
+
# SPEED: Per-category max tokens. Math/knowledge/reasoning answers are
|
| 711 |
+
# 1-10 tokens. Code needs 200+. Creativity needs 150+.
|
| 712 |
+
# Using 256 for a "yes/no" question wastes inference time.
|
| 713 |
+
CATEGORY_MAX_TOKENS = {
|
| 714 |
+
"math": 64,
|
| 715 |
+
"code": 300, # Functions can be long
|
| 716 |
+
"reasoning": 64,
|
| 717 |
+
"creativity": 200, # Stories/poems need room
|
| 718 |
+
"knowledge": 64,
|
| 719 |
+
"instruction_following": 128, # JSON output, alphabet, etc.
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
categories = [
|
| 723 |
+
("math", MATH_TESTS, check_math_answer),
|
| 724 |
+
("code", CODE_TESTS, check_code_answer),
|
| 725 |
+
("reasoning", REASONING_TESTS, check_reasoning_answer),
|
| 726 |
+
("creativity", CREATIVITY_TESTS, check_creativity_answer),
|
| 727 |
+
("knowledge", KNOWLEDGE_TESTS, check_knowledge_answer),
|
| 728 |
+
("instruction_following", INSTRUCTION_TESTS, check_instruction_answer),
|
| 729 |
+
]
|
| 730 |
+
|
| 731 |
+
all_results = {}
|
| 732 |
+
total_passed = 0
|
| 733 |
+
total_questions = 0
|
| 734 |
+
|
| 735 |
+
# Save original max_new_tokens and swap per-category
|
| 736 |
+
orig_max_tokens = cfg.max_new_tokens
|
| 737 |
+
for cat_name, tests, check_fn in categories:
|
| 738 |
+
cfg.max_new_tokens = CATEGORY_MAX_TOKENS.get(cat_name, orig_max_tokens)
|
| 739 |
+
result = run_category(model, tokenizer, cat_name, tests, check_fn, cfg)
|
| 740 |
+
all_results[cat_name] = result
|
| 741 |
+
total_passed += result["passed"]
|
| 742 |
+
total_questions += result["total"]
|
| 743 |
+
cfg.max_new_tokens = orig_max_tokens # Restore
|
| 744 |
+
|
| 745 |
+
# Cleanup
|
| 746 |
+
del model
|
| 747 |
+
gc.collect()
|
| 748 |
+
if torch.cuda.is_available():
|
| 749 |
+
torch.cuda.empty_cache()
|
| 750 |
+
|
| 751 |
+
elapsed = (time.time() - start) / 60
|
| 752 |
+
overall_score = total_passed / total_questions if total_questions > 0 else 0.0
|
| 753 |
+
|
| 754 |
+
# Print summary
|
| 755 |
+
print(f"\n{'='*60}")
|
| 756 |
+
print(f"BENCHMARK COMPLETE — {elapsed:.1f} min")
|
| 757 |
+
print(f"{'='*60}")
|
| 758 |
+
print(f"\n REPORT CARD:")
|
| 759 |
+
print(f" {'Category':<25} {'Score':>10} {'Grade':>8}")
|
| 760 |
+
print(f" {'-'*45}")
|
| 761 |
+
|
| 762 |
+
for cat_name in ["math", "code", "reasoning", "creativity", "knowledge", "instruction_following"]:
|
| 763 |
+
r = all_results[cat_name]
|
| 764 |
+
score = r["score"] # 0.0-1.0
|
| 765 |
+
if score >= 0.90: grade = "A"
|
| 766 |
+
elif score >= 0.80: grade = "B"
|
| 767 |
+
elif score >= 0.70: grade = "C"
|
| 768 |
+
elif score >= 0.60: grade = "D"
|
| 769 |
+
else: grade = "F"
|
| 770 |
+
bar = "█" * int(score * 20)
|
| 771 |
+
print(f" {cat_name:<25} {r['passed']:>3}/{r['total']:<3} ({score:>5.0%}) {grade:>4} {bar}")
|
| 772 |
+
|
| 773 |
+
print(f"\n OVERALL: {total_passed}/{total_questions} ({overall_score:.0%})")
|
| 774 |
+
print(f"{'='*60}")
|
| 775 |
+
|
| 776 |
+
# Identify weaknesses (bottom 3 sub-categories)
|
| 777 |
+
all_types = []
|
| 778 |
+
for cat_name, result in all_results.items():
|
| 779 |
+
for type_name, type_score in result["type_scores"].items():
|
| 780 |
+
all_types.append({
|
| 781 |
+
"category": cat_name,
|
| 782 |
+
"type": type_name,
|
| 783 |
+
"pct": type_score["pct"],
|
| 784 |
+
"correct": type_score["correct"],
|
| 785 |
+
"total": type_score["total"],
|
| 786 |
+
})
|
| 787 |
+
|
| 788 |
+
weakest = sorted(all_types, key=lambda x: x["pct"])[:5]
|
| 789 |
+
print(f"\n TOP 5 WEAKNESSES:")
|
| 790 |
+
for w in weakest:
|
| 791 |
+
print(f" {w['category']}/{w['type']}: {w['correct']}/{w['total']} ({w['pct']:.0%})")
|
| 792 |
+
|
| 793 |
+
# Save report
|
| 794 |
+
out_dir = Path(cfg.output_dir)
|
| 795 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 796 |
+
|
| 797 |
+
report = {
|
| 798 |
+
"model_path": cfg.model_path,
|
| 799 |
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 800 |
+
"duration_min": elapsed,
|
| 801 |
+
"overall_score": overall_score,
|
| 802 |
+
"total_passed": total_passed,
|
| 803 |
+
"total_questions": total_questions,
|
| 804 |
+
"categories": {name: {
|
| 805 |
+
"score": r["score"],
|
| 806 |
+
"passed": r["passed"],
|
| 807 |
+
"total": r["total"],
|
| 808 |
+
"type_scores": r["type_scores"],
|
| 809 |
+
"failed_questions": r["failed_questions"],
|
| 810 |
+
} for name, r in all_results.items()},
|
| 811 |
+
"weakest_areas": weakest,
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
# Support both output_path (exact path) and output_dir (auto-named)
|
| 815 |
+
if cfg.output_path:
|
| 816 |
+
report_path = Path(cfg.output_path)
|
| 817 |
+
report_path.parent.mkdir(parents=True, exist_ok=True)
|
| 818 |
+
else:
|
| 819 |
+
report_path = out_dir / f"benchmark_{time.strftime('%Y%m%d_%H%M%S')}.json"
|
| 820 |
+
|
| 821 |
+
try:
|
| 822 |
+
with open(report_path, "w") as f:
|
| 823 |
+
json.dump(report, f, indent=2)
|
| 824 |
+
print(f"\n Report saved: {report_path}")
|
| 825 |
+
except OSError as e:
|
| 826 |
+
print(f"\n WARNING: Could not save benchmark report: {e}")
|
| 827 |
+
print(f" Results are still returned in memory — loop can continue.")
|
| 828 |
+
|
| 829 |
+
return report
|
| 830 |
+
|
| 831 |
+
|
| 832 |
+
if __name__ == "__main__":
|
| 833 |
+
run_benchmark()
|
hugging/td_fuse/td_contamination.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TD Contamination Check — Benchmark Isolation Guard
|
| 3 |
+
|
| 4 |
+
Ensures training data doesn't contain benchmark or holdout questions.
|
| 5 |
+
If benchmark questions leak into training, scores become meaningless.
|
| 6 |
+
|
| 7 |
+
This runs BEFORE training starts. If contamination is found, it:
|
| 8 |
+
1. Removes the contaminated samples from training data
|
| 9 |
+
2. Logs what was removed and why
|
| 10 |
+
3. Returns the cleaned data
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
from td_contamination import check_contamination, get_all_benchmark_questions
|
| 14 |
+
clean_texts = check_contamination(train_texts)
|
| 15 |
+
|
| 16 |
+
# Or standalone:
|
| 17 |
+
python td_contamination.py path/to/training_data.jsonl
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import json
|
| 21 |
+
import re
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
from typing import List, Set, Tuple
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def get_all_benchmark_questions() -> Set[str]:
|
| 27 |
+
"""
|
| 28 |
+
Collect ALL questions from td_benchmark.py and td_holdout.py.
|
| 29 |
+
Returns a set of normalized question strings.
|
| 30 |
+
"""
|
| 31 |
+
questions = set()
|
| 32 |
+
|
| 33 |
+
# Import benchmark questions
|
| 34 |
+
try:
|
| 35 |
+
from td_benchmark import (MATH_TESTS, CODE_TESTS, REASONING_TESTS,
|
| 36 |
+
CREATIVITY_TESTS, KNOWLEDGE_TESTS, INSTRUCTION_TESTS)
|
| 37 |
+
for bank in [MATH_TESTS, CODE_TESTS, REASONING_TESTS,
|
| 38 |
+
CREATIVITY_TESTS, KNOWLEDGE_TESTS, INSTRUCTION_TESTS]:
|
| 39 |
+
for test in bank:
|
| 40 |
+
questions.add(_normalize(test["q"]))
|
| 41 |
+
except ImportError:
|
| 42 |
+
print(" WARNING: Could not import td_benchmark — skipping main benchmark")
|
| 43 |
+
|
| 44 |
+
# Import holdout questions
|
| 45 |
+
try:
|
| 46 |
+
from td_holdout import (HOLDOUT_MATH, HOLDOUT_CODE, HOLDOUT_REASONING,
|
| 47 |
+
HOLDOUT_CREATIVITY, HOLDOUT_KNOWLEDGE, HOLDOUT_INSTRUCTION)
|
| 48 |
+
for bank in [HOLDOUT_MATH, HOLDOUT_CODE, HOLDOUT_REASONING,
|
| 49 |
+
HOLDOUT_CREATIVITY, HOLDOUT_KNOWLEDGE, HOLDOUT_INSTRUCTION]:
|
| 50 |
+
for test in bank:
|
| 51 |
+
questions.add(_normalize(test["q"]))
|
| 52 |
+
except ImportError:
|
| 53 |
+
print(" WARNING: Could not import td_holdout — skipping holdout benchmark")
|
| 54 |
+
|
| 55 |
+
return questions
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _normalize(text: str) -> str:
|
| 59 |
+
"""Normalize text for fuzzy matching."""
|
| 60 |
+
text = text.lower().strip()
|
| 61 |
+
# Remove punctuation except basic math operators
|
| 62 |
+
text = re.sub(r'[^\w\s+\-*/=^]', '', text)
|
| 63 |
+
# Collapse whitespace
|
| 64 |
+
text = re.sub(r'\s+', ' ', text)
|
| 65 |
+
return text
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _extract_question_from_chat(text: str) -> str:
|
| 69 |
+
"""Extract the user question from a chat-formatted training sample."""
|
| 70 |
+
# Match <|im_start|>user\n...<|im_end|>
|
| 71 |
+
match = re.search(r'<\|im_start\|>user\n(.*?)<\|im_end\|>', text, re.DOTALL)
|
| 72 |
+
if match:
|
| 73 |
+
return match.group(1).strip()
|
| 74 |
+
return text
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def check_contamination(train_texts: List[str], verbose: bool = True) -> List[str]:
|
| 78 |
+
"""
|
| 79 |
+
Check training data for benchmark question contamination.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
train_texts: List of training text strings
|
| 83 |
+
verbose: Print details about contamination found
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
Cleaned list with contaminated samples removed
|
| 87 |
+
"""
|
| 88 |
+
benchmark_questions = get_all_benchmark_questions()
|
| 89 |
+
if not benchmark_questions:
|
| 90 |
+
if verbose:
|
| 91 |
+
print(" Contamination check: No benchmark questions loaded — SKIP")
|
| 92 |
+
return train_texts
|
| 93 |
+
|
| 94 |
+
if verbose:
|
| 95 |
+
print(f"\n Contamination check: scanning {len(train_texts)} samples against "
|
| 96 |
+
f"{len(benchmark_questions)} benchmark questions...")
|
| 97 |
+
|
| 98 |
+
clean = []
|
| 99 |
+
removed = []
|
| 100 |
+
# Also build n-gram set for partial matching (catches paraphrased questions)
|
| 101 |
+
benchmark_ngrams = set()
|
| 102 |
+
for q in benchmark_questions:
|
| 103 |
+
words = q.split()
|
| 104 |
+
# 5-grams from each question
|
| 105 |
+
for i in range(max(0, len(words) - 4)):
|
| 106 |
+
benchmark_ngrams.add(' '.join(words[i:i+5]))
|
| 107 |
+
|
| 108 |
+
for text in train_texts:
|
| 109 |
+
question = _extract_question_from_chat(text)
|
| 110 |
+
norm_q = _normalize(question)
|
| 111 |
+
|
| 112 |
+
# Check 1: Exact match
|
| 113 |
+
if norm_q in benchmark_questions:
|
| 114 |
+
removed.append(("exact", question[:80]))
|
| 115 |
+
continue
|
| 116 |
+
|
| 117 |
+
# Check 2: Substring match — benchmark question appears inside training text
|
| 118 |
+
contaminated = False
|
| 119 |
+
for bq in benchmark_questions:
|
| 120 |
+
if len(bq) > 15 and bq in _normalize(text):
|
| 121 |
+
removed.append(("substring", f"'{bq[:50]}' found in sample"))
|
| 122 |
+
contaminated = True
|
| 123 |
+
break
|
| 124 |
+
|
| 125 |
+
if contaminated:
|
| 126 |
+
continue
|
| 127 |
+
|
| 128 |
+
# Check 3: High n-gram overlap (catches paraphrases)
|
| 129 |
+
norm_words = norm_q.split()
|
| 130 |
+
if len(norm_words) >= 5:
|
| 131 |
+
sample_ngrams = set()
|
| 132 |
+
for i in range(len(norm_words) - 4):
|
| 133 |
+
sample_ngrams.add(' '.join(norm_words[i:i+5]))
|
| 134 |
+
if sample_ngrams and benchmark_ngrams:
|
| 135 |
+
overlap = len(sample_ngrams & benchmark_ngrams) / max(len(sample_ngrams), 1)
|
| 136 |
+
if overlap > 0.5: # More than half the 5-grams match a benchmark question
|
| 137 |
+
removed.append(("ngram_overlap", f"{overlap:.0%} overlap: {question[:60]}"))
|
| 138 |
+
continue
|
| 139 |
+
|
| 140 |
+
clean.append(text)
|
| 141 |
+
|
| 142 |
+
if verbose:
|
| 143 |
+
if removed:
|
| 144 |
+
print(f" CONTAMINATION FOUND: {len(removed)} samples removed!")
|
| 145 |
+
for reason, detail in removed[:10]: # Show first 10
|
| 146 |
+
print(f" [{reason}] {detail}")
|
| 147 |
+
if len(removed) > 10:
|
| 148 |
+
print(f" ... and {len(removed) - 10} more")
|
| 149 |
+
else:
|
| 150 |
+
print(f" Contamination check: CLEAN (0 matches found)")
|
| 151 |
+
|
| 152 |
+
return clean
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def check_file(filepath: str) -> Tuple[int, int]:
|
| 156 |
+
"""Check a JSONL file for contamination. Returns (clean_count, removed_count)."""
|
| 157 |
+
texts = []
|
| 158 |
+
with open(filepath) as f:
|
| 159 |
+
for line in f:
|
| 160 |
+
line = line.strip()
|
| 161 |
+
if not line:
|
| 162 |
+
continue
|
| 163 |
+
try:
|
| 164 |
+
item = json.loads(line)
|
| 165 |
+
text = item.get("text", "")
|
| 166 |
+
if text:
|
| 167 |
+
texts.append(text)
|
| 168 |
+
except json.JSONDecodeError:
|
| 169 |
+
continue
|
| 170 |
+
|
| 171 |
+
clean = check_contamination(texts)
|
| 172 |
+
return len(clean), len(texts) - len(clean)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
if __name__ == "__main__":
|
| 176 |
+
import sys
|
| 177 |
+
|
| 178 |
+
if len(sys.argv) < 2:
|
| 179 |
+
# Just report benchmark question count
|
| 180 |
+
questions = get_all_benchmark_questions()
|
| 181 |
+
print(f"Loaded {len(questions)} benchmark questions to check against")
|
| 182 |
+
print("\nUsage: python td_contamination.py path/to/training_data.jsonl")
|
| 183 |
+
sys.exit(0)
|
| 184 |
+
|
| 185 |
+
filepath = sys.argv[1]
|
| 186 |
+
if not Path(filepath).exists():
|
| 187 |
+
print(f"File not found: {filepath}")
|
| 188 |
+
sys.exit(1)
|
| 189 |
+
|
| 190 |
+
clean_count, removed_count = check_file(filepath)
|
| 191 |
+
print(f"\nResult: {clean_count} clean, {removed_count} contaminated")
|
| 192 |
+
if removed_count > 0:
|
| 193 |
+
print("ACTION: Training on this data would inflate benchmark scores!")
|
hugging/td_fuse/td_data_finder.py
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TD Dataset Finder v1 — Search Everywhere for Training Data
|
| 3 |
+
|
| 4 |
+
Takes the weakness report from td_weakness.py and finds training data
|
| 5 |
+
to fix those weaknesses. Searches multiple sources in priority order.
|
| 6 |
+
|
| 7 |
+
This is Step 3 of the self-improvement loop.
|
| 8 |
+
|
| 9 |
+
Sources (in order of priority):
|
| 10 |
+
1. HuggingFace Datasets API — biggest, easiest to download
|
| 11 |
+
2. GitHub — repos with training data, benchmarks, problem sets
|
| 12 |
+
3. Kaggle — competitions and datasets
|
| 13 |
+
4. (Future) Web scraping — Stack Overflow, math forums, etc.
|
| 14 |
+
|
| 15 |
+
Input:
|
| 16 |
+
- WeaknessReport from td_weakness.py
|
| 17 |
+
|
| 18 |
+
Output:
|
| 19 |
+
- Formatted training data in chat template, ready for training
|
| 20 |
+
- Data allocation follows the weakness report (worse = more data)
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
import json
|
| 24 |
+
import os
|
| 25 |
+
import random
|
| 26 |
+
import time
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Dict, List, Optional, Tuple
|
| 29 |
+
from dataclasses import dataclass, field
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# ============================================================
|
| 33 |
+
# DATA STRUCTURES
|
| 34 |
+
# ============================================================
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class DatasetResult:
|
| 38 |
+
"""A dataset found from searching."""
|
| 39 |
+
name: str
|
| 40 |
+
source: str # "huggingface", "github", "kaggle", "web"
|
| 41 |
+
url: str
|
| 42 |
+
description: str
|
| 43 |
+
size_estimate: int # approximate number of samples
|
| 44 |
+
relevance_score: float # 0-1, how well it matches the weakness
|
| 45 |
+
category: str # which weakness category it's for
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass
|
| 49 |
+
class FormattedSample:
|
| 50 |
+
"""A single training sample in chat template format."""
|
| 51 |
+
system: str
|
| 52 |
+
user: str
|
| 53 |
+
assistant: str
|
| 54 |
+
category: str
|
| 55 |
+
source: str
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class TrainingData:
|
| 60 |
+
"""Complete training data package ready for the trainer."""
|
| 61 |
+
samples: List[FormattedSample]
|
| 62 |
+
sources_used: List[DatasetResult]
|
| 63 |
+
allocation: Dict[str, int] # category -> actual samples collected
|
| 64 |
+
total_samples: int
|
| 65 |
+
categories_covered: List[str]
|
| 66 |
+
categories_missing: List[str] # categories we couldn't find data for
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ============================================================
|
| 70 |
+
# CHAT TEMPLATE FORMATTER
|
| 71 |
+
# ============================================================
|
| 72 |
+
|
| 73 |
+
def format_chat_template(
|
| 74 |
+
system: str,
|
| 75 |
+
user: str,
|
| 76 |
+
assistant: str,
|
| 77 |
+
) -> str:
|
| 78 |
+
"""
|
| 79 |
+
Format a sample into Qwen3-VL's chat template.
|
| 80 |
+
This is the <|im_start|> format that selfimprove.py already uses.
|
| 81 |
+
"""
|
| 82 |
+
parts = []
|
| 83 |
+
if system:
|
| 84 |
+
parts.append(f"<|im_start|>system\n{system}<|im_end|>")
|
| 85 |
+
parts.append(f"<|im_start|>user\n{user}<|im_end|>")
|
| 86 |
+
parts.append(f"<|im_start|>assistant\n{assistant}<|im_end|>")
|
| 87 |
+
return "\n".join(parts)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def format_sample_to_dict(sample: FormattedSample) -> Dict:
|
| 91 |
+
"""Convert a FormattedSample to the dict format selfimprove.py expects."""
|
| 92 |
+
return {
|
| 93 |
+
"text": format_chat_template(sample.system, sample.user, sample.assistant),
|
| 94 |
+
"category": sample.category,
|
| 95 |
+
"source": sample.source,
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ============================================================
|
| 100 |
+
# HUGGINGFACE SEARCH
|
| 101 |
+
# ============================================================
|
| 102 |
+
|
| 103 |
+
def search_huggingface(
|
| 104 |
+
keywords: List[str],
|
| 105 |
+
tags: List[str],
|
| 106 |
+
category: str,
|
| 107 |
+
max_datasets: int = 5,
|
| 108 |
+
) -> List[DatasetResult]:
|
| 109 |
+
"""
|
| 110 |
+
Search HuggingFace Datasets API for matching datasets.
|
| 111 |
+
|
| 112 |
+
Uses the huggingface_hub library to search by keyword and tags.
|
| 113 |
+
Returns metadata about found datasets (doesn't download yet).
|
| 114 |
+
"""
|
| 115 |
+
results = []
|
| 116 |
+
|
| 117 |
+
try:
|
| 118 |
+
from huggingface_hub import HfApi
|
| 119 |
+
api = HfApi()
|
| 120 |
+
|
| 121 |
+
# Search by each keyword
|
| 122 |
+
seen = set()
|
| 123 |
+
for keyword in keywords[:3]: # Top 3 keywords
|
| 124 |
+
try:
|
| 125 |
+
datasets = api.list_datasets(
|
| 126 |
+
search=keyword,
|
| 127 |
+
sort="downloads",
|
| 128 |
+
direction=-1,
|
| 129 |
+
limit=max_datasets,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
for ds in datasets:
|
| 133 |
+
if ds.id in seen:
|
| 134 |
+
continue
|
| 135 |
+
seen.add(ds.id)
|
| 136 |
+
|
| 137 |
+
# Estimate relevance based on tag overlap
|
| 138 |
+
ds_tags = set(ds.tags or [])
|
| 139 |
+
tag_overlap = len(ds_tags.intersection(set(tags)))
|
| 140 |
+
relevance = min(0.5 + (tag_overlap * 0.15), 1.0)
|
| 141 |
+
|
| 142 |
+
# Boost if keyword appears in name
|
| 143 |
+
if any(kw.lower() in ds.id.lower() for kw in keyword.split()):
|
| 144 |
+
relevance = min(relevance + 0.2, 1.0)
|
| 145 |
+
|
| 146 |
+
results.append(DatasetResult(
|
| 147 |
+
name=ds.id,
|
| 148 |
+
source="huggingface",
|
| 149 |
+
url=f"https://huggingface.co/datasets/{ds.id}",
|
| 150 |
+
description=ds.id, # HF API doesn't always return description
|
| 151 |
+
size_estimate=ds.downloads or 0, # Use downloads as proxy
|
| 152 |
+
relevance_score=relevance,
|
| 153 |
+
category=category,
|
| 154 |
+
))
|
| 155 |
+
except Exception as e:
|
| 156 |
+
print(f" Warning: HF search failed for '{keyword}': {e}")
|
| 157 |
+
continue
|
| 158 |
+
|
| 159 |
+
except ImportError:
|
| 160 |
+
print(" Warning: huggingface_hub not installed. Install with: pip install huggingface_hub")
|
| 161 |
+
return []
|
| 162 |
+
|
| 163 |
+
# Sort by relevance
|
| 164 |
+
results.sort(key=lambda r: r.relevance_score, reverse=True)
|
| 165 |
+
return results[:max_datasets]
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def download_huggingface_dataset(
|
| 169 |
+
dataset_name: str,
|
| 170 |
+
category: str,
|
| 171 |
+
num_samples: int,
|
| 172 |
+
split: str = "train",
|
| 173 |
+
) -> List[FormattedSample]:
|
| 174 |
+
"""
|
| 175 |
+
Download and format samples from a HuggingFace dataset.
|
| 176 |
+
|
| 177 |
+
Handles common dataset formats:
|
| 178 |
+
- QA pairs (question/answer columns)
|
| 179 |
+
- Instruction pairs (instruction/output columns)
|
| 180 |
+
- Chat format (conversations column)
|
| 181 |
+
- Text only (text column) — wrap in QA format
|
| 182 |
+
"""
|
| 183 |
+
samples = []
|
| 184 |
+
|
| 185 |
+
try:
|
| 186 |
+
from datasets import load_dataset
|
| 187 |
+
|
| 188 |
+
print(f" Downloading {dataset_name} (split={split})...")
|
| 189 |
+
try:
|
| 190 |
+
ds = load_dataset(dataset_name, split=split, streaming=True)
|
| 191 |
+
except Exception:
|
| 192 |
+
# Try without split specification
|
| 193 |
+
try:
|
| 194 |
+
ds = load_dataset(dataset_name, streaming=True)
|
| 195 |
+
# Get first available split
|
| 196 |
+
if hasattr(ds, 'keys'):
|
| 197 |
+
split_name = list(ds.keys())[0]
|
| 198 |
+
ds = ds[split_name]
|
| 199 |
+
else:
|
| 200 |
+
return []
|
| 201 |
+
except Exception as e:
|
| 202 |
+
print(f" Failed to load {dataset_name}: {e}")
|
| 203 |
+
return []
|
| 204 |
+
|
| 205 |
+
# Collect samples (streaming so we don't download everything)
|
| 206 |
+
# Timeout: 5 min per dataset to prevent hanging on slow/broken sources
|
| 207 |
+
import time as _time
|
| 208 |
+
download_start = _time.time()
|
| 209 |
+
download_timeout = 300 # 5 minutes max per dataset
|
| 210 |
+
count = 0
|
| 211 |
+
stall_count = 0
|
| 212 |
+
for item in ds:
|
| 213 |
+
if count >= num_samples:
|
| 214 |
+
break
|
| 215 |
+
if _time.time() - download_start > download_timeout:
|
| 216 |
+
print(f" Timeout: {dataset_name} took >{download_timeout}s — moving on with {count} samples")
|
| 217 |
+
break
|
| 218 |
+
|
| 219 |
+
sample = _format_hf_item(item, category, dataset_name)
|
| 220 |
+
if sample:
|
| 221 |
+
samples.append(sample)
|
| 222 |
+
count += 1
|
| 223 |
+
stall_count = 0
|
| 224 |
+
else:
|
| 225 |
+
stall_count += 1
|
| 226 |
+
# If 500 consecutive items produce nothing, dataset format is wrong
|
| 227 |
+
if stall_count > 500:
|
| 228 |
+
print(f" {dataset_name}: 500 consecutive unparseable items — skipping")
|
| 229 |
+
break
|
| 230 |
+
|
| 231 |
+
print(f" Got {len(samples)} samples from {dataset_name}")
|
| 232 |
+
|
| 233 |
+
except ImportError:
|
| 234 |
+
print(" Warning: datasets library not installed. Install with: pip install datasets")
|
| 235 |
+
except Exception as e:
|
| 236 |
+
print(f" Error downloading {dataset_name}: {e}")
|
| 237 |
+
|
| 238 |
+
return samples
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _format_hf_item(item: Dict, category: str, source: str) -> Optional[FormattedSample]:
|
| 242 |
+
"""
|
| 243 |
+
Try to format a HuggingFace dataset item into a chat sample.
|
| 244 |
+
Handles many common column naming conventions.
|
| 245 |
+
"""
|
| 246 |
+
# Guard: item must be a dict
|
| 247 |
+
if not isinstance(item, dict):
|
| 248 |
+
return None
|
| 249 |
+
|
| 250 |
+
# Common column name patterns for user/assistant content
|
| 251 |
+
user_keys = ["question", "instruction", "input", "prompt", "query", "problem", "text"]
|
| 252 |
+
assistant_keys = ["answer", "output", "response", "solution", "target", "completion"]
|
| 253 |
+
system_keys = ["system", "system_prompt", "context"]
|
| 254 |
+
|
| 255 |
+
user_text = None
|
| 256 |
+
assistant_text = None
|
| 257 |
+
system_text = ""
|
| 258 |
+
|
| 259 |
+
# Try to find user content (guard: value might be non-string like int, list, etc.)
|
| 260 |
+
for key in user_keys:
|
| 261 |
+
if key in item and item[key]:
|
| 262 |
+
val = item[key]
|
| 263 |
+
if isinstance(val, (str, int, float)):
|
| 264 |
+
user_text = str(val).strip()
|
| 265 |
+
if user_text:
|
| 266 |
+
break
|
| 267 |
+
# Skip lists, dicts, None-like values
|
| 268 |
+
|
| 269 |
+
# Try to find assistant content
|
| 270 |
+
for key in assistant_keys:
|
| 271 |
+
if key in item and item[key]:
|
| 272 |
+
val = item[key]
|
| 273 |
+
if isinstance(val, (str, int, float)):
|
| 274 |
+
assistant_text = str(val).strip()
|
| 275 |
+
if assistant_text:
|
| 276 |
+
break
|
| 277 |
+
|
| 278 |
+
# Try to find system content
|
| 279 |
+
for key in system_keys:
|
| 280 |
+
if key in item and item[key]:
|
| 281 |
+
val = item[key]
|
| 282 |
+
if isinstance(val, (str, int, float)):
|
| 283 |
+
system_text = str(val).strip()
|
| 284 |
+
break
|
| 285 |
+
|
| 286 |
+
# Handle conversation format
|
| 287 |
+
if not user_text and "conversations" in item:
|
| 288 |
+
convs = item["conversations"]
|
| 289 |
+
if isinstance(convs, list) and len(convs) >= 2:
|
| 290 |
+
for c in convs:
|
| 291 |
+
if not isinstance(c, dict):
|
| 292 |
+
continue # Skip malformed conversation entries
|
| 293 |
+
role = str(c.get("role", c.get("from", ""))).strip()
|
| 294 |
+
content = c.get("content", c.get("value", ""))
|
| 295 |
+
if content is None:
|
| 296 |
+
continue
|
| 297 |
+
content = str(content).strip()
|
| 298 |
+
if role in ["user", "human"] and not user_text:
|
| 299 |
+
user_text = content
|
| 300 |
+
elif role in ["assistant", "gpt", "bot"] and not assistant_text:
|
| 301 |
+
assistant_text = content
|
| 302 |
+
elif role == "system" and not system_text:
|
| 303 |
+
system_text = content
|
| 304 |
+
|
| 305 |
+
# Handle messages format (OpenAI-style)
|
| 306 |
+
if not user_text and "messages" in item:
|
| 307 |
+
msgs = item["messages"]
|
| 308 |
+
if isinstance(msgs, list):
|
| 309 |
+
for m in msgs:
|
| 310 |
+
if not isinstance(m, dict):
|
| 311 |
+
continue # Skip malformed message entries
|
| 312 |
+
role = str(m.get("role", "")).strip()
|
| 313 |
+
content = m.get("content", "")
|
| 314 |
+
if content is None:
|
| 315 |
+
continue
|
| 316 |
+
content = str(content).strip()
|
| 317 |
+
if role == "user" and not user_text:
|
| 318 |
+
user_text = content
|
| 319 |
+
elif role == "assistant" and not assistant_text:
|
| 320 |
+
assistant_text = content
|
| 321 |
+
elif role == "system" and not system_text:
|
| 322 |
+
system_text = content
|
| 323 |
+
|
| 324 |
+
# Handle MMLU-style multiple choice: question + choices + answer_index
|
| 325 |
+
if not assistant_text and "choices" in item and "answer" in item:
|
| 326 |
+
choices = item["choices"]
|
| 327 |
+
answer_idx = item["answer"]
|
| 328 |
+
if isinstance(choices, list) and len(choices) > 0 and len(choices) <= 26:
|
| 329 |
+
# Format as multiple choice question (cap at 26 = A-Z)
|
| 330 |
+
if user_text:
|
| 331 |
+
choice_strs = [f"{chr(65+i)}. {c}" for i, c in enumerate(choices)]
|
| 332 |
+
user_text = user_text + "\n" + "\n".join(choice_strs)
|
| 333 |
+
if isinstance(answer_idx, int) and 0 <= answer_idx < len(choices):
|
| 334 |
+
assistant_text = f"{chr(65+answer_idx)}. {choices[answer_idx]}"
|
| 335 |
+
elif isinstance(answer_idx, str) and len(answer_idx) == 1 and answer_idx.isalpha():
|
| 336 |
+
idx = ord(answer_idx.upper()) - 65
|
| 337 |
+
if 0 <= idx < len(choices):
|
| 338 |
+
assistant_text = f"{answer_idx.upper()}. {choices[idx]}"
|
| 339 |
+
|
| 340 |
+
# Handle GSM8K-style: answer contains "#### final_number"
|
| 341 |
+
if assistant_text and "####" in assistant_text:
|
| 342 |
+
# Keep the full chain-of-thought — it's good training data for reasoning
|
| 343 |
+
pass
|
| 344 |
+
|
| 345 |
+
# Need at least user + assistant
|
| 346 |
+
if not user_text or not assistant_text:
|
| 347 |
+
return None
|
| 348 |
+
|
| 349 |
+
# Skip very short or very long samples
|
| 350 |
+
if len(user_text) < 10 or len(assistant_text) < 5: # Allow short answers (like "42")
|
| 351 |
+
return None
|
| 352 |
+
if len(user_text) > 5000 or len(assistant_text) > 10000:
|
| 353 |
+
return None
|
| 354 |
+
|
| 355 |
+
return FormattedSample(
|
| 356 |
+
system=system_text,
|
| 357 |
+
user=user_text,
|
| 358 |
+
assistant=assistant_text,
|
| 359 |
+
category=category,
|
| 360 |
+
source=source,
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
# ============================================================
|
| 365 |
+
# WELL-KNOWN DATASETS PER CATEGORY
|
| 366 |
+
# ============================================================
|
| 367 |
+
|
| 368 |
+
# If the search finds nothing, fall back to these known-good datasets
|
| 369 |
+
# Verified dataset names on HuggingFace (March 2026)
|
| 370 |
+
# Each tuple: (dataset_name, config_or_subset)
|
| 371 |
+
# If a dataset fails to load, it moves to the next one silently
|
| 372 |
+
FALLBACK_DATASETS = {
|
| 373 |
+
"math": [
|
| 374 |
+
("openai/gsm8k", "main"), # 8.8K train, question/answer with ####
|
| 375 |
+
("lighteval/MATH", "all"), # 12.5K, competition math
|
| 376 |
+
("microsoft/orca-math-word-problems-200k", None), # 200K word problems
|
| 377 |
+
],
|
| 378 |
+
"code": [
|
| 379 |
+
("openai/openai_humaneval", None), # 164 problems — small but high quality
|
| 380 |
+
("google-research-datasets/mbpp", None), # FIX: full name (short "mbpp" fails)
|
| 381 |
+
("codeparrot/apps", "introductory"), # Competitive programming
|
| 382 |
+
],
|
| 383 |
+
"reasoning": [
|
| 384 |
+
("allenai/ai2_arc", "ARC-Challenge"), # 2.6K challenge set
|
| 385 |
+
("Rowan/hellaswag", None), # 40K commonsense
|
| 386 |
+
("allenai/openbookqa", "main"), # FIX: replaced openai_prm800k (not a public dataset)
|
| 387 |
+
],
|
| 388 |
+
"creativity": [
|
| 389 |
+
("teknium/OpenHermes-2.5", None), # 1M instruction pairs
|
| 390 |
+
("Open-Orca/OpenOrca", None), # 4.2M conversations
|
| 391 |
+
],
|
| 392 |
+
"knowledge": [
|
| 393 |
+
("TIGER-Lab/MMLU-Pro", None), # MMLU harder version
|
| 394 |
+
("cais/mmlu", "all"), # Classic MMLU
|
| 395 |
+
("truthfulqa/truthful_qa", "generation"), # 817 truthfulness questions
|
| 396 |
+
],
|
| 397 |
+
"instruction_following": [
|
| 398 |
+
("HuggingFaceH4/ultrafeedback_binarized", None), # 61K preference pairs
|
| 399 |
+
("tatsu-lab/alpaca", None), # 52K instructions
|
| 400 |
+
],
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
# ============================================================
|
| 405 |
+
# MAIN DATA FINDER
|
| 406 |
+
# ============================================================
|
| 407 |
+
|
| 408 |
+
def find_training_data(
|
| 409 |
+
search_plan: List[Dict],
|
| 410 |
+
data_allocation: Dict[str, int],
|
| 411 |
+
output_dir: str = "td_fuse_outputs/training_data",
|
| 412 |
+
hf_token: Optional[str] = None,
|
| 413 |
+
) -> TrainingData:
|
| 414 |
+
"""
|
| 415 |
+
Search for and download training data based on the weakness report.
|
| 416 |
+
|
| 417 |
+
This is the main entry point that td_loop.py calls.
|
| 418 |
+
|
| 419 |
+
Args:
|
| 420 |
+
search_plan: From WeaknessReport.search_plan
|
| 421 |
+
data_allocation: From WeaknessReport.data_allocation
|
| 422 |
+
output_dir: Where to save the formatted data
|
| 423 |
+
hf_token: HuggingFace token for private datasets
|
| 424 |
+
|
| 425 |
+
Returns:
|
| 426 |
+
TrainingData with formatted samples ready for training
|
| 427 |
+
"""
|
| 428 |
+
print("\n" + "=" * 60)
|
| 429 |
+
print("TD DATASET FINDER v1")
|
| 430 |
+
print("=" * 60)
|
| 431 |
+
|
| 432 |
+
if hf_token:
|
| 433 |
+
os.environ["HF_TOKEN"] = hf_token
|
| 434 |
+
|
| 435 |
+
all_samples = []
|
| 436 |
+
all_sources = []
|
| 437 |
+
categories_covered = []
|
| 438 |
+
categories_missing = []
|
| 439 |
+
|
| 440 |
+
for plan_item in search_plan:
|
| 441 |
+
category = plan_item["category"]
|
| 442 |
+
needed = data_allocation.get(category, 1000)
|
| 443 |
+
keywords = plan_item["keywords"]
|
| 444 |
+
hf_tags = plan_item.get("hf_tags", [])
|
| 445 |
+
|
| 446 |
+
print(f"\n--- Searching for: {category} ({needed} samples needed) ---")
|
| 447 |
+
|
| 448 |
+
# Step 1: Search HuggingFace
|
| 449 |
+
print(f" Searching HuggingFace...")
|
| 450 |
+
hf_results = search_huggingface(keywords, hf_tags, category)
|
| 451 |
+
|
| 452 |
+
if hf_results:
|
| 453 |
+
print(f" Found {len(hf_results)} datasets on HuggingFace:")
|
| 454 |
+
for r in hf_results[:3]:
|
| 455 |
+
print(f" - {r.name} (relevance: {r.relevance_score:.2f})")
|
| 456 |
+
|
| 457 |
+
# Step 2: Download from best matches
|
| 458 |
+
category_samples = []
|
| 459 |
+
for result in hf_results:
|
| 460 |
+
if len(category_samples) >= needed:
|
| 461 |
+
break
|
| 462 |
+
|
| 463 |
+
remaining = needed - len(category_samples)
|
| 464 |
+
samples = download_huggingface_dataset(
|
| 465 |
+
dataset_name=result.name,
|
| 466 |
+
category=category,
|
| 467 |
+
num_samples=remaining,
|
| 468 |
+
)
|
| 469 |
+
category_samples.extend(samples)
|
| 470 |
+
if samples:
|
| 471 |
+
all_sources.append(result)
|
| 472 |
+
|
| 473 |
+
# Step 3: If not enough, try fallback datasets
|
| 474 |
+
if len(category_samples) < needed:
|
| 475 |
+
print(f" Only got {len(category_samples)}/{needed}. Trying fallback datasets...")
|
| 476 |
+
fallbacks = FALLBACK_DATASETS.get(category, [])
|
| 477 |
+
try:
|
| 478 |
+
from datasets import load_dataset # Import once outside loop
|
| 479 |
+
except ImportError:
|
| 480 |
+
print(" Warning: datasets library not installed — cannot try fallbacks")
|
| 481 |
+
fallbacks = [] # Skip the fallback loop
|
| 482 |
+
for ds_name, config in fallbacks:
|
| 483 |
+
if len(category_samples) >= needed:
|
| 484 |
+
break
|
| 485 |
+
|
| 486 |
+
remaining = needed - len(category_samples)
|
| 487 |
+
try:
|
| 488 |
+
if config:
|
| 489 |
+
ds = load_dataset(ds_name, config, streaming=True, split="train")
|
| 490 |
+
else:
|
| 491 |
+
ds = load_dataset(ds_name, streaming=True, split="train")
|
| 492 |
+
|
| 493 |
+
count = 0
|
| 494 |
+
for item in ds:
|
| 495 |
+
if count >= remaining:
|
| 496 |
+
break
|
| 497 |
+
sample = _format_hf_item(item, category, ds_name)
|
| 498 |
+
if sample:
|
| 499 |
+
category_samples.append(sample)
|
| 500 |
+
count += 1
|
| 501 |
+
|
| 502 |
+
if count > 0:
|
| 503 |
+
print(f" Got {count} from fallback: {ds_name}")
|
| 504 |
+
all_sources.append(DatasetResult(
|
| 505 |
+
name=ds_name,
|
| 506 |
+
source="huggingface_fallback",
|
| 507 |
+
url=f"https://huggingface.co/datasets/{ds_name}",
|
| 508 |
+
description=f"Fallback dataset for {category}",
|
| 509 |
+
size_estimate=count,
|
| 510 |
+
relevance_score=0.5,
|
| 511 |
+
category=category,
|
| 512 |
+
))
|
| 513 |
+
except Exception as e:
|
| 514 |
+
print(f" Fallback {ds_name} failed: {e}")
|
| 515 |
+
continue
|
| 516 |
+
|
| 517 |
+
# Track what we got
|
| 518 |
+
if category_samples:
|
| 519 |
+
all_samples.extend(category_samples)
|
| 520 |
+
categories_covered.append(category)
|
| 521 |
+
print(f" Total for {category}: {len(category_samples)} samples")
|
| 522 |
+
else:
|
| 523 |
+
categories_missing.append(category)
|
| 524 |
+
print(f" WARNING: No data found for {category}!")
|
| 525 |
+
|
| 526 |
+
# Shuffle all samples together
|
| 527 |
+
random.shuffle(all_samples)
|
| 528 |
+
|
| 529 |
+
# Save to disk
|
| 530 |
+
output_path = Path(output_dir)
|
| 531 |
+
try:
|
| 532 |
+
output_path.mkdir(parents=True, exist_ok=True)
|
| 533 |
+
except OSError as e:
|
| 534 |
+
print(f"\n ERROR: Cannot create output dir {output_dir}: {e}")
|
| 535 |
+
# Still return what we collected even if can't save
|
| 536 |
+
actual_allocation = {}
|
| 537 |
+
for s in all_samples:
|
| 538 |
+
actual_allocation[s.category] = actual_allocation.get(s.category, 0) + 1
|
| 539 |
+
return TrainingData(
|
| 540 |
+
samples=all_samples, sources_used=all_sources,
|
| 541 |
+
allocation=actual_allocation, total_samples=len(all_samples),
|
| 542 |
+
categories_covered=categories_covered, categories_missing=categories_missing,
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
# Save as JSONL (one sample per line)
|
| 546 |
+
data_file = output_path / "training_data.jsonl"
|
| 547 |
+
try:
|
| 548 |
+
with open(data_file, "w") as f:
|
| 549 |
+
for sample in all_samples:
|
| 550 |
+
f.write(json.dumps(format_sample_to_dict(sample)) + "\n")
|
| 551 |
+
print(f"\nSaved {len(all_samples)} samples to {data_file}")
|
| 552 |
+
except OSError as e:
|
| 553 |
+
print(f"\n WARNING: Failed to save training data: {e} (disk may be full)")
|
| 554 |
+
|
| 555 |
+
# Save metadata
|
| 556 |
+
meta_file = output_path / "data_metadata.json"
|
| 557 |
+
actual_allocation = {}
|
| 558 |
+
for s in all_samples:
|
| 559 |
+
actual_allocation[s.category] = actual_allocation.get(s.category, 0) + 1
|
| 560 |
+
|
| 561 |
+
meta = {
|
| 562 |
+
"total_samples": len(all_samples),
|
| 563 |
+
"allocation": actual_allocation,
|
| 564 |
+
"sources": [
|
| 565 |
+
{"name": s.name, "source": s.source, "category": s.category, "url": s.url}
|
| 566 |
+
for s in all_sources
|
| 567 |
+
],
|
| 568 |
+
"categories_covered": categories_covered,
|
| 569 |
+
"categories_missing": categories_missing,
|
| 570 |
+
}
|
| 571 |
+
try:
|
| 572 |
+
with open(meta_file, "w") as f:
|
| 573 |
+
json.dump(meta, f, indent=2)
|
| 574 |
+
print(f"Saved metadata to {meta_file}")
|
| 575 |
+
except OSError as e:
|
| 576 |
+
print(f" WARNING: Failed to save metadata: {e}")
|
| 577 |
+
|
| 578 |
+
return TrainingData(
|
| 579 |
+
samples=all_samples,
|
| 580 |
+
sources_used=all_sources,
|
| 581 |
+
allocation=actual_allocation,
|
| 582 |
+
total_samples=len(all_samples),
|
| 583 |
+
categories_covered=categories_covered,
|
| 584 |
+
categories_missing=categories_missing,
|
| 585 |
+
)
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
# ============================================================
|
| 589 |
+
# STANDALONE USAGE
|
| 590 |
+
# ============================================================
|
| 591 |
+
|
| 592 |
+
if __name__ == "__main__":
|
| 593 |
+
import sys
|
| 594 |
+
|
| 595 |
+
if len(sys.argv) < 2:
|
| 596 |
+
print("Usage: python td_data_finder.py <weakness_report.json> [output_dir]")
|
| 597 |
+
print("\nSearches for training data based on the weakness report.")
|
| 598 |
+
sys.exit(1)
|
| 599 |
+
|
| 600 |
+
# Load weakness report
|
| 601 |
+
from td_weakness import load_report
|
| 602 |
+
report = load_report(sys.argv[1])
|
| 603 |
+
|
| 604 |
+
output_dir = sys.argv[2] if len(sys.argv) > 2 else "td_fuse_outputs/training_data"
|
| 605 |
+
|
| 606 |
+
# Find data
|
| 607 |
+
training_data = find_training_data(
|
| 608 |
+
search_plan=report.search_plan,
|
| 609 |
+
data_allocation=report.data_allocation,
|
| 610 |
+
output_dir=output_dir,
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
print(f"\nDone! Got {training_data.total_samples} samples across {len(training_data.categories_covered)} categories.")
|
| 614 |
+
if training_data.categories_missing:
|
| 615 |
+
print(f"Missing data for: {', '.join(training_data.categories_missing)}")
|
hugging/td_fuse/td_holdout.py
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TD Holdout Benchmark — The Truth Test
|
| 3 |
+
|
| 4 |
+
This is a SEPARATE benchmark that NEVER influences training decisions.
|
| 5 |
+
It only runs at the start and end to verify improvement is real.
|
| 6 |
+
|
| 7 |
+
Why this exists:
|
| 8 |
+
The main benchmark (td_benchmark.py) has 85 fixed questions.
|
| 9 |
+
After enough cycles, the model might get good at THOSE specific
|
| 10 |
+
questions without actually getting smarter. This is overfitting.
|
| 11 |
+
|
| 12 |
+
The holdout uses completely different questions testing the same
|
| 13 |
+
skills. If the main benchmark shows improvement but the holdout
|
| 14 |
+
doesn't, we know we're overfitting.
|
| 15 |
+
|
| 16 |
+
Rules:
|
| 17 |
+
1. NEVER use holdout results to decide training data
|
| 18 |
+
2. NEVER share holdout questions with the training pipeline
|
| 19 |
+
3. Only run holdout at: first cycle (baseline) and final cycle (verify)
|
| 20 |
+
4. If holdout score drops while main benchmark rises = OVERFITTING
|
| 21 |
+
|
| 22 |
+
Usage:
|
| 23 |
+
python td_holdout.py # Run on latest model
|
| 24 |
+
python td_holdout.py path/to/model # Run on specific model
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
import json
|
| 28 |
+
import time
|
| 29 |
+
import gc
|
| 30 |
+
import re
|
| 31 |
+
from pathlib import Path
|
| 32 |
+
from typing import Dict
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ============================================================
|
| 36 |
+
# HOLDOUT QUESTIONS — completely different from td_benchmark.py
|
| 37 |
+
# ============================================================
|
| 38 |
+
|
| 39 |
+
HOLDOUT_MATH = [
|
| 40 |
+
# Different numbers, same skills as main benchmark
|
| 41 |
+
{"q": "What is 63 * 17?", "answer": "1071", "type": "arithmetic"},
|
| 42 |
+
{"q": "What is 892 - 456?", "answer": "436", "type": "arithmetic"},
|
| 43 |
+
{"q": "What is 25% of 360?", "answer": "90", "type": "arithmetic"},
|
| 44 |
+
{"q": "Solve for x: 5x - 3 = 27", "answer": "6", "type": "algebra"},
|
| 45 |
+
{"q": "If f(x) = 3x - 1, what is f(5)?", "answer": "14", "type": "algebra"},
|
| 46 |
+
{"q": "A book costs $15. If there's a 20% discount, what's the price?", "answer": "12", "type": "word_problem"},
|
| 47 |
+
{"q": "A tank fills at 3 liters per minute. How long to fill a 45 liter tank?", "answer": "15", "type": "word_problem"},
|
| 48 |
+
{"q": "What is the sum of the first 5 prime numbers?", "answer": "28", "type": "number_theory"},
|
| 49 |
+
{"q": "What is 3^5?", "answer": "243", "type": "exponents"},
|
| 50 |
+
{"q": "What is the least common multiple of 6 and 8?", "answer": "24", "type": "number_theory"},
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
HOLDOUT_CODE = [
|
| 54 |
+
{"q": "Write a Python function called 'sum_list' that takes a list of numbers and returns their sum. Just the function.",
|
| 55 |
+
"check": "sum_list", "verify": "callable",
|
| 56 |
+
"tests": [{"args": [[1,2,3]], "expected": 6}, {"args": [[]], "expected": 0}]},
|
| 57 |
+
{"q": "Write a Python function called 'remove_duplicates' that takes a list and returns a new list with duplicates removed, preserving order. Just the function.",
|
| 58 |
+
"check": "remove_duplicates", "verify": "callable",
|
| 59 |
+
"tests": [{"args": [[1,2,2,3,1]], "expected": [1,2,3]}]},
|
| 60 |
+
{"q": "Write a Python function called 'is_prime' that returns True if a number is prime. Just the function.",
|
| 61 |
+
"check": "is_prime", "verify": "callable",
|
| 62 |
+
"tests": [{"args": [7], "expected": True}, {"args": [4], "expected": False}, {"args": [1], "expected": False}]},
|
| 63 |
+
{"q": "Write a Python function called 'count_words' that counts the number of words in a string. Just the function.",
|
| 64 |
+
"check": "count_words", "verify": "callable",
|
| 65 |
+
"tests": [{"args": ["hello world foo"], "expected": 3}, {"args": [""], "expected": 0}]},
|
| 66 |
+
{"q": "This code has a bug. Fix it:\ndef average(numbers):\n return sum(numbers) / len(numbers)",
|
| 67 |
+
"check": "len(numbers) == 0", "verify": "contains_any",
|
| 68 |
+
"alt_checks": ["not numbers", "len(numbers) == 0", "if numbers", "if not", "len(numbers) < 1", "ZeroDivisionError"]},
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
HOLDOUT_REASONING = [
|
| 72 |
+
{"q": "If all dogs are mammals and some mammals can swim, can all dogs swim? Answer yes or no.", "answer": "no", "type": "syllogism"},
|
| 73 |
+
{"q": "What comes next: 5, 10, 20, 40, ?", "answer": "80", "type": "pattern"},
|
| 74 |
+
{"q": "What comes next: 1, 3, 6, 10, 15, ?", "answer": "21", "type": "pattern"},
|
| 75 |
+
{"q": "There are 5 people in a room. Each shakes hands with everyone else exactly once. How many handshakes total?", "answer": "10", "type": "multi_step"},
|
| 76 |
+
{"q": "A farmer has chickens and cows. He counts 20 heads and 56 legs. How many chickens does he have?", "answer": "12", "type": "multi_step"},
|
| 77 |
+
{"q": "If you rearrange the letters 'CIFAIPC' you get the name of a what? Ocean or country?", "answer": "ocean", "type": "common_sense"},
|
| 78 |
+
{"q": "Is it possible for your mother's brother to be a woman? Answer yes or no.", "answer": "no", "type": "common_sense"},
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
HOLDOUT_CREATIVITY = [
|
| 82 |
+
{"q": "Write a 4-line poem about rain.", "min_lines": 3, "min_words": 15, "type": "poem"},
|
| 83 |
+
{"q": "Explain gravity to a 5 year old in 2-3 sentences.", "min_lines": 1, "min_words": 15, "type": "explain_simple"},
|
| 84 |
+
{"q": "Come up with 3 creative names for a pet goldfish.", "min_items": 3, "type": "brainstorm"},
|
| 85 |
+
{"q": "Rewrite this to be more dramatic: 'The sun set behind the mountains.'",
|
| 86 |
+
"min_words": 5, "type": "rewrite", "must_not_contain": "The sun set behind the mountains"},
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
HOLDOUT_KNOWLEDGE = [
|
| 90 |
+
{"q": "What is the largest planet in our solar system?", "answer": "jupiter", "type": "science"},
|
| 91 |
+
{"q": "What element has the atomic number 1?", "answer": "hydrogen", "type": "science"},
|
| 92 |
+
{"q": "Who wrote Romeo and Juliet?", "answer": "shakespeare", "type": "history", "alt_answers": ["william shakespeare"]},
|
| 93 |
+
{"q": "What is the capital of Australia?", "answer": "canberra", "type": "geography"},
|
| 94 |
+
{"q": "What year did the first iPhone come out?", "answer": "2007", "type": "tech"},
|
| 95 |
+
{"q": "What does RAM stand for?", "answer": "random access memory", "type": "tech"},
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
HOLDOUT_INSTRUCTION = [
|
| 99 |
+
{"q": "Respond with only the word 'goodbye'. Nothing else.",
|
| 100 |
+
"check": "goodbye", "verify": "exact_word", "type": "exact"},
|
| 101 |
+
{"q": "List exactly 4 animals, one per line. Nothing else.",
|
| 102 |
+
"check": 4, "verify": "line_count", "type": "format"},
|
| 103 |
+
{"q": "Write a sentence that contains exactly 7 words.",
|
| 104 |
+
"check": 7, "verify": "word_count", "type": "format"},
|
| 105 |
+
{"q": "Give me a number between 50 and 100. Just the number.",
|
| 106 |
+
"check": "number_50_100", "verify": "number_range_custom", "type": "exact"},
|
| 107 |
+
{"q": "Say 'no' if 10 > 20, otherwise say 'yes'. Only one word.",
|
| 108 |
+
"check": "yes", "verify": "exact_word", "type": "exact"},
|
| 109 |
+
]
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# ============================================================
|
| 113 |
+
# ANSWER CHECKING (reuses td_benchmark logic)
|
| 114 |
+
# ============================================================
|
| 115 |
+
|
| 116 |
+
def strip_thinking(response: str) -> str:
|
| 117 |
+
cleaned = re.sub(r'<think>.*?</think>', '', response, flags=re.DOTALL)
|
| 118 |
+
cleaned = re.sub(r'<think>.*$', '', cleaned, flags=re.DOTALL)
|
| 119 |
+
return cleaned.strip()
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def check_math(response: str, expected: str) -> bool:
|
| 123 |
+
response_clean = strip_thinking(response).lower().replace(",", "")
|
| 124 |
+
numbers = re.findall(r'-?\d+\.?\d*', response_clean)
|
| 125 |
+
for n in numbers:
|
| 126 |
+
try:
|
| 127 |
+
if float(n) == float(expected):
|
| 128 |
+
return True
|
| 129 |
+
except ValueError:
|
| 130 |
+
continue
|
| 131 |
+
return False
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def check_code(response: str, test: dict) -> bool:
|
| 135 |
+
response = strip_thinking(response)
|
| 136 |
+
code_block = response
|
| 137 |
+
md_match = re.search(r'```(?:python)?\s*\n(.*?)```', response, re.DOTALL)
|
| 138 |
+
if md_match:
|
| 139 |
+
code_block = md_match.group(1)
|
| 140 |
+
elif "def " in code_block:
|
| 141 |
+
def_start = code_block.index("def ")
|
| 142 |
+
code_block = code_block[def_start:]
|
| 143 |
+
|
| 144 |
+
if test["verify"] == "callable":
|
| 145 |
+
try:
|
| 146 |
+
import signal
|
| 147 |
+
|
| 148 |
+
def _timeout(signum, frame):
|
| 149 |
+
raise TimeoutError()
|
| 150 |
+
|
| 151 |
+
old = signal.signal(signal.SIGALRM, _timeout)
|
| 152 |
+
signal.alarm(10)
|
| 153 |
+
try:
|
| 154 |
+
ns = {}
|
| 155 |
+
exec(code_block.strip(), ns)
|
| 156 |
+
func = ns.get(test["check"])
|
| 157 |
+
if not func or not callable(func):
|
| 158 |
+
return False
|
| 159 |
+
for t in test.get("tests", []):
|
| 160 |
+
result = func(*t["args"])
|
| 161 |
+
if result != t["expected"]:
|
| 162 |
+
return False
|
| 163 |
+
return True
|
| 164 |
+
finally:
|
| 165 |
+
signal.alarm(0)
|
| 166 |
+
signal.signal(signal.SIGALRM, old)
|
| 167 |
+
except Exception:
|
| 168 |
+
return False
|
| 169 |
+
elif test["verify"] == "contains_any":
|
| 170 |
+
checks = test.get("alt_checks", [test["check"]])
|
| 171 |
+
return any(c.lower() in response.lower() for c in checks)
|
| 172 |
+
return False
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def check_reasoning(response: str, expected: str) -> bool:
|
| 176 |
+
response_lower = strip_thinking(response).lower().strip()
|
| 177 |
+
if expected.lower() in response_lower:
|
| 178 |
+
return True
|
| 179 |
+
if expected.lower() in ["yes", "no"]:
|
| 180 |
+
first = response_lower.split()[0].strip(".,!?") if response_lower.split() else ""
|
| 181 |
+
return first == expected.lower()
|
| 182 |
+
numbers = re.findall(r'-?\d+\.?\d*', response_lower)
|
| 183 |
+
return expected.lower() in numbers
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def check_creativity(response: str, test: dict) -> bool:
|
| 187 |
+
response = strip_thinking(response)
|
| 188 |
+
lines = [l.strip() for l in response.strip().split("\n") if l.strip()]
|
| 189 |
+
words = response.split()
|
| 190 |
+
if "must_not_contain" in test and test["must_not_contain"].lower() in response.lower():
|
| 191 |
+
return False
|
| 192 |
+
if "min_lines" in test and len(lines) < test["min_lines"]:
|
| 193 |
+
return False
|
| 194 |
+
if "min_words" in test and len(words) < test["min_words"]:
|
| 195 |
+
return False
|
| 196 |
+
if "min_items" in test:
|
| 197 |
+
items = [l for l in lines if re.match(r'^\d+[\.\):]|^[-*•]', l)]
|
| 198 |
+
if len(items) < test["min_items"] and len(lines) < test["min_items"]:
|
| 199 |
+
return False
|
| 200 |
+
return len(response.strip()) >= 10
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def check_knowledge(response: str, test: dict) -> bool:
|
| 204 |
+
response_lower = strip_thinking(response).lower()
|
| 205 |
+
if test["answer"].lower() in response_lower:
|
| 206 |
+
return True
|
| 207 |
+
for alt in test.get("alt_answers", []):
|
| 208 |
+
if alt.lower() in response_lower:
|
| 209 |
+
return True
|
| 210 |
+
return False
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def check_instruction(response: str, test: dict) -> bool:
|
| 214 |
+
response_clean = strip_thinking(response).strip()
|
| 215 |
+
response_lower = response_clean.lower()
|
| 216 |
+
verify = test["verify"]
|
| 217 |
+
check = test["check"]
|
| 218 |
+
|
| 219 |
+
if verify == "exact_word":
|
| 220 |
+
first = response_lower.split()[0] if response_lower.split() else ""
|
| 221 |
+
return re.sub(r'[.,!?"\']', '', first) == check.lower()
|
| 222 |
+
elif verify == "line_count":
|
| 223 |
+
lines = [l.strip() for l in response_clean.split("\n") if l.strip()]
|
| 224 |
+
return len(lines) == check
|
| 225 |
+
elif verify == "word_count":
|
| 226 |
+
lines = [l.strip() for l in response_clean.split("\n") if l.strip()]
|
| 227 |
+
for line in lines:
|
| 228 |
+
if any(s in line.lower() for s in ["here is", "here's", "sure", "okay"]):
|
| 229 |
+
continue
|
| 230 |
+
if len(line.split()) == check:
|
| 231 |
+
return True
|
| 232 |
+
return len(response_clean.split()) == check
|
| 233 |
+
elif verify == "number_range_custom":
|
| 234 |
+
numbers = re.findall(r'\d+', response_clean)
|
| 235 |
+
if numbers:
|
| 236 |
+
return 50 <= int(numbers[0]) <= 100
|
| 237 |
+
return False
|
| 238 |
+
return False
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# ============================================================
|
| 242 |
+
# RUN HOLDOUT
|
| 243 |
+
# ============================================================
|
| 244 |
+
|
| 245 |
+
def run_holdout(model_path: str, output_path: str = None) -> Dict:
|
| 246 |
+
"""Run the holdout benchmark. Returns scores per category."""
|
| 247 |
+
import torch
|
| 248 |
+
from transformers import AutoModelForImageTextToText, AutoTokenizer
|
| 249 |
+
|
| 250 |
+
print("=" * 60)
|
| 251 |
+
print("TD HOLDOUT BENCHMARK — Truth Test")
|
| 252 |
+
print(f"Model: {model_path}")
|
| 253 |
+
print("=" * 60)
|
| 254 |
+
print("\n WARNING: This benchmark must NEVER influence training!\n")
|
| 255 |
+
|
| 256 |
+
# Validate checkpoint before loading
|
| 257 |
+
model_dir = Path(model_path)
|
| 258 |
+
if model_dir.is_dir():
|
| 259 |
+
if not (model_dir / "config.json").exists():
|
| 260 |
+
raise FileNotFoundError(f"No config.json in {model_path} — stale or corrupt checkpoint!")
|
| 261 |
+
st_files = list(model_dir.glob("*.safetensors"))
|
| 262 |
+
if not st_files and not list(model_dir.glob("*.bin")):
|
| 263 |
+
raise FileNotFoundError(f"No model weights in {model_path} — checkpoint is empty!")
|
| 264 |
+
|
| 265 |
+
# Load model with OOM guard
|
| 266 |
+
try:
|
| 267 |
+
model = AutoModelForImageTextToText.from_pretrained(
|
| 268 |
+
model_path, dtype=torch.bfloat16,
|
| 269 |
+
device_map="auto", trust_remote_code=True
|
| 270 |
+
)
|
| 271 |
+
except (RuntimeError, Exception) as e:
|
| 272 |
+
gc.collect()
|
| 273 |
+
try:
|
| 274 |
+
import torch as _t
|
| 275 |
+
if _t.cuda.is_available():
|
| 276 |
+
_t.cuda.empty_cache()
|
| 277 |
+
except Exception:
|
| 278 |
+
pass
|
| 279 |
+
raise RuntimeError(f"Failed to load model for holdout: {e}") from e
|
| 280 |
+
|
| 281 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 282 |
+
|
| 283 |
+
def ask(question, max_tokens=128):
|
| 284 |
+
chat = (
|
| 285 |
+
"<|im_start|>system\n/no_think<|im_end|>\n"
|
| 286 |
+
f"<|im_start|>user\n{question}<|im_end|>\n"
|
| 287 |
+
"<|im_start|>assistant\n"
|
| 288 |
+
)
|
| 289 |
+
ids = tokenizer(chat, return_tensors="pt").to(model.device)
|
| 290 |
+
try:
|
| 291 |
+
with torch.no_grad():
|
| 292 |
+
out = model.generate(**ids, max_new_tokens=max_tokens, do_sample=False,
|
| 293 |
+
temperature=None, top_p=None)
|
| 294 |
+
except (torch.cuda.OutOfMemoryError, RuntimeError) as e:
|
| 295 |
+
gc.collect()
|
| 296 |
+
if torch.cuda.is_available():
|
| 297 |
+
torch.cuda.empty_cache()
|
| 298 |
+
print(f" OOM during holdout generation: {e}")
|
| 299 |
+
return "[OOM — no response]"
|
| 300 |
+
resp = tokenizer.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True).strip()
|
| 301 |
+
# Guard against garbage: truncate absurdly long responses
|
| 302 |
+
if len(resp) > 5000:
|
| 303 |
+
resp = resp[:5000]
|
| 304 |
+
return resp
|
| 305 |
+
|
| 306 |
+
categories = {
|
| 307 |
+
"math": (HOLDOUT_MATH, check_math, 64),
|
| 308 |
+
"code": (HOLDOUT_CODE, check_code, 300),
|
| 309 |
+
"reasoning": (HOLDOUT_REASONING, check_reasoning, 64),
|
| 310 |
+
"creativity": (HOLDOUT_CREATIVITY, check_creativity, 200),
|
| 311 |
+
"knowledge": (HOLDOUT_KNOWLEDGE, check_knowledge, 64),
|
| 312 |
+
"instruction_following": (HOLDOUT_INSTRUCTION, check_instruction, 128),
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
results = {}
|
| 316 |
+
total_passed = 0
|
| 317 |
+
total_questions = 0
|
| 318 |
+
start = time.time()
|
| 319 |
+
|
| 320 |
+
for cat_name, (tests, check_fn, max_tokens) in categories.items():
|
| 321 |
+
passed = 0
|
| 322 |
+
for test in tests:
|
| 323 |
+
q = test["q"]
|
| 324 |
+
|
| 325 |
+
try:
|
| 326 |
+
response = ask(q, max_tokens)
|
| 327 |
+
except Exception as e:
|
| 328 |
+
print(f" [ERROR] {cat_name} question crashed: {e}")
|
| 329 |
+
response = "[ERROR — question crashed]"
|
| 330 |
+
gc.collect()
|
| 331 |
+
if torch.cuda.is_available():
|
| 332 |
+
torch.cuda.empty_cache()
|
| 333 |
+
|
| 334 |
+
try:
|
| 335 |
+
if check_fn == check_math:
|
| 336 |
+
correct = check_fn(response, test["answer"])
|
| 337 |
+
elif check_fn == check_code:
|
| 338 |
+
correct = check_fn(response, test)
|
| 339 |
+
elif check_fn == check_reasoning:
|
| 340 |
+
correct = check_fn(response, test["answer"])
|
| 341 |
+
elif check_fn == check_creativity:
|
| 342 |
+
correct = check_fn(response, test)
|
| 343 |
+
elif check_fn == check_knowledge:
|
| 344 |
+
correct = check_fn(response, test)
|
| 345 |
+
elif check_fn == check_instruction:
|
| 346 |
+
correct = check_fn(response, test)
|
| 347 |
+
else:
|
| 348 |
+
correct = False
|
| 349 |
+
except Exception as e:
|
| 350 |
+
print(f" [ERROR] Answer checker crashed: {e}")
|
| 351 |
+
correct = False
|
| 352 |
+
|
| 353 |
+
if correct:
|
| 354 |
+
passed += 1
|
| 355 |
+
status = "PASS" if correct else "FAIL"
|
| 356 |
+
short_q = q[:55] + "..." if len(q) > 55 else q
|
| 357 |
+
print(f" [{status}] {cat_name}/{short_q}")
|
| 358 |
+
|
| 359 |
+
score = passed / len(tests) if tests else 0.0
|
| 360 |
+
results[cat_name] = {"score": score, "passed": passed, "total": len(tests)}
|
| 361 |
+
total_passed += passed
|
| 362 |
+
total_questions += len(tests)
|
| 363 |
+
print(f" → {cat_name}: {passed}/{len(tests)} ({score:.0%})\n")
|
| 364 |
+
|
| 365 |
+
elapsed = time.time() - start
|
| 366 |
+
overall = total_passed / total_questions if total_questions > 0 else 0.0
|
| 367 |
+
|
| 368 |
+
print(f"{'='*60}")
|
| 369 |
+
print(f"HOLDOUT RESULT: {total_passed}/{total_questions} ({overall:.0%})")
|
| 370 |
+
print(f"Time: {elapsed/60:.1f} min")
|
| 371 |
+
print(f"{'='*60}")
|
| 372 |
+
|
| 373 |
+
# Cleanup
|
| 374 |
+
del model
|
| 375 |
+
gc.collect()
|
| 376 |
+
try:
|
| 377 |
+
import torch
|
| 378 |
+
if torch.cuda.is_available():
|
| 379 |
+
torch.cuda.empty_cache()
|
| 380 |
+
except ImportError:
|
| 381 |
+
pass
|
| 382 |
+
|
| 383 |
+
report = {
|
| 384 |
+
"model_path": model_path,
|
| 385 |
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 386 |
+
"overall_score": overall,
|
| 387 |
+
"total_passed": total_passed,
|
| 388 |
+
"total_questions": total_questions,
|
| 389 |
+
"duration_min": elapsed / 60,
|
| 390 |
+
"categories": results,
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
if output_path:
|
| 394 |
+
try:
|
| 395 |
+
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
| 396 |
+
with open(output_path, "w") as f:
|
| 397 |
+
json.dump(report, f, indent=2)
|
| 398 |
+
print(f"\nSaved to {output_path}")
|
| 399 |
+
except OSError as e:
|
| 400 |
+
print(f"\nWARNING: Could not save holdout report: {e}")
|
| 401 |
+
|
| 402 |
+
return report
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
if __name__ == "__main__":
|
| 406 |
+
import sys
|
| 407 |
+
model = sys.argv[1] if len(sys.argv) > 1 else ""
|
| 408 |
+
if not model:
|
| 409 |
+
# Auto-detect
|
| 410 |
+
base = Path("td_fuse_outputs/self_improve")
|
| 411 |
+
if base.exists():
|
| 412 |
+
for n in range(50, 0, -1):
|
| 413 |
+
d = base / f"improved_cycle{n}"
|
| 414 |
+
if d.exists() and list(d.glob("*.safetensors")):
|
| 415 |
+
model = str(d)
|
| 416 |
+
break
|
| 417 |
+
if not model:
|
| 418 |
+
healed = Path("td_fuse_outputs/reasoning_healed")
|
| 419 |
+
if healed.exists():
|
| 420 |
+
model = str(healed)
|
| 421 |
+
if not model:
|
| 422 |
+
print("No model found!")
|
| 423 |
+
sys.exit(1)
|
| 424 |
+
|
| 425 |
+
output = sys.argv[2] if len(sys.argv) > 2 else None
|
| 426 |
+
run_holdout(model, output)
|
hugging/td_fuse/td_loop.py
ADDED
|
@@ -0,0 +1,1082 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TD Self-Improvement Loop v1 — The Full Autonomous Pipeline
|
| 3 |
+
|
| 4 |
+
Ties everything together into an endless self-improvement loop:
|
| 5 |
+
|
| 6 |
+
Step 1: Test (benchmark + weight health)
|
| 7 |
+
Step 2: Diagnose (weakness finder)
|
| 8 |
+
Step 3: Find data (dataset finder)
|
| 9 |
+
Step 4: Train + Repair (selfimprove.py)
|
| 10 |
+
Step 5: Verify (re-benchmark)
|
| 11 |
+
→ Loop
|
| 12 |
+
|
| 13 |
+
This is the script you start and walk away from.
|
| 14 |
+
|
| 15 |
+
Usage:
|
| 16 |
+
python td_loop.py # Start from latest checkpoint
|
| 17 |
+
python td_loop.py --model path/to/model # Start from specific model
|
| 18 |
+
python td_loop.py --max-cycles 10 # Stop after 10 cycles
|
| 19 |
+
python td_loop.py --dry-run # Just test+diagnose, don't train
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import json
|
| 23 |
+
import time
|
| 24 |
+
import shutil
|
| 25 |
+
import gc
|
| 26 |
+
import sys
|
| 27 |
+
import argparse
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
from typing import Dict, List, Optional, Tuple
|
| 30 |
+
from dataclasses import dataclass
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ============================================================
|
| 34 |
+
# LOOP CONFIGURATION
|
| 35 |
+
# ============================================================
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class LoopConfig:
|
| 39 |
+
"""Configuration for the self-improvement loop."""
|
| 40 |
+
|
| 41 |
+
# Model
|
| 42 |
+
model_path: str = "" # Auto-detected if empty
|
| 43 |
+
reference_model: str = "Qwen/Qwen3-VL-8B-Instruct"
|
| 44 |
+
|
| 45 |
+
# Directories
|
| 46 |
+
output_dir: str = "td_fuse_outputs/self_improve"
|
| 47 |
+
benchmark_dir: str = "td_fuse_outputs/benchmarks"
|
| 48 |
+
weakness_dir: str = "td_fuse_outputs/weakness_reports"
|
| 49 |
+
data_dir: str = "td_fuse_outputs/training_data"
|
| 50 |
+
|
| 51 |
+
# Loop settings
|
| 52 |
+
max_cycles: int = 0 # 0 = infinite
|
| 53 |
+
dry_run: bool = False # Test + diagnose only
|
| 54 |
+
training_budget: int = 8000 # Samples per cycle
|
| 55 |
+
|
| 56 |
+
# Score targets (when to stop)
|
| 57 |
+
targets: Dict = None
|
| 58 |
+
|
| 59 |
+
# HuggingFace
|
| 60 |
+
hf_token: str = ""
|
| 61 |
+
|
| 62 |
+
# Regression protection
|
| 63 |
+
max_regression_pct: float = 0.05 # 5% drop = rollback
|
| 64 |
+
max_consecutive_regressions: int = 2 # 2 in a row = stop
|
| 65 |
+
|
| 66 |
+
# Safety limits
|
| 67 |
+
max_training_hours: float = 4.0 # Warn if training exceeds this (hours per cycle)
|
| 68 |
+
|
| 69 |
+
def __post_init__(self):
|
| 70 |
+
if self.targets is None:
|
| 71 |
+
self.targets = {
|
| 72 |
+
"math": 0.80,
|
| 73 |
+
"code": 0.70,
|
| 74 |
+
"reasoning": 0.75,
|
| 75 |
+
"creativity": 0.65,
|
| 76 |
+
"knowledge": 0.80,
|
| 77 |
+
"instruction_following": 0.85,
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ============================================================
|
| 82 |
+
# CYCLE STATE TRACKING
|
| 83 |
+
# ============================================================
|
| 84 |
+
|
| 85 |
+
@dataclass
|
| 86 |
+
class CycleResult:
|
| 87 |
+
"""Results from one improvement cycle."""
|
| 88 |
+
cycle_num: int
|
| 89 |
+
benchmark_scores: Dict[str, float]
|
| 90 |
+
weight_health_summary: Dict
|
| 91 |
+
weaknesses_found: int
|
| 92 |
+
data_collected: int
|
| 93 |
+
training_loss: float
|
| 94 |
+
improved: bool # Overall, did we improve?
|
| 95 |
+
regressions: List[str] # Categories that got worse
|
| 96 |
+
duration_seconds: float
|
| 97 |
+
model_path: str
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def load_cycle_history(output_dir: str) -> List[CycleResult]:
|
| 101 |
+
"""Load previous cycle results. Returns empty list if file is corrupt."""
|
| 102 |
+
history_file = Path(output_dir) / "loop_history.json"
|
| 103 |
+
if not history_file.exists():
|
| 104 |
+
return []
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
with open(history_file) as f:
|
| 108 |
+
data = json.load(f)
|
| 109 |
+
return [CycleResult(**c) for c in data]
|
| 110 |
+
except (json.JSONDecodeError, TypeError, KeyError) as e:
|
| 111 |
+
print(f" WARNING: History file corrupt ({e}) — starting fresh")
|
| 112 |
+
# Back up the corrupt file
|
| 113 |
+
backup = history_file.with_suffix(".json.bak")
|
| 114 |
+
try:
|
| 115 |
+
shutil.copy2(str(history_file), str(backup))
|
| 116 |
+
print(f" Backed up corrupt history to {backup}")
|
| 117 |
+
except Exception:
|
| 118 |
+
pass
|
| 119 |
+
return []
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def save_cycle_history(history: List[CycleResult], output_dir: str):
|
| 123 |
+
"""Save cycle history to disk. Atomic write to prevent corruption."""
|
| 124 |
+
history_file = Path(output_dir) / "loop_history.json"
|
| 125 |
+
tmp_file = Path(output_dir) / "loop_history.json.tmp"
|
| 126 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 127 |
+
|
| 128 |
+
data = []
|
| 129 |
+
for c in history:
|
| 130 |
+
d = {
|
| 131 |
+
"cycle_num": c.cycle_num,
|
| 132 |
+
"benchmark_scores": c.benchmark_scores,
|
| 133 |
+
"weight_health_summary": c.weight_health_summary,
|
| 134 |
+
"weaknesses_found": c.weaknesses_found,
|
| 135 |
+
"data_collected": c.data_collected,
|
| 136 |
+
"training_loss": c.training_loss,
|
| 137 |
+
"improved": c.improved,
|
| 138 |
+
"regressions": c.regressions,
|
| 139 |
+
"duration_seconds": c.duration_seconds,
|
| 140 |
+
"model_path": c.model_path,
|
| 141 |
+
}
|
| 142 |
+
data.append(d)
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
# Write to temp file first, then atomic rename
|
| 146 |
+
with open(tmp_file, "w") as f:
|
| 147 |
+
json.dump(data, f, indent=2)
|
| 148 |
+
# Atomic rename — if this fails, the old file is still intact
|
| 149 |
+
tmp_file.replace(history_file)
|
| 150 |
+
except OSError as e:
|
| 151 |
+
print(f" WARNING: Failed to save history ({e}). Disk may be full!")
|
| 152 |
+
# Try to clean up temp file
|
| 153 |
+
try:
|
| 154 |
+
tmp_file.unlink(missing_ok=True)
|
| 155 |
+
except Exception:
|
| 156 |
+
pass
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# ============================================================
|
| 160 |
+
# THE LOOP
|
| 161 |
+
# ============================================================
|
| 162 |
+
|
| 163 |
+
def _setup_logging(output_dir: str):
|
| 164 |
+
"""
|
| 165 |
+
Tee all stdout/stderr to a log file so diagnostics survive terminal disconnects.
|
| 166 |
+
Returns the log file path.
|
| 167 |
+
"""
|
| 168 |
+
import sys as _sys
|
| 169 |
+
|
| 170 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 171 |
+
log_path = Path(output_dir) / f"loop_{time.strftime('%Y%m%d_%H%M%S')}.log"
|
| 172 |
+
|
| 173 |
+
class Tee:
|
| 174 |
+
def __init__(self, original, log_file):
|
| 175 |
+
self.original = original
|
| 176 |
+
self.log_file = log_file
|
| 177 |
+
def write(self, data):
|
| 178 |
+
self.original.write(data)
|
| 179 |
+
try:
|
| 180 |
+
self.log_file.write(data)
|
| 181 |
+
self.log_file.flush()
|
| 182 |
+
except Exception:
|
| 183 |
+
pass # Don't crash if log write fails
|
| 184 |
+
def flush(self):
|
| 185 |
+
self.original.flush()
|
| 186 |
+
try:
|
| 187 |
+
self.log_file.flush()
|
| 188 |
+
except Exception:
|
| 189 |
+
pass
|
| 190 |
+
|
| 191 |
+
log_fh = open(log_path, "a")
|
| 192 |
+
_sys.stdout = Tee(_sys.stdout, log_fh)
|
| 193 |
+
_sys.stderr = Tee(_sys.stderr, log_fh)
|
| 194 |
+
return str(log_path)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _acquire_lockfile(output_dir: str) -> Optional[str]:
|
| 198 |
+
"""
|
| 199 |
+
Prevent two td_loop instances from running simultaneously.
|
| 200 |
+
Returns lockfile path if acquired, None if another instance is running.
|
| 201 |
+
"""
|
| 202 |
+
import os
|
| 203 |
+
lock_path = Path(output_dir) / "td_loop.lock"
|
| 204 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 205 |
+
|
| 206 |
+
if lock_path.exists():
|
| 207 |
+
# Check if the PID in the lockfile is still alive
|
| 208 |
+
try:
|
| 209 |
+
with open(lock_path) as f:
|
| 210 |
+
old_pid = int(f.read().strip())
|
| 211 |
+
os.kill(old_pid, 0) # Signal 0 = check if alive
|
| 212 |
+
# Process is alive — another instance is running
|
| 213 |
+
return None
|
| 214 |
+
except (ValueError, ProcessLookupError, PermissionError, OSError):
|
| 215 |
+
# PID invalid or dead — stale lockfile, safe to remove
|
| 216 |
+
print(f" Removing stale lockfile (PID {lock_path.read_text().strip()} is dead)")
|
| 217 |
+
|
| 218 |
+
with open(lock_path, "w") as f:
|
| 219 |
+
f.write(str(os.getpid()))
|
| 220 |
+
return str(lock_path)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _release_lockfile(output_dir: str):
|
| 224 |
+
"""Release the lockfile."""
|
| 225 |
+
lock_path = Path(output_dir) / "td_loop.lock"
|
| 226 |
+
try:
|
| 227 |
+
lock_path.unlink(missing_ok=True)
|
| 228 |
+
except Exception:
|
| 229 |
+
pass
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def run_loop(cfg: LoopConfig):
|
| 233 |
+
"""
|
| 234 |
+
The main self-improvement loop.
|
| 235 |
+
|
| 236 |
+
Start it and walk away. It will:
|
| 237 |
+
1. Test the model (benchmark + weight health)
|
| 238 |
+
2. Find weaknesses
|
| 239 |
+
3. Search for training data
|
| 240 |
+
4. Train + repair
|
| 241 |
+
5. Verify improvement
|
| 242 |
+
6. Repeat
|
| 243 |
+
"""
|
| 244 |
+
# ── LOCKFILE: Prevent duplicate instances ──
|
| 245 |
+
lock = _acquire_lockfile(cfg.output_dir)
|
| 246 |
+
if lock is None:
|
| 247 |
+
print("ERROR: Another td_loop instance is already running!")
|
| 248 |
+
print(f" Lock file: {Path(cfg.output_dir) / 'td_loop.lock'}")
|
| 249 |
+
print(" If this is wrong (crashed process), delete the lock file and retry.")
|
| 250 |
+
return
|
| 251 |
+
|
| 252 |
+
# ── LOG TEE: Save all output to file ──
|
| 253 |
+
log_path = _setup_logging(cfg.output_dir)
|
| 254 |
+
|
| 255 |
+
# ── SIGINT HANDLER: Graceful shutdown ──
|
| 256 |
+
import signal
|
| 257 |
+
shutdown_requested = [False] # Mutable container for closure
|
| 258 |
+
|
| 259 |
+
def _signal_handler(signum, frame):
|
| 260 |
+
if shutdown_requested[0]:
|
| 261 |
+
print("\n FORCED SHUTDOWN — exiting immediately!")
|
| 262 |
+
_release_lockfile(cfg.output_dir)
|
| 263 |
+
raise SystemExit(1)
|
| 264 |
+
shutdown_requested[0] = True
|
| 265 |
+
print(f"\n SHUTDOWN REQUESTED (Ctrl+C) — will stop after current step completes.")
|
| 266 |
+
print(f" Press Ctrl+C again to force quit.")
|
| 267 |
+
|
| 268 |
+
signal.signal(signal.SIGINT, _signal_handler)
|
| 269 |
+
signal.signal(signal.SIGTERM, _signal_handler)
|
| 270 |
+
|
| 271 |
+
print("\n" + "=" * 60)
|
| 272 |
+
print("TD SELF-IMPROVEMENT LOOP v1")
|
| 273 |
+
print("=" * 60)
|
| 274 |
+
print(f" Max cycles: {'infinite' if cfg.max_cycles == 0 else cfg.max_cycles}")
|
| 275 |
+
print(f" Training budget: {cfg.training_budget} samples/cycle")
|
| 276 |
+
print(f" Dry run: {cfg.dry_run}")
|
| 277 |
+
print(f" Started: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
| 278 |
+
print(f" Log file: {log_path}")
|
| 279 |
+
print("=" * 60)
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
_run_loop_inner(cfg, shutdown_requested)
|
| 283 |
+
finally:
|
| 284 |
+
_release_lockfile(cfg.output_dir)
|
| 285 |
+
print(f"\n Lock released. Log saved to: {log_path}")
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def _run_loop_inner(cfg: LoopConfig, shutdown_requested: list):
|
| 289 |
+
"""Inner loop logic, separated so run_loop can handle cleanup."""
|
| 290 |
+
|
| 291 |
+
# ── GPU HEALTH CHECK ──
|
| 292 |
+
try:
|
| 293 |
+
import torch
|
| 294 |
+
if torch.cuda.is_available():
|
| 295 |
+
gpu_name = torch.cuda.get_device_name(0)
|
| 296 |
+
gpu_mem = torch.cuda.get_device_properties(0).total_mem / 1e9
|
| 297 |
+
print(f"\n GPU: {gpu_name} ({gpu_mem:.1f} GB)")
|
| 298 |
+
if gpu_mem < 20:
|
| 299 |
+
print(f" WARNING: GPU has only {gpu_mem:.1f} GB — Qwen3-VL-8B needs ~24 GB for training!")
|
| 300 |
+
# Quick allocation test — try to allocate 1GB tensor
|
| 301 |
+
test = torch.empty(256, 1024, 1024, dtype=torch.bfloat16, device="cuda")
|
| 302 |
+
del test
|
| 303 |
+
torch.cuda.empty_cache()
|
| 304 |
+
print(f" GPU health: OK (allocation test passed)")
|
| 305 |
+
else:
|
| 306 |
+
print("\n WARNING: No CUDA GPU detected! Training will be extremely slow or fail.")
|
| 307 |
+
except Exception as e:
|
| 308 |
+
print(f"\n WARNING: GPU check failed: {e}")
|
| 309 |
+
|
| 310 |
+
# ── DISK HEALTH CHECK ──
|
| 311 |
+
try:
|
| 312 |
+
disk = shutil.disk_usage(cfg.output_dir if Path(cfg.output_dir).exists() else ".")
|
| 313 |
+
free_gb = disk.free / 1e9
|
| 314 |
+
print(f" Disk: {free_gb:.1f} GB free")
|
| 315 |
+
if free_gb < 50:
|
| 316 |
+
print(f" WARNING: Low disk space! Need ~25 GB per cycle for checkpoints.")
|
| 317 |
+
except Exception:
|
| 318 |
+
pass
|
| 319 |
+
|
| 320 |
+
# Load history
|
| 321 |
+
history = load_cycle_history(cfg.output_dir)
|
| 322 |
+
cycle_num = len(history) + 1
|
| 323 |
+
consecutive_regressions = 0
|
| 324 |
+
|
| 325 |
+
# Auto-detect model if not specified
|
| 326 |
+
if not cfg.model_path:
|
| 327 |
+
cfg.model_path = _auto_detect_model()
|
| 328 |
+
|
| 329 |
+
print(f"\n Starting model: {cfg.model_path}")
|
| 330 |
+
print(f" Starting at cycle: {cycle_num}")
|
| 331 |
+
|
| 332 |
+
# The loop
|
| 333 |
+
while True:
|
| 334 |
+
# ── GRACEFUL SHUTDOWN CHECK ──
|
| 335 |
+
if shutdown_requested[0]:
|
| 336 |
+
print(f"\n Shutdown requested — stopping before cycle {cycle_num}")
|
| 337 |
+
_print_final_summary(history)
|
| 338 |
+
return
|
| 339 |
+
if cfg.max_cycles > 0 and cycle_num > cfg.max_cycles:
|
| 340 |
+
print(f"\n Reached max cycles ({cfg.max_cycles}). Stopping.")
|
| 341 |
+
break
|
| 342 |
+
|
| 343 |
+
cycle_start = time.time()
|
| 344 |
+
print(f"\n{'#' * 60}")
|
| 345 |
+
print(f"# CYCLE {cycle_num}")
|
| 346 |
+
print(f"# {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
| 347 |
+
print(f"{'#' * 60}")
|
| 348 |
+
|
| 349 |
+
# ============================================
|
| 350 |
+
# STEP 1: TEST (benchmark + weight health)
|
| 351 |
+
# ============================================
|
| 352 |
+
print(f"\n{'='*50}")
|
| 353 |
+
print(f"STEP 1/{5 if not cfg.dry_run else 2}: TEST MODEL")
|
| 354 |
+
print(f"{'='*50}")
|
| 355 |
+
|
| 356 |
+
# 1A: Run benchmark (with crash recovery)
|
| 357 |
+
try:
|
| 358 |
+
benchmark_scores = _run_benchmark(cfg.model_path, cfg.benchmark_dir, cycle_num)
|
| 359 |
+
except Exception as e:
|
| 360 |
+
print(f"\n CRITICAL: Benchmark crashed: {e}")
|
| 361 |
+
import traceback
|
| 362 |
+
traceback.print_exc()
|
| 363 |
+
# Can't proceed without benchmark scores — try to clean GPU and retry once
|
| 364 |
+
gc.collect()
|
| 365 |
+
try:
|
| 366 |
+
import torch
|
| 367 |
+
if torch.cuda.is_available():
|
| 368 |
+
torch.cuda.empty_cache()
|
| 369 |
+
except ImportError:
|
| 370 |
+
pass
|
| 371 |
+
try:
|
| 372 |
+
print(" Retrying benchmark after GPU cleanup...")
|
| 373 |
+
benchmark_scores = _run_benchmark(cfg.model_path, cfg.benchmark_dir, cycle_num)
|
| 374 |
+
except Exception as e2:
|
| 375 |
+
print(f" Benchmark failed twice: {e2}")
|
| 376 |
+
print(f" Skipping cycle {cycle_num}")
|
| 377 |
+
consecutive_regressions += 1
|
| 378 |
+
if consecutive_regressions >= cfg.max_consecutive_regressions:
|
| 379 |
+
print(f"\n {consecutive_regressions} consecutive failures — STOPPING LOOP")
|
| 380 |
+
break
|
| 381 |
+
cycle_num += 1
|
| 382 |
+
continue
|
| 383 |
+
|
| 384 |
+
# 1B: Analyze weight health
|
| 385 |
+
health_data = _analyze_weights(cfg.model_path, cfg.reference_model, cfg.output_dir)
|
| 386 |
+
|
| 387 |
+
# ============================================
|
| 388 |
+
# STEP 2: DIAGNOSE (find weaknesses)
|
| 389 |
+
# ============================================
|
| 390 |
+
print(f"\n{'='*50}")
|
| 391 |
+
print(f"STEP 2/{5 if not cfg.dry_run else 2}: DIAGNOSE WEAKNESSES")
|
| 392 |
+
print(f"{'='*50}")
|
| 393 |
+
|
| 394 |
+
try:
|
| 395 |
+
weakness_report = _find_weaknesses(
|
| 396 |
+
benchmark_path=str(Path(cfg.benchmark_dir) / f"cycle{cycle_num}" / "results.json"),
|
| 397 |
+
health_data=health_data,
|
| 398 |
+
targets=cfg.targets,
|
| 399 |
+
budget=cfg.training_budget,
|
| 400 |
+
output_dir=cfg.weakness_dir,
|
| 401 |
+
cycle_num=cycle_num,
|
| 402 |
+
)
|
| 403 |
+
except Exception as e:
|
| 404 |
+
print(f"\n WARNING: Weakness analysis crashed: {e}")
|
| 405 |
+
print(f" Using fallback: train all categories equally")
|
| 406 |
+
weakness_report = {
|
| 407 |
+
"search_plan": [{"category": c, "budget": cfg.training_budget // 6}
|
| 408 |
+
for c in ["math", "code", "reasoning", "creativity",
|
| 409 |
+
"knowledge", "instruction_following"]],
|
| 410 |
+
"data_allocation": {c: cfg.training_budget // 6
|
| 411 |
+
for c in ["math", "code", "reasoning", "creativity",
|
| 412 |
+
"knowledge", "instruction_following"]},
|
| 413 |
+
"lora_target_layers": list(range(36)),
|
| 414 |
+
"norms_to_repair": [],
|
| 415 |
+
"total_weaknesses": 6,
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
if cfg.dry_run:
|
| 419 |
+
print("\n DRY RUN — stopping after diagnosis.")
|
| 420 |
+
result = CycleResult(
|
| 421 |
+
cycle_num=cycle_num,
|
| 422 |
+
benchmark_scores=benchmark_scores,
|
| 423 |
+
weight_health_summary=health_data.get("summary", {}),
|
| 424 |
+
weaknesses_found=weakness_report.get("total_weaknesses", 0),
|
| 425 |
+
data_collected=0,
|
| 426 |
+
training_loss=0.0,
|
| 427 |
+
improved=False,
|
| 428 |
+
regressions=[],
|
| 429 |
+
duration_seconds=time.time() - cycle_start,
|
| 430 |
+
model_path=cfg.model_path,
|
| 431 |
+
)
|
| 432 |
+
history.append(result)
|
| 433 |
+
save_cycle_history(history, cfg.output_dir)
|
| 434 |
+
break
|
| 435 |
+
|
| 436 |
+
# ============================================
|
| 437 |
+
# STEP 3: FIND TRAINING DATA
|
| 438 |
+
# ============================================
|
| 439 |
+
print(f"\n{'='*50}")
|
| 440 |
+
print(f"STEP 3/5: FIND TRAINING DATA")
|
| 441 |
+
print(f"{'='*50}")
|
| 442 |
+
|
| 443 |
+
# Guard: If no weaknesses found, skip data search + training
|
| 444 |
+
if not weakness_report.get("search_plan"):
|
| 445 |
+
print("\n No weaknesses found — all targets met! Skipping training.")
|
| 446 |
+
print(" (This shouldn't happen unless the model is already great)")
|
| 447 |
+
training_data = {"total_samples": 0, "categories_covered": [], "categories_missing": []}
|
| 448 |
+
else:
|
| 449 |
+
try:
|
| 450 |
+
training_data = _find_training_data(
|
| 451 |
+
weakness_report=weakness_report,
|
| 452 |
+
output_dir=cfg.data_dir,
|
| 453 |
+
hf_token=cfg.hf_token,
|
| 454 |
+
cycle_num=cycle_num,
|
| 455 |
+
)
|
| 456 |
+
except Exception as e:
|
| 457 |
+
print(f"\n WARNING: Data finder crashed: {e}")
|
| 458 |
+
print(f" Continuing with generic datasets (selfimprove.py fallback)")
|
| 459 |
+
training_data = {"total_samples": 0, "categories_covered": [], "categories_missing": []}
|
| 460 |
+
|
| 461 |
+
# ============================================
|
| 462 |
+
# STEP 4: TRAIN + REPAIR
|
| 463 |
+
# ============================================
|
| 464 |
+
print(f"\n{'='*50}")
|
| 465 |
+
print(f"STEP 4/5: TRAIN + REPAIR")
|
| 466 |
+
print(f"{'='*50}")
|
| 467 |
+
|
| 468 |
+
# ── DISK SPACE PRE-CHECK ──
|
| 469 |
+
try:
|
| 470 |
+
disk = shutil.disk_usage(cfg.output_dir if Path(cfg.output_dir).exists() else ".")
|
| 471 |
+
free_gb = disk.free / 1e9
|
| 472 |
+
if free_gb < 10:
|
| 473 |
+
print(f"\n CRITICAL: Only {free_gb:.1f} GB disk free — skipping training!")
|
| 474 |
+
print(f" Need at least 10 GB to safely save a checkpoint.")
|
| 475 |
+
consecutive_regressions += 1
|
| 476 |
+
cycle_num += 1
|
| 477 |
+
continue
|
| 478 |
+
except Exception:
|
| 479 |
+
pass # Can't check disk — proceed anyway
|
| 480 |
+
|
| 481 |
+
try:
|
| 482 |
+
new_model_path, training_loss = _train_model(
|
| 483 |
+
model_path=cfg.model_path,
|
| 484 |
+
training_data_path=str(Path(cfg.data_dir) / f"cycle{cycle_num}" / "training_data.jsonl"),
|
| 485 |
+
lora_targets=weakness_report.get("lora_target_layers", list(range(36))),
|
| 486 |
+
norms_to_repair=weakness_report.get("norms_to_repair", []),
|
| 487 |
+
reference_model=cfg.reference_model,
|
| 488 |
+
output_dir=cfg.output_dir,
|
| 489 |
+
cycle_num=cycle_num,
|
| 490 |
+
)
|
| 491 |
+
except Exception as e:
|
| 492 |
+
print(f"\n CRITICAL: Training crashed: {e}")
|
| 493 |
+
print(f" Skipping this cycle and retrying with current model...")
|
| 494 |
+
import traceback
|
| 495 |
+
traceback.print_exc()
|
| 496 |
+
# Save partial results and continue to next cycle
|
| 497 |
+
cycle_duration = time.time() - cycle_start
|
| 498 |
+
result = CycleResult(
|
| 499 |
+
cycle_num=cycle_num,
|
| 500 |
+
benchmark_scores=benchmark_scores,
|
| 501 |
+
weight_health_summary=health_data.get("summary", {}),
|
| 502 |
+
weaknesses_found=weakness_report.get("total_weaknesses", 0),
|
| 503 |
+
data_collected=training_data.get("total_samples", 0),
|
| 504 |
+
training_loss=0.0,
|
| 505 |
+
improved=False,
|
| 506 |
+
regressions=["TRAINING_CRASH"],
|
| 507 |
+
duration_seconds=cycle_duration,
|
| 508 |
+
model_path=cfg.model_path,
|
| 509 |
+
)
|
| 510 |
+
history.append(result)
|
| 511 |
+
save_cycle_history(history, cfg.output_dir)
|
| 512 |
+
consecutive_regressions += 1
|
| 513 |
+
if consecutive_regressions >= cfg.max_consecutive_regressions:
|
| 514 |
+
print(f"\n {consecutive_regressions} consecutive failures — STOPPING LOOP")
|
| 515 |
+
break
|
| 516 |
+
cycle_num += 1
|
| 517 |
+
# Free GPU memory before retry
|
| 518 |
+
gc.collect()
|
| 519 |
+
try:
|
| 520 |
+
import torch
|
| 521 |
+
if torch.cuda.is_available():
|
| 522 |
+
torch.cuda.empty_cache()
|
| 523 |
+
except ImportError:
|
| 524 |
+
pass
|
| 525 |
+
continue
|
| 526 |
+
|
| 527 |
+
# ── TRAINING DURATION WATCHDOG ──
|
| 528 |
+
train_elapsed_h = (time.time() - cycle_start) / 3600
|
| 529 |
+
if train_elapsed_h > cfg.max_training_hours:
|
| 530 |
+
print(f"\n WARNING: Training took {train_elapsed_h:.1f}h (limit: {cfg.max_training_hours}h)")
|
| 531 |
+
print(f" This may indicate a stuck process or oversized dataset.")
|
| 532 |
+
|
| 533 |
+
# ============================================
|
| 534 |
+
# STEP 5: VERIFY
|
| 535 |
+
# ============================================
|
| 536 |
+
print(f"\n{'='*50}")
|
| 537 |
+
print(f"STEP 5/5: VERIFY IMPROVEMENT")
|
| 538 |
+
print(f"{'='*50}")
|
| 539 |
+
|
| 540 |
+
try:
|
| 541 |
+
new_scores = _run_benchmark(new_model_path, cfg.benchmark_dir, cycle_num, suffix="_post")
|
| 542 |
+
except Exception as e:
|
| 543 |
+
print(f"\n CRITICAL: Post-training benchmark crashed: {e}")
|
| 544 |
+
print(f" Cannot verify improvement — treating as regression for safety.")
|
| 545 |
+
import traceback
|
| 546 |
+
traceback.print_exc()
|
| 547 |
+
new_scores = {} # Empty = all categories missing = treated as regression
|
| 548 |
+
|
| 549 |
+
# Compare scores
|
| 550 |
+
improved, regressions = _compare_scores(benchmark_scores, new_scores, cfg.max_regression_pct)
|
| 551 |
+
|
| 552 |
+
if improved:
|
| 553 |
+
print(f"\n ✓ IMPROVEMENT CONFIRMED — cycle {cycle_num} successful!")
|
| 554 |
+
consecutive_regressions = 0
|
| 555 |
+
cfg.model_path = new_model_path # Use improved model next cycle
|
| 556 |
+
else:
|
| 557 |
+
print(f"\n ✗ REGRESSION DETECTED in: {', '.join(regressions)}")
|
| 558 |
+
consecutive_regressions += 1
|
| 559 |
+
|
| 560 |
+
if consecutive_regressions >= cfg.max_consecutive_regressions:
|
| 561 |
+
print(f"\n {consecutive_regressions} consecutive regressions — STOPPING LOOP")
|
| 562 |
+
print(f" The model may have hit its improvement ceiling.")
|
| 563 |
+
break
|
| 564 |
+
|
| 565 |
+
# Still use the new model — small regressions can recover
|
| 566 |
+
cfg.model_path = new_model_path
|
| 567 |
+
|
| 568 |
+
# Save cycle results
|
| 569 |
+
cycle_duration = time.time() - cycle_start
|
| 570 |
+
result = CycleResult(
|
| 571 |
+
cycle_num=cycle_num,
|
| 572 |
+
benchmark_scores=new_scores,
|
| 573 |
+
weight_health_summary=health_data.get("summary", {}),
|
| 574 |
+
weaknesses_found=weakness_report.get("total_weaknesses", 0),
|
| 575 |
+
data_collected=training_data.get("total_samples", 0),
|
| 576 |
+
training_loss=training_loss,
|
| 577 |
+
improved=improved,
|
| 578 |
+
regressions=regressions,
|
| 579 |
+
duration_seconds=cycle_duration,
|
| 580 |
+
model_path=new_model_path,
|
| 581 |
+
)
|
| 582 |
+
history.append(result)
|
| 583 |
+
save_cycle_history(history, cfg.output_dir)
|
| 584 |
+
|
| 585 |
+
# Print cycle summary
|
| 586 |
+
_print_cycle_summary(result, history)
|
| 587 |
+
|
| 588 |
+
# Check if all targets met
|
| 589 |
+
if _all_targets_met(new_scores, cfg.targets):
|
| 590 |
+
print(f"\n{'='*60}")
|
| 591 |
+
print(f"TARGET PERFORMANCE REACHED AFTER {cycle_num} CYCLES!")
|
| 592 |
+
print(f"{'='*60}")
|
| 593 |
+
break
|
| 594 |
+
|
| 595 |
+
# ── PERIODIC FULL GC between cycles ──
|
| 596 |
+
# After a full cycle, Python has accumulated lots of garbage
|
| 597 |
+
# (deleted models, tokenizers, datasets). Force collection.
|
| 598 |
+
gc.collect()
|
| 599 |
+
try:
|
| 600 |
+
import torch
|
| 601 |
+
if torch.cuda.is_available():
|
| 602 |
+
torch.cuda.empty_cache()
|
| 603 |
+
# Print GPU memory status for debugging
|
| 604 |
+
allocated = torch.cuda.memory_allocated() / 1e9
|
| 605 |
+
reserved = torch.cuda.memory_reserved() / 1e9
|
| 606 |
+
print(f"\n GPU memory: {allocated:.1f} GB allocated, {reserved:.1f} GB reserved")
|
| 607 |
+
except ImportError:
|
| 608 |
+
pass
|
| 609 |
+
|
| 610 |
+
# ── SHUTDOWN CHECK before starting next cycle ──
|
| 611 |
+
if shutdown_requested[0]:
|
| 612 |
+
print(f"\n Shutdown requested — stopping after cycle {cycle_num}")
|
| 613 |
+
break
|
| 614 |
+
|
| 615 |
+
cycle_num += 1
|
| 616 |
+
|
| 617 |
+
# Final summary
|
| 618 |
+
_print_final_summary(history)
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
# ============================================================
|
| 622 |
+
# STEP IMPLEMENTATIONS
|
| 623 |
+
# ============================================================
|
| 624 |
+
|
| 625 |
+
def _auto_detect_model() -> str:
|
| 626 |
+
"""Find the best starting model."""
|
| 627 |
+
base = Path("td_fuse_outputs/self_improve")
|
| 628 |
+
|
| 629 |
+
# Check for highest cycle output
|
| 630 |
+
if base.exists():
|
| 631 |
+
for n in range(50, 0, -1):
|
| 632 |
+
cycle_dir = base / f"improved_cycle{n}"
|
| 633 |
+
if cycle_dir.exists() and list(cycle_dir.glob("*.safetensors")):
|
| 634 |
+
print(f" Found cycle {n} output at {cycle_dir}")
|
| 635 |
+
return str(cycle_dir)
|
| 636 |
+
|
| 637 |
+
# Check for old "improved" folder
|
| 638 |
+
old_improved = base / "improved"
|
| 639 |
+
if old_improved.exists() and list(old_improved.glob("*.safetensors")):
|
| 640 |
+
return str(old_improved)
|
| 641 |
+
|
| 642 |
+
# Fall back to reasoning_healed
|
| 643 |
+
healed = Path("td_fuse_outputs/reasoning_healed")
|
| 644 |
+
if healed.exists() and list(healed.glob("*.safetensors")):
|
| 645 |
+
return str(healed)
|
| 646 |
+
|
| 647 |
+
raise FileNotFoundError("No model found! Need a starting model checkpoint.")
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _run_benchmark(
|
| 651 |
+
model_path: str,
|
| 652 |
+
benchmark_dir: str,
|
| 653 |
+
cycle_num: int,
|
| 654 |
+
suffix: str = "",
|
| 655 |
+
) -> Dict[str, float]:
|
| 656 |
+
"""Run td_benchmark.py and return scores per category."""
|
| 657 |
+
from td_benchmark import BenchmarkConfig, run_benchmark
|
| 658 |
+
|
| 659 |
+
output_dir = str(Path(benchmark_dir) / f"cycle{cycle_num}{suffix}")
|
| 660 |
+
results_path = str(Path(output_dir) / "results.json")
|
| 661 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 662 |
+
|
| 663 |
+
# Validate checkpoint before loading
|
| 664 |
+
mp = Path(model_path)
|
| 665 |
+
if mp.is_dir() and not (mp / "config.json").exists():
|
| 666 |
+
raise FileNotFoundError(f"No config.json in {model_path} — stale or corrupt checkpoint")
|
| 667 |
+
|
| 668 |
+
config = BenchmarkConfig(
|
| 669 |
+
model_path=model_path,
|
| 670 |
+
output_dir=output_dir,
|
| 671 |
+
output_path=results_path,
|
| 672 |
+
disable_thinking=True, # SPEED: Skip thinking for benchmark
|
| 673 |
+
)
|
| 674 |
+
|
| 675 |
+
print(f" Running benchmark on {model_path}...")
|
| 676 |
+
results = run_benchmark(config)
|
| 677 |
+
|
| 678 |
+
# Guard: results might be None or not a dict if benchmark crashed internally
|
| 679 |
+
if not isinstance(results, dict):
|
| 680 |
+
print(f" WARNING: Benchmark returned {type(results)} instead of dict — treating as empty")
|
| 681 |
+
results = {}
|
| 682 |
+
|
| 683 |
+
# Extract scores per category (already 0.0-1.0 after our fix)
|
| 684 |
+
scores = {}
|
| 685 |
+
for category, data in results.get("categories", {}).items():
|
| 686 |
+
if isinstance(data, dict):
|
| 687 |
+
scores[category] = data.get("score", 0.0)
|
| 688 |
+
else:
|
| 689 |
+
scores[category] = 0.0
|
| 690 |
+
|
| 691 |
+
print(f" Benchmark scores:")
|
| 692 |
+
for cat, score in sorted(scores.items()):
|
| 693 |
+
print(f" {cat}: {score:.0%}")
|
| 694 |
+
|
| 695 |
+
# Clean up model from GPU memory
|
| 696 |
+
gc.collect()
|
| 697 |
+
try:
|
| 698 |
+
import torch
|
| 699 |
+
if torch.cuda.is_available():
|
| 700 |
+
torch.cuda.empty_cache()
|
| 701 |
+
except ImportError:
|
| 702 |
+
pass
|
| 703 |
+
|
| 704 |
+
return scores
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
def _analyze_weights(
|
| 708 |
+
model_path: str,
|
| 709 |
+
reference_model: str,
|
| 710 |
+
output_dir: str,
|
| 711 |
+
) -> Dict:
|
| 712 |
+
"""Run weight health analysis from selfimprove.py."""
|
| 713 |
+
try:
|
| 714 |
+
from selfimprove import (
|
| 715 |
+
analyze_model_weights,
|
| 716 |
+
compute_health_scores,
|
| 717 |
+
compute_damage_scores,
|
| 718 |
+
compute_coherence_scores,
|
| 719 |
+
detect_clogged_norms,
|
| 720 |
+
)
|
| 721 |
+
|
| 722 |
+
print(f" Analyzing model weights...")
|
| 723 |
+
model_analysis = analyze_model_weights(model_path, "TD model")
|
| 724 |
+
health_scores = compute_health_scores(model_analysis)
|
| 725 |
+
coherence_scores = compute_coherence_scores(model_analysis)
|
| 726 |
+
clogged_norms = detect_clogged_norms(health_scores)
|
| 727 |
+
|
| 728 |
+
# Reference comparison — skip if no reference cached (saves ~5 min)
|
| 729 |
+
# selfimprove.py downloads it during training anyway
|
| 730 |
+
damage_scores = {}
|
| 731 |
+
ref_path = Path(output_dir) / "reference_weights"
|
| 732 |
+
if ref_path.exists() and list(ref_path.glob("*.safetensors")):
|
| 733 |
+
print(f" Found cached reference at {ref_path}, comparing...")
|
| 734 |
+
ref_analysis = analyze_model_weights(str(ref_path), "Reference")
|
| 735 |
+
damage_scores = compute_damage_scores(model_analysis, ref_analysis)
|
| 736 |
+
else:
|
| 737 |
+
print(f" No cached reference — skipping damage comparison (saves time)")
|
| 738 |
+
|
| 739 |
+
# Build summary
|
| 740 |
+
unhealthy = sum(1 for h in health_scores.values() if h["health_score"] > 0.2)
|
| 741 |
+
summary = {
|
| 742 |
+
"total_tensors": len(health_scores),
|
| 743 |
+
"unhealthy_tensors": unhealthy,
|
| 744 |
+
"clogged_norms": len(clogged_norms),
|
| 745 |
+
"coherence_issues": sum(1 for s in coherence_scores.values() if s > 0.3),
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
print(f" Weight health: {unhealthy}/{len(health_scores)} tensors with issues")
|
| 749 |
+
print(f" Clogged norms: {len(clogged_norms)}")
|
| 750 |
+
|
| 751 |
+
# Clean up
|
| 752 |
+
gc.collect()
|
| 753 |
+
|
| 754 |
+
return {
|
| 755 |
+
"health_scores": health_scores,
|
| 756 |
+
"damage_scores": damage_scores,
|
| 757 |
+
"coherence_scores": coherence_scores,
|
| 758 |
+
"clogged_norms": clogged_norms,
|
| 759 |
+
"summary": summary,
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
except Exception as e:
|
| 763 |
+
print(f" Warning: Weight analysis failed: {e}")
|
| 764 |
+
return {"summary": {}, "health_scores": {}, "clogged_norms": []}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def _find_weaknesses(
|
| 768 |
+
benchmark_path: str,
|
| 769 |
+
health_data: Dict,
|
| 770 |
+
targets: Dict,
|
| 771 |
+
budget: int,
|
| 772 |
+
output_dir: str,
|
| 773 |
+
cycle_num: int,
|
| 774 |
+
) -> Dict:
|
| 775 |
+
"""Run td_weakness.py analysis."""
|
| 776 |
+
from td_weakness import run_weakness_analysis
|
| 777 |
+
|
| 778 |
+
output_path = str(Path(output_dir) / f"cycle{cycle_num}" / "weakness_report.json")
|
| 779 |
+
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
| 780 |
+
|
| 781 |
+
report = run_weakness_analysis(
|
| 782 |
+
benchmark_path=benchmark_path,
|
| 783 |
+
health_scores=health_data.get("health_scores") or {},
|
| 784 |
+
damage_scores=health_data.get("damage_scores") or {},
|
| 785 |
+
coherence_scores=health_data.get("coherence_scores") or {},
|
| 786 |
+
clogged_norms=health_data.get("clogged_norms") or [],
|
| 787 |
+
targets=targets,
|
| 788 |
+
total_budget=budget,
|
| 789 |
+
output_path=output_path,
|
| 790 |
+
)
|
| 791 |
+
|
| 792 |
+
return {
|
| 793 |
+
"search_plan": report.search_plan,
|
| 794 |
+
"data_allocation": report.data_allocation,
|
| 795 |
+
"lora_target_layers": report.lora_target_layers,
|
| 796 |
+
"norms_to_repair": report.norms_to_repair,
|
| 797 |
+
"total_weaknesses": len(report.answer_weaknesses) + len(report.weight_weaknesses),
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
|
| 801 |
+
def _find_training_data(
|
| 802 |
+
weakness_report: Dict,
|
| 803 |
+
output_dir: str,
|
| 804 |
+
hf_token: str,
|
| 805 |
+
cycle_num: int,
|
| 806 |
+
) -> Dict:
|
| 807 |
+
"""Run td_data_finder.py to search for and download data."""
|
| 808 |
+
from td_data_finder import find_training_data
|
| 809 |
+
|
| 810 |
+
cycle_data_dir = str(Path(output_dir) / f"cycle{cycle_num}")
|
| 811 |
+
|
| 812 |
+
result = find_training_data(
|
| 813 |
+
search_plan=weakness_report["search_plan"],
|
| 814 |
+
data_allocation=weakness_report["data_allocation"],
|
| 815 |
+
output_dir=cycle_data_dir,
|
| 816 |
+
hf_token=hf_token if hf_token else None,
|
| 817 |
+
)
|
| 818 |
+
|
| 819 |
+
return {
|
| 820 |
+
"total_samples": result.total_samples,
|
| 821 |
+
"categories_covered": result.categories_covered,
|
| 822 |
+
"categories_missing": result.categories_missing,
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
|
| 826 |
+
def _train_model(
|
| 827 |
+
model_path: str,
|
| 828 |
+
training_data_path: str,
|
| 829 |
+
lora_targets: List[int],
|
| 830 |
+
norms_to_repair: List[str],
|
| 831 |
+
reference_model: str,
|
| 832 |
+
output_dir: str,
|
| 833 |
+
cycle_num: int,
|
| 834 |
+
) -> Tuple[str, float]:
|
| 835 |
+
"""
|
| 836 |
+
Run training using selfimprove.py's training pipeline.
|
| 837 |
+
|
| 838 |
+
Returns: (new_model_path, final_training_loss)
|
| 839 |
+
"""
|
| 840 |
+
from selfimprove import SelfImproveConfig, run_cycle
|
| 841 |
+
|
| 842 |
+
cfg = SelfImproveConfig()
|
| 843 |
+
# CRITICAL: Set model_path BEFORE run_cycle so auto_detect_model_path
|
| 844 |
+
# doesn't pick a different checkpoint
|
| 845 |
+
cfg.model_path = model_path
|
| 846 |
+
cfg.reference_model = reference_model
|
| 847 |
+
cfg.output_dir = output_dir
|
| 848 |
+
|
| 849 |
+
# Apply mega mode settings (always for loop cycles)
|
| 850 |
+
cfg.mega_mode = True
|
| 851 |
+
cfg.top_n_layers = 36
|
| 852 |
+
cfg.min_improvement_score = 0.0
|
| 853 |
+
cfg.lora_r = 128
|
| 854 |
+
cfg.lora_alpha = 256
|
| 855 |
+
cfg.train_batch = 4
|
| 856 |
+
cfg.train_grad_accum = 4
|
| 857 |
+
cfg.num_train_samples = 8000
|
| 858 |
+
cfg.learning_rate = 1e-5
|
| 859 |
+
cfg.norm_learning_rate = 1e-3
|
| 860 |
+
cfg.save_steps = 200
|
| 861 |
+
|
| 862 |
+
# Pass targeted training data from td_data_finder
|
| 863 |
+
# selfimprove.py will use this INSTEAD of its hardcoded datasets
|
| 864 |
+
training_data_file = Path(training_data_path)
|
| 865 |
+
if training_data_file.exists() and training_data_file.stat().st_size > 0:
|
| 866 |
+
cfg.custom_data_path = str(training_data_file)
|
| 867 |
+
print(f" Using targeted training data: {training_data_file}")
|
| 868 |
+
else:
|
| 869 |
+
print(f" No targeted data found — selfimprove.py will use generic datasets")
|
| 870 |
+
|
| 871 |
+
# Figure out what cycle_num selfimprove.py will ACTUALLY use
|
| 872 |
+
# so we know where the output will be saved
|
| 873 |
+
import re as _re
|
| 874 |
+
si_match = _re.search(r'improved_cycle(\d+)', model_path)
|
| 875 |
+
if si_match:
|
| 876 |
+
si_cycle_num = int(si_match.group(1)) + 1
|
| 877 |
+
elif "improve" in model_path:
|
| 878 |
+
si_cycle_num = 2
|
| 879 |
+
else:
|
| 880 |
+
si_cycle_num = 1
|
| 881 |
+
|
| 882 |
+
print(f" Starting training (loop cycle {cycle_num}, selfimprove cycle {si_cycle_num})...")
|
| 883 |
+
print(f" Model: {model_path}")
|
| 884 |
+
print(f" Output will be: {output_dir}/improved_cycle{si_cycle_num}")
|
| 885 |
+
run_cycle(cfg)
|
| 886 |
+
|
| 887 |
+
# Find the output model — use selfimprove's cycle numbering
|
| 888 |
+
new_model_path = str(Path(output_dir) / f"improved_cycle{si_cycle_num}")
|
| 889 |
+
if not Path(new_model_path).exists():
|
| 890 |
+
# Fallback: try loop's cycle number
|
| 891 |
+
alt_path = str(Path(output_dir) / f"improved_cycle{cycle_num}")
|
| 892 |
+
if Path(alt_path).exists():
|
| 893 |
+
new_model_path = alt_path
|
| 894 |
+
else:
|
| 895 |
+
# Last resort: scan for any improved_cycle* dir
|
| 896 |
+
base = Path(output_dir)
|
| 897 |
+
found = False
|
| 898 |
+
for n in range(50, 0, -1):
|
| 899 |
+
check = base / f"improved_cycle{n}"
|
| 900 |
+
if check.exists() and list(check.glob("*.safetensors")):
|
| 901 |
+
new_model_path = str(check)
|
| 902 |
+
print(f" Output path mismatch — found model at {check}")
|
| 903 |
+
found = True
|
| 904 |
+
break
|
| 905 |
+
if not found:
|
| 906 |
+
old_improved = base / "improved"
|
| 907 |
+
if old_improved.exists():
|
| 908 |
+
new_model_path = str(old_improved)
|
| 909 |
+
else:
|
| 910 |
+
# Training produced nothing — return original model
|
| 911 |
+
print(f" ERROR: No output model found! Training may have failed silently.")
|
| 912 |
+
print(f" Returning original model: {model_path}")
|
| 913 |
+
return model_path, 0.0
|
| 914 |
+
|
| 915 |
+
# Validate the output model has actual weights
|
| 916 |
+
out_p = Path(new_model_path)
|
| 917 |
+
if out_p.is_dir():
|
| 918 |
+
st_files = list(out_p.glob("*.safetensors"))
|
| 919 |
+
if not st_files:
|
| 920 |
+
print(f" ERROR: Output model has no safetensors files! Returning original.")
|
| 921 |
+
return model_path, 0.0
|
| 922 |
+
total_sz = sum(f.stat().st_size for f in st_files) / 1e9
|
| 923 |
+
if total_sz < 1.0:
|
| 924 |
+
print(f" ERROR: Output model only {total_sz:.2f} GB — likely corrupt. Returning original.")
|
| 925 |
+
return model_path, 0.0
|
| 926 |
+
|
| 927 |
+
# Try to get final loss from training log
|
| 928 |
+
training_loss = 0.0
|
| 929 |
+
log_file = Path(new_model_path) / "training_log.json"
|
| 930 |
+
if log_file.exists():
|
| 931 |
+
try:
|
| 932 |
+
with open(log_file) as f:
|
| 933 |
+
log = json.load(f)
|
| 934 |
+
training_loss = log.get("final_loss", 0.0)
|
| 935 |
+
except Exception:
|
| 936 |
+
pass
|
| 937 |
+
|
| 938 |
+
return new_model_path, training_loss
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
def _compare_scores(
|
| 942 |
+
old_scores: Dict[str, float],
|
| 943 |
+
new_scores: Dict[str, float],
|
| 944 |
+
max_regression: float,
|
| 945 |
+
) -> Tuple[bool, List[str]]:
|
| 946 |
+
"""
|
| 947 |
+
Compare benchmark scores before and after training.
|
| 948 |
+
Returns: (overall_improved, list_of_regressed_categories)
|
| 949 |
+
"""
|
| 950 |
+
regressions = []
|
| 951 |
+
improvements = 0
|
| 952 |
+
|
| 953 |
+
for category in old_scores:
|
| 954 |
+
if category not in new_scores:
|
| 955 |
+
print(f" ? {category}: MISSING from new scores — treating as regression!")
|
| 956 |
+
regressions.append(category)
|
| 957 |
+
continue
|
| 958 |
+
|
| 959 |
+
old = old_scores[category]
|
| 960 |
+
new = new_scores[category]
|
| 961 |
+
diff = new - old
|
| 962 |
+
|
| 963 |
+
if diff > 0.01: # Meaningful improvement (>1%)
|
| 964 |
+
improvements += 1
|
| 965 |
+
print(f" ↑ {category}: {old:.1%} → {new:.1%} (+{diff:.1%})")
|
| 966 |
+
elif diff < -max_regression: # Regression beyond threshold
|
| 967 |
+
regressions.append(category)
|
| 968 |
+
print(f" ↓ {category}: {old:.1%} → {new:.1%} ({diff:.1%}) REGRESSION!")
|
| 969 |
+
else:
|
| 970 |
+
print(f" = {category}: {old:.1%} → {new:.1%} (stable)")
|
| 971 |
+
|
| 972 |
+
# Improved if: at least one category improved AND improvements outnumber regressions.
|
| 973 |
+
# Old logic (improvements > 0 AND regressions == 0) was too strict —
|
| 974 |
+
# with 6 categories, some random fluctuation is expected. If math goes
|
| 975 |
+
# up 5% but creativity dips 2%, that's still a net win.
|
| 976 |
+
overall_improved = improvements > len(regressions)
|
| 977 |
+
return overall_improved, regressions
|
| 978 |
+
|
| 979 |
+
|
| 980 |
+
def _all_targets_met(scores: Dict[str, float], targets: Dict[str, float]) -> bool:
|
| 981 |
+
"""Check if all scores meet or exceed targets."""
|
| 982 |
+
for category, target in targets.items():
|
| 983 |
+
if category not in scores:
|
| 984 |
+
return False
|
| 985 |
+
if scores[category] < target:
|
| 986 |
+
return False
|
| 987 |
+
return True
|
| 988 |
+
|
| 989 |
+
|
| 990 |
+
# ============================================================
|
| 991 |
+
# SUMMARIES
|
| 992 |
+
# ============================================================
|
| 993 |
+
|
| 994 |
+
def _print_cycle_summary(result: CycleResult, history: List[CycleResult]):
|
| 995 |
+
"""Print a summary of the completed cycle."""
|
| 996 |
+
try:
|
| 997 |
+
hours = result.duration_seconds / 3600
|
| 998 |
+
print(f"\n{'='*50}")
|
| 999 |
+
print(f"CYCLE {result.cycle_num} SUMMARY")
|
| 1000 |
+
print(f"{'='*50}")
|
| 1001 |
+
print(f" Duration: {hours:.1f} hours")
|
| 1002 |
+
print(f" Weaknesses found: {result.weaknesses_found}")
|
| 1003 |
+
print(f" Data collected: {result.data_collected}")
|
| 1004 |
+
print(f" Training loss: {result.training_loss:.4f}")
|
| 1005 |
+
print(f" Improved: {'Yes' if result.improved else 'No'}")
|
| 1006 |
+
if result.regressions:
|
| 1007 |
+
print(f" Regressions: {', '.join(result.regressions)}")
|
| 1008 |
+
print(f" Model saved: {result.model_path}")
|
| 1009 |
+
|
| 1010 |
+
# Score history
|
| 1011 |
+
if len(history) > 1:
|
| 1012 |
+
print(f"\n Score progression:")
|
| 1013 |
+
for h in history[-5:]: # Last 5 cycles
|
| 1014 |
+
scores = h.benchmark_scores or {}
|
| 1015 |
+
avg = sum(scores.values()) / max(len(scores), 1) if scores else 0.0
|
| 1016 |
+
print(f" Cycle {h.cycle_num}: avg {avg:.1%}")
|
| 1017 |
+
except Exception as e:
|
| 1018 |
+
print(f" (Could not print cycle summary: {e})")
|
| 1019 |
+
|
| 1020 |
+
|
| 1021 |
+
def _print_final_summary(history: List[CycleResult]):
|
| 1022 |
+
"""Print final summary after loop ends."""
|
| 1023 |
+
if not history:
|
| 1024 |
+
return
|
| 1025 |
+
|
| 1026 |
+
try:
|
| 1027 |
+
total_hours = sum(r.duration_seconds for r in history) / 3600
|
| 1028 |
+
improved_cycles = sum(1 for r in history if r.improved)
|
| 1029 |
+
|
| 1030 |
+
print(f"\n{'#' * 60}")
|
| 1031 |
+
print(f"# LOOP COMPLETE")
|
| 1032 |
+
print(f"{'#' * 60}")
|
| 1033 |
+
print(f" Total cycles: {len(history)}")
|
| 1034 |
+
print(f" Improved cycles: {improved_cycles}/{len(history)}")
|
| 1035 |
+
print(f" Total time: {total_hours:.1f} hours")
|
| 1036 |
+
|
| 1037 |
+
if len(history) >= 2:
|
| 1038 |
+
first = history[0]
|
| 1039 |
+
last = history[-1]
|
| 1040 |
+
first_scores = first.benchmark_scores or {}
|
| 1041 |
+
last_scores = last.benchmark_scores or {}
|
| 1042 |
+
if first_scores and last_scores:
|
| 1043 |
+
print(f"\n Score changes (first → last):")
|
| 1044 |
+
for cat in sorted(first_scores.keys()):
|
| 1045 |
+
if cat in last_scores:
|
| 1046 |
+
old = first_scores[cat]
|
| 1047 |
+
new = last_scores[cat]
|
| 1048 |
+
diff = new - old
|
| 1049 |
+
arrow = "↑" if diff > 0 else "↓" if diff < 0 else "="
|
| 1050 |
+
print(f" {arrow} {cat}: {old:.1%} → {new:.1%} ({diff:+.1%})")
|
| 1051 |
+
|
| 1052 |
+
print(f"\n Final model: {history[-1].model_path}")
|
| 1053 |
+
except Exception as e:
|
| 1054 |
+
print(f" (Could not print final summary: {e})")
|
| 1055 |
+
|
| 1056 |
+
|
| 1057 |
+
# ============================================================
|
| 1058 |
+
# CLI
|
| 1059 |
+
# ============================================================
|
| 1060 |
+
|
| 1061 |
+
def main():
|
| 1062 |
+
parser = argparse.ArgumentParser(description="TD Self-Improvement Loop")
|
| 1063 |
+
parser.add_argument("--model", default="", help="Starting model path")
|
| 1064 |
+
parser.add_argument("--max-cycles", type=int, default=0, help="Max cycles (0=infinite)")
|
| 1065 |
+
parser.add_argument("--budget", type=int, default=8000, help="Training samples per cycle")
|
| 1066 |
+
parser.add_argument("--dry-run", action="store_true", help="Test + diagnose only")
|
| 1067 |
+
parser.add_argument("--hf-token", default="", help="HuggingFace token")
|
| 1068 |
+
args = parser.parse_args()
|
| 1069 |
+
|
| 1070 |
+
cfg = LoopConfig(
|
| 1071 |
+
model_path=args.model,
|
| 1072 |
+
max_cycles=args.max_cycles,
|
| 1073 |
+
training_budget=args.budget,
|
| 1074 |
+
dry_run=args.dry_run,
|
| 1075 |
+
hf_token=args.hf_token,
|
| 1076 |
+
)
|
| 1077 |
+
|
| 1078 |
+
run_loop(cfg)
|
| 1079 |
+
|
| 1080 |
+
|
| 1081 |
+
if __name__ == "__main__":
|
| 1082 |
+
main()
|
hugging/td_fuse/td_weakness.py
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TD Weakness Finder v1 — Diagnose What's Wrong
|
| 3 |
+
|
| 4 |
+
Takes BOTH benchmark results (what the model gets wrong) AND weight health
|
| 5 |
+
(what's broken inside) and outputs a structured weakness report.
|
| 6 |
+
|
| 7 |
+
This is Step 2 of the self-improvement loop.
|
| 8 |
+
|
| 9 |
+
Input:
|
| 10 |
+
- Benchmark JSON (from td_benchmark.py)
|
| 11 |
+
- Weight health data (from selfimprove.py health/damage/coherence functions)
|
| 12 |
+
|
| 13 |
+
Output:
|
| 14 |
+
- Structured weakness report with:
|
| 15 |
+
- Answer weaknesses (categories that scored low)
|
| 16 |
+
- Weight weaknesses (layers that are unhealthy)
|
| 17 |
+
- Search keywords for finding datasets
|
| 18 |
+
- Suggested actions (norm repair, LoRA targets, etc.)
|
| 19 |
+
- Data allocation (how many samples per weakness)
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import json
|
| 23 |
+
import math
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
from typing import Dict, List, Optional, Tuple
|
| 26 |
+
from dataclasses import dataclass, field
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ============================================================
|
| 30 |
+
# DATA STRUCTURES
|
| 31 |
+
# ============================================================
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class AnswerWeakness:
|
| 35 |
+
"""A weakness found from benchmark testing."""
|
| 36 |
+
category: str
|
| 37 |
+
score: float
|
| 38 |
+
target: float
|
| 39 |
+
gap: float # target - score
|
| 40 |
+
failed_questions: List[str]
|
| 41 |
+
search_keywords: List[str]
|
| 42 |
+
priority: float # higher = more urgent (based on gap size)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class WeightWeakness:
|
| 47 |
+
"""A weakness found from weight health analysis."""
|
| 48 |
+
description: str
|
| 49 |
+
layers: List[int]
|
| 50 |
+
layer_names: List[str]
|
| 51 |
+
severity: str # "critical", "high", "medium", "low"
|
| 52 |
+
action: str # what to do about it
|
| 53 |
+
metrics: Dict # the actual numbers
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class WeaknessReport:
|
| 58 |
+
"""Complete weakness report combining both sources."""
|
| 59 |
+
answer_weaknesses: List[AnswerWeakness]
|
| 60 |
+
weight_weaknesses: List[WeightWeakness]
|
| 61 |
+
lora_target_layers: List[int] # which layers to target with LoRA
|
| 62 |
+
norms_to_repair: List[str] # which norms need surgical repair
|
| 63 |
+
data_allocation: Dict[str, int] # category -> num_samples
|
| 64 |
+
total_samples_needed: int
|
| 65 |
+
search_plan: List[Dict] # ordered list of searches to do
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ============================================================
|
| 69 |
+
# CATEGORY SEARCH KEYWORDS
|
| 70 |
+
# ============================================================
|
| 71 |
+
|
| 72 |
+
# For each benchmark category, what to search for when it scores low
|
| 73 |
+
CATEGORY_SEARCH_MAP = {
|
| 74 |
+
"math": {
|
| 75 |
+
"keywords": [
|
| 76 |
+
"math word problems dataset",
|
| 77 |
+
"grade school math GSM8K",
|
| 78 |
+
"arithmetic reasoning dataset",
|
| 79 |
+
"algebra training data",
|
| 80 |
+
"competition math problems",
|
| 81 |
+
"math chain of thought",
|
| 82 |
+
],
|
| 83 |
+
"hf_tags": ["math", "arithmetic", "algebra", "gsm8k"],
|
| 84 |
+
"description": "Mathematical reasoning and problem solving",
|
| 85 |
+
},
|
| 86 |
+
"code": {
|
| 87 |
+
"keywords": [
|
| 88 |
+
"code generation dataset",
|
| 89 |
+
"python programming problems",
|
| 90 |
+
"code debugging pairs",
|
| 91 |
+
"competitive programming solutions",
|
| 92 |
+
"code review dataset",
|
| 93 |
+
"function implementation tasks",
|
| 94 |
+
],
|
| 95 |
+
"hf_tags": ["code", "python", "programming", "code-generation"],
|
| 96 |
+
"description": "Code writing, debugging, and understanding",
|
| 97 |
+
},
|
| 98 |
+
"reasoning": {
|
| 99 |
+
"keywords": [
|
| 100 |
+
"chain of thought reasoning dataset",
|
| 101 |
+
"multi-step logic problems",
|
| 102 |
+
"logical reasoning training",
|
| 103 |
+
"common sense reasoning",
|
| 104 |
+
"deductive reasoning dataset",
|
| 105 |
+
"ARC challenge training",
|
| 106 |
+
],
|
| 107 |
+
"hf_tags": ["reasoning", "logic", "commonsense", "chain-of-thought"],
|
| 108 |
+
"description": "Logic, multi-step deduction, common sense",
|
| 109 |
+
},
|
| 110 |
+
"creativity": {
|
| 111 |
+
"keywords": [
|
| 112 |
+
"creative writing dataset",
|
| 113 |
+
"story generation training",
|
| 114 |
+
"poetry writing pairs",
|
| 115 |
+
"text rewriting dataset",
|
| 116 |
+
"paraphrase generation",
|
| 117 |
+
"creative instruction following",
|
| 118 |
+
],
|
| 119 |
+
"hf_tags": ["creative-writing", "story", "paraphrase", "text-generation"],
|
| 120 |
+
"description": "Writing, storytelling, creative text generation",
|
| 121 |
+
},
|
| 122 |
+
"knowledge": {
|
| 123 |
+
"keywords": [
|
| 124 |
+
"MMLU training data",
|
| 125 |
+
"science QA dataset",
|
| 126 |
+
"trivia question answer pairs",
|
| 127 |
+
"general knowledge dataset",
|
| 128 |
+
"encyclopedia QA training",
|
| 129 |
+
"fact verification dataset",
|
| 130 |
+
],
|
| 131 |
+
"hf_tags": ["qa", "knowledge", "science", "trivia", "mmlu"],
|
| 132 |
+
"description": "Science, history, geography, general facts",
|
| 133 |
+
},
|
| 134 |
+
"instruction_following": {
|
| 135 |
+
"keywords": [
|
| 136 |
+
"instruction following dataset",
|
| 137 |
+
"format constraint training",
|
| 138 |
+
"structured output training",
|
| 139 |
+
"instruction tuning data",
|
| 140 |
+
"precise instruction pairs",
|
| 141 |
+
"constraint satisfaction dataset",
|
| 142 |
+
],
|
| 143 |
+
"hf_tags": ["instruction-following", "instruction-tuning", "chat"],
|
| 144 |
+
"description": "Following instructions precisely and exactly",
|
| 145 |
+
},
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ============================================================
|
| 150 |
+
# SCORE TARGETS (what "good" looks like for each category)
|
| 151 |
+
# ============================================================
|
| 152 |
+
|
| 153 |
+
DEFAULT_TARGETS = {
|
| 154 |
+
"math": 0.80,
|
| 155 |
+
"code": 0.70,
|
| 156 |
+
"reasoning": 0.75,
|
| 157 |
+
"creativity": 0.65,
|
| 158 |
+
"knowledge": 0.80,
|
| 159 |
+
"instruction_following": 0.85,
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# ============================================================
|
| 164 |
+
# ANSWER WEAKNESS DETECTION
|
| 165 |
+
# ============================================================
|
| 166 |
+
|
| 167 |
+
def find_answer_weaknesses(
|
| 168 |
+
benchmark_path: str,
|
| 169 |
+
targets: Optional[Dict[str, float]] = None,
|
| 170 |
+
) -> List[AnswerWeakness]:
|
| 171 |
+
"""
|
| 172 |
+
Read benchmark results and find which categories are weak.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
benchmark_path: Path to benchmark JSON output
|
| 176 |
+
targets: Score targets per category (default: DEFAULT_TARGETS)
|
| 177 |
+
|
| 178 |
+
Returns:
|
| 179 |
+
List of AnswerWeakness sorted by priority (worst first)
|
| 180 |
+
"""
|
| 181 |
+
targets = targets or DEFAULT_TARGETS
|
| 182 |
+
|
| 183 |
+
# Guard: benchmark file might be missing, empty, or corrupt
|
| 184 |
+
try:
|
| 185 |
+
with open(benchmark_path) as f:
|
| 186 |
+
results = json.load(f)
|
| 187 |
+
except FileNotFoundError:
|
| 188 |
+
print(f" WARNING: Benchmark file not found: {benchmark_path}")
|
| 189 |
+
return []
|
| 190 |
+
except json.JSONDecodeError as e:
|
| 191 |
+
print(f" WARNING: Benchmark file corrupt: {e}")
|
| 192 |
+
return []
|
| 193 |
+
except Exception as e:
|
| 194 |
+
print(f" WARNING: Could not read benchmark file: {e}")
|
| 195 |
+
return []
|
| 196 |
+
|
| 197 |
+
if not isinstance(results, dict) or "categories" not in results:
|
| 198 |
+
print(f" WARNING: Benchmark JSON missing 'categories' key — invalid format")
|
| 199 |
+
return []
|
| 200 |
+
|
| 201 |
+
weaknesses = []
|
| 202 |
+
|
| 203 |
+
for category, data in results.get("categories", {}).items():
|
| 204 |
+
score = data.get("score", 0.0)
|
| 205 |
+
target = targets.get(category, 0.75)
|
| 206 |
+
gap = target - score
|
| 207 |
+
|
| 208 |
+
# Only flag if below target
|
| 209 |
+
if gap <= 0:
|
| 210 |
+
continue
|
| 211 |
+
|
| 212 |
+
# BUG FIX: benchmark JSON uses "failed_questions" not "questions"
|
| 213 |
+
# Each failed question has "question", "response", "expected", "type"
|
| 214 |
+
failed = []
|
| 215 |
+
for q in data.get("failed_questions", []):
|
| 216 |
+
failed.append(q.get("question", "unknown")[:80])
|
| 217 |
+
|
| 218 |
+
# Look up search keywords for this category
|
| 219 |
+
search_info = CATEGORY_SEARCH_MAP.get(category, {})
|
| 220 |
+
keywords = search_info.get("keywords", [f"{category} training dataset"])
|
| 221 |
+
|
| 222 |
+
# Priority is based on how far below target we are
|
| 223 |
+
# Bigger gap = higher priority = more training data
|
| 224 |
+
priority = gap / max(target, 0.01)
|
| 225 |
+
|
| 226 |
+
weaknesses.append(AnswerWeakness(
|
| 227 |
+
category=category,
|
| 228 |
+
score=score,
|
| 229 |
+
target=target,
|
| 230 |
+
gap=gap,
|
| 231 |
+
failed_questions=failed,
|
| 232 |
+
search_keywords=keywords,
|
| 233 |
+
priority=priority,
|
| 234 |
+
))
|
| 235 |
+
|
| 236 |
+
# Sort by priority (worst gaps first)
|
| 237 |
+
weaknesses.sort(key=lambda w: w.priority, reverse=True)
|
| 238 |
+
return weaknesses
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# ============================================================
|
| 242 |
+
# WEIGHT WEAKNESS DETECTION
|
| 243 |
+
# ============================================================
|
| 244 |
+
|
| 245 |
+
def find_weight_weaknesses(
|
| 246 |
+
health_scores: Dict[str, Dict],
|
| 247 |
+
damage_scores: Optional[Dict[str, float]] = None,
|
| 248 |
+
coherence_scores: Optional[Dict[int, float]] = None,
|
| 249 |
+
clogged_norms: Optional[List[str]] = None,
|
| 250 |
+
) -> List[WeightWeakness]:
|
| 251 |
+
"""
|
| 252 |
+
Analyze weight health data and find structural problems.
|
| 253 |
+
|
| 254 |
+
Args:
|
| 255 |
+
health_scores: From selfimprove.compute_health_scores()
|
| 256 |
+
damage_scores: From selfimprove.compute_damage_scores()
|
| 257 |
+
coherence_scores: From selfimprove.compute_coherence_scores()
|
| 258 |
+
clogged_norms: From selfimprove.detect_clogged_norms()
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
List of WeightWeakness sorted by severity
|
| 262 |
+
"""
|
| 263 |
+
weaknesses = []
|
| 264 |
+
|
| 265 |
+
# Guard: health_scores might be empty or have unexpected structure
|
| 266 |
+
if not health_scores:
|
| 267 |
+
print(" WARNING: No health_scores provided — skipping weight weakness detection")
|
| 268 |
+
return []
|
| 269 |
+
|
| 270 |
+
# --- 1. Clogged norms (highest priority) ---
|
| 271 |
+
if clogged_norms:
|
| 272 |
+
# Group by layer number
|
| 273 |
+
norm_layers = _extract_layer_numbers(clogged_norms)
|
| 274 |
+
|
| 275 |
+
# Find the kurtosis values (with guards for missing keys)
|
| 276 |
+
kurtosis_vals = {}
|
| 277 |
+
for name in clogged_norms:
|
| 278 |
+
if name in health_scores:
|
| 279 |
+
stats = health_scores[name].get("stats", {})
|
| 280 |
+
if "kurtosis" in stats:
|
| 281 |
+
kurtosis_vals[name] = abs(stats["kurtosis"])
|
| 282 |
+
|
| 283 |
+
avg_kurt = sum(kurtosis_vals.values()) / max(len(kurtosis_vals), 1)
|
| 284 |
+
max_kurt = max(kurtosis_vals.values()) if kurtosis_vals else 0
|
| 285 |
+
|
| 286 |
+
severity = "critical" if max_kurt > 100 else "high" if max_kurt > 50 else "medium"
|
| 287 |
+
|
| 288 |
+
weaknesses.append(WeightWeakness(
|
| 289 |
+
description=f"{len(clogged_norms)} clogged norms (avg kurtosis {avg_kurt:.0f}, max {max_kurt:.0f})",
|
| 290 |
+
layers=sorted(set(norm_layers)),
|
| 291 |
+
layer_names=clogged_norms,
|
| 292 |
+
severity=severity,
|
| 293 |
+
action="surgical_norm_repair",
|
| 294 |
+
metrics={"avg_kurtosis": avg_kurt, "max_kurtosis": max_kurt, "count": len(clogged_norms)},
|
| 295 |
+
))
|
| 296 |
+
|
| 297 |
+
# --- 2. High condition number layers (numerically unstable) ---
|
| 298 |
+
high_condition = []
|
| 299 |
+
for name, h in health_scores.items():
|
| 300 |
+
stats = h.get("stats", {}) if isinstance(h, dict) else {}
|
| 301 |
+
sv_ratio = stats.get("sv_ratio", 0)
|
| 302 |
+
if sv_ratio > 10000:
|
| 303 |
+
high_condition.append((name, sv_ratio))
|
| 304 |
+
|
| 305 |
+
if high_condition:
|
| 306 |
+
layers = _extract_layer_numbers([n for n, _ in high_condition])
|
| 307 |
+
max_cond = max(v for _, v in high_condition)
|
| 308 |
+
|
| 309 |
+
weaknesses.append(WeightWeakness(
|
| 310 |
+
description=f"{len(high_condition)} layers with extreme condition numbers (max {max_cond:.0f})",
|
| 311 |
+
layers=sorted(set(layers)),
|
| 312 |
+
layer_names=[n for n, _ in high_condition],
|
| 313 |
+
severity="critical" if max_cond > 50000 else "high",
|
| 314 |
+
action="lora_target_high_priority",
|
| 315 |
+
metrics={"max_condition": max_cond, "count": len(high_condition)},
|
| 316 |
+
))
|
| 317 |
+
|
| 318 |
+
# --- 3. Low rank utilization (layers not using their capacity) ---
|
| 319 |
+
low_rank = []
|
| 320 |
+
for name, h in health_scores.items():
|
| 321 |
+
stats = h.get("stats", {}) if isinstance(h, dict) else {}
|
| 322 |
+
rank_util = stats.get("sv_rank_utilization", -1)
|
| 323 |
+
if 0 < rank_util < 0.1:
|
| 324 |
+
low_rank.append((name, rank_util))
|
| 325 |
+
|
| 326 |
+
if low_rank:
|
| 327 |
+
layers = _extract_layer_numbers([n for n, _ in low_rank])
|
| 328 |
+
avg_util = sum(v for _, v in low_rank) / len(low_rank)
|
| 329 |
+
|
| 330 |
+
weaknesses.append(WeightWeakness(
|
| 331 |
+
description=f"{len(low_rank)} layers with very low rank utilization (avg {avg_util:.1%})",
|
| 332 |
+
layers=sorted(set(layers)),
|
| 333 |
+
layer_names=[n for n, _ in low_rank],
|
| 334 |
+
severity="high",
|
| 335 |
+
action="lora_target_complex_tasks",
|
| 336 |
+
metrics={"avg_rank_util": avg_util, "count": len(low_rank)},
|
| 337 |
+
))
|
| 338 |
+
|
| 339 |
+
# --- 4. Dead neurons ---
|
| 340 |
+
dead_neuron_layers = []
|
| 341 |
+
for name, h in health_scores.items():
|
| 342 |
+
stats = h.get("stats", {}) if isinstance(h, dict) else {}
|
| 343 |
+
dead = stats.get("near_zero_pct", 0)
|
| 344 |
+
if dead > 0.3:
|
| 345 |
+
dead_neuron_layers.append((name, dead))
|
| 346 |
+
|
| 347 |
+
if dead_neuron_layers:
|
| 348 |
+
layers = _extract_layer_numbers([n for n, _ in dead_neuron_layers])
|
| 349 |
+
avg_dead = sum(v for _, v in dead_neuron_layers) / len(dead_neuron_layers)
|
| 350 |
+
|
| 351 |
+
weaknesses.append(WeightWeakness(
|
| 352 |
+
description=f"{len(dead_neuron_layers)} layers with >30% dead neurons (avg {avg_dead:.1%})",
|
| 353 |
+
layers=sorted(set(layers)),
|
| 354 |
+
layer_names=[n for n, _ in dead_neuron_layers],
|
| 355 |
+
severity="high",
|
| 356 |
+
action="lora_target_activation",
|
| 357 |
+
metrics={"avg_dead_pct": avg_dead, "count": len(dead_neuron_layers)},
|
| 358 |
+
))
|
| 359 |
+
|
| 360 |
+
# --- 5. Coherence issues (layer-to-layer jumps) ---
|
| 361 |
+
if coherence_scores:
|
| 362 |
+
bad_coherence = [(l, s) for l, s in coherence_scores.items() if s > 0.3]
|
| 363 |
+
if bad_coherence:
|
| 364 |
+
weaknesses.append(WeightWeakness(
|
| 365 |
+
description=f"{len(bad_coherence)} layers with coherence issues",
|
| 366 |
+
layers=sorted([l for l, _ in bad_coherence]),
|
| 367 |
+
layer_names=[],
|
| 368 |
+
severity="medium",
|
| 369 |
+
action="lora_target_smooth_transition",
|
| 370 |
+
metrics={"worst_score": max(s for _, s in bad_coherence)},
|
| 371 |
+
))
|
| 372 |
+
|
| 373 |
+
# --- 6. Damage vs reference (merge artifacts) ---
|
| 374 |
+
if damage_scores:
|
| 375 |
+
high_damage = [(n, s) for n, s in damage_scores.items() if s > 0.5]
|
| 376 |
+
if high_damage:
|
| 377 |
+
layers = _extract_layer_numbers([n for n, _ in high_damage])
|
| 378 |
+
avg_damage = sum(s for _, s in high_damage) / len(high_damage)
|
| 379 |
+
|
| 380 |
+
weaknesses.append(WeightWeakness(
|
| 381 |
+
description=f"{len(high_damage)} layers with high merge damage (avg {avg_damage:.2f})",
|
| 382 |
+
layers=sorted(set(layers)),
|
| 383 |
+
layer_names=[n for n, _ in high_damage],
|
| 384 |
+
severity="high" if avg_damage > 0.8 else "medium",
|
| 385 |
+
action="lora_target_repair",
|
| 386 |
+
metrics={"avg_damage": avg_damage, "count": len(high_damage)},
|
| 387 |
+
))
|
| 388 |
+
|
| 389 |
+
# Sort by severity
|
| 390 |
+
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
| 391 |
+
weaknesses.sort(key=lambda w: severity_order.get(w.severity, 99))
|
| 392 |
+
|
| 393 |
+
return weaknesses
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def _extract_layer_numbers(names: List[str]) -> List[int]:
|
| 397 |
+
"""Pull layer numbers from parameter names like 'model.layers.5.self_attn.q_proj'."""
|
| 398 |
+
layers = []
|
| 399 |
+
for name in names:
|
| 400 |
+
parts = name.split(".")
|
| 401 |
+
for i, part in enumerate(parts):
|
| 402 |
+
if part == "layers" and i + 1 < len(parts) and parts[i + 1].isdigit():
|
| 403 |
+
layers.append(int(parts[i + 1]))
|
| 404 |
+
break
|
| 405 |
+
return layers
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
# ============================================================
|
| 409 |
+
# COMBINE INTO REPORT
|
| 410 |
+
# ============================================================
|
| 411 |
+
|
| 412 |
+
def build_weakness_report(
|
| 413 |
+
answer_weaknesses: List[AnswerWeakness],
|
| 414 |
+
weight_weaknesses: List[WeightWeakness],
|
| 415 |
+
total_budget: int = 8000,
|
| 416 |
+
) -> WeaknessReport:
|
| 417 |
+
"""
|
| 418 |
+
Combine answer + weight weaknesses into a single action plan.
|
| 419 |
+
|
| 420 |
+
Args:
|
| 421 |
+
answer_weaknesses: From find_answer_weaknesses()
|
| 422 |
+
weight_weaknesses: From find_weight_weaknesses()
|
| 423 |
+
total_budget: Total training samples to allocate
|
| 424 |
+
|
| 425 |
+
Returns:
|
| 426 |
+
WeaknessReport with everything the training loop needs
|
| 427 |
+
"""
|
| 428 |
+
# --- Figure out LoRA target layers ---
|
| 429 |
+
lora_layers = set()
|
| 430 |
+
for ww in weight_weaknesses:
|
| 431 |
+
if "lora" in ww.action:
|
| 432 |
+
lora_layers.update(ww.layers)
|
| 433 |
+
|
| 434 |
+
# If no weight issues pointed to specific layers, target all layers
|
| 435 |
+
if not lora_layers:
|
| 436 |
+
lora_layers = set(range(36)) # Qwen3-VL has 36 layers
|
| 437 |
+
|
| 438 |
+
# --- Figure out norms to repair ---
|
| 439 |
+
norms_to_repair = []
|
| 440 |
+
for ww in weight_weaknesses:
|
| 441 |
+
if ww.action == "surgical_norm_repair":
|
| 442 |
+
norms_to_repair.extend(ww.layer_names)
|
| 443 |
+
|
| 444 |
+
# --- Allocate training data ---
|
| 445 |
+
data_allocation = _allocate_data(answer_weaknesses, total_budget)
|
| 446 |
+
|
| 447 |
+
# --- Build search plan ---
|
| 448 |
+
search_plan = _build_search_plan(answer_weaknesses)
|
| 449 |
+
|
| 450 |
+
return WeaknessReport(
|
| 451 |
+
answer_weaknesses=answer_weaknesses,
|
| 452 |
+
weight_weaknesses=weight_weaknesses,
|
| 453 |
+
lora_target_layers=sorted(lora_layers),
|
| 454 |
+
norms_to_repair=norms_to_repair,
|
| 455 |
+
data_allocation=data_allocation,
|
| 456 |
+
total_samples_needed=sum(data_allocation.values()),
|
| 457 |
+
search_plan=search_plan,
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _allocate_data(
|
| 462 |
+
weaknesses: List[AnswerWeakness],
|
| 463 |
+
total_budget: int,
|
| 464 |
+
) -> Dict[str, int]:
|
| 465 |
+
"""
|
| 466 |
+
Allocate training samples proportional to weakness severity.
|
| 467 |
+
Worse categories get more data.
|
| 468 |
+
|
| 469 |
+
IMPORTANT: Also allocates a FLOOR (10% of budget per category) to ALL
|
| 470 |
+
categories, even those above target. This prevents catastrophic forgetting
|
| 471 |
+
where training on weak categories makes strong categories worse.
|
| 472 |
+
|
| 473 |
+
Example:
|
| 474 |
+
math: 42% (gap 38%) → gets ~2x more data (from weakness pool)
|
| 475 |
+
code: 65% (gap 5%) → gets less data (from weakness pool)
|
| 476 |
+
knowledge: 85% (above target) → still gets floor allocation
|
| 477 |
+
"""
|
| 478 |
+
ALL_CATEGORIES = ["math", "code", "reasoning", "creativity",
|
| 479 |
+
"knowledge", "instruction_following"]
|
| 480 |
+
|
| 481 |
+
# FLOOR: 10% of budget split equally across ALL categories
|
| 482 |
+
# This prevents catastrophic forgetting on categories above target
|
| 483 |
+
floor_pct = 0.10
|
| 484 |
+
floor_total = int(total_budget * floor_pct)
|
| 485 |
+
floor_per_cat = max(floor_total // len(ALL_CATEGORIES), 50)
|
| 486 |
+
|
| 487 |
+
# Start with floor for every category
|
| 488 |
+
allocation = {cat: floor_per_cat for cat in ALL_CATEGORIES}
|
| 489 |
+
|
| 490 |
+
if not weaknesses:
|
| 491 |
+
return allocation
|
| 492 |
+
|
| 493 |
+
# Remaining budget goes to weak categories proportionally
|
| 494 |
+
weakness_budget = total_budget - (floor_per_cat * len(ALL_CATEGORIES))
|
| 495 |
+
if weakness_budget <= 0:
|
| 496 |
+
return allocation
|
| 497 |
+
|
| 498 |
+
# Weight by gap size (bigger gap = more data)
|
| 499 |
+
total_gap = sum(w.gap for w in weaknesses)
|
| 500 |
+
if total_gap <= 0:
|
| 501 |
+
per_cat = weakness_budget // len(weaknesses)
|
| 502 |
+
for w in weaknesses:
|
| 503 |
+
allocation[w.category] = allocation.get(w.category, 0) + per_cat
|
| 504 |
+
return allocation
|
| 505 |
+
|
| 506 |
+
remaining = weakness_budget
|
| 507 |
+
|
| 508 |
+
for i, w in enumerate(weaknesses):
|
| 509 |
+
if i == len(weaknesses) - 1:
|
| 510 |
+
# Last category gets whatever is left — but never negative
|
| 511 |
+
allocation[w.category] = allocation.get(w.category, 0) + max(remaining, 0)
|
| 512 |
+
else:
|
| 513 |
+
share = int(weakness_budget * (w.gap / total_gap))
|
| 514 |
+
share = min(max(share, 500), remaining) # Don't exceed remaining budget
|
| 515 |
+
allocation[w.category] = allocation.get(w.category, 0) + share
|
| 516 |
+
remaining -= share
|
| 517 |
+
|
| 518 |
+
# Make sure we don't exceed budget
|
| 519 |
+
total_alloc = sum(allocation.values())
|
| 520 |
+
if total_alloc > total_budget:
|
| 521 |
+
scale = total_budget / total_alloc
|
| 522 |
+
allocation = {k: max(int(v * scale), 50) for k, v in allocation.items()}
|
| 523 |
+
|
| 524 |
+
return allocation
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def _build_search_plan(weaknesses: List[AnswerWeakness]) -> List[Dict]:
|
| 528 |
+
"""
|
| 529 |
+
Build an ordered list of dataset searches to perform.
|
| 530 |
+
Each entry has: category, keywords, hf_tags, priority, num_samples_needed.
|
| 531 |
+
|
| 532 |
+
Includes ALL categories (even above target) because the data allocation
|
| 533 |
+
now has a floor for every category to prevent catastrophic forgetting.
|
| 534 |
+
Weak categories come first (higher priority).
|
| 535 |
+
"""
|
| 536 |
+
ALL_CATEGORIES = ["math", "code", "reasoning", "creativity",
|
| 537 |
+
"knowledge", "instruction_following"]
|
| 538 |
+
|
| 539 |
+
plan = []
|
| 540 |
+
weak_cats = set()
|
| 541 |
+
|
| 542 |
+
# First: weak categories (high priority, targeted search)
|
| 543 |
+
for w in weaknesses:
|
| 544 |
+
search_info = CATEGORY_SEARCH_MAP.get(w.category, {})
|
| 545 |
+
plan.append({
|
| 546 |
+
"category": w.category,
|
| 547 |
+
"keywords": w.search_keywords,
|
| 548 |
+
"hf_tags": search_info.get("hf_tags", []),
|
| 549 |
+
"priority": w.priority,
|
| 550 |
+
"score": w.score,
|
| 551 |
+
"target": w.target,
|
| 552 |
+
"gap": w.gap,
|
| 553 |
+
})
|
| 554 |
+
weak_cats.add(w.category)
|
| 555 |
+
|
| 556 |
+
# Then: strong categories (low priority, just use fallback datasets)
|
| 557 |
+
for cat in ALL_CATEGORIES:
|
| 558 |
+
if cat not in weak_cats:
|
| 559 |
+
search_info = CATEGORY_SEARCH_MAP.get(cat, {})
|
| 560 |
+
plan.append({
|
| 561 |
+
"category": cat,
|
| 562 |
+
"keywords": search_info.get("keywords", [f"{cat} training dataset"]),
|
| 563 |
+
"hf_tags": search_info.get("hf_tags", []),
|
| 564 |
+
"priority": 0.1, # Low priority — just floor allocation
|
| 565 |
+
"score": 1.0, # Above target
|
| 566 |
+
"target": DEFAULT_TARGETS.get(cat, 0.75),
|
| 567 |
+
"gap": 0.0,
|
| 568 |
+
})
|
| 569 |
+
|
| 570 |
+
return plan
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
# ============================================================
|
| 574 |
+
# PRETTY PRINT
|
| 575 |
+
# ============================================================
|
| 576 |
+
|
| 577 |
+
def print_report(report: WeaknessReport):
|
| 578 |
+
"""Print a human-readable weakness report."""
|
| 579 |
+
print("\n" + "=" * 60)
|
| 580 |
+
print("TD WEAKNESS REPORT")
|
| 581 |
+
print("=" * 60)
|
| 582 |
+
|
| 583 |
+
# Answer weaknesses
|
| 584 |
+
if report.answer_weaknesses:
|
| 585 |
+
print("\n ANSWER WEAKNESSES (what it gets wrong):")
|
| 586 |
+
print(" " + "-" * 50)
|
| 587 |
+
for w in report.answer_weaknesses:
|
| 588 |
+
status = "CRITICAL" if w.gap > 0.3 else "HIGH" if w.gap > 0.15 else "MEDIUM"
|
| 589 |
+
print(f" [{status}] {w.category}: {w.score:.0%} (target: {w.target:.0%}, gap: {w.gap:.0%})")
|
| 590 |
+
print(f" Failed: {len(w.failed_questions)} questions")
|
| 591 |
+
print(f" Search: {', '.join(w.search_keywords[:3])}")
|
| 592 |
+
else:
|
| 593 |
+
print("\n ANSWER WEAKNESSES: None! All categories at or above target.")
|
| 594 |
+
|
| 595 |
+
# Weight weaknesses
|
| 596 |
+
if report.weight_weaknesses:
|
| 597 |
+
print(f"\n WEIGHT WEAKNESSES (what's broken inside):")
|
| 598 |
+
print(" " + "-" * 50)
|
| 599 |
+
for w in report.weight_weaknesses:
|
| 600 |
+
print(f" [{w.severity.upper()}] {w.description}")
|
| 601 |
+
if w.layers:
|
| 602 |
+
print(f" Layers: {w.layers}")
|
| 603 |
+
print(f" Action: {w.action}")
|
| 604 |
+
else:
|
| 605 |
+
print("\n WEIGHT WEAKNESSES: None! All layers healthy.")
|
| 606 |
+
|
| 607 |
+
# Data allocation
|
| 608 |
+
if report.data_allocation:
|
| 609 |
+
print(f"\n DATA ALLOCATION ({report.total_samples_needed} total samples):")
|
| 610 |
+
print(" " + "-" * 50)
|
| 611 |
+
for cat, num in sorted(report.data_allocation.items(), key=lambda x: -x[1]):
|
| 612 |
+
pct = num / max(report.total_samples_needed, 1)
|
| 613 |
+
bar = "█" * int(pct * 30)
|
| 614 |
+
print(f" {cat:25s} {num:5d} samples ({pct:5.1%}) {bar}")
|
| 615 |
+
|
| 616 |
+
# LoRA targets
|
| 617 |
+
if report.lora_target_layers:
|
| 618 |
+
print(f"\n LORA TARGETS: {len(report.lora_target_layers)} layers")
|
| 619 |
+
print(f" Layers: {report.lora_target_layers}")
|
| 620 |
+
|
| 621 |
+
# Norm repair
|
| 622 |
+
if report.norms_to_repair:
|
| 623 |
+
print(f"\n NORMS TO REPAIR: {len(report.norms_to_repair)} parameters")
|
| 624 |
+
for n in report.norms_to_repair[:10]: # Show first 10
|
| 625 |
+
print(f" - {n}")
|
| 626 |
+
if len(report.norms_to_repair) > 10:
|
| 627 |
+
print(f" ... and {len(report.norms_to_repair) - 10} more")
|
| 628 |
+
|
| 629 |
+
# Search plan
|
| 630 |
+
if report.search_plan:
|
| 631 |
+
print(f"\n SEARCH PLAN ({len(report.search_plan)} categories to search):")
|
| 632 |
+
print(" " + "-" * 50)
|
| 633 |
+
for i, s in enumerate(report.search_plan, 1):
|
| 634 |
+
print(f" {i}. {s['category']} (score: {s['score']:.0%}, need: {s['gap']:.0%} improvement)")
|
| 635 |
+
print(f" Keywords: {', '.join(s['keywords'][:2])}")
|
| 636 |
+
|
| 637 |
+
print("\n" + "=" * 60)
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
# ============================================================
|
| 641 |
+
# SAVE / LOAD
|
| 642 |
+
# ============================================================
|
| 643 |
+
|
| 644 |
+
def save_report(report: WeaknessReport, path: str):
|
| 645 |
+
"""Save weakness report to JSON."""
|
| 646 |
+
data = {
|
| 647 |
+
"answer_weaknesses": [
|
| 648 |
+
{
|
| 649 |
+
"category": w.category,
|
| 650 |
+
"score": w.score,
|
| 651 |
+
"target": w.target,
|
| 652 |
+
"gap": w.gap,
|
| 653 |
+
"failed_questions": w.failed_questions,
|
| 654 |
+
"search_keywords": w.search_keywords,
|
| 655 |
+
"priority": w.priority,
|
| 656 |
+
}
|
| 657 |
+
for w in report.answer_weaknesses
|
| 658 |
+
],
|
| 659 |
+
"weight_weaknesses": [
|
| 660 |
+
{
|
| 661 |
+
"description": w.description,
|
| 662 |
+
"layers": w.layers,
|
| 663 |
+
"layer_names": w.layer_names,
|
| 664 |
+
"severity": w.severity,
|
| 665 |
+
"action": w.action,
|
| 666 |
+
"metrics": w.metrics,
|
| 667 |
+
}
|
| 668 |
+
for w in report.weight_weaknesses
|
| 669 |
+
],
|
| 670 |
+
"lora_target_layers": report.lora_target_layers,
|
| 671 |
+
"norms_to_repair": report.norms_to_repair,
|
| 672 |
+
"data_allocation": report.data_allocation,
|
| 673 |
+
"total_samples_needed": report.total_samples_needed,
|
| 674 |
+
"search_plan": report.search_plan,
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
| 678 |
+
with open(path, "w") as f:
|
| 679 |
+
json.dump(data, f, indent=2)
|
| 680 |
+
print(f"\nWeakness report saved to {path}")
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
def load_report(path: str) -> WeaknessReport:
|
| 684 |
+
"""Load weakness report from JSON."""
|
| 685 |
+
with open(path) as f:
|
| 686 |
+
data = json.load(f)
|
| 687 |
+
|
| 688 |
+
answer_weaknesses = [
|
| 689 |
+
AnswerWeakness(**aw) for aw in data["answer_weaknesses"]
|
| 690 |
+
]
|
| 691 |
+
weight_weaknesses = [
|
| 692 |
+
WeightWeakness(**ww) for ww in data["weight_weaknesses"]
|
| 693 |
+
]
|
| 694 |
+
|
| 695 |
+
return WeaknessReport(
|
| 696 |
+
answer_weaknesses=answer_weaknesses,
|
| 697 |
+
weight_weaknesses=weight_weaknesses,
|
| 698 |
+
lora_target_layers=data["lora_target_layers"],
|
| 699 |
+
norms_to_repair=data["norms_to_repair"],
|
| 700 |
+
data_allocation=data["data_allocation"],
|
| 701 |
+
total_samples_needed=data["total_samples_needed"],
|
| 702 |
+
search_plan=data["search_plan"],
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
# ============================================================
|
| 707 |
+
# MAIN — run from command line
|
| 708 |
+
# ============================================================
|
| 709 |
+
|
| 710 |
+
def run_weakness_analysis(
|
| 711 |
+
benchmark_path: str,
|
| 712 |
+
health_scores: Optional[Dict] = None,
|
| 713 |
+
damage_scores: Optional[Dict] = None,
|
| 714 |
+
coherence_scores: Optional[Dict] = None,
|
| 715 |
+
clogged_norms: Optional[List] = None,
|
| 716 |
+
targets: Optional[Dict[str, float]] = None,
|
| 717 |
+
total_budget: int = 8000,
|
| 718 |
+
output_path: Optional[str] = None,
|
| 719 |
+
) -> WeaknessReport:
|
| 720 |
+
"""
|
| 721 |
+
Run the full weakness analysis pipeline.
|
| 722 |
+
|
| 723 |
+
This is the main entry point that td_loop.py calls.
|
| 724 |
+
|
| 725 |
+
Args:
|
| 726 |
+
benchmark_path: Path to benchmark results JSON
|
| 727 |
+
health_scores: From selfimprove.compute_health_scores()
|
| 728 |
+
damage_scores: From selfimprove.compute_damage_scores()
|
| 729 |
+
coherence_scores: From selfimprove.compute_coherence_scores()
|
| 730 |
+
clogged_norms: From selfimprove.detect_clogged_norms()
|
| 731 |
+
targets: Score targets per category
|
| 732 |
+
total_budget: Total training samples to allocate
|
| 733 |
+
output_path: Where to save the report JSON
|
| 734 |
+
|
| 735 |
+
Returns:
|
| 736 |
+
Complete WeaknessReport
|
| 737 |
+
"""
|
| 738 |
+
print("\n" + "=" * 60)
|
| 739 |
+
print("TD WEAKNESS FINDER v1")
|
| 740 |
+
print("=" * 60)
|
| 741 |
+
|
| 742 |
+
# Step 1: Find answer weaknesses
|
| 743 |
+
print("\nAnalyzing benchmark results...")
|
| 744 |
+
answer_weaknesses = find_answer_weaknesses(benchmark_path, targets)
|
| 745 |
+
print(f" Found {len(answer_weaknesses)} answer weaknesses")
|
| 746 |
+
|
| 747 |
+
# Step 2: Find weight weaknesses
|
| 748 |
+
print("\nAnalyzing weight health...")
|
| 749 |
+
weight_weaknesses = []
|
| 750 |
+
if health_scores:
|
| 751 |
+
weight_weaknesses = find_weight_weaknesses(
|
| 752 |
+
health_scores=health_scores,
|
| 753 |
+
damage_scores=damage_scores,
|
| 754 |
+
coherence_scores=coherence_scores,
|
| 755 |
+
clogged_norms=clogged_norms,
|
| 756 |
+
)
|
| 757 |
+
print(f" Found {len(weight_weaknesses)} weight weaknesses")
|
| 758 |
+
else:
|
| 759 |
+
print(" No weight health data provided — skipping weight analysis")
|
| 760 |
+
|
| 761 |
+
# Step 3: Build combined report
|
| 762 |
+
print("\nBuilding combined report...")
|
| 763 |
+
report = build_weakness_report(answer_weaknesses, weight_weaknesses, total_budget)
|
| 764 |
+
|
| 765 |
+
# Step 4: Print it
|
| 766 |
+
print_report(report)
|
| 767 |
+
|
| 768 |
+
# Step 5: Save if requested
|
| 769 |
+
if output_path:
|
| 770 |
+
save_report(report, output_path)
|
| 771 |
+
|
| 772 |
+
return report
|
| 773 |
+
|
| 774 |
+
|
| 775 |
+
if __name__ == "__main__":
|
| 776 |
+
import sys
|
| 777 |
+
|
| 778 |
+
if len(sys.argv) < 2:
|
| 779 |
+
print("Usage: python td_weakness.py <benchmark_results.json> [output_path.json]")
|
| 780 |
+
print("\nThis reads benchmark results and outputs a weakness report.")
|
| 781 |
+
print("Weight health data is passed programmatically from td_loop.py.")
|
| 782 |
+
sys.exit(1)
|
| 783 |
+
|
| 784 |
+
benchmark_path = sys.argv[1]
|
| 785 |
+
output_path = sys.argv[2] if len(sys.argv) > 2 else None
|
| 786 |
+
|
| 787 |
+
# When run standalone, only uses benchmark data (no weight health)
|
| 788 |
+
report = run_weakness_analysis(
|
| 789 |
+
benchmark_path=benchmark_path,
|
| 790 |
+
output_path=output_path,
|
| 791 |
+
)
|