Skip to content

Commit f9e1aca

Browse files
authored
feat(config): add v6.1 ruleset and pin RNG seeds from submission ruleset (#389)
* feat(config): add v6.1 ruleset and pin RNG seeds from submission ruleset Adds the MLPerf Inference v6.1 RoundRuleset (schedule/sample_index seeds from loadgen/mlperf.conf; per-model latency targets unchanged from v5.1) and makes it CURRENT. The registry now registers every known round by version, so both v5.1 and v6.1 are resolvable. When a config carries submission_ref, _resolve_and_validate now pins the runtime RNG seeds from the selected ruleset during construction — before the config is dumped to the report dir, so the persisted config.yaml, RuntimeSettings, and report all reflect the seeds that actually ran. The ruleset value wins over any user-supplied seed (mirrors LoadGen locking core seeds from user.conf). Lenient by design: an unregistered ruleset leaves the config unchanged, so placeholder/non-submission configs are unaffected. Duration/latency overrides and the runtime-path integration (surfacing ruleset latency targets in the report) are deferred to a follow-up. --------- Signed-off-by: Rashid Kaleem <230885705+arekay-nv@users.noreply.github.com>
1 parent 21fa7e6 commit f9e1aca

8 files changed

Lines changed: 354 additions & 17 deletions

File tree

src/inference_endpoint/config/ruleset_registry.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from typing import TYPE_CHECKING
3030

31+
from .rulesets.mlcommons.rules import ALL_ROUNDS as mlcommons_rounds
3132
from .rulesets.mlcommons.rules import CURRENT as mlcommons_current
3233
from .rulesets.mlcommons.rules import EDGE_CURRENT as mlcommons_edge_current
3334

@@ -44,7 +45,16 @@ def register_ruleset(name: str, ruleset: BenchmarkSuiteRuleset) -> None:
4445
Args:
4546
name: Unique name/version identifier (e.g., "mlperf-inference-v5.1")
4647
ruleset: The ruleset instance to register
48+
49+
Raises:
50+
ValueError: If a ruleset is already registered under ``name`` (prevents
51+
an accidental overwrite via this function). The MLCommons
52+
auto-registration path uses ``setdefault`` and does not hit this
53+
guard; round-version uniqueness is enforced by
54+
``test_round_versions_are_unique``.
4755
"""
56+
if name in _RULESET_REGISTRY:
57+
raise ValueError(f"Ruleset {name!r} is already registered")
4858
_RULESET_REGISTRY[name] = ruleset
4959

5060

@@ -79,16 +89,22 @@ def list_rulesets() -> list[str]:
7989

8090
# Auto-register MLCommons rulesets
8191
def _auto_register_mlcommons():
82-
"""Auto-register MLCommons rulesets."""
83-
# Register with version-specific name
84-
register_ruleset(f"mlperf-inference-{mlcommons_current.version}", mlcommons_current)
85-
# Also register as "mlcommons-current" for convenience
86-
register_ruleset("mlcommons-current", mlcommons_current)
87-
# Edge-Agentic (BFCL v4) ruleset
88-
register_ruleset(
92+
"""Auto-register MLCommons rulesets (idempotent).
93+
94+
Uses ``setdefault`` so re-running (e.g. a module re-import) is a no-op
95+
rather than tripping ``register_ruleset``'s duplicate guard. Uniqueness of
96+
round versions is enforced separately by ``test_round_versions_are_unique``.
97+
"""
98+
# Register every known round under its version-specific name
99+
for ruleset in mlcommons_rounds:
100+
_RULESET_REGISTRY.setdefault(f"mlperf-inference-{ruleset.version}", ruleset)
101+
# Also register the current round as "mlcommons-current" for convenience
102+
_RULESET_REGISTRY.setdefault("mlcommons-current", mlcommons_current)
103+
# Edge-agentic ruleset: by version and as the edge "current".
104+
_RULESET_REGISTRY.setdefault(
89105
f"mlperf-{mlcommons_edge_current.version}", mlcommons_edge_current
90106
) # -> "mlperf-edge-v0.1"
91-
register_ruleset("mlperf-edge-current", mlcommons_edge_current)
107+
_RULESET_REGISTRY.setdefault("mlperf-edge-current", mlcommons_edge_current)
92108

93109

94110
# Auto-register on import

src/inference_endpoint/config/rulesets/mlcommons/rules.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
https://github.com/mlcommons/inference_policies/blob/master/inference_rules.adoc
2020
"""
2121

22+
import copy
2223
import random
2324
from dataclasses import dataclass, field
2425
from enum import Enum
@@ -272,7 +273,30 @@ def apply_user_config(
272273
)
273274

274275

275-
CURRENT = _v5_1
276+
# v6.1 per-model latency targets are unchanged from v5.1 (they match the
277+
# current mlperf.conf); only the round RNG seeds rotate. Seeds are the
278+
# schedule_rng_seed / sample_index_rng_seed values from loadgen/mlperf.conf,
279+
# pinned to a specific upstream commit for traceability:
280+
# https://github.com/mlcommons/inference/blob/10f823448fd38bb739e52690efe8191c3a55412b/loadgen/mlperf.conf#L42-L43
281+
# qsl_rng_seed is intentionally not modeled: it only selects the load order of
282+
# the first-N working set, which the sample-index shuffle already covers.
283+
_v6_1 = RoundRuleset(
284+
version="v6.1",
285+
scheduler_rng_seed=16159082839903944936,
286+
sample_index_rng_seed=2747215439041700203,
287+
benchmark_rulesets={
288+
# Keep the model-singleton keys; deep-copy only the per-model rules so
289+
# v6.1's mutable leaves (e.g. reported_metrics) can't leak into v5.1.
290+
model: copy.deepcopy(rules)
291+
for model, rules in _v5_1.benchmark_rulesets.items()
292+
},
293+
)
294+
295+
296+
# All known rounds, registered by version in the ruleset registry.
297+
ALL_ROUNDS = [_v5_1, _v6_1]
298+
299+
CURRENT = _v6_1
276300

277301

278302
# --- Edge-Agentic (BFCL v4) ruleset ---

src/inference_endpoint/config/schema.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from __future__ import annotations
2424

25+
import logging
2526
from collections import Counter
2627
from enum import Enum
2728
from pathlib import Path
@@ -50,6 +51,8 @@
5051
from .ruleset_base import BenchmarkSuiteRuleset
5152
from .utils import parse_dataset_string, resolve_env_vars
5253

54+
logger = logging.getLogger(__name__)
55+
5356

5457
class SystemDefaults(BaseModel):
5558
DEFAULT_TIMEOUT: ClassVar[float] = 300.0
@@ -1029,8 +1032,92 @@ def _resolve_and_validate(self) -> Self:
10291032
f"got '{lp.type}'"
10301033
)
10311034

