interp-engine 1.12.0__tar.gz → 1.13.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 (218) hide show
  1. {interp_engine-1.12.0 → interp_engine-1.13.0}/PKG-INFO +23 -18
  2. {interp_engine-1.12.0 → interp_engine-1.13.0}/README.md +17 -17
  3. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/bench_spec.py +4 -0
  4. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/publish.py +38 -8
  5. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/report_bench.py +4 -2
  6. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results-latest.md +7 -7
  7. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/AGENT_INTEGRATION.md +44 -9
  8. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/ARCHITECTURE_QUIRKS.md +6 -5
  9. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/ENGINE_HOOK_MAPPINGS.md +10 -9
  10. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/GRADIENTS.md +1 -1
  11. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/INTERNALS.md +7 -2
  12. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/PERFORMANCE.md +8 -6
  13. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/USAGE.md +53 -8
  14. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/__init__.py +27 -1
  15. interp_engine-1.13.0/interp_engine/_conformance.py +21 -0
  16. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/_loop.py +51 -4
  17. interp_engine-1.13.0/interp_engine/api.py +267 -0
  18. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/arch.py +14 -10
  19. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/capture.py +69 -56
  20. interp_engine-1.13.0/interp_engine/describe.py +64 -0
  21. interp_engine-1.13.0/interp_engine/directions.py +79 -0
  22. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/dispatch.py +22 -8
  23. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/facts.py +67 -0
  24. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/hooks.py +1 -1
  25. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/lens.py +4 -4
  26. interp_engine-1.13.0/interp_engine/lens_stream.py +488 -0
  27. interp_engine-1.13.0/interp_engine/lens_topk.py +59 -0
  28. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/load.py +3 -2
  29. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/mappers.py +1 -1
  30. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/memory.py +47 -3
  31. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/model.py +414 -20
  32. interp_engine-1.13.0/interp_engine/oracle.py +614 -0
  33. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/protocol.py +297 -10
  34. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/select.py +49 -19
  35. interp_engine-1.13.0/interp_engine/server/__init__.py +26 -0
  36. interp_engine-1.13.0/interp_engine/server/__main__.py +101 -0
  37. interp_engine-1.13.0/interp_engine/server/app.py +473 -0
  38. interp_engine-1.13.0/interp_engine/server/config.py +74 -0
  39. interp_engine-1.13.0/interp_engine/server/lens.py +87 -0
  40. interp_engine-1.13.0/interp_engine/server/runner.py +105 -0
  41. interp_engine-1.13.0/interp_engine/server/schemas.py +324 -0
  42. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/steer.py +325 -47
  43. interp_engine-1.13.0/interp_engine/steer_specs.py +318 -0
  44. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/sync.py +137 -3
  45. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/tokenize.py +32 -21
  46. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_backend.py +827 -331
  47. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/__init__.py +4 -2
  48. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/_demux.py +1 -7
  49. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/_tp.py +16 -0
  50. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/_tree.py +42 -0
  51. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/lens/__init__.py +7 -5
  52. interp_engine-1.13.0/interp_engine/vllm_capture/lens/intervene.py +111 -0
  53. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/lens/readout.py +80 -93
  54. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/lens/unembed.py +17 -0
  55. interp_engine-1.13.0/interp_engine/vllm_capture/project.py +37 -0
  56. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/requests.py +36 -76
  57. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/static.py +147 -58
  58. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/steering.py +47 -7
  59. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_plugin.py +23 -14
  60. {interp_engine-1.12.0 → interp_engine-1.13.0}/pyproject.toml +12 -1
  61. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/conftest.py +3 -0
  62. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/harness.py +2 -2
  63. interp_engine-1.13.0/tests/test_api_fixtures.py +112 -0
  64. interp_engine-1.13.0/tests/test_api_generated.py +146 -0
  65. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_attn_probs_indexing.py +5 -5
  66. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_attn_scores.py +11 -11
  67. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_attn_z_gqa.py +2 -2
  68. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_capability_refusals.py +6 -6
  69. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_capture_addressing.py +8 -8
  70. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_core.py +51 -6
  71. interp_engine-1.13.0/tests/test_describe.py +80 -0
  72. interp_engine-1.13.0/tests/test_directions.py +202 -0
  73. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_eager_autograd.py +6 -6
  74. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_facts.py +56 -0
  75. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_family_points.py +61 -8
  76. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_gated_attn_out.py +5 -5
  77. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_head_contributions.py +3 -3
  78. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_hook_call_conventions.py +2 -2
  79. interp_engine-1.13.0/tests/test_lens_stream.py +246 -0
  80. interp_engine-1.13.0/tests/test_lens_stream_pairing.py +390 -0
  81. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_load.py +7 -1
  82. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_memory.py +23 -0
  83. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_mlp_internals.py +5 -5
  84. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_model_expectations.py +2 -2
  85. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_moe.py +13 -13
  86. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_multimodal_arch.py +93 -0
  87. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_new_models_gpu.py +6 -6
  88. interp_engine-1.13.0/tests/test_oracle.py +257 -0
  89. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_packaging.py +5 -0
  90. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_padded_positions.py +59 -13
  91. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_parity_gpt2.py +10 -10
  92. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_per_layer_attn_dims.py +8 -8
  93. interp_engine-1.13.0/tests/test_point_capability.py +235 -0
  94. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_protocol.py +17 -4
  95. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_published_benchmarks.py +24 -0
  96. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_qk_norm.py +2 -2
  97. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_qkv_layout.py +5 -5
  98. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_resid_mid.py +3 -3
  99. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_residual_basis.py +7 -7
  100. interp_engine-1.13.0/tests/test_sample_from_embeds.py +145 -0
  101. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_sandwich_norms.py +2 -2
  102. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_select.py +27 -0
  103. interp_engine-1.13.0/tests/test_server.py +413 -0
  104. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_sliding_window_attn.py +2 -2
  105. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_small_models_gpu.py +3 -3
  106. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_static_dsv4_gpu.py +20 -1
  107. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_static_set.py +133 -4
  108. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_steer_context.py +32 -5
  109. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_steer_math_parity.py +103 -0
  110. interp_engine-1.13.0/tests/test_steer_reaches_generate.py +169 -0
  111. interp_engine-1.13.0/tests/test_steer_scope.py +166 -0
  112. interp_engine-1.13.0/tests/test_steer_specs.py +181 -0
  113. interp_engine-1.13.0/tests/test_steer_stream_stack.py +120 -0
  114. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_sync_loop.py +35 -0
  115. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_sync_parity.py +18 -0
  116. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_unified_free_functions.py +15 -4
  117. interp_engine-1.13.0/tests/test_userless_prefix_spans.py +89 -0
  118. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_capture_gpu.py +123 -0
  119. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_hook_availability.py +6 -10
  120. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_hyper_connections.py +16 -14
  121. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_kv_isolation.py +147 -6
  122. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_new_points.py +1 -1
  123. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_padded_gpu.py +4 -4
  124. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_worker_lens_capture_readout.py +93 -0
  125. interp_engine-1.12.0/interp_engine/steer_specs.py +0 -166
  126. interp_engine-1.12.0/interp_engine/vllm_capture/lens/intervene.py +0 -142
  127. {interp_engine-1.12.0 → interp_engine-1.13.0}/.gitignore +0 -0
  128. {interp_engine-1.12.0 → interp_engine-1.13.0}/LICENSE +0 -0
  129. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/README.md +0 -0
  130. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/__init__.py +0 -0
  131. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/cells.py +0 -0
  132. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/probe.py +0 -0
  133. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/probe_lens_stream.py +0 -0
  134. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
  135. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
  136. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
  137. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
  138. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/gemma-2-2b__eager.json +0 -0
  139. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
  140. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
  141. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
  142. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
  143. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
  144. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
  145. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
  146. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/qwen3-4b__eager.json +0 -0
  147. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
  148. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
  149. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/qwen3-4b__vllm.json +0 -0
  150. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
  151. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
  152. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
  153. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
  154. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/run_all.sh +0 -0
  155. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/run_bench.py +0 -0
  156. {interp_engine-1.12.0 → interp_engine-1.13.0}/benchmarks/workloads.py +0 -0
  157. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/COMPATIBILITY.md +0 -0
  158. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/PORTING.md +0 -0
  159. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/README.md +0 -0
  160. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/SUPPORTED_POINTS.md +0 -0
  161. {interp_engine-1.12.0 → interp_engine-1.13.0}/docs/VALIDATING_A_NEW_MODEL.md +0 -0
  162. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/address.py +0 -0
  163. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/attn_config.py +0 -0
  164. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/attn_scores.py +0 -0
  165. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/autograd_support.py +0 -0
  166. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/chat_compose.py +0 -0
  167. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/chat_conventions.py +0 -0
  168. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/chat_formatters.py +0 -0
  169. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/cuda_preflight.py +0 -0
  170. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/moe_routing.py +0 -0
  171. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/notebook_stdout.py +0 -0
  172. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/points.py +0 -0
  173. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/residual_basis.py +0 -0
  174. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/sampling.py +0 -0
  175. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/_hooks.py +0 -0
  176. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/_payload.py +0 -0
  177. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/attn.py +0 -0
  178. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/capture.py +0 -0
  179. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/graphs.py +0 -0
  180. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/mhc.py +0 -0
  181. {interp_engine-1.12.0 → interp_engine-1.13.0}/interp_engine/vllm_capture/native.py +0 -0
  182. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/model_expectations.yaml +0 -0
  183. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/synthetic_families.py +0 -0
  184. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_address.py +0 -0
  185. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_attn_config_tripwire.py +0 -0
  186. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_autograd_support.py +0 -0
  187. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_bench_workloads.py +0 -0
  188. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_chat_compose.py +0 -0
  189. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_chat_formatters.py +0 -0
  190. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_chat_templates.py +0 -0
  191. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_cuda_preflight.py +0 -0
  192. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_doc_code_fences.py +0 -0
  193. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_gpu_sizer.py +0 -0
  194. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_injected_system_spans.py +0 -0
  195. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_layer_kinds.py +0 -0
  196. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_logit_transform.py +0 -0
  197. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_mappers.py +0 -0
  198. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_multigpu.py +0 -0
  199. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_no_chat_template.py +0 -0
  200. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_normalized_hook.py +0 -0
  201. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_notebook_stdout.py +0 -0
  202. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_points_registry.py +0 -0
  203. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_reasoning_spans.py +0 -0
  204. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_release.py +0 -0
  205. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_sampling.py +0 -0
  206. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_static_parity_gpu.py +0 -0
  207. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_static_warmup.py +0 -0
  208. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_steer_ops_per_site_gpu.py +0 -0
  209. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_unresolved_families.py +0 -0
  210. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_capture_scales.py +0 -0
  211. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_engine_loop.py +0 -0
  212. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_graph_path.py +0 -0
  213. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_graphs_on_gpu.py +0 -0
  214. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_only_families.py +0 -0
  215. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_plugin.py +0 -0
  216. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vllm_wire_grammar.py +0 -0
  217. {interp_engine-1.12.0 → interp_engine-1.13.0}/tests/test_vocabulary_boundary.py +0 -0
  218. {interp_engine-1.12.0 → interp_engine-1.13.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.12.0
3
+ Version: 1.13.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
@@ -8,6 +8,7 @@ Project-URL: Issues, https://github.com/decoderesearch/interp-engine/issues
8
8
  License-Expression: Apache-2.0
9
9
  License-File: LICENSE
10
10
  Requires-Python: <3.14,>=3.11
11
+ Requires-Dist: accelerate>=1.0
11
12
  Requires-Dist: einops
12
13
  Requires-Dist: numpy>=1.24
13
14
  Requires-Dist: torch>=1.10
@@ -25,6 +26,10 @@ Requires-Dist: transformer-lens<4,>=3.0; extra == 'parity'
25
26
  Provides-Extra: quant
26
27
  Requires-Dist: accelerate>=1.0; extra == 'quant'
27
28
  Requires-Dist: kernels<0.17.0,>=0.15.2; extra == 'quant'
29
+ Provides-Extra: server
30
+ Requires-Dist: fastapi>=0.115; extra == 'server'
31
+ Requires-Dist: httpx>=0.27; extra == 'server'
32
+ Requires-Dist: uvicorn>=0.30; extra == 'server'
28
33
  Provides-Extra: vllm
29
34
  Requires-Dist: vllm==0.28.0; (sys_platform == 'linux') and extra == 'vllm'
30
35
  Description-Content-Type: text/markdown
@@ -72,7 +77,7 @@ pip install interp-engine # eager backend only
72
77
  ## Simple Usage
73
78
 
74
79
  ```python
75
- from interp_engine import Address, load_model, run_with_cache
80
+ from interp_engine import Address, capture, load_model
76
81
 
77
82
  # VLLM (default): low VRAM, medium speed, every point, chosen per request
78
83
  model = load_model("Qwen/Qwen3-8B")
@@ -87,7 +92,7 @@ model = load_model("Qwen/Qwen3-8B")
87
92
  # model = load_model("Qwen/Qwen3-8B", backend="eager")
88
93
 
89
94
  point = Address("resid_post", 10) # or string: "resid_post.10"
90
- cache = run_with_cache(model, model.to_tokens("Hello, world"), [point])
95
+ cache = capture(model, model.to_tokens("Hello, world"), [point])
91
96
  cache[point] # [batch, pos, ...]
92
97
  ```
93
98
 
@@ -107,27 +112,27 @@ vLLM gives `interp-engine` high throughput via concurrency, and `backend="vllm-s
107
112
 
108
113
  <!-- Generated by `python -m benchmarks.report_bench`. Do not edit: rerun the sweep. -->
109
114
 
110
- Measured on NVIDIA B200, bf16, 512-token prompt, 128 new tokens.
115
+ Measured on NVIDIA B200, 512-token prompt, 128 new tokens.
111
116
 
112
117
  One stream (tok/s):
113
118
 
114
- | model | eager | vLLM | vLLM + static taps |
115
- | ------------------------ | ----- | ---------- | ------------------ |
116
- | `gemma-2-2b` | 75 | 88 (1.2x) | **220 (2.9x)** |
117
- | `qwen3-4b` | 60 | 136 (2.3x) | **313 (5.2x)** |
118
- | `llama-3.1-8b` | 84 | 163 (1.9x) | **258 (3.1x)** |
119
- | `qwen3.8-27b` | 25 | 35 (1.4x) | **63 (2.5x)** |
120
- | `deepseek-v4-flash-0731` | 5.3 | 15 (2.9x) | **132 (25x)** |
119
+ | model | weights | eager | vLLM | vLLM + static taps |
120
+ | ------------------------ | ------- | ----- | ---------- | ------------------ |
121
+ | `gemma-2-2b` | bf16 | 75 | 88 (1.2x) | **220 (2.9x)** |
122
+ | `qwen3-4b` | bf16 | 60 | 136 (2.3x) | **313 (5.2x)** |
123
+ | `llama-3.1-8b` | bf16 | 84 | 163 (1.9x) | **258 (3.1x)** |
124
+ | `qwen3.8-27b` | bf16 | 25 | 35 (1.4x) | **63 (2.5x)** |
125
+ | `deepseek-v4-flash-0731` | fp8 | 5.3 | 15 (2.9x) | **132 (25x)** |
121
126
 
122
127
  8 concurrent requests (aggregate tok/s):
123
128
 
124
- | model | eager | vLLM | vLLM + static taps |
125
- | ------------------------ | ----- | ------------- | ------------------ |
126
- | `gemma-2-2b` | 75 | 675 (9.0x) | **1,436 (19.2x)** |
127
- | `qwen3-4b` | 61 | 973 (15.9x) | **2,219 (36x)** |
128
- | `llama-3.1-8b` | 81 | 1,187 (14.6x) | **1,755 (22x)** |
129
- | `qwen3.8-27b` | 24 | 246 (10.3x) | **408 (17.0x)** |
130
- | `deepseek-v4-flash-0731` | 5.4 | 117 (22x) | **631 (116x)** |
129
+ | model | weights | eager | vLLM | vLLM + static taps |
130
+ | ------------------------ | ------- | ----- | ------------- | ------------------ |
131
+ | `gemma-2-2b` | bf16 | 75 | 675 (9.0x) | **1,436 (19.2x)** |
132
+ | `qwen3-4b` | bf16 | 61 | 973 (15.9x) | **2,219 (36x)** |
133
+ | `llama-3.1-8b` | bf16 | 81 | 1,187 (14.6x) | **1,755 (22x)** |
134
+ | `qwen3.8-27b` | bf16 | 24 | 246 (10.3x) | **408 (17.0x)** |
135
+ | `deepseek-v4-flash-0731` | fp8 | 5.4 | 117 (22x) | **631 (116x)** |
131
136
 
132
137
  <!-- THROUGHPUT:END -->
133
138
 
@@ -41,7 +41,7 @@ pip install interp-engine # eager backend only
41
41
  ## Simple Usage
42
42
 
43
43
  ```python
44
- from interp_engine import Address, load_model, run_with_cache
44
+ from interp_engine import Address, capture, load_model
45
45
 
46
46
  # VLLM (default): low VRAM, medium speed, every point, chosen per request
47
47
  model = load_model("Qwen/Qwen3-8B")
@@ -56,7 +56,7 @@ model = load_model("Qwen/Qwen3-8B")
56
56
  # model = load_model("Qwen/Qwen3-8B", backend="eager")
57
57
 
58
58
  point = Address("resid_post", 10) # or string: "resid_post.10"
59
- cache = run_with_cache(model, model.to_tokens("Hello, world"), [point])
59
+ cache = capture(model, model.to_tokens("Hello, world"), [point])
60
60
  cache[point] # [batch, pos, ...]
61
61
  ```
62
62
 
@@ -76,27 +76,27 @@ vLLM gives `interp-engine` high throughput via concurrency, and `backend="vllm-s
76
76
 
77
77
  <!-- Generated by `python -m benchmarks.report_bench`. Do not edit: rerun the sweep. -->
78
78
 
79
- Measured on NVIDIA B200, bf16, 512-token prompt, 128 new tokens.
79
+ Measured on NVIDIA B200, 512-token prompt, 128 new tokens.
80
80
 
81
81
  One stream (tok/s):
82
82
 
83
- | model | eager | vLLM | vLLM + static taps |
84
- | ------------------------ | ----- | ---------- | ------------------ |
85
- | `gemma-2-2b` | 75 | 88 (1.2x) | **220 (2.9x)** |
86
- | `qwen3-4b` | 60 | 136 (2.3x) | **313 (5.2x)** |
87
- | `llama-3.1-8b` | 84 | 163 (1.9x) | **258 (3.1x)** |
88
- | `qwen3.8-27b` | 25 | 35 (1.4x) | **63 (2.5x)** |
89
- | `deepseek-v4-flash-0731` | 5.3 | 15 (2.9x) | **132 (25x)** |
83
+ | model | weights | eager | vLLM | vLLM + static taps |
84
+ | ------------------------ | ------- | ----- | ---------- | ------------------ |
85
+ | `gemma-2-2b` | bf16 | 75 | 88 (1.2x) | **220 (2.9x)** |
86
+ | `qwen3-4b` | bf16 | 60 | 136 (2.3x) | **313 (5.2x)** |
87
+ | `llama-3.1-8b` | bf16 | 84 | 163 (1.9x) | **258 (3.1x)** |
88
+ | `qwen3.8-27b` | bf16 | 25 | 35 (1.4x) | **63 (2.5x)** |
89
+ | `deepseek-v4-flash-0731` | fp8 | 5.3 | 15 (2.9x) | **132 (25x)** |
90
90
 
91
91
  8 concurrent requests (aggregate tok/s):
92
92
 
93
- | model | eager | vLLM | vLLM + static taps |
94
- | ------------------------ | ----- | ------------- | ------------------ |
95
- | `gemma-2-2b` | 75 | 675 (9.0x) | **1,436 (19.2x)** |
96
- | `qwen3-4b` | 61 | 973 (15.9x) | **2,219 (36x)** |
97
- | `llama-3.1-8b` | 81 | 1,187 (14.6x) | **1,755 (22x)** |
98
- | `qwen3.8-27b` | 24 | 246 (10.3x) | **408 (17.0x)** |
99
- | `deepseek-v4-flash-0731` | 5.4 | 117 (22x) | **631 (116x)** |
93
+ | model | weights | eager | vLLM | vLLM + static taps |
94
+ | ------------------------ | ------- | ----- | ------------- | ------------------ |
95
+ | `gemma-2-2b` | bf16 | 75 | 675 (9.0x) | **1,436 (19.2x)** |
96
+ | `qwen3-4b` | bf16 | 61 | 973 (15.9x) | **2,219 (36x)** |
97
+ | `llama-3.1-8b` | bf16 | 81 | 1,187 (14.6x) | **1,755 (22x)** |
98
+ | `qwen3.8-27b` | bf16 | 24 | 246 (10.3x) | **408 (17.0x)** |
99
+ | `deepseek-v4-flash-0731` | fp8 | 5.4 | 117 (22x) | **631 (116x)** |
100
100
 
101
101
  <!-- THROUGHPUT:END -->
102
102
 
@@ -27,6 +27,9 @@ class ModelSpec:
27
27
  precision and does not resolve to the same thing on both backends: eager honors a float32
28
28
  checkpoint while vLLM's "auto" downcasts it to bfloat16, which would make that model's row a
29
29
  precision comparison rather than a backend one."""
30
+ weights: str = ""
31
+ """What the checkpoint's weights stay in when that is not ``dtype`` and its cells do not record
32
+ it, for the README's weights column. Empty: the cell's quantization, else its dtype."""
30
33
  min_gpu_gib: float = 0.0
31
34
  """The smallest card this row fits on, in GiB of total VRAM. A sweep that was not given an
32
35
  explicit ``--models`` drops the rows the card cannot hold and says which (:func:`default_models`).
@@ -123,6 +126,7 @@ MODELS: tuple[ModelSpec, ...] = (
123
126
  "deepseek-ai/DeepSeek-V4-Flash-0731",
124
127
  "DeepseekV4",
125
128
  "291B (14B active)",
129
+ weights="fp8",
126
130
  min_gpu_gib=170.0,
127
131
  # `resid_post` does not exist on this trunk: the block carries `hc_mult` (4) parallel residual
128
132
  # streams, and the engine refuses the name rather than silently returning stream 0. `mlp_out`
@@ -26,7 +26,7 @@ The display form differs from the report's, deliberately:
26
26
  rounding, which is the worse of the two.
27
27
  - **the card gets the machine, the README gets the conditions.** Both need the reader to know these
28
28
  are one box's numbers, but the card is a hover card that links ``results-latest.md`` for the rest,
29
- and the dtype and the two lengths are four more things to read before reaching a figure. So it is
29
+ and the weights and the two lengths are four more things to read before reaching a figure. So it is
30
30
  handed ``BENCHMARK_GPU`` where the README prints the whole line -- the difference is the room each
31
31
  has, not a difference of opinion about what a figure means without them.
32
32
  """
@@ -34,11 +34,12 @@ The display form differs from the report's, deliberately:
34
34
  from __future__ import annotations
35
35
 
36
36
  import argparse
37
+ import re
37
38
  from dataclasses import dataclass
38
39
  from pathlib import Path
39
40
  from typing import Any
40
41
 
41
- from benchmarks.bench_spec import workload
42
+ from benchmarks.bench_spec import MODELS, workload
42
43
  from benchmarks.cells import cell_metric, cells_by_key, load_cells, model_order, nonuniform, row_spec
43
44
  from benchmarks.run_bench import RESULTS_DIR
44
45
 
@@ -110,7 +111,9 @@ class Conditions:
110
111
  concurrency: int
111
112
 
112
113
  def __str__(self) -> str:
113
- return f"{self.gpu}, {self.dtype}, {self.prompt_tokens}-token prompt, {self.new_tokens} new tokens"
114
+ # The dtype is per row, in the tables' `weights` column: a quantized row runs its matmuls in
115
+ # 4 or 8 bits whatever the activations are in.
116
+ return f"{self.gpu}, {self.prompt_tokens}-token prompt, {self.new_tokens} new tokens"
114
117
 
115
118
 
116
119
  def _only(values: set[Any], what: str) -> Any:
@@ -136,6 +139,28 @@ def conditions(cells: list[dict[str, Any]]) -> Conditions:
136
139
  )
137
140
 
138
141
 
142
+ def weights(cells: list[dict[str, Any]]) -> dict[str, str]:
143
+ """``{model: what its weights ran in}``: `ModelSpec.weights` where the spec states it, else the
144
+ cell's quantization as a bit width, else its dtype.
145
+
146
+ One value per row: a 4-bit figure beside a bf16 one in the same row is not a speedup.
147
+ """
148
+ stated = {m.key: m.weights for m in MODELS if m.weights}
149
+ labels: dict[str, set[str]] = {}
150
+ for cell in cells:
151
+ key = cell["model"]["key"]
152
+ load = cell.get("load") or {}
153
+ label = stated.get(key, "")
154
+ if not label and (quantization := load.get("quantization")):
155
+ bits = re.search(r"(\d+)-bit", str(quantization))
156
+ label = f"{bits.group(1)}-bit" if bits else str(quantization)
157
+ if not label and (resolved := load.get("resolved_dtype")):
158
+ label = DTYPE_SHORT.get(resolved) or resolved
159
+ if label:
160
+ labels.setdefault(key, set()).add(label)
161
+ return {key: str(_only(found, f"what {key}'s weights ran in")) for key, found in labels.items()}
162
+
163
+
139
164
  def rates(cells: list[dict[str, Any]]) -> dict[str, dict[str, dict[str, float | None]]]:
140
165
  """``{model: {variant: {regime: tok/s or None}}}``, in the sweep's row order."""
141
166
  index = cells_by_key(cells)
@@ -164,11 +189,15 @@ def _padded(header: list[str], rows: list[list[str]]) -> list[str]:
164
189
  return out
165
190
 
166
191
 
167
- def _table(measured: dict[str, dict[str, dict[str, float | None]]], regime: str) -> list[str]:
192
+ def _table(
193
+ measured: dict[str, dict[str, dict[str, float | None]]],
194
+ regime: str,
195
+ precision: dict[str, str] | None = None,
196
+ ) -> list[str]:
168
197
  rows: list[list[str]] = []
169
198
  for model_key, per_variant in measured.items():
170
199
  baseline = per_variant[BASELINE][regime]
171
- row = [f"`{model_key}`"]
200
+ row = [f"`{model_key}`", (precision or {}).get(model_key, MISSING_CELL)]
172
201
  for variant_key, _, _ in COLUMNS:
173
202
  value = per_variant[variant_key][regime]
174
203
  if value is None:
@@ -179,12 +208,13 @@ def _table(measured: dict[str, dict[str, dict[str, float | None]]], regime: str)
179
208
  text = f"{text} ({fmt_multiplier(value / baseline)})"
180
209
  row.append(f"**{text}**" if variant_key == HIGHLIGHT else text)
181
210
  rows.append(row)
182
- return _padded(["model", *(heading for _, heading, _ in COLUMNS)], rows)
211
+ return _padded(["model", "weights", *(heading for _, heading, _ in COLUMNS)], rows)
183
212
 
184
213
 
185
214
  def readme_block(cells: list[dict[str, Any]]) -> str:
186
215
  where = conditions(cells)
187
216
  measured = rates(cells)
217
+ precision = weights(cells)
188
218
  return "\n".join(
189
219
  [
190
220
  "<!-- Generated by `python -m benchmarks.report_bench`. Do not edit: rerun the sweep. -->",
@@ -193,11 +223,11 @@ def readme_block(cells: list[dict[str, Any]]) -> str:
193
223
  "",
194
224
  "One stream (tok/s):",
195
225
  "",
196
- *_table(measured, "single"),
226
+ *_table(measured, "single", precision),
197
227
  "",
198
228
  f"{where.concurrency} concurrent requests (aggregate tok/s):",
199
229
  "",
200
- *_table(measured, "concurrent"),
230
+ *_table(measured, "concurrent", precision),
201
231
  ]
202
232
  )
203
233
 
@@ -214,8 +214,9 @@ def _env_section(cells: list[dict[str, Any]]) -> list[str]:
214
214
 
215
215
  def _models_section(cells: list[dict[str, Any]]) -> list[str]:
216
216
  index = cells_by_key(cells)
217
+ precision = publish.weights(cells)
217
218
  lines = [
218
- "| model | HuggingFace id | family | params | native dtype | layers | d_model |",
219
+ "| model | HuggingFace id | family | params | weights | layers | d_model |",
219
220
  "| --- | --- | --- | --- | --- | --- | --- |",
220
221
  ]
221
222
  for model_key in model_order(cells):
@@ -230,7 +231,8 @@ def _models_section(cells: list[dict[str, Any]]) -> list[str]:
230
231
  load = cell.get("load", {})
231
232
  lines.append(
232
233
  f"| `{model_key}` | `{m['hf_id']}` | {m['family']} | {m['params']} | "
233
- f"{m.get('native_dtype', '?')} | {load.get('n_layers', '?')} | {load.get('d_model', '?')} |"
234
+ f"{precision.get(model_key) or m.get('native_dtype', '?')} | {load.get('n_layers', '?')} | "
235
+ f"{load.get('d_model', '?')} |"
234
236
  )
235
237
  return lines
236
238
 
@@ -3,7 +3,7 @@
3
3
  Generated by `python -m benchmarks.report_bench`. Do not edit by hand -- rerun the sweep.
4
4
 
5
5
  - **Run** 2026-09-04 (cells from `2026-09-04T07:18:04+00:00` to `2026-09-04T08:24:40+00:00` UTC)
6
- - **Report written** 2026-09-04T08:32:25+00:00
6
+ - **Report written** 2026-09-24T04:33:12+00:00
7
7
  - **Cells** 20
8
8
 
9
9
  ## What this measures
@@ -61,13 +61,13 @@ Taken from the deployed set rather than invented, and spanning the range a singl
61
61
  hold: from a 2.6B dense trunk to a 291B sparse one. A model whose weights do not fit is
62
62
  dropped from a plain sweep and named, rather than failing it (`ModelSpec.min_gpu_gib`).
63
63
 
64
- | model | HuggingFace id | family | params | native dtype | layers | d_model |
64
+ | model | HuggingFace id | family | params | weights | layers | d_model |
65
65
  | --- | --- | --- | --- | --- | --- | --- |
66
- | `gemma-2-2b` | `google/gemma-2-2b` | Gemma2 | 2.6B | float32 | 26 | 2304 |
67
- | `qwen3-4b` | `Qwen/Qwen3-4B` | Qwen3 | 4.0B | bfloat16 | 36 | 2560 |
68
- | `llama-3.1-8b` | `meta-llama/Llama-3.1-8B` | Llama3 | 8.0B | bfloat16 | 32 | 4096 |
69
- | `qwen3.8-27b` | `Qwen/Qwen3.8-27B` | Qwen3_5 | 27B | bfloat16 | 64 | 5120 |
70
- | `deepseek-v4-flash-0731` | `deepseek-ai/DeepSeek-V4-Flash-0731` | DeepseekV4 | 291B (14B active) | bfloat16 | 43 | 4096 |
66
+ | `gemma-2-2b` | `google/gemma-2-2b` | Gemma2 | 2.6B | bf16 | 26 | 2304 |
67
+ | `qwen3-4b` | `Qwen/Qwen3-4B` | Qwen3 | 4.0B | bf16 | 36 | 2560 |
68
+ | `llama-3.1-8b` | `meta-llama/Llama-3.1-8B` | Llama3 | 8.0B | bf16 | 32 | 4096 |
69
+ | `qwen3.8-27b` | `Qwen/Qwen3.8-27B` | Qwen3_5 | 27B | bf16 | 64 | 5120 |
70
+ | `deepseek-v4-flash-0731` | `deepseek-ai/DeepSeek-V4-Flash-0731` | DeepseekV4 | 291B (14B active) | fp8 | 43 | 4096 |
71
71
 
72
72
  ### Where a row differs
73
73
 
@@ -7,6 +7,7 @@ If you are a human reading this: [USAGE.md](USAGE.md) is the friendlier introduc
7
7
  [PORTING.md](PORTING.md) covers hook-name translation in more depth.
8
8
 
9
9
  - [Version stamp](#version-stamp)
10
+ - [`capture` and `run_with_cache`](#capture-and-run_with_cache)
10
11
  - [What changed in 1.3](#what-changed-in-13)
11
12
  - [What changed in 1.1](#what-changed-in-11)
12
13
  - [Pick a tier first](#pick-a-tier-first)
@@ -36,6 +37,18 @@ a minor version when keeping it would mean one of the two backends could not be
36
37
  [What changed in 1.1](#what-changed-in-11) — and pinning `interp-engine~=1.3` is the way to not be
37
38
  surprised by the next one.
38
39
 
40
+ ## `capture` and `run_with_cache`
41
+
42
+ The sync single-forward capture is `capture(model, tokens, points)`. `run_with_cache` is an alias
43
+ with the same signature and the same `Cache`, kept for code and habits that came from
44
+ TransformerLens; nothing is deprecated and nothing raises. New code should say `capture`.
45
+
46
+ Two other things share the word. `await model.capture(...)` is the async *method* on the protocol:
47
+ one prompt, a plain dict keyed by `Address`, no batch axis. `interp_engine.capture` is the
48
+ *submodule* that holds `Cache` and the capture helpers; `from interp_engine.capture import Cache`
49
+ works as before, but after `import interp_engine` the attribute `interp_engine.capture` is the
50
+ function.
51
+
39
52
  ## What changed in 1.3
40
53
 
41
54
  **"Freeze" is now "static", and the mode is a `backend=` value rather than a kwarg that turned it
@@ -86,7 +99,7 @@ edits and gains one capability.
86
99
  | `OrthogonalProjector(...).get_P()` / `.get_orthogonal_complement()` | `.delta(acts)` / `.project(acts)` | The projection matrix was `d_model × d_model` materialized per call; the same arithmetic is one dot product. |
87
100
  | `run_with_cache(model, ..., **forward_kwargs)` | *(gone)* | Passthrough kwargs were unreachable on vLLM, so a call using them silently meant something different per backend. |
88
101
 
89
- What is new, rather than moved: `run_with_cache`, `capture_generation`, `capture_attention`,
102
+ What is new, rather than moved: `capture` (then still spelled `run_with_cache`), `capture_generation`, `capture_attention`,
90
103
  `generate_stream`, `capture_residuals`, `layer_logits` and `steer` all take either backend now, and
91
104
  `sync_model(model)` gives you the whole protocol without `await`. `ProjectionCapSpec` works on eager
92
105
  (it used to raise `NotImplementedError`), so all three steering specs run on both backends.
@@ -145,8 +158,8 @@ Keyed on the pattern you find in the source you are porting.
145
158
  | `LLM(model=id)` + you own that engine | `worker_extension_cls=WORKER_EXTENSION_CLS` | Tier 1. Merge `capture_engine_kwargs()`. |
146
159
  | `LLM(model=id)` + you do not care how | `load_model(id)` | Tier 2. |
147
160
  | `HookedTransformer.from_pretrained(id)` | `EagerModel.from_pretrained(id)` | Then translate hook names — next row. |
148
- | `model.run_with_cache(...)["blocks.7.attn.hook_z"]` | `run_with_cache(model, tokens, [tlens_hook_to_point("blocks.7.attn.hook_z", model)])` | **Pass the model.** See the warning below. |
149
- | nnsight `with model.trace(...)` / `mlps_output[7]` | `nnsight_accessor_to_point("mlps_output[7]")` | Then `run_with_cache` or `await model.capture(...)`. |
161
+ | `model.run_with_cache(...)["blocks.7.attn.hook_z"]` | `capture(model, tokens, [tlens_hook_to_point("blocks.7.attn.hook_z", model)])` | **Pass the model.** See the warning below. |
162
+ | nnsight `with model.trace(...)` / `mlps_output[7]` | `nnsight_accessor_to_point("mlps_output[7]")` | Then `capture(model, ...)` or `await model.capture(...)`. |
150
163
  | `model.unembed(model.ln_final(x))` | `await model.decode_residuals(x)` | The method normalizes softcapping across backends; the sync free function does not. |
151
164
  | a hand-rolled `for _ in range(n): model(...)` decode loop | `generate_stream(model, tokens)` | Yields a `GenStep` per token with logprobs, and logits on eager (rule 12). |
152
165
  | `asyncio.run(model.capture(...))` in a script, once per call | `sync = sync_model(model)`, then `sync.capture(...)` | One background loop for the model's lifetime, instead of a new one per call — which on vLLM would abandon the engine's tasks. |
@@ -205,7 +218,7 @@ an obvious one.
205
218
 
206
219
  1. **Every model *method* is `async`,** including on eager where the work underneath is synchronous.
207
220
  `model.capture(...)` without `await` gives you a coroutine, not activations. The sync free
208
- functions (`run_with_cache`, `capture_generation`, `capture_attention`, `generate_stream`,
221
+ functions (`capture`, `capture_generation`, `capture_attention`, `generate_stream`,
209
222
  `steer`, `layer_logits`) take either backend, and `sync_model(model)` mirrors the whole protocol
210
223
  for the times you want a method. Neither can be called from inside a running event loop — they
211
224
  refuse, naming the `await` to use instead, rather than deadlocking. The mirror image also
@@ -239,9 +252,9 @@ an obvious one.
239
252
  7. **`await model.shutdown()` before loading a second model in the same process on vLLM.** Its KV
240
253
  cache lives in a child process that dropping a Python reference does not reap. Idempotent, so
241
254
  there is no cost to calling it always.
242
- 8. **`capture()` returns a plain dict keyed by `Address`, not by string.** You may ask with a string
255
+ 8. **`await model.capture()` returns a plain dict keyed by `Address`, not by string.** You may ask with a string
243
256
  or a `(name, layer)` tuple, but on the way back out use `to_address("resid_post.10")` as the key.
244
- (`run_with_cache`'s `Cache` is the exception and accepts either.)
257
+ (The free function `capture(model, ...)` returns a `Cache`, which accepts either.)
245
258
  9. **Do not apply `final_logit_softcapping` after `await model.decode_residuals(...)`.** The method
246
259
  applies the model's configured value so the two backends are comparable. The sync free function
247
260
  `interp_engine.decode_residuals` is the raw one and takes `softcap` explicitly.
@@ -289,9 +302,9 @@ mistake you made.
289
302
 
290
303
  ## When you are not sure, ask the model
291
304
 
292
- Capabilities are queryable, cheap, and answerable **before** `warmup()` — they read configuration
293
- rather than running a forward. Gate on these rather than on backend name or model id, because that is
294
- what keeps one code path working across both backends:
305
+ Capabilities are queryable and cheap: they read configuration rather than running a forward, and the
306
+ model-wide ones below are answerable **before** `warmup()`. Gate on these rather than on backend name
307
+ or model id, because that is what keeps one code path working across both backends:
295
308
 
296
309
  ```python
297
310
  from interp_engine import load_model
@@ -304,6 +317,28 @@ model.residual_basis.lens_valid # is a logit-lens read-out meaningful here?
304
317
  model.n_layers, model.d_model # remember z is n_heads * head_dim, not d_model
305
318
  ```
306
319
 
320
+ For one point rather than the whole model, ask `refuses()`. It returns the reason as a string, or
321
+ `None` when the point is servable, so a caller filtering a list gets to say which gap it hit rather
322
+ than logging "unavailable":
323
+
324
+ ```python
325
+ for point in ("resid_post", "router_logits", "q_norm_out"):
326
+ why = model.refuses(point, 12)
327
+ print(point, "ok" if why is None else f"skipped: {why}")
328
+ ```
329
+
330
+ **Do not rebuild this from the point tables in these docs.** `SUPPORTED_POINTS.md` and
331
+ `ENGINE_HOOK_MAPPINGS.md` say what a *point* is, per backend; `refuses()` also knows what this
332
+ *checkpoint* carries and how this instance was built, which no table can. A serving pod that derived
333
+ its advertised set from a table instead shipped a 400 for attention-output SAEs that blamed the
334
+ paged-attention kernel, and a second one that narrowed by tensor-parallel shard width refused points
335
+ the worker gathers. Both read as maintained lists right up until they were wrong.
336
+
337
+ The one caveat is the order on vLLM: the architecture half of the answer lives in the worker
338
+ process, so it arrives at `warmup()`. Before that, vLLM's `refuses()` answers from the point table
339
+ and how the engine was built, which can say yes to a point the checkpoint has no module for. Warm up
340
+ before advertising. Eager holds its own module tree and needs no warmup.
341
+
307
342
  For a coarse "which surface do I have", `isinstance(model, InterpModel)` works —
308
343
  but note it checks method *presence* only, not signatures:
309
344
 
@@ -311,7 +311,7 @@ must split", on a family whose `fused_qkv` is false — so `value` no longer ask
311
311
  **Reading a norm costs a rank, and the point does not pay it.** A norm over `head_dim` can only be
312
312
  given the per-head view, so both engines hand `v_norm` a `(…, n_kv_heads, head_dim)` tensor where a
313
313
  `v_proj` would have produced the flat one — and `value` would then be 4-D on this family and 3-D on the
314
- next, against a `Width.HEADS` declaration and `run_with_cache`'s `[batch, seq, width]`. Flattened back
314
+ next, against a `Width.HEADS` declaration and `capture`'s `[batch, seq, width]`. Flattened back
315
315
  on both sides (`hooks.flat_per_head`, `vllm_capture._hooks.flat_value`), including on the way *into* a
316
316
  steer, so a vector measured on a capture of `value` is the shape the write expects; the module gets its
317
317
  own rank back before the attention reshapes it. Eagerly the head count is checked rather than assumed,
@@ -501,7 +501,7 @@ router_logits)`. `facts.ROUTER_OUTPUTS` holds the exceptions; `facts.assert_rout
501
501
  - **A fused kernel can leave the router module unused.** transformers' MXFP4 path for gpt-oss replaces
502
502
  the _block's_ forward and routes inline (`F.linear` on `self.router.weight`, then a Triton top-k), so
503
503
  the router is in the tree, correct, and never called. `arch.moe_router` detects the replaced forward
504
- and refuses with the remedy (`Mxfp4Config(dequantize=True)`); more generally, `run_with_cache` now
504
+ and refuses with the remedy (`Mxfp4Config(dequantize=True)`); more generally, `capture` now
505
505
  raises when a resolved module did not run, rather than returning a short cache. `router_logits` is the
506
506
  exception, and it does not need the remedy: `mlp_forward` returns `(routed_out, router_logits)`, so the
507
507
  point resolves to the _block's_ `output:1` instead (`arch.inline_routing_logits`) and is bit-identical
@@ -510,7 +510,7 @@ router_logits)`. `facts.ROUTER_OUTPUTS` holds the exceptions; `facts.assert_rout
510
510
  index — a four-wide top-k against the logits' 32 — and gpt-oss carries a second, unverified kernel
511
511
  swap (`@use_kernel_forward_from_hub("MegaBlocksMoeMLP")`) whose layout nothing here has confirmed.
512
512
  `expert_weights` and `expert_indices` have no boundary on that path even so, and are _rebuilt_ from
513
- those logits by `run_with_cache` (`interp_engine.moe_routing`) — the one place a routing convention is
513
+ those logits by `capture` (`interp_engine.moe_routing`) — the one place a routing convention is
514
514
  reimplemented, allowed because the derivation is a top-k on an already-captured tensor and because it
515
515
  is verified against `GptOssTopKRouter` on the real checkpoint rather than read off the source. They
516
516
  still refuse from `resolve_point`, which answers with addresses and has none to give.
@@ -811,8 +811,9 @@ Two things a randomly-initialised model cannot show you, both worth knowing when
811
811
  - **`head_dim ≠ d_model/n_heads`** on Gemma (GQA + explicit `head_dim`): the attention-output /
812
812
  `z` width is `n_heads*head_dim`, not `d_model`. Attention-SAE code must use `head_dim`.
813
813
  - **Embedding scaling** (Gemma multiplies embeddings by `sqrt(d_model)`): free when you hook real
814
- modules, but if you ever feed embeddings _in_ (NLA `prompt_embeds`), you must apply the scale
815
- yourself — `resolve_embed_scale` in the NLA path exists for exactly this.
814
+ modules. When you feed embeddings _in_ (`generate_steps_from_embeds`, which NLA's concept
815
+ injection rides), the rows must carry the scale already — they are the `embeddings` point, and
816
+ the NLA path's `resolve_embed_scale` applies it.
816
817
  - **Multimodal `*ForConditionalGeneration`**: dims live under `config.text_config` (top-level are
817
818
  `None`); the text trunk nests under `model.language_model`. `resolve_arch` handles both; just
818
819
  don't assume top-level `num_hidden_layers`. The vLLM worker resolves its _own_ module handles
@@ -126,11 +126,11 @@ The norm's _boundaries_ are `resid_mid` (input) and `mlp_in` (output), and the e
126
126
  for you rather than making you reproduce TransformerLens' `RMSNorm.forward` from memory:
127
127
 
128
128
  ```python
129
- from interp_engine import pre_gain_normalized, rms_norm_eps_for_model, run_with_cache, tlens_normalized_hook
129
+ from interp_engine import capture, pre_gain_normalized, rms_norm_eps_for_model, tlens_normalized_hook
130
130
 
131
131
  address = tlens_normalized_hook("blocks.19.ln2.hook_normalized") # -> Address("resid_mid", 19)
132
132
  eps = rms_norm_eps_for_model(model) # None on a LayerNorm family, where this equation does not apply
133
- cache = run_with_cache(model, tokens, [address])
133
+ cache = capture(model, tokens, [address])
134
134
  normalized = pre_gain_normalized(cache[address], eps)
135
135
  ```
136
136
 
@@ -246,7 +246,7 @@ module is present and never called. `router_logits` survives that anyway — `ml
246
246
  `return routed_out, router_logits`, so the logits the kernel routed on leave the block, and the point
247
247
  resolves to the block's own `output:1`, bit-identical to `F.linear(hidden, router.weight, router.bias)`.
248
248
  `expert_weights` and `expert_indices` have no boundary there at all — the selection happens inside the
249
- kernel — so on that path they are **rebuilt** from those logits by `run_with_cache`
249
+ kernel — so on that path they are **rebuilt** from those logits by `capture`
250
250
  (`interp_engine.moe_routing`), and `resolve_point` still refuses them because there is no address to
251
251
  return. The index the logits come off is allowlisted per replacement (`facts.INLINE_ROUTING_FORWARDS`)
252
252
  rather than assumed, because `output:1` on the _un-replaced_ gpt-oss block is `router_scores` — the
@@ -422,12 +422,13 @@ Both mechanisms carry the optional `stream` coordinate, which on a write means o
422
422
  in one row of a stack the worker already holds — a weaker claim than the one a read makes, which is why
423
423
  vLLM serves it here while still refusing to *read* a single stream through the resid points.
424
424
 
425
- A jlens steer/ablate/swap goes through the same gate and the same sites: its spec carries a `point`
426
- (defaulting to `resid_post`, which is what it has always meant on a conventional trunk) and the
427
- per-request path keys the intervention by site rather than by layer. That is what makes the lens usable
428
- on this family at all — `resid_post` there names the whole stack, which no sublayer reads, so the tensor
429
- a swap wants is a collapse. The global `set_lens_intervention` stays pinned to the decoder layer's output
430
- and refuses a spec that names a point, rather than ignoring it.
425
+ A lens steer/ablate/swap is a `steer()` block of `NormScaledAddSpec` / `AblateSpec` / `SwapSpec` ops
426
+ and goes through the same gate and the same sites: the spec's `point` (`resid_post` by default, which
427
+ is what it has always meant on a conventional trunk) is where the per-request path keys the write.
428
+ That is what makes the lens usable on this family at all — `resid_post` there names the whole stack,
429
+ which no sublayer reads, so the tensor a swap wants is a collapse. The global `set_lens_intervention`
430
+ stays pinned to the decoder layer's output and refuses a spec that names a point, rather than ignoring
431
+ it.
431
432
 
432
433
  ### On the AMD and XPU trees they are ordinary modules
433
434
 
@@ -26,7 +26,7 @@ gradient *of the model* is eager-only.
26
26
 
27
27
  ```python
28
28
  model = EagerModel("openai-community/gpt2", requires_grad=True) # default is False
29
- cache = run_with_cache(model, input_ids, [("resid_post", 5)], detach=False)
29
+ cache = capture(model, input_ids, [("resid_post", 5)], detach=False)
30
30
  cache.get("resid_post", 5).sum().backward()
31
31
  model.arch.embed.weight.grad # populated: the graph reached the embedding
32
32
  ```
@@ -25,7 +25,8 @@ files and why they are where they are.
25
25
  `sync.py`; `tests/test_sync_parity.py` fails on a missing twin.
26
26
  - `sync.py` / `_loop.py` — `sync_model(model)`: the protocol without an event loop, one explicit
27
27
  wrapper per method over a background loop thread that is created lazily, reused per model, and
28
- refuses rather than deadlocks when called from inside a running loop.
28
+ refuses rather than deadlocks when called from inside a running loop. Each call runs in a copy of
29
+ the caller's context, so an open `steer()` block is what the method reads on the loop thread.
29
30
  - `dispatch.py` — the shared plumbing every free function's two arms sit on: token coercion
30
31
  (`TokensLike`, batch refusals) and `CAPABILITIES`, the table each `CapabilityUnsupported` message
31
32
  is built from.
@@ -56,7 +57,11 @@ files and why they are where they are.
56
57
  `tests/test_steer_math_parity.py` runs against it on CPU; `steer()` is the context both backends
57
58
  take, registering per-request on vLLM rather than installing a global hook. The methods are named
58
59
  once, in `steer_specs.SteerMethod`: the eager `SteerSpec.method` and the worker dict's `op` both
59
- read from it, so a method has one spelling on either backend.
60
+ read from it, so a method has one spelling on either backend. A block's
61
+ `position_mask` and `generated=False` are placed by `forward_from` on eager, which every engine
62
+ loop declares, and travel with the request on vLLM (`worker_register_steering`'s
63
+ `skip_positions` / `steer_generated`); `tests/test_steer_scope.py` checks the rows on eager, in
64
+ closed form, and `tests/test_vllm_capture_gpu.py` holds vLLM to them.
60
65
  - `mappers.py` — translation between canonical points and other frameworks' names:
61
66
  TransformerLens hook strings and nnsight/nnterp accessors, both directions. See [Porting from
62
67
  TransformerLens, nnsight or nnterp](PORTING.md#porting-from-transformerlens-nnsight-or-nnterp).
@@ -24,6 +24,8 @@ the cache normally. Worth ~1.75x on time-to-first-token for a repeated long pref
24
24
  pins which requests opt out; `tests/test_vllm_capture_gpu.py` pins the effect on a real engine,
25
25
  including a control that reproduces the short capture when the salt is removed. Pass
26
26
  `extra_vllm_kwargs={"enable_prefix_caching": False}` to get the old engine-wide behaviour back.
27
+ An engine with `enable_prompt_embeds=True` (LoRA reads, NLA) keeps it on too: vLLM hashes each
28
+ block's embeds rows into its key, and embeds requests take the same global-intervention salt.
27
29
 
28
30
  One further flag is set for you, and only on the architectures that cannot boot without it.
29
31
  `kv_cache_dtype` is left to vLLM everywhere else, but a few families serve attention through a KV
@@ -110,9 +112,9 @@ What is genuinely **free** — do not attribute slowness to these:
110
112
  `enforce_eager` is required *while capturing*, not permanently, so a generation-only instance
111
113
  should turn it off — preferably as `backend="vllm-generate"`, which is the same engine configuration
112
114
  reached through a name that says what it gives up. There is already a precedent in-tree:
113
- `apps/nla/vllm_verbalizer.py` runs its own `VLLMModel` with `enforce_eager=False` (CUDA graphs +
114
- inductor on) precisely because the verbalizer installs no hooks, with
115
- `NLA_VERBALIZER_ENFORCE_EAGER=1` as a debug escape hatch. It also passes
115
+ Neuronpedia's `apps/nla/verbalizer.py` loads `backend="vllm-generate"` (CUDA graphs + inductor on)
116
+ precisely because the verbalizer installs no hooks, with `NLA_VERBALIZER_ENFORCE_EAGER=1` as a
117
+ debug escape hatch back to the hooked engine. It also passes
116
118
  `compilation_config.cudagraph_capture_sizes` to match graph capture to its expected fan-out.
117
119
 
118
120
  What it costs, measured (`benchmarks/results-latest.md`, and gpt2 separately for the graph modes).
@@ -164,9 +166,9 @@ them:
164
166
  defaults to `"auto"`: `resid_post` at every layer, to read *and* to write, via static `copy_`
165
167
  taps. Turns Dynamo off (`VLLM_USE_BREAKABLE_CUDAGRAPH=1`), so the win is replay without compile.
166
168
  Because breakable `add_eager` keeps the wrap as ordinary PyTorch on the live tensor, the write path
167
- is not limited to additive — `orthogonal`, `projection_cap` and the j-lens `steer`/`ablate`/`swap`
168
- ops all ride the same wrap, per-request and with `position_mask` honoured
169
- (`register_static_write`). An op outside that set is refused rather than silently skipped. Auto
169
+ is not limited to additive — `orthogonal`, `projection_cap` and the lens's
170
+ `norm_scaled_add`/`ablate`/`swap` ops all ride the same wrap, per-request and with `position_mask`
171
+ and `generated=False` honoured (`register_static_write`). An op outside that set is refused rather than silently skipped. Auto
170
172
  covers the write because the two halves are one decision: a read tap alone serves the lens
171
173
  read-out and refuses every steer, ablation and swap derived from it, at an address already tapped.
172
174
  Pass `static_writes=[]` for the reads without the write sites, and an explicit `static_points`