Spaces:
Running
Running
| from __future__ import annotations | |
| import tomllib | |
| from functools import lru_cache | |
| from pathlib import Path | |
| from typing import Any | |
| import numpy as np | |
| import torch | |
| from huggingface_hub import hf_hub_download | |
| from safetensors.torch import load_file | |
| from .probe import _decode_token_text, _layer_key_to_hidden_index | |
| V6_ROOT = Path(__file__).resolve().parents[1] | |
| CONFIG_ROOT = V6_ROOT / "configs" / "comparisons" | |
| def list_sae_layers(model_name: str) -> list[str]: | |
| config = load_sae_config(model_name) | |
| model_config = load_comparison_config(model_name).get("model", {}) | |
| n_layers = int(model_config.get("num_hidden_layers") or 0) | |
| layer_checkpoints = config.get("layer_checkpoints") | |
| if isinstance(layer_checkpoints, dict) and layer_checkpoints: | |
| indices = sorted(int(key) for key in layer_checkpoints) | |
| else: | |
| indices = list(range(n_layers)) | |
| return [f"layer_{idx:02d}" for idx in indices] | |
| def load_comparison_config(model_name: str) -> dict[str, Any]: | |
| path = CONFIG_ROOT / f"{model_name}.toml" | |
| if not path.is_file(): | |
| raise FileNotFoundError(f"No SAE comparison config for {model_name!r}: {path}") | |
| return tomllib.loads(path.read_text(encoding="utf-8")) | |
| def load_sae_config(model_name: str) -> dict[str, Any]: | |
| data = load_comparison_config(model_name) | |
| if "sae" not in data: | |
| raise KeyError(f"Comparison config for {model_name!r} has no [sae] section.") | |
| return dict(data["sae"]) | |
| def interpret_text_sae_probe( | |
| *, | |
| model: torch.nn.Module, | |
| tokenizer: Any, | |
| text: str, | |
| model_name: str, | |
| layer: str, | |
| top_k: int, | |
| max_length: int, | |
| ) -> dict[str, Any]: | |
| if layer == "embedding": | |
| raise ValueError("SAE Explorer currently supports transformer layers, not embedding.") | |
| layer_index = int(layer.removeprefix("layer_")) | |
| hidden_index = _layer_key_to_hidden_index(layer, num_transformer_layers=int(model.config.num_hidden_layers)) | |
| full_ids = tokenizer.encode(text, add_special_tokens=True) | |
| truncated = len(full_ids) > max_length | |
| encoded = tokenizer(text, return_tensors="pt", truncation=True, max_length=max_length) | |
| input_ids = encoded["input_ids"].to(next(model.parameters()).device) | |
| attention_mask = encoded.get("attention_mask") | |
| if attention_mask is not None: | |
| attention_mask = attention_mask.to(input_ids.device) | |
| with torch.inference_mode(): | |
| outputs = model( | |
| input_ids=input_ids, | |
| attention_mask=attention_mask, | |
| output_hidden_states=True, | |
| use_cache=False, | |
| ) | |
| hidden_states = outputs.hidden_states | |
| if hidden_states is None: | |
| raise RuntimeError("Model did not return hidden states.") | |
| hidden = hidden_states[hidden_index][0].to(dtype=torch.float32) | |
| sae = load_sae(model_name, layer_index, hidden_size=int(hidden.shape[-1]), device=hidden.device) | |
| pre, acts = sae_forward(hidden, sae=sae) | |
| idx, vals = topk_features(acts, top_k=top_k) | |
| ids = input_ids[0].detach().cpu().tolist() | |
| idx_cpu = idx.detach().cpu().tolist() | |
| vals_cpu = vals.detach().cpu().tolist() | |
| pre_cpu = pre.detach().cpu() | |
| tokens = [] | |
| for pos, token_id in enumerate(ids): | |
| top = [] | |
| for j in range(len(idx_cpu[pos])): | |
| feature = int(idx_cpu[pos][j]) | |
| top.append( | |
| { | |
| "feature": feature, | |
| "activation": float(vals_cpu[pos][j]), | |
| "preactivation": float(pre_cpu[pos, feature].item()), | |
| } | |
| ) | |
| tokens.append( | |
| { | |
| "position": pos, | |
| "token_id": int(token_id), | |
| "token": tokenizer.convert_ids_to_tokens(int(token_id)), | |
| "token_text": _decode_token_text(tokenizer, int(token_id)), | |
| "top": top, | |
| } | |
| ) | |
| return { | |
| "model_name": model_name, | |
| "layer": layer, | |
| "top_k": int(top_k), | |
| "max_length": int(max_length), | |
| "tokens": tokens, | |
| "seq_len": len(tokens), | |
| "truncated": truncated, | |
| "sae_width": int(sae["w_enc"].shape[1]), | |
| "activation_rule": sae["activation_rule"], | |
| } | |
| def _load_sae_cpu(model_name: str, layer: int, hidden_size: int) -> dict[str, Any]: | |
| config = load_sae_config(model_name) | |
| path = _resolve_sae_weights_path(config, layer=layer) | |
| weights = _load_sae_weights(path, config=config) | |
| w_dec = _decoder_weight(weights, hidden_size=hidden_size, preferred_key=config.get("decoder_key")).to(dtype=torch.float32) | |
| w_enc = _encoder_weight(weights, hidden_size=hidden_size, d_sae=int(w_dec.shape[0])).to(dtype=torch.float32).contiguous() | |
| b_enc = _vector_weight(weights, names=("b_enc", "encoder.bias"), length=int(w_enc.shape[1])) | |
| b_dec = _vector_weight(weights, names=("b_dec", "decoder.bias", "bias"), length=hidden_size) | |
| threshold = weights.get("threshold") | |
| return { | |
| "w_enc": w_enc, | |
| "b_enc": b_enc.to(dtype=torch.float32).contiguous(), | |
| "b_dec": b_dec.to(dtype=torch.float32).contiguous(), | |
| "threshold": threshold.to(dtype=torch.float32).contiguous() if threshold is not None else None, | |
| "top_k": _sae_top_k(config), | |
| "activation_rule": _activation_rule_label(config), | |
| } | |
| def load_sae(model_name: str, layer: int, *, hidden_size: int, device: torch.device) -> dict[str, Any]: | |
| cpu = _load_sae_cpu(model_name, layer, hidden_size) | |
| return { | |
| key: value.to(device=device, dtype=torch.float32, non_blocking=True) if isinstance(value, torch.Tensor) else value | |
| for key, value in cpu.items() | |
| } | |
| def sae_forward(hidden: torch.Tensor, *, sae: dict[str, Any]) -> tuple[torch.Tensor, torch.Tensor]: | |
| centered = hidden.to(dtype=torch.float32) - sae["b_dec"].unsqueeze(0) | |
| pre = centered @ sae["w_enc"] + sae["b_enc"].unsqueeze(0) | |
| threshold = sae["threshold"] | |
| if threshold is not None: | |
| return pre, torch.where(pre > threshold.unsqueeze(0), pre, torch.zeros_like(pre)) | |
| top_k = sae["top_k"] | |
| if top_k is not None: | |
| k = min(int(top_k), int(pre.shape[1])) | |
| values, indices = torch.topk(pre, k=k, dim=1) | |
| acts = torch.zeros_like(pre) | |
| acts.scatter_(1, indices, torch.clamp(values, min=0.0)) | |
| return pre, acts | |
| return pre, torch.relu(pre) | |
| def topk_features(acts: torch.Tensor, *, top_k: int) -> tuple[torch.Tensor, torch.Tensor]: | |
| k = min(int(top_k), int(acts.shape[1])) | |
| if k <= 0: | |
| raise ValueError("top_k must be positive.") | |
| vals, idx = torch.topk(acts, k=k, dim=1) | |
| return idx, vals | |
| def _resolve_sae_weights_path(config: dict[str, Any], *, layer: int) -> str: | |
| layer_checkpoints = config.get("layer_checkpoints") | |
| if isinstance(layer_checkpoints, dict) and str(layer) in layer_checkpoints: | |
| filename = str(layer_checkpoints[str(layer)]) | |
| else: | |
| filename = str(config["checkpoint_template"]).format(layer=layer) | |
| return hf_hub_download(repo_id=str(config["repo_id"]), filename=filename, revision=config.get("revision")) | |
| def _load_sae_weights(path: str, *, config: dict[str, Any]) -> dict[str, torch.Tensor]: | |
| checkpoint_format = str(config.get("checkpoint_format", "")).lower() | |
| if checkpoint_format == "safetensors": | |
| return load_file(path, device="cpu") | |
| if checkpoint_format == "npz": | |
| with np.load(path) as arrays: | |
| return {key: torch.from_numpy(arrays[key]) for key in arrays.files} | |
| if checkpoint_format in {"torch", "pt", "pth"}: | |
| try: | |
| loaded = torch.load(path, map_location="cpu", weights_only=True) | |
| except TypeError: | |
| loaded = torch.load(path, map_location="cpu") | |
| if not isinstance(loaded, dict): | |
| raise TypeError(f"Expected torch checkpoint {path} to contain a dict, got {type(loaded).__name__}.") | |
| return _flatten_tensor_dict(loaded) | |
| raise ValueError(f"Unsupported SAE checkpoint_format {checkpoint_format!r}.") | |
| def _flatten_tensor_dict(payload: dict[str, Any], prefix: str = "") -> dict[str, torch.Tensor]: | |
| flattened: dict[str, torch.Tensor] = {} | |
| for key, value in payload.items(): | |
| name = f"{prefix}.{key}" if prefix else str(key) | |
| if isinstance(value, torch.Tensor): | |
| flattened[name] = value | |
| flattened[str(key)] = value | |
| elif isinstance(value, dict): | |
| flattened.update(_flatten_tensor_dict(value, name)) | |
| return flattened | |
| def _encoder_weight(weights: dict[str, torch.Tensor], *, hidden_size: int, d_sae: int) -> torch.Tensor: | |
| for key in ("W_enc", "encoder.weight", "W_enc.weight", "encoder.W_enc"): | |
| tensor = weights.get(key) | |
| if tensor is not None: | |
| return _orient_encoder(tensor, hidden_size=hidden_size, d_sae=d_sae) | |
| return _decoder_weight(weights, hidden_size=hidden_size).T.contiguous() | |
| def _decoder_weight( | |
| weights: dict[str, torch.Tensor], | |
| *, | |
| hidden_size: int, | |
| preferred_key: object = None, | |
| ) -> torch.Tensor: | |
| if preferred_key is not None: | |
| key = str(preferred_key) | |
| if key not in weights: | |
| raise KeyError(f"Configured decoder_key {key!r} not found. Available keys: {sorted(weights)}") | |
| return _orient_decoder(weights[key], hidden_size=hidden_size) | |
| for key in ("W_dec", "decoder.weight", "W_dec.weight", "decoder.W_dec"): | |
| tensor = weights.get(key) | |
| if tensor is not None: | |
| return _orient_decoder(tensor, hidden_size=hidden_size) | |
| candidates = [tensor for tensor in weights.values() if tensor.ndim == 2 and hidden_size in tensor.shape] | |
| if len(candidates) == 1: | |
| return _orient_decoder(candidates[0], hidden_size=hidden_size) | |
| raise KeyError(f"Could not identify SAE decoder weight. Available keys: {sorted(weights)}") | |
| def _vector_weight(weights: dict[str, torch.Tensor], *, names: tuple[str, ...], length: int) -> torch.Tensor: | |
| for name in names: | |
| tensor = weights.get(name) | |
| if tensor is not None: | |
| if int(tensor.numel()) != int(length): | |
| raise ValueError(f"SAE vector {name!r} has length {tensor.numel()}, expected {length}.") | |
| return tensor.reshape(length).to(dtype=torch.float32) | |
| return torch.zeros(length, dtype=torch.float32) | |
| def _orient_decoder(tensor: torch.Tensor, *, hidden_size: int) -> torch.Tensor: | |
| if int(tensor.shape[1]) == hidden_size: | |
| return tensor | |
| if int(tensor.shape[0]) == hidden_size: | |
| return tensor.T | |
| raise ValueError(f"Decoder weight shape {tuple(tensor.shape)} does not contain hidden size {hidden_size}.") | |
| def _orient_encoder(tensor: torch.Tensor, *, hidden_size: int, d_sae: int) -> torch.Tensor: | |
| if tuple(tensor.shape) == (hidden_size, d_sae): | |
| return tensor | |
| if tuple(tensor.shape) == (d_sae, hidden_size): | |
| return tensor.T | |
| if int(tensor.shape[0]) == hidden_size: | |
| return tensor | |
| if int(tensor.shape[1]) == hidden_size: | |
| return tensor.T | |
| raise ValueError(f"Encoder weight shape {tuple(tensor.shape)} does not match hidden={hidden_size}, d_sae={d_sae}.") | |
| def _sae_top_k(config: dict[str, Any]) -> int | None: | |
| if config.get("top_k") is not None: | |
| return int(config["top_k"]) | |
| repo = str(config.get("repo_id") or "") | |
| if "GPT2-Small-OAI-v5-32k-resid-post-SAEs" in repo: | |
| return 32 | |
| return None | |
| def _activation_rule_label(config: dict[str, Any]) -> str: | |
| if config.get("top_k") is not None: | |
| return f"topk-{int(config['top_k'])}" | |
| if str(config.get("activation") or "").lower() == "topk": | |
| return "topk" | |
| return "relu_or_threshold" | |