Skip to content

perf(server): gate the history-boundary render on model snapshot-reuse capability #1153

Closed

Description

@inureyes

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 String clone of it into PromptCacheRequestContext,
  • 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.

  1. Add a shared readiness-style flag to ModelProvider, following the existing loaded: Arc<AtomicBool> pattern, set by the worker immediately after the model is constructed and before the provider is considered ready.
  2. Surface it on AppState (a snapshot_reuse_capable accessor, or fold it into the existing ModelMediaSupport-style capability struct).
  3. Fold it into render_history_prefix in prepare_chat_request_with_cache, next to the existing prompt_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_tokens and snapshot_hits match the numbers recorded in docs/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_SNAPSHOT keeps 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

  1. self-assigned this
    on Aug 30, 2026
  2. added a commit that references this issue on Sep 6, 2026
    1a03192
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions