kaizen-42m / lora.py
qoa's picture
Add KAIZEN inference code, benchmarks, semantic head, example memory, README, requirements
4700286 verified
Raw
History Blame Contribute Delete
9.01 kB
"""
lora.py — LoRA adapter for KaizenLM Phase 5 online learning.
Architecture: rank-4 LoRA on Q and V projections of all 8 attention layers.
Q slice: qkv_out[..., :D] ← corrected by lora_q
V slice: qkv_out[..., 2*D:] ← corrected by lora_v
Adapter params per layer: 2 × (rank×D + D×rank) = 2 × (4×512 + 512×4) = 8,192
Total (8 layers): 65,536 params
Storage per adapter: 65,536 × 4 bytes ≈ 256 KB
Usage:
adapter = LoRAAdapter(n_layers=8, d=512, rank=4, alpha=8)
with torch.no_grad():
out = model_with_lora_forward(base_model, x, adapter)
loss.backward() # gradients only on adapter params
optimizer.step()
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
class LoRALinear(nn.Module):
"""Additive low-rank correction: output += B(A(x)) * scale."""
def __init__(self, in_features: int, out_features: int,
rank: int = 4, alpha: float = 8.0):
super().__init__()
self.rank = rank
self.scale = alpha / rank
self.A = nn.Linear(in_features, rank, bias=False)
self.B = nn.Linear(rank, out_features, bias=False)
nn.init.kaiming_uniform_(self.A.weight, a=math.sqrt(5))
nn.init.zeros_(self.B.weight) # zero init → adapter starts as identity
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.B(self.A(x)) * self.scale
class LoRAAdapter(nn.Module):
"""
Per-layer LoRA corrections for KaizenLM.
Applies to Q and V slices of the fused QKV projection in each attention block.
"""
def __init__(self, n_layers: int = 8, d: int = 512,
rank: int = 4, alpha: float = 8.0):
super().__init__()
self.d = d
self.lora_q = nn.ModuleList(
[LoRALinear(d, d, rank, alpha) for _ in range(n_layers)])
self.lora_v = nn.ModuleList(
[LoRALinear(d, d, rank, alpha) for _ in range(n_layers)])
def correct_qkv(self, layer_idx: int, x: torch.Tensor,
qkv: torch.Tensor) -> torch.Tensor:
"""
Add LoRA corrections to Q and V slices.
qkv shape: [B, T, 3*D]
Uses torch.cat (not in-place) to preserve autograd graph for backward.
"""
D = self.d
q_corrected = qkv[..., :D] + self.lora_q[layer_idx](x)
k_unchanged = qkv[..., D:2*D]
v_corrected = qkv[..., 2*D:] + self.lora_v[layer_idx](x)
return torch.cat([q_corrected, k_unchanged, v_corrected], dim=-1)
def param_count(self) -> int:
return sum(p.numel() for p in self.parameters())
def size_bytes(self) -> int:
return sum(p.numel() * p.element_size() for p in self.parameters())
@classmethod
def merged(cls, adapters: list, weights: list,
n_layers: int = 8, d: int = 512,
rank: int = 4, alpha: float = 8.0) -> 'LoRAAdapter':
"""
Weighted average merge of multiple adapters.
weights need not sum to 1 (normalised internally).
"""
assert len(adapters) == len(weights) and len(adapters) > 0
total = sum(weights)
merged = cls(n_layers=n_layers, d=d, rank=rank, alpha=alpha)
# zero-init merged state
with torch.no_grad():
for p in merged.parameters():
p.zero_()
norm_w = [w / total for w in weights]
for adapter, w in zip(adapters, norm_w):
for p_m, p_a in zip(merged.parameters(), adapter.parameters()):
p_m.add_(p_a * w)
return merged
# ── KaizenLM with LoRA forward ────────────────────────────────────────────────
# Copied model definition with LoRA injection — base weights unchanged.
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
h = x.shape[-1] // 2
return torch.cat([-x[..., h:], x[..., :h]], dim=-1)
def _apply_rope(q, k, cos, sin):
return q * cos + _rotate_half(q) * sin, k * cos + _rotate_half(k) * sin
def _precompute_rope(head_dim, max_seq, theta=10000.0):
inv = 1.0 / (theta ** (torch.arange(0, head_dim, 2).float() / head_dim))
t = torch.arange(max_seq).float()
f = torch.outer(t, inv)
return torch.cat([f, f], dim=-1).cos(), torch.cat([f, f], dim=-1).sin()
class RMSNorm(nn.Module):
def __init__(self, d: int, eps: float = 1e-6):
super().__init__()
self.weight = nn.Parameter(torch.ones(d))
self.eps = eps
def forward(self, x):
n = x.float().pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt()
return (x.float() * n * self.weight).type_as(x)
class CausalSelfAttentionWithLoRA(nn.Module):
def __init__(self, d: int, n_heads: int, drop: float = 0.0):
super().__init__()
self.nh = n_heads
self.hd = d // n_heads
self.drop = drop
self.qkv = nn.Linear(d, 3 * d, bias=False)
self.out_proj = nn.Linear(d, d, bias=False)
def forward(self, x, cos, sin, lora_adapter=None, layer_idx=None):
B, T, C = x.shape
qkv = self.qkv(x)
if lora_adapter is not None:
qkv = lora_adapter.correct_qkv(layer_idx, x, qkv)
q, k, v = qkv.split(C, dim=-1)
q = q.view(B, T, self.nh, self.hd)
k = k.view(B, T, self.nh, self.hd)
v = v.view(B, T, self.nh, self.hd)
q, k = _apply_rope(q, k, cos[:T].unsqueeze(1), sin[:T].unsqueeze(1))
q = q.transpose(1, 2); k = k.transpose(1, 2); v = v.transpose(1, 2)
o = F.scaled_dot_product_attention(q, k, v, is_causal=True,
dropout_p=self.drop if self.training else 0.0)
return self.out_proj(o.transpose(1, 2).contiguous().view(B, T, C))
class FFN(nn.Module):
def __init__(self, d: int, d_ff: int):
super().__init__()
self.w1 = nn.Linear(d, d_ff, bias=False)
self.w2 = nn.Linear(d_ff, d, bias=False)
def forward(self, x):
return self.w2(F.gelu(self.w1(x), approximate='tanh'))
class BlockWithLoRA(nn.Module):
def __init__(self, d: int, n_heads: int, d_ff: int):
super().__init__()
self.ln1 = RMSNorm(d)
self.attn = CausalSelfAttentionWithLoRA(d, n_heads)
self.ln2 = RMSNorm(d)
self.ff = FFN(d, d_ff)
def forward(self, x, cos, sin, lora_adapter=None, layer_idx=None):
x = x + self.attn(self.ln1(x), cos, sin, lora_adapter, layer_idx)
return x + self.ff(self.ln2(x))
class KaizenWithLoRA(nn.Module):
"""
KaizenLM base (frozen) + optional LoRAAdapter (trainable).
Base weights are never updated. LoRA adapter is swappable at inference time.
"""
VOCAB_SIZE = 32768
D_MODEL = 512
N_HEADS = 8
N_LAYERS = 8
D_FF = 2048
BLOCK_SIZE = 1024
def __init__(self):
super().__init__()
d, h, l, ff, b = (self.D_MODEL, self.N_HEADS, self.N_LAYERS,
self.D_FF, self.BLOCK_SIZE)
self.embed = nn.Embedding(self.VOCAB_SIZE, d)
self.blocks = nn.ModuleList(
[BlockWithLoRA(d, h, ff) for _ in range(l)])
self.ln_f = RMSNorm(d)
self.head = nn.Linear(d, self.VOCAB_SIZE, bias=False)
self.head.weight = self.embed.weight
cos, sin = _precompute_rope(d // h, b * 2)
self.register_buffer('rope_cos', cos)
self.register_buffer('rope_sin', sin)
def load_base(self, ckpt_path: str):
"""Load base weights, freeze all base params."""
ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=True)
sd = ckpt.get('model_state', ckpt.get('model', ckpt))
self.load_state_dict(sd, strict=True)
for p in self.parameters():
p.requires_grad_(False)
def forward(self, idx: torch.Tensor, targets=None,
adapter: 'LoRAAdapter' = None) -> torch.Tensor:
x = self.embed(idx)
cos = self.rope_cos[:idx.shape[1]]
sin = self.rope_sin[:idx.shape[1]]
for i, blk in enumerate(self.blocks):
x = blk(x, cos, sin, lora_adapter=adapter, layer_idx=i)
logits = self.head(self.ln_f(x))
if targets is None:
return logits
loss = F.cross_entropy(
logits.view(-1, logits.size(-1)),
targets.view(-1),
ignore_index=-100,
)
return logits, loss
def embed_task(self, idx: torch.Tensor,
adapter: 'LoRAAdapter' = None) -> torch.Tensor:
"""
Returns mean-pooled last hidden state (before lm_head) as task embedding.
Shape: [D_MODEL]
"""
cos = self.rope_cos[:idx.shape[1]]
sin = self.rope_sin[:idx.shape[1]]
with torch.no_grad():
x = self.embed(idx)
for i, blk in enumerate(self.blocks):
x = blk(x, cos, sin, lora_adapter=adapter, layer_idx=i)
x = self.ln_f(x)
return x[0].mean(0) # [T, D] → [D]