Skip to content

feat(jacobian_lens): add dynamic J-space coordinate-patch hooks - #1749

Merged
jlarson4 merged 10 commits into
TransformerLensOrg:devfrom
janmenjayap:feat/jacobian-lens-coordinate-patch-hooks
Sep 10, 2026
Merged

feat(jacobian_lens): add dynamic J-space coordinate-patch hooks#1749
jlarson4 merged 10 commits into
TransformerLensOrg:devfrom
janmenjayap:feat/jacobian-lens-coordinate-patch-hooks

Conversation

@janmenjayap

@janmenjayap janmenjayap commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds JacobianLens.coordinate_patch_hooks(...), a forward-hook variant of the offline
coordinate_patch(...) primitive shipped in #1741, so an anchored J-space coordinate edit can run
live inside model.run_with_hooks(...) / model.generate(...) instead of only on one
already-captured activation.

Underneath, a new model-free core function, solve_coordinate_patch_positions(...), loops
solve_coordinate_patch(...) over every (batch_idx, position) pair in a [batch, num_positions, d_model] chunk, with an optional caller-owned decomposition cache keyed (layer, batch_idx, position). coordinate_patch_hooks(...) is a thin wrapper that builds one forward hook per layer —
following the same _resid_post_hook_name / eager dictionary-build pattern as swap_hooks — whose
hook body slices the requested positions, calls the core loop, and writes the result back.

This is PR2 of the plan tracked by the companion issue. Arbitrary multi-slot permutations, a
causal-swap benchmark, and HookedTransformer support remain explicitly deferred (PR3/PR4).

Fixes #1748

Motivation

coordinate_patch(...) edits one pre-captured activation offline and returns a report-only
CoordinatePatch. Studying how a patched coordinate propagates through the rest of a forward pass —
or patching during generate(...) — previously required manually re-capturing, patching, and
re-injecting activations outside the hook system. #1739 deferred the hooked variant because each
hooked position would otherwise fire its own vocabulary-scale sparse decomposition live during the
forward pass. This PR ships that variant while making the per-position cost explicit (a required
positions argument and a construction-time warning) rather than hiding it behind a convenient API.

What ships (commit by commit)

  1. feat(jacobian_lens_coordinate_patch): add per-position patch loop core
    New model-free solve_coordinate_patch_positions(...) in jacobian_lens_coordinate_patch.py:
    loops solve_coordinate_patch(...) over every (batch_idx, position) pair — never once per
    position shared across the batch, since two batch rows can have different active supports.
    Supports an optional caller-owned decomposition_cache keyed (layer, batch_idx, position); a
    cache hit skips get_sparse_decomposition(...), a miss solves once and populates it. No
    try/except around the per-pair loop — an inactive source at any one pair propagates
    solve_coordinate_patch's existing ValueError out of the whole call, so nothing is a partial
    write. Model-free tests first, mirroring PR1's own core-before-wrapper split.

  2. test(tools): extract shared J-lens toy-bridge fixtures into conftest
    Mechanical move, no behavior change. Extracts the _ToyBridge fake TransformerBridge (and its
    supporting constants/classes/fixture) that test_jacobian_lens.py already defined into
    tests/unit/tools/conftest.py, so the new hook-test file in commit 3 can reuse it instead of
    duplicating ~90 lines that would otherwise risk drifting between two copies. test_jacobian_lens.py
    imports from the conftest; its pass/fail set is byte-identical to before the move.

  3. feat(jacobian_lens): expose coordinate_patch_hooks on the Bridge
    JacobianLens.coordinate_patch_hooks(model, source_token, target_token, layers, *, positions, decomposition_cache=None, ...) resolves token inputs and layers exactly like coordinate_patch,
    then builds one forward hook per layer via solve_coordinate_patch_positions. positions is
    required — no full-sequence default, since a silent default would trigger a vocabulary-scale
    sparse solve at every position. Emits one UserWarning per call, at hook-construction time,
    naming len(layers) x len(positions) — not a per-position warning, and not an attempt to report
    the exact number of live (non-cache-hit) solves, which is only known at forward time. New
    dedicated test file (test_jacobian_lens_coordinate_patch_hooks.py), not folded into the
    already-1600+-line test_jacobian_lens.py.

  4. feat(jacobian_lens): export and document coordinate_patch_hooks
    Exports solve_coordinate_patch_positions from transformer_lens.tools.analysis, extends the
    package docstring, replaces the now-stale "dynamic patching would require a vocabulary-scale
    solve" claim in jacobian_lens_fitting.md with a new #### Dynamic coordinate-patch hooks
    subsection, and adds one cached-model GPT-2 integration test asserting algebraic invariants
    only
    (alpha=0 is an exact no-op; untouched positions are bit-identical through a real
    run_with_hooks pass) — not a token-flip claim.

