interp-engine 1.11.2__tar.gz → 1.11.3__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.
Files changed (184) hide show
  1. {interp_engine-1.11.2 → interp_engine-1.11.3}/PKG-INFO +1 -1
  2. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/requests.py +9 -5
  3. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/static.py +55 -37
  4. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/steering.py +26 -0
  5. {interp_engine-1.11.2 → interp_engine-1.11.3}/pyproject.toml +1 -1
  6. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_steer_math_parity.py +108 -0
  7. interp_engine-1.11.3/tests/test_steer_ops_per_site_gpu.py +132 -0
  8. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_hyper_connections.py +2 -2
  9. {interp_engine-1.11.2 → interp_engine-1.11.3}/.gitignore +0 -0
  10. {interp_engine-1.11.2 → interp_engine-1.11.3}/LICENSE +0 -0
  11. {interp_engine-1.11.2 → interp_engine-1.11.3}/README.md +0 -0
  12. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/README.md +0 -0
  13. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/__init__.py +0 -0
  14. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/bench_spec.py +0 -0
  15. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/cells.py +0 -0
  16. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/probe.py +0 -0
  17. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/probe_lens_stream.py +0 -0
  18. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/publish.py +0 -0
  19. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/report_bench.py +0 -0
  20. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
  21. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
  22. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
  23. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
  24. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/gemma-2-2b__eager.json +0 -0
  25. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
  26. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
  27. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
  28. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
  29. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
  30. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
  31. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
  32. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/qwen3-4b__eager.json +0 -0
  33. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
  34. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
  35. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/qwen3-4b__vllm.json +0 -0
  36. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
  37. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
  38. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
  39. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
  40. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/results-latest.md +0 -0
  41. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/run_all.sh +0 -0
  42. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/run_bench.py +0 -0
  43. {interp_engine-1.11.2 → interp_engine-1.11.3}/benchmarks/workloads.py +0 -0
  44. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/AGENT_INTEGRATION.md +0 -0
  45. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/ARCHITECTURE_QUIRKS.md +0 -0
  46. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/COMPATIBILITY.md +0 -0
  47. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
  48. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/GRADIENTS.md +0 -0
  49. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/INTERNALS.md +0 -0
  50. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/PERFORMANCE.md +0 -0
  51. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/PORTING.md +0 -0
  52. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/README.md +0 -0
  53. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/SUPPORTED_POINTS.md +0 -0
  54. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/USAGE.md +0 -0
  55. {interp_engine-1.11.2 → interp_engine-1.11.3}/docs/VALIDATING_A_NEW_MODEL.md +0 -0
  56. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/__init__.py +0 -0
  57. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/_loop.py +0 -0
  58. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/address.py +0 -0
  59. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/arch.py +0 -0
  60. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/attn_config.py +0 -0
  61. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/attn_scores.py +0 -0
  62. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/autograd_support.py +0 -0
  63. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/capture.py +0 -0
  64. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/chat_compose.py +0 -0
  65. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/chat_conventions.py +0 -0
  66. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/chat_formatters.py +0 -0
  67. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/cuda_preflight.py +0 -0
  68. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/dispatch.py +0 -0
  69. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/facts.py +0 -0
  70. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/hooks.py +0 -0
  71. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/lens.py +0 -0
  72. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/load.py +0 -0
  73. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/mappers.py +0 -0
  74. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/memory.py +0 -0
  75. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/model.py +0 -0
  76. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/moe_routing.py +0 -0
  77. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/notebook_stdout.py +0 -0
  78. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/points.py +0 -0
  79. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/protocol.py +0 -0
  80. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/residual_basis.py +0 -0
  81. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/sampling.py +0 -0
  82. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/select.py +0 -0
  83. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/steer.py +0 -0
  84. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/steer_specs.py +0 -0
  85. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/sync.py +0 -0
  86. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/tokenize.py +0 -0
  87. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_backend.py +0 -0
  88. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/__init__.py +0 -0
  89. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/_demux.py +0 -0
  90. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/_hooks.py +0 -0
  91. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/_payload.py +0 -0
  92. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/_tp.py +0 -0
  93. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/_tree.py +0 -0
  94. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/attn.py +0 -0
  95. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/capture.py +0 -0
  96. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/graphs.py +0 -0
  97. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/lens/__init__.py +0 -0
  98. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/lens/intervene.py +0 -0
  99. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/lens/readout.py +0 -0
  100. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/lens/unembed.py +0 -0
  101. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/mhc.py +0 -0
  102. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_capture/native.py +0 -0
  103. {interp_engine-1.11.2 → interp_engine-1.11.3}/interp_engine/vllm_plugin.py +0 -0
  104. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/conftest.py +0 -0
  105. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/harness.py +0 -0
  106. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/model_expectations.yaml +0 -0
  107. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/synthetic_families.py +0 -0
  108. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_address.py +0 -0
  109. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_attn_config_tripwire.py +0 -0
  110. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_attn_probs_indexing.py +0 -0
  111. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_attn_scores.py +0 -0
  112. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_attn_z_gqa.py +0 -0
  113. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_autograd_support.py +0 -0
  114. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_bench_workloads.py +0 -0
  115. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_capability_refusals.py +0 -0
  116. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_capture_addressing.py +0 -0
  117. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_chat_compose.py +0 -0
  118. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_chat_formatters.py +0 -0
  119. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_chat_templates.py +0 -0
  120. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_core.py +0 -0
  121. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_cuda_preflight.py +0 -0
  122. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_doc_code_fences.py +0 -0
  123. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_eager_autograd.py +0 -0
  124. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_facts.py +0 -0
  125. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_family_points.py +0 -0
  126. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_gated_attn_out.py +0 -0
  127. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_gpu_sizer.py +0 -0
  128. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_head_contributions.py +0 -0
  129. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_hook_call_conventions.py +0 -0
  130. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_injected_system_spans.py +0 -0
  131. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_layer_kinds.py +0 -0
  132. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_load.py +0 -0
  133. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_logit_transform.py +0 -0
  134. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_mappers.py +0 -0
  135. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_memory.py +0 -0
  136. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_mlp_internals.py +0 -0
  137. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_model_expectations.py +0 -0
  138. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_moe.py +0 -0
  139. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_multigpu.py +0 -0
  140. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_multimodal_arch.py +0 -0
  141. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_new_models_gpu.py +0 -0
  142. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_no_chat_template.py +0 -0
  143. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_normalized_hook.py +0 -0
  144. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_notebook_stdout.py +0 -0
  145. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_packaging.py +0 -0
  146. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_parity_gpt2.py +0 -0
  147. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_per_layer_attn_dims.py +0 -0
  148. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_points_registry.py +0 -0
  149. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_protocol.py +0 -0
  150. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_published_benchmarks.py +0 -0
  151. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_qk_norm.py +0 -0
  152. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_qkv_layout.py +0 -0
  153. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_reasoning_spans.py +0 -0
  154. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_release.py +0 -0
  155. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_resid_mid.py +0 -0
  156. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_residual_basis.py +0 -0
  157. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_sampling.py +0 -0
  158. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_sandwich_norms.py +0 -0
  159. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_select.py +0 -0
  160. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_sliding_window_attn.py +0 -0
  161. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_small_models_gpu.py +0 -0
  162. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_static_dsv4_gpu.py +0 -0
  163. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_static_parity_gpu.py +0 -0
  164. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_static_set.py +0 -0
  165. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_static_warmup.py +0 -0
  166. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_steer_context.py +0 -0
  167. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_sync_loop.py +0 -0
  168. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_sync_parity.py +0 -0
  169. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_unified_free_functions.py +0 -0
  170. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_unresolved_families.py +0 -0
  171. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_capture_gpu.py +0 -0
  172. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_capture_scales.py +0 -0
  173. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_engine_loop.py +0 -0
  174. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_graph_path.py +0 -0
  175. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_graphs_on_gpu.py +0 -0
  176. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_hook_availability.py +0 -0
  177. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_kv_isolation.py +0 -0
  178. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_new_points.py +0 -0
  179. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_only_families.py +0 -0
  180. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_plugin.py +0 -0
  181. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vllm_wire_grammar.py +0 -0
  182. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_vocabulary_boundary.py +0 -0
  183. {interp_engine-1.11.2 → interp_engine-1.11.3}/tests/test_worker_lens_capture_readout.py +0 -0
  184. {interp_engine-1.11.2 → interp_engine-1.11.3}/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.2
