Skip to content

fix(evals): run the eval helpers on the model's own device - #1965

Merged
jlarson4 merged 2 commits into
TransformerLensOrg:devfrom
abhishekcpp007:abhishekcpp007/fix-evals-device
Nov 2, 2026
Merged

jlarson4 merged 2 commits into
TransformerLensOrg:devfrom
abhishekcpp007:abhishekcpp007/fix-evals-device

Conversation

@abhishekcpp007

Copy link
Copy Markdown

Description

transformer_lens/evals.py sends work to the wrong device on any machine that is not CUDA.

  • evaluate_on_dataset(..., device="cuda") moves every batch to a literal "cuda".
  • induction_loss(..., device="cuda") allocates its random sequence there too.
  • evaluate() never forwarded a device at all and exposed no parameter for one, so a caller
    had no way to correct it.

On a CPU-only or MPS machine evaluate(model) therefore moves batches to a device the model is
not on. mmlu_eval, in this same module, already does the right thing (.to(model.cfg.device)).

The right value is already computed for us: TransformerLensConfig.__post_init__ sets
self.device = "cuda" if torch.cuda.is_available() else "cpu", so cfg.device is always
populated.

Fix

The three helpers now default device to None and fall back to model.cfg.device.

  • An explicit device= argument still wins, so anyone already passing one is unaffected.
  • Anyone relying on the old implicit "cuda" now gets CUDA anyway whenever it is available,
    since that is what cfg.device resolves to.
  • evaluate gains the device parameter it was missing and forwards it.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Tests

tests/unit/test_evals_device.py, unit tier:

  • evaluate_on_dataset moves batches to the model's device
  • an explicit device still overrides it
  • induction_loss allocates its sequence on the model's device
  • none of the three helpers defaults device to a fixed device any more

It uses a synthetic stub that records the device of each tensor it is handed, so it loads no
model and mocks no model load, per tests/AGENTS.md.

Verification I was able to run, and what I was not

Being straight about this rather than claiming a green run, per AGENTS.md §10.

  • Ran: isort and black over both changed files with the repo's pyproject.toml config.
    Both report no changes, so make check-format is clean on them.
  • Checked by reading: [tool.mypy] exclude covers tests, so the new test file is outside
    mypy's surface. The change in evals.py adds no annotations.
  • Did not run: make unit-test and uv run mypy .. My environment could not install the
    project's dependencies, so I could not execute the suite, and I would rather say so than
    report a pass I did not see. CI is the real run here. If anything in the new test goes red I
    will fix it straight away.

`evaluate_on_dataset` and `induction_loss` defaulted `device` to the literal
string "cuda", and `evaluate` never forwarded a device at all and exposed no
parameter for one. On a CPU-only or MPS machine `evaluate(model)` therefore
moved batches to a device the model was not on, and the caller had no way to
correct it.

`cfg.device` is always populated by TransformerLensConfig.__post_init__, which
resolves to cuda when it is available and cpu otherwise. `mmlu_eval` in this
same module already uses it. The three helpers now default to None and fall
back to `model.cfg.device`, so an explicit argument still wins and callers who
were passing one are unaffected. `evaluate` gains the `device` parameter it was
missing and forwards it.

Adds tests/unit/test_evals_device.py: the helpers move tensors to the model's
device, an explicit device still overrides it, and none of the three defaults
to a fixed device any more. Uses a synthetic stub, so it loads no model and
stays at the unit tier.

@jlarson4 jlarson4 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for fixing this bug @abhishekcpp007, just a couple small comments about testing effectiveness & a quick comment cleanup. Please let me know if you have any questions

"""An explicit argument still wins over cfg.device."""
model = StubModel(device="cpu")

evaluate_on_dataset(model, make_loader(), truncate=1, device="cpu")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The stub's cfg.device and the explicit argument are both "cpu", so this passes even if the argument is ignored. Could you give the stub a different cfg.device, and do the same for test_induction_loss_builds_tokens_on_the_models_device below on line 68?

Comment thread tests/unit/test_evals_device.py Outdated
assert all(device.type == "cpu" for device in model.seen_devices)


def test_eval_helpers_no_longer_hardcode_cuda() -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This asserts the declared defaults rather than what the helpers do, and nothing in the file calls evaluate, so the device=device forwarding inside it has no test. Could you replace this with a test that monkeypatches evals.DATASET_NAMES and evals.DATASET_LOADERS to a stub loader and checks that evaluate(model, device="cpu") reaches the stub on CPU?

Comment thread transformer_lens/evals.py Outdated
@torch.inference_mode()
def evaluate_on_dataset(model, data_loader, truncate=100, device="cuda"):
def evaluate_on_dataset(model, data_loader, truncate=100, device=None):
# Default to where the model actually is. cfg.device is always populated, so

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we this comment to something like # None defaults to the model's own device, a hardcoded "cuda" would crash CPU/MPS callers?

Give the stub a "meta" cfg.device in the explicit-device and induction
tests so they can tell the argument apart from cfg.device, add an explicit
device test for induction_loss, and replace the signature check with a
test that runs evaluate against a monkeypatched stub loader. Also shorten
the comment in evaluate_on_dataset.
@abhishekcpp007

Copy link
Copy Markdown
Author

Thanks for the review @jlarson4! Pushed 0d2b1eb. The explicit-device tests now give the stub a "meta" cfg.device, so they fail if the argument gets ignored, and I added the same kind of test for induction_loss. I swapped the signature check for a test that monkeypatches DATASET_NAMES and DATASET_LOADERS with a stub loader and checks that evaluate(model, device="cpu") reaches it on CPU. Comment's updated too. I checked by dropping the forwarding locally and the 3 new tests fail.

@jlarson4

jlarson4 commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Great work on this, thanks for the updates. Approved, I'll merge once this it passes CI

@jlarson4
jlarson4 merged commit b80b825 into TransformerLensOrg:dev Nov 2, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants