Skip to content

DRAFT: feat(config): consolidate all durations and deadlines into one Timeouts model#409

Draft
viraatc wants to merge 5 commits into
mainfrom
timeouts-consolidation
Draft

DRAFT: feat(config): consolidate all durations and deadlines into one Timeouts model#409
viraatc wants to merge 5 commits into
mainfrom
timeouts-consolidation

Conversation

@viraatc

@viraatc viraatc commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Consolidates every global time knob into one frozen pydantic modelTimeouts in the new src/inference_endpoint/config/timeouts.py, mounted at settings.timeouts — gives the previously dead --timeout flag real semantics, splits the 1400-line config/schema.py into focused modules, and makes the sample-count knobs mutually exclusive.

The headline bug this fixes

BenchmarkConfig.timeout (--timeout, top-level timeout: YAML key) was consumed nowhere — a silent no-op. Several example YAMLs set it believing it bounded the run. It is now settings.timeouts.run_timeout_s: a real whole-run watchdog. When it fires, it SIGTERMs the metrics aggregator (whose handler writes an INTERRUPTED final_snapshot.json), stops the session, and run_benchmark raises ExecutionError (non-zero exit) after finalization — a fired watchdog can never yield a COMPLETE report and always leaves result_summary.json with complete: false. Locked by tests/integration/commands/test_run_timeout.py.

The one model

settings:
  timeouts:
    # workload durations (benchmark definition — reaching them is a normal end)
    min_duration_ms: null          # --duration (suffixes: 600s, 10m); sample count = QPS × duration
    max_duration_ms: null          # whole-perf-phase-only cap; never bounds warmup/accuracy
    # give-up deadlines (failure handling — None = wait indefinitely)
    run_timeout_s: null            # --timeout, whole-run watchdog (covers drains too), fires → INTERRUPTED
    service_ready_timeout_s: 30.0  # --service-ready-timeout
    warmup_drain_timeout_s: 240.0  # --warmup-drain-timeout
    performance_drain_timeout_s: null  # ⚠️ default changed: was 240 — now unbounded by request
    accuracy_drain_timeout_s: null # deliberately unbounded: every sample must complete
    metrics_drain_timeout_s: null  # None = unlimited (was 0 = unlimited)
  metrics:
    tokenizer_workers: 2           # --metrics-tokenizer-workers (not a timeout → own block)

All CLI flag aliases are unchanged. Only YAML key locations and auto-generated dotted flags moved.

Sample count: explicit XOR duration-derived (new)

runtime.n_samples_to_issue (--num-samples) and timeouts.min_duration_ms (--duration) are now mutually exclusive (Settings validator). Previously an explicit count silently won and the configured duration was dead weight — several examples carried both. Omitting both issues the dataset once, which also deletes the old min_duration_ms: 0 = "all samples" and max_duration_ms: 0 = "no limit" sentinels (0 is now rejected loudly).

⚠️ Reviewer callouts (deliberate behavior changes):

  • Bare-config default changed: neither knob set previously derived qps × 600s worth of samples; it now runs the dataset once.
  • performance_drain_timeout_s default is now None (unbounded). A default run has no time backstop on a wedged endpoint — set run_timeout_s for unattended runs.
  • metrics_drain_timeout_s: 0 / min_duration_ms: 0 / max_duration_ms: 0 now error loudly.
  • Example run_timeout_s values were dropped, not carried over (old top-level timeout: was inert; enforcing stale values would hard-kill valid runs). Also fixed compare_with_vllm.py, which capped the same budget twice via max_duration_ms and --timeout.

schema.py split

config/schema.py now owns only BenchmarkConfig/EndpointConfig and re-exports the full schema surface (import sites unchanged). Models moved to config/enums.py, audit.py, model_params.py, datasets.py, settings.py (+ existing timeouts.py). Dead code deleted: SystemDefaults (DEFAULT_TIMEOUT had zero consumers), TEMPLATE_TYPE_MAP.

Design invariants (multi-model design review: Codex/Opus/Grok/Gemini)

  • run_timeout_s never derives per-stage deadlines (no remaining-budget propagation).
  • min/max_duration_ms bound the whole performance phase (single phase; not per-dataset); max_duration_ms remains the sole perf ceiling.
  • turn_timeout_s stays dataset-scoped; worker lifecycle timeouts stay on settings.client.*.
  • Hot path untouched: everything resolves to plain values at setup.
  • No back-compat shims: old keys hard-error; templates/examples/docs/tests migrated in this PR.

Multi-AI review hardening

A Codex review + adversary-council pass found and fixed: watchdog now covers the unbounded metrics drain; watchdog SIGTERMs only the aggregator (event logger kept its buffered events.jsonl); timed-out runs skip accuracy scoring (artifacts still salvaged); teardown race can no longer skip result_summary.json; run_audit maps a watchdog fire to ExecutionError instead of the Ctrl-C exit-130 path.

Type of change

  • Bug fix
  • New feature
  • Refactor/cleanup

Testing

  • Tests added/updated — watchdog locking test, audit run-timeout test, sample-count mutual-exclusion + dataset-once tests; all suites migrated
  • All tests pass locally — tests/unit: 1522 passed, 5 skipped; integration command suites green (incl. full test_cli.py e2e)
  • Manual testing completed — every example YAML load-validated; CLI --help verified aliases unchanged

Checklist

  • Code follows project style
  • Pre-commit hooks pass
  • Documentation updated — AGENTS.md, CLI docs, LOCAL_TESTING, config DESIGN docs, example READMEs

🤖 Generated with Claude Code

…ts model

One frozen pydantic model (config/timeouts.py, mounted at settings.timeouts)
now owns every global time knob: min/max duration, per-phase drain deadlines,
service-ready deadline, metrics drain budget, and the whole-run watchdog.

- BenchmarkConfig.timeout was a silent no-op consumed nowhere; it is now
  settings.timeouts.run_timeout_s (--timeout alias preserved): a real
  whole-run watchdog that SIGTERMs the metrics aggregator (INTERRUPTED
  final snapshot), stops the session, and exits non-zero. A fired watchdog
  can never produce a COMPLETE report (locked by integration test).
- DrainConfig deleted; drain fields moved to settings.timeouts with CLI
  aliases unchanged. metrics_drain_timeout_s normalized to None=unlimited
  (was 0=unlimited); the aggregator argv boundary still speaks 0.
- metrics_tokenizer_workers is not a timeout: moved to settings.metrics
  (new MetricsConfig block).
- min/max_duration_ms (+ suffix parsing + cross-validator) moved from
  RuntimeConfig to Timeouts; RuntimeSettings resolves them to plain values
  at setup, so nothing in the hot path reads pydantic.
- ServiceLauncher.terminate_all() added (graceful SIGTERM counterpart to
  kill_all) for the watchdog path.
- Templates regenerated; examples/docs/tests migrated (YAML keys moved,
  no back-compat shims per repo convention). Example run_timeout_s values
  were dropped rather than carried over: the old top-level timeout was
  inert, and enforcing stale values would abort valid runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@viraatc
viraatc requested a review from a team July 13, 2026 20:20
@github-actions

Copy link
Copy Markdown

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

@github-actions
github-actions Bot requested review from arekay-nv and nvzhihanj July 13, 2026 20:21

@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 refactors the configuration schema by centralizing all global durations, deadlines, and timeouts into a new frozen Pydantic model Timeouts (accessible via settings.timeouts). This separates workload durations from failure-handling deadlines. Additionally, a whole-run watchdog (run_timeout_s) has been introduced to gracefully abort stuck runs, signaling managed subprocesses via SIGTERM to write an interrupted final snapshot before exiting non-zero. All configuration templates, examples, and tests have been updated to align with this new schema, and new integration tests have been added to verify the watchdog behavior. No review comments were provided, so there is no feedback to address.

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.

viraatc and others added 3 commits July 13, 2026 13:31
…on semantics

- performance_drain_timeout_s default 240.0 -> None (wait indefinitely),
  matching the accuracy drain default.
- min/max_duration_ms help/description now state they bound the whole
  performance phase (single phase; not per-dataset) and never bound
  warmup/accuracy. Templates regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
schema.py (1415 lines) now holds only BenchmarkConfig/EndpointConfig and
re-exports the full schema surface; models move to sibling modules:
enums.py, audit.py, model_params.py, datasets.py, settings.py (timeouts.py
already existed). Import sites are unchanged — config.schema remains the
single import point.

Dead code deleted: SystemDefaults (DEFAULT_TIMEOUT had no consumers;
DEFAULT_METRIC inlined into rulesets/mlcommons/rules.py) and the unused
TEMPLATE_TYPE_MAP. regenerate-templates pre-commit hook now watches the
new modules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Findings from the review council (Codex review + adversary council),
all verified against the code before fixing:

- Watchdog now stays armed through the unbounded metrics drain: it was
  cancelled right after session.run, so run_timeout_s could not bound a
  stuck aggregator drain (wait_for_exit(None)). Cancelled after services
  exit instead.
- Watchdog SIGTERMs only the metrics aggregator (ServiceLauncher.terminate
  with module suffix, replacing terminate_all): SIGTERMing the event
  logger dropped its buffered events.jsonl tail; the logger flushes on the
  ENDED event, which session.stop() still delivers.
- A timed-out run skips accuracy scoring in finalize: phases that never
  started KeyError in scorer init and partial phases would yield
  misleading subset scores. Artifacts are still salvaged.
- Teardown race no longer skips finalization: if session.run raises after
  the watchdog fired, fall through with an empty SessionResult so
  result_summary.json (INTERRUPTED, complete=false) is always written;
  run_benchmark raises the timeout ExecutionError after finalize.
- run_audit maps a watchdog fire to ExecutionError naming the timeout
  instead of the Ctrl-C KeyboardInterrupt path (exit 130).
- MetricsConfig gets cyclopts.Parameter(name='*') matching sibling
  settings blocks (flat --tokenizer-workers + --metrics-tokenizer-workers).
- Stale drain-key name fixed in session.py docstring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# (launched once from top-level model_params), so a per-dataset override would
# desync ISL/OSL/TTFT/TPOT accounting without changing what is measured. Rejected
# as generation_config_override keys — they are per-run/identity, not per-dataset.
_METRICS_DECOUPLED_OVERRIDE_KEYS = frozenset({"name", "streaming", "tokenizer_name"})
@viraatc viraatc changed the title feat(config): consolidate all durations and deadlines into one Timeouts model DRAFT: feat(config): consolidate all durations and deadlines into one Timeouts model Jul 13, 2026
@viraatc
viraatc marked this pull request as draft July 13, 2026 21:30
…entinels

min_duration_ms and n_samples_to_issue are now mutually exclusive
(Settings validator): the sample count is either an explicit count or
derived from QPS x duration — previously an explicit count silently won
and the configured duration was dead weight.

- min_duration_ms: int | None = None. Omitting both knobs issues the
  dataset once. BEHAVIOR CHANGE: a bare config (neither set) previously
  derived qps x 600s worth of samples; it now runs the dataset once.
  The 0 = 'all dataset samples' sentinel is gone (0 now rejected).
- max_duration_ms: int | None = None, replacing the 0 = 'no limit'
  sentinel; the argv-boundary 0->None conversion in RuntimeSettings is
  deleted (internal RuntimeSettings stays permissive for programmatic
  callers passing 0).
- Examples/docs/templates migrated: dropped min_duration where an
  explicit count is set (it was silently ignored), dropped --duration 0
  usage, fixed compare_with_vllm.py double-cap (it set max_duration_ms
  from the same value it passed as --timeout, capping one budget via two
  mechanisms), corrected CLI_QUICK_REFERENCE/LOCAL_TESTING stale
  defaults and key locations.

Co-Authored-By: Claude Fable 5 <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.

1 participant