Wingsmith
Measurement tools

hgrbench

Install, commands, flags, plan file keys, harnesses and outputs for the hgrbench load-test runner.

hgrbench runs load generators against an OpenAI-compatible endpoint, one at a time, from containers on your machine, and writes one HTML report. It does not write to the control plane. For a walk-through see Benchmark an endpoint. For how to judge the numbers see Measurement method.

Install

You need Python 3.11 or later and a container runtime, because the load generators run as containers. hgrbench has no Python dependencies. Nothing is published yet, so install from a checkout of the repository.

pip install ./libs/benchmarking     # provides the hgrbench command
python3 -m hgrbench plans           # or, from libs/benchmarking with no install

Tokenizers are downloaded from Hugging Face and cached in --hf-cache, so the first run needs network access to it.

Commands

CommandWhat it does
hgrbench run <plan>Take the measurements a plan describes, then write and open the report.
hgrbench report [<run>]Re-render a stored run as HTML without touching the endpoint. Latest run by default.
hgrbench harnessesList each harness, its image, how many canonical fields it reads, and whether it can walk a ladder.
hgrbench plansList the plans shipped with the package, with cell counts and request totals.

<plan> is a shipped plan by name (smoke, deep) or a path to your own TOML file.

run flags

FlagDefaultEffect
--results.local/benchDirectory where runs are kept, relative to the working directory.
--harnessall in the planOnly this harness. Repeatable.
--modelall in the planOnly this model. Repeatable.
--hardoffInclude the saturation search. It can degrade a live endpoint.
--dry-runoffPrint every command, run none, keep nothing.
--timeout1800Per-measurement ceiling in seconds.
--hf-cache~/.cache/huggingfaceTokenizer cache mounted into the containers.
--no-openoffDo not open the report when done. Opening is only automatic on macOS.

report flags

FlagEffect
<run>A run directory or its suite.json.
--resultsWhere to look for the latest run. Default .local/bench.
--outWhere to write the page. Default report.html in the run directory.
--no-openDo not open the page.

Shipped plans

PlanTimeMatrixLaddersSaturation search
smokeabout 10 minutes1 repeat, c1 and c4, 1000 input and 128 output tokensvllm-sweep at 1, 4off
deepabout 2 hours3 repeats, c1 to c16, 2000 input and 256 output tokensvllm-sweep and guidellm at 1 to 32on, needs --hard

Both plans use aiperf, vllm-bench and guidellm in the matrix and measure gpt-oss-120b and gpt-oss-20b. Edit a copy for your own models.

Plan file

A plan is TOML. The endpoint, models, load points and SLA come from the file, not from flags, so what was measured is reviewable in a diff.

KeyMeaning
name, descriptionLabel for the run. name defaults to the file stem.
repeatsRepeats of each matrix cell. Default 1.
cooldown_secondsPause between cells. Default 10.
input_tokens, output_tokensPrompt and generation length. Defaults 2000 and 256. A model entry can override both.
harnessesMatrix harnesses. Default ["aiperf"].
[endpoint] urlBase URL as the containers see it. Default http://host.docker.internal:8080.
[endpoint] probe_urlSame endpoint as the host sees it. Defaults to url with host.docker.internal replaced by localhost. Before the first cell, run sends GET /v1/models here and stops if it does not answer 200.
[endpoint] token_envName of the environment variable holding the bearer token. Default HANGAR_API_KEY.
[[models]]name is the served model name. tokenizer is a Hugging Face repository and is required.
[[load]]concurrency, plus requests, seconds or request_rate. request_rate is only honoured by open-loop harnesses.
[sweep]enabled, harnesses, ladder, requests_per_rung, seconds_per_rung, num_runs.
[knee]Saturation search: enabled, harness, field, limit, error_limit, ladder, requests_per_rung, seconds_per_rung.

[endpoint] accepts only url, probe_url and token_env. A token cannot be written into a plan.

Harnesses

HarnessSourceLoad model
aiperfNVIDIA AIPerf. The pinned image is the one the plane’s benchmarks module uses.Closed loop, N in flight
vllm-benchvLLM bench serve, from a client-only image built locally.Open or closed loop
vllm-sweepvLLM bench sweep serve. Writes upstream’s own summary.csv and plots.Ladder in one process
guidellmGuideLLM.Ladder in one process

Only vllm-sweep and guidellm can be listed under [sweep]. Run hgrbench harnesses for the pinned digests and versions on your machine.

Stages

  1. Matrix. Every model, load point, harness and repeat, one cell at a time.
  2. Ladders. Harnesses in [sweep] walk a list of concurrencies in one process.
  3. Saturation search. Climbs the [knee] ladder until field exceeds limit or more than error_limit requests fail, then reports the last rung that held. It runs only when the plan enables it and you pass --hard.

Output

Each run is a directory named <UTC timestamp>-<plan name> under --results.

PathContent
suite.jsonPlan source, endpoint, harness image digests, every measurement, readings and named misses. Written after every cell.
report.htmlThe rendered report.
One directory per cellThe harness’s own artifacts, verbatim.

suite.json is the only file report reads, so an interrupted run still renders. A correction to a field map is applied with hgrbench report, never by re-running.

Report sections

SectionShows
What this run saysThe headline findings and caveats.
Behaviour under loadLatency and throughput against concurrency, per model.
Do the harnesses agree?Each harness’s own value at the same load point and the spread between them.
Run to runMin, median, max and spread across repeats. With repeats at 1 the section only says there is no spread to show.
Every measurementOne row per cell. A value marked with a degree sign is derived; its formula is in the footnotes.

A field a harness could not report is listed as a named miss, not shown as zero.

Behaviour to know about

BehaviourConsequence
The AIPerf amd64 image hangs under emulation on Apple silicon.Images are pulled by digest for the host architecture.
vllm bench serve exits 0 when every request failed.A cell is graded on completed in the export, not the exit code.
The tokenizer is not the served model name.Every model needs a tokenizer; a model without one is refused.
GuideLLM’s backend validation calls GET /health, which the gateway answers with 400 (with a token) or 401 (without).The runner sets validate_backend=false. A “cannot connect” error from GuideLLM run by hand is this check, not the network.
GuideLLM’s max_requests applies per rung.A four-rung ladder at 20 requests sends 80.
GuideLLM’s own sweep profile saturates by design.It is not wired to a plan key. Plans use an explicit ladder.
The vLLM sweep echoes its full command, token included.Logs are redacted before they are written.
Realised input length differs per harness because of chat templates.Compare the reported input length in the table, not the flag value.
vLLM’s max_concurrent_requests and max_output_tokens_per_s are bucketed by integer second.Neither is read.

Not done yet

  • GuideLLM’s self-calibrating sweep needs its own --hard-style gate before it can be a plan key.
  • vllm bench sweep serve_workload works in the image but is not driven by hgrbench.
  • hgrbench does not submit runs to the control plane.