Design decisions (per the tracking issue)

# Decision Status
1 Batch handling: loop (batch_idx, position) pairs explicitly, never once per position shared across the batch Required correctness, not a style choice — settled by construction
2 Fail-fast (not skip-with-warning) when a source is inactive at any one pair in a batch Needs maintainer sign-off — the alternative (try/except ValueError + warnings.warn + copy the row through unedited) is a two-line hunk if preferred
3 decomposition_cache is caller-owned (Optional[MutableMapping[Tuple[int, int, int], JSpaceDecomposition]]), not a JacobianLens attribute Needs maintainer sign-off, lower risk than #2
4 Warning fires once per coordinate_patch_hooks(...) call, at construction time, naming layers x positions counts (not the exact number of live solves) This plan's own call, not yet reviewed — flagged explicitly here

Please weigh in on #2 and #3 in particular — they're the most likely to get pushback and are cheap
to change if so.

Testing

  • tests/unit/tools/test_jacobian_lens_coordinate_patch.py — model-free core: parity with
    solve_coordinate_patch, batch independence, fail-fast on any inactive source, cache miss/hit
    behavior, cache-hit vs. fresh-solve identical output, mismatched position_labels rejection.
  • tests/unit/tools/test_jacobian_lens.py — unchanged behavior after the conftest extraction
    (byte-identical pass/fail set).
  • tests/unit/tools/test_jacobian_lens_coordinate_patch_hooks.py — wrapper: hook-list shape mirrors
    swap_hooks, once-per-call warning naming layer/position counts, unfitted-layer rejection,
    empty-positions rejection, identical-token rejection, only-requested-positions-change, oracle
    parity with offline coordinate_patch, cache hits skip resolve across repeated hook firings,
    core errors and core warnings propagate uncaught through the hook.
  • tests/integration/test_jacobian_lens.py — one cached GPT-2 test: alpha=0 no-op and
    bit-identical untouched positions through a real run_with_hooks pass.

Local gates green:

  • uv run mypy .Success: no issues found in 397 source files.
  • uv run pytest tests/unit/tools/test_jacobian_lens_coordinate_patch.py tests/unit/tools/test_jacobian_lens.py tests/unit/tools/test_jacobian_lens_coordinate_patch_hooks.py157 passed.
  • source .env && uv run pytest tests/integration/test_jacobian_lens.py -k coordinate_patch_hooks1 passed.

Related work

Out of scope (deferred to a follow-up PR)

Arbitrary multi-slot permutations, any behavioral / causal-swap benchmark (PR3), and
HookedTransformer support (PR4) remain deferred to follow-up PRs.

