Buckets:
bbkdevops/unicosys-hypergraph-bucket / tinymind-native-colab-handoff /bundle /train /spectral_trainer.py
| """ | |
| SpectralMind Trainer — Pure Mathematics Training Loop | |
| ===================================================== | |
| นวัตกรรมการ train 4 อย่าง: | |
| 1. Stiefel Retraction Hook | |
| หลัง optimizer.step() ทุกครั้ง → project U, V กลับลง Stiefel manifold | |
| ผ่าน Cayley transform ทำให้ gradient ไม่ระเบิด + ไม่ต้องใช้ gradient clipping | |
| 2. Progressive Rank Growth | |
| เริ่มที่ rank_0 (เล็กมาก) → ตรวจ spectral gap ทุก K steps | |
| ถ้า σ_{r+1}/σ_r > threshold → เพิ่ม rank 1 | |
| ผลลัพธ์: เริ่ม train เร็ว (model เล็ก) → เติบโตเองอัตโนมัติ | |
| 3. Spectral Regularization | |
| Loss += λ · Σ_layer ‖σ_layer‖₁ (L1 บน singular values) | |
| บังคับให้ singular values sparse → โมเดลค้นหา rank ต่ำสุดที่เหมาะสมเอง | |
| เทียบเท่า nuclear norm regularization บน W matrix | |
| 4. Closed-Form Output Layer Warmstart | |
| ก่อน training loop: solve W_out* = Y·Xᵀ(XXᵀ + λI)⁻¹ (ridge regression) | |
| ด้วย batch แรกของข้อมูล → initialization ดีกว่า random มาก | |
| ลด training steps ที่ต้องการ ~30-50% | |
| คณิตศาสตร์: | |
| - Stiefel: ‖U‖_F = √r ตลอด training (orthonormal constraint) | |
| - Cayley: retraction cost O(r²·n) << O(n³) ของ QR | |
| - Spectral gap: σ_k / σ_{k+1} วัดว่า rank k เพียงพอหรือไม่ | |
| - Nuclear norm: ‖W‖_* = Σ_i σ_i เทียบเท่า rank minimization (convex relaxation) | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import time | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Callable | |
| import torch | |
| import torch.nn as nn | |
| from torch.optim import AdamW | |
| from torch.optim.lr_scheduler import CosineAnnealingLR | |
| from torch.utils.data import DataLoader | |
| from ..model.spectral_compact import SpectralMindModel, StiefelLinear, LowRankFFN | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Configuration | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| class SpectralTrainConfig: | |
| # ── Training basics ─────────────────────────────────────────────────── | |
| max_steps: int = 50_000 | |
| batch_size: int = 32 | |
| grad_accum: int = 4 # effective batch = batch_size * grad_accum | |
| max_seq_len: int = 512 | |
| dtype: str = "bf16" # bf16 | fp32 | |
| # ── Learning rate ───────────────────────────────────────────────────── | |
| lr: float = 3e-4 | |
| lr_min: float = 3e-5 | |
| warmup_steps: int = 1_000 | |
| weight_decay: float = 0.1 | |
| # ── Stiefel retraction ──────────────────────────────────────────────── | |
| stiefel_tau: float = 1.0 # Cayley step size | |
| stiefel_every: int = 1 # retract ทุกกี่ steps | |
| # ── Progressive rank growth ─────────────────────────────────────────── | |
| rank_grow_every: int = 2_000 # ตรวจสอบทุกกี่ steps | |
| rank_grow_threshold: float = 0.15 # spectral gap threshold (σ_{r+1}/σ_r) | |
| rank_max_per_layer: int = 128 # rank สูงสุด | |
| # ── Spectral regularization ─────────────────────────────────────────── | |
| spectral_reg: float = 1e-4 # λ สำหรับ L1(σ) (nuclear norm proxy) | |
| # ── Output warmstart ────────────────────────────────────────────────── | |
| warmstart_batches: int = 8 # จำนวน batch สำหรับ closed-form init | |
| warmstart_ridge: float = 1e-3 # λ สำหรับ ridge regression | |
| # ── Logging & saving ────────────────────────────────────────────────── | |
| log_every: int = 100 | |
| save_every: int = 5_000 | |
| save_dir: str = "checkpoints/spectral" | |
| run_name: str = "spectral_run" | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Closed-Form Output Warmstart | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def warmstart_output_layer( | |
| model: SpectralMindModel, | |
| loader: DataLoader, | |
| n_batches: int = 8, | |
| ridge: float = 1e-3, | |
| device: torch.device | str = "cuda", | |
| ) -> None: | |
| """ | |
| Closed-form ridge regression initialization สำหรับ lm_head. | |
| หลักคณิตศาสตร์: | |
| สำหรับ linear layer Y ≈ X·Wᵀ: | |
| W* = argmin ‖XWᵀ − Y‖²_F + λ‖W‖²_F | |
| = (XᵀX + λI)⁻¹ XᵀY ← Moore-Penrose pseudoinverse with ridge | |
| ที่นี่: | |
| X = hidden states ก่อน lm_head (N, d) | |
| Y = one-hot targets (N, V) แทนด้วย target embeddings | |
| แทนที่จะ solve บน vocab (V ใหญ่มาก) ใช้ projection: | |
| ประมาณ W* จาก covariance XᵀX ∈ ℝᵈˣᵈ (tractable) | |
| """ | |
| model.eval() | |
| model.to(device) | |
| dtype = next(model.parameters()).dtype | |
| X_list: list[torch.Tensor] = [] | |
| Y_list: list[torch.Tensor] = [] | |
| for i, batch in enumerate(loader): | |
| if i >= n_batches: | |
| break | |
| input_ids = batch["input_ids"].to(device) | |
| labels = batch["labels"].to(device) | |
| # Forward pass หยุดก่อน lm_head | |
| x = model.embed(input_ids) | |
| for block in model.blocks: | |
| x, _ = block(x) | |
| x = model.norm_out(x) # (B, T, d) | |
| # เก็บเฉพาะ positions ที่มี label | |
| mask = (labels[:, 1:] != -100) # (B, T-1) | |
| x_flat = x[:, :-1, :][mask] # (N, d) | |
| lbl_flat = labels[:, 1:][mask] # (N,) | |
| if x_flat.shape[0] == 0: | |
| continue | |
| # Target = embedding ของ token ถัดไป (ใช้ BloomEmbedding) | |
| y_flat = model.embed(lbl_flat) # (N, d) ← target ใน embedding space | |
| X_list.append(x_flat.float()) | |
| Y_list.append(y_flat.float()) | |
| if not X_list: | |
| return | |
| X = torch.cat(X_list, dim=0) # (N, d) | |
| Y = torch.cat(Y_list, dim=0) # (N, d) | |
| # Ridge regression: W* = (XᵀX + λI)⁻¹ XᵀY | |
| XtX = X.T @ X # (d, d) | |
| XtY = X.T @ Y # (d, d) | |
| I = torch.eye(XtX.shape[0], device=device, dtype=torch.float32) | |
| W_star = torch.linalg.solve(XtX + ridge * I, XtY).T # (d, d) | |
| # Initialize lm_head.sigma เพื่อให้ output ≈ W_star | |
| # W = V·diag(σ)·Uᵀ → ถ้า U, V orthogonal แล้ว W_star = U·diag(σ)·Vᵀ | |
| try: | |
| U_s, S_s, Vh_s = torch.linalg.svd(W_star.to(dtype), full_matrices=False) | |
| r = model.lm_head.rank | |
| # เอาแค่ top-r singular values | |
| S_r = S_s[:r] | |
| U_r = Vh_s[:r, :].T # (d, r) | |
| V_r = U_s[:, :r] # (V_out, r) — แต่ lm_head out_f=vocab | |
| # ปรับขนาด U, V ให้ตรงกับ rank | |
| if U_r.shape[0] == model.lm_head.in_f: | |
| model.lm_head.U.data = U_r.contiguous() | |
| if V_r.shape[0] == model.lm_head.out_f: | |
| model.lm_head.V.data = V_r.contiguous() | |
| model.lm_head.sigma.data = S_r.contiguous() | |
| except Exception: | |
| pass # ถ้า SVD fail ให้ใช้ init เดิม | |
| model.train() | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Spectral Gap Check (Progressive Rank Growth) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| def check_spectral_gap(model: SpectralMindModel) -> dict[str, float]: | |
| """ | |
| คำนวณ spectral gap สำหรับทุก StiefelLinear layer. | |
| spectral_gap = σ_min / σ_max (ถ้าน้อย → rank เล็กเกินไป) | |
| คืนค่า dict: {layer_name: gap_value} | |
| """ | |
| gaps: dict[str, float] = {} | |
| for name, m in model.named_modules(): | |
| if isinstance(m, StiefelLinear): | |
| s = m.sigma.detach().abs() | |
| if s.shape[0] < 2: | |
| continue | |
| s_sorted, _ = s.sort(descending=True) | |
| gap = (s_sorted[-1] / (s_sorted[0] + 1e-8)).item() | |
| gaps[name] = gap | |
| return gaps | |
| def try_grow_ranks( | |
| model: SpectralMindModel, | |
| threshold: float = 0.15, | |
| max_rank: int = 128, | |
| ) -> int: | |
| """ | |
| ตรวจ spectral gap และเพิ่ม rank ของ LowRankFFN ที่จำเป็น | |
| คืนค่าจำนวน layers ที่เพิ่ม rank | |
| """ | |
| grew = 0 | |
| for m in model.modules(): | |
| if isinstance(m, LowRankFFN): | |
| if m.rank >= max_rank: | |
| continue | |
| # ตรวจว่า gradient ยังใหญ่อยู่ → ต้องการ rank มากขึ้น | |
| # ใช้ norm ของ gradient บน U_up เป็น proxy | |
| if m.U_up.grad is not None: | |
| grad_norm = m.U_up.grad.norm().item() | |
| param_norm = m.U_up.data.norm().item() | |
| relative_grad = grad_norm / (param_norm + 1e-8) | |
| if relative_grad > threshold: | |
| if m.grow_rank(): | |
| grew += 1 | |
| return grew | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # SpectralTrainer | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| class SpectralTrainer: | |
| """ | |
| Training loop ที่รวม: | |
| 1. Stiefel retraction หลังทุก step | |
| 2. Progressive rank growth | |
| 3. Spectral regularization ใน loss | |
| 4. Closed-form warmstart | |
| 5. Cosine LR + warmup | |
| """ | |
| def __init__( | |
| self, | |
| model: SpectralMindModel, | |
| train_loader: DataLoader, | |
| val_loader: DataLoader | None, | |
| cfg: SpectralTrainConfig, | |
| device: torch.device | str = "cuda", | |
| ): | |
| self.model = model | |
| self.train_loader = train_loader | |
| self.val_loader = val_loader | |
| self.cfg = cfg | |
| self.device = torch.device(device) | |
| self.step = 0 | |
| self.best_val_loss: float = float("inf") | |
| # dtype | |
| self.amp_dtype = torch.bfloat16 if cfg.dtype == "bf16" else torch.float32 | |
| self.use_amp = cfg.dtype == "bf16" and torch.cuda.is_available() | |
| self.model.to(self.device) | |
| self._build_optimizer() | |
| self._build_scheduler() | |
| self.scaler = torch.cuda.amp.GradScaler(enabled=self.use_amp) | |
| # Save dir | |
| self.save_dir = Path(cfg.save_dir) / cfg.run_name | |
| self.save_dir.mkdir(parents=True, exist_ok=True) | |
| def _build_optimizer(self) -> None: | |
| # แยก Stiefel params (U, V) ออกจาก normal params | |
| # Stiefel params ควรใช้ lr สูงกว่าเล็กน้อย (optimization บน manifold) | |
| stiefel_params: list[nn.Parameter] = [] | |
| normal_params: list[nn.Parameter] = [] | |
| stiefel_names = set() | |
| for name, m in self.model.named_modules(): | |
| if isinstance(m, StiefelLinear): | |
| stiefel_names.add(id(m.U)) | |
| stiefel_names.add(id(m.V)) | |
| for p in self.model.parameters(): | |
| if id(p) in stiefel_names: | |
| stiefel_params.append(p) | |
| else: | |
| normal_params.append(p) | |
| self.optimizer = AdamW([ | |
| {"params": normal_params, "lr": self.cfg.lr, "weight_decay": self.cfg.weight_decay}, | |
| {"params": stiefel_params, "lr": self.cfg.lr * 1.5, "weight_decay": 0.0}, | |
| ], betas=(0.9, 0.95), eps=1e-8) | |
| def _build_scheduler(self) -> None: | |
| self.scheduler = CosineAnnealingLR( | |
| self.optimizer, | |
| T_max=self.cfg.max_steps - self.cfg.warmup_steps, | |
| eta_min=self.cfg.lr_min, | |
| ) | |
| def _warmup_lr(self) -> None: | |
| """Linear warmup สำหรับ step แรก""" | |
| if self.step < self.cfg.warmup_steps: | |
| scale = (self.step + 1) / self.cfg.warmup_steps | |
| for pg in self.optimizer.param_groups: | |
| pg["lr"] = pg["lr"] * scale | |
| def _spectral_reg_loss(self) -> torch.Tensor: | |
| """ | |
| L1 regularization บน singular values ของทุก StiefelLinear: | |
| L_reg = λ · Σ_layer ‖σ_layer‖₁ | |
| เทียบเท่า nuclear norm regularization บน weight matrices, | |
| ซึ่งเป็น convex relaxation ของ rank minimization. | |
| บังคับให้โมเดลใช้ rank น้อยที่สุดที่จำเป็น | |
| """ | |
| reg = torch.tensor(0.0, device=self.device) | |
| for m in self.model.modules(): | |
| if isinstance(m, StiefelLinear): | |
| reg = reg + m.sigma.abs().sum() | |
| return self.cfg.spectral_reg * reg | |
| def _retract_stiefel(self) -> None: | |
| """Project U, V กลับลง Stiefel manifold ด้วย Cayley transform""" | |
| for m in self.model.modules(): | |
| if isinstance(m, StiefelLinear): | |
| m.retract(tau=self.cfg.stiefel_tau) | |
| def _validate(self) -> float: | |
| if self.val_loader is None: | |
| return float("nan") | |
| self.model.eval() | |
| total_loss, n = 0.0, 0 | |
| for batch in self.val_loader: | |
| input_ids = batch["input_ids"].to(self.device) | |
| labels = batch["labels"].to(self.device) | |
| with torch.autocast(device_type="cuda", dtype=self.amp_dtype, enabled=self.use_amp): | |
| out = self.model(input_ids, labels=labels) | |
| total_loss += out["loss"].item() | |
| n += 1 | |
| if n >= 50: # จำกัด validation steps | |
| break | |
| self.model.train() | |
| return total_loss / max(n, 1) | |
| def _log(self, loss: float, reg: float, lr: float, elapsed: float) -> None: | |
| print( | |
| f"step {self.step:6d} | " | |
| f"loss {loss:.4f} | " | |
| f"reg {reg:.5f} | " | |
| f"lr {lr:.2e} | " | |
| f"{elapsed:.1f}s/100step" | |
| ) | |
| def _save(self, tag: str = "") -> None: | |
| path = self.save_dir / f"ckpt_{tag or self.step}.pt" | |
| torch.save({ | |
| "step": self.step, | |
| "model": self.model.state_dict(), | |
| "optimizer": self.optimizer.state_dict(), | |
| "best_val": self.best_val_loss, | |
| }, path) | |
| # ── Main Training Loop ──────────────────────────────────────────────── | |
| def train( | |
| self, | |
| warmstart: bool = True, | |
| on_step_end: Callable[[int, float], None] | None = None, | |
| ) -> None: | |
| # 1. Closed-form warmstart | |
| if warmstart and self.cfg.warmstart_batches > 0: | |
| print("Running closed-form output layer warmstart ...") | |
| warmstart_output_layer( | |
| self.model, | |
| self.train_loader, | |
| n_batches=self.cfg.warmstart_batches, | |
| ridge=self.cfg.warmstart_ridge, | |
| device=self.device, | |
| ) | |
| print("Warmstart complete.") | |
| self.model.train() | |
| loader_iter = iter(self.train_loader) | |
| accum_loss = 0.0 | |
| accum_reg = 0.0 | |
| t0 = time.time() | |
| for step_idx in range(self.cfg.max_steps): | |
| self.step = step_idx + 1 | |
| # ── Gradient accumulation loop ──────────────────────────────── | |
| self.optimizer.zero_grad(set_to_none=True) | |
| for micro_step in range(self.cfg.grad_accum): | |
| try: | |
| batch = next(loader_iter) | |
| except StopIteration: | |
| loader_iter = iter(self.train_loader) | |
| batch = next(loader_iter) | |
| input_ids = batch["input_ids"].to(self.device) | |
| labels = batch["labels"].to(self.device) | |
| with torch.autocast( | |
| device_type="cuda", | |
| dtype=self.amp_dtype, | |
| enabled=self.use_amp, | |
| ): | |
| out = self.model(input_ids, labels=labels) | |
| task_loss = out["loss"] / self.cfg.grad_accum | |
| # Spectral regularization (nuclear norm proxy) | |
| reg_loss = self._spectral_reg_loss() / self.cfg.grad_accum | |
| total = task_loss + reg_loss | |
| self.scaler.scale(total).backward() | |
| accum_loss += task_loss.item() | |
| accum_reg += reg_loss.item() | |
| # ── Optimizer step ──────────────────────────────────────────── | |
| self.scaler.unscale_(self.optimizer) | |
| # ไม่ใช้ gradient clipping (Stiefel retraction จัดการ gradient scale แล้ว) | |
| # แต่ clip normal params เพื่อความปลอดภัย | |
| nn.utils.clip_grad_norm_( | |
| [p for pg in self.optimizer.param_groups[0:1] for p in pg["params"]], | |
| max_norm=1.0, | |
| ) | |
| self.scaler.step(self.optimizer) | |
| self.scaler.update() | |
| # ── Stiefel retraction (ทุก step) ───────────────────────────── | |
| if self.step % self.cfg.stiefel_every == 0: | |
| self._retract_stiefel() | |
| # ── LR schedule ─────────────────────────────────────────────── | |
| if self.step <= self.cfg.warmup_steps: | |
| self._warmup_lr() | |
| else: | |
| self.scheduler.step() | |
| # ── Progressive rank growth ─────────────────────────────────── | |
| if self.step % self.cfg.rank_grow_every == 0: | |
| grew = try_grow_ranks( | |
| self.model, | |
| threshold=self.cfg.rank_grow_threshold, | |
| max_rank=self.cfg.rank_max_per_layer, | |
| ) | |
| if grew > 0: | |
| print(f" [rank growth] step {self.step}: grew {grew} layers") | |
| # ── Logging ─────────────────────────────────────────────────── | |
| if self.step % self.cfg.log_every == 0: | |
| elapsed = time.time() - t0 | |
| lr_now = self.optimizer.param_groups[0]["lr"] | |
| self._log( | |
| accum_loss / self.cfg.log_every, | |
| accum_reg / self.cfg.log_every, | |
| lr_now, | |
| elapsed, | |
| ) | |
| accum_loss = 0.0 | |
| accum_reg = 0.0 | |
| t0 = time.time() | |
| # ── Validation & save ───────────────────────────────────────── | |
| if self.step % self.cfg.save_every == 0: | |
| val_loss = self._validate() | |
| print(f" [val] step {self.step}: val_loss={val_loss:.4f}") | |
| if val_loss < self.best_val_loss: | |
| self.best_val_loss = val_loss | |
| self._save("best") | |
| self._save() | |
| if on_step_end is not None: | |
| on_step_end(self.step, accum_loss) | |
| self._save("final") | |
| print("Training complete.") | |
Xet Storage Details
- Size:
- 22.2 kB
- Xet hash:
- 76b2268ef1b9cadbf8e4569a5045a3de1728107680b96bdb3bb5ae2cc826d2bf
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.