Buckets:

Mercity/dpo-soft-prefix / 1906_scope /temporal_dynamics.py
Rishik001's picture
download
raw
14.7 kB
"""
Temporal dynamics analysis with properly calibrated per-step classifiers.
For each snapshot step t (every SNAPSHOT_INTERVAL steps), trains a separate
LogReg on cumulative SAE features 0..t from the training set, then evaluates
on the test set. No distribution shift — the classifier at step t has only
ever seen inputs of the same density as what it's tested on.
Also computes cross-topic variance across all 16,384 SAE features at each
snapshot to show which steps and which features carry the most information.
"""
import json
import sys
import types
import importlib.machinery
if "torchaudio" not in sys.modules:
_ta_stub = types.ModuleType("torchaudio")
_ta_stub.__spec__ = importlib.machinery.ModuleSpec("torchaudio", None)
sys.modules["torchaudio"] = _ta_stub
import numpy as np
import pandas as pd
import torch
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file as safetensors_load
from sklearn.linear_model import LogisticRegression
from transformers import AutoModelForCausalLM, AutoTokenizer
from models import JumpReLUSAE
from utils import load_topic_data, split_data, build_labeled_set
# ---------------------------------------------------------------------------
# Config — edit CHECKPOINT to point at the run you want to analyse
# ---------------------------------------------------------------------------
CHECKPOINT = None # auto-detected from checkpoints_1k_1b/
CHECKPOINT_DIR = "checkpoints_1k_1b"
MODEL_NAME = "google/gemma-3-1b-it"
SAE_REPO = "google/gemma-scope-2-1b-it"
SAE_WIDTH = "16k"
SAE_L0 = "small"
SNAPSHOT_INTERVAL = 10 # analyse every N steps
MAX_STEPS = 300 # stop analysis here (captures ~99% of signal)
BATCH_SIZE = 32
MAX_INPUT_TOKENS = 2048
SEED = 42
TRAIN_RATIO = 0.8
CONF_TARGETS = [0.90, 0.95, 0.99]
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
DTYPE = torch.bfloat16 if DEVICE == "cuda" else torch.float32
from pathlib import Path
import glob
# ---------------------------------------------------------------------------
# Locate checkpoint
# ---------------------------------------------------------------------------
runs = sorted(glob.glob(f"{CHECKPOINT_DIR}/*_meta.json"))
if not runs:
raise FileNotFoundError(f"No checkpoint found in {CHECKPOINT_DIR}/")
meta_path = runs[-1] # most recent
run_prefix = meta_path.replace("_meta.json", "")
meta = json.load(open(meta_path))
feature_ids = np.array(meta["feature_ids"])
topics = meta["topics"]
best_layer = meta["best_layer"]
n_classes = len(topics)
print(f"Checkpoint: {run_prefix}")
print(f"Best layer: L{best_layer} | Features: {len(feature_ids)} | Topics: {n_classes}\n")
# ---------------------------------------------------------------------------
# Load SAE (best layer only)
# ---------------------------------------------------------------------------
print(f"Loading SAE L{best_layer} ...")
filename = f"resid_post_all/layer_{best_layer}_width_{SAE_WIDTH}_l0_{SAE_L0}/params.safetensors"
path = hf_hub_download(repo_id=SAE_REPO, filename=filename)
raw = safetensors_load(path, device="cpu")
sae = JumpReLUSAE(raw["w_enc"].shape[0], raw["w_enc"].shape[1])
with torch.no_grad():
sae.W_enc.copy_(raw["w_enc"].float())
sae.b_enc.copy_(raw["b_enc"].float())
sae.b_dec.copy_(raw["b_dec"].float())
sae.threshold.copy_(raw["threshold"].float())
sae = sae.to(DEVICE).eval()
d_sae = sae.d_sae
print(f" SAE: d_model={sae.d_model}, d_sae={d_sae}\n")
# ---------------------------------------------------------------------------
# Load model
# ---------------------------------------------------------------------------
print("Loading model ...")
tok = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
if tok.pad_token is None:
tok.pad_token = tok.eos_token
tok.padding_side = "left"
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME, dtype=DTYPE, trust_remote_code=True,
attn_implementation="flash_attention_2"
).to(DEVICE).eval()
model.config.use_cache = True
layers_module = model.model.layers
print(" model loaded\n")
# ---------------------------------------------------------------------------
# Load data (same split as training run)
# ---------------------------------------------------------------------------
from utils import load_topic_data, split_data, build_labeled_set
from pathlib import Path
data = load_topic_data({}, 5000, SEED, jsonl_path=Path("prompts.jsonl"))
train_data, test_data = split_data(data, TRAIN_RATIO, seed=SEED)
train_texts, train_labels = build_labeled_set(train_data, topics)
test_texts, test_labels = build_labeled_set(test_data, topics)
train_labels = np.array(train_labels)
test_labels = np.array(test_labels)
print(f"Data: {len(train_texts)} train / {len(test_texts)} test\n")
# ---------------------------------------------------------------------------
# Generation with per-step capture at best_layer
# ---------------------------------------------------------------------------
snapshot_steps = list(range(SNAPSHOT_INTERVAL, MAX_STEPS + 1, SNAPSHOT_INTERVAL))
n_snapshots = len(snapshot_steps)
n_train = len(train_texts)
n_test = len(test_texts)
# Accumulators for cross-topic variance
# topic_sum_fired[s, topic, f] = sum of cumulative fired indicator over training samples
topic_sum_fired = np.zeros((n_snapshots, n_classes, d_sae), dtype=np.float32)
topic_counts = np.bincount(train_labels, minlength=n_classes).astype(np.float32)
# Per-sample cumulative feature vectors at each snapshot (top-100 only)
train_cum = np.zeros((n_train, n_snapshots, len(feature_ids)), dtype=np.bool_)
test_cum = np.zeros((n_test, n_snapshots, len(feature_ids)), dtype=np.bool_)
captured = {}
def make_hook():
def _hook(_module, _input, output):
hs = output[0] if isinstance(output, tuple) else output
captured["h"] = hs.float()
return _hook
hook = layers_module[best_layer].register_forward_hook(make_hook())
@torch.inference_mode()
def run_capture(texts, labels_arr, cum_out, topic_sum=None):
"""Generate and capture per-step SAE firings. Updates cum_out in-place."""
n = len(texts)
n_batches = (n + BATCH_SIZE - 1) // BATCH_SIZE
for batch_idx in range(n_batches):
start = batch_idx * BATCH_SIZE
end = min(start + BATCH_SIZE, n)
bs = end - start
print(f" batch {batch_idx+1}/{n_batches}", end="\r")
formatted = [
tok.apply_chat_template(
[{"role": "user", "content": t}],
tokenize=False, add_generation_prompt=True,
)
for t in texts[start:end]
]
enc = tok(
formatted, return_tensors="pt", padding=True,
truncation=True, max_length=MAX_INPUT_TOKENS,
add_special_tokens=False,
).to(DEVICE)
# running cumulative fired per sample in this batch: (bs, d_sae)
batch_cum = np.zeros((bs, d_sae), dtype=np.bool_)
active = torch.ones(bs, dtype=torch.bool, device=DEVICE)
# prefill
captured.clear()
out = model(**enc, use_cache=True)
past_kv = out.past_key_values
next_tok = out.logits[:, -1, :].argmax(-1, keepdim=True)
for step in range(1, MAX_STEPS + 1):
active = active & (next_tok.squeeze(-1) != tok.eos_token_id)
if not active.any():
break
captured.clear()
out = model(next_tok, past_key_values=past_kv, use_cache=True)
past_kv = out.past_key_values
next_tok = out.logits[:, -1, :].argmax(-1, keepdim=True)
h = captured["h"]
if h.dim() == 3:
h = h[:, -1, :]
fired = sae.fired_mask(h).cpu().numpy() # (bs, d_sae)
for i in range(bs):
if active[i].item():
batch_cum[i] |= fired[i]
# snapshot?
if step % SNAPSHOT_INTERVAL == 0:
si = step // SNAPSHOT_INTERVAL - 1
for i in range(bs):
gi = start + i # global sample index
cum_out[gi, si] = batch_cum[i][feature_ids]
if topic_sum is not None:
# Include all samples (inactive ones have frozen cumulative features)
topic_sum[si, int(labels_arr[gi])] += batch_cum[i]
del past_kv, out, enc
if DEVICE == "cuda":
torch.cuda.empty_cache()
print()
try:
print("="*72)
print("Generating from train set ...")
print("="*72)
run_capture(train_texts, train_labels, train_cum, topic_sum=topic_sum_fired)
print("="*72)
print("Generating from test set ...")
print("="*72)
run_capture(test_texts, test_labels, test_cum, topic_sum=None)
finally:
hook.remove()
# ---------------------------------------------------------------------------
# Cross-topic variance at each snapshot (all d_sae features)
# ---------------------------------------------------------------------------
# topic_mean_fired[s, t, f] = mean firing rate for snapshot s, topic t, feature f
topic_mean_fired = topic_sum_fired / topic_counts[None, :, None] # broadcast
# variance across 7 topics
variance_per_snapshot = topic_mean_fired.var(axis=1) # (n_snapshots, d_sae)
# ---------------------------------------------------------------------------
# Train per-step LogReg and evaluate
# ---------------------------------------------------------------------------
print("Training per-snapshot LogReg classifiers ...")
accuracy_per_snapshot = np.zeros(n_snapshots)
mean_conf_per_snapshot = np.zeros(n_snapshots)
# per-topic: (n_classes, n_snapshots)
topic_accuracy = np.zeros((n_classes, n_snapshots))
topic_conf = np.zeros((n_classes, n_snapshots))
for si, t in enumerate(snapshot_steps):
X_tr = train_cum[:, si, :].astype(np.float32)
X_te = test_cum[:, si, :].astype(np.float32)
clf = LogisticRegression(C=10.0, max_iter=1000, random_state=SEED)
clf.fit(X_tr, train_labels)
proba = clf.predict_proba(X_te) # (n_test, n_classes)
preds = proba.argmax(axis=1)
confs = proba.max(axis=1)
accuracy_per_snapshot[si] = (preds == test_labels).mean()
mean_conf_per_snapshot[si] = confs.mean()
for t_idx in range(n_classes):
mask = test_labels == t_idx
topic_accuracy[t_idx, si] = (preds[mask] == test_labels[mask]).mean()
topic_conf[t_idx, si] = proba[mask].max(axis=1).mean()
print(f" step {t:>4}: acc={accuracy_per_snapshot[si]:.1%} conf={mean_conf_per_snapshot[si]:.3f}", end="\r")
print("\nDone.\n")
# ---------------------------------------------------------------------------
# Print results
# ---------------------------------------------------------------------------
print("="*72)
print("SECTION 1 — Overall accuracy and confidence per snapshot step")
print("="*72)
print(f" {'Step':>5} {'Accuracy':>9} {'Mean conf':>10} {'Top-3 variance features'}")
for si, t in enumerate(snapshot_steps):
top3_idx = np.argsort(variance_per_snapshot[si])[::-1][:3]
top3_str = " ".join(f"feat_{i}({variance_per_snapshot[si,i]:.4f})" for i in top3_idx)
print(f" {t:>5} {accuracy_per_snapshot[si]:>9.1%} {mean_conf_per_snapshot[si]:>10.3f} {top3_str}")
print()
print("="*72)
print("SECTION 2 — Per-topic accuracy across steps")
print("="*72)
header = f" {'Step':>5}"
for topic in topics:
header += f" {topic[:8]:>8}"
print(header)
for si, t in enumerate(snapshot_steps):
row = f" {t:>5}"
for t_idx in range(n_classes):
row += f" {topic_accuracy[t_idx, si]:>8.1%}"
print(row)
print()
print("="*72)
print("SECTION 3 — Per-topic mean confidence across steps")
print("="*72)
print(header)
for si, t in enumerate(snapshot_steps):
row = f" {t:>5}"
for t_idx in range(n_classes):
row += f" {topic_conf[t_idx, si]:>8.3f}"
print(row)
print()
print("="*72)
print("SECTION 4 — Per-topic: step at which confidence first crosses threshold")
print("="*72)
hdr = f" {'Topic':<30}"
for ct in CONF_TARGETS:
hdr += f" {int(ct*100)}%"
print(hdr)
for t_idx, topic in enumerate(topics):
row = f" {topic:<30}"
for ct in CONF_TARGETS:
cross = np.where(topic_conf[t_idx] >= ct)[0]
step = snapshot_steps[cross[0]] if len(cross) > 0 else MAX_STEPS
row += f" {step:>4}"
print(row)
print()
print("="*72)
print("SECTION 5 — Variance distribution at each snapshot (all 16384 features)")
print("="*72)
print(f" {'Step':>5} {'Mean var':>10} {'p50':>8} {'p90':>8} {'p99':>8} {'Max':>8} {'Top feat var':>12}")
for si, t in enumerate(snapshot_steps):
v = variance_per_snapshot[si]
print(f" {t:>5} {v.mean():>10.5f} {np.percentile(v,50):>8.5f} "
f"{np.percentile(v,90):>8.5f} {np.percentile(v,99):>8.5f} "
f"{v.max():>8.5f} {v[np.argsort(v)[::-1][0]]:>12.5f}")
print()
print("="*72)
print("SECTION 6 — Top-10 most variant features at each snapshot")
print("="*72)
for si, t in enumerate(snapshot_steps):
v = variance_per_snapshot[si]
top = np.argsort(v)[::-1][:10]
vals = " ".join(f"{i}({v[i]:.4f})" for i in top)
print(f" step {t:>4}: {vals}")
# ---------------------------------------------------------------------------
# Save results
# ---------------------------------------------------------------------------
results = {
"snapshot_steps": snapshot_steps,
"accuracy_per_snapshot": accuracy_per_snapshot.tolist(),
"mean_conf_per_snapshot": mean_conf_per_snapshot.tolist(),
"topic_accuracy": topic_accuracy.tolist(),
"topic_conf": topic_conf.tolist(),
"topics": topics,
"variance_per_snapshot_stats": [
{
"step": snapshot_steps[si],
"mean": float(variance_per_snapshot[si].mean()),
"p90": float(np.percentile(variance_per_snapshot[si], 90)),
"p99": float(np.percentile(variance_per_snapshot[si], 99)),
"max": float(variance_per_snapshot[si].max()),
"top10_feat_ids": np.argsort(variance_per_snapshot[si])[::-1][:10].tolist(),
"top10_variances": sorted(variance_per_snapshot[si].tolist(), reverse=True)[:10],
}
for si in range(n_snapshots)
],
}
out = f"{run_prefix}_temporal_dynamics.json"
with open(out, "w") as f:
json.dump(results, f, indent=2)
np.save(f"{run_prefix}_variance_per_snapshot.npy", variance_per_snapshot)
print(f"\nResults saved to {out}")
print(f"Variance array saved to {run_prefix}_variance_per_snapshot.npy")

Xet Storage Details

Size:
14.7 kB
·
Xet hash:
04ab7fc0bf4cf81d3148a370097413df1d2f146c3a4430a2e6a101cdb41cbdb1

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.