|
22 | 22 |
|
23 | 23 | from __future__ import annotations |
24 | 24 |
|
| 25 | +import logging |
25 | 26 | from collections import Counter |
26 | 27 | from enum import Enum |
27 | 28 | from pathlib import Path |
|
50 | 51 | from .ruleset_base import BenchmarkSuiteRuleset |
51 | 52 | from .utils import parse_dataset_string, resolve_env_vars |
52 | 53 |
|
| 54 | +logger = logging.getLogger(__name__) |
| 55 | + |
53 | 56 |
|
54 | 57 | class SystemDefaults(BaseModel): |
55 | 58 | DEFAULT_TIMEOUT: ClassVar[float] = 300.0 |
@@ -1029,8 +1032,92 @@ def _resolve_and_validate(self) -> Self: |
1029 | 1032 | f"got '{lp.type}'" |
1030 | 1033 | ) |
1031 | 1034 |
|
| 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 | + |
1032 | 1041 | return self |
1033 | 1042 |
|
| 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 | + |
1034 | 1121 | @model_validator(mode="after") |
1035 | 1122 | def _propagate_client_api_type(self) -> Self: |
1036 | 1123 | """Sync client.api_type from endpoint_config.api_type at construction. |
|
0 commit comments