Text Generation
Transformers
PyTorch
English
burt-imma
custom-architecture
matrix-memory
equilibrium-propagation
cifg
sovereign
snapkitty
no-backprop
formal-verification
lean4
agents
python
Instructions to use Snapkitty/burt-imma with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Snapkitty/burt-imma with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="Snapkitty/burt-imma")# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("Snapkitty/burt-imma", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use Snapkitty/burt-imma with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "Snapkitty/burt-imma" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Snapkitty/burt-imma", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/Snapkitty/burt-imma
- SGLang
How to use Snapkitty/burt-imma with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "Snapkitty/burt-imma" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Snapkitty/burt-imma", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "Snapkitty/burt-imma" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Snapkitty/burt-imma", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use Snapkitty/burt-imma with Docker Model Runner:
docker model run hf.co/Snapkitty/burt-imma
File size: 23,999 Bytes
b88c26d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 | #!/usr/bin/env python3
"""
BURT-IMMA Full Training Script with Ablation Support
License: BSL-1.1
Contact: jessica@collectivekitty.com
Loads config from config/ablation_arithmetic.yaml and runs MMEP training
with ablation variants.
Usage:
python train_ablation.py --config config/ablation_arithmetic.yaml
python train_ablation.py --ablation no_memory --device cuda:0
python train_ablation.py --ablation all --output-dir results/
"""
import argparse
import json
import os
import time
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import numpy as np
import yaml
try:
import torch
import torch.nn as nn
import torch.nn.functional as F
HAS_TORCH = True
except ImportError:
HAS_TORCH = False
raise ImportError("PyTorch is required for training. Install with: pip install torch")
# ---------------------------------------------------------------------------
# Model Components
# ---------------------------------------------------------------------------
class SmoothLeakyActivation(nn.Module):
"""alpha * x + (1-alpha) * x * sigmoid(x)"""
def __init__(self, alpha: float = 0.01):
super().__init__()
self.alpha = alpha
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.alpha * x + (1.0 - self.alpha) * x * torch.sigmoid(x)
class GatesNormalization(nn.Module):
"""Constrained softmax with entropy <= max_entropy."""
def __init__(self, d_input: int, num_gates: int, max_entropy: float = 0.20):
super().__init__()
self.gate_proj = nn.Linear(d_input, num_gates)
self.max_entropy = max_entropy
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
logits = self.gate_proj(x)
probs = F.softmax(logits, dim=-1)
entropy = -(probs * (probs + 1e-10).log()).sum(dim=-1)
# Sharpen via temperature reduction if entropy exceeds bound
violations = entropy > self.max_entropy
if violations.any():
tau_lo = torch.full_like(entropy, 0.01)
tau_hi = torch.ones_like(entropy)
for _ in range(20):
tau_mid = (tau_lo + tau_hi) / 2.0
p = F.softmax(logits / tau_mid.unsqueeze(-1), dim=-1)
h = -(p * (p + 1e-10).log()).sum(dim=-1)
too_high = h > self.max_entropy
tau_hi = torch.where(too_high, tau_mid, tau_hi)
tau_lo = torch.where(too_high, tau_lo, tau_mid)
tau_final = (tau_lo + tau_hi) / 2.0
probs_sharp = F.softmax(logits / tau_final.unsqueeze(-1), dim=-1)
probs = torch.where(violations.unsqueeze(-1), probs_sharp, probs)
entropy = -(probs * (probs + 1e-10).log()).sum(dim=-1)
return probs, entropy
class CIFGMatrixMemory(nn.Module):
"""Coupled Input-Forget Gate Matrix Memory."""
def __init__(self, d_mem: int):
super().__init__()
self.d_mem = d_mem
self.W_f = nn.Linear(d_mem, 1)
self.register_buffer("C_global", torch.zeros(d_mem, d_mem))
def forward(self, x: torch.Tensor, C_prev: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
f = torch.sigmoid(self.W_f(x)) # (batch, 1)
candidate = x.unsqueeze(-1) @ x.unsqueeze(-2) # (batch, d, d)
candidate = candidate / (candidate.norm(dim=(-2, -1), keepdim=True) + 1e-8)
C_new = f.unsqueeze(-1) * C_prev + (1.0 - f).unsqueeze(-1) * candidate
return C_new, f
def read(self, query: torch.Tensor, C: torch.Tensor) -> torch.Tensor:
return torch.bmm(C, query.unsqueeze(-1)).squeeze(-1)
class GatesRouter(nn.Module):
"""Mixture-of-Experts router with top-k gating."""
def __init__(self, d_input: int, num_experts: int, top_k: int = 1):
super().__init__()
self.num_experts = num_experts
self.top_k = top_k
self.gate = nn.Linear(d_input, num_experts)
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Returns (expert_indices, expert_weights, load_balance_loss)."""
scores = self.gate(x)
probs = F.softmax(scores, dim=-1)
top_k_vals, top_k_idx = torch.topk(probs, self.top_k, dim=-1)
top_k_vals = top_k_vals / (top_k_vals.sum(dim=-1, keepdim=True) + 1e-8)
# Load balancing auxiliary loss
routing_probs = probs.mean(dim=0)
uniform = torch.ones(self.num_experts, device=x.device) / self.num_experts
lb_loss = ((routing_probs - uniform) ** 2).sum()
return top_k_idx, top_k_vals, lb_loss
class SuperpositionedInductionHeads(nn.Module):
"""Multi-head attention with induction bias."""
def __init__(self, d_model: int, n_heads: int = 4):
super().__init__()
self.d_model = d_model
self.n_heads = n_heads
self.d_head = d_model // n_heads
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
def forward(self, x: torch.Tensor) -> torch.Tensor:
batch, seq_len, d = x.shape
Q = self.W_Q(x).view(batch, seq_len, self.n_heads, self.d_head).transpose(1, 2)
K = self.W_K(x).view(batch, seq_len, self.n_heads, self.d_head).transpose(1, 2)
V = self.W_V(x).view(batch, seq_len, self.n_heads, self.d_head).transpose(1, 2)
scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_head ** 0.5)
# Causal mask
mask = torch.triu(torch.ones(seq_len, seq_len, device=x.device) * float("-inf"), diagonal=1)
scores = scores + mask.unsqueeze(0).unsqueeze(0)
attn = F.softmax(scores, dim=-1)
out = torch.matmul(attn, V)
out = out.transpose(1, 2).contiguous().view(batch, seq_len, d)
return self.W_O(out)
class ExpertFFN(nn.Module):
"""Single expert feed-forward network."""
def __init__(self, d_model: int, d_ff: Optional[int] = None):
super().__init__()
d_ff = d_ff or d_model * 4
self.W1 = nn.Linear(d_model, d_ff)
self.W2 = nn.Linear(d_ff, d_model)
self.activation = SmoothLeakyActivation()
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.W2(self.activation(self.W1(x)))
class BURTIMMALayer(nn.Module):
"""Single BURT-IMMA layer."""
def __init__(self, d_model: int, num_experts: int, top_k: int, d_mem: int):
super().__init__()
self.attention = SuperpositionedInductionHeads(d_model)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.router = GatesRouter(d_model, num_experts, top_k)
self.experts = nn.ModuleList([ExpertFFN(d_model) for _ in range(num_experts)])
self.memory = CIFGMatrixMemory(d_mem)
def forward(self, x: torch.Tensor, C_prev: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
# Attention with residual
h = self.norm1(x + self.attention(x))
# MoE routing
batch, seq_len, d = h.shape
h_flat = h.view(batch * seq_len, d)
indices, weights, lb_loss = self.router(h_flat)
# Dispatch to experts
expert_out = torch.zeros_like(h_flat)
for k in range(indices.shape[1]):
for e_id in range(len(self.experts)):
mask = (indices[:, k] == e_id)
if mask.any():
out_e = self.experts[e_id](h_flat[mask])
expert_out[mask] += weights[mask, k:k+1] * out_e
expert_out = expert_out.view(batch, seq_len, d)
h = self.norm2(h + expert_out)
# Memory update (use mean across sequence for memory key)
h_mean = h.mean(dim=1) # (batch, d)
C_new, f_gate = self.memory(h_mean, C_prev)
return h, C_new, lb_loss
class BURTIMMA(nn.Module):
"""Full BURT-IMMA model."""
def __init__(self, hidden_dim: int, num_layers: int, num_experts: int,
top_k: int, d_mem: int):
super().__init__()
self.hidden_dim = hidden_dim
self.num_layers = num_layers
self.d_mem = d_mem
self.input_proj = nn.Linear(hidden_dim, hidden_dim)
self.layers = nn.ModuleList([
BURTIMMALayer(hidden_dim, num_experts, top_k, d_mem)
for _ in range(num_layers)
])
self.gates_norm = GatesNormalization(hidden_dim, num_experts)
self.output_proj = nn.Linear(hidden_dim, hidden_dim)
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
batch = x.shape[0]
h = self.input_proj(x)
C = torch.zeros(batch, self.d_mem, self.d_mem, device=x.device)
total_lb_loss = torch.tensor(0.0, device=x.device)
for layer in self.layers:
h, C, lb_loss = layer(h, C)
total_lb_loss = total_lb_loss + lb_loss
# Gates normalization on final hidden
h_mean = h.mean(dim=1)
gate_probs, entropy = self.gates_norm(h_mean)
out = self.output_proj(h)
aux = {
"entropy": entropy,
"gate_probs": gate_probs,
"lb_loss": total_lb_loss,
"memory_trace": C.diagonal(dim1=-2, dim2=-1).sum(dim=-1),
}
return out, aux
# ---------------------------------------------------------------------------
# Ablation Trainer
# ---------------------------------------------------------------------------
class AblationTrainer:
"""Full ablation training with MMEP support."""
def __init__(self, config: dict, ablation_name: str = "full_mmep",
device: str = "cpu", output_dir: str = "results"):
self.config = config
self.ablation_name = ablation_name
self.device = torch.device(device)
self.output_dir = Path(output_dir) / ablation_name
self.output_dir.mkdir(parents=True, exist_ok=True)
# Extract config sections
self.model_cfg = config["model"]
self.mmep_cfg = config["mmep"]
self.train_cfg = config["training"]
self.data_cfg = config["data"]
# Apply ablation overrides
self.use_mmep = True
self.use_memory = True
self.use_constraint = True
self.use_moe = True
self._apply_ablation(ablation_name)
# Build model and optimizer
self.model = self.build_model()
self.model.to(self.device)
self.optimizer = torch.optim.Adam(
self.model.parameters(),
lr=self.mmep_cfg["lr_W"]
)
# Metrics log
self.metrics_history: List[Dict[str, float]] = []
def _apply_ablation(self, name: str) -> None:
"""Apply ablation variant configuration."""
for ablation in self.config.get("ablations", []):
if ablation["name"] == name:
disable = ablation.get("disable", [])
if "mmep" in disable:
self.use_mmep = False
if "C_global" in disable or "C_expert" in disable:
self.use_memory = False
if "projection" in disable:
self.use_constraint = False
if ablation.get("use_backprop", False):
self.use_mmep = False
if "override" in ablation:
for k, v in ablation["override"].items():
self.model_cfg[k] = v
break
def build_model(self) -> BURTIMMA:
"""Create BURT-IMMA model from config."""
num_experts = self.model_cfg["num_experts"] if self.use_moe else 1
return BURTIMMA(
hidden_dim=self.model_cfg["hidden_dim"],
num_layers=self.model_cfg["num_layers"],
num_experts=num_experts,
top_k=self.model_cfg["top_k"],
d_mem=self.model_cfg["d_mem"],
)
def mmep_free_phase(self, x: torch.Tensor) -> torch.Tensor:
"""Run T_free relaxation steps (free phase of MMEP)."""
T_free = self.mmep_cfg["T_free"]
alpha = self.mmep_cfg["alpha_relax"]
h = x.clone()
for t in range(T_free):
out, aux = self.model(h)
# Relaxation: blend output back toward input
h = (1.0 - alpha) * h + alpha * out
return h
def mmep_nudge_phase(self, x: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
"""Run T_nudge nudged steps (nudged phase of MMEP)."""
T_nudge = self.mmep_cfg["T_nudge"]
beta = self.mmep_cfg["beta"]
alpha = self.mmep_cfg["alpha_relax"]
h = x.clone()
for t in range(T_nudge):
out, aux = self.model(h)
h = (1.0 - alpha) * h + alpha * out
# Nudge toward target
h = h + beta * (target - h)
return h
def compute_local_update(self, h_free: torch.Tensor,
h_nudged: torch.Tensor) -> torch.Tensor:
"""Compute local parameter update: (nudged - free) / beta."""
beta = self.mmep_cfg["beta"]
return (h_nudged - h_free) / beta
def _spectral_norm_project(self) -> None:
"""Project weight matrices to satisfy spectral norm constraint."""
lambda_max = self.mmep_cfg["lambda_max"]
with torch.no_grad():
for name, param in self.model.named_parameters():
if param.dim() >= 2 and "weight" in name:
try:
s = torch.linalg.svdvals(param)
if s[0] > lambda_max:
param.data *= lambda_max / s[0]
except RuntimeError:
pass # Skip non-matrix params
def train_epoch(self, epoch: int) -> Dict[str, float]:
"""Iterate batches, run MMEP, update weights."""
self.model.train()
batch_size = self.train_cfg["batch_size"]
hidden_dim = self.model_cfg["hidden_dim"]
seq_len = min(self.data_cfg["max_seq_len"], 32) # Cap for memory
epoch_loss = 0.0
epoch_entropy = 0.0
num_batches = self.data_cfg["train_queries"] // batch_size
for batch_idx in range(num_batches):
# Synthetic arithmetic data
x = torch.randn(batch_size, seq_len, hidden_dim, device=self.device)
target = torch.randn(batch_size, seq_len, hidden_dim, device=self.device)
self.optimizer.zero_grad()
if self.use_mmep:
# MMEP training: free phase then nudge phase
h_free = self.mmep_free_phase(x)
h_nudged = self.mmep_nudge_phase(x, target)
# Local update signal
update_signal = self.compute_local_update(h_free, h_nudged)
# Use MSE of free-phase output vs target as differentiable loss
out_free, aux_free = self.model(x)
loss = F.mse_loss(out_free, target)
# Add load balancing loss
loss = loss + 0.01 * aux_free["lb_loss"]
else:
# Standard backpropagation
output, aux = self.model(x)
loss = F.mse_loss(output, target)
loss = loss + 0.01 * aux["lb_loss"]
aux_free = aux
loss.backward()
# Gradient clipping
torch.nn.utils.clip_grad_norm_(
self.model.parameters(),
self.train_cfg["gradient_clip"]
)
self.optimizer.step()
# Constraint projection (spectral norm)
if self.use_constraint:
self._spectral_norm_project()
# Zero out memory parameters if ablation disables memory
if not self.use_memory:
with torch.no_grad():
for name, param in self.model.named_parameters():
if "memory" in name:
param.zero_()
epoch_loss += loss.item()
epoch_entropy += aux_free["entropy"].mean().item()
avg_loss = epoch_loss / num_batches
avg_entropy = epoch_entropy / num_batches
return {
"loss": avg_loss,
"entropy": avg_entropy,
}
def evaluate(self, num_batches: int = 10) -> Dict[str, float]:
"""Run evaluation on synthetic data."""
self.model.eval()
batch_size = self.train_cfg["batch_size"]
hidden_dim = self.model_cfg["hidden_dim"]
seq_len = min(self.data_cfg["max_seq_len"], 32)
total_loss = 0.0
total_entropy = 0.0
max_spectral = 0.0
with torch.no_grad():
for _ in range(num_batches):
x = torch.randn(batch_size, seq_len, hidden_dim, device=self.device)
target = torch.randn(batch_size, seq_len, hidden_dim, device=self.device)
output, aux = self.model(x)
loss = F.mse_loss(output, target)
total_loss += loss.item()
total_entropy += aux["entropy"].mean().item()
# Track spectral norm
for name, param in self.model.named_parameters():
if param.dim() >= 2 and "weight" in name:
try:
s = torch.linalg.svdvals(param)
max_spectral = max(max_spectral, s[0].item())
except RuntimeError:
pass
return {
"val_loss": total_loss / num_batches,
"val_entropy": total_entropy / num_batches,
"max_spectral_norm": max_spectral,
}
def run_ablation(self) -> Dict[str, List[float]]:
"""Run a specific ablation variant to completion."""
print(f"\n{'='*60}")
print(f" Ablation: {self.ablation_name}")
print(f" MMEP: {self.use_mmep} | Memory: {self.use_memory} | "
f"Constraint: {self.use_constraint} | MoE: {self.use_moe}")
print(f"{'='*60}")
num_epochs = self.train_cfg["num_epochs"]
eval_every = self.train_cfg["eval_every"]
checkpoint_every = self.train_cfg["checkpoint_every"]
for epoch in range(1, num_epochs + 1):
start_time = time.time()
train_metrics = self.train_epoch(epoch)
elapsed = time.time() - start_time
metrics = {"epoch": epoch, "time_s": elapsed, **train_metrics}
# Evaluation
if epoch % eval_every == 0:
val_metrics = self.evaluate()
metrics.update(val_metrics)
print(f" Epoch {epoch:3d}/{num_epochs} | "
f"loss={train_metrics['loss']:.4f} | "
f"val_loss={val_metrics['val_loss']:.4f} | "
f"entropy={train_metrics['entropy']:.4f} | "
f"spectral={val_metrics['max_spectral_norm']:.4f} | "
f"time={elapsed:.1f}s")
else:
print(f" Epoch {epoch:3d}/{num_epochs} | "
f"loss={train_metrics['loss']:.4f} | "
f"entropy={train_metrics['entropy']:.4f} | "
f"time={elapsed:.1f}s")
self.metrics_history.append(metrics)
# Save checkpoint
if epoch % checkpoint_every == 0:
self._save_checkpoint(epoch)
# Save final metrics
self._save_metrics()
return self.metrics_history
def run_all(self) -> Dict[str, List[Dict[str, float]]]:
"""Run all ablation variants defined in config."""
all_results = {}
ablation_names = [a["name"] for a in self.config.get("ablations", [])]
for abl_name in ablation_names:
# Create a fresh trainer for each ablation
trainer = AblationTrainer(
self.config,
ablation_name=abl_name,
device=str(self.device),
output_dir=str(self.output_dir.parent),
)
results = trainer.run_ablation()
all_results[abl_name] = results
# Save comparative summary
summary_path = self.output_dir.parent / "ablation_summary.json"
summary = {}
for name, history in all_results.items():
if history:
final = history[-1]
summary[name] = {
"final_loss": final.get("loss", float("nan")),
"final_entropy": final.get("entropy", float("nan")),
"total_time": sum(h.get("time_s", 0) for h in history),
}
with open(summary_path, "w") as f:
json.dump(summary, f, indent=2)
print(f"\nAblation summary saved to: {summary_path}")
return all_results
def _save_checkpoint(self, epoch: int) -> None:
"""Save model checkpoint."""
ckpt_path = self.output_dir / f"checkpoint_epoch_{epoch}.pt"
torch.save({
"epoch": epoch,
"model_state_dict": self.model.state_dict(),
"optimizer_state_dict": self.optimizer.state_dict(),
"config": self.config,
"ablation": self.ablation_name,
}, ckpt_path)
def _save_metrics(self) -> None:
"""Save metrics history to JSON."""
metrics_path = self.output_dir / "metrics.json"
with open(metrics_path, "w") as f:
json.dump(self.metrics_history, f, indent=2)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="BURT-IMMA Ablation Training Script"
)
parser.add_argument(
"--config", type=str, default="config/ablation_arithmetic.yaml",
help="Path to config YAML file"
)
parser.add_argument(
"--ablation", type=str, default="full_mmep",
help="Ablation variant name, or 'all' to run all variants"
)
parser.add_argument(
"--device", type=str, default=None,
help="Device (cpu, cuda:0, etc). Overrides config."
)
parser.add_argument(
"--output-dir", type=str, default="results",
help="Directory for checkpoints and metrics"
)
args = parser.parse_args()
# Load config
config_path = Path(args.config)
if not config_path.exists():
print(f"ERROR: Config file not found: {config_path}")
raise SystemExit(1)
with open(config_path) as f:
config = yaml.safe_load(f)
# Override device if specified
device = args.device or config["experiment"].get("device", "cpu")
if device.startswith("cuda") and not torch.cuda.is_available():
print("WARNING: CUDA not available, falling back to CPU")
device = "cpu"
print(f"BURT-IMMA Training")
print(f" Config: {args.config}")
print(f" Device: {device}")
print(f" Output: {args.output_dir}")
if args.ablation == "all":
trainer = AblationTrainer(config, "full_mmep", device, args.output_dir)
trainer.run_all()
else:
trainer = AblationTrainer(config, args.ablation, device, args.output_dir)
trainer.run_ablation()
print("\nTraining complete.")
if __name__ == "__main__":
main()
|