You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thanks again for repeng — extracting per-layer directions for a whole model in a
couple of lines has been great to build on.
Following up the Secondary note in #77: that note observed that sklearn picks
the randomized SVD solver on LM activations, but said "in our runs the resulting pca_diff direction happened to be stable run-to-run … so we're not claiming
non-determinism." This issue closes that open question — ControlVector.train() is non-deterministic in general, and it's easy to demonstrate on weak-signal
concepts. It's related to #77 but a separate, solver-level root cause: #77 is
about pca_diff concept-blindness under a constant-positive contrast; this is
about svd_solver="auto" giving unseeded results in train() regardless of
method.
train is (n_pairs, hidden_size). hidden_size is > 500 for every real
transformer (3584 for Qwen2.5-7B, 4096 for Llama-3-8B, …), so max(X.shape) is
always > 500 and n_components=1 always lands in the middle branch → the randomized solver. randomized_svd takes a random_state, which PCA
leaves at None, so every call seeds a fresh RNG → different directions.
(The PCA sign ambiguity is already handled by the projection step below, so
that's not what this is — it's solver-level.)
Minimal repro (no model / no GPU — just numpy + scikit-learn)
auto selects: randomized
identical? False cosine: 0.58 # varies every run — this is the point
full identical? True
Two regimes, both real:
Well-separated concept (a dominant contrastive axis): the direction is
stable (cosine ≈ 1.0), but the vector is still not bit-identical across
runs — enough to break exact reproducibility, caching, and regression tests.
Weakly-separated concept (subtle/abstract, low signal-to-noise): the
direction itself diverges — cosine between two runs on identical data lands
roughly in the 0.5–0.9 range and changes every run. Here the non-determinism
isn't cosmetic; it changes the vector you ship. (This is also the regime where pca_diff returns a concept-independent axis when the positive prompt is held constant across pairs #77's constant-positive contrast produces a weak signal, which is likely why
the effect is easy to surface there.)
Proposed fix (one line, no API change)
svd_solver="full" is exact and deterministic, and is cheap for n_components=1:
With full, the repro above is bit-identical across runs in both regimes. I've
opened a small PR with exactly this change that references this issue. If you'd
rather keep the randomized solver's speed for very large datasets, an alternative
is to thread an optional random_state through train → read_representations
→ PCA(...); glad to switch to whichever you prefer.
Thanks again for repeng — extracting per-layer directions for a whole model in a
couple of lines has been great to build on.
Following up the Secondary note in #77: that note observed that sklearn picks
the randomized SVD solver on LM activations, but said "in our runs the resulting
pca_diffdirection happened to be stable run-to-run … so we're not claimingnon-determinism." This issue closes that open question —
ControlVector.train()is non-deterministic in general, and it's easy to demonstrate on weak-signal
concepts. It's related to #77 but a separate, solver-level root cause: #77 is
about
pca_diffconcept-blindness under a constant-positive contrast; this isabout
svd_solver="auto"giving unseeded results intrain()regardless ofmethod.
Root cause
In
read_representations(repeng/extract.py):PCAdefaults tosvd_solver="auto". sklearn'sautoheuristic chooses thesolver from the input shape:
max(X.shape) <= 500→"full"1 <= n_components < 0.8 * min(X.shape)→"randomized""full"trainis(n_pairs, hidden_size).hidden_sizeis > 500 for every realtransformer (3584 for Qwen2.5-7B, 4096 for Llama-3-8B, …), so
max(X.shape)isalways > 500 and
n_components=1always lands in the middle branch → therandomized solver.
randomized_svdtakes arandom_state, whichPCAleaves at
None, so every call seeds a fresh RNG → different directions.(The PCA sign ambiguity is already handled by the projection step below, so
that's not what this is — it's solver-level.)
Minimal repro (no model / no GPU — just numpy + scikit-learn)
Typical output (scikit-learn 1.7.2):
Two regimes, both real:
stable (cosine ≈ 1.0), but the vector is still not bit-identical across
runs — enough to break exact reproducibility, caching, and regression tests.
direction itself diverges — cosine between two runs on identical data lands
roughly in the 0.5–0.9 range and changes every run. Here the non-determinism
isn't cosmetic; it changes the vector you ship. (This is also the regime where
pca_diffreturns a concept-independent axis when the positive prompt is held constant across pairs #77's constant-positive contrast produces a weak signal, which is likely whythe effect is easy to surface there.)
Proposed fix (one line, no API change)
svd_solver="full"is exact and deterministic, and is cheap forn_components=1:With
full, the repro above is bit-identical across runs in both regimes. I'veopened a small PR with exactly this change that references this issue. If you'd
rather keep the randomized solver's speed for very large datasets, an alternative
is to thread an optional
random_statethroughtrain→read_representations→
PCA(...); glad to switch to whichever you prefer.Thanks!