interp-engine 1.6.0__tar.gz → 1.7.1__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.6.0 → interp_engine-1.7.1}/PKG-INFO +2 -1
  2. {interp_engine-1.6.0 → interp_engine-1.7.1}/README.md +1 -0
  3. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/facts.py +4 -1
  4. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_backend.py +35 -0
  5. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/static.py +48 -0
  6. {interp_engine-1.6.0 → interp_engine-1.7.1}/pyproject.toml +1 -1
  7. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/synthetic_families.py +38 -0
  8. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_mlp_internals.py +7 -5
  9. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_static_set.py +105 -0
  10. {interp_engine-1.6.0 → interp_engine-1.7.1}/.gitignore +0 -0
  11. {interp_engine-1.6.0 → interp_engine-1.7.1}/LICENSE +0 -0
  12. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/README.md +0 -0
  13. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/__init__.py +0 -0
  14. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/bench_spec.py +0 -0
  15. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/cells.py +0 -0
  16. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/probe.py +0 -0
  17. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/probe_lens_stream.py +0 -0
  18. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/publish.py +0 -0
  19. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/report_bench.py +0 -0
  20. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
  21. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
  22. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
  23. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
  24. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/gemma-2-2b__eager.json +0 -0
  25. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
  26. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
  27. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
  28. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
  29. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
  30. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
  31. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
  32. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/qwen3-4b__eager.json +0 -0
  33. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
  34. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
  35. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/qwen3-4b__vllm.json +0 -0
  36. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
  37. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
  38. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
  39. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
  40. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/results-latest.md +0 -0
  41. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/run_all.sh +0 -0
  42. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/run_bench.py +0 -0
  43. {interp_engine-1.6.0 → interp_engine-1.7.1}/benchmarks/workloads.py +0 -0
  44. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/AGENT_INTEGRATION.md +0 -0
  45. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/ARCHITECTURE_QUIRKS.md +0 -0
  46. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/COMPATIBILITY.md +0 -0
  47. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
  48. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/GRADIENTS.md +0 -0
  49. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/INTERNALS.md +0 -0
  50. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/PERFORMANCE.md +0 -0
  51. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/PORTING.md +0 -0
  52. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/README.md +0 -0
  53. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/SUPPORTED_POINTS.md +0 -0
  54. {interp_engine-1.6.0 → interp_engine-1.7.1}/docs/USAGE.md +0 -0
  55. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/__init__.py +0 -0
  56. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/_loop.py +0 -0
  57. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/address.py +0 -0
  58. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/arch.py +0 -0
  59. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/attn_config.py +0 -0
  60. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/attn_scores.py +0 -0
  61. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/autograd_support.py +0 -0
  62. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/capture.py +0 -0
  63. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/chat_compose.py +0 -0
  64. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/chat_conventions.py +0 -0
  65. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/chat_formatters.py +0 -0
  66. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/cuda_preflight.py +0 -0
  67. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/dispatch.py +0 -0
  68. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/hooks.py +0 -0
  69. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/lens.py +0 -0
  70. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/load.py +0 -0
  71. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/mappers.py +0 -0
  72. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/memory.py +0 -0
  73. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/model.py +0 -0
  74. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/moe_routing.py +0 -0
  75. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/notebook_stdout.py +0 -0
  76. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/points.py +0 -0
  77. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/protocol.py +0 -0
  78. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/residual_basis.py +0 -0
  79. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/select.py +0 -0
  80. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/steer.py +0 -0
  81. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/steer_specs.py +0 -0
  82. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/sync.py +0 -0
  83. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/tokenize.py +0 -0
  84. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/__init__.py +0 -0
  85. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/_demux.py +0 -0
  86. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/_hooks.py +0 -0
  87. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/_payload.py +0 -0
  88. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/_tree.py +0 -0
  89. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/attn.py +0 -0
  90. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/capture.py +0 -0
  91. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/graphs.py +0 -0
  92. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/lens/__init__.py +0 -0
  93. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/lens/intervene.py +0 -0
  94. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/lens/readout.py +0 -0
  95. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/lens/unembed.py +0 -0
  96. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/mhc.py +0 -0
  97. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/native.py +0 -0
  98. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/requests.py +0 -0
  99. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_capture/steering.py +0 -0
  100. {interp_engine-1.6.0 → interp_engine-1.7.1}/interp_engine/vllm_plugin.py +0 -0
  101. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/conftest.py +0 -0
  102. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/harness.py +0 -0
  103. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/model_expectations.yaml +0 -0
  104. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_address.py +0 -0
  105. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_attn_config_tripwire.py +0 -0
  106. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_attn_probs_indexing.py +0 -0
  107. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_attn_scores.py +0 -0
  108. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_attn_z_gqa.py +0 -0
  109. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_autograd_support.py +0 -0
  110. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_bench_workloads.py +0 -0
  111. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_capability_refusals.py +0 -0
  112. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_capture_addressing.py +0 -0
  113. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_chat_compose.py +0 -0
  114. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_chat_formatters.py +0 -0
  115. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_chat_templates.py +0 -0
  116. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_core.py +0 -0
  117. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_cuda_preflight.py +0 -0
  118. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_doc_code_fences.py +0 -0
  119. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_eager_autograd.py +0 -0
  120. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_facts.py +0 -0
  121. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_family_points.py +0 -0
  122. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_gated_attn_out.py +0 -0
  123. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_gpu_sizer.py +0 -0
  124. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_head_contributions.py +0 -0
  125. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_hook_call_conventions.py +0 -0
  126. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_layer_kinds.py +0 -0
  127. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_load.py +0 -0
  128. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_logit_transform.py +0 -0
  129. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_mappers.py +0 -0
  130. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_memory.py +0 -0
  131. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_model_expectations.py +0 -0
  132. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_moe.py +0 -0
  133. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_multimodal_arch.py +0 -0
  134. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_new_models_gpu.py +0 -0
  135. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_no_chat_template.py +0 -0
  136. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_normalized_hook.py +0 -0
  137. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_notebook_stdout.py +0 -0
  138. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_packaging.py +0 -0
  139. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_parity_gpt2.py +0 -0
  140. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_per_layer_attn_dims.py +0 -0
  141. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_points_registry.py +0 -0
  142. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_protocol.py +0 -0
  143. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_published_benchmarks.py +0 -0
  144. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_qk_norm.py +0 -0
  145. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_qkv_layout.py +0 -0
  146. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_reasoning_spans.py +0 -0
  147. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_release.py +0 -0
  148. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_resid_mid.py +0 -0
  149. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_residual_basis.py +0 -0
  150. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_sandwich_norms.py +0 -0
  151. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_select.py +0 -0
  152. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_sliding_window_attn.py +0 -0
  153. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_small_models_gpu.py +0 -0
  154. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_static_dsv4_gpu.py +0 -0
  155. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_static_parity_gpu.py +0 -0
  156. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_static_warmup.py +0 -0
  157. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_steer_context.py +0 -0
  158. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_steer_math_parity.py +0 -0
  159. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_sync_loop.py +0 -0
  160. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_sync_parity.py +0 -0
  161. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_unified_free_functions.py +0 -0
  162. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_unresolved_families.py +0 -0
  163. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_capture_gpu.py +0 -0
  164. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_capture_scales.py +0 -0
  165. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_engine_loop.py +0 -0
  166. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_graph_path.py +0 -0
  167. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_graphs_on_gpu.py +0 -0
  168. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_hook_availability.py +0 -0
  169. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_hyper_connections.py +0 -0
  170. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_kv_isolation.py +0 -0
  171. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_new_points.py +0 -0
  172. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_only_families.py +0 -0
  173. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_plugin.py +0 -0
  174. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vllm_wire_grammar.py +0 -0
  175. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_vocabulary_boundary.py +0 -0
  176. {interp_engine-1.6.0 → interp_engine-1.7.1}/tests/test_worker_lens_capture_readout.py +0 -0
  177. {interp_engine-1.6.0 → interp_engine-1.7.1}/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.6.0
3
+ Version: 1.7.1
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
 
@@ -478,6 +478,7 @@ class GateUpLayout(StrEnum):
478
478
 
479
479
  #: ``[all_gate | all_up]``, two contiguous halves, gate first. Phi-3's ``gate_up_proj``, whose
480
480
  #: forward is ``gate, up = self.gate_up_proj(x).chunk(2, dim=-1); down(up * act(gate))``.
481
+ #: GLM-4's ``Glm4MLP`` performs the identical chunk.
481
482
  GATE_FIRST = "gate_first"
482
483
  #: ``[gate_0 up_0 | gate_1 up_1 | ...]``, the two branches interleaved per neuron. gpt-oss's
483
484
  #: MXFP4 expert weights, where the kernel reads ``[..., ::2]`` and ``[..., 1::2]``.
@@ -488,8 +489,10 @@ class GateUpLayout(StrEnum):
488
489
  #: packed. Absent means the refusal in ``ArchSpec.mlp_projection`` stands: a family may fuse and pack
489
490
  #: either way, and "probably gate first" is exactly the guess that produces silent garbage.
490
491
  #: Verified by the identity the branches exist to satisfy -- ``act(mlp_pre) * mlp_pre_linear`` is the
491
- #: down projection's input, which is captured independently as ``mlp_act`` (tests/test_fused_mlp.py).
492
+ #: down projection's input, which is captured independently as ``mlp_act``
493
+ #: (tests/test_mlp_internals.py).
492
494
  FUSED_GATE_UP_LAYOUTS: dict[str, GateUpLayout] = {
495
+ "Glm4ForCausalLM": GateUpLayout.GATE_FIRST,
493
496
  "Phi3ForCausalLM": GateUpLayout.GATE_FIRST,
494
497
  }
495
498
 
@@ -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.6.0"
3
+ version = "1.7.1"
4
4
  description = "A fast, standardized, and easy to use interpretability engine."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -437,6 +437,44 @@ def shrunk_phi3(*, seed: int = 0) -> Any:
437
437
  return EagerModel("Phi3ForCausalLM", hf_model=model, tokenizer=NoTokenizer(), device=None, dtype="float32")
438
438
 
439
439
 
440
+ #: Dims for :func:`shrunk_glm4`. Same subject as :data:`_PHI3_SHRUNK` -- the MLP's single
441
+ #: ``gate_up_proj`` -- so the only constraint is an even ``intermediate_size``. GLM-4 rotates half
442
+ #: its head dimension, so ``head_dim`` stays a multiple of four.
443
+ _GLM4_SHRUNK: dict[str, Any] = {
444
+ "hidden_size": 32,
445
+ "intermediate_size": 64,
446
+ "num_hidden_layers": 2,
447
+ "num_attention_heads": 4,
448
+ "num_key_value_heads": 2,
449
+ "head_dim": 8,
450
+ "vocab_size": 128,
451
+ "max_position_embeddings": 64,
452
+ "pad_token_id": 0,
453
+ "eos_token_id": 1,
454
+ }
455
+
456
+
457
+ def shrunk_glm4(*, seed: int = 0) -> Any:
458
+ """A tiny real GLM-4 with float32 weights on CPU, as an ``EagerModel``.
459
+
460
+ The second family to fuse its two pre-activation projections into one ``gate_up_proj``, and the
461
+ reason the packing is a table rather than a default: ``Glm4MLP`` chunks gate-first exactly as
462
+ Phi-3 does, but that is a fact about the family, established by running it, not one the shared
463
+ attribute name implies.
464
+ """
465
+ from transformers import Glm4Config, Glm4ForCausalLM
466
+
467
+ from interp_engine import EagerModel
468
+
469
+ config = Glm4Config(**_GLM4_SHRUNK)
470
+ config.architectures = ["Glm4ForCausalLM"]
471
+ torch.manual_seed(seed)
472
+ with warnings.catch_warnings():
473
+ warnings.simplefilter("ignore")
474
+ model = Glm4ForCausalLM(config).to(torch.float32).eval()
475
+ return EagerModel("Glm4ForCausalLM", hf_model=model, tokenizer=NoTokenizer(), device=None, dtype="float32")
476
+
477
+
440
478
  def shrunk_lfm2_moe(*, seed: int = 0) -> Any:
441
479
  """A tiny LFM2-MoE with real float32 weights on CPU, as an ``EagerModel``.
442
480
 
@@ -29,7 +29,7 @@ from types import SimpleNamespace
29
29
  import pytest
30
30
  import torch
31
31
  from harness import GEMMA_IT, GPT2, QWEN_THINKING, ModelSpec, load_model, require_hf_token
32
- from synthetic_families import shrunk_opt, shrunk_phi3
32
+ from synthetic_families import shrunk_glm4, shrunk_opt, shrunk_phi3
33
33
 
34
34
  from interp_engine import run_with_cache
35
35
  from interp_engine.capture import AddressLike
@@ -176,8 +176,9 @@ def test_a_plain_mlp_refuses_the_multiplied_branch():
176
176
  model.resolve_point("mlp_pre_linear", 0)
177
177
 
178
178
 
179
- def test_a_fused_gate_up_is_sliced_into_its_two_branches():
180
- """Phi-3's shape: one projection holding both branches, so neither is a module output.
179
+ @pytest.mark.parametrize("build", [shrunk_phi3, shrunk_glm4], ids=["phi3", "glm4"])
180
+ def test_a_fused_gate_up_is_sliced_into_its_two_branches(build):
181
+ """Phi-3's and GLM-4's shape: one projection holding both branches, so neither is a module output.
181
182
 
182
183
  Served rather than refused, because a dense MLP's neuron basis exists whether or not the
183
184
  checkpoint stores the two matrices concatenated -- and this is a read plus a last-axis slice, the
@@ -186,9 +187,10 @@ def test_a_fused_gate_up_is_sliced_into_its_two_branches():
186
187
  Checked by the identity the branches exist to satisfy: `act(mlp_pre) * mlp_pre_linear` is the
187
188
  down projection's input, which arrives independently as `mlp_act`. That is what says the halves
188
189
  were cut the right way round, and it is exact -- swapping them changes the answer on any
189
- non-symmetric activation, which SiLU is.
190
+ non-symmetric activation, which SiLU is. Run per family rather than once, because the packing is
191
+ a property of the family: both of these chunk gate-first, and nothing but running them says so.
190
192
  """
191
- model = shrunk_phi3()
193
+ model = build()
192
194
  ids = torch.arange(7).unsqueeze(0) % 128
193
195
  cache = run_with_cache(model, ids, [("mlp_pre", 1), ("mlp_pre_linear", 1), ("mlp_act", 1)])
194
196
  pre, linear, act = (cache.get(name, 1) for name in ("mlp_pre", "mlp_pre_linear", "mlp_act"))
@@ -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
 
File without changes
File without changes