Skip to content

fix: Salt failure is a hard error - #406

Open
arekay-nv wants to merge 17 commits into
mainfrom
arekay/salt_failure_hard_error
Open

fix: Salt failure is a hard error#406
arekay-nv wants to merge 17 commits into
mainfrom
arekay/salt_failure_hard_error

Conversation

@arekay-nv

@arekay-nv arekay-nv commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Makes salt failures a hard error instead of a silent skip. Previously, when --warmup-salt was on (the default) but a sample couldn't be salted, Dataset._apply_salt logged a warning (or silently passed the sample through) and the warmup ran with no cache-busting — the exact condition salt exists to prevent, failing quietly.

Now salt=True requires every sample to be a dict with a text (str) prompt and no input_tokens; anything else raises DatasetValidationError at dataset-load time, before any load is issued.

Key changes:

Area Change
Validation New Dataset.validate_saltable() — rejects non-dict samples, input_tokens (pre-tokenized; adapters send these verbatim so a salted prompt never reaches the server), missing prompt, and non-str prompt. Error names the offending sample index + remediation.
Fail-fast placement Called from _load_datasets when warmup.enabled and warmup.salt, so an unsaltable dataset fails before worker/aggregator subprocesses spawn. Also called from with_salt() so the salting mechanism stays self-protecting.
Hot path _apply_salt collapses to a single dict-merge ({**data, "prompt": f"[{salt}] {data['prompt']}"}) — the multimodal-list handling, input_tokens warnings, and silent passthroughs are deleted; the contract is guaranteed upstream.

⚠️ Behavior change (note for reviewers)

  • Multimodal / pre-tokenized warmup + salt now hard-errors. A prompt that is a list (image/video workloads) or a dataset with input_tokens (e.g. gpt-oss-120b, DeepSeek-R1 via /v1/completions) will fail warmup instead of silently skipping salt. Fix: set --warmup-salt=false / warmup.salt: false.
  • Whole-dataset validation. The run is rejected if any sample is non-conforming — including samples the n_requests subset would never issue. Stricter than the old per-sample skip; intended by the hard-error design.

Type of change

  • Bug fix
  • New feature
  • Documentation update
  • Refactor/cleanup

Related issues

Testing

  • Tests added/updated
  • All tests pass locally
  • Manual testing completed

Checklist

  • Code follows project style
  • Pre-commit hooks pass
  • Documentation updated (if needed)

Signed-off-by: Rashid Kaleem <230885705+arekay-nv@users.noreply.github.com>
@arekay-nv
arekay-nv requested a review from a team July 10, 2026 01:59
@github-actions

Copy link
Copy Markdown

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

@github-actions
github-actions Bot requested a review from nvzhihanj July 10, 2026 01:59

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces early validation for dataset salting during the benchmark setup phase. It adds a validate_saltable method to ensure that all dataset samples are compatible with salting (i.e., they are dictionaries containing a string prompt and no input_tokens) before any subprocesses are spawned. The review feedback highlights a critical bug in validate_saltable where iterating over self.data directly will fail if it is a pandas.DataFrame (as it would iterate over column names instead of rows), and provides a code suggestion to handle this case.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/inference_endpoint/dataset_manager/dataset.py Outdated
Comment thread src/inference_endpoint/dataset_manager/dataset.py
Comment thread src/inference_endpoint/dataset_manager/dataset.py
Comment thread src/inference_endpoint/dataset_manager/dataset.py Outdated
@nv-alicheng

Copy link
Copy Markdown
Collaborator

Review Council — PR #406 (3× Claude, depth: thorough)

Three independent Claude reviewers (bugs/correctness, design/edge, testing/docs) plus a code-quality pass, deduped and severity-recalibrated. "R" marks issues flagged independently by multiple reviewers. All line numbers verified against HEAD (44fc32d7).

Verdict: Solid, focused bug-fix. Fail-fast placement is correct — validation at _load_datasets (execute.py:387) runs before any worker/aggregator subprocess spawns, and warmup's with_salt() (execute.py:546) salts the same ctx.dataloader it re-validates. Transforms apply eagerly in load(), so validate_saltable() scans the exact post-transform shape load_sample() returns — no mismatch. No security issues, green suite. The findings are about strictness + a default, not broken logic.

🟡 Should fix

# File:line Cat R Issue
1 config/schema.py:762 + execute.py:386 design warmup.salt defaults True + the new hard error = footgun for first-class pre-tokenized workloads. openai_completions (gpt-oss-120b), sglang, and DeepSeek-R1 run Harmonize()+ColumnFilter(["input_tokens"]) at load, so their samples always carry input_tokens and no prompt. Plain --warmup (without --warmup-salt false) now aborts setup for every such run that previously warned and ran unsalted. Options: default salt=False, auto-downgrade to warning+unsalted when input_tokens is present, or (minimum) state the text-prompt requirement in the field help/description.
2 tests/unit/commands/test_benchmark.py:248 testing The 3 TestLoadDatasetsSaltValidation tests @patch.object(Dataset, "validate_saltable"), so they only assert called/not-called — they never exercise real validation through _load_datasets. The exact integration this PR adds (unsaltable perf dataset → DatasetValidationError before subprocess spawn) has zero coverage. Add a test that writes an input_tokens/int-prompt JSONL and asserts pytest.raises(DatasetValidationError) through _load_datasets unpatched.
3 execute.py:386 api-contract Whole-dataset strictness vs the warmup subset. Validation rejects the entire run if any sample is non-saltable, but salt only touches warmup, which issues just a seeded n_requests subset — and the perf phase runs unsalted regardless. A mostly-text dataset with one anomalous row now hard-fails at load even if that row is never issued. Consider scoping validation to the warmup-issued subset.
4 dataset_manager/dataset.py:470 error-handling if self.data is None: return silently passes validation on an unloaded dataset — contradicts the PR's own fail-loud thesis and masks a "dataset not loaded" ordering bug. A truly-empty loaded dataset is [] (already a no-op via the loop), not None; EmptyDataset is never actually instantiated. Distinguish: raise on None (not-loaded), no-op on [].
5 tests/unit/dataset_manager/test_salted_dataset.py:196 testing match=r"\b1\b" is a weak/brittle index assertion — re.search passes on any word-bounded 1 anywhere in the message and fails for two-digit indices. Anchor to match=r"sample 1\b".

🔵 Consider

  • dataset.py:270 — both-keys (input_tokens + valid str prompt) is now rejected, but the deleted code salted the prompt in that case. A prompt-consuming adapter on a mixed-schema dataset loses a salt it used to get. Confirm no dataset+adapter pairing sends prompt while input_tokens is present.
  • dataset.py:495 / execute.py:387 (2×) — the full dataset is scanned twice (load-time guard + with_salt re-scan) over unchanged shared data. Cold-path and fine, but for the 50k+-sample corpora this project targets, add a one-line comment marking the early call as the intentional pre-spawn fail-fast so a future reader doesn't "dedupe" it away.
  • dataset.py:477 — the error names sample {i}, an index into post-transform/filtered loaded order, not the user's source-file line; a JSONL with column filters won't map cleanly. Clarify "index into loaded order."
  • Testing — the online/agentic path and accuracy_only (dataloader None → skip) are uncovered; pin the intended skip so a refactor can't start validating a None dataloader.

🧹 Code quality

  • dataset.py:464 & :490 — docstrings narrate removed behavior ("rejected rather than skipped", "fails here rather than silently issuing unsalted prompts"). Per AGENTS.md ("describe current state, not development history"), reframe positively and drop the "rather than …" clauses.
  • dataset.py:506assert self._salt_rng is not None vanishes under python -O → a bare AttributeError on the warmup path. For a PR about making salt failures loud, a raised error fits better (same applies to the pre-existing assert self.data is not None on the hot access path).

Convergence: all three reviewers independently landed on the salt-default-True footgun (#1) — the one decision worth the author's attention; the rest is polish.

arekay-nv and others added 3 commits July 24, 2026 09:41
- Default warmup.salt to False so pre-tokenized (input_tokens) workloads
  no longer hard-fail on a plain --warmup; the hard error now fires only
  when salt is explicitly enabled.
- Replace raw-string salt-violation reasons with a typed
  DatasetValidationError.Reason enum plus optional detail; _salt_violation
  becomes _can_salt returning the enum. UNSPECIFIED covers not-yet-mapped
  --dataset parse errors.
- validate_saltable: assert on unloaded data (no silent skip); clarify the
  error index is into the loaded post-transform order; document the
  intentional whole-dataset (fail-on-any-invalid) strictness and the
  deliberate pre-spawn check.
- Tests: unpatched integration coverage (offline/online raise, accuracy-only
  skip), agentic messages sample, typed-reason assertions; drop brittle
  index regex.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arekay-nv
arekay-nv requested a review from nv-alicheng July 28, 2026 01:59
Comment thread src/inference_endpoint/dataset_manager/dataset.py
Comment thread src/inference_endpoint/config/schema.py
arekay-nv and others added 2 commits August 14, 2026 21:26
Signed-off-by: arekay-nv <230885705+arekay-nv@users.noreply.github.com>
Warmup without --warmup-salt issues prompts verbatim, priming the server
KV/prefix cache so the measured phase can be served warm and understate
latency. Surface it as a warning at dataset-load time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arekay-nv
arekay-nv requested a review from viraatc August 17, 2026 14:44

@nv-alicheng nv-alicheng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Council — Multi-AI Code Review

Reviewed by: Codex + Claude + Code-Quality | Depth: standard

Found 3 issues (1 cross-confirmed by two reviewers). Overall a clean, well-tested refactor — typed Reason enum, fail-fast placement, and the collapsed _apply_salt are sound.

# File Line Severity Category Reviewer(s) Summary
1 dataset_manager/dataset.py 279 medium regression/api-contract Codex + Claude Multimodal list-form prompts now hard-error at load; list prompts were previously salted (first text part), not silently skipped — tested behavior removed.
2 commands/benchmark/cli.py 70 low code-quality Code-Quality Comment narrates future work ("aren't yet mapped") without a tracking issue.
3 dataset_manager/dataset.py 471 low documentation Claude validate_saltable docstring omits messages (code also rejects it).

#1 is the decision point: the "silent skip" justification doesn't hold for list-form prompts — confirm the multimodal-salt drop is intended (and give list prompts a distinct Reason), or restore the branch.

return Reason.MESSAGES_SHADOWING
if "prompt" not in sample:
return Reason.PROMPT_MISSING
if not isinstance(sample["prompt"], str):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Codex + Claude] medium (regression / api-contract): This str-only check makes multimodal list-form prompts hard-error at load with PROMPT_TYPE_MISMATCH. Both reviewers independently verified that list prompts were previously salted correctly by prefixing the first type == "text" part (covered by the now-deleted test_multimodal_list_prompt_first_text_part_is_salted / test_multimodal_image_first_text_at_index_1_is_salted) — they were not silently skipped. So the PR's "salt was silently skipped" premise doesn't hold for the list case: this removes tested, working behavior, and any existing warmup.salt: true VLM/multimodal config now aborts at setup.

