td-builder commited on
Commit
bfc6b74
·
verified ·
1 Parent(s): 6af73e9

Upload 148 files

Browse files
hugging/td_fuse/grpo_reason.py CHANGED
@@ -204,6 +204,8 @@ def generate_all_problems(cfg: GRPOConfig) -> List[Dict]:
204
  elif ptype == "comparison":
205
  a_val = random.randint(10, 100)
206
  b_val = random.randint(10, 100)
 
 
207
  diff = abs(a_val - b_val)
208
  who = "Alice" if a_val > b_val else "Bob"
209
  problems.append({
@@ -280,11 +282,19 @@ def verify_answer(problem: Dict, response: str) -> float:
280
 
281
  # For "all" (months with 28 days trick):
282
  if expected == "all":
283
- return 1.0 if ("all" in answer_text and "12" in answer_text) or "every month" in answer_text or "all 12" in answer_text else 0.0
 
 
 
 
 
 
284
 
285
  # For "neither" (rooster trick):
286
  if expected == "neither":
287
- return 1.0 if ("neither" in answer_text or "don't lay" in answer_text or "rooster" in answer_text and "can't" in answer_text) else 0.0
 
 
288
 
289
  # For "white" (polar bear trick):
290
  if expected == "white":
@@ -321,12 +331,9 @@ def verify_answer(problem: Dict, response: str) -> float:
321
  return 0.0
322
 
323
  # For single-letter answers (a, b, c, etc.) — ordering questions
 
324
  if len(expected) == 1 and expected.isalpha():
325
- # Check for the name in the conclusion
326
- names_map = {"c": "c", "w": "w", "tyke": "tyke", "sam": "sam", "dave": "dave",
327
- "carol": "carol", "yellow": "yellow", "library": "library"}
328
- # Just check if expected appears in last 150 chars
329
- if expected in answer_text[-150:]:
330
  return 1.0
331
  return 0.0
332
 
@@ -382,6 +389,7 @@ def generate_and_score_cot(model, tokenizer, problems, cfg, eos_id, resume_from=
382
  Saves progress every batch_save_every problems for crash recovery.
383
  """
384
  print(f"\n=== GENERATE & SCORE CoT ({len(problems)} problems, {cfg.num_candidates} candidates each) ===")
 
385
 
386
  progress_path = Path(cfg.progress_file)
387
  progress_path.parent.mkdir(parents=True, exist_ok=True)
@@ -446,7 +454,10 @@ def generate_and_score_cot(model, tokenizer, problems, cfg, eos_id, resume_from=
446
  # Progress update & save
447
  if (i + 1) % cfg.batch_save_every == 0:
448
  pct = total_correct / total_attempted * 100
449
- print(f" [{i+1}/{len(problems)}] Correct: {total_correct}/{total_attempted} ({pct:.0f}%) | Winners: {len(winning_pairs)}")
 
 
 
450
 
451
  # Save progress to disk
452
  with open(progress_path, "w") as f:
@@ -458,6 +469,10 @@ def generate_and_score_cot(model, tokenizer, problems, cfg, eos_id, resume_from=
458
  "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
459
  }, f)
460
 
 
 
 
 
461
  # Final save
462
  pct = total_correct / total_attempted * 100 if total_attempted > 0 else 0
463
  print(f"\n FINAL: {total_correct}/{total_attempted} ({pct:.0f}%) | Winners: {len(winning_pairs)} | Losers: {len(losing_pairs)}")
@@ -553,14 +568,15 @@ def train_grpo(model, tokenizer, winning_pairs, losing_pairs, cfg):
553
  print(f" Starting GRPO training...")
554
  trainer.train()
555
 
556
- # Clean checkpoints, merge LoRA
 
 
 
 
557
  import shutil
558
  shutil.rmtree(str(out_dir), ignore_errors=True)
559
  gc.collect()
560
 
561
- print(" Merging LoRA...")
562
- merged = model.merge_and_unload()
563
- gc.collect()
564
  return merged
565
 
566
 
@@ -710,7 +726,8 @@ def run_grpo_cycle(cfg: GRPOConfig = None):
710
  print(f"\nSaving improved model to {save_dir}...")
711
  model.save_pretrained(str(save_dir), safe_serialization=True)
712
  tokenizer.save_pretrained(str(save_dir))
713
- sz = (save_dir / "model.safetensors").stat().st_size / 1e9
 
714
  print(f"SAVED: {save_dir} ({sz:.1f} GB)")
715
 
716
  # Post-benchmark
@@ -741,3 +758,7 @@ def run_grpo_cycle(cfg: GRPOConfig = None):
741
  json.dump(report, f, indent=2)
742
 
743
  return str(save_dir)
 
 
 
 
 
204
  elif ptype == "comparison":
205
  a_val = random.randint(10, 100)
206
  b_val = random.randint(10, 100)
207
+ while a_val == b_val: # Avoid "who has more?" with equal values
208
+ b_val = random.randint(10, 100)
209
  diff = abs(a_val - b_val)
210
  who = "Alice" if a_val > b_val else "Bob"
211
  problems.append({
 
282
 
283
  # For "all" (months with 28 days trick):
284
  if expected == "all":
285
+ all_indicators = ["all 12", "every month", "all months", "all of them"]
286
+ if any(w in answer_text for w in all_indicators):
287
+ return 1.0
288
+ # "all" + "12" combo
289
+ if "all" in answer_text and "12" in answer_text:
290
+ return 1.0
291
+ return 0.0
292
 
293
  # For "neither" (rooster trick):
294
  if expected == "neither":
295
+ rooster_words = ["neither", "don't lay", "doesn't lay", "cannot lay", "can't lay",
296
+ "do not lay", "does not lay", "impossible"]
297
+ return 1.0 if any(w in answer_text for w in rooster_words) else 0.0
298
 
299
  # For "white" (polar bear trick):
300
  if expected == "white":
 
331
  return 0.0
332
 
333
  # For single-letter answers (a, b, c, etc.) — ordering questions
334
+ # Must use word boundary to avoid matching inside words like "because"
335
  if len(expected) == 1 and expected.isalpha():
336
+ if re.search(rf'\b{expected}\b', answer_text[-150:]):
 
 
 
 
337
  return 1.0
338
  return 0.0
339
 
 
389
  Saves progress every batch_save_every problems for crash recovery.
390
  """
391
  print(f"\n=== GENERATE & SCORE CoT ({len(problems)} problems, {cfg.num_candidates} candidates each) ===")
392
+ generate_and_score_cot._start = time.time()
393
 
394
  progress_path = Path(cfg.progress_file)
395
  progress_path.parent.mkdir(parents=True, exist_ok=True)
 
454
  # Progress update & save
455
  if (i + 1) % cfg.batch_save_every == 0:
456
  pct = total_correct / total_attempted * 100
457
+ elapsed = time.time()
458
+ rate = (i + 1 - resume_from) / max(1, elapsed - getattr(generate_and_score_cot, '_start', elapsed))
459
+ eta = (len(problems) - i - 1) / max(0.001, rate) / 60 if rate > 0 else 0
460
+ print(f" [{i+1}/{len(problems)}] Correct: {total_correct}/{total_attempted} ({pct:.0f}%) | Winners: {len(winning_pairs)} | ETA: {eta:.0f}min")
461
 
462
  # Save progress to disk
463
  with open(progress_path, "w") as f:
 
469
  "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
470
  }, f)
471
 
472
+ # Periodic VRAM cleanup to prevent OOM during long generation
473
+ if torch.cuda.is_available():
474
+ torch.cuda.empty_cache()
475
+
476
  # Final save
477
  pct = total_correct / total_attempted * 100 if total_attempted > 0 else 0
478
  print(f"\n FINAL: {total_correct}/{total_attempted} ({pct:.0f}%) | Winners: {len(winning_pairs)} | Losers: {len(losing_pairs)}")
 
568
  print(f" Starting GRPO training...")
569
  trainer.train()
570
 
571
+ # Merge LoRA FIRST, then clean checkpoints (safer — don't lose data before merge succeeds)
572
+ print(" Merging LoRA...")
573
+ merged = model.merge_and_unload()
574
+ gc.collect()
575
+
576
  import shutil
577
  shutil.rmtree(str(out_dir), ignore_errors=True)
578
  gc.collect()
579
 
 
 
 
580
  return merged
581
 
582
 
 
726
  print(f"\nSaving improved model to {save_dir}...")
727
  model.save_pretrained(str(save_dir), safe_serialization=True)
728
  tokenizer.save_pretrained(str(save_dir))
729
+ # Calculate total size (handles both single and sharded safetensors)
730
+ sz = sum(f.stat().st_size for f in save_dir.glob("*.safetensors")) / 1e9
731
  print(f"SAVED: {save_dir} ({sz:.1f} GB)")
732
 
733
  # Post-benchmark
 
758
  json.dump(report, f, indent=2)
759
 
760
  return str(save_dir)
761
+
762
+
763
+ if __name__ == "__main__":
764
+ run_grpo_cycle()
hugging/td_fuse/heal.py CHANGED
@@ -378,12 +378,12 @@ def apply_qlora_standard(
378
  print(f"[heal] SAVED OK: {healed_dir}")
379
 
380
  # Verify the save actually worked before cleaning up ANYTHING
381
- saved_model = healed_dir / "model.safetensors"
382
- if not saved_model.exists() or saved_model.stat().st_size < 1_000_000:
 
383
  print(f"[heal] WARNING: Save may have failed — NOT deleting any backups!")
384
  else:
385
- save_size = saved_model.stat().st_size / 1e9
386
- print(f"[heal] Verified: {saved_model} ({save_size:.1f} GB)")
387
  # NOW safe to clean up old stuff
388
  cleanup_targets = [
389
  "td_fuse_outputs/final",
 
378
  print(f"[heal] SAVED OK: {healed_dir}")
379
 
380
  # Verify the save actually worked before cleaning up ANYTHING
381
+ saved_files = list(healed_dir.glob("*.safetensors"))
382
+ total_size = sum(f.stat().st_size for f in saved_files) if saved_files else 0
383
+ if not saved_files or total_size < 1_000_000:
384
  print(f"[heal] WARNING: Save may have failed — NOT deleting any backups!")
385
  else:
386
+ print(f"[heal] Verified: {len(saved_files)} safetensors files ({total_size/1e9:.1f} GB)")
 
387
  # NOW safe to clean up old stuff
388
  cleanup_targets = [
389
  "td_fuse_outputs/final",
hugging/td_fuse/merge_klear.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Merge #6: Klear-Reasoner-8B → existing 5-model merge.
3
+
4
+ Klear-Reasoner-8B is based on Qwen3-8B-Base (SAME architecture as our
5
+ Qwen3-VL-8B target). This means:
6
+ - Direct 1:1 layer matching (36 → 36)
7
+ - Neurons are aligned (identity Q matrices)
8
+ - No permutation needed
9
+ - Transport plans take under 1 minute
10
+ - Total merge: ~10-15 minutes
11
+
12
+ Think of this like adding more reasoning spice to an already-cooked dish.
13
+ The base model already has reasoning from DeepSeek, MiMo, and Falcon.
14
+ Klear-Reasoner adds EVEN MORE focused reasoning (90.5% AIME 2024).
15
+
16
+ Alpha = 0.3 (conservative — we don't want to overwrite what's already
17
+ merged). The existing model is the dominant one; Klear adds a boost.
18
+
19
+ Usage:
20
+ python -m td_fuse.merge_klear
21
+
22
+ Expects:
23
+ - Existing merge at: td_fuse_outputs/healed_final/ (or after_falcon_5model/)
24
+ - Will download Klear-Reasoner-8B from HuggingFace
25
+ - Output: td_fuse_outputs/6model_klear/
26
+ """
27
+
28
+ import os
29
+ import sys
30
+ import gc
31
+ import time
32
+ import torch
33
+ import numpy as np
34
+ from pathlib import Path
35
+
36
+ # ============================================================================
37
+ # CONFIGURATION
38
+ # ============================================================================
39
+
40
+ # Where is our existing 5-model merge?
41
+ EXISTING_MERGE_PATHS = [
42
+ "td_fuse_outputs/healed_final",
43
+ "td_fuse_checkpoints/after_falcon", # backup location
44
+ "td_fuse_checkpoints/after_falcon_5model", # HF backup
45
+ ]
46
+
47
+ # Klear-Reasoner-8B config
48
+ KLEAR_HF_ID = "Kwai-Klear/Klear-Reasoner-8B"
49
+ KLEAR_ALPHA = 0.3 # Conservative: keep 70% existing, add 30% Klear reasoning
50
+ KLEAR_NAME = "Klear-Reasoner-8B"
51
+
52
+ # Output
53
+ OUTPUT_DIR = "td_fuse_outputs/6model_klear"
54
+
55
+ # Vision encoder prefixes — NEVER merge into these
56
+ VISION_SKIP = ["visual", "merger", "model.visual", "model.merger"]
57
+
58
+ # Thinking tokens to protect
59
+ THINK_TOKEN_IDS = [151667, 151668]
60
+
61
+
62
+ def find_existing_merge():
63
+ """Find the existing 5-model merge checkpoint."""
64
+ for path in EXISTING_MERGE_PATHS:
65
+ safetensors = list(Path(path).glob("*.safetensors")) if Path(path).exists() else []
66
+ if safetensors:
67
+ total_size = sum(f.stat().st_size for f in safetensors)
68
+ if total_size > 1_000_000_000: # > 1GB
69
+ print(f"[merge6] Found existing merge at: {path} ({total_size/1e9:.1f} GB)")
70
+ return path
71
+ return None
72
+
73
+
74
+ def load_model(path, dtype=torch.bfloat16):
75
+ """Load model — auto-detects Qwen3-VL."""
76
+ from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
77
+
78
+ print(f"[merge6] Loading model from: {path}")
79
+ t = time.time()
80
+
81
+ try:
82
+ config = AutoConfig.from_pretrained(path, trust_remote_code=True)
83
+ model_type = getattr(config, 'model_type', '')
84
+ config_class = type(config).__name__.lower()
85
+ if 'qwen3_vl' in model_type or 'qwen3vl' in config_class:
86
+ from transformers import Qwen3VLForConditionalGeneration
87
+ print(f"[merge6] Loading as Qwen3-VL model")
88
+ model = Qwen3VLForConditionalGeneration.from_pretrained(
89
+ path, device_map="auto", torch_dtype=dtype
90
+ )
91
+ else:
92
+ model = AutoModelForCausalLM.from_pretrained(
93
+ path, device_map="auto", torch_dtype=dtype
94
+ )
95
+ except Exception as e:
96
+ print(f"[merge6] Auto-detect failed ({e}), using AutoModelForCausalLM")
97
+ model = AutoModelForCausalLM.from_pretrained(
98
+ path, device_map="auto", torch_dtype=dtype
99
+ )
100
+
101
+ tokenizer = AutoTokenizer.from_pretrained(path)
102
+ print(f"[merge6] Loaded in {time.time()-t:.0f}s")
103
+ return model, tokenizer
104
+
105
+
106
+ def should_skip(key):
107
+ """Check if a parameter should be skipped during merge."""
108
+ # Skip vision encoder
109
+ for prefix in VISION_SKIP:
110
+ if key.startswith(prefix):
111
+ return True
112
+ return False
113
+
114
+
115
+ def direct_merge(target_model, source_state, alpha=0.3):
116
+ """
117
+ Direct same-architecture merge: blend source weights into target.
118
+
119
+ Since Klear-Reasoner-8B has the same Qwen3-8B architecture:
120
+ - All parameter names match exactly
121
+ - No layer mapping needed
122
+ - No neuron permutation needed
123
+ - Just simple alpha blending: W = alpha * source + (1-alpha) * target
124
+
125
+ We skip:
126
+ - Vision encoder weights (visual.*, merger.*)
127
+ - Embedding/lm_head IF vocab sizes don't match
128
+ """
129
+ target_state = target_model.state_dict()
130
+
131
+ # Check vocab sizes
132
+ target_vocab = None
133
+ source_vocab = None
134
+ for key in ["model.embed_tokens.weight", "embed_tokens.weight"]:
135
+ if key in target_state:
136
+ target_vocab = target_state[key].shape[0]
137
+ if key in source_state:
138
+ source_vocab = source_state[key].shape[0]
139
+ skip_embeddings = (target_vocab != source_vocab) if (target_vocab and source_vocab) else False
140
+ if skip_embeddings:
141
+ print(f"[merge6] Vocab mismatch ({target_vocab} vs {source_vocab}) — skipping embeddings")
142
+ else:
143
+ print(f"[merge6] Vocab sizes match — merging embeddings too")
144
+
145
+ fused = 0
146
+ skipped = 0
147
+ total = len(target_state)
148
+
149
+ # Save original think token embeddings for restoration
150
+ think_embeds = {}
151
+ for key in target_state:
152
+ if "embed_tokens" in key:
153
+ for tid in THINK_TOKEN_IDS:
154
+ if tid < target_state[key].shape[0]:
155
+ think_embeds[(key, tid)] = target_state[key][tid].clone()
156
+
157
+ for i, target_key in enumerate(target_state):
158
+ if should_skip(target_key):
159
+ skipped += 1
160
+ continue
161
+
162
+ if skip_embeddings and ("embed_tokens" in target_key or "lm_head" in target_key):
163
+ skipped += 1
164
+ continue
165
+
166
+ # Find source key — same name for same architecture
167
+ if target_key not in source_state:
168
+ # Try without "model." prefix or with it
169
+ alt_key = target_key.replace("model.", "", 1) if target_key.startswith("model.") else f"model.{target_key}"
170
+ if alt_key not in source_state:
171
+ skipped += 1
172
+ continue
173
+ source_key = alt_key
174
+ else:
175
+ source_key = target_key
176
+
177
+ target_w = target_state[target_key]
178
+ source_w = source_state[source_key]
179
+
180
+ # Shape check
181
+ if target_w.shape != source_w.shape:
182
+ skipped += 1
183
+ if skipped <= 5:
184
+ print(f" [skip] Shape mismatch: {target_key} ({target_w.shape} vs {source_w.shape})")
185
+ continue
186
+
187
+ # Blend: W_final = alpha * source + (1-alpha) * target
188
+ fused_w = alpha * source_w.to(target_w.device, dtype=target_w.dtype) + (1 - alpha) * target_w
189
+ target_state[target_key] = fused_w
190
+ fused += 1
191
+
192
+ if fused % 50 == 0:
193
+ print(f" Fused {fused}/{total} params...")
194
+ sys.stdout.flush()
195
+
196
+ # Restore think token embeddings
197
+ for (key, tid), embed in think_embeds.items():
198
+ if key in target_state and tid < target_state[key].shape[0]:
199
+ target_state[key][tid] = embed
200
+ print(f"[merge6] Protected think token {tid}")
201
+
202
+ # Load fused state
203
+ missing, unexpected = target_model.load_state_dict(target_state, strict=False)
204
+ if missing:
205
+ print(f"[merge6] {len(missing)} missing keys (likely vision quant params — safe)")
206
+ print(f"[merge6] Fused {fused} params, skipped {skipped}")
207
+ return target_model
208
+
209
+
210
+ def run_merge():
211
+ """Main merge pipeline for Klear-Reasoner-8B."""
212
+ merge_start = time.time()
213
+
214
+ print("\n" + "=" * 60)
215
+ print("MERGE #6: Klear-Reasoner-8B → 5-model merge")
216
+ print(f"Alpha: {KLEAR_ALPHA} (30% Klear, 70% existing)")
217
+ print(f"Started at: {time.strftime('%H:%M:%S')}")
218
+ print("=" * 60)
219
+ sys.stdout.flush()
220
+
221
+ # --- Step 1: Find existing merge ---
222
+ existing_path = find_existing_merge()
223
+ if not existing_path:
224
+ print("[merge6] ERROR: Cannot find existing 5-model merge!")
225
+ print(f"[merge6] Looked in: {EXISTING_MERGE_PATHS}")
226
+ sys.exit(1)
227
+
228
+ # --- Step 2: Load existing merged model ---
229
+ print("\n[merge6] Step 1/4: Loading existing 5-model merge...")
230
+ sys.stdout.flush()
231
+ target_model, target_tokenizer = load_model(existing_path)
232
+
233
+ # --- Step 3: Download & load Klear-Reasoner-8B ---
234
+ print("\n[merge6] Step 2/4: Loading Klear-Reasoner-8B (will download if needed)...")
235
+ sys.stdout.flush()
236
+ t = time.time()
237
+
238
+ from transformers import AutoModelForCausalLM as AMLM
239
+ source_model = AMLM.from_pretrained(
240
+ KLEAR_HF_ID,
241
+ device_map="cpu", # Keep on CPU to save GPU VRAM
242
+ torch_dtype=torch.bfloat16,
243
+ )
244
+ source_state = source_model.state_dict()
245
+ print(f"[merge6] Klear loaded in {time.time()-t:.0f}s ({len(source_state)} params)")
246
+ sys.stdout.flush()
247
+
248
+ # Free source model object (keep state dict)
249
+ del source_model
250
+ gc.collect()
251
+
252
+ # --- Step 4: Direct merge ---
253
+ print("\n[merge6] Step 3/4: Merging weights (direct same-arch blend)...")
254
+ sys.stdout.flush()
255
+ t = time.time()
256
+ target_model = direct_merge(target_model, source_state, alpha=KLEAR_ALPHA)
257
+ del source_state
258
+ gc.collect()
259
+ torch.cuda.empty_cache()
260
+ print(f"[merge6] Merge done in {time.time()-t:.0f}s")
261
+
262
+ # --- Step 5: Save ---
263
+ print(f"\n[merge6] Step 4/4: Saving to {OUTPUT_DIR}...")
264
+ sys.stdout.flush()
265
+ t = time.time()
266
+ output_path = Path(OUTPUT_DIR)
267
+ output_path.mkdir(parents=True, exist_ok=True)
268
+
269
+ target_model.save_pretrained(str(output_path), safe_serialization=True)
270
+ target_tokenizer.save_pretrained(str(output_path))
271
+
272
+ # Verify save
273
+ saved_files = list(output_path.glob("*.safetensors"))
274
+ total_size = sum(f.stat().st_size for f in saved_files) if saved_files else 0
275
+ if not saved_files or total_size < 1_000_000_000:
276
+ print(f"[merge6] WARNING: Save may have failed! Only {total_size/1e9:.1f} GB")
277
+ else:
278
+ print(f"[merge6] Saved: {len(saved_files)} files, {total_size/1e9:.1f} GB")
279
+ print(f"[merge6] Save done in {time.time()-t:.0f}s")
280
+
281
+ total_min = (time.time() - merge_start) / 60
282
+ print(f"\n{'=' * 60}")
283
+ print(f"MERGE #6 COMPLETE: {total_min:.1f} min total")
284
+ print(f"Output: {OUTPUT_DIR}")
285
+ print(f"{'=' * 60}")
286
+ sys.stdout.flush()
287
+
288
+ return str(output_path)
289
+
290
+
291
+ if __name__ == "__main__":
292
+ run_merge()
hugging/td_fuse/reasoning_heal.py ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 3-Phase Reasoning Heal — overnight restoration after merge.
3
+
4
+ This isn't a normal heal. Normal heal just fixes language coherence.
5
+ This one specifically targets the reasoning circuits in the model:
6
+
7
+ Phase 1: Language Coherence (standard heal)
8
+ - Raw text data, fixes general language ability
9
+ - 2000 samples, 2 epochs
10
+ - ~30 min on A6000
11
+
12
+ Phase 2: Stop-Token Fix (chat format heal)
13
+ - Chat-formatted data with <|im_start|>/<|im_end|> tokens
14
+ - Teaches model when to stop generating (prevents loop)
15
+ - 800 samples, 3 epochs
16
+ - ~20 min on A6000
17
+
18
+ Phase 3: Reasoning Restoration (THE BIG ONE)
19
+ - Chain-of-thought reasoning data (GSM8K, MATH, ARC-Challenge)
20
+ - Higher LoRA rank (r=64, alpha=128) to really move the weights
21
+ - Targets layers 16-28 specifically (where reasoning lives)
22
+ - 3000+ samples, 4 epochs
23
+ - ~60-90 min on A6000
24
+
25
+ Total estimated time: ~2-3 hours.
26
+
27
+ Usage:
28
+ nohup python3 -m td_fuse.reasoning_heal > reasoning_heal.log 2>&1 &
29
+
30
+ Config notes:
31
+ - bf16 throughout (not 4-bit — avoids dequantize corruption)
32
+ - Learning rate schedule: Phase 1 (5e-5) → Phase 2 (3e-5) → Phase 3 (2e-5)
33
+ - Phase 3 uses LOWER lr + HIGHER rank — big gentle changes, not small sharp ones
34
+
35
+ Findings: #12, #16, #20
36
+ """
37
+
38
+ import os
39
+ import sys
40
+ import gc
41
+ import time
42
+ import torch
43
+ from pathlib import Path
44
+ from datasets import load_dataset
45
+ from transformers import (
46
+ AutoModelForCausalLM, AutoTokenizer, AutoConfig,
47
+ TrainingArguments, Trainer,
48
+ )
49
+ from peft import LoraConfig, get_peft_model, TaskType
50
+
51
+ # ============================================================================
52
+ # CONFIGURATION
53
+ # ============================================================================
54
+
55
+ # Input: the 6-model merge (output of merge_klear.py)
56
+ INPUT_PATHS = [
57
+ "td_fuse_outputs/6model_klear",
58
+ "td_fuse_outputs/healed_final", # fallback: 5-model merge
59
+ ]
60
+
61
+ # Output: final healed model
62
+ OUTPUT_DIR = "td_fuse_outputs/reasoning_healed"
63
+
64
+ # Phase configs
65
+ PHASES = {
66
+ "phase1_language": {
67
+ "name": "Phase 1: Language Coherence",
68
+ "lora_r": 32,
69
+ "lora_alpha": 64,
70
+ "learning_rate": 5e-5,
71
+ "epochs": 2,
72
+ "batch_size": 1,
73
+ "grad_accum": 8,
74
+ "seq_len": 2048,
75
+ "target_modules": [
76
+ "q_proj", "k_proj", "v_proj", "o_proj",
77
+ "gate_proj", "up_proj", "down_proj",
78
+ ],
79
+ "datasets": [
80
+ # General language calibration
81
+ ("neuralmagic/LLM_compression_calibration", None, "train", 1200, "text", None),
82
+ # Math reasoning text
83
+ ("openai/gsm8k", "main", "train", 500, "question", None),
84
+ # Code
85
+ ("sahil2801/CodeAlpaca-20k", None, "train", 300, "output", None),
86
+ ],
87
+ },
88
+ "phase2_stop_tokens": {
89
+ "name": "Phase 2: Stop-Token Fix",
90
+ "lora_r": 16,
91
+ "lora_alpha": 32,
92
+ "learning_rate": 3e-5,
93
+ "epochs": 3,
94
+ "batch_size": 1,
95
+ "grad_accum": 4,
96
+ "seq_len": 1024,
97
+ "target_modules": [
98
+ "q_proj", "k_proj", "v_proj", "o_proj",
99
+ "gate_proj", "up_proj", "down_proj",
100
+ ],
101
+ "datasets": [
102
+ # Chat-formatted data with proper turn structure
103
+ ("HuggingFaceH4/ultrachat_200k", None, "train_sft", 800, None, "chat"),
104
+ ],
105
+ },
106
+ "phase3_reasoning": {
107
+ "name": "Phase 3: Reasoning Restoration",
108
+ "lora_r": 64, # HIGH rank — really move the reasoning weights
109
+ "lora_alpha": 128, # 2x rank
110
+ "learning_rate": 2e-5, # LOWER lr — big gentle changes
111
+ "epochs": 4, # More epochs — let it really learn
112
+ "batch_size": 1,
113
+ "grad_accum": 8,
114
+ "seq_len": 2048,
115
+ # Target ONLY the reasoning layers (16-28 out of 0-35)
116
+ # This is where chain-of-thought reasoning lives
117
+ "target_modules": [
118
+ "q_proj", "k_proj", "v_proj", "o_proj",
119
+ "gate_proj", "up_proj", "down_proj",
120
+ ],
121
+ "layers_to_transform": list(range(16, 29)), # Layers 16-28 only
122
+ "datasets": [
123
+ # GSM8K with full solutions (chain-of-thought)
124
+ ("openai/gsm8k", "main", "train", 1500, "answer", None),
125
+ # MATH dataset — harder problems
126
+ ("lighteval/MATH", "all", "train", 800, "solution", None),
127
+ # ARC-Challenge — science reasoning
128
+ ("allenai/ai2_arc", "ARC-Challenge", "train", 400, "question", None),
129
+ # More GSM8K questions (exercises reasoning path generation)
130
+ ("openai/gsm8k", "main", "train", 300, "question", None),
131
+ ],
132
+ },
133
+ }
134
+
135
+
136
+ # ============================================================================
137
+ # DATA LOADING
138
+ # ============================================================================
139
+
140
+ def load_phase_data(phase_config, tokenizer):
141
+ """Load training data for a phase."""
142
+ all_texts = []
143
+ chat_template = None
144
+
145
+ for entry in phase_config["datasets"]:
146
+ dataset_id, config_name, split, count, text_field, mode = entry
147
+
148
+ try:
149
+ if config_name:
150
+ ds = load_dataset(dataset_id, config_name, split=split, streaming=True)
151
+ else:
152
+ ds = load_dataset(dataset_id, split=split, streaming=True)
153
+
154
+ loaded = 0
155
+ for example in ds:
156
+ if loaded >= count:
157
+ break
158
+
159
+ if mode == "chat":
160
+ # Chat-formatted data — use the messages field
161
+ messages = example.get("messages", [])
162
+ if messages and len(messages) >= 2:
163
+ # Format as chat with proper tokens
164
+ text = format_chat(messages, tokenizer)
165
+ if text and len(text) > 50:
166
+ all_texts.append(text)
167
+ loaded += 1
168
+ else:
169
+ # Raw text mode
170
+ text = example.get(text_field, "")
171
+ if len(str(text)) > 50:
172
+ all_texts.append(str(text))
173
+ loaded += 1
174
+
175
+ print(f" {dataset_id}: {loaded} samples")
176
+ except Exception as e:
177
+ print(f" WARNING: {dataset_id} failed: {e}")
178
+
179
+ print(f" Total: {len(all_texts)} samples")
180
+ return all_texts
181
+
182
+
183
+ def format_chat(messages, tokenizer):
184
+ """Format chat messages with proper tokens for Qwen3-style models."""
185
+ try:
186
+ # Try using the tokenizer's built-in chat template
187
+ text = tokenizer.apply_chat_template(
188
+ messages,
189
+ tokenize=False,
190
+ add_generation_prompt=False,
191
+ )
192
+ return text
193
+ except Exception:
194
+ pass
195
+
196
+ # Fallback: manual ChatML format
197
+ parts = []
198
+ for msg in messages:
199
+ role = msg.get("role", "user")
200
+ content = msg.get("content", "")
201
+ parts.append(f"<|im_start|>{role}\n{content}<|im_end|>")
202
+ return "\n".join(parts)
203
+
204
+
205
+ # ============================================================================
206
+ # MODEL LOADING
207
+ # ============================================================================
208
+
209
+ def find_input_model():
210
+ """Find the model to heal."""
211
+ for path in INPUT_PATHS:
212
+ safetensors = list(Path(path).glob("*.safetensors")) if Path(path).exists() else []
213
+ if safetensors:
214
+ total_size = sum(f.stat().st_size for f in safetensors)
215
+ if total_size > 1_000_000_000:
216
+ print(f"[heal] Found model at: {path} ({total_size/1e9:.1f} GB)")
217
+ return path
218
+ return None
219
+
220
+
221
+ def load_model_smart(path):
222
+ """Load model — auto-detects Qwen3-VL."""
223
+ print(f"[heal] Loading model from {path}...")
224
+ t = time.time()
225
+
226
+ try:
227
+ config = AutoConfig.from_pretrained(path, trust_remote_code=True)
228
+ model_type = getattr(config, 'model_type', '')
229
+ config_class = type(config).__name__.lower()
230
+ if 'qwen3_vl' in model_type or 'qwen3vl' in config_class:
231
+ from transformers import Qwen3VLForConditionalGeneration
232
+ print(f"[heal] Loading as Qwen3-VL model")
233
+ model = Qwen3VLForConditionalGeneration.from_pretrained(
234
+ path, device_map="auto", torch_dtype=torch.bfloat16
235
+ )
236
+ else:
237
+ model = AutoModelForCausalLM.from_pretrained(
238
+ path, device_map="auto", torch_dtype=torch.bfloat16
239
+ )
240
+ except Exception as e:
241
+ print(f"[heal] Auto-detect failed ({e}), using AutoModelForCausalLM")
242
+ model = AutoModelForCausalLM.from_pretrained(
243
+ path, device_map="auto", torch_dtype=torch.bfloat16
244
+ )
245
+
246
+ tokenizer = AutoTokenizer.from_pretrained(path)
247
+ print(f"[heal] Loaded in {time.time()-t:.0f}s")
248
+ return model, tokenizer
249
+
250
+
251
+ # ============================================================================
252
+ # TRAINING
253
+ # ============================================================================
254
+
255
+ class TextDataset(torch.utils.data.Dataset):
256
+ """Simple tokenized text dataset."""
257
+ def __init__(self, texts, tokenizer, max_len):
258
+ self.encodings = []
259
+ for text in texts:
260
+ enc = tokenizer(
261
+ text,
262
+ truncation=True,
263
+ max_length=max_len,
264
+ padding="max_length",
265
+ return_tensors="pt",
266
+ )
267
+ self.encodings.append({
268
+ "input_ids": enc["input_ids"].squeeze(),
269
+ "attention_mask": enc["attention_mask"].squeeze(),
270
+ "labels": enc["input_ids"].squeeze(),
271
+ })
272
+
273
+ def __len__(self):
274
+ return len(self.encodings)
275
+
276
+ def __getitem__(self, idx):
277
+ return self.encodings[idx]
278
+
279
+
280
+ def run_phase(model, tokenizer, phase_name, phase_config, output_subdir):
281
+ """Run a single healing phase."""
282
+ phase_start = time.time()
283
+ print(f"\n{'=' * 60}")
284
+ print(f"{phase_config['name']}")
285
+ print(f"LoRA r={phase_config['lora_r']}, alpha={phase_config['lora_alpha']}")
286
+ print(f"Epochs: {phase_config['epochs']}, LR: {phase_config['learning_rate']}")
287
+ if 'layers_to_transform' in phase_config:
288
+ layers = phase_config['layers_to_transform']
289
+ print(f"Target layers: {layers[0]}-{layers[-1]} (reasoning circuit)")
290
+ print(f"Started at: {time.strftime('%H:%M:%S')}")
291
+ print(f"{'=' * 60}")
292
+ sys.stdout.flush()
293
+
294
+ # Load data
295
+ print(f"\n[{phase_name}] Loading data...")
296
+ texts = load_phase_data(phase_config, tokenizer)
297
+ if not texts:
298
+ print(f"[{phase_name}] WARNING: No data loaded! Skipping phase.")
299
+ return model
300
+
301
+ # Create dataset
302
+ dataset = TextDataset(texts, tokenizer, phase_config["seq_len"])
303
+ del texts
304
+ gc.collect()
305
+
306
+ # LoRA config
307
+ lora_kwargs = {
308
+ "r": phase_config["lora_r"],
309
+ "lora_alpha": phase_config["lora_alpha"],
310
+ "lora_dropout": 0.0,
311
+ "target_modules": phase_config["target_modules"],
312
+ "bias": "none",
313
+ "task_type": TaskType.CAUSAL_LM,
314
+ }
315
+ if "layers_to_transform" in phase_config:
316
+ lora_kwargs["layers_to_transform"] = phase_config["layers_to_transform"]
317
+
318
+ lora_config = LoraConfig(**lora_kwargs)
319
+
320
+ # Apply LoRA
321
+ peft_model = get_peft_model(model, lora_config)
322
+ peft_model.print_trainable_parameters()
323
+
324
+ # Training args
325
+ train_output = Path(output_subdir) / f"{phase_name}_checkpoints"
326
+ train_output.mkdir(parents=True, exist_ok=True)
327
+
328
+ training_args = TrainingArguments(
329
+ output_dir=str(train_output),
330
+ num_train_epochs=phase_config["epochs"],
331
+ per_device_train_batch_size=phase_config["batch_size"],
332
+ gradient_accumulation_steps=phase_config["grad_accum"],
333
+ learning_rate=phase_config["learning_rate"],
334
+ bf16=True,
335
+ logging_steps=10,
336
+ save_strategy="steps",
337
+ save_steps=100,
338
+ save_total_limit=1,
339
+ warmup_ratio=0.05,
340
+ lr_scheduler_type="cosine",
341
+ optim="adamw_torch",
342
+ report_to="none",
343
+ )
344
+
345
+ trainer = Trainer(
346
+ model=peft_model,
347
+ processing_class=tokenizer,
348
+ train_dataset=dataset,
349
+ args=training_args,
350
+ )
351
+
352
+ print(f"\n[{phase_name}] Training...")
353
+ sys.stdout.flush()
354
+ trainer.train()
355
+
356
+ # Merge LoRA back into base
357
+ print(f"\n[{phase_name}] Merging LoRA adapters back into model...")
358
+ merged = peft_model.merge_and_unload()
359
+
360
+ # Clean up checkpoints to free disk
361
+ import shutil
362
+ if train_output.exists():
363
+ shutil.rmtree(str(train_output), ignore_errors=True)
364
+ print(f"[{phase_name}] Cleaned up training checkpoints")
365
+
366
+ gc.collect()
367
+ torch.cuda.empty_cache()
368
+
369
+ phase_min = (time.time() - phase_start) / 60
370
+ print(f"[{phase_name}] Complete in {phase_min:.1f} min")
371
+ sys.stdout.flush()
372
+
373
+ return merged
374
+
375
+
376
+ # ============================================================================
377
+ # MAIN
378
+ # ============================================================================
379
+
380
+ def run_reasoning_heal():
381
+ """Run the full 3-phase reasoning heal."""
382
+ total_start = time.time()
383
+
384
+ print("\n" + "=" * 70)
385
+ print("3-PHASE REASONING HEAL")
386
+ print("Phase 1: Language coherence (general text)")
387
+ print("Phase 2: Stop-token fix (chat format)")
388
+ print("Phase 3: Reasoning restoration (CoT data, layers 16-28)")
389
+ print(f"Started at: {time.strftime('%H:%M:%S')}")
390
+ print("=" * 70)
391
+ sys.stdout.flush()
392
+
393
+ # Check for existing healed model
394
+ healed_check = os.path.join(OUTPUT_DIR, 'model.safetensors')
395
+ healed_check_shard = list(Path(OUTPUT_DIR).glob("model-*.safetensors")) if Path(OUTPUT_DIR).exists() else []
396
+ if os.path.exists(healed_check) or healed_check_shard:
397
+ total_size = sum(f.stat().st_size for f in (healed_check_shard or [Path(healed_check)])) if (healed_check_shard or os.path.exists(healed_check)) else 0
398
+ if total_size > 1_000_000_000:
399
+ print(f"[heal] Found existing reasoning-healed model ({total_size/1e9:.1f} GB) — SKIPPING!")
400
+ return OUTPUT_DIR
401
+
402
+ # Find model
403
+ model_path = find_input_model()
404
+ if not model_path:
405
+ print("[heal] ERROR: Cannot find input model!")
406
+ print(f"[heal] Looked in: {INPUT_PATHS}")
407
+ sys.exit(1)
408
+
409
+ # Load model
410
+ model, tokenizer = load_model_smart(model_path)
411
+
412
+ # Run each phase
413
+ output_subdir = Path(OUTPUT_DIR)
414
+ output_subdir.mkdir(parents=True, exist_ok=True)
415
+
416
+ for phase_name, phase_config in PHASES.items():
417
+ model = run_phase(model, tokenizer, phase_name, phase_config, str(output_subdir))
418
+ gc.collect()
419
+ torch.cuda.empty_cache()
420
+
421
+ # Save final model
422
+ print(f"\n[heal] Saving final reasoning-healed model to {OUTPUT_DIR}...")
423
+ sys.stdout.flush()
424
+ t = time.time()
425
+ model.save_pretrained(str(output_subdir), safe_serialization=True)
426
+ tokenizer.save_pretrained(str(output_subdir))
427
+
428
+ # Verify save
429
+ saved_files = list(output_subdir.glob("*.safetensors"))
430
+ total_size = sum(f.stat().st_size for f in saved_files) if saved_files else 0
431
+ if not saved_files or total_size < 1_000_000_000:
432
+ print(f"[heal] WARNING: Save may have failed! Only {total_size/1e9:.1f} GB")
433
+ else:
434
+ print(f"[heal] Saved: {len(saved_files)} files, {total_size/1e9:.1f} GB")
435
+
436
+ total_min = (time.time() - total_start) / 60
437
+ print(f"\n{'=' * 70}")
438
+ print(f"3-PHASE REASONING HEAL COMPLETE: {total_min:.1f} min total")
439
+ print(f"Output: {OUTPUT_DIR}")
440
+ print(f"{'=' * 70}")
441
+ sys.stdout.flush()
442
+
443
+ return str(output_subdir)
444
+
445
+
446
+ if __name__ == "__main__":
447
+ run_reasoning_heal()
hugging/td_fuse/run_overnight.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Overnight Pipeline: Merge Klear-Reasoner + 3-Phase Reasoning Heal + Benchmark.
3
+
4
+ Run this before going to sleep:
5
+ nohup python3 -m td_fuse.run_overnight > overnight.log 2>&1 &
6
+
7
+ What it does:
8
+ 1. Merges Klear-Reasoner-8B into the existing 5-model merge (~10-15 min)
9
+ 2. Runs 3-phase reasoning heal (~2-3 hours)
10
+ 3. Runs benchmark to see how good the final model is
11
+
12
+ Total estimated time: 3-4 hours.
13
+ """
14
+
15
+ import sys
16
+ import time
17
+
18
+
19
+ def main():
20
+ total_start = time.time()
21
+ print("=" * 70)
22
+ print("OVERNIGHT PIPELINE: Merge + Reasoning Heal + Benchmark")
23
+ print(f"Started at: {time.strftime('%Y-%m-%d %H:%M:%S')}")
24
+ print("=" * 70)
25
+ sys.stdout.flush()
26
+
27
+ # --- Step 1: Merge Klear-Reasoner-8B ---
28
+ print("\n\n>>> STEP 1/3: MERGING KLEAR-REASONER-8B <<<\n")
29
+ sys.stdout.flush()
30
+ from td_fuse.merge_klear import run_merge
31
+ merge_path = run_merge()
32
+ print(f"\n[overnight] Merge complete. Output: {merge_path}")
33
+ sys.stdout.flush()
34
+
35
+ # --- Step 2: 3-Phase Reasoning Heal ---
36
+ print("\n\n>>> STEP 2/3: 3-PHASE REASONING HEAL <<<\n")
37
+ sys.stdout.flush()
38
+ from td_fuse.reasoning_heal import run_reasoning_heal
39
+ heal_path = run_reasoning_heal()
40
+ print(f"\n[overnight] Reasoning heal complete. Output: {heal_path}")
41
+ sys.stdout.flush()
42
+
43
+ # --- Step 3: Benchmark ---
44
+ print("\n\n>>> STEP 3/3: BENCHMARK <<<\n")
45
+ sys.stdout.flush()
46
+ try:
47
+ from td_fuse.grpo_reason import pre_benchmark
48
+ pre_benchmark(heal_path)
49
+ except Exception as e:
50
+ print(f"[overnight] Benchmark failed: {e}")
51
+ print("[overnight] Model is saved — you can benchmark manually later.")
52
+ sys.stdout.flush()
53
+
54
+ # --- Done ---
55
+ total_hours = (time.time() - total_start) / 3600
56
+ print(f"\n\n{'=' * 70}")
57
+ print(f"OVERNIGHT PIPELINE COMPLETE: {total_hours:.1f} hours total")
58
+ print(f"Final model at: {heal_path}")
59
+ print(f"Finished at: {time.strftime('%Y-%m-%d %H:%M:%S')}")
60
+ print(f"{'=' * 70}")
61
+ sys.stdout.flush()
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
hugging/td_fuse/selfimprove.py CHANGED
@@ -240,29 +240,15 @@ def generate_math_problems(count=200):
240
 
241
  def verify_answer(problem, model_answer):
242
  """
243
- Verified reward: check if the answer is correct.
244
- This is the GRPO reward signal — objective, not learned.
245
  """
246
- expected = problem.get("verify_key", problem["answer"]).lower().strip()
247
- answer_lower = model_answer.lower().strip()
248
-
249
- # Check if expected answer appears in model output
250
- if expected in answer_lower:
251
- return 1.0
252
-
253
- # For numeric answers, try to find the number
254
- if expected.replace(".", "").replace("-", "").isdigit():
255
- # Look for the number in the output
256
- import re
257
- numbers = re.findall(r'-?\d+\.?\d*', answer_lower)
258
- for num in numbers:
259
- try:
260
- if abs(float(num) - float(expected)) < 0.01:
261
- return 1.0
262
- except ValueError:
263
- pass
264
-
265
- return 0.0
266
 
267
 
268
  def generate_and_score(model, tokenizer, problems, cfg, eos_id):
@@ -390,15 +376,14 @@ def train_on_winners(model, tokenizer, winning_pairs, cfg):
390
  print(f" Training: ~{total_steps} steps")
391
  trainer.train()
392
 
393
- # Clean up training checkpoints
394
- import shutil
395
- shutil.rmtree(str(out_dir), ignore_errors=True)
396
-
397
- # Merge LoRA back
398
  print(" Merging LoRA...")
399
  merged = model.merge_and_unload()
400
  gc.collect()
401
 
 
 
 
402
  return merged
403
 
404
 
@@ -418,13 +403,13 @@ def benchmark(model, tokenizer, eos_id):
418
  out = model.generate(**ids, max_new_tokens=200, do_sample=False, eos_token_id=eos_id)
419
  return tokenizer.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
420
 
421
- # Math
422
  math_tests = [("7+8", "15"), ("123+456", "579"), ("1000-387", "613"), ("12*13", "156"), ("144/12", "12")]
423
- math_correct = sum(1 for q, e in math_tests if e in ask(f"What is {q}? Give just the number."))
424
  results["basic_math"] = f"{math_correct}/5"
425
  print(f" Math: {math_correct}/5")
426
 
427
- # Reasoning
428
  reason_tests = [
429
  ("If all roses are flowers and all flowers need water, do roses need water?", "yes"),
430
  ("Which is heavier: a pound of feathers or a pound of bricks?", "same"),
@@ -435,19 +420,19 @@ def benchmark(model, tokenizer, eos_id):
435
  reason_correct = 0
436
  for q, expected in reason_tests:
437
  a = ask(q)
438
- correct = expected.lower() in a.lower()
439
  reason_correct += correct
440
  print(f" {'OK' if correct else 'FAIL'}: {q[:50]}... -> {a[:60]}")
441
  results["reasoning"] = f"{reason_correct}/5"
442
  print(f" Reasoning: {reason_correct}/5")
443
 
444
- # Word problems
445
  wp_tests = [
446
  ("A train travels 60 mph for 2.5 hours. How far does it go?", "150"),
447
  ("If 3 shirts cost $45, how much do 7 shirts cost?", "105"),
448
  ("I have 24 cookies split equally among 6 friends. How many each?", "4"),
449
  ]
450
- wp_correct = sum(1 for q, e in wp_tests if e in ask(q))
451
  results["word_problems"] = f"{wp_correct}/3"
452
  print(f" Word problems: {wp_correct}/3")
453
 
@@ -521,7 +506,7 @@ def run_cycle(cfg: SelfImproveConfig = None, cycle_num: int = 1):
521
  print(f"\nSaving improved model to {improved_dir}...")
522
  model.save_pretrained(str(improved_dir), safe_serialization=True)
523
  tokenizer.save_pretrained(str(improved_dir))
524
- sz = (improved_dir / "model.safetensors").stat().st_size / 1e9
525
  print(f"SAVED: {improved_dir} ({sz:.1f} GB)")
526
 
527
  # Step 5: Benchmark
 
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):
 
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
 
 
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"),
 
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
 
 
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