psikosen's picture
Update to v5: Prefix Sliding KV-cache, SMELT scaling, CMA checkpoint, sPTC tool caller
2976bd9 verified
Raw History Blame Contribute Delete
4.76 kB
"""
Normalized Low-Rank Adaptation (NoRA & NoRA-Init).
Reference: SphereLab, Microsoft Research, CUHK (arXiv:2608.31036, Aug 31, 2026).
Normalizes the down-projection matrix A along the rank dimension:
delta_W = alpha * B * Norm(A)
Provides improved convergence, zero parameter overhead, and exact weight mergeability.
"""
from __future__ import annotations
import math
from typing import List, Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
class NoRALinear(nn.Module):
"""
Linear layer with Normalized Low-Rank Adaptation (NoRA).
Applies rank-dimensional normalization to down-projection A:
y = W_0 x + (alpha / r) * B * Norm_r(A) * x
"""
def __init__(
self,
base_linear: nn.Linear,
rank: int = 16,
alpha: float = 32.0,
dropout: float = 0.0,
nora_mode: str = "dynamic", # "dynamic" (re-normalizes per step) or "init" (NoRA-init)
):
super().__init__()
self.in_features = base_linear.in_features
self.out_features = base_linear.out_features
self.rank = rank
self.alpha = alpha
self.scaling = alpha / rank
self.nora_mode = nora_mode
self.merged = False
# Base frozen weights
self.weight = base_linear.weight
self.bias = base_linear.bias
self.weight.requires_grad = False
if self.bias is not None:
self.bias.requires_grad = False
# NoRA adapter weights: A (r, in_features), B (out_features, r)
self.nora_a = nn.Parameter(torch.empty(rank, self.in_features))
self.nora_b = nn.Parameter(torch.zeros(self.out_features, rank))
self.dropout = nn.Dropout(p=dropout) if dropout > 0.0 else nn.Identity()
self.reset_parameters()
def reset_parameters(self):
# Kaiming uniform for down-projection A
nn.init.kaiming_uniform_(self.nora_a, a=math.sqrt(5))
# Zero-initialize B so adapter starts as exact identity
nn.init.zeros_(self.nora_b)
# Apply NoRA-init: normalize rows along the rank dimension
with torch.no_grad():
self.nora_a.copy_(self._normalize_a(self.nora_a))
def _normalize_a(self, a_matrix: torch.Tensor) -> torch.Tensor:
"""Normalizes projection vectors along the rank dimension (dim 0)."""
norm = torch.norm(a_matrix, p=2, dim=-1, keepdim=True) + 1e-8
# Scale by sqrt(in_features) to preserve expected activation variance
return (a_matrix / norm) * math.sqrt(self.in_features)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.merged:
return F.linear(x, self.weight, self.bias)
base_out = F.linear(x, self.weight, self.bias)
# Normalize A dynamically if in dynamic mode
if self.nora_mode == "dynamic" and self.training:
a_effective = self._normalize_a(self.nora_a)
else:
a_effective = self.nora_a
# Low-rank forward: (B, S, D_in) -> (B, S, r) -> (B, S, D_out)
dropped_x = self.dropout(x)
latent = F.linear(dropped_x, a_effective)
adapter_out = F.linear(latent, self.nora_b) * self.scaling
return base_out + adapter_out
def merge_weights(self):
"""Exact merge of adapter weights into base weights."""
if self.merged:
return
with torch.no_grad():
a_effective = self._normalize_a(self.nora_a) if self.nora_mode == "dynamic" else self.nora_a
delta_w = (self.nora_b @ a_effective) * self.scaling
self.weight.add_(delta_w)
self.merged = True
def unmerge_weights(self):
"""Reverses the weight merge."""
if not self.merged:
return
with torch.no_grad():
a_effective = self._normalize_a(self.nora_a) if self.nora_mode == "dynamic" else self.nora_a
delta_w = (self.nora_b @ a_effective) * self.scaling
self.weight.sub_(delta_w)
self.merged = False
def apply_nora(
model: nn.Module,
rank: int = 16,
alpha: float = 32.0,
target_names: List[str] = ["q_proj", "v_proj", "k_proj"],
nora_mode: str = "dynamic",
) -> List[str]:
"""Applies NoRA adapters to designated linear layers in the model."""
adapted_layers = []
for name, module in model.named_modules():
for child_name, child in module.named_children():
if isinstance(child, nn.Linear) and any(t in child_name for t in target_names):
nora_layer = NoRALinear(child, rank=rank, alpha=alpha, nora_mode=nora_mode)
setattr(module, child_name, nora_layer)
adapted_layers.append(f"{name}.{child_name}")
return adapted_layers