Repository navigation
perf(server): gate the history-boundary render on model snapshot-reuse capability #1153
Description
Problem / Background
Issue #1143 added a history-boundary render to the chat request path: when the prompt cache is enabled and the request is text-only, prepare_chat_request_with_cache renders the conversation a second time with add_generation_prompt = false, and ModelProvider::send_generate_request_with_cancellation_and_metadata tokenizes it on the dispatch thread.
Only models reporting LanguageModel::supports_snapshot_reuse() can use the result. Every other family (llama, qwen3, mistral, and every other dense-KV model) reuses prefixes through the longest-prefix trie instead, and the scheduler discards the boundary vector at the first gate in resolve_history_boundary.
The route cannot currently tell the difference. supports_snapshot_reuse() lives on the LoadedModel behind the worker thread, and AppState has no capability flag for it: the model is constructed inside the worker constructor after model_path is moved in, so it is never in scope at the point AppState is assembled in server::startup.
The result is that a dense-KV deployment pays, on every text-only chat request with the prompt cache on:
- a second full minijinja render of the conversation,
- a
Stringclone of it intoPromptCacheRequestContext, - a second full tokenizer encode,
and then the scheduler throws all of it away. The cost grows with conversation length, and the return is exactly zero.
MLXCEL_DISABLE_BOUNDARY_SNAPSHOT=1 is the interim opt-out and does remove the whole path end to end, but it is an operator action for what should be an automatic decision, and an operator running a dense-KV model has no reason to know the switch exists.
Proposed Solution
Publish the capability from the worker to the HTTP layer, then gate on it.
- Add a shared readiness-style flag to
ModelProvider, following the existingloaded: Arc<AtomicBool>pattern, set by the worker immediately after the model is constructed and before the provider is considered ready. - Surface it on
AppState(asnapshot_reuse_capableaccessor, or fold it into the existingModelMediaSupport-style capability struct). - Fold it into
render_history_prefixinprepare_chat_request_with_cache, next to the existingprompt_cache_enabled, media, and kill-switch conditions.
Deliberately not solved by a static model_type table: supports_snapshot_reuse() is implemented per model and its family list (bailing_moe_linear, falcon_h1, gemma4, granitemoehybrid, jamba, lfm2, mamba, mamba2, muse_glimmer, nemotron_h, plamo2, qwen3_5, qwen3_next) does not match HYBRID_SSM_MODEL_TYPES, so a second table would drift from the trait method it is meant to mirror.
Watch the startup ordering: requests must not be admitted before the flag is set, or the first few requests on a snapshot-only model would skip the render and silently lose their first boundary snapshot. The existing loaded flag already establishes the ordering this needs.
Acceptance Criteria
- A dense-KV model (llama-3.2-1b-instruct) performs no history-boundary render and no second tokenization: verified by a counter or trace that can only move on the render path, not by timing alone
- A snapshot-only model (qwen3.5-0.8b-4bit) is unaffected: turn 2
cached_tokensandsnapshot_hitsmatch the numbers recorded indocs/benchmark_results/history-boundary-snapshot-m1ultra-2026-08-14.md - No request admitted before the capability is resolved can lose a boundary snapshot it should have taken
-
MLXCEL_DISABLE_BOUNDARY_SNAPSHOTkeeps working as the manual override on top of the automatic gate
Technical Considerations
Found during the review of PR #1150 (issue #1143). The correctness of the boundary snapshot does not depend on this; it is purely wasted work on families that cannot use it.
Activity
feat(server): donate a history-boundary snapshot during prefill (#1150)
fix: preserve startup snapshot boundary ordering
docs: add technical report for PR #1507
feat: gate boundary snapshots by model capability (lablup#1507)