Skip to content

4th: feat(reportgen): fixed webpage-parity schema for results.{csv,json} - #829

Draft
FileSystemGuy wants to merge 79 commits into
reportgen-validator-fixesfrom
reportgen-column-parity
Draft

4th: feat(reportgen): fixed webpage-parity schema for results.{csv,json}#829
FileSystemGuy wants to merge 79 commits into
reportgen-validator-fixesfrom
reportgen-column-parity

Conversation

@FileSystemGuy

Copy link
Copy Markdown
Contributor

4th (stacked on #828): fixed webpage-parity schema for results.{csv,json}

Stacked on top of reportgen-validator-fixes (#828). Part of the reportgen review-tooling stack #826#827#828this. Retarget base to main once the lower PRs land.

What

reports reportgen now emits results.csv / results.json as a fixed contract — exactly the reference columns for all 8 v3.0 results-web-page tables (Results Table Structure.xlsx) plus three discriminator columns (Division, Benchmark Type, Model) so one flat file can carry every table's rows. The column set is never data-driven: a MLCommons staff member opens results.csv in Excel and reduces it to any single web table by deleting the workload blocks + discriminator columns that don't apply.

How

Output projection, not an aggregation rewrite. The internal _aggregate_* helpers (train_mean_of_* / checkpoint_mean_of_* / vdb_* / kvcache_* machine keys, and the empty-metric StatisticsError → INVALID gate) are untouched, so validity behavior is unchanged. A new _final_row() runs inside write_json_file / write_csv_file and cherry-picks each fixed column from the in-memory row. The dynamic *_mean_of_* per-metric means and the trailing issues column are simply no longer emitted — this is an output-only change with no path from pass to fail.

Schema (54 columns)

  • Left edge (13): Public ID, Organization, Division, Benchmark Type, Model, Name, Description, Type, Access Protocol, Availability, RU's, Integrated Client Storage, Usable Capacity (TiB).
  • Workload blocks: Training - …, Checkpointing - …, VDB - …, KVCache - …. Shared reference names (# Client Nodes, Code, Read B/W (GiB/s), …) are workload-qualified so every column key is unique.
  • Division = category upper-cased (CLOSED/OPEN); Model = reference display label (llama3-1t1250B), blank for kvcache/vdb (single-table workloads).
  • Name shows the system name and links to system-description.yaml; Description shows PDF and links to the .pdf.
  • Code/Logs are per-workload (an OPEN row may ship its own code), routed into the row's own block from the run's code-image pointer.
  • KVCache option 1/2/3 map to the reference groups llama3.1-8b Storage Only / Storage + Mem / llama3.1-70b Storage Only.
  • Removed: the machine-key prefix (category/orgname/systemname/benchmark_type/model/accelerator), the issues column, and the now-dead _ordered_fieldnames helper.

Tests

RED-first. Output-file tests migrated to the fixed-schema contract; internal-structure tests (aggregation helper keys) unchanged. All 4 CI suites green: tests/ 2975p·1skip, mlpstorage_py/tests 898p, vdb_benchmark/tests 228p, kv_cache_benchmark/tests 238p.

wolfgang-desalvador and others added 3 commits July 21, 2026 12:35
* Fix calculation of checkpoint gap in reports

* Implement fallback to collection last timestamp
…chema

The v3.0 results web page is 8 tables sharing a System-Under-Test block.
reportgen must emit ONE flat results.csv/json that a staff member reduces
to any single table by deleting non-applicable columns. That requires a
FIXED schema (exact reference columns + Division/Benchmark Type/Model
discriminators), not the current data-driven column set.

Pins the emitted-file contract: header is EXACTLY the 54-column fixed
schema; JSON keys match in order; internal machine columns (category/
orgname/systemname/benchmark_type prefix, *_mean_of_* dynamic means,
trailing issues) are gone; values cherry-picked into fixed columns
(Division=CLOSED/OPEN, Model=display label, kvcache option N -> its
fixed group). Fails against the current dynamic _ordered_fieldnames.
results.{csv,json} is now a FIXED contract — exactly the reference columns
for all 8 v3.0 tables (Results Table Structure.xlsx) plus 3 discriminator
columns (Division / Benchmark Type / Model), never data-driven. A staff
member opens results.csv in Excel and reduces it to any single webpage
table by deleting the workload blocks + discriminators that don't apply.

Approach: output projection, not a rewrite of aggregation. The internal
_aggregate_* helpers (train_mean_of_* / checkpoint_mean_of_* / vdb_* /
kvcache_* machine keys, and the empty-metric StatisticsError -> INVALID
gate) are untouched, so validity behavior is unchanged. A new _final_row()
projection runs inside write_json_file/write_csv_file and cherry-picks each
fixed column from the in-memory row; the dynamic *_mean_of_* means and the
trailing issues column are simply no longer emitted (output-only change).

Schema (54 cols): 13-col left edge (Public ID, Organization, Division,
Benchmark Type, Model, Name, Description, Type, Access Protocol,
Availability, RU's, Integrated Client Storage, Usable Capacity) then
workload-qualified Training/Checkpointing/VDB/KVCache blocks (shared metric
names are unique-per-column, so they carry a 'Training - ' etc. prefix).
- Division = category upper-cased; Model = reference display label
  (llama3-1t -> 1250B), blank for kvcache/vdb (single-table workloads).
- Name shows the system name -> .yaml; Description shows 'PDF' -> .pdf.
- Code/Logs are per-workload (an OPEN row may ship its own code), routed
  into the row's own block from the code-image pointer.
- KVCache option 1/2/3 -> the reference groups (llama3.1-8b Storage Only /
  Storage + Mem / llama3.1-70b Storage Only).
- Removed the machine-key prefix (category/orgname/systemname/
  benchmark_type/model/accelerator), the issues column, and the now-dead
  _ordered_fieldnames helper.

Output-file tests migrated to the fixed-schema contract; internal-structure
tests (test_aggregation helper keys) unchanged. All 4 CI suites green.
@github-actions

Copy link
Copy Markdown

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

FileSystemGuy and others added 26 commits July 21, 2026 17:26
…scores

Two-invocation checkpoint submissions (Rules §2.1.23/§4.7.1: mandatory
write→read split when checkpoint-per-node < 3x client RAM) blank all four
score columns. Each phase dir carries only its own scalars (write dir:
save_*; read dir: load_*), so _scalar_mean's all-summaries-must-be-numeric
gate sees [4.64, None] and returns None.

Adds three cases to TestCheckpointingFinalTableColumns:
- split write→read populates all four (RED — reporter's Alluxio bug)
- two combined invocations average per direction (locks averaging)
- a metric missing from a phase that WAS configured to produce it stays a
  loud blank, never a silent partial-mean (locks the guard)
…t invocations

Two-invocation checkpoint submissions (Rules §2.1.23/§4.7.1 write→read
split, mandatory when checkpoint-per-node < 3x client RAM) blanked all four
score columns: the write dir carries only save_* and the read dir only
load_*, so the old all-summaries-must-be-numeric gate saw [4.64, None] and
returned None.

Replace _scalar_mean with _directional_mean: classify each invocation as a
write/read producer from its configured checkpoint.num_checkpoints_{write,
read} (CLOSED forces 10 or 0), then take the mean over the producing phase
only. The loud-failure guard (D-23 / PITFALLS #3: no silent partial-mean)
is preserved but scoped to the producing phase — a scalar missing from an
invocation that WAS configured to produce it stays a blank, never a partial
mean over the survivors. Falls back to a present-only mean when the phase
signal is absent (legacy / OPEN packages) so those rows don't regress.

Fixes the reporter's Alluxio CLOSED v3.0 case: llama3-8b/70b/405b now
populate Write/Read B/W and durations from their split timestamp dirs.
…heck (#831)

Datasets generated before the floor->ceil datagen alignment (commit
f9d414d, first shipped in mlpstorage 3.0.43) were sized with integer
floor division in rules/utils.py. Rule 3.1.2 in the submission checker
now re-derives the threshold with ceil, so it rejects a valid
pre-alignment dataset by exactly one file (a submitter hit "actual files
4710038 < minimum required 4710039" on a Jun-28 run made with 3.0.24).

Relax the 3.1.2 threshold to ceil-minus-one for the v3.0 round. We can't
just floor it: datagen floored via integer `//` while this check
recomputes the same formula as a float chain, so float drift can push
floor() one above the recorded count and still reject. ceil-1 equals
floor() for non-integer ratios and grants one file of slack exactly at
the integer boundary where that drift bites, absorbing the bounded
<=1-file divergence. Stays int-vs-int (no "N < N" display bug) and still
rejects genuinely undersized runs (short by >1 file).

Validator-only change; rules/utils.py (datagen/runtime verifier) stays
on ceil. Revert to plain ceil once the v3.0 round closes -- post-3.0.43
datagen ceils, so newer datasets pass either way.
The VectorDB workload grouping key (D-06) is
(category, orgname, systemname, engine, index_type) — it does not
include `command`, so a workload's datasize/datagen/run leaves all
group under one key and arrive at `_aggregate_vdb` as a single `runs`
list in discovery order. Only the `run` leaf carries the native
query-phase metrics (statistics.json / summary.json); the datasize and
datagen leaves have none.

`_aggregate_vdb` used `run = runs[0]`, so whenever a non-run leaf sorted
first the QPS / latency / recall / read-B/W columns came out blank even
though the run leaf's statistics.json held the real values (identity
columns still populated because they live in every leaf's parameters).

Select the `run` invocation for metrics and recall, falling back to
runs[0] only when no run leaf is present (e.g. a datasize-only tree) so
identity columns still populate. Resolve identity columns across all
grouped leaves (run first) so a value recorded only on the datasize leaf
(e.g. num_vectors / dimension) still fills in.

Regression test: a [datasize, datagen, run] group now reads its metrics
from the run leaf.
…thering) into reportgen-column-parity integration branch

# Conflicts:
#	mlpstorage_py/rules/models.py
…orts mode

An assembled multi-submitter submission tree has no single-org
mlperf-results.yaml and must stay read-only, so reportgen against it
dies at E101 today. reportgen already supports orgname=None
(discover_scan_roots flat-layout fallback); pin the intended contract:
reports mode proceeds with args.orgname=None when the sentinel is
absent, keeps sentinel resolution when present, and benchmark modes
keep the hard-fail.
…mode

reportgen against an assembled multi-submitter submission tree died at
E101 because _apply_lay03_orgname_gate hard-requires the
mlperf-results.yaml sentinel for every --results-dir-bearing mode. Such
a tree has no single-org sentinel, must stay read-only (so
'mlpstorage init' is not an option), and ReportGenerator already
handles orgname=None via the discover_scan_roots flat-layout fallback.

For mode=='reports' only: catch ResultsDirNotInitializedError, set
args.orgname=None, log, and proceed. Sentinel-bearing dirs keep exact
prior behavior (canonical-slice discovery); benchmark modes keep the
locked hard-fail. All 4 CI suites green.

Worklist: .planning/validate-cleanup-worklist.md item A9.
An assembled multi-submitter tree (no sentinel, orgname=None) must:
- A10: yield every {closed,open}/<org>/results/<system> slice as scan
  roots so structure validation passes, instead of validating the raw
  root and exiting 3.
- A11 (decision (a), 2026-07-24): write the global results.{csv,json}
  at the tree root covering every org, plus a per-org rollup in each
  <div>/<org>/results/ — instead of rebinding to the alphabetically
  first org and dropping the aggregate for everyone into its tree.
A10: discover_scan_roots with orgname=None now probes the canonical
{closed,open}/<org>/results/<system> shape for EVERY org and returns
each system slice as a scan root. The raw root of an assembled
multi-submitter tree holds only division dirs and code-pool dirs, so
pre-fix it failed structure validation and reportgen exited 3 before
generating anything. Flat layouts still pass through unchanged.

A11 (decision (a), 2026-07-24): _resolve_effective_results_dir now
collects every (division, org) canonical match instead of returning on
the first org. With >= 2 distinct orgs the tree root stays the
effective results dir — the global results.{csv,json} covering every
org lands there — and each org additionally receives its own rollup in
<division>/<org>/results/. Single-org trees keep the established
rebind-to-org-results behavior exactly.

Also relocates the D-08 top-level assertions back to the end of
test_multi_orgname_produces_single_top_level_file_with_mixed_rows (the
RED commit accidentally split that test) and updates its stale
first-org-wins comment.
Special build for one submitter: a >30s write→read inter-phase gap in a
split-mode checkpointing submission must be reported as a warning rather
than a hard error, in both the submission checker
(cache_flush_validation) and the reportgen-side rules checker
(check_invocation_structure).
Downgrade the >30s write→read inter-phase gap breach from a hard error
to a warning in both enforcement points:

- submission checker cache_flush_validation: warn_violation instead of
  log_violation; the check no longer returns False for a gap breach
- reportgen-side check_invocation_structure: warning-severity CLOSED
  issue instead of PARAM_VALIDATION.INVALID

Missing/unparseable timestamps and overlapping phases remain hard
errors.

Originally authored on the checkpoint-split-gap-warning special build
(PR #834) for one submitter; Curtis decided 2026-07-24 the relaxation
applies to the whole v3.0 review tree, so it lands on the integration
branch for all submitters (worklist A7). Cherry-picked from c45278c.
…k label

Third enforcement point of the A7 §4.7.1 relaxation: the 2.1.24 gap
breach warns instead of invalidating, and the message carries an
'upper bound' label when the gap was measured via the DLIO summary
fallback (no invocation bookends in metadata), since that path charges
startup/collection overhead against the quiet window. Unparseable
timestamps remain hard errors.
…lback measured

Completes worklist A7: the third gap-enforcement point downgrades to a
warning like cache_flush_validation and check_invocation_structure. The
message gains an upper-bound note when either gap endpoint fell back to
the DLIO summary fields (bookends absent from metadata) — that path
charges read-side startup and write-side cluster collection against the
quiet window, so the number overstates the true gap on large
topologies. Unparseable timestamps remain hard errors.
…rning

A1: training datagen dirs can never hold summary.json (DLIO
generate_only skips stats.save_data), so their absence must not warn;
run-dir summaries keep warning. A4: a missing metadata file must warn
once (find-time, with dir context), not the find+load pair.
A1: the training datagen summary.json load is warn_if_missing=False —
DLIO's generate_only path never writes one, so its absence was pure
noise (77 of 79 baseline 'Could not load Summary log' warnings).
Presence still loads; run-phase summaries keep warning.

A4: find_metadata_path returns None (after its own directory-context
warning) instead of a nonexistent default path, and load_single_log
treats a None path as an already-reported miss — collapsing the
46 find+load warning pairs to singles.
…uity

--data-dir is optional for training datasize, so null-dir records must
match any run; a single datasize record matches regardless of dir
(cardinality comparison is the rule's point); pruned metadata is
MALFORMED, not MISMATCH. MISMATCH fires only with >=2 distinguishable
records none of which match — the existing mismatch test gains a second
distinct record accordingly.
_match_datasize_for_run gains two fallbacks when no exact data_dir
match exists: the all-null-dir group matches any run (--data-dir is
optional for datasize), and a single overall record matches regardless
of dir. The mismatch caller keys off the readable grouping instead of
raw datasize_files, so pruned-metadata trees report DATASIZE-MALFORMED
without a per-run MISMATCH pile-on. Eliminates the bulk of the 160
baseline 3.3.1 warnings as false positives.
The write+read invocation pair shares checkpoint_size_GB and host
memory, so the per-invocation loop emitted the identical advisory twice
per workload. Warn once per distinct (size, memory, hosts) tuple.
…ation

The 5.1.1 scale-table / 5.3.2 recall-target / 5.3.3 query-target
'not yet defined in config.py' notes describe validator incompleteness,
not submission problems — they no longer appear as per-submission
WARNINGs (12 baseline lines). BaseCheck gains info_violation so the
notes keep the grep-stable [rule_id ruleName] prefix at INFO.
Subclass names already end in 'checks', so the literal in the format
produced 'Some directory checks Checks failed for' (107 baseline
lines). No test depended on the old sentinel — the docstring claim was
stale.
…rtial failures

Deterministic selection plus two warnings, on the evidence that the
crux-eagle case is not unique:

- `_select_kvcache_run` replaces `runs[0]`. Discovery order was genuinely
  arbitrary — of the three v3.0 groups holding two real runs, it
  published the earlier run for two and the later run for the third.
  Earliest `run_datetime` has no better claim than latest, but it is a
  rule rather than an accident.

- `_kvcache_measured_runs` excludes `datasize` from selection. The
  kv_cache grouping key has no `command` component (D-05), so a
  `datasize` leaf shares the group with the run it sized; it writes no
  summary.json. An earlier draft of this fix blanked all twelve KVCache
  metrics on v3.0-0019/0021/0023/0142 before the expectation diff caught
  it, which is now pinned by a test.

- Collapsed-group and partial_failure warnings ride on `Result.issues`.
  The collapsed-group warning requires two or more actual `run`
  invocations, so ANL polaris-nvme's pair of `datasize` leaves stays
  quiet.

Against the v3.0 tree: 3 collapsed groups (ANL crux-eagle 3 runs,
Everpure 51hosts_20 and 51hosts_30 2 runs each) and 1 partial failure
(open/farmgpu llama3.1-70b-instruct, 45 missing rank files). Six of the
seven collapses the first draft reported were datasize siblings, not
dropped measurements.

One published row moves: v3.0-0019 switches from its later run to its
earlier one, since discovery order had picked the later.

Refs #836
…ilter

format_issues_list(show_all=False) drops every Issue whose validation is
CLOSED, conflating two orthogonal fields: validation is the submission
category, severity is whether a human needs to look. A finding that does
not disqualify a CLOSED submission but does mean its published row is
misleading is exactly CLOSED + severity="warning".

Two families are invisible today — the datagen leaf-presence warnings,
written to be "worth surfacing but not invalidating", and the #836
kv_cache collapsed-group and partial_failure warnings. Both reach only
the log stream, alongside 183 other warnings.

Refs #836
…h a stray None

The badge is the first thing read, and a green [CLOSED] on "row built
from 1 of 3 runs" reads as approval of the row. The optional `parameter`
field also prints literally as "None: " when unset, which every #836
warning and every rules-strict INVALID message is.

Refs #836
… list

format_issues_list(show_all=False) now keeps CLOSED issues whose severity
is "warning", badges them [WARN] in yellow rather than green [CLOSED],
drops the "None: " placeholder when an issue carries no parameter, and
absorbs the message's own "[WARN] " lead-in so badges don't stack.

Ordinary CLOSED chatter still goes. Against the v3.0 tree this adds five
lines to the printed summary — the four #836 kv_cache warnings and one
datagen leaf-presence warning that had been written to be surfaced and
never was.

Refs #836
A kv_cache or vector_database run leaf missing its *_metadata.json is
the only case where the metadata file is load-bearing: DLIO training
and checkpointing runs still resolve their type through Hydra configs.
The leaf raises "Could not determine benchmark type", get_runs_files
logs "Failed to load run from <dir>" at WARNING and drops it, and the
workload publishes with every metric column blank.

The existing warning names neither the consequence, nor the missing
input, nor that restoring one small file recovers it — and it is one
line in a ~15,000-line reportgen pass.

Pins #835 suggestions 1 and 2:

  1. get_runs_files records each dropped leaf as a SkippedRunDir
     (path, reason, actionable detail) and its warning names both the
     blank-metrics consequence and *_metadata.json;
  2. reportgen ends with a SKIPPED RUN DIRECTORIES section, including
     on the all-dropped path where print_results short-circuits to
     "No results to display".

Also covers the present-but-incomplete metadata case, which drops
identically but is not described in #835, and the multiple-metadata
continue. Negative controls pin no false positives on a clean tree and
back-compat for callers that pass no skipped list.
get_runs_files now takes an optional `skipped` list and records each
directory it could not load as a SkippedRunDir(path, reason, detail).
Three reasons are distinguished:

  undetermined_type  no usable *_metadata.json and no DLIO workflow
                     signal — the kv_cache / vector_database case, where
                     the metadata file is the only type signal and the
                     remedy is restoring one small file, not rerunning
                     the benchmark;
  multiple_metadata  more than one *_metadata.json in a leaf;
  load_error         anything else.

The first is separated from the rest by a new BenchmarkTypeUndetermined
exception rather than by matching the message text. It subclasses
ValueError, so existing handlers are unaffected.

Each warning now names the consequence ("metrics will be BLANK in
results.csv") and the remedy instead of only "Failed to load run".
ReportGenerator collects the drops across all scan roots, logs a count
mid-pass, and prints a SKIPPED RUN DIRECTORIES section at the end —
including on the all-dropped path, where print_results short-circuits
to "No results to display" and previously said nothing about what it
had thrown away.

Verified against a hardlink clone of the v3.0 submission tree: zero
directories skipped as it stands today, and with one Everpure kv_cache
and one TTA vector_database metadata file moved aside, both are named
in the section with the correct count.

Seven get_runs_files test doubles in test_reporting.py pinned the old
two-argument signature. accumulate_results wraps the call in
`except Exception`, so the TypeError surfaced as an empty scan rather
than an error — the doubles now take **kwargs.
…remedy

ResultsDirectoryValidator reports errors as DirectoryValidationError
records (path, error_type, message, suggestion) but warnings as bare
strings — twelve append sites with no path, no category, no remedy.

The gap is worst in _validate_model_dir. A model directory whose
children are neither YYYYMMDD_HHMMSS run dirs nor known command dirs
yields one line — "No valid run directories found in <dir>" — and the
walk stops there. Nothing beneath those children is examined, so any
runs inside them are invisible to both validation and the report. The
message states none of that. It is live on the v3.0 tree against
closed/ANL/results/crux-eagle/kv_cache/llama3-8b-10u, whose children
are 1nodex8ppn / 8nodex8ppn / 64nodex8ppn.

Beside it, a quieter gap: _validate_command_dir warns about children it
does not recognize, _validate_model_dir has no such branch, so junk
sitting next to real runs is dropped in silence.

Pins:

  - warnings become DirectoryValidationWarning with the same four
    fields as errors, and every one carries a path and a suggestion;
  - a subtree that could not be descended names each child it could not
    interpret and says it was not examined;
  - unrecognized siblings of valid runs are reported, without either
    double-reporting the all-invalid case or costing a valid run its
    count;
  - the detail reaches a human — get_error_report renders path and Fix,
    and reportgen logs the message rather than a dataclass repr.

Same property as the #835 skipped-run work — never drop part of the
tree without saying so — applied to the structural layer.
DirectoryValidationWarning gives warnings the four fields errors have
had all along — path, warning_type, message, suggestion — across all
twelve append sites. Four types: unexpected, empty, incomplete, and
unreachable, the last meaning a subtree the walk could not descend, so
anything inside it is invisible to validation and the report alike.
__str__ returns the bare message so anything that stringifies a warning
keeps reading as prose.

_validate_model_dir gains the branch its command-level twin already
had. Children that are neither YYYYMMDD_HHMMSS run dirs nor known
command dirs are now collected and named — in the aggregate warning
when nothing in the directory is valid, individually when real runs sit
beside them. Never both, so a directory is reported once. Previously
the second case was silent.

The aggregate message now says what it found instead of what it
expected and that nothing below was examined. Against the live tree:

  No run directories found in .../crux-eagle/kv_cache/llama3-8b-10u:
  none of its 3 subdirectories is a YYYYMMDD_HHMMSS run directory or a
  known command directory (['run', 'datagen', 'datasize']). Found
  instead: 1nodex8ppn, 64nodex8ppn, 8nodex8ppn. Nothing below them was
  examined — any runs they contain are invisible to both this
  validation and the report.
      Fix: Place each run at <model>/<command>/<YYYYMMDD_HHMMSS>/ ...

replacing "No valid run directories found in <dir>".

get_error_report renders each warning with its path and fix, matching
the error block, and reportgen logs message plus fix rather than a
dataclass repr. Five assertions in test_directory_validator.py move
from substring-on-string to .message.

Whether the validator should descend such subtrees anyway is a
behavior question that overlaps the crux-eagle layout decision in #836
and is deliberately not settled here.
Earliest-`run_datetime` selection was a rule rather than an accident, but
it was still the wrong rule. A submitter iterates — run, change
something, run again — so the last run is the one they mean to publish.
Everpure's 51hosts_20 and 51hosts_30 each package exactly that pair, and
earliest-wins publishes the first attempt of each.

Latest alone is not enough either. ANL crux-eagle's two later runs lost
70% and 97% of their per-rank result files with all 9 mpirun trials
exiting non-zero; their figures are an fmean over the 3-30% of ranks that
survived. Proved against a copy of the tree: shifting the 64-node run's
run_datetime to sort last publishes 0.0 tok/s and a 281-second P95 into
v3.0-0005. So: latest error-free run, falling back to latest when none
is error-free.

Two existing tests re-pinned (earliest -> latest), three added:

- prefers-latest-error-free, and the collapsed-group warning marks which
  dropped runs recorded a partial failure, so the choice reads as
  principled rather than positional.
- the all-failed fallback, which must still publish deterministically and
  flag the published run.
- a summary-less leaf must not win on recency. ZettaLane's
  run/20260627_194318 is one: option dirs, trial logs and metadata, but
  its summary is under the older kvcache_run_summary_*.json name, so
  every pass-through metric reads blank. Naive latest-wins would let such
  a leaf blank a row that has real measurements.

The datasize-filter test now puts the datasize leaf LATER than the run
(both timestamps Everpure 51hosts_20's own) so it keeps guarding under
the new rule instead of passing vacuously.

Refs #836
`_select_kvcache_run` now takes the LATEST error-free run in the group,
falling back to the latest of any kind when none is error-free. Rationale
is the submitter's own workflow: run, change something, run again — the
last run is the one they mean to publish. Everpure's 51hosts_20 and
51hosts_30 each package exactly that pair.

`_kvcache_run_defect` decides eligibility from the run's own summary and
returns the marker a warning shows:

- `partial_failure` — an option lost per-rank result files, or an mpirun
  trial exited non-zero (`trial_failures`). Recency must not outrank this:
  crux-eagle's two later runs lost 70% and 97% of their ranks with all 9
  trials failing, so latest-alone publishes 0.0 tok/s and a 281-second
  P95.
- `no summary.json` — nothing to publish from, so every pass-through
  metric would read blank. ZettaLane's run/20260627_194318 is such a leaf
  (option dirs, trial logs, metadata; summary under the older
  kvcache_run_summary_*.json name).

Candidates are read with a new `_load_workload_summary(quiet=True)`:
selection consults every run in the group, and the missing-summary
warning belongs to the run that gets published, not to each one weighed.

The collapsed-group warning now marks each dropped run's defect, so a
later run in the dropped list reads as the rule working rather than as
positional accident:

  published 1nodex8ppn (20260723_062638); dropped
  8nodex8ppn (20260723_191548) [partial_failure],
  64nodex8ppn (20260724_051511) [partial_failure]

Full-tree diff, 179 rows, before/after on a copy of
submissions_storage_v3.0 @ be782229a: exactly 2 rows move, both Everpure
iteration pairs advancing to their later run (v3.0-0019, v3.0-0021).
v3.0-0019 improves on all 12 cells; v3.0-0021 improves throughput and
bandwidth but its P95 latencies rise — the rule is "latest", not "best",
so it does not cherry-pick. crux-eagle (v3.0-0005) is unchanged: it was
already publishing the clean 1-node run by luck, and now does so by rule.

All four CI suites pass (238 + 3083 + 911 + 228).

Refs #836
A workload good enough to extract, verify and aggregate must appear in
the table that is supposed to contain it. crux-eagle's does not: its
model directory holds a header-only results.csv while the top-level table
publishes v3.0-0005 from the same runs.

Placement and extraction disagree about what a run is. Extraction reads
each leaf's self-describing *_metadata.json, so a leaf named after its
topology rather than its timestamp still yields a BenchmarkRun.
`_model_group_folder` instead hops a fixed two levels up from the leaf on
the assumption it sits at `<model>/<command>/<ts>/`; one level shallower,
those hops land ON the kv_cache benchmark-type directory. The row is
written to `<system>/kv_cache/results.csv` — a path the canonical layout
has no table at — and the real model directory gets its header-only file
from the D-03 empty-model-dir pass.

`closed/ANL/results/crux-eagle/kv_cache/results.csv` is the only such
file in the v3.0 tree; every other system's kv_cache table sits one level
deeper, inside its model directory.

Two tests, both against the full ReportGenerator pipeline on a planted
two-run flattened tree:

  - the model dir's results.json must hold the row the top-level table
    publishes, with the same Public ID;
  - nothing is written at the benchmark-type directory.

Refs #836
…l dir

`_model_group_folder`'s hop count is right at canonical depth and
overshoots below it. `_clamp_to_benchmark_type_subtree` now checks the
result against the `<system>/<benchmark-type>/` ancestor: if the hops
landed on that directory or above it, the model directory is the ancestor
that is a direct child of it, and the row goes there instead.

Canonical trees are untouched — their hop result already sits below the
anchor, so the clamp returns it unchanged. Fixture trees that plant
`<model>/run/<ts>/` straight under the results root have no
benchmark-type ancestor to anchor on; those also pass through unchanged.

When the leaf itself is the only level below the benchmark-type directory
there is no model level to write a table into. Returning the
benchmark-type directory would write the phantom this fix removes;
returning the leaf would write results.csv inside a run directory. It
returns None and logs the expected layout instead, so the row still
reaches the rollups and the reason is on the record. No such tree exists
in v3.0 today.

Verified on a copy of submissions_storage_v3.0 @ be782229a:

  - crux-eagle/kv_cache/llama3-8b-10u/results.csv now holds the row it
    was missing (1 row, was header-only), and nothing is written to
    crux-eagle/kv_cache/ any more. That stale file is committed in the
    submission repo from an earlier run and needs deleting there —
    reportgen never prunes (D-04).
  - 179 published rows, 0 cells changed. Placement decides which file a
    row is written to, not what it says.
  - One empty-model-table-with-a-published-rollup case remains, from a
    different cause: OpenLake's vdb runs carry empty `parameters`, so
    _derive_workload_key's `engine`/`index_type` are both "" and its
    HNSW and DISKANN runs land in ONE group. The group is flagged
    [INVALID] "Inconsistent models across runs", the row publishes as
    HNSW, and five DISKANN runs appear in no table. Left alone here:
    fixing it adds a published row and renumbers Public IDs.

All four CI suites pass (238 + 3116 + 911 + 228).

Refs #836
Two rows publish today with every measurement cell blank. The fixed final
schema has no phase column and no issues column, so neither is
distinguishable from a real result that happens to read zero:

  v3.0-0006  ANL polaris-nvme  — kv_cache group holding only two
             `datasize` invocations. R3 keeps training/checkpointing
             auxiliary phases out of the rollups via the issue-#771/#791
             6-element workload keys, but kv_cache and vector_database
             keys are always 5-tuples, so their auxiliary groups were
             never marked.
  v3.0-0138  ZettaLane mayascale — a `run` group whose summary is named
             kvcache_run_summary_<ts>.json, so summary.json is absent and
             every pass-through column reads blank.

Three tests. The datasize fixture deliberately keeps a summary.json,
which real kv_cache datasize leaves do not have (0 of them in the v3.0
tree) — that leaves `# Client Nodes` populated, so the test cannot be
satisfied by a blankness check and forces the phase-aware rule instead.
The no-summary test additionally pins that the withheld row is absent
from the MODEL-level table too, not just the rollups, and that the log
names the run directory and says the row carried no measurements. Third
test is the negative control: a run with a summary still publishes as
v3.0-0001.

Refs #836
Withholding ANL polaris-nvme's kv_cache row is right, but as written it
happens in silence: the row is marked auxiliary and dropped from the
rollups with no diagnostic, so a submitter sees a workload vanish with
nothing to act on.

For that system this is the ONLY signal available. Its `run` leaf
(kv_cache/llama3.1-8b/run/20260721_203800) holds trial logs, option_1 and
option_2 but no *_metadata.json, and the #835 skipped-run reporting never
names it: a full-tree pass over submissions_storage_v3.0 @ be782229a logs
zero "Could not find metadata" lines and prints no SKIPPED RUN
DIRECTORIES section, because a leaf with no metadata is not enumerated as
a run directory in the first place. Same for open/farmgpu's two
pointer-only leaves. That gap is #835's to close; this pins that the
withholding itself is at least explained.

Extends the datasize-only test: the log must name the system and say the
group holds no measurement invocation.

Refs #836
Two mechanisms, one per cause found in the v3.0 tree.

`_is_auxiliary_only_group` marks a kv_cache / vector_database group that
holds no `run` invocation, extending R3 to the 5-tuple keys that have no
`command` component to lengthen. ANL polaris-nvme's kv_cache group holds
two `datasize` leaves and nothing else — its `run` leaf lost its metadata
(#835) — and published v3.0-0006 with every metric blank.

Judged on the group's CONTENTS, not on `Result.benchmark_command`. The
first draft marked by that label and withheld ALL FIVE vector_database
rows in the tree: a vdb group mixes datagen/datasize/run under one
5-tuple key, so its label is only the first run's command, and every vdb
group in v3.0 is labelled `datagen` while holding five real `run`
invocations. Caught by the full-tree diff, now pinned by the negative
control.

`_withhold_metricless_row` drops a measurement row whose workload block
is entirely blank, from the leaf table and the rollups alike so the two
stay consistent. ZettaLane mayascale (v3.0-0138) is the case: its run's
summary sits under the older kvcache_run_summary_<ts>.json name, so every
pass-through column reads blank. Deliberately conservative — only a
wholly blank block qualifies, Code/Logs excluded since a code image is
captured before the run; configuration cells like `# Client Nodes` count
as content, so a thin row still publishes and the #835/#836 warnings
describe it. Withholding a real submission's row would be worse.

Both paths log. `_log_auxiliary_only_withholding` exists because the
polaris drop was otherwise silent: that leaf holds trial logs and option
dirs but no *_metadata.json, and a leaf with no metadata is never
enumerated as a run directory, so a full-tree pass logs zero "Could not
find metadata" lines and prints no SKIPPED RUN DIRECTORIES section for
it. Same for open/farmgpu's two pointer-only leaves — a #835 coverage gap
worth reporting there. Training and checkpointing keep their existing
silence: their auxiliary phases sit beside a `run` group that publishes.

Full tree @ be782229a: 179 rows -> 177, exactly the two all-blank rows
gone, zero remaining rows with an empty workload block, and both drops
named in the log. Public IDs are positional and regenerated per run, so
155 rows shift up a position or two. polaris-nvme keeps its
Rules-mandated datasize leaf file (1 row); mayascale's model table is now
empty, matching the rollups.

All four CI suites pass (238 + 3119 + 911 + 228).

Refs #836
Public IDs are positional: sorted, then numbered v3.0-0001 upward on every
run, so withholding or inserting a row renumbers everything after it. That
is fine until reviewers, issues and assignment documents cite IDs by number
-- then a stale citation does not look broken, it points at a different,
valid row. The v3.0 tree renumbered twice today for exactly this reason.

Pins IDs to a `public_ids.json` registry beside the global results.csv:

- Opt-in by presence. No file, no behavior change -- numbering stays
  positional and nothing is written. Two tests guard this so existing trees
  and the whole current test suite keep their semantics.
- Seeded from the first run, so bootstrapping does not mean hand-authoring
  177 entries.
- Sticky thereafter: a row keeps its ID however the table re-sorts around
  it; a genuinely new row mints max+1.
- IDs are never reused. A row that stops publishing leaves its ID reserved
  and a gap in the table; a later new row mints ABOVE the gap, never into
  it. Reusing a number would silently redirect every old citation. Restoring
  the row restores its original ID.
- A hand-authored registry wins over what a positional pass would produce
  (this is the migration path), and a stale `next_index` is repaired from
  the highest ID present rather than being allowed to mint a duplicate.

Identity is the generator's own workload key (D-05/D-06) plus benchmark
type, NOT row position and NOT the display columns: the CSV `Model` cell is
blank for kv_cache and vector_database while the key's id1/id2 carry model +
performance profile / engine + index type. Two kv_cache profiles for one
system are two rows that a display-column key would collapse into one.

Two identical identities cannot be told apart by any registry, so that case
must be reported out loud and the IDs kept unique rather than one being
issued twice.

9 failing, 4 passing (the opt-in guards, already correct).

Refs #836
Opt-in by presence: a tree carrying `public_ids.json` beside its global
results.csv gets pinned ids; a tree without one keeps positional numbering
and never has the file created for it. That keeps every existing tree and
the whole existing suite semantically unchanged -- including
test_reportgen_public_id.py's `test_inserting_an_earlier_org_renumbers`,
which pins the positional behavior on purpose.

Identity is the workload key, carried on the row as `__workload_key__`
rather than reconstructed later from the display columns. The CSV `Model`
cell is blank for kv_cache and vector_database while the key's id1/id2 hold
model + performance profile / engine + index type, so a display-column key
would collapse two kv_cache profiles for one system into one row and issue
them the same id. `_final_row` projects the internal key away, so nothing
reaches the output.

`_pin_public_ids` guarantees ids are never reused: `next_index` is repaired
from the highest id present rather than trusted, so a hand-edited or stale
registry cannot mint a duplicate, and a retired row's entry stays in the
file reserving its number. Restoring the row restores its original id. New
rows mint above the highest ever issued, never into a gap -- reusing a
number would leave every earlier citation pointing at a different, valid
submission, which is the failure mode that looks correct on inspection.

Three failure modes chosen to be loud rather than convenient:

- An unreadable or non-object registry is NOT treated as absent. Falling
  back to positional would renumber the one kind of tree that exists to
  stop that happening, so it is reported as an error and treated as empty
  (mint everything), which is visible in the diff.
- Two rows with one identity cannot be told apart by any registry. Warn,
  naming the identity, and mint for the duplicate so ids stay unique.
- Minting anything at all warns that `public_ids.json` must be committed,
  since an unrecorded mint is re-issued differently next run.

`_public_id_registry_path` also honors the results-dir root, because
`global_summary_dir` MOVES with the tree's shape -- a multi-org tree rolls
up at the root, a single-org tree inside that org. Found while writing the
retirement test: deleting one of two orgs relocated the global dir, the
registry stopped being found, and pinning silently reverted to positional.
The test now withholds a run leaf instead of an org, and the lookup no
longer depends on a layout classification.

This is the one non-`results.{csv,json}` file reportgen writes under
<results-dir> -- a deliberate, narrow exception to D-04, and only for a
tree that already carries it.

13 tests in tests/unit/test_reportgen_public_id_registry.py.
All four CI suites pass (238 / 3132 / 911 / 228).

Refs #836
…anged ids

Two changes to how pinning engages, both from the same observation: a magic
file somebody has to remember to create is a file that will not exist when it
matters.

**Created automatically.** A tree without a `public_ids.json` gets one,
filled from that run's positional assignment. Pinning becomes a ratchet
rather than a step in a checklist, and the file that results is the thing
staff delete once, before publication, to renumber for release. Nothing else
in the tool renumbers -- no flag, since a mechanism whose only use is the
once-per-cycle renumber says nothing that deleting the file does not.

The cost is accepted deliberately: organizations arriving after the ratchet
engages mint above the rows already published rather than displacing them, so
numbering drifts out of sort order during a review cycle. Displacing a
published id is worse, and the release-time renumber restores order and
closes gaps. `test_reportgen_public_id.py`'s renumbering test inverts to say
exactly this, and its module docstring no longer claims ids are fully
regenerated per run.

**The guard.** Because deletion is the renumber trigger, the tool cannot tell
a deliberate renumber from a registry that was lost -- a stale clone, a file
never checked out, a `git clean`. Both mint a plausible, wrong numbering and
record it as authoritative. So the guard does not gate on intent, which it
cannot know; it reports consequence: any run that changes an id the tree's
existing results.csv already published says so, names the count, and says how
to restore the registry if it was lost rather than removed. That text has to
read correctly for the staffer who deleted the file on purpose and the one
who did not, so it accuses nobody.

Generalized past the create case on purpose -- an emptied or truncated
registry renumbers identically to a deleted one, and guarding only on absence
would miss it. Silent in normal pinned operation: pinning never changes a
matched row's id, a minted row was in no earlier table, and a withheld row
keeps its id reserved. All three are pinned as tests.

The published table is matched on its display columns, which are NOT
guaranteed unique -- two kv_cache performance profiles for one system share
all five. Ambiguous keys are excluded from the comparison and counted rather
than matched arbitrarily and reported as a change that never happened.

7 failing, 24 passing.

Refs #836
…hanged ids

`public_ids.json` is now created rather than opted into. A tree without one
gets one, filled from that run's positional assignment, and is pinned from
then on. Deleting the file is the only thing that renumbers -- staff do it
once before publication to close gaps and restore sort order, and the run
that follows recreates the registry, so the RELEASED numbering is pinned too
and a later correction cannot silently move it.

Numbering therefore drifts out of sort order during a cycle: an organization
that merges after the ratchet engages mints above the rows already published
instead of displacing them. That is the trade being made -- displacing a
published id is worse than an out-of-order one -- and the release renumber
is what fixes ordering.

The guard reports consequence, not intent. Deletion being the renumber
trigger means the tool cannot distinguish a deliberate renumber from a
registry that was lost (stale clone, file never checked out, `git clean`);
both mint a plausible numbering and record it as authoritative. So any run
that changes an id the tree's existing results.csv already published warns,
names the count and three examples, and gives both readings: expected if you
removed the file to renumber (refresh the cited ids in the same change), and
restorable if you did not. Generalized past the create case so an emptied or
truncated registry is caught the same way.

Matching the published table has to go through the display columns, since a
written results.csv has no workload key in it. Two wrinkles, both pinned by
tests: those columns are not unique (two kv_cache performance profiles for
one system share all five), so ambiguous keys are excluded from the
comparison and counted rather than matched arbitrarily; and `Name` /
`Description` are `Hyperlink` objects until write time, so the key is built
from `_final_row` output coerced with `str()` -- which is exactly the anchor
the file carries. Unhashable-Hyperlink was a real crash before the coercion,
caught by the existing pinning tests rather than the new ones.

Creation is silent at INFO on a fresh tree: everything is minted by
definition there, and a WARNING would make a submitter's first run look like
a problem. Minting into an EXISTING registry still warns, because that one
does need the file committed.

Real-tree acceptance on a hardlink clone of the v3.0 submissions repo:
- registry present -> zero files changed, guard silent, 177 ids kept.
- registry deleted -> contiguous v3.0-0001..0177, gapless, registry
  recreated, and the warning naming 172 changed rows.

All four CI suites pass (238 / 3141 / 911 / 228).

Refs #836
…pt floats

Both fields are declared int(ge=1), so a submitter reporting a fractional
TiB capacity fails validation. XSKY's AIMesh systems report 111.78 TiB
usable, which the schema currently rejects outright.

Adds coverage for the widening:
  * fractional values pass (111.78)
  * whole numbers still pass — every example_*.yaml uses one
  * the ge=1 bound survives, so 0.5 is still rejected on range rather
    than incidentally on int-ness
  * non-numeric strings still rejected

Renames the two test_non_integer_rejected cases to test_non_numeric_
rejected — they assert on "lots"/"some", which is a numeric-ness check,
not an integer-ness one.

RED: the two fractional cases fail with "Input should be a valid integer,
got a number with a fractional part".
usable_capacity_tib and int_client_store_tib become float. A usable
capacity is rarely a whole number of TiB once RAID/EC overhead is taken
out — XSKY's AIMesh systems report 111.78 — and rounding a submitter's
stated figure to satisfy the schema is not ours to do.

  * schema_validator.py — int -> float, Optional[int] -> Optional[float]
  * schema.yaml — int( min=1 ) -> num( min=1 ) in the reference schema
  * conftest.py / test_reportgen_sut_derived.py — helper annotations

ge=1 is deliberately unchanged: the widening is the type only, not the
accepted range, so sub-1-TiB values are still rejected.

Whole numbers keep validating. The model is lax — StrictModel sets
extra="forbid", not strict=True — so an int coerces to float, and every
example_*.yaml template still passes.

No results-table output changes: report_generator.py reads the raw YAML
via solution.get() and never goes through the pydantic model, so an
integer capacity still renders as 8192, not 8192.0.

Full CI-equivalent sweep green: tests 3146 passed / 1 skipped,
mlpstorage_py/tests 911, kv_cache_benchmark 238, vdb_benchmark 228.
…inations at launch

Rules.md 4.3.5 is missing the word 'not', which inverted subset-run
validation everywhere downstream (#841). Pins the corrected
runtime behavior: an explicit --checkpoint-subset CLI flag (4.3.5 sentence 1,
never previously implemented) valid only for llama3-8b at 8 processes, a new
check_subset_mode fail-fast gate, and check_num_processes no longer accepting
8 processes as a CLOSED form for the large models (70B@8 becomes OPEN-eligible
as a TP*PP multiple; 405B@8 and 1T@8 are INVALID).
…binations at launch

Corrects the runtime half of the Rules.md 4.3.5 inversion
(#841 — a missing 'not' read as endorsing 8-process subset
runs of the large models; ten published v3.0 rows launched through that gap).

- cli: register --checkpoint-subset on the checkpointing subcommands — the
  explicit declaration 4.3.5's first sentence requires, whose help text had
  sat orphaned in common_args.py since it was written. The help text now
  describes the actual semantics (8B-only, one 8-GPU node, linear-scale-out
  claim) instead of the inverted large-model reading.
- run checker: new check_subset_mode — the flag with any model but llama3-8b,
  or with a process count other than 8, is INVALID and aborts before DLIO.
- run checker: check_num_processes drops the '8 (subset run)' CLOSED
  allowance the #792 fix codified for every model. CLOSED now requires the
  respective full count; a large-model run at 8 processes is OPEN-eligible
  when 8 is a TP*PP multiple (70b) and INVALID otherwise (405b, 1t). 8B is
  unchanged (its full count is 8).
- benchmarks/dlio: auto-labeling a downscaled run now warns at classification
  time that a partial checkpoint is not a CLOSED form and that subset
  submissions are 8B-only, so the label never again reads as an opt-in.
- test_issue792: the three test_subset_run_passes cases pinned the inverted
  allowance; updated to the corrected verdicts with a pointer to #841.

The validator half (subset_run_validation inversion, 4.6.1 subset carve-out)
lands separately.
… the missing 'not'

Pins the corrected detection half of #841:
subset_run_validation errors on a CLOSED subset run of any model but 8B
(via the auto-set checkpoint.mode override OR the new explicit
checkpoint_subset arg recorded in metadata), passes the well-formed 8B
subset it previously rejected, and leaves OPEN downscaled runs to 4.6.4;
closed_mpi_processes loses its 'subset mode: 8 processes for any model'
carve-out so CLOSED counts are strict-respective; the reportgen Checkpoint
Mode column honors the explicit flag (the 8B claim run is
execution-identical to full, so the args snapshot is its only signal).
…s subset carve-out

Corrects the detection half of the Rules.md 4.3.5 inversion
(#841):

- subset_run_validation now errors on a CLOSED subset run of any model but
  8B — the previous version implemented the published typo faithfully,
  erroring 'subset run cannot use 8B model' (the only legitimate form)
  while passing the large-model subset runs, ten of which published in
  v3.0. Subset-ness is read from either signal: the auto-set
  checkpoint.mode override or the explicit checkpoint_subset arg (the 8B
  claim run is execution-identical to full, so the args snapshot is its
  only signal). Scope is CLOSED only — an OPEN run below the full count
  uses the same partial-checkpoint mechanics but is 4.6.4's business.
  The model regex gains a lookbehind so '405b'/'1t' never match the '8b'
  token inside their own names.
- closed_mpi_processes drops the 'subset mode: 8 processes for any model'
  carve-out; CLOSED counts are strict-respective (8/64/512/1024). The
  legitimate 8B subset needs no exception — its respective count is 8. A
  mislabeled large-model run gets a parenthetical naming the corrected
  rule reading.
- report_generator: the Checkpoint Mode column honors the explicit flag,
  so a declared 8B subset run publishes as Subset even though no DLIO
  override is written.
- test_bug03 / test_issue792 post-hoc tests pinned the carve-out and the
  405b-subset-as-happy-path shapes; updated to the corrected verdicts
  with pointers to #841.
…et sizes Invalid in Table 2

The published 4.3.5 text read '…or the model is "8B"' where the intent was
'…or the model is not "8B"' (#841). Subset mode is defined
only for the 8B workload — one industry-standard 8-GPU node checkpointing to
solution-managed local storage, claiming linear scale-out — and the larger
models have no subset form; they exist to measure architectures where
checkpoint data must reach shared central storage.

Table 2's 'Subset: 8-Process Size' row carried computed sizes for all four
models, which the corrected rule contradicts on its face: a subset size for
70B/405B/1T describes a run the validator must reject. Those cells are now
marked Invalid; the 8B value (105 GB, identical to the full run — the whole
point) stands.
…'closed'

Real metadata carries verification: 'CLOSED' (uppercase); the lowercase-only
comparisons meant the new 4.3.5 CLOSED gate skipped every real run — and
4.6.1 has never fired on a real tree at all. Caught by the clone gate: a
validate sweep with the new code returned counts byte-identical to the old
baseline, zero 4.3.5 lines against the ten known-affected rows.
…al trees for the first time

metadata records verification as 'CLOSED'/'OPEN' (uppercase); the 4.3.5
CLOSED gate and 4.6.1's closed-branch compared lowercase-only, so the new
detection skipped every real run and 4.6.1 had never fired on a real tree
at all. Both now fold case. Deliberately NOT touched: 4.6.2 / 4.6.3 / 4.6.4
carry the same lowercase-only pattern and are equally dead on real trees —
enabling them changes real-tree behavior beyond #841's scope and needs its
own gated verification; flagged in-code and to the review chairs.
…s-reference

Follow-through on #841, per review-chair direction:

- 4.3.5 now states the requirement affirmatively — a subset run must use
  the 8B model and exactly 8 accelerators; the validator must flag any
  other combination — instead of the double-negative sentence the missing
  'not' hid in. The command-side requirement (accept and record the
  declaration) is unchanged.
- The rationale for subset being 8B-only (local-NVMe architectures whose
  bandwidth scales linearly with node count) appears as an italic aside
  explicitly marked not part of the rule, and as a one-line footnote
  explaining Table 2's Invalid entries.
- 4.6.1 gains a declarative sentence closing the reading that produced the
  ten v3.0 rows: no reduced-process form exists for the larger models.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants