interp-engine 1.2.7__tar.gz → 1.2.9__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 (174) hide show
  1. {interp_engine-1.2.7 → interp_engine-1.2.9}/PKG-INFO +12 -12
  2. {interp_engine-1.2.7 → interp_engine-1.2.9}/README.md +11 -11
  3. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/README.md +24 -6
  4. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/bench_spec.py +6 -4
  5. interp_engine-1.2.9/benchmarks/probe_lens_stream.py +622 -0
  6. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/AGENT_INTEGRATION.md +4 -1
  7. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/PERFORMANCE.md +30 -9
  8. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/USAGE.md +11 -0
  9. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/_loop.py +47 -4
  10. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/protocol.py +6 -0
  11. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_backend.py +28 -0
  12. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/_hooks.py +25 -0
  13. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/freeze.py +22 -8
  14. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/requests.py +1 -17
  15. {interp_engine-1.2.7 → interp_engine-1.2.9}/pyproject.toml +1 -1
  16. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_sync_loop.py +46 -1
  17. interp_engine-1.2.9/tests/test_vllm_engine_loop.py +101 -0
  18. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_hyper_connections.py +276 -0
  19. {interp_engine-1.2.7 → interp_engine-1.2.9}/.gitignore +0 -0
  20. {interp_engine-1.2.7 → interp_engine-1.2.9}/LICENSE +0 -0
  21. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/__init__.py +0 -0
  22. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/cells.py +0 -0
  23. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/probe.py +0 -0
  24. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/publish.py +0 -0
  25. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/report_bench.py +0 -0
  26. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
  27. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
  28. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark-cudagraph.json +0 -0
  29. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark.json +0 -0
  30. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/deepseek-v4-flash-0731__vllm-freeze.json +0 -0
  31. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
  32. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/gemma-2-2b__eager.json +0 -0
  33. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
  34. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/gemma-2-2b__vllm-freeze.json +0 -0
  35. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
  36. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
  37. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
  38. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/llama-3.1-8b__vllm-freeze.json +0 -0
  39. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
  40. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/qwen3-4b__eager.json +0 -0
  41. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
  42. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/qwen3-4b__vllm-freeze.json +0 -0
  43. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/qwen3-4b__vllm.json +0 -0
  44. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
  45. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
  46. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/qwen3.8-27b__vllm-freeze.json +0 -0
  47. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
  48. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/results-latest.md +0 -0
  49. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/run_all.sh +0 -0
  50. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/run_bench.py +0 -0
  51. {interp_engine-1.2.7 → interp_engine-1.2.9}/benchmarks/workloads.py +0 -0
  52. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/ARCHITECTURE_QUIRKS.md +0 -0
  53. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/COMPATIBILITY.md +0 -0
  54. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
  55. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/GRADIENTS.md +0 -0
  56. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/INTERNALS.md +0 -0
  57. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/PORTING.md +0 -0
  58. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/README.md +0 -0
  59. {interp_engine-1.2.7 → interp_engine-1.2.9}/docs/SUPPORTED_POINTS.md +0 -0
  60. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/__init__.py +0 -0
  61. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/address.py +0 -0
  62. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/arch.py +0 -0
  63. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/attn_config.py +0 -0
  64. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/attn_scores.py +0 -0
  65. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/autograd_support.py +0 -0
  66. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/capture.py +0 -0
  67. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/chat_compose.py +0 -0
  68. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/chat_conventions.py +0 -0
  69. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/chat_formatters.py +0 -0
  70. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/cuda_preflight.py +0 -0
  71. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/dispatch.py +0 -0
  72. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/facts.py +0 -0
  73. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/hooks.py +0 -0
  74. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/lens.py +0 -0
  75. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/load.py +0 -0
  76. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/mappers.py +0 -0
  77. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/model.py +0 -0
  78. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/moe_routing.py +0 -0
  79. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/points.py +0 -0
  80. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/residual_basis.py +0 -0
  81. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/select.py +0 -0
  82. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/steer.py +0 -0
  83. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/steer_specs.py +0 -0
  84. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/sync.py +0 -0
  85. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/tokenize.py +0 -0
  86. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/__init__.py +0 -0
  87. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/_demux.py +0 -0
  88. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/_payload.py +0 -0
  89. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/_tree.py +0 -0
  90. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/attn.py +0 -0
  91. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/capture.py +0 -0
  92. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/graphs.py +0 -0
  93. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/lens/__init__.py +0 -0
  94. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/lens/intervene.py +0 -0
  95. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/lens/readout.py +0 -0
  96. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/lens/unembed.py +0 -0
  97. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/mhc.py +0 -0
  98. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/native.py +0 -0
  99. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_capture/steering.py +0 -0
  100. {interp_engine-1.2.7 → interp_engine-1.2.9}/interp_engine/vllm_plugin.py +0 -0
  101. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/conftest.py +0 -0
  102. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/harness.py +0 -0
  103. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/model_expectations.yaml +0 -0
  104. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/synthetic_families.py +0 -0
  105. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_address.py +0 -0
  106. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_attn_config_tripwire.py +0 -0
  107. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_attn_probs_indexing.py +0 -0
  108. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_attn_scores.py +0 -0
  109. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_attn_z_gqa.py +0 -0
  110. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_autograd_support.py +0 -0
  111. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_bench_workloads.py +0 -0
  112. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_capability_refusals.py +0 -0
  113. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_capture_addressing.py +0 -0
  114. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_chat_compose.py +0 -0
  115. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_chat_formatters.py +0 -0
  116. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_chat_templates.py +0 -0
  117. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_core.py +0 -0
  118. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_cuda_preflight.py +0 -0
  119. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_doc_code_fences.py +0 -0
  120. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_eager_autograd.py +0 -0
  121. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_facts.py +0 -0
  122. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_family_points.py +0 -0
  123. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_freeze_dsv4_gpu.py +0 -0
  124. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_freeze_parity_gpu.py +0 -0
  125. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_freeze_set.py +0 -0
  126. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_freeze_warmup.py +0 -0
  127. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_gated_attn_out.py +0 -0
  128. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_head_contributions.py +0 -0
  129. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_hook_call_conventions.py +0 -0
  130. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_layer_kinds.py +0 -0
  131. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_load.py +0 -0
  132. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_logit_transform.py +0 -0
  133. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_mappers.py +0 -0
  134. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_mlp_internals.py +0 -0
  135. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_model_expectations.py +0 -0
  136. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_moe.py +0 -0
  137. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_multimodal_arch.py +0 -0
  138. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_new_models_gpu.py +0 -0
  139. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_no_chat_template.py +0 -0
  140. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_normalized_hook.py +0 -0
  141. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_packaging.py +0 -0
  142. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_parity_gpt2.py +0 -0
  143. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_per_layer_attn_dims.py +0 -0
  144. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_points_registry.py +0 -0
  145. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_protocol.py +0 -0
  146. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_published_benchmarks.py +0 -0
  147. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_qk_norm.py +0 -0
  148. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_qkv_layout.py +0 -0
  149. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_reasoning_spans.py +0 -0
  150. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_release.py +0 -0
  151. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_resid_mid.py +0 -0
  152. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_residual_basis.py +0 -0
  153. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_sandwich_norms.py +0 -0
  154. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_select.py +0 -0
  155. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_sliding_window_attn.py +0 -0
  156. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_small_models_gpu.py +0 -0
  157. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_steer_context.py +0 -0
  158. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_steer_math_parity.py +0 -0
  159. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_sync_parity.py +0 -0
  160. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_unified_free_functions.py +0 -0
  161. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_unresolved_families.py +0 -0
  162. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_capture_gpu.py +0 -0
  163. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_capture_scales.py +0 -0
  164. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_graph_path.py +0 -0
  165. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_graphs_on_gpu.py +0 -0
  166. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_hook_availability.py +0 -0
  167. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_kv_isolation.py +0 -0
  168. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_new_points.py +0 -0
  169. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_only_families.py +0 -0
  170. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_plugin.py +0 -0
  171. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vllm_wire_grammar.py +0 -0
  172. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_vocabulary_boundary.py +0 -0
  173. {interp_engine-1.2.7 → interp_engine-1.2.9}/tests/test_worker_lens_capture_readout.py +0 -0
  174. {interp_engine-1.2.7 → interp_engine-1.2.9}/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.7