3
+ Version: 1.11.3
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
@@ -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 _make_steer_modifier
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
- site = _write_site(worker, s)
684
- mods[site] = (_make_steer_modifier(s, demux.dev, demux.dt), skip_set, int(prompt_len))
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
- site = _write_site(worker, s)
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
- ``op="additive"`` without a lens scope fills the static ``delta`` buffer. Orthogonal,
1816
- projection_cap, and lens ops attach a live ``modify`` that reads the residual each
1817
- forward (breakable ``add_eager``). ``lens_scope`` is the jlens prefill/decode skip.
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
- from interp_engine.vllm_capture.lens.intervene import _make_lens_modifier
1831
- from interp_engine.vllm_capture.steering import _make_steer_modifier
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["point"]), int(spec["layer"])))
1838
- assert site.delta is not None
1839
- if op == SteerMethod.ADDITIVE and lens_scope is None and spec.get("stream") is None:
1840
- vec = torch.tensor(spec["vector"], dtype=torch.float32, device=site.delta.device)
1841
- vec = (vec * float(spec["coeff"])).to(dtype=site.delta.dtype)
1842
- site.delta.copy_(vec.reshape(1, -1).expand_as(site.delta))
1843
- site.delta_set = True
1844
- continue
1845
- device, dtype = site.delta.device, site.delta.dtype
1846
- if op in _LENS_OPS:
1847
- site.modify = _make_lens_modifier(spec, device, dtype)
1848
- else:
1849
- site.modify = _make_steer_modifier(spec, device, dtype)
1850
- site.lens_scope = dict(lens_scope) if lens_scope else None
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
- spec: dict,
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
- # A constant `[1, width]` vector broadcasts over a stream axis and so cannot exclude one; see
1867
- # `worker_set_static_delta` for why a `stream` therefore has to go the modifier way.
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=vec,
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=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 spec in specs:
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
- spec, site, skip_positions=skip, prompt_len=length, steer_generated=generated
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "interp-engine"
3
- version = "1.11.2"
3
+ version = "1.11.3"
4
4
  description = "A fast, standardized, and easy to use interpretability engine."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -320,3 +320,111 @@ def test_the_worker_refuses_the_old_spelling() -> None:
