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.
Files changed (186) hide show
  1. {interp_engine-1.11.2 → interp_engine-1.12.0}/PKG-INFO +1 -1
  2. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/USAGE.md +7 -0
  3. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/__init__.py +2 -0
  4. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/capture.py +144 -19
  5. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/dispatch.py +4 -4
  6. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/requests.py +9 -5
  7. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/static.py +55 -37
  8. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/steering.py +26 -0
  9. {interp_engine-1.11.2 → interp_engine-1.12.0}/pyproject.toml +1 -1
  10. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_capability_refusals.py +3 -3
  11. interp_engine-1.12.0/tests/test_padded_positions.py +174 -0
  12. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_steer_math_parity.py +108 -0
  13. interp_engine-1.12.0/tests/test_steer_ops_per_site_gpu.py +132 -0
  14. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_hyper_connections.py +2 -2
  15. interp_engine-1.12.0/tests/test_vllm_padded_gpu.py +76 -0
  16. {interp_engine-1.11.2 → interp_engine-1.12.0}/.gitignore +0 -0
  17. {interp_engine-1.11.2 → interp_engine-1.12.0}/LICENSE +0 -0
  18. {interp_engine-1.11.2 → interp_engine-1.12.0}/README.md +0 -0
  19. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/README.md +0 -0
  20. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/__init__.py +0 -0
  21. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/bench_spec.py +0 -0
  22. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/cells.py +0 -0
  23. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/probe.py +0 -0
  24. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/probe_lens_stream.py +0 -0
  25. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/publish.py +0 -0
  26. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/report_bench.py +0 -0
  27. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
  28. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
  29. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
  30. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
  31. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/gemma-2-2b__eager.json +0 -0
  32. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
  33. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
  34. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
  35. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
  36. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
  37. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
  38. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
  39. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3-4b__eager.json +0 -0
  40. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
  41. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
  42. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3-4b__vllm.json +0 -0
  43. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
  44. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
  45. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
  46. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
  47. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/results-latest.md +0 -0
  48. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/run_all.sh +0 -0
  49. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/run_bench.py +0 -0
  50. {interp_engine-1.11.2 → interp_engine-1.12.0}/benchmarks/workloads.py +0 -0
  51. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/AGENT_INTEGRATION.md +0 -0
  52. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/ARCHITECTURE_QUIRKS.md +0 -0
  53. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/COMPATIBILITY.md +0 -0
  54. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
  55. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/GRADIENTS.md +0 -0
  56. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/INTERNALS.md +0 -0
  57. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/PERFORMANCE.md +0 -0
  58. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/PORTING.md +0 -0
  59. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/README.md +0 -0
  60. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/SUPPORTED_POINTS.md +0 -0
  61. {interp_engine-1.11.2 → interp_engine-1.12.0}/docs/VALIDATING_A_NEW_MODEL.md +0 -0
  62. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/_loop.py +0 -0
  63. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/address.py +0 -0
  64. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/arch.py +0 -0
  65. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/attn_config.py +0 -0
  66. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/attn_scores.py +0 -0
  67. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/autograd_support.py +0 -0
  68. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/chat_compose.py +0 -0
  69. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/chat_conventions.py +0 -0
  70. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/chat_formatters.py +0 -0
  71. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/cuda_preflight.py +0 -0
  72. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/facts.py +0 -0
  73. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/hooks.py +0 -0
  74. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/lens.py +0 -0
  75. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/load.py +0 -0
  76. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/mappers.py +0 -0
  77. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/memory.py +0 -0
  78. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/model.py +0 -0
  79. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/moe_routing.py +0 -0
  80. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/notebook_stdout.py +0 -0
  81. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/points.py +0 -0
  82. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/protocol.py +0 -0
  83. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/residual_basis.py +0 -0
  84. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/sampling.py +0 -0
  85. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/select.py +0 -0
  86. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/steer.py +0 -0
  87. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/steer_specs.py +0 -0
  88. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/sync.py +0 -0
  89. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/tokenize.py +0 -0
  90. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_backend.py +0 -0
  91. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/__init__.py +0 -0
  92. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_demux.py +0 -0
  93. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_hooks.py +0 -0
  94. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_payload.py +0 -0
  95. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_tp.py +0 -0
  96. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/_tree.py +0 -0
  97. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/attn.py +0 -0
  98. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/capture.py +0 -0
  99. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/graphs.py +0 -0
  100. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/lens/__init__.py +0 -0
  101. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/lens/intervene.py +0 -0
  102. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/lens/readout.py +0 -0
  103. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/lens/unembed.py +0 -0
  104. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/mhc.py +0 -0
  105. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_capture/native.py +0 -0
  106. {interp_engine-1.11.2 → interp_engine-1.12.0}/interp_engine/vllm_plugin.py +0 -0
  107. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/conftest.py +0 -0
  108. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/harness.py +0 -0
  109. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/model_expectations.yaml +0 -0
  110. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/synthetic_families.py +0 -0
  111. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_address.py +0 -0
  112. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_attn_config_tripwire.py +0 -0
  113. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_attn_probs_indexing.py +0 -0
  114. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_attn_scores.py +0 -0
  115. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_attn_z_gqa.py +0 -0
  116. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_autograd_support.py +0 -0
  117. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_bench_workloads.py +0 -0
  118. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_capture_addressing.py +0 -0
  119. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_chat_compose.py +0 -0
  120. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_chat_formatters.py +0 -0
  121. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_chat_templates.py +0 -0
  122. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_core.py +0 -0
  123. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_cuda_preflight.py +0 -0
  124. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_doc_code_fences.py +0 -0
  125. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_eager_autograd.py +0 -0
  126. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_facts.py +0 -0
  127. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_family_points.py +0 -0
  128. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_gated_attn_out.py +0 -0
  129. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_gpu_sizer.py +0 -0
  130. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_head_contributions.py +0 -0
  131. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_hook_call_conventions.py +0 -0
  132. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_injected_system_spans.py +0 -0
  133. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_layer_kinds.py +0 -0
  134. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_load.py +0 -0
  135. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_logit_transform.py +0 -0
  136. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_mappers.py +0 -0
  137. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_memory.py +0 -0
  138. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_mlp_internals.py +0 -0
  139. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_model_expectations.py +0 -0
  140. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_moe.py +0 -0
  141. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_multigpu.py +0 -0
  142. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_multimodal_arch.py +0 -0
  143. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_new_models_gpu.py +0 -0
  144. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_no_chat_template.py +0 -0
  145. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_normalized_hook.py +0 -0
  146. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_notebook_stdout.py +0 -0
  147. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_packaging.py +0 -0
  148. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_parity_gpt2.py +0 -0
  149. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_per_layer_attn_dims.py +0 -0
  150. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_points_registry.py +0 -0
  151. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_protocol.py +0 -0
  152. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_published_benchmarks.py +0 -0
  153. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_qk_norm.py +0 -0
  154. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_qkv_layout.py +0 -0
  155. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_reasoning_spans.py +0 -0
  156. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_release.py +0 -0
  157. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_resid_mid.py +0 -0
  158. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_residual_basis.py +0 -0
  159. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sampling.py +0 -0
  160. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sandwich_norms.py +0 -0
  161. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_select.py +0 -0
  162. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sliding_window_attn.py +0 -0
  163. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_small_models_gpu.py +0 -0
  164. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_static_dsv4_gpu.py +0 -0
  165. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_static_parity_gpu.py +0 -0
  166. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_static_set.py +0 -0
  167. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_static_warmup.py +0 -0
  168. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_steer_context.py +0 -0
  169. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sync_loop.py +0 -0
  170. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_sync_parity.py +0 -0
  171. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_unified_free_functions.py +0 -0
  172. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_unresolved_families.py +0 -0
  173. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_capture_gpu.py +0 -0
  174. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_capture_scales.py +0 -0
  175. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_engine_loop.py +0 -0
  176. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_graph_path.py +0 -0
  177. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_graphs_on_gpu.py +0 -0
  178. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_hook_availability.py +0 -0
  179. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_kv_isolation.py +0 -0
  180. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_new_points.py +0 -0
  181. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_only_families.py +0 -0
  182. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_plugin.py +0 -0
  183. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vllm_wire_grammar.py +0 -0
  184. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_vocabulary_boundary.py +0 -0
  185. {interp_engine-1.11.2 → interp_engine-1.12.0}/tests/test_worker_lens_capture_readout.py +0 -0
  186. {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.11.2
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(model, tokens, points, detach=detach, attention_mask=attention_mask)
180
- return _run_with_cache_eager(model, tokens, points, detach=detach, attention_mask=attention_mask)
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: one prompt through :meth:`InterpModel.capture`, shaped like a Cache.
267
+ """The non-eager arm: each row through :meth:`InterpModel.capture`, shaped like a Cache.
192
268
 
193
- ``attention_mask`` is refused rather than ignored -- a single unpadded sequence needs none,
194
- and a mask that was going to be dropped is worse than one that raises. The batch refusal
195
- lives in :func:`~interp_engine.dispatch.as_token_ids`, for the same reason.
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
- ids = as_token_ids(tokens, model=model, what="run_with_cache")
207
- captured = sync_model(model).capture(
208
- ids,
209
- _normalize_points(points),
210
- steering_spec=None if steering is None else steering.spec,
211
- detach=detach,
212
- )
213
- # Restore the batch axis the eager path keeps, so `cache[point][0]` means the same thing on
214
- # both backends. A view, so this costs nothing.
215
- return Cache(tensors={address: t.unsqueeze(0) for address, t in captured.items()}, output=None)
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=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
- "attention_mask": Capability(
141
- what="a padding mask",
142
- why="this arm forwards one unpadded sequence, so there is nothing for a mask to cover",
143
- instead="omitting it, and issuing one call per prompt",
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 _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.12.0"
4
4
  description = "A fast, standardized, and easy to use interpretability engine."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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 test_run_with_cache_refuses_an_attention_mask_on_a_non_eager_model() -> None:
134
- with pytest.raises(CapabilityUnsupported, match="mask"):
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
- attention_mask=torch.ones(1, 3),
139
+ position_ids=torch.tensor([[5, 6, 7]]),
140
140
  )
141
141
 
142
142