interp-engine 1.7.0__tar.gz → 1.7.2__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 (177) hide show
  1. {interp_engine-1.7.0 → interp_engine-1.7.2}/PKG-INFO +2 -1
  2. {interp_engine-1.7.0 → interp_engine-1.7.2}/README.md +1 -0
  3. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/capture.py +4 -1
  4. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_backend.py +35 -0
  5. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/static.py +48 -0
  6. {interp_engine-1.7.0 → interp_engine-1.7.2}/pyproject.toml +1 -1
  7. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_static_set.py +105 -0
  8. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_unified_free_functions.py +24 -0
  9. {interp_engine-1.7.0 → interp_engine-1.7.2}/.gitignore +0 -0
  10. {interp_engine-1.7.0 → interp_engine-1.7.2}/LICENSE +0 -0
  11. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/README.md +0 -0
  12. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/__init__.py +0 -0
  13. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/bench_spec.py +0 -0
  14. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/cells.py +0 -0
  15. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/probe.py +0 -0
  16. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/probe_lens_stream.py +0 -0
  17. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/publish.py +0 -0
  18. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/report_bench.py +0 -0
  19. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
  20. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
  21. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
  22. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
  23. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/gemma-2-2b__eager.json +0 -0
  24. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
  25. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
  26. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
  27. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
  28. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
  29. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
  30. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
  31. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/qwen3-4b__eager.json +0 -0
  32. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
  33. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
  34. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/qwen3-4b__vllm.json +0 -0
  35. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
  36. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
  37. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
  38. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
  39. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/results-latest.md +0 -0
  40. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/run_all.sh +0 -0
  41. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/run_bench.py +0 -0
  42. {interp_engine-1.7.0 → interp_engine-1.7.2}/benchmarks/workloads.py +0 -0
  43. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/AGENT_INTEGRATION.md +0 -0
  44. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/ARCHITECTURE_QUIRKS.md +0 -0
  45. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/COMPATIBILITY.md +0 -0
  46. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
  47. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/GRADIENTS.md +0 -0
  48. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/INTERNALS.md +0 -0
  49. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/PERFORMANCE.md +0 -0
  50. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/PORTING.md +0 -0
  51. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/README.md +0 -0
  52. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/SUPPORTED_POINTS.md +0 -0
  53. {interp_engine-1.7.0 → interp_engine-1.7.2}/docs/USAGE.md +0 -0
  54. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/__init__.py +0 -0
  55. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/_loop.py +0 -0
  56. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/address.py +0 -0
  57. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/arch.py +0 -0
  58. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/attn_config.py +0 -0
  59. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/attn_scores.py +0 -0
  60. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/autograd_support.py +0 -0
  61. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/chat_compose.py +0 -0
  62. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/chat_conventions.py +0 -0
  63. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/chat_formatters.py +0 -0
  64. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/cuda_preflight.py +0 -0
  65. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/dispatch.py +0 -0
  66. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/facts.py +0 -0
  67. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/hooks.py +0 -0
  68. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/lens.py +0 -0
  69. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/load.py +0 -0
  70. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/mappers.py +0 -0
  71. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/memory.py +0 -0
  72. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/model.py +0 -0
  73. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/moe_routing.py +0 -0
  74. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/notebook_stdout.py +0 -0
  75. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/points.py +0 -0
  76. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/protocol.py +0 -0
  77. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/residual_basis.py +0 -0
  78. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/select.py +0 -0
  79. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/steer.py +0 -0
  80. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/steer_specs.py +0 -0
  81. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/sync.py +0 -0
  82. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/tokenize.py +0 -0
  83. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/__init__.py +0 -0
  84. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/_demux.py +0 -0
  85. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/_hooks.py +0 -0
  86. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/_payload.py +0 -0
  87. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/_tree.py +0 -0
  88. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/attn.py +0 -0
  89. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/capture.py +0 -0
  90. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/graphs.py +0 -0
  91. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/lens/__init__.py +0 -0
  92. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/lens/intervene.py +0 -0
  93. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/lens/readout.py +0 -0
  94. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/lens/unembed.py +0 -0
  95. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/mhc.py +0 -0
  96. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/native.py +0 -0
  97. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/requests.py +0 -0
  98. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_capture/steering.py +0 -0
  99. {interp_engine-1.7.0 → interp_engine-1.7.2}/interp_engine/vllm_plugin.py +0 -0
  100. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/conftest.py +0 -0
  101. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/harness.py +0 -0
  102. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/model_expectations.yaml +0 -0
  103. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/synthetic_families.py +0 -0
  104. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_address.py +0 -0
  105. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_attn_config_tripwire.py +0 -0
  106. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_attn_probs_indexing.py +0 -0
  107. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_attn_scores.py +0 -0
  108. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_attn_z_gqa.py +0 -0
  109. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_autograd_support.py +0 -0
  110. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_bench_workloads.py +0 -0
  111. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_capability_refusals.py +0 -0
  112. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_capture_addressing.py +0 -0
  113. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_chat_compose.py +0 -0
  114. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_chat_formatters.py +0 -0
  115. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_chat_templates.py +0 -0
  116. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_core.py +0 -0
  117. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_cuda_preflight.py +0 -0
  118. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_doc_code_fences.py +0 -0
  119. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_eager_autograd.py +0 -0
  120. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_facts.py +0 -0
  121. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_family_points.py +0 -0
  122. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_gated_attn_out.py +0 -0
  123. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_gpu_sizer.py +0 -0
  124. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_head_contributions.py +0 -0
  125. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_hook_call_conventions.py +0 -0
  126. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_layer_kinds.py +0 -0
  127. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_load.py +0 -0
  128. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_logit_transform.py +0 -0
  129. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_mappers.py +0 -0
  130. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_memory.py +0 -0
  131. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_mlp_internals.py +0 -0
  132. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_model_expectations.py +0 -0
  133. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_moe.py +0 -0
  134. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_multimodal_arch.py +0 -0
  135. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_new_models_gpu.py +0 -0
  136. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_no_chat_template.py +0 -0
  137. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_normalized_hook.py +0 -0
  138. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_notebook_stdout.py +0 -0
  139. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_packaging.py +0 -0
  140. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_parity_gpt2.py +0 -0
  141. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_per_layer_attn_dims.py +0 -0
  142. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_points_registry.py +0 -0
  143. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_protocol.py +0 -0
  144. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_published_benchmarks.py +0 -0
  145. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_qk_norm.py +0 -0
  146. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_qkv_layout.py +0 -0
  147. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_reasoning_spans.py +0 -0
  148. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_release.py +0 -0
  149. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_resid_mid.py +0 -0
  150. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_residual_basis.py +0 -0
  151. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_sandwich_norms.py +0 -0
  152. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_select.py +0 -0
  153. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_sliding_window_attn.py +0 -0
  154. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_small_models_gpu.py +0 -0
  155. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_static_dsv4_gpu.py +0 -0
  156. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_static_parity_gpu.py +0 -0
  157. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_static_warmup.py +0 -0
  158. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_steer_context.py +0 -0
  159. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_steer_math_parity.py +0 -0
  160. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_sync_loop.py +0 -0
  161. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_sync_parity.py +0 -0
  162. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_unresolved_families.py +0 -0
  163. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_capture_gpu.py +0 -0
  164. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_capture_scales.py +0 -0
  165. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_engine_loop.py +0 -0
  166. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_graph_path.py +0 -0
  167. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_graphs_on_gpu.py +0 -0
  168. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_hook_availability.py +0 -0
  169. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_hyper_connections.py +0 -0
  170. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_kv_isolation.py +0 -0
  171. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_new_points.py +0 -0
  172. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_only_families.py +0 -0
  173. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_plugin.py +0 -0
  174. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vllm_wire_grammar.py +0 -0
  175. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_vocabulary_boundary.py +0 -0
  176. {interp_engine-1.7.0 → interp_engine-1.7.2}/tests/test_worker_lens_capture_readout.py +0 -0
  177. {interp_engine-1.7.0 → interp_engine-1.7.2}/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.7.0
3
+ Version: 1.7.2
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
@@ -162,6 +162,7 @@ Never OOM again - `interp-engine` includes gpu-sizer, an intuitive UI which tell
162
162
  - **DeepSeek-V2 on transformers older than 5.15.0** captures a wrong attention temperature — the engine warns at load, and upgrading is the fix ([COMPATIBILITY.md](docs/COMPATIBILITY.md)).
163
163
  - **DeepSeek-V4 on the eager backend needs two flags outside Hopper.** Its FP8 paths reach for `kernels-community/deep-gemm`, which declares a `9.0a` build, and with `kernels` >= 0.16.1 the arch refusal escapes transformers' Triton fallback and kills the first forward. Load with `TRANSFORMERS_DISABLE_DEEPGEMM_LINEAR=1` and `experts_implementation="grouped_mm"`, which transformers [already recommends on B200](https://github.com/huggingface/transformers/blob/93c8b7b485963a10800c91f55304db6be211c2bd/src/transformers/integrations/finegrained_fp8.py#L252-L254) for an unrelated DeepGEMM accuracy problem. The engine raises `HubKernelUnsupported` naming both; vLLM is unaffected.
164
164
  - **MXFP4 checkpoints (gpt-oss) need `interp-engine[quant]`**, which the `[vllm]` extra does not include; without it transformers dequantizes them to bf16 at roughly 3x the weights, which can turn a model that fits into one that does not.
165
+ - **`gemma-4-26B-A4B-it` on the static vLLM backend needs `VLLM_BATCH_INVARIANT=1` on B200/B300.** On compute capability 10.x, vLLM's own greedy output moves by up to 6.8 nats under the CUDA-graph replay a static set requires, so the taps would report a forward pass vLLM itself gets wrong ([vllm#55238](https://github.com/vllm-project/vllm/issues/55238)). The engine refuses this combination at load and names the flag; capability 9.x and 12.x are bit-identical and unaffected.
165
166
 
166
167
  Per-architecture structural quirks — which points a family serves and why — are not caveats but facts about the architecture, and live in [ARCHITECTURE_QUIRKS.md](docs/ARCHITECTURE_QUIRKS.md).
167
168
 
@@ -131,6 +131,7 @@ Never OOM again - `interp-engine` includes gpu-sizer, an intuitive UI which tell
131
131
  - **DeepSeek-V2 on transformers older than 5.15.0** captures a wrong attention temperature — the engine warns at load, and upgrading is the fix ([COMPATIBILITY.md](docs/COMPATIBILITY.md)).
132
132
  - **DeepSeek-V4 on the eager backend needs two flags outside Hopper.** Its FP8 paths reach for `kernels-community/deep-gemm`, which declares a `9.0a` build, and with `kernels` >= 0.16.1 the arch refusal escapes transformers' Triton fallback and kills the first forward. Load with `TRANSFORMERS_DISABLE_DEEPGEMM_LINEAR=1` and `experts_implementation="grouped_mm"`, which transformers [already recommends on B200](https://github.com/huggingface/transformers/blob/93c8b7b485963a10800c91f55304db6be211c2bd/src/transformers/integrations/finegrained_fp8.py#L252-L254) for an unrelated DeepGEMM accuracy problem. The engine raises `HubKernelUnsupported` naming both; vLLM is unaffected.
133
133
  - **MXFP4 checkpoints (gpt-oss) need `interp-engine[quant]`**, which the `[vllm]` extra does not include; without it transformers dequantizes them to bf16 at roughly 3x the weights, which can turn a model that fits into one that does not.
134
+ - **`gemma-4-26B-A4B-it` on the static vLLM backend needs `VLLM_BATCH_INVARIANT=1` on B200/B300.** On compute capability 10.x, vLLM's own greedy output moves by up to 6.8 nats under the CUDA-graph replay a static set requires, so the taps would report a forward pass vLLM itself gets wrong ([vllm#55238](https://github.com/vllm-project/vllm/issues/55238)). The engine refuses this combination at load and names the flag; capability 9.x and 12.x are bit-identical and unaffected.
134
135
 
135
136
  Per-architecture structural quirks — which points a family serves and why — are not caveats but facts about the architecture, and live in [ARCHITECTURE_QUIRKS.md](docs/ARCHITECTURE_QUIRKS.md).
136
137
 
@@ -224,7 +224,10 @@ def _run_with_cache_eager(
224
224
  attention_mask: torch.Tensor | None = None,
225
225
  ) -> Cache:
226
226
  """Capture in-process off the live module tree. See :func:`run_with_cache`."""
227
- input_ids = as_batched_tokens(tokens)
227
+ # Placed here rather than by each caller: these ids go straight into `hf_model`, so a list or a
228
+ # host tensor -- both documented inputs -- fails on every accelerator otherwise, through either
229
+ # entry point that shares this body. A tensor already on the device is unmoved.
230
+ input_ids = as_batched_tokens(tokens, device=model.device)
228
231
  addresses = _normalize_points(points)
229
232
  cache = Cache()
230
233
 
@@ -60,6 +60,7 @@ from interp_engine.vllm_capture.static import (
60
60
  kv_cache_width,
61
61
  resid_stream_aliases,
62
62
  resolve_static_points,
63
+ sm100_cudagraph_refusal_reason,
63
64
  static_read_width,
64
65
  static_unsupported_reason,
65
66
  )
@@ -67,6 +68,22 @@ from interp_engine.vllm_plugin import WORKER_EXTENSION_CLS
67
68
 
68
69
  logger = logging.getLogger(__name__)
69
70
 
71
+
72
+ def _device_capability() -> tuple[int, int] | None:
73
+ """This GPU's compute capability, or None when there is no CUDA device to ask.
74
+
75
+ None rather than a default, because callers gate refusals on it: a guess would either refuse a
76
+ machine nobody measured or wave through the one that is known wrong.
77
+ """
78
+ try:
79
+ if not torch.cuda.is_available():
80
+ return None
81
+ major, minor = torch.cuda.get_device_capability(0)
82
+ return int(major), int(minor)
83
+ except Exception: # noqa: BLE001 - no driver, or a torch without the call
84
+ return None
85
+
86
+
70
87
  # Residual-width sites the warmup sentinel can prove. ``mlp_act`` / ``z`` need a different
71
88
  # vector length than ``d_model``, which this process does not know until a worker wrap exists.
72
89
  _STATIC_SENTINEL_WRITE_POINTS = frozenset(
@@ -1099,6 +1116,7 @@ class VLLMModel:
1099
1116
  # Same condition as `apply_breakable_env`: only a non-empty static set turns torch.compile
1100
1117
  # off, and it is that combination a linear-attention trunk cannot survive.
1101
1118
  if reads or writes:
1119
+ self._refuse_static_where_vllm_reads_wrong()
1102
1120
  self._pin_decode_only_graphs_on_hybrid_trunk()
1103
1121
  # Reads only. A write allocates a `[1, width]` delta (see `static._alloc_site`), so it does
1104
1122
  # not scale with `max_num_batched_tokens` and has no business in a budget whose whole job is
@@ -1144,6 +1162,23 @@ class VLLMModel:
1144
1162
  logger.warning("lowering max_num_batched_tokens %s -> %s so static buffers fit", max_n, fitted)
1145
1163
  self._engine_kwargs["max_num_batched_tokens"] = fitted
1146
1164
 
1165
+ def _refuse_static_where_vllm_reads_wrong(self) -> None:
1166
+ """Refuse a static set that would report a forward pass vLLM itself gets wrong.
1167
+
1168
+ Raised here rather than after the engine exists, because it is decided by the checkpoint and
1169
+ the device: building 26B of weights first only delays the same answer. See
1170
+ :func:`~interp_engine.vllm_capture.static.sm100_cudagraph_refusal_reason`.
1171
+ """
1172
+ compilation = self._engine_kwargs.get("compilation_config")
1173
+ reason = sm100_cudagraph_refusal_reason(
1174
+ getattr(self, "hf_model_id", None),
1175
+ _device_capability(),
1176
+ cudagraph_mode=str(compilation.get("cudagraph_mode") or "") if isinstance(compilation, dict) else "",
1177
+ batch_invariant=os.environ.get("VLLM_BATCH_INVARIANT") == "1",
1178
+ )
1179
+ if reason:
1180
+ raise ValueError(reason)
1181
+
1147
1182
  def _pin_decode_only_graphs_on_hybrid_trunk(self) -> None:
1148
1183
  """Capture graphs for decode only when the trunk is linear attention.
1149
1184
 
@@ -142,6 +142,54 @@ def decode_only_graphs_reason(layer_types: Sequence[str] | None, n_layers: int)
142
142
  )
143
143
 
144
144
 
145
+ #: The upstream issue behind :func:`sm100_cudagraph_refusal_reason`.
146
+ SM100_CUDAGRAPH_ISSUE = "https://github.com/vllm-project/vllm/issues/55238"
147
+
148
+ #: The one setting measured to make that capture correct again.
149
+ SM100_CUDAGRAPH_WORKAROUND = "VLLM_BATCH_INVARIANT=1"
150
+
151
+ #: Checkpoints measured wrong under a static set on compute capability 10.x. Exact ids rather than a
152
+ #: glob over MoE Gemma 4: this is the one that was measured, and refusing a checkpoint nobody has
153
+ #: tested trades a silent wrong answer for a wrong refusal.
154
+ SM100_CUDAGRAPH_MODELS = ("google/gemma-4-26B-A4B-it",)
155
+
156
+
157
+ def sm100_cudagraph_refusal_reason(
158
+ hf_id: str | None,
159
+ capability: tuple[int, int] | None,
160
+ *,
161
+ cudagraph_mode: str = "",
162
+ batch_invariant: bool = False,
163
+ ) -> str | None:
164
+ """Why this checkpoint cannot be captured under a static set on this GPU, or None.
165
+
166
+ A static set needs graph replay with torch.compile off, and on compute capability 10.x (B200,
167
+ B300) vLLM's own greedy output moves under exactly that: up to 6.8 nats of logprob and a different
168
+ token, deterministic, and bit-identical on 9.x and 12.x. It follows CUDA-graph padding rather than
169
+ the prompt, so no choice of prompt avoids it, and the taps sit downstream -- they would report a
170
+ forward pass vLLM itself gets wrong, at every point of the affected layers.
171
+
172
+ Refused rather than worked around quietly. ``VLLM_BATCH_INVARIANT=1`` fixes it but changes kernel
173
+ selection for the whole run, which is a decision about what the captured numbers mean, and that
174
+ belongs to the caller. See :data:`SM100_CUDAGRAPH_ISSUE` and validator/comparison/engine_bugs.py.
175
+ """
176
+ if not hf_id or hf_id not in SM100_CUDAGRAPH_MODELS:
177
+ return None
178
+ if capability is None or capability[0] != 10:
179
+ return None
180
+ # Either of these removes the defect: the flag was measured bit-identical at every prompt length,
181
+ # and without replay there is no padding to be wrong about.
182
+ if batch_invariant or str(cudagraph_mode).upper() == "NONE":
183
+ return None
184
+ return (
185
+ f"{hf_id} on compute capability {capability[0]}.{capability[1]}: vLLM's own output moves under "
186
+ "CUDA-graph replay with torch.compile off, which is what a static set requires -- up to 6.8 nats "
187
+ "of logprob and a different greedy token -- so these taps would read a forward pass vLLM itself "
188
+ f"gets wrong ({SM100_CUDAGRAPH_ISSUE}). Set {SM100_CUDAGRAPH_WORKAROUND} to capture anyway, or "
189
+ "use the hooked `vllm` backend, or capture on capability 9.x/12.x."
190
+ )
191
+
192
+
145
193
  def multi_stream_refusal_reason(name: str, n_streams: int) -> str | None:
146
194
  """Why ``name`` cannot be declared as a static tap on a trunk carrying ``n_streams`` residual streams, or None.
147
195
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "interp-engine"
3
- version = "1.7.0"
3
+ version = "1.7.2"
4
4
  description = "A fast, standardized, and easy to use interpretability engine."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -11,6 +11,8 @@ from interp_engine.address import Address
11
11
  from interp_engine.points import steer_refusal_reason
12
12
  from interp_engine.vllm_capture.static import (
13
13
  BREAKABLE_ENV,
14
+ SM100_CUDAGRAPH_ISSUE,
15
+ SM100_CUDAGRAPH_WORKAROUND,
14
16
  STATIC_SKIP_ABSENT_ENV,
15
17
  StaticState,
16
18
  _activation_width,
@@ -41,6 +43,7 @@ from interp_engine.vllm_capture.static import (
41
43
  multi_stream_refusal_reason,
42
44
  resid_stream_aliases,
43
45
  resolve_static_points,
46
+ sm100_cudagraph_refusal_reason,
44
47
  static_buffer_bytes,
45
48
  static_unsupported_reason,
46
49
  steer_write_for_sae_point,
@@ -603,6 +606,38 @@ def test_unknown_layer_types_are_not_a_hybrid_claim():
603
606
  assert decode_only_graphs_reason([], 12) is None
604
607
 
605
608
 
609
+ def test_the_checkpoint_vllm_reads_wrong_on_sm100_is_refused_rather_than_captured():
610
+ """A static set on this pair would report vLLM's own wrong forward as an interp result.
611
+
612
+ The refusal names the workaround and the issue, because a caller who hits this needs to decide
613
+ between `VLLM_BATCH_INVARIANT=1` and different hardware, and cannot do that from "unsupported".
614
+ """
615
+ reason = sm100_cudagraph_refusal_reason("google/gemma-4-26B-A4B-it", (10, 0))
616
+ assert reason is not None
617
+ assert SM100_CUDAGRAPH_WORKAROUND in reason
618
+ assert SM100_CUDAGRAPH_ISSUE in reason
619
+ assert sm100_cudagraph_refusal_reason("google/gemma-4-26B-A4B-it", (10, 3)) is not None
620
+
621
+
622
+ def test_the_sm100_refusal_lifts_for_each_setting_measured_to_remove_the_defect():
623
+ """`VLLM_BATCH_INVARIANT=1` was bit-identical at every prompt length, and without replay there is
624
+ no padding to be wrong about. Both are the caller's call to make, so neither is refused."""
625
+ args = ("google/gemma-4-26B-A4B-it", (10, 0))
626
+ assert sm100_cudagraph_refusal_reason(*args, batch_invariant=True) is None
627
+ assert sm100_cudagraph_refusal_reason(*args, cudagraph_mode="NONE") is None
628
+ assert sm100_cudagraph_refusal_reason(*args, cudagraph_mode="none") is None
629
+
630
+
631
+ def test_the_sm100_refusal_does_not_reach_hardware_or_checkpoints_it_was_not_measured_on():
632
+ """Capability 9.0 and 12.0 were measured bit-identical across all 60 points, and no other
633
+ checkpoint was measured at all -- refusing those would trade a wrong answer for a wrong refusal."""
634
+ assert sm100_cudagraph_refusal_reason("google/gemma-4-26B-A4B-it", (9, 0)) is None
635
+ assert sm100_cudagraph_refusal_reason("google/gemma-4-26B-A4B-it", (12, 0)) is None
636
+ assert sm100_cudagraph_refusal_reason("google/gemma-4-26B-A4B-it", None) is None
637
+ assert sm100_cudagraph_refusal_reason("google/gemma-4-12B-it", (10, 0)) is None
638
+ assert sm100_cudagraph_refusal_reason(None, (10, 0)) is None
639
+
640
+
606
641
  def test_every_recurrent_kind_upstream_leaves_unbroken_is_pinned():
607
642
  """The shared recurrent layers carry no `eager_break_during_capture`, so each of these families is
608
643
  exposed the same way Qwen3.5 was: mamba_mixer (Jamba), mamba_mixer2 (Nemotron-H, Bamba,
@@ -1239,6 +1274,76 @@ def test_capturing_attn_wrap_weak_refs_qkv_before_add_eager(monkeypatch):
1239
1274
  assert torch.equal(v_site.buf[:3], v)
1240
1275
 
1241
1276
 
1277
+ class _ReplayableBreakableCapture(_FakeBreakableCapture):
1278
+ """:class:`_FakeBreakableCapture`, plus the replays a real graph runs after recording.
1279
+
1280
+ The base class stops at capture time. That is what the weak-ref tests above need -- and it is
1281
+ also why they cannot see this: a recorded eager segment runs again on *every* replay, reading
1282
+ whatever the weak-reffed addresses hold then, and :func:`_harvest` reads the site buffer after
1283
+ the replay rather than after the recording.
1284
+ """
1285
+
1286
+ def replay(self) -> None:
1287
+ for fn in self.fns:
1288
+ fn()
1289
+
1290
+
1291
+ class _KwargAttn(torch.nn.Module):
1292
+ """An attention module called the way vLLM calls it on a Gemma-shaped block."""
1293
+
1294
+ def forward(self, positions=None, hidden_states=None): # noqa: ANN001
1295
+ return hidden_states * 0.5
1296
+
1297
+
1298
+ def _tap_attn_in(monkeypatch) -> tuple[_ReplayableBreakableCapture, _Site, torch.Tensor]:
1299
+ """An ``attn_in`` read tapped on a recording graph, with the trunk tensor it was called with."""
1300
+ import interp_engine.vllm_capture.static as static_mod
1301
+
1302
+ cap = _ReplayableBreakableCapture()
1303
+ monkeypatch.setattr(static_mod, "_breakable_capture", lambda: cap)
1304
+ attn = _KwargAttn()
1305
+ site = _Site(address=Address("attn_in", 22), buf=torch.zeros(4, 2), module=attn)
1306
+ _wrap_module(attn, [(site, "read")])
1307
+ hidden = torch.full((3, 2), 22.0)
1308
+ attn(positions=torch.arange(3), hidden_states=hidden)
1309
+ return cap, site, hidden
1310
+
1311
+
1312
+ def test_replay_recopies_the_same_rows_when_nothing_overwrites_the_tapped_buffer(monkeypatch):
1313
+ """Replaying the segment is harmless by itself -- it recopies what it copied at capture.
1314
+
1315
+ Read with the test below: deferral alone is safe, and only a reused block moves the rows. That
1316
+ separates the two candidate explanations whenever a static column disagrees with a hooked one.
1317
+ """
1318
+ cap, site, _ = _tap_attn_in(monkeypatch)
1319
+ assert torch.equal(site.buf[:3], torch.full((3, 2), 22.0))
1320
+ cap.replay()
1321
+ assert torch.equal(site.buf[:3], torch.full((3, 2), 22.0))
1322
+
1323
+
1324
+ def test_a_replayed_read_follows_the_address_so_vllms_callback_order_is_load_bearing(monkeypatch):
1325
+ """The recorded read holds an address, so its correctness rests on vLLM's callback order.
1326
+
1327
+ The weak ref handed to ``add_eager`` is deliberate: a strong Python ref pins the capture-time
1328
+ cudagraph-pool slot, and replay then recopies the profile run into the site buffer instead of the
1329
+ live request (:func:`_cuda_weak_ref`). Materialising a copy at registration would instead cost a
1330
+ copy and its VRAM per site per step, on every model. So the tap reads an address and depends on
1331
+ vLLM running the callback where it was registered, ahead of anything that writes that block.
1332
+
1333
+ Overwrite the block first and the read follows the address. That is the shape to watch for if a
1334
+ future vLLM stops ordering eager segments this way, and this test is here to catch that.
1335
+
1336
+ It is not the explanation for ``attn_in.22`` on capability 10.x. Plain vLLM moves by 6.8 nats on
1337
+ that hardware with no tap in the process, and a fresh engine and a reversed run order return the
1338
+ same wrong numbers, which a reused block would not
1339
+ (validator/docs/VLLM_SM100_CUDAGRAPH.md).
1340
+ """
1341
+ cap, site, hidden = _tap_attn_in(monkeypatch)
1342
+ hidden.copy_(torch.full((3, 2), 23.0)) # a later layer reusing the block
1343
+ cap.replay()
1344
+ assert torch.equal(site.buf[:3], torch.full((3, 2), 23.0))
1345
+
1346
+
1242
1347
  class _FusedQKV(torch.nn.Module):
1243
1348
  """The three numbers :func:`value_span` reads off vLLM's ``QKVParallelLinear``."""
1244
1349
 
@@ -8,6 +8,7 @@ checked against these same shapes by ``scripts/vllm_capture_generation_check.py`
8
8
 
9
9
  import asyncio
10
10
 
11
+ import pytest
11
12
  import torch
12
13
 
13
14
  from interp_engine import (
@@ -75,6 +76,29 @@ def test_capture_attention_returns_scores_probs_and_per_head_value(gpt2: EagerMo
75
76
  assert got[layer]["value"].shape == (seq, gpt2.n_kv_heads, gpt2.head_dim)
76
77
 
77
78
 
79
+ def test_every_eager_entry_point_takes_a_plain_list_of_ids(gpt2: EagerModel, prompt: str):
80
+ """A list of ints is a documented input, and it is the one that used to reach the device wrong.
81
+
82
+ The tests around this one all pass ``to_tokens`` output, a tensor already on the model's device,
83
+ so the host tensor a list builds was never exercised and both functions below raised on any
84
+ accelerator. ``EagerModel.capture`` hid it by placing its own tensor. On a CPU box both arms are
85
+ the same tensor and this only pins the signature; the device half is the gpu test below.
86
+ """
87
+ ids = [int(t) for t in gpt2.to_tokens(prompt)[0]]
88
+ assert sorted(capture_attention(gpt2, ids, [0])) == [0]
89
+ assert run_with_cache(gpt2, ids, [("resid_post", 0)]).get("resid_post", 0).shape[1] == len(ids)
90
+
91
+
92
+ @pytest.mark.gpu
93
+ @pytest.mark.skipif(not torch.cuda.is_available(), reason="a host tensor only misplaces on an accelerator")
94
+ def test_a_list_of_ids_reaches_the_accelerator(prompt: str):
95
+ """The regression itself: ids built off-device must be moved before they reach ``hf_model``."""
96
+ model = EagerModel("openai-community/gpt2", device="cuda", attn_implementation="eager")
97
+ ids = [int(t) for t in model.to_tokens(prompt)[0]]
98
+ assert capture_attention(model, ids, [0])[0]["scores"].device.type == "cuda"
99
+ assert run_with_cache(model, ids, [("resid_post", 0)]).get("resid_post", 0).device.type == "cuda"
100
+
101
+
78
102
  def test_capture_attention_agrees_with_the_points_it_is_built_from(gpt2: EagerModel, prompt: str):
79
103
  ids = gpt2.to_tokens(prompt)
80
104
  got = capture_attention(gpt2, ids, [4])
File without changes
File without changes