Skip to content

v3.7.2 - #1675

Merged
jlarson4 merged 13 commits into
mainfrom
dev
Aug 15, 2026
Merged

v3.7.2#1675
jlarson4 merged 13 commits into
mainfrom
dev

Conversation

@jlarson4

Copy link
Copy Markdown
Collaborator

Description

A series of Bug fixes aimed at improving compatibility with HookedTransformer, and expanding overall hook coverage

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have not rewritten tests relating to key interfaces which would affect backward compatibility

koriyoshi2041 and others added 13 commits August 12, 2026 08:53
* Fixed issue 1647

* OLMo fixes

* Granite resolution

* comment cleanup
* initial numerics fix

* Additional clarification and bug cleanup
* fix DeepSeek V2 dense MLP hooks

* Isolate DeepSeek compatibility fixtures
)

* fix(bridge): respect use_parallel_residual in the GPTNeoX adapter

The NeoX adapter hardcoded the parallel-residual wiring, ignoring HF's
use_parallel_residual. On the sequential branch HF computes a genuine
post-attention residual that post_attention_layernorm reads, but
ParallelBlockBridge pops the hook_resid_mid alias by design -- so the
hook was silently missing on exactly the checkpoints that have one, and
cfg.parallel_attn_mlp reported True for a model that is not parallel.
Logits stayed correct because the bridge delegates to HF's own block, so
nothing raised.

All six RedPajama-INCITE checkpoints in the registry are
GPTNeoXForCausalLM with use_parallel_residual=false, so resid-mid
patching, attribution and SAE work were unavailable on registered
models. Pythia is genuinely parallel and is unaffected.

Select the block class from the flag, mirroring the guards already in
stablelm.py:121 and falcon.py:148, and add use_parallel_residual to
_HF_PASSTHROUGH_ATTRS so the adapter can see it -- reading the resolved
cfg.parallel_attn_mlp instead would flip hand-built configs to
sequential, since it defaults to False while HF's NeoX default is True.
Mirror the same hardcode on the HookedTransformer side.

Add test_parallel_residual_identities.py asserting the identity
ParallelBlockBridge documents in its own docstring
(resid_post == resid_pre + attn_out + mlp_out) across all seven adapters
that use it, exercising the three config-switchable families in both
wirings. Fixtures are seeded tinies from local HF configs, so no hub
access is needed.

Fixes #1644

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(bridge): honour parallel_attn_mlp on caller-supplied NeoX configs

Review feedback from @jlarson4 on #1649.

TransformerBridgeConfig has no use_parallel_residual field, so on the
build_bridge_from_module(..., tl_config=...) path the getattr default
fired and a config with parallel_attn_mlp=False still got
ParallelBlockBridge with no hook_resid_mid. Fall back to
parallel_attn_mlp before defaulting to HF's True.

The existing _make_cfg fixture relied on the old hardcode, so it now
states parallel_attn_mlp=True explicitly -- a caller-supplied NeoX config
is otherwise indistinguishable from one that asked for sequential, since
the dataclass default is False.

Wrap the tiny-model seeding in test_parallel_residual_identities.py in
torch.random.fork_rng(devices=[]), matching bridge.py:360. Seeding ran
only on a cache miss, so it leaked RNG state into later unseeded tests
depending on ordering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(bridge): pin use_parallel_residual passthrough

The fallback added for caller-supplied configs made the
_HF_PASSTHROUGH_ATTRS entry redundant -- reverting it left every test
green, since sources/transformers.py already derives parallel_attn_mlp
and the fallback picks it up. That only holds while TransformerBridgeConfig
keeps defaulting parallel_attn_mlp to False, so assert the adapter sees
HF's own flag rather than resting on two defaults agreeing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#1666)

* initial numerics fix

* Additional clarification and bug cleanup

* Fixing issues with MoE and Dense hooks

* test cleanup and improvements
* initial numerics fix

* Additional clarification and bug cleanup

* Fixing issues with MoE and Dense hooks

* test cleanup and improvements

