feat(specprefill): draft-model-free self-scoring for single-request TTFT - #1225
feat(specprefill): draft-model-free self-scoring for single-request TTFT#1225drumtorben wants to merge 1 commit into
Conversation
Adds score_tokens_self() — a SpecPrefill scoring path that requires no draft model. Instead of a separate smaller model, it runs the first n_score_layers (default 4) of the target model itself, using an early-exit exception (_ScoringComplete) to stop Python graph construction before the remaining layers are built. MLX therefore only executes Metal ops for those first N layers (~12.5% of a 32-layer model's dense prefill cost). Q vectors from the last n_tail_queries prompt positions (proxy for future-generation queries) score which earlier tokens matter most, feeding into the existing _compute_importance + select_chunks + sparse_prefill path unchanged. Motivation: IDE workflows (opencode, Continue) suffer a full re-prefill penalty whenever an in-context file changes. With no draft model loaded, there was no SpecPrefill path. Self-scoring makes the ~42% total cost (vs 100% dense) accessible to all users without configuration burden. Architecture-safe: uses make_prompt_cache(model) and _build_layer_to_cache_map on the real model — no truncation of model.layers — so Gemma4 (previous_kvs), Nemotron-H (block_type sparse attention), and VLM wrappers all work correctly. New surface: - score_tokens_self() in omlx/patches/specprefill.py - _ScoringComplete, _EarlyExitAttn helper classes - _patch_attention_for_capture gains optional layers_to_patch parameter - SchedulerConfig.specprefill_self_score_layers (default 4) - EngineCore.add_request(..., specprefill_self_score=True) - Request._specprefill_self_score field 9 new unit tests in TestScoreTokensSelf covering output shape, finiteness, module cleanup (normal + exception path), edge cases, and engine propagation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thanks for putting this together. The BaseException early-exit and the clean if/else split from the existing draft path are both nice, easy to read and obviously off-by-default. Going to close this for now, not because the idea is bad but because I think it needs another pass before it's safe to merge:
Happy to re-open and revisit if you want to come back with 1+2 wired up. 3 can be a follow-up. Thanks again for the contribution! |
Summary
score_tokens_self()— a SpecPrefill scoring path that works without a draft model, using only the firstn_score_layers(default 4) of the target model itself_ScoringComplete) to stop MLX graph construction after layer N, so Metal only executes ops for those layers (~12.5% of dense prefill cost for a 32-layer model)n_tail_queriesprompt positions act as a proxy for future-generation queries, feeding into the existing_compute_importance → select_chunks → sparse_prefillpipeline unchangedMotivation
IDE workflows (opencode, Continue, Cursor) suffer a full re-prefill penalty whenever an in-context file changes — the prefix cache ends at
[system+tools]and everything after must be re-prefilled densely every turn. Previously,SpecPrefillrequired a configured draft model (additional 4–8 GB RAM), making it inaccessible to most single-server deployments. This change makes the speedup available to everyone with a single flag.Architecture safety
No truncation of
model.layersis used.make_prompt_cache(model)and_build_layer_to_cache_map()operate on the real model structure throughout, so the following special cases are handled correctly without any extra code:previous_kvsshared-KV cache routing)block_typesparse attention, compacted layer-to-cache mapping)VLMModelAdapter.layersproperty delegation)_compute_importance)New API surface
score_tokens_self()omlx/patches/specprefill.py_ScoringCompleteomlx/patches/specprefill.pyBaseExceptionsentinel_EarlyExitAttnomlx/patches/specprefill.py_patch_attention_for_capture(..., layers_to_patch=None)omlx/patches/specprefill.pySchedulerConfig.specprefill_self_score_layersomlx/scheduler.py4EngineCore.add_request(..., specprefill_self_score=True)omlx/engine_core.py_specprefill_enabledRequest._specprefill_self_scoreomlx/request.pyTest plan
TestScoreTokensSelf(shape, finiteness, module cleanup on normal + exception path, edge cases for clamped layer/query counts,mx.arrayinput, progress callback, engine propagation)test_specprefill.pytests passmainunchanged)specprefill_self_score=Trueand prompt >8192 tokens, verify SpecPrefill fires in dashboard and TTFT is reduced🤖 Generated with Claude Code