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.
@@ -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`.