Distillation¶
Activation capture, closed-form / fitted regression, and metrics for distilling a target block into a single layer.
polyweave.distill ¶
Activation-space distillation: fit a single layer to a module's I/O map.
The future GPT-2 / Pythia / SwiGLU experiment replaces a transformer feed-forward
sub-block with one position-wise layer, trained by regressing the block's cached
(input, output) activation pairs. This sub-package is the model-agnostic
machinery for that:
capture — forward-hook collection of a submodule's (input, output) pairs
metrics — relative MSE and R^2 (coefficient of determination)
regression — ``fit_layer``: MSE-regress any nn.Module onto cached pairs,
tracking the multiplicative-recruitment gate if the layer has one
Nothing here imports transformers; capture works on any nn.Module, so
the same harness fits a synthetic target in the tests and a real FFN later.
IOCapture ¶
IOCapture(module: Module, *, max_rows: Optional[int] = None, device: str = 'cpu', flatten_leading: bool = True)
Context manager that records inputs/outputs of module during forward.
Usage::
with IOCapture(model.transformer.h[6].mlp) as cap:
for batch in loader:
model(batch) # forward passes drive the hook
X, Y = cap.pairs() # [N, in], [N, out]
Leading dims (batch, sequence, ...) are flattened so each token becomes one independent training example, matching the position-wise nature of an FFN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
module
|
Module
|
the submodule to tap. |
required |
max_rows
|
Optional[int]
|
stop accumulating once this many flattened rows are collected
( |
None
|
device
|
str
|
where to store captured tensors (default |
'cpu'
|
flatten_leading
|
bool
|
flatten all but the last dim into rows (default |
True
|
Source code in polyweave/distill/capture.py
pairs ¶
Return the concatenated (X, Y) activation pairs.
Source code in polyweave/distill/capture.py
DistillResult
dataclass
¶
DistillResult(train_losses: List[Tuple[int, float]] = list(), val_mse: float = float('nan'), val_rel_mse: float = float('nan'), val_r2: float = float('nan'), val_rmse: float = float('nan'), val_cosine: float = float('nan'), recruit_curve: List[Tuple[int, float]] = list(), num_params: int = 0)
Outcome of fitting one layer to activation pairs.
recruit_delta
property
¶
Final − initial recruitment gate, or None if not tracked.
collect_io ¶
collect_io(module: Module, run_fn: Callable[[], None], *, max_rows: Optional[int] = None, device: str = 'cpu') -> Tuple[torch.Tensor, torch.Tensor]
Convenience wrapper: run run_fn under an :class:IOCapture and return pairs.
run_fn should drive whatever forward passes exercise module (e.g. a
loop feeding text batches through a language model).
Source code in polyweave/distill/capture.py
cosine_similarity ¶
Mean per-row cosine similarity between predicted and target vectors.
Each row (token) contributes the cosine of the angle between its predicted
and true activation vectors; we average over rows. Complementary to R²: it
ignores per-token magnitude and reads only directional agreement, so a
layer can track the shape of the output manifold even where it mis-scales.
1.0 is perfect alignment, 0.0 orthogonal.
Source code in polyweave/distill/metrics.py
r2_score ¶
Coefficient of determination R² = 1 - SS_res / SS_tot.
Computed over all elements against the global target mean. 1.0 is a
perfect fit; 0.0 matches the constant-mean predictor; negative is worse
than predicting the mean.
Source code in polyweave/distill/metrics.py
relative_mse ¶
Squared error normalised by target energy: ||y - ŷ||² / ||y||².
Scale-free and 1.0 for the trivial all-zeros predictor (when the target
has zero mean), so it reads as a fraction of the signal left unexplained.
Source code in polyweave/distill/metrics.py
rmse ¶
Root mean squared error in the raw activation units.
Scale-dependent (unlike relative_mse / r2_score), so it is only
comparable across candidates fitted to the same target block — which is
exactly how the distillation experiment uses it.
Source code in polyweave/distill/metrics.py
fit_closed_form_linear ¶
fit_closed_form_linear(layer: Module, X: Tensor, Y: Tensor, *, val_frac: float = 0.2, device: str = 'cpu') -> DistillResult
Solve an affine map X -> Y in closed form (least squares) and load it into
layer (an nn.Linear), reporting the same held-out metrics as fit_layer.
This is the exact linear baseline: on ill-conditioned transformer activations a
first-order optimiser can leave a plain nn.Linear badly underfit (it may need
tens of thousands of steps to converge), which silently inflates the apparent
nonlinearity of the target. The closed-form solution removes that confound — it is
the true linear ceiling against which the multiplicative / depth candidates are
judged. Uses the same fixed tail validation split as :func:fit_layer, so the
numbers are directly comparable.
Source code in polyweave/distill/regression.py
fit_layer ¶
fit_layer(layer: Module, X: Tensor, Y: Tensor, *, steps: int = 2000, lr: float = 0.001, batch_size: int = 256, weight_decay: float = 0.0, val_frac: float = 0.2, eval_every: int = 100, device: str = 'cpu', seed: int = 0, log_fn: Optional[Callable[[str], None]] = None) -> DistillResult
MSE-regress layer onto (X, Y) and report held-out fit + recruitment.
The last val_frac of the rows form a fixed validation split (the inputs
are assumed already shuffled at capture time — tokens are independent rows).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer
|
Module
|
module mapping |
required |
X
|
Tensor
|
input activations, shape |
required |
Y
|
Tensor
|
target activations, shape |
required |
steps
|
int
|
optimisation steps (minibatches). |
2000
|
lr
|
float
|
AdamW learning rate. |
0.001
|
weight_decay
|
float
|
AdamW weight decay. |
0.0
|
batch_size
|
int
|
minibatch size sampled (with replacement) from the train split. |
256
|
val_frac
|
float
|
fraction of rows held out for validation metrics. |
0.2
|
eval_every
|
int
|
record train loss + recruitment gate every this many steps. |
100
|
device
|
str
|
compute device. |
'cpu'
|
seed
|
int
|
seed for minibatch sampling. |
0
|
log_fn
|
Optional[Callable[[str], None]]
|
optional progress sink. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
DistillResult
|
class: |
DistillResult
|
|
|
DistillResult
|
recruitment-gate curve (if the layer exposes |
|
DistillResult
|
|
Source code in polyweave/distill/regression.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |