MAGIC (Unrolling)¶
MAGIC attributes evaluation loss to individual training examples by backpropagating through the entire training trajectory.
We provide a Trainer class that takes differentiable training steps and handles all three phases of MAGIC attribution. We support FSDP training using the bergson.magic.dtensor_patch runtime patch, which makes PyTorch’s DTensor redistribution twice-differentiable (pytorch/pytorch#160509). The patch is applied in memory, so no torch source files are modified.
How it works¶
MAGIC attribution has three phases:
Forward training with checkpoints: Fine-tune the model, saving intermediate checkpoints at each step.
Evaluate: Compute the evaluation loss and its gradients with respect to the final model parameters.
Backward through training: Backpropagate the evaluation gradients through the checkpointed training steps using reverse-mode autodiff, accumulating attribution scores for each training example (or token).
The Trainer class handles all three phases. It uses torchopt for functional (stateless) differentiable optimization.
Usage¶
CUDA_VISIBLE_DEVICES="0" bergson magic runs/magic-ckpts \
--data.dataset NeelNanda/pile-10k \
--query.dataset NeelNanda/pile-10k \
--query.split "train[:8]" \
--model EleutherAI/pythia-14m
Contrastive queries¶
When --contrast.dataset is set the aggregated query loss minus the
aggregated control loss is attributed. Requires an aggregated query. See
examples/powerseeking_contrastive_query for an example.
Output files¶
After a run completes, run_cfg.run_path contains:
config.yaml— the serialized configuration needed to replicate the run.checkpoints/— forward-pass trainer checkpoints, pluslog_history.json(the per-step learning rates the schedule produced). With the defaultcleanup_ckpts=Truethe backward pass deletes checkpoints as it consumes them, but multi-query runs keep them for re-use.scores/— a score directory, the same self-describing format the scoring pipeline writes.info.jsonrecordsattribute_tokensandnum_scores. Read it withload_scores_loss_signed(), which returns(scores, multi_query):Per-document:
(num_train_docs, 1), indexed directly bydoc_id.Per-token:
(num_examples, seq_len), indexed by(example_idx, token_idx), with every epoch’s pass over an example summed into its row. An example is a chunk of the training dataset whenchunk_length > 0or otherwise a row/document, andexample_idxis its position in the training set after chunking, before each epoch is shuffled.Per-query (
query.aggregation: none) adds a trailing query axis, so per-token per-query scores are(num_examples, seq_len, num_query_docs).
Per-token scores are stored ragged — a row holds
length - 1values — and are unpacked back into a dense grid on load.score_vs_step.png— if matplotlib is installed (pip install 'bergson[plot]') the medianlog10|score|of each optimizer step against training step is emitted, for assessing run stability.per_query/q{i}.pt— per-query runs only. The score tensor for query documenti, written as soon as that query’s backward finishes so an interrupted run resumes without redoing completed queries. The trailing query axis inscores/is these tensors stacked.scores/doc_ids.npy— written for per-token runs, shape(num_examples, seq_len). Each entry is the document id at that token position, so the scores can be summed over examples and token positions into per-document scores:from bergson.data import load_scores_loss_signed scores, _ = load_scores_loss_signed("runs/magic/scores") doc_ids = torch.from_numpy(np.load("runs/magic/scores/doc_ids.npy")) num_train_docs = int(doc_ids.max()) + 1 # Trailing axis is the query axis on per-query runs, absent otherwise; # reshaping to it keeps both cases on one path. flat = scores.reshape(doc_ids.numel(), -1) per_doc = torch.zeros(num_train_docs, flat.shape[1], dtype=flat.dtype) per_doc.scatter_add_(0, doc_ids.flatten()[:, None].expand_as(flat), flat)
per_doccomes back as(num_train_docs, num_query_docs), or(num_train_docs, 1)for a single-query run.With
data.chunk_length == 0each example is one document and scores for documentican be accessed likescores[i]. Withdata.chunk_length > 0a chunk may hold several documents, and a document may span chunks. In this case, usedoc_idsto aggregate the scores for a document over chunks.
Models and optimizer state¶
optimizer.pt— second moments of the trained optimizer state, whensave_optimizer_stateis not"none".retrained/base/andretrained/subset_{i}/— the fully-trained model and each leave-subset-out retrain, with tokenizer, whensave_models=True.retrained/baseis the query baselineevaluate_retrainedreads; thesubset_{i}retrains only exist for a run that actually validates (bergson validate, orbergson magic --skip_validation False). (Commands outside the leave-k-out family — plainbergson train— write the trained model tomodel/instead.)
Smoothness¶
MAGIC is valid when the model training function you differentiate through is smooth with respect to the data weightings (metasmooth). There a few heuristics known to encourage smoothness:
Use the Muon optimizer
Increase batch size
Scale model outputs down
Clip gradients
Pre-activation batch norm
QK norm
Tune weight decay
Many of these methods boil down to “Identify and manage spikes in your training loss.” You can measure your smoothness with bergson metasmoothness.
Core components¶
Trainer: Functional trainer that supports forward training with checkpoints and backward-through-training.
from bergson.magic import BackwardState, DataStream, Trainer, TrainerState
import torchopt
# Initialize
opt = torchopt.adam(lr=1e-4)
trainer, state = Trainer.initialize(model, opt)
# Forward training with checkpoints
stream = DataStream(dataset, batch_size=4, device="cuda")
state = trainer.train(state, stream, save_dir="checkpoints/")
# Compute eval gradients, then backward through training
bwd_state = trainer.backward("checkpoints/", stream, bwd_state, state)
scores = bwd_state.weight_grads # attribution scores
DataStream: Wraps a dataset with differentiable per-document (or per-token) weights that receive gradients during the backward pass.
# Per-document attribution
stream = DataStream(dataset, batch_size=4, device="cuda")
# Per-token attribution
stream = DataStream(dataset, batch_size=4, device="cuda", weight_shape=(len(dataset), max_length))
DTensor patch: For multi-GPU runs with FSDP, apply the DTensor patch before any distributed operations:
from bergson.magic.dtensor_patch import apply_dtensor_patch
apply_dtensor_patch()
# Your MAGIC worker call here
Per-token vs per-document attribution¶
By default, DataStream creates a 1D weight tensor [num_train_docs] for per-document attribution, indexed by doc_id. By passing a 2D tensor [num_examples, max_length] as the weight_shape parameter, each token receives its own attribution score. The weighted_causal_lm_ce loss function supports both shapes.
To use per-token attribution, set model.loss_function = weighted_causal_lm_ce so the model uses the weighted loss during training.
from bergson.utils.math import weighted_causal_lm_ce
model.loss_function = weighted_causal_lm_ce
Key implementation details¶
Functional optimization:
torchopt.adam(or similar) provides a pure-function optimizer whose state is a pytree of tensors. This allowstorch.autograd.gradto differentiate through optimizer updates.Checkpoint strategy: By default, checkpoints are saved at
sqrt(N)intervals, givingO(sqrt(N))memory andO(N)recomputation cost.save_modealso supportsall(O(N)space) and the original MAGIC paper’slog(O(log N)space,O(N log N)time).FSDP compatibility: The DTensor runtime patch adds a
NestedRedistributeautograd function that makes the FSDP all-gather/reduce-scatter differentiable through second-order backward passes.Loss weighting:
weighted_causal_lm_cemultiplies per-token cross-entropy by the DataStream weights before averaging. During backward-through-training, autograd accumulates gradients into these weights, yielding the attribution scores.