Jacobian lens walkthrough

O-lens readouts J-lens readouts notebookcharacter training .ipynbslice viewer

The official walkthrough from anthropics/jacobian-lens (Apache-2.0, license), run on Qwen3.5-4B with the neuronpedia n=1000 lens on one H200. Sections 5 (local file server) and 6 (fitting a new lens) were not run.

Jacobian lens — walkthrough

Load a model, load a pre-fitted Jacobian lens from the Hub, apply it to a prompt, and render the interactive slice visualisation.

[1]
import jlens

jlens.configure_logging()

MODEL_NAME = "Qwen/Qwen3.5-4B"
# MODEL_NAME = "Qwen/Qwen3.6-27B"

LENS_REPO = "neuronpedia/jacobian-lens"
LENS_REVISION = "qwen-n1000"
LENS_FILE = {
    "Qwen/Qwen3.5-4B": "qwen3.5-4b/jlens/Salesforce-wikitext/Qwen3.5-4B_jacobian_lens_n1000.pt",
    "Qwen/Qwen3.6-27B": "qwen3.6-27b/jlens/Salesforce-wikitext/Qwen3.6-27B_jacobian_lens_n1000.pt",
}[MODEL_NAME]

1. Load the model

jlens.from_hf wraps an already-loaded HuggingFace model into LensModel interface

[2]
import torch
import transformers

hf_model = transformers.AutoModelForCausalLM.from_pretrained(
    MODEL_NAME, dtype=torch.bfloat16
).cuda()
tokenizer = transformers.AutoTokenizer.from_pretrained(MODEL_NAME)
model = jlens.from_hf(hf_model, tokenizer)
model
[ERROR] `loss` is part of Qwen3_5CausalLMOutputWithPast.__init__'s signature, but not documented. Make sure to add it to the docstring of the function in /data/agastyas/Miniconda3/envs/ml/lib/python3.13/site-packages/transformers/models/qwen3_5/modeling_qwen3_5.py.
[ERROR] `logits` is part of Qwen3_5CausalLMOutputWithPast.__init__'s signature, but not documented. Make sure to add it to the docstring of the function in /data/agastyas/Miniconda3/envs/ml/lib/python3.13/site-packages/transformers/models/qwen3_5/modeling_qwen3_5.py.
HFLensModel(Qwen3_5ForCausalLM, n_layers=32, d_model=2560)

2. Load a pre-fitted lens

JacobianLens.from_pretrained pulls a .pt from the Hub (or a local path). The lens holds one [d_model, d_model] matrix per layer.

[3]
lens = jlens.JacobianLens.from_pretrained(
    LENS_REPO, filename=LENS_FILE, revision=LENS_REVISION
)
lens
JacobianLens(d_model=2560, n_prompts=1000, source_layers=[0..30] (31 layers))

3. Apply: J-lens vs logit lens

lens.apply(model, prompt, positions=...) runs one forward pass, transports each layer's residual into the final-layer basis with J_l, and decodes through the model's own unembedding. use_jacobian=False skips the transport — that's the vanilla logit lens.

Below: a two-hop factual question, read out at the boot token. The J-lens surfaces interpretable tokens at layers where the logit lens is still noise.

[4]
prompt = "Fact: The currency used in the country shaped like a boot is"
layers = [
    model.n_layers // 4,
    model.n_layers // 2,
    model.n_layers // 4 * 3,
    model.n_layers - 2,
]

jlens_logits, model_logits, _ = lens.apply(model, prompt, layers=layers, positions=[-2])
logit_lens, _, _ = lens.apply(
    model, prompt, layers=layers, positions=[-2], use_jacobian=False
)


def top5(logits):
    return [tokenizer.decode([t]) for t in logits.topk(5).indices]


for layer in layers:
    print(f"L{layer:>3} logit-lens: {top5(logit_lens[layer][0])}")
    print(f"L{layer:>3} J-lens:     {top5(jlens_logits[layer][0])}")
