server: prompt cache is populated by rerank tasks that can never read it back (cache_idle_slots missing the SERVER_TASK_TYPE_COMPLETION guard)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- cpp
- Domain
- backend, performance
Research direction
Start in server-context.cpp at the idle-slot save loop around lines 2398-2414 and compare it with the completion guard around lines 1617-1618. Ensure idle prompt caching is limited to completion tasks, then run the rerank reproduction with distinct documents and confirm memory no longer grows from prompt-cache entries.
Written by the indexing model from the issue text.
Description
Summary
On a rerank-only server, host RAM grows by ~23 MB per /v1/rerank call until it reaches the --cache-ram limit (8192 MiB by default), and is never released while the process lives. The growth is the prompt cache being populated by rerank tasks that can never read it back - for SERVER_TASK_TYPE_RERANK the cache is write-only.
This is not the same as #25740 (closed as not-planned). There the cache was doing its job on the completion path and the reporter simply hadn't set -cram 0. Here the entries are provably never read, so the memory is pure waste plus a per-request serialisation cost.
Version / environment
b4d6c7d8ff69c2e05e4e8ee7e6e710a08abd7b45(b10091), also present on master at time of writing- CUDA, aarch64 (NVIDIA GB10), driver 580.173.02
- Model: Qwen3-Reranker-0.6B Q8_0 GGUF
- Server:
llama-server --reranking --pooling rank -c 8192 -np 2 -ngl 999 -fa on --no-mmap - Reproduced identically on two hosts
The path
POST /v1/rerankcreates independentSERVER_TASK_TYPE_RERANKtasks with no parent.server_slot::release()clears the prompt only for child tasks (server-context.cpp:488-491):
Rerank tasks are not children, so// do not keep context of the child slots - the parent's context is enough if (task->is_child()) { prompt_clear(); }slot.promptand its KV state survive release.- On the next task launch, the idle-slot pass saves every idle slot to the prompt cache (
server-context.cpp:2398-2414) - with no task-type guard:if (params_base.cache_idle_slots) { for (auto & slot : slots) { if (!slot.is_processing()) { if (slot.prompt_save(*prompt_cache)) { - The other call site does have the guard it needs (
server-context.cpp:1617-1618):// cache prompts only for completion tasks update_cache = update_cache && task.type == SERVER_TASK_TYPE_COMPLETION; prompt_loadis only reached from inside that completion-gated branch, so nothing ever reads a rerank entry back.
With -np 2, kv_unified is false, so the [TAG_IDLE_SLOT_CLEAR] path does not run and the slot keeps its prompt after the save as well.
Measurements
Fresh restart each arm, 40 calls, resident memory of the service:
| workload | growth |
|---|---|
| 40 calls, identical query+docs | +68 MB, then flat from call ~10 |
| 40 calls, 20 docs, 40 distinct doc lengths | +3032 MB by call 20, climbing until capped |
same, with --cache-ram 0 |
+626 MB |
same, --cache-ram 0 + GGML_CUDA_DISABLE_GRAPHS=1 |
+620 MB (graph cache not involved) |
| same 40 lengths descending | +620 MB immediately, then flat |
Two separate effects: the cache (dominant, content-keyed, deduped only on full prefix match, so every distinct query+document pair is a new entry) and a benign high-water-mark buffer allocation of ~620 MB that stops growing once the largest request has been seen.
The arithmetic matches: Qwen3-0.6B is 28 layers x 8 KV heads x 128 head_dim, f16 → 112 KiB/token, so ~23 MB/call ≈ 210 tokens of KV state - a query plus a few short documents. In production the process sat at 9137 MB with zero busy slots, consistent with ~1.1 GB baseline plus the 8 GiB cap.
Reproduce
llama-server -m Qwen3-Reranker-0.6B-Q8_0.gguf --reranking --pooling rank -c 8192 -np 2
# then POST /v1/rerank repeatedly with DIFFERENT documents each time and watch RSS.
# Identical documents will not reproduce it - the cache dedups on full prefix match.
Suggested fix
Apply the same guard the completion path already uses, so idle slots are only saved for task types that can read the cache back:
if (params_base.cache_idle_slots) {
for (auto & slot : slots) {
if (!slot.is_processing() && slot.task && slot.task->type == SERVER_TASK_TYPE_COMPLETION) {
Alternatively (or additionally) clear the prompt on release for task types that never reuse it.
Workaround
--cache-ram 0 (which also auto-disables cache_idle_slots), or --no-cache-idle-slots. For a rerank/embedding-only server this costs nothing, since those entries are never read. Confirmed: 9137 MB → ~1.1 GB steady state on our fleet.
Note on scope
/v1/embeddings builds non-child SERVER_TASK_TYPE_EMBEDDING tasks through the same send_embedding → release() path, so it looks structurally exposed. I could not reproduce growth on an embedding-only server (Qwen3-Embedding-0.6B, 30 distinct input lengths, +20 MB and flat), so I am reporting only what I measured rather than assuming.
- Dominant language
- C++
- Stars
- 129k
- Forks
- 23.5k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 411
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ggml-org/llama.cpp
-
bug-unconfirmed
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
bug-unconfirmed
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
bug-unconfirmed
Difficulty 1/5 Under an hour Newbie friendliness 90/100
-
/v1/responses: reasoning item with "summary": null rejected as "Cannot determine type of 'item'" Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
All issues in ggml-org/llama.cpp
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
AXERA-TECH/ax-llm#77 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
NVIDIA/cuda-samples#453 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
infiniflow/infinity#3502 ·
-
level/task module/gcp type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100