Skip to content

Commit 26dd036

Browse files
rootclaude
andcommitted
test(sae): cover standardize's zero-variance floor (dead-latent safety)
standardize z-scores SAE codes for the linear/codon probes, where ~20% of latents are dead (constant 0). Add a direct test that the 1e-6 std floor keeps those columns finite (no NaN into the logreg fit) and that mean/std use the train rows only (no test-set leakage). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Polina Binder <pbinder@nvidia.com>
1 parent 896b253 commit 26dd036

1 file changed

Lines changed: 22 additions & 2 deletions

File tree

interpretability/sparse_autoencoders/sae/tests/test_probing.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
1818
One strong test per non-trivial metric: each checks the result against an independent
1919
reference (a definitional oracle or a hand-computed value) rather than a loose sanity bound.
20-
The trivial standardize helper is exercised transitively (decode_eval test); split_indices
21-
folds into the buffer roundtrip.
20+
standardize has a direct test for its zero-variance floor (the dead-latent safety the probe
21+
relies on); split_indices folds into the buffer roundtrip.
2222
"""
2323

2424
import numpy as np
@@ -32,6 +32,7 @@
3232
decode_eval,
3333
domain_f1,
3434
split_indices,
35+
standardize,
3536
)
3637

3738

@@ -196,3 +197,22 @@ def test_buffer_roundtrip_without_dense_or_instances(tmp_path):
196197
lo = ActivationBuffer.load(path)
197198
assert lo.dense is None and lo.instances is None
198199
assert np.array_equal(lo.codes, codes) and lo.label_names == ["a", "b"]
200+
201+
202+
def test_standardize_floors_zero_variance_and_uses_train_rows():
203+
"""standardize floors std at 1e-6 (constant/dead latents don't divide to NaN) and uses only tr rows.
204+
205+
The probe's linear/codon paths z-score SAE codes, where ~20% of latents are dead (constant 0);
206+
without the floor those columns would produce inf/NaN and poison every logreg fit.
207+
"""
208+
X = torch.zeros(6, 3)
209+
X[:, 0] = 5.0 # constant feature -> zero variance (dead-latent-like)
210+
X[:, 1] = torch.tensor([1.0, 2.0, 3.0, 9.0, 9.0, 9.0]) # varies within the train rows
211+
tr = torch.tensor([0, 1, 2]) # train split = first three rows
212+
213+
mu, sd = standardize(X, tr)
214+
assert torch.all(sd >= 1e-6) # constant column floored, never exactly 0
215+
assert sd[1] > 1e-6 # a varying feature keeps its real std
216+
assert torch.isfinite((X - mu) / sd).all() # so the z-score is finite everywhere
217+
# stats are computed over the train rows only (no test-set leakage)
218+
assert torch.allclose(mu, X[tr].mean(0)) and torch.allclose(sd, X[tr].std(0) + 1e-6)

0 commit comments

Comments
 (0)