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)