interp-engine 1.2.6__tar.gz → 1.2.7__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 (172) hide show
  1. {interp_engine-1.2.6 → interp_engine-1.2.7}/PKG-INFO +1 -1
  2. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/freeze.py +39 -4
  3. {interp_engine-1.2.6 → interp_engine-1.2.7}/pyproject.toml +1 -1
  4. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_freeze_set.py +65 -5
  5. {interp_engine-1.2.6 → interp_engine-1.2.7}/.gitignore +0 -0
  6. {interp_engine-1.2.6 → interp_engine-1.2.7}/LICENSE +0 -0
  7. {interp_engine-1.2.6 → interp_engine-1.2.7}/README.md +0 -0
  8. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/README.md +0 -0
  9. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/__init__.py +0 -0
  10. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/bench_spec.py +0 -0
  11. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/cells.py +0 -0
  12. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/probe.py +0 -0
  13. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/publish.py +0 -0
  14. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/report_bench.py +0 -0
  15. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
  16. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
  17. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark-cudagraph.json +0 -0
  18. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark.json +0 -0
  19. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/deepseek-v4-flash-0731__vllm-freeze.json +0 -0
  20. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
  21. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/gemma-2-2b__eager.json +0 -0
  22. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
  23. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/gemma-2-2b__vllm-freeze.json +0 -0
  24. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
  25. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
  26. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
  27. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/llama-3.1-8b__vllm-freeze.json +0 -0
  28. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
  29. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/qwen3-4b__eager.json +0 -0
  30. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
  31. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/qwen3-4b__vllm-freeze.json +0 -0
  32. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/qwen3-4b__vllm.json +0 -0
  33. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
  34. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
  35. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/qwen3.8-27b__vllm-freeze.json +0 -0
  36. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
  37. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/results-latest.md +0 -0
  38. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/run_all.sh +0 -0
  39. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/run_bench.py +0 -0
  40. {interp_engine-1.2.6 → interp_engine-1.2.7}/benchmarks/workloads.py +0 -0
  41. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/AGENT_INTEGRATION.md +0 -0
  42. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/ARCHITECTURE_QUIRKS.md +0 -0
  43. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/COMPATIBILITY.md +0 -0
  44. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
  45. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/GRADIENTS.md +0 -0
  46. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/INTERNALS.md +0 -0
  47. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/PERFORMANCE.md +0 -0
  48. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/PORTING.md +0 -0
  49. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/README.md +0 -0
  50. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/SUPPORTED_POINTS.md +0 -0
  51. {interp_engine-1.2.6 → interp_engine-1.2.7}/docs/USAGE.md +0 -0
  52. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/__init__.py +0 -0
  53. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/_loop.py +0 -0
  54. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/address.py +0 -0
  55. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/arch.py +0 -0
  56. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/attn_config.py +0 -0
  57. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/attn_scores.py +0 -0
  58. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/autograd_support.py +0 -0
  59. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/capture.py +0 -0
  60. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/chat_compose.py +0 -0
  61. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/chat_conventions.py +0 -0
  62. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/chat_formatters.py +0 -0
  63. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/cuda_preflight.py +0 -0
  64. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/dispatch.py +0 -0
  65. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/facts.py +0 -0
  66. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/hooks.py +0 -0
  67. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/lens.py +0 -0
  68. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/load.py +0 -0
  69. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/mappers.py +0 -0
  70. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/model.py +0 -0
  71. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/moe_routing.py +0 -0
  72. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/points.py +0 -0
  73. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/protocol.py +0 -0
  74. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/residual_basis.py +0 -0
  75. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/select.py +0 -0
  76. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/steer.py +0 -0
  77. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/steer_specs.py +0 -0
  78. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/sync.py +0 -0
  79. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/tokenize.py +0 -0
  80. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_backend.py +0 -0
  81. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/__init__.py +0 -0
  82. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/_demux.py +0 -0
  83. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/_hooks.py +0 -0
  84. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/_payload.py +0 -0
  85. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/_tree.py +0 -0
  86. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/attn.py +0 -0
  87. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/capture.py +0 -0
  88. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/graphs.py +0 -0
  89. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/lens/__init__.py +0 -0
  90. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/lens/intervene.py +0 -0
  91. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/lens/readout.py +0 -0
  92. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/lens/unembed.py +0 -0
  93. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/mhc.py +0 -0
  94. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/native.py +0 -0
  95. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/requests.py +0 -0
  96. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_capture/steering.py +0 -0
  97. {interp_engine-1.2.6 → interp_engine-1.2.7}/interp_engine/vllm_plugin.py +0 -0
  98. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/conftest.py +0 -0
  99. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/harness.py +0 -0
  100. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/model_expectations.yaml +0 -0
  101. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/synthetic_families.py +0 -0
  102. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_address.py +0 -0
  103. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_attn_config_tripwire.py +0 -0
  104. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_attn_probs_indexing.py +0 -0
  105. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_attn_scores.py +0 -0
  106. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_attn_z_gqa.py +0 -0
  107. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_autograd_support.py +0 -0
  108. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_bench_workloads.py +0 -0
  109. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_capability_refusals.py +0 -0
  110. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_capture_addressing.py +0 -0
  111. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_chat_compose.py +0 -0
  112. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_chat_formatters.py +0 -0
  113. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_chat_templates.py +0 -0
  114. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_core.py +0 -0
  115. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_cuda_preflight.py +0 -0
  116. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_doc_code_fences.py +0 -0
  117. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_eager_autograd.py +0 -0
  118. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_facts.py +0 -0
  119. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_family_points.py +0 -0
  120. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_freeze_dsv4_gpu.py +0 -0
  121. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_freeze_parity_gpu.py +0 -0
  122. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_freeze_warmup.py +0 -0
  123. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_gated_attn_out.py +0 -0
  124. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_head_contributions.py +0 -0
  125. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_hook_call_conventions.py +0 -0
  126. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_layer_kinds.py +0 -0
  127. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_load.py +0 -0
  128. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_logit_transform.py +0 -0
  129. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_mappers.py +0 -0
  130. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_mlp_internals.py +0 -0
  131. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_model_expectations.py +0 -0
  132. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_moe.py +0 -0
  133. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_multimodal_arch.py +0 -0
  134. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_new_models_gpu.py +0 -0
  135. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_no_chat_template.py +0 -0
  136. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_normalized_hook.py +0 -0
  137. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_packaging.py +0 -0
  138. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_parity_gpt2.py +0 -0
  139. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_per_layer_attn_dims.py +0 -0
  140. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_points_registry.py +0 -0
  141. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_protocol.py +0 -0
  142. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_published_benchmarks.py +0 -0
  143. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_qk_norm.py +0 -0
  144. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_qkv_layout.py +0 -0
  145. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_reasoning_spans.py +0 -0
  146. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_release.py +0 -0
  147. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_resid_mid.py +0 -0
  148. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_residual_basis.py +0 -0
  149. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_sandwich_norms.py +0 -0
  150. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_select.py +0 -0
  151. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_sliding_window_attn.py +0 -0
  152. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_small_models_gpu.py +0 -0
  153. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_steer_context.py +0 -0
  154. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_steer_math_parity.py +0 -0
  155. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_sync_loop.py +0 -0
  156. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_sync_parity.py +0 -0
  157. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_unified_free_functions.py +0 -0
  158. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_unresolved_families.py +0 -0
  159. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_capture_gpu.py +0 -0
  160. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_capture_scales.py +0 -0
  161. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_graph_path.py +0 -0
  162. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_graphs_on_gpu.py +0 -0
  163. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_hook_availability.py +0 -0
  164. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_hyper_connections.py +0 -0
  165. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_kv_isolation.py +0 -0
  166. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_new_points.py +0 -0
  167. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_only_families.py +0 -0
  168. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_plugin.py +0 -0
  169. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vllm_wire_grammar.py +0 -0
  170. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_vocabulary_boundary.py +0 -0
  171. {interp_engine-1.2.6 → interp_engine-1.2.7}/tests/test_worker_lens_capture_readout.py +0 -0
  172. {interp_engine-1.2.6 → interp_engine-1.2.7}/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.2.6
3
+ Version: 1.2.7
4
4
  Summary: A fast, standardized interpretability engine that supports most modern models and architectures. Powers Neuronpedia.
5
5
  Project-URL: Homepage, https://github.com/decoderesearch/interp-engine
6
6
  Project-URL: Repository, https://github.com/decoderesearch/interp-engine
@@ -252,8 +252,24 @@ def resolve_freeze_points(
252
252
  ``graph_replay`` is False only when freeze is omitted and ``enforce_eager`` is not False
253
253
  (today's hooked vLLM). ``freeze_points="auto"`` or ``enforce_eager=False`` with no list
254
254
  freezes ``resid_post`` at every layer on a conventional trunk, and ``resid_streams`` at
255
- every layer on a hyper-connection trunk.
255
+ every layer on a hyper-connection trunk -- to read AND to write.
256
+
257
+ Auto covers the write because the two are one decision for the caller who asks for it. ``auto``
258
+ says "serve the residual endpoints on this engine", and read taps alone serve half of them: a
259
+ lens read-out works, and every steer, ablation and swap derived from that read-out is refused
260
+ for want of a site at the very address already being tapped. Nothing in the caller's vocabulary
261
+ distinguished the two halves, either -- ``freeze_writes`` is a list of addresses, so asking for
262
+ the write meant restating every layer of a set ``auto`` had just built.
263
+
264
+ An explicit list is left alone, since a caller who named the points named what they wanted. So
265
+ is ``freeze_writes=[]``, which is how to ask for the reads WITHOUT the write buffers: those
266
+ buffers are per layer and per token, and they are what steps ``max_num_batched_tokens`` down
267
+ when the ladder in :func:`fit_max_num_batched_tokens` cannot fit them.
256
268
  """
269
+ # `None` and `[]` mean different things here and nowhere else in this signature: the first is
270
+ # "say nothing about writes", which auto fills in, and the second is "no writes", which it must
271
+ # not. Read before the comprehension below, which cannot tell them apart.
272
+ writes_declared = freeze_writes is not None
257
273
  writes = [to_address(a) for a in (freeze_writes or ())]
258
274
  graph = freeze_points is not None or bool(writes) or enforce_eager is False
259
275
  if not graph:
@@ -264,14 +280,21 @@ def resolve_freeze_points(
264
280
  "Do not pass enforce_eager=True with a freeze set; omit freeze_points for hooked vLLM."
265
281
  )
266
282
 
283
+ auto = False
267
284
  if freeze_points is None:
268
- reads: list[Address] = [] if writes else _auto_reads(n_layers, n_streams)
285
+ # Writes without a read list stay writes-only: naming a write site is already explicit
286
+ # about what this engine is for, and auto-reading every layer beside it is not implied.
287
+ auto = not writes
288
+ reads: list[Address] = _auto_reads(n_layers, n_streams) if auto else []
269
289
  elif isinstance(freeze_points, str):
270
290
  if freeze_points != "auto":
271
291
  raise ValueError(f"freeze_points must be 'auto', a list of addresses, or []; got {freeze_points!r}")
272
292
  reads = _auto_reads(n_layers, n_streams)
293
+ auto = True
273
294
  else:
274
295
  reads = [to_address(a) for a in freeze_points]
296
+ if auto and not writes_declared:
297
+ writes = list(reads)
275
298
 
276
299
  for address in (*reads, *writes):
277
300
  reason = freeze_unsupported_reason(address.name) or multi_stream_refusal_reason(address.name, n_streams)
@@ -990,7 +1013,16 @@ def worker_install_freeze(worker: object) -> None:
990
1013
 
991
1014
 
992
1015
  def _install_mhc_freeze(worker: object, mhc_sites: Sequence[_Site]) -> None:
993
- from interp_engine.vllm_capture.mhc import mhc_taps, require_available
1016
+ """Wrap the mHC kernels for every stacked freeze site, refusing one this model cannot serve.
1017
+
1018
+ A site that carries a ``delta`` is asked the STEER question rather than the capture one, because
1019
+ they differ by exactly the thing a write depends on: ``resid_streams`` is written by running the
1020
+ fused kernel's second half again on the edited stack, so that half has to be forwardable
1021
+ (:func:`~interp_engine.vllm_capture.mhc.pre_rerun_gap`). Asked here because freeze installs in
1022
+ ``Worker.load_model`` -- a gap found later surfaces mid-forward, on an engine that has already
1023
+ started and reported itself healthy.
1024
+ """
1025
+ from interp_engine.vllm_capture.mhc import mhc_taps, require_available, require_steerable
994
1026
 
995
1027
  model = _worker_model(worker)
996
1028
  taps = mhc_taps(worker)
@@ -999,7 +1031,10 @@ def _install_mhc_freeze(worker: object, mhc_sites: Sequence[_Site]) -> None:
999
1031
  if id(site) in seen:
1000
1032
  continue
1001
1033
  seen.add(id(site))
1002
- require_available(model, site.address.name, site.address.layer)
1034
+ if site.delta is not None:
1035
+ require_steerable(model, site.address.name, site.address.layer)
1036
+ else:
1037
+ require_available(model, site.address.name, site.address.layer)
1003
1038
  taps.add(site.address, _freeze_mhc_recorder(worker, site))
1004
1039
 
1005
1040
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "interp-engine"
3
- version = "1.2.6"
3
+ version = "1.2.7"
4
4
  description = "A fast, standardized interpretability engine that supports most modern models and architectures. Powers Neuronpedia."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -8,6 +8,7 @@ import pytest
8
8
  import torch
9
9
 
10
10
  from interp_engine.address import Address
11
+ from interp_engine.points import steer_refusal_reason
11
12
  from interp_engine.vllm_capture.freeze import (
12
13
  BREAKABLE_ENV,
13
14
  FREEZE_SKIP_ABSENT_ENV,
@@ -59,21 +60,22 @@ def test_empty_list_is_graphs_with_no_taps():
59
60
  def test_auto_is_resid_post_at_every_layer():
60
61
  reads, writes, graph = resolve_freeze_points("auto", n_layers=3, n_streams=1)
61
62
  assert graph is True
62
- assert writes == []
63
63
  assert reads == [Address("resid_post", 0), Address("resid_post", 1), Address("resid_post", 2)]
64
+ assert writes == reads
64
65
 
65
66
 
66
67
  def test_enforce_eager_false_with_no_list_is_auto():
67
- reads, _, graph = resolve_freeze_points(None, n_layers=2, n_streams=1, enforce_eager=False)
68
+ reads, writes, graph = resolve_freeze_points(None, n_layers=2, n_streams=1, enforce_eager=False)
68
69
  assert graph is True
69
70
  assert reads == [Address("resid_post", 0), Address("resid_post", 1)]
71
+ assert writes == reads, "the same set either way in; auto is auto however it was reached"
70
72
 
71
73
 
72
74
  def test_auto_is_resid_streams_on_a_hyper_connection_trunk():
73
75
  reads, writes, graph = resolve_freeze_points("auto", n_layers=3, n_streams=4)
74
76
  assert graph is True
75
- assert writes == []
76
77
  assert reads == [Address("resid_streams", 0), Address("resid_streams", 1), Address("resid_streams", 2)]
78
+ assert writes == reads
77
79
 
78
80
 
79
81
  def test_resid_streams_can_be_frozen():
@@ -105,11 +107,69 @@ def test_explicit_mlp_out_is_allowed_on_a_hyper_connection_trunk():
105
107
  assert reads == [Address("mlp_out", 21)]
106
108
 
107
109
 
108
- def test_writes_are_not_implied_by_auto():
109
- _, writes, _ = resolve_freeze_points("auto", n_layers=2, n_streams=1)
110
+ def test_auto_writes_where_it_reads_so_a_read_out_can_be_intervened_on():
111
+ """The point of the change: an auto engine can steer at the addresses it captures.
112
+
113
+ Reads alone made the two halves of the residual endpoints disagree -- a lens read at layer 7
114
+ came back, and the steer, ablation or swap that read implies was refused for want of a site at
115
+ ``resid_post.7``, which was already tapped a few bytes away.
116
+ """
117
+ reads, writes, _ = resolve_freeze_points("auto", n_layers=2, n_streams=1)
118
+ assert writes == reads
119
+ assert writes == [Address("resid_post", 0), Address("resid_post", 1)]
120
+
121
+
122
+ def test_an_explicit_read_list_still_implies_no_writes():
123
+ """Auto is a default, not a rewrite rule. A caller who named the points named all of them."""
124
+ reads, writes, _ = resolve_freeze_points([Address("resid_post", 1)], n_layers=4, n_streams=1)
125
+ assert reads == [Address("resid_post", 1)]
110
126
  assert writes == []
111
127
 
112
128
 
129
+ def test_an_empty_write_list_asks_auto_for_the_reads_without_the_write_buffers():
130
+ """``[]`` and None have to part company here, and nowhere else in this signature.
131
+
132
+ This is the opt-out for the memory: write buffers are per layer and per token, so on a pod
133
+ where they would step ``max_num_batched_tokens`` down, a caller who only ever reads can say so.
134
+ """
135
+ reads, writes, graph = resolve_freeze_points("auto", n_layers=2, n_streams=1, freeze_writes=[])
136
+ assert graph is True
137
+ assert reads == [Address("resid_post", 0), Address("resid_post", 1)]
138
+ assert writes == []
139
+
140
+
141
+ def test_an_empty_write_list_alone_is_still_not_a_reason_to_capture_graphs():
142
+ """Distinguishing ``[]`` from None must not turn ``freeze_writes=[]`` into a freeze request."""
143
+ reads, writes, graph = resolve_freeze_points(None, n_layers=2, n_streams=1, freeze_writes=[])
144
+ assert (reads, writes, graph) == ([], [], False)
145
+
146
+
147
+ def test_a_named_write_is_not_joined_by_reads_it_did_not_ask_for():
148
+ reads, writes, graph = resolve_freeze_points(
149
+ None, n_layers=4, n_streams=1, freeze_writes=[Address("resid_post", 1)], enforce_eager=False
150
+ )
151
+ assert graph is True
152
+ assert reads == []
153
+ assert writes == [Address("resid_post", 1)]
154
+
155
+
156
+ def test_a_generation_only_engine_gains_neither_half():
157
+ """``[]`` is graphs with no taps at all, and the auto write must not creep into it."""
158
+ reads, writes, graph = resolve_freeze_points([], n_layers=12, n_streams=1)
159
+ assert (reads, writes, graph) == ([], [], True)
160
+
161
+
162
+ def test_the_auto_writes_on_a_stacked_trunk_are_ones_the_engine_agrees_are_writable():
163
+ """Auto now hands ``resid_streams`` to the write validation below, which used to see only reads.
164
+
165
+ A write set built by default has to clear the same bar as one a caller typed, or DeepSeek-V4
166
+ would refuse to load with `cannot freeze-write` from a set nobody asked for.
167
+ """
168
+ _, writes, _ = resolve_freeze_points("auto", n_layers=3, n_streams=4)
169
+ assert {a.name for a in writes} == {"resid_streams"}
170
+ assert all(steer_refusal_reason(a.name) is None for a in writes)
171
+
172
+
113
173
  def test_freeze_writes_without_reads_is_still_graph_mode():
114
174
  reads, writes, graph = resolve_freeze_points(
115
175
  None, n_layers=4, n_streams=1, freeze_writes=[Address("resid_post", 1)]
File without changes
File without changes
File without changes