interp-engine 1.3.2__tar.gz → 1.3.4__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 (176) hide show
  1. {interp_engine-1.3.2 → interp_engine-1.3.4}/.gitignore +4 -1
  2. {interp_engine-1.3.2 → interp_engine-1.3.4}/PKG-INFO +7 -4
  3. {interp_engine-1.3.2 → interp_engine-1.3.4}/README.md +5 -2
  4. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/INTERNALS.md +6 -0
  5. interp_engine-1.3.4/interp_engine/notebook_stdout.py +90 -0
  6. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_backend.py +4 -0
  7. {interp_engine-1.3.2 → interp_engine-1.3.4}/pyproject.toml +2 -2
  8. interp_engine-1.3.4/tests/test_notebook_stdout.py +132 -0
  9. {interp_engine-1.3.2 → interp_engine-1.3.4}/LICENSE +0 -0
  10. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/README.md +0 -0
  11. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/__init__.py +0 -0
  12. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/bench_spec.py +0 -0
  13. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/cells.py +0 -0
  14. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/probe.py +0 -0
  15. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/probe_lens_stream.py +0 -0
  16. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/publish.py +0 -0
  17. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/report_bench.py +0 -0
  18. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
  19. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
  20. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark-cudagraph.json +0 -0
  21. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark.json +0 -0
  22. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
  23. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
  24. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/gemma-2-2b__eager.json +0 -0
  25. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
  26. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
  27. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
  28. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
  29. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
  30. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
  31. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
  32. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/qwen3-4b__eager.json +0 -0
  33. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
  34. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
  35. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/qwen3-4b__vllm.json +0 -0
  36. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
  37. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
  38. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
  39. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
  40. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/results-latest.md +0 -0
  41. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/run_all.sh +0 -0
  42. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/run_bench.py +0 -0
  43. {interp_engine-1.3.2 → interp_engine-1.3.4}/benchmarks/workloads.py +0 -0
  44. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/AGENT_INTEGRATION.md +0 -0
  45. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/ARCHITECTURE_QUIRKS.md +0 -0
  46. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/COMPATIBILITY.md +0 -0
  47. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
  48. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/GRADIENTS.md +0 -0
  49. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/PERFORMANCE.md +0 -0
  50. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/PORTING.md +0 -0
  51. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/README.md +0 -0
  52. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/SUPPORTED_POINTS.md +0 -0
  53. {interp_engine-1.3.2 → interp_engine-1.3.4}/docs/USAGE.md +0 -0
  54. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/__init__.py +0 -0
  55. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/_loop.py +0 -0
  56. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/address.py +0 -0
  57. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/arch.py +0 -0
  58. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/attn_config.py +0 -0
  59. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/attn_scores.py +0 -0
  60. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/autograd_support.py +0 -0
  61. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/capture.py +0 -0
  62. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/chat_compose.py +0 -0
  63. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/chat_conventions.py +0 -0
  64. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/chat_formatters.py +0 -0
  65. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/cuda_preflight.py +0 -0
  66. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/dispatch.py +0 -0
  67. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/facts.py +0 -0
  68. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/hooks.py +0 -0
  69. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/lens.py +0 -0
  70. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/load.py +0 -0
  71. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/mappers.py +0 -0
  72. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/model.py +0 -0
  73. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/moe_routing.py +0 -0
  74. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/points.py +0 -0
  75. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/protocol.py +0 -0
  76. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/residual_basis.py +0 -0
  77. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/select.py +0 -0
  78. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/steer.py +0 -0
  79. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/steer_specs.py +0 -0
  80. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/sync.py +0 -0
  81. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/tokenize.py +0 -0
  82. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/__init__.py +0 -0
  83. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/_demux.py +0 -0
  84. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/_hooks.py +0 -0
  85. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/_payload.py +0 -0
  86. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/_tree.py +0 -0
  87. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/attn.py +0 -0
  88. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/capture.py +0 -0
  89. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/graphs.py +0 -0
  90. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/lens/__init__.py +0 -0
  91. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/lens/intervene.py +0 -0
  92. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/lens/readout.py +0 -0
  93. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/lens/unembed.py +0 -0
  94. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/mhc.py +0 -0
  95. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/native.py +0 -0
  96. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/requests.py +0 -0
  97. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/static.py +0 -0
  98. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_capture/steering.py +0 -0
  99. {interp_engine-1.3.2 → interp_engine-1.3.4}/interp_engine/vllm_plugin.py +0 -0
  100. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/conftest.py +0 -0
  101. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/harness.py +0 -0
  102. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/model_expectations.yaml +0 -0
  103. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/synthetic_families.py +0 -0
  104. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_address.py +0 -0
  105. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_attn_config_tripwire.py +0 -0
  106. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_attn_probs_indexing.py +0 -0
  107. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_attn_scores.py +0 -0
  108. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_attn_z_gqa.py +0 -0
  109. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_autograd_support.py +0 -0
  110. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_bench_workloads.py +0 -0
  111. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_capability_refusals.py +0 -0
  112. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_capture_addressing.py +0 -0
  113. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_chat_compose.py +0 -0
  114. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_chat_formatters.py +0 -0
  115. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_chat_templates.py +0 -0
  116. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_core.py +0 -0
  117. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_cuda_preflight.py +0 -0
  118. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_doc_code_fences.py +0 -0
  119. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_eager_autograd.py +0 -0
  120. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_facts.py +0 -0
  121. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_family_points.py +0 -0
  122. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_gated_attn_out.py +0 -0
  123. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_head_contributions.py +0 -0
  124. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_hook_call_conventions.py +0 -0
  125. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_layer_kinds.py +0 -0
  126. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_load.py +0 -0
  127. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_logit_transform.py +0 -0
  128. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_mappers.py +0 -0
  129. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_mlp_internals.py +0 -0
  130. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_model_expectations.py +0 -0
  131. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_moe.py +0 -0
  132. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_multimodal_arch.py +0 -0
  133. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_new_models_gpu.py +0 -0
  134. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_no_chat_template.py +0 -0
  135. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_normalized_hook.py +0 -0
  136. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_packaging.py +0 -0
  137. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_parity_gpt2.py +0 -0
  138. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_per_layer_attn_dims.py +0 -0
  139. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_points_registry.py +0 -0
  140. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_protocol.py +0 -0
  141. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_published_benchmarks.py +0 -0
  142. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_qk_norm.py +0 -0
  143. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_qkv_layout.py +0 -0
  144. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_reasoning_spans.py +0 -0
  145. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_release.py +0 -0
  146. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_resid_mid.py +0 -0
  147. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_residual_basis.py +0 -0
  148. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_sandwich_norms.py +0 -0
  149. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_select.py +0 -0
  150. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_sliding_window_attn.py +0 -0
  151. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_small_models_gpu.py +0 -0
  152. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_static_dsv4_gpu.py +0 -0
  153. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_static_parity_gpu.py +0 -0
  154. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_static_set.py +0 -0
  155. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_static_warmup.py +0 -0
  156. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_steer_context.py +0 -0
  157. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_steer_math_parity.py +0 -0
  158. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_sync_loop.py +0 -0
  159. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_sync_parity.py +0 -0
  160. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_unified_free_functions.py +0 -0
  161. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_unresolved_families.py +0 -0
  162. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_capture_gpu.py +0 -0
  163. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_capture_scales.py +0 -0
  164. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_engine_loop.py +0 -0
  165. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_graph_path.py +0 -0
  166. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_graphs_on_gpu.py +0 -0
  167. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_hook_availability.py +0 -0
  168. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_hyper_connections.py +0 -0
  169. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_kv_isolation.py +0 -0
  170. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_new_points.py +0 -0
  171. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_only_families.py +0 -0
  172. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_plugin.py +0 -0
  173. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vllm_wire_grammar.py +0 -0
  174. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_vocabulary_boundary.py +0 -0
  175. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_worker_lens_capture_readout.py +0 -0
  176. {interp_engine-1.3.2 → interp_engine-1.3.4}/tests/test_worker_lens_readout.py +0 -0
@@ -33,4 +33,7 @@ dist/
33
33
  .venv-ci/
34
34
 
35
35
  plans/
36
- *.plan.md
36
+ *.plan.md
37
+
38
+ logs/*
39
+ logs/
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: interp-engine
3
- Version: 1.3.2
4
- Summary: A fast, standardized interpretability engine that supports most modern models and architectures. Powers Neuronpedia.
3
+ Version: 1.3.4
4
+ Summary: A fast, standardized, and easy to use interpretability engine.
5
5
  Project-URL: Homepage, https://github.com/decoderesearch/interp-engine
6
6
  Project-URL: Repository, https://github.com/decoderesearch/interp-engine
7
7
  Project-URL: Issues, https://github.com/decoderesearch/interp-engine/issues
@@ -44,10 +44,13 @@ Description-Content-Type: text/markdown
44
44
  <a href="https://join.slack.com/t/opensourcemechanistic/shared_invite/zt-3z9o0hxjl-MDX9pbATO2qESOazNDLpdQ"><img src="https://img.shields.io/badge/Slack-Open%20Source%20Mechanistic%20Interpretability-4A154B?logo=slack&logoColor=white" alt="Join the Slack"></a>
45
45
  </p>
46
46
 
47
- `interp-engine` is an interpretability engine that is fast (>40x tok/s vs HF eager), standardized (34 'points'/addresses across architectures), and easy to use and debug. It powers all of [Neuronpedia](https://neuronpedia.org)'s inference and is checked for accuracy against HF Transformers and other engines.
47
+ `interp-engine` is an interpretability engine that is fast, standardized (34 'points'/addresses across architectures), and easy to use and debug. It powers all of [Neuronpedia](https://neuronpedia.org)'s inference and is checked for accuracy against HF Transformers and other engines.
48
48
 
49
49
  <p align="center">
50
- <img src="https://neuronpedia.s3.amazonaws.com/site-assets/ie-perf.png" alt="Tokens per second while capturing and generating: eager against IE-vLLM and IE-vLLM-static on qwen3.8-27b and deepseek-v4-flash-0731, 8 requests in flight" width="100%">
50
+ <img src="https://neuronpedia.s3.amazonaws.com/site-assets/ie-benchmark.gif" alt="benchmark between ie and transformers" width="100%">
51
+ </p>
52
+ <p align="center">
53
+ <img src="https://neuronpedia.s3.amazonaws.com/site-assets/toks.png" alt="Tokens per second while capturing and generating: eager against IE-vLLM and IE-vLLM-static on qwen3.8-27b and deepseek-v4-flash-0731, 8 requests in flight" width="100%">
51
54
  </p>
52
55
  <p align="center">
53
56
  <img src="https://neuronpedia.s3.amazonaws.com/site-assets/interp-engine-demo.gif" alt="interp-engine demo gif" width="100%">
@@ -13,10 +13,13 @@
13
13
  <a href="https://join.slack.com/t/opensourcemechanistic/shared_invite/zt-3z9o0hxjl-MDX9pbATO2qESOazNDLpdQ"><img src="https://img.shields.io/badge/Slack-Open%20Source%20Mechanistic%20Interpretability-4A154B?logo=slack&logoColor=white" alt="Join the Slack"></a>
14
14
  </p>
15
15
 
16
- `interp-engine` is an interpretability engine that is fast (>40x tok/s vs HF eager), standardized (34 'points'/addresses across architectures), and easy to use and debug. It powers all of [Neuronpedia](https://neuronpedia.org)'s inference and is checked for accuracy against HF Transformers and other engines.
16
+ `interp-engine` is an interpretability engine that is fast, standardized (34 'points'/addresses across architectures), and easy to use and debug. It powers all of [Neuronpedia](https://neuronpedia.org)'s inference and is checked for accuracy against HF Transformers and other engines.
17
17
 
18
18
  <p align="center">
19
- <img src="https://neuronpedia.s3.amazonaws.com/site-assets/ie-perf.png" alt="Tokens per second while capturing and generating: eager against IE-vLLM and IE-vLLM-static on qwen3.8-27b and deepseek-v4-flash-0731, 8 requests in flight" width="100%">
19
+ <img src="https://neuronpedia.s3.amazonaws.com/site-assets/ie-benchmark.gif" alt="benchmark between ie and transformers" width="100%">
20
+ </p>
21
+ <p align="center">
22
+ <img src="https://neuronpedia.s3.amazonaws.com/site-assets/toks.png" alt="Tokens per second while capturing and generating: eager against IE-vLLM and IE-vLLM-static on qwen3.8-27b and deepseek-v4-flash-0731, 8 requests in flight" width="100%">
20
23
  </p>
21
24
  <p align="center">
22
25
  <img src="https://neuronpedia.s3.amazonaws.com/site-assets/interp-engine-demo.gif" alt="interp-engine demo gif" width="100%">
@@ -66,6 +66,12 @@ files and why they are where they are.
66
66
  - `LD_LIBRARY_PATH`) before the first CUDA call, instead of failing ten frames deep in
67
67
  `torch.cuda._lazy_init`. Lives here because every app on the engine inherits the same CUDA
68
68
  floor — the `[vllm]` wheels link `libcudart.so.13` directly.
69
+ - `notebook_stdout.py` — `ensure_stdout_descriptor`: gives a notebook kernel's `sys.stdout` the file
70
+ descriptor vLLM's engine start needs. vLLM silences C-level output by dup'ing over one and forks
71
+ its EngineCore child, so under ipykernel — whose stream writes to a socket and has none — the
72
+ child dies before it loads anything, and the caller is told to see a root cause that is in another
73
+ process. Called from `_ensure_engine`; a no-op in a script, a server, or a kernel that captured
74
+ the real descriptors itself.
69
75
 
70
76
  ## Correctness
71
77
 
@@ -0,0 +1,90 @@
1
+ """Give a notebook kernel's stdout the file descriptor vLLM's engine start needs.
2
+
3
+ vLLM silences C-level output around ``torch.distributed.new_group`` by dup'ing over a
4
+ descriptor (``suppress_stdout`` in ``vllm/utils/system_utils.py``), and its EngineCore
5
+ child is forked, so the child inherits whichever ``sys.stdout`` the parent had. Under
6
+ Jupyter that is ``ipykernel.iostream.OutStream``, which writes through a ZMQ socket and
7
+ raises ``io.UnsupportedOperation`` from ``fileno()``. The child then dies before it loads
8
+ anything, and what reaches the caller is vLLM's ``Engine core initialization failed. See
9
+ root cause above.`` -- above being a traceback in another process, about a descriptor.
10
+
11
+ A descriptor is what that stream is supposed to hand back: ipykernel answers ``fileno()``
12
+ with ``_original_stdstream_copy``, a dup of the real stdout it took over, whenever the
13
+ kernel was started capturing the low-level ones. This supplies the one a kernel that was
14
+ not started that way -- Colab's -- did not keep. Colab is the case that matters, because
15
+ this repo's own notebook templates run there.
16
+
17
+ The descriptor stays on the stream rather than being restored after the engine is built.
18
+ The fork is not the only caller: vLLM suppresses stdout the same way around its stateless
19
+ process groups, from whichever process reaches one.
20
+
21
+ Forcing ``spawn`` would also give the child a stdout of its own, and is what
22
+ ``VLLM_WORKER_MULTIPROC_METHOD`` is for. It is not what this does, because a spawned
23
+ child writes to the kernel's real stdout rather than to the cell -- which takes the engine
24
+ logs, and the root cause behind that message, out of the notebook with it.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import logging
30
+ import os
31
+ import sys
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ def _answers_fileno(stream: object) -> bool:
37
+ """Whether ``stream.fileno()`` returns rather than raises.
38
+
39
+ ``io.UnsupportedOperation`` is both an ``OSError`` and a ``ValueError``, which is what
40
+ a kernel stream raises; a detached or closed one raises ``ValueError`` on its own.
41
+ """
42
+ try:
43
+ stream.fileno() # type: ignore[attr-defined]
44
+ except (AttributeError, OSError, ValueError):
45
+ return False
46
+ return True
47
+
48
+
49
+ def _process_stdout_fd() -> int | None:
50
+ """The descriptor this process's stdout is, or None when it has none.
51
+
52
+ ``sys.__stdout__`` is the stream Python opened at startup, which Jupyter leaves alone
53
+ when it replaces ``sys.stdout``, so it is the first place to ask. Fd 1 covers an
54
+ embedding that replaced both. Either is confirmed with ``fstat`` before being handed
55
+ out, since a daemonized process may have closed it.
56
+ """
57
+ original = sys.__stdout__
58
+ fd = original.fileno() if original is not None and _answers_fileno(original) else 1
59
+ try:
60
+ os.fstat(fd)
61
+ except OSError:
62
+ return None
63
+ return fd
64
+
65
+
66
+ def ensure_stdout_descriptor() -> bool:
67
+ """Give ``sys.stdout`` a ``fileno()`` if it has none, and say whether it needed one.
68
+
69
+ Idempotent, and a no-op outside a notebook. A stream that already answers is left
70
+ alone, and so is a process whose stdout is closed: there is no descriptor to offer
71
+ there, and vLLM's refusal is then the honest outcome rather than one to work around.
72
+ """
73
+ stream = sys.stdout
74
+ if stream is None or _answers_fileno(stream):
75
+ return False
76
+ fd = _process_stdout_fd()
77
+ if fd is None:
78
+ return False
79
+ try:
80
+ stream.fileno = lambda: fd # type: ignore[method-assign]
81
+ except (AttributeError, TypeError):
82
+ # A stream that takes no new attribute, which a C-level or slotted one does not.
83
+ # Nothing is lost: this is the refusal vLLM was about to raise anyway.
84
+ return False
85
+ logger.info(
86
+ "%s has no file descriptor, which vLLM's engine start requires; answering fileno() with fd %d.",
87
+ type(stream).__name__,
88
+ fd,
89
+ )
90
+ return True
@@ -30,6 +30,7 @@ from interp_engine import facts
30
30
  from interp_engine._loop import refuse_foreign_loop
31
31
  from interp_engine.address import Address, format_address, to_address
32
32
  from interp_engine.autograd_support import GradSupport, vllm_grad_support
33
+ from interp_engine.notebook_stdout import ensure_stdout_descriptor
33
34
  from interp_engine.points import d_model_wide, hyper_connection_names, refusal_reasons
34
35
  from interp_engine.points import steer_refusal_reason as points_steer_refusal
35
36
  from interp_engine.residual_basis import ResidualBasis, vllm_residual_basis
@@ -1213,6 +1214,9 @@ class VLLMModel:
1213
1214
  tuple(getattr(self, "_static_reads", ())),
1214
1215
  tuple(getattr(self, "_static_writes", ())),
1215
1216
  )
1217
+ # The line below forks a child that suppresses stdout by descriptor, which a
1218
+ # notebook kernel's stdout does not have. See `notebook_stdout`.
1219
+ ensure_stdout_descriptor()
1216
1220
  self.engine = AsyncLLM.from_engine_args(AsyncEngineArgs(**self._engine_kwargs))
1217
1221
  self._engine_loop = asyncio.get_running_loop()
1218
1222
  return self.engine
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "interp-engine"
3
- version = "1.3.2"
4
- description = "A fast, standardized interpretability engine that supports most modern models and architectures. Powers Neuronpedia."
3
+ version = "1.3.4"
4
+ description = "A fast, standardized, and easy to use interpretability engine."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
7
7
  license-files = ["LICENSE"]
@@ -0,0 +1,132 @@
1
+ """Unit tests for the stdout descriptor a notebook kernel does not provide.
2
+
3
+ No vLLM and no GPU: what is under test is a property of ``sys.stdout``, and the stream
4
+ that has that property in the field -- ipykernel's ``OutStream`` -- is stood in for by a
5
+ class which raises from ``fileno()`` the same way. The engine-shaped half is a vLLM engine
6
+ started under such a stream, which fails in the EngineCore child rather than here.
7
+
8
+ ``sys.stdout`` is replaced **inside each test body** rather than by a fixture. pytest
9
+ captures at the descriptor level and reassigns ``sys.stdout`` every time it resumes
10
+ capture, including on the way from setup into the call phase -- so a patch applied in a
11
+ fixture is gone by the time the test runs, and the test passes or fails on the runner's
12
+ stream instead of the one it meant to use.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import io
18
+ import os
19
+ import sys
20
+ from typing import IO, Any
21
+
22
+ import pytest
23
+
24
+ from interp_engine.notebook_stdout import ensure_stdout_descriptor
25
+
26
+
27
+ class KernelStdout:
28
+ """``ipykernel.iostream.OutStream``, as far as this matters.
29
+
30
+ Writes go somewhere that is not a file -- a ZMQ socket, in the real one -- so there is
31
+ no descriptor to answer with, which is the whole of the failure being reproduced.
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ self.written: list[str] = []
36
+
37
+ def write(self, text: str) -> int:
38
+ self.written.append(text)
39
+ return len(text)
40
+
41
+ def flush(self) -> None:
42
+ pass
43
+
44
+ def fileno(self) -> int:
45
+ raise io.UnsupportedOperation("fileno")
46
+
47
+
48
+ class SlottedStdout:
49
+ """A stream that takes no new attribute, as a C-level one does not."""
50
+
51
+ __slots__ = ()
52
+
53
+ def fileno(self) -> int:
54
+ raise io.UnsupportedOperation("fileno")
55
+
56
+
57
+ def _as_stdout(monkeypatch: pytest.MonkeyPatch, stream: object) -> None:
58
+ """Make ``stream`` this test's ``sys.stdout``. Call from the test body; see above."""
59
+ monkeypatch.setattr(sys, "stdout", stream)
60
+
61
+
62
+ def test_a_kernel_stream_gains_a_descriptor_the_os_accepts(monkeypatch: pytest.MonkeyPatch) -> None:
63
+ """The point of the exercise: ``fileno()`` answers, and with a live descriptor.
64
+
65
+ ``fstat`` is the assertion rather than a particular number, because what vLLM does
66
+ with the answer is ``os.dup``, which fails on anything ``fstat`` would reject too.
67
+ """
68
+ _as_stdout(monkeypatch, KernelStdout())
69
+ assert ensure_stdout_descriptor() is True
70
+ os.fstat(sys.stdout.fileno())
71
+
72
+
73
+ def test_writes_still_go_where_they_did(monkeypatch: pytest.MonkeyPatch) -> None:
74
+ """A descriptor is added; nothing is redirected. The cell keeps its output."""
75
+ stream = KernelStdout()
76
+ _as_stdout(monkeypatch, stream)
77
+ ensure_stdout_descriptor()
78
+ print("to the notebook")
79
+ assert "to the notebook" in "".join(stream.written)
80
+
81
+
82
+ def test_it_is_idempotent(monkeypatch: pytest.MonkeyPatch) -> None:
83
+ """Every engine build calls this, and only the first one has anything to do."""
84
+ _as_stdout(monkeypatch, KernelStdout())
85
+ assert ensure_stdout_descriptor() is True
86
+ assert ensure_stdout_descriptor() is False
87
+
88
+
89
+ def test_a_real_stream_is_left_alone(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
90
+ """A script's stdout already answers, so this must not touch it.
91
+
92
+ The second assertion is what says *untouched* rather than merely still working: a
93
+ descriptor of our own, put on the instance, would satisfy the first one too.
94
+ """
95
+ with open(tmp_path / "out", "w") as real:
96
+ _as_stdout(monkeypatch, real)
97
+ assert ensure_stdout_descriptor() is False
98
+ assert "fileno" not in getattr(real, "__dict__", {})
99
+
100
+
101
+ def test_a_stream_that_takes_no_attribute_is_refused(monkeypatch: pytest.MonkeyPatch) -> None:
102
+ """Refused rather than raised: vLLM's own error is a better one than ours here."""
103
+ _as_stdout(monkeypatch, SlottedStdout())
104
+ assert ensure_stdout_descriptor() is False
105
+
106
+
107
+ def test_a_closed_process_stdout_offers_nothing(monkeypatch: pytest.MonkeyPatch) -> None:
108
+ """With none to lend, the answer is no rather than a number that fails later.
109
+
110
+ This is the daemonized process, whose stdout was closed before Python started. Both
111
+ sources are taken away: ``sys.__stdout__``, and the fd 1 fallback that stands in for
112
+ it, which is open in a test runner and would otherwise be handed out.
113
+ """
114
+ _as_stdout(monkeypatch, KernelStdout())
115
+ monkeypatch.setattr(sys, "__stdout__", None)
116
+ monkeypatch.setattr("interp_engine.notebook_stdout.os.fstat", _raise_ebadf)
117
+ assert ensure_stdout_descriptor() is False
118
+
119
+
120
+ def _raise_ebadf(fd: int) -> os.stat_result:
121
+ raise OSError(9, "Bad file descriptor")
122
+
123
+
124
+ def test_the_stand_in_matches_ipykernel() -> None:
125
+ """The premise, asserted where it can be: ``fileno()`` raises what is caught for.
126
+
127
+ ``io.UnsupportedOperation`` is both an ``OSError`` and a ``ValueError``, which is why
128
+ the module catches those two rather than importing ipykernel to name the class.
129
+ """
130
+ stream: IO[Any] = KernelStdout() # type: ignore[assignment]
131
+ with pytest.raises((OSError, ValueError)):
132
+ stream.fileno()
File without changes