Activation Catcher¶
Capture module inputs/outputs during a forward pass and select them by
product_id. See Architecture (“Capturing activations as products”)
for how this plugs into a teacher supplier.
- class softs.catcher.ModelIOCatcher[source]¶
Capture inputs and/or outputs of named submodules during forward passes.
Hooks are installed once on every submodule (
attach_hooks). Each capture session is then scoped with awithblock that arms a chosen subset of modules and collects their activations into a buffer; outside the block the hooks stay attached but inert.Construction.
modelis the module to instrument -dict(model.named_modules())defines the valid target names (e.g."model.layers.0"or"model.layers.0.mlp").deviceis where captured tensors (inputs and outputs) are moved viatransfer_to_device(); passNoneto leave them in place.Selecting what to capture.
inputsandoutputsare iterables of module names - the dotted keys frommodel.named_modules(). They are independent; a name may appear in either, both, or neither:a name in
inputscaptures theargs/kwargsthe module is called with, via a forward pre-hook (before the module runs);a name in
outputscaptures whatever the module returns, via a forward hook (after the module and its whole subtree have run).
The buffer.
__enter__returns a buffer shapedbuf[name][kind] -> list:buf["model.layers.0"]["inputs"] # list of {"args": ..., "kwargs": ...} buf["model.layers.0"]["outputs"] # list of module return values
One entry is appended per forward call, so running N forwards inside the same
withaccumulates N entries (e.g. to gather a batch of activations).early_exit. With
early_exit=Truea forward is cut short the instant every requested input/output has been captured: the last capturing hook raises an internal_StopForwardto skip the rest of the network. Drive forwards throughrun()(not the model directly) so that signal is caught and the budget re-seeded per call - thenfor batch in loader: catcher.run(**batch)runs every batch, each stopping after the deepest requested module. Output hooks fire in post-order (a parent fires after its children), so requesting the output ofmodel.layers.0stops after layer 0 finishes, not inside itsmlp. A cut-short forward returnsNone(read results frombuf); if a requested module never runs, no exit fires and the forward completes. Withearly_exit=Falseno stop signal is raised, so calling the model directly or viarun()is interchangeable.Example
>>> catcher = ModelIOCatcher(model, torch.device("cuda")) >>> >>> # Full forwards: capture layer 0's input+output over the loader. >>> with catcher(inputs=["model.layers.0"], ... outputs=["model.layers.0"]) as buf: ... for batch in loader: ... catcher.run(**batch) # or model(**batch) here >>> len(buf["model.layers.0"]["outputs"]) # one entry per batch >>> buf["model.layers.0"]["inputs"][0]["args"][0].shape # layer 0 input >>> >>> # Only need layer 5's output? Skip layers 6.. on *every* batch: >>> with catcher.for_product("outputs[model.layers.5]", early_exit=True) as buf: ... for batch in loader: ... catcher.run(**batch) # returns None, cut short >>> hidden = buf["model.layers.5"]["outputs"] # one tensor per batch
Notes
The model is never modified -
run()is a plain model call wrapped in atry/except, so it composes with DDP,torch.compile, etc. Capturing is scoped to thewithblock:__exit__disarms the flag sets, and the returnedbufstays valid for reading afterwards. Hooks fromattach_hooksstay registered untildetach_hooks, so every forward pays a cheap per-module membership check.- for_product(product_id, early_exit=True)[source]¶
Arm a session from a
product_idcapture spec (seeparse_io_spec()).product_idlooks like"inputs[model.layers.0]|outputs[model.layers.0]". Returnsselffor use aswith catcher.for_product(pid) as buf:. RaisesValueErrorif the spec selects nothing andKeyErrorfor unknown module names.
- run(*args, **kwargs)[source]¶
Run one forward under the active capture session.
Use this instead of calling the model directly: it re-seeds the early-exit budget for this forward and absorbs the internal
_StopForwardsignal, sofor batch in loader: catcher.run(**batch)runs every batch. Returns the model’s output, orNoneif early exit cut the forward short. (Withearly_exit=Falseit is just the model call and always returns the real output.)The model is never modified - this is a plain call wrapped in a
try/except, so it composes with DDP,torch.compile, etc.
- softs.catcher.parse_io_spec(product_id)[source]¶
Parse a capture spec string into
(inputs, outputs)module-name lists.The spec names which modules to capture, e.g.:
"inputs[model.layers.0]|outputs[model.layers.0]" "inputs[]|outputs[model.layers.0|model.layers.5]" "outputs[model.layers.0]" # inputs section optional
Items inside the brackets are
|-separated; the separator between theinputs[...]andoutputs[...]blocks is ignored (the regex matches each block independently), so the inner and outer|never clash. Whitespace and empty items are dropped, and a missing section yields[].- Returns:
Two lists of module names (either may be empty).
- Return type:
(inputs, outputs)
- Parameters:
product_id (str)