Skip to content

server : allow cache reuse for text-only prompts with mmproj loaded - #60

Open
Piggidragon wants to merge 4 commits into
GenerelSchwerz:llama/devfrom
Piggidragon:server/cache-reuse-mmproj-text-only
Open

server : allow cache reuse for text-only prompts with mmproj loaded#60
Piggidragon wants to merge 4 commits into
GenerelSchwerz:llama/devfrom
Piggidragon:server/cache-reuse-mmproj-text-only

Conversation

@Piggidragon

Copy link
Copy Markdown

Overview

Loading an --mmproj currently disables --cache-reuse for the whole server. The reuse gate keys on server_tokens::has_mtmd, which only means "an mmproj is loaded", not "this prompt contains media". So a text-only request against a multimodal model loses prompt cache reuse even though nothing about it needs media handling.

This changes the gate to look at real media chunks instead:

  • add server_tokens::has_media_chunks() (!map_idx_to_media.empty())
  • stop forcing params_base.n_cache_reuse = 0 when an mmproj is loaded
  • gate can_cache_reuse on !slot.prompt.tokens.has_media_chunks() && !input_tokens.has_media_chunks() so reuse only runs when both the cached prompt and the new prompt are text-only
  • relax server_tokens::set_token from GGML_ASSERT(!has_mtmd) to GGML_ASSERT(!has_mtmd || map_idx_to_media.empty()), since the reuse loop calls it and it now runs with an mmproj loaded
  • drop the now-unreachable GGML_ABORT("not supported by multimodal") and the has_mtmd assert inside the reuse block

Once an image or audio chunk is in either prompt, can_cache_reuse is false and behaviour is unchanged. Context shift and the global mmproj ctx_shift disable are left as-is.

Additional information

Addresses the cache-reuse part of ggml-org#21133 (capability flag has_mtmd conflated with "media present in this conversation"). Slot save/restore and checkpoints for text-only + mmproj are out of scope here.

Requirements

🤖 Generated with Claude Code

@github-actions github-actions Bot added the server label Sep 1, 2026
@GenerelSchwerz

Copy link
Copy Markdown
Owner

Reviewed head: f3d4d77f458f330773c9ff7f22a1018ce0ecf9f6
Declared base: 43aaa59b6c1d9b33e686f393607b660047a68e19

Review outcome: request changes. The implementation is narrow, follows the accepted capability-versus-content direction, builds successfully, and passed focused runtime validation. I found one blocking repository-policy issue and two review-readiness issues. I did not find an actionable correctness, security, portability, lifetime, or architectural defect in the implementation.

Blocking

  1. The exact reviewed commit records Claude as a co-author.

The raw metadata of commit f3d4d77f contains:

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Repository policy requires AI contributions to use Assisted-by: and reserves Co-authored-by: for human co-authors. See AGENTS.md lines 96-101. The contributor should correct the commit metadata themselves.

Will slow review

  1. The newly enabled cache-mutation path has no committed regression coverage.

The change adds the content-based gate at server-context.cpp lines 3390-3404, then moves KV positions and rewrites cached prompt tokens at server-context.cpp lines 3420-3434. Before this PR, an mmproj-loaded slot could not enter this path.

The PR changes no tests, and no server test at the reviewed head references cache_reuse, cache-reuse, or n_cache_reuse. This conflicts with the bug-fix regression-test requirement in CONTRIBUTING.md line 35.

The existing mmproj_server fixture in test_slot_save.py lines 162-202 can be reused without adding a test file. Coverage should prove both:

  • Text-only prompts can reuse a shifted matching chunk while an mmproj is loaded.
  • Reuse remains disabled when either the incoming prompt or the slot's cached prompt contains media.

The closely analogous upstream fix, ggml-org/llama.cpp PR #25076, also changed capability-based gating to content-based gating and included regression tests.

  1. A new code comment is split mid-sentence.

server-context.cpp lines 3392-3393 splits one sentence after a semicolon. This directly conflicts with the no-mid-sentence-wrapping rule in AGENTS.md line 89. Make each line a complete sentence or remove commentary that the predicate already makes clear.

Nits

None.

Changed-line review and design assessment

The content-tracking helper is at server-common.h lines 223-225. It reads the existing map_idx_to_media state rather than introducing parallel state.

The relaxed mutation assertion is at server-common.cpp lines 631-634. The call site reaches set_token() only after both cached and incoming token lists pass the no-media predicate.

The global cache-reuse disable is removed at server-context.cpp lines 1209-1215. The per-request check still requires llama_memory_can_shift(), so model-memory restrictions remain enforced independently of mmproj capability.

The media map is cleared, trimmed, cloned, serialized, and restored with the token list in the existing server_tokens infrastructure. I did not find a path where a live media chunk becomes invisible to the new predicate while its media KV state remains eligible for cache reuse.

Upstream precedent and duplicate search

The direct documented need is issue #21133, which explicitly identifies has_mtmd as a capability flag and lists cache reuse as incorrectly blocked for text-only prompts. Issue #19466 is related but broader slot-state work.

Relevant upstream PRs:

  • PR #25076 merged the same capability-versus-content distinction for text-only slot save/restore.
  • PR #26640 later added media serialization for slot state.
  • PR #24004 was a much broader rejected automatic prompt/KV cache design, not an equivalent focused implementation.
  • PR #22288 and issue #21468 concern SWA memory-shift capability, not the mmproj capability/content distinction.

Separate plain-term gh search prs and gh search issues searches for mmproj cache-reuse, mmproj cache reuse, n_cache_reuse mmproj, mmproj prompt cache, and related title searches found no open, merged, or rejected upstream PR that duplicates this focused change.

At pristine upstream commit 159b741427337a2e9a58b08121001545d66b5825, the old restriction is still present in the global mmproj gate, per-request cache-reuse gate and abort, and set_token assertion.

Exact-head build and CI

I built llama-server from an archive of the exact reviewed head. The three touched source files had Git blob IDs identical to the frozen head:

tools/server/server-context.cpp  b04f2592d540e3ab9ac88b182425bfe62fc242af
tools/server/server-common.cpp   6da15ebfd32838587c381ae1b40d2f89f1a48f18
tools/server/server-common.h     d79679f067201cd51583d8094ad7f6ec54446ec8

Configuration: Release, GGML_NATIVE=OFF, GGML_CUDA=OFF, LLAMA_BUILD_SERVER=ON, and LLAMA_BUILD_TESTS=ON. The full GCC 16.2.1 build completed successfully ([334/334]), and this target rebuild also exited 0:

cmake --build /tmp/llama-pr60-review.liQWzP/build --target llama-server -j 8

CI interpretation:

  • Server Ubuntu and Server Windows passed their builds and tests.
  • The CPU workflow passed Ubuntu x64, Ubuntu arm64, Windows x64 static, Windows x64 OpenBLAS, and Windows arm64 builds/tests as applicable.
  • The WebGPU workflow passed.
  • The CMake-package job and self-hosted accelerator workflow were canceled exactly 24 hours after starting with no job steps executed. They did not report code or test failures. The PR's UNSTABLE state reflects these canceled checks.

Focused runtime reproduction

I used the exact-head CPU build with the official ggml-org/tinygemma3-GGUF model and mmproj fixture.

Text-only reuse with mmproj loaded:

LLAMA_MEDIA_MARKER='<__media__>' /tmp/llama-pr60-review.liQWzP/build/bin/llama-server -m /tmp/llama-pr60-review.liQWzP/cache/models--ggml-org--tinygemma3-GGUF/blobs/7566ae7219c93ea2ecc692a931ee122d30c55261d0e2c3347acb8b939d2e9abd --mmproj /tmp/llama-pr60-review.liQWzP/cache/models--ggml-org--tinygemma3-GGUF/blobs/93c2ba8c34574dd8f2dfda64931fc20943de2f941bfe03e6e9eca68951b80604 --cache-reuse 4 -c 1024 -np 1 -ngl 0 --host 127.0.0.1 --port 18081 --verbose
curl -sS http://127.0.0.1:18081/completion -H 'Content-Type: application/json' -d '{"prompt":"REMOVE THIS PREFIX Alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu.","n_predict":0,"temperature":0,"id_slot":0}'

curl -sS http://127.0.0.1:18081/completion -H 'Content-Type: application/json' -d '{"prompt":" Alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu.","n_predict":0,"temperature":0,"id_slot":0}'

The server logged:

reusing chunk with size 14, shifting KV cache [4, 18) -> [1, 15)

The second response reported cache_n=13 and prompt_n=2. This proves that text-only cache reuse executes successfully while an mmproj is loaded.

I then disabled the separate RAM prompt cache so it could not replace a media-bearing slot with an older text-only state:

LLAMA_MEDIA_MARKER='<__media__>' /tmp/llama-pr60-review.liQWzP/build/bin/llama-server -m /tmp/llama-pr60-review.liQWzP/cache/models--ggml-org--tinygemma3-GGUF/blobs/7566ae7219c93ea2ecc692a931ee122d30c55261d0e2c3347acb8b939d2e9abd --mmproj /tmp/llama-pr60-review.liQWzP/cache/models--ggml-org--tinygemma3-GGUF/blobs/93c2ba8c34574dd8f2dfda64931fc20943de2f941bfe03e6e9eca68951b80604 --cache-reuse 4 --cache-ram 0 -c 1024 -np 1 -ngl 0 --host 127.0.0.1 --port 18081 -lv 2
curl -sS http://127.0.0.1:18081/completion -H 'Content-Type: application/json' -d '{"prompt":{"prompt_string":"What is this: <__media__>\n","multimodal_data":["iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="]},"n_predict":0,"temperature":0,"id_slot":0}'

curl -sS http://127.0.0.1:18081/completion -H 'Content-Type: application/json' -d '{"prompt":" Alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu.","n_predict":0,"temperature":0,"id_slot":0}'

Both the incoming-media request and the following text request with media cached logged:

cache reuse is not supported - ignoring n_cache_reuse = 4

Their response timings were respectively cache_n=0, prompt_n=265 and cache_n=0, prompt_n=15. This verifies both halves of the new media predicate.

No existing top-level comment or review duplicated these findings at the time of review.

@Piggidragon
Piggidragon force-pushed the server/cache-reuse-mmproj-text-only branch from f3d4d77 to 732f0b4 Compare September 3, 2026 07:20
@GenerelSchwerz

Copy link
Copy Markdown
Owner

Automated preliminary review by Codex; the repository owner plans a separate manual review.

Verdict: FAIL

Blocking

  • tools/server/tests/unit/test_slot_save.py:535: The new positive case uses tinygemma3 with its default SWA cache. The executed Ubuntu and Windows server jobs both fail here with cache_n == 0: the server finds and shifts a 28-token match, but checkpoint validation invalidates it and forces full prompt reprocessing. Enable swa_full for this positive case so it exercises a cache configuration that can preserve the shifted match.

Will slow review

  • tools/server/tests/unit/test_slot_save.py:539-586: The negative media-gate coverage is not fully discriminating. In the incoming-media half, the cached prompt starts with Throw away... while the incoming prompt starts with What is this...; because the reuse loop advances only the cached head (tools/server/server-context.cpp:3405-3440), this can produce fewer than 10 cached tokens even if the incoming-media guard is broken. Make each negative setup contain a shifted match that would be reused if its guard were missing. Also add a follow-up proving text reuse resumes after stale media is removed from the cached slot.
  • tools/server/tests/unit/test_slot_save.py:499 and tools/server/tests/unit/test_slot_save.py:524: Both new comments split a sentence across lines. Make each comment line a complete sentence or remove the wrapping.

Nits

None.

Product code

  • tools/server/server-common.h:223-225, tools/server/server-common.cpp:631-634, and tools/server/server-context.cpp:3390-3404: The product-code change itself appears correct, narrow, O(1), and compatible with pristine upstream.

Developmental progress

The prohibited AI co-author trailer and production comment from the earlier revision were fixed. Coverage was added, but it is currently failing and incomplete.

Reviewed head: 732f0b4

has_mtmd only means an mmproj is loaded, not that the current prompt
carries media. Loading an mmproj disabled cache reuse for the whole
server, so text-only requests lost prompt cache reuse too.

Gate cache reuse on real media chunks in the cached prompt and the new
prompt instead. It stays disabled once an image or audio chunk is
present, and works again as soon as both prompts are text-only.

Assisted-by: Claude Sonnet
@Piggidragon
Piggidragon force-pushed the server/cache-reuse-mmproj-text-only branch from 732f0b4 to c639ce2 Compare September 5, 2026 21:06
Cache reuse is no longer disabled at startup for mmproj, so the per-slot
warning now fires on every request that carries media. Warn only when the
memory cannot shift, and log the expected media case at debug level.

Also simplify the set_token assert: a non-empty media map already implies
has_mtmd.

Assisted-by: Claude Opus 5
Review follow-ups on the cache reuse block:

- the shift is applied to the draft context too, so require both contexts
  to support it
- skip reuse while an alora is invoked, the loop moved n_past past the cap
- drop the context checkpoints after a shift, they no longer match the cache
- set_token asserts on the media placeholder instead of the media map, and
  takes a token index, which is what the only caller passes

Assisted-by: Claude Opus 5
One server instead of two. The shared chunk now starts on a newline, so it
tokenizes the same with and without the leading sentence.

Assisted-by: Claude Opus 5
@pippo73

pippo73 commented Sep 7, 2026

Copy link
Copy Markdown

Confirming this from a real deployment, in case a second data point helps.

Running Ornith-1.5-35B-A3B on moe-cache-drafting with --mmproj loaded and --cache-reuse 256, the server logs cache_reuse is not supported by multimodal, it will be disabled at load, and the flag is a silent no-op for the whole session — even though this workload is text-only in the large majority of requests.

Worth recording for anyone who reads that log line and worries: ordinary prefix caching is unaffected. On a repeated 62,449-token prompt the slot still matches and reprocesses 4 tokens, TTFT 0.085 s against 26.6 s cold. What is lost is only the mid-prompt divergence case, so the practical impact is narrower than "loses prompt cache reuse" suggests — but it is real for any client that edits or trims history.

Hardware: RTX 5060 Ti 16 GB, CUDA 13.3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants