llm-cachecheck 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_cachecheck-0.3.0/PKG-INFO +285 -0
- llm_cachecheck-0.3.0/README.md +261 -0
- {llm_cachecheck-0.2.2 → llm_cachecheck-0.3.0}/cachecheck.py +216 -26
- llm_cachecheck-0.3.0/examples/conversations_example.jsonl +2 -0
- {llm_cachecheck-0.2.2 → llm_cachecheck-0.3.0}/pyproject.toml +1 -1
- llm_cachecheck-0.3.0/test_cachecheck.py +657 -0
- llm_cachecheck-0.2.2/PKG-INFO +0 -187
- llm_cachecheck-0.2.2/README.md +0 -163
- llm_cachecheck-0.2.2/test_cachecheck.py +0 -365
- {llm_cachecheck-0.2.2 → llm_cachecheck-0.3.0}/.gitignore +0 -0
- {llm_cachecheck-0.2.2 → llm_cachecheck-0.3.0}/LICENSE +0 -0
- {llm_cachecheck-0.2.2 → llm_cachecheck-0.3.0}/examples/qwen2.5-3b-q4km_metal_b12138.json +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: llm-cachecheck
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Measure how often the prompt cache changes greedy answers on your LLM server (llama.cpp, vLLM and SGLang)
|
|
5
|
+
Project-URL: Homepage, https://github.com/aditi-p31/cache-divergence-study/tree/main/cachecheck
|
|
6
|
+
Project-URL: Source, https://github.com/aditi-p31/cache-divergence-study
|
|
7
|
+
Project-URL: Paper, https://doi.org/10.1109/ACCESS.2026.3749922
|
|
8
|
+
Project-URL: Preprint, https://arxiv.org/abs/2609.04748
|
|
9
|
+
Project-URL: Issues, https://github.com/aditi-p31/cache-divergence-study/issues
|
|
10
|
+
Author: Aditi Patodiya
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: inference,kv cache,llama.cpp,llm,nondeterminism,prefix caching,prompt cache,reproducibility,sglang,vllm
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# llm-cachecheck
|
|
26
|
+
|
|
27
|
+
On PyPI this tool is `llm-cachecheck`. The PyPI package named `cachecheck` is
|
|
28
|
+
a different project.
|
|
29
|
+
|
|
30
|
+
How often does the prompt cache change your server's answers?
|
|
31
|
+
|
|
32
|
+
Serving stacks reuse the cached key-value state of a shared prompt prefix. A
|
|
33
|
+
cache hit and a recomputation run different operations, so with greedy
|
|
34
|
+
decoding a small numeric change can flip a token. After that the answer goes
|
|
35
|
+
its own way. This is known behavior: the llama.cpp server README says that
|
|
36
|
+
`cache_prompt` "can cause nondeterministic results". What varies is how often
|
|
37
|
+
it happens on your model, weights and build. `cachecheck` measures that.
|
|
38
|
+
|
|
39
|
+
It sends short two-turn conversations with greedy decoding and compares the
|
|
40
|
+
second answer on two paths:
|
|
41
|
+
|
|
42
|
+
- **cache path:** turn 2 reuses the state the server wrote while it generated
|
|
43
|
+
turn 1
|
|
44
|
+
- **recompute path:** turn 2 is processed from scratch
|
|
45
|
+
|
|
46
|
+
Each path runs twice. A probe counts as valid only when all of these hold:
|
|
47
|
+
|
|
48
|
+
- both paths repeat on their own
|
|
49
|
+
- the server's own counts show that the cache hit covered the turn-1 answer,
|
|
50
|
+
to within `--hit-tolerance` tokens and at least past the turn-1 prompt
|
|
51
|
+
- the server's own counts show that the recompute path reused nothing
|
|
52
|
+
|
|
53
|
+
## Run it
|
|
54
|
+
|
|
55
|
+
Python 3.8 or newer, no dependencies. Install it from PyPI:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install llm-cachecheck
|
|
59
|
+
llm-cachecheck --help
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or download the single file
|
|
63
|
+
[`cachecheck.py`](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck/cachecheck.py)
|
|
64
|
+
and run `python cachecheck.py` in place of `llm-cachecheck`. Both take the
|
|
65
|
+
same options. Each conversation takes six requests, so a run of the 20
|
|
66
|
+
built-in conversations sends 120.
|
|
67
|
+
|
|
68
|
+
**llama.cpp.** The cache is switched per request. Start the server with one
|
|
69
|
+
slot, because the default is several slots that share a cache. Turn the
|
|
70
|
+
host-memory prompt cache off to match the study's controlled runs:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
llama-server -m model.gguf -np 1 --cache-ram 0 --port 8080
|
|
74
|
+
llm-cachecheck --base-url http://127.0.0.1:8080
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**vLLM or SGLang, one server.** Start the server with prefix caching on (the
|
|
78
|
+
default) and with cached-token reporting on. The tool sends every request with
|
|
79
|
+
a `cache_salt`. The two turns of the cache path share one salt, so turn 2
|
|
80
|
+
reuses the state of turn 1. Each recompute request gets a salt that no other
|
|
81
|
+
request has used, so it cannot reuse any cached state.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
vllm serve Qwen/Qwen2.5-3B-Instruct --port 8000 \
|
|
85
|
+
--generation-config vllm --enable-prompt-tokens-details
|
|
86
|
+
llm-cachecheck --mode one-server --base-url http://127.0.0.1:8000 \
|
|
87
|
+
--model Qwen/Qwen2.5-3B-Instruct
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For SGLang, add `--enable-cache-report` and `--sampling-defaults openai`.
|
|
91
|
+
On real vLLM 0.33.0 and SGLang 0.5.23 servers (SGLang with CUDA graphs disabled), the recompute answers in this mode had the same text as those of a second server with prefix caching off, on all 20 conversations in every run ([details](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck-0.3-validation/README.md)).
|
|
92
|
+
|
|
93
|
+
The server must honour `cache_salt`. If it does not, the recompute requests
|
|
94
|
+
reuse cached state, the tool marks those probes as inconclusive and says so,
|
|
95
|
+
and the two-server setup below still works. The server must also report cached
|
|
96
|
+
tokens, so `--skip-hit-check` is not available in this mode.
|
|
97
|
+
|
|
98
|
+
This mode compares a cache hit with a cache miss on one server. From reading
|
|
99
|
+
the vLLM 0.33.0 and SGLang 0.5.23 source, a miss on a standard attention model,
|
|
100
|
+
served without a KV connector such as LMCache, takes the same code path as on a
|
|
101
|
+
server with prefix caching off. For some setups, for example models with Mamba
|
|
102
|
+
or sliding-window layers, or speculative decoding, turning prefix caching on
|
|
103
|
+
also changes how the server handles requests that miss. To compare with
|
|
104
|
+
caching off there, use two servers.
|
|
105
|
+
|
|
106
|
+
Run it on a server that gets no other traffic, such as a staging copy. On a
|
|
107
|
+
busy server, batching with other requests can change answers by itself. The
|
|
108
|
+
tool's repeat checks mark many such probes as inconclusive, but they cannot
|
|
109
|
+
rule out a difference caused by other traffic.
|
|
110
|
+
|
|
111
|
+
**vLLM, SGLang or another OpenAI-compatible server, two servers.** The cache
|
|
112
|
+
is a server setting, so serve the same model twice, once with prefix caching
|
|
113
|
+
on and once with it off. Both servers must report cached tokens. For vLLM on
|
|
114
|
+
one 24 GB GPU (a 3B model, so two copies fit):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
vllm serve Qwen/Qwen2.5-3B-Instruct --port 8000 --gpu-memory-utilization 0.45 \
|
|
118
|
+
--generation-config vllm --enable-prompt-tokens-details
|
|
119
|
+
vllm serve Qwen/Qwen2.5-3B-Instruct --port 8100 --gpu-memory-utilization 0.45 \
|
|
120
|
+
--generation-config vllm --enable-prompt-tokens-details --no-enable-prefix-caching
|
|
121
|
+
llm-cachecheck --mode two-servers --base-url http://127.0.0.1:8000 \
|
|
122
|
+
--nocache-url http://127.0.0.1:8100 --model Qwen/Qwen2.5-3B-Instruct
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For SGLang, add `--enable-cache-report` and `--sampling-defaults openai` to
|
|
126
|
+
both servers, and `--disable-radix-cache` to the second one.
|
|
127
|
+
|
|
128
|
+
Make sure each server really started on its own port. If another process
|
|
129
|
+
already holds a port, it can answer for it: RunPod's standard images, for
|
|
130
|
+
example, run an nginx proxy on port 8001 that forwards to port 8000, so both
|
|
131
|
+
URLs would reach the same server.
|
|
132
|
+
The tool then reports the recompute path reusing cached tokens and marks every
|
|
133
|
+
probe invalid, so check the server logs if that happens.
|
|
134
|
+
|
|
135
|
+
Tested on real servers (one RTX 4090; the tool's 20 conversations). The two-server and llama.cpp rows are from 2026-11-11 and 11-12 UTC ([currency note](https://github.com/aditi-p31/cache-divergence-study/blob/main/CURRENCY_CHECK_2026-11.md)); the one-server rows are from 2026-11-14 UTC ([details](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck-0.3-validation/README.md)):
|
|
136
|
+
|
|
137
|
+
| Server | Model | Conversations with a different turn-2 answer | Both caches off |
|
|
138
|
+
|---|---|---|---|
|
|
139
|
+
| vLLM 0.33.0, two servers | Qwen2.5-3B-Instruct, BF16 | 14 of 20, in each of 5 runs | 0 of 20 |
|
|
140
|
+
| SGLang 0.5.23, two servers (CUDA graphs disabled) | Qwen2.5-3B-Instruct, BF16 | 12 of 20, in each of 2 runs | 0 of 20 |
|
|
141
|
+
| llama.cpp v0.9.0 | Qwen2.5-7B-Instruct, Q4_K_M | 11 of 20, in each of 2 runs | |
|
|
142
|
+
| vLLM 0.33.0, one server | Qwen2.5-3B-Instruct, BF16 | 14 of 20, in each of 2 runs | |
|
|
143
|
+
| SGLang 0.5.23, one server (CUDA graphs disabled) | Qwen2.5-3B-Instruct, BF16 | 12 of 20, in each of 2 runs | |
|
|
144
|
+
| SGLang 0.5.23, one server, default settings | Qwen2.5-3B-Instruct, BF16 | 12 of 20, in each of 2 runs | |
|
|
145
|
+
|
|
146
|
+
If you run it, an [issue](https://github.com/aditi-p31/cache-divergence-study/issues) with what you saw is welcome.
|
|
147
|
+
|
|
148
|
+
Two server processes can also differ for other reasons. To check that, start
|
|
149
|
+
both with prefix caching off and run with `--skip-hit-check`. Every probe
|
|
150
|
+
should come out the same.
|
|
151
|
+
|
|
152
|
+
Options: `--probes` reads your own conversations (next section), `--n` sets
|
|
153
|
+
the number of probes (default: all), `--max-tokens` the answer length
|
|
154
|
+
(default 256), `--out` the JSON report path. If your server caches in blocks or
|
|
155
|
+
pages larger than 32 tokens, set `--hit-tolerance` to that size, or cache hits
|
|
156
|
+
can look too short. With large pages, also ask for longer turn-1 answers, so
|
|
157
|
+
the hit can reach past the prompt.
|
|
158
|
+
|
|
159
|
+
Models that think before answering need thinking turned off for this check.
|
|
160
|
+
Their templates often drop past thinking from the history, which shortens the
|
|
161
|
+
cache hit, and the tool marks those probes as inconclusive.
|
|
162
|
+
|
|
163
|
+
## Run it on your own conversations
|
|
164
|
+
|
|
165
|
+
The built-in conversations are short and generic. To check the kind of
|
|
166
|
+
prompts your application sends, put your own in a JSON Lines file, one
|
|
167
|
+
conversation per line, and pass it with `--probes`:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
llm-cachecheck --base-url http://127.0.0.1:8080 --probes my_conversations.jsonl
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Each line has one of two forms
|
|
174
|
+
([example file](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck/examples/conversations_example.jsonl)):
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{"id": "returns-1", "turn1": "A customer wants to return shoes bought 40 days ago. ...", "turn2": "Rewrite the reply so it offers store credit instead."}
|
|
178
|
+
{"id": "sql-1", "messages": [{"role": "system", "content": "You are a careful SQL assistant."}, {"role": "user", "content": "Write a query for the ten largest orders."}], "followup": "Change it so it only counts orders created in 2025."}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- `turn1` is the first user message and `turn2` the follow-up.
|
|
182
|
+
- In the longer form, `messages` is the conversation up to and including the
|
|
183
|
+
turn-1 user message, so it can carry your system prompt and earlier turns.
|
|
184
|
+
`followup` is the turn-2 user message.
|
|
185
|
+
- `id` is optional and is copied into the report.
|
|
186
|
+
- Text messages only, with the roles system, developer, user and assistant.
|
|
187
|
+
Tool calls and images are not supported.
|
|
188
|
+
|
|
189
|
+
The tool generates the turn-1 answer itself and builds turn 2 on it, as with
|
|
190
|
+
the built-in conversations, and it always decodes greedily. A file that holds
|
|
191
|
+
one JSON array of these objects also works.
|
|
192
|
+
|
|
193
|
+
Let turn 1 ask for an answer of a few sentences. The tool counts a cache hit
|
|
194
|
+
only when it reaches past the turn-1 prompt into the answer. A server that
|
|
195
|
+
caches in blocks (vLLM uses 16 tokens by default) can stop the hit at the
|
|
196
|
+
prompt when the answer is short, and the probe is then inconclusive.
|
|
197
|
+
|
|
198
|
+
The JSON report saves every answer, so keep it private if your conversations
|
|
199
|
+
are. The printed result block holds no conversation text: only the counts
|
|
200
|
+
and the run settings, including the model name.
|
|
201
|
+
|
|
202
|
+
## Reading the result
|
|
203
|
+
|
|
204
|
+
A real run on Apple Silicon (Metal) with Qwen2.5-3B-Instruct at Q4_K_M,
|
|
205
|
+
llama.cpp b12138, `-np 1 --cache-ram 0`, default options:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
probe 1/20: same
|
|
209
|
+
...
|
|
210
|
+
probe 10/20: DIFFERENT (from character 578)
|
|
211
|
+
...
|
|
212
|
+
probe 13/20: DIFFERENT (from character 235)
|
|
213
|
+
...
|
|
214
|
+
probe 15/20: DIFFERENT (from character 376)
|
|
215
|
+
...
|
|
216
|
+
3 of 20 valid probes gave a different turn-2 answer on the cache path.
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
All 20 probes were valid. On every probe the server reported that the cache
|
|
220
|
+
hit covered all of turn 1 and its answer, and that the recompute path reused
|
|
221
|
+
nothing. The full report (made with a pre-release build of 0.2.0) is in
|
|
222
|
+
[`examples/qwen2.5-3b-q4km_metal_b12138.json`](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck/examples/qwen2.5-3b-q4km_metal_b12138.json).
|
|
223
|
+
It took about 15 minutes on a laptop.
|
|
224
|
+
|
|
225
|
+
| Exit status | Meaning |
|
|
226
|
+
|---|---|
|
|
227
|
+
| 0 | No valid probe differed, and at least 80% of probes were valid |
|
|
228
|
+
| 1 | At least one valid probe differed |
|
|
229
|
+
| 2 | Inconclusive: too few valid probes, or a request failed |
|
|
230
|
+
|
|
231
|
+
A usage error (a bad or conflicting option) also exits with status 2, before
|
|
232
|
+
any request is sent and without writing a report.
|
|
233
|
+
|
|
234
|
+
The JSON report keeps every answer, the finish reasons, token counts, cached
|
|
235
|
+
token counts and the server build.
|
|
236
|
+
|
|
237
|
+
The rate is specific to these short probes. It is not comparable to the
|
|
238
|
+
episode rates below, where one agent episode chains about ten requests.
|
|
239
|
+
|
|
240
|
+
## What to do about a difference
|
|
241
|
+
|
|
242
|
+
For runs that must repeat, turn prefix reuse off and record that setting with
|
|
243
|
+
your results:
|
|
244
|
+
|
|
245
|
+
- llama.cpp: `"cache_prompt": false` per request, or `--no-cache-prompt` for
|
|
246
|
+
the server
|
|
247
|
+
- vLLM: `--no-enable-prefix-caching`
|
|
248
|
+
- SGLang: `--disable-radix-cache`
|
|
249
|
+
|
|
250
|
+
A difference is not a bug in your model. It shows that the answer depends on
|
|
251
|
+
the server's cache state as well as on the request.
|
|
252
|
+
|
|
253
|
+
## Share your result
|
|
254
|
+
|
|
255
|
+
At the end of a run the tool prints a short result block. It leaves out your
|
|
256
|
+
server URL and any key. If you would like to share it, open a
|
|
257
|
+
[result issue](https://github.com/aditi-p31/cache-divergence-study/issues/new?template=cachecheck-result.yml)
|
|
258
|
+
and paste the block. Results where no probe differed are just as useful.
|
|
259
|
+
|
|
260
|
+
## Background and citation
|
|
261
|
+
|
|
262
|
+
`cachecheck` adapts the paired cache-on and cache-off design of the study in
|
|
263
|
+
[cache-divergence-study repository](https://github.com/aditi-p31/cache-divergence-study).
|
|
264
|
+
The study used Qwen2.5-7B-Instruct on llama.cpp with greedy
|
|
265
|
+
decoding, 80 BFCL multi-turn agent episodes and the host-memory prompt cache
|
|
266
|
+
off. The share of episodes whose trajectory changed between a cache hit and a
|
|
267
|
+
recomputation was 37.4% with F16 weights and 82.5% with Q4_K_M weights, as a
|
|
268
|
+
mean over ten server histories.
|
|
269
|
+
|
|
270
|
+
If `cachecheck` helps your work, please cite the paper:
|
|
271
|
+
|
|
272
|
+
```bibtex
|
|
273
|
+
@article{patodiya2026samerequest,
|
|
274
|
+
title = {Same Request, Different Answer: Quantization Amplifies
|
|
275
|
+
Cache-Induced Divergence in LLM Serving},
|
|
276
|
+
author = {Patodiya, Aditi},
|
|
277
|
+
journal = {IEEE Access},
|
|
278
|
+
year = {2026},
|
|
279
|
+
doi = {10.1109/ACCESS.2026.3749922},
|
|
280
|
+
note = {Early Access},
|
|
281
|
+
url = {https://doi.org/10.1109/ACCESS.2026.3749922}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
MIT License. See `LICENSE`.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# llm-cachecheck
|
|
2
|
+
|
|
3
|
+
On PyPI this tool is `llm-cachecheck`. The PyPI package named `cachecheck` is
|
|
4
|
+
a different project.
|
|
5
|
+
|
|
6
|
+
How often does the prompt cache change your server's answers?
|
|
7
|
+
|
|
8
|
+
Serving stacks reuse the cached key-value state of a shared prompt prefix. A
|
|
9
|
+
cache hit and a recomputation run different operations, so with greedy
|
|
10
|
+
decoding a small numeric change can flip a token. After that the answer goes
|
|
11
|
+
its own way. This is known behavior: the llama.cpp server README says that
|
|
12
|
+
`cache_prompt` "can cause nondeterministic results". What varies is how often
|
|
13
|
+
it happens on your model, weights and build. `cachecheck` measures that.
|
|
14
|
+
|
|
15
|
+
It sends short two-turn conversations with greedy decoding and compares the
|
|
16
|
+
second answer on two paths:
|
|
17
|
+
|
|
18
|
+
- **cache path:** turn 2 reuses the state the server wrote while it generated
|
|
19
|
+
turn 1
|
|
20
|
+
- **recompute path:** turn 2 is processed from scratch
|
|
21
|
+
|
|
22
|
+
Each path runs twice. A probe counts as valid only when all of these hold:
|
|
23
|
+
|
|
24
|
+
- both paths repeat on their own
|
|
25
|
+
- the server's own counts show that the cache hit covered the turn-1 answer,
|
|
26
|
+
to within `--hit-tolerance` tokens and at least past the turn-1 prompt
|
|
27
|
+
- the server's own counts show that the recompute path reused nothing
|
|
28
|
+
|
|
29
|
+
## Run it
|
|
30
|
+
|
|
31
|
+
Python 3.8 or newer, no dependencies. Install it from PyPI:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install llm-cachecheck
|
|
35
|
+
llm-cachecheck --help
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or download the single file
|
|
39
|
+
[`cachecheck.py`](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck/cachecheck.py)
|
|
40
|
+
and run `python cachecheck.py` in place of `llm-cachecheck`. Both take the
|
|
41
|
+
same options. Each conversation takes six requests, so a run of the 20
|
|
42
|
+
built-in conversations sends 120.
|
|
43
|
+
|
|
44
|
+
**llama.cpp.** The cache is switched per request. Start the server with one
|
|
45
|
+
slot, because the default is several slots that share a cache. Turn the
|
|
46
|
+
host-memory prompt cache off to match the study's controlled runs:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
llama-server -m model.gguf -np 1 --cache-ram 0 --port 8080
|
|
50
|
+
llm-cachecheck --base-url http://127.0.0.1:8080
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**vLLM or SGLang, one server.** Start the server with prefix caching on (the
|
|
54
|
+
default) and with cached-token reporting on. The tool sends every request with
|
|
55
|
+
a `cache_salt`. The two turns of the cache path share one salt, so turn 2
|
|
56
|
+
reuses the state of turn 1. Each recompute request gets a salt that no other
|
|
57
|
+
request has used, so it cannot reuse any cached state.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
vllm serve Qwen/Qwen2.5-3B-Instruct --port 8000 \
|
|
61
|
+
--generation-config vllm --enable-prompt-tokens-details
|
|
62
|
+
llm-cachecheck --mode one-server --base-url http://127.0.0.1:8000 \
|
|
63
|
+
--model Qwen/Qwen2.5-3B-Instruct
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For SGLang, add `--enable-cache-report` and `--sampling-defaults openai`.
|
|
67
|
+
On real vLLM 0.33.0 and SGLang 0.5.23 servers (SGLang with CUDA graphs disabled), the recompute answers in this mode had the same text as those of a second server with prefix caching off, on all 20 conversations in every run ([details](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck-0.3-validation/README.md)).
|
|
68
|
+
|
|
69
|
+
The server must honour `cache_salt`. If it does not, the recompute requests
|
|
70
|
+
reuse cached state, the tool marks those probes as inconclusive and says so,
|
|
71
|
+
and the two-server setup below still works. The server must also report cached
|
|
72
|
+
tokens, so `--skip-hit-check` is not available in this mode.
|
|
73
|
+
|
|
74
|
+
This mode compares a cache hit with a cache miss on one server. From reading
|
|
75
|
+
the vLLM 0.33.0 and SGLang 0.5.23 source, a miss on a standard attention model,
|
|
76
|
+
served without a KV connector such as LMCache, takes the same code path as on a
|
|
77
|
+
server with prefix caching off. For some setups, for example models with Mamba
|
|
78
|
+
or sliding-window layers, or speculative decoding, turning prefix caching on
|
|
79
|
+
also changes how the server handles requests that miss. To compare with
|
|
80
|
+
caching off there, use two servers.
|
|
81
|
+
|
|
82
|
+
Run it on a server that gets no other traffic, such as a staging copy. On a
|
|
83
|
+
busy server, batching with other requests can change answers by itself. The
|
|
84
|
+
tool's repeat checks mark many such probes as inconclusive, but they cannot
|
|
85
|
+
rule out a difference caused by other traffic.
|
|
86
|
+
|
|
87
|
+
**vLLM, SGLang or another OpenAI-compatible server, two servers.** The cache
|
|
88
|
+
is a server setting, so serve the same model twice, once with prefix caching
|
|
89
|
+
on and once with it off. Both servers must report cached tokens. For vLLM on
|
|
90
|
+
one 24 GB GPU (a 3B model, so two copies fit):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
vllm serve Qwen/Qwen2.5-3B-Instruct --port 8000 --gpu-memory-utilization 0.45 \
|
|
94
|
+
--generation-config vllm --enable-prompt-tokens-details
|
|
95
|
+
vllm serve Qwen/Qwen2.5-3B-Instruct --port 8100 --gpu-memory-utilization 0.45 \
|
|
96
|
+
--generation-config vllm --enable-prompt-tokens-details --no-enable-prefix-caching
|
|
97
|
+
llm-cachecheck --mode two-servers --base-url http://127.0.0.1:8000 \
|
|
98
|
+
--nocache-url http://127.0.0.1:8100 --model Qwen/Qwen2.5-3B-Instruct
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For SGLang, add `--enable-cache-report` and `--sampling-defaults openai` to
|
|
102
|
+
both servers, and `--disable-radix-cache` to the second one.
|
|
103
|
+
|
|
104
|
+
Make sure each server really started on its own port. If another process
|
|
105
|
+
already holds a port, it can answer for it: RunPod's standard images, for
|
|
106
|
+
example, run an nginx proxy on port 8001 that forwards to port 8000, so both
|
|
107
|
+
URLs would reach the same server.
|
|
108
|
+
The tool then reports the recompute path reusing cached tokens and marks every
|
|
109
|
+
probe invalid, so check the server logs if that happens.
|
|
110
|
+
|
|
111
|
+
Tested on real servers (one RTX 4090; the tool's 20 conversations). The two-server and llama.cpp rows are from 2026-11-11 and 11-12 UTC ([currency note](https://github.com/aditi-p31/cache-divergence-study/blob/main/CURRENCY_CHECK_2026-11.md)); the one-server rows are from 2026-11-14 UTC ([details](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck-0.3-validation/README.md)):
|
|
112
|
+
|
|
113
|
+
| Server | Model | Conversations with a different turn-2 answer | Both caches off |
|
|
114
|
+
|---|---|---|---|
|
|
115
|
+
| vLLM 0.33.0, two servers | Qwen2.5-3B-Instruct, BF16 | 14 of 20, in each of 5 runs | 0 of 20 |
|
|
116
|
+
| SGLang 0.5.23, two servers (CUDA graphs disabled) | Qwen2.5-3B-Instruct, BF16 | 12 of 20, in each of 2 runs | 0 of 20 |
|
|
117
|
+
| llama.cpp v0.9.0 | Qwen2.5-7B-Instruct, Q4_K_M | 11 of 20, in each of 2 runs | |
|
|
118
|
+
| vLLM 0.33.0, one server | Qwen2.5-3B-Instruct, BF16 | 14 of 20, in each of 2 runs | |
|
|
119
|
+
| SGLang 0.5.23, one server (CUDA graphs disabled) | Qwen2.5-3B-Instruct, BF16 | 12 of 20, in each of 2 runs | |
|
|
120
|
+
| SGLang 0.5.23, one server, default settings | Qwen2.5-3B-Instruct, BF16 | 12 of 20, in each of 2 runs | |
|
|
121
|
+
|
|
122
|
+
If you run it, an [issue](https://github.com/aditi-p31/cache-divergence-study/issues) with what you saw is welcome.
|
|
123
|
+
|
|
124
|
+
Two server processes can also differ for other reasons. To check that, start
|
|
125
|
+
both with prefix caching off and run with `--skip-hit-check`. Every probe
|
|
126
|
+
should come out the same.
|
|
127
|
+
|
|
128
|
+
Options: `--probes` reads your own conversations (next section), `--n` sets
|
|
129
|
+
the number of probes (default: all), `--max-tokens` the answer length
|
|
130
|
+
(default 256), `--out` the JSON report path. If your server caches in blocks or
|
|
131
|
+
pages larger than 32 tokens, set `--hit-tolerance` to that size, or cache hits
|
|
132
|
+
can look too short. With large pages, also ask for longer turn-1 answers, so
|
|
133
|
+
the hit can reach past the prompt.
|
|
134
|
+
|
|
135
|
+
Models that think before answering need thinking turned off for this check.
|
|
136
|
+
Their templates often drop past thinking from the history, which shortens the
|
|
137
|
+
cache hit, and the tool marks those probes as inconclusive.
|
|
138
|
+
|
|
139
|
+
## Run it on your own conversations
|
|
140
|
+
|
|
141
|
+
The built-in conversations are short and generic. To check the kind of
|
|
142
|
+
prompts your application sends, put your own in a JSON Lines file, one
|
|
143
|
+
conversation per line, and pass it with `--probes`:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
llm-cachecheck --base-url http://127.0.0.1:8080 --probes my_conversations.jsonl
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Each line has one of two forms
|
|
150
|
+
([example file](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck/examples/conversations_example.jsonl)):
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{"id": "returns-1", "turn1": "A customer wants to return shoes bought 40 days ago. ...", "turn2": "Rewrite the reply so it offers store credit instead."}
|
|
154
|
+
{"id": "sql-1", "messages": [{"role": "system", "content": "You are a careful SQL assistant."}, {"role": "user", "content": "Write a query for the ten largest orders."}], "followup": "Change it so it only counts orders created in 2025."}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- `turn1` is the first user message and `turn2` the follow-up.
|
|
158
|
+
- In the longer form, `messages` is the conversation up to and including the
|
|
159
|
+
turn-1 user message, so it can carry your system prompt and earlier turns.
|
|
160
|
+
`followup` is the turn-2 user message.
|
|
161
|
+
- `id` is optional and is copied into the report.
|
|
162
|
+
- Text messages only, with the roles system, developer, user and assistant.
|
|
163
|
+
Tool calls and images are not supported.
|
|
164
|
+
|
|
165
|
+
The tool generates the turn-1 answer itself and builds turn 2 on it, as with
|
|
166
|
+
the built-in conversations, and it always decodes greedily. A file that holds
|
|
167
|
+
one JSON array of these objects also works.
|
|
168
|
+
|
|
169
|
+
Let turn 1 ask for an answer of a few sentences. The tool counts a cache hit
|
|
170
|
+
only when it reaches past the turn-1 prompt into the answer. A server that
|
|
171
|
+
caches in blocks (vLLM uses 16 tokens by default) can stop the hit at the
|
|
172
|
+
prompt when the answer is short, and the probe is then inconclusive.
|
|
173
|
+
|
|
174
|
+
The JSON report saves every answer, so keep it private if your conversations
|
|
175
|
+
are. The printed result block holds no conversation text: only the counts
|
|
176
|
+
and the run settings, including the model name.
|
|
177
|
+
|
|
178
|
+
## Reading the result
|
|
179
|
+
|
|
180
|
+
A real run on Apple Silicon (Metal) with Qwen2.5-3B-Instruct at Q4_K_M,
|
|
181
|
+
llama.cpp b12138, `-np 1 --cache-ram 0`, default options:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
probe 1/20: same
|
|
185
|
+
...
|
|
186
|
+
probe 10/20: DIFFERENT (from character 578)
|
|
187
|
+
...
|
|
188
|
+
probe 13/20: DIFFERENT (from character 235)
|
|
189
|
+
...
|
|
190
|
+
probe 15/20: DIFFERENT (from character 376)
|
|
191
|
+
...
|
|
192
|
+
3 of 20 valid probes gave a different turn-2 answer on the cache path.
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
All 20 probes were valid. On every probe the server reported that the cache
|
|
196
|
+
hit covered all of turn 1 and its answer, and that the recompute path reused
|
|
197
|
+
nothing. The full report (made with a pre-release build of 0.2.0) is in
|
|
198
|
+
[`examples/qwen2.5-3b-q4km_metal_b12138.json`](https://github.com/aditi-p31/cache-divergence-study/blob/main/cachecheck/examples/qwen2.5-3b-q4km_metal_b12138.json).
|
|
199
|
+
It took about 15 minutes on a laptop.
|
|
200
|
+
|
|
201
|
+
| Exit status | Meaning |
|
|
202
|
+
|---|---|
|
|
203
|
+
| 0 | No valid probe differed, and at least 80% of probes were valid |
|
|
204
|
+
| 1 | At least one valid probe differed |
|
|
205
|
+
| 2 | Inconclusive: too few valid probes, or a request failed |
|
|
206
|
+
|
|
207
|
+
A usage error (a bad or conflicting option) also exits with status 2, before
|
|
208
|
+
any request is sent and without writing a report.
|
|
209
|
+
|
|
210
|
+
The JSON report keeps every answer, the finish reasons, token counts, cached
|
|
211
|
+
token counts and the server build.
|
|
212
|
+
|
|
213
|
+
The rate is specific to these short probes. It is not comparable to the
|
|
214
|
+
episode rates below, where one agent episode chains about ten requests.
|
|
215
|
+
|
|
216
|
+
## What to do about a difference
|
|
217
|
+
|
|
218
|
+
For runs that must repeat, turn prefix reuse off and record that setting with
|
|
219
|
+
your results:
|
|
220
|
+
|
|
221
|
+
- llama.cpp: `"cache_prompt": false` per request, or `--no-cache-prompt` for
|
|
222
|
+
the server
|
|
223
|
+
- vLLM: `--no-enable-prefix-caching`
|
|
224
|
+
- SGLang: `--disable-radix-cache`
|
|
225
|
+
|
|
226
|
+
A difference is not a bug in your model. It shows that the answer depends on
|
|
227
|
+
the server's cache state as well as on the request.
|
|
228
|
+
|
|
229
|
+
## Share your result
|
|
230
|
+
|
|
231
|
+
At the end of a run the tool prints a short result block. It leaves out your
|
|
232
|
+
server URL and any key. If you would like to share it, open a
|
|
233
|
+
[result issue](https://github.com/aditi-p31/cache-divergence-study/issues/new?template=cachecheck-result.yml)
|
|
234
|
+
and paste the block. Results where no probe differed are just as useful.
|
|
235
|
+
|
|
236
|
+
## Background and citation
|
|
237
|
+
|
|
238
|
+
`cachecheck` adapts the paired cache-on and cache-off design of the study in
|
|
239
|
+
[cache-divergence-study repository](https://github.com/aditi-p31/cache-divergence-study).
|
|
240
|
+
The study used Qwen2.5-7B-Instruct on llama.cpp with greedy
|
|
241
|
+
decoding, 80 BFCL multi-turn agent episodes and the host-memory prompt cache
|
|
242
|
+
off. The share of episodes whose trajectory changed between a cache hit and a
|
|
243
|
+
recomputation was 37.4% with F16 weights and 82.5% with Q4_K_M weights, as a
|
|
244
|
+
mean over ten server histories.
|
|
245
|
+
|
|
246
|
+
If `cachecheck` helps your work, please cite the paper:
|
|
247
|
+
|
|
248
|
+
```bibtex
|
|
249
|
+
@article{patodiya2026samerequest,
|
|
250
|
+
title = {Same Request, Different Answer: Quantization Amplifies
|
|
251
|
+
Cache-Induced Divergence in LLM Serving},
|
|
252
|
+
author = {Patodiya, Aditi},
|
|
253
|
+
journal = {IEEE Access},
|
|
254
|
+
year = {2026},
|
|
255
|
+
doi = {10.1109/ACCESS.2026.3749922},
|
|
256
|
+
note = {Early Access},
|
|
257
|
+
url = {https://doi.org/10.1109/ACCESS.2026.3749922}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
MIT License. See `LICENSE`.
|