FeatureLens / tests /test_live_runtime_helpers.py
ArchitSharma's picture
Release FeatureLens v0.13.0
0481a55
Raw
History Blame Contribute Delete
14.8 kB
from __future__ import annotations
import math
import sys
import types
from types import SimpleNamespace
import torch
try:
import transformers # noqa: F401
except ModuleNotFoundError:
stub = types.ModuleType('transformers')
stub.AutoModelForCausalLM = type('AutoModelForCausalLM', (), {})
stub.AutoTokenizer = type('AutoTokenizer', (), {})
sys.modules['transformers'] = stub
from featurelens.config import Settings
from featurelens.runtime import FeatureLensRuntime
from featurelens.sae import SAEWeights
class FakeTokenizer:
pad_token_id = 0
eos_token_id = 0
eos_token = '<pad>'
pad_token = '<pad>'
padding_side = 'left'
@staticmethod
def _ids(text: str) -> list[int]:
# Keep 0 reserved for padding; make deterministic small-vocabulary ids.
return [1 + (ord(char) % 7) for char in text] or [1]
def __call__(
self,
text,
*,
return_tensors=None,
padding=False,
truncation=False,
max_length=None,
add_special_tokens=True,
):
if isinstance(text, str):
ids = self._ids(text)
if max_length is not None:
ids = ids[-int(max_length) :]
if return_tensors == 'pt':
tensor = torch.tensor([ids], dtype=torch.long)
return {'input_ids': tensor, 'attention_mask': torch.ones_like(tensor)}
return {'input_ids': ids}
sequences = [self._ids(item) for item in text]
if max_length is not None:
sequences = [ids[-int(max_length) :] for ids in sequences]
width = max(len(ids) for ids in sequences)
padded = []
masks = []
for ids in sequences:
pad = width - len(ids)
padded.append([0] * pad + ids)
masks.append([0] * pad + [1] * len(ids))
return {
'input_ids': torch.tensor(padded, dtype=torch.long),
'attention_mask': torch.tensor(masks, dtype=torch.long),
}
def decode(self, ids) -> str:
token_id = int(ids[0])
return '<pad>' if token_id == 0 else f't{token_id}'
class FakeBackbone(torch.nn.Module):
def __init__(self, d_model: int) -> None:
super().__init__()
self.layers = torch.nn.ModuleList([torch.nn.Identity()])
self.embedding = torch.nn.Embedding(8, d_model)
with torch.no_grad():
self.embedding.weight.copy_(
torch.tensor(
[
[0.0, 0.0, 0.0],
[1.0, 0.2, 0.1],
[0.8, 0.3, 0.2],
[0.6, 0.4, 0.3],
[0.4, 0.5, 0.4],
[0.3, 0.6, 0.5],
[0.2, 0.7, 0.6],
[0.1, 0.8, 0.7],
]
)
)
class FakeLM(torch.nn.Module):
def __init__(self) -> None:
super().__init__()
self.model = FakeBackbone(d_model=3)
self.proj = torch.nn.Linear(3, 8, bias=False)
with torch.no_grad():
self.proj.weight.copy_(
torch.tensor(
[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[0.5, 0.5, 0.0],
[0.5, 0.0, 0.5],
[0.0, 0.5, 0.5],
[-0.4, 0.3, 0.2],
]
)
)
def forward(self, input_ids, attention_mask=None, use_cache=False):
hidden = self.model.embedding(input_ids)
for layer in self.model.layers:
hidden = layer(hidden)
return SimpleNamespace(logits=self.proj(hidden))
class FakeSAEStore:
def __init__(self) -> None:
self.sae = SAEWeights(
layer=0,
w_enc_t=torch.tensor(
[
[1.0, 0.0, 0.3, -0.2],
[0.0, 1.0, 0.2, 0.1],
[0.0, 0.0, 1.0, 0.4],
]
),
w_dec=torch.tensor(
[
[1.0, 0.0, 0.5, -0.2],
[0.0, 1.0, 0.2, 0.3],
[0.0, 0.0, 1.0, 0.4],
]
),
b_enc=torch.zeros(4),
b_dec=torch.zeros(3),
top_k=2,
)
def get(self, layer: int):
assert layer == 0
return self.sae
def preload(self) -> None:
return None
def make_runtime() -> FeatureLensRuntime:
settings = Settings(
layers=(0,),
sae_top_k=2,
sae_width=4,
d_model=3,
max_prompt_tokens=32,
live_random_controls=3,
contrast_prompts_per_concept=2,
eager_load=False,
sae_dtype='float32',
)
runtime = FeatureLensRuntime(settings)
runtime.device = torch.device('cpu')
runtime.model = FakeLM().eval()
runtime.tokenizer = FakeTokenizer()
runtime.sae_store = FakeSAEStore()
return runtime
def test_feature_token_trace_runs_end_to_end_on_toy_runtime() -> None:
runtime = make_runtime()
result = runtime.feature_token_trace('abc', layer=0, feature_id=0)
assert result.token_count == 3
assert len(result.rows) == 3
assert result.active_token_count >= 1
assert result.max_activation > 0
def test_feature_geometry_runs_end_to_end_on_toy_runtime() -> None:
runtime = make_runtime()
result = runtime.feature_geometry('abc', layer=0, token_index=-1, feature_ids=[0, 1, 2])
assert len(result.rows) == 3 # 3 choose 2
assert math.isfinite(result.alignment_ratio)
assert result.independent_norm >= 0
def test_contrastive_intervention_runs_end_to_end_on_toy_runtime() -> None:
runtime = make_runtime()
result = runtime.contrastive_intervention(
text='abc',
layer=0,
token_index=-1,
feature_id=0,
mode='ablate',
coefficient=0.0,
target_a='a',
target_b='b',
)
assert len(result.rows) == 2
assert result.random_control_count == 3
assert math.isfinite(result.delta_log_odds)
assert math.isfinite(result.specificity_ratio)
def test_concept_contrast_promptwide_scan_runs_on_toy_runtime() -> None:
runtime = make_runtime()
result = runtime.concept_contrast_scan(feature_id=0, layer=0, prompts_per_concept=1)
assert result.total_prompt_count == 7
assert len(result.rows) == 7
assert all(len(row) == 7 for row in result.rows)
assert 0 <= result.active_prompt_count <= result.total_prompt_count
def test_concept_feature_discovery_runs_on_toy_runtime() -> None:
runtime = make_runtime()
result = runtime.concept_feature_discovery(
concept='mathematics',
layer=0,
prompts_per_concept=1,
top_n=3,
ranking_mode='balanced_selectivity',
current_text='abc',
current_token_index=-1,
)
assert result.concept == 'mathematics'
assert result.ranking_mode == 'balanced_selectivity'
assert result.current_context_available is True
assert result.current_token_index == 2
assert len(result.rows) <= 3
assert result.candidate_ids == [int(row[1]) for row in result.rows]
assert all(len(row) == 14 for row in result.rows)
if result.rows:
assert result.default_candidate_id in result.candidate_ids
assert all(math.isfinite(float(row[2])) for row in result.rows)
assert all(float(row[9]) >= 0 for row in result.rows) # current prompt max
assert all(float(row[10]) >= 0 for row in result.rows) # current token activation
def test_concept_feature_discovery_supports_raw_mean_difference() -> None:
runtime = make_runtime()
result = runtime.concept_feature_discovery(
concept='mathematics',
layer=0,
prompts_per_concept=1,
top_n=3,
ranking_mode='raw_mean_difference',
)
assert result.ranking_mode == 'raw_mean_difference'
assert result.current_context_available is False
assert result.current_token_index is None
assert all(len(row) == 14 for row in result.rows)
def test_concept_feature_discovery_supports_causal_ready_mode() -> None:
runtime = make_runtime()
result = runtime.concept_feature_discovery(
concept='mathematics',
layer=0,
prompts_per_concept=1,
top_n=5,
ranking_mode='causal_ready',
current_text='abc',
current_token_index=-1,
)
assert result.ranking_mode == 'causal_ready'
assert result.current_context_available is True
assert result.displayed_current_active_count == len(result.rows)
assert all(bool(row[11]) for row in result.rows)
def test_concept_feature_discovery_causal_ready_requires_workbench_context() -> None:
runtime = make_runtime()
try:
runtime.concept_feature_discovery(
concept='mathematics',
layer=0,
prompts_per_concept=1,
top_n=3,
ranking_mode='causal_ready',
)
except ValueError as exc:
assert 'Workbench' in str(exc)
else:
raise AssertionError('causal_ready should require Workbench context')
def test_feature_cue_scan_runs_on_toy_runtime() -> None:
runtime = make_runtime()
result = runtime.feature_cue_scan(
feature_id=0,
layer=0,
prompt_stem='abc',
cues=['is', '=', ':'],
)
assert result.cue_count == 3
assert len(result.rows) == 3
assert all(len(row) == 5 for row in result.rows)
assert 0 <= result.active_cue_count <= result.cue_count
def test_feature_cue_context_scan_runs_on_toy_runtime() -> None:
runtime = make_runtime()
result = runtime.feature_cue_context_scan(
feature_id=0,
layer=0,
stems=['abc', 'xyz'],
cues=['is', '=', ':'],
)
assert result.condition_count == 6
assert len(result.rows) == 6
assert all(len(row) == 6 for row in result.rows)
assert 0 <= result.active_condition_count <= result.condition_count
assert set(result.cue_active_context_counts) == {'is', '=', ':'}
assert set(result.cue_mean_activations) == {'is', '=', ':'}
assert result.dominant_cue in {'is', '=', ':'}
assert 0 <= result.dominant_cue_context_count <= 2
assert 0 <= result.off_dominant_active_count <= result.active_condition_count
assert len(result.chart_rows) == 6
def test_candidate_causal_screen_batches_multiple_ablation_candidates() -> None:
runtime = make_runtime()
result = runtime.candidate_causal_screen(
text='abc',
layer=0,
token_index=-1,
feature_ids=[0, 1, 2],
target_text='d',
)
assert result.candidate_count == 3
assert len(result.rows) == 3
assert len(result.chart_rows) == 3
assert all(len(row) == 8 for row in result.rows)
assert all(row[0] == rank for rank, row in enumerate(result.rows, start=1))
assert 0 <= result.active_feature_count <= result.candidate_count
assert all(math.isfinite(float(row[5])) for row in result.rows)
assert all(float(row[7]) >= 0 for row in result.rows)
def test_candidate_specificity_screen_batches_random_controlled_candidates() -> None:
runtime = make_runtime()
result = runtime.candidate_specificity_screen(
text='abc',
layer=0,
token_index=-1,
feature_ids=[0, 1],
target_text='d',
)
assert result.candidate_count == 2
assert result.random_control_count == 3
assert len(result.rows) == 2
assert len(result.chart_rows) == 4
assert all(len(row) == 17 for row in result.rows)
assert all(row[0] == rank for rank, row in enumerate(result.rows, start=1))
assert all(float(row[9]) >= 0 for row in result.rows) # target specificity
assert all(0 < float(row[10]) <= 1 for row in result.rows) # empirical tail
assert all(float(row[15]) >= 0 for row in result.rows) # JS specificity
assert all(0 < float(row[16]) <= 1 for row in result.rows)
def test_concept_feature_discovery_reports_split_half_stability_without_extra_forward() -> None:
runtime = make_runtime()
result = runtime.concept_feature_discovery(
concept='mathematics',
layer=0,
prompts_per_concept=2,
top_n=3,
ranking_mode='balanced_selectivity',
current_text='abc',
current_token_index=-1,
)
if result.split_half_jaccard is not None:
assert 0.0 <= result.split_half_jaccard <= 1.0
assert result.split_half_k is not None
assert 0 <= result.split_half_shared_count <= max(len(result.candidate_ids), result.split_half_k)
def test_concept_feature_discovery_reports_resample_support_from_same_batch() -> None:
runtime = make_runtime()
result = runtime.concept_feature_discovery(
concept='mathematics',
layer=0,
prompts_per_concept=2,
top_n=3,
ranking_mode='balanced_selectivity',
current_text='abc',
current_token_index=-1,
)
assert 0 <= result.resample_replicates <= 32
if result.resample_replicates and result.rows:
assert result.resample_mean_support is not None
assert 0.0 <= result.resample_mean_support <= 1.0
assert 0 <= result.resample_high_support_count <= len(result.rows)
for row in result.rows:
assert row[12] is not None
assert 0.0 <= float(row[12]) <= 1.0
if row[13] is not None:
assert 1.0 <= float(row[13]) <= result.top_n
def test_candidate_cross_target_profile_runs_multiple_features_and_targets() -> None:
runtime = make_runtime()
result = runtime.candidate_cross_target_profile(
text='abc',
layer=0,
token_index=-1,
feature_ids=[0, 1],
targets=['d', 'e', 'f'],
)
assert result.feature_ids == [0, 1]
assert result.targets == ['d', 'e', 'f']
assert len(result.rows) == 6
assert len(result.chart_rows) == 6
assert len(result.summary_rows) == 2
assert all(len(row) == 8 for row in result.rows)
assert all(len(row) == 12 for row in result.summary_rows)
assert len(result.pairwise_rows) == 2 * 3 # 2 features × C(3 targets, 2)
assert all(len(row) == 6 for row in result.pairwise_rows)
assert all(0.0 <= float(row[7]) <= 1.0 for row in result.summary_rows) # normalized entropy
assert all(0.0 <= float(row[8]) <= 1.0 for row in result.summary_rows) # concentration
assert all(-1.0 <= float(row[9]) <= 1.0 for row in result.summary_rows) # signed bias
assert all(isinstance(row[10], str) and row[10] for row in result.summary_rows)
assert all(math.isfinite(float(row[5])) for row in result.rows)