Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| import hashlib | |
| import html | |
| import json | |
| import math | |
| import os | |
| import random | |
| from collections.abc import Iterator, Sequence | |
| from contextlib import contextmanager | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| from .catalog import FeatureCatalog | |
| from .config import SETTINGS, Settings | |
| from .interventions import ( | |
| InterventionSpec, | |
| joint_residual_delta, | |
| normalized_random_control, | |
| residual_delta, | |
| ) | |
| from .metrics import ( | |
| contrastive_log_odds, | |
| decoder_cosine_matrix, | |
| joint_direction_norm_ratio, | |
| js_divergence_from_logits, | |
| reconstruction_metrics, | |
| sequence_logprob_summary, | |
| sparse_topk_cosine, | |
| ) | |
| from .sae import SAEStore, SparseEncoding | |
| def _dtype_from_name(name: str) -> torch.dtype: | |
| name = name.lower().strip() | |
| if name in {'float16', 'fp16', 'half'}: | |
| return torch.float16 | |
| if name in {'bfloat16', 'bf16'}: | |
| return torch.bfloat16 | |
| if name in {'float32', 'fp32'}: | |
| return torch.float32 | |
| raise ValueError(f'Unsupported dtype: {name}') | |
| def _default_device() -> torch.device: | |
| # ZeroGPU exposes CUDA emulation at module load time. Hugging Face recommends | |
| # placing models on CUDA at module scope so startup transfers can be optimized. | |
| if os.getenv('SPACE_ID'): | |
| return torch.device('cuda') | |
| if torch.cuda.is_available(): | |
| return torch.device('cuda') | |
| return torch.device('cpu') | |
| class AnalysisResult: | |
| tokens: list[str] | |
| token_index: int | |
| layer: int | |
| features: SparseEncoding | |
| rows: list[list[object]] | |
| metrics: dict[str, float] | |
| class InterventionResult: | |
| baseline_text: str | |
| modified_text: str | |
| feature_activation: float | |
| delta_activation: float | |
| perturbation_norm: float | |
| js_divergence: float | |
| random_js_divergence: float | |
| random_js_std: float | |
| js_specificity_ratio: float | |
| js_empirical_p: float | |
| random_control_count: int | |
| execution_drift_js: float | |
| execution_drift_mean_logprob: float | None | |
| target_text: str | |
| target_token_count: int | |
| target_tokens: list[str] | |
| baseline_target_prob: float | None | |
| modified_target_prob: float | None | |
| random_target_prob: float | None | |
| baseline_sequence_logprob: float | None | |
| modified_sequence_logprob: float | None | |
| random_sequence_logprob: float | None | |
| sequence_logprob_delta: float | None | |
| random_sequence_logprob_delta: float | None | |
| mean_logprob_delta: float | None | |
| random_mean_logprob_delta: float | None | |
| random_abs_mean_logprob_delta: float | None | |
| random_mean_logprob_std: float | None | |
| target_specificity_ratio: float | None | |
| target_empirical_p: float | None | |
| target_token_rows: list[list[object]] | |
| top_token_rows: list[list[object]] | |
| class LayerSweepResult: | |
| tokens: list[str] | |
| token_index: int | |
| rows: list[list[object]] | |
| class DoseResponseResult: | |
| feature_activation: float | |
| target_tokens: list[str] | |
| execution_drift_mean_logprob: float | |
| execution_drift_js: float | |
| rows: list[list[object]] | |
| class FeatureSetResult: | |
| feature_ids: list[int] | |
| feature_rows: list[list[object]] | |
| perturbation_norm: float | |
| js_divergence: float | |
| random_js_divergence: float | |
| random_js_std: float | |
| js_specificity_ratio: float | |
| js_empirical_p: float | |
| random_control_count: int | |
| execution_drift_mean_logprob: float | |
| execution_drift_js: float | |
| baseline_sequence_logprob: float | |
| modified_sequence_logprob: float | |
| random_sequence_logprob: float | |
| sequence_logprob_delta: float | |
| random_sequence_logprob_delta: float | |
| mean_logprob_delta: float | |
| random_mean_logprob_delta: float | |
| random_abs_mean_logprob_delta: float | |
| random_mean_logprob_std: float | |
| target_specificity_ratio: float | |
| target_empirical_p: float | |
| target_tokens: list[str] | |
| target_token_rows: list[list[object]] | |
| class FeatureSetSweepResult: | |
| target_tokens: list[str] | |
| random_control_count: int | |
| execution_drift_mean_logprob: float | |
| execution_drift_js: float | |
| rows: list[list[object]] | |
| class FeatureInteractionResult: | |
| feature_ids: list[int] | |
| target_tokens: list[str] | |
| rows: list[list[object]] | |
| additive_expected_mean_delta: float | |
| joint_mean_delta: float | |
| interaction_excess_mean_delta: float | |
| normalized_interaction: float | |
| execution_drift_mean_logprob: float | |
| class ConceptContrastResult: | |
| feature_id: int | |
| layer: int | |
| prompts_per_concept: int | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| leading_concept: str | None | |
| leading_ratio: float | None | |
| active_prompt_count: int | |
| total_prompt_count: int | |
| class FeatureTraceResult: | |
| feature_id: int | |
| layer: int | |
| tokens: list[str] | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| active_token_count: int | |
| token_count: int | |
| max_activation: float | |
| max_token_index: int | None | |
| class FeatureGeometryResult: | |
| feature_ids: list[int] | |
| layer: int | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| mean_abs_decoder_cosine: float | |
| max_abs_decoder_cosine: float | |
| joint_ablation_norm: float | |
| independent_norm: float | |
| alignment_ratio: float | |
| class ContrastiveCausalResult: | |
| feature_id: int | |
| layer: int | |
| feature_activation: float | |
| perturbation_norm: float | |
| target_a_tokens: list[str] | |
| target_b_tokens: list[str] | |
| rows: list[list[object]] | |
| baseline_log_odds: float | |
| modified_log_odds: float | |
| delta_log_odds: float | |
| baseline_normalized_preference: float | |
| modified_normalized_preference: float | |
| delta_normalized_preference: float | |
| random_signed_mean_delta: float | |
| random_abs_mean_delta: float | |
| random_delta_std: float | |
| specificity_ratio: float | |
| empirical_p: float | |
| random_control_count: int | |
| class ConceptFeatureDiscoveryResult: | |
| concept: str | |
| layer: int | |
| prompts_per_concept: int | |
| top_n: int | |
| ranking_mode: str | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| candidate_ids: list[int] | |
| default_candidate_id: int | None | |
| current_context_available: bool | |
| current_token_index: int | None | |
| displayed_current_active_count: int | |
| split_half_k: int | None | |
| split_half_shared_count: int | |
| split_half_jaccard: float | None | |
| resample_replicates: int | |
| resample_mean_support: float | None | |
| resample_high_support_count: int | |
| class CandidateCausalScreenResult: | |
| feature_ids: list[int] | |
| target_tokens: list[str] | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| active_feature_count: int | |
| candidate_count: int | |
| execution_drift_mean_logprob: float | |
| execution_drift_js: float | |
| class CandidateSpecificityResult: | |
| feature_ids: list[int] | |
| target_tokens: list[str] | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| active_feature_count: int | |
| candidate_count: int | |
| random_control_count: int | |
| execution_drift_mean_logprob: float | |
| execution_drift_js: float | |
| class CandidateCrossTargetResult: | |
| feature_ids: list[int] | |
| targets: list[str] | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| summary_rows: list[list[object]] | |
| pairwise_rows: list[list[object]] | |
| active_feature_count: int | |
| class FeatureCueScanResult: | |
| feature_id: int | |
| layer: int | |
| prompt_stem: str | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| active_cue_count: int | |
| cue_count: int | |
| class FeatureCueContextResult: | |
| feature_id: int | |
| layer: int | |
| stems: list[str] | |
| cues: list[str] | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| active_condition_count: int | |
| condition_count: int | |
| cue_active_context_counts: dict[str, int] | |
| cue_mean_activations: dict[str, float] | |
| dominant_cue: str | None | |
| dominant_cue_context_count: int | |
| off_dominant_active_count: int | |
| class ParaphraseResult: | |
| tokens_a: list[str] | |
| token_index_a: int | |
| tokens_b: list[str] | |
| token_index_b: int | |
| topk_jaccard: float | |
| sparse_cosine: float | |
| promptwide_jaccard: float | |
| promptwide_cosine: float | |
| shared_top_n: int | |
| top_n: int | |
| rows: list[list[object]] | |
| chart_rows: list[list[object]] | |
| class FeatureLensRuntime: | |
| def __init__(self, settings: Settings = SETTINGS) -> None: | |
| self.settings = settings | |
| self.device = _default_device() | |
| self.model_dtype = torch.float16 if self.device.type == 'cuda' else torch.float32 | |
| self.sae_dtype = _dtype_from_name(settings.sae_dtype) | |
| if self.device.type == 'cpu' and self.sae_dtype != torch.float32: | |
| self.sae_dtype = torch.float32 | |
| self.model = None | |
| self.tokenizer = None | |
| self.sae_store: SAEStore | None = None | |
| self.catalog = FeatureCatalog() | |
| self.load_error: str | None = None | |
| def ready(self) -> bool: | |
| return self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| def ensure_ready(self, preload_saes: bool = False) -> None: | |
| if self.ready: | |
| assert self.sae_store is not None | |
| if preload_saes: | |
| self.sae_store.preload() | |
| return | |
| self.tokenizer = AutoTokenizer.from_pretrained(self.settings.model_id) | |
| if self.tokenizer.pad_token_id is None: | |
| self.tokenizer.pad_token = self.tokenizer.eos_token | |
| self.tokenizer.padding_side = 'left' | |
| self.model = AutoModelForCausalLM.from_pretrained( | |
| self.settings.model_id, | |
| torch_dtype=self.model_dtype, | |
| low_cpu_mem_usage=True, | |
| ) | |
| self.model.to(self.device) | |
| self.model.eval() | |
| self.sae_store = SAEStore( | |
| repo_id=self.settings.sae_repo_id, | |
| layers=self.settings.layers, | |
| device=self.device, | |
| dtype=self.sae_dtype, | |
| top_k=self.settings.sae_top_k, | |
| ) | |
| if preload_saes: | |
| self.sae_store.preload() | |
| self.load_error = None | |
| def token_choices(self, text: str) -> list[tuple[str, int]]: | |
| self.ensure_ready(preload_saes=False) | |
| assert self.tokenizer is not None | |
| ids = self.tokenizer(text, add_special_tokens=True)['input_ids'] | |
| tokens = [self.tokenizer.decode([token_id]) for token_id in ids] | |
| return [(f'{idx}: {token!r}', idx) for idx, token in enumerate(tokens)] | |
| def _inputs(self, text: str) -> dict[str, torch.Tensor]: | |
| assert self.tokenizer is not None | |
| batch = self.tokenizer( | |
| text, | |
| return_tensors='pt', | |
| truncation=True, | |
| max_length=self.settings.max_prompt_tokens, | |
| ) | |
| return {key: value.to(self.device) for key, value in batch.items()} | |
| def _target_ids(self, target_text: str) -> list[int]: | |
| assert self.tokenizer is not None | |
| ids = self.tokenizer(target_text, add_special_tokens=False)['input_ids'] | |
| if not ids: | |
| raise ValueError('Target continuation tokenized to an empty sequence.') | |
| return [int(x) for x in ids] | |
| def _append_target( | |
| self, | |
| prompt_inputs: dict[str, torch.Tensor], | |
| target_ids: Sequence[int], | |
| ) -> dict[str, torch.Tensor]: | |
| prompt_ids = prompt_inputs['input_ids'] | |
| target = torch.tensor( | |
| list(target_ids), | |
| dtype=prompt_ids.dtype, | |
| device=prompt_ids.device, | |
| ).unsqueeze(0) | |
| full_ids = torch.cat([prompt_ids, target], dim=1) | |
| if 'attention_mask' in prompt_inputs: | |
| target_mask = torch.ones( | |
| (prompt_ids.shape[0], len(target_ids)), | |
| dtype=prompt_inputs['attention_mask'].dtype, | |
| device=prompt_ids.device, | |
| ) | |
| attention = torch.cat([prompt_inputs['attention_mask'], target_mask], dim=1) | |
| else: | |
| attention = torch.ones_like(full_ids) | |
| return {'input_ids': full_ids, 'attention_mask': attention} | |
| def _repeat_inputs(inputs: dict[str, torch.Tensor], repeats: int) -> dict[str, torch.Tensor]: | |
| return {key: value.repeat(int(repeats), 1) for key, value in inputs.items()} | |
| def _hidden_from_output(output): | |
| return output[0] if isinstance(output, tuple) else output | |
| def _replace_hidden_in_output(output, hidden: torch.Tensor): | |
| if isinstance(output, tuple): | |
| return (hidden, *output[1:]) | |
| return hidden | |
| def _capture_hook(self, layer: int, bucket: dict) -> Iterator[None]: | |
| assert self.model is not None | |
| def hook(_module, _inputs, output): | |
| hidden = self._hidden_from_output(output) | |
| if 'hidden' not in bucket: | |
| bucket['hidden'] = hidden.detach() | |
| handle = self.model.model.layers[int(layer)].register_forward_hook(hook) | |
| try: | |
| yield | |
| finally: | |
| handle.remove() | |
| def _capture_hooks(self, layers: Sequence[int], buckets: dict[int, dict]) -> Iterator[None]: | |
| assert self.model is not None | |
| handles = [] | |
| for layer in layers: | |
| bucket = buckets[int(layer)] | |
| def hook(_module, _inputs, output, *, target=bucket): | |
| hidden = self._hidden_from_output(output) | |
| if 'hidden' not in target: | |
| target['hidden'] = hidden.detach() | |
| handles.append(self.model.model.layers[int(layer)].register_forward_hook(hook)) | |
| try: | |
| yield | |
| finally: | |
| for handle in handles: | |
| handle.remove() | |
| def _delta_hook(self, layer: int, token_index: int, delta: torch.Tensor) -> Iterator[None]: | |
| assert self.model is not None | |
| applied = {'done': False} | |
| def hook(_module, _inputs, output): | |
| if applied['done']: | |
| return output | |
| hidden = self._hidden_from_output(output) | |
| if hidden.ndim != 3: | |
| return output | |
| seq_len = hidden.shape[1] | |
| idx = self._resolve_index(int(token_index), seq_len) | |
| modified = hidden.clone() | |
| modified[:, idx, :] = modified[:, idx, :] + delta.to(hidden.device, hidden.dtype) | |
| applied['done'] = True | |
| return self._replace_hidden_in_output(output, modified) | |
| handle = self.model.model.layers[int(layer)].register_forward_hook(hook) | |
| try: | |
| yield | |
| finally: | |
| handle.remove() | |
| def _batch_delta_hook( | |
| self, | |
| layer: int, | |
| token_index: int, | |
| deltas: torch.Tensor, | |
| ) -> Iterator[None]: | |
| """Apply one residual delta per batch row in a single model forward.""" | |
| assert self.model is not None | |
| if deltas.ndim != 2: | |
| raise ValueError('deltas must have shape [batch, d_model].') | |
| applied = {'done': False} | |
| def hook(_module, _inputs, output): | |
| if applied['done']: | |
| return output | |
| hidden = self._hidden_from_output(output) | |
| if hidden.ndim != 3: | |
| return output | |
| if hidden.shape[0] != deltas.shape[0]: | |
| raise ValueError('Delta batch size does not match model batch size.') | |
| idx = self._resolve_index(int(token_index), hidden.shape[1]) | |
| modified = hidden.clone() | |
| modified[:, idx, :] = modified[:, idx, :] + deltas.to(hidden.device, hidden.dtype) | |
| applied['done'] = True | |
| return self._replace_hidden_in_output(output, modified) | |
| handle = self.model.model.layers[int(layer)].register_forward_hook(hook) | |
| try: | |
| yield | |
| finally: | |
| handle.remove() | |
| def _resolve_index(token_index: int, seq_len: int) -> int: | |
| idx = int(token_index) | |
| if idx < 0: | |
| idx = seq_len + idx | |
| if idx < 0 or idx >= seq_len: | |
| raise IndexError(f'Token index {token_index} outside prompt length {seq_len}.') | |
| return idx | |
| def _control_seed(text: str, layer: int, key: str, mode: str, coefficient: float) -> int: | |
| payload = f'{text}\0{layer}\0{key}\0{mode}\0{coefficient:.8g}'.encode() | |
| return int.from_bytes(hashlib.sha256(payload).digest()[:4], 'big', signed=False) | |
| def _random_controls(delta: torch.Tensor, seed: int, count: int) -> list[torch.Tensor]: | |
| if int(count) < 1: | |
| raise ValueError('Random-control ensemble must contain at least one direction.') | |
| return [ | |
| normalized_random_control(delta, seed=int(seed) + 104729 * idx) | |
| for idx in range(int(count)) | |
| ] | |
| def _random_effect_summary(values: Sequence[float], target_effect: float) -> tuple[float, float, float, float]: | |
| if not values: | |
| raise ValueError('Random-control values must not be empty.') | |
| tensor = torch.tensor([float(x) for x in values], dtype=torch.float64) | |
| signed_mean = float(tensor.mean().item()) | |
| abs_mean = float(tensor.abs().mean().item()) | |
| std = float(tensor.std(unbiased=False).item()) | |
| empirical_p = float( | |
| (1 + int((tensor.abs() >= abs(float(target_effect))).sum().item())) | |
| / (len(values) + 1) | |
| ) | |
| return signed_mean, abs_mean, std, empirical_p | |
| def _dict_cosine(a: dict[int, float], b: dict[int, float]) -> float: | |
| if not a and not b: | |
| return 1.0 | |
| if not a or not b: | |
| return 0.0 | |
| dot = sum(float(value) * float(b.get(feature_id, 0.0)) for feature_id, value in a.items()) | |
| norm_a = math.sqrt(sum(float(value) ** 2 for value in a.values())) | |
| norm_b = math.sqrt(sum(float(value) ** 2 for value in b.values())) | |
| if norm_a == 0.0 or norm_b == 0.0: | |
| return 0.0 | |
| return float(dot / (norm_a * norm_b)) | |
| def _dict_jaccard(a: dict[int, float], b: dict[int, float]) -> float: | |
| set_a = {feature_id for feature_id, value in a.items() if float(value) > 0} | |
| set_b = {feature_id for feature_id, value in b.items() if float(value) > 0} | |
| union = set_a | set_b | |
| return float(len(set_a & set_b) / len(union)) if union else 1.0 | |
| def _max_pool_encoding(encoding: SparseEncoding) -> dict[int, float]: | |
| indices = encoding.indices.detach().cpu() | |
| values = encoding.values.detach().float().cpu() | |
| if indices.ndim == 1: | |
| indices = indices.unsqueeze(0) | |
| values = values.unsqueeze(0) | |
| pooled: dict[int, float] = {} | |
| for row_ids, row_values in zip(indices.tolist(), values.tolist(), strict=True): | |
| for feature_id, value in zip(row_ids, row_values, strict=True): | |
| value = float(value) | |
| if value <= 0: | |
| continue | |
| feature_id = int(feature_id) | |
| pooled[feature_id] = max(pooled.get(feature_id, 0.0), value) | |
| return pooled | |
| def _encoding_map(encoding: SparseEncoding) -> dict[int, float]: | |
| return { | |
| int(feature_id): float(value) | |
| for feature_id, value in zip( | |
| encoding.indices.detach().cpu().tolist(), | |
| encoding.values.detach().float().cpu().tolist(), | |
| strict=True, | |
| ) | |
| if float(value) > 0 | |
| } | |
| def _analyze_and_pool( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int = -1, | |
| top_n: int = 12, | |
| ) -> tuple[AnalysisResult, dict[int, float]]: | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| if int(layer) not in self.settings.layers: | |
| raise ValueError(f'Layer must be one of {self.settings.layers}.') | |
| inputs = self._inputs(text) | |
| bucket: dict = {} | |
| with self._capture_hook(int(layer), bucket): | |
| self.model(**inputs, use_cache=False) | |
| hidden = bucket['hidden'][0] | |
| idx = self._resolve_index(int(token_index), hidden.shape[0]) | |
| sae = self.sae_store.get(int(layer)) | |
| all_encoding = sae.encode(hidden) | |
| encoding = SparseEncoding( | |
| indices=all_encoding.indices[idx], | |
| values=all_encoding.values[idx], | |
| ) | |
| residual = hidden[idx] | |
| reconstruction = sae.decode_sparse(encoding) | |
| metrics = reconstruction_metrics(residual, reconstruction) | |
| metrics['active_features'] = float(encoding.active_count) | |
| values = encoding.values.float().clamp_min(0) | |
| total = float(values.sum().item()) | |
| metrics['top5_mass_fraction'] = ( | |
| float(values[: min(5, values.numel())].sum().item()) / total if total > 0 else 0.0 | |
| ) | |
| ids = inputs['input_ids'][0].tolist() | |
| tokens = [self.tokenizer.decode([token_id]) for token_id in ids] | |
| rows: list[list[object]] = [] | |
| rank_count = min(int(top_n), encoding.indices.numel()) | |
| for rank in range(rank_count): | |
| feature_id = int(encoding.indices[rank].item()) | |
| activation = float(encoding.values[rank].item()) | |
| rows.append( | |
| [rank + 1, feature_id, activation, self.catalog.hint(int(layer), feature_id)] | |
| ) | |
| result = AnalysisResult( | |
| tokens=tokens, | |
| token_index=idx, | |
| layer=int(layer), | |
| features=encoding, | |
| rows=rows, | |
| metrics=metrics, | |
| ) | |
| return result, self._max_pool_encoding(all_encoding) | |
| def analyze(self, text: str, layer: int, token_index: int = -1, top_n: int = 12) -> AnalysisResult: | |
| result, _ = self._analyze_and_pool(text, layer, token_index, top_n) | |
| return result | |
| def layer_sweep(self, text: str, token_index: int = -1) -> LayerSweepResult: | |
| self.ensure_ready(preload_saes=True) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| inputs = self._inputs(text) | |
| buckets = {int(layer): {} for layer in self.settings.layers} | |
| with self._capture_hooks(self.settings.layers, buckets): | |
| self.model(**inputs, use_cache=False) | |
| seq_len = int(inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), seq_len) | |
| rows: list[list[object]] = [] | |
| for layer in self.settings.layers: | |
| residual = buckets[int(layer)]['hidden'][0, idx] | |
| sae = self.sae_store.get(int(layer)) | |
| encoding = sae.encode(residual) | |
| reconstruction = sae.decode_sparse(encoding) | |
| metrics = reconstruction_metrics(residual, reconstruction) | |
| values = encoding.values.float().clamp_min(0) | |
| positive = values[values > 0] | |
| total = positive.sum() | |
| if positive.numel() <= 1 or float(total.item()) <= 0: | |
| entropy = 0.0 | |
| else: | |
| probs = positive / total | |
| entropy = float( | |
| (-(probs * torch.log(probs)).sum() / math.log(positive.numel())).item() | |
| ) | |
| top5_fraction = ( | |
| float(values[: min(5, values.numel())].sum().item() / total.item()) | |
| if float(total.item()) > 0 | |
| else 0.0 | |
| ) | |
| rows.append( | |
| [ | |
| int(layer), | |
| float(metrics['cosine']), | |
| float(metrics['nmse']), | |
| int(encoding.active_count), | |
| float(values[0].item()) if values.numel() else 0.0, | |
| top5_fraction, | |
| entropy, | |
| ] | |
| ) | |
| ids = inputs['input_ids'][0].tolist() | |
| tokens = [self.tokenizer.decode([token_id]) for token_id in ids] | |
| return LayerSweepResult(tokens=tokens, token_index=idx, rows=rows) | |
| def token_html(self, tokens: list[str], selected_index: int) -> str: | |
| chips = [] | |
| for idx, token in enumerate(tokens): | |
| safe = html.escape(token if token.strip() else repr(token)) | |
| selected = idx == int(selected_index) | |
| cls = 'token selected' if selected else 'token' | |
| chips.append(f'<span class="{cls}"><sup>{idx}</sup>{safe}</span>') | |
| return '<div class="token-wrap">' + ''.join(chips) + '</div>' | |
| def _top_token_rows(tokenizer, baseline_logits: torch.Tensor, modified_logits: torch.Tensor, k: int = 8): | |
| p = torch.softmax(baseline_logits.float(), dim=-1) | |
| q = torch.softmax(modified_logits.float(), dim=-1) | |
| union_ids = torch.unique(torch.cat([torch.topk(p, k).indices, torch.topk(q, k).indices])) | |
| rows = [] | |
| for token_id in union_ids.tolist(): | |
| token = tokenizer.decode([int(token_id)]) | |
| bp = float(p[token_id].item()) | |
| mp = float(q[token_id].item()) | |
| rows.append([repr(token), bp, mp, mp - bp]) | |
| rows.sort(key=lambda row: max(row[1], row[2]), reverse=True) | |
| return rows[: min(len(rows), 12)] | |
| def _target_rows( | |
| self, | |
| target_ids: Sequence[int], | |
| baseline_token_logps: Sequence[float], | |
| modified_token_logps: Sequence[float], | |
| random_token_logps: Sequence[float], | |
| ) -> list[list[object]]: | |
| assert self.tokenizer is not None | |
| rows = [] | |
| for idx, (token_id, bp, mp, rp) in enumerate( | |
| zip( | |
| target_ids, | |
| baseline_token_logps, | |
| modified_token_logps, | |
| random_token_logps, | |
| strict=True, | |
| ) | |
| ): | |
| rows.append( | |
| [ | |
| idx, | |
| repr(self.tokenizer.decode([int(token_id)])), | |
| float(bp), | |
| float(mp), | |
| float(rp), | |
| float(mp - bp), | |
| float(rp - bp), | |
| ] | |
| ) | |
| return rows | |
| def intervene( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| feature_id: int, | |
| mode: str, | |
| coefficient: float, | |
| target_text: str = '', | |
| max_new_tokens: int = 24, | |
| ) -> InterventionResult: | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompt_inputs = self._inputs(text) | |
| prompt_len = int(prompt_inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), prompt_len) | |
| sae = self.sae_store.get(int(layer)) | |
| target_ids: list[int] = [] | |
| capture: dict = {} | |
| single_baseline_logits: torch.Tensor | |
| single_baseline_mean: float | None = None | |
| if target_text.strip(): | |
| target_ids = self._target_ids(target_text) | |
| scoring_inputs = self._append_target(prompt_inputs, target_ids) | |
| with self._capture_hook(int(layer), capture): | |
| single_baseline_out = self.model(**scoring_inputs, use_cache=False) | |
| single_baseline_logits = single_baseline_out.logits[0] | |
| _, single_baseline_mean, _ = sequence_logprob_summary( | |
| single_baseline_logits, | |
| prompt_length=prompt_len, | |
| target_ids=target_ids, | |
| ) | |
| else: | |
| scoring_inputs = prompt_inputs | |
| with self._capture_hook(int(layer), capture): | |
| single_baseline_out = self.model(**scoring_inputs, use_cache=False) | |
| single_baseline_logits = single_baseline_out.logits[0] | |
| residual = capture['hidden'][0, idx] | |
| encoding = sae.encode(residual) | |
| original_activation = encoding.activation_for(int(feature_id)) | |
| spec = InterventionSpec(mode=mode, coefficient=float(coefficient)) | |
| delta = residual_delta(sae.decoder_direction(int(feature_id)), original_activation, spec) | |
| seed = self._control_seed( | |
| text, int(layer), str(int(feature_id)), mode, float(coefficient) | |
| ) | |
| random_controls = self._random_controls( | |
| delta, seed=seed, count=self.settings.live_random_controls | |
| ) | |
| zero = torch.zeros_like(delta) | |
| all_deltas = torch.stack([zero, delta, *random_controls], dim=0) | |
| repeated = self._repeat_inputs(scoring_inputs, all_deltas.shape[0]) | |
| with self._batch_delta_hook(int(layer), idx, all_deltas): | |
| edited = self.model(**repeated, use_cache=False) | |
| baseline_logits = edited.logits[0] | |
| modified_logits = edited.logits[1] | |
| random_logits = [edited.logits[row] for row in range(2, edited.logits.shape[0])] | |
| next_idx = prompt_len - 1 | |
| baseline_next_logits = baseline_logits[next_idx] | |
| modified_next_logits = modified_logits[next_idx] | |
| random_next_logits = [row[next_idx] for row in random_logits] | |
| execution_drift_js = js_divergence_from_logits( | |
| single_baseline_logits[next_idx], baseline_next_logits | |
| ) | |
| js = js_divergence_from_logits(baseline_next_logits, modified_next_logits) | |
| random_js_values = [ | |
| js_divergence_from_logits(baseline_next_logits, logits) | |
| for logits in random_next_logits | |
| ] | |
| random_js_mean, random_js_abs_mean, random_js_std, js_empirical_p = ( | |
| self._random_effect_summary(random_js_values, js) | |
| ) | |
| # JS divergence is non-negative, so signed and absolute means are identical up to numerical noise. | |
| random_js_reference = random_js_abs_mean | |
| js_ratio = abs(js) / max(random_js_reference, 1e-12) | |
| baseline_seq = baseline_mean = modified_seq = modified_mean = None | |
| random_seq_mean = random_mean_signed = random_abs_mean = random_mean_std = None | |
| sequence_delta = random_sequence_delta = mean_delta = specificity = target_p = None | |
| target_rows: list[list[object]] = [] | |
| target_tokens: list[str] = [] | |
| bp = mp = rp = None | |
| execution_drift_mean = None | |
| if target_ids: | |
| baseline_seq, baseline_mean, baseline_token_logps = sequence_logprob_summary( | |
| baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| modified_seq, modified_mean, modified_token_logps = sequence_logprob_summary( | |
| modified_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| random_summaries = [ | |
| sequence_logprob_summary(logits, prompt_length=prompt_len, target_ids=target_ids) | |
| for logits in random_logits | |
| ] | |
| random_seqs = [item[0] for item in random_summaries] | |
| random_means = [item[1] for item in random_summaries] | |
| random_token_matrix = [item[2] for item in random_summaries] | |
| random_token_mean = [ | |
| float(sum(row[token_pos] for row in random_token_matrix) / len(random_token_matrix)) | |
| for token_pos in range(len(target_ids)) | |
| ] | |
| random_mean_deltas = [float(value - baseline_mean) for value in random_means] | |
| random_seq_deltas = [float(value - baseline_seq) for value in random_seqs] | |
| mean_delta = float(modified_mean - baseline_mean) | |
| sequence_delta = float(modified_seq - baseline_seq) | |
| random_mean_signed, random_abs_mean, random_mean_std, target_p = self._random_effect_summary( | |
| random_mean_deltas, mean_delta | |
| ) | |
| random_sequence_delta = float(sum(random_seq_deltas) / len(random_seq_deltas)) | |
| random_seq_mean = float(sum(random_seqs) / len(random_seqs)) | |
| specificity = abs(mean_delta) / max(random_abs_mean, 1e-12) | |
| _, single_mean, _ = sequence_logprob_summary( | |
| single_baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| execution_drift_mean = float(baseline_mean - single_mean) | |
| p = torch.softmax(baseline_next_logits.float(), dim=-1) | |
| q = torch.softmax(modified_next_logits.float(), dim=-1) | |
| random_probs = [torch.softmax(logits.float(), dim=-1) for logits in random_next_logits] | |
| first_id = int(target_ids[0]) | |
| bp = float(p[first_id].item()) | |
| mp = float(q[first_id].item()) | |
| rp = float(sum(prob[first_id].item() for prob in random_probs) / len(random_probs)) | |
| target_tokens = [self.tokenizer.decode([int(token_id)]) for token_id in target_ids] | |
| target_rows = self._target_rows( | |
| target_ids, baseline_token_logps, modified_token_logps, random_token_mean | |
| ) | |
| generation_kwargs = { | |
| 'max_new_tokens': min(int(max_new_tokens), self.settings.max_new_tokens), | |
| 'do_sample': False, | |
| 'return_dict_in_generate': True, | |
| 'output_scores': False, | |
| 'pad_token_id': self.tokenizer.eos_token_id, | |
| } | |
| baseline_generation = self.model.generate(**prompt_inputs, **generation_kwargs) | |
| with self._delta_hook(int(layer), idx, delta): | |
| modified_generation = self.model.generate(**prompt_inputs, **generation_kwargs) | |
| baseline_ids = baseline_generation.sequences[0, prompt_len:] | |
| modified_ids = modified_generation.sequences[0, prompt_len:] | |
| baseline_text = self.tokenizer.decode(baseline_ids, skip_special_tokens=True) | |
| modified_text = self.tokenizer.decode(modified_ids, skip_special_tokens=True) | |
| return InterventionResult( | |
| baseline_text=baseline_text, | |
| modified_text=modified_text, | |
| feature_activation=float(original_activation), | |
| delta_activation=float(spec.delta_activation(original_activation)), | |
| perturbation_norm=float(torch.linalg.vector_norm(delta.float()).item()), | |
| js_divergence=float(js), | |
| random_js_divergence=float(random_js_reference), | |
| random_js_std=float(random_js_std), | |
| js_specificity_ratio=float(js_ratio), | |
| js_empirical_p=float(js_empirical_p), | |
| random_control_count=len(random_controls), | |
| execution_drift_js=float(execution_drift_js), | |
| execution_drift_mean_logprob=execution_drift_mean, | |
| target_text=target_text, | |
| target_token_count=len(target_ids), | |
| target_tokens=target_tokens, | |
| baseline_target_prob=bp, | |
| modified_target_prob=mp, | |
| random_target_prob=rp, | |
| baseline_sequence_logprob=baseline_seq, | |
| modified_sequence_logprob=modified_seq, | |
| random_sequence_logprob=random_seq_mean, | |
| sequence_logprob_delta=sequence_delta, | |
| random_sequence_logprob_delta=random_sequence_delta, | |
| mean_logprob_delta=mean_delta, | |
| random_mean_logprob_delta=random_mean_signed, | |
| random_abs_mean_logprob_delta=random_abs_mean, | |
| random_mean_logprob_std=random_mean_std, | |
| target_specificity_ratio=specificity, | |
| target_empirical_p=target_p, | |
| target_token_rows=target_rows, | |
| top_token_rows=self._top_token_rows( | |
| self.tokenizer, baseline_next_logits, modified_next_logits, k=8 | |
| ), | |
| ) | |
| def dose_response( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| feature_id: int, | |
| target_text: str, | |
| multipliers: Sequence[float] = (0.0, 0.5, 1.0, 1.5, 2.0, 3.0), | |
| ) -> DoseResponseResult: | |
| if not target_text.strip(): | |
| raise ValueError('Dose-response requires a target continuation.') | |
| if not any(abs(float(multiplier) - 1.0) < 1e-12 for multiplier in multipliers): | |
| raise ValueError('Dose-response multipliers must include 1.0 as the zero-edit reference.') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompt_inputs = self._inputs(text) | |
| prompt_len = int(prompt_inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), prompt_len) | |
| target_ids = self._target_ids(target_text) | |
| full_inputs = self._append_target(prompt_inputs, target_ids) | |
| sae = self.sae_store.get(int(layer)) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| single_baseline_out = self.model(**full_inputs, use_cache=False) | |
| single_baseline_logits = single_baseline_out.logits[0] | |
| _, single_baseline_mean, _ = sequence_logprob_summary( | |
| single_baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| residual = capture['hidden'][0, idx] | |
| encoding = sae.encode(residual) | |
| original_activation = encoding.activation_for(int(feature_id)) | |
| direction = sae.decoder_direction(int(feature_id)) | |
| deltas: list[torch.Tensor] = [] | |
| delta_coefficients: list[float] = [] | |
| norms: list[float] = [] | |
| for multiplier in multipliers: | |
| spec = InterventionSpec('scale', float(multiplier)) | |
| delta = residual_delta(direction, original_activation, spec) | |
| deltas.append(delta) | |
| delta_coefficients.append(float(spec.delta_activation(original_activation))) | |
| norms.append(float(torch.linalg.vector_norm(delta.float()).item())) | |
| repeated = self._repeat_inputs(full_inputs, len(deltas)) | |
| with self._batch_delta_hook(int(layer), idx, torch.stack(deltas, dim=0)): | |
| outputs = self.model(**repeated, use_cache=False) | |
| reference_idx = next( | |
| idx for idx, multiplier in enumerate(multipliers) if abs(float(multiplier) - 1.0) < 1e-12 | |
| ) | |
| reference_logits = outputs.logits[reference_idx] | |
| baseline_seq, baseline_mean, _ = sequence_logprob_summary( | |
| reference_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| baseline_next = reference_logits[prompt_len - 1] | |
| execution_drift_mean = float(baseline_mean - single_baseline_mean) | |
| execution_drift_js = js_divergence_from_logits( | |
| single_baseline_logits[prompt_len - 1], baseline_next | |
| ) | |
| rows: list[list[object]] = [] | |
| for row_idx, multiplier in enumerate(multipliers): | |
| modified_logits = outputs.logits[row_idx] | |
| modified_seq, modified_mean, _ = sequence_logprob_summary( | |
| modified_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| rows.append( | |
| [ | |
| float(multiplier), | |
| delta_coefficients[row_idx], | |
| norms[row_idx], | |
| float(baseline_mean), | |
| float(modified_mean), | |
| float(modified_mean - baseline_mean), | |
| float(modified_seq - baseline_seq), | |
| float(js_divergence_from_logits(baseline_next, modified_logits[prompt_len - 1])), | |
| ] | |
| ) | |
| return DoseResponseResult( | |
| feature_activation=float(original_activation), | |
| target_tokens=[self.tokenizer.decode([int(token_id)]) for token_id in target_ids], | |
| execution_drift_mean_logprob=execution_drift_mean, | |
| execution_drift_js=float(execution_drift_js), | |
| rows=rows, | |
| ) | |
| def intervene_feature_set( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| feature_ids: Sequence[int], | |
| mode: str, | |
| coefficient: float, | |
| target_text: str, | |
| ) -> FeatureSetResult: | |
| if not target_text.strip(): | |
| raise ValueError('Feature-set causal testing requires a target continuation.') | |
| ids = list(dict.fromkeys(int(x) for x in feature_ids)) | |
| if not ids: | |
| raise ValueError('Select at least one feature.') | |
| if len(ids) > 12: | |
| raise ValueError('Select at most 12 features for a live feature-set intervention.') | |
| if mode not in {'ablate', 'scale'}: | |
| raise ValueError("Feature-set mode must be 'ablate' or 'scale'.") | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompt_inputs = self._inputs(text) | |
| prompt_len = int(prompt_inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), prompt_len) | |
| target_ids = self._target_ids(target_text) | |
| full_inputs = self._append_target(prompt_inputs, target_ids) | |
| sae = self.sae_store.get(int(layer)) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| single_baseline_out = self.model(**full_inputs, use_cache=False) | |
| single_baseline_logits = single_baseline_out.logits[0] | |
| _, single_baseline_mean, _ = sequence_logprob_summary( | |
| single_baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| residual = capture['hidden'][0, idx] | |
| encoding = sae.encode(residual) | |
| activations = [encoding.activation_for(feature_id) for feature_id in ids] | |
| directions = torch.stack([sae.decoder_direction(feature_id) for feature_id in ids], dim=0) | |
| spec = InterventionSpec(mode, float(coefficient)) | |
| delta, coefficient_deltas = joint_residual_delta(directions, activations, spec) | |
| seed = self._control_seed( | |
| text, int(layer), ','.join(str(x) for x in ids), mode, float(coefficient) | |
| ) | |
| controls = self._random_controls( | |
| delta, seed=seed, count=self.settings.live_random_controls | |
| ) | |
| zero = torch.zeros_like(delta) | |
| all_deltas = torch.stack([zero, delta, *controls], dim=0) | |
| repeated = self._repeat_inputs(full_inputs, all_deltas.shape[0]) | |
| with self._batch_delta_hook(int(layer), idx, all_deltas): | |
| outputs = self.model(**repeated, use_cache=False) | |
| baseline_logits = outputs.logits[0] | |
| modified_logits = outputs.logits[1] | |
| random_logits = [outputs.logits[row] for row in range(2, outputs.logits.shape[0])] | |
| baseline_seq, baseline_mean, baseline_tokens = sequence_logprob_summary( | |
| baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| modified_seq, modified_mean, modified_tokens = sequence_logprob_summary( | |
| modified_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| random_summaries = [ | |
| sequence_logprob_summary(logits, prompt_length=prompt_len, target_ids=target_ids) | |
| for logits in random_logits | |
| ] | |
| random_seqs = [item[0] for item in random_summaries] | |
| random_means = [item[1] for item in random_summaries] | |
| random_token_matrix = [item[2] for item in random_summaries] | |
| random_token_mean = [ | |
| float(sum(row[token_pos] for row in random_token_matrix) / len(random_token_matrix)) | |
| for token_pos in range(len(target_ids)) | |
| ] | |
| mean_delta = float(modified_mean - baseline_mean) | |
| sequence_delta = float(modified_seq - baseline_seq) | |
| random_mean_deltas = [float(value - baseline_mean) for value in random_means] | |
| random_seq_deltas = [float(value - baseline_seq) for value in random_seqs] | |
| random_mean_signed, random_abs_mean, random_mean_std, target_p = self._random_effect_summary( | |
| random_mean_deltas, mean_delta | |
| ) | |
| next_idx = prompt_len - 1 | |
| js = js_divergence_from_logits(baseline_logits[next_idx], modified_logits[next_idx]) | |
| random_js_values = [ | |
| js_divergence_from_logits(baseline_logits[next_idx], logits[next_idx]) | |
| for logits in random_logits | |
| ] | |
| _, random_js_abs_mean, random_js_std, js_p = self._random_effect_summary( | |
| random_js_values, js | |
| ) | |
| execution_drift_mean = float(baseline_mean - single_baseline_mean) | |
| execution_drift_js = js_divergence_from_logits( | |
| single_baseline_logits[next_idx], baseline_logits[next_idx] | |
| ) | |
| feature_rows = [ | |
| [ | |
| feature_id, | |
| float(activation), | |
| float(delta_coefficient), | |
| self.catalog.hint(int(layer), feature_id), | |
| ] | |
| for feature_id, activation, delta_coefficient in zip( | |
| ids, activations, coefficient_deltas, strict=True | |
| ) | |
| ] | |
| return FeatureSetResult( | |
| feature_ids=ids, | |
| feature_rows=feature_rows, | |
| perturbation_norm=float(torch.linalg.vector_norm(delta.float()).item()), | |
| js_divergence=float(js), | |
| random_js_divergence=float(random_js_abs_mean), | |
| random_js_std=float(random_js_std), | |
| js_specificity_ratio=float(abs(js) / max(random_js_abs_mean, 1e-12)), | |
| js_empirical_p=float(js_p), | |
| random_control_count=len(controls), | |
| execution_drift_mean_logprob=execution_drift_mean, | |
| execution_drift_js=float(execution_drift_js), | |
| baseline_sequence_logprob=float(baseline_seq), | |
| modified_sequence_logprob=float(modified_seq), | |
| random_sequence_logprob=float(sum(random_seqs) / len(random_seqs)), | |
| sequence_logprob_delta=sequence_delta, | |
| random_sequence_logprob_delta=float(sum(random_seq_deltas) / len(random_seq_deltas)), | |
| mean_logprob_delta=mean_delta, | |
| random_mean_logprob_delta=random_mean_signed, | |
| random_abs_mean_logprob_delta=random_abs_mean, | |
| random_mean_logprob_std=float(random_mean_std), | |
| target_specificity_ratio=float(abs(mean_delta) / max(random_abs_mean, 1e-12)), | |
| target_empirical_p=float(target_p), | |
| target_tokens=[self.tokenizer.decode([int(token_id)]) for token_id in target_ids], | |
| target_token_rows=self._target_rows( | |
| target_ids, baseline_tokens, modified_tokens, random_token_mean | |
| ), | |
| ) | |
| def feature_set_size_sweep( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| target_text: str, | |
| sizes: Sequence[int] = (1, 3, 5), | |
| ) -> FeatureSetSweepResult: | |
| """Jointly ablate the strongest k active features and compare to random ensembles.""" | |
| if not target_text.strip(): | |
| raise ValueError('Feature-set size sweep requires a target continuation.') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompt_inputs = self._inputs(text) | |
| prompt_len = int(prompt_inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), prompt_len) | |
| target_ids = self._target_ids(target_text) | |
| full_inputs = self._append_target(prompt_inputs, target_ids) | |
| sae = self.sae_store.get(int(layer)) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| single_baseline_out = self.model(**full_inputs, use_cache=False) | |
| single_baseline_logits = single_baseline_out.logits[0] | |
| _, single_baseline_mean, _ = sequence_logprob_summary( | |
| single_baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| residual = capture['hidden'][0, idx] | |
| encoding = sae.encode(residual) | |
| active_ids = [ | |
| int(feature_id) | |
| for feature_id, value in zip( | |
| encoding.indices.detach().cpu().tolist(), | |
| encoding.values.detach().float().cpu().tolist(), | |
| strict=True, | |
| ) | |
| if float(value) > 0 | |
| ] | |
| valid_sizes = [int(size) for size in sizes if int(size) > 0 and int(size) <= len(active_ids)] | |
| if not valid_sizes: | |
| raise ValueError('Not enough active features for the requested set sizes.') | |
| condition_deltas: list[torch.Tensor] = [torch.zeros_like(residual)] | |
| metadata: list[tuple[int, str, list[int], float]] = [] | |
| for size in valid_sizes: | |
| selected = active_ids[:size] | |
| activations = [encoding.activation_for(feature_id) for feature_id in selected] | |
| directions = torch.stack([sae.decoder_direction(feature_id) for feature_id in selected]) | |
| delta, _ = joint_residual_delta( | |
| directions, activations, InterventionSpec('ablate', 0.0) | |
| ) | |
| norm = float(torch.linalg.vector_norm(delta.float()).item()) | |
| condition_deltas.append(delta) | |
| metadata.append((size, 'sae', selected, norm)) | |
| seed = self._control_seed(text, int(layer), f'top-{size}', 'ablate_set', 0.0) | |
| controls = self._random_controls( | |
| delta, seed=seed, count=self.settings.live_random_controls | |
| ) | |
| for control_idx, control in enumerate(controls): | |
| condition_deltas.append(control) | |
| metadata.append((size, f'random_{control_idx}', selected, norm)) | |
| all_deltas = torch.stack(condition_deltas, dim=0) | |
| repeated = self._repeat_inputs(full_inputs, all_deltas.shape[0]) | |
| with self._batch_delta_hook(int(layer), idx, all_deltas): | |
| outputs = self.model(**repeated, use_cache=False) | |
| baseline_logits = outputs.logits[0] | |
| baseline_seq, baseline_mean, _ = sequence_logprob_summary( | |
| baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| baseline_next = baseline_logits[prompt_len - 1] | |
| execution_drift_mean = float(baseline_mean - single_baseline_mean) | |
| execution_drift_js = js_divergence_from_logits( | |
| single_baseline_logits[prompt_len - 1], baseline_next | |
| ) | |
| grouped: dict[int, dict[str, object]] = { | |
| size: {'features': active_ids[:size], 'norm': None, 'sae': None, 'random': []} | |
| for size in valid_sizes | |
| } | |
| for output_idx, meta in enumerate(metadata, start=1): | |
| size, kind, selected, norm = meta | |
| logits = outputs.logits[output_idx] | |
| seq_logp, mean_logp, _ = sequence_logprob_summary( | |
| logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| item = { | |
| 'seq': float(seq_logp), | |
| 'mean': float(mean_logp), | |
| 'js': float(js_divergence_from_logits(baseline_next, logits[prompt_len - 1])), | |
| } | |
| grouped[size]['norm'] = norm | |
| grouped[size]['features'] = selected | |
| if kind == 'sae': | |
| grouped[size]['sae'] = item | |
| else: | |
| grouped[size]['random'].append(item) | |
| rows: list[list[object]] = [] | |
| for size in valid_sizes: | |
| group = grouped[size] | |
| sae_item = group['sae'] | |
| random_items = group['random'] | |
| assert isinstance(sae_item, dict) | |
| assert isinstance(random_items, list) and random_items | |
| sae_mean_delta = float(sae_item['mean'] - baseline_mean) | |
| sae_seq_delta = float(sae_item['seq'] - baseline_seq) | |
| random_mean_deltas = [float(item['mean'] - baseline_mean) for item in random_items] | |
| random_js_values = [float(item['js']) for item in random_items] | |
| random_signed, random_abs, random_std, target_p = self._random_effect_summary( | |
| random_mean_deltas, sae_mean_delta | |
| ) | |
| _, random_js_abs, random_js_std, js_p = self._random_effect_summary( | |
| random_js_values, float(sae_item['js']) | |
| ) | |
| rows.append( | |
| [ | |
| int(size), | |
| ', '.join(str(x) for x in group['features']), | |
| float(group['norm']), | |
| float(baseline_mean), | |
| float(sae_item['mean']), | |
| sae_mean_delta, | |
| random_signed, | |
| random_abs, | |
| random_std, | |
| float(abs(sae_mean_delta) / max(random_abs, 1e-12)), | |
| float(target_p), | |
| sae_seq_delta, | |
| float(sae_item['js']), | |
| random_js_abs, | |
| random_js_std, | |
| float(js_p), | |
| ] | |
| ) | |
| return FeatureSetSweepResult( | |
| target_tokens=[self.tokenizer.decode([int(token_id)]) for token_id in target_ids], | |
| random_control_count=self.settings.live_random_controls, | |
| execution_drift_mean_logprob=execution_drift_mean, | |
| execution_drift_js=float(execution_drift_js), | |
| rows=rows, | |
| ) | |
| def feature_interaction_test( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| feature_ids: Sequence[int], | |
| target_text: str, | |
| ) -> FeatureInteractionResult: | |
| """Compare individual ablations with their joint ablation to measure non-additivity.""" | |
| if not target_text.strip(): | |
| raise ValueError('Feature interaction testing requires a target continuation.') | |
| ids = list(dict.fromkeys(int(x) for x in feature_ids)) | |
| if len(ids) < 2: | |
| raise ValueError('Select at least two features for the interaction decomposition.') | |
| if len(ids) > 5: | |
| raise ValueError('Select at most five features for the live interaction decomposition.') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompt_inputs = self._inputs(text) | |
| prompt_len = int(prompt_inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), prompt_len) | |
| target_ids = self._target_ids(target_text) | |
| full_inputs = self._append_target(prompt_inputs, target_ids) | |
| sae = self.sae_store.get(int(layer)) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| single_baseline_out = self.model(**full_inputs, use_cache=False) | |
| single_logits = single_baseline_out.logits[0] | |
| _, single_mean, _ = sequence_logprob_summary( | |
| single_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| residual = capture['hidden'][0, idx] | |
| encoding = sae.encode(residual) | |
| activations = [encoding.activation_for(feature_id) for feature_id in ids] | |
| directions = [sae.decoder_direction(feature_id) for feature_id in ids] | |
| individual_deltas = [ | |
| residual_delta(direction, activation, InterventionSpec('ablate', 0.0)) | |
| for direction, activation in zip(directions, activations, strict=True) | |
| ] | |
| joint_delta = torch.stack(individual_deltas, dim=0).sum(dim=0) | |
| all_deltas = torch.stack([torch.zeros_like(joint_delta), *individual_deltas, joint_delta], dim=0) | |
| repeated = self._repeat_inputs(full_inputs, all_deltas.shape[0]) | |
| with self._batch_delta_hook(int(layer), idx, all_deltas): | |
| outputs = self.model(**repeated, use_cache=False) | |
| baseline_logits = outputs.logits[0] | |
| baseline_seq, baseline_mean, _ = sequence_logprob_summary( | |
| baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| baseline_next = baseline_logits[prompt_len - 1] | |
| rows: list[list[object]] = [] | |
| individual_mean_deltas: list[float] = [] | |
| for feature_idx, feature_id in enumerate(ids, start=1): | |
| logits = outputs.logits[feature_idx] | |
| seq_logp, mean_logp, _ = sequence_logprob_summary( | |
| logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| delta_mean = float(mean_logp - baseline_mean) | |
| individual_mean_deltas.append(delta_mean) | |
| rows.append( | |
| [ | |
| f'Feature {feature_id}', | |
| str(feature_id), | |
| float(activations[feature_idx - 1]), | |
| float(torch.linalg.vector_norm(individual_deltas[feature_idx - 1].float()).item()), | |
| delta_mean, | |
| float(seq_logp - baseline_seq), | |
| float(js_divergence_from_logits(baseline_next, logits[prompt_len - 1])), | |
| ] | |
| ) | |
| joint_logits = outputs.logits[len(ids) + 1] | |
| joint_seq, joint_mean, _ = sequence_logprob_summary( | |
| joint_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| joint_mean_delta = float(joint_mean - baseline_mean) | |
| rows.append( | |
| [ | |
| 'Joint ablation', | |
| ', '.join(str(x) for x in ids), | |
| float(sum(activations)), | |
| float(torch.linalg.vector_norm(joint_delta.float()).item()), | |
| joint_mean_delta, | |
| float(joint_seq - baseline_seq), | |
| float(js_divergence_from_logits(baseline_next, joint_logits[prompt_len - 1])), | |
| ] | |
| ) | |
| additive_expected = float(sum(individual_mean_deltas)) | |
| interaction_excess = float(joint_mean_delta - additive_expected) | |
| scale = max(sum(abs(value) for value in individual_mean_deltas), 1e-12) | |
| return FeatureInteractionResult( | |
| feature_ids=ids, | |
| target_tokens=[self.tokenizer.decode([int(token_id)]) for token_id in target_ids], | |
| rows=rows, | |
| additive_expected_mean_delta=additive_expected, | |
| joint_mean_delta=joint_mean_delta, | |
| interaction_excess_mean_delta=interaction_excess, | |
| normalized_interaction=float(interaction_excess / scale), | |
| execution_drift_mean_logprob=float(baseline_mean - single_mean), | |
| ) | |
| def compare_paraphrases( | |
| self, | |
| text_a: str, | |
| text_b: str, | |
| layer: int, | |
| token_index_a: int = -1, | |
| token_index_b: int = -1, | |
| top_n: int = 12, | |
| ) -> ParaphraseResult: | |
| if not text_a.strip() or not text_b.strip(): | |
| raise ValueError('Enter both the original prompt and a paraphrase.') | |
| a, pooled_a = self._analyze_and_pool(text_a, int(layer), int(token_index_a), max(int(top_n), 12)) | |
| b, pooled_b = self._analyze_and_pool(text_b, int(layer), int(token_index_b), max(int(top_n), 12)) | |
| map_a = self._encoding_map(a.features) | |
| map_b = self._encoding_map(b.features) | |
| set_a = set(map_a) | |
| set_b = set(map_b) | |
| union = set_a | set_b | |
| jaccard = len(set_a & set_b) / len(union) if union else 1.0 | |
| cosine = sparse_topk_cosine( | |
| a.features.indices, a.features.values, b.features.indices, b.features.values | |
| ) | |
| promptwide_jaccard = self._dict_jaccard(pooled_a, pooled_b) | |
| promptwide_cosine = self._dict_cosine(pooled_a, pooled_b) | |
| top_ids_a = [int(row[1]) for row in a.rows[: int(top_n)]] | |
| top_ids_b = [int(row[1]) for row in b.rows[: int(top_n)]] | |
| top_union = list(dict.fromkeys(top_ids_a + top_ids_b)) | |
| shared_top_n = len(set(top_ids_a) & set(top_ids_b)) | |
| rows: list[list[object]] = [] | |
| chart_rows: list[list[object]] = [] | |
| for feature_id in top_union: | |
| va = float(map_a.get(feature_id, 0.0)) | |
| vb = float(map_b.get(feature_id, 0.0)) | |
| status = 'shared' if va > 0 and vb > 0 else ('original only' if va > 0 else 'paraphrase only') | |
| rows.append( | |
| [feature_id, va, vb, status, self.catalog.hint(int(layer), feature_id)] | |
| ) | |
| chart_rows.append([str(feature_id), 'Original', va]) | |
| chart_rows.append([str(feature_id), 'Paraphrase', vb]) | |
| rows.sort(key=lambda row: max(float(row[1]), float(row[2])), reverse=True) | |
| return ParaphraseResult( | |
| tokens_a=a.tokens, | |
| token_index_a=a.token_index, | |
| tokens_b=b.tokens, | |
| token_index_b=b.token_index, | |
| topk_jaccard=float(jaccard), | |
| sparse_cosine=float(cosine), | |
| promptwide_jaccard=float(promptwide_jaccard), | |
| promptwide_cosine=float(promptwide_cosine), | |
| shared_top_n=int(shared_top_n), | |
| top_n=int(top_n), | |
| rows=rows, | |
| chart_rows=chart_rows, | |
| ) | |
| def _contrast_prompt_rows(self, prompts_per_concept: int) -> list[dict]: | |
| data_path = Path(__file__).resolve().parents[1] / 'data' / 'prompts.jsonl' | |
| rows = [ | |
| json.loads(line) | |
| for line in data_path.read_text(encoding='utf-8').splitlines() | |
| if line.strip() | |
| ] | |
| selected: list[dict] = [] | |
| by_concept: dict[str, list[dict]] = {} | |
| for row in rows: | |
| # Use one wording per paraphrase pair so the live contrast is not dominated by near-duplicates. | |
| if int(row.get('variant', 0)) != 0: | |
| continue | |
| by_concept.setdefault(str(row['concept']), []).append(row) | |
| for concept in sorted(by_concept): | |
| selected.extend(by_concept[concept][: int(prompts_per_concept)]) | |
| return selected | |
| def feature_token_trace( | |
| self, | |
| text: str, | |
| layer: int, | |
| feature_id: int, | |
| ) -> FeatureTraceResult: | |
| """Trace one SAE feature across every non-padding token in a prompt.""" | |
| if not text.strip(): | |
| raise ValueError('Enter a prompt first.') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| if int(layer) not in self.settings.layers: | |
| raise ValueError(f'Layer must be one of {self.settings.layers}.') | |
| if int(feature_id) < 0 or int(feature_id) >= self.settings.sae_width: | |
| raise ValueError(f'Feature id must be in [0, {self.settings.sae_width - 1}].') | |
| inputs = self._inputs(text) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| self.model(**inputs, use_cache=False) | |
| hidden = capture['hidden'][0] | |
| sae = self.sae_store.get(int(layer)) | |
| encoding = sae.encode(hidden) | |
| mask = encoding.indices == int(feature_id) | |
| activations = torch.where(mask, encoding.values, torch.zeros_like(encoding.values)).sum(dim=-1) | |
| values = activations.detach().float().cpu().tolist() | |
| ids = inputs['input_ids'][0].tolist() | |
| tokens = [self.tokenizer.decode([int(token_id)]) for token_id in ids] | |
| rows: list[list[object]] = [] | |
| chart_rows: list[list[object]] = [] | |
| for idx, (token, value) in enumerate(zip(tokens, values, strict=True)): | |
| value = float(value) | |
| rows.append([idx, repr(token), value, bool(value > 0)]) | |
| chart_rows.append([f'{idx}: {token if token.strip() else repr(token)}', value]) | |
| active = [idx for idx, value in enumerate(values) if float(value) > 0] | |
| if active: | |
| max_idx = max(active, key=lambda idx: float(values[idx])) | |
| max_value = float(values[max_idx]) | |
| else: | |
| max_idx = None | |
| max_value = 0.0 | |
| return FeatureTraceResult( | |
| feature_id=int(feature_id), | |
| layer=int(layer), | |
| tokens=tokens, | |
| rows=rows, | |
| chart_rows=chart_rows, | |
| active_token_count=len(active), | |
| token_count=len(tokens), | |
| max_activation=max_value, | |
| max_token_index=max_idx, | |
| ) | |
| def feature_geometry( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| feature_ids: Sequence[int], | |
| ) -> FeatureGeometryResult: | |
| """Inspect pairwise SAE decoder geometry and activation-weighted ablation geometry.""" | |
| if not text.strip(): | |
| raise ValueError('Enter a prompt first.') | |
| ids = list(dict.fromkeys(int(x) for x in feature_ids)) | |
| if len(ids) < 2: | |
| raise ValueError('Select at least two distinct features for geometry analysis.') | |
| if len(ids) > 8: | |
| raise ValueError('Geometry analysis supports at most eight features in the live app.') | |
| if any(feature_id < 0 or feature_id >= self.settings.sae_width for feature_id in ids): | |
| raise ValueError(f'Feature ids must be in [0, {self.settings.sae_width - 1}].') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.sae_store is not None | |
| inputs = self._inputs(text) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| self.model(**inputs, use_cache=False) | |
| hidden = capture['hidden'][0] | |
| idx = self._resolve_index(int(token_index), hidden.shape[0]) | |
| sae = self.sae_store.get(int(layer)) | |
| encoding = sae.encode(hidden[idx]) | |
| activations = [float(encoding.activation_for(feature_id)) for feature_id in ids] | |
| directions = torch.stack([sae.decoder_direction(feature_id).float() for feature_id in ids]) | |
| gram = decoder_cosine_matrix(directions) | |
| rows: list[list[object]] = [] | |
| chart_rows: list[list[object]] = [] | |
| offdiag: list[float] = [] | |
| for i in range(len(ids)): | |
| for j in range(i + 1, len(ids)): | |
| cosine = float(gram[i, j].item()) | |
| offdiag.append(abs(cosine)) | |
| rows.append([ids[i], ids[j], activations[i], activations[j], cosine]) | |
| chart_rows.append([f'{ids[i]} ↔ {ids[j]}', cosine]) | |
| individual_deltas = torch.stack( | |
| [-float(activation) * direction for activation, direction in zip(activations, directions, strict=True)] | |
| ) | |
| joint_norm, independent_norm, alignment_ratio = joint_direction_norm_ratio(individual_deltas) | |
| return FeatureGeometryResult( | |
| feature_ids=ids, | |
| layer=int(layer), | |
| rows=rows, | |
| chart_rows=chart_rows, | |
| mean_abs_decoder_cosine=float(sum(offdiag) / len(offdiag)) if offdiag else 0.0, | |
| max_abs_decoder_cosine=float(max(offdiag)) if offdiag else 0.0, | |
| joint_ablation_norm=joint_norm, | |
| independent_norm=independent_norm, | |
| alignment_ratio=float(alignment_ratio), | |
| ) | |
| def contrastive_intervention( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| feature_id: int, | |
| mode: str, | |
| coefficient: float, | |
| target_a: str, | |
| target_b: str, | |
| ) -> ContrastiveCausalResult: | |
| """Measure whether an SAE edit shifts preference between two exact continuations.""" | |
| if not text.strip(): | |
| raise ValueError('Enter a prompt first.') | |
| if not target_a: | |
| raise ValueError('Enter preferred continuation A.') | |
| if not target_b: | |
| raise ValueError('Enter comparison continuation B.') | |
| if target_a == target_b: | |
| raise ValueError('Continuations A and B must be different.') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompt_inputs = self._inputs(text) | |
| prompt_len = int(prompt_inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), prompt_len) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| self.model(**prompt_inputs, use_cache=False) | |
| residual = capture['hidden'][0, idx] | |
| sae = self.sae_store.get(int(layer)) | |
| encoding = sae.encode(residual) | |
| activation = float(encoding.activation_for(int(feature_id))) | |
| delta = residual_delta( | |
| sae.decoder_direction(int(feature_id)), | |
| activation, | |
| InterventionSpec(mode=mode, coefficient=float(coefficient)), | |
| ) | |
| seed = self._control_seed(text, int(layer), str(int(feature_id)), mode, float(coefficient)) | |
| controls = self._random_controls(delta, seed=seed, count=self.settings.live_random_controls) | |
| deltas = torch.stack([torch.zeros_like(delta), delta, *controls], dim=0) | |
| def score(target_text: str): | |
| target_ids = self._target_ids(target_text) | |
| scoring_inputs = self._append_target(prompt_inputs, target_ids) | |
| repeated = self._repeat_inputs(scoring_inputs, deltas.shape[0]) | |
| with self._batch_delta_hook(int(layer), idx, deltas): | |
| output = self.model(**repeated, use_cache=False) | |
| summaries = [ | |
| sequence_logprob_summary(output.logits[row], prompt_length=prompt_len, target_ids=target_ids) | |
| for row in range(output.logits.shape[0]) | |
| ] | |
| return target_ids, summaries | |
| ids_a, scores_a = score(target_a) | |
| ids_b, scores_b = score(target_b) | |
| base_a_seq, base_a_mean, _ = scores_a[0] | |
| edit_a_seq, edit_a_mean, _ = scores_a[1] | |
| base_b_seq, base_b_mean, _ = scores_b[0] | |
| edit_b_seq, edit_b_mean, _ = scores_b[1] | |
| baseline_log_odds, modified_log_odds, delta_log_odds = contrastive_log_odds( | |
| base_a_seq, edit_a_seq, base_b_seq, edit_b_seq | |
| ) | |
| baseline_norm_pref, modified_norm_pref, delta_norm_pref = contrastive_log_odds( | |
| base_a_mean, edit_a_mean, base_b_mean, edit_b_mean | |
| ) | |
| random_delta_log_odds: list[float] = [] | |
| for row in range(2, len(scores_a)): | |
| random_a_seq = float(scores_a[row][0]) | |
| random_b_seq = float(scores_b[row][0]) | |
| random_delta_log_odds.append( | |
| float((random_a_seq - random_b_seq) - baseline_log_odds) | |
| ) | |
| random_signed, random_abs, random_std, empirical_p = self._random_effect_summary( | |
| random_delta_log_odds, delta_log_odds | |
| ) | |
| ratio = abs(delta_log_odds) / max(random_abs, 1e-12) | |
| rows = [ | |
| [ | |
| 'A (preferred)', | |
| target_a, | |
| len(ids_a), | |
| float(base_a_seq), | |
| float(edit_a_seq), | |
| float(edit_a_seq - base_a_seq), | |
| float(base_a_mean), | |
| float(edit_a_mean), | |
| float(edit_a_mean - base_a_mean), | |
| ], | |
| [ | |
| 'B (comparison)', | |
| target_b, | |
| len(ids_b), | |
| float(base_b_seq), | |
| float(edit_b_seq), | |
| float(edit_b_seq - base_b_seq), | |
| float(base_b_mean), | |
| float(edit_b_mean), | |
| float(edit_b_mean - base_b_mean), | |
| ], | |
| ] | |
| return ContrastiveCausalResult( | |
| feature_id=int(feature_id), | |
| layer=int(layer), | |
| feature_activation=activation, | |
| perturbation_norm=float(torch.linalg.vector_norm(delta.float()).item()), | |
| target_a_tokens=[self.tokenizer.decode([int(x)]) for x in ids_a], | |
| target_b_tokens=[self.tokenizer.decode([int(x)]) for x in ids_b], | |
| rows=rows, | |
| baseline_log_odds=baseline_log_odds, | |
| modified_log_odds=modified_log_odds, | |
| delta_log_odds=delta_log_odds, | |
| baseline_normalized_preference=baseline_norm_pref, | |
| modified_normalized_preference=modified_norm_pref, | |
| delta_normalized_preference=delta_norm_pref, | |
| random_signed_mean_delta=random_signed, | |
| random_abs_mean_delta=random_abs, | |
| random_delta_std=random_std, | |
| specificity_ratio=float(ratio), | |
| empirical_p=float(empirical_p), | |
| random_control_count=len(controls), | |
| ) | |
| def concept_contrast_scan( | |
| self, | |
| feature_id: int, | |
| layer: int, | |
| prompts_per_concept: int | None = None, | |
| ) -> ConceptContrastResult: | |
| """Measure one SAE feature using prompt-wide max activation on a balanced concept batch.""" | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| if int(feature_id) < 0 or int(feature_id) >= self.settings.sae_width: | |
| raise ValueError(f'Feature id must be in [0, {self.settings.sae_width - 1}].') | |
| n = int(prompts_per_concept or self.settings.contrast_prompts_per_concept) | |
| if n < 1 or n > 8: | |
| raise ValueError('Contrast prompts per concept must be between 1 and 8.') | |
| rows = self._contrast_prompt_rows(n) | |
| if not rows: | |
| raise RuntimeError('No controlled contrast prompts are available.') | |
| texts = [str(row['text']) for row in rows] | |
| batch = self.tokenizer( | |
| texts, | |
| return_tensors='pt', | |
| padding=True, | |
| truncation=True, | |
| max_length=self.settings.max_prompt_tokens, | |
| ) | |
| batch = {key: value.to(self.device) for key, value in batch.items()} | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| self.model(**batch, use_cache=False) | |
| residuals = capture['hidden'] | |
| sae = self.sae_store.get(int(layer)) | |
| encoding = sae.encode(residuals) | |
| feature_mask = encoding.indices == int(feature_id) | |
| token_activations = torch.where( | |
| feature_mask, encoding.values, torch.zeros_like(encoding.values) | |
| ).sum(dim=-1) | |
| attention = batch.get('attention_mask', torch.ones_like(batch['input_ids'])).bool() | |
| token_activations = torch.where(attention, token_activations, torch.zeros_like(token_activations)) | |
| prompt_activations = token_activations.max(dim=1).values | |
| values = prompt_activations.detach().float().cpu().tolist() | |
| grouped: dict[str, list[float]] = {} | |
| for row, value in zip(rows, values, strict=True): | |
| grouped.setdefault(str(row['concept']), []).append(float(value)) | |
| table_rows: list[list[object]] = [] | |
| for concept in sorted(grouped): | |
| vals = grouped[concept] | |
| tensor = torch.tensor(vals, dtype=torch.float64) | |
| active = [value for value in vals if value > 0] | |
| positive_mean = float(sum(active) / len(active)) if active else 0.0 | |
| table_rows.append( | |
| [ | |
| concept, | |
| len(vals), | |
| float(tensor.mean().item()), | |
| float(torch.median(tensor).item()), | |
| float(len(active) / len(vals)), | |
| positive_mean, | |
| float(max(vals) if vals else 0.0), | |
| ] | |
| ) | |
| table_rows.sort(key=lambda row: (float(row[2]), float(row[4]), float(row[6])), reverse=True) | |
| active_prompt_count = sum(1 for value in values if float(value) > 0) | |
| if table_rows and float(table_rows[0][2]) > 0: | |
| leader: str | None = str(table_rows[0][0]) | |
| first = float(table_rows[0][2]) | |
| second = float(table_rows[1][2]) if len(table_rows) > 1 else 0.0 | |
| ratio: float | None = first / second if second > 0 else None | |
| else: | |
| leader = None | |
| ratio = None | |
| chart_rows = [[str(row[0]), float(row[2])] for row in table_rows] | |
| return ConceptContrastResult( | |
| feature_id=int(feature_id), | |
| layer=int(layer), | |
| prompts_per_concept=n, | |
| rows=table_rows, | |
| chart_rows=chart_rows, | |
| leading_concept=leader, | |
| leading_ratio=ratio, | |
| active_prompt_count=int(active_prompt_count), | |
| total_prompt_count=len(values), | |
| ) | |
| def concept_feature_discovery( | |
| self, | |
| concept: str, | |
| layer: int, | |
| prompts_per_concept: int | None = None, | |
| top_n: int = 12, | |
| ranking_mode: str = 'balanced_selectivity', | |
| current_text: str | None = None, | |
| current_token_index: int = -1, | |
| ) -> ConceptFeatureDiscoveryResult: | |
| """Find exploratory concept candidates and show whether they are usable in the current Workbench context. | |
| ``balanced_selectivity`` downweights globally high-activation features by combining target selectivity, | |
| target coverage, and target activation magnitude. ``raw_mean_difference`` preserves the simpler raw | |
| mean-difference ranking for comparison. ``causal_ready`` further requires activation at the currently | |
| selected Workbench token and ranks those compatible candidates by balanced evidence plus a log-scaled | |
| current-token activation term. If a Workbench prompt is supplied it is appended to the same model batch, | |
| so current-prompt compatibility does not require another forward pass. | |
| """ | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| concept = str(concept).strip() | |
| if not concept: | |
| raise ValueError('Choose a target concept.') | |
| n = int(prompts_per_concept or self.settings.contrast_prompts_per_concept) | |
| if n < 1 or n > 8: | |
| raise ValueError('Discovery prompts per concept must be between 1 and 8.') | |
| top_n = int(top_n) | |
| if top_n < 1 or top_n > 25: | |
| raise ValueError('Number of candidate features must be between 1 and 25.') | |
| if ranking_mode not in {'balanced_selectivity', 'raw_mean_difference', 'causal_ready'}: | |
| raise ValueError( | |
| "ranking_mode must be 'balanced_selectivity', 'raw_mean_difference', or 'causal_ready'." | |
| ) | |
| rows = self._contrast_prompt_rows(n) | |
| available = sorted({str(row['concept']) for row in rows}) | |
| if concept not in available: | |
| raise ValueError(f'Concept must be one of {available}.') | |
| current_text = str(current_text or '').strip() | |
| current_context_available = bool(current_text) | |
| texts = [str(row['text']) for row in rows] | |
| if current_context_available: | |
| texts.append(current_text) | |
| batch = self.tokenizer( | |
| texts, | |
| return_tensors='pt', | |
| padding=True, | |
| truncation=True, | |
| max_length=self.settings.max_prompt_tokens, | |
| ) | |
| batch = {key: value.to(self.device) for key, value in batch.items()} | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| self.model(**batch, use_cache=False) | |
| sae = self.sae_store.get(int(layer)) | |
| encoding = sae.encode(capture['hidden']) | |
| attention = batch.get('attention_mask', torch.ones_like(batch['input_ids'])).bool() | |
| valid = attention.unsqueeze(-1).expand_as(encoding.values) | |
| values = torch.where(valid, encoding.values, torch.zeros_like(encoding.values)).float() | |
| dense = torch.zeros( | |
| (values.shape[0], self.settings.sae_width), | |
| device=values.device, | |
| dtype=torch.float32, | |
| ) | |
| dense.scatter_reduce_( | |
| 1, | |
| encoding.indices.reshape(values.shape[0], -1), | |
| values.reshape(values.shape[0], -1), | |
| reduce='amax', | |
| include_self=True, | |
| ) | |
| controlled_count = len(rows) | |
| controlled_dense = dense[:controlled_count] | |
| target_mask = torch.tensor( | |
| [str(row['concept']) == concept for row in rows], | |
| device=dense.device, | |
| dtype=torch.bool, | |
| ) | |
| other_mask = ~target_mask | |
| target = controlled_dense[target_mask] | |
| other = controlled_dense[other_mask] | |
| target_mean = target.mean(dim=0) | |
| other_mean = other.mean(dim=0) | |
| target_rate = (target > 0).float().mean(dim=0) | |
| other_rate = (other > 0).float().mean(dim=0) | |
| mean_diff = target_mean - other_mean | |
| selectivity = mean_diff / (target_mean + other_mean + 1e-8) | |
| # A scale-aware but selectivity-first exploratory score. log1p prevents very large SAE coefficients from | |
| # overwhelming features that are much more exclusive to the target concept. | |
| balanced_score = selectivity.clamp_min(0) * target_rate * torch.log1p(target_mean.clamp_min(0)) | |
| current_prompt_max = torch.zeros(self.settings.sae_width, device=dense.device, dtype=torch.float32) | |
| current_token_dense = torch.zeros_like(current_prompt_max) | |
| resolved_current_idx: int | None = None | |
| if current_context_available: | |
| current_row = controlled_count | |
| current_prompt_max = dense[current_row] | |
| valid_positions = torch.nonzero(attention[current_row], as_tuple=False).flatten() | |
| prompt_len = int(valid_positions.numel()) | |
| if prompt_len: | |
| resolved_current_idx = self._resolve_index(int(current_token_index), prompt_len) | |
| padded_position = int(valid_positions[resolved_current_idx].item()) | |
| token_ids = encoding.indices[current_row, padded_position] | |
| token_values = encoding.values[current_row, padded_position].float().clamp_min(0) | |
| current_token_dense.scatter_reduce_( | |
| 0, | |
| token_ids, | |
| token_values, | |
| reduce='amax', | |
| include_self=True, | |
| ) | |
| # Stability diagnostics reuse the same controlled activation batch, so they add no model inference. | |
| # Split-half overlap is intentionally simple; deterministic balanced bootstrap support gives a second | |
| # view of how often each displayed feature survives small changes to the live prompt sample. | |
| split_half_k: int | None = None | |
| split_half_shared_count = 0 | |
| split_half_jaccard: float | None = None | |
| resample_replicates = 0 | |
| resample_rank_lists: list[list[int]] = [] | |
| row_concepts = [str(row['concept']) for row in rows] | |
| def _rank_indices(row_indices: list[int]) -> list[int]: | |
| if not row_indices: | |
| return [] | |
| index_tensor = torch.tensor(row_indices, device=dense.device, dtype=torch.long) | |
| sub_dense = controlled_dense.index_select(0, index_tensor) | |
| sub_labels = [row_concepts[index] for index in row_indices] | |
| sub_target_mask = torch.tensor( | |
| [label == concept for label in sub_labels], | |
| device=dense.device, | |
| dtype=torch.bool, | |
| ) | |
| sub_other_mask = ~sub_target_mask | |
| sub_target = sub_dense[sub_target_mask] | |
| sub_other = sub_dense[sub_other_mask] | |
| if sub_target.shape[0] == 0 or sub_other.shape[0] == 0: | |
| return [] | |
| sub_target_mean = sub_target.mean(dim=0) | |
| sub_other_mean = sub_other.mean(dim=0) | |
| sub_target_rate = (sub_target > 0).float().mean(dim=0) | |
| sub_mean_diff = sub_target_mean - sub_other_mean | |
| sub_selectivity = sub_mean_diff / (sub_target_mean + sub_other_mean + 1e-8) | |
| sub_balanced = ( | |
| sub_selectivity.clamp_min(0) | |
| * sub_target_rate | |
| * torch.log1p(sub_target_mean.clamp_min(0)) | |
| ) | |
| sub_eligible = (sub_target_mean > 0) & (sub_mean_diff > 0) | |
| if ranking_mode == 'causal_ready': | |
| sub_eligible = sub_eligible & (current_token_dense > 0) | |
| sub_ranking = sub_balanced * torch.log1p(current_token_dense.clamp_min(0)) | |
| elif ranking_mode == 'balanced_selectivity': | |
| sub_ranking = sub_balanced | |
| else: | |
| sub_ranking = sub_mean_diff | |
| sub_idx = torch.nonzero(sub_eligible, as_tuple=False).flatten() | |
| if sub_idx.numel() == 0: | |
| return [] | |
| sub_order = torch.argsort(sub_ranking[sub_idx], descending=True) | |
| return [int(value.item()) for value in sub_idx[sub_order[:top_n]]] | |
| if n >= 2: | |
| half = max(1, n // 2) | |
| seen_by_concept: dict[str, int] = {} | |
| indices_a: list[int] = [] | |
| indices_b: list[int] = [] | |
| for row_idx, row_concept in enumerate(row_concepts): | |
| local_idx = seen_by_concept.get(row_concept, 0) | |
| seen_by_concept[row_concept] = local_idx + 1 | |
| if local_idx < half: | |
| indices_a.append(row_idx) | |
| else: | |
| indices_b.append(row_idx) | |
| ids_a = _rank_indices(indices_a) | |
| ids_b = _rank_indices(indices_b) | |
| if ids_a and ids_b: | |
| set_a, set_b = set(ids_a), set(ids_b) | |
| shared = set_a & set_b | |
| union = set_a | set_b | |
| split_half_k = min(len(ids_a), len(ids_b), top_n) | |
| split_half_shared_count = len(shared) | |
| split_half_jaccard = float(len(shared) / len(union)) if union else 1.0 | |
| pools: dict[str, list[int]] = {} | |
| for row_idx, row_concept in enumerate(row_concepts): | |
| pools.setdefault(row_concept, []).append(row_idx) | |
| seed = 13013 + int(layer) * 97 + n * 17 + sum(ord(ch) for ch in concept) | |
| rng = random.Random(seed) | |
| resample_replicates = 32 | |
| for _ in range(resample_replicates): | |
| sampled_indices: list[int] = [] | |
| for row_concept in sorted(pools): | |
| pool = pools[row_concept] | |
| sampled_indices.extend(rng.choice(pool) for _ in range(len(pool))) | |
| ranked = _rank_indices(sampled_indices) | |
| if ranked: | |
| resample_rank_lists.append(ranked) | |
| resample_replicates = len(resample_rank_lists) | |
| eligible = (target_mean > 0) & (mean_diff > 0) | |
| if ranking_mode == 'causal_ready': | |
| if not current_context_available or resolved_current_idx is None: | |
| raise ValueError( | |
| "Causal-ready ranking requires a current Workbench prompt/token. " | |
| "Set the Workbench prompt/layer/token first." | |
| ) | |
| eligible = eligible & (current_token_dense > 0) | |
| ranking_values = balanced_score * torch.log1p(current_token_dense.clamp_min(0)) | |
| elif ranking_mode == 'balanced_selectivity': | |
| ranking_values = balanced_score | |
| else: | |
| ranking_values = mean_diff | |
| candidate_idx = torch.nonzero(eligible, as_tuple=False).flatten() | |
| if candidate_idx.numel() == 0: | |
| return ConceptFeatureDiscoveryResult( | |
| concept=concept, | |
| layer=int(layer), | |
| prompts_per_concept=n, | |
| top_n=top_n, | |
| ranking_mode=ranking_mode, | |
| rows=[], | |
| chart_rows=[], | |
| candidate_ids=[], | |
| default_candidate_id=None, | |
| current_context_available=current_context_available, | |
| current_token_index=resolved_current_idx, | |
| displayed_current_active_count=0, | |
| split_half_k=split_half_k, | |
| split_half_shared_count=split_half_shared_count, | |
| split_half_jaccard=split_half_jaccard, | |
| resample_replicates=resample_replicates, | |
| resample_mean_support=None, | |
| resample_high_support_count=0, | |
| ) | |
| order = torch.argsort(ranking_values[candidate_idx], descending=True) | |
| candidate_idx = candidate_idx[order[:top_n]] | |
| resample_support: dict[int, float] = {} | |
| resample_median_rank: dict[int, float | None] = {} | |
| displayed_supports: list[float] = [] | |
| if resample_replicates: | |
| for feature_tensor in candidate_idx: | |
| fid = int(feature_tensor.item()) | |
| ranks = [ranked.index(fid) + 1 for ranked in resample_rank_lists if fid in ranked] | |
| support = len(ranks) / resample_replicates | |
| if ranks: | |
| ordered_ranks = sorted(ranks) | |
| midpoint = len(ordered_ranks) // 2 | |
| if len(ordered_ranks) % 2: | |
| median_rank = float(ordered_ranks[midpoint]) | |
| else: | |
| median_rank = float((ordered_ranks[midpoint - 1] + ordered_ranks[midpoint]) / 2) | |
| else: | |
| median_rank = None | |
| resample_support[fid] = float(support) | |
| resample_median_rank[fid] = median_rank | |
| displayed_supports.append(float(support)) | |
| resample_mean_support = ( | |
| float(sum(displayed_supports) / len(displayed_supports)) if displayed_supports else None | |
| ) | |
| resample_high_support_count = sum(support >= 0.75 for support in displayed_supports) | |
| table_rows: list[list[object]] = [] | |
| chart_rows: list[list[object]] = [] | |
| default_candidate_id: int | None = None | |
| for rank, feature_tensor in enumerate(candidate_idx, start=1): | |
| fid = int(feature_tensor.item()) | |
| current_max = float(current_prompt_max[fid].item()) if current_context_available else 0.0 | |
| current_token = float(current_token_dense[fid].item()) if current_context_available else 0.0 | |
| if default_candidate_id is None and current_token > 0: | |
| default_candidate_id = fid | |
| score = float(ranking_values[fid].item()) | |
| row = [ | |
| rank, | |
| fid, | |
| score, | |
| float(target_mean[fid].item()), | |
| float(other_mean[fid].item()), | |
| float(mean_diff[fid].item()), | |
| float(selectivity[fid].item()), | |
| float(target_rate[fid].item()), | |
| float(other_rate[fid].item()), | |
| current_max, | |
| current_token, | |
| bool(current_token > 0), | |
| resample_support.get(fid) if resample_replicates else None, | |
| resample_median_rank.get(fid) if resample_replicates else None, | |
| ] | |
| table_rows.append(row) | |
| chart_rows.append([str(fid), score]) | |
| if default_candidate_id is None and candidate_idx.numel(): | |
| default_candidate_id = int(candidate_idx[0].item()) | |
| return ConceptFeatureDiscoveryResult( | |
| concept=concept, | |
| layer=int(layer), | |
| prompts_per_concept=n, | |
| top_n=top_n, | |
| ranking_mode=ranking_mode, | |
| rows=table_rows, | |
| chart_rows=chart_rows, | |
| candidate_ids=[int(x.item()) for x in candidate_idx], | |
| default_candidate_id=default_candidate_id, | |
| current_context_available=current_context_available, | |
| current_token_index=resolved_current_idx, | |
| displayed_current_active_count=sum(bool(row[11]) for row in table_rows), | |
| split_half_k=split_half_k, | |
| split_half_shared_count=split_half_shared_count, | |
| split_half_jaccard=split_half_jaccard, | |
| resample_replicates=resample_replicates, | |
| resample_mean_support=resample_mean_support, | |
| resample_high_support_count=resample_high_support_count, | |
| ) | |
| def candidate_causal_screen( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| feature_ids: Sequence[int], | |
| target_text: str, | |
| ) -> CandidateCausalScreenResult: | |
| """Cheaply triage several candidate features with one batched ablation screen. | |
| This deliberately omits random controls. Its purpose is to rank candidates before | |
| spending a full live causal test (with the random-control ensemble) on one or two | |
| promising features. All feature ablations share the same batched zero-edit reference. | |
| """ | |
| if not text.strip(): | |
| raise ValueError('Enter and inspect a Workbench prompt first.') | |
| if not target_text.strip(): | |
| raise ValueError('Enter a target continuation for candidate causal screening.') | |
| ids = list(dict.fromkeys(int(x) for x in feature_ids)) | |
| if not ids: | |
| raise ValueError('Select at least one candidate feature to screen.') | |
| if len(ids) > 8: | |
| raise ValueError('Candidate causal screening supports at most eight features per run.') | |
| if any(feature_id < 0 or feature_id >= self.settings.sae_width for feature_id in ids): | |
| raise ValueError(f'Feature ids must be in [0, {self.settings.sae_width - 1}].') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompt_inputs = self._inputs(text) | |
| prompt_len = int(prompt_inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), prompt_len) | |
| target_ids = self._target_ids(target_text) | |
| full_inputs = self._append_target(prompt_inputs, target_ids) | |
| sae = self.sae_store.get(int(layer)) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| single_baseline_out = self.model(**full_inputs, use_cache=False) | |
| single_logits = single_baseline_out.logits[0] | |
| _, single_mean, _ = sequence_logprob_summary( | |
| single_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| residual = capture['hidden'][0, idx] | |
| encoding = sae.encode(residual) | |
| activations = [float(encoding.activation_for(feature_id)) for feature_id in ids] | |
| deltas = [ | |
| residual_delta( | |
| sae.decoder_direction(feature_id), | |
| activation, | |
| InterventionSpec('ablate', 0.0), | |
| ) | |
| for feature_id, activation in zip(ids, activations, strict=True) | |
| ] | |
| all_deltas = torch.stack([torch.zeros_like(deltas[0]), *deltas], dim=0) | |
| repeated = self._repeat_inputs(full_inputs, all_deltas.shape[0]) | |
| with self._batch_delta_hook(int(layer), idx, all_deltas): | |
| outputs = self.model(**repeated, use_cache=False) | |
| baseline_logits = outputs.logits[0] | |
| baseline_seq, baseline_mean, _ = sequence_logprob_summary( | |
| baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| baseline_next = baseline_logits[prompt_len - 1] | |
| execution_drift_mean = float(baseline_mean - single_mean) | |
| execution_drift_js = js_divergence_from_logits( | |
| single_logits[prompt_len - 1], baseline_next | |
| ) | |
| scored: list[dict[str, object]] = [] | |
| for row_idx, (feature_id, activation, delta) in enumerate( | |
| zip(ids, activations, deltas, strict=True), start=1 | |
| ): | |
| logits = outputs.logits[row_idx] | |
| seq_logp, mean_logp, _ = sequence_logprob_summary( | |
| logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| mean_delta = float(mean_logp - baseline_mean) | |
| seq_delta = float(seq_logp - baseline_seq) | |
| js = float(js_divergence_from_logits(baseline_next, logits[prompt_len - 1])) | |
| norm = float(torch.linalg.vector_norm(delta.float()).item()) | |
| scored.append( | |
| { | |
| 'feature_id': int(feature_id), | |
| 'activation': float(activation), | |
| 'active': bool(activation > 0), | |
| 'norm': norm, | |
| 'mean_delta': mean_delta, | |
| 'seq_delta': seq_delta, | |
| 'js': js, | |
| } | |
| ) | |
| scored.sort( | |
| key=lambda item: (abs(float(item['mean_delta'])), float(item['js'])), | |
| reverse=True, | |
| ) | |
| rows: list[list[object]] = [] | |
| chart_rows: list[list[object]] = [] | |
| for rank, item in enumerate(scored, start=1): | |
| feature_id = int(item['feature_id']) | |
| mean_delta = float(item['mean_delta']) | |
| rows.append( | |
| [ | |
| rank, | |
| feature_id, | |
| float(item['activation']), | |
| bool(item['active']), | |
| float(item['norm']), | |
| mean_delta, | |
| float(item['seq_delta']), | |
| float(item['js']), | |
| ] | |
| ) | |
| chart_rows.append([str(feature_id), mean_delta]) | |
| return CandidateCausalScreenResult( | |
| feature_ids=[int(item['feature_id']) for item in scored], | |
| target_tokens=[self.tokenizer.decode([int(token_id)]) for token_id in target_ids], | |
| rows=rows, | |
| chart_rows=chart_rows, | |
| active_feature_count=sum(bool(item['active']) for item in scored), | |
| candidate_count=len(scored), | |
| execution_drift_mean_logprob=execution_drift_mean, | |
| execution_drift_js=float(execution_drift_js), | |
| ) | |
| def candidate_specificity_screen( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| feature_ids: Sequence[int], | |
| target_text: str, | |
| ) -> CandidateSpecificityResult: | |
| """Compare a small candidate set against per-feature norm-matched random ensembles. | |
| This is the controlled follow-up to :meth:`candidate_causal_screen`. Each SAE | |
| ablation gets its own deterministic random-control ensemble with the same L2 norm. | |
| All targeted and random conditions share one batched zero-edit reference so the | |
| comparison is both GPU-efficient and execution-context consistent. | |
| """ | |
| if not text.strip(): | |
| raise ValueError('Enter and inspect a Workbench prompt first.') | |
| if not target_text.strip(): | |
| raise ValueError('Enter a target continuation for controlled candidate comparison.') | |
| ids = list(dict.fromkeys(int(x) for x in feature_ids)) | |
| if not ids: | |
| raise ValueError('Select at least one candidate feature for controlled comparison.') | |
| if len(ids) > 3: | |
| raise ValueError('Controlled candidate comparison supports at most three features per run.') | |
| if any(feature_id < 0 or feature_id >= self.settings.sae_width for feature_id in ids): | |
| raise ValueError(f'Feature ids must be in [0, {self.settings.sae_width - 1}].') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompt_inputs = self._inputs(text) | |
| prompt_len = int(prompt_inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), prompt_len) | |
| target_ids = self._target_ids(target_text) | |
| full_inputs = self._append_target(prompt_inputs, target_ids) | |
| sae = self.sae_store.get(int(layer)) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| single_baseline_out = self.model(**full_inputs, use_cache=False) | |
| single_logits = single_baseline_out.logits[0] | |
| _, single_mean, _ = sequence_logprob_summary( | |
| single_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| residual = capture['hidden'][0, idx] | |
| encoding = sae.encode(residual) | |
| activations = [float(encoding.activation_for(feature_id)) for feature_id in ids] | |
| zero = torch.zeros_like(residual) | |
| all_deltas: list[torch.Tensor] = [zero] | |
| metadata: list[tuple[int, str, float, float]] = [] | |
| for feature_id, activation in zip(ids, activations, strict=True): | |
| delta = residual_delta( | |
| sae.decoder_direction(feature_id), | |
| activation, | |
| InterventionSpec('ablate', 0.0), | |
| ) | |
| norm = float(torch.linalg.vector_norm(delta.float()).item()) | |
| all_deltas.append(delta) | |
| metadata.append((feature_id, 'sae', activation, norm)) | |
| seed = self._control_seed(text, int(layer), str(feature_id), 'candidate_specificity', 0.0) | |
| controls = self._random_controls( | |
| delta, seed=seed, count=self.settings.live_random_controls | |
| ) | |
| for control_idx, control in enumerate(controls): | |
| all_deltas.append(control) | |
| metadata.append((feature_id, f'random_{control_idx}', activation, norm)) | |
| delta_batch = torch.stack(all_deltas, dim=0) | |
| repeated = self._repeat_inputs(full_inputs, delta_batch.shape[0]) | |
| with self._batch_delta_hook(int(layer), idx, delta_batch): | |
| outputs = self.model(**repeated, use_cache=False) | |
| baseline_logits = outputs.logits[0] | |
| baseline_seq, baseline_mean, _ = sequence_logprob_summary( | |
| baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| baseline_next = baseline_logits[prompt_len - 1] | |
| execution_drift_mean = float(baseline_mean - single_mean) | |
| execution_drift_js = js_divergence_from_logits( | |
| single_logits[prompt_len - 1], baseline_next | |
| ) | |
| grouped: dict[int, dict[str, object]] = { | |
| feature_id: { | |
| 'activation': activation, | |
| 'norm': 0.0, | |
| 'sae': None, | |
| 'random': [], | |
| } | |
| for feature_id, activation in zip(ids, activations, strict=True) | |
| } | |
| for output_idx, meta in enumerate(metadata, start=1): | |
| feature_id, kind, activation, norm = meta | |
| logits = outputs.logits[output_idx] | |
| seq_logp, mean_logp, _ = sequence_logprob_summary( | |
| logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| item = { | |
| 'seq_delta': float(seq_logp - baseline_seq), | |
| 'mean_delta': float(mean_logp - baseline_mean), | |
| 'js': float(js_divergence_from_logits(baseline_next, logits[prompt_len - 1])), | |
| } | |
| grouped[feature_id]['activation'] = float(activation) | |
| grouped[feature_id]['norm'] = float(norm) | |
| if kind == 'sae': | |
| grouped[feature_id]['sae'] = item | |
| else: | |
| random_items = grouped[feature_id]['random'] | |
| assert isinstance(random_items, list) | |
| random_items.append(item) | |
| scored: list[dict[str, object]] = [] | |
| for feature_id in ids: | |
| group = grouped[feature_id] | |
| sae_item = group['sae'] | |
| random_items = group['random'] | |
| assert isinstance(sae_item, dict) | |
| assert isinstance(random_items, list) and random_items | |
| mean_delta = float(sae_item['mean_delta']) | |
| js = float(sae_item['js']) | |
| random_mean_deltas = [float(item['mean_delta']) for item in random_items] | |
| random_js_values = [float(item['js']) for item in random_items] | |
| random_signed, random_abs, random_std, target_p = self._random_effect_summary( | |
| random_mean_deltas, mean_delta | |
| ) | |
| _, random_js_abs, random_js_std, js_p = self._random_effect_summary( | |
| random_js_values, js | |
| ) | |
| scored.append( | |
| { | |
| 'feature_id': int(feature_id), | |
| 'activation': float(group['activation']), | |
| 'active': bool(float(group['activation']) > 0), | |
| 'norm': float(group['norm']), | |
| 'mean_delta': mean_delta, | |
| 'seq_delta': float(sae_item['seq_delta']), | |
| 'random_signed': float(random_signed), | |
| 'random_abs': float(random_abs), | |
| 'random_std': float(random_std), | |
| 'target_ratio': float(abs(mean_delta) / max(random_abs, 1e-12)), | |
| 'target_p': float(target_p), | |
| 'js': js, | |
| 'random_js': float(random_js_abs), | |
| 'random_js_std': float(random_js_std), | |
| 'js_ratio': float(js / max(random_js_abs, 1e-12)), | |
| 'js_p': float(js_p), | |
| } | |
| ) | |
| scored.sort( | |
| key=lambda item: (float(item['target_ratio']), abs(float(item['mean_delta']))), | |
| reverse=True, | |
| ) | |
| rows: list[list[object]] = [] | |
| chart_rows: list[list[object]] = [] | |
| for rank, item in enumerate(scored, start=1): | |
| feature_id = int(item['feature_id']) | |
| rows.append( | |
| [ | |
| rank, | |
| feature_id, | |
| float(item['activation']), | |
| bool(item['active']), | |
| float(item['norm']), | |
| float(item['mean_delta']), | |
| float(item['random_signed']), | |
| float(item['random_abs']), | |
| float(item['random_std']), | |
| float(item['target_ratio']), | |
| float(item['target_p']), | |
| float(item['seq_delta']), | |
| float(item['js']), | |
| float(item['random_js']), | |
| float(item['random_js_std']), | |
| float(item['js_ratio']), | |
| float(item['js_p']), | |
| ] | |
| ) | |
| chart_rows.extend( | |
| [ | |
| [str(feature_id), 'Target specificity', float(item['target_ratio'])], | |
| [str(feature_id), 'JS specificity', float(item['js_ratio'])], | |
| ] | |
| ) | |
| return CandidateSpecificityResult( | |
| feature_ids=[int(item['feature_id']) for item in scored], | |
| target_tokens=[self.tokenizer.decode([int(token_id)]) for token_id in target_ids], | |
| rows=rows, | |
| chart_rows=chart_rows, | |
| active_feature_count=sum(bool(item['active']) for item in scored), | |
| candidate_count=len(scored), | |
| random_control_count=self.settings.live_random_controls, | |
| execution_drift_mean_logprob=execution_drift_mean, | |
| execution_drift_js=float(execution_drift_js), | |
| ) | |
| def candidate_cross_target_profile( | |
| self, | |
| text: str, | |
| layer: int, | |
| token_index: int, | |
| feature_ids: Sequence[int], | |
| targets: Sequence[str], | |
| ) -> CandidateCrossTargetResult: | |
| """Profile native candidate ablations across several exact target continuations. | |
| This is a screening diagnostic, not a random-controlled causal claim. The residual | |
| representation and native feature deltas are captured once from the Workbench prompt. | |
| Each target is then evaluated with one small batched forward containing the zero-edit | |
| reference plus every selected candidate ablation. | |
| """ | |
| if not text.strip(): | |
| raise ValueError('Enter and inspect a Workbench prompt first.') | |
| ids = list(dict.fromkeys(int(x) for x in feature_ids)) | |
| if not ids: | |
| raise ValueError('Select at least one candidate feature for cross-target profiling.') | |
| if len(ids) > 3: | |
| raise ValueError('Cross-target profiling supports at most three features per run.') | |
| if any(feature_id < 0 or feature_id >= self.settings.sae_width for feature_id in ids): | |
| raise ValueError(f'Feature ids must be in [0, {self.settings.sae_width - 1}].') | |
| target_list: list[str] = [] | |
| seen_targets: set[str] = set() | |
| for raw in targets: | |
| target = str(raw) | |
| if not target.strip() or target in seen_targets: | |
| continue | |
| seen_targets.add(target) | |
| target_list.append(target) | |
| if len(target_list) < 2: | |
| raise ValueError('Enter at least two distinct target continuations.') | |
| if len(target_list) > 5: | |
| raise ValueError('Cross-target profiling supports at most five target continuations.') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompt_inputs = self._inputs(text) | |
| prompt_len = int(prompt_inputs['input_ids'].shape[1]) | |
| idx = self._resolve_index(int(token_index), prompt_len) | |
| sae = self.sae_store.get(int(layer)) | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| self.model(**prompt_inputs, use_cache=False) | |
| residual = capture['hidden'][0, idx] | |
| encoding = sae.encode(residual) | |
| activations = [float(encoding.activation_for(feature_id)) for feature_id in ids] | |
| deltas = [ | |
| residual_delta( | |
| sae.decoder_direction(feature_id), | |
| activation, | |
| InterventionSpec('ablate', 0.0), | |
| ) | |
| for feature_id, activation in zip(ids, activations, strict=True) | |
| ] | |
| delta_batch = torch.stack([torch.zeros_like(residual), *deltas], dim=0) | |
| rows: list[list[object]] = [] | |
| chart_rows: list[list[object]] = [] | |
| by_feature: dict[int, list[tuple[str, float, float]]] = {feature_id: [] for feature_id in ids} | |
| for target_text in target_list: | |
| target_ids = self._target_ids(target_text) | |
| full_inputs = self._append_target(prompt_inputs, target_ids) | |
| repeated = self._repeat_inputs(full_inputs, delta_batch.shape[0]) | |
| with self._batch_delta_hook(int(layer), idx, delta_batch): | |
| outputs = self.model(**repeated, use_cache=False) | |
| baseline_logits = outputs.logits[0] | |
| baseline_seq, baseline_mean, _ = sequence_logprob_summary( | |
| baseline_logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| baseline_next = baseline_logits[prompt_len - 1] | |
| token_count = len(target_ids) | |
| for output_idx, (feature_id, activation, delta) in enumerate( | |
| zip(ids, activations, deltas, strict=True), start=1 | |
| ): | |
| logits = outputs.logits[output_idx] | |
| seq_logp, mean_logp, _ = sequence_logprob_summary( | |
| logits, prompt_length=prompt_len, target_ids=target_ids | |
| ) | |
| mean_delta = float(mean_logp - baseline_mean) | |
| seq_delta = float(seq_logp - baseline_seq) | |
| js = float(js_divergence_from_logits(baseline_next, logits[prompt_len - 1])) | |
| rows.append( | |
| [ | |
| int(feature_id), | |
| target_text, | |
| int(token_count), | |
| float(activation), | |
| float(torch.linalg.vector_norm(delta.float()).item()), | |
| mean_delta, | |
| seq_delta, | |
| js, | |
| ] | |
| ) | |
| chart_rows.append([target_text, str(feature_id), mean_delta]) | |
| by_feature[feature_id].append((target_text, mean_delta, js)) | |
| summary_rows: list[list[object]] = [] | |
| pairwise_rows: list[list[object]] = [] | |
| for feature_id in ids: | |
| items = by_feature[feature_id] | |
| strongest = max(items, key=lambda item: abs(item[1])) | |
| strongest_abs = abs(float(strongest[1])) | |
| other_abs = [abs(float(item[1])) for item in items if item is not strongest] | |
| mean_other = float(sum(other_abs) / len(other_abs)) if other_abs else 0.0 | |
| profile_ratio = float(strongest_abs / max(mean_other, 1e-12)) | |
| deltas = [float(item[1]) for item in items] | |
| abs_values = [abs(delta) for delta in deltas] | |
| total_abs = float(sum(abs_values)) | |
| if total_abs > 0 and len(abs_values) > 1: | |
| proportions = [value / total_abs for value in abs_values if value > 0] | |
| normalized_entropy = float( | |
| -sum(value * math.log(value) for value in proportions) / math.log(len(abs_values)) | |
| ) | |
| else: | |
| normalized_entropy = 0.0 | |
| effect_concentration = float(1.0 - normalized_entropy) | |
| signed_bias = float(sum(deltas) / total_abs) if total_abs > 0 else 0.0 | |
| signs = {1 if delta > 0 else -1 if delta < 0 else 0 for delta in deltas} | |
| nonzero_signs = {sign for sign in signs if sign != 0} | |
| sign_consistency = 'same sign' if len(nonzero_signs) <= 1 else 'mixed signs' | |
| if effect_concentration >= 0.25: | |
| profile_pattern = ( | |
| 'target-concentrated / mixed-sign' | |
| if sign_consistency == 'mixed signs' | |
| else 'target-concentrated / same-sign' | |
| ) | |
| elif signed_bias <= -0.8: | |
| profile_pattern = 'broad same-sign suppression' | |
| elif signed_bias >= 0.8: | |
| profile_pattern = 'broad same-sign enhancement' | |
| else: | |
| profile_pattern = 'broad mixed-sign' | |
| summary_rows.append( | |
| [ | |
| int(feature_id), | |
| str(strongest[0]), | |
| float(strongest[1]), | |
| strongest_abs, | |
| mean_other, | |
| profile_ratio, | |
| sign_consistency, | |
| normalized_entropy, | |
| effect_concentration, | |
| signed_bias, | |
| profile_pattern, | |
| max(float(item[2]) for item in items), | |
| ] | |
| ) | |
| for left_idx in range(len(items)): | |
| for right_idx in range(left_idx + 1, len(items)): | |
| target_a, delta_a, _ = items[left_idx] | |
| target_b, delta_b, _ = items[right_idx] | |
| preference_shift = float(delta_a - delta_b) | |
| direction = ( | |
| f'toward {target_a}' | |
| if preference_shift > 0 | |
| else f'toward {target_b}' if preference_shift < 0 else 'no shift' | |
| ) | |
| pairwise_rows.append( | |
| [ | |
| int(feature_id), | |
| str(target_a), | |
| str(target_b), | |
| preference_shift, | |
| abs(preference_shift), | |
| direction, | |
| ] | |
| ) | |
| summary_rows.sort(key=lambda row: float(row[3]), reverse=True) | |
| pairwise_rows.sort(key=lambda row: float(row[4]), reverse=True) | |
| return CandidateCrossTargetResult( | |
| feature_ids=ids, | |
| targets=target_list, | |
| rows=rows, | |
| chart_rows=chart_rows, | |
| summary_rows=summary_rows, | |
| pairwise_rows=pairwise_rows, | |
| active_feature_count=sum(activation > 0 for activation in activations), | |
| ) | |
| def _cue_prompt(stem: str, cue: str) -> str: | |
| stem = stem.rstrip() | |
| cue = cue.strip() | |
| if not cue: | |
| return stem | |
| if cue[0] in ':;,.!?=)]}': | |
| return stem + cue | |
| return stem + ' ' + cue | |
| def feature_cue_scan( | |
| self, | |
| feature_id: int, | |
| layer: int, | |
| prompt_stem: str, | |
| cues: Sequence[str], | |
| ) -> FeatureCueScanResult: | |
| """Measure one feature at the final token after appending controlled completion cues.""" | |
| if not prompt_stem.strip(): | |
| raise ValueError('Enter a prompt stem.') | |
| cue_list: list[str] = [] | |
| seen: set[str] = set() | |
| for raw in cues: | |
| cue = str(raw).strip() | |
| if not cue or cue in seen: | |
| continue | |
| seen.add(cue) | |
| cue_list.append(cue) | |
| if not cue_list: | |
| raise ValueError('Enter at least one cue.') | |
| if len(cue_list) > 12: | |
| raise ValueError('Cue scan supports at most 12 cues per run.') | |
| if int(feature_id) < 0 or int(feature_id) >= self.settings.sae_width: | |
| raise ValueError(f'Feature id must be in [0, {self.settings.sae_width - 1}].') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| prompts = [self._cue_prompt(prompt_stem, cue) for cue in cue_list] | |
| batch = self.tokenizer( | |
| prompts, | |
| return_tensors='pt', | |
| padding=True, | |
| truncation=True, | |
| max_length=self.settings.max_prompt_tokens, | |
| ) | |
| batch = {key: value.to(self.device) for key, value in batch.items()} | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| self.model(**batch, use_cache=False) | |
| sae = self.sae_store.get(int(layer)) | |
| encoding = sae.encode(capture['hidden']) | |
| attention = batch.get('attention_mask', torch.ones_like(batch['input_ids'])).bool() | |
| rows: list[list[object]] = [] | |
| chart_rows: list[list[object]] = [] | |
| active_count = 0 | |
| for row_idx, (cue, full_prompt) in enumerate(zip(cue_list, prompts, strict=True)): | |
| valid_positions = torch.nonzero(attention[row_idx], as_tuple=False).flatten() | |
| final_pos = int(valid_positions[-1].item()) | |
| indices = encoding.indices[row_idx, final_pos] | |
| values = encoding.values[row_idx, final_pos] | |
| mask = indices == int(feature_id) | |
| activation = float(values[mask][0].item()) if bool(mask.any()) else 0.0 | |
| active = activation > 0 | |
| active_count += int(active) | |
| token_id = int(batch['input_ids'][row_idx, final_pos].item()) | |
| final_token = self.tokenizer.decode([token_id]) | |
| rows.append([cue, full_prompt, repr(final_token), activation, active]) | |
| chart_rows.append([cue, activation]) | |
| return FeatureCueScanResult( | |
| feature_id=int(feature_id), | |
| layer=int(layer), | |
| prompt_stem=prompt_stem, | |
| rows=rows, | |
| chart_rows=chart_rows, | |
| active_cue_count=active_count, | |
| cue_count=len(cue_list), | |
| ) | |
| def feature_cue_context_scan( | |
| self, | |
| feature_id: int, | |
| layer: int, | |
| stems: Sequence[str], | |
| cues: Sequence[str], | |
| ) -> FeatureCueContextResult: | |
| """Cross completion cues with multiple prompt stems in one batch. | |
| This distinguishes a cue-specific response (for example a feature that responds to ``is`` everywhere) | |
| from a context-sensitive completion-boundary response. | |
| """ | |
| stem_list: list[str] = [] | |
| seen_stems: set[str] = set() | |
| for raw in stems: | |
| stem = str(raw).strip() | |
| if not stem or stem in seen_stems: | |
| continue | |
| seen_stems.add(stem) | |
| stem_list.append(stem) | |
| cue_list: list[str] = [] | |
| seen_cues: set[str] = set() | |
| for raw in cues: | |
| cue = str(raw).strip() | |
| if not cue or cue in seen_cues: | |
| continue | |
| seen_cues.add(cue) | |
| cue_list.append(cue) | |
| if not stem_list: | |
| raise ValueError('Enter at least one prompt stem.') | |
| if not cue_list: | |
| raise ValueError('Enter at least one completion cue.') | |
| if len(stem_list) > 8: | |
| raise ValueError('Cue-context scan supports at most 8 prompt stems.') | |
| if len(cue_list) > 8: | |
| raise ValueError('Cue-context scan supports at most 8 completion cues.') | |
| if len(stem_list) * len(cue_list) > 40: | |
| raise ValueError('Cue-context scan supports at most 40 stem × cue conditions per run.') | |
| if int(feature_id) < 0 or int(feature_id) >= self.settings.sae_width: | |
| raise ValueError(f'Feature id must be in [0, {self.settings.sae_width - 1}].') | |
| self.ensure_ready(preload_saes=False) | |
| assert self.model is not None and self.tokenizer is not None and self.sae_store is not None | |
| conditions: list[tuple[str, str, str]] = [] | |
| for stem in stem_list: | |
| for cue in cue_list: | |
| conditions.append((stem, cue, self._cue_prompt(stem, cue))) | |
| prompts = [full for _, _, full in conditions] | |
| batch = self.tokenizer( | |
| prompts, | |
| return_tensors='pt', | |
| padding=True, | |
| truncation=True, | |
| max_length=self.settings.max_prompt_tokens, | |
| ) | |
| batch = {key: value.to(self.device) for key, value in batch.items()} | |
| capture: dict = {} | |
| with self._capture_hook(int(layer), capture): | |
| self.model(**batch, use_cache=False) | |
| sae = self.sae_store.get(int(layer)) | |
| encoding = sae.encode(capture['hidden']) | |
| attention = batch.get('attention_mask', torch.ones_like(batch['input_ids'])).bool() | |
| rows: list[list[object]] = [] | |
| chart_rows: list[list[object]] = [] | |
| active_count = 0 | |
| cue_active_context_counts = {cue: 0 for cue in cue_list} | |
| cue_activation_values = {cue: [] for cue in cue_list} | |
| for row_idx, (stem, cue, full_prompt) in enumerate(conditions): | |
| valid_positions = torch.nonzero(attention[row_idx], as_tuple=False).flatten() | |
| final_pos = int(valid_positions[-1].item()) | |
| indices = encoding.indices[row_idx, final_pos] | |
| values = encoding.values[row_idx, final_pos] | |
| mask = indices == int(feature_id) | |
| activation = float(values[mask][0].item()) if bool(mask.any()) else 0.0 | |
| active = activation > 0 | |
| active_count += int(active) | |
| cue_active_context_counts[cue] += int(active) | |
| cue_activation_values[cue].append(activation) | |
| token_id = int(batch['input_ids'][row_idx, final_pos].item()) | |
| final_token = self.tokenizer.decode([token_id]) | |
| short_stem = stem if len(stem) <= 42 else stem[:39] + '…' | |
| rows.append([stem, cue, full_prompt, repr(final_token), activation, active]) | |
| chart_rows.append([short_stem, cue, activation]) | |
| cue_mean_activations = { | |
| cue: float(sum(vals) / len(vals)) if vals else 0.0 | |
| for cue, vals in cue_activation_values.items() | |
| } | |
| dominant_cue = max( | |
| cue_list, | |
| key=lambda cue: (cue_active_context_counts[cue], cue_mean_activations[cue]), | |
| ) if cue_list else None | |
| dominant_count = cue_active_context_counts.get(dominant_cue, 0) if dominant_cue else 0 | |
| off_dominant_active_count = sum( | |
| count for cue, count in cue_active_context_counts.items() if cue != dominant_cue | |
| ) | |
| return FeatureCueContextResult( | |
| feature_id=int(feature_id), | |
| layer=int(layer), | |
| stems=stem_list, | |
| cues=cue_list, | |
| rows=rows, | |
| chart_rows=chart_rows, | |
| active_condition_count=active_count, | |
| condition_count=len(conditions), | |
| cue_active_context_counts=cue_active_context_counts, | |
| cue_mean_activations=cue_mean_activations, | |
| dominant_cue=dominant_cue, | |
| dominant_cue_context_count=dominant_count, | |
| off_dominant_active_count=off_dominant_active_count, | |
| ) | |
| RUNTIME = FeatureLensRuntime() | |
| if SETTINGS.eager_load: | |
| try: | |
| RUNTIME.ensure_ready(preload_saes=True) | |
| except Exception as exc: # Keep the UI alive and surface a useful error on first call. | |
| RUNTIME.load_error = f'{type(exc).__name__}: {exc}' | |
| print(f'FeatureLens eager load failed: {RUNTIME.load_error}') | |