td-builder commited on
Commit
c98555a
·
verified ·
1 Parent(s): c620ae1

Upload 149 files

Browse files
hugging/td_fuse/grpo_reason.py CHANGED
@@ -268,7 +268,10 @@ def verify_answer(problem: Dict, response: str) -> float:
268
  # Positive indicators anywhere in answer
269
  yes_words = ["yes", "do breathe", "does breathe", "does need", "do need",
270
  "correct", "true", "indeed", "absolutely", "certainly",
271
- "they do", "it does", "they can", "it can"]
 
 
 
272
  # Negative indicators that would flip it
273
  no_words = ["no,", "no.", "not ", "cannot", "can't", "doesn't", "don't",
274
  "is not", "are not", "do not"]
 
268
  # Positive indicators anywhere in answer
269
  yes_words = ["yes", "do breathe", "does breathe", "does need", "do need",
270
  "correct", "true", "indeed", "absolutely", "certainly",
271
+ "they do", "it does", "they can", "it can",
272
+ "therefore,", "therefore ", "so yes", "the answer is yes",
273
+ "breathe air", "have feathers", "have 4 sides",
274
+ "need water"]
275
  # Negative indicators that would flip it
276
  no_words = ["no,", "no.", "not ", "cannot", "can't", "doesn't", "don't",
277
  "is not", "are not", "do not"]
hugging/td_fuse/selfimprove.py CHANGED
@@ -1,27 +1,26 @@
1
  """
2
  TD Self-Improvement Loop — the core of Time Dilation.
3
 
4
- This is the part that makes the model actually get smarter over time.
5
- Based on findings from test_1 through test_18 interviews:
6
-
7
- THE LOOP:
8
  1. Ask the model "what are you bad at?" → it identifies weak spots
9
- 2. Generate targeted synthetic training data for those weaknesses
10
- 3. Train with GRPO (verified rewards only — no learned reward model)
11
- 4. Re-benchmark → measure improvement
12
- 5. Repeat — each cycle is small (1-5%) but compounds
13
-
14
- KEY PRINCIPLES (from interviews + dad's tests):
15
- - Verified rewards only: code compiles, math correct, logic valid
16
- - No learned reward model (saves VRAM, avoids reward hacking)
 
 
 
 
 
 
 
17
  - Cherry_LLM perplexity filter prevents mode collapse
18
- - Mix external data to avoid "100 steps on own outputs → dumber" trap
19
- - Target mid-to-late layers (16-28 for 32-layer, ~20-30 for 36-layer)
20
-
21
- COST SPLIT (from test_16):
22
- - 70-80% inference scaling (generate many, pick best)
23
- - 10-20% short GRPO training
24
- - 5-10% tooling/evaluation
25
  """
26
 
27
  import torch
@@ -30,44 +29,52 @@ import json
30
  import math
31
  import random
32
  import gc
 
 
33
  from pathlib import Path
34
- from typing import Optional
35
- from dataclasses import dataclass, field
36
 
37
 
38
  @dataclass
39
  class SelfImproveConfig:
40
  """Configuration for one self-improvement cycle."""
41
- model_path: str = "td_fuse_outputs/healed_final"
42
- output_dir: str = "td_fuse_outputs/improved"
 
 
 
 
 
 
43
 
44
- # Generation settings
45
- num_candidates: int = 8 # Generate N answers per question, pick best (inference scaling)
46
- max_gen_tokens: int = 512
47
- temperature: float = 0.7 # For diverse candidate generation
 
 
48
 
49
- # Training settings
50
- lora_r: int = 16
51
- lora_alpha: int = 32
52
- train_epochs: int = 2
53
- train_batch: int = 4
54
- train_grad_accum: int = 4
55
- learning_rate: float = 2e-5 # Lower than healing — small nudges
 
 
56
 
57
- # Data settings
58
- num_reasoning_problems: int = 200 # Logic/reasoning problems to generate
59
- num_math_problems: int = 200 # Math problems
60
- num_code_problems: int = 100 # Code problems
61
 
62
- # Quality filter
63
- perplexity_threshold: float = 50.0 # Cherry_LLM: reject if perplexity > this
64
 
65
 
66
  # ============================================================
67
  # STEP 1: DIAGNOSE — Ask the model what it's bad at
68
  # ============================================================
69
 
70
- def diagnose_weaknesses(model, tokenizer, eos_id):
71
  """
72
  Ask the model to identify its own weaknesses.
73
  All 3 AIs (ChatGPT, Grok, Gemini) confirmed models can do this.
@@ -75,244 +82,431 @@ def diagnose_weaknesses(model, tokenizer, eos_id):
75
  print("\n=== STEP 1: SELF-DIAGNOSIS ===")
76
 
77
  prompts = [
78
- "What kinds of questions or tasks are you worst at? Be specific and honest. List your top 5 weaknesses.",
79
- "Give me 5 examples of questions that would be hard for you to answer correctly.",
80
- "What types of reasoning do you struggle with most? Give specific examples.",
 
 
 
81
  ]
82
 
83
- weaknesses = []
84
- for prompt in prompts:
85
- p = f"<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
 
86
  ids = tokenizer(p, return_tensors="pt").to(model.device)
87
  out = model.generate(
88
- **ids, max_new_tokens=500, do_sample=True,
89
  temperature=0.7, eos_token_id=eos_id
90
  )
91
  response = tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
92
- weaknesses.append(response)
93
- print(f" Diagnosis: {response[:150]}...")
 
 
94
 
95
- return weaknesses
 
 
 
 
 
 
96
 
97
 
98
  # ============================================================
99
- # STEP 2: GENERATE — Create targeted training problems
100
  # ============================================================
101
 
102
- def generate_reasoning_problems():
103
  """
