Skip to content

[DRAFT] Add validation module to serve as backend for submission checker and policies enforcement - #544

Open
nv-alicheng wants to merge 54 commits into
mainfrom
feat/validation-module
Open

nv-alicheng wants to merge 54 commits into
mainfrom
feat/validation-module

Conversation

@nv-alicheng

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds a cohort-driven validator for submission bundles: src/inference_endpoint/validation/ (fourteen modules), run as

uv run python -m inference_endpoint.validation <cohort-dir> <submission-root>

A cohort is a directory that declares what one benchmark round requires: a cohort.yaml manifest (hint, ruleset or cohort-bundled catalog), one YAML file per scope (system.yaml, point.yaml, pareto.yaml), and a valid_refs.yaml naming the artifact fields a row may address. Checks are declarative rows — scope, dotted reference into the submission's artifacts, operator (comparison, membership, regex, aggregate over lists/regions), severity, and the policy rule it enforces. Each row prints one finding per instance with the rule citation and the fix, and the run exits 0 (pass), 1 (a fail row failed) or 2 (refused before any check ran). warn rows are reported but do not fail the run.

What the module covers:

  • Declared vocabulary — valid_refs.yaml gives every addressable field its expected type. A present value that contradicts its declaration, a name no declaration covers, and an artifact the upload walk would silently skip are all refused up front, so rows only judge facts that actually exist.
  • Upload walk — results/<system>/<model>/r<N>/ with the scope files (point.yaml, system_desc.json, result_summary.json, accuracy_results.json, system_power.json); directories that do not match the point shape, symlinks, and entries resolving outside the submission are refused once, naming each.
  • Regions and pareto arithmetic — region declarations, per-pareto folds, and derived facts (point cap, out-of-range points) computed a single time and shared by every row that names them.
  • Catalog — model and dataset vocabularies, either from a packaged ruleset or from a catalog that ships inside the cohort.
  • Honest reporting — each row reports whether it ran and at how many instances; a row whose when gated every instance off prints NEVER-RAN instead of looking like a pass, so a cohort cannot quietly stop checking things.

Docs: docs/validation/README.md is the usage contract (how to write a cohort, run it, and read findings); docs/validation/DESIGN.md holds the internals, invariants, and the engineering calls with their rationale. AGENTS.md maps the new package.

The change is additive: nothing outside the new package changes except the AGENTS.md component map and one .gitignore entry.

Type of change

  • New feature

Related issues

None.

Testing

  • Tests added/updated — nine unit test modules under tests/unit/validation/ (~4.4k lines) cover ref resolution and refusals, comparison and aggregate operands, region arithmetic, the upload walk, runner reporting, and the CLI, with stub cohort assets under tests/assets/cohorts/stub/.
  • All tests pass locally — uv run pytest tests/unit/validation -q → 407 passed.
  • Manual testing completed — end-to-end CLI runs against the stub cohort, against deliberately malformed uploads to exercise every refusal path, and against a real MLPerf submission bundle with the first real cohort (built on top of this branch): 11 points, one system, one model, 291 findings, 0 failing, exit 0.

Checklist

  • Code follows project style — ruff and ruff-format clean.
  • Pre-commit hooks pass — locally with SKIP=mypy: the mypy hook trips on three pre-existing macOS-typeshed errors unrelated to this branch.
  • Documentation updated (if needed) — README, DESIGN, and the AGENTS.md map.

