File size: 8,332 Bytes
44682d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Pure-transformers steering runtime for the America AI deployment.

Consumes ``steering_bundle.pt`` (schema_version 1) produced by
``america_export.py``. No TransformerLens dependency: forward hooks are
registered on ``model.model.layers[L]`` and reproduce the harness semantics

    resid += multiplier * base_strength * typical_norm * unit_vector

at each concept's layer. This module is intentionally self-contained (no
imports from the ``america_ai`` package) so it can be copied verbatim into
the Hugging Face Space repo as a flat ``runtime.py``.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path

import torch

SCHEMA_VERSION = 1
HOOK_MODES = {"all_positions", "generation_only"}
CONCEPT_KEYS = {"unit_vector", "layer", "typical_norm", "base_strength"}


@dataclass
class Bundle:
    schema_version: int
    source_model: str
    target_model: str
    d_model: int
    concepts: dict[str, dict]
    presets: dict[str, dict[str, float]]
    hook_mode: str
    provenance: dict = field(default_factory=dict)


def save_bundle(bundle: Bundle, path: Path | str) -> None:
    validate_bundle(bundle)
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    torch.save(
        {
            "schema_version": bundle.schema_version,
            "source_model": bundle.source_model,
            "target_model": bundle.target_model,
            "d_model": bundle.d_model,
            "concepts": bundle.concepts,
            "presets": bundle.presets,
            "hook_mode": bundle.hook_mode,
            "provenance": bundle.provenance,
        },
        path,
    )


def load_bundle(path: Path | str) -> Bundle:
    raw = torch.load(Path(path), map_location="cpu", weights_only=False)
    bundle = Bundle(
        schema_version=int(raw["schema_version"]),
        source_model=raw["source_model"],
        target_model=raw["target_model"],
        d_model=int(raw["d_model"]),
        concepts=raw["concepts"],
        presets=raw["presets"],
        hook_mode=raw["hook_mode"],
        provenance=raw.get("provenance", {}),
    )
    validate_bundle(bundle)
    return bundle


def validate_bundle(bundle: Bundle) -> None:
    if bundle.schema_version != SCHEMA_VERSION:
        raise ValueError(f"unsupported schema_version: {bundle.schema_version}")
    if bundle.hook_mode not in HOOK_MODES:
        raise ValueError(f"unknown hook mode: {bundle.hook_mode}")
    if not bundle.concepts:
        raise ValueError("bundle has no concepts")
    for name, concept in bundle.concepts.items():
        missing = CONCEPT_KEYS - concept.keys()
        if missing:
            raise ValueError(f"concept {name} missing keys: {sorted(missing)}")
        vec = concept["unit_vector"]
        if not isinstance(vec, torch.Tensor) or vec.shape != (bundle.d_model,):
            raise ValueError(f"concept {name} unit_vector must be shape ({bundle.d_model},)")
        if abs(float(vec.float().norm()) - 1.0) > 1e-3:
            raise ValueError(f"concept {name} unit_vector is not unit norm")
    for preset, multipliers in bundle.presets.items():
        unknown = set(multipliers) - set(bundle.concepts)
        if unknown:
            raise ValueError(f"preset {preset} references unknown concepts: {sorted(unknown)}")


