Skip to content

Commit e9ad7fd

Browse files
nvzhihanjclaude
andauthored
feat(report): record run_config (seeds + load/warmup) in result_summary.json (#377)
Snapshot the run's load_pattern, warmup, and the scheduler/dataloader RNG seeds into the report so result_summary.json is self-describing and a valid run is identified by its settings. Rendered as a 'Run config:' block in report.txt. endpoint_config (api_key/URLs) is a sibling of settings and never included, so no secrets leak. Replaces the standalone 'seeds' field (#353). The resolved/effective runtime settings (sample count + ordering, which can differ per audit phase) are left for a follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 55e518a commit e9ad7fd

3 files changed

Lines changed: 66 additions & 34 deletions

File tree

src/inference_endpoint/commands/benchmark/execute.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,16 +1110,28 @@ def _on_phase_start(phase: PhaseConfig) -> None:
11101110

11111111
if snap_dict is not None:
11121112
try:
1113-
runtime = ctx.config.settings.runtime
1114-
warmup = ctx.config.settings.warmup
11151113
load_pattern = ctx.config.settings.load_pattern
1114+
runtime_cfg = ctx.config.settings.runtime
1115+
# load_pattern + warmup config and the RNG seeds, so
1116+
# result_summary.json is self-describing and a valid run is
1117+
# identified by its settings. The full, re-runnable config
1118+
# lives in config.yaml alongside. The resolved/effective
1119+
# runtime settings (sample count + ordering, which can differ
1120+
# per audit phase) are deferred to a follow-up. endpoint_config
1121+
# (api_key/URLs) is a sibling of settings and never included,
1122+
# so no secrets.
1123+
run_config = ctx.config.settings.model_dump(
1124+
mode="json", include={"load_pattern", "warmup"}
1125+
)
1126+
run_config["scheduler_random_seed"] = (
1127+
runtime_cfg.scheduler_random_seed
1128+
)
1129+
run_config["dataloader_random_seed"] = (
1130+
runtime_cfg.dataloader_random_seed
1131+
)
11161132
report = Report.from_snapshot(
11171133
snap_dict,
1118-
seeds={
1119-
"scheduler_random_seed": runtime.scheduler_random_seed,
1120-
"dataloader_random_seed": runtime.dataloader_random_seed,
1121-
"warmup_random_seed": warmup.warmup_random_seed,
1122-
},
1134+
run_config=run_config,
11231135
use_legacy_loadgen_qps_metrics=(
11241136
load_pattern.type == LoadPatternType.POISSON
11251137
and load_pattern.use_legacy_loadgen_qps_metrics

src/inference_endpoint/metrics/report.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,25 +146,28 @@ class Report(msgspec.Struct, frozen=True): # type: ignore[call-arg]
146146
qps: float | None = None
147147
tps: float | None = None
148148

149-
# RNG seeds for this run (scheduler/dataloader/warmup, from config). Carried
150-
# so result_summary.json is self-validating: a reproducible run is identified
151-
# by its seeds. These are config, not a measured metric, so the from_snapshot
152-
# caller supplies them rather than reading them from the metrics snapshot.
153-
seeds: dict[str, int] | None = None
149+
# Run configuration (load_pattern, warmup, and the scheduler/dataloader RNG
150+
# seeds), from config. Carried so result_summary.json is self-describing and a
151+
# valid run is identified by its settings. Config, not a measured metric, so
152+
# the from_snapshot caller supplies it rather than reading it from the metrics
153+
# snapshot. (Resolved/effective runtime settings — sample count + ordering,
154+
# which can differ per audit phase — are deferred to a follow-up.)
155+
run_config: dict[str, Any] | None = None
154156

155157
@classmethod
156158
def from_snapshot(
157159
cls,
158160
snap: dict[str, Any],
159161
*,
160-
seeds: dict[str, int] | None = None,
162+
run_config: dict[str, Any] | None = None,
161163
use_legacy_loadgen_qps_metrics: bool = True,
162164
) -> Report:
163165
"""Build a Report from a snapshot dict.
164166
165-
``seeds`` (optional) carries the run's RNG seeds from config into the
166-
report so result_summary.json is self-validating; it is keyword-only
167-
because it is config, not part of the metrics snapshot.
167+
``run_config`` (optional, keyword-only) carries the run's configuration
168+
(load_pattern, warmup, and the scheduler/dataloader RNG seeds) into the
169+
report so result_summary.json is self-describing; it is config, not part
170+
of the metrics snapshot.
168171
169172
Input is the dict form produced by
170173
``inference_endpoint.async_utils.services.metrics_aggregator.snapshot
@@ -274,7 +277,7 @@ def _series_dict(key: str) -> dict[str, Any]:
274277
legacy_loadgen_window_duration_ns=legacy_loadgen_window_duration_ns,
275278
qps=qps,
276279
tps=tps,
277-
seeds=seeds,
280+
run_config=run_config,
278281
)
279282

280283
def to_json(self, save_to: os.PathLike | None = None) -> bytes:
@@ -304,9 +307,14 @@ def display(
304307
fn(f"Version: {self.version}{newline}")
305308
if self.git_sha:
306309
fn(f"Git SHA: {self.git_sha}{newline}")
307-
if self.seeds:
308-
seed_str = ", ".join(f"{k}={v}" for k, v in self.seeds.items())
309-
fn(f"Seeds: {seed_str}{newline}")
310+
if self.run_config:
311+
fn(f"Run config:{newline}")
312+
for section, params in self.run_config.items():
313+
if isinstance(params, dict):
314+
inner = ", ".join(f"{k}={v}" for k, v in params.items())
315+
fn(f" {section}: {inner}{newline}")
316+
else:
317+
fn(f" {section}: {params}{newline}")
310318
if self.test_started_at > 0:
311319
approx = monotime_to_datetime(self.test_started_at)
312320
fn(f"Test started at: {approx.strftime('%Y-%m-%d %H:%M:%S')}{newline}")

tests/unit/metrics/test_report_builder.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,16 +181,23 @@ def test_with_metrics(self):
181181
# OSL data was written → tps is computable.
182182
assert report.tps is not None
183183

184-
def test_seeds_keyword_only_passthrough(self):
185-
"""seeds is config, not a snapshot metric: None unless the caller
184+
def test_run_config_keyword_only_passthrough(self):
185+
"""run_config is config, not a snapshot metric: None unless the caller
186186
supplies it, and carried verbatim into the report when it does."""
187187
registry = _make_registry(n_samples=5)
188188
snap = snapshot_to_dict(
189189
registry.build_snapshot(state=SessionState.COMPLETE, n_pending_tasks=0)
190190
)
191-
assert Report.from_snapshot(snap).seeds is None
192-
seeds = {"scheduler_random_seed": 42, "dataloader_random_seed": 7}
193-
assert Report.from_snapshot(snap, seeds=seeds).seeds == seeds
191+
assert Report.from_snapshot(snap).run_config is None
192+
run_config = {
193+
"load_pattern": {"type": "poisson", "target_qps": 14.75},
194+
"warmup": {"enabled": False, "warmup_random_seed": 42},
195+
"scheduler_random_seed": 42,
196+
"dataloader_random_seed": 42,
197+
}
198+
assert (
199+
Report.from_snapshot(snap, run_config=run_config).run_config == run_config
200+
)
194201

195202
def test_failed_uses_tracked_counter(self):
196203
"""``n_samples_failed`` reads from ``tracked_samples_failed``, not
@@ -279,23 +286,28 @@ def test_to_json_qps_tps_null_without_duration(self):
279286
assert data["qps"] is None
280287
assert data["tps"] is None
281288

282-
def test_to_json_serializes_seeds(self):
283-
"""result_summary.json carries the run's RNG seeds so a run can be
284-
validated as reproducible; absent seeds serialize as null."""
289+
def test_to_json_and_display_carry_run_config(self):
290+
"""result_summary.json + report.txt carry the run's config so a run is
291+
self-describing/reproducible; absent run_config serializes as null."""
285292
registry = _make_registry(n_samples=5)
286293
snap = snapshot_to_dict(
287294
registry.build_snapshot(state=SessionState.COMPLETE, n_pending_tasks=0)
288295
)
289-
seeds = {"scheduler_random_seed": 42, "dataloader_random_seed": 42}
290-
report = Report.from_snapshot(snap, seeds=seeds)
291-
assert json.loads(report.to_json())["seeds"] == seeds
296+
run_config = {
297+
"load_pattern": {"type": "poisson", "target_qps": 14.75},
298+
"scheduler_random_seed": 42,
299+
"dataloader_random_seed": 42,
300+
}
301+
report = Report.from_snapshot(snap, run_config=run_config)
302+
assert json.loads(report.to_json())["run_config"] == run_config
292303

293304
lines: list[str] = []
294305
report.display(fn=lines.append, summary_only=True)
295-
assert any("Seeds:" in ln for ln in lines)
306+
assert any("Run config:" in ln for ln in lines)
307+
assert any("load_pattern:" in ln and "poisson" in ln for ln in lines)
296308

297-
# Absent seeds -> null, not omitted.
298-
assert json.loads(Report.from_snapshot(snap).to_json())["seeds"] is None
309+
# Absent run_config -> null, not omitted.
310+
assert json.loads(Report.from_snapshot(snap).to_json())["run_config"] is None
299311

300312
def test_to_json_save(self, tmp_path: Path):
301313
registry = _make_registry(n_samples=5)

0 commit comments

Comments
 (0)