104
- Generate reasoning problems that target common weaknesses.
105
- These have VERIFIABLE answers (the reward signal for GRPO).
106
  """
107
  problems = []
108
 
109
- # Logic chain problems (model failed "yesterday Monday → tomorrow Wednesday")
110
- days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
111
- for i in range(len(days)):
112
- yesterday = days[i]
113
- today = days[(i + 1) % 7]
114
- tomorrow = days[(i + 2) % 7]
115
- problems.append({
116
- "question": f"If yesterday was {yesterday}, what day is tomorrow?",
117
- "answer": tomorrow,
118
- "type": "temporal_reasoning"
119
- })
120
- # Day after tomorrow
121
- day_after = days[(i + 3) % 7]
122
- problems.append({
123
- "question": f"If today is {today}, what day is the day after tomorrow?",
124
- "answer": day_after,
125
- "type": "temporal_reasoning"
126
- })
127
-
128
- # Trick questions (model failed "pound of feathers vs bricks")
129
- trick_qs = [
130
- ("Which is heavier: a pound of feathers or a pound of bricks?", "same", "They weigh the same — both are one pound."),
131
- ("Which is heavier: a ton of feathers or a ton of steel?", "same", "They weigh the same — both are one ton."),
132
- ("Which weighs more: 1kg of cotton or 1kg of iron?", "same", "They weigh the same — both are 1 kilogram."),
133
- ("If you have 5 apples and take away 3, how many do YOU have?", "3", "You have 3 apples — you took them."),
134
- ("A farmer has 17 sheep. All but 9 die. How many are left?", "9", "9 sheep are left — 'all but 9' means 9 survive."),
135
- ("How many times can you subtract 5 from 25?", "1", "Once — after that it's 20, not 25."),
136
- ("If there are 3 apples and you take away 2, how many do you have?", "2", "You have 2 — you took them."),
137
- ("What has a head and a tail but no body?", "coin", "A coin has a head and a tail but no body."),
138
- ]
139
- for q, key, full_answer in trick_qs:
140
- problems.append({
141
- "question": q,
142
- "answer": full_answer,
143
- "verify_key": key,
144
- "type": "trick_question"
145
- })
146
-
147
- # Syllogism / deductive reasoning
148
- syllogisms = [
149
- ("All mammals are warm-blooded. A whale is a mammal. Is a whale warm-blooded?", "yes"),
150
- ("All birds have feathers. A penguin is a bird. Does a penguin have feathers?", "yes"),
151
- ("No reptiles are mammals. A snake is a reptile. Is a snake a mammal?", "no"),
152
- ("All squares are rectangles. All rectangles have 4 sides. Do all squares have 4 sides?", "yes"),
153
- ("Some dogs are brown. Max is a dog. Is Max definitely brown?", "no"),
154
- ("All cats are animals. Some animals are pets. Are all cats pets?", "no"),
155
- ]
156
- for q, a in syllogisms:
157
- problems.append({
158
- "question": q + " Explain your reasoning.",
159
- "answer": a,
160
- "type": "syllogism"
161
- })
162
-
163
- # Multi-step reasoning
164
- multi_step = [
165
- ("If A is taller than B, and B is taller than C, who is the shortest?", "c"),
166
- ("If X is older than Y, Y is older than Z, and Z is older than W, who is the youngest?", "w"),
167
- ("In a race, Tom finished before Jerry but after Sam. Who won the race?", "sam"),
168
- ("Amy is shorter than Bob. Bob is shorter than Carol. Carol is shorter than Dave. Who is the tallest?", "dave"),
169
- ]
170
- for q, a in multi_step:
171
- problems.append({
172
- "question": q,
173
- "answer": a,
174
- "type": "multi_step"
175
- })
176
 
177
- return problems
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- def generate_math_problems(count=200):
181
- """Generate math problems with verified correct answers."""
182
- problems = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
- for _ in range(count):
185
- ptype = random.choice(["arithmetic", "word", "fraction", "percentage"])
 
 
 
 
 
 
 
 
 
 
 
186
 
187
- if ptype == "arithmetic":
188
- a, b = random.randint(10, 999), random.randint(10, 999)
189
- op = random.choice(["+", "-", "*"])
190
- if op == "+":
191
- answer = a + b
192
- elif op == "-":
193
- answer = a - b
194
- else:
195
- a, b = random.randint(2, 50), random.randint(2, 50)
196
- answer = a * b
197
  problems.append({
198
- "question": f"What is {a} {op} {b}?",
199
- "answer": str(answer),
200
- "type": "math_arithmetic"
 
 
201
  })
202
 
203
- elif ptype == "word":
204
- templates = [
205
- lambda: (f"A store sells apples for ${(p:=random.randint(1,5))} each. If you buy {(n:=random.randint(3,20))} apples, how much do you spend?", str(p*n)),
206
- lambda: (f"A train travels at {(s:=random.randint(30,120))} mph for {(h:=random.randint(1,8))} hours. How many miles does it travel?", str(s*h)),
207
- lambda: (f"If {(n:=random.randint(4,12))} friends split a ${(t:=random.randint(2,20)*n)} bill equally, how much does each person pay?", str(t//n)),
208
- lambda: (f"A rectangle has length {(l:=random.randint(3,20))} and width {(w:=random.randint(3,20))}. What is its area?", str(l*w)),
209
- ]
210
- q, a = random.choice(templates)()
211
- problems.append({"question": q, "answer": a, "type": "math_word"})
212
-
213
- elif ptype == "percentage":
214
- base = random.choice([50, 100, 200, 250, 400, 500, 1000])
215
- pct = random.choice([10, 15, 20, 25, 30, 50, 75])
216
- answer = base * pct // 100
 
 
 
 
 
 
 
 
 
 
 
 
217
  problems.append({
218
- "question": f"What is {pct}% of {base}?",
219
- "answer": str(answer),
220
- "type": "math_percentage"
221
  })
222
 
223
- elif ptype == "fraction":
224
- n = random.randint(1, 10)
225
- d = random.choice([2, 3, 4, 5, 8, 10])
226
- total = d * random.randint(2, 10)
227
- answer = total * n // d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  problems.append({
229
- "question": f"What is {n}/{d} of {total}?",
230
- "answer": str(answer),
231
- "type": "math_fraction"
232
  })
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  return problems
235
 
236
 
237
  # ============================================================
238
- # STEP 3: SCORE — Verified rewards (no learned reward model)
239
  # ============================================================
240
 
241
- def verify_answer(problem, model_answer):
242
- """
243
- Verified reward: uses the smart verify from grpo_reason.
244
- Handles verify_key for trick questions.
245
- """
246
- # Use verify_key if available (trick questions have separate verify keys)
247
- check_problem = dict(problem)
248
- if "verify_key" in check_problem:
249
- check_problem["answer"] = check_problem["verify_key"]
250
- from .grpo_reason import verify_answer as smart_verify
251
- return smart_verify(check_problem, model_answer)
252
 
253
 
254
- def generate_and_score(model, tokenizer, problems, cfg, eos_id):
255
  """
256
- Inference scaling: generate N candidates per problem, keep the best.
257
- This is the 70-80% of the cost budget (from test_16).
258
  """
259
- print(f"\n=== STEP 2-3: GENERATE & SCORE ({len(problems)} problems, {cfg.num_candidates} candidates each) ===")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- winning_pairs = [] # (question_chat, best_answer_chat) pairs for training
262
  total_correct = 0
 
263
 
264
- for i, prob in enumerate(problems):
265
- question = prob["question"]
266
- prompt = f"<|im_start|>user\n{question}<|im_end|>\n<|im_start|>assistant\n"
267
  ids = tokenizer(prompt, return_tensors="pt").to(model.device)
268
 
269
- # Generate N candidates
270
- candidates = []
 
271
  for _ in range(cfg.num_candidates):
272
  out = model.generate(
273
  **ids, max_new_tokens=cfg.max_gen_tokens,
274
  do_sample=True, temperature=cfg.temperature,
275
  eos_token_id=eos_id
276
  )
277
- answer = tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
278
- score = verify_answer(prob, answer)
279
- candidates.append((answer, score))
280
 
281
- # Pick the best candidate (highest score, shortest if tied)
282
- correct_candidates = [(a, s) for a, s in candidates if s > 0]
 
 
 
283
 
284
- if correct_candidates:
285
- # Among correct answers, prefer shorter ones (more concise)
286
- best = min(correct_candidates, key=lambda x: len(x[0]))
287
- total_correct += 1
288
-
289
- # Format as chat for training
290
- chat = f"<|im_start|>user\n{question}<|im_end|>\n<|im_start|>assistant\n{best[0]}<|im_end|>"
291
- winning_pairs.append(chat)
292
 
293
- if (i + 1) % 50 == 0:
294
- pct = total_correct / (i + 1) * 100
295
- print(f" [{i+1}/{len(problems)}] Correct so far: {total_correct}/{i+1} ({pct:.0f}%)")
296
 
297
- pct = total_correct / len(problems) * 100
298
- print(f" TOTAL: {total_correct}/{len(problems)} correct ({pct:.0f}%)")
299
- print(f" Training pairs: {len(winning_pairs)}")
 
 
 
 
 
 
300
 
301
- return winning_pairs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
 
304
  # ============================================================
305
- # STEP 4: TRAIN — Short GRPO/SFT on winning answers
306
  # ============================================================
307
 
308
  def train_on_winners(model, tokenizer, winning_pairs, cfg):
309
  """
