interp-engine 1.11.2__tar.gz → 1.12.0__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.2 → interp_engine-1.12.0}/PKG-INFO +1 -1
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/USAGE.md +7 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/__init__.py +2 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/capture.py +144 -19
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/dispatch.py +4 -4
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/requests.py +9 -5
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/static.py +55 -37
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/steering.py +26 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/pyproject.toml +1 -1
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_capability_refusals.py +3 -3
- interp_engine-1.12.0/tests/test_padded_positions.py +174 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_steer_math_parity.py +108 -0
- interp_engine-1.12.0/tests/test_steer_ops_per_site_gpu.py +132 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_hyper_connections.py +2 -2
- interp_engine-1.12.0/tests/test_vllm_padded_gpu.py +76 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/.gitignore +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/LICENSE +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/README.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/README.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/__init__.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/bench_spec.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/cells.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/probe.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/probe_lens_stream.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/publish.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/report_bench.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/gemma-2-2b__eager.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3-4b__eager.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3-4b__vllm.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results-latest.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/run_all.sh +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/run_bench.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/workloads.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/AGENT_INTEGRATION.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/ARCHITECTURE_QUIRKS.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/COMPATIBILITY.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/GRADIENTS.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/INTERNALS.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/PERFORMANCE.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/PORTING.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/README.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/SUPPORTED_POINTS.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/VALIDATING_A_NEW_MODEL.md +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/_loop.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/address.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/arch.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/attn_config.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/attn_scores.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/autograd_support.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/chat_compose.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/chat_conventions.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/chat_formatters.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/cuda_preflight.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/facts.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/hooks.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/lens.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/load.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/mappers.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/memory.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/model.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/moe_routing.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/notebook_stdout.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/points.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/protocol.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/residual_basis.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/sampling.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/select.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/steer.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/steer_specs.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/sync.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/tokenize.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_backend.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/__init__.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_demux.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_hooks.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_payload.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_tp.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_tree.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/attn.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/capture.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/graphs.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/lens/__init__.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/lens/intervene.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/lens/readout.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/lens/unembed.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/mhc.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/native.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_plugin.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/conftest.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/harness.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/model_expectations.yaml +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/synthetic_families.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_address.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_attn_config_tripwire.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_attn_probs_indexing.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_attn_scores.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_attn_z_gqa.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_autograd_support.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_bench_workloads.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_capture_addressing.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_chat_compose.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_chat_formatters.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_chat_templates.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_core.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_cuda_preflight.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_doc_code_fences.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_eager_autograd.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_facts.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_family_points.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_gated_attn_out.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_gpu_sizer.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_head_contributions.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_hook_call_conventions.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_injected_system_spans.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_layer_kinds.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_load.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_logit_transform.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_mappers.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_memory.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_mlp_internals.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_model_expectations.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_moe.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_multigpu.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_multimodal_arch.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_new_models_gpu.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_no_chat_template.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_normalized_hook.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_notebook_stdout.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_packaging.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_parity_gpt2.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_per_layer_attn_dims.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_points_registry.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_protocol.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_published_benchmarks.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_qk_norm.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_qkv_layout.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_reasoning_spans.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_release.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_resid_mid.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_residual_basis.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sampling.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sandwich_norms.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_select.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sliding_window_attn.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_small_models_gpu.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_static_dsv4_gpu.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_static_parity_gpu.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_static_set.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_static_warmup.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_steer_context.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sync_loop.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sync_parity.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_unified_free_functions.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_unresolved_families.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_capture_gpu.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_capture_scales.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_engine_loop.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_graph_path.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_graphs_on_gpu.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_hook_availability.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_kv_isolation.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_new_points.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_only_families.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_plugin.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_wire_grammar.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vocabulary_boundary.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_worker_lens_capture_readout.py +0 -0
- {interp_engine-1.11.2 → interp_engine-1.12.0}/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.
|
|
3
|
+
Version: 1.12.0
|
|
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
|
|
@@ -446,6 +446,13 @@ for step in generate_stream(model, tokens, max_tokens=8, n_logprobs=5):
|
|
|
446
446
|
accepts either an `Address` or its string form on lookup. `generate_stream` yields a `GenStep` per
|
|
447
447
|
token, which is richer than the text deltas the protocol's streaming method gives you.
|
|
448
448
|
|
|
449
|
+
`run_with_cache` also takes a padded batch with its `attention_mask`, on both backends. Each row
|
|
450
|
+
counts its positions from 0 at its first real token, as HF `generate` does, so a left-padded row
|
|
451
|
+
gives the same values as the prompt alone. `position_ids_from_mask(mask)` returns those positions.
|
|
452
|
+
Do not read values at masked positions: eager computes them from pad tokens, and vLLM fills them
|
|
453
|
+
with zeros. On vLLM each row is a separate request, and `position_ids` is refused unless it equals
|
|
454
|
+
those positions.
|
|
455
|
+
|
|
449
456
|
`load_model` on eager still returns a model whose *methods* are async, and calling one from sync code
|
|
450
457
|
is the thing these functions save you from. When you want a method rather than a free function, wrap
|
|
451
458
|
the model once:
|
|
@@ -40,6 +40,7 @@ from interp_engine.capture import (
|
|
|
40
40
|
head_contributions,
|
|
41
41
|
is_rms_norm,
|
|
42
42
|
per_head_value,
|
|
43
|
+
position_ids_from_mask,
|
|
43
44
|
pre_gain_normalized,
|
|
44
45
|
rms_norm_parts,
|
|
45
46
|
run_with_cache,
|
|
@@ -238,6 +239,7 @@ __all__ = [
|
|
|
238
239
|
"tlens_hook_to_point",
|
|
239
240
|
"tlens_normalized_hook",
|
|
240
241
|
"per_head_value",
|
|
242
|
+
"position_ids_from_mask",
|
|
241
243
|
"pre_gain_normalized",
|
|
242
244
|
"rms_norm_eps_for_model",
|
|
243
245
|
"rms_norm_parts",
|
|
@@ -15,6 +15,8 @@ Design invariants (protect future probing/monitoring apps):
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import asyncio
|
|
19
|
+
import inspect
|
|
18
20
|
import math
|
|
19
21
|
from collections.abc import Sequence
|
|
20
22
|
from dataclasses import dataclass, field
|
|
@@ -161,6 +163,7 @@ def run_with_cache(
|
|
|
161
163
|
*,
|
|
162
164
|
detach: bool = True,
|
|
163
165
|
attention_mask: torch.Tensor | None = None,
|
|
166
|
+
position_ids: torch.Tensor | None = None,
|
|
164
167
|
) -> Cache:
|
|
165
168
|
"""Run a single forward pass, capturing the requested points into a :class:`Cache`.
|
|
166
169
|
|
|
@@ -171,13 +174,85 @@ def run_with_cache(
|
|
|
171
174
|
``tokens`` may be the ``[batch, seq]`` tensor ``model.to_tokens`` returns, a bare ``[seq]``
|
|
172
175
|
tensor, or a list of ids.
|
|
173
176
|
|
|
177
|
+
``attention_mask`` marks a padded batch's real tokens with 1. With a mask and no
|
|
178
|
+
``position_ids``, each row counts its positions from 0 at its first real token, as HF
|
|
179
|
+
``generate`` does (see :func:`position_ids_from_mask`). So a left-padded row gives the values
|
|
180
|
+
it gives unpadded, on both backends. Values at masked positions have no meaning; do not read them.
|
|
181
|
+
|
|
174
182
|
``detach=True`` (the default, for activation endpoints) stores detached clones. Pass
|
|
175
183
|
``detach=False`` to keep the autograd graph (the lens does this on residuals); it raises
|
|
176
184
|
on vLLM, and on eager unless the model was built with ``requires_grad=True``.
|
|
177
185
|
"""
|
|
178
186
|
if not isinstance(model, EagerModel):
|
|
179
|
-
return _run_with_cache_via_protocol(
|
|
180
|
-
|
|
187
|
+
return _run_with_cache_via_protocol(
|
|
188
|
+
model, tokens, points, detach=detach, attention_mask=attention_mask, position_ids=position_ids
|
|
189
|
+
)
|
|
190
|
+
return _run_with_cache_eager(
|
|
191
|
+
model, tokens, points, detach=detach, attention_mask=attention_mask, position_ids=position_ids
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def position_ids_from_mask(attention_mask: torch.Tensor) -> torch.Tensor:
|
|
196
|
+
"""The position ids HF ``generate`` builds from a ``[batch, seq]`` attention mask.
|
|
197
|
+
|
|
198
|
+
Each row counts from 0 at its first unmasked token, so a left-padded row gets the positions it
|
|
199
|
+
has unpadded. Masked positions get 0. :func:`run_with_cache` uses this when given a mask alone.
|
|
200
|
+
"""
|
|
201
|
+
mask = attention_mask.long()
|
|
202
|
+
return (mask.cumsum(-1) - 1).masked_fill(mask == 0, 0)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _as_mask(attention_mask: torch.Tensor, input_ids: torch.Tensor) -> torch.Tensor:
|
|
206
|
+
"""``attention_mask`` on the ids' device, with the ids' ``[batch, seq]`` shape when it is 1-D or 2-D.
|
|
207
|
+
|
|
208
|
+
A mask of more dimensions is the caller's own attention pattern and goes to the model unchanged.
|
|
209
|
+
"""
|
|
210
|
+
mask = attention_mask.to(input_ids.device)
|
|
211
|
+
if mask.ndim == 1:
|
|
212
|
+
mask = mask.unsqueeze(0)
|
|
213
|
+
if mask.ndim == 2 and mask.shape != input_ids.shape:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"attention_mask has shape {tuple(mask.shape)} and the tokens have shape "
|
|
216
|
+
f"{tuple(input_ids.shape)}; a padding mask has one entry per token."
|
|
217
|
+
)
|
|
218
|
+
return mask
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _as_positions(position_ids: torch.Tensor, input_ids: torch.Tensor) -> torch.Tensor:
|
|
222
|
+
"""``position_ids`` on the ids' device, with a batch axis when it is 1-D."""
|
|
223
|
+
given = position_ids.to(input_ids.device)
|
|
224
|
+
if given.ndim == 1:
|
|
225
|
+
given = given.unsqueeze(0)
|
|
226
|
+
if given.shape[-1] != input_ids.shape[-1]:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f"position_ids has shape {tuple(given.shape)} and the tokens have shape "
|
|
229
|
+
f"{tuple(input_ids.shape)}; it needs one position per token."
|
|
230
|
+
)
|
|
231
|
+
return given
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _forward_position_ids(
|
|
235
|
+
hf_model: Any, input_ids: torch.Tensor, mask: torch.Tensor | None, position_ids: torch.Tensor | None
|
|
236
|
+
) -> torch.Tensor | None:
|
|
237
|
+
"""The ``position_ids`` for ``hf_model``, or ``None`` to let the model count from 0.
|
|
238
|
+
|
|
239
|
+
The same rule as HF ``generate``: a 2-D mask with no explicit ids gives
|
|
240
|
+
:func:`position_ids_from_mask`, if the model is decoder-only and its forward takes them.
|
|
241
|
+
"""
|
|
242
|
+
takes = "position_ids" in inspect.signature(hf_model.forward).parameters
|
|
243
|
+
if position_ids is not None:
|
|
244
|
+
if not takes:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f"{type(hf_model).__name__}.forward takes no position_ids, so they cannot be honored. "
|
|
247
|
+
"Its positions come from the attention mask (ALiBi) or are not an input at all: "
|
|
248
|
+
"pass attention_mask and omit position_ids."
|
|
249
|
+
)
|
|
250
|
+
return _as_positions(position_ids, input_ids)
|
|
251
|
+
if mask is None or mask.ndim != 2 or not takes:
|
|
252
|
+
return None
|
|
253
|
+
if getattr(hf_model.config, "is_encoder_decoder", False):
|
|
254
|
+
return None
|
|
255
|
+
return position_ids_from_mask(mask)
|
|
181
256
|
|
|
182
257
|
|
|
183
258
|
def _run_with_cache_via_protocol(
|
|
@@ -187,32 +262,76 @@ def _run_with_cache_via_protocol(
|
|
|
187
262
|
*,
|
|
188
263
|
detach: bool,
|
|
189
264
|
attention_mask: torch.Tensor | None,
|
|
265
|
+
position_ids: torch.Tensor | None,
|
|
190
266
|
) -> Cache:
|
|
191
|
-
"""The non-eager arm:
|
|
267
|
+
"""The non-eager arm: each row through :meth:`InterpModel.capture`, shaped like a Cache.
|
|
192
268
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
269
|
+
The backend takes one unpadded prompt per request and counts its positions from 0. So each
|
|
270
|
+
row runs as its unmasked tokens alone, which gives the positions :func:`position_ids_from_mask`
|
|
271
|
+
gives the eager arm. The rows go as concurrent requests, which the backend's scheduler batches.
|
|
272
|
+
Masked positions hold zeros.
|
|
273
|
+
|
|
274
|
+
``position_ids`` is accepted only where it is those positions, as nothing else can reach
|
|
275
|
+
the backend.
|
|
196
276
|
|
|
197
277
|
:attr:`Cache.output` stays ``None``: the forward happened in a worker subprocess and there
|
|
198
278
|
is no HF output object to hand back. Everything reading ``cache.tensors`` is unaffected,
|
|
199
279
|
which is nearly every caller.
|
|
200
280
|
"""
|
|
201
|
-
if attention_mask is not None:
|
|
202
|
-
raise refuse(model, "run_with_cache", capability="attention_mask")
|
|
203
281
|
steering = active_steering(model)
|
|
204
282
|
if steering is not None and steering.position_mask is not None:
|
|
205
283
|
raise refuse(model, "steer(..., position_mask=...) around a capture", capability="masked_steer_positions")
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
284
|
+
input_ids = as_batched_tokens(tokens).cpu()
|
|
285
|
+
keep = torch.ones_like(input_ids, dtype=torch.bool)
|
|
286
|
+
if attention_mask is not None:
|
|
287
|
+
mask = _as_mask(attention_mask, input_ids)
|
|
288
|
+
if mask.ndim != 2:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f"attention_mask has shape {tuple(mask.shape)}; this backend takes a [batch, seq] "
|
|
291
|
+
"padding mask, not an attention pattern."
|
|
292
|
+
)
|
|
293
|
+
keep = mask != 0
|
|
294
|
+
if position_ids is not None:
|
|
295
|
+
given = _as_positions(position_ids, input_ids)
|
|
296
|
+
want = position_ids_from_mask(keep)
|
|
297
|
+
if (
|
|
298
|
+
given.ndim != 2
|
|
299
|
+
or given.shape[0] not in (1, want.shape[0])
|
|
300
|
+
or not torch.equal(given.expand_as(want)[keep], want[keep])
|
|
301
|
+
):
|
|
302
|
+
raise refuse(model, "run_with_cache(position_ids=...)", capability="position_ids")
|
|
303
|
+
rows = [input_ids[b][keep[b]].tolist() for b in range(input_ids.shape[0])]
|
|
304
|
+
for b, row in enumerate(rows):
|
|
305
|
+
if not row:
|
|
306
|
+
raise ValueError(f"run_with_cache: row {b} has no unmasked token, so it has nothing to run.")
|
|
307
|
+
addresses = _normalize_points(points)
|
|
308
|
+
spec = None if steering is None else steering.spec
|
|
309
|
+
|
|
310
|
+
async def _every_row() -> list[dict[Address, torch.Tensor]]:
|
|
311
|
+
return await asyncio.gather(*(model.capture(row, addresses, steering_spec=spec, detach=detach) for row in rows))
|
|
312
|
+
|
|
313
|
+
captured = sync_model(model).runner.run(_every_row(), what="run_with_cache()")
|
|
314
|
+
if len(rows) == 1 and bool(keep.all()):
|
|
315
|
+
# Restore the batch axis the eager path keeps, so `cache[point][0]` means the same thing on
|
|
316
|
+
# both backends. A view, so this costs nothing.
|
|
317
|
+
return Cache(tensors={address: t.unsqueeze(0) for address, t in captured[0].items()}, output=None)
|
|
318
|
+
return Cache(tensors={a: _pad_rows(a, [c[a] for c in captured], keep) for a in captured[0]}, output=None)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _pad_rows(address: Address, rows: list[torch.Tensor], keep: torch.Tensor) -> torch.Tensor:
|
|
322
|
+
"""One ``[batch, seq, ...]`` tensor from each row's ``[kept, ...]`` capture, zeros where ``keep`` is False."""
|
|
323
|
+
first = rows[0]
|
|
324
|
+
out = first.new_zeros((keep.shape[0], keep.shape[1], *first.shape[1:]))
|
|
325
|
+
for b, row in enumerate(rows):
|
|
326
|
+
kept = int(keep[b].sum())
|
|
327
|
+
if row.shape[0] != kept or row.shape[1:] != first.shape[1:]:
|
|
328
|
+
raise ValueError(
|
|
329
|
+
f"{address} came back with shape {tuple(row.shape)} for a {kept}-token row, which is "
|
|
330
|
+
"not one entry per token, so it cannot go back into a padded batch. Capture it one "
|
|
331
|
+
"prompt at a time."
|
|
332
|
+
)
|
|
333
|
+
out[b, keep[b].to(out.device)] = row
|
|
334
|
+
return out
|
|
216
335
|
|
|
217
336
|
|
|
218
337
|
def _run_with_cache_eager(
|
|
@@ -222,12 +341,15 @@ def _run_with_cache_eager(
|
|
|
222
341
|
*,
|
|
223
342
|
detach: bool = True,
|
|
224
343
|
attention_mask: torch.Tensor | None = None,
|
|
344
|
+
position_ids: torch.Tensor | None = None,
|
|
225
345
|
) -> Cache:
|
|
226
346
|
"""Capture in-process off the live module tree. See :func:`run_with_cache`."""
|
|
227
347
|
# Placed here rather than by each caller: these ids go straight into `hf_model`, so a list or a
|
|
228
348
|
# host tensor -- both documented inputs -- fails on every accelerator otherwise, through either
|
|
229
349
|
# entry point that shares this body. A tensor already on the device is unmoved.
|
|
230
350
|
input_ids = as_batched_tokens(tokens, device=model.device)
|
|
351
|
+
mask = None if attention_mask is None else _as_mask(attention_mask, input_ids)
|
|
352
|
+
positions = _forward_position_ids(model.hf_model, input_ids, mask, position_ids)
|
|
231
353
|
addresses = _normalize_points(points)
|
|
232
354
|
cache = Cache()
|
|
233
355
|
|
|
@@ -324,12 +446,15 @@ def _run_with_cache_eager(
|
|
|
324
446
|
hm.read(module, make_reader(keys), point=point) # pyright: ignore[reportArgumentType]
|
|
325
447
|
|
|
326
448
|
forward_ctx = torch.no_grad() if detach else torch.enable_grad()
|
|
449
|
+
# Left out rather than passed as `None`, so a forward with no such argument never sees it.
|
|
450
|
+
extra = {} if positions is None else {"position_ids": positions}
|
|
327
451
|
with forward_ctx:
|
|
328
452
|
output = model.hf_model(
|
|
329
453
|
input_ids,
|
|
330
|
-
attention_mask=
|
|
454
|
+
attention_mask=mask,
|
|
331
455
|
output_attentions=wants_attn,
|
|
332
456
|
use_cache=False,
|
|
457
|
+
**extra,
|
|
333
458
|
)
|
|
334
459
|
cache.output = output
|
|
335
460
|
for layer, tensor in scores.items():
|
|
@@ -137,10 +137,10 @@ CAPABILITIES: dict[str, Capability] = {
|
|
|
137
137
|
why="vLLM owns its weights in a worker subprocess and does not expose them to this process",
|
|
138
138
|
instead="running this on an eager model, which holds the live module tree",
|
|
139
139
|
),
|
|
140
|
-
"
|
|
141
|
-
what="
|
|
142
|
-
why="
|
|
143
|
-
instead="omitting
|
|
140
|
+
"position_ids": Capability(
|
|
141
|
+
what="position ids other than the ones its attention mask gives",
|
|
142
|
+
why="each row runs as its own request, and the backend counts that request's positions from 0",
|
|
143
|
+
instead="omitting position_ids, which counts each row from its first unmasked token on both backends",
|
|
144
144
|
),
|
|
145
145
|
"masked_steer_positions": Capability(
|
|
146
146
|
what="steering that skips some prompt positions",
|
|
@@ -65,7 +65,7 @@ from interp_engine.vllm_capture.capture import worker_addresses
|
|
|
65
65
|
from interp_engine.vllm_capture.graphs import graph_debug, refuse_writes_reason
|
|
66
66
|
from interp_engine.vllm_capture.lens import _make_lens_modifier
|
|
67
67
|
from interp_engine.vllm_capture.mhc import mhc_taps, require_available, require_steerable
|
|
68
|
-
from interp_engine.vllm_capture.steering import
|
|
68
|
+
from interp_engine.vllm_capture.steering import _compose_modifiers, _make_steer_modifiers
|
|
69
69
|
|
|
70
70
|
# --- the per-request combined hook body ---------------------------------------
|
|
71
71
|
|
|
@@ -679,9 +679,11 @@ def worker_register_steering(
|
|
|
679
679
|
demux.registered.add(req_id)
|
|
680
680
|
mods = demux.steer_mods.setdefault(req_id, {})
|
|
681
681
|
skip_set = {int(i) for i in (skip_positions or [])}
|
|
682
|
+
by_site: dict[Address, list[dict]] = {}
|
|
682
683
|
for s in specs:
|
|
683
|
-
|
|
684
|
-
|
|
684
|
+
by_site.setdefault(_write_site(worker, s), []).append(s)
|
|
685
|
+
for site, group in by_site.items():
|
|
686
|
+
mods[site] = (_make_steer_modifiers(group, demux.dev, demux.dt), skip_set, int(prompt_len))
|
|
685
687
|
_ensure_hook(worker, demux, site)
|
|
686
688
|
|
|
687
689
|
|
|
@@ -708,10 +710,12 @@ def worker_register_lens(
|
|
|
708
710
|
demux.registered.add(req_id)
|
|
709
711
|
lm = demux.lens_mods.setdefault(req_id, {})
|
|
710
712
|
skip_set = {int(i) for i in (skip_positions or [])}
|
|
713
|
+
by_site: dict[Address, list[dict]] = {}
|
|
711
714
|
for s in specs:
|
|
712
|
-
|
|
715
|
+
by_site.setdefault(_write_site(worker, s), []).append(s)
|
|
716
|
+
for site, group in by_site.items():
|
|
713
717
|
lm[site] = (
|
|
714
|
-
_make_lens_modifier(s, demux.dev, demux.dt),
|
|
718
|
+
_compose_modifiers([_make_lens_modifier(s, demux.dev, demux.dt) for s in group]),
|
|
715
719
|
bool(steer_generated),
|
|
716
720
|
skip_set,
|
|
717
721
|
int(prompt_len),
|
|
@@ -1812,9 +1812,9 @@ def worker_set_static_delta(
|
|
|
1812
1812
|
) -> None:
|
|
1813
1813
|
"""Install static writes from worker specs. Zeros every write site first.
|
|
1814
1814
|
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
forward (breakable ``add_eager``). ``lens_scope`` is the jlens
|
|
1815
|
+
A site of only plain adds, with no lens scope, fills the static ``delta`` buffer with their
|
|
1816
|
+
sum. Any other op attaches one live ``modify`` for the site's ops, in order, that reads the
|
|
1817
|
+
residual each forward (breakable ``add_eager``). ``lens_scope`` is the jlens skip.
|
|
1818
1818
|
|
|
1819
1819
|
A ``stream`` disqualifies the static buffer too. The buffer is added whole to a ``[tokens,
|
|
1820
1820
|
streams, width]`` activation, so it has no way to say "this stream and not the others"; taking
|
|
@@ -1827,59 +1827,78 @@ def worker_set_static_delta(
|
|
|
1827
1827
|
if static is None:
|
|
1828
1828
|
raise RuntimeError("set_static_delta: this worker has no static wraps")
|
|
1829
1829
|
worker_clear_static_delta(worker)
|
|
1830
|
-
|
|
1831
|
-
|
|
1830
|
+
for site, group in _group_by_site(static, specs).items():
|
|
1831
|
+
assert site.delta is not None
|
|
1832
|
+
constant = _constant_delta(group, site.delta.device, site.delta.dtype) if lens_scope is None else None
|
|
1833
|
+
if constant is not None:
|
|
1834
|
+
site.delta.copy_(constant.expand_as(site.delta))
|
|
1835
|
+
site.delta_set = True
|
|
1836
|
+
continue
|
|
1837
|
+
site.modify = _group_modifier(group, site.delta.device, site.delta.dtype)
|
|
1838
|
+
site.lens_scope = dict(lens_scope) if lens_scope else None
|
|
1839
|
+
|
|
1832
1840
|
|
|
1841
|
+
def _group_by_site(static: StaticState, specs: list[dict]) -> dict[_Site, list[dict]]:
|
|
1842
|
+
"""``specs`` per write site, in order, each op checked against what a static tap can serve."""
|
|
1843
|
+
out: dict[_Site, list[dict]] = {}
|
|
1833
1844
|
for spec in specs:
|
|
1834
1845
|
op = str(spec.get("op", SteerMethod.ADDITIVE))
|
|
1835
1846
|
if op not in STATIC_WRITE_OPS:
|
|
1836
1847
|
raise ValueError(f"static taps cannot serve op={op!r}; supported ops are {sorted(STATIC_WRITE_OPS)}")
|
|
1837
|
-
site = _write_site(static, Address(str(spec
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1848
|
+
site = _write_site(static, Address(str(spec.get("point") or "resid_post"), int(spec["layer"])))
|
|
1849
|
+
out.setdefault(site, []).append(spec)
|
|
1850
|
+
return out
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
def _constant_delta(group: list[dict], device: torch.device, dtype: torch.dtype) -> torch.Tensor | None:
|
|
1854
|
+
"""The ``[1, width]`` sum of ``group``'s deltas when every op is a plain add; else ``None``.
|
|
1855
|
+
|
|
1856
|
+
A constant vector broadcasts over a stream axis and so cannot exclude one: a ``stream`` has to
|
|
1857
|
+
go the modifier way.
|
|
1858
|
+
"""
|
|
1859
|
+
if any(
|
|
1860
|
+
str(s.get("op", SteerMethod.ADDITIVE)) != SteerMethod.ADDITIVE or s.get("stream") is not None for s in group
|
|
1861
|
+
):
|
|
1862
|
+
return None
|
|
1863
|
+
total = sum(torch.tensor(s["vector"], dtype=torch.float32, device=device) * float(s["coeff"]) for s in group)
|
|
1864
|
+
return torch.as_tensor(total).to(dtype=dtype).reshape(1, -1)
|
|
1865
|
+
|
|
1866
|
+
|
|
1867
|
+
def _group_modifier(group: list[dict], device: torch.device, dtype: torch.dtype):
|
|
1868
|
+
"""One ``modify`` for every op in ``group``, steer and lens ops alike, applied in order."""
|
|
1869
|
+
from interp_engine.vllm_capture.lens.intervene import _make_lens_modifier
|
|
1870
|
+
from interp_engine.vllm_capture.steering import _compose_modifiers, _make_steer_modifier
|
|
1871
|
+
|
|
1872
|
+
return _compose_modifiers(
|
|
1873
|
+
[
|
|
1874
|
+
_make_lens_modifier(s, device, dtype)
|
|
1875
|
+
if str(s.get("op", SteerMethod.ADDITIVE)) in _LENS_OPS
|
|
1876
|
+
else _make_steer_modifier(s, device, dtype)
|
|
1877
|
+
for s in group
|
|
1878
|
+
]
|
|
1879
|
+
)
|
|
1851
1880
|
|
|
1852
1881
|
|
|
1853
1882
|
def _compile_write_req(
|
|
1854
|
-
|
|
1883
|
+
group: list[dict],
|
|
1855
1884
|
site: _Site,
|
|
1856
1885
|
*,
|
|
1857
1886
|
skip_positions: tuple[int, ...],
|
|
1858
1887
|
prompt_len: int,
|
|
1859
1888
|
steer_generated: bool,
|
|
1860
1889
|
) -> _WriteReq:
|
|
1861
|
-
op = str(spec.get("op", SteerMethod.ADDITIVE))
|
|
1862
|
-
if op not in STATIC_WRITE_OPS:
|
|
1863
|
-
raise ValueError(f"static taps cannot serve op={op!r}; supported ops are {sorted(STATIC_WRITE_OPS)}")
|
|
1864
1890
|
assert site.delta is not None
|
|
1865
1891
|
device, dtype = site.delta.device, site.delta.dtype
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
if op == SteerMethod.ADDITIVE and spec.get("stream") is None:
|
|
1869
|
-
vec = torch.tensor(spec["vector"], dtype=torch.float32, device=device)
|
|
1870
|
-
vec = (vec * float(spec["coeff"])).to(dtype=dtype).reshape(1, -1)
|
|
1892
|
+
constant = _constant_delta(group, device, dtype)
|
|
1893
|
+
if constant is not None:
|
|
1871
1894
|
return _WriteReq(
|
|
1872
|
-
vector=
|
|
1895
|
+
vector=constant,
|
|
1873
1896
|
skip_positions=skip_positions,
|
|
1874
1897
|
prompt_len=prompt_len,
|
|
1875
1898
|
steer_generated=steer_generated,
|
|
1876
1899
|
)
|
|
1877
|
-
from interp_engine.vllm_capture.lens.intervene import _make_lens_modifier
|
|
1878
|
-
from interp_engine.vllm_capture.steering import _make_steer_modifier
|
|
1879
|
-
|
|
1880
|
-
modify = _make_lens_modifier(spec, device, dtype) if op in _LENS_OPS else _make_steer_modifier(spec, device, dtype)
|
|
1881
1900
|
return _WriteReq(
|
|
1882
|
-
modify=
|
|
1901
|
+
modify=_group_modifier(group, device, dtype),
|
|
1883
1902
|
skip_positions=skip_positions,
|
|
1884
1903
|
prompt_len=prompt_len,
|
|
1885
1904
|
steer_generated=steer_generated,
|
|
@@ -1906,10 +1925,9 @@ def worker_register_static_write(
|
|
|
1906
1925
|
length = int(lens_scope.get("prompt_len") or length)
|
|
1907
1926
|
generated = bool(lens_scope.get("steer_generated", False))
|
|
1908
1927
|
by_site: dict[str, _WriteReq] = {}
|
|
1909
|
-
for
|
|
1910
|
-
site = _write_site(static, Address(str(spec.get("point") or "resid_post"), int(spec["layer"])))
|
|
1928
|
+
for site, group in _group_by_site(static, specs).items():
|
|
1911
1929
|
by_site[format_address(site.address)] = _compile_write_req(
|
|
1912
|
-
|
|
1930
|
+
group, site, skip_positions=skip, prompt_len=length, steer_generated=generated
|
|
1913
1931
|
)
|
|
1914
1932
|
static.write_reqs[req_id] = by_site
|
|
1915
1933
|
static.registered.add(req_id)
|
|
@@ -51,6 +51,32 @@ def _one_stream(modify, stream: int):
|
|
|
51
51
|
return _modify
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def _compose_modifiers(mods: list):
|
|
55
|
+
"""One ``modify`` for every op at one site, applied in order as eager applies a layer's ops.
|
|
56
|
+
|
|
57
|
+
Each op reads the residual the ops before it wrote, and the delta returned is their sum, so a
|
|
58
|
+
site that holds one write slot still carries all of them.
|
|
59
|
+
"""
|
|
60
|
+
if len(mods) == 1:
|
|
61
|
+
return mods[0]
|
|
62
|
+
|
|
63
|
+
def _modify_all(full: torch.Tensor) -> torch.Tensor:
|
|
64
|
+
total = mods[0](full)
|
|
65
|
+
out = full + total
|
|
66
|
+
for modify in mods[1:]:
|
|
67
|
+
delta = modify(out)
|
|
68
|
+
out = out + delta
|
|
69
|
+
total = total + delta
|
|
70
|
+
return total
|
|
71
|
+
|
|
72
|
+
return _modify_all
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _make_steer_modifiers(specs: list[dict], dev, dt):
|
|
76
|
+
""":func:`_compose_modifiers` over ``specs``, which all write one site."""
|
|
77
|
+
return _compose_modifiers([_make_steer_modifier(s, dev, dt) for s in specs])
|
|
78
|
+
|
|
79
|
+
|
|
54
80
|
def _make_steer_modifier(spec: dict, dev, dt):
|
|
55
81
|
"""Return ``modify(full_resid) -> delta`` (the tensor to ADD to the residual).
|
|
56
82
|
|
|
@@ -130,13 +130,13 @@ def test_layer_logits_refuses_an_explicit_transform_on_a_non_eager_model() -> No
|
|
|
130
130
|
layer_logits(NotEager(), [1, 2, 3], {"logit_lens": [0]}, softcap=30.0) # pyright: ignore[reportArgumentType]
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
def
|
|
134
|
-
with pytest.raises(CapabilityUnsupported, match="
|
|
133
|
+
def test_run_with_cache_refuses_position_ids_its_mask_does_not_give_on_a_non_eager_model() -> None:
|
|
134
|
+
with pytest.raises(CapabilityUnsupported, match="position ids"):
|
|
135
135
|
run_with_cache(
|
|
136
136
|
NotEager(), # pyright: ignore[reportArgumentType]
|
|
137
137
|
torch.tensor([[1, 2, 3]]),
|
|
138
138
|
["resid_post.0"],
|
|
139
|
-
|
|
139
|
+
position_ids=torch.tensor([[5, 6, 7]]),
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
|