Skip to content

Add --duration flag to perf for time-budgeted benchmarking#1168

Open
xieofxie wants to merge 1 commit into
mainfrom
hualxie/perf_add_duration
Open

Add --duration flag to perf for time-budgeted benchmarking#1168
xieofxie wants to merge 1 commit into
mainfrom
hualxie/perf_add_duration

Conversation

@xieofxie

Copy link
Copy Markdown
Contributor

Summary

Adds a --duration <seconds> flag to winml perf. When set, the benchmark runs for a wall-clock time budget instead of a fixed --iterations count. After warmup, inference loops until the duration elapses (always running at least one benchmark iteration so stats are never empty).

This is ideal with --monitor, whose PDH hardware counters need time to emit real utilization data — a small fixed iteration count often finishes before the counters produce meaningful samples.

winml perf -m model.onnx --device npu --monitor --duration 15

Details

  • New --duration FLOAT option (must be > 0), plumbed through BenchmarkConfig.duration.
  • _benchmark_indices() helper centralizes the loop: warmup first, then either a fixed iteration count (default) or a timed loop. Used by the single-model, monitored, and per-module paths.
  • Mutually exclusive with --op-tracing (op-tracing runs its own fixed, small iteration count) — rejected with a clear UsageError.
  • Live --monitor chart shows elapsed/total time progress (Time: 12.3/30s) in duration mode instead of an iteration counter.
  • Accurate iteration reporting: in duration mode the reported iteration count reflects the actual number of timed samples rather than the unused --iterations default. For per-module runs (where each module runs a different count under a shared budget), the count is recorded per instance and the top-level value is null; both single-model and module reports also record duration_sec.
  • --duration is added to the genai "ignored flags" warning (genai benchmarks by token generation).

Tests

  • --duration help/forwarding/default, --op-tracing conflict, non-positive rejection.
  • _benchmark_indices iteration mode, timed budget, and at-least-one-iteration guarantee (deterministic via a faked clock).
  • LiveMonitorDisplay time-based vs iteration-based progress.
  • benchmark_info.iterations reports configured count without duration and actual sample count with duration.

All perf suites pass (test_perf_cli, test_perf_module, test_perf_composite, test_ep_monitor): 200 passed, 2 NPU-only skips; ruff clean.

Run the benchmark for a wall-clock budget (seconds) instead of a fixed
--iterations count. After warmup, inference loops until the duration
elapses (always at least one benchmark iteration). Ideal with --monitor,
whose PDH counters need time to emit real utilization data. Rejected with
--op-tracing, which runs its own fixed iteration count.

Works across the single-model, monitored, and per-module paths. The live
--monitor chart shows elapsed/total time progress in duration mode. The
reported iteration count reflects the actual timed sample count instead of
the unused --iterations default.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@xieofxie
xieofxie requested a review from a team as a code owner July 22, 2026 08:41
@xieofxie

Copy link
Copy Markdown
Contributor Author

Needed for #1149

@DingmaomaoBJTU DingmaomaoBJTU 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.

Took a pass through this — the overall approach looks good. The warmup→timed-loop split, the at-least-one-iteration guarantee, the --op-tracing conflict, and the genai ignored-flag warning are all correct, and the tests cover the important paths.

A few minor nits (none blocking):

perf.py L732-734logger.info("Running benchmark: %d iterations + %d warmup", ...) still logs the fixed iterations count in --duration mode, so it reads e.g. "Running benchmark: 100 iterations + 10 warmup" for a run that's actually time-bounded. Might be worth branching to log the time budget there.

Two more inline below.

for i in _benchmark_indices(total_iterations, warmup, duration_sec):
session.run(inputs)

if (i + 1) % max(1, total_iterations // 10) == 0:

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.

In --duration mode i keeps counting past total_iterations (the loop is time-bounded, not count-bounded), so this progress log can read e.g. Progress: 350/110. The total_iterations denominator here — and on the logger.debug line just below — is meaningless once the run is timed. Minor since it's debug-only, but might be worth switching to an elapsed/duration readout in duration mode.

and self._bench_start is None
and iteration > self._warmup
):
self._bench_start = time.perf_counter()

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.

This stamps _bench_start on the first post-warmup update(), which runs after that iteration's session.run(). But _benchmark_indices starts its own budget clock before that run (perf.py L1771) and the loop terminates on the generator's clock. Net effect: this display clock lags by ~one inference, so Time: x/Ns tends to finish a bit under 100%. Purely cosmetic — if you want the bar to land on 100%, share the generator's start timestamp with the display instead of re-stamping here.

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