open-interpretability 0.0.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- openinterp/wordnet.py +362 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""The experts forward that openinterp runs mixture-of-experts models with (docs/moe.md).
|
|
2
|
+
|
|
3
|
+
transformers 5 runs the experts of about 60 MoE architectures through
|
|
4
|
+
``ExpertsInterface``: an experts class decorated with ``use_experts_implementation`` looks
|
|
5
|
+
its forward up by name. :func:`experts_forward` is registered there as ``openinterp``,
|
|
6
|
+
and :func:`use_openinterp_experts` selects it when a model loads. It is the ``eager``
|
|
7
|
+
forward written out once for every layout the decorator describes, with two additions:
|
|
8
|
+
|
|
9
|
+
* Each expert's hidden activation passes through the hook of :func:`expert_hook`
|
|
10
|
+
before the down projection. That is where an expert neuron is read and edited.
|
|
11
|
+
* In a streamed model (``--memory stream``) the experts' tensors stay on disk, and the
|
|
12
|
+
forward reads only the routed experts' slices of them (:func:`expert_weight`). One
|
|
13
|
+
expert of Mistral Large 4 is about 113 MB in FP8, and all 128 of a layer 14.5 GB.
|
|
14
|
+
|
|
15
|
+
``scripts/moe_experts_check.py`` and ``tests/test_moe.py`` compare it with ``eager``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import threading
|
|
21
|
+
from collections.abc import Callable, Iterator
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
from torch import nn
|
|
26
|
+
|
|
27
|
+
EXPERTS_IMPLEMENTATION = "openinterp"
|
|
28
|
+
|
|
29
|
+
# fn(act [tokens, d_expert], expert index, token indices, routing weights) -> act
|
|
30
|
+
ExpertHook = Callable[[torch.Tensor, int, torch.Tensor, torch.Tensor], torch.Tensor]
|
|
31
|
+
|
|
32
|
+
_hooks = threading.local() # per thread, as the layer-major review needs
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _linear(x: torch.Tensor, weight: torch.Tensor, bias, transposed: bool) -> torch.Tensor:
|
|
36
|
+
weight = weight.to(x.device, x.dtype) # a streamed slice is read to the CPU
|
|
37
|
+
y = x @ weight if transposed else x @ weight.T
|
|
38
|
+
return y if bias is None else y + bias.to(x.device, x.dtype)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def experts_forward(self, hidden, top_k_index, top_k_weights) -> torch.Tensor:
|
|
42
|
+
"""The routed experts' summed, weighted outputs ``[tokens, d_model]``.
|
|
43
|
+
|
|
44
|
+
``top_k_index`` and ``top_k_weights`` are each token's chosen experts and their
|
|
45
|
+
routing weights ``[tokens, k]``. An index of ``num_experts`` or more is an
|
|
46
|
+
expert-parallel placeholder, and is skipped.
|
|
47
|
+
"""
|
|
48
|
+
out = torch.zeros_like(hidden)
|
|
49
|
+
hook = getattr(_hooks, "by_module", {}).get(id(self))
|
|
50
|
+
up = "gate_up_proj" if self.has_gate else "up_proj"
|
|
51
|
+
for e in torch.unique(top_k_index).tolist():
|
|
52
|
+
if e >= self.num_experts:
|
|
53
|
+
continue
|
|
54
|
+
tokens, slot = torch.where(top_k_index == e)
|
|
55
|
+
pre = _linear(
|
|
56
|
+
hidden[tokens], expert_weight(self, up, e), _bias(self, up, e), self.is_transposed
|
|
57
|
+
)
|
|
58
|
+
act = self._apply_gate(pre) if self.has_gate else self.act_fn(pre)
|
|
59
|
+
weights = top_k_weights[tokens, slot]
|
|
60
|
+
if hook is not None:
|
|
61
|
+
act = hook(act, e, tokens, weights)
|
|
62
|
+
down = expert_weight(self, "down_proj", e)
|
|
63
|
+
y = _linear(act, down, _bias(self, "down_proj", e), self.is_transposed)
|
|
64
|
+
if self.has_post_expert_norm:
|
|
65
|
+
y = self.post_expert_norm(y)
|
|
66
|
+
out.index_add_(0, tokens, (y * weights[:, None]).to(out.dtype))
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _bias(experts: nn.Module, name: str, e: int) -> torch.Tensor | None:
|
|
71
|
+
return expert_weight(experts, f"{name}_bias", e) if experts.has_bias else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def expert_weight(experts: nn.Module, name: str, e: int) -> torch.Tensor:
|
|
75
|
+
"""Expert ``e``'s slice of the stacked tensor ``name``, read from disk if streamed."""
|
|
76
|
+
param = getattr(experts, name)
|
|
77
|
+
if param.device.type != "meta":
|
|
78
|
+
return param[e]
|
|
79
|
+
weights = experts._hf_hook.weights_map # streamed: the tensor stays on disk
|
|
80
|
+
return weights.dataset.slice(weights.prefix + name, e).to(param.dtype)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@contextmanager
|
|
84
|
+
def expert_hook(experts: nn.Module, fn: ExpertHook) -> Iterator[None]:
|
|
85
|
+
"""Pass every expert's hidden activation in ``experts`` through ``fn``, in this thread."""
|
|
86
|
+
by_module = getattr(_hooks, "by_module", None)
|
|
87
|
+
if by_module is None:
|
|
88
|
+
by_module = _hooks.by_module = {}
|
|
89
|
+
previous = by_module.get(id(experts))
|
|
90
|
+
by_module[id(experts)] = fn
|
|
91
|
+
try:
|
|
92
|
+
yield
|
|
93
|
+
finally:
|
|
94
|
+
if previous is None:
|
|
95
|
+
del by_module[id(experts)]
|
|
96
|
+
else:
|
|
97
|
+
by_module[id(experts)] = previous
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def experts_modules(model: nn.Module) -> list[nn.Module]:
|
|
101
|
+
"""The experts modules that ``use_experts_implementation`` decorated, in model order."""
|
|
102
|
+
return [m for m in model.modules() if hasattr(m, "has_gate") and hasattr(m, "down_proj")]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def use_openinterp_experts(model: nn.Module) -> bool:
|
|
106
|
+
"""Run ``model``'s experts with :func:`experts_forward`; False if it has none.
|
|
107
|
+
|
|
108
|
+
In a streamed model each experts module's tensors then stay on disk between passes.
|
|
109
|
+
Each pass reads only the routed experts' slices of them.
|
|
110
|
+
"""
|
|
111
|
+
experts = experts_modules(model)
|
|
112
|
+
if not experts:
|
|
113
|
+
return False
|
|
114
|
+
_register()
|
|
115
|
+
model.set_experts_implementation(EXPERTS_IMPLEMENTATION)
|
|
116
|
+
for module in experts:
|
|
117
|
+
hook = getattr(module, "_hf_hook", None)
|
|
118
|
+
if hook is not None and hook.offload:
|
|
119
|
+
hook.offload = False # the forward reads slices: don't load the whole stack
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def experts_implementation(model: nn.Module) -> str | None:
|
|
124
|
+
"""The experts forward ``model`` runs with, or ``None`` if it has no experts."""
|
|
125
|
+
if not experts_modules(model):
|
|
126
|
+
return None
|
|
127
|
+
return getattr(model.config, "_experts_implementation", None)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _register() -> None:
|
|
131
|
+
from transformers.integrations.moe import ALL_EXPERTS_FUNCTIONS, ExpertsInterface
|
|
132
|
+
|
|
133
|
+
if EXPERTS_IMPLEMENTATION not in ALL_EXPERTS_FUNCTIONS:
|
|
134
|
+
ExpertsInterface.register(EXPERTS_IMPLEMENTATION, experts_forward)
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Activation patching on single neuron poles.
|
|
2
|
+
|
|
3
|
+
Two directions are supported:
|
|
4
|
+
|
|
5
|
+
* **Denoising** (clean -> corrupted): run the corrupted prompt but inject the
|
|
6
|
+
neuron's activation cached from the clean run. The normalised recovery of the
|
|
7
|
+
clean behaviour measures *sufficiency*.
|
|
8
|
+
* **Noising** (corrupted -> clean): run the clean prompt but inject the activation
|
|
9
|
+
from the corrupted run (a resample ablation). The normalised movement towards
|
|
10
|
+
the corrupted behaviour measures *necessity*.
|
|
11
|
+
|
|
12
|
+
Only the requested pole is swapped. The opposite pole keeps its own value. With
|
|
13
|
+
``freeze_downstream=True``, the outputs of every later MLP are pinned to their values
|
|
14
|
+
from the unpatched run. This blocks MLP-mediated self-repair (the "hydra effect"), so
|
|
15
|
+
the measured effect is closer to the neuron's direct effect.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
from collections.abc import Callable, Sequence
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Literal, Protocol
|
|
24
|
+
|
|
25
|
+
import torch
|
|
26
|
+
|
|
27
|
+
from openinterp.core.exceptions import ShapeMismatchError
|
|
28
|
+
from openinterp.core.schemas import Pole
|
|
29
|
+
from openinterp.harness.hooks import (
|
|
30
|
+
Inputs,
|
|
31
|
+
Intervention,
|
|
32
|
+
NeuronHookRunner,
|
|
33
|
+
_recorders,
|
|
34
|
+
replace_pole,
|
|
35
|
+
)
|
|
36
|
+
from openinterp.harness.memory import stream_rows
|
|
37
|
+
|
|
38
|
+
Metric = Callable[[torch.Tensor], torch.Tensor]
|
|
39
|
+
# (patched logits [1, seq, vocab], unpatched target logits [1, seq, vocab]) -> effect
|
|
40
|
+
Effect = Callable[[torch.Tensor, torch.Tensor], float]
|
|
41
|
+
Positions = Literal["all", "last"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NeuronRef(Protocol):
|
|
45
|
+
"""Anything that names one neuron pole."""
|
|
46
|
+
|
|
47
|
+
layer: int
|
|
48
|
+
neuron_idx: int
|
|
49
|
+
pole: Pole
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def token_logit_metric(token_id: int, position: int = -1) -> Metric:
|
|
53
|
+
"""Logit of ``token_id`` at ``position`` (averaged over the batch)."""
|
|
54
|
+
return lambda logits: logits[:, position, token_id].mean()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def logit_diff_metric(correct_id: int, incorrect_id: int, position: int = -1) -> Metric:
|
|
58
|
+
"""Logit of ``correct_id`` minus logit of ``incorrect_id`` at ``position`` (batch mean)."""
|
|
59
|
+
return lambda logits: (
|
|
60
|
+
logits[:, position, correct_id] - logits[:, position, incorrect_id]
|
|
61
|
+
).mean()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def logit_direction_metric(weights: torch.Tensor, position: int = -1) -> Metric:
|
|
65
|
+
"""Projection of the logits at ``position`` onto a vocabulary direction (batch mean).
|
|
66
|
+
|
|
67
|
+
``weights`` is centred first, so the shared logit offset (which softmax ignores)
|
|
68
|
+
cannot move the metric. With weights 1/k on k tokens this is the mean logit of
|
|
69
|
+
those tokens minus the mean logit over the vocabulary.
|
|
70
|
+
"""
|
|
71
|
+
w = weights.float()
|
|
72
|
+
w = w - w.mean()
|
|
73
|
+
return lambda logits: (logits[:, position].float() @ w.to(logits.device)).mean()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def metric_change(metric: Metric) -> Effect:
|
|
77
|
+
"""Signed change in ``metric`` caused by the patch."""
|
|
78
|
+
return lambda patched, target: float(metric(patched)) - float(metric(target))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def next_token_kl(position: int = -1) -> Effect:
|
|
82
|
+
"""KL(unpatched ‖ patched) between next-token distributions at ``position``."""
|
|
83
|
+
|
|
84
|
+
def effect(patched: torch.Tensor, target: torch.Tensor) -> float:
|
|
85
|
+
p = torch.log_softmax(target[:, position].float(), dim=-1)
|
|
86
|
+
q = torch.log_softmax(patched[:, position].float(), dim=-1)
|
|
87
|
+
return max(0.0, float((p.exp() * (p - q)).sum(-1).mean()))
|
|
88
|
+
|
|
89
|
+
return effect
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _constant(value: torch.Tensor) -> Intervention:
|
|
93
|
+
return lambda _out: value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _normalised(patched: float, baseline: float, reference: float, eps: float = 1e-6) -> float:
|
|
97
|
+
denom = reference - baseline
|
|
98
|
+
return math.nan if abs(denom) < eps else (patched - baseline) / denom
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class PatchResult:
|
|
103
|
+
"""Outcome of patching a neuron pole from a source run into a target run.
|
|
104
|
+
|
|
105
|
+
``baseline`` is the target run's metric, ``reference`` the source run's, and
|
|
106
|
+
``patched`` the target run with the source activation injected.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
baseline_metric: float
|
|
110
|
+
reference_metric: float
|
|
111
|
+
patched_metric: float
|
|
112
|
+
logit_delta: torch.Tensor # patched - baseline logits at the final position, [vocab]
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def metric_delta(self) -> float:
|
|
116
|
+
"""The patched metric minus the baseline metric."""
|
|
117
|
+
return self.patched_metric - self.baseline_metric
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def recovery(self) -> float:
|
|
121
|
+
"""Fraction of the source-vs-target metric gap closed by the patch (NaN if no gap)."""
|
|
122
|
+
return _normalised(self.patched_metric, self.baseline_metric, self.reference_metric)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class AblationResult:
|
|
127
|
+
"""The patch results of one ablation over several source prompts."""
|
|
128
|
+
|
|
129
|
+
per_source: list[PatchResult] = field(default_factory=list)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def mean_metric_delta(self) -> float:
|
|
133
|
+
"""The mean metric delta over the source prompts."""
|
|
134
|
+
return sum(r.metric_delta for r in self.per_source) / len(self.per_source)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def mean_recovery(self) -> float:
|
|
138
|
+
"""The mean recovery over the prompts where it is defined; NaN when none is."""
|
|
139
|
+
finite = [r.recovery for r in self.per_source if math.isfinite(r.recovery)]
|
|
140
|
+
return sum(finite) / len(finite) if finite else math.nan
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ActivationPatcher:
|
|
144
|
+
"""Patch one neuron pole's activation between two runs (see the module docstring)."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, runner: NeuronHookRunner) -> None:
|
|
147
|
+
self.runner = runner
|
|
148
|
+
|
|
149
|
+
def patch(
|
|
150
|
+
self,
|
|
151
|
+
source: Inputs,
|
|
152
|
+
target: Inputs,
|
|
153
|
+
neuron: NeuronRef,
|
|
154
|
+
*,
|
|
155
|
+
metric: Metric | None = None,
|
|
156
|
+
positions: Positions = "all",
|
|
157
|
+
freeze_downstream: bool = False,
|
|
158
|
+
) -> PatchResult:
|
|
159
|
+
"""Inject ``neuron``'s pole activation from the ``source`` run into the ``target`` run.
|
|
160
|
+
|
|
161
|
+
If ``metric`` is omitted it defaults to the logit of the source run's
|
|
162
|
+
top-1 next token at the final position.
|
|
163
|
+
"""
|
|
164
|
+
runner = self.runner
|
|
165
|
+
layer, idx, pole = neuron.layer, neuron.neuron_idx, neuron.pole
|
|
166
|
+
downstream = list(range(layer + 1, runner.n_layers)) if freeze_downstream else []
|
|
167
|
+
|
|
168
|
+
source_logits, source_cache = runner.run_with_cache(source, [layer])
|
|
169
|
+
target_enc = runner.encode(target)
|
|
170
|
+
target_logits, target_cache, downstream_out = self._target_pass(
|
|
171
|
+
target_enc, downstream, [layer]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
src_act, tgt_act = source_cache[layer], target_cache[layer]
|
|
175
|
+
if positions == "all" and src_act.shape[:2] != tgt_act.shape[:2]:
|
|
176
|
+
raise ShapeMismatchError(
|
|
177
|
+
f"source {tuple(src_act.shape[:2])} and target {tuple(tgt_act.shape[:2])} "
|
|
178
|
+
"differ in batch/sequence length; use positions='last' or align the prompts."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def inject(act: torch.Tensor) -> torch.Tensor:
|
|
182
|
+
act = act.clone()
|
|
183
|
+
if positions == "all":
|
|
184
|
+
act[..., idx] = replace_pole(act[..., idx], src_act[..., idx], pole)
|
|
185
|
+
else:
|
|
186
|
+
act[:, -1, idx] = replace_pole(act[:, -1, idx], src_act[:, -1, idx], pole)
|
|
187
|
+
return act
|
|
188
|
+
|
|
189
|
+
frozen: dict[int, Intervention] = {
|
|
190
|
+
i: (lambda _out, cached=cached: cached) for i, cached in downstream_out.items()
|
|
191
|
+
}
|
|
192
|
+
patched_logits = runner.run_with_interventions(target_enc, {layer: inject}, frozen)
|
|
193
|
+
|
|
194
|
+
if metric is None:
|
|
195
|
+
metric = token_logit_metric(int(source_logits[0, -1].argmax()))
|
|
196
|
+
return PatchResult(
|
|
197
|
+
baseline_metric=float(metric(target_logits)),
|
|
198
|
+
reference_metric=float(metric(source_logits)),
|
|
199
|
+
patched_metric=float(metric(patched_logits)),
|
|
200
|
+
logit_delta=(patched_logits[0, -1] - target_logits[0, -1]).detach().cpu(),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def patch_many(
|
|
204
|
+
self,
|
|
205
|
+
source: Inputs,
|
|
206
|
+
target: Inputs,
|
|
207
|
+
layer: int,
|
|
208
|
+
neuron_indices: Sequence[int],
|
|
209
|
+
pole: Pole,
|
|
210
|
+
*,
|
|
211
|
+
effect: Effect,
|
|
212
|
+
positions: Positions = "all",
|
|
213
|
+
freeze_downstream: bool = False,
|
|
214
|
+
batch_size: int = 8,
|
|
215
|
+
) -> list[float]:
|
|
216
|
+
"""``effect`` of patching each neuron in ``layer``, one at a time, from source to target.
|
|
217
|
+
|
|
218
|
+
With ``effect=metric_change(metric)`` this is :meth:`patch`'s ``metric_delta``.
|
|
219
|
+
|
|
220
|
+
Each neuron gets its own row of a batch, so ``k`` neurons cost ``k / batch_size``
|
|
221
|
+
forward passes instead of ``k``. A streamed model runs more rows per pass
|
|
222
|
+
(:func:`~openinterp.harness.memory.stream_rows`). ``source`` and ``target`` must be
|
|
223
|
+
single prompts.
|
|
224
|
+
"""
|
|
225
|
+
runner = self.runner
|
|
226
|
+
downstream = list(range(layer + 1, runner.n_layers)) if freeze_downstream else []
|
|
227
|
+
src_act = runner.layer_activations(source, [layer])[layer] # stops at the layer
|
|
228
|
+
target_enc = runner.encode(target)
|
|
229
|
+
target_logits, target_cache, downstream_out = self._target_pass(
|
|
230
|
+
target_enc, downstream, [layer]
|
|
231
|
+
)
|
|
232
|
+
batch_size = stream_rows(runner, target_enc["input_ids"].shape[1], batch_size, logits=True)
|
|
233
|
+
if src_act.shape[0] != 1 or target_cache[layer].shape[0] != 1:
|
|
234
|
+
raise ValueError("patch_many takes one source and one target prompt")
|
|
235
|
+
if positions == "all" and src_act.shape[:2] != target_cache[layer].shape[:2]:
|
|
236
|
+
raise ShapeMismatchError(
|
|
237
|
+
f"source {tuple(src_act.shape[:2])} and target "
|
|
238
|
+
f"{tuple(target_cache[layer].shape[:2])} differ in sequence length; "
|
|
239
|
+
"use positions='last' or align the prompts."
|
|
240
|
+
)
|
|
241
|
+
span = slice(None) if positions == "all" else slice(-1, None)
|
|
242
|
+
|
|
243
|
+
deltas: list[float] = []
|
|
244
|
+
for start in range(0, len(neuron_indices), batch_size):
|
|
245
|
+
chunk = list(neuron_indices[start : start + batch_size])
|
|
246
|
+
batch = {k: v.repeat(len(chunk), *[1] * (v.dim() - 1)) for k, v in target_enc.items()}
|
|
247
|
+
|
|
248
|
+
def inject(act: torch.Tensor, chunk=chunk) -> torch.Tensor:
|
|
249
|
+
act = act.clone()
|
|
250
|
+
for row, idx in enumerate(chunk):
|
|
251
|
+
act[row, span, idx] = replace_pole(
|
|
252
|
+
act[row, span, idx], src_act[0, span, idx], pole
|
|
253
|
+
)
|
|
254
|
+
return act
|
|
255
|
+
|
|
256
|
+
n = len(chunk)
|
|
257
|
+
frozen = {i: _constant(cached.repeat(n, 1, 1)) for i, cached in downstream_out.items()}
|
|
258
|
+
patched = runner.run_with_interventions(batch, {layer: inject}, frozen)
|
|
259
|
+
deltas += [effect(patched[row : row + 1], target_logits) for row in range(len(chunk))]
|
|
260
|
+
return deltas
|
|
261
|
+
|
|
262
|
+
def _target_pass(
|
|
263
|
+
self, encoded, downstream: Sequence[int], layers: Sequence[int] = ()
|
|
264
|
+
) -> tuple[torch.Tensor, dict[int, torch.Tensor], dict[int, torch.Tensor]]:
|
|
265
|
+
"""One pass over the target: its logits, ``layers``' activations, ``downstream``'s outputs.
|
|
266
|
+
|
|
267
|
+
Patching freezes the downstream outputs at their target values. They were read in a
|
|
268
|
+
second pass over the target. One pass gives the same values for one read of the
|
|
269
|
+
layers, which is what a streamed model pays for.
|
|
270
|
+
"""
|
|
271
|
+
acts: dict[int, torch.Tensor] = {}
|
|
272
|
+
outputs: dict[int, torch.Tensor] = {}
|
|
273
|
+
|
|
274
|
+
def recorder(layer: int) -> Intervention:
|
|
275
|
+
def record(out: torch.Tensor) -> None:
|
|
276
|
+
outputs[layer] = out.detach().clone()
|
|
277
|
+
|
|
278
|
+
return record
|
|
279
|
+
|
|
280
|
+
logits = self.runner.run_with_interventions(
|
|
281
|
+
encoded, _recorders(acts, layers), {i: recorder(i) for i in downstream}
|
|
282
|
+
)
|
|
283
|
+
return logits, acts, outputs
|
|
284
|
+
|
|
285
|
+
def denoise(self, clean: Inputs, corrupted: Inputs, neuron: NeuronRef, **kw) -> PatchResult:
|
|
286
|
+
"""Clean -> corrupted patch. ``recovery`` is the neuron's sufficiency."""
|
|
287
|
+
return self.patch(clean, corrupted, neuron, **kw)
|
|
288
|
+
|
|
289
|
+
def noise(self, clean: Inputs, corrupted: Inputs, neuron: NeuronRef, **kw) -> PatchResult:
|
|
290
|
+
"""Corrupted -> clean patch. ``recovery`` is the neuron's necessity."""
|
|
291
|
+
return self.patch(corrupted, clean, neuron, **kw)
|
|
292
|
+
|
|
293
|
+
def resample_ablation(
|
|
294
|
+
self,
|
|
295
|
+
target: Inputs,
|
|
296
|
+
resample_sources: Sequence[Inputs],
|
|
297
|
+
neuron: NeuronRef,
|
|
298
|
+
*,
|
|
299
|
+
metric: Metric | None = None,
|
|
300
|
+
positions: Positions = "last",
|
|
301
|
+
freeze_downstream: bool = False,
|
|
302
|
+
) -> AblationResult:
|
|
303
|
+
"""Replace the neuron pole in ``target`` with its value on each resample source.
|
|
304
|
+
|
|
305
|
+
The default metric is fixed from the *target* run, so every source is scored
|
|
306
|
+
against the same token.
|
|
307
|
+
"""
|
|
308
|
+
if metric is None:
|
|
309
|
+
target_logits = self.runner.run_with_interventions(target)
|
|
310
|
+
metric = token_logit_metric(int(target_logits[0, -1].argmax()))
|
|
311
|
+
results = [
|
|
312
|
+
self.patch(
|
|
313
|
+
src,
|
|
314
|
+
target,
|
|
315
|
+
neuron,
|
|
316
|
+
metric=metric,
|
|
317
|
+
positions=positions,
|
|
318
|
+
freeze_downstream=freeze_downstream,
|
|
319
|
+
)
|
|
320
|
+
for src in resample_sources
|
|
321
|
+
]
|
|
322
|
+
return AblationResult(per_source=results)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Verbalise a unit's output direction with a Patchscope (Ghandeharioun et al., 2024).
|
|
2
|
+
|
|
3
|
+
Take the residual stream after layer ℓ's block, at a placeholder token of an inspection
|
|
4
|
+
prompt. It is replaced by the unit's output direction, scaled to a multiple of the norm
|
|
5
|
+
that it replaces. The model then continues greedily, and so describes the direction without
|
|
6
|
+
seeing a single example. A random direction injected the same way is the control: any
|
|
7
|
+
text the two share comes from the prompt, not the unit.
|
|
8
|
+
|
|
9
|
+
There are two inspection prompts: a few-shot "word: category" list and a definition
|
|
10
|
+
frame. Each is used at the unit's own layer ℓ (source layer = target layer). Each is also
|
|
11
|
+
used at an early layer, as SelfIE (Chen et al., 2024) does. On Qwen2.5-0.5B, the few-shot
|
|
12
|
+
list injected at layer 2 names the concept of several VERIFIED poles. It gives "crop",
|
|
13
|
+
"newspaper", "official" and "culture", where the control gives "animal", "emotion" or
|
|
14
|
+
"period".
|
|
15
|
+
Injected at their own layer, poles from layer 17 up mostly read back the control's
|
|
16
|
+
text. The decoding is greedy, so a reading is deterministic given the weights.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Mapping, Sequence
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
from openinterp.core.exceptions import ArchitectureNotSupportedError
|
|
26
|
+
from openinterp.core.patchscope import PatchscopeReading, PatchscopeReadout
|
|
27
|
+
from openinterp.harness.hooks import NeuronHookRunner
|
|
28
|
+
from openinterp.harness.units import FeatureUnit, NeuronUnit, Unit
|
|
29
|
+
|
|
30
|
+
PROMPTS = {
|
|
31
|
+
"category": "banana: fruit\nhammer: tool\nviolin: music\nsenator: politics\n{x}:",
|
|
32
|
+
"meaning": 'The meaning of the word "{x}" is',
|
|
33
|
+
}
|
|
34
|
+
PLACEHOLDER = "?"
|
|
35
|
+
SCALES = (1.0, 2.0, 4.0)
|
|
36
|
+
EARLY_LAYER = 2
|
|
37
|
+
MAX_NEW_TOKENS = 12
|
|
38
|
+
CONTROL_SEED = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def write_direction(runner: NeuronHookRunner, unit: Unit) -> torch.Tensor | None:
|
|
42
|
+
"""Return the vector that one unit of the unit's strength adds to the residual stream.
|
|
43
|
+
|
|
44
|
+
That is a pole's signed ``w_out``, or a dictionary latent's decoder row. A head writes
|
|
45
|
+
a subspace, not one direction, so it has none.
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(unit, NeuronUnit):
|
|
48
|
+
ident = unit.ident
|
|
49
|
+
direction = runner.neuron_output_direction(ident.layer, ident.neuron_idx).float()
|
|
50
|
+
return -direction if ident.pole == "negative" else direction
|
|
51
|
+
if isinstance(unit, FeatureUnit):
|
|
52
|
+
return unit.dictionary(runner).w_dec[unit.ident.feature_idx].float()
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _prompt_ids(runner: NeuronHookRunner, template: str, placeholder: str) -> tuple[list[int], int]:
|
|
57
|
+
"""Token ids of ``template`` with the placeholder as exactly one token, and its index."""
|
|
58
|
+
tok = runner.tokenizer
|
|
59
|
+
before, after = template.split("{x}")
|
|
60
|
+
marker = tok.encode(placeholder, add_special_tokens=False)
|
|
61
|
+
if len(marker) != 1:
|
|
62
|
+
raise ValueError(f"the placeholder {placeholder!r} must be one token, not {len(marker)}")
|
|
63
|
+
prefix = tok.encode(before, add_special_tokens=True) if before else []
|
|
64
|
+
return prefix + marker + tok.encode(after, add_special_tokens=False), len(prefix)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _block_output(output) -> torch.Tensor:
|
|
68
|
+
return output[0] if isinstance(output, tuple) else output
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@torch.no_grad()
|
|
72
|
+
def _residual_norm(runner: NeuronHookRunner, ids: list[int], layer: int, pos: int) -> float:
|
|
73
|
+
captured: list[float] = []
|
|
74
|
+
|
|
75
|
+
def capture(_module, _args, output):
|
|
76
|
+
captured.append(float(_block_output(output)[0, pos].float().norm()))
|
|
77
|
+
|
|
78
|
+
handle = runner.layers[layer].register_forward_hook(capture)
|
|
79
|
+
try:
|
|
80
|
+
runner.forward({"input_ids": torch.tensor([ids], device=runner.device)})
|
|
81
|
+
finally:
|
|
82
|
+
handle.remove()
|
|
83
|
+
if not captured:
|
|
84
|
+
raise ArchitectureNotSupportedError(f"layer {layer}'s block never ran as a module")
|
|
85
|
+
return captured[-1]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@torch.no_grad()
|
|
89
|
+
def _continue(
|
|
90
|
+
runner: NeuronHookRunner,
|
|
91
|
+
ids: list[int],
|
|
92
|
+
pos: int,
|
|
93
|
+
layer: int,
|
|
94
|
+
vectors: torch.Tensor,
|
|
95
|
+
max_new_tokens: int,
|
|
96
|
+
) -> list[str]:
|
|
97
|
+
"""Return greedy continuations of ``ids``, one per row of ``vectors``.
|
|
98
|
+
|
|
99
|
+
In each, the residual after ``layer``'s block at ``pos`` is replaced by that row. Each
|
|
100
|
+
stops at a line break or the end-of-text token.
|
|
101
|
+
"""
|
|
102
|
+
tok = runner.tokenizer
|
|
103
|
+
eos = getattr(tok, "eos_token_id", None)
|
|
104
|
+
|
|
105
|
+
def patch(_module, _args, output):
|
|
106
|
+
hidden = _block_output(output).clone()
|
|
107
|
+
hidden[:, pos] = vectors.to(hidden.device, hidden.dtype)
|
|
108
|
+
return (hidden, *output[1:]) if isinstance(output, tuple) else hidden
|
|
109
|
+
|
|
110
|
+
seq = torch.tensor([ids] * len(vectors), device=runner.device)
|
|
111
|
+
done = [False] * len(vectors)
|
|
112
|
+
handle = runner.layers[layer].register_forward_hook(patch)
|
|
113
|
+
try:
|
|
114
|
+
for _ in range(max_new_tokens):
|
|
115
|
+
nxt = runner.forward({"input_ids": seq})[:, -1].argmax(-1)
|
|
116
|
+
seq = torch.cat([seq, nxt[:, None]], dim=1)
|
|
117
|
+
for row, token in enumerate(nxt.tolist()):
|
|
118
|
+
done[row] |= token == eos or "\n" in tok.decode([token])
|
|
119
|
+
if all(done):
|
|
120
|
+
break
|
|
121
|
+
finally:
|
|
122
|
+
handle.remove()
|
|
123
|
+
texts = []
|
|
124
|
+
for row in seq[:, len(ids) :].tolist():
|
|
125
|
+
if eos in row:
|
|
126
|
+
row = row[: row.index(eos)]
|
|
127
|
+
texts.append(tok.decode(row).split("\n")[0].strip())
|
|
128
|
+
return texts
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def patchscope(
|
|
132
|
+
runner: NeuronHookRunner,
|
|
133
|
+
layer: int,
|
|
134
|
+
direction: torch.Tensor,
|
|
135
|
+
*,
|
|
136
|
+
prompts: Mapping[str, str] = PROMPTS,
|
|
137
|
+
placeholder: str = PLACEHOLDER,
|
|
138
|
+
scales: Sequence[float] = SCALES,
|
|
139
|
+
early_layer: int = EARLY_LAYER,
|
|
140
|
+
max_new_tokens: int = MAX_NEW_TOKENS,
|
|
141
|
+
control_seed: int = CONTROL_SEED,
|
|
142
|
+
) -> PatchscopeReadout:
|
|
143
|
+
"""Read ``direction``, written after ``layer``'s MLP, through every inspection prompt.
|
|
144
|
+
|
|
145
|
+
It is read at ``layer``, and at ``early_layer`` when that is earlier, at each scale.
|
|
146
|
+
"""
|
|
147
|
+
if runner.tokenizer is None:
|
|
148
|
+
raise ValueError("a Patchscope needs a tokenizer")
|
|
149
|
+
direction = direction.float().cpu()
|
|
150
|
+
generator = torch.Generator().manual_seed(control_seed)
|
|
151
|
+
control = torch.randn(direction.shape, generator=generator)
|
|
152
|
+
units = torch.stack([direction / direction.norm(), control / control.norm()])
|
|
153
|
+
readings = []
|
|
154
|
+
for name, template in prompts.items():
|
|
155
|
+
ids, pos = _prompt_ids(runner, template, placeholder)
|
|
156
|
+
for target in sorted({min(early_layer, layer), layer}):
|
|
157
|
+
norm = _residual_norm(runner, ids, target, pos)
|
|
158
|
+
# Rows: the unit at each scale, then the control at each scale.
|
|
159
|
+
vectors = torch.stack([units[i] * norm * s for i in (0, 1) for s in scales])
|
|
160
|
+
texts = _continue(runner, ids, pos, target, vectors, max_new_tokens)
|
|
161
|
+
for i, scale in enumerate(scales):
|
|
162
|
+
readings.append(
|
|
163
|
+
PatchscopeReading(
|
|
164
|
+
prompt=name,
|
|
165
|
+
layer=target,
|
|
166
|
+
scale=float(scale),
|
|
167
|
+
text=texts[i],
|
|
168
|
+
control_text=texts[len(scales) + i],
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
return PatchscopeReadout(
|
|
172
|
+
prompts=dict(prompts),
|
|
173
|
+
placeholder=placeholder,
|
|
174
|
+
max_new_tokens=max_new_tokens,
|
|
175
|
+
control_seed=control_seed,
|
|
176
|
+
readings=readings,
|
|
177
|
+
)
|