interp-engine 1.8.0__tar.gz → 1.9.0__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.8.0 → interp_engine-1.9.0}/PKG-INFO +1 -1
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/probe.py +8 -2
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/run_all.sh +29 -5
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/run_bench.py +25 -6
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/AGENT_INTEGRATION.md +7 -5
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/INTERNALS.md +9 -1
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/SUPPORTED_POINTS.md +14 -11
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/capture.py +2 -2
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/facts.py +19 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/load.py +4 -3
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/memory.py +72 -15
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/model.py +153 -12
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/points.py +2 -1
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/protocol.py +1 -1
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_backend.py +22 -10
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/__init__.py +2 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/_hooks.py +15 -3
- interp_engine-1.9.0/interp_engine/vllm_capture/_tp.py +147 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/_tree.py +104 -20
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/attn.py +43 -5
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/capture.py +16 -2
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/requests.py +36 -5
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/static.py +42 -7
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_plugin.py +10 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/pyproject.toml +2 -1
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_facts.py +7 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_load.py +98 -1
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_memory.py +56 -0
- interp_engine-1.9.0/tests/test_multigpu.py +236 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_multimodal_arch.py +2 -1
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_static_set.py +18 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_kv_isolation.py +2 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_new_points.py +112 -21
- {interp_engine-1.8.0 → interp_engine-1.9.0}/.gitignore +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/LICENSE +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/README.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/README.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/__init__.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/bench_spec.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/cells.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/probe_lens_stream.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/publish.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/report_bench.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/deepseek-v4-flash-0731__vllm-static.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/gemma-2-2b__eager.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/gemma-2-2b__vllm-static.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/llama-3.1-8b__vllm-static.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/qwen3-4b__eager.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/qwen3-4b__vllm-static.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/qwen3-4b__vllm.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/qwen3.8-27b__vllm-static.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/results-latest.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/benchmarks/workloads.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/ARCHITECTURE_QUIRKS.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/COMPATIBILITY.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/GRADIENTS.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/PERFORMANCE.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/PORTING.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/README.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/docs/USAGE.md +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/__init__.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/_loop.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/address.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/arch.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/attn_config.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/attn_scores.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/autograd_support.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/chat_compose.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/chat_conventions.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/chat_formatters.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/cuda_preflight.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/dispatch.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/hooks.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/lens.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/mappers.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/moe_routing.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/notebook_stdout.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/residual_basis.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/select.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/steer.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/steer_specs.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/sync.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/tokenize.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/_demux.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/_payload.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/graphs.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/lens/__init__.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/lens/intervene.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/lens/readout.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/lens/unembed.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/mhc.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/native.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/interp_engine/vllm_capture/steering.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/conftest.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/harness.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/model_expectations.yaml +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/synthetic_families.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_address.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_attn_config_tripwire.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_attn_probs_indexing.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_attn_scores.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_attn_z_gqa.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_autograd_support.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_bench_workloads.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_capability_refusals.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_capture_addressing.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_chat_compose.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_chat_formatters.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_chat_templates.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_core.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_cuda_preflight.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_doc_code_fences.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_eager_autograd.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_family_points.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_gated_attn_out.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_gpu_sizer.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_head_contributions.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_hook_call_conventions.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_injected_system_spans.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_layer_kinds.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_logit_transform.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_mappers.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_mlp_internals.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_model_expectations.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_moe.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_new_models_gpu.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_no_chat_template.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_normalized_hook.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_notebook_stdout.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_packaging.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_parity_gpt2.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_per_layer_attn_dims.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_points_registry.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_protocol.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_published_benchmarks.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_qk_norm.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_qkv_layout.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_reasoning_spans.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_release.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_resid_mid.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_residual_basis.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_sandwich_norms.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_select.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_sliding_window_attn.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_small_models_gpu.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_static_dsv4_gpu.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_static_parity_gpu.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_static_warmup.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_steer_context.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_steer_math_parity.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_sync_loop.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_sync_parity.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_unified_free_functions.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_unresolved_families.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_capture_gpu.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_capture_scales.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_engine_loop.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_graph_path.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_graphs_on_gpu.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_hook_availability.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_hyper_connections.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_only_families.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_plugin.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vllm_wire_grammar.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_vocabulary_boundary.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/tests/test_worker_lens_capture_readout.py +0 -0
- {interp_engine-1.8.0 → interp_engine-1.9.0}/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
|
+
Version: 1.9.0
|
|
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
|
|
@@ -21,7 +21,11 @@ class EnvStamp:
|
|
|
21
21
|
"""What the numbers were produced on. Recorded per run, so a stale result is identifiable."""
|
|
22
22
|
|
|
23
23
|
gpu_name: str = ""
|
|
24
|
+
"""The card, prefixed with the count when the model was sharded across more than one
|
|
25
|
+
(``2x NVIDIA A40``), so every view keyed on this field says how many cards the number is from."""
|
|
24
26
|
gpu_total_gib: float = 0.0
|
|
27
|
+
"""Per card, not summed: the figure a reader compares against a card's spec sheet."""
|
|
28
|
+
gpu_count: int = 1
|
|
25
29
|
driver_version: str = ""
|
|
26
30
|
cuda_version: str = ""
|
|
27
31
|
torch_version: str = ""
|
|
@@ -76,11 +80,13 @@ def _driver_version() -> str:
|
|
|
76
80
|
return out.stdout.strip().splitlines()[0].strip() if out.stdout.strip() else "unknown"
|
|
77
81
|
|
|
78
82
|
|
|
79
|
-
def env_stamp(index: int = 0) -> EnvStamp:
|
|
83
|
+
def env_stamp(index: int = 0, num_gpus: int = 1) -> EnvStamp:
|
|
80
84
|
props = torch.cuda.get_device_properties(index)
|
|
85
|
+
name = torch.cuda.get_device_name(index)
|
|
81
86
|
return EnvStamp(
|
|
82
|
-
gpu_name=
|
|
87
|
+
gpu_name=f"{num_gpus}x {name}" if num_gpus > 1 else name,
|
|
83
88
|
gpu_total_gib=props.total_memory / GIB,
|
|
89
|
+
gpu_count=num_gpus,
|
|
84
90
|
driver_version=_driver_version(),
|
|
85
91
|
cuda_version=torch.version.cuda or "unknown",
|
|
86
92
|
torch_version=torch.__version__,
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
# bash benchmarks/run_all.sh --models gemma-2-2b,qwen3-4b --variants eager,vllm
|
|
12
12
|
# bash benchmarks/run_all.sh --workloads generate,capture_mid --no-report
|
|
13
13
|
# bash benchmarks/run_all.sh --gpu-memory-utilization 0.7 # smaller card, or more worker scratch
|
|
14
|
+
# bash benchmarks/run_all.sh --num-gpus 1 --models qwen3.8-27b # pin a multi-card box to one card
|
|
15
|
+
#
|
|
16
|
+
# Every cell is sharded across every visible CUDA card unless --num-gpus says otherwise.
|
|
14
17
|
# BENCH_PYTHON=/path/to/venv/bin/python bash benchmarks/run_all.sh
|
|
15
18
|
#
|
|
16
19
|
# With no --models, the sweep runs every model in the spec that this card can hold and names the ones
|
|
@@ -43,6 +46,7 @@ MODELS=""
|
|
|
43
46
|
VARIANTS=""
|
|
44
47
|
WORKLOADS=""
|
|
45
48
|
GPU_MEM_UTIL=""
|
|
49
|
+
NUM_GPUS=""
|
|
46
50
|
RUN_REPORT=1
|
|
47
51
|
SKIP_EXISTING=0
|
|
48
52
|
|
|
@@ -52,6 +56,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
52
56
|
--variants) VARIANTS="$2"; shift 2 ;;
|
|
53
57
|
--workloads) WORKLOADS="$2"; shift 2 ;;
|
|
54
58
|
--gpu-memory-utilization) GPU_MEM_UTIL="$2"; shift 2 ;;
|
|
59
|
+
--num-gpus) NUM_GPUS="$2"; shift 2 ;;
|
|
55
60
|
--python) PYTHON="$2"; shift 2 ;;
|
|
56
61
|
--no-report) RUN_REPORT=0; shift ;;
|
|
57
62
|
# Resume a sweep that was interrupted. Off by default: a normal rerun should replace stale
|
|
@@ -70,13 +75,30 @@ if ! "$PYTHON" -c 'import interp_engine' 2>/dev/null; then
|
|
|
70
75
|
exit 2
|
|
71
76
|
fi
|
|
72
77
|
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
# No --num-gpus means every card the process can see: CUDA_VISIBLE_DEVICES when set, else what
|
|
79
|
+
# nvidia-smi lists, else 1.
|
|
80
|
+
if [[ -z "$NUM_GPUS" ]]; then
|
|
81
|
+
if [[ -n "${CUDA_VISIBLE_DEVICES+x}" ]]; then
|
|
82
|
+
NUM_GPUS=$(echo "$CUDA_VISIBLE_DEVICES" | tr ',' '\n' | grep -c .)
|
|
83
|
+
else
|
|
84
|
+
NUM_GPUS=$(nvidia-smi -L 2>/dev/null | grep -c '^GPU')
|
|
85
|
+
fi
|
|
86
|
+
(( NUM_GPUS > 0 )) || NUM_GPUS=1
|
|
87
|
+
fi
|
|
88
|
+
if ! [[ "$NUM_GPUS" =~ ^[1-9][0-9]*$ ]]; then
|
|
89
|
+
echo "error: --num-gpus must be a positive integer (got '$NUM_GPUS')" >&2
|
|
90
|
+
exit 2
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Total VRAM this sweep will use, in GiB, or empty if there is no nvidia-smi to ask: the first
|
|
94
|
+
# --num-gpus cards summed, since that is what a sharded load has to fit into. GiB rather than the
|
|
95
|
+
# vendor's GB, to match `min_gpu_gib` in the spec and the `gpu_total_gib` every cell records -- a
|
|
96
|
+
# "180 GB" B200 reads as 179 GiB here, and comparing the two units is how a row gets dropped on the
|
|
97
|
+
# one card that fits it.
|
|
77
98
|
gpu_total_gib() {
|
|
78
99
|
local mib
|
|
79
|
-
mib="$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null
|
|
100
|
+
mib="$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
|
|
101
|
+
| head -n "$NUM_GPUS" | tr -d ' ' | awk '{ s += $1 } END { if (NR) print s }')"
|
|
80
102
|
[[ -z "$mib" ]] && return 0
|
|
81
103
|
awk -v mib="$mib" 'BEGIN { printf "%.1f", mib / 1024 }'
|
|
82
104
|
}
|
|
@@ -171,6 +193,7 @@ for pair in "${PAIRS[@]}"; do
|
|
|
171
193
|
args=(--model "$model" --variant "$variant")
|
|
172
194
|
[[ -n "$WORKLOADS" ]] && args+=(--workloads "$WORKLOADS")
|
|
173
195
|
[[ -n "$GPU_MEM_UTIL" ]] && args+=(--gpu-memory-utilization "$GPU_MEM_UTIL")
|
|
196
|
+
(( NUM_GPUS > 1 )) && args+=(--num-gpus "$NUM_GPUS")
|
|
174
197
|
|
|
175
198
|
# TOKENIZERS_PARALLELISM: the tokenizer is forked by vLLM's workers and warns on every cell
|
|
176
199
|
# otherwise. VLLM_LOGGING_LEVEL: vLLM's per-step INFO logging would bury the workload lines.
|
|
@@ -200,6 +223,7 @@ if (( RUN_REPORT )); then
|
|
|
200
223
|
[[ -n "$VARIANTS" ]] && sweep_cmd="$sweep_cmd --variants $VARIANTS"
|
|
201
224
|
[[ -n "$WORKLOADS" ]] && sweep_cmd="$sweep_cmd --workloads $WORKLOADS"
|
|
202
225
|
[[ -n "$GPU_MEM_UTIL" ]] && sweep_cmd="$sweep_cmd --gpu-memory-utilization $GPU_MEM_UTIL"
|
|
226
|
+
(( NUM_GPUS > 1 )) && sweep_cmd="$sweep_cmd --num-gpus $NUM_GPUS"
|
|
203
227
|
"$PYTHON" -m benchmarks.report_bench --sweep-command "$sweep_cmd"
|
|
204
228
|
fi
|
|
205
229
|
|
|
@@ -209,9 +209,13 @@ def _load_kwargs(
|
|
|
209
209
|
m: ModelSpec,
|
|
210
210
|
*,
|
|
211
211
|
gpu_memory_utilization: float = GPU_MEMORY_UTILIZATION,
|
|
212
|
+
num_gpus: int = 1,
|
|
212
213
|
) -> dict[str, Any]:
|
|
213
214
|
"""Variant kwargs plus the per-backend knobs that only one backend accepts.
|
|
214
215
|
|
|
216
|
+
``num_gpus`` is ``load_model``'s own knob and goes through as is: tensor parallelism on vLLM,
|
|
217
|
+
accelerate's layer placement on eager, where it takes the place of ``device="cuda"``.
|
|
218
|
+
|
|
215
219
|
``max_model_len`` and ``gpu_memory_utilization`` are vLLM-only, and passing them to the eager
|
|
216
220
|
constructor would raise. Kept here rather than duplicated into every vLLM variant so the variant
|
|
217
221
|
table stays about the thing being varied.
|
|
@@ -234,6 +238,8 @@ def _load_kwargs(
|
|
|
234
238
|
kwargs = dict(v.kwargs)
|
|
235
239
|
if kwargs.get("static_writes") == STEER_WRITES:
|
|
236
240
|
kwargs["static_writes"] = [_steer_site(v, m)]
|
|
241
|
+
if num_gpus > 1:
|
|
242
|
+
kwargs["num_gpus"] = num_gpus
|
|
237
243
|
# Every vLLM backend, not just the hooked one: these are engine settings, and a graph variant that
|
|
238
244
|
# silently lost `max_model_len` would be measured against a different context than the row beside
|
|
239
245
|
# it. `VLLM_BACKENDS` is the engine's own list, so a fourth backend is covered the day it lands.
|
|
@@ -270,9 +276,10 @@ def _load_kwargs(
|
|
|
270
276
|
if isinstance(declared_model_kwargs, dict):
|
|
271
277
|
merged.update(declared_model_kwargs)
|
|
272
278
|
kwargs["model_kwargs"] = merged
|
|
273
|
-
# A device_map places the weights itself, and `load_model` drops `device` when one is given
|
|
274
|
-
#
|
|
275
|
-
|
|
279
|
+
# A device_map places the weights itself, and `load_model` drops `device` when one is given
|
|
280
|
+
# -- as it does at `num_gpus > 1`, which is a device_map. Setting it anyway would put a
|
|
281
|
+
# `device="cuda"` in the recorded kwargs that had no effect.
|
|
282
|
+
if "device_map" not in kwargs and num_gpus == 1:
|
|
276
283
|
kwargs.setdefault("device", "cuda")
|
|
277
284
|
return kwargs
|
|
278
285
|
|
|
@@ -284,10 +291,11 @@ async def run_cell(
|
|
|
284
291
|
*,
|
|
285
292
|
command: str,
|
|
286
293
|
gpu_memory_utilization: float = GPU_MEMORY_UTILIZATION,
|
|
294
|
+
num_gpus: int = 1,
|
|
287
295
|
) -> dict[str, Any]:
|
|
288
296
|
from interp_engine import load_model
|
|
289
297
|
|
|
290
|
-
stamp = env_stamp()
|
|
298
|
+
stamp = env_stamp(num_gpus=num_gpus)
|
|
291
299
|
record: dict[str, Any] = {
|
|
292
300
|
"schema": SCHEMA,
|
|
293
301
|
"started_at": datetime.now(UTC).isoformat(timespec="seconds"),
|
|
@@ -298,7 +306,7 @@ async def run_cell(
|
|
|
298
306
|
"env": dataclasses.asdict(stamp),
|
|
299
307
|
"workloads": {},
|
|
300
308
|
}
|
|
301
|
-
kwargs = _load_kwargs(variant, model_spec, gpu_memory_utilization=gpu_memory_utilization)
|
|
309
|
+
kwargs = _load_kwargs(variant, model_spec, gpu_memory_utilization=gpu_memory_utilization, num_gpus=num_gpus)
|
|
302
310
|
record["variant"]["kwargs"] = {k: str(val) for k, val in kwargs.items()}
|
|
303
311
|
record["model"]["native_dtype"] = _native_dtype(model_spec.hf_id)
|
|
304
312
|
|
|
@@ -392,8 +400,18 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
392
400
|
f"model's own declared fraction, or {GPU_MEMORY_UTILIZATION} where it has none"
|
|
393
401
|
),
|
|
394
402
|
)
|
|
403
|
+
p.add_argument(
|
|
404
|
+
"--num-gpus",
|
|
405
|
+
type=int,
|
|
406
|
+
default=1,
|
|
407
|
+
help="shard the model across this many cards (vLLM tensor parallelism; accelerate placement "
|
|
408
|
+
"on eager). Stamped on the cell, since a two-card number is not a one-card number",
|
|
409
|
+
)
|
|
395
410
|
p.add_argument("--list", action="store_true", help="print the models, variants and workloads and exit")
|
|
396
|
-
|
|
411
|
+
args = p.parse_args(argv)
|
|
412
|
+
if args.num_gpus < 1:
|
|
413
|
+
p.error("--num-gpus must be at least 1")
|
|
414
|
+
return args
|
|
397
415
|
|
|
398
416
|
|
|
399
417
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -455,6 +473,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
455
473
|
workload_keys,
|
|
456
474
|
command=command,
|
|
457
475
|
gpu_memory_utilization=bench_spec.gpu_memory_utilization_for(model_spec.key, args.gpu_memory_utilization),
|
|
476
|
+
num_gpus=args.num_gpus,
|
|
458
477
|
)
|
|
459
478
|
)
|
|
460
479
|
|
|
@@ -180,7 +180,7 @@ Canonical names, with the layer after a dot: `resid_post.10`. Extra coordinates
|
|
|
180
180
|
| `mlp_act`, `mlp_pre`, `mlp_pre_linear` | MLP internals, `d_mlp` wide | **eager only** |
|
|
181
181
|
| `router_logits` | MoE routing scores, every expert | both |
|
|
182
182
|
| `expert_weights`, `expert_indices` | the top-k the router selected, and its weights | **eager only** |
|
|
183
|
-
| the QK-norm points | inside the attention module | both
|
|
183
|
+
| the QK-norm points | inside the attention module | both (head-sharded; gathered across TP ranks) |
|
|
184
184
|
|
|
185
185
|
Eager-only is not an omission: vLLM's fused MLP and MoE kernels compute those tensors inline, so
|
|
186
186
|
there is no module boundary to hook. This table is the working subset;
|
|
@@ -191,7 +191,8 @@ Attention is the one row that reads "both" with a caveat. No boundary holds a sc
|
|
|
191
191
|
backend, so `capture_attention(model, tokens, layers)` is how you ask, and it returns the same
|
|
192
192
|
`{layer: {"scores", "probs", "value"}}` either way — from `output_attentions` on eager (which needs
|
|
193
193
|
the model loaded with `attn_implementation="eager"`) and from an off-kernel recompute over captured
|
|
194
|
-
post-RoPE q/k on vLLM (
|
|
194
|
+
post-RoPE q/k on vLLM (gathered across ranks under tensor parallelism, so it works at any
|
|
195
|
+
`num_gpus`). Different code paths, same contract; `value` there is the
|
|
195
196
|
per-head, family-scaled tensor satisfying `probs @ value == z`, not the raw projection output.
|
|
196
197
|
|
|
197
198
|
[ENGINE_HOOK_MAPPINGS.md](ENGINE_HOOK_MAPPINGS.md) is the full dictionary across all three
|
|
@@ -228,9 +229,10 @@ an obvious one.
|
|
|
228
229
|
always, on every configuration — the unembed happens in another process. Eager can do it, but only
|
|
229
230
|
if the model was loaded with `requires_grad=True`. Gate on `model.grad_support`, not on backend
|
|
230
231
|
name. See [GRADIENTS.md](GRADIENTS.md).
|
|
231
|
-
5. **vLLM with `num_gpus > 1` serves
|
|
232
|
-
across ranks,
|
|
233
|
-
|
|
232
|
+
5. **vLLM with `num_gpus > 1` serves the same points as one GPU.** Heads and MLP neurons are
|
|
233
|
+
sharded across ranks, and the worker gathers `z`, `value`, `mlp_act`, the QK-norm points and the
|
|
234
|
+
q/k behind `capture_attention` back to full width at collect, so rank 0's payload is the whole
|
|
235
|
+
tensor. Every `collect_*` is a collective on every rank; do not call one from a single rank.
|
|
234
236
|
6. **`await model.warmup()` before timing anything.** Construction is deliberately cheap and lazy on
|
|
235
237
|
both backends — on vLLM nearly the whole load happens in `warmup()`, so without it your first
|
|
236
238
|
request's latency is the load time.
|
|
@@ -108,7 +108,15 @@ index names the layer HF's does, which nothing checked before and which would fa
|
|
|
108
108
|
than raise. It needs `interp-engine[vllm]`, so it self-skips elsewhere; note that running it via
|
|
109
109
|
`.venv-vllm/bin/python` needs that directory on `PATH` too, because vLLM shells out to `ninja` to
|
|
110
110
|
build a sampler kernel at startup. `tests/test_vllm_wire_grammar.py` covers the same process
|
|
111
|
-
boundary on CPU, over a synthetic demux.
|
|
111
|
+
boundary on CPU, over a synthetic demux. `tests/test_multigpu.py` (`-m multigpu`, two CUDA cards)
|
|
112
|
+
repeats the comparison at `num_gpus=2`: eager under accelerate's layer placement and vLLM under
|
|
113
|
+
tensor parallelism, where `vllm_capture/_tp.py` gathers the head- and neuron-sharded points across
|
|
114
|
+
ranks, against the one-card eager reference — captures, attention, steering, decode and the lens.
|
|
115
|
+
One rule follows from tensor parallelism: a worker RPC refuses by *returning* a reason, never by
|
|
116
|
+
raising. Under vLLM's multiprocess executor a raise inside `collective_rpc` is read from one rank
|
|
117
|
+
while the others' replies stay queued, and the next RPC consumes those — so a single refused layer
|
|
118
|
+
took every point of a TP=8 cell with it. `resolvable_points` and `resolvable_attn` are the ask-first
|
|
119
|
+
calls; `tests/test_vllm_new_points.py` pins that they install nothing.
|
|
112
120
|
|
|
113
121
|
**The docs are parsed, not maintained.** The point table in
|
|
114
122
|
[SUPPORTED_POINTS.md](SUPPORTED_POINTS.md) and the footnote markers in
|
|
@@ -14,12 +14,12 @@ nnsight and nnterp.
|
|
|
14
14
|
| [`embeddings`][embeddings] | `d_model` | ✅ | ✅ | trunk-level, so addressed with no layer index; distinct from `resid_pre` at layer 0 only where the trunk adds positional embeddings or scales the embedding |
|
|
15
15
|
| [`resid_pre`][resid_pre] | `d_model` | ✅ | ✅ | |
|
|
16
16
|
| [`attn_in`][attn_in] | `d_model` | ✅ | ✅ | |
|
|
17
|
-
| [`q_norm_in`][q_norm_in] / [`q_norm_out`][q_norm_out] | `n_heads * head_dim` | ✅ | ✅ | head-sharded
|
|
18
|
-
| [`k_norm_in`][k_norm_in] / [`k_norm_out`][k_norm_out] | `n_kv_heads * head_dim` | ✅ | ✅ | head-sharded
|
|
19
|
-
| [`value`][value] | `n_heads * head_dim` | ✅ | ✅ | head-sharded
|
|
17
|
+
| [`q_norm_in`][q_norm_in] / [`q_norm_out`][q_norm_out] | `n_heads * head_dim` | ✅ | ✅ | head-sharded; gathered across TP ranks at collect |
|
|
18
|
+
| [`k_norm_in`][k_norm_in] / [`k_norm_out`][k_norm_out] | `n_kv_heads * head_dim` | ✅ | ✅ | head-sharded; gathered across TP ranks at collect |
|
|
19
|
+
| [`value`][value] | `n_heads * head_dim` | ✅ | ✅ | head-sharded; gathered across TP ranks at collect |
|
|
20
20
|
| [`attn_scores`][attn_scores] | `n_heads * query * key` | ✅ | ♻️ | no module boundary holds the pre-softmax matrix on **either** backend; vLLM rebuilds it from captured post-RoPE q/k |
|
|
21
21
|
| [`attn_probs`][attn_probs] | `n_heads * query * key` | ✅ | ♻️ | fused paged attention never materializes the probabilities; same recompute |
|
|
22
|
-
| [`z`][z] | `n_heads * head_dim` | ✅ | ✅ | head-sharded
|
|
22
|
+
| [`z`][z] | `n_heads * head_dim` | ✅ | ✅ | head-sharded; gathered across TP ranks at collect |
|
|
23
23
|
| [`attn_gate`][attn_gate] | `n_heads * head_dim` | ✅ | ❌ | unimplemented — a real module on both trees |
|
|
24
24
|
| [`attn_out`][attn_out] | `d_model` | ✅ | ✅ | |
|
|
25
25
|
| [`attn_out_post`][attn_out_post] | `d_model` | ✅ | ✅ | |
|
|
@@ -27,7 +27,7 @@ nnsight and nnterp.
|
|
|
27
27
|
| [`mlp_in`][mlp_in] | `d_model` | ✅ | ✅ | |
|
|
28
28
|
| [`mlp_pre`][mlp_pre] | `d_mlp` | ✅ | ❌ | unreachable — vLLM fuses `gate_proj` and `up_proj` into one `gate_up_proj`, so neither branch is a module output |
|
|
29
29
|
| [`mlp_pre_linear`][mlp_pre_linear] | `d_mlp` | ✅ | ❌ | as `mlp_pre`; gated MLPs only |
|
|
30
|
-
| [`mlp_act`][mlp_act] | `d_mlp` | ✅ | ✅ | neuron-sharded
|
|
30
|
+
| [`mlp_act`][mlp_act] | `d_mlp` | ✅ | ✅ | neuron-sharded; gathered across TP ranks at collect |
|
|
31
31
|
| [`router_logits`][router_logits] | `n_experts` | ✅ | ✅ | replicated gate, so it survives tensor parallelism |
|
|
32
32
|
| [`expert_weights`][expert_weights] | `n_experts` | ✅ | ❌ | unreachable — the top-k happens inside the FusedMoE kernel, which returns the combined output with the selection never materialized |
|
|
33
33
|
| [`expert_indices`][expert_indices] | `n_experts` | ✅ | ❌ | as `expert_weights` |
|
|
@@ -54,13 +54,16 @@ a fused kernel ate the tensor and no module boundary holds it. Ask the code rath
|
|
|
54
54
|
you are branching on it — `points.vllm_hookable()` is the served set, `points.reason(name)` is the
|
|
55
55
|
sentence for one refusal, and `model.points()` is what a loaded model has.
|
|
56
56
|
|
|
57
|
-
## Tensor parallelism
|
|
57
|
+
## Tensor parallelism does not narrow the vLLM column
|
|
58
58
|
|
|
59
|
-
The capture path reads rank 0's payload alone,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
The capture path reads rank 0's payload alone, and at `num_gpus > 1` vLLM shards `z`, `value`,
|
|
60
|
+
`mlp_act`, the four QK-norm points and the q/k/v behind the attention recompute by head or by neuron.
|
|
61
|
+
The worker gathers those across ranks at collect time (`interp_engine.vllm_capture._tp`), so rank 0
|
|
62
|
+
hands back the same full-width tensor a single card would have, in the same layout; a KV head that
|
|
63
|
+
vLLM replicates rather than shards is kept once. Everything `d_model` wide is all-reduced before the
|
|
64
|
+
hook sees it, and `router_logits` comes off a replicated gate, so those never needed gathering.
|
|
65
|
+
Verified against the eager reference at TP=2 by the validator (`validator/`, `NUM_GPUS=2`) and by
|
|
66
|
+
`tests/test_multigpu.py`.
|
|
64
67
|
|
|
65
68
|
## The last seven rows need a hyper-connection trunk
|
|
66
69
|
|
|
@@ -480,8 +480,8 @@ def capture_attention(
|
|
|
480
480
|
pass rather than one rebuilt from the other.
|
|
481
481
|
|
|
482
482
|
The eager arm needs the model loaded with eager attention for ``attn_probs``; the vLLM arm
|
|
483
|
-
recomputes off-kernel from captured post-RoPE q/k
|
|
484
|
-
themselves.
|
|
483
|
+
recomputes off-kernel from captured post-RoPE q/k, gathered across ranks under tensor
|
|
484
|
+
parallelism. Both refusals name themselves.
|
|
485
485
|
"""
|
|
486
486
|
if not isinstance(model, EagerModel):
|
|
487
487
|
ids = as_token_ids(tokens, model=model, what="capture_attention")
|
|
@@ -1630,6 +1630,21 @@ def value_head_dim(cfg: Any, head_dim: int) -> int:
|
|
|
1630
1630
|
return _first_int(cfg, ("v_head_dim",)) or head_dim
|
|
1631
1631
|
|
|
1632
1632
|
|
|
1633
|
+
def kv_latent_width(cfg: Any) -> int:
|
|
1634
|
+
"""Elements one token of KV cache holds per layer under MLA, or 0 where keys and values are cached.
|
|
1635
|
+
|
|
1636
|
+
DeepSeek's multi-head latent attention never caches K and V: it caches the ``kv_lora_rank``-wide
|
|
1637
|
+
latent both are expanded from, plus the ``qk_rope_head_dim`` positional key part that rides beside
|
|
1638
|
+
it -- one 576-wide row per token on DeepSeek-V3 and Kimi-K2, in place of 64 heads of K and V. A
|
|
1639
|
+
cache sized from ``n_kv_heads * (head_dim + v_head_dim)`` is 21x too large there, and it is the
|
|
1640
|
+
figure vLLM builds its pool from, so this is what a sizer has to charge. 0 on every non-MLA config.
|
|
1641
|
+
"""
|
|
1642
|
+
rank = _first_int(cfg, ("kv_lora_rank",))
|
|
1643
|
+
if not rank:
|
|
1644
|
+
return 0
|
|
1645
|
+
return rank + (_first_int(cfg, ("qk_rope_head_dim",)) or 0)
|
|
1646
|
+
|
|
1647
|
+
|
|
1633
1648
|
def first_kv_shared_layer(cfg: Any, n_layers: int) -> int | None:
|
|
1634
1649
|
"""The first layer that reuses an earlier layer's keys/values, or None if none do."""
|
|
1635
1650
|
shared = _first_int(cfg, ("num_kv_shared_layers",))
|
|
@@ -2020,6 +2035,9 @@ class ModelFacts:
|
|
|
2020
2035
|
# The width of one value head, which differs from ``head_dim`` on MiMo-V2 and the DeepSeek MLA
|
|
2021
2036
|
# families. See :func:`value_head_dim`; ``value`` and ``z`` are this wide per head, not ``head_dim``.
|
|
2022
2037
|
v_head_dim: int = 0
|
|
2038
|
+
# Elements per token per layer that an MLA trunk caches in place of K and V (the latent plus the
|
|
2039
|
+
# RoPE'd key part); 0 where the cache holds K and V. See :func:`kv_latent_width`.
|
|
2040
|
+
kv_latent_width: int = 0
|
|
2023
2041
|
# First layer that reuses an earlier layer's keys/values and so has no k/v projection of its own
|
|
2024
2042
|
# (Gemma-4); None when every layer computes its own.
|
|
2025
2043
|
first_kv_shared_layer: int | None = None
|
|
@@ -2476,6 +2494,7 @@ def resolve_facts(config: Any, *, n_layers_fallback: int | None = None) -> Model
|
|
|
2476
2494
|
global_kv_heads=_first_int(cfg, ("num_global_key_value_heads",)) or None,
|
|
2477
2495
|
k_eq_v=bool(config_attr(cfg, "attention_k_eq_v", False)),
|
|
2478
2496
|
v_head_dim=value_head_dim(cfg, head_dim),
|
|
2497
|
+
kv_latent_width=kv_latent_width(cfg),
|
|
2479
2498
|
first_kv_shared_layer=first_kv_shared_layer(cfg, n_layers),
|
|
2480
2499
|
n_experts=n_experts(cfg),
|
|
2481
2500
|
experts_per_token=_first_int(cfg, _EXPERTS_PER_TOKEN_FIELDS) or 0,
|
|
@@ -152,9 +152,10 @@ def load_model(
|
|
|
152
152
|
doubles the context or concurrency a card holds. Refused on the eager backend, which
|
|
153
153
|
has no paged cache to set the dtype of.
|
|
154
154
|
num_gpus: Shard across this many GPUs on one node -- vLLM ``tensor_parallel_size``,
|
|
155
|
-
eager accelerate ``device_map="auto"``.
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
eager accelerate ``device_map="auto"``. The vLLM worker gathers the head- and
|
|
156
|
+
neuron-sharded points (``z``, ``value``, ``mlp_act``, the QK-norm points, the q/k
|
|
157
|
+
behind the attention recompute) across ranks at collect, so the served point set
|
|
158
|
+
and every tensor's width are the same as on one GPU.
|
|
158
159
|
trust_remote_code: Passed to both the config probe and the backend. The default ``None``
|
|
159
160
|
means "only where the checkpoint has no alternative": eager prefers a native
|
|
160
161
|
transformers class over a checkpoint's bundled copy of one when both exist, since the
|
|
@@ -902,8 +902,9 @@ class WeightBytes:
|
|
|
902
902
|
on_disk_bytes: int
|
|
903
903
|
#: The dtype the checkpoint is stored in, e.g. ``bfloat16``. Empty when unknown.
|
|
904
904
|
stored_dtype: str = ""
|
|
905
|
-
#: ``quantization_config.quant_method`` when there is one: ``mxfp4``, ``fp8``,
|
|
906
|
-
#: ``compressed-tensors
|
|
905
|
+
#: ``quantization_config.quant_method`` when there is one: ``mxfp4``, ``fp8``, ``awq``, ``gptq``.
|
|
906
|
+
#: A ``compressed-tensors`` container carries the width its ``config_groups`` declare, as
|
|
907
|
+
#: ``compressed-tensors int4`` or ``compressed-tensors fp8``. Empty on an unquantized checkpoint.
|
|
907
908
|
quant_method: str = ""
|
|
908
909
|
#: ``config.expert_dtype`` when an MoE checkpoint stores its routed experts narrower than the rest
|
|
909
910
|
#: of itself, e.g. ``fp4`` beside a ``fp8`` scheme. Empty everywhere else.
|
|
@@ -1050,6 +1051,9 @@ def _quant_method(config: Any) -> tuple[str, str]:
|
|
|
1050
1051
|
elif q is not None:
|
|
1051
1052
|
method = str(getattr(q, "quant_method", "") or getattr(q, "quantization", "") or "")
|
|
1052
1053
|
if method:
|
|
1054
|
+
scheme = _compressed_tensors_scheme(q) if method.lower() == "compressed-tensors" else ""
|
|
1055
|
+
if scheme:
|
|
1056
|
+
method = f"{method} {scheme}"
|
|
1053
1057
|
break
|
|
1054
1058
|
stored = ""
|
|
1055
1059
|
for holder in (config, text_config(config)):
|
|
@@ -1062,6 +1066,41 @@ def _quant_method(config: Any) -> tuple[str, str]:
|
|
|
1062
1066
|
return method.lower(), stored
|
|
1063
1067
|
|
|
1064
1068
|
|
|
1069
|
+
def _compressed_tensors_scheme(quantization_config: Any) -> str:
|
|
1070
|
+
"""The weight format a ``compressed-tensors`` config really declares: ``int4``, ``int8``, ``fp8``.
|
|
1071
|
+
|
|
1072
|
+
``compressed-tensors`` is a container, and the width it packs at sits one level down in
|
|
1073
|
+
``config_groups.*.weights`` as ``num_bits`` and ``type``. Read as a bare ``compressed-tensors``,
|
|
1074
|
+
Kimi-K2.6's W4A16 experts are priced at one byte a parameter -- half the true dequantized size,
|
|
1075
|
+
and the wrong answer to whether an eager load fits. The narrowest group wins, since on a mixed
|
|
1076
|
+
checkpoint that is what the bulk of the weights are packed at. Empty when no group says.
|
|
1077
|
+
"""
|
|
1078
|
+
groups = _get(quantization_config, "config_groups")
|
|
1079
|
+
if isinstance(groups, dict):
|
|
1080
|
+
groups = list(groups.values())
|
|
1081
|
+
if not isinstance(groups, (list, tuple)):
|
|
1082
|
+
return ""
|
|
1083
|
+
best: tuple[int, str] | None = None
|
|
1084
|
+
for group in groups:
|
|
1085
|
+
weights = _get(group, "weights")
|
|
1086
|
+
if weights is None:
|
|
1087
|
+
continue
|
|
1088
|
+
bits = _get(weights, "num_bits")
|
|
1089
|
+
if not isinstance(bits, int) or bits not in (4, 8):
|
|
1090
|
+
continue
|
|
1091
|
+
kind = "fp" if str(_get(weights, "type") or "int").lower() == "float" else "int"
|
|
1092
|
+
if best is None or bits < best[0]:
|
|
1093
|
+
best = (bits, f"{kind}{bits}")
|
|
1094
|
+
return best[1] if best else ""
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _get(holder: Any, key: str) -> Any:
|
|
1098
|
+
"""One field of a config that may be a dict or an object."""
|
|
1099
|
+
if isinstance(holder, dict):
|
|
1100
|
+
return holder.get(key)
|
|
1101
|
+
return getattr(holder, key, None)
|
|
1102
|
+
|
|
1103
|
+
|
|
1065
1104
|
def _expert_dtype(config: Any) -> str:
|
|
1066
1105
|
"""The dtype of the ROUTED EXPERT weights, which need not be the checkpoint's dtype or its scheme.
|
|
1067
1106
|
|
|
@@ -1403,6 +1442,10 @@ class ModelMemoryFacts:
|
|
|
1403
1442
|
v_head_dim: int
|
|
1404
1443
|
vocab_size: int
|
|
1405
1444
|
intermediate_size: int
|
|
1445
|
+
#: What an MLA trunk caches per token per layer instead of K and V: the latent plus the RoPE'd
|
|
1446
|
+
#: key part, 576 on DeepSeek-V3 and Kimi-K2. 0 where the cache holds K and V. One row, not one
|
|
1447
|
+
#: per head, so it is also what says the cache cannot be sharded (:func:`kv_shards`).
|
|
1448
|
+
kv_latent_width: int = 0
|
|
1406
1449
|
#: Routed experts per sparse layer; 0 on a dense trunk. Here for the static tap widths --
|
|
1407
1450
|
#: ``router_logits`` is as wide as the expert bank -- and because it is what says a layer's MLP is
|
|
1408
1451
|
#: a fused kernel rather than three Linears, which decides whether ``mlp_act`` exists at all.
|
|
@@ -1431,7 +1474,11 @@ class ModelMemoryFacts:
|
|
|
1431
1474
|
def kv_width(self) -> int:
|
|
1432
1475
|
"""KV-cache elements per token per layer, K and V together."""
|
|
1433
1476
|
return kv_cache_width(
|
|
1434
|
-
n_kv_heads=self.n_kv_heads,
|
|
1477
|
+
n_kv_heads=self.n_kv_heads,
|
|
1478
|
+
head_dim=self.head_dim,
|
|
1479
|
+
v_head_dim=self.v_head_dim,
|
|
1480
|
+
d_model=self.d_model,
|
|
1481
|
+
latent_width=self.kv_latent_width,
|
|
1435
1482
|
)
|
|
1436
1483
|
|
|
1437
1484
|
@property
|
|
@@ -1494,17 +1541,25 @@ class ModelMemoryFacts:
|
|
|
1494
1541
|
return self.n_layers > 0 and self.d_model > 0
|
|
1495
1542
|
|
|
1496
1543
|
|
|
1497
|
-
def kv_cache_width(
|
|
1544
|
+
def kv_cache_width(
|
|
1545
|
+
*, n_kv_heads: int = 0, head_dim: int = 0, v_head_dim: int = 0, d_model: int = 0, latent_width: int = 0
|
|
1546
|
+
) -> int:
|
|
1498
1547
|
"""KV-cache elements per token per layer, K and V together, for the whole model.
|
|
1499
1548
|
|
|
1549
|
+
``latent_width`` wins when set: an MLA trunk caches one latent row per token in place of K and V
|
|
1550
|
+
(:func:`facts.kv_latent_width`), and its head dims describe tensors that never reach the cache.
|
|
1551
|
+
Kimi-K2.6 at TP=8 on 8x H200 built 2.5x the tokens the head-dim figure predicted.
|
|
1552
|
+
|
|
1500
1553
|
``2 * d_model`` -- the fallback when a caller has no head dims -- is the *pre-GQA* worst case and
|
|
1501
1554
|
is wrong by 8x on the models where sizing is tight: Llama-3.3-70B caches 8 kv heads of 128 rather
|
|
1502
|
-
than 8192
|
|
1503
|
-
|
|
1555
|
+
than 8192. That factor is the difference between fitting a default capture size and refusing to
|
|
1556
|
+
start, so pass the head dims.
|
|
1504
1557
|
|
|
1505
1558
|
Mirrors ``vllm_capture/static.kv_cache_width`` and is checked against it in the tests; this copy
|
|
1506
1559
|
exists so that sizing a model needs neither torch nor vLLM.
|
|
1507
1560
|
"""
|
|
1561
|
+
if int(latent_width) > 0:
|
|
1562
|
+
return int(latent_width)
|
|
1508
1563
|
heads = max(int(n_kv_heads), 0)
|
|
1509
1564
|
if heads <= 0 or int(head_dim) <= 0:
|
|
1510
1565
|
return 2 * max(int(d_model), 1)
|
|
@@ -1517,8 +1572,8 @@ def kv_shards(facts: ModelMemoryFacts, num_gpus: int) -> int:
|
|
|
1517
1572
|
**Not the rank count**, which is the assumption to get rid of. vLLM shards the cache by *KV
|
|
1518
1573
|
head*, so how far it divides is a property of the attention shape rather than of the machine:
|
|
1519
1574
|
Llama-3.3-70B's 8 KV heads go 2-per-rank at TP=4 and the cache really is a quarter on each card,
|
|
1520
|
-
while a DeepSeek MLA trunk caches a single
|
|
1521
|
-
vLLM replicates it, and four cards hold four copies of the same cache.
|
|
1575
|
+
while a DeepSeek MLA trunk caches a single 576-wide latent row (``kv_latent_width``), which
|
|
1576
|
+
cannot be cut at all -- vLLM replicates it, and four cards hold four copies of the same cache.
|
|
1522
1577
|
|
|
1523
1578
|
``min`` covers both ends, including the case past the second one: vLLM pads a head count up to
|
|
1524
1579
|
the rank count by duplicating heads, so 8 heads across 16 ranks still costs what 8 ranks cost
|
|
@@ -1530,7 +1585,7 @@ def kv_shards(facts: ModelMemoryFacts, num_gpus: int) -> int:
|
|
|
1530
1585
|
lack.
|
|
1531
1586
|
"""
|
|
1532
1587
|
tp = max(int(num_gpus), 1)
|
|
1533
|
-
if facts.n_kv_heads <= 0 or facts.head_dim <= 0:
|
|
1588
|
+
if facts.kv_latent_width > 0 or facts.n_kv_heads <= 0 or facts.head_dim <= 0:
|
|
1534
1589
|
return 1
|
|
1535
1590
|
return min(tp, facts.n_kv_heads)
|
|
1536
1591
|
|
|
@@ -2206,14 +2261,15 @@ def estimate(
|
|
|
2206
2261
|
warnings: list[str] = []
|
|
2207
2262
|
advice: list[str] = []
|
|
2208
2263
|
|
|
2209
|
-
#
|
|
2210
|
-
#
|
|
2211
|
-
#
|
|
2212
|
-
#
|
|
2264
|
+
# Tensor parallelism has two hardware measurements behind it (`gpu-sizer/VERIFIED.md`): Qwen3.8-27B
|
|
2265
|
+
# at TP=2 on 2x A40 and Kimi-K2.6 at TP=8 on 8x H200, where the per-rank weight split and the margin
|
|
2266
|
+
# outside the pool came out as priced. Every other count is still arithmetic. The Kimi run is also
|
|
2267
|
+
# what showed the cache term had to price an MLA trunk's latent row rather than its heads.
|
|
2213
2268
|
if tp > 1:
|
|
2214
2269
|
warnings.append(
|
|
2215
|
-
f"{tp}-GPU figures
|
|
2216
|
-
f"
|
|
2270
|
+
f"{tp}-GPU figures rest on two measurements: tensor parallelism has been checked on hardware "
|
|
2271
|
+
f"at num_gpus=2 (Qwen3.8-27B on 2x A40) and num_gpus=8 (Kimi-K2.6 on 8x H200), where the "
|
|
2272
|
+
f"weight split and the margin outside the pool held; other counts are still arithmetic"
|
|
2217
2273
|
)
|
|
2218
2274
|
|
|
2219
2275
|
# Only the eager backend expands a quantized checkpoint to the requested dtype; vLLM reads the same
|
|
@@ -2693,6 +2749,7 @@ def model_memory_facts(
|
|
|
2693
2749
|
v_head_dim=f.v_head_dim,
|
|
2694
2750
|
vocab_size=f.vocab_size,
|
|
2695
2751
|
intermediate_size=intermediate,
|
|
2752
|
+
kv_latent_width=f.kv_latent_width,
|
|
2696
2753
|
n_experts=f.n_experts,
|
|
2697
2754
|
layer_types=f.layer_types,
|
|
2698
2755
|
sliding_window=f.sliding_window,
|