Matoub-4B / adaptation.py
ainouche-abderahmane's picture
Upload folder using huggingface_hub
3699b0a verified
Raw History Blame Contribute Delete
5.02 kB
"""Adapt only the selected decoder projections with rank-32 LoRA."""
from __future__ import annotations
import math
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from typing import Final
import torch
from torch import Tensor, nn
from torch.nn import functional
from .backbone import Generator
PROJECTIONS: Final = frozenset(('q_proj', 'k_proj', 'v_proj', 'o_proj', 'gate_proj', 'up_proj', 'down_proj'))
RANK: Final = 32
SCALE: Final = 2.0
class AdaptedLinear(nn.Module):
def __init__(self, original: nn.Linear) -> None:
super().__init__()
self.original = original
self.original.requires_grad_(False)
self.factor_a = nn.Parameter(torch.empty(RANK, original.in_features, dtype=torch.float32, device=original.weight.device))
self.factor_b = nn.Parameter(torch.zeros(original.out_features, RANK, dtype=torch.float32, device=original.weight.device))
nn.init.kaiming_uniform_(self.factor_a, a=math.sqrt(5))
self.is_enabled = True
self.inference_scale = 1.0
def forward(self, inputs: Tensor) -> Tensor:
output: Tensor = self.original(inputs)
if not self.is_enabled:
return output
adapted = functional.linear(functional.linear(inputs.to(self.factor_a.dtype), self.factor_a), self.factor_b)
return output + (adapted * (SCALE * self.inference_scale)).to(output.dtype)
def merged_weight(self) -> Tensor:
delta = self.factor_b.float() @ self.factor_a.float() * (SCALE * self.inference_scale)
return (self.original.weight.float() + delta).to(self.original.weight.dtype)
def inject_adapter(model: Generator) -> tuple[str, ...]:
model.requires_grad_(False)
selected: list[str] = []
for name, layer in tuple(model.named_modules()):
if isinstance(layer, nn.Linear) and name.rsplit('.', 1)[-1] in PROJECTIONS:
parent_name, attribute = name.rsplit('.', 1)
model.get_submodule(parent_name).set_submodule(attribute, AdaptedLinear(layer))
selected.append(name)
expected = model.geometry.layers * len(PROJECTIONS)
trainable = sum((parameter.numel() for parameter in model.parameters() if parameter.requires_grad))
if len(selected) != expected or trainable != model.geometry.adapter_parameters:
message = f'adapter contract failed: projections={len(selected)}/{expected}'
message += f', parameters={trainable}/{model.geometry.adapter_parameters}'
raise ValueError(message)
return tuple(selected)
def adapter_state(model: Generator) -> dict[str, Tensor]:
return {name: parameter.detach().cpu().clone() for name, parameter in model.named_parameters() if parameter.requires_grad}
def restore_adapter(model: Generator, state: Mapping[str, Tensor]) -> None:
expected = {name: parameter for name, parameter in model.named_parameters() if parameter.requires_grad}
if set(state) != set(expected):
message = f'adapter keys mismatch: missing={set(expected) - set(state)}'
message += f', extra={set(state) - set(expected)}'
raise ValueError(message)
with torch.no_grad():
for name, parameter in expected.items():
tensor = state[name]
if tensor.shape != parameter.shape or not bool(torch.isfinite(tensor).all().item()):
message = f'invalid adapter tensor {name}: shape={tensor.shape}'
message += f', expected={parameter.shape}'
raise ValueError(message)
parameter.copy_(tensor)
def merge_adapter(model: Generator) -> None:
for name, layer in tuple(model.named_modules()):
if isinstance(layer, AdaptedLinear):
with torch.no_grad():
layer.original.weight.copy_(layer.merged_weight())
parent, attribute = name.rsplit('.', 1)
model.get_submodule(parent).set_submodule(attribute, layer.original)
@contextmanager
def disabled_adapter(model: Generator) -> Iterator[None]:
layers = [(layer, layer.is_enabled) for layer in model.modules() if isinstance(layer, AdaptedLinear)]
try:
for layer, _ in layers:
layer.is_enabled = False
yield
finally:
for layer, previous in layers:
layer.is_enabled = previous
@contextmanager
def scaled_adapter(model: Generator, multiplier: float) -> Iterator[None]:
if not math.isfinite(multiplier) or multiplier < 0:
message = f'adapter multiplier must be finite and nonnegative, got {multiplier!r}'
raise ValueError(message)
layers = [(layer, layer.inference_scale) for layer in model.modules() if isinstance(layer, AdaptedLinear)]
if not layers and multiplier != 1:
message = 'adapter scaling requires unmerged adapter weights'
raise ValueError(message)
try:
for layer, _ in layers:
layer.inference_scale = multiplier
yield
finally:
for layer, previous in layers:
layer.inference_scale = previous