interp-engine 1.11.0__tar.gz → 1.11.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {interp_engine-1.11.0 → interp_engine-1.11.2}/PKG-INFO +2 -2
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/ENGINE_HOOK_MAPPINGS.md +31 -4
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/USAGE.md +38 -1
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/__init__.py +19 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/mappers.py +94 -10
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/model.py +61 -4
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/protocol.py +42 -3
- interp_engine-1.11.2/interp_engine/sampling.py +264 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/steer.py +30 -5
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/sync.py +44 -4
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_backend.py +64 -12
- {interp_engine-1.11.0 → interp_engine-1.11.2}/pyproject.toml +4 -3
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/conftest.py +15 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_mappers.py +104 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_parity_gpt2.py +120 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_points_registry.py +1 -1
- interp_engine-1.11.2/tests/test_sampling.py +226 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_kv_isolation.py +4 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/.gitignore +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/LICENSE +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/README.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/README.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/__init__.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/bench_spec.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/cells.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/probe.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/probe_lens_stream.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/publish.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/report_bench.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/gemma-2-2b__eager.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/qwen3-4b__eager.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/qwen3-4b__vllm.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/results-latest.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/run_all.sh +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/run_bench.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/benchmarks/workloads.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/AGENT_INTEGRATION.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/ARCHITECTURE_QUIRKS.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/COMPATIBILITY.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/GRADIENTS.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/INTERNALS.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/PERFORMANCE.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/PORTING.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/README.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/SUPPORTED_POINTS.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/docs/VALIDATING_A_NEW_MODEL.md +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/_loop.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/address.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/arch.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/attn_config.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/attn_scores.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/autograd_support.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/capture.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/chat_compose.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/chat_conventions.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/chat_formatters.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/cuda_preflight.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/dispatch.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/facts.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/hooks.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/lens.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/load.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/memory.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/moe_routing.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/notebook_stdout.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/points.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/residual_basis.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/select.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/steer_specs.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/tokenize.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/__init__.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/_demux.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/_hooks.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/_payload.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/_tp.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/_tree.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/attn.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/capture.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/graphs.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/lens/__init__.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/lens/intervene.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/lens/readout.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/lens/unembed.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/mhc.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/native.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/requests.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/static.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_capture/steering.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/interp_engine/vllm_plugin.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/harness.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/model_expectations.yaml +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/synthetic_families.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_address.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_attn_config_tripwire.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_attn_probs_indexing.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_attn_scores.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_attn_z_gqa.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_autograd_support.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_bench_workloads.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_capability_refusals.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_capture_addressing.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_chat_compose.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_chat_formatters.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_chat_templates.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_core.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_cuda_preflight.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_doc_code_fences.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_eager_autograd.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_facts.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_family_points.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_gated_attn_out.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_gpu_sizer.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_head_contributions.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_hook_call_conventions.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_injected_system_spans.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_layer_kinds.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_load.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_logit_transform.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_memory.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_mlp_internals.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_model_expectations.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_moe.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_multigpu.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_multimodal_arch.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_new_models_gpu.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_no_chat_template.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_normalized_hook.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_notebook_stdout.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_packaging.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_per_layer_attn_dims.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_protocol.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_published_benchmarks.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_qk_norm.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_qkv_layout.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_reasoning_spans.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_release.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_resid_mid.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_residual_basis.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_sandwich_norms.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_select.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_sliding_window_attn.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_small_models_gpu.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_static_dsv4_gpu.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_static_parity_gpu.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_static_set.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_static_warmup.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_steer_context.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_steer_math_parity.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_sync_loop.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_sync_parity.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_unified_free_functions.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_unresolved_families.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_capture_gpu.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_capture_scales.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_engine_loop.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_graph_path.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_graphs_on_gpu.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_hook_availability.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_hyper_connections.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_new_points.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_only_families.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_plugin.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vllm_wire_grammar.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_vocabulary_boundary.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_worker_lens_capture_readout.py +0 -0
- {interp_engine-1.11.0 → interp_engine-1.11.2}/tests/test_worker_lens_readout.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: interp-engine
|
|
3
|
-
Version: 1.11.
|
|
3
|
+
Version: 1.11.2
|
|
4
4
|
Summary: A fast, standardized, and easy to use interpretability engine.
|
|
5
5
|
Project-URL: Homepage, https://github.com/decoderesearch/interp-engine
|
|
6
6
|
Project-URL: Repository, https://github.com/decoderesearch/interp-engine
|
|
@@ -21,7 +21,7 @@ Requires-Dist: pytest<9,>=8.3.1; extra == 'dev'
|
|
|
21
21
|
Requires-Dist: pyyaml>=6; extra == 'dev'
|
|
22
22
|
Requires-Dist: ruff<0.17,>=0.16.2; extra == 'dev'
|
|
23
23
|
Provides-Extra: parity
|
|
24
|
-
Requires-Dist: transformer-lens
|
|
24
|
+
Requires-Dist: transformer-lens<4,>=3.0; extra == 'parity'
|
|
25
25
|
Provides-Extra: quant
|
|
26
26
|
Requires-Dist: accelerate>=1.0; extra == 'quant'
|
|
27
27
|
Requires-Dist: kernels<0.17.0,>=0.15.2; extra == 'quant'
|
|
@@ -24,7 +24,7 @@ that capture is conditional. Each links to the note under the table.
|
|
|
24
24
|
| -------------------------------------------- | --------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
25
25
|
| `embeddings` | `cache["embeddings"]` | `hook_embed` | `token_embeddings` | [three tensors, three conventions](#embeddings-three-different-tensors) |
|
|
26
26
|
| `resid_pre` | `cache["resid_pre", 5]` | `blocks.5.hook_resid_pre` | `layers_input[5]` | |
|
|
27
|
-
| `attn_in` | `cache["attn_in", 5]` | `blocks.5.
|
|
27
|
+
| `attn_in` | `cache["attn_in", 5]` | `blocks.5.attn.hook_in` (**not** `hook_attn_in`) | `attentions_input[5]` | [the norm's output; the block-level hook is its input](#the-block-level-hook_attn_in-and-hook_mlp_in-are-the-norms-input) |
|
|
28
28
|
| `q_norm_in` / `k_norm_in` (QK-norm input) | `cache["q_norm_in", 5]` | `blocks.5.attn.q_norm.hook_in` (bridge only) | — | [shape is family-dependent](#qk-norm-shape-is-family-dependent) |
|
|
29
29
|
| `q_norm_out` / `k_norm_out` (QK-norm output) | `cache["q_norm_out", 5]` | `blocks.5.attn.q_norm.hook_out` (bridge only) | — | [shape is family-dependent](#qk-norm-shape-is-family-dependent) |
|
|
30
30
|
| `value` | `cache["value", 5]` | `blocks.5.attn.hook_v` | — | [KV heads, and flattened](#addressing-and-shape-which-do-not-translate-for-free) |
|
|
@@ -35,7 +35,7 @@ that capture is conditional. Each links to the note under the table.
|
|
|
35
35
|
| `attn_out` (raw module output) | `cache["attn_out", 5]` | `blocks.5.attn.hook_out` | `attentions_output[5]` | [raw output, not the contribution](#raw-output-versus-residual-contribution) |
|
|
36
36
|
| `attn_out_post` (residual contribution) | `cache["attn_out_post", 5]` | `blocks.5.hook_attn_out` | — | [same TL name, different tensor](#raw-output-versus-residual-contribution) |
|
|
37
37
|
| `resid_mid` | `cache["resid_mid", 5]` (writes refused under vLLM) | `blocks.5.hook_resid_mid` | _no accessor_: the pre-MLP norm's `.input`, under its HF name | [captured, not reconstructed](#resid_mid-is-captured-not-reconstructed) |
|
|
38
|
-
| `mlp_in` | `cache["mlp_in", 5]` | `blocks.5.mlp.hook_in` (
|
|
38
|
+
| `mlp_in` | `cache["mlp_in", 5]` | `blocks.5.mlp.hook_in` (**not** `hook_mlp_in`) | `mlps_input[5]` | [not `hook_mlp_in`, which is pre-norm](#the-block-level-hook_attn_in-and-hook_mlp_in-are-the-norms-input) |
|
|
39
39
|
| `mlp_pre` (pre-activation) | `cache["mlp_pre", 5]` `*` | `blocks.5.mlp.hook_pre` | — | [the neuron basis, and `up_proj` is ambiguous](#the-mlp-internal-points-are-the-neuron-basis) |
|
|
40
40
|
| `mlp_pre_linear` (gated MLPs only) | `cache["mlp_pre_linear", 5]` `*` | `blocks.5.mlp.hook_pre_linear` | — | [refuses on a plain MLP](#the-mlp-internal-points-are-the-neuron-basis) |
|
|
41
41
|
| `mlp_act` (post-activation neurons) | `cache["mlp_act", 5]` | `blocks.5.mlp.hook_post` | — | [`d_mlp` wide; refuses on sparse layers](#the-mlp-internal-points-are-the-neuron-basis) |
|
|
@@ -45,7 +45,7 @@ that capture is conditional. Each links to the note under the table.
|
|
|
45
45
|
| `mlp_out` (raw module output) | `cache["mlp_out", 5]` | `blocks.5.mlp.hook_out` | `mlps_output[5]` | [raw output, not the contribution](#raw-output-versus-residual-contribution) |
|
|
46
46
|
| `mlp_out_post` (residual contribution) | `cache["mlp_out_post", 5]` | `blocks.5.hook_mlp_out` | — | [same TL name, different tensor](#raw-output-versus-residual-contribution) |
|
|
47
47
|
| `resid_post` | `cache["resid_post", 5]` | `blocks.5.hook_resid_post` | `layers_output[5]` | |
|
|
48
|
-
| `final_norm` | `cache["final_norm"]` | `
|
|
48
|
+
| `final_norm` | `cache["final_norm"]` | `unembed.hook_in` (TransformerLens 3) | `ln_final.output` | |
|
|
49
49
|
| `lm_head` | `cache["lm_head"]` `*` | _(logits are returned, not hooked)_ | `logits` | [a bare unembed, which vLLM does not offer](#the-unembed-and-what-vllm-returns) |
|
|
50
50
|
|
|
51
51
|
A hyper-connection trunk adds seven more points, which are conditional on the family and so are
|
|
@@ -147,6 +147,33 @@ Scope's transcoders declare it as their SAELens `hook_name`, circuit-tracer's as
|
|
|
147
147
|
Substituting `mlp_in` reads them off the wrong activations. Same reasoning one level down for
|
|
148
148
|
`q_norm.hook_normalized`, in the table at the end.
|
|
149
149
|
|
|
150
|
+
### The block-level `hook_attn_in` and `hook_mlp_in` are the norm's input
|
|
151
|
+
|
|
152
|
+
TransformerLens has two names each for the sublayer inputs, and they are a whole normalization apart.
|
|
153
|
+
`blocks.5.mlp.hook_in` is the MLP module's argument: `ln2`'s output, gain included, which is our
|
|
154
|
+
`mlp_in`. `blocks.5.hook_mlp_in` is the block-level hook, and `TransformerBlock.forward` fires it on
|
|
155
|
+
`resid_mid` and passes the result _into_ `ln2` — so it is the norm's **input**, `resid_mid` under
|
|
156
|
+
another name. `hook_attn_in` is the same story on `resid_pre`, broadcast per head to
|
|
157
|
+
`[batch, pos, n_heads, d_model]`. Measured on the HF modules with neither library in the loop: on
|
|
158
|
+
gemma-3-1b-it the norm's input and output share cosine 0.175 at layer 5 and differ in norm by 231x,
|
|
159
|
+
past 3000x deeper in the stack; on gpt2 the cosine is 0.75 and the norms differ by 27–86x.
|
|
160
|
+
|
|
161
|
+
`tlens_hook_to_point` therefore **refuses** `hook_attn_in` / `hook_mlp_in` rather than mapping them to
|
|
162
|
+
either side. Mapping them to `attn_in` / `mlp_in` was the bug (issue #24): a caller got a plausible
|
|
163
|
+
tensor of the right shape, and an SAE or steering vector trained on TransformerLens' tensor was read
|
|
164
|
+
off the wrong one. Mapping them to `resid_pre` / `resid_mid` would be faithful to what the hook carries
|
|
165
|
+
and still wrong to do silently, because the caller porting `use_hook_mlp_in=True` code may have meant
|
|
166
|
+
either side of the norm. The refusal names both: `mlp.hook_in` for the sublayer's argument,
|
|
167
|
+
`hook_resid_mid` for the pre-norm residual. `point_to_tlens_hook("attn_in")` emits `attn.hook_in`, the
|
|
168
|
+
name that denotes the tensor.
|
|
169
|
+
|
|
170
|
+
Two more names outside this table map (issue #25). `blocks.5.hook_in`, the block's forward pre-hook,
|
|
171
|
+
is `resid_pre` on a single-stream trunk — and is refused on a hyper-connection one, where it is the
|
|
172
|
+
_previous_ block's `resid_streams`, for the reason `attn_hc.hook_in` is. `unembed.hook_in` is the only
|
|
173
|
+
mapped name outside `blocks.{i}.`: the unembedding's input is `final_norm`, the final norm's output.
|
|
174
|
+
`ln_final.hook_normalized` is not that tensor — it fires pre-gain, and a `fold_ln` `HookedTransformer`
|
|
175
|
+
has folded that gain into `W_U` — so it is refused with the reason.
|
|
176
|
+
|
|
150
177
|
### `attn_in` is the attention module's input, not a norm's output
|
|
151
178
|
|
|
152
179
|
Which matters on a post-norm family. On a Llama-shaped block the two coincide — the attention reads
|
|
@@ -553,7 +580,7 @@ path captures that module's **output** (`cache["model.layers.5.self_attn.q_proj"
|
|
|
553
580
|
| `blocks.5.attn.hook_rot_q`, `.hook_rot_k` | q/k after RoPE | No such tensor crosses a module boundary: `transformers` applies RoPE inside the attention forward |
|
|
554
581
|
| `blocks.5.attn.hook_result` | per-head output _after_ `W_O` | It is `n_heads` times the size of `z`, which is why TL gates it behind a flag too. Computed on demand instead: `capture.head_contributions(model, cache, 5)` applies the split `W_O` to a captured `z` |
|
|
555
582
|
| `blocks.5.hook_q_input`, `.hook_k_input`, `.hook_v_input` | per-head writable inputs (`use_split_qkv_input`) | A TransformerLens parameterization rather than a place in the HF forward — the tensor exists only because TL splits the input per head to make it writable |
|
|
556
|
-
| `blocks.5.hook_attn_in`, `blocks.5.hook_mlp_in` |
|
|
583
|
+
| `blocks.5.hook_attn_in`, `blocks.5.hook_mlp_in` | the block-level sublayer inputs (`use_attn_in`, `use_hook_mlp_in`) | **Not** `attn_in` / `mlp_in`, despite the names: TransformerLens fires them on `resid_pre` / `resid_mid` _before_ the block norm, so they are the norm's input where our points are its output. Refused with both readings named; ask for `attn.hook_in` / `mlp.hook_in` (the sublayer's argument) or `hook_resid_pre` / `hook_resid_mid` (the tensor these hooks carry). [Below](#the-block-level-hook_attn_in-and-hook_mlp_in-are-the-norms-input) |
|
|
557
584
|
| `blocks.5.mlp.hook_mid` | post-activation, pre-LayerNorm, on a SoLU model | No HF checkpoint in scope has an activation-internal norm, so the point would never resolve |
|
|
558
585
|
| `blocks.5.mlp.hook_gate` | one expert's SwiGLU gate, at `blocks.5.mlp.experts.3.hook_gate` | **Not the router**, despite the name, so mapping it to anything routing-shaped would be wrong. Our `mlp_pre` is the same tensor on a dense MLP; per expert it is not a module output at all on the families that fuse their expert banks |
|
|
559
586
|
| `blocks.5.mlp.hook_expert_weights` | the softmax over _all_ experts, pre-top-k | It is a different tensor from our `expert_weights` (post-top-k, `k` wide), and mapping two different tensors to one name is the failure this table exists to prevent. Write it as `softmax(cache["router_logits", 5])`, exact for the softmax-routed families |
|
|
@@ -204,7 +204,44 @@ async def generate():
|
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
The stream yields decoded text deltas that concatenate to exactly what `generate_text` would have
|
|
207
|
-
returned.
|
|
207
|
+
returned.
|
|
208
|
+
|
|
209
|
+
### Sampling knobs
|
|
210
|
+
|
|
211
|
+
Every generation method takes `temperature`, `top_k`, `top_p` and `presence_penalty`, and decides
|
|
212
|
+
each one the same way on every backend: a knob you pass is used as passed; a knob you leave `None`
|
|
213
|
+
takes the checkpoint's own `generation_config.json`; a knob neither states is neutral (temperature
|
|
214
|
+
1, no filtering, no penalty). This is what `transformers.generate` and `vllm serve` do, so a
|
|
215
|
+
checkpoint samples here the way its authors tuned it to, and `temperature=0.0` is greedy
|
|
216
|
+
everywhere. To turn a recommended filter off, say so in `transformers`' terms: `top_k=0`,
|
|
217
|
+
`top_p=1.0`.
|
|
218
|
+
|
|
219
|
+
`model.sampling_settings(...)` answers what a call with those arguments runs with, and
|
|
220
|
+
`model.recommended_sampling` is the file as read (`None` where it says nothing; Qwen3.5 ships no
|
|
221
|
+
file at all, so everything is neutral there unless you pass it):
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from interp_engine import load_model
|
|
225
|
+
|
|
226
|
+
model = load_model("google/gemma-3-270m-it")
|
|
227
|
+
model.recommended_sampling # temperature 1.0, top_k 64, top_p 0.95
|
|
228
|
+
model.sampling_settings(temperature=0.3) # SamplingSettings(temperature=0.3, top_k=64, top_p=0.95, presence_penalty=0.0)
|
|
229
|
+
text = await model.generate_text(token_ids, max_tokens=64, temperature=0.3, presence_penalty=1.5)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The presence penalty is the one repetition control: a flat subtraction from the logit of every
|
|
233
|
+
token the generation has produced so far, applied before the temperature and before a greedy
|
|
234
|
+
argmax, as vLLM orders it. It breaks a loop without scaling with the loop's length, which is what a
|
|
235
|
+
frequency penalty does and why that one punishes function words in a long reply. The file cannot
|
|
236
|
+
state a presence penalty (it is not a `transformers` field), so unset means 0; a card that
|
|
237
|
+
recommends one (Qwen3.5: 1.5) is for the caller to read. `transformers`' multiplicative
|
|
238
|
+
`repetition_penalty` is read but never applied.
|
|
239
|
+
|
|
240
|
+
Code that generates through `transformers.generate` directly gets the same decision with
|
|
241
|
+
`hf_generate_kwargs(model.sampling_settings(...), prompt_len=n)`: every knob stated, the file's
|
|
242
|
+
own `repetition_penalty` off, and the presence penalty as a logits processor.
|
|
243
|
+
|
|
244
|
+
For a chat model, build the prompt with the tokenizer's own template rather than by hand.
|
|
208
245
|
Both backends carry a `Tokenize` helper on `.tok` for this:
|
|
209
246
|
|
|
210
247
|
```python
|
|
@@ -96,6 +96,16 @@ from interp_engine.residual_basis import (
|
|
|
96
96
|
reduce_streams,
|
|
97
97
|
vllm_residual_basis,
|
|
98
98
|
)
|
|
99
|
+
from interp_engine.sampling import (
|
|
100
|
+
PresencePenaltyProcessor,
|
|
101
|
+
RecommendedSampling,
|
|
102
|
+
SamplingSettings,
|
|
103
|
+
VLLMSamplingKwargs,
|
|
104
|
+
apply_presence_penalty,
|
|
105
|
+
hf_generate_kwargs,
|
|
106
|
+
read_recommended_sampling,
|
|
107
|
+
resolve_sampling,
|
|
108
|
+
)
|
|
99
109
|
from interp_engine.select import BackendSelection, select_backend
|
|
100
110
|
from interp_engine.steer import (
|
|
101
111
|
STEER_METHODS,
|
|
@@ -185,6 +195,15 @@ __all__ = [
|
|
|
185
195
|
"HubKernelUnsupported",
|
|
186
196
|
"deepgemm_fallback_kwargs",
|
|
187
197
|
"InterpModel",
|
|
198
|
+
# Sampling: what the checkpoint recommends, and what a generation runs with.
|
|
199
|
+
"PresencePenaltyProcessor",
|
|
200
|
+
"RecommendedSampling",
|
|
201
|
+
"SamplingSettings",
|
|
202
|
+
"VLLMSamplingKwargs",
|
|
203
|
+
"apply_presence_penalty",
|
|
204
|
+
"hf_generate_kwargs",
|
|
205
|
+
"read_recommended_sampling",
|
|
206
|
+
"resolve_sampling",
|
|
188
207
|
"VLLMModel",
|
|
189
208
|
# That same surface without an event loop, for scripts and notebooks. The sync free
|
|
190
209
|
# functions below dispatch through this, so most callers never name it.
|
|
@@ -56,9 +56,10 @@ _TLENS_STABLE: dict[str, str] = {
|
|
|
56
56
|
# and that multiply -- a third tensor, wrong by an elementwise factor (cosine 0.89 on gemma-2
|
|
57
57
|
# layer 19) and one that real Gemma Scope transcoders are trained on, so substituting these
|
|
58
58
|
# would read a shipping artifact off the wrong activations. See :func:`tlens_hook_to_point`.
|
|
59
|
-
|
|
59
|
+
#
|
|
60
|
+
# Nor the block-level `hook_mlp_in` / `hook_attn_in`, which fire BEFORE the norm runs -- the
|
|
61
|
+
# norm's input, a whole normalization away from these. Refused in `_TLENS_PRENORM_BLOCK_INPUTS`.
|
|
60
62
|
"mlp.hook_in": "mlp_in",
|
|
61
|
-
"hook_attn_in": "attn_in",
|
|
62
63
|
"attn.hook_in": "attn_in",
|
|
63
64
|
# The RAW module output, as distinct from the block-level hook below.
|
|
64
65
|
"mlp.hook_out": "mlp_out",
|
|
@@ -146,6 +147,56 @@ _TLENS_STREAM_DEPENDENT: dict[str, tuple[str, str]] = {
|
|
|
146
147
|
"hook_out": ("resid_post", "resid_streams"),
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
# The block's own input, `hook_in`, is `resid_pre` on a single-stream trunk -- the mirror of
|
|
151
|
+
# `hook_out` above, and a forward pre-hook on the block is where a bridge puts it. On a
|
|
152
|
+
# hyper-connection trunk it is the stack ENTERING the block, which is the previous block's
|
|
153
|
+
# `resid_streams` and at layer 0 the embedding stack, so it is refused there with the same reason
|
|
154
|
+
# as `attn_hc.hook_in` rather than resolved to this layer's stack under a wrong index.
|
|
155
|
+
_TLENS_BLOCK_INPUT = "hook_in"
|
|
156
|
+
|
|
157
|
+
# The block-level sublayer inputs. TransformerLens fires `hook_mlp_in` on `resid_mid` and
|
|
158
|
+
# `hook_attn_in` on `resid_pre` (per head, `[batch, pos, n_heads, d_model]`), BEFORE the block norm
|
|
159
|
+
# runs. They look like `mlp_in`/`attn_in` and are not: those are the norm's OUTPUT, gain included,
|
|
160
|
+
# per the criterion in `_TLENS_STABLE`. The two differ by a whole normalization -- cosine 0.175 and
|
|
161
|
+
# a norm ratio of 231x at gemma-3-1b-it layer 5, past 3000x deeper in the stack; 27-86x on gpt2.
|
|
162
|
+
#
|
|
163
|
+
# Refused rather than resolved, for the reason `ln1/ln2.hook_normalized` are: substituting one for
|
|
164
|
+
# the other reads a shipping artifact off the wrong activations, silently, because the result is a
|
|
165
|
+
# plausible tensor of the right shape. Not remapped to the residual either -- a caller porting
|
|
166
|
+
# TransformerLens code may mean either side of the norm, and only they can say which.
|
|
167
|
+
_TLENS_PRENORM_BLOCK_INPUTS: dict[str, str] = {
|
|
168
|
+
"hook_mlp_in": (
|
|
169
|
+
"TransformerLens' block-level MLP input, fired on `resid_mid` BEFORE the block norm runs, so "
|
|
170
|
+
"it is the norm's input. The engine's `mlp_in` is the norm's output -- the tensor the MLP "
|
|
171
|
+
"receives. Ask for `mlp.hook_in` for the sublayer's argument, or `hook_resid_mid` for the "
|
|
172
|
+
"pre-norm residual this hook carries"
|
|
173
|
+
),
|
|
174
|
+
"hook_attn_in": (
|
|
175
|
+
"TransformerLens' block-level attention input, fired on `resid_pre` BEFORE the block norm "
|
|
176
|
+
"runs (and broadcast per head), so it is the norm's input. The engine's `attn_in` is the "
|
|
177
|
+
"norm's output. Ask for `attn.hook_in` for the sublayer's argument, or `hook_resid_pre` for "
|
|
178
|
+
"the pre-norm residual this hook carries"
|
|
179
|
+
),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Hooks outside the `blocks.{i}.` namespace. `unembed.hook_in` is the unembedding's input, which is
|
|
183
|
+
# the final norm's output, gain included -- a forward pre-hook on `lm_head` reads the same tensor.
|
|
184
|
+
# TransformerLens 3 registers it on the bridge and on `HookedTransformer` alike.
|
|
185
|
+
_TLENS_GLOBAL: dict[str, str] = {"unembed.hook_in": "final_norm"}
|
|
186
|
+
_POINT_TO_TLENS_GLOBAL: dict[str, str] = {point: hook for hook, point in _TLENS_GLOBAL.items()}
|
|
187
|
+
|
|
188
|
+
# `ln_final.hook_normalized` is the nearest legacy name for the same tensor and is not it, twice
|
|
189
|
+
# over: the hook fires on `x / scale` before the gain, and `HookedTransformer` folds that gain into
|
|
190
|
+
# `W_U` by default (`fold_ln`), so on a folded model the hook IS the unembedding's input while the
|
|
191
|
+
# gain has moved into the weights. Neither reading is `final_norm`.
|
|
192
|
+
_TLENS_GLOBAL_REFUSED: dict[str, str] = {
|
|
193
|
+
"ln_final.hook_normalized": (
|
|
194
|
+
"a tensor inside the final norm -- `x / scale`, before the gain (which a `fold_ln` "
|
|
195
|
+
"HookedTransformer has folded into `W_U`). It is neither the norm's input nor `final_norm`, "
|
|
196
|
+
"the norm's output; `unembed.hook_in` is the output's name on TransformerLens 3's bridge"
|
|
197
|
+
),
|
|
198
|
+
}
|
|
199
|
+
|
|
149
200
|
# The two mHC hooks that are real tensors with no canonical point, refused by name so the reason
|
|
150
201
|
# arrives instead of a list of near misses. Both are stream stacks, which is exactly why they are
|
|
151
202
|
# dangerous: `resid_streams` has their shape, so a caller mapping by eye would take either.
|
|
@@ -176,7 +227,6 @@ UNMAPPED_TLENS: frozenset[str] = frozenset(
|
|
|
176
227
|
{
|
|
177
228
|
"attn_gate", # gated attention output; TL models no family that has one
|
|
178
229
|
"embeddings", # TL's `hook_embed` is pre-positional/pre-scaling, a different tensor
|
|
179
|
-
"final_norm", # `ln_final.hook_normalized` is outside the `blocks.{i}.` namespace
|
|
180
230
|
"lm_head", # TL returns logits rather than hooking the unembed
|
|
181
231
|
"router_logits", # TL hooks the softmax over all experts, not the logits
|
|
182
232
|
"expert_weights", # TL's `hook_expert_weights` is pre-top-k, so it is a different tensor
|
|
@@ -185,14 +235,14 @@ UNMAPPED_TLENS: frozenset[str] = frozenset(
|
|
|
185
235
|
|
|
186
236
|
# Canonical point -> the TransformerLens name to emit, for the rest. `mlp_out_post`/`attn_out_post`
|
|
187
237
|
# are ours alone as *names*, but TL reaches those tensors through its aliased block-level hook, which
|
|
188
|
-
# is what this emits.
|
|
238
|
+
# is what this emits. Per-layer points only; the global ones are in `_POINT_TO_TLENS_GLOBAL`.
|
|
189
239
|
_POINT_TO_TLENS: dict[str, str] = {
|
|
190
240
|
"resid_pre": "hook_resid_pre",
|
|
191
241
|
"resid_post": "hook_resid_post",
|
|
192
242
|
"resid_mid": "hook_resid_mid",
|
|
193
243
|
"mlp_in": "mlp.hook_in",
|
|
194
244
|
"mlp_out": "mlp.hook_out",
|
|
195
|
-
"attn_in": "
|
|
245
|
+
"attn_in": "attn.hook_in",
|
|
196
246
|
"attn_out": "attn.hook_out",
|
|
197
247
|
"mlp_out_post": "hook_mlp_out",
|
|
198
248
|
"attn_out_post": "hook_attn_out",
|
|
@@ -330,7 +380,10 @@ def tlens_hook_to_point(hook_name: str, model: object | None = None) -> Address:
|
|
|
330
380
|
conventional trunk and ``resid_streams`` on a hyper-connection one, because TransformerLens 3's
|
|
331
381
|
DeepSeek-V4 bridge drops the ``hook_resid_post`` alias and returns the stream stack from that
|
|
332
382
|
hook instead. Without ``model`` it resolves to ``resid_post``, the reading that is right almost
|
|
333
|
-
everywhere -- and wrong by a rank on DeepSeek-V4 and Motif 3.
|
|
383
|
+
everywhere -- and wrong by a rank on DeepSeek-V4 and Motif 3. Its mirror ``hook_in`` is
|
|
384
|
+
``resid_pre`` on a conventional trunk and is *refused* on a hyper-connection one: the stack
|
|
385
|
+
entering block ``i`` is block ``i-1``'s ``resid_streams``, and resolving it under index ``i``
|
|
386
|
+
would name a real tensor one layer off.
|
|
334
387
|
|
|
335
388
|
The two mHC hooks that carry a stream stack *in* are refused rather than mapped, with the reason:
|
|
336
389
|
``attn_hc.hook_in`` is the previous block's ``resid_streams`` and ``mlp_hc.hook_in`` is the
|
|
@@ -338,6 +391,12 @@ def tlens_hook_to_point(hook_name: str, model: object | None = None) -> Address:
|
|
|
338
391
|
its shape. The mHC hooks that come *out* -- ``hook_post``, ``hook_comb``, ``hook_out`` at both
|
|
339
392
|
the ``attn_hc`` and ``mlp_hc`` sites -- all map, and need no ``model``.
|
|
340
393
|
|
|
394
|
+
The block-level ``hook_mlp_in``/``hook_attn_in`` are refused, because they are not the sublayer
|
|
395
|
+
inputs their names suggest: TransformerLens fires them on ``resid_mid``/``resid_pre`` *before*
|
|
396
|
+
the block norm, so they are the norm's input where ``mlp_in``/``attn_in`` are its output. The
|
|
397
|
+
sublayer-scoped ``mlp.hook_in``/``attn.hook_in`` are the output and map. The refusal names both,
|
|
398
|
+
since a caller may mean either side of the norm.
|
|
399
|
+
|
|
341
400
|
``ln1.hook_normalized``/``ln2.hook_normalized`` are unmapped for a reason worth stating, because
|
|
342
401
|
they look like the sublayer inputs: TransformerLens fires them on ``x / scale``, before the norm's
|
|
343
402
|
gain, so they are neither the norm's input (``resid_pre``/``resid_mid``) nor its output
|
|
@@ -347,6 +406,10 @@ def tlens_hook_to_point(hook_name: str, model: object | None = None) -> Address:
|
|
|
347
406
|
own drift, not the formula). Gemma Scope's transcoders are trained there, so this is a live
|
|
348
407
|
distinction, not a pedantic one.
|
|
349
408
|
|
|
409
|
+
One name outside the ``blocks.{i}.`` namespace maps: ``unembed.hook_in`` is ``final_norm``,
|
|
410
|
+
the unembedding's input. ``ln_final.hook_normalized`` is refused for the ``hook_normalized``
|
|
411
|
+
reason above, plus one more -- a ``fold_ln`` HookedTransformer has moved the gain into ``W_U``.
|
|
412
|
+
|
|
350
413
|
Two QK-norm hooks are intentionally unmapped rather than approximated. ``q_norm.hook_scale`` is
|
|
351
414
|
the norm's own denominator, an intermediate of its arithmetic that no hook on the module can
|
|
352
415
|
return -- recompute it from ``q_norm_in`` and ``facts.rms_norm_eps``, which reproduces TL's
|
|
@@ -361,10 +424,16 @@ def tlens_hook_to_point(hook_name: str, model: object | None = None) -> Address:
|
|
|
361
424
|
``softmax(cache["router_logits", layer])``, which is exact for the softmax-routed families and
|
|
362
425
|
is *not* what gpt-oss or DeepSeek-V3 compute.
|
|
363
426
|
"""
|
|
427
|
+
if hook_name in _TLENS_GLOBAL:
|
|
428
|
+
return Address(_TLENS_GLOBAL[hook_name])
|
|
429
|
+
if hook_name in _TLENS_GLOBAL_REFUSED:
|
|
430
|
+
raise UnmappedHook(f"TransformerLens hook {hook_name!r} is {_TLENS_GLOBAL_REFUSED[hook_name]}.")
|
|
431
|
+
|
|
364
432
|
match = _BLOCK_RE.match(hook_name)
|
|
365
433
|
if not match:
|
|
366
434
|
raise UnmappedHook(
|
|
367
|
-
f"Cannot parse a layer out of TransformerLens hook name {hook_name!r} (expected
|
|
435
|
+
f"Cannot parse a layer out of TransformerLens hook name {hook_name!r} (expected "
|
|
436
|
+
f"'blocks.<layer>.<hook>', or one of {', '.join(sorted(_TLENS_GLOBAL))})"
|
|
368
437
|
)
|
|
369
438
|
layer, suffix = int(match.group(1)), match.group(2)
|
|
370
439
|
|
|
@@ -376,6 +445,15 @@ def tlens_hook_to_point(hook_name: str, model: object | None = None) -> Address:
|
|
|
376
445
|
if suffix in _TLENS_STREAM_DEPENDENT:
|
|
377
446
|
single, stacked = _TLENS_STREAM_DEPENDENT[suffix]
|
|
378
447
|
return Address(stacked if model is not None and has_hyper_connections(model) else single, layer)
|
|
448
|
+
if suffix == _TLENS_BLOCK_INPUT:
|
|
449
|
+
if model is not None and has_hyper_connections(model):
|
|
450
|
+
raise UnmappedHook(
|
|
451
|
+
f"TransformerLens hook {hook_name!r} is {_TLENS_STREAM_STACK_INPUTS['attn_hc.hook_in']}."
|
|
452
|
+
)
|
|
453
|
+
return Address("resid_pre", layer)
|
|
454
|
+
|
|
455
|
+
if suffix in _TLENS_PRENORM_BLOCK_INPUTS:
|
|
456
|
+
raise UnmappedHook(f"TransformerLens hook {hook_name!r} is {_TLENS_PRENORM_BLOCK_INPUTS[suffix]}.")
|
|
379
457
|
|
|
380
458
|
if suffix in _TLENS_STREAM_STACK_INPUTS:
|
|
381
459
|
raise UnmappedHook(f"TransformerLens hook {hook_name!r} is {_TLENS_STREAM_STACK_INPUTS[suffix]}.")
|
|
@@ -388,7 +466,7 @@ def tlens_hook_to_point(hook_name: str, model: object | None = None) -> Address:
|
|
|
388
466
|
f"and `capture.pre_gain_normalized` for the arithmetic."
|
|
389
467
|
)
|
|
390
468
|
|
|
391
|
-
known = sorted({*_TLENS_STABLE, *_TLENS_CONTRIBUTION, *_TLENS_STREAM_DEPENDENT})
|
|
469
|
+
known = sorted({*_TLENS_STABLE, *_TLENS_CONTRIBUTION, *_TLENS_STREAM_DEPENDENT, _TLENS_BLOCK_INPUT})
|
|
392
470
|
raise UnmappedHook(f"TransformerLens hook {hook_name!r} has no canonical point (known: {', '.join(known)})")
|
|
393
471
|
|
|
394
472
|
|
|
@@ -455,8 +533,14 @@ def point_to_tlens_hook(point: str | Address, layer: int | None = None) -> str:
|
|
|
455
533
|
|
|
456
534
|
Takes an :class:`~interp_engine.address.Address` or the ``(point, layer)`` pair. An address
|
|
457
535
|
carrying a coordinate TransformerLens cannot express raises rather than losing it -- see
|
|
458
|
-
:func:`_refuse_unmappable_coordinates`.
|
|
536
|
+
:func:`_refuse_unmappable_coordinates`. The one global point with a name, ``final_norm``, takes
|
|
537
|
+
no layer and emits ``unembed.hook_in``.
|
|
459
538
|
"""
|
|
539
|
+
name = point.name if isinstance(point, Address) else point
|
|
540
|
+
if name in _POINT_TO_TLENS_GLOBAL:
|
|
541
|
+
if (point.layer if isinstance(point, Address) else layer) is not None:
|
|
542
|
+
raise UnmappedHook(f"{name!r} is a global point and takes no layer")
|
|
543
|
+
return _POINT_TO_TLENS_GLOBAL[name]
|
|
460
544
|
point, layer, address = _split(point, layer)
|
|
461
545
|
_refuse_unmappable_coordinates(address, "TransformerLens", "blocks.{i}.{hook}")
|
|
462
546
|
if point not in _POINT_TO_TLENS:
|
|
@@ -467,7 +551,7 @@ def point_to_tlens_hook(point: str | Address, layer: int | None = None) -> str:
|
|
|
467
551
|
"is a canonical point that TransformerLens has no equivalent for" if known else "is not a canonical point"
|
|
468
552
|
)
|
|
469
553
|
raise UnmappedHook(
|
|
470
|
-
f"{point!r} {reason} (mappable: {', '.join(sorted(_POINT_TO_TLENS))})"
|
|
554
|
+
f"{point!r} {reason} (mappable: {', '.join(sorted({*_POINT_TO_TLENS, *_POINT_TO_TLENS_GLOBAL}))})"
|
|
471
555
|
+ (f"; ours alone: {', '.join(sorted(UNMAPPED_TLENS))}" if known else "")
|
|
472
556
|
)
|
|
473
557
|
return f"blocks.{layer}.{_POINT_TO_TLENS[point]}"
|
|
@@ -44,6 +44,7 @@ from interp_engine.facts import factored_projection, text_config
|
|
|
44
44
|
from interp_engine.points import PointSpec, Scope, known_names, point_spec, points_for
|
|
45
45
|
from interp_engine.protocol import Completion
|
|
46
46
|
from interp_engine.residual_basis import ResidualBasis, eager_residual_basis
|
|
47
|
+
from interp_engine.sampling import RecommendedSampling, SamplingSettings, read_recommended_sampling, resolve_sampling
|
|
47
48
|
from interp_engine.tokenize import Tokenize
|
|
48
49
|
|
|
49
50
|
logger = logging.getLogger(__name__)
|
|
@@ -504,6 +505,7 @@ class EagerModel:
|
|
|
504
505
|
self._requires_grad = requires_grad
|
|
505
506
|
# Lazily computed in `grad_support`, never here: a verdict must not be part of loading.
|
|
506
507
|
self._grad_support: GradSupport | None = None
|
|
508
|
+
self._recommended_sampling: RecommendedSampling | None = None
|
|
507
509
|
torch_dtype = _normalize_dtype(dtype)
|
|
508
510
|
model_kwargs = dict(model_kwargs or {})
|
|
509
511
|
# Resolved once, and only when something is actually fetched by id: a caller handing over
|
|
@@ -623,6 +625,33 @@ class EagerModel:
|
|
|
623
625
|
def default_prepend_bos(self) -> bool:
|
|
624
626
|
return self._default_prepend_bos
|
|
625
627
|
|
|
628
|
+
@property
|
|
629
|
+
def recommended_sampling(self) -> RecommendedSampling:
|
|
630
|
+
"""Read when first asked, never at load: a caller handing over its own ``hf_model`` and
|
|
631
|
+
``tokenizer`` may use an id that resolves to nothing, and gets an empty answer here."""
|
|
632
|
+
# `getattr`: a test double built without `__init__` has no slot yet.
|
|
633
|
+
stated = getattr(self, "_recommended_sampling", None)
|
|
634
|
+
if stated is None:
|
|
635
|
+
stated = self._recommended_sampling = read_recommended_sampling(self.hf_model_id)
|
|
636
|
+
return stated
|
|
637
|
+
|
|
638
|
+
def sampling_settings(
|
|
639
|
+
self,
|
|
640
|
+
*,
|
|
641
|
+
temperature: float | None = None,
|
|
642
|
+
top_k: int | None = None,
|
|
643
|
+
top_p: float | None = None,
|
|
644
|
+
presence_penalty: float | None = None,
|
|
645
|
+
) -> SamplingSettings:
|
|
646
|
+
"""See the protocol."""
|
|
647
|
+
return resolve_sampling(
|
|
648
|
+
self.recommended_sampling,
|
|
649
|
+
temperature=temperature,
|
|
650
|
+
top_k=top_k,
|
|
651
|
+
top_p=top_p,
|
|
652
|
+
presence_penalty=presence_penalty,
|
|
653
|
+
)
|
|
654
|
+
|
|
626
655
|
# --- classmethod convenience --------------------------------------------
|
|
627
656
|
@classmethod
|
|
628
657
|
def from_pretrained(
|
|
@@ -1273,7 +1302,15 @@ class EagerModel:
|
|
|
1273
1302
|
return capture_attention_eager(self, [int(t) for t in prompt_token_ids], layers)
|
|
1274
1303
|
|
|
1275
1304
|
def _generate_completion(
|
|
1276
|
-
self,
|
|
1305
|
+
self,
|
|
1306
|
+
prompt_ids: list[int],
|
|
1307
|
+
max_tokens: int,
|
|
1308
|
+
temperature: float | None,
|
|
1309
|
+
seed: int | None,
|
|
1310
|
+
*,
|
|
1311
|
+
top_k: int | None = None,
|
|
1312
|
+
top_p: float | None = None,
|
|
1313
|
+
presence_penalty: float | None = None,
|
|
1277
1314
|
) -> Completion:
|
|
1278
1315
|
from interp_engine.steer import generate_stream
|
|
1279
1316
|
|
|
@@ -1283,6 +1320,9 @@ class EagerModel:
|
|
|
1283
1320
|
torch.tensor([prompt_ids], device=self.device),
|
|
1284
1321
|
max_tokens=max_tokens,
|
|
1285
1322
|
temperature=temperature,
|
|
1323
|
+
top_k=top_k,
|
|
1324
|
+
top_p=top_p,
|
|
1325
|
+
presence_penalty=presence_penalty,
|
|
1286
1326
|
seed=seed,
|
|
1287
1327
|
)
|
|
1288
1328
|
)
|
|
@@ -1296,18 +1336,32 @@ class EagerModel:
|
|
|
1296
1336
|
prompt_token_ids: Any,
|
|
1297
1337
|
*,
|
|
1298
1338
|
max_tokens: int = 200,
|
|
1299
|
-
temperature: float =
|
|
1339
|
+
temperature: float | None = None,
|
|
1340
|
+
top_k: int | None = None,
|
|
1341
|
+
top_p: float | None = None,
|
|
1342
|
+
presence_penalty: float | None = None,
|
|
1300
1343
|
seed: int | None = None,
|
|
1301
1344
|
) -> str:
|
|
1302
1345
|
"""Generate and return the completion text. See the protocol."""
|
|
1303
|
-
return self._generate_completion(
|
|
1346
|
+
return self._generate_completion(
|
|
1347
|
+
[int(t) for t in prompt_token_ids],
|
|
1348
|
+
max_tokens,
|
|
1349
|
+
temperature,
|
|
1350
|
+
seed,
|
|
1351
|
+
top_k=top_k,
|
|
1352
|
+
top_p=top_p,
|
|
1353
|
+
presence_penalty=presence_penalty,
|
|
1354
|
+
).text
|
|
1304
1355
|
|
|
1305
1356
|
async def generate_stream(
|
|
1306
1357
|
self,
|
|
1307
1358
|
prompt_token_ids: Any,
|
|
1308
1359
|
*,
|
|
1309
1360
|
max_tokens: int = 200,
|
|
1310
|
-
temperature: float =
|
|
1361
|
+
temperature: float | None = None,
|
|
1362
|
+
top_k: int | None = None,
|
|
1363
|
+
top_p: float | None = None,
|
|
1364
|
+
presence_penalty: float | None = None,
|
|
1311
1365
|
seed: int | None = None,
|
|
1312
1366
|
) -> AsyncIterator[str]:
|
|
1313
1367
|
"""Yield decoded text deltas, one per token. See the protocol.
|
|
@@ -1323,6 +1377,9 @@ class EagerModel:
|
|
|
1323
1377
|
torch.tensor([[int(t) for t in prompt_token_ids]], device=self.device),
|
|
1324
1378
|
max_tokens=max_tokens,
|
|
1325
1379
|
temperature=temperature,
|
|
1380
|
+
top_k=top_k,
|
|
1381
|
+
top_p=top_p,
|
|
1382
|
+
presence_penalty=presence_penalty,
|
|
1326
1383
|
seed=seed,
|
|
1327
1384
|
):
|
|
1328
1385
|
yield step.token_str
|
|
@@ -47,6 +47,7 @@ import torch
|
|
|
47
47
|
from interp_engine.address import Address
|
|
48
48
|
from interp_engine.autograd_support import GradSupport
|
|
49
49
|
from interp_engine.residual_basis import ResidualBasis
|
|
50
|
+
from interp_engine.sampling import RecommendedSampling, SamplingSettings
|
|
50
51
|
|
|
51
52
|
# Re-exported so a protocol-typed caller needs one import. The type itself lives in
|
|
52
53
|
# `interp_engine.address`, which owns the grammar; this module defines a Protocol and should not.
|
|
@@ -150,6 +151,34 @@ class InterpModel(Protocol):
|
|
|
150
151
|
"""
|
|
151
152
|
...
|
|
152
153
|
|
|
154
|
+
# --- sampling -----------------------------------------------------------
|
|
155
|
+
@property
|
|
156
|
+
def recommended_sampling(self) -> RecommendedSampling:
|
|
157
|
+
"""What the checkpoint's ``generation_config.json`` states: temperature, top-k, top-p,
|
|
158
|
+
``do_sample``. Empty when the checkpoint has none (Qwen3.5) or the file names only token
|
|
159
|
+
ids (GPT-2). The file is Hugging Face's format, so a presence penalty cannot appear here
|
|
160
|
+
whatever the model card says.
|
|
161
|
+
"""
|
|
162
|
+
...
|
|
163
|
+
|
|
164
|
+
def sampling_settings(
|
|
165
|
+
self,
|
|
166
|
+
*,
|
|
167
|
+
temperature: float | None = None,
|
|
168
|
+
top_k: int | None = None,
|
|
169
|
+
top_p: float | None = None,
|
|
170
|
+
presence_penalty: float | None = None,
|
|
171
|
+
) -> SamplingSettings:
|
|
172
|
+
"""What a generation called with these arguments runs with, every knob decided.
|
|
173
|
+
|
|
174
|
+
The rule is :func:`interp_engine.sampling.resolve_sampling`: a knob passed is used as
|
|
175
|
+
passed, a knob left ``None`` takes :attr:`recommended_sampling`, and a knob neither states
|
|
176
|
+
is neutral (temperature 1, no filtering, no penalty). Every generation method below
|
|
177
|
+
resolves its knobs this way, so a run is reproducible from its arguments plus the
|
|
178
|
+
checkpoint; a server reports the result beside the completion.
|
|
179
|
+
"""
|
|
180
|
+
...
|
|
181
|
+
|
|
153
182
|
# --- tokenization -------------------------------------------------------
|
|
154
183
|
tokenizer: Any
|
|
155
184
|
"""The HF tokenizer (or processor on multimodal archs), for chat templating and
|
|
@@ -254,10 +283,17 @@ class InterpModel(Protocol):
|
|
|
254
283
|
prompt_token_ids: Sequence[int],
|
|
255
284
|
*,
|
|
256
285
|
max_tokens: int = 200,
|
|
257
|
-
temperature: float =
|
|
286
|
+
temperature: float | None = None,
|
|
287
|
+
top_k: int | None = None,
|
|
288
|
+
top_p: float | None = None,
|
|
289
|
+
presence_penalty: float | None = None,
|
|
258
290
|
seed: int | None = None,
|
|
259
291
|
) -> str:
|
|
260
|
-
"""Generate and return the completion text (no prompt echo).
|
|
292
|
+
"""Generate and return the completion text (no prompt echo).
|
|
293
|
+
|
|
294
|
+
Sampling knobs left ``None`` take the checkpoint's recommendation; see
|
|
295
|
+
:meth:`sampling_settings`.
|
|
296
|
+
"""
|
|
261
297
|
...
|
|
262
298
|
|
|
263
299
|
def generate_stream(
|
|
@@ -265,7 +301,10 @@ class InterpModel(Protocol):
|
|
|
265
301
|
prompt_token_ids: Sequence[int],
|
|
266
302
|
*,
|
|
267
303
|
max_tokens: int = 200,
|
|
268
|
-
temperature: float =
|
|
304
|
+
temperature: float | None = None,
|
|
305
|
+
top_k: int | None = None,
|
|
306
|
+
top_p: float | None = None,
|
|
307
|
+
presence_penalty: float | None = None,
|
|
269
308
|
seed: int | None = None,
|
|
270
309
|
) -> AsyncIterator[str]:
|
|
271
310
|
"""Yield decoded text deltas as they are produced.
|