Skip to content

Latest library refinements, including experiments - #1

Merged
sx-liu merged 73 commits into
mainfrom
dev
Sep 29, 2026
Merged

sx-liu merged 73 commits into
mainfrom
dev

Conversation

@TonyZhou05

Copy link
Copy Markdown
Collaborator

No description provided.

sx-liu and others added 30 commits July 20, 2026 03:14
sx-liu and others added 26 commits September 17, 2026 20:50
…reaches

The end-of-backward reconciliation (drop forward captures whose backward
never came, re-evaluate completion) was queued only from a hooked layer's
own backward hook, and only when a hooked layer had a trainable parameter.
With frozen layers (include_frozen=True) it therefore never ran: under
non-reentrant gradient checkpointing the forward fires twice and the
backward once, so the step never completed and the buffers grew step over
step; a step whose hooked layers the backward never reached stalled the
same way, silently.

- A forward hook on the model root arms a tensor hook on the output, which
  queues the end-of-backward callback from the outermost backward task
  whatever the hooked layers' parameters.
- At the end of the backward a step in which no hooked layer received a
  gradient raises, naming the layers and the reason (the loss must depend
  on their output through a parameter that requires grad; frozen layers
  need a trainable parameter upstream, typically the input embedding).
  Layers left out of an otherwise complete step are reported once per
  layer set.  The streamer's own "no gradient captured" message is
  reduced to a guard for a loss_fn that never runs the model.
- Registration warns when include_frozen=True hooks layers without a
  trainable parameter (and what that requires of the model), and when
  include_frozen=False left nothing hooked because every candidate was
  frozen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Hooks store a layer's factors raw, and every product kernel preprocesses
them on each call (the bias column of a linear layer, the normalized
activation of a norm layer, the unfolded patches of a convolution).  A query
layer that stayed factorized was therefore re-preprocessed for every train
block, and a train block once per test block it met.

finalize_factors (attribution/utils.py) replaces each raw factorized layer
by its final factors (Factorized with module_kwargs=None), releasing the raw
payload as it goes.  _route_test_rep applies it to the query layers that
stay factorized, under the same cache budget as the dense copies, and the
base score_sources applies it to every transformed train block, so every
inner-product attributor inherits it.  Embedding layers stay raw: their
materialization needs module_kwargs.  ops.effective_dims reads final factors
directly instead of misreading a finalized convolution.

Scores are unchanged (exact); at 32-token sequences the scoring loop is
about 5% faster.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cross_gram's factorized route issued three einsums per layer (two 4-D
token grams and their contraction) and the materialized route two.  For
short sequences the kernels take microseconds and an einsum's equation
parsing and view building dominated: scoring a block of 96 layers cost
22 ms against 8 ms for the same math as two matrix products, a multiply
and a sum.  The 4-D einsums were also slow on the device: at 2048 tokens
the factorized route drops from 433 ms to 167 ms per block.

Both routes are now plain matrix products.  Results are unchanged, the
two sides may still differ in token count, and the measured crossover
between the routes stays where the FLOP rule puts it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With a live source, asking for the next block runs the next forward and
backward pass, which is where the memory peak sits.  A finished block was
still resident then, up to three times over:

* rebatch_blocks ran torch.cat over the pending tensors of every dense
  block even when there was one (the usual case, block size == batch
  size) -- a full copy;
* it kept the whole previous block as its metadata template, and the last
  layer through a loop variable;
* the scoring loop still held the finished batch when it requested the
  next one.

