Skip to content

Commit b332a38

Browse files
Added registry setup for metrics, with accompanying CLI option (#863)
## Summary Creates a registry-based input for metrics. ## Details - The `kind` for the metrics input is `generative`, since it's designed to go with generative benchmarks. - Includes options for `sample_size` and `prefer_response_metrics`. - Example of the arg: `--metrics kind=generative,sample_size=2` ## Test Plan Run a benchmark with a low sample size, and count them in the output. The `extract_conversation.py` script is a good one for this. --- - [x] "I certify that all code in this PR is my own, except as noted below." ## Use of AI - [x] Includes code generated or substantially modified by an AI agent - [x] Includes tests generated or substantially modified by an AI agent > NOTE: the `Generated-by` or `Assisted-by` trailers should be used in git commit messages when code or tests were generated or substantially modified by an AI agent, as described in the project's [`DEVELOPING.md`](https://github.com/vllm-project/guidellm/blob/main/DEVELOPING.md) file. --- # git log commit b4c4335 Author: Jared O'Connell <joconnel@redhat.com> Date: Thu Jun 25 17:36:35 2026 -0400 Added registry setup for metrics, with accompanying CLI option Generated-by: Cursor AI Claude Opus 4.6 Signed-off-by: Jared O'Connell <joconnel@redhat.com> commit c50a786 Author: Jared O'Connell <joconnel@redhat.com> Date: Thu Jun 25 17:53:28 2026 -0400 Added docs for the sample size setting Generated-by: Cursor AI Claude Opus 4.6 Signed-off-by: Jared O'Connell <joconnel@redhat.com> commit 2af6e9f Author: Jared O'Connell <joconnel@redhat.com> Date: Thu Jun 25 18:26:30 2026 -0400 Fix linter errors Signed-off-by: Jared O'Connell <joconnel@redhat.com> --------- Generated-by: Cursor AI Claude Opus 4.6 Signed-off-by: Jared O'Connell <joconnel@redhat.com>
1 parent 0973d88 commit b332a38

8 files changed

Lines changed: 294 additions & 25 deletions

File tree

docs/guides/outputs.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,31 @@ guidellm run \
9494
--output kind=json,path=results/benchmark.json
9595
```
9696

97+
### Controlling Output File Size
98+
99+
Long benchmark runs with thousands of requests can produce large JSON and YAML output files because, by default, every request's full data (prompt text, output text, tool calls) is retained. The `--metrics` option lets you limit how much request data is kept using reservoir sampling, while lightweight stats (latency, token counts, timing) are always preserved for every request.
100+
101+
Use `sample_size` to set the maximum number of requests **per status group** (completed, errored, incomplete) that retain their full data:
102+
103+
| Value | Behavior |
104+
| ----------------- | ---------------------------------------------------------- |
105+
| Not set (default) | Keep full data for all requests |
106+
| `0` | Strip all request data (stats only) |
107+
| `N` (e.g. `100`) | Retain full data for N randomly sampled requests per group |
108+
109+
```bash
110+
# Keep full data for only 100 sampled requests per group
111+
guidellm run \
112+
--backend kind=openai_http,target=http://localhost:8000 \
113+
--profile kind=sweep \
114+
--constraint kind=max_requests,count=10000 \
115+
--data kind=synthetic_text,prompt_tokens=256,output_tokens=128 \
116+
--metrics kind=generative,sample_size=100 \
117+
--output kind=json,path=results/benchmark.json
118+
```
119+
120+
The `--metrics` option also accepts `prefer_response_metrics` (default `true`), which controls whether server-reported token counts are preferred over client-calculated counts when both are available. This rarely needs to be changed.
121+
97122
### Reloading Results
98123

99124
JSON files can be reloaded into Python for further analysis using the `GenerativeBenchmarksReport` class. Below is a sample code snippet for reloading results:

src/guidellm/benchmark/benchmarker.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ async def run(
6464
environment: Environment,
6565
warmup: TransientPhaseConfig,
6666
cooldown: TransientPhaseConfig,
67-
sample_requests: int | None = None,
67+
sample_size: int | None = None,
6868
prefer_response_metrics: bool = True,
6969
progress: (
7070
BenchmarkerProgress[BenchmarkAccumulatorT, BenchmarkT] | None
@@ -81,8 +81,9 @@ async def run(
8181
:param environment: Environment for execution coordination
8282
:param warmup: Warmup phase configuration before benchmarking
8383
:param cooldown: Cooldown phase configuration after benchmarking
84-
:param sample_requests: Number of requests to sample for estimation,
85-
defaults to 20
84+
:param sample_size: Maximum number of requests per status group
85+
(completed, errored, incomplete) to retain full data for.
86+
None keeps all, 0 strips all, N > 0 uses reservoir sampling.
8687
:param prefer_response_metrics: Whether to prefer response metrics over
8788
request metrics, defaults to True
8889
:param progress: Optional tracker for benchmark lifecycle events
@@ -117,7 +118,7 @@ async def run(
117118
if constraints
118119
else {}
119120
),
120-
sample_requests=sample_requests,
121+
sample_size=sample_size,
121122
warmup=warmup,
122123
cooldown=cooldown,
123124
prefer_response_metrics=prefer_response_metrics,

src/guidellm/benchmark/entrypoints.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
GenerativeBenchmark,
3131
GenerativeBenchmarkAccumulator,
3232
GenerativeBenchmarksReport,
33+
GenerativeMetricsArgs,
3334
ProfileArgs,
3435
)
3536
from guidellm.data import (
@@ -433,6 +434,13 @@ async def benchmark_generative_text(
433434
"""
434435
benchmark_args = resolve_to_single_benchmark(args.get_benchmarks())
435436

437+
metrics_args = benchmark_args.metrics
438+
if not isinstance(metrics_args, GenerativeMetricsArgs):
439+
raise TypeError(
440+
f"Expected GenerativeMetricsArgs for generative text benchmark, "
441+
f"got {type(metrics_args).__name__}"
442+
)
443+
436444
backend, model = await resolve_backend(
437445
backend_args=benchmark_args.backend,
438446
console=console,
@@ -481,10 +489,10 @@ async def benchmark_generative_text(
481489
profile=profile,
482490
environment=NonDistributedEnvironment(),
483491
progress=progress,
484-
sample_requests=None,
492+
sample_size=metrics_args.sample_size,
485493
warmup=warmup,
486494
cooldown=cooldown,
487-
prefer_response_metrics=True,
495+
prefer_response_metrics=metrics_args.prefer_response_metrics,
488496
):
489497
if benchmark:
490498
report.benchmarks.append(benchmark)

src/guidellm/benchmark/schemas/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@
3030
TransientPhaseConfig,
3131
)
3232
from .benchmark import GenerativeBenchmark
33-
from .entrypoints import BenchmarkArgs, BenchmarkMetadata, BenchmarkScenario
33+
from .entrypoints import (
34+
BenchmarkArgs,
35+
BenchmarkMetadata,
36+
BenchmarkScenario,
37+
GenerativeMetricsArgs,
38+
MetricsArgs,
39+
)
3440
from .metrics import (
3541
GenerativeAudioMetricsSummary,
3642
GenerativeImageMetricsSummary,
@@ -64,10 +70,12 @@
6470
"GenerativeImageMetricsSummary",
6571
"GenerativeMetrics",
6672
"GenerativeMetricsAccumulator",
73+
"GenerativeMetricsArgs",
6774
"GenerativeMetricsSummary",
6875
"GenerativeRequestsAccumulator",
6976
"GenerativeTextMetricsSummary",
7077
"GenerativeVideoMetricsSummary",
78+
"MetricsArgs",
7179
"ProfileArgs",
7280
"RandomArgs",
7381
"RunningMetricStats",

src/guidellm/benchmark/schemas/accumulator.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -576,10 +576,12 @@ class GenerativeRequestsAccumulator(StandardBaseModel):
576576
retention (clearing request arguments and/or outputs for non-sampled requests).
577577
"""
578578

579-
sample_requests: int | None = Field(
579+
sample_size: int | None = Field(
580580
default=None,
581581
description=(
582-
"Number of requests to sample and keep in the final benchmark for metrics"
582+
"Maximum number of requests in each status group to retain full data "
583+
"(prompt, output, tool calls) for. Lightweight stats are always kept. "
584+
"None keeps all, 0 strips all, N > 0 uses reservoir sampling."
583585
),
584586
)
585587
requests_stats: list[GenerativeRequestStats] = Field(
@@ -663,18 +665,18 @@ def update_estimate(
663665
current_index = len(self.requests_stats)
664666
self.requests_stats.append(stats)
665667

666-
if self.sample_requests is None:
668+
if self.sample_size is None:
667669
# Keeping all requests, don't need to sample
668670
self.samples = None
669-
elif self.sample_requests <= 0:
671+
elif self.sample_size <= 0:
670672
# Not keeping any requests, clear out unnecessary memory usage for current
671673
self.clear_stats_data(stats)
672-
elif self.sample_requests >= len(self.requests_stats):
674+
elif self.sample_size >= len(self.requests_stats):
673675
# Add directly to samples, haven't filled yet
674676
if self.samples is None:
675677
self.samples = []
676678
self.samples.append(current_index)
677-
elif self.sample_requests / len(self.requests_stats) >= random.random():
679+
elif self.sample_size / len(self.requests_stats) >= random.random():
678680
# Sampling logic: choose to replace with decreasing probability s / n
679681
# where s is sample size, n is current number of requests.
680682
# If chosen, choose random existing sample to replace.
@@ -802,16 +804,16 @@ def model_post_init(self, __context):
802804
"""
803805
Initialize child accumulators with config values after model construction.
804806
805-
Propagates sample_requests from config to child request accumulators to ensure
807+
Propagates sample_size from config to child request accumulators to ensure
806808
consistent sampling behavior across completed, errored, and incomplete request
807-
collections. This ensures the --sample-requests option functions correctly.
809+
collections. This ensures the --metrics-sample-size option functions correctly.
808810
"""
809811
super().model_post_init(__context)
810812

811-
# Propagate sample_requests from config to child accumulators
812-
self.completed.sample_requests = self.config.sample_requests
813-
self.errored.sample_requests = self.config.sample_requests
814-
self.incomplete.sample_requests = self.config.sample_requests
813+
# Propagate sample_size from config to child accumulators
814+
self.completed.sample_size = self.config.sample_size
815+
self.errored.sample_size = self.config.sample_size
816+
self.incomplete.sample_size = self.config.sample_size
815817

816818
def update_estimate(
817819
self,

src/guidellm/benchmark/schemas/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,12 @@ class BenchmarkConfig(StandardBaseDict):
275275
constraints: dict[str, dict[str, Any]] = Field(
276276
description="Constraint definitions applied to scheduler strategy execution",
277277
)
278-
sample_requests: int | None = Field(
278+
sample_size: int | None = Field(
279279
default=None,
280-
description="Request count for statistical sampling in final metrics",
280+
description=(
281+
"Maximum number of requests per status group to retain full data for. "
282+
"None keeps all, 0 strips all, N > 0 uses reservoir sampling."
283+
),
281284
)
282285
warmup: TransientPhaseConfig = Field(
283286
default_factory=TransientPhaseConfig,

src/guidellm/benchmark/schemas/entrypoints.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
from __future__ import annotations
1212

1313
import json
14+
from abc import ABC
1415
from pathlib import Path
15-
from typing import Any
16+
from typing import Any, ClassVar, Literal
1617

1718
import yaml
1819
from pydantic import (
@@ -39,6 +40,7 @@
3940
)
4041
from guidellm.scheduler.constraints import ConstraintArgs
4142
from guidellm.schemas import (
43+
PydanticClassRegistryMixin,
4244
ReloadableBaseModel,
4345
StandardBaseModel,
4446
standard_model_config,
@@ -49,6 +51,8 @@
4951
"BenchmarkArgs",
5052
"BenchmarkMetadata",
5153
"BenchmarkScenario",
54+
"GenerativeMetricsArgs",
55+
"MetricsArgs",
5256
]
5357

5458

@@ -77,6 +81,61 @@ def default_kind_list(*kinds: str) -> list[dict[str, Any]]:
7781
return [default_kind(kind) for kind in kinds]
7882

7983

84+
class MetricsArgs(PydanticClassRegistryMixin["MetricsArgs"], ABC):
85+
"""Base class for metrics collection arguments.
86+
87+
:cvar schema_discriminator: Field name for polymorphic deserialization
88+
"""
89+
90+
model_config = standard_model_config()
91+
92+
schema_discriminator: ClassVar[str] = "kind"
93+
94+
@classmethod
95+
def __pydantic_schema_base_type__(cls) -> type[MetricsArgs]:
96+
"""
97+
Return base type for polymorphic validation hierarchy.
98+
99+
:return: Base MetricsArgs class for schema validation
100+
"""
101+
if cls.__name__ == "MetricsArgs":
102+
return cls
103+
104+
return MetricsArgs
105+
106+
kind: str = Field(
107+
description="The kind of metrics configuration to use.",
108+
)
109+
110+
111+
@MetricsArgs.register("generative")
112+
class GenerativeMetricsArgs(MetricsArgs):
113+
"""Metrics configuration for generative (autoregressive) benchmarks."""
114+
115+
kind: Literal["generative"] = Field(
116+
default="generative",
117+
description="The kind of metrics configuration to use.",
118+
)
119+
sample_size: int | None = Field(
120+
default=None,
121+
description=(
122+
"Maximum number of requests per status group (completed, errored, "
123+
"incomplete) to retain full data (prompt, output, tool calls) for in "
124+
"the final benchmark. Lightweight stats (latency, token counts) are "
125+
"always kept for every request. None keeps all request data, 0 strips "
126+
"all request data, N > 0 uses reservoir sampling to retain N per group."
127+
),
128+
examples=[None, 0, 100],
129+
)
130+
prefer_response_metrics: bool = Field(
131+
default=True,
132+
description=(
133+
"Prioritize server-reported metrics over client-calculated metrics "
134+
"when both are available."
135+
),
136+
)
137+
138+
80139
class BenchmarkArgs(ReloadableBaseModel):
81140
"""Common benchmark configuration arguments."""
82141

@@ -164,6 +223,11 @@ class BenchmarkArgs(ReloadableBaseModel):
164223
],
165224
json_schema_extra={"argument_alias": "output"},
166225
)
226+
metrics: MetricsArgs = Field( # type: ignore[assignment]
227+
default_factory=lambda: default_kind("generative"),
228+
description="Configuration for metrics collection and request sampling.",
229+
json_schema_extra={"argument_alias": "metrics"},
230+
)
167231

168232

169233
class BenchmarkMetadata(StandardBaseModel):

0 commit comments

Comments
 (0)