Unified-LoRA / controller.py
Simo76's picture
Add Unified LoRA Controller implementation
28c5d43
Raw
History Blame
6.18 kB
"""
Unified LoRA Controller
========================
Adaptive parameter-efficient fine-tuning controller with automatic
Single/Multi/Mirror mode switching based on synaptic stress signals.
Author: Simona Vargiu
License: Apache 2.0
"""
import torch
from typing import Dict, Optional, Tuple
class UnifiedController:
"""
Unified LoRA adaptive controller.
Monitors training stress via synaptic signal φ(t) and automatically
switches between three operational modes:
- Mode 0 (Single): Shared adapter for low conflict
- Mode 1 (Multi): Task-specific adapters for moderate stress
- Mode 2 (Mirror): Stability snapshots for catastrophic forgetting
Args:
alpha (float): Learning rate for φ(t) updates (default: 0.1)
beta (float): EMA smoothing factor for loss (default: 0.9)
theta0 (float): Single/Multi threshold (default: 0.3)
theta1 (float): Multi/Mirror threshold (default: 0.7)
lr_single (float): Learning rate for Single mode (default: 5e-5)
lr_multi (float): Learning rate for Multi mode (default: 3e-5)
lr_mirror (float): Learning rate for Mirror mode (default: 1e-5)
Example:
>>> controller = UnifiedController()
>>> for step, batch in enumerate(train_loader):
... outputs = model(**batch)
... new_lr = controller.update(outputs.loss.item())
... # Apply new_lr to optimizer
"""
def __init__(
self,
alpha: float = 0.1,
beta: float = 0.9,
theta0: float = 0.3,
theta1: float = 0.7,
lr_single: float = 5e-5,
lr_multi: float = 3e-5,
lr_mirror: float = 1e-5,
):
self.alpha = alpha
self.beta = beta
self.theta0 = theta0
self.theta1 = theta1
# Learning rates per mode
self.lr_map = {
0: lr_single,
1: lr_multi,
2: lr_mirror,
}
# State variables
self.phi = 0.5 # Synaptic stress signal
self.E_smooth = 1.0 # Smoothed loss
self.mode = 1 # Current mode (start with Multi)
self.step = 0
# History tracking
self.history = {
"phi": [],
"E_smooth": [],
"mode": [],
"step": [],
}
def update(self, loss: float) -> float:
"""
Update controller state and return new learning rate.
Args:
loss (float): Current training loss
Returns:
float: New learning rate based on current mode
"""
self.step += 1
# Update smoothed loss (EMA)
E = float(loss)
self.E_smooth = self.beta * self.E_smooth + (1 - self.beta) * E
# Compute normalized stress signal
D = self.E_smooth / (1 + self.E_smooth) # Normalize to [0,1]
# Update synaptic signal φ(t) with EMA
self.phi = (1 - self.alpha) * self.phi + self.alpha * D
# FSM: Determine mode based on φ(t)
if self.phi < self.theta0:
self.mode = 0 # Single
elif self.phi < self.theta1:
self.mode = 1 # Multi
else:
self.mode = 2 # Mirror
# Log history
self.history["phi"].append(self.phi)
self.history["E_smooth"].append(self.E_smooth)
self.history["mode"].append(self.mode)
self.history["step"].append(self.step)
# Return learning rate for current mode
return self.lr_map[self.mode]
def get_state(self) -> Dict[str, float]:
"""
Get current controller state.
Returns:
dict: Current values of phi, E_smooth, mode, step
"""
return {
"phi": self.phi,
"E_smooth": self.E_smooth,
"mode": self.mode,
"step": self.step,
}
def get_history(self) -> Dict[str, list]:
"""
Get complete training history.
Returns:
dict: History of phi, E_smooth, mode, step
"""
return self.history
def reset(self):
"""Reset controller to initial state."""
self.phi = 0.5
self.E_smooth = 1.0
self.mode = 1
self.step = 0
self.history = {
"phi": [],
"E_smooth": [],
"mode": [],
"step": [],
}
@staticmethod
def mode_name(mode: int) -> str:
"""
Get human-readable mode name.
Args:
mode (int): Mode number (0, 1, or 2)
Returns:
str: Mode name
"""
names = {0: "Single", 1: "Multi", 2: "Mirror"}
return names.get(mode, "Unknown")
def __repr__(self) -> str:
"""String representation of controller state."""
return (
f"UnifiedController(step={self.step}, phi={self.phi:.3f}, "
f"mode={self.mode} ({self.mode_name(self.mode)}), "
f"E_smooth={self.E_smooth:.3f})"
)
# Example usage
if __name__ == "__main__":
import numpy as np
print("Unified LoRA Controller - Example")
print("=" * 50)
controller = UnifiedController()
# Simulate training with stress events
print("\nSimulating training with SHOCK at step 150...")
print()
for step in range(300):
# Simulate loss
if step < 150:
loss = np.random.uniform(0.4, 0.6) # Normal training
else:
loss = np.random.uniform(2.0, 4.0) # SHOCK
# Update controller
new_lr = controller.update(loss)
# Log every 50 steps
if step % 50 == 0:
state = controller.get_state()
print(
f"[{step:3d}] phi={state['phi']:.3f} | "
f"mode={state['mode']} ({controller.mode_name(state['mode'])}) | "
f"lr={new_lr:.1e}"
)
print("\n" + "=" * 50)
print("Simulation complete!")
print(f"\nFinal state: {controller}")