Skip to content

sae: add scorer correction adapter, ported to the canonical SAE stack - #26

Open
mgagvani wants to merge 2 commits into
sae/foundationfrom
sae/scorer-adapter
Open

sae: add scorer correction adapter, ported to the canonical SAE stack#26
mgagvani wants to merge 2 commits into
sae/foundationfrom
sae/scorer-adapter

Conversation

@mgagvani

Copy link
Copy Markdown
Owner

Trains a lightweight residual adapter over the frozen planner's proposal
scorer, predicting the scorer's own error from SAE latents and correcting
scores additively at inference. The base planner and proposal set are
untouched - only the ranking changes.

Ported off the legacy sparseAE L1 stack onto models/sae.py +
sae_utils.py. sparseAE.py, its colliding 88-line sae_utils.py, and
test_sparseAE.py are deliberately not merged: main now has exactly one SAE
implementation and one checkpoint format.

Port surface was narrow - SparseAutoencoder already exposes .encoder, so
infer_residual_dims needed only a dim source, and Ben had already ported
compute_hidden_activations and load_model_and_sae in new_sae_utils. The
substantive change is activation capture: sae.internal_acts (state hung off
the SAE) becomes an explicit ActivationCapture threaded through
extract_batch_targets.

Adds --latent_source {sae,raw}. The paper's control - the same adapter fed
the dense activation the SAE reconstructs instead of its sparse latents -
was previously run by hand and left no reproducible path. It is now a flag,
persisted into the residual checkpoint and read back by the evaluator, so
the control arm can be re-run exactly.

sae_utils gains three things this needs:

  • ActivationCapture.register/remove - the hook handle was previously
    discarded, so hooks could not be detached.
  • freeze_module / set_eval_mode, carried over from the legacy utils.
  • normalize_compiled_state_dict, salvaged from sparseAE.py. A checkpoint
    saved from a compiled SAE stores encoder._orig_mod.weight; loading it
    into an eager module under strict=False silently dropped every tensor
    and yielded a randomly-initialised SAE that still appeared to load.

Verified on CPU against the real block-3 checkpoint: hook fires, captured
activation is (B,1,384), latents are (B,384), and the handle detaches.

Trains a lightweight residual adapter over the frozen planner's proposal
scorer, predicting the scorer's own error from SAE latents and correcting
scores additively at inference. The base planner and proposal set are
untouched - only the ranking changes.

Ported off the legacy sparseAE L1 stack onto models/sae.py +
sae_utils.py. sparseAE.py, its colliding 88-line sae_utils.py, and
test_sparseAE.py are deliberately not merged: main now has exactly one SAE
implementation and one checkpoint format.

Port surface was narrow - SparseAutoencoder already exposes .encoder, so
infer_residual_dims needed only a dim source, and Ben had already ported
compute_hidden_activations and load_model_and_sae in new_sae_utils. The
substantive change is activation capture: sae.internal_acts (state hung off
the SAE) becomes an explicit ActivationCapture threaded through
extract_batch_targets.

Adds --latent_source {sae,raw}. The paper's control - the same adapter fed
the dense activation the SAE reconstructs instead of its sparse latents -
was previously run by hand and left no reproducible path. It is now a flag,
persisted into the residual checkpoint and read back by the evaluator, so
the control arm can be re-run exactly.

sae_utils gains three things this needs:
  * ActivationCapture.register/remove - the hook handle was previously
    discarded, so hooks could not be detached.
  * freeze_module / set_eval_mode, carried over from the legacy utils.
  * normalize_compiled_state_dict, salvaged from sparseAE.py. A checkpoint
    saved from a compiled SAE stores encoder._orig_mod.weight; loading it
    into an eager module under strict=False silently dropped every tensor
    and yielded a randomly-initialised SAE that still appeared to load.

Verified on CPU against the real block-3 checkpoint: hook fires, captured
activation is (B,1,384), latents are (B,384), and the handle detaches.
Copilot AI review requested due to automatic review settings July 29, 2026 06:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a69e65d1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +361 to +365
backbone_model, sae, capture, _ = load_model_and_sae(
model_checkpoint_path=model_checkpoint_path,
sae_checkpoint_path=sae_checkpoint_path,
block_idx=block_idx,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject SAE checkpoints trained for a different block

When --sae_checkpoint_path points directly to a .pt file whose saved block_index differs from --block_idx, this call hooks the requested backbone block but loads the incompatible SAE without checking its metadata. Because every current block has the same 384-dimensional activation, the mismatch passes all shape checks and silently trains and evaluates on semantically invalid latents, corrupting experiment results; compare the checkpoint's recorded block_index with block_idx before proceeding.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an SAE-informed residual “scorer correction” adapter that learns to predict the proposal scorer’s error from SAE latents (or a reproducible “raw activation” control arm) and applies an additive correction at inference, without changing the base planner or proposal set.

Changes:

  • Introduces sae_scorer_correction.py with training/eval utilities, residual model, ranking loss, and checkpoint I/O (including persisted --latent_source {sae,raw}).
  • Updates sae_utils.py to make activation capture explicit and removable, adds freeze/eval helpers, and normalizes compiled vs eager SAE checkpoint key formats.
  • Adds a CLI evaluation script for scorer residual checkpoints.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/camera-based-e2e/test_sae_scorer_residual.py Adds a CLI evaluator for scorer residual checkpoints and metrics reporting.
src/camera-based-e2e/sae_utils.py Adds hook handle lifecycle management and compiled-checkpoint key normalization for SAE loading.
src/camera-based-e2e/sae_scorer_correction.py Implements the residual correction model, training loop, metrics, and checkpoint export/load.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +5 to +9
from __future__ import annotations

import argparse
import json
from collections import defaultdict
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