A pull request can pass every test and still make inference slower. Putting a benchmark in CI is the obvious fix, and it goes wrong in three ways:
- false alarms: GPU benchmarks are noisy, so a gate on two numbers fails changes that did nothing, and the team turns it off,
- missed regressions: with two or three runs, a real 12% slowdown can land inside the noise and pass,
- apples to oranges: the two sides ran on a different GPU, model or load, and the difference is the setup, not the change.
inference-doctor compares repeated baseline and candidate runs and returns PASS or
FAIL only when the data can support it. Otherwise it returns INCONCLUSIVE and says
why. It reads the files vllm bench serve, AIPerf, GuideLLM and SGLang already write.
A real H100 run (examples/inference-ci-reliability-001), rendered from the CLI by
tools/render_readme_media.py. Throughput is 4.19% lower, but one noisy baseline block leaves an interval
from -3.33% to +11.17%. Six runs cannot rule out a 10% regression, so there is no merge recommendation.
How it decides
- Is the comparison valid? Same GPU, model and benchmark parameters on every run, no
failed requests, the metric present everywhere, and at least 3 runs per side. If not,
the result is
INCONCLUSIVEand the reason names the check that failed. - How big is the change, and how sure are we? A 90% confidence interval for the regression (positive is worse), from a Welch t interval on the log ratio of means.
- Verdict.
FAILwhen the whole interval is at or past the threshold (default 10%),PASSwhen the whole interval is below it,INCONCLUSIVEwhen it contains the threshold. More runs narrow the interval.
Outliers are flagged but never removed: a bad run widens the interval instead of being hidden. Details and references: measurement quality.
Quick start
Python 3.11 or newer. No GPU: the example results are checked in, so the clone is only for them.
pip install inference-doctor
git clone https://github.com/dundysm/inference-doctor.git && cd inference-doctor
inference-doctor compare-runs \
--baseline examples/quickstart/baseline \
--candidate examples/quickstart/candidate \
--metric texts_per_second
PASS: the candidate is 2% slower, and the interval, +1.08% to +2.92%, is well below 10%.
Point --candidate at examples/quickstart/regressed-candidate to see a FAIL.
Use your benchmark's own output
No converter needed. Put each side's runs in a directory and point compare-runs at it:
inference-doctor compare-runs \
--baseline results/main \
--candidate results/my-branch \
--metric output_token_throughput --metric ttft_p99_ms
Repeat --metric to gate on several at once: the result is FAIL if any metric fails,
INCONCLUSIVE if none fails but one cannot be decided, and PASS only when all pass.
| Tool | What it reads |
|---|---|
vllm bench serve --save-result |
each result file, or each line with --append-result |
| NVIDIA AIPerf / GenAI-Perf | each profile_export_aiperf.json |
| GuideLLM | benchmarks.json, one load point of the sweep (--guidellm-benchmark) |
SGLang bench_serving |
each line of its .jsonl output |
| anything else | the small normalized format |
Metrics: output, total and request throughput, and TTFT, TPOT, ITL and end-to-end latency
at mean, median, p95 and p99. Most tools do not record the GPU; pass --gpu to label it,
or record it in the results (for vLLM, --metadata gpu=...). The
input formats page lists every field read from each tool.
examples/vllm-bench-serve is a small example in
vLLM's format where throughput passes and TTFT fails, so gating on both catches what a
throughput-only gate would let through.
In GitHub Actions
Run your benchmark on your own GPU runner, then gate on the results:
- name: Gate inference performance
uses: dundysm/inference-doctor@v1
with:
baseline: results/main
candidate: results/my-branch
metric: output_token_throughput, ttft_p99_ms
min-repetitions: "5"
The Action writes a job summary with the verdict, the interval and the reason, and exposes
them as outputs. FAIL, INCONCLUSIVE and errors fail the step, so noisy evidence cannot
approve a change. inference-doctor does not provision GPUs or run the benchmark. See
GitHub Actions.
How much to trust it
Simulated with known truth, at default settings, from gate calibration (4,000 trials per row):
| Runs per side, noise (CV) | No change | True 5% drop | True 15% drop | True 20% drop |
|---|---|---|---|---|
| 3 runs, 4% | 78% PASS, 0% FAIL | 34% PASS, 0.1% FAIL | 0% PASS, 37% FAIL | 0% PASS, 85% FAIL |
| 5 runs, 4% | 98% PASS, 0% FAIL | 60% PASS, 0% FAIL | 0% PASS, 65% FAIL | 0% PASS, 99% FAIL |
| 10 runs, 4% | 100% PASS, 0% FAIL | 90% PASS, 0% FAIL | 0% PASS, 92% FAIL | 0% PASS, 100% FAIL |
The rest of each row is INCONCLUSIVE. Right at the 10% threshold, each wrong verdict
happens about 5% of the time; further from it, at most 0.5%. Noise costs decisions, not
correctness. Use 5 or more runs per side. These rates assume independent, roughly normal
runs; interleave baseline and candidate runs
to stay close to that.
Commands
| Command | What it does |
|---|---|
compare-runs |
The gate: repeated baseline and candidate runs, one or more metrics, PASS, FAIL or INCONCLUSIVE. |
compare |
Deprecated. One normalized file against another, every metric. A single run per side cannot measure noise; use compare-runs. See upgrade guard. |
diagnose |
Experimental. Reads a live vLLM server's Prometheus metrics and reports queue, prefill, decode and KV-cache pressure. Read-only. Exits 1 on a critical finding (--fail-on changes that). See example reports. |
compare-runs options: --metric (repeatable), --regression-percent (10), --confidence (0.90),
--min-repetitions (3), --stability-cv-percent (an optional CV ceiling, off),
--format (auto), --gpu, --model, --guidellm-benchmark and --json.
Exit codes, the same for every command: 0 PASS, 1 FAIL (for diagnose, a
finding at or above --fail-on), 2 INCONCLUSIVE (or UNSTABLE for compare), 3 error
with no decision (bad arguments or input, an unreachable Prometheus, or an internal
error). inference-doctor --version prints the
version.
What has been measured
- The decision rule: the calibration table above, regenerated by
tools/gate_simulation.pyand checked bytests/test_gate.pyon every CI run. - One real GPU run:
examples/inference-ci-reliability-001, six runs per side of BAAI/bge-m3 on one H100 across two vLLM versions. The honest answer wasINCONCLUSIVE, and it still is. - The format readers: tested against records built from each tool's source code. They have not yet been run on a large set of real files from each tool, so please report any file they misread.
- Not yet: a gate running in another project's CI. See STATUS.md.
Help test it
The most useful thing right now is a real change: run compare-runs on results from your
own benchmark and open a Benchmark feedback issue saying what it decided, whether that
was right, and what was confusing. Redact prompts, credentials, private model names and
customer data.
Contributing
See CONTRIBUTING.md. The changelog is CHANGELOG.md.
The experiments that informed the design live under experiments/.
python -m pip install -e ".[dev]"
python -m pytest -q
ruff check . && ruff format --check . && mypy
License
Apache-2.0. See LICENSE.
Metadata
Release files for inference-doctor 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| inference_doctor-0.4.0.tar.gz | 74.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| inference_doctor-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 117.5 kB
Release files / inference_doctor-0.4.0.tar.gz
| Download URL | inference_doctor-0.4.0.tar.gz |
|---|---|
| Size | 74.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0f6e6e15d56562c50fcb631958d966de6fee0bc2edb5c5e3ffb73e0d06c32522
|
|
BLAKE2b-256 checksum How to use checksums |
1a80c59d046569f1120099a43fa30abf3e4b46acf39e286d78556704ab59f956
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / inference_doctor-0.4.0-py3-none-any.whl
| Download URL | inference_doctor-0.4.0-py3-none-any.whl |
|---|---|
| Size | 43.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
84a3a789448647204cf4e6460121470929f59fae3a12c5777c98a33821e79358
|
|
BLAKE2b-256 checksum How to use checksums |
ae76066617a1f355bd34f933f1425b7e468b7d30b4b310d195e74455d19f2f08
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency log