File size: 8,160 Bytes
a2ffd07 | 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 | """
Training Utilities — Shared helpers for additive delta-W fine-tuning.
Instead of directly modifying model weights, we freeze all weights and
learn an additive ``delta_w`` per weight tensor. The effective weight is
``W_original + delta_w``. The loss penalises delta_w for low rank
(nuclear norm) and small magnitude (Frobenius norm).
"""
from typing import Dict, List, Optional
import torch
import torch.nn as nn
import torch.nn.utils.parametrize as parametrize
from sae.Training_Utils import freeze_model
from experiment.config.train_config import LayerSelectionConfig
# ---------------------------------------------------------------------------
# Object token ID resolution
# ---------------------------------------------------------------------------
def get_object_token_ids(tokenizer, keywords: List[str]) -> torch.Tensor:
"""Resolve a list of keywords into a unique set of token IDs.
For each keyword, we encode it and collect all resulting token IDs.
Returns a 1-D tensor of unique token IDs to suppress.
"""
token_ids = set()
for kw in keywords:
ids = tokenizer.encode(kw, add_special_tokens=False)
token_ids.update(ids)
return torch.tensor(sorted(token_ids), dtype=torch.long)
# Backward-compatible alias
get_toilet_token_ids = get_object_token_ids
# ---------------------------------------------------------------------------
# Target logit generation (clamp toilet tokens + renormalise)
# ---------------------------------------------------------------------------
def generate_clamped_target(
model: nn.Module,
inputs: dict,
object_token_ids: torch.Tensor,
) -> torch.Tensor:
"""Run a forward pass, clamp object token logits to -inf, return as target.
After softmax the clamped tokens will have 0 probability, and the
remaining distribution is automatically renormalised by softmax.
The model may already have delta parametrizations registered — that is
fine; the forward pass will use the current ``W_original + delta_w``.
"""
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits.clone()
logits[:, :, object_token_ids] = float("-inf")
return logits.detach()
# ---------------------------------------------------------------------------
# Weight delta parametrization
# ---------------------------------------------------------------------------
class WeightDelta(nn.Module):
"""Additive delta registered via ``torch.nn.utils.parametrize``.
On every forward pass the effective weight becomes ``W_original + delta_w``.
The delta is initialised to **zeros** so the model starts unchanged.
"""
def __init__(self, shape: tuple, device=None, dtype=None):
super().__init__()
# Always use float32 for the delta to avoid fp16 overflow in optimizer states
self.delta_w = nn.Parameter(torch.zeros(shape, device=device, dtype=torch.float32))
def forward(self, X: torch.Tensor) -> torch.Tensor:
return X + self.delta_w
# ---------------------------------------------------------------------------
# Parameter selection (name-only, no grad changes)
# ---------------------------------------------------------------------------
_PATTERN_MAP = {
"all": None, # all params
"text_only": ["language_model", "lm_head"],
"vision_only": ["vision_tower"],
"projector_only": ["multi_modal_projector"],
}
def get_target_param_names(
model: nn.Module,
layer_config: LayerSelectionConfig,
) -> List[str]:
"""Return the names of parameters that should be masked.
Uses the same pattern-matching logic as the old ``select_trainable_params``
but does **not** modify ``requires_grad`` on any parameter.
If ``top_k_layers > 0``, only parameters in the last K transformer layers
(plus non-layer params like lm_head) are included.
"""
import re
patterns: Optional[List[str]] = None
if layer_config.mode == "specific_layers":
patterns = layer_config.specific_layer_patterns
else:
patterns = _PATTERN_MAP.get(layer_config.mode)
names = []
for name, _param in model.named_parameters():
if patterns is None:
names.append(name)
elif any(p in name for p in patterns):
names.append(name)
# Filter to only the last K transformer layers if requested
if layer_config.top_k_layers > 0 and names:
# Discover the maximum layer index
layer_indices = set()
layer_re = re.compile(r"\.layers\.(\d+)\.")
for name in names:
m = layer_re.search(name)
if m:
layer_indices.add(int(m.group(1)))
if layer_indices:
max_layer = max(layer_indices)
min_kept = max_layer - layer_config.top_k_layers + 1
filtered = []
for name in names:
m = layer_re.search(name)
if m:
idx = int(m.group(1))
if idx >= min_kept:
filtered.append(name)
else:
# Non-layer params (lm_head, embed_tokens, etc.) — keep them
filtered.append(name)
names = filtered
return names
# ---------------------------------------------------------------------------
# Register deltas on the model
# ---------------------------------------------------------------------------
def register_weight_deltas(
model: nn.Module,
target_param_names: List[str],
) -> Dict[str, nn.Parameter]:
"""Register a ``WeightDelta`` parametrization on every target parameter.
1. Register a ``WeightDelta`` so that ``module.attr`` now returns
``W_original + delta_w`` during the forward pass.
2. Freeze the original weight — only the delta is trainable.
Returns:
deltas: ``{param_name: delta_w_parameter}``
"""
deltas: Dict[str, nn.Parameter] = {}
for name in target_param_names:
parts = name.split(".")
attr = parts[-1] # e.g. "weight" or "bias"
# Navigate to the parent module
module = model
for part in parts[:-1]:
module = getattr(module, part)
# Create & register parametrization
weight_tensor = getattr(module, attr)
delta_module = WeightDelta(
weight_tensor.shape,
device=weight_tensor.device,
dtype=weight_tensor.dtype,
)
parametrize.register_parametrization(module, attr, delta_module)
# Freeze the stored original weight — only the delta is trainable
module.parametrizations[attr].original.requires_grad_(False)
deltas[name] = delta_module.delta_w
return deltas
# ---------------------------------------------------------------------------
# Collect delta-W dict (for loss computation)
# ---------------------------------------------------------------------------
def collect_delta_w(
deltas: Dict[str, nn.Parameter],
) -> Dict[str, torch.Tensor]:
"""Return the delta_w dict for loss computation.
Since deltas are learned directly, this simply returns them as-is.
The returned tensors remain **in the computation graph** so that the
Frobenius / nuclear-norm losses can back-propagate into the deltas.
"""
return {name: dw for name, dw in deltas.items()}
# ---------------------------------------------------------------------------
# Bake deltas into weights (for saving a clean final model)
# ---------------------------------------------------------------------------
def remove_deltas(model: nn.Module, target_param_names: List[str]) -> None:
"""Remove all delta parametrizations, baking ``W + delta_w`` into each weight.
After this call every ``module.attr`` is a plain ``nn.Parameter`` again
whose value equals ``W_original + delta_w``.
"""
for name in target_param_names:
parts = name.split(".")
attr = parts[-1]
module = model
for part in parts[:-1]:
module = getattr(module, part)
parametrize.remove_parametrizations(module, attr, leave_parametrized=True)
|