3
+ Version: 1.2.9
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
@@ -71,7 +71,15 @@ pip install interp-engine # eager backend only
71
71
  ```python
72
72
  from interp_engine import Address, load_model, run_with_cache
73
73
 
74
- model = load_model("Qwen/Qwen3-8B") # vLLM default, use backend='eager' to override
74
+ # VLLM MODE (default): low VRAM, medium speed
75
+ model = load_model("Qwen/Qwen3-8B")
76
+
77
+ # VLLM-FREEZE MODE: high VRAM, high speed, only frozen points (default resid_post)
78
+ # model = load_model("Qwen/Qwen3-8B", freeze_points="auto")
79
+
80
+ # EAGER MODE: low VRAM, low speed
81
+ # model = load_model("Qwen/Qwen3-8B", backend="eager")
82
+
75
83
  point = Address("resid_post", 10) # or string: "resid_post.10"
76
84
  cache = run_with_cache(model, model.to_tokens("Hello, world"), [point])
77
85
  cache[point] # [batch, pos, ...]
@@ -87,7 +95,7 @@ Add "use interp-engine" to your prompt and let your agent figure it out - everyt
87
95
 
88
96
  ## Performance / Speed
89
97
 
90
- vLLM gives `interp-engine` high throughput via concurrency, and **graph freeze** adds CUDA-graph replay on top of that _without giving up capture or steering_. Every column below is capture-capable.
98
+ vLLM gives `interp-engine` high throughput via concurrency, and **graph freeze** adds CUDA-graph replay on top of that *without giving up capture or steering*. Every column below is capture-capable.
91
99
 
92
100
  <!-- THROUGHPUT:START -->
93
101
 
@@ -117,15 +125,7 @@ One stream (tok/s):
117
125
 
118
126
  <!-- THROUGHPUT:END -->
119
127
 
120
- Every multiplier is against eager on the same workload, and is the ratio of the _unrounded_ figures — tok/s is printed whole at 10 and above, so dividing two cells by hand can differ in the last place. Eager's generation loop is synchronous underneath, so eight requests serialize rather than batch — which is why it earns almost nothing from the second table and why the multipliers there are so much larger. `deepseek-v4-flash-0731` is block-quantized FP8 on both backends rather than bf16, so read its row as two backends serving the same quantized weights; it also keeps a tenth, because at 3 tok/s that digit is worth several percent. [`benchmarks/results-latest.md`](benchmarks/results-latest.md) has every figure at full precision.
121
-
122
- **Graph freeze** is opt-in, via `freeze_points`. Ordinary hooks cannot survive CUDA graphs — replay never calls the Python forward a hook is attached to — so the tap becomes a preallocated buffer plus a `copy_` that graph capture records and replay re-executes. Steering rides the same wrap, so additive, orthogonal, projection-cap and j-lens steer/ablate/swap all keep working under graphs, position masks included.
123
-
124
- ```python
125
- model = load_model("Qwen/Qwen3-8B", freeze_points="auto") # resid_post at every layer, under graphs
126
- ```
127
-
128
- The trade is that a frozen engine serves _the set it froze_ rather than any point on request: `"auto"` covers `resid_post` at every layer on a conventional trunk, an explicit list covers whatever you name, and anything outside the set is refused rather than quietly returned empty. Omit `freeze_points` for today's hooked vLLM, which still serves every point. `qwen3.8-27b` is a hybrid trunk, so its freeze row runs prefill eagerly and keeps the decode graphs — freeze pins that mode, because breakable graphs turn `torch.compile` off and vLLM's mixed prefill-decode capture then miscomputes prefill on a gated-delta trunk. On a hyper-connection trunk such as `deepseek-v4-flash-0731`, whose block carries four parallel residual streams, `"auto"` freezes `resid_streams` instead — the whole stack per layer, which is four times the width and so four times the buffer, and is what that row's freeze cell prices. See [PERFORMANCE.md](docs/PERFORMANCE.md) for the full trade-off, and [`benchmarks/results-latest.md`](benchmarks/results-latest.md) for the sweep, including capture, steering and lens latencies.
128
+ **Graph freeze** is opt-in via `freeze_points`, and a frozen engine serves only the set it froze. [PERFORMANCE.md](docs/PERFORMANCE.md) has how it works and what it trades; [benchmarks/results-latest.md](benchmarks/results-latest.md) has every figure at full precision, including capture, steering and lens latencies; [benchmarks/README.md](benchmarks/README.md) has how the tables above are rounded.
129
129
 
130
130
  ## Correctness
131
131
 
@@ -40,7 +40,15 @@ pip install interp-engine # eager backend only
40
40
  ```python
41
41
  from interp_engine import Address, load_model, run_with_cache
42
42
 
43
- model = load_model("Qwen/Qwen3-8B") # vLLM default, use backend='eager' to override
43
+ # VLLM MODE (default): low VRAM, medium speed
44
+ model = load_model("Qwen/Qwen3-8B")
45
+
46
+ # VLLM-FREEZE MODE: high VRAM, high speed, only frozen points (default resid_post)
47
+ # model = load_model("Qwen/Qwen3-8B", freeze_points="auto")
48
+
49
+ # EAGER MODE: low VRAM, low speed
50
+ # model = load_model("Qwen/Qwen3-8B", backend="eager")
51
+
44
52
  point = Address("resid_post", 10) # or string: "resid_post.10"
45
53
  cache = run_with_cache(model, model.to_tokens("Hello, world"), [point])
46
54
  cache[point] # [batch, pos, ...]
@@ -56,7 +64,7 @@ Add "use interp-engine" to your prompt and let your agent figure it out - everyt
56
64
 
57
65
  ## Performance / Speed
58
66
 
59
- vLLM gives `interp-engine` high throughput via concurrency, and **graph freeze** adds CUDA-graph replay on top of that _without giving up capture or steering_. Every column below is capture-capable.
67
+ vLLM gives `interp-engine` high throughput via concurrency, and **graph freeze** adds CUDA-graph replay on top of that *without giving up capture or steering*. Every column below is capture-capable.
60
68
 
61
69
  <!-- THROUGHPUT:START -->
62
70
 
@@ -86,15 +94,7 @@ One stream (tok/s):
86
94
 
87
95
  <!-- THROUGHPUT:END -->
88
96
 
89
- Every multiplier is against eager on the same workload, and is the ratio of the _unrounded_ figures — tok/s is printed whole at 10 and above, so dividing two cells by hand can differ in the last place. Eager's generation loop is synchronous underneath, so eight requests serialize rather than batch — which is why it earns almost nothing from the second table and why the multipliers there are so much larger. `deepseek-v4-flash-0731` is block-quantized FP8 on both backends rather than bf16, so read its row as two backends serving the same quantized weights; it also keeps a tenth, because at 3 tok/s that digit is worth several percent. [`benchmarks/results-latest.md`](benchmarks/results-latest.md) has every figure at full precision.
90
-
91
- **Graph freeze** is opt-in, via `freeze_points`. Ordinary hooks cannot survive CUDA graphs — replay never calls the Python forward a hook is attached to — so the tap becomes a preallocated buffer plus a `copy_` that graph capture records and replay re-executes. Steering rides the same wrap, so additive, orthogonal, projection-cap and j-lens steer/ablate/swap all keep working under graphs, position masks included.
92
-
93
- ```python
94
- model = load_model("Qwen/Qwen3-8B", freeze_points="auto") # resid_post at every layer, under graphs
95
- ```
96
-
97
- The trade is that a frozen engine serves _the set it froze_ rather than any point on request: `"auto"` covers `resid_post` at every layer on a conventional trunk, an explicit list covers whatever you name, and anything outside the set is refused rather than quietly returned empty. Omit `freeze_points` for today's hooked vLLM, which still serves every point. `qwen3.8-27b` is a hybrid trunk, so its freeze row runs prefill eagerly and keeps the decode graphs — freeze pins that mode, because breakable graphs turn `torch.compile` off and vLLM's mixed prefill-decode capture then miscomputes prefill on a gated-delta trunk. On a hyper-connection trunk such as `deepseek-v4-flash-0731`, whose block carries four parallel residual streams, `"auto"` freezes `resid_streams` instead — the whole stack per layer, which is four times the width and so four times the buffer, and is what that row's freeze cell prices. See [PERFORMANCE.md](docs/PERFORMANCE.md) for the full trade-off, and [`benchmarks/results-latest.md`](benchmarks/results-latest.md) for the sweep, including capture, steering and lens latencies.
97
+ **Graph freeze** is opt-in via `freeze_points`, and a frozen engine serves only the set it froze. [PERFORMANCE.md](docs/PERFORMANCE.md) has how it works and what it trades; [benchmarks/results-latest.md](benchmarks/results-latest.md) has every figure at full precision, including capture, steering and lens latencies; [benchmarks/README.md](benchmarks/README.md) has how the tables above are rounded.
98
98
 
99
99
  ## Correctness
100
100
 
@@ -104,6 +104,21 @@ python -m benchmarks.publish # rewrite both
104
104
  python -m benchmarks.publish --check # exit 1 if either has drifted, and name it
105
105
  ```
106
106
 
107
+ Both print the same cells in a display form of their own, applied by `publish.py` and pinned by
108
+ `tests/test_published_benchmarks.py`:
109
+
110
+ - **tok/s is whole at 10 and above, one decimal below.** A tenth beside a four-digit figure in the
111
+ next column claims a resolution the reader cannot use. At 3 tok/s that same tenth is worth several
112
+ percent, so the small rows keep it.
113
+ - **every comparison is a multiplier, never a percent**, with one decimal below 20x, where it is
114
+ still checkable against the two printed figures. `+20%` beside `27x` makes the reader convert one
115
+ of them.
116
+ - **multipliers are ratios of the unrounded metrics**, each against eager on the same workload, so
117
+ dividing two printed figures by hand can differ in the last place. Ratios of the rounded figures
118
+ would make the published win depend on the rounding, which is worse.
119
+
120
+ `results-latest.md` keeps every figure at full precision, so nothing is lost to those rules.
121
+
107
122
  `tests/test_published_benchmarks.py` runs that check over the committed cells, so a stale copy is a
108
123
  red suite rather than a claim nobody re-read. The visualizer's chatbot answers out of a bundle holding
109
124
  the README verbatim, so a publish that changed the README also wants `make viz-knowledge` -- the
@@ -225,12 +240,15 @@ already refers to, so a replay serves capture and steering without a Python forw
225
240
  `freeze_points="auto"` takes in production, and the row exists to price it against the
226
241
  `enforce_eager=True` column capture would otherwise have to use.
227
242
 
228
- `"auto"` installs **reads** only, and a steering op needs a write tap to land in, so this row priced
229
- half the feature and reported the other half as `n/a` -- with a message that blamed graph replay for
230
- it, which is the thing freeze exists to work around. It now also passes `freeze_writes`, whose value
231
- is the sentinel `run_bench.STEER_WRITES` rather than a site: the `steer` workload writes mid-stack,
232
- that layer differs per model, and a freeze write is a `load_model` argument, so it has to be resolved
233
- from the config before a model exists to ask.
243
+ `"auto"` once installed **reads** only, and a steering op needs a write tap to land in, so this row
244
+ priced half the feature and reported the other half as `n/a` -- with a message that blamed graph
245
+ replay for it, which is the thing freeze exists to work around. Auto now covers both halves, so the
246
+ cell would be a number either way, and this row still passes `freeze_writes` on purpose: an explicit
247
+ list *narrows* what auto would install, to the one mid-stack site the `steer` workload actually
248
+ writes. A row that priced a write buffer at every layer would not be comparable with the ones beside
249
+ it, which is the whole job of the column. Its value is the sentinel `run_bench.STEER_WRITES` rather
250
+ than a site, because that layer differs per model and a freeze write is a `load_model` argument, so
251
+ it has to be resolved from the config before a model exists to ask.
234
252
 
235
253
  `VariantSpec.models` restricts the row to the checkpoints freeze has been shown correct on, so a model
236
254
  missing from it renders `--` rather than a number nobody checked.
@@ -264,12 +264,14 @@ VARIANTS: tuple[VariantSpec, ...] = (
264
264
  # Its write tap is `mlp_out`, not `resid_post`: the steer workload addresses the model's
265
265
  # `capture_point`, and a hyper-connection trunk refuses the default name (`run_bench._steer_site`).
266
266
  #
267
- # `freeze_writes` is what makes the `steer` cell a number instead of `n/a`. `"auto"` installs
267
+ # `freeze_writes` was once what made the `steer` cell a number instead of `n/a`: `"auto"` installed
268
268
  # reads, and a steering op needs a write tap to land in, so this row priced capture under replay
269
269
  # and left the other half of the feature unmeasured -- with a message that blamed graph replay for
270
- # it. The value is the sentinel `run_bench.STEER_WRITES`, resolved there to the mid-stack
271
- # `resid_post` the workload steers, because the layer differs per model and a freeze write has to
272
- # be named before the model exists.
270
+ # it. Auto covers writes now, so the cell stands either way, and naming them here has become a
271
+ # *narrowing*: one write buffer at the site the workload steers rather than one per layer, which
272
+ # is what keeps this row's memory comparable with the columns beside it. The value is the sentinel
273
+ # `run_bench.STEER_WRITES`, resolved there to the mid-stack `resid_post` the workload steers,
274
+ # because the layer differs per model and a freeze write has to be named before the model exists.
273
275
  VariantSpec(
274
276
  "vllm-freeze",
275
277
  "vllm",