310
- Train on the correct answers only (STaR approach).
311
- Short training — we're making small nudges, not retraining.
312
  """
313
- print(f"\n=== STEP 4: TRAIN ON WINNERS ({len(winning_pairs)} pairs) ===")
314
 
315
- if len(winning_pairs) < 10:
316
  print(" Too few winning pairs — skipping training")
317
  return model
318
 
@@ -320,18 +514,22 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
320
  from transformers import TrainingArguments, Trainer
321
  from torch.utils.data import Dataset
322
 
323
- # LoRA — small rank for targeted improvement
 
 
 
324
  lora_config = LoraConfig(
325
  r=cfg.lora_r, lora_alpha=cfg.lora_alpha, lora_dropout=0.0,
326
  target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
327
  "gate_proj", "up_proj", "down_proj"],
 
328
  bias="none", task_type=TaskType.CAUSAL_LM,
329
  )
330
  model = get_peft_model(model, lora_config)
331
  model.print_trainable_parameters()
332
 
333
- class WinnerDataset(Dataset):
334
- def __init__(self, texts, tokenizer, max_len=512):
335
  self.data = []
336
  for t in texts:
337
  e = tokenizer(t, truncation=True, max_length=max_len,
@@ -344,12 +542,13 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
344
  def __len__(self): return len(self.data)
345
  def __getitem__(self, i): return self.data[i]
346
 
347
- dataset = WinnerDataset(winning_pairs, tokenizer)
348
 
349
  out_dir = Path(cfg.output_dir) / "train_output"
350
  out_dir.mkdir(parents=True, exist_ok=True)
351
 
352
  total_steps = (len(dataset) * cfg.train_epochs) // (cfg.train_batch * cfg.train_grad_accum)
 
353
 
354
  args = TrainingArguments(
355
  output_dir=str(out_dir),
@@ -358,10 +557,10 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
358
  gradient_accumulation_steps=cfg.train_grad_accum,
359
  learning_rate=cfg.learning_rate,
360
  bf16=True,
361
- logging_steps=max(1, total_steps // 10),
362
  save_strategy="steps",
363
- save_steps=max(50, total_steps // 4),
364
- save_total_limit=1,
365
  warmup_ratio=0.05,
366
  lr_scheduler_type="cosine",
367
  optim="adamw_torch",
@@ -373,91 +572,120 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
373
  train_dataset=dataset, args=args
374
  )
375
 
376
- print(f" Training: ~{total_steps} steps")
377
  trainer.train()
378
 
379
- # Merge LoRA FIRST, then clean checkpoints (safer)
380
  print(" Merging LoRA...")
381
  merged = model.merge_and_unload()
382
  gc.collect()
383
 
384
- import shutil
385
  shutil.rmtree(str(out_dir), ignore_errors=True)
 
386
 
387
  return merged
388
 
389
 
390
  # ============================================================
391
- # STEP 5: BENCHMARK — Measure improvement
392
  # ============================================================
393
 
394
  def benchmark(model, tokenizer, eos_id):
395
- """Run the standard benchmark to measure improvement."""
396
  print("\n=== STEP 5: BENCHMARK ===")
397
-
398
  results = {}
399
 
400
- def ask(prompt):
401
- p = f"<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
 
 
 
402
  ids = tokenizer(p, return_tensors="pt").to(model.device)
403
- out = model.generate(**ids, max_new_tokens=200, do_sample=False, eos_token_id=eos_id)
404
  return tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
405
 
406
- # Math — using smart verify
407
- math_tests = [("7+8", "15"), ("123+456", "579"), ("1000-387", "613"), ("12*13", "156"), ("144/12", "12")]
408
- math_correct = sum(1 for q, e in math_tests if verify_answer({"answer": e}, ask(f"What is {q}? Give just the number.")) > 0)
409
- results["basic_math"] = f"{math_correct}/5"
410
- print(f" Math: {math_correct}/5")
411
-
412
- # Reasoning — using smart verify
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  reason_tests = [
414
- ("If all roses are flowers and all flowers need water, do roses need water?", "yes"),
415
  ("Which is heavier: a pound of feathers or a pound of bricks?", "same"),
416
  ("If yesterday was Monday, what day is tomorrow?", "wednesday"),
417
  ("A farmer has 17 sheep. All but 9 die. How many are left?", "9"),
418
- ("If you have 5 apples and take away 3, how many do YOU have?", "3"),
 
 
 
 
 
 
419
  ]
420
- reason_correct = 0
421
  for q, expected in reason_tests:
422
- a = ask(q)
423
- correct = verify_answer({"answer": expected}, a) > 0
424
- reason_correct += correct
425
- print(f" {'OK' if correct else 'FAIL'}: {q[:50]}... -> {a[:60]}")
426
- results["reasoning"] = f"{reason_correct}/5"
427
- print(f" Reasoning: {reason_correct}/5")
428
-
429
- # Word problems — using smart verify
430
- wp_tests = [
431
- ("A train travels 60 mph for 2.5 hours. How far does it go?", "150"),
432
- ("If 3 shirts cost $45, how much do 7 shirts cost?", "105"),
433
- ("I have 24 cookies split equally among 6 friends. How many each?", "4"),
 
 
434
  ]
435
- wp_correct = sum(1 for q, e in wp_tests if verify_answer({"answer": e}, ask(q)) > 0)
436
- results["word_problems"] = f"{wp_correct}/3"
437
- print(f" Word problems: {wp_correct}/3")
 
 
 
 
 
 
 
438
 
439
  # Perplexity
440
  test_text = "The quick brown fox jumps over the lazy dog. Machine learning models can process natural language."
441
  enc = tokenizer(test_text, return_tensors="pt").to(model.device)
442
  with torch.no_grad():
443
  loss = model(**enc, labels=enc.input_ids).loss.item()
444
- ppl = math.exp(loss)
445
- results["perplexity"] = f"{ppl:.2f}"
446
- print(f" Perplexity: {ppl:.2f}")
447
 
448
  return results
449
 
450
 
451
  # ============================================================
452
- # MAIN: Run one self-improvement cycle
453
  # ============================================================
454
 
455
  def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
456
- """
457
- Run one complete self-improvement cycle.
458
-
459
- Returns path to improved model.
460
- """
461
  if cfg is None:
462
  cfg = SelfImproveConfig()
463
 
@@ -468,9 +696,16 @@ def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
468
  print(f"Started: {time.strftime('%H:%M:%S')}")
469
  print("=" * 60)
470
 
 
 
 
 
 
 
 
 
471
  # Load model
472
  from transformers import AutoModelForImageTextToText, AutoTokenizer
473
-
474
  print("\nLoading model...")
475
  model = AutoModelForImageTextToText.from_pretrained(
476
  cfg.model_path, dtype=torch.bfloat16,
@@ -479,52 +714,71 @@ def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
479
  tokenizer = AutoTokenizer.from_pretrained(cfg.model_path, trust_remote_code=True)
480
  eos_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
481
 
482
- # Step 1: Diagnose
483
- weaknesses = diagnose_weaknesses(model, tokenizer, eos_id)
484
-
485
- # Step 2: Generate problems
486
- print("\n=== STEP 2: GENERATING PROBLEMS ===")
487
- problems = []
488
- problems.extend(generate_reasoning_problems())
489
- print(f" Reasoning problems: {len(problems)}")
490
- math_probs = generate_math_problems(cfg.num_math_problems)
491
- problems.extend(math_probs)
492
- print(f" Math problems: {len(math_probs)}")
493
- print(f" Total: {len(problems)}")
494
 
495
- random.shuffle(problems)
496
-
497
- # Step 3: Generate candidates and score
498
- winning_pairs = generate_and_score(model, tokenizer, problems, cfg, eos_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
- # Step 4: Train on winners
501
  model = train_on_winners(model, tokenizer, winning_pairs, cfg)
502
 
503
  # Save improved model
504
- improved_dir = Path(cfg.output_dir) / f"cycle_{cycle_num}"
505
- improved_dir.mkdir(parents=True, exist_ok=True)
506
- print(f"\nSaving improved model to {improved_dir}...")
507
- model.save_pretrained(str(improved_dir), safe_serialization=True)
508
- tokenizer.save_pretrained(str(improved_dir))
509
- sz = sum(f.stat().st_size for f in improved_dir.glob("*.safetensors")) / 1e9
510
- print(f"SAVED: {improved_dir} ({sz:.1f} GB)")
511
-
512
- # Step 5: Benchmark
513
- results = benchmark(model, tokenizer, eos_id)
514
-
515
- # Save results
516
- results_file = improved_dir / "benchmark_results.json"
517
- results["cycle"] = cycle_num
518
- results["timestamp"] = time.strftime("%Y-%m-%d %H:%M:%S")
519
- results["duration_min"] = (time.time() - start) / 60
520
- with open(results_file, "w") as f:
521
- json.dump(results, f, indent=2)
522
-
523
  elapsed = (time.time() - start) / 60
524
  print(f"\n{'=' * 60}")
525
  print(f"CYCLE {cycle_num} COMPLETE — {elapsed:.1f} min")
526
- print(f"Results: {results}")
527
- print(f"Model saved to: {improved_dir}")
 
528
  print(f"{'=' * 60}")
529
 
530
- return str(improved_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  TD Self-Improvement Loop — the core of Time Dilation.
3
 
4
+ THE LOOP (from test_1 through test_18 interviews):
 
 
 
5
  1. Ask the model "what are you bad at?" → it identifies weak spots
6
+ 2. Generate HARD problems targeting those weaknesses (stuff it fails 30-50%)
7
+ 3. Generate N candidates per problem, score with verified rewards
8
+ 4. Train on winners (STaR/GRPO approach)
9
+ 5. Benchmark → measure improvement
10
+ 6. Save → repeat
11
+
12
+ KEY DIFFERENCE FROM GRPO CYCLE 2:
13
+ - Problems are HARDER — designed so the model fails 30-50% of the time
14
+ - This creates real contrast for learning (correct vs wrong chains)
15
+ - Uses CoT (<think>...</think>) for all reasoning
16
+ - Saves after every major step (diagnosis, generation, training)
17
+ - Targets layers 16-28 specifically for reasoning improvement
18
+
19
+ PRINCIPLES (from interviews + dad's tests):
20
+ - Verified rewards only: math correct, logic valid, no learned reward model
21
  - Cherry_LLM perplexity filter prevents mode collapse
22
+ - Mix problem types to avoid overfitting
23
+ - Small nudges per cycle (1-5%), compounds over time
 
 
 
 
 
24
  """
25
 
26
  import torch
 
29
  import math
30
  import random
31
  import gc
32
+ import re
33
+ import shutil
34
  from pathlib import Path
35
+ from typing import List, Dict, Optional
36
+ from dataclasses import dataclass
37
 
38
 
39
  @dataclass
40
  class SelfImproveConfig:
41
  """Configuration for one self-improvement cycle."""
42
+ model_path: str = "td_fuse_outputs/reasoning_healed"
43
+ output_dir: str = "td_fuse_outputs/self_improve"
44
+ progress_file: str = "td_fuse_outputs/self_improve/progress.json"
45
+
46
+ # Generation
47
+ num_candidates: int = 8 # Generate N answers per question, pick best
48
+ max_gen_tokens: int = 1024 # Long — CoT needs room
49
+ temperature: float = 0.8 # Higher than GRPO — we want diversity + some failures
50
 
51
+ # Problems — more than GRPO, and harder
52
+ num_hard_math: int = 150 # Multi-step math (the model should fail ~30-50%)
53
+ num_hard_logic: int = 100 # Complex logic chains
54
+ num_hard_tricks: int = 50 # Tricky questions that trip up LLMs
55
+ num_hard_coding: int = 50 # Code reasoning problems
56
+ batch_save_every: int = 25 # Save progress more often
57
 
58
+ # Training
59
+ lora_r: int = 32 # Higher rank — more capacity for reasoning
60
+ lora_alpha: int = 64
61
+ train_epochs: int = 3
62
+ train_batch: int = 2
63
+ train_grad_accum: int = 8 # Effective batch = 16
64
+ learning_rate: float = 1e-5 # Gentle
65
+ save_steps: int = 50
66
+ target_layers: str = "16-28" # Reasoning circuit (from test_18)
67
 
 
 
 
 
68
 
69
+ # Import verify from grpo_reason
70
+ from .grpo_reason import verify_answer as smart_verify
71
 
72
 
73
  # ============================================================
74
  # STEP 1: DIAGNOSE — Ask the model what it's bad at
75
  # ============================================================
76
 
77
+ def diagnose_weaknesses(model, tokenizer, eos_id, save_path=None):
78
  """
79
  Ask the model to identify its own weaknesses.
80
  All 3 AIs (ChatGPT, Grok, Gemini) confirmed models can do this.
 
82
  print("\n=== STEP 1: SELF-DIAGNOSIS ===")
83
 
84
  prompts = [
85
+ ("What kinds of questions are you worst at? Be brutally honest. "
86
+ "List your top 5 weaknesses with specific examples of questions you'd get wrong."),
87
+ ("Give me 10 math or logic problems that would be genuinely hard for you. "
88
+ "Problems where you'd likely make mistakes. Be specific."),
89
+ ("What types of multi-step reasoning cause you to make errors? "
90
+ "Give 5 specific examples where you'd probably get the wrong answer."),
91
  ]
92
 
93
+ all_responses = []
94
+ for i, prompt in enumerate(prompts):
95
+ p = (f"<|im_start|>user\n{prompt}<|im_end|>\n"
96
+ f"<|im_start|>assistant\n<think>\nLet me think carefully about my actual weaknesses.\n")
97
  ids = tokenizer(p, return_tensors="pt").to(model.device)
98
  out = model.generate(
99
+ **ids, max_new_tokens=800, do_sample=True,
100
  temperature=0.7, eos_token_id=eos_id
101
  )
102
  response = tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
103
+ all_responses.append(response)
104
+ # Show the answer part (after </think>) if available
105
+ display = response.split("</think>")[-1].strip() if "</think>" in response else response
106
+ print(f" Diagnosis {i+1}: {display[:200]}...")
107
 
108
+ # Save diagnosis
109
+ if save_path:
110
+ with open(save_path, "w") as f:
111
+ json.dump({"diagnoses": all_responses, "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")}, f, indent=2)
112
+ print(f" Saved diagnosis to {save_path}")
113
+
114
+ return all_responses
115
 
116
 
117
  # ============================================================
118
+ # STEP 2: GENERATE HARD PROBLEMS
119
  # ============================================================
120
 
121
+ def generate_hard_problems(cfg: SelfImproveConfig) -> List[Dict]:
122
  """