Checklist

  • Full make test-pr run attached (unit + docstring + acceptance + integration).
  • uv run build-docs run clean (new #### Dynamic coordinate-patch hooks section renders).
  • Reviewer sign-off on design decisions #2 (fail-fast vs. skip-with-warning) and #3
    (caller-owned decomposition_cache shape).

@jlarson4 jlarson4 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together @janmenjayap! Review comments below:

Comment thread .cursor/rules/transformerlens.mdc
Comment thread transformer_lens/tools/analysis/jacobian_lens.py
Comment thread docs/source/content/jacobian_lens_fitting.md Outdated
Comment thread docs/source/content/jacobian_lens_fitting.md
Comment thread transformer_lens/tools/analysis/jacobian_lens_coordinate_patch.py Outdated
Comment thread transformer_lens/tools/analysis/jacobian_lens_coordinate_patch.py
Comment thread tests/mps/test_mps_basic.py Outdated
Comment thread docs/source/index.md Outdated
- Add solve_coordinate_patch_positions: model-free loop applying solve_coordinate_patch independently to every (batch, position) pair in a [batch, num_positions, d_model] chunk
- Support an optional caller-owned decomposition_cache keyed (layer, batch_idx, position); a hit skips get_sparse_decomposition, a miss solves once and stores
- Fail fast (no try/except) when a source is inactive at any pair, so no partial write reaches the activation tensor
- Validate 3-D activations and matching position_labels length
- Add unit tests for offline parity, batch independence, fail-fast, cache miss/hit behavior, and label-length validation
- move D_MODEL/N_LAYERS/D_VOCAB/SEQ_LEN/SKIP_FIRST/CORPUS, _ToyBlock,
  _CausalSumBlock, _ToyTokenizer, _ToyBridge, _NotABridge, _lens, and the
  toy_model fixture from test_jacobian_lens.py into conftest.py
- import the shared symbols back into test_jacobian_lens.py and remove the
  now-unused contextmanager and HookPoint imports
- centralize the shared test setup so additional hook tests can reuse the
  same fixtures without duplicating roughly 90 lines of test code
- Add JacobianLens.coordinate_patch_hooks, a forward-hook variant of the
  offline coordinate_patch primitive, following the swap_hooks builder pattern
- Solve one J-space coordinate patch per (batch, position) pair at each layer
  via solve_coordinate_patch_positions, with an optional caller-owned
  decomposition_cache keyed (layer, batch_idx, position)
- Require positions explicitly and reject identical source/target tokens; fail
  fast on any inactive source rather than partially patching a batch
- Warn once per call naming the layer x position count that performs a live
  vocabulary-scale solve on every cache miss
- Add a dedicated test file covering shape parity, warning-once, cache
  hit/miss, oracle parity with offline coordinate_patch, and uncaught error
  and warning propagation through the hook
Export solve_coordinate_patch_positions from tools.analysis and document the distinction between offline and dynamic/hooked coordinate patching.

Replace the stale dynamic-patching claim in jacobian_lens_fitting.md and add documentation covering required positions, caller-owned decomposition_cache, per-pair fail-fast behavior, and the once-per-call cost warning.

Add a cached GPT-2 integration test verifying alpha=0 is an exact no-op and untouched positions remain bit-identical.
…__init__

Commit 02ffc27 accidentally added sparse_probing imports and __all__
entries to transformer_lens/tools/analysis/__init__.py without committing
the sparse_probing module itself. On CI (which only checks out tracked
files) importing transformer_lens.tools.analysis raised
ModuleNotFoundError, breaking package import and failing every job that
imports transformer_lens (unit, docstring, compatibility, benchmark,
coverage, notebooks). Remove the out-of-scope sparse_probing exports;
that work belongs to its own PR.
…s from PR scope

- Restore .cursor/rules/transformerlens.mdc deleted on this branch (unrelated to coordinate-patch hooks)

- Remove sparse_probing import + test from tests/mps/test_mps_basic.py that broke tests/mps collection with ModuleNotFoundError (accidentally picked up from another branch)

- Remove content/sparse_probing toctree line from docs/source/index.md referencing a nonexistent page
…ition loop

- Scatter `x + patch.delta` instead of the detached `patch.patched` in solve_coordinate_patch_positions, keeping x's graph; bitwise identical forward for float32 activations.

- Add test_solve_coordinate_patch_positions_preserves_gradient_to_patched_position: asserts finite nonzero grad at the patched position (exactly 0 before) and bitwise-equal forward output.
- Add test_solve_coordinate_patch_positions_populates_batch_component_of_cache_key: two distinct batch rows share one cache at the same (layer, position); assert both (layer, 0, position) and (layer, 1, position) populate with different active supports.
- Closes the gap where the batch-agnostic [1, 1, d_model] fixtures let a collapsed (layer, 0, position) key pass the whole unit tier; the new test fails when the key drops batch_idx.
…er plural-layer install

- Strengthen coordinate_patch_hooks Raises note: source must be in every patched position's top-k active support after earlier band hooks edit the residual, not merely active on a clean pass; substitute/swap remove the source coordinate, so stacking layers/positions makes it progressively harder.

- Add the first plural (two-layer) end-to-end hook test, pinning the per-closure layer=layer/dictionary=dictionary binding via distinct per-layer dictionaries and per-layer cache keys.
…reuse caveat

- Drop the model.generate(...) claim from the dynamic coordinate-patch hooks section: on GPT-2 with the published lens, generate re-decomposes each freshly generated token whose top-k support generally lacks the source concept, so fail-fast aborts generation. Keep run_with_hooks(...).

- Document that the (layer, batch_idx, position) cache key uses the chunk-local position index, so decomposition_cache is valid only across identical-chunking passes; warn against reuse across decode steps (use_past_kv_cache prefill vs per-step shapes collide) or differently-shaped prompts.
@janmenjayap
janmenjayap force-pushed the feat/jacobian-lens-coordinate-patch-hooks branch from 8074f2e to 454b587 Compare September 10, 2026 04:21
@jlarson4

Copy link
Copy Markdown
Collaborator

Looks good, great work @janmenjayap Merging now

@jlarson4
jlarson4 merged commit c1ea673 into TransformerLensOrg:dev Sep 10, 2026
26 checks passed
@janmenjayap
janmenjayap deleted the feat/jacobian-lens-coordinate-patch-hooks branch September 10, 2026 15:29
@jlarson4 jlarson4 mentioned this pull request Sep 11, 2026
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants