| """ |
| TD Self-Improvement v7 — SPEED OPTIMIZED MEGA CYCLE |
| |
| v7 speed changes (March 2026): |
| - Flash Attention 2: 30-40% faster attention computation |
| - TF32 math: ~15-20% faster matrix multiply on Ampere GPUs (A6000) |
| - Dynamic padding: pad to longest-in-batch, not fixed 1024 tokens |
| - 4 dataloader workers + pin_memory: parallel data loading |
| - 2 epochs (was 3): model at 95%+, third epoch had diminishing returns |
| - Combined: ~50-60% faster → estimated 3-4 hours per cycle (was 6-7) |
| |
| v6 MEGA MODE (unchanged): |
| - 8000+ training samples (was 3000) |
| - Priority: code, math, reasoning, creativity (top), then general (second) |
| - LoRA r=128 (was 64) — maximum reshape capacity |
| - ALL 36 layers targeted (was 16) |
| - gradient_checkpointing for VRAM savings |
| - batch_size 4 + grad_accum 4 = effective batch 16 |
| - Norm unclogging continues from cycle 2 |
| """ |
|
|
| import torch |
| import time |
| import json |
| import math |
| import gc |
| import shutil |
| import random |
| import numpy as np |
| from pathlib import Path |
| from typing import List, Dict, Tuple, Optional |
| from dataclasses import dataclass, field |
|
|
|
|
| @dataclass |
| class SelfImproveConfig: |
| """Configuration for full self-improvement.""" |
| model_path: str = "" |
| reference_model: str = "Qwen/Qwen3-VL-8B-Instruct" |
| output_dir: str = "td_fuse_outputs/self_improve" |
| mega_mode: bool = False |
|
|
| |
| top_n_layers: int = 16 |
| min_improvement_score: float = 0.05 |
|
|
| |
| lora_r: int = 64 |
| lora_alpha: int = 128 |
| train_epochs: int = 3 |
| train_batch: int = 2 |
| train_grad_accum: int = 8 |
| learning_rate: float = 1.5e-5 |
| norm_learning_rate: float = 5e-5 |
| save_steps: int = 100 |
| num_train_samples: int = 3000 |
|
|
| |
| custom_data_path: str = "" |
|
|
| |
| norm_kurtosis_threshold: float = 15.0 |
|
|
| |
| w_health: float = 0.55 |
| w_damage: float = 0.25 |
| w_coherence: float = 0.20 |
|
|
|
|
| def apply_mega_mode(cfg: SelfImproveConfig): |
| """Crank everything up for mega cycles (3+). |
| |
| v7 SPEED optimizations (March 2026): |
| - Flash Attention 2: 30-40% faster attention |
| - TF32 math: ~15-20% faster matmul on Ampere GPUs |
| - Dynamic padding: no wasted compute on short samples |
| - 4 dataloader workers: parallel data prep |
| - 2 epochs (was 3): model is 95%+, diminishing returns on epoch 3 |
| Combined: ~50-60% faster → ~3-4 hours per cycle instead of 6-7 |
| """ |
| print("\n *** MEGA MODE ENABLED (v7 — speed optimized) ***") |
| cfg.mega_mode = True |
| cfg.top_n_layers = 36 |
| cfg.min_improvement_score = 0.0 |
| cfg.lora_r = 128 |
| cfg.lora_alpha = 256 |
| cfg.train_epochs = 2 |
| cfg.train_batch = 4 |
| cfg.train_grad_accum = 4 |
| cfg.num_train_samples = 8000 |
| cfg.learning_rate = 1e-5 |
| cfg.norm_learning_rate = 1e-3 |
| cfg.save_steps = 200 |
| print(f" LoRA: r={cfg.lora_r}, alpha={cfg.lora_alpha}") |
| print(f" Layers: ALL {cfg.top_n_layers}") |
| print(f" Data: {cfg.num_train_samples} samples") |
| print(f" Batch: {cfg.train_batch} × {cfg.train_grad_accum} = {cfg.train_batch * cfg.train_grad_accum}") |
| print(f" Epochs: {cfg.train_epochs} (was 3 — 33% time saved)") |
| print(f" Speed: flash_attn2 + tf32 + dynamic_pad + gradient_checkpointing") |
|
|
|
|
| def auto_detect_model_path() -> Tuple[str, int]: |
| """Find the best starting model and determine cycle number.""" |
| base = Path("td_fuse_outputs/self_improve") |
|
|
| |
| if base.exists(): |
| for n in range(50, 0, -1): |
| cycle_dir = base / f"improved_cycle{n}" |
| if cycle_dir.exists() and list(cycle_dir.glob("*.safetensors")): |
| print(f" Found cycle {n} output at {cycle_dir}") |
| return str(cycle_dir), n + 1 |
|
|
| |
| old_improved = base / "improved" |
| if old_improved.exists() and list(old_improved.glob("*.safetensors")): |
| print(f" Found cycle 1 output at {old_improved}") |
| return str(old_improved), 2 |
|
|
| |
| healed = Path("td_fuse_outputs/reasoning_healed") |
| if healed.exists() and list(healed.glob("*.safetensors")): |
| print(f" Found healed model at {healed}") |
| return str(healed), 1 |
|
|
| raise FileNotFoundError("No model found! Need reasoning_healed or cycle output.") |
|
|
|
|
| |
| |
| |
|
|
| def analyze_layer(name: str, tensor: torch.Tensor) -> Dict: |
| """Full health analysis for a single weight tensor.""" |
| t = tensor.float() |
|
|
| std_val = t.std().item() |
| mean_val = t.mean().item() |
| abs_mean_val = t.abs().mean().item() |
|
|
| |
| |
| try: |
| normalized = (t - mean_val) / max(std_val, 1e-8) |
| |
| normalized = normalized.clamp(-1e4, 1e4) |
| kurtosis_val = normalized.pow(4).mean().item() - 3 |
| skewness_val = normalized.pow(3).mean().item() |
| |
| if math.isnan(kurtosis_val) or math.isinf(kurtosis_val): |
| kurtosis_val = 0.0 |
| if math.isnan(skewness_val) or math.isinf(skewness_val): |
| skewness_val = 0.0 |
| except Exception: |
| kurtosis_val = 0.0 |
| skewness_val = 0.0 |
|
|
| stats = { |
| "name": name, |
| "shape": list(tensor.shape), |
| "params": tensor.numel(), |
| "dtype": str(tensor.dtype), |
| "l2_norm": t.norm(2).item(), |
| "frobenius_norm": t.norm('fro').item() if t.dim() >= 2 else t.norm(2).item(), |
| "mean": mean_val, |
| "std": std_val, |
| "min": t.min().item(), |
| "max": t.max().item(), |
| "abs_mean": abs_mean_val, |
| "sparsity": (t.abs() < 1e-6).float().mean().item(), |
| "kurtosis": kurtosis_val, |
| "skewness": skewness_val, |
| "near_zero_pct": (t.abs() < 1e-5).float().mean().item(), |
| "extreme_pct": (t.abs() > abs_mean_val * 5).float().mean().item() if abs_mean_val > 0 else 0.0, |
| } |
|
|
| if t.dim() == 2 and min(t.shape) >= 2: |
| try: |
| rows = min(512, t.shape[0]) |
| cols = min(512, t.shape[1]) |
| sample = t[:rows, :cols] |
| svd = torch.linalg.svdvals(sample) |
| stats["sv_max"] = svd[0].item() |
| stats["sv_min"] = svd[-1].item() |
| stats["sv_ratio"] = svd[0].item() / max(svd[-1].item(), 1e-10) |
| sv_norm = svd / svd.sum() |
| entropy = -(sv_norm * torch.log(sv_norm + 1e-10)).sum() |
| stats["sv_effective_rank"] = torch.exp(entropy).item() |
| stats["sv_max_rank"] = float(min(sample.shape)) |
| stats["sv_rank_utilization"] = stats["sv_effective_rank"] / stats["sv_max_rank"] |
| top5_energy = (svd[:5] ** 2).sum() / (svd ** 2).sum() |
| stats["sv_top5_energy"] = top5_energy.item() |
| stats["stable_rank"] = (sample.norm('fro').item() ** 2) / max(svd[0].item() ** 2, 1e-10) |
| except Exception: |
| stats["sv_ratio"] = -1 |
| stats["sv_rank_utilization"] = -1 |
|
|
| return stats |
|
|
|
|
| def analyze_model_weights(model_path: str, label: str = "model") -> Dict: |
| """Load and analyze every language layer.""" |
| print(f"\n{'='*50}") |
| print(f"ANALYZING: {label}") |
| print(f"Path: {model_path}") |
| print(f"{'='*50}") |
|
|
| from safetensors.torch import load_file |
| import glob |
|
|
| st_files = sorted(glob.glob(f"{model_path}/*.safetensors")) |
| if not st_files: |
| raise FileNotFoundError(f"No safetensors files in {model_path}") |
|
|
| all_stats = {} |
| total_params = 0 |
|
|
| for st_file in st_files: |
| print(f" Loading {Path(st_file).name}...") |
| try: |
| state_dict = load_file(st_file, device="cpu") |
| except Exception as e: |
| print(f" WARNING: Failed to load {st_file}: {e} — skipping shard") |
| continue |
| for name, tensor in state_dict.items(): |
| if any(skip in name for skip in ["visual.", "merger."]): |
| continue |
| try: |
| stats = analyze_layer(name, tensor) |
| all_stats[name] = stats |
| total_params += stats["params"] |
| except Exception as e: |
| print(f" WARNING: analyze_layer failed for {name}: {e}") |
| continue |
| del state_dict |
| gc.collect() |
|
|
| print(f" Analyzed {len(all_stats)} tensors ({total_params/1e9:.2f}B params)") |
| return {"label": label, "layers": all_stats, "total_params": total_params} |
|
|
|
|
| |
| |
| |
|
|
| def compute_health_scores(analysis: Dict) -> Dict[str, Dict]: |
| """Score each layer's health. Higher = more room for improvement.""" |
| print("\n=== STANDALONE HEALTH ANALYSIS ===") |
| health_scores = {} |
|
|
| for name, stats in analysis["layers"].items(): |
| issues = [] |
| score = 0.0 |
|
|
| |
| kurt = abs(stats.get("kurtosis", 0)) |
| if kurt > 10: |
| score += 0.3 |
| issues.append(f"extreme kurtosis ({kurt:.1f})") |
| elif kurt > 5: |
| score += 0.15 |
| issues.append(f"high kurtosis ({kurt:.1f})") |
|
|
| skew = abs(stats.get("skewness", 0)) |
| if skew > 2: |
| score += 0.15 |
| issues.append(f"high skew ({skew:.2f})") |
|
|
| rank_util = stats.get("sv_rank_utilization", -1) |
| if rank_util > 0: |
| if rank_util < 0.1: |
| score += 0.3 |
| issues.append(f"very low rank utilization ({rank_util:.2%})") |
| elif rank_util < 0.25: |
| score += 0.15 |
| issues.append(f"low rank utilization ({rank_util:.2%})") |
| top5 = stats.get("sv_top5_energy", 0) |
| if top5 > 0.9: |
| score += 0.2 |
| issues.append(f"top-5 SVD concentration ({top5:.2%})") |
| elif top5 > 0.75: |
| score += 0.1 |
|
|
| sv_ratio = stats.get("sv_ratio", -1) |
| if sv_ratio > 0: |
| if sv_ratio > 10000: |
| score += 0.25 |
| issues.append(f"extreme condition number ({sv_ratio:.0f})") |
| elif sv_ratio > 1000: |
| score += 0.1 |
|
|
| dead = stats.get("near_zero_pct", 0) |
| if dead > 0.3: |
| score += 0.2 |
| issues.append(f"high dead neuron % ({dead:.1%})") |
| elif dead > 0.15: |
| score += 0.1 |
|
|
| extreme = stats.get("extreme_pct", 0) |
| if extreme > 0.05: |
| score += 0.15 |
| issues.append(f"many extreme weights ({extreme:.1%})") |
|
|
| std_val = stats.get("std", 0) |
| if std_val < 1e-4: |
| score += 0.25 |
| issues.append(f"near-zero variance ({std_val:.6f})") |
| elif std_val > 1.0: |
| score += 0.15 |
| issues.append(f"high variance ({std_val:.4f})") |
|
|
| health_scores[name] = { |
| "health_score": min(score, 1.0), |
| "issues": issues, |
| "stats": stats, |
| } |
|
|
| return health_scores |
|
|
|
|
| |
| |
| |
|
|
| def compute_damage_scores(model_analysis: Dict, ref_analysis: Dict) -> Dict[str, float]: |
| """Compare each layer to reference.""" |
| print("\n=== REFERENCE COMPARISON (merge damage) ===") |
| damages = {} |
| matched = 0 |
|
|
| for name, m_stats in model_analysis["layers"].items(): |
| ref_key = None |
| for c in [name, name.replace("model.", "", 1), f"model.{name}"]: |
| if c in ref_analysis["layers"]: |
| ref_key = c |
| break |
| if ref_key is None: |
| continue |
|
|
| r_stats = ref_analysis["layers"][ref_key] |
| matched += 1 |
|
|
| |
| norm_diff = abs(m_stats.get("l2_norm", 0) - r_stats.get("l2_norm", 0)) / max(r_stats.get("l2_norm", 1e-8), 1e-8) |
| std_diff = abs(m_stats.get("std", 0) - r_stats.get("std", 0)) / max(r_stats.get("std", 1e-8), 1e-8) |
| kurt_diff = abs(m_stats.get("kurtosis", 0) - r_stats.get("kurtosis", 0)) / max(abs(r_stats.get("kurtosis", 0)) + 1, 1e-8) |
| sparsity_diff = abs(m_stats.get("sparsity", 0) - r_stats.get("sparsity", 0)) |
|
|
| sv_ratio_diff = 0 |
| if "sv_ratio" in m_stats and "sv_ratio" in r_stats: |
| if m_stats["sv_ratio"] > 0 and r_stats["sv_ratio"] > 0: |
| sv_ratio_diff = abs( |
| math.log(m_stats["sv_ratio"] + 1) - math.log(r_stats["sv_ratio"] + 1) |
| ) / max(math.log(r_stats["sv_ratio"] + 1), 1e-8) |
|
|
| combined = (0.30 * norm_diff + 0.25 * std_diff + 0.20 * kurt_diff + |
| 0.15 * sv_ratio_diff + 0.10 * sparsity_diff) |
| damages[name] = combined |
|
|
| print(f" Matched {matched} layers between model and reference") |
| return damages |
|
|
|
|
| |
| |
| |
|
|
| def compute_coherence_scores(analysis: Dict) -> Dict[int, float]: |
| """Check layer-to-layer coherence.""" |
| print("\n=== LAYER COHERENCE ANALYSIS ===") |
|
|
| layer_norms = {} |
| layer_stds = {} |
|
|
| for name, stats in analysis["layers"].items(): |
| parts = name.split(".") |
| layer_num = None |
| for j, part in enumerate(parts): |
| if part == "layers" and j + 1 < len(parts) and parts[j+1].isdigit(): |
| layer_num = int(parts[j+1]) |
| break |
| if layer_num is not None: |
| if layer_num not in layer_norms: |
| layer_norms[layer_num] = [] |
| layer_stds[layer_num] = [] |
| layer_norms[layer_num].append(stats.get("l2_norm", 0)) |
| layer_stds[layer_num].append(stats.get("std", 0)) |
|
|
| avg_norms = {l: sum(ns)/len(ns) for l, ns in layer_norms.items()} |
| avg_stds = {l: sum(ss)/len(ss) for l, ss in layer_stds.items()} |
| sorted_layers = sorted(avg_norms.keys()) |
| coherence_issues = {} |
|
|
| for i in range(len(sorted_layers)): |
| l = sorted_layers[i] |
| score = 0.0 |
| if i > 0: |
| prev = sorted_layers[i-1] |
| norm_jump = abs(avg_norms[l] - avg_norms[prev]) / max(avg_norms[prev], 1e-8) |
| std_jump = abs(avg_stds[l] - avg_stds[prev]) / max(avg_stds[prev], 1e-8) |
| if norm_jump > 0.5: |
| score += norm_jump * 0.5 |
| if std_jump > 0.5: |
| score += std_jump * 0.3 |
| if i < len(sorted_layers) - 1: |
| nxt = sorted_layers[i+1] |
| norm_jump = abs(avg_norms[l] - avg_norms[nxt]) / max(avg_norms[nxt], 1e-8) |
| if norm_jump > 0.5: |
| score += norm_jump * 0.3 |
| coherence_issues[l] = min(score, 1.0) |
|
|
| return coherence_issues |
|
|
|
|
| |
| |
| |
|
|
| def rank_layers_for_improvement( |
| health_scores: Dict[str, Dict], |
| damage_scores: Dict[str, float], |
| coherence_scores: Dict[int, float], |
| cfg: SelfImproveConfig |
| ) -> Tuple[List[int], Dict]: |
| """Combine all signals into improvement potential per layer.""" |
| print("\n=== RANKING LAYERS BY IMPROVEMENT POTENTIAL ===") |
|
|
| layer_health = {} |
| layer_damage = {} |
|
|
| for name, h in health_scores.items(): |
| parts = name.split(".") |
| layer_num = None |
| for j, part in enumerate(parts): |
| if part == "layers" and j + 1 < len(parts) and parts[j+1].isdigit(): |
| layer_num = int(parts[j+1]) |
| break |
| if layer_num is not None: |
| if layer_num not in layer_health: |
| layer_health[layer_num] = [] |
| layer_damage[layer_num] = [] |
| layer_health[layer_num].append(h["health_score"]) |
| if name in damage_scores: |
| layer_damage[layer_num].append(damage_scores[name]) |
|
|
| layer_scores = {} |
| layer_details = {} |
|
|
| for layer_num in layer_health: |
| avg_health = sum(layer_health[layer_num]) / len(layer_health[layer_num]) |
| avg_damage = (sum(layer_damage[layer_num]) / len(layer_damage[layer_num]) |
| if layer_damage.get(layer_num) else 0.0) |
| coherence = coherence_scores.get(layer_num, 0.0) |
| combined = (cfg.w_health * avg_health + cfg.w_damage * avg_damage + |
| cfg.w_coherence * coherence) |
| layer_scores[layer_num] = combined |
| layer_details[layer_num] = { |
| "health": avg_health, "damage": avg_damage, |
| "coherence": coherence, "combined": combined, |
| } |
|
|
| sorted_layers = sorted(layer_scores.items(), key=lambda x: x[1], reverse=True) |
|
|
| print(f"\n {'Layer':>7} {'Combined':>9} {'Health':>8} {'Damage':>8} {'Coherence':>10} {'Status'}") |
| print(f" {'-'*65}") |
| for layer_num, score in sorted_layers: |
| d = layer_details[layer_num] |
| bar = "█" * int(score * 50) |
| target = " ← TARGET" if score >= cfg.min_improvement_score else "" |
| print(f" Layer {layer_num:>2} {score:>9.4f} {d['health']:>8.4f} {d['damage']:>8.4f} {d['coherence']:>10.4f} {bar}{target}") |
|
|
| target_layers = [] |
| for layer_num, score in sorted_layers: |
| if score >= cfg.min_improvement_score and len(target_layers) < cfg.top_n_layers: |
| target_layers.append(layer_num) |
|
|
| if not target_layers: |
| target_layers = [l for l, _ in sorted_layers[:cfg.top_n_layers]] |
|
|
| target_layers.sort() |
| print(f"\n TARGET LAYERS FOR IMPROVEMENT: {target_layers}") |
|
|
| |
| all_issues = [] |
| for name, h in health_scores.items(): |
| parts = name.split(".") |
| layer_num = None |
| for j, part in enumerate(parts): |
| if part == "layers" and j + 1 < len(parts) and parts[j+1].isdigit(): |
| layer_num = int(parts[j+1]) |
| break |
| if layer_num in target_layers and h["issues"]: |
| for issue in h["issues"]: |
| all_issues.append(f" Layer {layer_num} {name.split('.')[-2]}: {issue}") |
| if all_issues: |
| print(f"\n Key issues found:") |
| for issue in all_issues[:30]: |
| print(f" {issue}") |
|
|
| return target_layers, layer_details |
|
|
|
|
| |
| |
| |
|
|
| def detect_clogged_norms(health_scores: Dict[str, Dict], threshold: float = 15.0) -> List[str]: |
| """ |
| Find layernorm and k_norm parameters with extreme kurtosis. |
| These are the "clogged pipes" that LoRA can't fix. |
| Returns list of parameter name patterns to unfreeze. |
| """ |
| clogged = [] |
| for name, h in health_scores.items(): |
| if any(norm_type in name.lower() for norm_type in ["layernorm", "ln_", "_norm"]): |
| stats = h.get("stats", {}) if isinstance(h, dict) else {} |
| kurt = abs(stats.get("kurtosis", 0)) |
| if kurt > threshold: |
| clogged.append(name) |
|
|
| return clogged |
|
|
|
|
| def repair_clogged_norms(model_path: str, clogged_norms: List[str], |
| ref_path: str, health_scores: Dict[str, Dict]) -> str: |
| """ |
| DIRECT SURGICAL REPAIR of clogged layernorms. |
| |
| Training can't fix kurtosis of 2446 — gradient signal too weak for 1D norms. |
| Instead: blend broken norms with reference values. |
| |
| Strategy: |
| - Kurtosis > 100: 80% reference, 20% current (severely broken) |
| - Kurtosis > 50: 60% reference, 40% current |
| - Kurtosis > 15: 40% reference, 60% current (mildly clogged) |
| |
| This is like replacing a clogged pipe instead of pushing harder. |
| """ |
| if not clogged_norms: |
| print(" No clogged norms to repair.") |
| return model_path |
|
|
| print(f"\n{'='*50}") |
| print(f"SURGICAL NORM REPAIR — {len(clogged_norms)} clogged norms") |
| print(f"{'='*50}") |
|
|
| from safetensors.torch import load_file, save_file |
| import glob |
|
|
| |
| print(" Loading reference norms...") |
| ref_norms = {} |
| for st_file in sorted(glob.glob(f"{ref_path}/*.safetensors")): |
| try: |
| state = load_file(st_file, device="cpu") |
| except Exception as e: |
| print(f" WARNING: Failed to load reference {st_file}: {e} — skipping") |
| continue |
| for name, tensor in state.items(): |
| if name in clogged_norms: |
| ref_norms[name] = tensor.clone() |
| del state |
|
|
| print(f" Matched {len(ref_norms)}/{len(clogged_norms)} norms in reference") |
|
|
| if not ref_norms: |
| print(" WARNING: No reference norms found! Skipping repair.") |
| return model_path |
|
|
| |
| repaired = 0 |
| st_files = sorted(glob.glob(f"{model_path}/*.safetensors")) |
|
|
| if not st_files: |
| print(" WARNING: No safetensors files found in model path! Skipping repair.") |
| return model_path |
|
|
| for st_file in st_files: |
| try: |
| state = load_file(st_file, device="cpu") |
| except Exception as e: |
| print(f" WARNING: Failed to load {st_file}: {e} — skipping this shard") |
| continue |
| changed = False |
|
|
| for name in list(state.keys()): |
| if name in ref_norms: |
| |
| if name not in health_scores or "stats" not in health_scores[name]: |
| print(f" {name}: not in health_scores — SKIP") |
| continue |
| if "kurtosis" not in health_scores[name]["stats"]: |
| print(f" {name}: no kurtosis in stats — SKIP") |
| continue |
| kurt = abs(health_scores[name]["stats"]["kurtosis"]) |
|
|
| |
| |
| |
| |
| ref_t = ref_norms[name].float() |
| ref_kurt = abs(((ref_t - ref_t.mean()) / max(ref_t.std().item(), 1e-8)).pow(4).mean().item() - 3) |
|
|
| if ref_kurt >= kurt: |
| print(f" {name}: kurtosis {kurt:.1f} (ref is worse: {ref_kurt:.1f}) — SKIP") |
| continue |
|
|
| |
| if kurt > 100: |
| ref_weight = 0.80 |
| elif kurt > 50: |
| ref_weight = 0.60 |
| else: |
| ref_weight = 0.40 |
|
|
| old_tensor = state[name] |
| ref_tensor = ref_norms[name].to(old_tensor.dtype) |
|
|
| |
| state[name] = ref_weight * ref_tensor + (1 - ref_weight) * old_tensor |
|
|
| |
| t = state[name].float() |
| new_kurt = abs(((t - t.mean()) / max(t.std().item(), 1e-8)).pow(4).mean().item() - 3) |
|
|
| print(f" {name}: kurtosis {kurt:.1f} → {new_kurt:.1f} (blend {ref_weight:.0%} ref)") |
| repaired += 1 |
| changed = True |
|
|
| if changed: |
| try: |
| save_file(state, st_file) |
| except Exception as e: |
| print(f" ERROR: Failed to save repaired weights to {st_file}: {e}") |
| print(f" Disk may be full! Norm repair aborted for this shard.") |
| del state |
|
|
| gc.collect() |
| print(f"\n Repaired {repaired} norms via reference blending") |
| return model_path |
|
|
|
|
| |
| |
| |
|
|
| def improve_with_lora(model_path: str, target_layers: List[int], |
| clogged_norms: List[str], cfg: SelfImproveConfig, |
| cycle_num: int = 2): |
| """ |
| Two-pronged improvement: |
| 1. LoRA (r=64) on projection weights in target layers |
| 2. Direct training of clogged layernorm/k_norm parameters |
| |
| This is the "unclogging" — LoRA reshapes projections while |
| unfrozen norms fix the distribution bottlenecks. |
| """ |
| print(f"\n{'='*50}") |
| print(f"LORA + NORM UNCLOGGING") |
| print(f" LoRA layers: {target_layers}") |
| print(f" Clogged norms to unfreeze: {len(clogged_norms)}") |
| print(f"{'='*50}") |
|
|
| from transformers import AutoModelForImageTextToText, AutoTokenizer, TrainingArguments, Trainer |
| from peft import LoraConfig, get_peft_model, TaskType |
| from datasets import load_dataset |
| from torch.utils.data import Dataset |
|
|
| |
| |
| try: |
| disk = shutil.disk_usage(cfg.output_dir if Path(cfg.output_dir).exists() else ".") |
| free_gb = disk.free / 1e9 |
| print(f" Disk space: {free_gb:.1f} GB free") |
| if free_gb < 25: |
| print(f" WARNING: Only {free_gb:.1f} GB free! Need ~25 GB for training + save.") |
| if free_gb < 10: |
| print(f" CRITICAL: Less than 10 GB free — ABORTING to prevent disk-full corruption!") |
| return model_path |
| except Exception as e: |
| print(f" Could not check disk space: {e} — continuing anyway") |
|
|
| |
| model_dir = Path(model_path) |
| if model_dir.is_dir(): |
| if not (model_dir / "config.json").exists(): |
| print(f" ERROR: No config.json in {model_path} — stale or corrupt checkpoint!") |
| return model_path |
| st_files = list(model_dir.glob("*.safetensors")) |
| if not st_files and not list(model_dir.glob("*.bin")): |
| print(f" ERROR: No model weights in {model_path} — checkpoint is empty!") |
| return model_path |
|
|
| |
| |
| torch.backends.cuda.matmul.allow_tf32 = True |
| torch.backends.cudnn.allow_tf32 = True |
| print(" Loading model...") |
| try: |
| model = AutoModelForImageTextToText.from_pretrained( |
| model_path, dtype=torch.bfloat16, |
| device_map="auto", trust_remote_code=True, |
| attn_implementation="flash_attention_2" |
| ) |
| except (RuntimeError, Exception) as e: |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| print(f" CRITICAL: Failed to load model: {e}") |
| return model_path |
| tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) |
|
|
| |
| if cfg.mega_mode: |
| model.gradient_checkpointing_enable() |
| print(" Gradient checkpointing: ENABLED (saves ~40% VRAM)") |
|
|
| |
| lora_config = LoraConfig( |
| r=cfg.lora_r, |
| lora_alpha=cfg.lora_alpha, |
| lora_dropout=0.0, |
| target_modules=["q_proj", "k_proj", "v_proj", "o_proj", |
| "gate_proj", "up_proj", "down_proj"], |
| layers_to_transform=target_layers, |
| bias="none", |
| task_type=TaskType.CAUSAL_LM, |
| ) |
| try: |
| model = get_peft_model(model, lora_config) |
| except Exception as e: |
| print(f"\n CRITICAL: get_peft_model failed: {e}") |
| print(f" LoRA config may be incompatible with this model.") |
| del model |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| return model_path |
|
|
| |
| unfrozen_count = 0 |
| unfrozen_params = 0 |
| for name, param in model.named_parameters(): |
| |
| |
| clean_name = name.replace("base_model.model.", "").replace("base_model.", "") |
| for clogged_name in clogged_norms: |
| if clean_name == clogged_name or clogged_name in clean_name: |
| param.requires_grad = True |
| unfrozen_count += 1 |
| unfrozen_params += param.numel() |
| break |
|
|
| if unfrozen_count == 0 and clogged_norms: |
| print(f"\n WARNING: Found {len(clogged_norms)} clogged norms but matched 0!") |
| print(f" Expected names like: {clogged_norms[:3]}") |
| |
| for name, _ in list(model.named_parameters())[:5]: |
| print(f" Actual param: {name}") |
| print(f"\n LoRA trainable + {unfrozen_count} unfrozen norm layers ({unfrozen_params:,} params)") |
| model.print_trainable_parameters() |
|
|
| |
| |
| print("\n Loading training data...") |
| train_texts = [] |
| total = cfg.num_train_samples |
|
|
| |
| |
| |
| |
| if cfg.custom_data_path and Path(cfg.custom_data_path).exists(): |
| print(f" Using TARGETED training data from: {cfg.custom_data_path}") |
| with open(cfg.custom_data_path) as _f: |
| for line in _f: |
| line = line.strip() |
| if not line: |
| continue |
| try: |
| item = json.loads(line) |
| text = item.get("text", "") |
| if text and len(text) > 20: |
| train_texts.append(text) |
| except json.JSONDecodeError: |
| continue |
| if len(train_texts) >= total: |
| break |
| print(f" Loaded {len(train_texts)} targeted samples") |
| if len(train_texts) < 200: |
| print(f" WARNING: Only got {len(train_texts)} targeted samples (very low).") |
| print(f" Supplementing with generic data...") |
| |
| else: |
| |
| |
| |
| print(f" Using {len(train_texts)} targeted samples ONLY (no generic filler)") |
| print(f" Reason: generic data causes loss=0 because model already knows it") |
| total = len(train_texts) |
|
|
| |
| |
| |
| |
| alloc = {"code": 0, "math": 0, "reasoning": 0, "creativity": 0, |
| "instruction_following": 0, "general": 0} |
| if len(train_texts) < total: |
| remaining_needed = total - len(train_texts) |
| if cfg.custom_data_path and train_texts: |
| print(f" Need {remaining_needed} more samples from generic datasets...") |
| scale = remaining_needed / total |
| else: |
| scale = 1.0 |
|
|
| if cfg.mega_mode: |
| alloc = {"code": int(total*0.25*scale), "math": int(total*0.25*scale), |
| "reasoning": int(total*0.19*scale), "creativity": int(total*0.12*scale), |
| "instruction_following": int(total*0.12*scale), "general": int(total*0.07*scale)} |
| else: |
| per = int((total // 5) * scale) |
| alloc = {"code": per, "math": per, "reasoning": per, |
| "creativity": 0, "instruction_following": per, "general": per} |
| else: |
| print(f" Targeted data sufficient ({len(train_texts)} samples). Skipping generic datasets.") |
|
|
| |
| try: |
| count = 0 |
| if alloc["math"] > 0: |
| ds = load_dataset("openai/gsm8k", "main", split="train") |
| for row in ds: |
| if count >= alloc["math"]: |
| break |
| q, a = row.get("question", ""), row.get("answer", "") |
| if q and a: |
| chat = (f"<|im_start|>user\n{q}\nThink step by step.<|im_end|>\n" |
| f"<|im_start|>assistant\n<think>\n{a}\n</think><|im_end|>") |
| train_texts.append(chat) |
| count += 1 |
| print(f" Math (GSM8K): {count}") |
| except Exception as e: |
| print(f" WARNING: GSM8K failed: {e}") |
|
|
| |
| if cfg.mega_mode and count < alloc["math"]: |
| try: |
| ds2 = load_dataset("lighteval/MATH", "all", split="train", trust_remote_code=True) |
| extra = 0 |
| for row in ds2: |
| if count + extra >= alloc["math"]: |
| break |
| q = row.get("problem", "") |
| a = row.get("solution", "") |
| if q and a: |
| chat = (f"<|im_start|>user\n{q}\nSolve this step by step.<|im_end|>\n" |
| f"<|im_start|>assistant\n<think>\n{a}\n</think><|im_end|>") |
| train_texts.append(chat) |
| extra += 1 |
| print(f" Math (MATH hard): {extra}") |
| except Exception as e: |
| print(f" WARNING: MATH dataset failed: {e}") |
|
|
| |
| try: |
| count = 0 |
| if alloc["code"] > 0: |
| ds = load_dataset("sahil2801/CodeAlpaca-20k", split="train") |
| for row in ds: |
| if count >= alloc["code"]: |
| break |
| inst = row.get("instruction", "") |
| inp = row.get("input", "") |
| out = row.get("output", "") |
| if inst and out and len(out) > 20: |
| prompt = f"{inst}\n{inp}" if inp else inst |
| chat = (f"<|im_start|>user\n{prompt}<|im_end|>\n" |
| f"<|im_start|>assistant\n{out}<|im_end|>") |
| train_texts.append(chat) |
| count += 1 |
| print(f" Code (CodeAlpaca): {count}") |
| except Exception as e: |
| print(f" WARNING: CodeAlpaca failed: {e}") |
|
|
| |
| if cfg.mega_mode and count < alloc["code"]: |
| try: |
| ds2 = load_dataset("m-a-p/CodeFeedback-Filtered-Instruction", split="train") |
| extra = 0 |
| for row in ds2: |
| if count + extra >= alloc["code"]: |
| break |
| q = row.get("query", "") |
| a = row.get("answer", "") |
| if q and a and len(a) > 30: |
| chat = (f"<|im_start|>user\n{q}<|im_end|>\n" |
| f"<|im_start|>assistant\n{a[:1500]}<|im_end|>") |
| train_texts.append(chat) |
| extra += 1 |
| print(f" Code (CodeFeedback): {extra}") |
| except Exception as e: |
| print(f" WARNING: CodeFeedback failed: {e}") |
|
|
| |
| try: |
| count = 0 |
| if alloc["reasoning"] > 0: |
| ds = load_dataset("TIGER-Lab/MMLU-Pro", split="test") |
| for row in ds: |
| if count >= alloc["reasoning"]: |
| break |
| q = row.get("question", "") |
| options = row.get("options", []) |
| answer_idx = row.get("answer_index", -1) |
| if q and options and 0 <= answer_idx < len(options): |
| answer_text = options[answer_idx] |
| chat = (f"<|im_start|>user\n{q}<|im_end|>\n" |
| f"<|im_start|>assistant\n<think>\nLet me reason through this.\n" |
| f"{answer_text}\n</think>\n{answer_text}<|im_end|>") |
| train_texts.append(chat) |
| count += 1 |
| print(f" Reasoning (MMLU-Pro): {count}") |
| except Exception as e: |
| print(f" WARNING: MMLU-Pro failed: {e}") |
|
|
| |
| if alloc.get("creativity", 0) > 0: |
| try: |
| ds = load_dataset("yahma/alpaca-cleaned", split="train") |
| count = 0 |
| creative_words = ["write", "story", "poem", "creative", "imagine", "describe", |
| "compose", "draft", "narrative", "fiction", "essay", "letter", |
| "explain", "summarize", "rewrite", "paraphrase"] |
| for row in ds: |
| if count >= alloc["creativity"]: |
| break |
| inst = row.get("instruction", "").lower() |
| out = row.get("output", "") |
| if any(w in inst for w in creative_words) and out and len(out) > 50: |
| inp = row.get("input", "") |
| prompt = f"{row['instruction']}\n{inp}" if inp else row["instruction"] |
| chat = (f"<|im_start|>user\n{prompt}<|im_end|>\n" |
| f"<|im_start|>assistant\n{out}<|im_end|>") |
| train_texts.append(chat) |
| count += 1 |
| print(f" Creativity (Alpaca filtered): {count}") |
| except Exception as e: |
| print(f" WARNING: Creativity data failed: {e}") |
|
|
| |
| try: |
| count = 0 |
| if alloc.get("instruction_following", 0) > 0: |
| ds = load_dataset("yahma/alpaca-cleaned", split="train") |
| for row in ds: |
| if count >= alloc["instruction_following"]: |
| break |
| inst = row.get("instruction", "") |
| inp = row.get("input", "") |
| out = row.get("output", "") |
| if inst and out and len(out) > 30: |
| prompt = f"{inst}\n{inp}" if inp else inst |
| chat = (f"<|im_start|>user\n{prompt}<|im_end|>\n" |
| f"<|im_start|>assistant\n{out}<|im_end|>") |
| train_texts.append(chat) |
| count += 1 |
| print(f" Instructions (Alpaca): {count}") |
| except Exception as e: |
| print(f" WARNING: Alpaca failed: {e}") |
|
|
| |
| try: |
| count = 0 |
| if alloc.get("general", 0) > 0: |
| ds = load_dataset("neuralmagic/LLM_compression_calibration", split="train") |
| for row in ds: |
| if count >= alloc["general"]: |
| break |
| text = row.get("text", "") |
| if len(text) > 100: |
| chat = (f"<|im_start|>user\nContinue this text naturally.<|im_end|>\n" |
| f"<|im_start|>assistant\n{text[:800]}<|im_end|>") |
| train_texts.append(chat) |
| count += 1 |
| print(f" General text: {count}") |
| except Exception as e: |
| print(f" WARNING: General text failed: {e}") |
|
|
| print(f"\n Total training samples: {len(train_texts)}") |
|
|
| if len(train_texts) < 100: |
| print(" ERROR: Not enough training data!") |
| return model_path |
|
|
| |
| |
| |
| try: |
| from td_contamination import check_contamination |
| before = len(train_texts) |
| train_texts = check_contamination(train_texts) |
| if len(train_texts) < before: |
| print(f" Contamination guard removed {before - len(train_texts)} samples") |
| except ImportError: |
| print(" WARNING: td_contamination.py not found — skipping contamination check") |
|
|
| |
| |
| |
| unique_prefixes = set() |
| for t in train_texts: |
| |
| prefix = t[:100].strip().lower() |
| unique_prefixes.add(prefix) |
| diversity_ratio = len(unique_prefixes) / max(len(train_texts), 1) |
| print(f" Diversity check: {len(unique_prefixes)}/{len(train_texts)} unique prefixes ({diversity_ratio:.0%})") |
| if diversity_ratio < 0.5: |
| print(" WARNING: Low diversity (<50%) — high risk of mode collapse!") |
| print(" Deduplicating training data...") |
| seen = set() |
| deduped = [] |
| for t in train_texts: |
| prefix = t[:100].strip().lower() |
| if prefix not in seen: |
| seen.add(prefix) |
| deduped.append(t) |
| removed = len(train_texts) - len(deduped) |
| train_texts = deduped |
| print(f" Removed {removed} near-duplicate samples. Now {len(train_texts)} unique.") |
| elif diversity_ratio < 0.7: |
| print(" NOTE: Moderate diversity (50-70%) — monitor for early signs of collapse") |
|
|
| random.shuffle(train_texts) |
|
|
| |
| class ImproveDataset(Dataset): |
| def __init__(self, texts, tokenizer, max_len=1024): |
| self.data = [] |
| for t in texts: |
| e = tokenizer(t, truncation=True, max_length=max_len, |
| return_tensors="pt") |
| self.data.append({ |
| "input_ids": e["input_ids"].squeeze(), |
| "attention_mask": e["attention_mask"].squeeze(), |
| "labels": e["input_ids"].squeeze(), |
| }) |
| def __len__(self): return len(self.data) |
| def __getitem__(self, i): return self.data[i] |
|
|
| print(" Tokenizing...") |
| dataset = ImproveDataset(train_texts, tokenizer) |
|
|
| if len(dataset) == 0: |
| print(" ERROR: Dataset is empty after tokenization!") |
| return model_path |
|
|
| out_dir = Path(cfg.output_dir) / "train_output" |
| out_dir.mkdir(parents=True, exist_ok=True) |
|
|
| effective_batch = cfg.train_batch * cfg.train_grad_accum |
| total_steps = (len(dataset) * cfg.train_epochs) // max(effective_batch, 1) |
|
|
| |
| |
| norm_params = [] |
| lora_params = [] |
| for name, param in model.named_parameters(): |
| if param.requires_grad: |
| if any(nt in name.lower() for nt in ["layernorm", "ln_", "_norm"]): |
| norm_params.append(param) |
| else: |
| lora_params.append(param) |
|
|
| print(f"\n Parameter groups: {len(lora_params)} LoRA params, {len(norm_params)} norm params") |
|
|
| if norm_params: |
| from torch.optim import AdamW |
| optimizer = AdamW([ |
| {"params": lora_params, "lr": cfg.learning_rate}, |
| {"params": norm_params, "lr": cfg.norm_learning_rate}, |
| ], weight_decay=0.01) |
| print(f" LoRA LR: {cfg.learning_rate}, Norm LR: {cfg.norm_learning_rate}") |
| else: |
| optimizer = None |
|
|
| args = TrainingArguments( |
| output_dir=str(out_dir), |
| num_train_epochs=cfg.train_epochs, |
| per_device_train_batch_size=cfg.train_batch, |
| gradient_accumulation_steps=cfg.train_grad_accum, |
| learning_rate=cfg.learning_rate, |
| bf16=True, |
| tf32=True, |
| gradient_checkpointing=cfg.mega_mode, |
| logging_steps=max(1, total_steps // 20), |
| save_strategy="steps", |
| save_steps=cfg.save_steps, |
| save_total_limit=2, |
| warmup_ratio=0.05, |
| lr_scheduler_type="cosine", |
| optim="adamw_torch", |
| report_to="none", |
| dataloader_num_workers=4, |
| dataloader_pin_memory=True, |
| ) |
|
|
| |
| |
| def dynamic_pad_collator(batch): |
| max_len = max(item["input_ids"].size(0) for item in batch) |
| pad_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0 |
| input_ids, attention_mask, labels = [], [], [] |
| for item in batch: |
| seq_len = item["input_ids"].size(0) |
| pad_len = max_len - seq_len |
| input_ids.append(torch.cat([item["input_ids"], torch.full((pad_len,), pad_id, dtype=torch.long)])) |
| attention_mask.append(torch.cat([item["attention_mask"], torch.zeros(pad_len, dtype=torch.long)])) |
| labels.append(torch.cat([item["labels"], torch.full((pad_len,), -100, dtype=torch.long)])) |
| return {"input_ids": torch.stack(input_ids), "attention_mask": torch.stack(attention_mask), "labels": torch.stack(labels)} |
|
|
| trainer_kwargs = { |
| "model": model, |
| "processing_class": tokenizer, |
| "train_dataset": dataset, |
| "data_collator": dynamic_pad_collator, |
| "args": args, |
| } |
| if optimizer is not None: |
| trainer_kwargs["optimizers"] = (optimizer, None) |
|
|
| trainer = Trainer(**trainer_kwargs) |
|
|
| print(f"\n Training: ~{total_steps} steps on {len(target_layers)} layers + {len(clogged_norms)} norms") |
| print(f" Data: {len(train_texts)} samples × {cfg.train_epochs} epochs") |
| print(f" LoRA: r={cfg.lora_r}, alpha={cfg.lora_alpha}") |
|
|
| try: |
| train_result = trainer.train() |
| except (torch.cuda.OutOfMemoryError, RuntimeError) as e: |
| err_str = str(e).lower() |
| if "out of memory" in err_str or "cuda" in err_str: |
| print(f"\n OOM DURING TRAINING: {e}") |
| print(" Cleaning up GPU memory and returning previous model...") |
| del model, trainer |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| shutil.rmtree(str(out_dir), ignore_errors=True) |
| return model_path |
| else: |
| raise |
|
|
| |
| |
| try: |
| final_loss = train_result.training_loss |
| if final_loss is not None and (math.isnan(final_loss) or math.isinf(final_loss)): |
| print(f"\n CRITICAL: Training loss is {final_loss}! Model weights are corrupted.") |
| print(f" Falling back to previous model: {model_path}") |
| del model, trainer |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| shutil.rmtree(str(out_dir), ignore_errors=True) |
| return model_path |
| print(f"\n Training complete. Final loss: {final_loss:.4f}") |
| except (AttributeError, TypeError): |
| print("\n Training complete. (Could not read final loss)") |
|
|
| |
| print("\n Merging LoRA adapters into model...") |
| try: |
| merged = model.merge_and_unload() |
| except Exception as e: |
| print(f"\n CRITICAL: LoRA merge failed: {e}") |
| print(f" Falling back to previous model: {model_path}") |
| del model, trainer |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| shutil.rmtree(str(out_dir), ignore_errors=True) |
| return model_path |
|
|
| |
| save_dir = Path(cfg.output_dir) / f"improved_cycle{cycle_num}" |
| save_dir.mkdir(parents=True, exist_ok=True) |
| print(f" Saving to {save_dir}...") |
| try: |
| merged.save_pretrained(str(save_dir), safe_serialization=True) |
| tokenizer.save_pretrained(str(save_dir)) |
| except Exception as e: |
| print(f"\n CRITICAL: Model save failed: {e}") |
| print(f" Disk may be full! Falling back to previous model: {model_path}") |
| del merged, model |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| shutil.rmtree(str(save_dir), ignore_errors=True) |
| shutil.rmtree(str(out_dir), ignore_errors=True) |
| return model_path |
|
|
| |
| st_files = list(save_dir.glob("*.safetensors")) |
| if not st_files: |
| print(" ERROR: No safetensors files saved! Disk may be full.") |
| print(f" Falling back to previous model: {model_path}") |
| shutil.rmtree(str(save_dir), ignore_errors=True) |
| return model_path |
| sz = sum(f.stat().st_size for f in st_files) / 1e9 |
| if sz < 1.0: |
| print(f" ERROR: Saved model suspiciously small ({sz:.2f} GB). Likely corrupt!") |
| print(f" Falling back to previous model: {model_path}") |
| shutil.rmtree(str(save_dir), ignore_errors=True) |
| return model_path |
| print(f" SAVED: {save_dir} ({sz:.1f} GB)") |
|
|
| shutil.rmtree(str(out_dir), ignore_errors=True) |
| del merged, model |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|
| return str(save_dir) |
|
|
|
|
| |
| |
| |
|
|
| def quick_benchmark(model_path: str) -> Dict: |
| """Run 5 questions to verify the model actually works well.""" |
| print(f"\n{'='*50}") |
| print(f"QUICK BENCHMARK — Testing actual capability") |
| print(f"{'='*50}") |
|
|
| from transformers import AutoModelForImageTextToText, AutoTokenizer |
|
|
| |
| mp = Path(model_path) |
| if mp.is_dir(): |
| if not (mp / "config.json").exists(): |
| print(f" ERROR: No config.json in {model_path} — skipping quick benchmark") |
| return {"score": 0, "total": 5, "results": [], "error": "no config.json"} |
| if not list(mp.glob("*.safetensors")) and not list(mp.glob("*.bin")): |
| print(f" ERROR: No model weights in {model_path} — skipping quick benchmark") |
| return {"score": 0, "total": 5, "results": [], "error": "no weights"} |
|
|
| |
| try: |
| model = AutoModelForImageTextToText.from_pretrained( |
| model_path, dtype=torch.bfloat16, |
| device_map="auto", trust_remote_code=True |
| ) |
| tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) |
| except (RuntimeError, Exception) as e: |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| print(f" CRITICAL: Failed to load model for quick benchmark: {e}") |
| return {"score": 0, "total": 5, "results": [], "error": str(e)} |
|
|
| tests = [ |
| {"q": "What is 47 * 23?", "expected": "1081", "type": "math"}, |
| {"q": "What is 156 + 289?", "expected": "445", "type": "math"}, |
| {"q": "If all roses are flowers and all flowers need water, do roses need water?", |
| "expected": "yes", "type": "logic"}, |
| {"q": "Write a Python function to reverse a string.", |
| "expected": "[::-1]", "type": "code"}, |
| {"q": "What comes next: 2, 4, 8, 16, ?", |
| "expected": "32", "type": "reasoning"}, |
| ] |
|
|
| results = [] |
| for test in tests: |
| try: |
| chat = f"<|im_start|>user\n{test['q']}<|im_end|>\n<|im_start|>assistant\n" |
| ids = tokenizer(chat, return_tensors="pt").to(model.device) |
| try: |
| with torch.no_grad(): |
| out = model.generate(**ids, max_new_tokens=200, do_sample=False) |
| except (torch.cuda.OutOfMemoryError, RuntimeError) as e: |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| print(f"\n [OOM] {test['type']}: {test['q']}") |
| results.append({"type": test["type"], "passed": False, "error": "OOM"}) |
| continue |
| answer = tokenizer.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True) |
| |
| if len(answer) > 5000: |
| answer = answer[:5000] |
|
|
| passed = test["expected"].lower() in answer.lower() |
| status = "PASS" if passed else "FAIL" |
| results.append({"type": test["type"], "passed": passed}) |
|
|
| print(f"\n [{status}] {test['type']}: {test['q']}") |
| print(f" Answer: {answer[:200]}") |
| print(f" Expected to contain: {test['expected']}") |
| except Exception as e: |
| print(f"\n [ERROR] {test['type']}: {test['q']} — {e}") |
| results.append({"type": test["type"], "passed": False, "error": str(e)}) |
|
|
| score = sum(1 for r in results if r.get("passed", False)) |
| print(f"\n BENCHMARK: {score}/{len(tests)} passed") |
|
|
| del model |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|
| return {"score": score, "total": len(tests), "results": results} |
|
|
|
|
| |
| |
| |
|
|
| def run_cycle(cfg: SelfImproveConfig = None): |
| """Full self-improvement cycle with norm unclogging.""" |
| if cfg is None: |
| cfg = SelfImproveConfig() |
|
|
| |
| if not cfg.model_path: |
| cfg.model_path, cycle_num = auto_detect_model_path() |
| else: |
| |
| |
| |
| |
| import re as _re |
| cycle_match = _re.search(r'improved_cycle(\d+)', cfg.model_path) |
| if cycle_match: |
| cycle_num = int(cycle_match.group(1)) + 1 |
| elif "improve" in cfg.model_path: |
| cycle_num = 2 |
| else: |
| cycle_num = 1 |
|
|
| |
| if cycle_num >= 3 and not cfg.mega_mode: |
| apply_mega_mode(cfg) |
|
|
| start = time.time() |
| mode_label = "MEGA CYCLE" if cfg.mega_mode else "NORM UNCLOGGING + LoRA" |
| print("=" * 60) |
| print(f"TD SELF-IMPROVEMENT v6 — {mode_label}") |
| print(f"Cycle: {cycle_num}") |
| print(f"Model: {cfg.model_path}") |
| print(f"LoRA rank: {cfg.lora_r}") |
| print(f"Samples: {cfg.num_train_samples}") |
| print(f"Batch: {cfg.train_batch} × {cfg.train_grad_accum} = {cfg.train_batch * cfg.train_grad_accum}") |
| print(f"Norm training: ENABLED (LR={cfg.norm_learning_rate})") |
| if cfg.mega_mode: |
| print(f"Gradient checkpointing: ENABLED") |
| print(f"Started: {time.strftime('%H:%M:%S')}") |
| print("=" * 60) |
|
|
| |
| print(f"\n{'='*40}") |
| print("STEP 1/7: ANALYZE MODEL") |
| print(f"{'='*40}") |
| model_analysis = analyze_model_weights(cfg.model_path, "TD model") |
|
|
| |
| print(f"\n{'='*40}") |
| print("STEP 2/7: HEALTH SCORING") |
| print(f"{'='*40}") |
| health_scores = compute_health_scores(model_analysis) |
| unhealthy = sum(1 for h in health_scores.values() if h["health_score"] > 0.2) |
| print(f"\n Health summary: {unhealthy}/{len(health_scores)} tensors have notable issues") |
|
|
| |
| print(f"\n{'='*40}") |
| print("STEP 3/7: REFERENCE COMPARISON") |
| print(f"{'='*40}") |
| ref_path = Path(cfg.output_dir) / "reference_weights" |
| ref_available = False |
| try: |
| if not ref_path.exists() or not list(ref_path.glob("*.safetensors")): |
| print(f" Downloading reference: {cfg.reference_model}...") |
| from huggingface_hub import snapshot_download |
| snapshot_download( |
| cfg.reference_model, local_dir=str(ref_path), |
| allow_patterns=["*.safetensors", "*.json"], |
| ignore_patterns=["*.bin", "*.pt", "*.onnx"], |
| ) |
| ref_analysis = analyze_model_weights(str(ref_path), "Qwen3-VL-8B (reference)") |
| damage_scores = compute_damage_scores(model_analysis, ref_analysis) |
| ref_available = True |
| except Exception as e: |
| print(f" WARNING: Reference comparison failed: {e}") |
| print(f" Continuing without damage scores (norm repair + LoRA still work)") |
| damage_scores = {} |
| ref_analysis = None |
| |
| gc.collect() |
|
|
| |
| print(f"\n{'='*40}") |
| print("STEP 4/7: LAYER COHERENCE") |
| print(f"{'='*40}") |
| coherence_scores = compute_coherence_scores(model_analysis) |
|
|
| |
| print(f"\n{'='*40}") |
| print("STEP 5/7: RANK LAYERS + DETECT CLOGGED NORMS") |
| print(f"{'='*40}") |
| target_layers, layer_details = rank_layers_for_improvement( |
| health_scores, damage_scores, coherence_scores, cfg |
| ) |
|
|
| |
| clogged_norms = detect_clogged_norms(health_scores, cfg.norm_kurtosis_threshold) |
| if clogged_norms: |
| print(f"\n CLOGGED NORMS DETECTED ({len(clogged_norms)}):") |
| for cn in clogged_norms: |
| stats = health_scores.get(cn, {}).get("stats", {}) |
| kurt = abs(stats.get("kurtosis", 0)) |
| print(f" {cn}: kurtosis={kurt:.1f}") |
| else: |
| print(f"\n No clogged norms above threshold {cfg.norm_kurtosis_threshold}") |
|
|
| |
| if clogged_norms and ref_available: |
| print(f"\n{'='*40}") |
| print("STEP 5c/7: SURGICAL NORM REPAIR") |
| print(f"{'='*40}") |
| try: |
| repair_clogged_norms(cfg.model_path, clogged_norms, str(ref_path), health_scores) |
| except Exception as e: |
| print(f" WARNING: Norm repair failed: {e}") |
| print(f" Continuing without repair — LoRA training can still help") |
| elif clogged_norms and not ref_available: |
| print(f"\n Skipping norm repair — reference model not available") |
|
|
| |
| print(f" Cleaning up reference weights...") |
| shutil.rmtree(str(ref_path), ignore_errors=True) |
| gc.collect() |
|
|
| |
| report_dir = Path(cfg.output_dir) |
| report_dir.mkdir(parents=True, exist_ok=True) |
| pre_report = { |
| "cycle": cycle_num, |
| "model_path": cfg.model_path, |
| "target_layers": target_layers, |
| "clogged_norms": clogged_norms, |
| "lora_r": cfg.lora_r, |
| "norm_lr": cfg.norm_learning_rate, |
| "layer_details": {str(k): v for k, v in layer_details.items()}, |
| "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), |
| } |
| try: |
| with open(report_dir / f"pre_cycle{cycle_num}_report.json", "w") as f: |
| json.dump(pre_report, f, indent=2) |
| except OSError as e: |
| print(f" WARNING: Could not save pre-report: {e}") |
|
|
| |
| print(f"\n{'='*40}") |
| print("STEP 6/7: LORA + NORM UNCLOGGING") |
| print(f"{'='*40}") |
| improved_path = improve_with_lora(cfg.model_path, target_layers, clogged_norms, cfg, cycle_num) |
|
|
| |
| print(f"\n{'='*40}") |
| print("STEP 7/7: VERIFY + BENCHMARK") |
| print(f"{'='*40}") |
|
|
| |
| pre_avg = sum(h["health_score"] for h in health_scores.values()) / max(len(health_scores), 1) |
| post_avg = pre_avg |
| post_health = {} |
| try: |
| post_analysis = analyze_model_weights(improved_path, "TD improved") |
| post_health = compute_health_scores(post_analysis) |
| post_avg = sum(h["health_score"] for h in post_health.values()) / max(len(post_health), 1) |
| except Exception as e: |
| print(f" WARNING: Post-training health analysis failed: {e}") |
| print(f" Cannot compare before/after — but training output is still valid.") |
|
|
| |
| if post_health: |
| print(f"\n Per-layer improvement:") |
| else: |
| print(f"\n (Skipping per-layer comparison — post-analysis unavailable)") |
| for layer_num in target_layers: |
| if not post_health: |
| break |
| post_layer_scores = [] |
| for name, h in post_health.items(): |
| parts = name.split(".") |
| for j, part in enumerate(parts): |
| if part == "layers" and j + 1 < len(parts) and parts[j+1].isdigit(): |
| if int(parts[j+1]) == layer_num: |
| post_layer_scores.append(h["health_score"]) |
| break |
| post_layer_avg = sum(post_layer_scores) / max(len(post_layer_scores), 1) if post_layer_scores else 0 |
| pre_h = layer_details.get(layer_num, {}).get("health", 0) |
| delta = pre_h - post_layer_avg |
| symbol = "+" if delta > 0 else "-" if delta < 0 else "=" |
| print(f" Layer {layer_num:>2}: {pre_h:.4f} → {post_layer_avg:.4f} ({symbol}{abs(delta):.4f})") |
|
|
| |
| if clogged_norms: |
| print(f"\n Clogged norm changes:") |
| for cn in clogged_norms: |
| pre_stats = health_scores.get(cn, {}).get("stats", {}) |
| pre_kurt = abs(pre_stats.get("kurtosis", 0)) |
| |
| if cn in post_health: |
| post_stats = post_health.get(cn, {}).get("stats", {}) |
| post_kurt = abs(post_stats.get("kurtosis", 0)) |
| delta = pre_kurt - post_kurt |
| symbol = "+" if delta > 0 else "-" |
| print(f" {cn}: kurtosis {pre_kurt:.1f} → {post_kurt:.1f} ({symbol}{abs(delta):.1f})") |
|
|
| |
| try: |
| benchmark = quick_benchmark(improved_path) |
| except Exception as e: |
| print(f" WARNING: Quick benchmark failed: {e}") |
| benchmark = {"score": 0, "total": 0, "error": str(e)} |
|
|
| elapsed = (time.time() - start) / 60 |
| health_improvement = (pre_avg - post_avg) / max(pre_avg, 1e-8) * 100 |
|
|
| print(f"\n{'='*60}") |
| print(f"CYCLE {cycle_num} COMPLETE — {elapsed:.1f} min") |
| print(f"{'='*60}") |
| print(f" Target layers: {target_layers}") |
| print(f" Clogged norms unfrozen: {len(clogged_norms)}") |
| print(f" Health BEFORE: {pre_avg:.6f}") |
| print(f" Health AFTER: {post_avg:.6f}") |
| print(f" Health improvement: {health_improvement:.1f}%") |
| print(f" Benchmark: {benchmark.get('score', '?')}/{benchmark.get('total', '?')}") |
| print(f" Improved model: {improved_path}") |
| print(f"{'='*60}") |
|
|
| |
| try: |
| final_report = { |
| **pre_report, |
| "pre_avg_health": pre_avg, |
| "post_avg_health": post_avg, |
| "health_improvement_pct": health_improvement, |
| "benchmark": benchmark, |
| "duration_min": elapsed, |
| "improved_path": improved_path, |
| } |
| report_save_path = Path(improved_path) / "cycle_report.json" |
| with open(report_save_path, "w") as f: |
| json.dump(final_report, f, indent=2) |
| except Exception as e: |
| print(f" WARNING: Could not save cycle report: {e}") |
|
|
| return improved_path |
|
|
|
|
| if __name__ == "__main__": |
| run_cycle() |
|
|