123
+ Generate problems that are HARD enough for the model to fail 30-50%.
124
+ This is the key difference from GRPO cycle 2 (which was too easy at 97%).
125
  """
126
  problems = []
127
 
128
+ # --- HARD MATH: multi-step, carrying, fractions, negative numbers ---
129
+ for _ in range(cfg.num_hard_math):
130
+ ptype = random.choice([
131
+ "multi_step", "carrying", "negative", "fraction_word",
132
+ "percentage_chain", "rate_comparison", "sequence_hard"
133
+ ])
134
+
135
+ if ptype == "multi_step":
136
+ # 3-4 step word problems
137
+ items = random.choice(["books", "apples", "shirts", "tickets", "cookies"])
138
+ start = random.randint(20, 100)
139
+ buy = random.randint(5, 30)
140
+ give = random.randint(1, min(15, start + buy - 1))
141
+ lose = random.randint(1, min(5, start + buy - give - 1))
142
+ final = start + buy - give - lose
143
+ problems.append({
144
+ "question": (f"You have {start} {items}. You buy {buy} more. "
145
+ f"You give {give} to a friend. You lose {lose}. "
146
+ f"How many {items} do you have now?"),
147
+ "answer": str(final), "type": "hard_math",
148
+ "think_hint": f"{start} + {buy} - {give} - {lose} = {final}"
149
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ elif ptype == "carrying":
152
+ # Large number arithmetic that requires carrying
153
+ a = random.randint(500, 9999)
154
+ b = random.randint(500, 9999)
155
+ op = random.choice(["+", "-", "*"])
156
+ if op == "+": answer = a + b
157
+ elif op == "-": answer = a - b
158
+ else:
159
+ a = random.randint(20, 200)
160
+ b = random.randint(20, 200)
161
+ answer = a * b
162
+ problems.append({
163
+ "question": f"Calculate: {a} {op} {b}",
164
+ "answer": str(answer), "type": "hard_math",
165
+ "think_hint": f"{a} {op} {b} = {answer}"
166
+ })
167
 
168
+ elif ptype == "negative":
169
+ a = random.randint(-50, 50)
170
+ b = random.randint(-50, 50)
171
+ op = random.choice(["+", "-", "*"])
172
+ if op == "+": answer = a + b
173
+ elif op == "-": answer = a - b
174
+ else: answer = a * b
175
+ a_str = str(a) if a >= 0 else f"({a})"
176
+ b_str = str(b) if b >= 0 else f"({b})"
177
+ problems.append({
178
+ "question": f"What is {a_str} {op} {b_str}?",
179
+ "answer": str(answer), "type": "hard_math",
180
+ "think_hint": f"{a} {op} {b} = {answer}"
181
+ })
182
 
183
+ elif ptype == "fraction_word":
184
+ total = random.choice([12, 16, 20, 24, 30, 36, 40, 48, 60])
185
+ frac_n = random.choice([1, 2, 3])
186
+ frac_d = random.choice([2, 3, 4, 5, 6])
187
+ while frac_n >= frac_d:
188
+ frac_n = random.choice([1, 2, 3])
189
+ first = total * frac_n // frac_d
190
+ remaining = total - first
191
+ second_frac = random.choice([2, 3, 4])
192
+ second = remaining // second_frac
193
+ final = remaining - second
194
+ problems.append({
195
+ "question": (f"A jar has {total} marbles. You take {frac_n}/{frac_d} of them. "
196
+ f"Then you give away {second} of what's left. How many remain in the jar?"),
197
+ "answer": str(final), "type": "hard_math",
198
+ "think_hint": (f"Take {frac_n}/{frac_d} of {total} = {first}. "
199
+ f"Left: {remaining}. Give {second}. Final: {final}")
200
+ })
201
 
202
+ elif ptype == "percentage_chain":
203
+ base = random.choice([100, 200, 400, 500, 800, 1000])
204
+ pct1 = random.choice([10, 20, 25, 30, 50])
205
+ after1 = base + base * pct1 // 100
206
+ pct2 = random.choice([10, 20, 25])
207
+ final = after1 - after1 * pct2 // 100
208
+ problems.append({
209
+ "question": (f"A stock is worth ${base}. It goes up {pct1}%, "
210
+ f"then drops {pct2}%. What is it worth now?"),
211
+ "answer": str(final), "type": "hard_math",
212
+ "think_hint": (f"Up {pct1}%: {base} + {base*pct1//100} = {after1}. "
213
+ f"Down {pct2}%: {after1} - {after1*pct2//100} = {final}")
214
+ })
215
 
216
+ elif ptype == "rate_comparison":
217
+ speed1 = random.randint(30, 80)
218
+ time1 = random.randint(2, 6)
219
+ speed2 = random.randint(40, 100)
220
+ time2 = random.randint(1, 5)
221
+ dist1 = speed1 * time1
222
+ dist2 = speed2 * time2
223
+ diff = abs(dist1 - dist2)
224
+ who = "first" if dist1 > dist2 else "second"
 
225
  problems.append({
226
+ "question": (f"Car A drives {speed1} mph for {time1} hours. "
227
+ f"Car B drives {speed2} mph for {time2} hours. "
228
+ f"How much further did the car that drove more go?"),
229
+ "answer": str(diff), "type": "hard_math",
230
+ "think_hint": f"Car A: {dist1}mi. Car B: {dist2}mi. Diff: {diff}mi."
231
  })
232
 
233
+ elif ptype == "sequence_hard":
234
+ # Non-obvious sequences
235
+ seq_type = random.choice(["quadratic", "fibonacci_like", "alternating"])
236
+ if seq_type == "quadratic":
237
+ a, b, c = random.randint(1, 3), random.randint(1, 5), random.randint(0, 10)
238
+ seq = [a*n*n + b*n + c for n in range(1, 6)]
239
+ next_val = a*36 + b*6 + c
240
+ elif seq_type == "fibonacci_like":
241
+ a, b = random.randint(1, 5), random.randint(1, 5)
242
+ seq = [a, b]
243
+ for _ in range(4):
244
+ seq.append(seq[-1] + seq[-2])
245
+ next_val = seq[-1] + seq[-2]
246
+ seq = seq[:5]
247
+ else: # alternating
248
+ start = random.randint(1, 10)
249
+ step1 = random.randint(2, 5)
250
+ step2 = random.randint(1, 3)
251
+ seq = [start]
252
+ for i in range(4):
253
+ if i % 2 == 0:
254
+ seq.append(seq[-1] + step1)
255
+ else:
256
+ seq.append(seq[-1] - step2)
257
+ # Next follows the pattern
258
+ next_val = seq[-1] + step1 if len(seq) % 2 == 1 else seq[-1] - step2
259
  problems.append({
260
+ "question": f"What comes next: {', '.join(map(str, seq))}?",
261
+ "answer": str(next_val), "type": "hard_math",
 
262
  })
263
 
264
+ # --- HARD LOGIC: longer chains, negations, misdirection ---
265
+ logic_templates = [
266
+ # 4+ entity ordering
267
+ ("Amy is taller than Bob. Carol is shorter than Bob but taller than Dave. "
268
+ "Eve is taller than Amy. Who is the shortest?",
269
+ "dave", "Eve > Amy > Bob > Carol > Dave. Dave is shortest."),
270
+ ("In a tournament: A beat B, C beat A, D beat C, B beat D. Who beat the most players?",
271
+ "1", "Each player beat exactly 1 other player (it's circular)."),
272
+ # Negation chains
273
+ ("If it's not true that all cats are black, and Fluffy is a cat, "
274
+ "can we conclude Fluffy is not black?",
275
+ "no", "We can't conclude either way — some cats might be black, some not."),
276
+ ("None of the winners cheated. Tom didn't cheat. Did Tom win?",
277
+ "no", "Not cheating is necessary but not sufficient for winning."),
278
+ # Conditional logic
279
+ ("If it rains, the ground is wet. The ground is wet. Did it rain?",
280
+ "no", "The ground could be wet for other reasons (sprinkler, etc). Affirming the consequent."),
281
+ ("If it rains, the ground is wet. It didn't rain. Is the ground dry?",
282
+ "no", "The ground could still be wet from other causes. Denying the antecedent."),
283
+ ("If A then B. If B then C. A is true. Is C true?",
284
+ "yes", "A→B→C. A is true, so B is true, so C is true. Modus ponens chain."),
285
+ ("If A then B. If B then C. C is false. Is A true?",
286
+ "no", "C is false means B must be false (contrapositive), which means A must be false."),
287
+ # Set theory
288
+ ("In a class of 30 students, 18 play soccer and 15 play basketball. "
289
+ "At least how many play both?",
290
+ "3", "18 + 15 = 33. Must overlap by at least 33 - 30 = 3."),
291
+ ("A group has 40 people. 25 speak English, 20 speak French, and 10 speak both. "
292
+ "How many speak neither?",
293
+ "5", "English or French: 25 + 20 - 10 = 35. Neither: 40 - 35 = 5."),
294
+ ]
295
+
296
+ for _ in range(cfg.num_hard_logic):
297
+ if logic_templates:
298
+ q, a, hint = random.choice(logic_templates)
299
  problems.append({
300
+ "question": q, "answer": a, "type": "hard_logic",
301
+ "think_hint": hint
 
302
  })
303
 
304
+ # Also generate parameterized ordering problems (harder: 5-6 entities)
305
+ for _ in range(cfg.num_hard_logic // 2):
306
+ names = random.sample(["Amy", "Bob", "Carol", "Dave", "Eve", "Frank"], 5)
307
+ # Create a random ordering
308
+ order = list(range(5))
309
+ random.shuffle(order)
310
+ # Generate comparison clues (not all pairs — make them work it out)
311
+ clues = []
312
+ for i in range(4):
313
+ a_idx = order[i]
314
+ b_idx = order[i + 1]
315
+ clues.append(f"{names[a_idx]} is taller than {names[b_idx]}")
316
+ random.shuffle(clues)
317
+ tallest = names[order[0]]
318
+ shortest = names[order[4]]
319
+ q_type = random.choice(["tallest", "shortest"])
320
+ answer = tallest.lower() if q_type == "tallest" else shortest.lower()
321
+ problems.append({
322
+ "question": f"{'. '.join(clues)}. Who is the {q_type}?",
323
+ "answer": answer, "type": "hard_logic",
324
+ })
325
+
326
+ # --- HARD TRICKS: LLM failure modes ---
327
+ tricks = [
328
+ ("How many times can you subtract 5 from 25?",
329
+ "once", "Only once — after that it's 20, not 25."),
330
+ ("How many months have 28 days?",
331
+ "all", "All 12 months have at least 28 days."),
332
+ ("I have 6 eggs. I broke 2, cooked 2, and ate 2. How many eggs do I have left?",
333
+ "4", "You broke 2 to cook them, and ate the cooked ones. Same 2 eggs through the process. 6-2=4."),
334
+ ("A doctor gives you 3 pills and says take one every 30 minutes. How long do they last?",
335
+ "60", "Take first at 0 min, second at 30 min, third at 60 min. Total: 60 minutes."),
336
+ ("How many times does the digit 9 appear between 1 and 100?",
337
+ "20", "9,19,29,39,49,59,69,79,89 (nine times in ones) + 90-99 (ten times in tens, plus 99 double-counts). Total: 20."),
338
+ ("If you overtake the last person in a race, what position are you in?",
339
+ "impossible", "You can't overtake the last person — if you're behind them, YOU are last."),
340
+ ("A bat and ball cost $1.10 total. The bat costs $1 more than the ball. How much is the ball?",
341
+ "5", "Ball = x, Bat = x+1.00. Total: 2x+1.00=1.10. x=0.05. Ball costs $0.05 or 5 cents."),
342
+ ("If 5 machines take 5 minutes to make 5 widgets, how long for 100 machines to make 100 widgets?",
343
+ "5", "Each machine makes 1 widget in 5 minutes. 100 machines make 100 widgets in 5 minutes."),
344
+ ("You're in a dark room with a candle, a wood stove, and a gas lamp. You have one match. What do you light first?",
345
+ "match", "You light the match first — you need it to light anything else."),
346
+ ("Is it legal for a man to marry his widow's sister?",
347
+ "no", "If he has a widow, he's dead. Dead people can't marry."),
348
+ ]
349
+ for _ in range(cfg.num_hard_tricks):
350
+ q, a, hint = random.choice(tricks)
351
+ problems.append({
352
+ "question": q, "answer": a, "type": "hard_trick",
353
+ "think_hint": hint
354
+ })
355
+
356
+ # --- CODE REASONING (no actual code execution, just logic) ---
357
+ code_problems = [
358
+ ("What does this print?\nx = 5\nx = x + 3\nprint(x)", "8",
359
+ "x starts at 5, then x = 5+3 = 8. Prints 8."),
360
+ ("What does this print?\nfor i in range(3): print(i)\n(List all numbers printed)", "0 1 2",
361
+ "range(3) gives 0, 1, 2."),
362
+ ("What's wrong with this code?\ndef add(a, b):\n return a - b", "subtracts instead of adds",
363
+ "Function named 'add' but uses minus operator."),
364
+ ("What does this evaluate to?\nTrue and False or True", "true",
365
+ "(True and False) = False. False or True = True."),
366
+ ("After this code runs, what is x?\nx = [1,2,3]\ny = x\ny.append(4)\nprint(len(x))", "4",
367
+ "y = x makes y point to same list. Appending to y also changes x. len(x) = 4."),
368
+ ]
369
+ for _ in range(cfg.num_hard_coding):
370
+ q, a, hint = random.choice(code_problems)
371
+ problems.append({
372
+ "question": q, "answer": a.lower(), "type": "hard_coding",
373
+ "think_hint": hint
374
+ })
375
+
376
+ random.shuffle(problems)
377
+ print(f" Generated {len(problems)} hard problems")
378
+ print(f" Math: {cfg.num_hard_math}, Logic: {cfg.num_hard_logic + cfg.num_hard_logic//2}, "
379
+ f"Tricks: {cfg.num_hard_tricks}, Code: {cfg.num_hard_coding}")
380
+
381
  return problems
382
 
383
 
384
  # ============================================================
385
+ # STEP 3: GENERATE & SCORE WITH COT
386
  # ============================================================
387
 
388
+ def build_cot_prompt(question: str) -> str:
389
+ """Build prompt that encourages chain-of-thought reasoning."""
390
+ return (
391
+ f"<|im_start|>user\n{question}\n"
392
+ f"Think step by step before giving your final answer.<|im_end|>\n"
393
+ f"<|im_start|>assistant\n<think>\n"
394
+ )
 
 
 
 
395
 
396
 
397
+ def generate_and_score(model, tokenizer, problems, cfg, eos_id, resume_from=0):
398
  """
