u-space / tests /test_decode_real.py
loose-bits's picture
Deploy 7123e8e
d58cfe6 verified
Raw History Blame Contribute Delete
3.54 kB
"""Decode a real model's axes and check they read as their own concepts.
Needs the Qwen3.5-4B weights and its published lens, so it is skipped unless
`ULENS_TEST_NETWORK=1`:
ULENS_TEST_NETWORK=1 pytest tests/test_decode_real.py
The assertions are deliberately about *shape of outcome*, not exact word lists: a decode is
a ranking over a quarter of a million tokens and a tokenizer revision can reorder its tail.
What must hold is that each axis returns its own concept far more than a random direction
does, and that the random direction returns nothing.
"""
import os
import numpy as np
import pytest
import ulens
pytestmark = pytest.mark.skipif(os.environ.get("ULENS_TEST_NETWORK") != "1",
reason="set ULENS_TEST_NETWORK=1 to decode a real model")
MODEL = "qwen3.5-4b"
K = 10
@pytest.fixture(scope="module")
def decoded():
block = ulens.models.decode_block(MODEL)
cone = ulens.basis.get(MODEL, block=block)
voc = ulens.decode.Vocabulary(MODEL, block)
return cone, voc, voc.decode(cone, k=K)
def stem_hits(words, anchors, n=5) -> int:
"""How many decoded words share a 5-character stem with any anchor of their own axis."""
low = {a.lower() for a in anchors}
return sum(any(w[:n] == a[:n] or a.startswith(w) or w.startswith(a) for a in low)
for w in (x.lower() for x in words))
def test_decode_block_is_not_the_readout_block(decoded):
cone, voc, _ = decoded
assert cone.block == voc.block == ulens.models.decode_block(MODEL) == 23
assert ulens.models.get(MODEL).readout_block == 20
def test_every_axis_reads_as_its_own_concept(decoded):
cone, _, dec = decoded
for d in dec:
n = stem_hits(d.words("+"), cone.axis(d.name).anchors)
assert n >= 3, f"{d.name} decoded to {d.words('+')}"
def test_a_random_direction_decodes_to_nothing(decoded):
cone, voc, _dec = decoded
rng = np.random.default_rng(0)
anchors = [w for a in cone.axes for w in a.anchors]
for _ in range(3):
v = rng.normal(size=cone.hidden_size).astype(np.float32)
assert stem_hits([h.word for h in voc.top(v, k=K)], anchors) <= 1
def test_each_axis_beats_the_other_axes_anchors(decoded):
"""An axis must match its own anchors more than another axis's."""
cone, _, dec = decoded
for d in dec:
own = stem_hits(d.words("+"), cone.axis(d.name).anchors)
for other in cone.names:
if other == d.name:
continue
assert own >= stem_hits(d.words("+"), cone.axis(other).anchors), \
f"{d.name} matches {other}'s anchors at least as well"
def test_the_negative_end_is_not_the_positive_end(decoded):
_, _, dec = decoded
for d in dec:
assert not set(d.words("+")) & set(d.words("-"))
assert all(h.score > 0 for h in d.positive)
assert all(h.score < 0 for h in d.negative)
def test_norms_cache_is_tiny_next_to_the_matrix_it_replaces(decoded):
_, voc, _ = decoded
assert voc.cache_path.exists()
on_disk = voc.cache_path.stat().st_size
materialised = voc.n_tokens * voc.spec.hidden_size * 2 # fp16
assert on_disk * 100 < materialised
assert len(voc.norms) == voc.n_tokens
assert np.all(voc.norms > 0)
def test_scores_are_bounded_like_cosines(decoded):
_, voc, _ = decoded
rng = np.random.default_rng(1)
s = voc.scores(rng.normal(size=voc.spec.hidden_size).astype(np.float32))
assert s.shape == (voc.n_tokens,)
assert s.min() >= -1.001 and s.max() <= 1.001