servebench 1.0.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,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,323 @@
1
+ Metadata-Version: 2.4
2
+ Name: servebench
3
+ Version: 1.0.0
4
+ Summary: Load-test any OpenAI-compatible LLM endpoint: TTFT, TPOT, ITL, throughput, goodput, charts.
5
+ Keywords: llm,benchmark,inference,serving,ttft,openai,vllm,ollama,llama.cpp
6
+ Author: Prajwal Pujari
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Classifier: Topic :: System :: Benchmark
13
+ Requires-Dist: typer>=0.12
14
+ Requires-Dist: rich>=13
15
+ Requires-Dist: pydantic>=2.6
16
+ Requires-Dist: matplotlib>=3.8
17
+ Requires-Dist: aiohttp>=3.10
18
+ Requires-Dist: pyyaml>=6.0.3
19
+ Requires-Dist: nvidia-ml-py>=13.615.71
20
+ Requires-Dist: transformers>=4.40 ; extra == 'hf'
21
+ Requires-Dist: fastapi>=0.110 ; extra == 'mock'
22
+ Requires-Dist: uvicorn>=0.29 ; extra == 'mock'
23
+ Requires-Python: >=3.11
24
+ Project-URL: Homepage, https://github.com/Prajwal7-2002/servebench
25
+ Project-URL: Issues, https://github.com/Prajwal7-2002/servebench/issues
26
+ Provides-Extra: hf
27
+ Provides-Extra: mock
28
+ Description-Content-Type: text/markdown
29
+
30
+ # servebench
31
+
32
+ [![CI](https://github.com/Prajwal7-2002/servebench/actions/workflows/ci.yml/badge.svg)](https://github.com/Prajwal7-2002/servebench/actions/workflows/ci.yml)
33
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
34
+ ![Python](https://img.shields.io/badge/python-3.11%2B-blue)
35
+
36
+ Load-test any OpenAI-compatible LLM endpoint (Ollama, llama.cpp server, vLLM, SGLang, your
37
+ own engine) and get the numbers that matter for serving: **TTFT, TPOT, inter-token latency,
38
+ throughput and goodput**, at p50/p90/p99, over a sweep of load levels, with charts and
39
+ side-by-side comparison of runs.
40
+
41
+ No GPU or engine install is needed on the client: it is one async Python process talking HTTP.
42
+
43
+ ## Quickstart
44
+
45
+ Uses [uv](https://docs.astral.sh/uv/) (or pipx/pip). PyPI release coming; for now, from a
46
+ clone:
47
+
48
+ ```bash
49
+ uv tool install --from . servebench # puts `servebench` on your PATH
50
+ servebench quick # finds your local server, ~1 minute
51
+ ```
52
+
53
+ `quick` looks for Ollama, llama.cpp, LM Studio, vLLM and SGLang on their usual ports, picks
54
+ the loaded model, measures, and answers in plain language. A real run (Ollama with
55
+ `OLLAMA_NUM_PARALLEL=4` on a laptop):
56
+
57
+ ```
58
+ servebench quick: Ollama 0.35.1 at http://127.0.0.1:11434/v1, model qwen2.5:0.5b
59
+ ...
60
+ Ollama 0.35.1, qwen2.5:0.5b on AMD Ryzen 5 6600H, 15.3 GB RAM, NVIDIA GeForce RTX 3050 6GB Laptop GPU
61
+ One user: first token in 43 ms, then 203 tokens/s
62
+ Capacity: 4 users at once (first token < 500 ms and >= 20 tokens/s each, for 90% of requests)
63
+ At 4 users: 361 tokens/s in total, 136 tokens/s each, first token p99 161 ms
64
+ ```
65
+
66
+ With default Ollama settings it also explains *why* the limit is where it is ("requests
67
+ queue: the server works on about 1 at once") and suggests the fix. Then
68
+ `servebench share <run.json>` prints a Markdown table to paste into a post or issue and
69
+ makes a result card:
70
+
71
+ ![servebench result card: 4 users at once, 203 tokens/s for one user, 43 ms to first token](results/quick-ollama-np4-card.png)
72
+
73
+ Full control:
74
+
75
+ ```bash
76
+ # sweep a running server, e.g. Ollama (on Windows use 127.0.0.1, not localhost)
77
+ servebench run --url http://127.0.0.1:11434/v1 --model qwen2.5:0.5b \
78
+ --concurrency 1,2,4,8 --input-len 256 --output-len 128 --out runs/ollama.json
79
+
80
+ # how many concurrent users fit your latency targets?
81
+ servebench find-capacity --url http://127.0.0.1:11434/v1 --model qwen2.5:0.5b \
82
+ --out runs/ollama-cap.json
83
+
84
+ # compare multiple runs side by side, with charts and GPU telemetry
85
+ servebench compare-models runs/model-q4.json runs/model-fp16.json
86
+
87
+ # CI performance regression gate (fails with exit code 1 on regressions)
88
+ servebench check candidate.json --baseline baseline.json \
89
+ --max-ttft-degradation-pct 15.0 --max-throughput-degradation-pct 10.0
90
+
91
+ # generate interactive HTML report
92
+ servebench report runs/ollama.json runs/llamacpp.json --out report.html
93
+ ```
94
+
95
+ ### Python API
96
+
97
+ Use Servebench programmatically in your own evaluation harnesses or notebooks:
98
+
99
+ ```python
100
+ import servebench
101
+
102
+ res = servebench.benchmark(
103
+ url="http://127.0.0.1:8000/v1",
104
+ model="llama-3-8b",
105
+ levels=[1, 2, 4, 8],
106
+ preset="chat",
107
+ gpu_telemetry=True,
108
+ )
109
+ print(f"Peak: {max(lv.summary.output_throughput for lv in res.levels):.1f} tok/s")
110
+
111
+ # Evaluate CI performance regression
112
+ report = servebench.check_regression("candidate.json", "baseline.json")
113
+ assert report.passed, "Serving latency regressed!"
114
+ ```
115
+
116
+ No model at hand? `uv tool install --from . "servebench[mock]"` adds `servebench mock-server`,
117
+ a stand-in server with set timings, so you can try every command without a GPU
118
+ ([see How it's tested](#how-its-tested)).
119
+
120
+ ## Learn LLM inference by measuring it
121
+
122
+ Want to understand how inference engines, batching, and queueing behave under the hood?
123
+ Follow our hands-on guide: **[docs/learn-inference.md](docs/learn-inference.md)**.
124
+
125
+ It walks through 7 reproducible experiments with Ollama:
126
+ 1. **Prefill scaling**: Why TTFT scales linearly with prompt length (~100 ms / 1k tokens).
127
+ 2. **Decode bottleneck**: Why TPOT is constant (~4.9 ms/tok, ~200 tok/s) regardless of prompt length.
128
+ 3. **Batching & slots**: Why `OLLAMA_NUM_PARALLEL=4` doubles throughput to 385 tok/s, and why 8 slots plateaus.
129
+ 4. **The p99 latency knee**: Why p50 hides queues while p99 spikes 5× (197 ms → 943 ms) when slots fill.
130
+ 5. **Little's Law in practice**: Proving $L = \lambda W$ with real serving numbers.
131
+ 6. **Closed vs. open loop**: Why concurrency sweeps mask production breaking points.
132
+ 7. **Benchmarking pitfalls**: Prompt cache hits, connection resets, and warm-up bias.
133
+
134
+ ## Results
135
+
136
+ ### Ollama on a laptop GPU: what `OLLAMA_NUM_PARALLEL` buys
137
+
138
+ Qwen2.5-0.5B (Q4_K_M) on Ollama 0.35.1, fully on an RTX 3050 6 GB laptop GPU. Synthetic
139
+ prompts of 256 tokens, 128 output tokens, 64 requests per level, `find-capacity` with
140
+ SLO TTFT < 500 ms and TPOT < 50 ms for 90% of requests. Only `OLLAMA_NUM_PARALLEL` changes.
141
+
142
+ ![TTFT, TPOT, throughput and E2E latency vs concurrent requests for 1, 4 and 8 Ollama slots](results/ollama-parallel_files/sweep.png)
143
+
144
+ | Slots | Capacity (concurrent requests) | Completed req/s at capacity | Output tok/s | TTFT p50 / p99 | TPOT p50 | E2E p50 |
145
+ |---|---|---|---|---|---|---|
146
+ | 1 | 1 | 1.51 | 190 | 45 / 59 ms | 4.9 ms | 0.67 s |
147
+ | 4 | 4 | 3.16 | 385 | 101 / 197 ms | 9.4 ms | 1.30 s |
148
+ | 8 | 8 | 3.30 | 400 | 224 / 386 ms | 16 ms | 2.22 s |
149
+
150
+ - **Capacity follows the slot count exactly.** With one slot, a second request waits for the
151
+ whole first answer (TTFT 688 ms at c=2), so only one user fits the SLO.
152
+ - **The GPU saturates at about 400 tok/s with 4 streams.** Going from 4 to 8 slots doubles
153
+ the users who fit the SLO, but finishes only 4% more requests per second; each answer
154
+ takes 71% longer (TPOT 9.4 → 16 ms) because the same GPU time is split more ways.
155
+ - So for this model and GPU, 4 slots maximise work done; 8 slots only help if you need more
156
+ simultaneous users within the TTFT target and can accept slower answers.
157
+
158
+ Full report with every level: [results/ollama-parallel.md](results/ollama-parallel.md).
159
+ Raw data: `results/ollama-np{1,4,8}.json` (every request's timings). Next: llama.cpp server
160
+ on the same GPU, and vLLM on a free Kaggle T4.
161
+
162
+ ## Metrics
163
+
164
+ | Metric | Meaning |
165
+ |---|---|
166
+ | TTFT | Request sent → first streamed chunk with non-empty content (role-only and empty chunks don't count) |
167
+ | TPOT | (E2E − TTFT) / (output tokens − 1) |
168
+ | ITL | Gaps between consecutive content chunks. Some servers put several tokens in one chunk; the report shows `tok/chunk` so you can tell |
169
+ | E2E | Request sent → end of stream |
170
+ | req/s, out tok/s | Completed requests and output tokens per second of wall time for the level |
171
+ | Goodput | Requests/s that meet the SLOs (default TTFT < 500 ms and TPOT < 50 ms; change with `--slo-ttft-ms`/`--slo-tpot-ms`, also at report time) |
172
+ | Errors | Failed requests grouped by kind (`HTTP 500`, `timeout`, `ConnectError`, ...) |
173
+
174
+ Token counts come from the server's `usage` (requested with `stream_options.include_usage`).
175
+ If the server sends none, servebench counts with a Hugging Face tokenizer if you pass
176
+ `--tokenizer Qwen/Qwen2.5-0.5B-Instruct` (install the `hf` extra: `servebench[hf]`), or falls back to
177
+ counting chunks. Every request records which source was used, and the report warns about
178
+ chunk counts.
179
+
180
+ ## Load modes
181
+
182
+ - **Concurrency sweep** (`--concurrency 1,2,4,8`, closed loop): exactly N requests in flight
183
+ until the level's requests are done. Shows how latency degrades as load grows.
184
+ - **Request rate** (`--rate 0.5,1,2,inf`, open loop): Poisson arrivals at R req/s, whether or
185
+ not earlier requests finished; `inf` sends everything at once, `--max-concurrency` caps it.
186
+ This is how you find the point where a server falls over.
187
+
188
+ Each level gets `--num-requests` requests (default `max(16, 4N)` for concurrency, 64 for
189
+ rate). Two warm-up requests run first and are not measured. Each run uses a new random
190
+ prompt seed (printed and saved in run.json): with a fixed seed, a second run sends the same
191
+ prompts and a server with prompt caching skips prefill (on Ollama TTFT dropped from 43 ms to
192
+ 20 ms), so you would measure the cache. Pass `--seed N` to repeat a run exactly.
193
+
194
+ ## Find capacity
195
+
196
+ `find-capacity` answers "how much load can this server take within my latency targets?"
197
+ with one number:
198
+
199
+ Real run: Ollama with `OLLAMA_NUM_PARALLEL=4` on the RTX 3050 from the results above.
200
+
201
+ ```bash
202
+ servebench find-capacity --url http://127.0.0.1:11434/v1 --model qwen2.5:0.5b \
203
+ --max 64 --num-requests 64 --out results/ollama-np4.json
204
+ ```
205
+ ```
206
+ c=1 64 ok, 0 failed, 188.5 tok/s, TTFT p50 45 ms, 100% in SLO -> pass
207
+ c=2 64 ok, 0 failed, 274.0 tok/s, TTFT p50 71 ms, 100% in SLO -> pass
208
+ c=4 64 ok, 0 failed, 384.6 tok/s, TTFT p50 101 ms, 100% in SLO -> pass
209
+ c=8 64 ok, 0 failed, 375.7 tok/s, TTFT p50 1424 ms, 6% in SLO -> FAIL
210
+ c=6 64 ok, 0 failed, 405.2 tok/s, TTFT p50 566 ms, 47% in SLO -> FAIL
211
+ c=5 64 ok, 0 failed, 369.8 tok/s, TTFT p50 367 ms, 72% in SLO -> FAIL
212
+ ...
213
+ Capacity: 4 concurrent requests. At 4, 100% met the SLO; at 5, only 72%.
214
+ SLO: TTFT < 500 ms and TPOT < 50 ms for 90% of requests.
215
+ ```
216
+
217
+ - A level **passes** when no request failed and at least `--target` (default 90%) of the
218
+ requests met `--slo-ttft-ms` and `--slo-tpot-ms`.
219
+ - The search doubles the load from `--start` (1, 2, 4, 8, ...) until a level fails, then
220
+ binary-searches between the last pass and the first fail until the gap is within
221
+ `--tolerance` (10%) of the passing value. It never goes above `--max` (256).
222
+ - `--mode rate` searches Poisson arrival rates (req/s) instead of concurrency.
223
+ - Every level it tried is saved, so `servebench report` charts it like any sweep.
224
+ - Each level is measured once and the search assumes latency grows with load. Near the
225
+ limit a noisy server can pass a level above one that failed; rerun to confirm.
226
+
227
+ ## Prompts
228
+
229
+ - `--prompts synthetic` (default): `--input-len` tokens of random common words, a different
230
+ prompt per request so prefix caches cannot reuse one prompt's work for the next.
231
+ `--ignore-eos` (on by default) asks the server to always generate `--output-len` tokens;
232
+ vLLM, SGLang and llama.cpp honour it, and servers that don't know it ignore it, so check
233
+ `out tok/s` against `--output-len` when comparing.
234
+ - `--prompts dolly`: real prompts from
235
+ [databricks-dolly-15k](https://huggingface.co/datasets/databricks/databricks-dolly-15k)
236
+ (CC BY-SA 3.0, © Databricks), downloaded once (12.8 MB) into `~/.cache/servebench` (set
237
+ `SERVEBENCH_CACHE` to move it). Not bundled with this repo. `--input-len` caps the prompt
238
+ length in words.
239
+ - `--prompts my_prompts.jsonl` (`{"prompt": ...}` per line) or a `.txt` file, one prompt per
240
+ line.
241
+
242
+ ## How it works
243
+
244
+ ```
245
+ prompts.py ──► load.py ─────────────► client.py ──HTTP/SSE──► server
246
+ synthetic concurrency workers one streaming request,
247
+ dolly or Poisson arrivals perf_counter per chunk
248
+ file + event-loop lag check
249
+ │
250
+ ▼
251
+ metrics.py ──► run.json ──► report.py
252
+ percentiles, terminal table,
253
+ goodput PNG charts, MD / HTML
254
+ ```
255
+
256
+ - One `asyncio` process with `aiohttp` (the HTTP client vLLM's and SGLang's own serving
257
+ benchmarks use). Timings use `time.perf_counter()`. servebench started on `httpx`; profiling
258
+ showed its connection pool re-checks every connection on every request, so at 128
259
+ concurrent requests the *client* capped out at ~45 req/s and inflated TTFT from 100 ms to
260
+ 1.6 s. With aiohttp the same test reaches ~400 req/s with TTFT 135 ms.
261
+ - Every request's chunk arrival times are saved in `run.json`, so reports can be recomputed
262
+ (for example with different SLOs) without rerunning anything.
263
+ - The client checks itself: a background task measures how long a ready callback waits for
264
+ the event loop. If the p99 is above 10 ms, the report warns that the client, not the server,
265
+ may be the bottleneck.
266
+ ## How it's tested
267
+
268
+ You can't check a benchmark's numbers against a real model, because nobody knows the true
269
+ answer in advance. So the tests use `servebench mock-server`: a FastAPI server that speaks the
270
+ same OpenAI streaming API but, instead of running a model, sends placeholder tokens with
271
+ timings we set exactly (TTFT, time per token, tokens per chunk, slowdown under load). The
272
+ tests then check servebench measures what was set:
273
+
274
+ - TTFT and TPOT within a few ms of the set delays, on Linux and Windows (CI on both)
275
+ - role-only and empty chunks don't count as the first token; several tokens per chunk
276
+ - N concurrent requests really means N in flight (the mock counts them); Poisson arrival rates
277
+ - connections are reused (the mock counts TCP connections: 4 requests, 1 connection)
278
+ - `find-capacity` finds the known limit: with TTFT = 80 ms × requests in flight and a 360 ms
279
+ target, capacity must be exactly 4
280
+
281
+ Example: a mock set to TTFT 80 ms, 15 ms per token and 15% slowdown per extra request in
282
+ flight should show TPOT 15 × (1 + 0.15 × 15) ≈ 49 ms at 16 concurrent requests, and
283
+ servebench measures 49 ms ([examples/report.md](examples/report.md)). The mock is a test
284
+ tool only; every number under [Results](#results) comes from a real server.
285
+
286
+ ## Limitations
287
+
288
+ - One client process. Measured on a laptop (Ryzen 5 6600H, Windows) against the mock
289
+ server: accurate to ~128 concurrent requests / ~400 req/s / ~12k streamed tokens/s (client
290
+ loop lag p99 under 10 ms); at 256 concurrent it starts to work hard (lag ~20 ms). That
291
+ covers what a single GPU serves; it is not a multi-machine load generator. The lag check
292
+ warns when the client is struggling.
293
+ - The first requests of each level include opening TCP connections, as a real client would.
294
+ - TPOT and ITL are only as good as the token counts: without `usage` or `--tokenizer`, a
295
+ server that sends several tokens per chunk will look slower per token than it is.
296
+ - Measures what the client sees over HTTP, including network and serialization, not the
297
+ engine's internal timings.
298
+ - Text endpoints only (chat and completions); no images, tool calls or embeddings.
299
+
300
+ ## Related tools
301
+
302
+ servebench does not try to replace the bigger tools; use them if they fit:
303
+ [vLLM's `vllm bench serve`](https://docs.vllm.ai/),
304
+ [NVIDIA GenAI-Perf](https://github.com/triton-inference-server/perf_analyzer),
305
+ [guidellm](https://github.com/vllm-project/guidellm),
306
+ [LLMPerf](https://github.com/ray-project/llmperf).
307
+ servebench is a small install that works against any engine, makes clean multi-run
308
+ comparisons, and is used to measure the rest of this series of projects on free hardware.
309
+
310
+ ## Development
311
+
312
+ ```bash
313
+ uv sync # creates .venv and installs the package + dev tools
314
+ uv run ruff check src tests
315
+ uv run pytest -q
316
+ ```
317
+
318
+ No need to create or activate a virtualenv yourself; `uv run` uses the project's `.venv`.
319
+
320
+ ## License
321
+
322
+ Apache 2.0. The dolly-15k dataset, if you use it, is CC BY-SA 3.0 and is downloaded from
323
+ Hugging Face at runtime, not distributed here.