bbkdevops's picture
download
raw
22.2 kB
"""
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
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
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
# ─────────────────────────────────────────────────────────────────────────────
@torch.no_grad()
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)
@torch.no_grad()
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.