feat(svd_circuits): singular-direction readout, projection, and causal patch gate - #1775
Open
janmenjayap wants to merge 6 commits into
Open
feat(svd_circuits): singular-direction readout, projection, and causal patch gate#1775janmenjayap wants to merge 6 commits into
janmenjayap wants to merge 6 commits into
Conversation
…a cross-check test HeadSVD's docstring had U and V swapped for the OV map: for a factored map A @ B, U's columns live in A's input space and V's columns live in B's output space, so for OV (A = W_V_h, B = W_O_h) it is V, not U, whose columns are the residual-stream directions this head writes into and should be projected through W_U for a vocab or logit readout. The swap never raised a shape error because both spaces are d_model-dimensional, so it would have silently used the wrong basis in every downstream readout, projection, or patch built on top of it. Adds a regression test that cross-checks HeadSVD.OV.V against the already-shipped SVDInterpreter (which projects its OV singular vectors through the unembedding the other way), on a tiny no-download TransformerBridge, so the assertion cannot pass under either labeling by construction.
vocab_readout projects a head's top-k OV output directions (HeadSVD.V, per the corrected convention) through the unembedding, gated on TransformerBridge compatibility mode so LayerNorm stays folded into W_U. logit_signature reconstructs one direction's rank-1 OV output and reads its signed logit effect on given tokens; it requires the direction to be isolated first, since a rotation-ambiguous or null direction's signature is not attributable to it alone.
Add ActivationProjection and project_activations to recover per-position firing coefficients by projecting a head's actual OV output (hook_result) onto its V basis. Summing the coefficients against V reconstructs the head's output because V's columns are orthonormal and define the basis the head writes in. Restore the model's prior use_attn_result setting after the forward pass so read-only analysis does not leave a configuration side effect.
patch_along_directions causally validates a claimed OV subfunction by reconstructing a head's hook_result onto a chosen span of HeadSVD.V directions (keep) or its complement (ablate), then comparing a caller's metric before and after against an equally-sized random-subspace baseline. gated is true only when the requested subspace moves the metric by more than the random baseline does, so a claim cannot be called causal merely because patching moved the metric at all. Before touching the model, the retained set is checked against every degenerate block reported by HeadSVD.degenerate_blocks(): a block must be kept whole or dropped whole, since attributing an effect to part of a rotation-ambiguous or null block would let a caller hand-pick around the same guard require_isolated already enforces per direction. The forward passes run under torch.no_grad(), matching the other analysis tools that repeat a forward pass per call, and the model's prior use_attn_result setting is restored in a finally block so this read-only check leaves no configuration side effect.
…ty mode Adds decompose_head, project_activations, patch_along_directions, vocab_readout, logit_signature, and their supporting types (HeadSVD, HeadDecomposition, RankReportRow, ActivationProjection, LogitSignature, PatchResult, DegenerateDirectionError) to transformer_lens.tools.analysis's imports, __all__, and tool-listing docstring, in the same ASCII-sorted order the module already uses for its other tools. No compatibility-mode logic changes here: vocab_readout and logit_signature already gate on TransformerBridge compatibility mode, and patch_along_directions already restores use_attn_result. This commit only widens the public surface, so the causal gate ships alongside the readouts rather than either landing without the other.
Exercise vocab_readout and patch_along_directions against GPT-2 small's layer 9 head 9 (name-mover head) on an IOI-style prompt with a Mary/John logit-diff metric, checking that the readout and causal gate agree on a real Bridge with compatibility mode enabled.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds the causal-validation layer on top of PR1's per-head QK/OV SVD (#1768): a direction can now be read out to vocab/logit space, projected against a real forward pass, and, critically, causally patched before any subfunction claim is accepted. Second of three PRs implementing per-head singular-vector decomposition of a head's QK (
W_Q W_K^T) and OV (W_V W_O) maps (tracking issue: #1767).vocab_readout(model, head_svd, k=10)— projects the top-k OV output directions (HeadSVD.V) through the unembedding, gated onTransformerBridgecompatibility mode.logit_signature(model, head_svd, direction, tokens)— signed logit effect of one OV direction's rank-1 reconstruction; requires the direction to passrequire_isolatedfirst, since a rotation-ambiguous or null direction's signature is not attributable to it alone.project_activations(model, head_svd, prompt)— per-position firing coefficients of a head's actualhook_resultoutput against itsVbasis; summing the coefficients againstVreconstructs the real output to numerical precision, since both read the same basis the head actually writes in.patch_along_directions(model, head_svd, prompt, metric, keep=..., ablate=...)— the mandatory causal gate. Reconstructs or ablates a head's output onto a chosen span ofVdirections via a directhook_resulthook, and reportsdelta_metricagainst an equally-sized random-subspace baseline (gatedis true only when the requested subspace beats that baseline, not merely when the metric moves at all). Degenerate blocks reported byHeadSVD.degenerate_blocks()must be kept or dropped whole; a caller cannot route aroundrequire_isolatedby hand-picking part of a rotation-ambiguous block throughkeep/ablate.decompose_head,project_activations,patch_along_directions,vocab_readout,logit_signature, and their supporting types now ship fromtransformer_lens.tools.analysis— the first time this module is part of the public API, by design, so a readout was never importable without the causal gate attached to it.tests/integration/test_svd_circuits.py(new): exercisesvocab_readoutandpatch_along_directionsend to end against GPT-2 small's layer 9 head 9 (name-mover head) on an IOI-style prompt.Correctness fix folded in first:
HeadSVD's docstring had the OV output direction backwards —U's columns are the value-computation input space,V's are the residual-stream output space this head writes into and the one to project throughW_U. The swap never raised a shape error because both spaces ared_model-dimensional, so every downstream readout, projection, or patch would have silently used the wrong basis. Fixed first, with a regression test that cross-checks against the already-shippedSVDInterpreter(which projects the OV map the other way already), so the assertion cannot pass under either labeling.Two implementation notes for anyone diffing this against the original proposal:
vocab_readoutdoes not wrapSVDInterpreter.get_singular_vectors. That method indexesW_V/W_Kwith a raw query-head index, which is wrong on grouped-query models (the same bug PR1 already fixed for this module's own weight reads).vocab_readoutreuses PR1's already-kv-mappedHeadSVD.Vand projects it throughW_Udirectly instead of introducing a second, buggy SVD path.patch_along_directionscallsmodel.run_with_hooksdirectly rather thangeneric_activation_patch. That helper is built for a clean-vs-corrupted two-run sweep over an index grid; this tool is a single-prompt reconstruct/ablate on a live projection, which does not fit the sweep shape.Unit-tested with a tiny no-download
TransformerBridge(real forward passes, no Hub access) for projection/patch behavior, plus model-free tests for the pure weight-space math. Integration test downloadsgpt2-small.PR3 (docs, demo notebook, slow multi-head oracle-parity check) follows this one.
Part of #1767
Type of change
Checklist: