| """Attention-pooling sequence probes over raw model hidden states. |
| |
| Replaces the SAE-feature + max-pool probe (``finetune_adv.LayerProbes``) with a |
| per-layer probe that reads the *sequence* of per-token hidden states directly, |
| with no fixed pooling. Motivation: |
| |
| * Operating on raw ``h_l`` (d_model) instead of SAE latents removes the |
| dependence on whether the concept is well represented in the SAE dictionary |
| (the relation-to-relation variance we observed). |
| * A learned-query multi-head attention pool replaces ``masked_max_pool`` / |
| ``masked_mean_pool`` so no information is discarded by a hard reduction, and |
| the probe can co-adapt to *where* the concept lives in the sequence. This |
| follows HyperSteer (arXiv:2506.03292), whose best variant reads the base-LM |
| residual-stream sequence via attention rather than a pooled summary. |
| |
| The probe stays fully differentiable w.r.t. the hidden states, so the |
| adversarial suppression gradient still flows from the probe logits into the |
| LoRA parameters during the model step. |
| |
| API mirrors ``LayerProbes`` so the training loop can swap between them: |
| probes.forward_logits(feats_seq, key_padding_mask) -> list[(B,)] |
| where ``feats_seq[l]`` is (B, T, d_model) and ``key_padding_mask`` is (B, T) |
| bool with True at PAD positions to ignore. |
| """ |
| from __future__ import annotations |
|
|
| import torch |
| import torch.nn as nn |
|
|
|
|
| class _AttnPoolProbe(nn.Module): |
| """Single-layer attention-pooling probe: (B, T, d_model) -> (B,) logit.""" |
|
|
| def __init__( |
| self, |
| d_model: int, |
| d_probe: int = 256, |
| n_heads: int = 4, |
| n_ctx_blocks: int = 1, |
| dropout: float = 0.0, |
| spectral_norm: bool = False, |
| ): |
| super().__init__() |
| self.in_proj = nn.Linear(d_model, d_probe) |
| self.ctx_blocks = nn.ModuleList([ |
| nn.TransformerEncoderLayer( |
| d_model=d_probe, |
| nhead=n_heads, |
| dim_feedforward=4 * d_probe, |
| dropout=dropout, |
| batch_first=True, |
| norm_first=True, |
| ) |
| for _ in range(n_ctx_blocks) |
| ]) |
| |
| self.query = nn.Parameter(torch.zeros(1, 1, d_probe)) |
| nn.init.normal_(self.query, std=0.02) |
| self.pool_attn = nn.MultiheadAttention( |
| d_probe, n_heads, dropout=dropout, batch_first=True |
| ) |
| self.norm = nn.LayerNorm(d_probe) |
| head = nn.Linear(d_probe, 1) |
| self.head = nn.utils.spectral_norm(head) if spectral_norm else head |
|
|
| def forward( |
| self, x: torch.Tensor, key_padding_mask: torch.Tensor | None |
| ) -> torch.Tensor: |
| |
| x = self.in_proj(x) |
| for blk in self.ctx_blocks: |
| x = blk(x, src_key_padding_mask=key_padding_mask) |
| B = x.shape[0] |
| q = self.query.expand(B, -1, -1) |
| pooled, _ = self.pool_attn( |
| q, x, x, key_padding_mask=key_padding_mask, need_weights=False |
| ) |
| pooled = self.norm(pooled.squeeze(1)) |
| return self.head(pooled).squeeze(-1) |
|
|
| def token_logits( |
| self, x: torch.Tensor, key_padding_mask: torch.Tensor | None |
| ) -> torch.Tensor: |
| """Per-token logits (no attention pooling): (B, T, d_model) -> (B, T). |
| |
| Reuses in_proj + context blocks + norm + head so a single probe module |
| supports both the per-sequence (attention-pooled) and per-token readouts. |
| Used by the general per-token hallucination probe. |
| """ |
| x = self.in_proj(x) |
| for blk in self.ctx_blocks: |
| x = blk(x, src_key_padding_mask=key_padding_mask) |
| return self.head(self.norm(x)).squeeze(-1) |
|
|
|
|
| class SequenceLayerProbes(nn.Module): |
| """One attention-pooling probe per monitored layer, run on raw hidden states.""" |
|
|
| def __init__( |
| self, |
| layer_indices: list[int], |
| d_model: int, |
| d_probe: int = 256, |
| n_heads: int = 4, |
| n_ctx_blocks: int = 1, |
| dropout: float = 0.0, |
| spectral_norm: bool = False, |
| ): |
| super().__init__() |
| self.layer_indices = list(layer_indices) |
| self.d_model = d_model |
| self.probes = nn.ModuleList([ |
| _AttnPoolProbe( |
| d_model, d_probe=d_probe, n_heads=n_heads, |
| n_ctx_blocks=n_ctx_blocks, dropout=dropout, |
| spectral_norm=spectral_norm, |
| ) |
| for _ in layer_indices |
| ]) |
| self._idx = {l: i for i, l in enumerate(self.layer_indices)} |
|
|
| def forward_logits( |
| self, |
| feats_seq: dict[int, torch.Tensor], |
| key_padding_mask: torch.Tensor, |
| ) -> list[torch.Tensor]: |
| |
| w_dtype = self.probes[0].in_proj.weight.dtype |
| return [ |
| self.probes[self._idx[l]]( |
| feats_seq[l].to(w_dtype), key_padding_mask |
| ) |
| for l in self.layer_indices |
| ] |
|
|
| def forward_token_logits( |
| self, |
| feats_seq: dict[int, torch.Tensor], |
| key_padding_mask: torch.Tensor, |
| ) -> list[torch.Tensor]: |
| """Per-token logits per layer: list of (B, T). For the general probe.""" |
| w_dtype = self.probes[0].in_proj.weight.dtype |
| return [ |
| self.probes[self._idx[l]].token_logits(feats_seq[l].to(w_dtype), key_padding_mask) |
| for l in self.layer_indices |
| ] |
|
|
| def forward( |
| self, feats_seq: dict[int, torch.Tensor], key_padding_mask: torch.Tensor |
| ) -> list[torch.Tensor]: |
| return [torch.sigmoid(z) for z in self.forward_logits(feats_seq, key_padding_mask)] |
|
|
|
|
| def sequence_layer_probes_from_checkpoint( |
| path: str, |
| layer_indices: list[int], |
| d_model: int, |
| device: torch.device | str | None = None, |
| d_probe: int = 256, |
| n_heads: int = 4, |
| n_ctx_blocks: int = 1, |
| spectral_norm: bool = False, |
| ) -> SequenceLayerProbes: |
| """Load a SequenceLayerProbes state_dict (handles a ``module.`` DDP prefix).""" |
| sd = torch.load(path, map_location="cpu", weights_only=True) |
| if any(k.startswith("module.") for k in sd): |
| sd = {k.replace("module.", "", 1): v for k, v in sd.items()} |
| use_sn = spectral_norm or any("weight_orig" in k for k in sd) |
| probes = SequenceLayerProbes( |
| layer_indices, d_model, d_probe=d_probe, n_heads=n_heads, |
| n_ctx_blocks=n_ctx_blocks, spectral_norm=use_sn, |
| ) |
| probes.load_state_dict(sd, strict=True) |
| if device is not None: |
| dev = device if isinstance(device, torch.device) else torch.device(device) |
| probes = probes.to(dev) |
| return probes |
|
|