1035+
# Pin RNG seeds from the submission ruleset. Done last so the values
1036+
# are baked into the config before any consumer reads them — the config
1037+
# dump to the report dir, RuntimeSettings.from_config, and the report
1038+
# seeds block all see the pinned values.
1039+
self._apply_ruleset_seed_overrides()
1040+
10321041
return self
10331042

1043+
def _apply_ruleset_seed_overrides(self) -> None:
1044+
"""Override runtime + warmup RNG seeds from the selected submission ruleset.
1045+
1046+
MLPerf rounds pin the RNG seeds; this mirrors LoadGen locking the core
1047+
seeds from ``user.conf`` (a submitter cannot substitute their own).
1048+
If ``submission_ref`` is unset, the config is left unchanged. If it
1049+
names an unregistered ruleset, a ``type=SUBMISSION`` config errors (a
1050+
submission cannot silently fall back to default seeds), while any other
1051+
type is left unchanged so non-submission/placeholder configs still work.
1052+
1053+
The warmup phase is reseeded from the sample-index (dataloader) seed so
1054+
its sample order derives from the same pinned seed as the perf phase.
1055+
Only the seed *value* is propagated — each phase builds its own
1056+
``random.Random`` downstream, so the RNG object is never shared.
1057+
"""
1058+
if self.submission_ref is None:
1059+
return
1060+
try:
1061+
ruleset = self.submission_ref.get_ruleset_instance()
1062+
except KeyError as e:
1063+
if self.type == TestType.SUBMISSION:
1064+
raise ValueError(
1065+
f"submission_ref.ruleset {self.submission_ref.ruleset!r} is not "
1066+
"registered; a submission must pin official RNG seeds and cannot "
1067+
"fall back to defaults."
1068+
) from e
1069+
logger.warning(
1070+
"submission_ref.ruleset %r is not registered; skipping ruleset "
1071+
"seed overrides.",
1072+
self.submission_ref.ruleset,
1073+
)
1074+
return
1075+
1076+
# A ruleset used as a submission_ref must pin both seeds. ``None`` means
1077+
# "unseeded" in the general ruleset contract (ruleset_base.py), but an
1078+
# unseeded submission is incoherent and would silently diverge from the
1079+
# random.Random(None) path in RoundRuleset.apply_user_config. Reject it.
1080+
if ruleset.scheduler_rng_seed is None or ruleset.sample_index_rng_seed is None:
1081+
raise ValueError(
1082+
f"submission_ref.ruleset {self.submission_ref.ruleset!r} leaves an "
1083+
"RNG seed unset; a pinned ruleset must define both the scheduler "
1084+
"and sample-index seeds."
1085+
)
1086+
1087+
# Rebuild through model_validate (not model_copy(update=)): with
1088+
# extra='forbid' this validates seed *values* and rejects renamed/unknown
1089+
# fields. model_copy(update=) writes straight into __dict__, so a
1090+
# wrong-typed (e.g. str) or renamed seed would slip through unchecked.
1091+
runtime = self.settings.runtime
1092+
new_runtime = type(runtime).model_validate(
1093+
{
1094+
**runtime.model_dump(),
1095+
"scheduler_random_seed": ruleset.scheduler_rng_seed,
1096+
"dataloader_random_seed": ruleset.sample_index_rng_seed,
1097+
}
1098+
)
1099+
warmup = self.settings.warmup
1100+
new_warmup = type(warmup).model_validate(
1101+
{
1102+
**warmup.model_dump(),
1103+
"warmup_random_seed": ruleset.sample_index_rng_seed,
1104+
}
1105+
)
1106+
object.__setattr__(
1107+
self,
1108+
"settings",
1109+
self.settings.model_copy(
1110+
update={"runtime": new_runtime, "warmup": new_warmup}
1111+
),
1112+
)
1113+
logger.debug(
1114+
"Pinned RNG seeds from ruleset %r: scheduler=%s sample_index=%s "
1115+
"(warmup reseeded from sample_index)",
1116+
self.submission_ref.ruleset,
1117+
ruleset.scheduler_rng_seed,
1118+
ruleset.sample_index_rng_seed,
1119+
)
1120+
10341121
@model_validator(mode="after")
10351122
def _propagate_client_api_type(self) -> Self:
10361123
"""Sync client.api_type from endpoint_config.api_type at construction.

src/inference_endpoint/config/templates/submission_template.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ benchmark_mode: "offline" # Required for submission: "offline" or "online"
77
# Submission reference (model + ruleset for compliance checks)
88
submission_ref:
99
model: "llama-2-70b"
10-
ruleset: "mlperf-inference-v6.0"
10+
ruleset: "mlperf-inference-v6.1"
1111

1212
model_params:
1313
name: "meta-llama/Llama-2-70B-Chat-HF"

tests/unit/commands/test_benchmark.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ def test_from_config_submission_defaults_to_both(self, mock_run, tmp_path):
382382
- path: "test.jsonl"
383383
submission_ref:
384384
model: "test-model"
385-
ruleset: "test"
385+
ruleset: "mlperf-inference-v6.1"
386386
"""
387387
config_file = tmp_path / "sub.yaml"
388388
config_file.write_text(yaml_content)

tests/unit/commands/test_util_commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ def test_submission_ref_logging(self, tmp_path, caplog):
8181
- path: "test.jsonl"
8282
submission_ref:
8383
model: "llama"
84-
ruleset: "mlperf-v6"
84+
ruleset: "mlperf-inference-v6.1"
8585
""")
8686
with caplog.at_level("INFO"):
8787
execute_validate(config_file)
8888
assert "Submission:" in caplog.text
89-
assert "mlperf-v6" in caplog.text
89+
assert "mlperf-inference-v6.1" in caplog.text
9090

9191
@pytest.mark.unit
9292
def test_invalid_yaml(self, tmp_path):

tests/unit/config/rulesets/mlcommons/test_rules.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@
1515

1616
import pytest
1717
from inference_endpoint import metrics
18-
from inference_endpoint.config.ruleset_registry import get_ruleset, list_rulesets
18+
from inference_endpoint.config.ruleset_registry import (
19+
get_ruleset,
20+
list_rulesets,
21+
register_ruleset,
22+
)
1923
from inference_endpoint.config.rulesets.mlcommons import datasets, models
2024
from inference_endpoint.config.rulesets.mlcommons.rules import (
25+
ALL_ROUNDS,
2126
CURRENT,
2227
EDGE_CURRENT,
2328
OptimizationPriority,
@@ -116,6 +121,62 @@ def test_apply_user_config_min_sample_count_override():
116121

117122

118123
@pytest.mark.unit
124+
def test_current_round_is_v6_1():
125+
assert CURRENT.version == "v6.1"
126+
127+
128+
@pytest.mark.unit
129+
def test_v6_1_official_seeds():
130+
"""Seeds are the schedule/sample_index values from loadgen/mlperf.conf."""
131+
v6_1 = get_ruleset("mlperf-inference-v6.1")
132+
assert v6_1.scheduler_rng_seed == 16159082839903944936
133+
assert v6_1.sample_index_rng_seed == 2747215439041700203
134+
135+
136+
@pytest.mark.unit
137+
def test_all_rounds_registered_by_version():
138+
names = list_rulesets()
139+
for ruleset in ALL_ROUNDS:
140+
assert f"mlperf-inference-{ruleset.version}" in names
141+
# Both the prior and current round remain resolvable.
142+
assert "mlperf-inference-v5.1" in names
143+
assert "mlperf-inference-v6.1" in names
144+
assert get_ruleset("mlcommons-current") is CURRENT
145+
146+
147+
@pytest.mark.unit
148+
def test_round_versions_are_unique():
149+
"""Each registered round must own a distinct version string, else the
150+
registry would silently overwrite one round's entry with another's."""
151+
assert len(ALL_ROUNDS) == len({r.version for r in ALL_ROUNDS})
152+
153+
154+
@pytest.mark.unit
155+
def test_register_ruleset_rejects_duplicate():
156+
"""Re-registering an existing name raises rather than clobbering it."""
157+
with pytest.raises(ValueError):
158+
register_ruleset("mlperf-inference-v6.1", CURRENT)
159+
160+
161+
@pytest.mark.unit
162+
def test_v6_1_latency_targets_match_v5_1():
163+
"""Only the round seeds rotate; per-model targets are identical."""
164+
v5_1 = get_ruleset("mlperf-inference-v5.1")
165+
v6_1 = get_ruleset("mlperf-inference-v6.1")
166+
assert v6_1.benchmark_rulesets == v5_1.benchmark_rulesets
167+
# v6.1 holds an independent copy, not a shared reference to v5.1's dict.
168+
assert v6_1.benchmark_rulesets is not v5_1.benchmark_rulesets
169+
# Anchor on a concrete target so the equality above verifies real structure.
170+
assert (
171+
v6_1.benchmark_rulesets[models.Llama3_1_405b][
172+
OptimizationPriority.THROUGHPUT
173+
].max_tpot_latency_ms
174+
== 175
175+
)
176+
assert v6_1.scheduler_rng_seed != v5_1.scheduler_rng_seed
177+
assert v6_1.sample_index_rng_seed != v5_1.sample_index_rng_seed
178+
179+
119180
def test_edge_ruleset_registered():
120181
# Resolvable by version-specific name and the "current" alias.
121182
assert get_ruleset("mlperf-edge-v0.1") is EDGE_CURRENT

0 commit comments

Comments
 (0)