399
+ Generate CoT candidates, score them, return winning pairs.
400
+ Saves progress every batch_save_every problems.
401
  """
402
+ print(f"\n=== STEP 3: GENERATE & SCORE ({len(problems)} problems, {cfg.num_candidates} candidates) ===")
403
+ gen_start = time.time()
404
+
405
+ progress_path = Path(cfg.progress_file)
406
+ progress_path.parent.mkdir(parents=True, exist_ok=True)
407
+
408
+ winning_pairs = []
409
+ losing_pairs = []
410
+
411
+ if resume_from > 0 and progress_path.exists():
412
+ with open(progress_path) as f:
413
+ saved = json.load(f)
414
+ winning_pairs = saved.get("winning_pairs", [])
415
+ losing_pairs = saved.get("losing_pairs", [])
416
+ print(f" Resumed from problem {resume_from}, {len(winning_pairs)} winners loaded")
417
 
 
418
  total_correct = 0
419
+ total_attempted = resume_from
420
 
421
+ for i in range(resume_from, len(problems)):
422
+ prob = problems[i]
423
+ prompt = build_cot_prompt(prob["question"])
424
  ids = tokenizer(prompt, return_tensors="pt").to(model.device)
425
 
426
+ correct_responses = []
427
+ wrong_responses = []
428
+
429
  for _ in range(cfg.num_candidates):
430
  out = model.generate(
431
  **ids, max_new_tokens=cfg.max_gen_tokens,
432
  do_sample=True, temperature=cfg.temperature,
433
  eos_token_id=eos_id
434
  )
435
+ response = tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
 
 
436
 
437
+ # Use verify_key if it exists (trick questions)
438
+ check_prob = dict(prob)
439
+ if "verify_key" in check_prob:
440
+ check_prob["answer"] = check_prob["verify_key"]
441
+ score = smart_verify(check_prob, response)
442
 
443
+ if score > 0:
444
+ correct_responses.append(response)
445
+ else:
446
+ wrong_responses.append(response)
 
 
 
 
447
 
448
+ total_attempted += 1
 
 
449
 
450
+ if correct_responses:
451
+ total_correct += 1
452
+ best = min(correct_responses, key=len) # Shortest correct = most concise
453
+ chat = (
454
+ f"<|im_start|>user\n{prob['question']}\n"
455
+ f"Think step by step before giving your final answer.<|im_end|>\n"
456
+ f"<|im_start|>assistant\n{best}<|im_end|>"
457
+ )
458
+ winning_pairs.append(chat)
459
 
460
+ if wrong_responses:
461
+ worst = max(wrong_responses, key=len)
462
+ losing_pairs.append(
463
+ f"<|im_start|>user\n{prob['question']}<|im_end|>\n"
464
+ f"<|im_start|>assistant\n{worst}<|im_end|>"
465
+ )
466
+
467
+ # Progress
468
+ if (i + 1) % cfg.batch_save_every == 0 or i == len(problems) - 1:
469
+ pct = total_correct / max(1, total_attempted) * 100
470
+ elapsed = time.time() - gen_start
471
+ rate = (i + 1 - resume_from) / max(1, elapsed)
472
+ eta = (len(problems) - i - 1) / max(0.001, rate) / 60
473
+ print(f" [{i+1}/{len(problems)}] Correct: {total_correct}/{total_attempted} ({pct:.0f}%) "
474
+ f"| Winners: {len(winning_pairs)} | ETA: {eta:.0f}min")
475
+
476
+ with open(progress_path, "w") as f:
477
+ json.dump({
478
+ "winning_pairs": winning_pairs,
479
+ "losing_pairs": losing_pairs,
480
+ "problems_completed": i + 1,
481
+ "total_correct": total_correct,
482
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
483
+ }, f)
484
+
485
+ if torch.cuda.is_available():
486
+ torch.cuda.empty_cache()
487
+
488
+ fail_rate = 100 - (total_correct / max(1, total_attempted) * 100)
489
+ print(f"\n FINAL: {total_correct}/{total_attempted} correct "
490
+ f"({total_correct/max(1,total_attempted)*100:.0f}%)")
491
+ print(f" Failure rate: {fail_rate:.0f}% "
492
+ f"({'GOOD — enough contrast for learning' if 15 < fail_rate < 60 else 'WARNING — might be too easy or too hard'})")
493
+ print(f" Winners: {len(winning_pairs)} | Losers: {len(losing_pairs)}")
494
+
495
+ return winning_pairs, losing_pairs
496
 
497
 
498
  # ============================================================
499
+ # STEP 4: TRAIN — SFT on winning CoT chains
500
  # ============================================================
501
 
502
  def train_on_winners(model, tokenizer, winning_pairs, cfg):
503
  """