320
320
  _make_steer_modifier(
321
321
  {"op": "add", "vector": _vector().tolist(), "coeff": 1.0}, torch.device("cpu"), torch.float32
322
322
  )
323
+
324
+
325
+ # --- several ops at one site --------------------------------------------------------------------
326
+ #
327
+ # Eager applies a layer's ops in order, each reading the residual the one before it wrote. The
328
+ # worker holds one write per site, so it has to fold the ops into that one write. It used to keep
329
+ # only the last, so two features on one layer steered with the second alone.
330
+
331
+
332
+ def _other_vector(seed: int) -> torch.Tensor:
333
+ return torch.randn(D_MODEL, generator=torch.Generator().manual_seed(seed))
334
+
335
+
336
+ def _three_ops_at_one_layer() -> SteeringSpec:
337
+ return SteeringSpec(
338
+ layers={
339
+ 0: LayerSteeringSpec(
340
+ operations=[
341
+ AddSpec(vector=_other_vector(1), scale=2.0),
342
+ OrthogonalDecompSpec(vector=_other_vector(2), coeff=0.5),
343
+ ProjectionCapSpec(vector=_other_vector(3), min=None, max=0.1),
344
+ ]
345
+ )
346
+ }
347
+ )
348
+
349
+
350
+ def _eager_in_order(spec: SteeringSpec, residual: torch.Tensor) -> torch.Tensor:
351
+ out = residual
352
+ for one in steering_spec_to_eager_specs(spec):
353
+ out = out + steer_delta(one, out, one.vector)
354
+ return out
355
+
356
+
357
+ def test_ops_at_one_site_compose_on_the_worker_as_eager_applies_them() -> None:
358
+ from interp_engine.vllm_capture.steering import _make_steer_modifiers
359
+
360
+ spec, residual = _three_ops_at_one_layer(), _residual()
361
+ modify = _make_steer_modifiers(steering_spec_to_worker_specs(spec), residual.device, residual.dtype)
362
+ torch.testing.assert_close(residual + modify(residual), _eager_in_order(spec, residual))
363
+
364
+
365
+ def test_one_op_at_a_site_is_its_own_modifier() -> None:
366
+ from interp_engine.vllm_capture.steering import _make_steer_modifiers
367
+
368
+ spec = {"op": "orthogonal", "vector": _vector().tolist(), "coeff": 0.5}
369
+ residual = _residual()
370
+ torch.testing.assert_close(
371
+ _make_steer_modifiers([spec], residual.device, residual.dtype)(residual), _worker_delta(spec, residual)
372
+ )
373
+
374
+
375
+ def test_plain_adds_at_a_static_site_are_summed_into_one_constant() -> None:
376
+ from interp_engine.vllm_capture.static import _constant_delta
377
+
378
+ a, b = _other_vector(1), _other_vector(2)
379
+ group = [
380
+ {"op": "additive", "vector": a.tolist(), "coeff": 2.0},
381
+ {"op": "additive", "vector": b.tolist(), "coeff": -1.0},
382
+ ]
383
+ got = _constant_delta(group, torch.device("cpu"), torch.float32)
384
+ assert got is not None
385
+ torch.testing.assert_close(got[0], a * 2.0 - b)
386
+
387
+
388
+ @pytest.mark.parametrize(
389
+ "extra",
390
+ [
391
+ {"op": "orthogonal", "vector": [1.0] * D_MODEL, "coeff": 0.5},
392
+ {"op": "additive", "vector": [1.0] * D_MODEL, "coeff": 1.0, "stream": 0},
393
+ ],
394
+ )
395
+ def test_a_static_site_with_any_other_op_or_a_stream_needs_the_modifier(extra: dict) -> None:
396
+ from interp_engine.vllm_capture.static import _constant_delta
397
+
398
+ plain = {"op": "additive", "vector": [1.0] * D_MODEL, "coeff": 1.0}
399
+ assert _constant_delta([plain, extra], torch.device("cpu"), torch.float32) is None
400
+
401
+
402
+ def _static_worker(rows: int):
403
+ """A worker with one static write site at ``resid_post.0``, and nothing else."""
404
+ from types import SimpleNamespace
405
+
406
+ from interp_engine.address import Address
407
+ from interp_engine.vllm_capture.static import StaticState, _Site
408
+
409
+ site = _Site(Address("resid_post", 0), delta=torch.zeros(rows, D_MODEL))
410
+ return SimpleNamespace(_ie_static=StaticState(writes={"resid_post.0": site}), model_runner=None), site
411
+
412
+
413
+ @pytest.mark.parametrize("static_path", ["per_request", "global"])
414
+ def test_ops_at_one_static_site_compose_as_eager_applies_them(static_path: str) -> None:
415
+ from interp_engine.vllm_capture.static import (
416
+ _apply_write,
417
+ worker_register_static_write,
418
+ worker_set_static_delta,
419
+ )
420
+
421
+ spec, residual = _three_ops_at_one_layer(), _residual()
422
+ specs = [{**s, "point": "resid_post"} for s in steering_spec_to_worker_specs(spec)]
423
+ worker, site = _static_worker(residual.shape[0])
424
+ if static_path == "per_request":
425
+ worker_register_static_write(worker, "r", specs)
426
+ else:
427
+ worker_set_static_delta(worker, specs)
428
+ hidden = residual.clone()
429
+ _apply_write(hidden, None, site, residual.shape[0], fused=False, worker=worker)
430
+ torch.testing.assert_close(hidden, _eager_in_order(spec, residual))
@@ -0,0 +1,132 @@
1
+ """Several steer ops at one site: vLLM against eager, hooked and static.
2
+
3
+ Eager applies a layer's ops in order. The vLLM worker holds one write per site, and used to keep
4
+ only the last op there, so two features on one layer steered with the second alone. Hooked and
5
+ static vLLM agreed with each other, so a hooked-vs-static parity check could not see it: the
6
+ reference here is eager.
7
+
8
+ Two vLLM engines cannot share this process (static sets ``VLLM_USE_BREAKABLE_CUDAGRAPH``
9
+ process-wide), so hooked runs first, then shuts down, then static.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import gc
16
+
17
+ import pytest
18
+ import torch
19
+ from harness import require_vllm
20
+
21
+ from interp_engine.address import Address
22
+
23
+ require_vllm()
24
+
25
+ pytestmark = [
26
+ pytest.mark.gpu,
27
+ pytest.mark.skipif(not torch.cuda.is_available(), reason="the vLLM backend initializes on CUDA"),
28
+ ]
29
+
30
+ MODEL = "openai-community/gpt2"
31
+ PROMPT = "The capital of France is Paris, and the capital of Germany is"
32
+ MID = Address("resid_post", 6)
33
+ DOWN = Address("resid_post", 9)
34
+ COSINE_MIN = 0.999
35
+ LOAD_KW = {"dtype": "float32", "max_model_len": 512, "gpu_memory_utilization": 0.2}
36
+
37
+
38
+ def _vector(width: int, seed: int, norm: float) -> torch.Tensor:
39
+ v = torch.randn(width, generator=torch.Generator().manual_seed(seed))
40
+ return v / v.norm() * norm
41
+
42
+
43
+ def _specs(width: int) -> dict:
44
+ from interp_engine.steer_specs import (
45
+ AddSpec,
46
+ LayerSteeringSpec,
47
+ OrthogonalDecompSpec,
48
+ ProjectionCapSpec,
49
+ SteeringSpec,
50
+ )
51
+
52
+ def at_mid(*ops) -> SteeringSpec:
53
+ return SteeringSpec(layers={int(MID.layer): LayerSteeringSpec(operations=list(ops))}, point="resid_post")
54
+
55
+ a, b, c = _vector(width, 1, 60.0), _vector(width, 2, 60.0), _vector(width, 3, 1.0)
56
+ return {
57
+ # All plain adds: the static constant path.
58
+ "adds": at_mid(AddSpec(vector=a, scale=1.0), AddSpec(vector=b, scale=1.0)),
59
+ # An op that reads the residual: the static modifier path.
60
+ "mixed": at_mid(
61
+ AddSpec(vector=a, scale=1.0),
62
+ OrthogonalDecompSpec(vector=c, coeff=0.0),
63
+ ProjectionCapSpec(vector=b, min=None, max=-5.0),
64
+ ),
65
+ "last_add_only": at_mid(AddSpec(vector=b, scale=1.0)),
66
+ }
67
+
68
+
69
+ def _cpu(captures: dict[Address, torch.Tensor]) -> torch.Tensor:
70
+ return captures[DOWN].detach().float().cpu()
71
+
72
+
73
+ def _min_cosine(a: torch.Tensor, b: torch.Tensor) -> float:
74
+ assert a.shape == b.shape, f"{tuple(a.shape)} vs {tuple(b.shape)}"
75
+ return float(torch.nn.functional.cosine_similarity(a, b, dim=-1).min())
76
+
77
+
78
+ @pytest.fixture(scope="module")
79
+ def loop():
80
+ """One event loop for the module -- AsyncLLM dies if asyncio.run closes the loop."""
81
+ made = asyncio.new_event_loop()
82
+ asyncio.set_event_loop(made)
83
+ yield made
84
+ asyncio.set_event_loop(None)
85
+ made.close()
86
+
87
+
88
+ @pytest.fixture(scope="module")
89
+ def runs(loop):
90
+ from transformers import AutoTokenizer
91
+
92
+ from interp_engine import load_model
93
+
94
+ tokens = list(AutoTokenizer.from_pretrained(MODEL)(PROMPT)["input_ids"])
95
+
96
+ eager = load_model(MODEL, backend="eager", dtype="float32", device="cuda")
97
+ specs = _specs(int(eager.d_model))
98
+ want = {k: _cpu(loop.run_until_complete(eager.capture(tokens, [DOWN], steering_spec=s))) for k, s in specs.items()}
99
+ eager = None
100
+ gc.collect()
101
+ torch.cuda.empty_cache()
102
+
103
+ async def _vllm(model) -> dict:
104
+ return {k: _cpu(await model.capture(tokens, [DOWN], steering_spec=specs[k])) for k in ("adds", "mixed")}
105
+
106
+ hooked = load_model(MODEL, backend="vllm", **LOAD_KW)
107
+ loop.run_until_complete(hooked.warmup())
108
+ hooked_out = loop.run_until_complete(_vllm(hooked))
109
+ loop.run_until_complete(hooked.shutdown())
110
+ hooked = None
111
+ gc.collect()
112
+ torch.cuda.empty_cache()
113
+
114
+ static = load_model(MODEL, backend="vllm-static", static_points=[DOWN], static_writes=[MID], **LOAD_KW)
115
+ loop.run_until_complete(static.warmup())
116
+ try:
117
+ static_out = loop.run_until_complete(_vllm(static))
118
+ finally:
119
+ loop.run_until_complete(static.shutdown())
120
+ return {"eager": want, "hooked": hooked_out, "static": static_out}
121
+
122
+
123
+ def test_the_reference_would_see_a_dropped_op(runs) -> None:
124
+ """Keeping only the last op moves the downstream residual well past the tolerance."""
125
+ assert _min_cosine(runs["eager"]["adds"], runs["eager"]["last_add_only"]) < 0.99
126
+
127
+
128
+ @pytest.mark.parametrize("backend", ["hooked", "static"])
129
+ @pytest.mark.parametrize("spec", ["adds", "mixed"])
130
+ def test_every_op_at_a_site_applies_as_on_eager(runs, backend: str, spec: str) -> None:
131
+ worst = _min_cosine(runs[backend][spec], runs["eager"][spec])
132
+ assert worst >= COSINE_MIN, f"{backend} {spec}: min cosine {worst:.6f} against eager"
@@ -1385,10 +1385,10 @@ def test_a_static_additive_steer_confined_to_one_stream_does_not_take_the_consta
1385
1385
 
1386
1386
  site = _Site(Address("resid_streams", 0), delta=torch.zeros(8, HC_MULT, D_MODEL))
1387
1387
  spec = {"op": "additive", "vector": [1.0] * D_MODEL, "coeff": 1.0}
1388
- plain = _compile_write_req(spec, site, skip_positions=(), prompt_len=0, steer_generated=True)
1388
+ plain = _compile_write_req([spec], site, skip_positions=(), prompt_len=0, steer_generated=True)
1389
1389
  assert plain.vector is not None and plain.modify is None, "no stream is still the constant path"
1390
1390
 
1391
- scoped = _compile_write_req({**spec, "stream": 2}, site, skip_positions=(), prompt_len=0, steer_generated=True)
1391
+ scoped = _compile_write_req([{**spec, "stream": 2}], site, skip_positions=(), prompt_len=0, steer_generated=True)
1392
1392
  assert scoped.vector is None and scoped.modify is not None
1393
1393
  delta = scoped.modify(torch.zeros(TOKENS, HC_MULT, D_MODEL))
1394
1394
  torch.testing.assert_close(delta[:, 2, :], torch.ones(TOKENS, D_MODEL))
File without changes
File without changes