ToiTenBao's picture
Upload hallucination folder
a2ffd07 verified
Raw
History Blame Contribute Delete
8.16 kB
"""
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)