504
+ Train on correct chain-of-thought responses (STaR approach).
505
+ Targets layers 16-28 for reasoning improvement.
506
  """
507
+ print(f"\n=== STEP 4: TRAIN ON WINNERS ({len(winning_pairs)} chains) ===")
508
 
509
+ if len(winning_pairs) < 20:
510
  print(" Too few winning pairs — skipping training")
511
  return model
512
 
 
514
  from transformers import TrainingArguments, Trainer
515
  from torch.utils.data import Dataset
516
 
517
+ # Parse target layers
518
+ layer_start, layer_end = map(int, cfg.target_layers.split("-"))
519
+ target_layers = list(range(layer_start, layer_end + 1))
520
+
521
  lora_config = LoraConfig(
522
  r=cfg.lora_r, lora_alpha=cfg.lora_alpha, lora_dropout=0.0,
523
  target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
524
  "gate_proj", "up_proj", "down_proj"],
525
+ layers_to_transform=target_layers,
526
  bias="none", task_type=TaskType.CAUSAL_LM,
527
  )
528
  model = get_peft_model(model, lora_config)
529
  model.print_trainable_parameters()
530
 
531
+ class CoTDataset(Dataset):
532
+ def __init__(self, texts, tokenizer, max_len=1024):
533
  self.data = []
534
  for t in texts:
535
  e = tokenizer(t, truncation=True, max_length=max_len,
 
542
  def __len__(self): return len(self.data)
543
  def __getitem__(self, i): return self.data[i]
544
 
545
+ dataset = CoTDataset(winning_pairs, tokenizer)
546
 
547
  out_dir = Path(cfg.output_dir) / "train_output"
548
  out_dir.mkdir(parents=True, exist_ok=True)
549
 
550
  total_steps = (len(dataset) * cfg.train_epochs) // (cfg.train_batch * cfg.train_grad_accum)
551
+ print(f" Steps: ~{total_steps} | Target layers: {cfg.target_layers}")
552
 
553
  args = TrainingArguments(
554
  output_dir=str(out_dir),
 
557
  gradient_accumulation_steps=cfg.train_grad_accum,
558
  learning_rate=cfg.learning_rate,
559
  bf16=True,
560
+ logging_steps=max(1, total_steps // 20),
561
  save_strategy="steps",
562
+ save_steps=cfg.save_steps,
563
+ save_total_limit=2,
564
  warmup_ratio=0.05,
565
  lr_scheduler_type="cosine",
566
  optim="adamw_torch",
 
572
  train_dataset=dataset, args=args
573
  )
574
 
575
+ print(" Training...")
576
  trainer.train()
577
 
 
578
  print(" Merging LoRA...")
579
  merged = model.merge_and_unload()
580
  gc.collect()
581
 
 
582
  shutil.rmtree(str(out_dir), ignore_errors=True)
583
+ gc.collect()
584
 
585
  return merged
586
 
587
 
588
  # ============================================================
589
+ # STEP 5: BENCHMARK
590
  # ============================================================
591
 
592
  def benchmark(model, tokenizer, eos_id):
593
+ """Comprehensive reasoning benchmark."""
594
  print("\n=== STEP 5: BENCHMARK ===")
 
595
  results = {}
596
 
597
+ def ask(prompt, use_cot=False):
598
+ if use_cot:
599
+ p = build_cot_prompt(prompt)
600
+ else:
601
+ p = f"<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
602
  ids = tokenizer(p, return_tensors="pt").to(model.device)
603
+ out = model.generate(**ids, max_new_tokens=500, do_sample=False, eos_token_id=eos_id)
604
  return tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
605
 
606
+ # Basic math
607
+ math_tests = [("7+8", "15"), ("123+456", "579"), ("1000-387", "613"),
608
+ ("12*13", "156"), ("144/12", "12")]
609
+ mc = sum(1 for q, e in math_tests
610
+ if smart_verify({"answer": e}, ask(f"What is {q}? Give just the number.")) > 0)
611
+ results["math"] = f"{mc}/5"
612
+ print(f" Math: {mc}/5")
613
+
614
+ # Hard math (the stuff GRPO couldn't handle)
615
+ hard_math = [
616
+ ("A stock is worth $200. It goes up 25%, then drops 20%. What is it worth?", "200"),
617
+ ("You have 50 cookies. You eat 1/5, give 10 away, then split the rest equally among 4 friends. How many each?", "7"),
618
+ ("Car A drives 60mph for 3 hours. Car B drives 80mph for 2 hours. How much further did Car A go?", "20"),
619
+ ]
620
+ hmc = 0
621
+ for q, e in hard_math:
622
+ a = ask(q, use_cot=True)
623
+ correct = smart_verify({"answer": e}, a) > 0
624
+ hmc += correct
625
+ status = "OK" if correct else "FAIL"
626
+ answer_display = a.split("</think>")[-1].strip() if "</think>" in a else a
627
+ print(f" {status}: {q[:55]}... -> {answer_display[:80]}")
628
+ results["hard_math"] = f"{hmc}/3"
629
+ print(f" Hard math: {hmc}/3")
630
+
631
+ # Reasoning with CoT
632
  reason_tests = [
 
633
  ("Which is heavier: a pound of feathers or a pound of bricks?", "same"),
634
  ("If yesterday was Monday, what day is tomorrow?", "wednesday"),
635
  ("A farmer has 17 sheep. All but 9 die. How many are left?", "9"),
636
+ ("If you're running and pass the person in 2nd place, what position are you in?", "2nd"),
637
+ ("How many times can you subtract 5 from 25?", "once"),
638
+ ("All mammals breathe air. Whales are mammals. Do whales breathe air?", "yes"),
639
+ ("How many months have 28 days?", "all"),
640
+ ("If A > B and B > C, who is smallest?", "c"),
641
+ ("A bat and ball cost $1.10. The bat costs $1 more than the ball. How much is the ball?", "5"),
642
+ ("5 machines take 5 min to make 5 widgets. How long for 100 machines to make 100 widgets?", "5"),
643
  ]
644
+ rc = 0
645
  for q, expected in reason_tests:
646
+ a = ask(q, use_cot=True)
647
+ correct = smart_verify({"answer": expected}, a) > 0
648
+ rc += correct
649
+ status = "OK" if correct else "FAIL"
650
+ answer_display = a.split("</think>")[-1].strip() if "</think>" in a else a
651
+ print(f" {status}: {q[:55]}... -> {answer_display[:80]}")
652
+ results["reasoning"] = f"{rc}/10"
653
+ print(f" Reasoning: {rc}/10")
654
+
655
+ # Logic
656
+ logic_tests = [
657
+ ("If it rains, the ground is wet. The ground is wet. Did it rain?", "no"),
658
+ ("None of the winners cheated. Tom didn't cheat. Did Tom win?", "no"),
659
+ ("If A then B. If B then C. A is true. Is C true?", "yes"),
660
  ]
661
+ lc = 0
662
+ for q, expected in logic_tests:
663
+ a = ask(q, use_cot=True)
664
+ correct = smart_verify({"answer": expected}, a) > 0
665
+ lc += correct
666
+ status = "OK" if correct else "FAIL"
667
+ answer_display = a.split("</think>")[-1].strip() if "</think>" in a else a
668
+ print(f" {status}: {q[:55]}... -> {answer_display[:80]}")
669
+ results["logic"] = f"{lc}/3"
670
+ print(f" Logic: {lc}/3")
671
 
672
  # Perplexity
673
  test_text = "The quick brown fox jumps over the lazy dog. Machine learning models can process natural language."
674
  enc = tokenizer(test_text, return_tensors="pt").to(model.device)
675
  with torch.no_grad():
676
  loss = model(**enc, labels=enc.input_ids).loss.item()
677
+ results["perplexity"] = f"{math.exp(loss):.2f}"
678
+ print(f" Perplexity: {results['perplexity']}")
 
679
 
680
  return results
681
 
682
 
683
  # ============================================================
684
+ # MAIN
685
  # ============================================================
686
 
687
  def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
688
+ """Run one complete self-improvement cycle."""
 
 
 
 
689
  if cfg is None:
690
  cfg = SelfImproveConfig()
691
 
 
696
  print(f"Started: {time.strftime('%H:%M:%S')}")
697
  print("=" * 60)
698
 
699
+ # Check for resume
700
+ progress_path = Path(cfg.progress_file)
701
+ resume_from = 0
702
+ if progress_path.exists():
703
+ with open(progress_path) as f:
704
+ saved = json.load(f)
705
+ resume_from = saved.get("problems_completed", 0)
706
+
707
  # Load model
708
  from transformers import AutoModelForImageTextToText, AutoTokenizer
 
709
  print("\nLoading model...")
710
  model = AutoModelForImageTextToText.from_pretrained(
711
  cfg.model_path, dtype=torch.bfloat16,
 
714
  tokenizer = AutoTokenizer.from_pretrained(cfg.model_path, trust_remote_code=True)
715
  eos_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
716
 
717
+ # Pre-benchmark
718
+ print("\n--- PRE-TRAINING BENCHMARK ---")
719
+ pre_results = benchmark(model, tokenizer, eos_id)
 
 
 
 
 
 
 
 
 
720
 
721
+ # Step 1: Diagnose
722
+ diagnosis_path = Path(cfg.output_dir) / "diagnosis.json"
723
+ diagnosis_path.parent.mkdir(parents=True, exist_ok=True)
724
+ weaknesses = diagnose_weaknesses(model, tokenizer, eos_id, save_path=str(diagnosis_path))
725
+
726
+ # Step 2: Generate hard problems
727
+ print("\n=== STEP 2: GENERATING HARD PROBLEMS ===")
728
+ random.seed(42 + cycle_num) # Different problems each cycle
729
+ problems = generate_hard_problems(cfg)
730
+
731
+ # Step 3: Generate & score
732
+ if resume_from < len(problems):
733
+ winning_pairs, losing_pairs = generate_and_score(
734
+ model, tokenizer, problems, cfg, eos_id, resume_from=resume_from
735
+ )
736
+ else:
737
+ with open(progress_path) as f:
738
+ saved = json.load(f)
739
+ winning_pairs = saved["winning_pairs"]
740
+ losing_pairs = saved.get("losing_pairs", [])
741
 
742
+ # Step 4: Train
743
  model = train_on_winners(model, tokenizer, winning_pairs, cfg)
744
 
745
  # Save improved model
746
+ save_dir = Path(cfg.output_dir) / f"cycle_{cycle_num}"
747
+ save_dir.mkdir(parents=True, exist_ok=True)
748
+ print(f"\nSaving improved model to {save_dir}...")
749
+ model.save_pretrained(str(save_dir), safe_serialization=True)
750
+ tokenizer.save_pretrained(str(save_dir))
751
+ sz = sum(f.stat().st_size for f in save_dir.glob("*.safetensors")) / 1e9
752
+ print(f"SAVED: {save_dir} ({sz:.1f} GB)")
753
+
754
+ # Step 5: Post-benchmark
755
+ print("\n--- POST-TRAINING BENCHMARK ---")
756
+ post_results = benchmark(model, tokenizer, eos_id)
757
+
758
+ # Compare & save report
 
 
 
 
 
 
759
  elapsed = (time.time() - start) / 60
760
  print(f"\n{'=' * 60}")
761
  print(f"CYCLE {cycle_num} COMPLETE — {elapsed:.1f} min")
762
+ print(f"PRE: {pre_results}")
763
+ print(f"POST: {post_results}")
764
+ print(f"Model: {save_dir}")
765
  print(f"{'=' * 60}")
766
 
767
+ report = {
768
+ "cycle": cycle_num,
769
+ "pre_benchmark": pre_results,
770
+ "post_benchmark": post_results,
771
+ "problems_total": len(problems),
772
+ "winning_pairs": len(winning_pairs),
773
+ "losing_pairs": len(losing_pairs),
774
+ "duration_min": elapsed,
775
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
776
+ }
777
+ with open(save_dir / "cycle_report.json", "w") as f:
778
+ json.dump(report, f, indent=2)
779
+
780
+ return str(save_dir)
781
+
782
+
783
+ if __name__ == "__main__":
784
+ run_cycle()