* Mixtral 5.x bug fix
…g them as matrices (#1669)

* initial numerics fix

* Additional clarification and bug cleanup

* Fixing issues with MoE and Dense hooks

* test cleanup and improvements

* Mixtral 5.x bug fix

* Quantization bugs

* Missing tests restored

* Cleanup of potential errors

* fix formatting
* feat(jacobian_lens): add J-space sparse-decomposition solver

- Add `get_sparse_decomposition` to decompose an activation into a k-sparse nonnegative combination of J-lens vectors (Gurnee et al., 2026).
- Support `nonnegative_orthogonal_matching_pursuit` (default, exact NNLS re-solve) and `gradient_pursuit` algorithms.
- Return both the nonnegative coordinates and the orthogonal-projection J-space component.
- Distinguish the projection from the coefficient reconstruction; the projection residual matches `swap_hooks`.
- Keep the implementation model-free by operating directly on the raw dictionary tensor.
- Add tests covering both algorithms, exact-resolve NNLS correctness, a brute-force optimum oracle, and input validation.

Part of #1539 (Tier 2).

* feat(jacobian_lens): add full-vocabulary lens-vector dictionary

- Add `JacobianLens.lens_vector_dictionary(model, layer)` returning the `[d_vocab, d_model]` dictionary whose rows are the J-lens vectors `v_t = J[layer]^T W_U[:, t]`.
- Cache the dictionary per (layer, device) and release it in `clear_device_cache`, so a sparse decomposition can reuse it; document its vocabulary-sized memory cost.
- Add tests asserting the dictionary matches `lens_vectors` over every token, is cached and invalidated by `clear_device_cache`, and rejects an unfitted layer.

* feat(jacobian_lens): add JacobianLens.decompose wrapper and exports

- Add `JacobianLens.decompose(model, activation_or_prompt, layer, *, position, k, algorithm)` decomposing either a raw activation vector or the `blocks.{layer}.hook_out` activation at a prompt position, validating inputs before building the dictionary.
- Build and cache the layer dictionary via `lens_vector_dictionary` and solve with `get_sparse_decomposition`.
- Export `JSpaceDecomposition` and `get_sparse_decomposition` from `transformer_lens.tools.analysis`.
- Add end-to-end tests for the raw-activation and prompt paths, the algorithm passthrough, and the input-validation error paths.

* test(jacobian_lens): add real-model decompose tests and docs

- Add a GPT-2 integration test (regular CI): `decompose` on a real
  `blocks.6.hook_out` activation returns k nonnegative atoms, the
  non-J-space residual is orthogonal to every selected J-lens vector,
  and the J-space component plus residual recover the activation.
- Add a slow gemma-2-2b-it integration test validating `decompose` on
  the published lens artifact: support size, nonnegative coordinates,
  in-vocabulary token ids, and component-plus-residual reconstruction.
- Document J-space sparse decomposition in `jacobian_lens_fitting.md`:
  the `decompose` API, local coordinates versus the orthogonal-projection
  J-space component, and the paper's variance facts with closed-model
  caveats.

* docs(jacobian_lens): cite the decomposition algorithm sources

Add a References section to the decomposition module docstring: Gurnee
et al. (2026) for the J-space method, Pati et al. (1993) for the greedy
orthogonal-matching-pursuit selection, Blumensath & Davies (2008) for the
gradient-pursuit update, and Lawson & Hanson (1974) for the active-set
nonnegative least-squares re-solve.

* fix(jacobian_lens): complete and independently validate NNLS

Resolve PR #1596 review comments 1 and 3 as one numerical-correctness unit:
the drop-only active-set approximation could strand an atom that is optimal
later, returning a non-KKT point (the reviewer measured 4/288 GPT-2
decompositions off, relative dual violation up to 0.55).

Solver (`_nonnegative_least_squares`):
- Replace the drop-only loop with the full Lawson-Hanson active-set method, so
  a released atom can re-enter. Solve the passive set in float64 with an
  explicit pseudoinverse rank threshold.
- Fail closed: the feasibility corrections use the classical `3 * num_active`
  budget with a separate admission safeguard; an exhausted budget, an invalid
  line-search step, a stalled correction, or a failed KKT check raises
  `RuntimeError` rather than clamping and returning an unverified vector.
- Enforce the passive-set invariant and guard the blocking-ratio step against
  zero denominators and zero-current/zero-candidate degeneracy.
- Use one scale- and dtype-aware tolerance policy (`_nnls_tolerances`) for dual
  feasibility, coefficient cleanup, and the KKT test.
- Validate the KKT conditions (`_validate_nnls_kkt`) before returning, in both
  the float64 work dtype and the caller's result dtype.

Independent validation (tests):
- Add `_reference_nnls`, a brute-force support-enumeration NNLS that shares no
  code with the solver, and compare objectives *two-sidedly* over many shapes.
- Add an independent `_assert_nnls_kkt` certificate (primal/dual feasibility,
  stationarity, complementarity) with a deliberately looser, scale-aware
  tolerance, used on rank-deficient, duplicate-column, near-collinear,
  boundary, zero-target, and jointly-rescaled systems.
- Add a fail-closed test proving the safeguard raises instead of returning an
  unverified vector, plus dtype/device, zero-row-invariance, and realistic
  width (768x25) checks.

Document the new public contract: `decompose` now raises `RuntimeError` on a
KKT-uncertifiable solve, and the fitting docs describe the float64 re-solve and
KKT check.

Verification:
- tests/unit/tools/test_jacobian_lens_decomposition.py: 127 passed.
- KKT reproduction over 935 real GPT-2 decompositions on the full branch
  (commits 6-7; the NNLS solver added here is unchanged by commit 7): 0 KKT
  failures, max relative dual violation 2.77e-08 (old drop-only solver: up to
  0.55 on 4/288).
- black/isort/pycln clean; mypy clean on the touched source.

* fix(jacobian_lens): distinguish active and selected support

Resolve PR #1596 review comment 2: the greedy loop ran exactly `k` times and
kept every selected atom in `support`, so a coordinate the NNLS solve drove to
zero still consumed a support slot and was never reconsidered. On GPT-2 this
returned `support` of size 25 with only ~9 nonzero coordinates ("dead slots"),
and the docs presented all 25 as active concepts.

Make `k` an upper bound and separate the two supports the paper conflates:

- Early stopping: selection stops once no unselected atom is materially
  positively correlated with the residual (under nonnegativity a
  negatively-correlated atom cannot reduce it). The stop threshold sits at the
  float32 residual noise floor, so a full-rank target stops instead of
  selecting noise atoms -- this makes the selected support scale-invariant.
- `support` is now the numerically *active* set: selected atoms whose
  contribution `c_i * ||v_i||` is a materially nonzero fraction of `||x||`
  (a scale-invariant activity threshold matching the NNLS coefficient-zeroing
  scale, so `support` equals the strictly-positive NNLS coordinates).
  `coordinates` is aligned with it and every entry is strictly positive.
- New `selected_support` holds every greedily selected atom and defines the
  span for `j_space_component`. Hence `support <= selected_support <= k`.
- `reconstruction` is the nonnegative combination over the active support;
  for the exact NNLS re-solve it equals the projection onto that support (KKT
  stationarity), so it differs from `j_space_component` exactly when a selected
  atom has a zero coordinate. Empty sets (zero target, or a target orthogonal
  to every atom) are handled explicitly.
- `gradient_pursuit`: its projected line-search step uses a backtracking line
  search -- the exact unconstrained step is projected onto the nonnegative
  orthant, then halved until the projected update no longer increases the
  residual (falling back to the feasible incoming point if the bounded search
  finds none), so the objective is monotonically non-increasing. Its `support`
  is filtered to the final active coordinates like the default algorithm.
- Autograd: the scalars used only for control flow (the target norm, the
  early-stop correlation gate, and the gradient-pursuit residual comparison)
  are detached, so the model-free primitive never pulls caller-owned tensors
  out of the graph; the autograd-contract test backpropagates through the
  reconstruction and asserts finite gradients on both inputs.
- Keep `support`/token tensors on CPU and vector outputs on the compute device.

Propagate the contract through the wrapper, tests, and docs: `decompose` and
`jacobian_lens_fitting.md` document `k` as an upper bound and the active vs
selected/projection distinction, naming which operationalization the paper's
variance figures measure. Integration and unit assertions check
`support <= selected_support <= k`, subset, and all-active instead of
`support == k`.

Verification (conda env, python 3.12, H100):
- tests/unit/tools/test_jacobian_lens_decomposition.py + test_jacobian_lens.py:
  219 passed.
- tests/integration/test_jacobian_lens.py (GPT-2): 13 passed.
- mypy clean on the touched source. The only `mypy .` error is pre-existing and
  unrelated (olmo_hybrid.py, from transformers 5.15.0 vs the locked 5.13.0).
- black/isort/pycln clean on the touched files.
- KKT reproduction over 935 real GPT-2 decompositions (3 prompts x 11 source
  layers x all positions, k=25): 0 dead slots (support == strictly-positive
  coordinates), 322/935 with selected > active, 0 KKT failures, max relative
  dual violation 2.77e-08 (old drop-only solver: up to 0.55).
- Scale-invariance of support verified across many seeds; CUDA device-safety
  checked.

---------

Co-authored-by: Jonah Larson <jonahalarson@comcast.net>
* Hook extensions for specialized bridges

* Additional bug fixes
* Hook extensions for specialized bridges

* Additional bug fixes

* Comment cleanup
@jlarson4
jlarson4 merged commit a640e11 into main Aug 15, 2026
69 of 75 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants