Skip to content

Commit 1ecb9e8

Browse files
arekay-nvclaude
andcommitted
feat(metrics): consolidate gpt-oss/deepseek accuracy, dedup reporting, surface TPS
Deduplicate the reporting layer, give gpt-oss-120b and deepseek-r1 a uniform per-subset accuracy breakdown, and surface TPS everywhere QPS appears. - Add shared evaluation/accuracy_results.py (to_float, find_accuracy_breakdown, ACCURACY_METRIC_KEYS, build_breakdown); metrics/results_plots.py and compliance/checker.py import it instead of copy-pasted helpers. - DeepSeek-R1 scorer surfaces its per-subset breakdown via score_breakdown() (BFCL-shaped: overall_accuracy/subset_scores/total_samples, 0-100). - Add GptOssAccuracyScorer: runs the 3 gpt-oss member scorers once and rolls them up sample-weighted by unique problem count. finalize emits both the per-subset <subset>::gptoss entries (audit + downstream compat) and one consolidated `gptoss` entry with a breakdown, from the single scoring pass. - Consolidation is opt-in via explicit accuracy_config.group; the ::variant suffix is a naming convention, not a grouping signal. Group members must return scalar scores in [0,1]. - Add accuracy to Report.display()/report.txt/result_summary.json; write the report in a finally so a scoring failure can't discard perf artifacts. - Single-source results.json qps/tps from Report (drop the duplicate QPS recompute); add tps + a symmetric "TPS: N/A" display branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f9e1aca commit 1ecb9e8

16 files changed

Lines changed: 1009 additions & 106 deletions

File tree

examples/04_GPTOSS120B_Example/sglang_gptoss_120b_example.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,23 @@ datasets:
2323
eval_method: "code_bench_scorer"
2424
extractor: "python_code_extractor"
2525
num_repeats: 3
26+
group: "gptoss"
2627
- name: "aime25::gptoss"
2728
type: "accuracy"
2829
accuracy_config:
2930
eval_method: "pass_at_1"
3031
ground_truth: "answer"
3132
extractor: "boxed_math_extractor"
3233
num_repeats: 8
34+
group: "gptoss"
3335
- name: "gpqa::gptoss"
3436
type: "accuracy"
3537
accuracy_config:
3638
eval_method: "pass_at_1"
3739
extractor: "abcd_extractor"
3840
ground_truth: "ground_truth"
3941
num_repeats: 5
42+
group: "gptoss"
4043
settings:
4144
runtime:
4245
min_duration_ms: 3000

examples/04_GPTOSS120B_Example/vllm_gptoss_120b_example.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,23 @@ datasets:
2525
eval_method: "code_bench_scorer"
2626
extractor: "python_code_extractor"
2727
num_repeats: 3
28+
group: "gptoss"
2829
- name: "aime25::gptoss"
2930
type: "accuracy"
3031
accuracy_config:
3132
eval_method: "pass_at_1"
3233
ground_truth: "answer"
3334
extractor: "boxed_math_extractor"
3435
num_repeats: 8
36+
group: "gptoss"
3537
- name: "gpqa::gptoss"
3638
type: "accuracy"
3739
accuracy_config:
3840
eval_method: "pass_at_1"
3941
extractor: "abcd_extractor"
4042
ground_truth: "ground_truth"
4143
num_repeats: 5
44+
group: "gptoss"
4245

4346
settings:
4447
runtime:

src/inference_endpoint/commands/benchmark/execute.py

Lines changed: 155 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444

4545
import msgspec
4646
import msgspec.json
47+
import msgspec.structs
4748
from huggingface_hub import model_info
4849
from tqdm import tqdm
4950
from transformers.utils import logging as transformers_logging
@@ -83,7 +84,11 @@
8384
from inference_endpoint.endpoint_client.http_client import HTTPEndpointClient
8485
from inference_endpoint.endpoint_client.http_sample_issuer import HttpClientSampleIssuer
8586
from inference_endpoint.evaluation import Extractor
86-
from inference_endpoint.evaluation.scoring import Scorer
87+
from inference_endpoint.evaluation.scoring import (
88+
GptOssAccuracyScorer,
89+
GptOssMember,
90+
Scorer,
91+
)
8792
from inference_endpoint.exceptions import (
8893
ExecutionError,
8994
InputValidationError,
@@ -160,6 +165,9 @@ class AccuracyConfiguration:
160165
ground_truth_column: str | None
161166
num_repeats: int
162167
extras: dict[str, Any] = field(default_factory=dict)
168+
# Consolidation group tag; members sharing a group (>=2) roll up into one
169+
# combined score. None -> scored on its own.
170+
group: str | None = None
163171

164172

165173
@dataclass
@@ -274,6 +282,19 @@ def _resolve_accuracy_components(
274282
return scorer_cls, extractor_cls
275283

276284

285+
def _accuracy_group(accuracy_config: Any | None) -> str | None:
286+
"""Explicit consolidation group for an accuracy dataset.
287+
288+
Only an explicit ``accuracy_config.group`` triggers consolidation. The
289+
``::<variant>`` name suffix is a naming convention (``open_orca::llama2_70b``,
290+
``cnn_dailymail::llama3_8b``), NOT a grouping signal, so it must not roll
291+
datasets up implicitly — that would silently consolidate any future config
292+
with two accuracy datasets sharing a model suffix. The gpt-oss example
293+
configs set ``group: "gptoss"`` explicitly.
294+
"""
295+
return getattr(accuracy_config, "group", None) if accuracy_config else None
296+
297+
277298
def _load_datasets(
278299
config: BenchmarkConfig,
279300
report_dir: Path,
@@ -319,6 +340,7 @@ def _load_datasets(
319340
acc_cfg.accuracy_config.ground_truth,
320341
acc_cfg.accuracy_config.num_repeats,
321342
acc_cfg.accuracy_config.extras or {},
343+
group=_accuracy_group(acc_cfg.accuracy_config),
322344
)
323345
)
324346
ds.load(
@@ -373,6 +395,7 @@ def _load_datasets(
373395
accuracy_config.ground_truth,
374396
accuracy_config.num_repeats,
375397
accuracy_config.extras or {},
398+
group=_accuracy_group(accuracy_config),
376399
)
377400
)
378401

@@ -1169,41 +1192,76 @@ def _salvage_tmpfs(report_dir: Path, tmpfs_dir: Path) -> None:
11691192
logger.debug(f"Copied {src_events} -> {dst_events}")
11701193

11711194

1172-
def finalize_benchmark(ctx: BenchmarkContext, bench: BenchmarkResult) -> None:
1173-
"""Score accuracy, aggregate results, write JSON."""
1174-
config = ctx.config
1175-
result = bench.session
1176-
collector = bench.collector
1177-
report = bench.report
1195+
def _score_accuracy(ctx: BenchmarkContext, result: SessionResult) -> dict[str, Any]:
1196+
"""Run accuracy scorers, consolidating grouped datasets into one entry each.
11781197
1179-
# Display report if available (from MetricsAggregator pub/sub snapshot).
1180-
# result_summary.json is the self-complete machine-readable report (carries
1181-
# qps/tps/seeds via Report.to_json); report.txt is the full human-readable
1182-
# dump (histograms + percentiles); the console log shows just the summary.
1183-
if report is not None:
1184-
report.display(fn=lambda s: logger.info(s), summary_only=True)
1185-
report.to_json(save_to=ctx.report_dir / "result_summary.json")
1186-
1187-
report_txt = ctx.report_dir / "report.txt"
1188-
with report_txt.open("w") as f:
1189-
report.display(fn=lambda s: print(s, file=f))
1190-
if bench.profiling is not None:
1191-
_write_profiling_section(f, bench.profiling)
1192-
logger.info("Report written to %s", report_txt)
1198+
Datasets sharing a consolidation group with >=2 members (e.g. gpt-oss's
1199+
aime25/gpqa/livecodebench subsets) are rolled up by GptOssAccuracyScorer into
1200+
a single combined entry keyed by the group name; everything else is scored on
1201+
its own exactly as before.
1202+
"""
1203+
accuracy_scores: dict[str, Any] = {}
11931204

1194-
# Sibling profiling.json — kept separate so Report stays a pure
1195-
# snapshot-derived struct.
1196-
if bench.profiling is not None:
1197-
(ctx.report_dir / "profiling.json").write_text(
1198-
json.dumps(bench.profiling, indent=2)
1205+
groups: dict[str, list[AccuracyConfiguration]] = {}
1206+
for cfg in ctx.eval_configs:
1207+
if cfg.group:
1208+
groups.setdefault(cfg.group, []).append(cfg)
1209+
combined = {g: members for g, members in groups.items() if len(members) >= 2}
1210+
combined_ids = {id(cfg) for members in combined.values() for cfg in members}
1211+
1212+
for group_name, members in combined.items():
1213+
gpt_members = [
1214+
GptOssMember(
1215+
full_name=cfg.dataset_name,
1216+
subset=cfg.dataset_name.split("::")[0],
1217+
scorer_cls=cfg.scorer,
1218+
extractor=cfg.extractor,
1219+
dataset=cfg.dataset,
1220+
ground_truth_column=cfg.ground_truth_column,
1221+
extras=cfg.extras,
1222+
)
1223+
for cfg in members
1224+
]
1225+
scorer = GptOssAccuracyScorer(gpt_members, members[0].report_dir)
1226+
score, n_repeats = scorer.score()
1227+
breakdown = scorer.score_breakdown()
1228+
entry: dict[str, Any] = {
1229+
"dataset_name": group_name,
1230+
"num_samples": (breakdown or {}).get("total_samples", 0),
1231+
"score": score,
1232+
"complete": scorer.complete,
1233+
}
1234+
if breakdown is not None:
1235+
entry["breakdown"] = breakdown
1236+
accuracy_scores[group_name] = entry
1237+
logger.info(
1238+
f"Score for {group_name} (combined {len(members)} subsets): {score} "
1239+
f"({n_repeats} repeats, complete={scorer.complete})"
11991240
)
12001241

1201-
# Write scoring artifacts + copy event log from tmpfs to disk
1202-
_write_scoring_artifacts(ctx, result, bench.tmpfs_dir)
1242+
# Per-subset audit entries from the same scoring pass (no re-scoring):
1243+
# keeps the individual <subset>::gptoss scores in results.json for audit
1244+
# and for downstream consumers that read them. These carry no breakdown,
1245+
# so only the consolidated entry surfaces in the report headline.
1246+
for cfg in members:
1247+
member_score, member_complete = scorer.member_scores.get(
1248+
cfg.dataset_name, (None, False)
1249+
)
1250+
assert cfg.dataset.data is not None
1251+
accuracy_scores[cfg.dataset_name] = {
1252+
"dataset_name": cfg.dataset_name,
1253+
"num_samples": len(cfg.dataset.data),
1254+
"extractor": (
1255+
cfg.extractor.__name__ if cfg.extractor is not None else None
1256+
),
1257+
"ground_truth_column": cfg.ground_truth_column,
1258+
"score": member_score,
1259+
"complete": member_complete,
1260+
}
12031261

1204-
# Accuracy scoring
1205-
accuracy_scores: dict[str, Any] = {}
12061262
for eval_cfg in ctx.eval_configs:
1263+
if id(eval_cfg) in combined_ids:
1264+
continue
12071265
try:
12081266
scorer_instance = eval_cfg.scorer(
12091267
eval_cfg.dataset_name,
@@ -1223,7 +1281,7 @@ def finalize_benchmark(ctx: BenchmarkContext, bench: BenchmarkResult) -> None:
12231281
num_samples = len(eval_cfg.dataset.data)
12241282
if eval_cfg.dataset_name == "performance":
12251283
num_samples = sum(phase.issued_count for phase in result.perf_results)
1226-
entry: dict[str, Any] = {
1284+
singleton_entry: dict[str, Any] = {
12271285
"dataset_name": eval_cfg.dataset_name,
12281286
"num_samples": num_samples,
12291287
"extractor": (
@@ -1232,25 +1290,84 @@ def finalize_benchmark(ctx: BenchmarkContext, bench: BenchmarkResult) -> None:
12321290
"ground_truth_column": eval_cfg.ground_truth_column,
12331291
"score": score,
12341292
# False when the scorer produced only a partial headline (e.g.
1235-
# LegacyMLPerfDeepSeekR1Scorer when the lcb-service container was unreachable),
1236-
# so a partial number is never mistaken for a complete one.
1293+
# LegacyMLPerfDeepSeekR1Scorer when the lcb-service container was
1294+
# unreachable), so a partial number is never mistaken for a complete one.
12371295
"complete": scorer_instance.complete,
12381296
}
12391297
breakdown = scorer_instance.score_breakdown()
12401298
if breakdown is not None:
1241-
entry["breakdown"] = breakdown
1242-
accuracy_scores[eval_cfg.dataset_name] = entry
1299+
singleton_entry["breakdown"] = breakdown
1300+
accuracy_scores[eval_cfg.dataset_name] = singleton_entry
12431301
logger.info(
12441302
f"Score for {eval_cfg.dataset_name}: {score} "
12451303
f"({n_repeats} repeats, complete={scorer_instance.complete})"
12461304
)
12471305

1306+
return accuracy_scores
1307+
1308+
1309+
def finalize_benchmark(ctx: BenchmarkContext, bench: BenchmarkResult) -> None:
1310+
"""Score accuracy, aggregate results, write JSON."""
1311+
config = ctx.config
1312+
result = bench.session
1313+
collector = bench.collector
1314+
report = bench.report
1315+
1316+
# Sibling profiling.json — kept separate so Report stays a pure
1317+
# snapshot-derived struct.
1318+
if bench.profiling is not None:
1319+
(ctx.report_dir / "profiling.json").write_text(
1320+
json.dumps(bench.profiling, indent=2)
1321+
)
1322+
1323+
# Write scoring artifacts + copy event log from tmpfs to disk (scorers read
1324+
# sample_idx_map.json + events.jsonl from here).
1325+
_write_scoring_artifacts(ctx, result, bench.tmpfs_dir)
1326+
1327+
# Accuracy scoring (grouped datasets consolidated into one entry each).
1328+
# Scoring runs before the report is written so the accuracy headline can be
1329+
# attached, but the report is written in the `finally` below so a scoring
1330+
# failure (e.g. lcb-service unreachable, missing eval subproject, bad extras)
1331+
# still leaves the perf run's result_summary.json / report.txt on disk
1332+
# instead of discarding them — then the exception propagates as before.
1333+
accuracy_scores: dict[str, Any] = {}
1334+
try:
1335+
accuracy_scores = _score_accuracy(ctx, result)
1336+
finally:
1337+
# Attach accuracy breakdowns so result_summary.json, the console summary,
1338+
# and report.txt all carry the headline from this one scoring pass
1339+
# (absent on a scoring failure — accuracy_scores stays {}).
1340+
if report is not None:
1341+
accuracy_for_report = {
1342+
name: entry["breakdown"]
1343+
for name, entry in accuracy_scores.items()
1344+
if "breakdown" in entry
1345+
}
1346+
if accuracy_for_report:
1347+
report = msgspec.structs.replace(report, accuracy=accuracy_for_report)
1348+
1349+
# Display report if available (from MetricsAggregator pub/sub snapshot).
1350+
# result_summary.json is the self-complete machine-readable report
1351+
# (carries qps/tps/seeds/accuracy via Report.to_json); report.txt is the
1352+
# full human-readable dump; the console log shows the summary.
1353+
if report is not None:
1354+
report.display(fn=lambda s: logger.info(s), summary_only=True)
1355+
report.to_json(save_to=ctx.report_dir / "result_summary.json")
1356+
1357+
report_txt = ctx.report_dir / "report.txt"
1358+
with report_txt.open("w") as f:
1359+
report.display(fn=lambda s: print(s, file=f))
1360+
if bench.profiling is not None:
1361+
_write_profiling_section(f, bench.profiling)
1362+
logger.info("Report written to %s", report_txt)
1363+
12481364
# Report metrics: prefer Report from MetricsSnapshot, fall back to SessionResult
12491365
if report is not None and report.duration_ns is not None:
12501366
perf_elapsed = report.duration_ns / 1e9
12511367
total_issued = report.n_samples_issued
12521368
n_errors = report.n_samples_failed
12531369
qps = report.qps or 0.0
1370+
tps = report.tps
12541371
else:
12551372
perf = result.perf_results[0] if result.perf_results else None
12561373
if perf:
@@ -1261,6 +1378,8 @@ def finalize_benchmark(ctx: BenchmarkContext, bench: BenchmarkResult) -> None:
12611378
total_issued = 0
12621379
n_errors = len(collector.errors)
12631380
qps = total_issued / perf_elapsed if perf_elapsed > 0 else 0.0
1381+
# No OSL source outside the metrics snapshot, so TPS is unknown here.
1382+
tps = None
12641383

12651384
logger.info(f"Completed in {perf_elapsed:.1f}s")
12661385
if ctx.accuracy_only:
@@ -1295,6 +1414,7 @@ def finalize_benchmark(ctx: BenchmarkContext, bench: BenchmarkResult) -> None:
12951414
"failed": n_errors,
12961415
"elapsed_time": perf_elapsed,
12971416
"qps": qps,
1417+
"tps": tps,
12981418
},
12991419
}
13001420
if accuracy_scores:

0 commit comments

Comments
 (0)