Two options:

  1. If dropping multimodal salt is intended, call it out as a breaking change and give list prompts a distinct, actionable Reason — the current message ("sample 'prompt' is not a str") is misleading, since list prompts can be salted.
  2. If unintended, restore the first-text-part salting branch for list prompts.

Comment thread src/inference_endpoint/commands/benchmark/cli.py Outdated
Comment thread src/inference_endpoint/dataset_manager/dataset.py Outdated
Comment thread src/inference_endpoint/config/schema.py Outdated
Comment thread src/inference_endpoint/dataset_manager/dataset.py Outdated
Comment thread src/inference_endpoint/dataset_manager/dataset.py Outdated

@nv-alicheng nv-alicheng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved, but comments need to be addressed.

enabled: false # Enable warmup phase before performance run
n_requests: null # Warmup request count (None = full dataset once)
salt: true # Prepend a unique random hex salt to each warmup prompt
salt: false # Prepend a unique random hex salt to each warmup prompt. Requires text-'prompt' samples; enabling it on a pre-tokenized ('input_tokens') dataset is a hard error.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Changing the default behavior to false might fail a lot of the submission silently (simply because the submitter forgot to turn them on).
Can we warm up with random token if the salt cannot be done, or some other alternative approach (e.g. add random tokens)? Based on the endpoints v0.7 and inference 6.1 I am really worried a non submit-compatible default flag will invalidate many submissions

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.

By default, both warmup and warmup salting are disabled. Enabling warmup alone leaves salting disabled; the run logs a warning and proceeds with unsalted warmup requests. A hard setup error occurs only when both warmup and salt are enabled and the loaded dataset contains a sample the implementation cannot safely salt. The bug this PR addresses is that, previously, requesting salt could still result in an unsalted outgoing request—sometimes silently and sometimes with only a warning.
This change would ensure that warmup with salting is either observed correctly or flagged as an error.

@nvzhihanj nvzhihanj Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes, that part makes sense. But for submission, only a warmup with SALT will ever be accepted for single-turn/agentic LLM workload. Will "SALT by default enabled" work? Or can a ruleset force that to be true? Thinking about reducing human mistakes and amount of context knowledge here

This is given that all future LLM/VLM workload will use text prompt whenever possible

Address review-council + reviewer feedback on the salt-failure-hard-error change:

- Rename _can_salt -> _check_unsaltable (truthy Reason == unsaltable) and fix
  the validate_saltable walrus so it raises when a sample IS unsaltable
- Add DatasetParseError for --dataset string parse failures; remove the
  vestigial Reason.UNSPECIFIED
- Rename the list-prompt reason to PROMPT_LIST_UNSUPPORTED (spec-accurate: an
  OpenAI list prompt is a batch / token-ID array, not necessarily multimodal)
- Drop the duplicate cyclopts help= on WarmupConfig.salt; Field(description=)
  is the single source for --help and the YAML templates
- Make DatasetValidationError copy/pickle-safe via a 3-tuple __reduce__
  (restores __dict__/__notes__ state, matching CPython's default reducer)
- Propagate typed CLI errors from audit phases (InputValidationError -> exit 2)
  instead of recasting them to ExecutionError (exit 4)
- Raise a clear ValueError on conflicting scalar/nested --dataset keys instead
  of an uncaught TypeError that escaped to an exit-1 traceback
- Document the newly reachable exit 2 in audit.py and compliance_audit_plan.md
- Add regression tests for all of the above

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
arekay-nv and others added 2 commits August 17, 2026 21:21
main #437 refactored _load_datasets to return (dataloader, eval_configs).
test_accuracy_only_skips_salt_validation is branch-only, so the merge kept
its stale 3-tuple unpack — the sole site the merge missed. Adopt the 2-tuple
form; perf_loader is still the first element, so the assertion is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

4 participants