W1 condition row semantics (Compare/Verdict/Ref with abs/rel tolerance and the absent-vs-null grid) and W2 submission tree walk (kind-registered Node subclasses, hardened artifact load, MISSING sentinel reads, scopes for Ref resolution), with their unit suites.
Op family (self-registering subclasses, __new__ dispatch), Predicate(subject, target) with MISSING-typed subjects, Interval with __contains__, Aggregate family (count/sum/mean/min/max/any/all) with __new__ dispatch, and the shared MISSING sentinel.
Check/Cohort/Row/Severity (YAML check blocks with when/assert row decode), the catalog view (ruleset root of Ref), the unified Interval (closed/open-upper/half-open/scalar) with the accuracy-gate band (Endpoints Policy Section 2.9.8) and the Section 5.5 concurrency-regions reference algorithm across its three worked examples.
covers returns which RegionKind covers a concurrency value (Rules-compute_regions key names as values) and None outside every region; truthiness keeps its predicate use. Includes removal of swap-file artifacts swept into f7c3a80 and two whitespace-only import-gap touches.
Regions.for_submission (C_max from system_desc.max_supported_concurrency, C_min from the lone [1,32] point), Regions.coverage (the tiled Interval(1, margin.hi)), and a regions Ref root fed by SubmissionNode.from_directory, matching the catalog hook.
Point/Submission carries (offline, concurrency, system_tps per Endpoints Policy Section 8.3/4.1), those fields terminology sweep through compare.py/tree.py docstrings, Submission.offline_ordering (Section 5.7.2 pair of clauses with bool|None where elected/agentic is not-applicable) and Submission.point_cap (Section 5.6 <=32 cap, machine-verified boundaries).
Finding (audit-trail record per evaluated assert row, holds and failures alike), Finding.collect with 3 phases (collect candidates / needs gate with recursive memo + cycle ValueError / evaluate rows via should_run filtering), Check.scope rename plus needs/optional_needs pair, optional Check.fix (fix: str | None per the oracle's Message.fix where-declared semantics), vocab/YAML-key clean cuts (root->scope across fixtures and stubs), the tests/assets/cohorts/stub cohort with PR_CHARTER and devdocs notes.
Submission.point_count_min (Section 5.3: 8 dedicated-offline else 7) and Submission.accuracy_run_count_requirement (Section 4.3: 5 non-agentic else 4), each with its 3-case declaration-shape grid.
Point.point_power_kw (Endpoints Policy Section 4.5.2/4.5.3 formula with 2dp-round-once per :897) with both :1906/:1907 golden rows pinned as a 2-case grid, machine-verified before landing.
…ndary, tolerance-exact

Tree._read_artifact single bounded symlink-safe byte channel (O_NOFOLLOW, cap-before-decode, UnicodeDecodeError funnel, dead-handler cleanup); Verdict.evaluate boundary converts incommensurable pairs to failing verdicts naming both sides + exc-class, retargeting test_incommensurable_ordering_pair_fails_with_why; Op.tolerant ClassVar + Eq/Ne opt-in + Compare._check_shape's tolerance-capability-rejection + _close_to's int-exact-short-circuit (2^53-int-false-pass fixed) + 2 new test-pins.
docs/validation/README.md = the 4-section how-to-use (what the submodule is for / cohort YAML organization / how files are written with worked examples / full decoded-key vocabulary table). docs/validation/DESIGN.md = the what-happens-under-the-hood counterpart (module map, row-operator mechanics, runner cascade, derivation formulas table with Appendix C goldens, notable engineering calls). .agents/devdocs/ de-tracked from git + gitignored per policy (files kept locally for my reference).
…unification

DerivedFields lazy mapping per key (a row naming no derivation runs none); _evaluate_rows restored (Finding per assert row per when-applicable node); Finding.collect 2-arg with test call-sites unified and two regression pins.
- refuse-loud load contracts: FIFO/special-file and empty/non-mapping artifact reads, duplicate keys in both decoders, Point domain plus tree↔points bijection and system_desc agreement, empty/stray cohort directories, ruleset folds, unjudgeable when verdicts (Verdict.broken)

- cohort layout: strict scope files (system/submission/curve/point.yaml, id-keyed blocks); Submission.from_directory builds the typed view from the same artifacts; normalize_offline moves to submission

- CLI: python -m inference_endpoint.validation (--ruleset, --audit)

- docs: README author how-to (predicate forms, queued/passed gating, Reference forms, refusals, keys) and DESIGN module map + data-flow diagram; 3 template_full.yaml regenerated by the templates hook
Wave 1 - strict `Reference` decode (`_decode_operand`): a dotted scalar
must name a scope root or use `{"literal": ...}`; a one-edit near-miss
of a root keeps its misspelled refusal. Non-dotted scalars stay bare
literals. `exists`/`absent`/`not_null` refuse a non-`Reference` `left`
at load: `Compare._outcome` previously returned `subject is not MISSING`
for any literal, a silently always-true `exists` on `left: blah.foo`.

Wave 2 - one-use leaf-helper inlines: `_same_multiset` -> `SameMultiset.holds`,
`_one_edit_apart` -> `_near_root_misspelling`, `_point_from_node` ->
`Submission.from_directory`, `_check_from_entry` + `_load_scope_checks` ->
`Cohort.from_directory`, `_raise_on_needs_cycles` -> `_collect_candidates`,
`_parser` -> `main`. Behavior-neutral (287 passed + CLI smoke unchanged).

Wave 3 - `matches` patterns are always form-literal:
`Compare._matches_right_is_explicit_literal` (a `mode=before` validator -
the {}-literal pocket in `_decode_operand` unwraps to the same bare string
downstream) refuses any non-`{"literal": <str>}` `right`. A pattern is
never a `Reference`, and no ruleset field can carry one anyway, so the
constraint is unambiguous instead of collided with by the missed-reference
guard. `vet_pattern` still compile-checks at load.

Docs mirror each rule (README's References list, unary-operator lead,
`matches` clause, Refusals bullet, closing decode claims; DESIGN.md's
operand-decode bullet names `Compare._matches_right_is_explicit_literal`).
Tests pin the new refusals in `test_compare.py` and drop superseded
literal-reasoning pins (stale-repin policy).

One commit carrying all three waves: `docs/validation/DESIGN.md`,
`tests/unit/validation/test_compare.py` and `docs/validation/README.md`
interleave the waves' hunks at block granularity, so per-wave commits would
have required unverifiable line-level patch surgery. The body order above
is the wave history.
- `_StrictLoader` -> `StrictLoader`: it has three importers beyond
  `tree.py` (`catalog.py`, `check.py`), so the underscore misread
  as private. All 7 sites (`tree.py`, `catalog.py`, `check.py`,
  `docs/validation/DESIGN.md`) renamed in lockstep.
- `Ref.ROOTS` carries `ClassVar[tuple[str, ...]]` explicitly: typed
  rather than an unannotated class attribute, matching its documented
  class-constant (non-field) role.
- `Verdict`'s docstring stops using `row` and says `assertion`, per
  the standing terminology rule (whitespace-vocabulary cleanup only --
  no behavior change: 287 passed + CLI smoke unchanged).
- `matches` example swaps `submission.system_desc.model_name` plus an
  invented slug pattern for a genuine shape check: `point.config.target_cohort`
  against the documented `YYYY-MM-C0`/`YYYY-MM-C1` form
  `20[0-9]{2}-(0[1-9]|1[0-2])-C[01]`.
- `model_naming` is now the membership check it should always have been:
  `in` over `ruleset.models`, scanned by keys (each supported model name).
  `left` corrected to `point.config.model_name` (per `FILE_TO_KEY_MAP`,
  `point.yaml` lands under `point.config`), and the `rule` field now cites
  `endpoints_rules.md` §8.3, Measurement Point (point.yaml).

Policy ground truth: `mlcommons/endpoints_policies/endpoints_rules.md`
§8.2/§8.3 (verified `d2d9da6`, 2026-10-02). The retired sample text
("The model name follows the system.model naming convention") traced to
the AGENTS placeholder-token convention (`system.model`), not policy.
Reformat the docstrings to the default format: summary sentence, `Example:`
block, `Args:`/`Returns:`/`Raises:` with continuations aligned under the
description. Split semicolon-chained clauses into sentences, and document
`Op` ClassVars on the fields themselves.

Docstring and comment changes only: the code AST is unchanged from HEAD in
all three files.
A stray edit in this branch had removed the `steady_state` block, the
`metrics_isl` flag and the extended `dataloader_random_seed` note from all
three `*_template_full.yaml` files (flagged in review). Restored to the
schema-derived template output.
Continuation lines sit one indent past their item instead of aligning under the description text. Reindents catalog/operations/ranges; also rejoins sentinels.py's split `value is MISSING` span.
Cohort directories now require valid_refs.yaml beside the scope files (thread A4): RefTable parses the declared names and required/default/allowlist notes, every row Ref is validated at cohort load (misspellings get a one-edit near_match hint, below-leaf paths are refused), and Ref moves out of compare.py into refs.py with a dependency-free typo_checks.near_match. compare's operand resolution collapses to one _operand/_missing_outcome with a single mapping-only path (no getattr fallback); map_refs lets Cohort.from_directory annotate every row.
Pareto is one results/<system>/<model>/ (threads A5/A16): point cap, C_min/regions, offline rules and point/accuracy counts compute per pareto; each r<N>/ reads its own system_desc.json (A2/A3); c_min_override is honored and validated (A6); elected must sit at C_max, dedicated is exempt from out_of_range_points, and thresholds become named constants (A10/A11/B15). Regions gain for_pareto/for_bounds(*, c_min, c_max), ULTRA_LOW_CONCURRENCY naming, and pinned margin/override constants (A18/A19/A20).
RuleScopes.PARETO and ParetoNode replace CURVE/CurveNode, the cohort file is pareto.yaml, the Ref root is pareto. (thread A26); scopes() folds and node docstrings follow the per-pareto contract.
… in runner

runner validates declared values per node against the cohort's valid_refs.yaml (refusal names the offending run directory) and resolves derived.<name> through the owning Pareto; fold requirements narrow to ruleset; needs/gate docs updated.
Exit 1 counts failing fail findings only; a failing warn reports and passes through with a WARN mark; the summary reads N finding(s), F failing (B blocking); per-check ran counts plus NEVER-RAN on stderr (A9/A22).
DESIGN.md/README.md: rewritten runner cascade, accuracy-gate bounds, out-of-range anchor, when/assert provenance, check-field Type column, measure/aggregate naming (A12/A13/A15/A25/A27/A28).
compare.py: `_finite` guards every resolved operand slot, so a NaN/Inf out
of a fact fails the row with a sentence instead of poisoning the comparison;
`between` refuses null bounds; `Aggregate` guards its measured value and
declared threshold. operations.py: `Matches` caps its subject at 64 KiB,
`SameMultiset`'s unhashable fallback caps its scan at 1024 elements, and `In`
refuses scalar targets. Each refusal names the row and the limit.
tree.py: `Node` compares by identity (`eq=False`) so two nodes with equal
payloads stay distinct; `SubmissionNode.from_directory` refuses a symlinked
root with the remedy in the message; `_read_artifact` spends a shared
`_ArtifactBudget` (8x the single-artifact cap) so a bloated upload cannot
grow the walk unboundedly. submission.py: `system_desc` is excluded from
`Point`/`Pareto` hashing.
…s disavow

runner.py: `evaluate_at` returns one `_Evaluation` (state plus the row
verdicts) that gating and emission both consume, so a dependent check can
never gate on a different evaluation than the one reported; the state folds a
broken row into FAILS as `Verdict.evaluate`'s fail-not-abort policy dictates.
A SILENCED claim now disavows a `needs` dependent like FAILS (a check whose
results were withheld did not vouch); NOT_APPLICABLE stays neutral. Findings
carry the row's own label and the check's `rule`/`fix` for the report side.
catalog.py: `_load_mapping` wraps the YAML parse in a ValueError naming the
file, so a malformed catalog reports which catalog broke and refuses the load
instead of raising a bare parser traceback.
…ames

__main__.py: a report line reads `<mark> <id> [<severity>] <dir>: <row>`
with ` -> <why>` when the row states one, the rules citation on failures, and
an indented `fix:` line when the check declares one; directory names are
escaped so upload-controlled control characters cannot forge lines.
README.md: the report paragraph and check-table rows match, and a malformed
catalog is documented as a load error.
…eal owners

Committed text stops citing session-local shorthand: internal slice/wave labels
in DESIGN and four test module docstrings, finding-code suffixes in two test
docstrings, the stub cohort's planning-directive reference, and
submission.py's "the charter". References to helpers that no longer exist
point at their real owners instead: the `needs`-cycle DFS in
`_collect_candidates`, and `Cohort.from_directory`'s per-file wrapper (was
`_load_check`). DESIGN's reserved-keys caveat and `refs._entry`'s docstring
now state the actual rule (a mapping is the annotated leaf only when `type`
is a type name and every key is reserved), the stale `right`/`lo`/`hi`
field-presence line drops the removed `lo`/`hi`, and the catalog docstring's
example no longer names an unshipped `config/rulesets/<name>/` path.
The checker read its `ruleset` catalog from a directory passed on the command
line (`--ruleset DIR`), a second home for data the benchmark side already
pins in Python (`config/rulesets/mlcommons/models.py` / `datasets.py`). The
catalog now derives from the registered round itself: `Catalog.from_ruleset`
reads `config/ruleset_registry.py`, and a cohort links to one through a new
reserved `cohort.yaml` manifest whose one key, `ruleset`, names the round
(e.g. `mlperf-inference-v6.1`). `--ruleset` is gone; a cohort without a
manifest has no ruleset fold, and `Catalog.from_directory` stays for
hand-built development catalogs.

What a cohort's submissions are valid against is a packaged, versioned
ruleset; the manifest is where a cohort pins it.

Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py
(375 passed); the `--ruleset` end-to-end test is replaced by a manifest-driven
CLI test, and a rule row reads `ruleset.models` of `mlperf-inference-v6.1`.
The validation package is a new top-level module, so AGENTS.md's own rules
put it in the Key Components table and the Code Organization tree in the
same PR. Also corrects the module map's count in docs/validation/DESIGN.md.
…ecker roots

"fold" named the three roots whose namespaces the checker owns, but the term
was never defined in the docs and its reduce sense fits only `derived`
(computed over a pareto's points); `ruleset` is static packaged data and
`regions` a per-pareto computation. `Ref.CHECKER_ROOTS` and the docs now say
what the class is: roots the checker owns - the cohort's `valid_refs.yaml` may
not declare them and no table annotates them. `_validate_fold_needs` becomes
`_validate_ruleset_needs`: it refuses a cohort naming the `ruleset` root the
tree was built without, nothing else.

Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py
(375 passed).
…ore either pass

`_read_artifact` now spends the tree budget before the size refusal: an
oversized artifact is still a read, so a crafted upload of many oversized
files is refused at load instead of multiplying the per-file cap by node
count.

`same_multiset` refuses a pair longer than `_MULTISET_SCAN_LIMIT` before
either pass: `Counter` degrades quadratically on same-hash collisions, so the
cap could not live in the unhashable fallback alone.

Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py
(394 passed).
…ent, empty systems

Cohort load cross-checks a ref table's notes: a `default` and every allowlist
value must match the declared type, and `required` cannot carry a `default`
(a default answers the fact's absence).

Upload load refuses numeric facts no arithmetic can carry (a non-finite
`system_tps`, or a `system_tps`/`concurrency`/`max_supported_concurrency`
outside `float` range) and a `<system>/` directory with no model directories,
so an empty system cannot hide from system-scoped checks.

`_shared_facts` compares by presence and typed value, so a later run's
`c_min_override: null` can no longer pass as equal to a first run's omission.

The stub cohort's range row gates on the dedicated Offline exemption
(Section 5.7.2), the same exemption `Pareto.out_of_range_points` applies, so
a conforming dedicated run draws no spurious warn.

Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py
(394 passed).
`Finding.collect` returns a `Collected` record: the row findings plus one
`CheckRun` per check, counting the nodes it ran at, the nodes its `when`
gated off, and the nodes a `needs` chain silenced. The report words its
`ran at N instance(s)`/`NEVER-RAN` lines from those counts, so a `when`-only
check that ran everywhere is no longer reported as never-run with the wrong
cause, and a silenced check says so.

The CLI's refusal net also takes `OverflowError`, belt for numbers the
load-side guards do not reach.

Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py
(394 passed).
Module-map notes for the tree-budget charge, the ref-note cross-validation,
the numeric guards, the typed descriptor agreement, the empty-system refusal,
the `Collected`/`CheckRun` records, and the stub's gated range row.
"root" carried three senses in the module's prose: a ref's first segment, the
tree's top node, and an upload directory. The docs now call the ref sense a
namespace — a ref's first segment (`point.config.concurrency`'s namespace is
`point`). Code identifiers keep their names (`Ref.ROOTS`, `Ref.SCOPE_ROOTS`),
except the private compare helper, renamed `_near_namespace_misspelling`.

Two refusal strings follow the vocabulary: "is dotted but 'x' is no scope
root" becomes "... is no ref namespace" (the gate was never scope-only — it is
`Ref.ROOTS`), and "fix the root" becomes "fix the namespace".
The user-facing page catches up with the checker as it stands:

- the cohort artifact: `cohort.yaml` is the optional manifest naming the
  packaged ruleset its `ruleset.*` rows read, and a submission may carry
  several `<system>/<model>` paretos - the stray-file rule, the ruleset
  paragraph and an instance note follow
- the report: findings are one per evaluated `assert` predicate (holds
  included), and NEVER-RAN reasons name the gating/silencing state exactly
- the ref vocabulary: the ref-head sense of "root" becomes "namespace", with
  the quoted refusal strings matching what the checker emits
- prose converted to lists through Sections 3 and 4: the valid_refs entry
  forms, predicate kinds and instance cadence, the aggregate lead-in and the
  Possible-keys close
The finer-scope sentence said a dependent runs only if the listed check passes
for every point in the pareto. That overstates it: the chain vouches over the
nodes where the listed check holds, fails or was silenced, while a node its
`when` skipped makes no claim at all -- so a gated-off point neither vouches
nor disavows.
A wrong `cohort`/`submission` argument printed the path alone (`refused:
/tmp/no-such-cohort`), because three `NotADirectoryError(root)` guards carried
the path as their whole message. Each now says which role the path belongs to
and whether it is missing or not a directory -- `the cohort directory <path>
does not exist` / `the submission path <path> is not a directory` -- so a
swapped or mistyped argument reads as a statement instead of a bare path.
Exit 2 and the `refused:` prefix are unchanged.
A directory that is not an `r<N>` point (`r32_rerun`), a symlinked directory, a
point-named entry that is not a directory, and a directory that resolves
outside the upload all escaped every check while the rest of the upload
passed. `_walk` now collects them per level and refuses once, naming each with
its reason -- `<dir> holds entries no check can reach: r32_rerun (not an `r<N>`
point directory)` -- the upload-side counterpart of the cohort's stray-file
refusal. Plain files with other names stay ignored: uploads carry them.
Two present values read as if they were fine while they were not:

- an aggregate's `over` resolving to a non-collection (`17`, a stored `null`)
  returned a plain false verdict -- which as a `when` gate reads as "not
  applicable" and switched the check off by omission. It raises now, the
  `_finite` treatment: `Verdict.evaluate` maps it to a broken verdict, so a
  gate over it refuses loudly and names the row.
- a `dict` leaf's own value was never checked (`_Fact.matches` has no `dict`
  branch and `validate_values` skipped subtrees), so `steady_state: 17`
  resolved nothing while claiming the document shape. The leaf's own value
  must be a block; the values below it stay unchecked.
The checker required `result_summary.system_tps` at load while the benchmark's
report writes the same aggregate as `tps` (`metrics/report.py`; None on a run
that recorded no OSL), so a self-produced upload was refused with exit 2 and
rows over `point.result_summary.system_tps` could only ever read absent.

`Node.read` now surfaces the policy name when the upload carries only the
client's spelling: a shallow copy gains `system_tps` beside the document's own
`tps`, an explicit `system_tps` is returned untouched, and anything that is
not a mapping is left for its own refusal to name. The parsed document stays
as written, and a `tps: null` (no OSL) still refuses -- `needs a numeric
system_tps` -- since the metric is not there to read. Which spelling an upload
must carry is a policy question for MLCommons.
The row side of the client-`tps` bridge: a cohort table declaring
`result_summary.system_tps` plus an upload whose summary carries only the
client's spelling still evaluates the row, where the fact would have read
absent before.
`pareto_at` scanned `self.paretos` per lookup, and `Submission.points`
rebuilt its tuple on every access (the runner reads it while resolving
`needs` and enumerating nodes). Both are computed once in `__post_init__`
now: a `{id(pareto.node): pareto}` map behind `pareto_at` -- a dict hit after
the parent walk -- and a `points` field in place of the property. The API is
unchanged for callers and hand-built `Submission`s, and the multi-pareto
lookup is pinned by the test.
A reader sees two checkers in one repo and no sentence telling them apart:
`compliance/checker.py` validates one completed Edge-Agentic (BFCL) run's
report directory (resolved `config.yaml` plus scorer output) against its
ruleset -- config lock, accuracy gate, dropped-turn validity -- while this
module checks an assembled Endpoints submission against a cohort's declared
checks. Neither imports the other.
…s declared

A declared `int`/`float` fact now reads a numeric string as the number it
spells, and a numeric path component reads a key spelling the same number, so
an artifact that stringifies its numbers or keys a percentile block `"90.0"` is
read the way the submission checker reads it, instead of being refused or read
as absent.

- `decode_number` is the one rule: exact numeric spellings only (`"512"`, and
  the integral `"512.0"` for an `int`; whatever `float` reads as finite). Units,
  separators, non-finite text and non-numeric declarations stay as they were.
- The parser-enforced facts (`_int_fact`, `_finite_fact`) decode through it too,
  so a stringified `concurrency` is read once and identically by the parser and
  by the ref table.
- `Ref.type` carries the leaf's declared type from `RefTable.annotate`, so
  `Ref.resolve` decodes only where a declaration exists: an un-annotated ref (a
  checker-owned namespace, a hand-built row) still reads the artifact raw.
- `_key_for`: the exact key always wins, and equivalent spellings break by
  sorted key, so one artifact always reads the same way.
- `RefTable.validate_values` returns one note per decoding, threaded through
  `Collected.coercions` to the CLI's `coerced:` lines on stderr, so the leniency
  is reported rather than silent.
@nv-alicheng
nv-alicheng requested a review from a team October 23, 2026 15:15
@github-actions

Copy link
Copy Markdown

MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅

@github-actions github-actions Bot added the size/very-large PR Review Policy: >1500 lines or >50 files label Oct 23, 2026
}

# The `derived.<name>` names a row may name (single owner: the table above).
DERIVATION_NAMES: tuple[str, ...] = tuple(_DERIVATION_METHODS)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DERIVATION_NAMES is used: check.py:74 imports it and check.py:112 checks every
derived.<name> ref against it at cohort load, and
tests/unit/validation/test_submission.py asserts the registry matches it. Leaving the constant
where it is.

Comment thread tests/unit/validation/test_compare.py Fixed
"""
return self.type == "dict"

def matches(self, value: Any) -> bool:
Comment thread tests/unit/validation/test_submission.py Fixed
…alyzer nits

The full suite collects every test file by module basename, so
`tests/unit/validation/test_runner.py` collided with the pre-existing
`tests/unit/evaluation/swebench_service/test_runner.py` (and `test_cli.py` with
`tests/integration/commands/test_cli.py`): the CI test job died at collection
with an `import file mismatch`. Both files are renamed to `test_validation_*`,
which is unique in the tree; the module's own suite never caught it because it
collects one directory at a time.

Two analyzer notes settled while here:

- `_Fact.matches` spells out a `dict` arm beside the `_` catch-all, so every
  declared type name has a branch and no path can look like a fall-through.
- `derived["nope"]` in `test_unknown_derivation_is_absent` binds its lookup, so
  the statement is not a bare expression with no effect. The `LoudDerived` stub
  keeps raising `ValueError` (not a `LookupError`): `Verdict.evaluate` turns a
  `ValueError` into the fail-loud verdict the test pins, while a `KeyError`
  would escape the verdict entirely, with a comment saying so.
Review findings on the decoding change, all five addressed:

- `Point.from_node` discarded the decoded `max_supported_concurrency`, so a
  stringified maximum passed the parser and was refused later by the pareto
  shape check as "must declare one integer max_supported_concurrency".
  `_descriptor_facts` now carries the decoded maximum, and decodes a
  stringified `c_min_override` too, while the artifact document itself stays as
  written for the ref table (`coerced:` still reports the decoding). The shape
  check and the region bounds read the carried descriptor.
- `_check_values` matched declared names by exact membership while
  `Ref.resolve` reads a numeric-equivalent key, so a declared `90` skipped an
  artifact key `"90.0"` and left a reachable value unjudged. Both now look the
  key up through `_key_for`.
- The `int` decode tested integrality on a `float` pass, so
  `"512.9999999999999999"` rounded to a whole number and
  `"9007199254740993.0"` lost a digit. `_spelled_integer` reads integrality
  from the spelling with `Decimal`, exactly.
- The `TestResolve` class statement was lost in an earlier edit: its docstring
  sat as a bare string inside `TestMapRefs` and its tests collected there.
- DESIGN's contract-validation paragraph had lost the declaration-refusal
  clause ("a present value its `valid_refs.yaml` declaration will not carry").
def __contains__(self, name):
return True

def __getitem__(self, name):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept as ValueError, with a comment. I ran the suggested KeyError recipe: __contains__ false +
__getitem__ raising KeyError + a get that raises ValueError makes the verdict
expected derived.x to exist, got absent (broken=False) and the test fails —
Verdict.evaluate maps ValueError/OverflowError to the fail-loud verdict this test pins, and
nothing calls Mapping.get on the way there. The stub is deliberately non-standard.

@nv-alicheng

Copy link
Copy Markdown
Collaborator Author

Thanks — both the CI break and the four code-quality notes are settled, plus five review-council
findings on the decoding commit.

CI test job (fixed, now green). The full suite imports every test file by module basename, so
tests/unit/validation/test_runner.py collided with the pre-existing
tests/unit/evaluation/swebench_service/test_runner.py (and test_cli.py with
tests/integration/commands/test_cli.py). CI died at collection with
import file mismatch: imported module 'test_runner' has this __file__ attribute …. The module's own
suite never caught it because it always collected one directory at a time; a whole-tree
--collect-only is now part of the local gate. Both files are renamed to
test_validation_runner.py / test_validation_cli.py: the tree collects clean (3272/3279 collected,
7 deselected, 0 errors), the module suite passes (439), and the test job is green (8m50s) with
every other job on this PR.

Code-quality notes, triaged:

  • DERIVATION_NAMES reported unused — not accurate, no change: check.py imports it (line 74) and
    uses it at line 112 to refuse a derived.<name> ref at cohort load, and
    tests/unit/validation/test_submission.py asserts the derivation registry against it.
  • _Fact.matches mixing explicit and implicit returns — fixed: a dict arm is spelled out beside
    the catch-all, so every declared type name has a branch.
  • LoudDerived.__getitem__ raising ValueError — intentional, kept, with a comment. The bot
    re-flagged it with a KeyError recipe, so I ran that recipe: with __contains__ false and
    __getitem__ raising KeyError, the verdict becomes
    expected derived.x to exist, got absent (broken=False) and the test fails — Verdict.evaluate
    maps ValueError/OverflowError to the fail-loud verdict this test pins, and a KeyError never
    reaches it. The stub is deliberately non-standard; that is its job here.
  • derived["nope"] statement has no effect — fixed: the lookup binds to _ inside pytest.raises.

Review-council findings on the decoding commit (all five addressed):

  1. (high) Point.from_node discarded the decoded max_supported_concurrency, so a stringified
    maximum passed the parser and was refused later by the pareto shape check. The decoded maximum
    now rides on the carried descriptor (with a stringified c_min_override decoded alongside),
    while the artifact document stays as written so the ref table still reports the decoding.
  2. (medium) _check_values matched declared names exactly while Ref.resolve reads a
    numeric-equivalent key, so a declared 90 skipped an artifact key "90.0" and a reachable value
    was never type-checked. Both paths now share _key_for.
  3. (medium) The int decode judged integrality on a float pass, so "512.9999999999999999"
    rounded to a whole number and "9007199254740993.0" lost a digit. Integrality is now read from
    the spelling with Decimal, exactly.
  4. (low) The TestResolve class statement had been dropped in an earlier edit, leaving its
    docstring as a bare string inside TestMapRefs; restored.
  5. (low) DESIGN's contract-validation paragraph had lost its declaration-refusal clause; restored.

Verification: module suite 439 passed; whole tree collects with 0 errors; the cohort still reports
291 findings / 0 failing against the reference submission bundle, with the P90 row reading the
submission's "90.0" key at every measurement point; and a copy of that bundle with stringified
concurrency and max_supported_concurrency now runs clean, reporting both decodings
(coerced: … point.config.concurrency: '512' read as 512, coerced: … point.system_desc.max_supported_concurrency: '1024' read as 1024) where the stringified maximum
previously failed the shape check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/very-large PR Review Policy: >1500 lines or >50 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant