interp-engine 1.3.3__tar.gz → 1.3.5__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.
- {interp_engine-1.3.3 → interp_engine-1.3.5}/PKG-INFO +1 -1
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/ARCHITECTURE_QUIRKS.md +49 -6
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/INTERNALS.md +6 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/arch.py +20 -5
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/facts.py +74 -10
- interp_engine-1.3.5/interp_engine/notebook_stdout.py +90 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_backend.py +19 -2
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/static.py +4 -1
- {interp_engine-1.3.3 → interp_engine-1.3.5}/pyproject.toml +1 -1
- interp_engine-1.3.5/tests/test_notebook_stdout.py +132 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/.gitignore +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/LICENSE +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/README.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/README.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/__init__.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/bench_spec.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/cells.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/probe.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/probe_lens_stream.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/publish.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/report_bench.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark-cudagraph.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/gemma-2-2b__eager.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/qwen3-4b__eager.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/qwen3-4b__vllm.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results-latest.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/run_all.sh +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/run_bench.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/workloads.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/AGENT_INTEGRATION.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/COMPATIBILITY.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/GRADIENTS.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/PERFORMANCE.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/PORTING.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/README.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/SUPPORTED_POINTS.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/docs/USAGE.md +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/__init__.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/_loop.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/address.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/attn_config.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/attn_scores.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/autograd_support.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/capture.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/chat_compose.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/chat_conventions.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/chat_formatters.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/cuda_preflight.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/dispatch.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/hooks.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/lens.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/load.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/mappers.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/model.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/moe_routing.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/points.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/protocol.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/residual_basis.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/select.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/steer.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/steer_specs.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/sync.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/tokenize.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/__init__.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/_demux.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/_hooks.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/_payload.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/_tree.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/attn.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/capture.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/graphs.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/lens/__init__.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/lens/intervene.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/lens/readout.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/lens/unembed.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/mhc.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/native.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/requests.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_capture/steering.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/interp_engine/vllm_plugin.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/conftest.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/harness.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/model_expectations.yaml +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/synthetic_families.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_address.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_attn_config_tripwire.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_attn_probs_indexing.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_attn_scores.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_attn_z_gqa.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_autograd_support.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_bench_workloads.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_capability_refusals.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_capture_addressing.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_chat_compose.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_chat_formatters.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_chat_templates.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_core.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_cuda_preflight.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_doc_code_fences.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_eager_autograd.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_facts.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_family_points.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_gated_attn_out.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_head_contributions.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_hook_call_conventions.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_layer_kinds.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_load.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_logit_transform.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_mappers.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_mlp_internals.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_model_expectations.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_moe.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_multimodal_arch.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_new_models_gpu.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_no_chat_template.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_normalized_hook.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_packaging.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_parity_gpt2.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_per_layer_attn_dims.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_points_registry.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_protocol.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_published_benchmarks.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_qk_norm.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_qkv_layout.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_reasoning_spans.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_release.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_resid_mid.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_residual_basis.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_sandwich_norms.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_select.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_sliding_window_attn.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_small_models_gpu.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_static_dsv4_gpu.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_static_parity_gpu.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_static_set.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_static_warmup.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_steer_context.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_steer_math_parity.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_sync_loop.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_sync_parity.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_unified_free_functions.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_unresolved_families.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_capture_gpu.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_capture_scales.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_engine_loop.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_graph_path.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_graphs_on_gpu.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_hook_availability.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_hyper_connections.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_kv_isolation.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_new_points.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_only_families.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_plugin.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vllm_wire_grammar.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_vocabulary_boundary.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/tests/test_worker_lens_capture_readout.py +0 -0
- {interp_engine-1.3.3 → interp_engine-1.3.5}/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.3.
|
|
3
|
+
Version: 1.3.5
|
|
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
|
|
@@ -277,9 +277,26 @@ is therefore asked per side (`facts.has_qk_norm(attn, "q")`), because the pair-w
|
|
|
277
277
|
"no" for such a layer and refusing the query norm on the key norm's absence declines a tensor the model
|
|
278
278
|
plainly computes.
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
280
|
+
- **`kv_heads_for_layer(layer)`** — the kv-head count moves with the head width, and in the opposite
|
|
281
|
+
direction: 4 against 16 on the 31B, 2 against 8 on the 26B, 1 against 8 on the 12B. transformers
|
|
282
|
+
5.15 states this per layer (`per_layer_config`); below that the family spells it
|
|
283
|
+
`num_global_key_value_heads`, which `Gemma4TextAttention` applies to a layer only when
|
|
284
|
+
`attention_k_eq_v` is set **and** the layer is not sliding — so `ArchSpec` carries the flag beside
|
|
285
|
+
the count rather than reading the count on its own.
|
|
286
|
+
|
|
287
|
+
`attention_k_eq_v` is on for the 26B, 31B and 12B, and off for E2B and E4B. It is a second, separate
|
|
288
|
+
way for a layer to have no `v_proj`, and it does not overlap with KV sharing: those three SKUs set
|
|
289
|
+
`num_kv_shared_layers: 0`, so **every** one of their `full_attention` layers is built with
|
|
290
|
+
`v_proj = None` and takes the key projection's output as the value instead — `v_norm(k_proj(h))`
|
|
291
|
+
against a key of `rope(k_norm(k_proj(h)))`, so the two are the same projection read through
|
|
292
|
+
different norms, not the same tensor.
|
|
293
|
+
|
|
294
|
+
**What the engine does with that today is wrong in the quiet direction.** `_first_attr` treats a
|
|
295
|
+
present-but-`None` attribute as absent, so `ArchSpec.v_proj` returns `None` on those layers — which
|
|
296
|
+
is the signal it uses for "fused QKV, the caller must split" — on a family whose `fused_qkv` is
|
|
297
|
+
false. The layer needs a third answer: `value` is capturable, from `k_proj` and the `v_norm` after
|
|
298
|
+
it, and refusing or mis-addressing it is not the same as it not being there. Nothing exercises this
|
|
299
|
+
yet because the 31B has been swept without the value points.
|
|
283
300
|
|
|
284
301
|
Two more ways the value side can differ from the query side, both of which produce a correctly shaped
|
|
285
302
|
and completely wrong per-head split if you reshape by `head_dim`:
|
|
@@ -408,9 +425,20 @@ n_experts]` tensor that is plausible rather than obviously wrong.
|
|
|
408
425
|
Mistral-4, dots1, GLM-4.5) or **every k-th layer sparse with a dense opt-out list** (`(layer + 1)
|
|
409
426
|
% decoder_sparse_step == 0 and layer not in mlp_only_layers`; Qwen2/3-MoE, Qwen3-Next,
|
|
410
427
|
Qwen3-VL/Omni). Expert counts have four live spellings (`num_local_experts`, `num_experts`,
|
|
411
|
-
`n_routed_experts`, `moe_num_experts`), all read
|
|
412
|
-
|
|
428
|
+
`n_routed_experts`, `moe_num_experts`), all read, and the top-k has four of its own
|
|
429
|
+
(`num_experts_per_tok`, `experts_per_token`, `moe_topk`, `top_k_experts`). A third idiom, an
|
|
430
|
+
explicit `mlp_layer_types` pattern, wins over both — see the spelling warning in [block
|
|
413
431
|
types](#block-types-classify-them-do-not-pattern-match-them).
|
|
432
|
+
- **Gemma-4 breaks the first sentence of this section.** Its sparse layers do not swap the MLP for an
|
|
433
|
+
expert bank: `Gemma4TextDecoderLayer` builds `self.mlp` on every layer and, where
|
|
434
|
+
`enable_moe_block` is set, hangs `self.router` and `self.experts` **beside** it on the block,
|
|
435
|
+
summing the two branches (`post_ffn_norm_1(mlp(x)) + post_ffn_norm_2(experts(x))`, both reading the
|
|
436
|
+
pre-feedforward residual). Three consequences, and `facts.dense_mlp_beside_experts` is the fact they
|
|
437
|
+
hang off. A sparse layer keeps a real neuron basis, so `mlp_pre`/`mlp_act` must stay served there —
|
|
438
|
+
which they are, because the LongCat guard makes the `mlp_pre` refusal require a router **on the MLP
|
|
439
|
+
module**, and Gemma-4's is a sibling. The parameter count of a sparse layer includes the dense MLP,
|
|
440
|
+
so the usual `(n_layers - n_sparse) * dense_mlp` undercounts (the 26B by ~0.5B). And `mlp_out` is
|
|
441
|
+
**not** the whole feed-forward here, which is the one that has no fix yet — see below.
|
|
414
442
|
|
|
415
443
|
Which experts fired _is_ capturable, one level down: `router_logits` / `expert_weights` /
|
|
416
444
|
`expert_indices` are three elements of the router module's own output tuple, resolved by
|
|
@@ -452,7 +480,22 @@ router_logits)`. `facts.ROUTER_OUTPUTS` holds the exceptions; `facts.assert_rout
|
|
|
452
480
|
|
|
453
481
|
The MLP's neuron basis (`mlp_pre`, `mlp_pre_linear`, `mlp_act`) is refused on a sparse layer, since
|
|
454
482
|
the projections live on the experts — often as one fused 3-D parameter per bank, with no per-expert
|
|
455
|
-
module at all.
|
|
483
|
+
module at all. Gemma-4 is the exception noted above, and the refusal already lets it through for the
|
|
484
|
+
right reason rather than by luck: it is gated on the router being found *on the MLP module*, the same
|
|
485
|
+
guard that keeps LongCat's shortcut MoE from suppressing its two real feed-forwards.
|
|
486
|
+
|
|
487
|
+
**Open on Gemma-4's 26B**, both from the same fact — the routed branch is a sibling of `layer.mlp`
|
|
488
|
+
rather than a part of it:
|
|
489
|
+
|
|
490
|
+
- `mlp_out` taps `layer.mlp` and so returns the dense branch alone, missing the experts' contribution
|
|
491
|
+
entirely. Correctly shaped, wrong tensor, no tell. Either the point has to mean the summed
|
|
492
|
+
feed-forward here (which has no module boundary — the sum happens in the block's `forward`) or it
|
|
493
|
+
has to be refused with an explanation, and that choice has not been made.
|
|
494
|
+
- `arch.moe_router` looks for the router under `layer.mlp` and Gemma-4 hangs it on the block, so
|
|
495
|
+
`router_logits` / `expert_weights` / `expert_indices` do not resolve at all. When they do, note that
|
|
496
|
+
`Gemma4TextRouter` returns `(router_probabilities, top_k_weights, top_k_index)` — element 0 is
|
|
497
|
+
already softmaxed over all 128 experts, so it matches the default tuple *order* while not being
|
|
498
|
+
logits, and `assert_routing_shapes` cannot tell the difference because the width is right.
|
|
456
499
|
|
|
457
500
|
In our shipping set only gpt-oss-20b is MoE (32 experts, top-4, no shared expert, every layer
|
|
458
501
|
sparse). Its 3D batched expert weights matter only to code reading MLP weight matrices, and the
|
|
@@ -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
|
|
|
@@ -324,6 +324,11 @@ class ArchSpec:
|
|
|
324
324
|
# elsewhere; use :meth:`head_dim_for_layer` / :meth:`kv_heads_for_layer`.
|
|
325
325
|
per_layer_head_dim: tuple[int, ...] = ()
|
|
326
326
|
per_layer_kv_heads: tuple[int, ...] = ()
|
|
327
|
+
# Gemma-4's ``num_global_key_value_heads`` and ``attention_k_eq_v``: the older spelling of the
|
|
328
|
+
# per-layer kv-head count, and the flag that decides whether it applies. See
|
|
329
|
+
# :func:`facts.kv_heads_for_layer`.
|
|
330
|
+
global_kv_heads: int | None = None
|
|
331
|
+
k_eq_v: bool = False
|
|
327
332
|
# The width of one *value* head, where the family makes it differ from the q/k head (MiMo-V2,
|
|
328
333
|
# DeepSeek). 0 means "same as the q/k head"; use :meth:`value_head_dim_for_layer`.
|
|
329
334
|
v_head_dim: int = 0
|
|
@@ -914,12 +919,20 @@ class ArchSpec:
|
|
|
914
919
|
def kv_heads_for_layer(self, layer: int) -> int:
|
|
915
920
|
"""How many key/value heads ``layer`` attends with.
|
|
916
921
|
|
|
917
|
-
:attr:`n_kv_heads` for every family but Gemma-4
|
|
918
|
-
|
|
919
|
-
per head: the model-wide number divides cleanly into
|
|
920
|
-
wrong scrambles heads rather than raising. See
|
|
922
|
+
:attr:`n_kv_heads` for every family but Gemma-4, whose full-attention layers carry 4 where its
|
|
923
|
+
sliding ones carry 16 on the 31B, and 2 against 8 on the 26B. Prefer this anywhere ``k``,
|
|
924
|
+
``value`` or a k-side norm is reshaped per head: the model-wide number divides cleanly into
|
|
925
|
+
the other layers' widths, so getting it wrong scrambles heads rather than raising. See
|
|
926
|
+
:func:`facts.kv_heads_for_layer`.
|
|
921
927
|
"""
|
|
922
|
-
return facts.kv_heads_for_layer(
|
|
928
|
+
return facts.kv_heads_for_layer(
|
|
929
|
+
self.n_kv_heads,
|
|
930
|
+
layer,
|
|
931
|
+
self.per_layer_kv_heads,
|
|
932
|
+
self.global_kv_heads,
|
|
933
|
+
self.quirks.hybrid_layer_types,
|
|
934
|
+
self.k_eq_v,
|
|
935
|
+
)
|
|
923
936
|
|
|
924
937
|
def value_head_dim_for_layer(self, layer: int) -> int:
|
|
925
938
|
"""``layer``'s *value* head width -- what ``value`` and ``z`` are per head.
|
|
@@ -1160,6 +1173,8 @@ def resolve_arch(model: nn.Module, config: Any) -> ArchSpec:
|
|
|
1160
1173
|
global_head_dim=model_facts.global_head_dim,
|
|
1161
1174
|
per_layer_head_dim=model_facts.per_layer_head_dim,
|
|
1162
1175
|
per_layer_kv_heads=model_facts.per_layer_kv_heads,
|
|
1176
|
+
global_kv_heads=model_facts.global_kv_heads,
|
|
1177
|
+
k_eq_v=model_facts.k_eq_v,
|
|
1163
1178
|
v_head_dim=model_facts.v_head_dim,
|
|
1164
1179
|
first_kv_shared_layer=model_facts.first_kv_shared_layer,
|
|
1165
1180
|
layer_slots=layer_slots,
|
|
@@ -1072,7 +1072,15 @@ def logit_multiplier(cfg: Any) -> tuple[float | None, str]:
|
|
|
1072
1072
|
|
|
1073
1073
|
# Four spellings of the routed-expert count, all in current use.
|
|
1074
1074
|
_N_EXPERTS_FIELDS: tuple[str, ...] = ("num_local_experts", "num_experts", "n_routed_experts", "moe_num_experts")
|
|
1075
|
-
|
|
1075
|
+
# ``top_k_experts`` is Gemma-4's. Missing it did not merely lose a number: :func:`assert_routing_shapes`
|
|
1076
|
+
# guards its width check with ``and top_k``, so a zero here switched off the check that catches a
|
|
1077
|
+
# router tuple read in the wrong order -- on the newest MoE family, and silently.
|
|
1078
|
+
_EXPERTS_PER_TOKEN_FIELDS: tuple[str, ...] = (
|
|
1079
|
+
"num_experts_per_tok",
|
|
1080
|
+
"experts_per_token",
|
|
1081
|
+
"moe_topk",
|
|
1082
|
+
"top_k_experts",
|
|
1083
|
+
)
|
|
1076
1084
|
_N_SHARED_EXPERTS_FIELDS: tuple[str, ...] = ("n_shared_experts", "num_shared_experts")
|
|
1077
1085
|
|
|
1078
1086
|
|
|
@@ -1228,6 +1236,27 @@ def block_types_name_the_feed_forward(cfg: Any) -> bool:
|
|
|
1228
1236
|
return any(str(kind).lower() in SPARSE_MLP_LAYER_KINDS | DENSE_MLP_LAYER_KINDS for kind in kinds)
|
|
1229
1237
|
|
|
1230
1238
|
|
|
1239
|
+
def dense_mlp_beside_experts(cfg: Any) -> bool:
|
|
1240
|
+
"""Whether a sparse layer *also* runs a dense MLP, whose output is added to the experts'.
|
|
1241
|
+
|
|
1242
|
+
Every other MoE family replaces the feed-forward with an expert bank, so a sparse layer has no
|
|
1243
|
+
dense MLP and no neuron basis. Gemma-4 does not: ``Gemma4TextDecoderLayer`` builds ``self.mlp`` on
|
|
1244
|
+
every layer, and where ``enable_moe_block`` is set it adds a routed branch *beside* it, combining
|
|
1245
|
+
them as ``hidden_states_1 + hidden_states_2`` -- two separately normed branches that both read the
|
|
1246
|
+
pre-feedforward residual.
|
|
1247
|
+
|
|
1248
|
+
Two consequences, and they pull in opposite directions, which is why this is its own fact. The
|
|
1249
|
+
parameter count of a sparse layer includes the dense MLP, so subtracting it (as the arithmetic
|
|
1250
|
+
does everywhere else) undercounts. And the dense neuron basis is real on a sparse layer here, so
|
|
1251
|
+
refusing ``mlp_act`` on one would be wrong.
|
|
1252
|
+
|
|
1253
|
+
Not the same thing as a shared expert: that lives inside the MoE block and is sized by
|
|
1254
|
+
``moe_intermediate_size``, while this branch is the ordinary ``intermediate_size`` MLP that the
|
|
1255
|
+
checkpoint's dense siblings also carry.
|
|
1256
|
+
"""
|
|
1257
|
+
return bool(config_attr(cfg, "enable_moe_block", False))
|
|
1258
|
+
|
|
1259
|
+
|
|
1231
1260
|
def is_moe_layer(cfg: Any, layer: int) -> bool:
|
|
1232
1261
|
"""Whether ``layer``'s ``mlp`` is a sparse MoE block rather than a dense MLP.
|
|
1233
1262
|
|
|
@@ -1322,18 +1351,36 @@ def head_dim_for_layer(
|
|
|
1322
1351
|
return head_dim if "sliding" in str(layer_types[layer]).lower() else global_head_dim
|
|
1323
1352
|
|
|
1324
1353
|
|
|
1325
|
-
def kv_heads_for_layer(
|
|
1354
|
+
def kv_heads_for_layer(
|
|
1355
|
+
n_kv_heads: int,
|
|
1356
|
+
layer: int,
|
|
1357
|
+
per_layer: tuple[int, ...] = (),
|
|
1358
|
+
global_kv_heads: int | None = None,
|
|
1359
|
+
layer_types: tuple[str, ...] | None = None,
|
|
1360
|
+
k_eq_v: bool = False,
|
|
1361
|
+
) -> int:
|
|
1326
1362
|
"""How many key/value heads ``layer`` attends with.
|
|
1327
1363
|
|
|
1328
|
-
One number for the whole model on every family but Gemma-4
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1364
|
+
One number for the whole model on every family but Gemma-4, whose full-attention layers carry 4
|
|
1365
|
+
where its sliding ones carry 16 on the 31B (2 against 8 on the 26B, 1 against 8 on the 12B).
|
|
1366
|
+
Getting it wrong is not a shape error (see :func:`effective_kv_heads`): the reshape succeeds into
|
|
1367
|
+
a head count the layer does not have.
|
|
1368
|
+
|
|
1369
|
+
Two spellings, as with :func:`head_dim_for_layer`. ``per_layer`` is the table a heterogeneous
|
|
1370
|
+
config states outright (transformers >= 5.15) and wins where it exists. ``global_kv_heads`` is the
|
|
1371
|
+
older ``num_global_key_value_heads``, and it applies only where the modeling code applies it:
|
|
1372
|
+
``Gemma4TextAttention`` takes it when ``attention_k_eq_v and not is_sliding``, and the model-wide
|
|
1373
|
+
count otherwise. The ``k_eq_v`` gate is not decoration -- E2B and E4B set that flag false and
|
|
1374
|
+
carry ``num_global_key_value_heads: null``, so reading the field unconditionally would be wrong
|
|
1375
|
+
the moment a checkpoint states one without switching the flag on.
|
|
1333
1376
|
"""
|
|
1334
1377
|
if per_layer and layer < len(per_layer):
|
|
1335
1378
|
return per_layer[layer]
|
|
1336
|
-
|
|
1379
|
+
if not (k_eq_v and global_kv_heads):
|
|
1380
|
+
return n_kv_heads
|
|
1381
|
+
if not layer_types or layer >= len(layer_types):
|
|
1382
|
+
return n_kv_heads
|
|
1383
|
+
return n_kv_heads if "sliding" in str(layer_types[layer]).lower() else global_kv_heads
|
|
1337
1384
|
|
|
1338
1385
|
|
|
1339
1386
|
# A scalar the attention module multiplies its value vectors by *after* the projection, so the tensor
|
|
@@ -1747,6 +1794,15 @@ class ModelFacts:
|
|
|
1747
1794
|
# prefer these; see :func:`per_layer_ints`.
|
|
1748
1795
|
per_layer_head_dim: tuple[int, ...] = ()
|
|
1749
1796
|
per_layer_kv_heads: tuple[int, ...] = ()
|
|
1797
|
+
# Gemma-4's kv-head count for its ``full_attention`` layers, which it states as
|
|
1798
|
+
# ``num_global_key_value_heads`` and applies only when ``attention_k_eq_v`` is on. The older
|
|
1799
|
+
# spelling of what ``per_layer_kv_heads`` carries on transformers >= 5.15; ask
|
|
1800
|
+
# :meth:`kv_heads_for_layer`.
|
|
1801
|
+
global_kv_heads: int | None = None
|
|
1802
|
+
# Gemma-4's ``attention_k_eq_v``: on its ``full_attention`` layers the value tensor *is* the key
|
|
1803
|
+
# projection's output (differently normed and un-RoPE'd), and those layers are built with
|
|
1804
|
+
# ``v_proj = None``. True on 26B/31B/12B, false on E2B/E4B.
|
|
1805
|
+
k_eq_v: bool = False
|
|
1750
1806
|
# The width of one value head, which differs from ``head_dim`` on MiMo-V2 and the DeepSeek MLA
|
|
1751
1807
|
# families. See :func:`value_head_dim`; ``value`` and ``z`` are this wide per head, not ``head_dim``.
|
|
1752
1808
|
v_head_dim: int = 0
|
|
@@ -1759,6 +1815,9 @@ class ModelFacts:
|
|
|
1759
1815
|
n_experts: int = 0
|
|
1760
1816
|
experts_per_token: int = 0
|
|
1761
1817
|
n_shared_experts: int = 0
|
|
1818
|
+
# Gemma-4: a sparse layer keeps its dense MLP and adds the routed branch beside it, so the two
|
|
1819
|
+
# coexist rather than the experts replacing the MLP. See :func:`dense_mlp_beside_experts`.
|
|
1820
|
+
dense_mlp_beside_experts: bool = False
|
|
1762
1821
|
# Layers whose ``mlp`` is a sparse block. Precomputed here because the branch needs config
|
|
1763
1822
|
# fields that the vLLM client does not carry across the process boundary.
|
|
1764
1823
|
moe_layers: tuple[int, ...] = ()
|
|
@@ -1842,8 +1901,10 @@ class ModelFacts:
|
|
|
1842
1901
|
return head_dim_for_layer(self.head_dim, self.global_head_dim, self.layer_types, layer, self.per_layer_head_dim)
|
|
1843
1902
|
|
|
1844
1903
|
def kv_heads_for_layer(self, layer: int) -> int:
|
|
1845
|
-
"""``layer``'s kv-head count. Prefer this to :attr:`n_kv_heads`, which is wrong on Gemma-4
|
|
1846
|
-
return kv_heads_for_layer(
|
|
1904
|
+
"""``layer``'s kv-head count. Prefer this to :attr:`n_kv_heads`, which is wrong on Gemma-4."""
|
|
1905
|
+
return kv_heads_for_layer(
|
|
1906
|
+
self.n_kv_heads, layer, self.per_layer_kv_heads, self.global_kv_heads, self.layer_types, self.k_eq_v
|
|
1907
|
+
)
|
|
1847
1908
|
|
|
1848
1909
|
def value_head_dim_for_layer(self, layer: int) -> int:
|
|
1849
1910
|
"""``layer``'s *value* head width, for reshaping ``value`` and ``z``. See :func:`value_head_dim`.
|
|
@@ -2198,11 +2259,14 @@ def resolve_facts(config: Any, *, n_layers_fallback: int | None = None) -> Model
|
|
|
2198
2259
|
global_head_dim=_first_int(cfg, ("global_head_dim",)) or None,
|
|
2199
2260
|
per_layer_head_dim=per_layer_ints(cfg, "head_dim", n_layers),
|
|
2200
2261
|
per_layer_kv_heads=per_layer_ints(cfg, "num_key_value_heads", n_layers),
|
|
2262
|
+
global_kv_heads=_first_int(cfg, ("num_global_key_value_heads",)) or None,
|
|
2263
|
+
k_eq_v=bool(config_attr(cfg, "attention_k_eq_v", False)),
|
|
2201
2264
|
v_head_dim=value_head_dim(cfg, head_dim),
|
|
2202
2265
|
first_kv_shared_layer=first_kv_shared_layer(cfg, n_layers),
|
|
2203
2266
|
n_experts=n_experts(cfg),
|
|
2204
2267
|
experts_per_token=_first_int(cfg, _EXPERTS_PER_TOKEN_FIELDS) or 0,
|
|
2205
2268
|
n_shared_experts=_first_int(cfg, _N_SHARED_EXPERTS_FIELDS) or 0,
|
|
2269
|
+
dense_mlp_beside_experts=dense_mlp_beside_experts(cfg),
|
|
2206
2270
|
moe_layers=tuple(layer for layer in range(n_layers) if is_moe_layer(cfg, layer)),
|
|
2207
2271
|
logit_multiplier=multiplier,
|
|
2208
2272
|
logit_multiplier_source=multiplier_source,
|
|
@@ -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
|
|
@@ -314,6 +315,11 @@ def read_attn_dims(hf_model_id: str, trust_remote_code: bool = True) -> dict[str
|
|
|
314
315
|
# Empty on a config that describes one shape for the whole model.
|
|
315
316
|
"per_layer_head_dim": model_facts.per_layer_head_dim,
|
|
316
317
|
"per_layer_kv_heads": model_facts.per_layer_kv_heads,
|
|
318
|
+
# The older spelling of that kv-head count (`num_global_key_value_heads`), and the flag
|
|
319
|
+
# Gemma-4 gates it on. Carried so a transformers below 5.15 -- which states no per-layer
|
|
320
|
+
# table -- still gets the wide layers' count right rather than the sliding one's.
|
|
321
|
+
"global_kv_heads": model_facts.global_kv_heads,
|
|
322
|
+
"k_eq_v": model_facts.k_eq_v,
|
|
317
323
|
# From here on a layer reuses an earlier layer's keys/values and has no v_proj to hook, so
|
|
318
324
|
# `value`/DFA is unavailable there (Gemma-4). None when every layer projects its own.
|
|
319
325
|
"first_kv_shared_layer": model_facts.first_kv_shared_layer,
|
|
@@ -406,7 +412,12 @@ def kv_heads_for_layer(dims: dict[str, Any], layer: int) -> int:
|
|
|
406
412
|
disagreement between the two is worth saying out loud.
|
|
407
413
|
"""
|
|
408
414
|
return facts.kv_heads_for_layer(
|
|
409
|
-
int(dims.get("n_kv_heads") or 0),
|
|
415
|
+
int(dims.get("n_kv_heads") or 0),
|
|
416
|
+
layer,
|
|
417
|
+
tuple(dims.get("per_layer_kv_heads") or ()),
|
|
418
|
+
dims.get("global_kv_heads"),
|
|
419
|
+
tuple(dims.get("layer_types") or ()),
|
|
420
|
+
bool(dims.get("k_eq_v")),
|
|
410
421
|
)
|
|
411
422
|
|
|
412
423
|
|
|
@@ -550,7 +561,10 @@ def recompute_attn_from_payloads(payloads, layers, dims, tensor_parallel_size: i
|
|
|
550
561
|
# one is not a claim about this layer -- it disagrees with Gemma-4's wide layers by design, and
|
|
551
562
|
# on an MLA or tensor-parallel capture it describes something other than the width in hand --
|
|
552
563
|
# so passing it as `expected` would turn a working recompute into a raise.
|
|
553
|
-
|
|
564
|
+
states_per_layer_kv = bool(dims.get("per_layer_kv_heads")) or bool(
|
|
565
|
+
dims.get("k_eq_v") and dims.get("global_kv_heads")
|
|
566
|
+
)
|
|
567
|
+
stated_kv = kv_heads_for_layer(dims, int(layer)) if states_per_layer_kv else None
|
|
554
568
|
n_kv_heads = _heads_in(k, head_dim, "k", int(layer), expected=stated_kv)
|
|
555
569
|
scores = recompute_attn_scores(
|
|
556
570
|
q,
|
|
@@ -1213,6 +1227,9 @@ class VLLMModel:
|
|
|
1213
1227
|
tuple(getattr(self, "_static_reads", ())),
|
|
1214
1228
|
tuple(getattr(self, "_static_writes", ())),
|
|
1215
1229
|
)
|
|
1230
|
+
# The line below forks a child that suppresses stdout by descriptor, which a
|
|
1231
|
+
# notebook kernel's stdout does not have. See `notebook_stdout`.
|
|
1232
|
+
ensure_stdout_descriptor()
|
|
1216
1233
|
self.engine = AsyncLLM.from_engine_args(AsyncEngineArgs(**self._engine_kwargs))
|
|
1217
1234
|
self._engine_loop = asyncio.get_running_loop()
|
|
1218
1235
|
return self.engine
|
|
@@ -446,9 +446,12 @@ def _config_weight_bytes(config: Any) -> int:
|
|
|
446
446
|
routed_mlp = f.n_experts * 3 * f.d_model * moe_inter
|
|
447
447
|
shared_mlp = f.n_shared_experts * 3 * f.d_model * moe_inter
|
|
448
448
|
n_sparse = len(f.moe_layers) if f.n_experts else 0
|
|
449
|
+
# Gemma-4's sparse layers keep their dense MLP beside the experts, so it is paid on every layer.
|
|
450
|
+
# It stays at the model dtype either way -- it is not part of the quantized routed bank.
|
|
451
|
+
n_dense = f.n_layers if f.dense_mlp_beside_experts else f.n_layers - n_sparse
|
|
449
452
|
embeddings = f.vocab_size * f.d_model * (1 if f.tied_embeddings else 2)
|
|
450
453
|
stored = _storage_dtype_bytes(config)
|
|
451
|
-
at_model_dtype = f.n_layers * attn +
|
|
454
|
+
at_model_dtype = f.n_layers * attn + n_dense * dense_mlp + n_sparse * shared_mlp + embeddings
|
|
452
455
|
return int(at_model_dtype * stored + n_sparse * routed_mlp * _expert_dtype_bytes(config, stored))
|
|
453
456
|
|
|
454
457
|
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/deepseek-v4-flash-0731__eager.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/deepseek-v4-flash-0731__vllm.json
RENAMED
|
File without changes
|
|
File without changes
|
{interp_engine-1.3.3 → interp_engine-1.3.5}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json
RENAMED
|
File without changes
|
|
File without changes
|