print(f"model:           {top5(model_logits[0])}")
L  8 logit-lens: ['oman', 'edom', 'ולי', 'GPC', ' Urlaubs']
L  8 J-lens:     [' `', ' boots', ' *', ' `\\', ' heel']
L 16 logit-lens: ['shaw', 'วย', 'amaz', 'REA', '举世']
L 16 J-lens:     ['?', '?', "'?", '____', ' Italy']
L 24 logit-lens: ['的形状', '形状的', 'shape', '形状', '-shaped']
L 24 J-lens:     ['-shaped', ' shape', ' shaped', 'shape', '形状']
L 30 logit-lens: [' is', ' shape', '-shaped', ' heel', ' shaped']
L 30 J-lens:     [' is', ' shape', ' shaped', '-shaped', ' heel']
model:           [' is', ' in', ' on', '.', ' with']

4. Render a slice page (inline)

compute_slice + build_page produce an interactive position × layer view of the lens's token ranks (the ? in the corner explains the controls). mode="embed" inlines everything so the page is self-contained.

[5]
import gzip
import json

from jlens.examples import EXAMPLES, resolve_prompt
from jlens.vis import build_page, compute_slice, notebook_iframe

# English gloss for Qwen's Chinese/Japanese/Korean vocab tokens (machine-
# generated, best-effort), shown next to
# the token in the page (alt_token=).
gloss = {
    int(k): v for k, v in json.load(gzip.open("assets/qwen_gloss.json.gz")).items()
}

example = next(e for e in EXAMPLES if e.slug == "multihop")
prompt = resolve_prompt(example, tokenizer)

slice_data = compute_slice(
    model,
    lens,
    prompt,
    layer_stride=2,
    # Empirically on Qwen, the interesting word tokens trail punctuation and
    # single-character tokens in the raw top-K; mask to word-like tokens only.
    mask_display=True,
)
page, _, _ = build_page(
    slice_data,
    prompt,
    title=example.section,
    description=example.description,
    alt_token=gloss,
)
notebook_iframe(page)
[interactive slice page: saved as slice.html]

Output: open the interactive slice page (position × layer view of the lens readouts).

5. Render a slice page (served)

For longer prompts prefer mode="fetch": build_page writes the data as sidecar files to out_dir and the page fetches rank files lazily on pin, so it stays small regardless of how many tokens are tracked.

[ ]
import os
import threading
from functools import partial
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path

example = next(e for e in EXAMPLES if e.slug == "ascii-face")
prompt = resolve_prompt(example, tokenizer)

slice_data = compute_slice(model, lens, prompt, mask_display=True)
out_dir = Path("slices") / example.slug
page, _, _ = build_page(
    slice_data,
    prompt,
    title=example.section,
    description=example.description,
    alt_token=gloss,
    mode="fetch",
    out_dir=out_dir,
)
(out_dir / "index.html").write_text(page)

if "_jlens_httpd" not in globals():
    _handler = partial(SimpleHTTPRequestHandler, directory=os.path.abspath("slices"))
    _jlens_httpd = HTTPServer(("127.0.0.1", 0), _handler)
    threading.Thread(target=_jlens_httpd.serve_forever, daemon=True).start()
print(f"-> http://localhost:{_jlens_httpd.server_address[1]}/{example.slug}/")

Not run here.

More to explore

A few more prompts are bundled in jlens.examples.EXAMPLES — change the slug above and see what surfaces, or try a prompt of your own.

[6]
for e in EXAMPLES:
    print(f"{e.slug:>24}  {e.section}")
                multihop  Multi-hop reasoning
        modulation-topic  Voluntary modulation: topic
   modulation-arithmetic  Voluntary modulation: arithmetic
              ascii-face  ASCII face
              off-by-one  Bug in code
           overdose-flag  Overdose flag
           greatest-fear  Greatest fear (don't say it)
               blackmail  Agentic Misalignment (blackmail honeypot)

6. Fitting

fit(model, prompts) computes J_l over the supplied prompts. 100 prompts is enough for a usable lens; the released lenses use 1000. dim_batch is the memory knob — each prompt does ceil(d_model / dim_batch) backward passes on a retained graph.

[ ]
from jlens.examples import load_wikitext_prompts

prompts = load_wikitext_prompts(n_prompts=100)
lens = jlens.fit(
    model, prompts, dim_batch=32, max_seq_len=128, checkpoint_path="ckpt.pt"
)
lens.save("jacobian_lens.pt")

Not run here.