Coming from Hugging Face output_hidden_states and output_attentions¶
This page is for people who read a transformers model's internals with the keyword arguments
output_hidden_states=True and output_attentions=True (and its attention registry) and want
something below whole-block grain. If one keyword argument gives you what you need, keep using
it. The examples use a two-block GPT-2 built from a config (no download) and were run against
the transformers range TorchLens's test environment installs (transformers>=4.45,<6; the
CI rows install 5.14.1). Checked 2026-11-03.
The same task in both¶
Read the first block's hidden state and the first layer's attention probabilities, loading with eager attention so transformers can return the probabilities.
In transformers¶
# migration-test: tool=torchlens requires=transformers expected=((1, 4, 32), (1, 2, 4, 4), 3)
import torch
import transformers
torch.manual_seed(0)
config = transformers.GPT2Config(n_layer=2, n_embd=32, n_head=2, vocab_size=64, n_positions=16)
config._attn_implementation = "eager"
model = transformers.GPT2LMHeadModel(config).eval()
ids = torch.tensor([[1, 2, 3, 4]])
with torch.no_grad():
native = model(ids, output_hidden_states=True, output_attentions=True)
RESULT = (
tuple(native.hidden_states[1].shape),
tuple(native.attentions[0].shape),
len(native.hidden_states),
)
In TorchLens¶
# migration-test: tool=torchlens requires=transformers expected=(True, True, True)
import torch
import transformers
import torchlens as tl
torch.manual_seed(0)
config = transformers.GPT2Config(n_layer=2, n_embd=32, n_head=2, vocab_size=64, n_positions=16)
config._attn_implementation = "eager"
model = transformers.GPT2LMHeadModel(config).eval()
ids = torch.tensor([[1, 2, 3, 4]])
with torch.no_grad():
native = model(ids, output_hidden_states=True, output_attentions=True)
trace = tl.trace(model, ids)
block0 = tl.extract(model, ids, ["transformer.h.0"])["transformer.h.0"]
pattern0 = trace.modules["transformer.h.0.attn"].facets["pattern"]
last_block = tl.extract(model, ids, ["transformer.h.1"])["transformer.h.1"]
RESULT = (
torch.equal(block0, native.hidden_states[1]),
torch.allclose(pattern0.value, native.attentions[0]),
not torch.equal(last_block, native.hidden_states[-1]), # hidden_states[-1] is after ln_f
)
Translation table¶
| transformers | TorchLens | Notes |
|---|---|---|
model(ids, output_hidden_states=True).hidden_states[i] for i >= 1 |
tl.extract(model, ids, ["transformer.h.<i-1>"]) (the block's module address) |
Equal for every block except the last: transformers applies the final norm (ln_f on GPT-2) to hidden_states[-1], while the last block's own output is before it. |
hidden_states[0] (the embeddings) |
The embedding module's output, or the op after the position embeddings are added | Find it with tl.trace(model, ids).layers. |
model(ids, output_attentions=True).attentions[i] with attn_implementation="eager" |
trace.modules["transformer.h.<i>.attn"].facets["pattern"] |
Under eager attention the softmax is a captured op. |
output_attentions=True under the default sdpa (or flash) backend |
Read the reconstructed facets: tl.trace(model, ids) |
The fused kernel never materializes the probabilities, so transformers has nothing to return there (see below). |
Registering your own function in AttentionInterface |
tl.bridge.transformers.register_attention(model), then with tl.bridge.transformers.record_attention(model) as attn: |
Registers the attention function and its matching mask function together. |
| Queries, keys, values, per-head outputs | module.facets["q"], ["k"], ["v"], ["z"] |
No keyword argument returns these. |
Two traps these keyword arguments set:
- Silent empty attentions under fused backends. With the
sdpaor flash attention backend the probabilities are never computed as a tensor, so the call-leveloutput_attentions=Truegives you no maps, without an error. Load withattn_implementation="eager", or read the TorchLens facets. - The mask trap. A custom function registered in
AttentionInterfacealone gets no attention mask, because transformers builds masks only for keys its mask registry knows; the model then silently computes non-causal attention and its predictions change.tl.bridge.transformers.register_attentionregisters the matching mask function in the same call, so causal and padding masks always arrive. It never registers over a built-in key.
The TorchLens attention backend, on a model with a padded batch:
# migration-test: tool=torchlens requires=transformers expected=((2, 2, 4, 4), True)
import torch
import transformers
import torchlens as tl
torch.manual_seed(0)
config = transformers.GPT2Config(n_layer=2, n_embd=32, n_head=2, vocab_size=64, n_positions=16)
model = transformers.GPT2LMHeadModel(config).eval()
batch = {
"input_ids": torch.tensor([[1, 5, 9, 2], [7, 3, 0, 0]]),
"attention_mask": torch.tensor([[1, 1, 1, 1], [1, 1, 0, 0]]),
}
tl.bridge.transformers.register_attention(model)
with torch.no_grad(), tl.bridge.transformers.record_attention(model, check_sdpa=True) as attn:
model(**batch)
probs = attn["transformer.h.0.attn"] # (batch, heads, query, key)
RESULT = (tuple(probs.shape), torch.count_nonzero(probs[1, :, :2, 2:]).item() == 0)
What TorchLens adds¶
- Grain below the block: queries, keys, values and per-head outputs through
module.facets["q"],["k"],["v"],["z"], and any executed op throughtl.trace(model, ids).layers. - Attention probabilities under the default fused
sdpabackend, as reconstructed facets (tl.trace(model, ids), seedocs/facets.md), whereoutput_attentions=Truereturns nothing. - Attention registered together with its mask function, so a custom function never loses its
causal or padding mask (
tl.bridge.transformers.register_attention). - Selecting by module address after the call, with no keyword argument to pass and no change to how the model is loaded.
What the transformers keyword arguments do that TorchLens does not¶
output_hidden_states=Truereturns every block in one call with no lookups to write, and its last entry includes the final norm. If that tuple is all you need, it costs less than a trace.- The keyword arguments run inside the model's own forward and add no capture step, so nothing is recorded beyond the tuples they return.
- Model-specific outputs (
past_key_valuesand the like) come back in the model's own output object, which is where transformers code expects them.
See where to go instead for tools outside TorchLens's scope.
Using both¶
transformers feeds TorchLens¶
A transformers model is an nn.Module: pass it to tl.trace or tl.extract.
tl.bridge.transformers adds the pieces that need Hugging Face knowledge:
trace_text(model, text) traces a language model from raw text, and register_attention and
record_attention hook the attention interface.
TorchLens feeds transformers¶
tl.bridge.transformers.register_attention(model) registers a TorchLens attention function
in transformers' own AttentionInterface, so a normal model(**batch) call runs through it.
Outside that, results are plain tensors you can hand to any transformers code.
Semantic differences¶
- The last hidden state.
hidden_states[-1]is after the final norm; the last block's own output (tl.extract(model, ids, ["transformer.h.1"])) is before it, so the two differ, and the first example checks that. - Silent versus loud.
output_attentions=Trueunder a fused backend returns no maps and no error; TorchLens attention reads either work from a captured op under eager attention or from a reconstructed facet that says so. - Registration.
AttentionInterfacealone registers no mask function; TorchLens'sregister_attentionregisters both.