interp-engine 1.2.2__tar.gz → 1.2.4__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.2.2 → interp_engine-1.2.4}/PKG-INFO +3 -2
- {interp_engine-1.2.2 → interp_engine-1.2.4}/README.md +1 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/PERFORMANCE.md +9 -5
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/__init__.py +12 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/chat_conventions.py +3 -0
- interp_engine-1.2.4/interp_engine/chat_formatters.py +451 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/model.py +9 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/tokenize.py +110 -20
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_backend.py +15 -1
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/mhc.py +39 -2
- {interp_engine-1.2.2 → interp_engine-1.2.4}/pyproject.toml +11 -5
- interp_engine-1.2.4/tests/test_chat_formatters.py +510 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_hyper_connections.py +19 -1
- {interp_engine-1.2.2 → interp_engine-1.2.4}/.gitignore +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/LICENSE +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/README.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/__init__.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/bench_spec.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/cells.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/probe.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/publish.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/report_bench.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/deepseek-v4-flash-0731__eager.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/deepseek-v4-flash-0731__vllm-cudagraph.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark-cudagraph.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/deepseek-v4-flash-0731__vllm-dspark.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/deepseek-v4-flash-0731__vllm-freeze.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/deepseek-v4-flash-0731__vllm.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/gemma-2-2b__eager.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/gemma-2-2b__vllm-cudagraph.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/gemma-2-2b__vllm-freeze.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/gemma-2-2b__vllm.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/llama-3.1-8b__eager.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/llama-3.1-8b__vllm-cudagraph.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/llama-3.1-8b__vllm-freeze.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/llama-3.1-8b__vllm.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/qwen3-4b__eager.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/qwen3-4b__vllm-cudagraph.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/qwen3-4b__vllm-freeze.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/qwen3-4b__vllm.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/qwen3.8-27b__eager.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/qwen3.8-27b__vllm-cudagraph.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/qwen3.8-27b__vllm-freeze.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results/qwen3.8-27b__vllm.json +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/results-latest.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/run_all.sh +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/run_bench.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/benchmarks/workloads.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/AGENT_INTEGRATION.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/ARCHITECTURE_QUIRKS.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/COMPATIBILITY.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/ENGINE_HOOK_MAPPINGS.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/GRADIENTS.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/INTERNALS.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/PORTING.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/README.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/SUPPORTED_POINTS.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/docs/USAGE.md +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/_loop.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/address.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/arch.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/attn_config.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/attn_scores.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/autograd_support.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/capture.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/chat_compose.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/cuda_preflight.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/dispatch.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/facts.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/hooks.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/lens.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/load.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/mappers.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/moe_routing.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/points.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/protocol.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/residual_basis.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/select.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/steer.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/steer_specs.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/sync.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/__init__.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/_demux.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/_hooks.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/_payload.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/_tree.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/attn.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/capture.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/freeze.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/graphs.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/lens/__init__.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/lens/intervene.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/lens/readout.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/lens/unembed.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/native.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/requests.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_capture/steering.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/interp_engine/vllm_plugin.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/conftest.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/harness.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/model_expectations.yaml +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/synthetic_families.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_address.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_attn_config_tripwire.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_attn_probs_indexing.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_attn_scores.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_attn_z_gqa.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_autograd_support.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_bench_workloads.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_capability_refusals.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_capture_addressing.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_chat_compose.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_chat_templates.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_core.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_cuda_preflight.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_doc_code_fences.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_eager_autograd.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_facts.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_family_points.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_freeze_dsv4_gpu.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_freeze_parity_gpu.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_freeze_set.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_freeze_warmup.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_gated_attn_out.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_head_contributions.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_hook_call_conventions.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_layer_kinds.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_load.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_logit_transform.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_mappers.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_mlp_internals.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_model_expectations.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_moe.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_multimodal_arch.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_new_models_gpu.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_no_chat_template.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_normalized_hook.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_packaging.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_parity_gpt2.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_per_layer_attn_dims.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_points_registry.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_protocol.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_published_benchmarks.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_qk_norm.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_qkv_layout.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_reasoning_spans.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_release.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_resid_mid.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_residual_basis.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_sandwich_norms.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_select.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_sliding_window_attn.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_small_models_gpu.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_steer_context.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_steer_math_parity.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_sync_loop.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_sync_parity.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_unified_free_functions.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_unresolved_families.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_capture_gpu.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_capture_scales.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_graph_path.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_graphs_on_gpu.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_hook_availability.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_kv_isolation.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_new_points.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_only_families.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_plugin.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vllm_wire_grammar.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_vocabulary_boundary.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/tests/test_worker_lens_capture_readout.py +0 -0
- {interp_engine-1.2.2 → interp_engine-1.2.4}/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.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: A fast, standardized interpretability engine that supports most modern models and architectures. Powers Neuronpedia.
|
|
5
5
|
Project-URL: Homepage, https://github.com/decoderesearch/interp-engine
|
|
6
6
|
Project-URL: Repository, https://github.com/decoderesearch/interp-engine
|
|
@@ -26,7 +26,7 @@ Provides-Extra: quant
|
|
|
26
26
|
Requires-Dist: accelerate>=1.0; extra == 'quant'
|
|
27
27
|
Requires-Dist: kernels<0.17.0,>=0.15.2; extra == 'quant'
|
|
28
28
|
Provides-Extra: vllm
|
|
29
|
-
Requires-Dist: vllm>=0.
|
|
29
|
+
Requires-Dist: vllm>=0.27.1; (sys_platform == 'linux') and extra == 'vllm'
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
|
|
32
32
|
# interp-engine
|
|
@@ -38,6 +38,7 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
🔗 <a href="https://interp-engine.org"><strong>interp-engine.org</strong></a>
|
|
39
39
|
</p>
|
|
40
40
|
|
|
41
|
+
|
|
41
42
|
`interp-engine` is an interpretability engine that is fast (>40x tok/s vs HF eager), standardized (34 'points'/addresses across architectures), and easy to use and debug. It powers all of [Neuronpedia](https://neuronpedia.org)'s inference and is checked for accuracy against HF Transformers and other engines.
|
|
42
43
|
|
|
43
44
|
<p align="center">
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
🔗 <a href="https://interp-engine.org"><strong>interp-engine.org</strong></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
`interp-engine` is an interpretability engine that is fast (>40x tok/s vs HF eager), standardized (34 'points'/addresses across architectures), and easy to use and debug. It powers all of [Neuronpedia](https://neuronpedia.org)'s inference and is checked for accuracy against HF Transformers and other engines.
|
|
11
12
|
|
|
12
13
|
<p align="center">
|
|
@@ -42,13 +42,17 @@ every new upstream flag into an engine change.
|
|
|
42
42
|
|
|
43
43
|
### The vLLM version is a floor, not a ceiling
|
|
44
44
|
|
|
45
|
-
`interp-engine[vllm]` declares `vllm>=0.
|
|
45
|
+
`interp-engine[vllm]` declares `vllm>=0.27.1` on Linux and no upper bound. Read that as **the oldest
|
|
46
46
|
version we have evidence for**, not the version to run:
|
|
47
47
|
|
|
48
|
-
- **The floor is measured, not guessed.** 0.
|
|
49
|
-
on. The
|
|
50
|
-
**0.26.0**,
|
|
51
|
-
is the normal state, not drift.
|
|
48
|
+
- **The floor is measured, not guessed.** 0.27.1 is the version the DeepSeek-V4-Flash-0731
|
|
49
|
+
cross-engine comparison scored, and the oldest the engine is supported on. The performance sweep in
|
|
50
|
+
[`benchmarks/results-latest.md`](../benchmarks/results-latest.md) ran on **0.26.0**, one below —
|
|
51
|
+
being ahead of the floor is the normal state, not drift.
|
|
52
|
+
- **This floor is load-bearing, not hygiene.** It was 0.25.1 until `vllm_capture/mhc.py` needed
|
|
53
|
+
`mhc_pre_broadcast_tilelang`, which arrived in 0.26.0. Below that, every DeepSeek-V4
|
|
54
|
+
hyper-connection point is refused at install, and since `FREEZE_POINTS=auto` selects
|
|
55
|
+
`resid_streams` on such a trunk, the pod does not load at all.
|
|
52
56
|
- **There is deliberately no cap.** vLLM moves fast and a `<` bound would make every upstream release
|
|
53
57
|
an engine release; a lock file is the right place to pin a deployment, and both apps have one. The
|
|
54
58
|
version an app runs is whatever its `uv.lock` resolved, which is normally newer than this floor.
|
|
@@ -58,6 +58,13 @@ from interp_engine.chat_conventions import (
|
|
|
58
58
|
detect_reasoning_tags,
|
|
59
59
|
is_harmony,
|
|
60
60
|
)
|
|
61
|
+
from interp_engine.chat_formatters import (
|
|
62
|
+
CODE_CHAT_FORMATS,
|
|
63
|
+
ChatFormatter,
|
|
64
|
+
ChatFormatterUnavailable,
|
|
65
|
+
RenderedChat,
|
|
66
|
+
resolve_chat_formatter,
|
|
67
|
+
)
|
|
61
68
|
from interp_engine.cuda_preflight import check_cuda_driver
|
|
62
69
|
from interp_engine.dispatch import CAPABILITIES, Capability, CapabilityUnsupported, TokensLike
|
|
63
70
|
from interp_engine.facts import rms_norm_eps_for_model
|
|
@@ -265,11 +272,15 @@ __all__ = [
|
|
|
265
272
|
"resolve_arch",
|
|
266
273
|
"unsupported_attn_config",
|
|
267
274
|
# Tokenization and chat templates.
|
|
275
|
+
"CODE_CHAT_FORMATS",
|
|
276
|
+
"ChatFormatter",
|
|
277
|
+
"ChatFormatterUnavailable",
|
|
268
278
|
"ChatTurn",
|
|
269
279
|
"GeneratedTurnSpans",
|
|
270
280
|
"NoChatTemplateError",
|
|
271
281
|
"REASONING_TAGS",
|
|
272
282
|
"ReasoningTags",
|
|
283
|
+
"RenderedChat",
|
|
273
284
|
"TURN_END_TOKENS",
|
|
274
285
|
"TokenSpan",
|
|
275
286
|
"Tokenize",
|
|
@@ -277,6 +288,7 @@ __all__ = [
|
|
|
277
288
|
"compose_assistant_turns",
|
|
278
289
|
"detect_reasoning_tags",
|
|
279
290
|
"is_harmony",
|
|
291
|
+
"resolve_chat_formatter",
|
|
280
292
|
"special_token_ids",
|
|
281
293
|
"special_token_positions",
|
|
282
294
|
"strip_wire_reasoning",
|
|
@@ -50,6 +50,9 @@ TURN_END_TOKENS = frozenset(
|
|
|
50
50
|
"<|end_of_text|>",
|
|
51
51
|
"<|endoftext|>",
|
|
52
52
|
"<|eom_id|>",
|
|
53
|
+
# DeepSeek (V3 and V4 lines). Note the full-width bars and the U+2581 word separators:
|
|
54
|
+
# these are the checkpoint's actual added-token strings, not the ASCII lookalikes.
|
|
55
|
+
"<|end▁of▁sentence|>",
|
|
53
56
|
}
|
|
54
57
|
)
|
|
55
58
|
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""Chat formats that live in Python instead of in a Jinja ``chat_template``.
|
|
2
|
+
|
|
3
|
+
Almost every chat model describes its own prompt format in the tokenizer's ``chat_template``,
|
|
4
|
+
which is why :meth:`Tokenize.message_spans` needs no per-model knowledge: it renders the real
|
|
5
|
+
template and diffs. A few checkpoints ship no template at all and define the format in code
|
|
6
|
+
instead, because the thing they need to express is not a render. DeepSeek-V4 is the case this
|
|
7
|
+
module exists for: its ``encoding/`` folder carries an encoder *and* a parser
|
|
8
|
+
(``parse_message_from_completion_text``), and a template can only ever do the first half.
|
|
9
|
+
|
|
10
|
+
**The reference implementation ships inside the checkpoint**, at
|
|
11
|
+
``encoding/encoding_dsv4.py``, beside the weights and versioned with them. So this module
|
|
12
|
+
downloads and imports it rather than carrying a copy: a vendored fork is a second source of
|
|
13
|
+
truth for a format whose first source of truth we already have on disk. That the risk is real
|
|
14
|
+
rather than theoretical is easy to check -- vLLM's fork of the same file
|
|
15
|
+
(``vllm/tokenizers/deepseek_v4_encoding.py``) renamed the assistant field
|
|
16
|
+
``reasoning_content`` to ``reasoning``, so feeding it upstream-shaped messages silently drops
|
|
17
|
+
every thinking block. :func:`_reasoning_field` reads the field name back out of whichever copy
|
|
18
|
+
got loaded rather than assuming either spelling.
|
|
19
|
+
|
|
20
|
+
Loading it is remote code execution, so it is gated on the same ``trust_remote_code`` flag the
|
|
21
|
+
weights are. When the file cannot be fetched the engine keeps loading and simply reports no
|
|
22
|
+
chat support, which lands the caller on the existing raw-text refusal rather than on a failed
|
|
23
|
+
load -- chat is one endpoint, not the model.
|
|
24
|
+
|
|
25
|
+
Adding a family means one entry in :data:`CODE_CHAT_FORMATS` and a class satisfying
|
|
26
|
+
:class:`ChatFormatter`. Generation-side structure (reasoning delimiters, harmony channels)
|
|
27
|
+
stays in ``chat_conventions``; this module is only about rendering the prompt.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import importlib.util
|
|
33
|
+
import inspect
|
|
34
|
+
import logging
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from types import ModuleType
|
|
40
|
+
from typing import Any, Protocol
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
Message = Mapping[str, Any]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ChatFormatterUnavailable(RuntimeError):
|
|
48
|
+
"""The architecture needs a code formatter and the engine could not load one.
|
|
49
|
+
|
|
50
|
+
Raised by the loaders, caught by :func:`resolve_chat_formatter`, which downgrades it to a
|
|
51
|
+
warning: a model whose chat format is unreachable still captures, steers and completes raw
|
|
52
|
+
text, so this must not gate loading.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class RenderedChat:
|
|
58
|
+
"""A chat render, kept split at the boundaries span metadata is derived from.
|
|
59
|
+
|
|
60
|
+
``blocks`` is 1:1 with the input messages, in order, so ``blocks[k]`` is exactly what
|
|
61
|
+
message ``k`` contributed to the prompt. ``prefix`` holds what precedes message 0 (a BOS
|
|
62
|
+
token, an injected tools preamble) and ``suffix`` the trailing generation scaffold.
|
|
63
|
+
|
|
64
|
+
The split is the point. A Jinja template can only be rendered whole, so ``message_spans``
|
|
65
|
+
has to recover boundaries by re-rendering growing message prefixes and diffing tokens --
|
|
66
|
+
which assumes that growing the message list only appends, and DeepSeek-V4 breaks that
|
|
67
|
+
assumption (whether a turn is the *last* user turn changes how earlier turns render). A
|
|
68
|
+
formatter that reports its own boundaries sidesteps the diff entirely: ``upto`` returns a
|
|
69
|
+
genuine string prefix of ``text``, so the token prefix is exact.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
prefix: str
|
|
73
|
+
blocks: tuple[str, ...]
|
|
74
|
+
suffix: str
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def text(self) -> str:
|
|
78
|
+
"""The whole prompt, exactly as it would be tokenized."""
|
|
79
|
+
return self.prefix + "".join(self.blocks) + self.suffix
|
|
80
|
+
|
|
81
|
+
def upto(self, count: int) -> str:
|
|
82
|
+
"""``text`` truncated to the first ``count`` messages, generation scaffold excluded."""
|
|
83
|
+
return self.prefix + "".join(self.blocks[:count])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ChatFormatter(Protocol):
|
|
87
|
+
"""What :class:`~interp_engine.tokenize.Tokenize` needs from a code-defined chat format.
|
|
88
|
+
|
|
89
|
+
``template_kwargs`` is the set of keyword arguments the format actually reads. Callers use
|
|
90
|
+
it the way they used to grep the Jinja source for a variable name: to pass
|
|
91
|
+
``enable_thinking`` only where it means something. A formatter refuses an unknown kwarg
|
|
92
|
+
rather than ignoring it, since a silently dropped ``enable_thinking`` renders the wrong
|
|
93
|
+
prompt and returns 200.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
name: str
|
|
97
|
+
template_kwargs: frozenset[str]
|
|
98
|
+
|
|
99
|
+
def render(
|
|
100
|
+
self,
|
|
101
|
+
messages: Sequence[Message],
|
|
102
|
+
*,
|
|
103
|
+
add_generation_prompt: bool = True,
|
|
104
|
+
continue_final_message: bool = False,
|
|
105
|
+
**template_kwargs: Any,
|
|
106
|
+
) -> RenderedChat: ...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --------------------------------------------------------------------------- #
|
|
110
|
+
# DeepSeek-V4
|
|
111
|
+
# --------------------------------------------------------------------------- #
|
|
112
|
+
|
|
113
|
+
# Where the reference encoder sits in every DeepSeek-V4 repo (V4-Flash, V4-Pro). The folder
|
|
114
|
+
# also carries README.md (the format spec) and tests/ (input/expected-output fixtures, which
|
|
115
|
+
# validator/ uses as golden cases).
|
|
116
|
+
DEEPSEEK_V4_ENCODER_FILE = "encoding/encoding_dsv4.py"
|
|
117
|
+
|
|
118
|
+
# vLLM vendors the same file. Only consulted when the checkpoint's own copy cannot be fetched,
|
|
119
|
+
# and it is a fork rather than a mirror -- see the module docstring.
|
|
120
|
+
DEEPSEEK_V4_FALLBACK_MODULE = "vllm.tokenizers.deepseek_v4_encoding"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class DeepseekV4Formatter:
|
|
124
|
+
"""Renders DeepSeek-V4 prompts through the encoder shipped with the checkpoint.
|
|
125
|
+
|
|
126
|
+
Three of the encoder's conventions differ from the ``apply_chat_template`` vocabulary the
|
|
127
|
+
rest of the engine speaks, and each is translated here rather than at the call sites:
|
|
128
|
+
|
|
129
|
+
- **There is no ``add_generation_prompt`` flag.** The encoder appends
|
|
130
|
+
``<|Assistant|>`` plus a thinking delimiter whenever the conversation ends on a user or
|
|
131
|
+
developer turn, unconditionally. So the scaffold is *identified* after the fact and
|
|
132
|
+
reported as :attr:`RenderedChat.suffix`, which callers keep or drop. Note this makes
|
|
133
|
+
``add_generation_prompt=True`` a no-op after a closed assistant turn, where a Jinja
|
|
134
|
+
template would open a fresh one: the encoder's own answer is that a transcript ending in
|
|
135
|
+
a completed answer is a transcript, not a prompt, and synthesizing the opener anyway
|
|
136
|
+
would put two tokens in front of the model that the checkpoint's own reference encoder
|
|
137
|
+
never emits. The fixtures in ``encoding/tests/`` are exactly such transcripts.
|
|
138
|
+
- **There is no ``continue_final_message`` flag.** A prefill is spelled per message, as
|
|
139
|
+
``wo_eos``, which suppresses that turn's end-of-sentence token. That is a better fit
|
|
140
|
+
than the flag it replaces: transformers implements ``continue_final_message`` by
|
|
141
|
+
rendering a sentinel into the content and cutting the string, and here nothing is cut.
|
|
142
|
+
- **Historical reasoning is dropped by default** (``drop_thinking``), which is what makes
|
|
143
|
+
the render non-monotonic and is why this class reports message blocks itself.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
name = "deepseek_v4"
|
|
147
|
+
# `thinking` is the encoder's own spelling and `enable_thinking` the one every other family
|
|
148
|
+
# in this engine uses; both are accepted so a caller need not know which model it has.
|
|
149
|
+
template_kwargs = frozenset({"enable_thinking", "thinking", "reasoning_effort", "drop_thinking", "tools"})
|
|
150
|
+
|
|
151
|
+
def __init__(self, module: ModuleType):
|
|
152
|
+
self._module = module
|
|
153
|
+
self._encode: Callable[..., str] = getattr(module, "encode_messages") # noqa: B009 - module attr
|
|
154
|
+
self._reasoning_key = _reasoning_field(module)
|
|
155
|
+
self._assistant_token = str(getattr(module, "ASSISTANT_SP_TOKEN", "<|Assistant|>"))
|
|
156
|
+
self._think_open = str(getattr(module, "thinking_start_token", "<think>"))
|
|
157
|
+
self._think_close = str(getattr(module, "thinking_end_token", "</think>"))
|
|
158
|
+
self._bos = str(getattr(module, "bos_token", ""))
|
|
159
|
+
self._encode_params = _accepted_parameters(self._encode)
|
|
160
|
+
|
|
161
|
+
# --- rendering ----------------------------------------------------------
|
|
162
|
+
def render(
|
|
163
|
+
self,
|
|
164
|
+
messages: Sequence[Message],
|
|
165
|
+
*,
|
|
166
|
+
add_generation_prompt: bool = True,
|
|
167
|
+
continue_final_message: bool = False,
|
|
168
|
+
**template_kwargs: Any,
|
|
169
|
+
) -> RenderedChat:
|
|
170
|
+
config, tools = self._encode_config(template_kwargs)
|
|
171
|
+
turns = [self._normalize(m) for m in messages]
|
|
172
|
+
|
|
173
|
+
if continue_final_message:
|
|
174
|
+
if not turns or turns[-1].get("role") != "assistant":
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"continue_final_message=True keeps a trailing ASSISTANT turn open, but the last "
|
|
177
|
+
f"message is {turns[-1].get('role') if turns else 'absent'!r}."
|
|
178
|
+
)
|
|
179
|
+
turns[-1] = {**turns[-1], "wo_eos": True}
|
|
180
|
+
|
|
181
|
+
# Tools ride on a system message in this format. Injecting one shifts every index, so
|
|
182
|
+
# its render is folded into `prefix` and `blocks` stays 1:1 with the input messages.
|
|
183
|
+
lead: list[dict[str, Any]] = [{"role": "system", "tools": list(tools)}] if tools else []
|
|
184
|
+
conversation = lead + turns
|
|
185
|
+
|
|
186
|
+
tails = self._tails(conversation, config)
|
|
187
|
+
blocks = [tails[k][: len(tails[k]) - len(tails[k + 1])] for k in range(len(conversation))]
|
|
188
|
+
prefix = self._bos + "".join(blocks[: len(lead)])
|
|
189
|
+
blocks = blocks[len(lead) :]
|
|
190
|
+
|
|
191
|
+
scaffold = self._split_scaffold(blocks, turns)
|
|
192
|
+
return RenderedChat(prefix=prefix, blocks=tuple(blocks), suffix=scaffold if add_generation_prompt else "")
|
|
193
|
+
|
|
194
|
+
def _tails(self, conversation: Sequence[Message], config: dict[str, Any]) -> list[str]:
|
|
195
|
+
"""``tails[k]`` = messages ``k..end`` rendered *in the context of the whole conversation*.
|
|
196
|
+
|
|
197
|
+
The encoder's own ``context`` argument is what makes per-message blocks exact. Passing
|
|
198
|
+
``messages[k:]`` with ``context=messages[:k]`` renders only the tail, but computes
|
|
199
|
+
``last_user_index`` -- the fact that makes this format non-monotonic -- over the whole
|
|
200
|
+
list, so ``tails[k] == block_k + tails[k + 1]`` holds by construction and subtracting
|
|
201
|
+
one from the next recovers each block exactly.
|
|
202
|
+
"""
|
|
203
|
+
tails = [""] * (len(conversation) + 1)
|
|
204
|
+
for k in range(len(conversation) - 1, -1, -1):
|
|
205
|
+
tails[k] = self._call_encoder(
|
|
206
|
+
list(conversation[k:]),
|
|
207
|
+
context=list(conversation[:k]),
|
|
208
|
+
**config,
|
|
209
|
+
)
|
|
210
|
+
if not tails[k].endswith(tails[k + 1]):
|
|
211
|
+
# Would mean the encoder's `context` no longer decomposes the render this way,
|
|
212
|
+
# which is the assumption every span position rests on.
|
|
213
|
+
raise ChatFormatterUnavailable(
|
|
214
|
+
f"{DEEPSEEK_V4_ENCODER_FILE} did not render message {k} as a prefix of the "
|
|
215
|
+
"messages after it, so message boundaries cannot be located. The encoder's "
|
|
216
|
+
"`context` argument may have changed meaning."
|
|
217
|
+
)
|
|
218
|
+
return tails
|
|
219
|
+
|
|
220
|
+
def _call_encoder(self, messages: list[Any], **kwargs: Any) -> str:
|
|
221
|
+
"""Call ``encode_messages``, passing only arguments this copy of it declares.
|
|
222
|
+
|
|
223
|
+
The checkpoint's encoder and vLLM's fork of it do not have identical signatures, and a
|
|
224
|
+
future revision may add or drop a knob. Filtering here means an unknown argument
|
|
225
|
+
degrades to the encoder's own default instead of raising ``TypeError`` at render time.
|
|
226
|
+
"""
|
|
227
|
+
kwargs.setdefault("add_default_bos_token", False)
|
|
228
|
+
if self._encode_params is not None:
|
|
229
|
+
kwargs = {k: v for k, v in kwargs.items() if k in self._encode_params}
|
|
230
|
+
return str(self._encode(messages, **kwargs))
|
|
231
|
+
|
|
232
|
+
# --- translation --------------------------------------------------------
|
|
233
|
+
def _encode_config(self, template_kwargs: Mapping[str, Any]) -> tuple[dict[str, Any], Sequence[Any]]:
|
|
234
|
+
"""Map the engine's template kwargs onto ``encode_messages`` arguments."""
|
|
235
|
+
unknown = sorted(set(template_kwargs) - self.template_kwargs)
|
|
236
|
+
if unknown:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"{self.name} does not read {', '.join(unknown)}. It accepts: "
|
|
239
|
+
f"{', '.join(sorted(self.template_kwargs))}."
|
|
240
|
+
)
|
|
241
|
+
thinking = bool(template_kwargs.get("enable_thinking") or template_kwargs.get("thinking"))
|
|
242
|
+
effort = template_kwargs.get("reasoning_effort")
|
|
243
|
+
effort = effort if isinstance(effort, str) else None
|
|
244
|
+
if effort == "none":
|
|
245
|
+
thinking, effort = False, None
|
|
246
|
+
elif effort in ("max", "xhigh"):
|
|
247
|
+
effort = "max"
|
|
248
|
+
elif effort == "low":
|
|
249
|
+
# The encoder only branches on "max"; it asserts on anything outside
|
|
250
|
+
# {"max", "high", None}, and treats "high" and None identically. So "low" is None
|
|
251
|
+
# rather than "high" -- same render either way, but it does not claim otherwise.
|
|
252
|
+
effort = None
|
|
253
|
+
elif effort is not None:
|
|
254
|
+
effort = "high"
|
|
255
|
+
return (
|
|
256
|
+
{
|
|
257
|
+
"thinking_mode": "thinking" if thinking else "chat",
|
|
258
|
+
"drop_thinking": bool(template_kwargs.get("drop_thinking", True)),
|
|
259
|
+
"reasoning_effort": effort,
|
|
260
|
+
},
|
|
261
|
+
template_kwargs.get("tools") or (),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def _normalize(self, message: Message) -> dict[str, Any]:
|
|
265
|
+
"""A message dict spelling reasoning the way the loaded encoder reads it."""
|
|
266
|
+
turn = dict(message)
|
|
267
|
+
alias = "reasoning" if self._reasoning_key == "reasoning_content" else "reasoning_content"
|
|
268
|
+
if alias in turn:
|
|
269
|
+
value = turn.pop(alias)
|
|
270
|
+
turn.setdefault(self._reasoning_key, value)
|
|
271
|
+
return turn
|
|
272
|
+
|
|
273
|
+
def _split_scaffold(self, blocks: list[str], turns: Sequence[Message]) -> str:
|
|
274
|
+
"""Remove and return the trailing generation scaffold from ``blocks``, or ``""``.
|
|
275
|
+
|
|
276
|
+
The encoder emits it only after a user or developer turn that carries no ``task``, so
|
|
277
|
+
that is the only case checked. Narrowing it matters twice over: assistant content that
|
|
278
|
+
happened to end in these two tokens would otherwise be mistaken for scaffolding, and a
|
|
279
|
+
``task`` turn puts its own task token *after* the scaffold, so the suffix match would
|
|
280
|
+
miss it and the strip would cut in the wrong place.
|
|
281
|
+
"""
|
|
282
|
+
if not blocks or not turns:
|
|
283
|
+
return ""
|
|
284
|
+
last = turns[-1]
|
|
285
|
+
if last.get("role") not in ("user", "developer") or last.get("task") is not None:
|
|
286
|
+
return ""
|
|
287
|
+
for delimiter in (self._think_open, self._think_close):
|
|
288
|
+
scaffold = self._assistant_token + delimiter
|
|
289
|
+
if blocks[-1].endswith(scaffold):
|
|
290
|
+
blocks[-1] = blocks[-1][: -len(scaffold)]
|
|
291
|
+
return scaffold
|
|
292
|
+
return ""
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _reasoning_field(module: ModuleType) -> str:
|
|
296
|
+
"""The message key this copy of the encoder reads a thinking block from.
|
|
297
|
+
|
|
298
|
+
Read out of ``thinking_template`` -- ``"{reasoning_content}"`` upstream,
|
|
299
|
+
``"{reasoning}"`` in vLLM's fork -- rather than assumed, because guessing wrong drops
|
|
300
|
+
every thinking block without raising.
|
|
301
|
+
"""
|
|
302
|
+
template = str(getattr(module, "thinking_template", "") or "")
|
|
303
|
+
match = re.fullmatch(r"\s*\{(\w+)\}\s*", template)
|
|
304
|
+
if match is None:
|
|
305
|
+
logger.warning("Could not read the reasoning field name from %r; assuming reasoning_content", template)
|
|
306
|
+
return "reasoning_content"
|
|
307
|
+
return match.group(1)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _accepted_parameters(func: Callable[..., Any]) -> frozenset[str] | None:
|
|
311
|
+
"""The keyword arguments ``func`` declares. ``None`` means "anything" (it takes ``**kwargs``)."""
|
|
312
|
+
try:
|
|
313
|
+
parameters = inspect.signature(func).parameters
|
|
314
|
+
except (TypeError, ValueError): # pragma: no cover - C-implemented callable
|
|
315
|
+
return None
|
|
316
|
+
if any(p.kind is inspect.Parameter.VAR_KEYWORD for p in parameters.values()):
|
|
317
|
+
return None
|
|
318
|
+
return frozenset(parameters)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# --------------------------------------------------------------------------- #
|
|
322
|
+
# Loading the checkpoint's own encoder
|
|
323
|
+
# --------------------------------------------------------------------------- #
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _download_repo_file(hf_model_id: str, filename: str) -> str | None:
|
|
327
|
+
"""Local path to ``filename`` from the checkpoint repo, cache first, then the hub."""
|
|
328
|
+
try:
|
|
329
|
+
from huggingface_hub import hf_hub_download
|
|
330
|
+
except ImportError: # pragma: no cover - transformers depends on it, so this is belt-and-braces
|
|
331
|
+
logger.warning("huggingface_hub is not installed, so %s cannot be fetched", filename)
|
|
332
|
+
return None
|
|
333
|
+
last: Exception | None = None
|
|
334
|
+
for local_files_only in (True, False):
|
|
335
|
+
try:
|
|
336
|
+
return hf_hub_download(hf_model_id, filename, local_files_only=local_files_only)
|
|
337
|
+
except Exception as exc: # noqa: BLE001 - offline, gated, or absent: all mean "no file"
|
|
338
|
+
last = exc
|
|
339
|
+
logger.info("Could not fetch %s from %s (%s)", filename, hf_model_id, last)
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _import_file(path: str, module_name: str) -> ModuleType | None:
|
|
344
|
+
"""Import a standalone ``.py`` file under ``module_name``."""
|
|
345
|
+
existing = sys.modules.get(module_name)
|
|
346
|
+
if existing is not None:
|
|
347
|
+
return existing
|
|
348
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
349
|
+
if spec is None or spec.loader is None:
|
|
350
|
+
return None
|
|
351
|
+
module = importlib.util.module_from_spec(spec)
|
|
352
|
+
# Registered before exec so the module can import itself by name if it ever needs to; also
|
|
353
|
+
# what makes the check above a cache rather than a repeated exec.
|
|
354
|
+
sys.modules[module_name] = module
|
|
355
|
+
try:
|
|
356
|
+
spec.loader.exec_module(module)
|
|
357
|
+
except Exception:
|
|
358
|
+
sys.modules.pop(module_name, None)
|
|
359
|
+
raise
|
|
360
|
+
return module
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _module_alias(hf_model_id: str, suffix: str) -> str:
|
|
364
|
+
"""A ``sys.modules`` key unique to this checkpoint, so two revisions cannot collide."""
|
|
365
|
+
return f"interp_engine._chat_encoders.{re.sub(r'[^0-9A-Za-z_]+', '_', hf_model_id)}_{suffix}"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def load_deepseek_v4_formatter(hf_model_id: str, *, trust_remote_code: bool = True) -> DeepseekV4Formatter:
|
|
369
|
+
"""Load DeepSeek-V4's reference encoder and wrap it as a :class:`ChatFormatter`.
|
|
370
|
+
|
|
371
|
+
Prefers the copy inside the checkpoint, which is the format's source of truth and travels
|
|
372
|
+
with the weights. Falls back to vLLM's vendored fork only when that file cannot be
|
|
373
|
+
fetched -- it is a fork, and the module docstring lists how it has diverged.
|
|
374
|
+
"""
|
|
375
|
+
if not trust_remote_code:
|
|
376
|
+
raise ChatFormatterUnavailable(
|
|
377
|
+
f"{hf_model_id} defines its chat format in {DEEPSEEK_V4_ENCODER_FILE} rather than in a "
|
|
378
|
+
"chat template, and importing it is remote code execution. Load with "
|
|
379
|
+
"trust_remote_code=True to render chat messages for this model."
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
path = _download_repo_file(hf_model_id, DEEPSEEK_V4_ENCODER_FILE)
|
|
383
|
+
if path is not None:
|
|
384
|
+
module = _import_file(path, _module_alias(hf_model_id, "dsv4"))
|
|
385
|
+
if module is not None and hasattr(module, "encode_messages"):
|
|
386
|
+
logger.info("Loaded the DeepSeek-V4 chat encoder from %s", path)
|
|
387
|
+
return DeepseekV4Formatter(module)
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
module = importlib.import_module(DEEPSEEK_V4_FALLBACK_MODULE)
|
|
391
|
+
except ImportError:
|
|
392
|
+
module = None
|
|
393
|
+
if module is not None:
|
|
394
|
+
logger.warning(
|
|
395
|
+
"Using vLLM's vendored copy of the DeepSeek-V4 encoder: %s could not be fetched from %s. "
|
|
396
|
+
"It is a fork of the checkpoint's own encoder, not a mirror.",
|
|
397
|
+
DEEPSEEK_V4_ENCODER_FILE,
|
|
398
|
+
hf_model_id,
|
|
399
|
+
)
|
|
400
|
+
return DeepseekV4Formatter(module)
|
|
401
|
+
|
|
402
|
+
raise ChatFormatterUnavailable(
|
|
403
|
+
f"{hf_model_id} ships no chat template; its format is defined by "
|
|
404
|
+
f"{DEEPSEEK_V4_ENCODER_FILE}, which could not be fetched from the repo, and "
|
|
405
|
+
f"{DEEPSEEK_V4_FALLBACK_MODULE} is not importable either."
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# Architecture *prefixes* (as they appear in ``config.architectures``) whose chat format is
|
|
410
|
+
# code rather than a template, mapped to the loader that builds it. Prefixes and families
|
|
411
|
+
# rather than checkpoints, matching `facts.MANDATORY_KV_CACHE_DTYPES`: this follows from the
|
|
412
|
+
# format the family was post-trained on, so every DeepSeek-V4 checkpoint shares it.
|
|
413
|
+
CODE_CHAT_FORMATS: dict[str, Callable[..., ChatFormatter]] = {
|
|
414
|
+
"DeepseekV4": load_deepseek_v4_formatter,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
_CACHE: dict[tuple[str, str, bool], ChatFormatter | None] = {}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def resolve_chat_formatter(
|
|
422
|
+
architectures: Sequence[str] | None,
|
|
423
|
+
hf_model_id: str,
|
|
424
|
+
*,
|
|
425
|
+
trust_remote_code: bool = True,
|
|
426
|
+
) -> ChatFormatter | None:
|
|
427
|
+
"""The code formatter these architectures need, or ``None`` when a template is enough.
|
|
428
|
+
|
|
429
|
+
``None`` is the answer for the great majority of models, and also the answer when a
|
|
430
|
+
formatter is needed but unreachable -- that case logs a warning naming the file. Loading a
|
|
431
|
+
model must not fail because one endpoint is unavailable, and the caller already has a
|
|
432
|
+
refusal for "this model cannot take chat input" (``NoChatTemplateError``) that says to send
|
|
433
|
+
raw text instead.
|
|
434
|
+
|
|
435
|
+
Takes the config's whole ``architectures`` list for the reason
|
|
436
|
+
:func:`facts.mandatory_kv_cache_dtype` does: it is the shape callers already hold, and a
|
|
437
|
+
composite config can name more than one.
|
|
438
|
+
"""
|
|
439
|
+
for name in architectures or ():
|
|
440
|
+
for prefix, loader in CODE_CHAT_FORMATS.items():
|
|
441
|
+
if not str(name).startswith(prefix):
|
|
442
|
+
continue
|
|
443
|
+
key = (str(name), hf_model_id, bool(trust_remote_code))
|
|
444
|
+
if key not in _CACHE:
|
|
445
|
+
try:
|
|
446
|
+
_CACHE[key] = loader(hf_model_id, trust_remote_code=trust_remote_code)
|
|
447
|
+
except ChatFormatterUnavailable as exc:
|
|
448
|
+
logger.warning("No chat formatter for %s: %s", hf_model_id, exc)
|
|
449
|
+
_CACHE[key] = None
|
|
450
|
+
return _CACHE[key]
|
|
451
|
+
return None
|
|
@@ -38,6 +38,7 @@ from interp_engine import facts, moe_routing
|
|
|
38
38
|
from interp_engine.address import Address, to_address
|
|
39
39
|
from interp_engine.arch import ArchSpec, resolve_arch
|
|
40
40
|
from interp_engine.autograd_support import GradSupport, eager_grad_support
|
|
41
|
+
from interp_engine.chat_formatters import resolve_chat_formatter
|
|
41
42
|
from interp_engine.facts import factored_projection, text_config
|
|
42
43
|
from interp_engine.points import PointSpec, Scope, known_names, point_spec, points_for
|
|
43
44
|
from interp_engine.protocol import Completion
|
|
@@ -310,6 +311,14 @@ class EagerModel:
|
|
|
310
311
|
self.tokenizer,
|
|
311
312
|
default_prepend_bos=default_prepend_bos,
|
|
312
313
|
device=str(self.device),
|
|
314
|
+
# None for every family whose tokenizer carries its own chat template, which is
|
|
315
|
+
# nearly all of them. Never raises: a model whose code-defined format cannot be
|
|
316
|
+
# loaded still serves everything except chat input.
|
|
317
|
+
formatter=resolve_chat_formatter(
|
|
318
|
+
getattr(self.config, "architectures", None),
|
|
319
|
+
hf_model_id,
|
|
320
|
+
trust_remote_code=trust_remote,
|
|
321
|
+
),
|
|
313
322
|
)
|
|
314
323
|
|
|
315
324
|
# `requires_grad=True` IS a gradient request, so this is its point of use and the same gate
|