Skip to content

feat(init): coupled component initialization (weight_init: kaiming | coupled) - #991

Open
Antovigo wants to merge 5 commits into
goodfire-ai:mainfrom
Antovigo:feature/coupled_init
Open

feat(init): coupled component initialization (weight_init: kaiming | coupled)#991
Antovigo wants to merge 5 commits into
goodfire-ai:mainfrom
Antovigo:feature/coupled_init

Conversation

@Antovigo

@Antovigo Antovigo commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the coupled component initialization to the JAX trainer as pd.weight_init: kaiming | coupled (kaiming stays the default).

With the "coupled" initialization scheme:

  • For matrices with d_in ≤ d_out, the U vectors lie in the column space of the matrix. For matrices with d_in > d_out, the V vectors lie in the row space.
  • The two vectors are coupled, such that one is the image of the other according to W

Concretely:

  • If a matrix’s d_in ≤ d_out, draw V as random vectors normalized to unit norm, and set U = (W @ V)ᵀ
  • If d_in > d_out, draw unit-norm U and set V = (Wᵀ @ Uᵀ).

Testing

This was tested on the targeted decomposition of Llama-8B L18 on arithmetic tasks, with 3 different random seeds per condition.

Black is normal kaiming init, red is the coupled init introduced in this PR. Green initializes the components within the row/column space of the matrices without coupling. It outperforms the baseline, but is not as good as the coupled init, so I didn’t include it in the PR.

image

In practice this saves about 1000 steps on the arithmetic task, i.e. 5% of training.

Validation on Layer 17 for the same task (evals taken directly at init):
image

Validation on a full-data decomposition: initialization + 100 steps of faithfulness warmup + 200 steps of training for the "Jose" decomposition (Pile llama 4L):
image

imageimage

The coupled initialization is also in the right direction for full-data decomposition, but it’s less clear whether this changes anything in practice. On the other hand, it runs only once and has no practical cost, so it probably doesn’t hurt to use it.

Note that these tests where done using the old Pytorch implementation, which should behave the same as the Jax version.

For the new Jax implementation:

  • New param_decomp/tests/test_component_init.py: the defining W-image property on both orientation branches (unit-norm seeds, span membership), and independent per-site draws within a stacked shape group.
  • TMS 5-2 engine smokes run end-to-end for both weight_init modes (init → faith warmup → training → eval → checkpoint)

Antovigo and others added 5 commits July 25, 2026 01:31
…coupled)

JAX port of the torch-lineage coupled init (experiment/8B_targeted
e5cdcbc): unit-norm seed on the narrow side, wide side its raw W-image
(d_in <= d_out: v_c ~ unit norm, U_c <- (W v_c)^T; else u_c ~ unit norm,
V_c <- W^T u_c). No C-dependent rescale — components sit at W's natural
scale and the component sum equals W restricted to the seed span, the
delta carrying the complement.

pd.weight_init selects the init inside init_decomposition (match
dispatch, kaiming default keeps the eval_shape restore path and stored
configs untouched). W is recovered protocol-only via weight_deltas on
zero V/U — no new DecomposedModel method, no per-target changes. The
draw is vmapped per shape group like init_stack_arrays so the compiled
init doesn't scale with site count, and the placed init grew an optional
target_weights arg (riding as jit arguments, not closure constants)
instead of a second placement helper.

Verified: unit tests (seed determinism, W-image/span properties, stacked
same-shape sites), make check, and TMS 5-2 engine smokes for both modes.
The full core suite segfaults at test_llama8b under this box's memory
pressure with or without this change; the test passes in isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVg2f69eq8mhDrpZZ3ZEmM
…t helper

The optional-target_weights overload made init_component_stacks_placed
select the init implicitly (None => kaiming) — too implicit, and a dead
end for future weight_init options. Back to named entry points
(init_component_stacks_placed / init_coupled_component_stacks_placed),
each a thin wrapper over _init_component_stacks_via, which owns the one
eval_shape -> component_stacks_shardings -> jit(out_shardings) dance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVg2f69eq8mhDrpZZ3ZEmM
…weight_init selector

Per review: per-mode entry points (init_kaiming_* / init_coupled_*) made
every call site import one name per option and left the bare
init_component_stacks name falsely suggesting universality. Now there is
ONE public init_component_stacks(sites, key, weight_init, target_weights)
— mode is an explicit argument (match-dispatched; target_weights required
iff coupled, asserted), the per-mode draws are private _kaiming/_coupled
stack-array helpers, and the placed variant carries the same signature.
The WeightInit literal lives in components.py and PDConfig reuses it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVg2f69eq8mhDrpZZ3ZEmM
Drop the component-sum test (an algebraic corollary of the exact
U = (W V)^T assertion) and the seed-determinism test (a property of
JAX's keyed PRNG, not of this code). What remains: the defining W-image
property on both orientation branches, and the multi-site shape-group
stacking wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVg2f69eq8mhDrpZZ3ZEmM
…t recovery

DecomposedModel grows target_weight(name) -> frozen W in stored dtype —
the torch lineage's ComponentModel.target_weight, restored. weight_deltas
in every target is rewritten on top of it, and the coupled init reads
W directly instead of the weight_deltas(zeros) trick, which cost a full
zero V/U tree, an eager fp32 copy of every site's W, and all-site zero
matmuls at startup. The fp32 upcast now happens inside the jitted init,
per shape group.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVg2f69eq8mhDrpZZ3ZEmM
@Antovigo
Antovigo force-pushed the feature/coupled_init branch from 1848ab5 to eb3b5bc Compare July 25, 2026 01:31
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.

1 participant