AQIT
0.1.0
Toggle main menu visibility
Loading...
Searching...
No Matches
layer_analysis.py
Go to the documentation of this file.
1
# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2
# This file is part of the Aquin Engine. Unauthorized copying, modification,
3
# distribution, or use of this file, via any medium, is strictly prohibited.
4
# Proprietary and confidential. See LICENSE for terms.
5
6
"""LLM layer health: activation stability + OOD similarity."""
7
8
from
__future__
import
annotations
9
10
import
torch
11
from
transformer_lens
import
HookedTransformer
12
13
DEFAULT_STABILITY_PROMPTS = [
14
"The capital of France is"
,
15
"Write a function that sorts a list"
,
16
"Explain photosynthesis in simple terms"
,
17
"Hello, how are you today?"
,
18
"The quick brown fox jumps over the lazy dog"
,
19
]
20
21
DEFAULT_IN_DOMAIN_PROMPTS = [
22
"The capital of France is Paris."
,
23
"Water boils at 100 degrees Celsius."
,
24
"Python is a programming language."
,
25
"The Earth orbits the Sun."
,
26
]
27
28
DEFAULT_OOD_PROMPTS = [
29
"asdf jkl qwerty zxcv"
,
30
"!!! ??? ### @@@"
,
31
"florp sniggle wumpus"
,
32
"9918273645 192837465"
,
33
]
34
35
COLLAPSE_THRESHOLD = 0.85
36
DEAD_THRESHOLD = 0.01
37
MIN_STABILITY_PROMPTS = 2
38
39
40
def
_resolve_stability_prompts
(prompts: list[str] |
None
) -> tuple[list[str], bool]:
41
"""PCA needs ≥2 samples. Pad with defaults when the user passes fewer."""
42
if
not
prompts:
43
return
list(DEFAULT_STABILITY_PROMPTS),
False
44
resolved = list(prompts)
45
if
len(resolved) >= MIN_STABILITY_PROMPTS:
46
return
resolved,
False
47
for
p
in
DEFAULT_STABILITY_PROMPTS:
48
if
p
not
in
resolved:
49
resolved.append(p)
50
if
len(resolved) >= MIN_STABILITY_PROMPTS:
51
break
52
return
resolved,
True
53
54
55
def
_pca_variance_ratios
(acts: torch.Tensor, top_k: int) -> tuple[float, float]:
56
"""acts: (n_samples, d_model). Returns (top1_ratio, topk_ratio)."""
57
if
acts.shape[0] < 2:
58
return
0.0, 0.0
59
centered = acts - acts.mean(dim=0, keepdim=
True
)
60
try
:
61
_, s, _ = torch.linalg.svd(centered, full_matrices=
False
)
62
except
Exception:
63
return
0.0, 0.0
64
var = s ** 2
65
total = var.sum()
66
if
total < 1e-12:
67
return
0.0, 0.0
68
top1 = float(var[0] / total)
69
k = min(top_k, len(var))
70
topk = float(var[:k].sum() / total)
71
return
top1, topk
72
73
74
def
_collect_layer_activations
(
75
model: HookedTransformer,
76
prompts: list[str],
77
) -> dict[int, torch.Tensor]:
78
"""Last-token resid_post per layer. layer -> (n_prompts, d_model)."""
79
n_layers = model.cfg.n_layers
80
per_layer: dict[int, list[torch.Tensor]] = {i: []
for
i
in
range(n_layers)}
81
82
for
prompt
in
prompts:
83
tokens = model.to_tokens(prompt)
84
with
torch.no_grad():
85
_, cache = model.run_with_cache(
86
tokens,
87
names_filter=
lambda
name: name.endswith(
"hook_resid_post"
),
88
)
89
for
layer
in
range(n_layers):
90
key = f
"blocks.{layer}.hook_resid_post"
91
if
key
in
cache:
92
per_layer[layer].append(cache[key][0, -1].float().cpu())
93
94
return
{layer: torch.stack(vecs)
for
layer, vecs
in
per_layer.items()
if
vecs}
95
96
97
def
run_activation_stability
(
98
model: HookedTransformer,
99
prompts: list[str] |
None
=
None
,
100
top_k: int = 10,
101
) -> dict:
102
user_n = len(prompts)
if
prompts
else
0
103
prompts, padded_defaults =
_resolve_stability_prompts
(prompts)
104
layer_acts =
_collect_layer_activations
(model, prompts)
105
n_layers = model.cfg.n_layers
106
layers_out: list[dict] = []
107
dead_count = 0
108
collapsed_count = 0
109
110
for
layer
in
range(n_layers):
111
acts = layer_acts.get(layer)
112
if
acts
is
None
or
acts.shape[0] < 2:
113
continue
114
top1, topk =
_pca_variance_ratios
(acts, top_k)
115
mean_act = float(acts.mean())
116
std_act = float(acts.std())
117
status =
"ok"
118
if
std_act < DEAD_THRESHOLD:
119
status =
"dead"
120
dead_count += 1
121
elif
top1 >= COLLAPSE_THRESHOLD:
122
status =
"collapsed"
123
collapsed_count += 1
124
layers_out.append({
125
"layer"
: layer,
126
"top1_variance_ratio"
: round(top1, 4),
127
"topk_variance_ratio"
: round(topk, 4),
128
"mean_activation"
: round(mean_act, 4),
129
"activation_std"
: round(std_act, 4),
130
"status"
: status,
131
})
132
133
return
{
134
"n_layers"
: n_layers,
135
"top_k"
: top_k,
136
"n_prompts"
: len(prompts),
137
"n_user_prompts"
: user_n
or
len(prompts),
138
"padded_defaults"
: padded_defaults,
139
"layers"
: layers_out,
140
"dead_count"
: dead_count,
141
"collapsed_count"
: collapsed_count,
142
"collapse_threshold"
: COLLAPSE_THRESHOLD,
143
"dead_threshold"
: DEAD_THRESHOLD,
144
}
145
146
147
def
_mean_pairwise_cos
(vecs: torch.Tensor) -> float:
148
if
vecs.shape[0] < 2:
149
return
1.0
150
v = vecs / vecs.norm(dim=-1, keepdim=
True
).clamp(min=1e-8)
151
sims = v @ v.T
152
n = sims.shape[0]
153
return
float((sims.sum() - n) / (n * (n - 1)))
154
155
156
def
_mean_cross_cos
(a: torch.Tensor, b: torch.Tensor) -> float:
157
if
a.shape[0] == 0
or
b.shape[0] == 0:
158
return
0.0
159
a = a / a.norm(dim=-1, keepdim=
True
).clamp(min=1e-8)
160
b = b / b.norm(dim=-1, keepdim=
True
).clamp(min=1e-8)
161
return
float((a @ b.T).mean())
162
163
164
def
_rbf_mmd
(x: torch.Tensor, y: torch.Tensor) -> float:
165
if
x.shape[0] < 2
or
y.shape[0] < 2:
166
return
0.0
167
xy = torch.cat([x, y], dim=0)
168
dists = torch.cdist(xy, xy, p=2)
169
sigma = float(dists.median())
170
if
sigma < 1e-8:
171
sigma = 1.0
172
173
def
_kernel(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
174
d = torch.cdist(a, b, p=2)
175
return
torch.exp(-(d ** 2) / (2 * sigma ** 2))
176
177
k_xx = _kernel(x, x).mean()
178
k_yy = _kernel(y, y).mean()
179
k_xy = _kernel(x, y).mean()
180
return
float(torch.clamp(k_xx + k_yy - 2 * k_xy, min=0.0))
181
182
183
def
run_ood_similarity
(
184
model: HookedTransformer,
185
in_domain_prompts: list[str] |
None
=
None
,
186
ood_prompts: list[str] |
None
=
None
,
187
) -> dict:
188
in_prompts = in_domain_prompts
or
DEFAULT_IN_DOMAIN_PROMPTS
189
ood_ps = ood_prompts
or
DEFAULT_OOD_PROMPTS
190
in_acts =
_collect_layer_activations
(model, in_prompts)
191
ood_acts =
_collect_layer_activations
(model, ood_ps)
192
n_layers = model.cfg.n_layers
193
layers_out: list[dict] = []
194
195
for
layer
in
range(n_layers):
196
in_a = in_acts.get(layer)
197
ood_a = ood_acts.get(layer)
198
if
in_a
is
None
or
ood_a
is
None
:
199
continue
200
in_cos =
_mean_pairwise_cos
(in_a)
201
ood_cos =
_mean_pairwise_cos
(ood_a)
202
cross_cos =
_mean_cross_cos
(in_a, ood_a)
203
separation = in_cos - cross_cos
204
mmd =
_rbf_mmd
(in_a, ood_a)
205
layers_out.append({
206
"layer"
: layer,
207
"in_cos"
: round(in_cos, 4),
208
"ood_cos"
: round(ood_cos, 4),
209
"cross_cos"
: round(cross_cos, 4),
210
"mmd"
: round(mmd, 4),
211
"separation"
: round(separation, 4),
212
})
213
214
peak_layer = max(layers_out, key=
lambda
row: row[
"separation"
])[
"layer"
]
if
layers_out
else
0
215
mean_sep = (
216
sum(row[
"separation"
]
for
row
in
layers_out) / len(layers_out)
217
if
layers_out
else
0.0
218
)
219
220
return
{
221
"n_layers"
: n_layers,
222
"n_in_prompts"
: len(in_prompts),
223
"n_ood_prompts"
: len(ood_ps),
224
"layers"
: layers_out,
225
"peak_layer"
: peak_layer,
226
"mean_separation"
: round(mean_sep, 4),
227
}
228
229
230
def
run_layer_analysis
(model: HookedTransformer, args: dict) -> dict:
231
prompts = args.get(
"prompts"
)
232
do_localize = bool(args.get(
"localize"
))
233
234
# When --localize uses a probe file path, don't feed that path into stability PCA.
235
stability_prompts = prompts
236
localize_prompts = args.get(
"localize_prompts"
)
237
if
do_localize:
238
if
localize_prompts
is
None
and
isinstance(prompts, str)
and
not
prompts.strip().startswith(
"["
):
239
localize_prompts = prompts
240
stability_prompts =
None
241
elif
localize_prompts
is
None
and
prompts
is
None
:
242
localize_prompts =
None
# default deception fixture
243
if
isinstance(prompts, str)
and
prompts.strip().startswith(
"["
):
244
try
:
245
import
json
246
stability_prompts = json.loads(prompts)
247
except
Exception:
248
stability_prompts = [prompts]
249
250
if
isinstance(stability_prompts, str):
251
stability_prompts = [stability_prompts]
252
253
in_domain = args.get(
"in_domain_prompts"
)
254
ood = args.get(
"ood_prompts"
)
255
top_k = int(args.get(
"top_k"
, 10))
256
257
stability =
run_activation_stability
(model, prompts=stability_prompts, top_k=top_k)
258
ood_result =
run_ood_similarity
(
259
model,
260
in_domain_prompts=in_domain,
261
ood_prompts=ood,
262
)
263
out: dict = {
264
"stability"
: stability,
265
"ood"
: ood_result,
266
"model_id"
: args.get(
"model_id"
,
""
),
267
}
268
269
if
do_localize:
270
from
aquin.compute.localize_collapse
import
run_localize_collapse
271
272
feature_idx = args.get(
"feature_idx"
)
273
if
feature_idx
is
not
None
:
274
feature_idx = int(feature_idx)
275
layer = args.get(
"layer"
)
276
if
layer
is
not
None
:
277
layer = int(layer)
278
279
localize = run_localize_collapse(
280
model,
281
str(args.get(
"model_id"
)
or
""
),
282
prompts=localize_prompts,
283
stressor_prompts=args.get(
"stressor_prompts"
)
or
args.get(
"stressor"
),
284
feature_idx=feature_idx,
285
vector_path=args.get(
"vector"
)
or
args.get(
"vector_path"
),
286
layer=layer,
287
top_k_features=int(args.get(
"top_k_features"
)
or
args.get(
"top_k"
)
or
8),
288
collect_layer_activations=_collect_layer_activations,
289
)
290
out[
"localize"
] = localize
291
292
return
out
aquin.compute.layer_analysis.run_activation_stability
dict run_activation_stability(HookedTransformer model, list[str]|None prompts=None, int top_k=10)
Definition
layer_analysis.py:105
aquin.compute.layer_analysis.run_layer_analysis
dict run_layer_analysis(HookedTransformer model, dict args)
Definition
layer_analysis.py:234
aquin.compute.layer_analysis.run_ood_similarity
dict run_ood_similarity(HookedTransformer model, list[str]|None in_domain_prompts=None, list[str]|None ood_prompts=None)
Definition
layer_analysis.py:191
aquin.compute.layer_analysis._mean_pairwise_cos
float _mean_pairwise_cos(torch.Tensor vecs)
Definition
layer_analysis.py:151
aquin.compute.layer_analysis._resolve_stability_prompts
tuple[list[str], bool] _resolve_stability_prompts(list[str]|None prompts)
Definition
layer_analysis.py:44
aquin.compute.layer_analysis._pca_variance_ratios
tuple[float, float] _pca_variance_ratios(torch.Tensor acts, int top_k)
Definition
layer_analysis.py:59
aquin.compute.layer_analysis._collect_layer_activations
dict[int, torch.Tensor] _collect_layer_activations(HookedTransformer model, list[str] prompts)
Definition
layer_analysis.py:81
aquin.compute.layer_analysis._rbf_mmd
float _rbf_mmd(torch.Tensor x, torch.Tensor y)
Definition
layer_analysis.py:168
aquin.compute.layer_analysis._mean_cross_cos
float _mean_cross_cos(torch.Tensor a, torch.Tensor b)
Definition
layer_analysis.py:160
aquin.compute.localize_collapse
Definition
localize_collapse.py:1
aquin
compute
layer_analysis.py
AQIT · Aquin Labs Private Limited · Apache 2.0 · Generated by
1.18.0