A single-block batch now reuses the block's tensors, only the metadata is
remembered, and the references are dropped before the next request.  At
2048 tokens and batch 4 the peak falls from 29.2 GB to 20.2 GB (the
forward/backward floor), at 512 tokens from 10.8 GB to 6.8 GB; scores are
unchanged and the step is a few percent faster where blocks are dense.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
At full dimension the fitted K-FAC / EK-FAC factors are as large as the
squared layer widths, and only one layer's factors are needed at a time.
KroneckerAttributor(factor_cache_residency=...) holds them in a
TensorCache the attributor owns: "memory" (host memory), "disk" (files),
or "tiered" (host memory that spills to files once its budget, a share of
the free host memory, is used).  None, the default, keeps them on the
device.  The fit, the damping and a persisted fit loaded from fisher_dir
place each layer's factors in the cache; damping and scoring bring one
layer to the device at a time.  A tensor the damping leaves unchanged
(EK-FAC's eigenbases) is not copied again, EK-FAC's intermediate
eigenbases use the same cache, and a cache is closed once a newer fit has
replaced it, which deletes any spilled files.

The Kronecker accumulators hand their buffers over on
result(consume=True) instead of returning a normalized copy, so the fit
never holds the covariances twice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
K-FAC and EK-FAC attributors take covariances_at_capture (default True).
When the attributor collects the train gradients into a store -- cache(),
or attribute() with a gradient_cache_residency -- it attaches a
KroneckerCovarianceCallback to that pass.  The covariances are reduced
across ranks, kept for the store and, for a disk store, saved next to the
train gradients (kronecker_covariances.pt); prepare_scoring and fit use
them instead of sweeping the store, as long as the source reads every
stored step.  A materialized "logra" store can therefore be fitted and
scored through the one-call workflows.  With the option off the fit is
swept from the stored factors as before.

The callback's module docstring now describes this use and states that
the covariances follow the capture's projection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
attribute() with a gradient_cache_residency and cache() sharded the test
set across the ranks, so under DDP/FSDP each rank's store held only its
own queries and the assembled score matrix was missing the columns of the
other ranks.  The store path now captures the whole test set on every
rank, as the live path does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The per-run options of the K-FAC family are arguments of attribute,
attribute_from_cache, fit, cache and cache_preconditioned_test, handled
by _set_options, and no longer of the constructor:
covariances_at_capture, factor_cache_residency and the new
relative_damping, which reads damping as a multiple of the mean
eigenvalue of whatever it is added to (each covariance factor, the
corrected EK-FAC spectrum, each dense Fisher block).

fisher_dir is accepted by the live attribute() too.  A directory that
holds a fit is loaded; an empty one receives the fit of the call, so later
calls over the same training set skip the pre-pass.  The file records what
defines the fit (algorithm, mode, non_kfac_strategy,
direct_fim_max_params, layer_name, selected_training_steps) and a
mismatch raises; damping and relative_damping are recorded but applied
after loading, so one fit serves any damping.  The attributor keeps the
fit it last saved to or loaded from a directory, and a call naming the
same unchanged file does not read it again.  The file is written under a
per-rank name and renamed into place.

The warning of the dense-Fisher fallback states how many of the
K-FAC-eligible layers it covers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
OptimizerSnapshot reads a layer's optimizer state under
FullyShardedDataParallel (use_orig_params=True) and fully_shard.  Under
the wrapper each rank lays its slice of a state tensor out on zeros and
the slices are summed across ranks, one all_reduce per layer read; under
fully_shard a DTensor state is gathered with full_tensor().  A layer is
bound to the parameters the optimizer holds from inside its backward,
where the wrapper exposes the flat parameter and fully_shard the
unsharded view; the HookManager's preconditioner and the
OptimizerStateCallback bind there.  Only use_orig_params=False is
refused.

OptimizerStateCallback now copies the moments it records: a dense read
of a bias-free layer on CPU was a view of the optimizer's tensor, so the
recorded pre-step moments followed the next update.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fully_shard without a mesh builds its device mesh on the default
accelerator, so with a GPU visible the test's collectives ran on CUDA
against a gloo process group and the worker died with SIGSEGV.  The test
passes an explicit CPU mesh.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ut and routing benchmarks

Fidelity: the setting runs on GPT-2, Qwen2.5 (0.5B, 1.5B, 3B) and, under
OLMo-core's trainer, OLMo-2-1B (FSDP over four GPUs) and OLMo-3-7B (one GPU);
masked AdamW-influence at 512, 2048 and 8192 coordinates per layer; the
scripts split into utils/truth (the ground truth) and utils/attribution (one
script per method), over the shared setting, protocol and helpers in utils/.

Benchmark: the throughput ladder on four GPUs at each library's largest
batch, the routing sweep across sequence lengths, timing of Bergson's worker
start-up, attribution-only timing for every table.

The ordinary-versus-invasive capture benchmark moves to experiments/capture,
with its own adapter, over the runner, data and logger of experiments/benchmark.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
HookManager(sample_hash_fields=[...]) restricts a sample's content hash to
the named forward arguments (a kwarg by name, a positional argument by
index), an alternative to sample_id_key.  A trainer that passes per-sample
inputs which are not part of a sample's identity, such as a label mask or
the sample's position in the dataset, can then hash the same fields as a
query loop that passes only the labels, so one identity serves both
stores.  A named field missing from a step raises; the scheme stays the
content hash, so records are stamped sample_id_key=None as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A batch is padded to its longest sequence, so the padded tokens, the
attention mask and the -100 label fill carried the batch's length into a
sample's identity: the same sample hashed differently in two batches.  With
an attention_mask in the inputs, hash_sample keeps only the unmasked
positions of every field of the mask's length (left or right padding alike)
and leaves the mask, all ones after that, out of the digest.  A sample's
hash is then that of its unpadded tokens and equals the hash of the raw
dataset item.  Without a mask nothing is stripped; a field of another
length, such as a shifted labels row, is kept whole.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DataSelectionCallback takes scoring_kwargs and selection_kwargs; the
example passed threshold, threshold_mode and score_mode flat and raised
TypeError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ttings

make check runs ruff's lint and format checks over the library, the tests
and the examples; make test runs the suite on one GPU; make darglint checks
docstrings against signatures.  pytest.ini declares the gpu marker the tests
use; .darglint holds darglint's google-style settings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A non-blocking copy from the device to pageable host memory returns before
the data has arrived, so a test block on the CPU read stale factors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@TonyZhou05
TonyZhou05 requested a review from sx-liu September 28, 2026 03:54
@sx-liu
sx-liu merged commit 4d5f835 into main Sep 29, 2026
8 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.

2 participants