class SteeringState:
    """Holds slider multipliers and the per-layer injection tensors they imply.

    Injection tensors live on CPU in float32; ``apply`` casts to the hidden
    state's device/dtype so the same state works on CPU, CUDA, and ZeroGPU.
    """

    def __init__(self, bundle: Bundle):
        self.bundle = bundle
        self.hook_mode = bundle.hook_mode
        self.multipliers = {name: 0.0 for name in bundle.concepts}
        self._by_layer: dict[int, list[str]] = {}
        for name, concept in bundle.concepts.items():
            self._by_layer.setdefault(int(concept["layer"]), []).append(name)
        self._injections: dict[int, torch.Tensor | None] = {}
        self._rebuild()

    def layers(self) -> list[int]:
        return sorted(self._by_layer)

    def set_strengths(self, multipliers: dict[str, float]) -> None:
        unknown = set(multipliers) - set(self.multipliers)
        if unknown:
            raise KeyError(f"unknown concepts: {sorted(unknown)}")
        self.multipliers.update({name: float(value) for name, value in multipliers.items()})
        self._rebuild()

    def set_preset(self, name: str) -> None:
        self.set_strengths(self.bundle.presets[name])

    def injection(self, layer: int) -> torch.Tensor | None:
        return self._injections.get(layer)

    def _rebuild(self) -> None:
        for layer, names in self._by_layer.items():
            total = torch.zeros(self.bundle.d_model, dtype=torch.float32)
            active = False
            for name in names:
                multiplier = self.multipliers[name]
                if multiplier == 0.0:
                    continue
                concept = self.bundle.concepts[name]
                scale = multiplier * float(concept["base_strength"]) * float(concept["typical_norm"])
                total = total + scale * concept["unit_vector"].float()
                active = True
            self._injections[layer] = total if active else None

    def apply(self, hidden: torch.Tensor, layer: int) -> torch.Tensor:
        """Return steered hidden states; returns ``hidden`` unchanged if inactive."""
        vec = self._injections.get(layer)
        if vec is None:
            return hidden
        vec = vec.to(device=hidden.device, dtype=hidden.dtype)
        out = hidden.clone()
        if self.hook_mode == "all_positions":
            out = out + vec
        else:  # generation_only: prompt pass -> final position; cached decode -> that token
            out[:, -1, :] = out[:, -1, :] + vec
        return out


def attach_hooks(model, state: SteeringState) -> list:
    """Register one forward hook per steered layer on ``model.model.layers``."""
    layer_modules = model.model.layers
    handles = []
    for layer in state.layers():
        handles.append(layer_modules[layer].register_forward_hook(_make_hook(state, layer)))
    return handles


def _make_hook(state: SteeringState, layer: int):
    def hook(module, args, output):
        hidden = output[0] if isinstance(output, tuple) else output
        steered = state.apply(hidden, layer)
        if steered is hidden:
            return output
        if isinstance(output, tuple):
            return (steered,) + tuple(output[1:])
        return steered

    return hook


class SteeredGemma:
    """Bundle + plain-transformers model with steering hooks attached."""

    def __init__(
        self,
        bundle: Bundle,
        model_id: str | None = None,
        device: str | None = None,
        dtype: torch.dtype = torch.bfloat16,
        hf_token: str | None = None,
    ):
        from transformers import AutoModelForCausalLM, AutoTokenizer

        self.bundle = bundle
        self.model_id = model_id or bundle.target_model
        self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
        self.tokenizer = AutoTokenizer.from_pretrained(self.model_id, token=hf_token)
        self.model = AutoModelForCausalLM.from_pretrained(
            self.model_id,
            torch_dtype=dtype,
            attn_implementation="eager",  # recommended for gemma-2
            token=hf_token,
        ).to(self.device)
        self.model.eval()
        self.state = SteeringState(bundle)
        self._handles = attach_hooks(self.model, self.state)

    def set_strengths(self, multipliers: dict[str, float]) -> None:
        self.state.set_strengths(multipliers)

    def set_preset(self, name: str) -> None:
        self.state.set_preset(name)

    def build_input_ids(self, messages: list[dict[str, str]]) -> torch.Tensor:
        return self.tokenizer.apply_chat_template(
            messages, add_generation_prompt=True, return_tensors="pt"
        ).to(self.device)

    @torch.no_grad()
    def generate(self, messages: list[dict[str, str]], **generate_kwargs) -> str:
        input_ids = self.build_input_ids(messages)
        output = self.model.generate(input_ids=input_ids, **generate_kwargs)
        return self.tokenizer.decode(output[0, input_ids.shape[-1] :], skip_special_tokens=True)