interp-engine 1.3.4__tar.gz → 1.3.6__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.4 → interp_engine-1.3.6}/PKG-INFO +1 -1
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/ARCHITECTURE_QUIRKS.md +49 -6
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/arch.py +20 -5
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/facts.py +103 -10
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_backend.py +16 -2
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/static.py +13 -3
- {interp_engine-1.3.4 → interp_engine-1.3.6}/pyproject.toml +1 -1
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_static_set.py +27 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/.gitignore +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/LICENSE +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/README.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/README.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/__init__.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/bench_spec.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/cells.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/probe.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/probe_lens_stream.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/publish.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/report_bench.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark-cudagraph.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/gemma-2-2b__eager.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3-4b__eager.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3-4b__vllm.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results-latest.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/run_all.sh +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/run_bench.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/workloads.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/AGENT_INTEGRATION.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/COMPATIBILITY.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/GRADIENTS.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/INTERNALS.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/PERFORMANCE.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/PORTING.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/README.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/SUPPORTED_POINTS.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/docs/USAGE.md +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/__init__.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/_loop.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/address.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/attn_config.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/attn_scores.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/autograd_support.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/capture.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/chat_compose.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/chat_conventions.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/chat_formatters.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/cuda_preflight.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/dispatch.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/hooks.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/lens.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/load.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/mappers.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/model.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/moe_routing.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/notebook_stdout.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/points.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/protocol.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/residual_basis.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/select.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/steer.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/steer_specs.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/sync.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/tokenize.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/__init__.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/_demux.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/_hooks.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/_payload.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/_tree.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/attn.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/capture.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/graphs.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/lens/__init__.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/lens/intervene.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/lens/readout.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/lens/unembed.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/mhc.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/native.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/requests.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_capture/steering.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/interp_engine/vllm_plugin.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/conftest.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/harness.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/model_expectations.yaml +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/synthetic_families.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_address.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_attn_config_tripwire.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_attn_probs_indexing.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_attn_scores.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_attn_z_gqa.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_autograd_support.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_bench_workloads.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_capability_refusals.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_capture_addressing.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_chat_compose.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_chat_formatters.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_chat_templates.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_core.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_cuda_preflight.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_doc_code_fences.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_eager_autograd.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_facts.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_family_points.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_gated_attn_out.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_head_contributions.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_hook_call_conventions.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_layer_kinds.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_load.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_logit_transform.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_mappers.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_mlp_internals.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_model_expectations.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_moe.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_multimodal_arch.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_new_models_gpu.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_no_chat_template.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_normalized_hook.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_notebook_stdout.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_packaging.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_parity_gpt2.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_per_layer_attn_dims.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_points_registry.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_protocol.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_published_benchmarks.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_qk_norm.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_qkv_layout.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_reasoning_spans.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_release.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_resid_mid.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_residual_basis.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_sandwich_norms.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_select.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_sliding_window_attn.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_small_models_gpu.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_static_dsv4_gpu.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_static_parity_gpu.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_static_warmup.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_steer_context.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_steer_math_parity.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_sync_loop.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_sync_parity.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_unified_free_functions.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_unresolved_families.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_capture_gpu.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_capture_scales.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_engine_loop.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_graph_path.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_graphs_on_gpu.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_hook_availability.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_hyper_connections.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_kv_isolation.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_new_points.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_only_families.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_plugin.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vllm_wire_grammar.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_vocabulary_boundary.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/tests/test_worker_lens_capture_readout.py +0 -0
- {interp_engine-1.3.4 → interp_engine-1.3.6}/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.6
|
|
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
|
|
@@ -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,
|
|
@@ -777,6 +777,35 @@ def mandatory_kv_cache_dtype(architectures: Sequence[str] | None) -> str | None:
|
|
|
777
777
|
return None
|
|
778
778
|
|
|
779
779
|
|
|
780
|
+
# A multimodal model whose image tokens attend bidirectionally (Gemma 3, Gemma 4) has chunked
|
|
781
|
+
# multimodal input forced OFF by vLLM, and one whole item must then fit in a single batch or the
|
|
782
|
+
# engine refuses to start: "Chunked MM input disabled but max_tokens_per_mm_item (2496) is larger
|
|
783
|
+
# than max_num_batched_tokens (512)". vLLM raises its own floor for this, but only while defaulting
|
|
784
|
+
# `max_num_batched_tokens`, and clamps the result against `max_model_len` immediately after -- so a
|
|
785
|
+
# caller who sizes the window to its prompt, which capture does, defeats the fix and gets the raise.
|
|
786
|
+
#
|
|
787
|
+
# A floor rather than the number, because the number is not in the HF config: the per-item token
|
|
788
|
+
# count comes from vLLM's processing info for that family (2496 on Gemma 4, 256 on Gemma 3), and
|
|
789
|
+
# deriving it here would mean reimplementing each family's image-token math against a private API.
|
|
790
|
+
# Generous enough for every multimodal family served so far; if one ever needs more, vLLM's refusal
|
|
791
|
+
# names the value it wanted, which is the only honest way to revise this.
|
|
792
|
+
MM_MIN_BATCHED_TOKENS = 8192
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def min_batched_tokens(config: Any) -> int | None:
|
|
796
|
+
"""The ``max_num_batched_tokens`` this checkpoint cannot boot below, or ``None`` if unconstrained.
|
|
797
|
+
|
|
798
|
+
Companion to :func:`mandatory_kv_cache_dtype`, and here for the same reason: a boot requirement
|
|
799
|
+
every harness has to know is one the library should answer, not one each caller rediscovers from
|
|
800
|
+
a stack trace.
|
|
801
|
+
|
|
802
|
+
Keyed on the config being a multimodal wrapper rather than on the family, since the constraint
|
|
803
|
+
follows from having a non-text modality at all. Text-only checkpoints get ``None`` and keep
|
|
804
|
+
vLLM's own defaults, which is what makes this safe to ask about unconditionally.
|
|
805
|
+
"""
|
|
806
|
+
return MM_MIN_BATCHED_TOKENS if config is not None and text_config(config) is not config else None
|
|
807
|
+
|
|
808
|
+
|
|
780
809
|
# --- quantization and the backward pass ---------------------------------------
|
|
781
810
|
#
|
|
782
811
|
# Quantization is invisible to *capture*: hooks read activations, which transformers dequantizes to a
|
|
@@ -1072,7 +1101,15 @@ def logit_multiplier(cfg: Any) -> tuple[float | None, str]:
|
|
|
1072
1101
|
|
|
1073
1102
|
# Four spellings of the routed-expert count, all in current use.
|
|
1074
1103
|
_N_EXPERTS_FIELDS: tuple[str, ...] = ("num_local_experts", "num_experts", "n_routed_experts", "moe_num_experts")
|
|
1075
|
-
|
|
1104
|
+
# ``top_k_experts`` is Gemma-4's. Missing it did not merely lose a number: :func:`assert_routing_shapes`
|
|
1105
|
+
# guards its width check with ``and top_k``, so a zero here switched off the check that catches a
|
|
1106
|
+
# router tuple read in the wrong order -- on the newest MoE family, and silently.
|
|
1107
|
+
_EXPERTS_PER_TOKEN_FIELDS: tuple[str, ...] = (
|
|
1108
|
+
"num_experts_per_tok",
|
|
1109
|
+
"experts_per_token",
|
|
1110
|
+
"moe_topk",
|
|
1111
|
+
"top_k_experts",
|
|
1112
|
+
)
|
|
1076
1113
|
_N_SHARED_EXPERTS_FIELDS: tuple[str, ...] = ("n_shared_experts", "num_shared_experts")
|
|
1077
1114
|
|
|
1078
1115
|
|
|
@@ -1228,6 +1265,27 @@ def block_types_name_the_feed_forward(cfg: Any) -> bool:
|
|
|
1228
1265
|
return any(str(kind).lower() in SPARSE_MLP_LAYER_KINDS | DENSE_MLP_LAYER_KINDS for kind in kinds)
|
|
1229
1266
|
|
|
1230
1267
|
|
|
1268
|
+
def dense_mlp_beside_experts(cfg: Any) -> bool:
|
|
1269
|
+
"""Whether a sparse layer *also* runs a dense MLP, whose output is added to the experts'.
|
|
1270
|
+
|
|
1271
|
+
Every other MoE family replaces the feed-forward with an expert bank, so a sparse layer has no
|
|
1272
|
+
dense MLP and no neuron basis. Gemma-4 does not: ``Gemma4TextDecoderLayer`` builds ``self.mlp`` on
|
|
1273
|
+
every layer, and where ``enable_moe_block`` is set it adds a routed branch *beside* it, combining
|
|
1274
|
+
them as ``hidden_states_1 + hidden_states_2`` -- two separately normed branches that both read the
|
|
1275
|
+
pre-feedforward residual.
|
|
1276
|
+
|
|
1277
|
+
Two consequences, and they pull in opposite directions, which is why this is its own fact. The
|
|
1278
|
+
parameter count of a sparse layer includes the dense MLP, so subtracting it (as the arithmetic
|
|
1279
|
+
does everywhere else) undercounts. And the dense neuron basis is real on a sparse layer here, so
|
|
1280
|
+
refusing ``mlp_act`` on one would be wrong.
|
|
1281
|
+
|
|
1282
|
+
Not the same thing as a shared expert: that lives inside the MoE block and is sized by
|
|
1283
|
+
``moe_intermediate_size``, while this branch is the ordinary ``intermediate_size`` MLP that the
|
|
1284
|
+
checkpoint's dense siblings also carry.
|
|
1285
|
+
"""
|
|
1286
|
+
return bool(config_attr(cfg, "enable_moe_block", False))
|
|
1287
|
+
|
|
1288
|
+
|
|
1231
1289
|
def is_moe_layer(cfg: Any, layer: int) -> bool:
|
|
1232
1290
|
"""Whether ``layer``'s ``mlp`` is a sparse MoE block rather than a dense MLP.
|
|
1233
1291
|
|
|
@@ -1322,18 +1380,36 @@ def head_dim_for_layer(
|
|
|
1322
1380
|
return head_dim if "sliding" in str(layer_types[layer]).lower() else global_head_dim
|
|
1323
1381
|
|
|
1324
1382
|
|
|
1325
|
-
def kv_heads_for_layer(
|
|
1383
|
+
def kv_heads_for_layer(
|
|
1384
|
+
n_kv_heads: int,
|
|
1385
|
+
layer: int,
|
|
1386
|
+
per_layer: tuple[int, ...] = (),
|
|
1387
|
+
global_kv_heads: int | None = None,
|
|
1388
|
+
layer_types: tuple[str, ...] | None = None,
|
|
1389
|
+
k_eq_v: bool = False,
|
|
1390
|
+
) -> int:
|
|
1326
1391
|
"""How many key/value heads ``layer`` attends with.
|
|
1327
1392
|
|
|
1328
|
-
One number for the whole model on every family but Gemma-4
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1393
|
+
One number for the whole model on every family but Gemma-4, whose full-attention layers carry 4
|
|
1394
|
+
where its sliding ones carry 16 on the 31B (2 against 8 on the 26B, 1 against 8 on the 12B).
|
|
1395
|
+
Getting it wrong is not a shape error (see :func:`effective_kv_heads`): the reshape succeeds into
|
|
1396
|
+
a head count the layer does not have.
|
|
1397
|
+
|
|
1398
|
+
Two spellings, as with :func:`head_dim_for_layer`. ``per_layer`` is the table a heterogeneous
|
|
1399
|
+
config states outright (transformers >= 5.15) and wins where it exists. ``global_kv_heads`` is the
|
|
1400
|
+
older ``num_global_key_value_heads``, and it applies only where the modeling code applies it:
|
|
1401
|
+
``Gemma4TextAttention`` takes it when ``attention_k_eq_v and not is_sliding``, and the model-wide
|
|
1402
|
+
count otherwise. The ``k_eq_v`` gate is not decoration -- E2B and E4B set that flag false and
|
|
1403
|
+
carry ``num_global_key_value_heads: null``, so reading the field unconditionally would be wrong
|
|
1404
|
+
the moment a checkpoint states one without switching the flag on.
|
|
1333
1405
|
"""
|
|
1334
1406
|
if per_layer and layer < len(per_layer):
|
|
1335
1407
|
return per_layer[layer]
|
|
1336
|
-
|
|
1408
|
+
if not (k_eq_v and global_kv_heads):
|
|
1409
|
+
return n_kv_heads
|
|
1410
|
+
if not layer_types or layer >= len(layer_types):
|
|
1411
|
+
return n_kv_heads
|
|
1412
|
+
return n_kv_heads if "sliding" in str(layer_types[layer]).lower() else global_kv_heads
|
|
1337
1413
|
|
|
1338
1414
|
|
|
1339
1415
|
# A scalar the attention module multiplies its value vectors by *after* the projection, so the tensor
|
|
@@ -1747,6 +1823,15 @@ class ModelFacts:
|
|
|
1747
1823
|
# prefer these; see :func:`per_layer_ints`.
|
|
1748
1824
|
per_layer_head_dim: tuple[int, ...] = ()
|
|
1749
1825
|
per_layer_kv_heads: tuple[int, ...] = ()
|
|
1826
|
+
# Gemma-4's kv-head count for its ``full_attention`` layers, which it states as
|
|
1827
|
+
# ``num_global_key_value_heads`` and applies only when ``attention_k_eq_v`` is on. The older
|
|
1828
|
+
# spelling of what ``per_layer_kv_heads`` carries on transformers >= 5.15; ask
|
|
1829
|
+
# :meth:`kv_heads_for_layer`.
|
|
1830
|
+
global_kv_heads: int | None = None
|
|
1831
|
+
# Gemma-4's ``attention_k_eq_v``: on its ``full_attention`` layers the value tensor *is* the key
|
|
1832
|
+
# projection's output (differently normed and un-RoPE'd), and those layers are built with
|
|
1833
|
+
# ``v_proj = None``. True on 26B/31B/12B, false on E2B/E4B.
|
|
1834
|
+
k_eq_v: bool = False
|
|
1750
1835
|
# The width of one value head, which differs from ``head_dim`` on MiMo-V2 and the DeepSeek MLA
|
|
1751
1836
|
# families. See :func:`value_head_dim`; ``value`` and ``z`` are this wide per head, not ``head_dim``.
|
|
1752
1837
|
v_head_dim: int = 0
|
|
@@ -1759,6 +1844,9 @@ class ModelFacts:
|
|
|
1759
1844
|
n_experts: int = 0
|
|
1760
1845
|
experts_per_token: int = 0
|
|
1761
1846
|
n_shared_experts: int = 0
|
|
1847
|
+
# Gemma-4: a sparse layer keeps its dense MLP and adds the routed branch beside it, so the two
|
|
1848
|
+
# coexist rather than the experts replacing the MLP. See :func:`dense_mlp_beside_experts`.
|
|
1849
|
+
dense_mlp_beside_experts: bool = False
|
|
1762
1850
|
# Layers whose ``mlp`` is a sparse block. Precomputed here because the branch needs config
|
|
1763
1851
|
# fields that the vLLM client does not carry across the process boundary.
|
|
1764
1852
|
moe_layers: tuple[int, ...] = ()
|
|
@@ -1842,8 +1930,10 @@ class ModelFacts:
|
|
|
1842
1930
|
return head_dim_for_layer(self.head_dim, self.global_head_dim, self.layer_types, layer, self.per_layer_head_dim)
|
|
1843
1931
|
|
|
1844
1932
|
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(
|
|
1933
|
+
"""``layer``'s kv-head count. Prefer this to :attr:`n_kv_heads`, which is wrong on Gemma-4."""
|
|
1934
|
+
return kv_heads_for_layer(
|
|
1935
|
+
self.n_kv_heads, layer, self.per_layer_kv_heads, self.global_kv_heads, self.layer_types, self.k_eq_v
|
|
1936
|
+
)
|
|
1847
1937
|
|
|
1848
1938
|
def value_head_dim_for_layer(self, layer: int) -> int:
|
|
1849
1939
|
"""``layer``'s *value* head width, for reshaping ``value`` and ``z``. See :func:`value_head_dim`.
|
|
@@ -2198,11 +2288,14 @@ def resolve_facts(config: Any, *, n_layers_fallback: int | None = None) -> Model
|
|
|
2198
2288
|
global_head_dim=_first_int(cfg, ("global_head_dim",)) or None,
|
|
2199
2289
|
per_layer_head_dim=per_layer_ints(cfg, "head_dim", n_layers),
|
|
2200
2290
|
per_layer_kv_heads=per_layer_ints(cfg, "num_key_value_heads", n_layers),
|
|
2291
|
+
global_kv_heads=_first_int(cfg, ("num_global_key_value_heads",)) or None,
|
|
2292
|
+
k_eq_v=bool(config_attr(cfg, "attention_k_eq_v", False)),
|
|
2201
2293
|
v_head_dim=value_head_dim(cfg, head_dim),
|
|
2202
2294
|
first_kv_shared_layer=first_kv_shared_layer(cfg, n_layers),
|
|
2203
2295
|
n_experts=n_experts(cfg),
|
|
2204
2296
|
experts_per_token=_first_int(cfg, _EXPERTS_PER_TOKEN_FIELDS) or 0,
|
|
2205
2297
|
n_shared_experts=_first_int(cfg, _N_SHARED_EXPERTS_FIELDS) or 0,
|
|
2298
|
+
dense_mlp_beside_experts=dense_mlp_beside_experts(cfg),
|
|
2206
2299
|
moe_layers=tuple(layer for layer in range(n_layers) if is_moe_layer(cfg, layer)),
|
|
2207
2300
|
logit_multiplier=multiplier,
|
|
2208
2301
|
logit_multiplier_source=multiplier_source,
|
|
@@ -315,6 +315,11 @@ def read_attn_dims(hf_model_id: str, trust_remote_code: bool = True) -> dict[str
|
|
|
315
315
|
# Empty on a config that describes one shape for the whole model.
|
|
316
316
|
"per_layer_head_dim": model_facts.per_layer_head_dim,
|
|
317
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,
|
|
318
323
|
# From here on a layer reuses an earlier layer's keys/values and has no v_proj to hook, so
|
|
319
324
|
# `value`/DFA is unavailable there (Gemma-4). None when every layer projects its own.
|
|
320
325
|
"first_kv_shared_layer": model_facts.first_kv_shared_layer,
|
|
@@ -407,7 +412,12 @@ def kv_heads_for_layer(dims: dict[str, Any], layer: int) -> int:
|
|
|
407
412
|
disagreement between the two is worth saying out loud.
|
|
408
413
|
"""
|
|
409
414
|
return facts.kv_heads_for_layer(
|
|
410
|
-
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")),
|
|
411
421
|
)
|
|
412
422
|
|
|
413
423
|
|
|
@@ -551,7 +561,10 @@ def recompute_attn_from_payloads(payloads, layers, dims, tensor_parallel_size: i
|
|
|
551
561
|
# one is not a claim about this layer -- it disagrees with Gemma-4's wide layers by design, and
|
|
552
562
|
# on an MLA or tensor-parallel capture it describes something other than the width in hand --
|
|
553
563
|
# so passing it as `expected` would turn a working recompute into a raise.
|
|
554
|
-
|
|
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
|
|
555
568
|
n_kv_heads = _heads_in(k, head_dim, "k", int(layer), expected=stated_kv)
|
|
556
569
|
scores = recompute_attn_scores(
|
|
557
570
|
q,
|
|
@@ -1121,6 +1134,7 @@ class VLLMModel:
|
|
|
1121
1134
|
),
|
|
1122
1135
|
n_layers=self.num_hidden_layers,
|
|
1123
1136
|
tensor_parallel_size=self.tensor_parallel_size,
|
|
1137
|
+
min_n=facts.min_batched_tokens(cfg) or 0,
|
|
1124
1138
|
)
|
|
1125
1139
|
if fitted != max_n:
|
|
1126
1140
|
logger.warning("lowering max_num_batched_tokens %s -> %s so static buffers fit", max_n, fitted)
|
|
@@ -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
|
|
|
@@ -541,6 +544,7 @@ def fit_max_num_batched_tokens(
|
|
|
541
544
|
kv_width: int,
|
|
542
545
|
n_layers: int,
|
|
543
546
|
tensor_parallel_size: int = 1,
|
|
547
|
+
min_n: int = 0,
|
|
544
548
|
) -> int:
|
|
545
549
|
"""Largest capture size at or below ``max_n`` whose static buffers fit, or raise.
|
|
546
550
|
|
|
@@ -558,14 +562,20 @@ def fit_max_num_batched_tokens(
|
|
|
558
562
|
The 1024 floor is for ``"auto"`` / default ``max_n``: vLLM will not usefully serve with a
|
|
559
563
|
tiny batch. A caller who already passed a smaller ``max_n`` (chunked-prefill tests) keeps it
|
|
560
564
|
when it fits.
|
|
565
|
+
|
|
566
|
+
``min_n`` raises that floor to a size the *engine* will not start below, which is a different
|
|
567
|
+
kind of limit and has to outrank the fit: shrinking under it trades an out-of-memory error for a
|
|
568
|
+
refusal to boot, and the refusal comes from vLLM's scheduler talking about multimodal items,
|
|
569
|
+
which reads as anything but a static-buffer problem. Above the floor it changes nothing --
|
|
570
|
+
``fitted == asked`` stays the common case. See :func:`interp_engine.facts.min_batched_tokens`.
|
|
561
571
|
"""
|
|
562
572
|
graph_fudge = 3 * 1024**3
|
|
563
573
|
tp = max(int(tensor_parallel_size), 1)
|
|
564
574
|
min_kv = max(int(n_layers), 1) * max(int(max_model_len), 1) * max(int(kv_width), 1) * 2
|
|
565
575
|
budget = int(gpu_memory_utilization * device_memory) - int(weight_bytes) // tp - graph_fudge - min_kv
|
|
566
576
|
asked = int(max_n)
|
|
567
|
-
floor = min(1024, asked)
|
|
568
|
-
candidates = [asked] + [s for s in _CAPTURE_SIZES if s < asked]
|
|
577
|
+
floor = max(min(1024, asked), int(min_n))
|
|
578
|
+
candidates = [max(asked, floor)] + [s for s in _CAPTURE_SIZES if s < asked]
|
|
569
579
|
for n in candidates:
|
|
570
580
|
if n < floor:
|
|
571
581
|
continue
|
|
@@ -221,6 +221,33 @@ def test_vram_check_lowers_max_n_instead_of_ooming():
|
|
|
221
221
|
assert fitted >= 1024
|
|
222
222
|
|
|
223
223
|
|
|
224
|
+
def test_vram_check_will_not_shrink_below_an_engine_boot_floor():
|
|
225
|
+
"""A multimodal prefix-LM will not start below one whole image, so shrinking under it is no fit.
|
|
226
|
+
|
|
227
|
+
Better to refuse here, naming the buffers, than to hand back a size vLLM rejects later with a
|
|
228
|
+
scheduler error about multimodal items that reads as anything but a static-buffer problem.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def fit(device_gib: int, **extra: int) -> int:
|
|
232
|
+
return fit_max_num_batched_tokens(
|
|
233
|
+
n_sites=80,
|
|
234
|
+
width=5120,
|
|
235
|
+
max_n=16384,
|
|
236
|
+
device_memory=device_gib * 1024**3,
|
|
237
|
+
gpu_memory_utilization=0.95,
|
|
238
|
+
weight_bytes=10 * 1024**3,
|
|
239
|
+
max_model_len=4096,
|
|
240
|
+
kv_width=kv_cache_width(d_model=5120),
|
|
241
|
+
n_layers=32,
|
|
242
|
+
**extra,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
assert fit(20) == 4096 # left alone, this card shrinks under the floor
|
|
246
|
+
with pytest.raises(ValueError, match="do not fit even at max_num_batched_tokens=8192"):
|
|
247
|
+
fit(20, min_n=8192)
|
|
248
|
+
assert fit(24, min_n=8192) == 8192 # a floor it can meet changes nothing
|
|
249
|
+
|
|
250
|
+
|
|
224
251
|
def test_vram_check_keeps_a_caller_max_n_below_the_1024_floor():
|
|
225
252
|
"""Chunked-prefill tests pass max_num_batched_tokens=32; skipping that candidate used to raise."""
|
|
226
253
|
fitted = fit_max_num_batched_tokens(
|
|
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.4 → interp_engine-1.3.6}/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.4 → interp_engine-1.3.6}/benchmarks/results/deepseek-v4-flash-0731__vllm.json
RENAMED
|
File without changes
|
|
File without changes
|
{interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json
RENAMED
|
File without changes
|
{interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/llama-3.1-8b__vllm-static.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3-4b__vllm-cudagraph.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json
RENAMED
|
File without changes
|
{interp_engine-1.3.4 → interp_engine-1.3.6}/benchmarks/results/qwen3.8-27b__vllm-static.json
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|