Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/heretic/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ def get_residuals(
) -> Tensor:
# We only generate one token, and we return the residual vectors
# at that token position, for each prompt and layer.
_, outputs = self.generate(
inputs, outputs = self.generate(
prompts,
max_new_tokens=1,
output_hidden_states=True,
Expand All @@ -632,6 +632,32 @@ def get_residuals(
# This cast is valid because we passed output_hidden_states=True above.
hidden_states = cast(tuple[tuple[FloatTensor]], outputs.hidden_states)[0]

if hidden_states is None:
# Some remote-code models (e.g. ZGCM-1) rebuild the model inputs in their own
# prepare_inputs_for_generation, returning a fresh dict rather than merging the
# remaining kwargs, so output_hidden_states never reaches forward() and generate()
# returns no hidden states. A plain forward pass over the same prompts yields the
# hidden states at the last prompt position that generate would have reported.
forward_inputs = dict(inputs)

# Supply left-padding-aware position ids, as prepare_inputs_for_generation normally
# would. A model that falls back to a plain arange over the padded sequence would

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, I'm really unconvinced by this. You're making all kinds of assumptions about what specific implementations do in that method, despite the whole point of custom code being that they are free to do whatever they want there.

It seems to me that the whole thing is really a bug in the custom code of the model. They are supposed to follow a certain contract and they don't, which breaks standard Transformers functionality. The correct fix is to fix the model code upstream. Fixing it in Heretic by blindly hoping that if the model breaks hidden states, it won't break certain other assumptions feels extremely brittle.

# otherwise place the real tokens at the wrong absolute positions. This is a no-op
# for purely relative (RoPE) position encoding, where shifting a contiguous block
# of tokens cancels out, but it is required for absolute position encodings.
attention_mask = forward_inputs.get("attention_mask")
if attention_mask is not None and "position_ids" not in forward_inputs:
position_ids = attention_mask.long().cumsum(-1) - 1
position_ids.masked_fill_(attention_mask == 0, 0)
forward_inputs["position_ids"] = position_ids

with torch.no_grad():
hidden_states = self.model(
**forward_inputs,
output_hidden_states=True,
use_cache=False,
).hidden_states

# The returned tensor has shape (prompt, layer, component).
residuals = torch.stack(
# layer_hidden_states has shape (prompt, position, component),
Expand Down