Skip to content

[None][feat] Add duration-based execution to benchmark - #13385

Open
weikuo0506 wants to merge 19 commits into
NVIDIA:mainfrom
weikuo0506:feat/duration-bench
Open

[None][feat] Add duration-based execution to benchmark#13385
weikuo0506 wants to merge 19 commits into
NVIDIA:mainfrom
weikuo0506:feat/duration-bench

Conversation

@weikuo0506

@weikuo0506 weikuo0506 commented Apr 23, 2026

Copy link
Copy Markdown

Description

This PR adds a duration-based execution feature to the TensorRT-LLM Python benchmark suite (trtllm-bench). Currently, benchmarks rely on a fixed num_requests, which can lead to excessively long run times for scenarios with high Input Sequence Length (ISL) and Output Sequence Length (OSL).

The new --duration option (in seconds) allows users to limit the benchmark run time. The implementation uses a wall-clock check in the worker loop to stop pulling new requests after the duration has elapsed, while allowing in-flight requests to drain to ensure valid statistics.

This affects both throughput and latency commands as they share the same execution logic.

Fixes #13487

Potential Impacts

  • Performance: This change only affects the benchmarking tool and has no impact on inference performance.
  • Functional: It adds a new optional CLI parameter and changes the stop condition of the benchmark loop when used.

Test Coverage

  • Added a unit test test_bench_async.py to verify LlmManager duration logic using mocks.
  • Local verification was skipped due to missing dependencies (torch, pytest) in the host environment, but the test is designed to be run in the standard container.

PR Checklist

[✓] PR description clearly explains what and why.
[✓] PR Follows TRT-LLM CODING GUIDELINES.
[✓] Test cases are provided for new code paths.

Signed-off-by: Kuo Wei <weikuo@google.com>
@weikuo0506
weikuo0506 requested a review from a team as a code owner April 23, 2026 14:19
@weikuo0506
weikuo0506 requested a review from dc3671 April 23, 2026 14:19
@weikuo0506

Copy link
Copy Markdown
Author

/bot run

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR adds an optional duration parameter to benchmark execution settings and CLI options, enabling runtime-limited benchmarks. The parameter threads through the benchmark stack from CLI arguments to the async execution manager, where it enforces a time-based execution cap by monitoring elapsed time and stopping request processing when reached.

Changes

Cohort / File(s) Summary
Settings & Configuration
tensorrt_llm/bench/benchmark/__init__.py, tensorrt_llm/bench/benchmark/low_latency.py, tensorrt_llm/bench/benchmark/throughput.py
Add duration field to GeneralExecSettings and introduce --duration CLI parameter in latency and throughput benchmark commands, forwarding it to async execution.
Core Implementation
tensorrt_llm/bench/benchmark/utils/asynchronous.py
Implement duration-based execution control: add duration parameter to async_benchmark and LlmManager, track request start time, check elapsed time in worker loop, drain inbox when duration limit reached, and modify shutdown logic to conditionally cancel tasks.
Testing
tests/unittest/llmapi/test_bench_async.py
Add unit test for LlmManager duration enforcement, verifying request processing stops when time limit is exceeded.

Sequence Diagram

sequenceDiagram
    actor CLI
    participant async_benchmark
    participant LlmManager
    participant worker as Worker Loop
    participant inbox as Inbox Queue
    
    CLI->>async_benchmark: Call with duration=N seconds
    async_benchmark->>LlmManager: Create with duration=N
    
    Note over CLI,LlmManager: Execution Phase
    activate worker
    worker->>worker: Initialize start_time=None
    
    loop For each request
        inbox->>worker: Receive request
        worker->>worker: Set start_time on first request
        worker->>worker: Calculate elapsed = now() - start_time
        alt elapsed < duration
            worker->>worker: Process request
        else elapsed >= duration
            worker->>worker: Log duration reached
            worker->>inbox: Drain remaining requests
            worker->>worker: Break loop
        end
    end
    
    deactivate worker
    
    Note over worker: Shutdown Phase
    alt stop event is set
        worker->>worker: Cancel remaining tasks
    else stop event not set
        worker->>worker: Wait for in-flight tasks
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding duration-based execution capability to the benchmark system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description provides clear explanation of the feature, changes, impacts, test coverage, and checklist verification.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

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

🧹 Nitpick comments (1)
tests/unittest/llmapi/test_bench_async.py (1)

17-17: Remove unused import.

The time module is imported but never used in this file.

🧹 Proposed fix
 import asyncio
-import time
 from unittest.mock import MagicMock
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unittest/llmapi/test_bench_async.py` at line 17, Remove the unused
top-level import "time" from tests/unittest/llmapi/test_bench_async.py; locate
the import statement at the top of the file and delete the line "import time" so
the file no longer imports an unused module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/unittest/llmapi/test_bench_async.py`:
- Line 17: Remove the unused top-level import "time" from
tests/unittest/llmapi/test_bench_async.py; locate the import statement at the
top of the file and delete the line "import time" so the file no longer imports
an unused module.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a86d31f-8f27-450a-8a37-83b52e329f83

📥 Commits

Reviewing files that changed from the base of the PR and between b3a3b94 and abb477e.

📒 Files selected for processing (5)
  • tensorrt_llm/bench/benchmark/__init__.py
  • tensorrt_llm/bench/benchmark/low_latency.py
  • tensorrt_llm/bench/benchmark/throughput.py
  • tensorrt_llm/bench/benchmark/utils/asynchronous.py
  • tests/unittest/llmapi/test_bench_async.py

Signed-off-by: Kuo Wei <weikuo@google.com>
@weikuo0506

Copy link
Copy Markdown
Author

/bot run

@svc-trtllm-gh-bot svc-trtllm-gh-bot added the Community want to contribute PRs initiated from Community label Apr 23, 2026
@dc3671
dc3671 requested a review from FrankD412 April 27, 2026 03:35
@dc3671

dc3671 commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

@FrankD412 Can you help review this?

@FrankD412

Copy link
Copy Markdown
Collaborator

@FrankD412 Can you help review this?

Can do -- will handle it asap 🙂


if self.duration is not None and self.start_time and time.perf_counter() - self.start_time >= self.duration:
logger.info("Duration reached. Stopping pulling requests.")
while not self._inbox.empty():

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.

Question here -- if the duration is over, do we want to accept new requests? Is this simply to make sure that we don't violate concurrency/keep the engine saturated? Couldn't we just set the stop event and break the loop?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hi @FrankD412, thanks for the review! The reason we don't set self._stop.set() and just break here is to support the drain period for in-flight requests:

  1. If we set self._stop.set(), the finally block will detect it and proactively cancel all remaining active tasks (for task in self._tasks: task.cancel()). This would interrupt requests that are currently being processed and cause us to lose their performance statistics.
  2. The reason we clear _inbox is that async_benchmark relies on backend.busy returning False to determine when it can wrap up. Since the dataset submission process eagerly enqueues all requests into the _inbox at the start, the inbox is full. If we break without clearing the inbox, backend.busy will stay True, causing the benchmark to hang indefinitely. Clearing the inbox allows us to skip all upcoming requests while letting the in-flight ones run to completion and be recorded.

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.

Ok — yeah, looking over this it makes sense. For some reason it wasn’t clicking on my last review but that makes sense!

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 this while loop body, all calls are synchronous, so it will fully drain all requests from the inbox and dispatch them until the queue is empty. Only then does the first scheduling point occur, at which time the first request gets scheduled and self.start_time is set in the _process_single_request function.

If the above logic is correct, then regardless of how duration is configured, all requests will still be dispatched in full (i.e., the control logic runs before the actual behavior, resulting in a priority inversion).

Given that dispatched requests are not actively killed at present, those requests will eventually complete as in-flight requests. Therefore, duration cannot effectively control the benchmark run time.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hi @YihuiLu512, thanks for catching this — you're right, and the analysis is exactly on point.

Since the dispatch path in the worker loop has no await point (get_nowait() and create_task() are both synchronous), the entire inbox is dispatched into tasks in one synchronous burst before the first request coroutine ever runs. start_time is therefore still None throughout dispatch, so the duration check could never fire, and the finally block then waited for every dispatched task to complete — --duration had no effect on runtime.

I've pushed a fix that enforces the deadline at execution time instead:

  1. Each request coroutine now checks the deadline after acquiring its concurrency slot and returns without submitting (and without emitting a perf item) if the duration has elapsed. In-flight requests still drain to completion, preserving the statistics semantics discussed earlier in this thread with @FrankD412. The multi-turn path also stops issuing further turns once the deadline passes.
  2. The worker-side check you quoted is kept, with a comment documenting precisely why it cannot fire during dispatch and what it does do: it lets the worker exit its idle loop cleanly after the deadline, and the inbox drain defensively guarantees busy can reach False (preventing a benchmark hang if request submission ever becomes paced).
  3. Known limitation: with unbounded concurrency (--concurrency unset), all requests are submitted to the engine immediately and no client-side checkpoint remains — enforcing duration there would require aborting engine-side requests, which also raises a stats-censoring question (requests still running at the deadline skew long). I've added a warning log for this combination and propose handling engine-side abort in a follow-up issue to keep this PR's scope contained.
  4. Tests: the previous unit test passed only due to a timing artifact (it sampled outbox.qsize() while the third request was still in flight). It now awaits full worker drain, and I've added a regression test for exactly the eager-dispatch scenario you described (20 requests, concurrency=2, duration=1s → ~10 processed, wall time bounded by duration plus one drain).

Please take another look when you get a chance. Thanks again for the careful review!

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.

@weikuo0506 , thank you for the quick turnaround!
A quick comment, while waiting for @YihuiLu512 's reply.
Given the known limitation you mentioned, the test_async_benchmark_duration also doesn't provide --concurrency setting, and expecting only 2 of 3 requests to be processed. I'm not sure if the test will pass.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hi @karljang, good catch — thank you!

You're right that test_async_benchmark_duration runs without a concurrency setting. Digging into it: without a concurrency limit, all three mock requests (0.6s each) start immediately and finish at ~t=0.6s, before the 1s deadline — so the == 2 assertion couldn't hold. This was an oversight in the test when I first wrote it (its sibling tests test_llm_manager_duration and test_llm_manager_duration_not_exceeded both correctly pass concurrency=1, and its own comment — "only 2 should complete within 1s limit" — assumes serialized execution). The flaw predates yesterday's enforcement fix: with unbounded concurrency, every request starts before the deadline under both the old and new code.

I've pushed a commit that passes concurrency=1 to async_benchmark in that test, with a comment explaining why the limit is required for duration to bound the run (matching the warning now emitted by LlmManager for the unbounded case).

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.

@YihuiLu512 , could you please take a look at the updated code? Thank you!

weikuo0506 added 3 commits May 4, 2026 08:55
Signed-off-by: Kuo Wei <weikuo@google.com>
…async_benchmark

Signed-off-by: Kuo Wei <weikuo@google.com>
Signed-off-by: Kuo Wei <weikuo@google.com>
@weikuo0506

Copy link
Copy Markdown
Author

Hi @dc3671 , can you help to review? Thanks

@karljang

Copy link
Copy Markdown
Collaborator

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #53742 [ run ] triggered by Bot. Commit: a18ac06 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #53742 [ run ] completed with state FAILURE. Commit: a18ac06
/LLM/main/L0_MergeRequest_PR pipeline #42867 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

Link to invocation

@karljang

Copy link
Copy Markdown
Collaborator

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #54496 [ run ] triggered by Bot. Commit: a18ac06 Link to invocation

@karljang

Copy link
Copy Markdown
Collaborator

/bot kill

@karljang

Copy link
Copy Markdown
Collaborator

@weikuo0506,
It appears there are pre-commit failures. Could you please address them before we proceed?

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #54501 [ kill ] triggered by Bot. Commit: a18ac06 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #54496 [ run ] completed with state ABORTED. Commit: a18ac06

Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #54501 [ kill ] completed with state SUCCESS. Commit: a18ac06
Successfully killed previous jobs for commit a18ac06

Link to invocation

Signed-off-by: Kuo Wei <weikuo@google.com>
The worker dispatches the entire inbox in one synchronous burst (no
await point on the dispatch path), so start_time is still None when the
duration check runs and every request is dispatched and completed
regardless of the configured duration.

Enforce the deadline in the request coroutines instead, after a
concurrency slot is acquired: requests past the deadline are skipped
while in-flight requests drain to completion, keeping recorded
statistics unbiased. The multi-turn path stops issuing further turns
past the deadline. Warn when --duration is combined with unbounded
concurrency, where no client-side checkpoint exists to enforce it.

Rework the unit test to await full worker drain (it previously passed
only due to a timing artifact) and add a regression test for the
eager-dispatch scenario raised in review.

Signed-off-by: Kuo Wei <weikuo@google.com>
@weikuo0506
weikuo0506 force-pushed the feat/duration-bench branch 2 times, most recently from 70d1283 to 10a197e Compare July 23, 2026 13:54
The test asserts that 2 of 3 requests (0.6s each) complete within a 1s
duration, which requires requests to run back-to-back. Without a
concurrency limit all three start immediately and finish before the
deadline, so the assertion could not hold: duration cannot bound an
unbounded-concurrency run, as there is no client-side checkpoint after
dispatch. Pass concurrency=1, matching the test's sibling LlmManager
duration tests and its own stated intent.

Signed-off-by: Kuo Wei <weikuo@google.com>
@weikuo0506
weikuo0506 force-pushed the feat/duration-bench branch from 10a197e to acd262b Compare July 23, 2026 14:55
duration=duration,
)

req = InferenceRequest(input_ids=[1, 2, 3], output_tokens=10)

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.

InferenceRequest’s task_id: int is still required and has no default value at head 1dd8b9 (tensorrt_llm/bench/dataclasses/general.py:22). Can the current constructor still run successfully?

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.

same problem for: L62/L134/L192/L241

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed and fixed — InferenceRequest.task_id is required with no default, so all four constructions (L62/L134/L192/L241) raised ValidationError and no test in the file could run. Each now passes a task_id; the three requests in test_async_benchmark_duration get distinct ids since they model distinct dataset entries.

# 1s deadline and is skipped. Without a concurrency limit all three would
# start immediately and finish within 0.6s, before the deadline — duration
# cannot bound an unbounded-concurrency run (see LlmManager warning).
assert len(stats.requests) == 2

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 assertion seems impossible to satisfy. stats is a dictionary deduplicated by request_id, and here request_id is always 1 (L231). Therefore, both items have request_id == 1, so they both hit key 1, leaving only one key in the dictionary. As a result, len(stats.requests) == 1, while the assertion expects == 2, so the test will inevitably fail.

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.

Does this test pass on your local machine?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed on both counts.

The assertion: StatsKeeper.requests is keyed by request_id, which comes from response.id, and the mock returned a single shared object with id = 1. Both completed requests therefore collapsed into one RequestRecord — which not only made len(stats.requests) == 2 unsatisfiable but merged their events, skewing the recorded statistics. generate_async now returns a fresh output with a distinct id per call. (Worth noting num_complete increments without dedup, so it would have passed either way — len(stats.requests) was the only assertion that could catch this.)

"Does this test pass on your local machine?" — it did not, and I had not run it. I've since set up an environment that can (tensorrt-llm==1.3.0rc21 matching the branch, CPU-only — the tests mock LLM entirely and need no GPU) and confirmed the before/after:

before:  4 failed in 17.88s
after:   4 passed in 22.83s   (stable across 3 consecutive runs)

Running it also surfaced a third defect neither of us had caught: from tensorrt_llm._tensorrt_engine import LLM — that module was removed in #15918 and reached this branch through a main merge, so the file failed at collection. It now imports LLM from tensorrt_llm, matching the other tests in this directory.

Root cause: tests/unittest/llmapi/test_bench_async.py is not referenced by any list under tests/integration/test_lists/, so CI has never collected it — which is how all three defects survived. I've registered it in l0_cpu_x86.yml and l0_cpu_arm.yml (both system_gpu_count: 0). I also replaced a fixed 1.5s sleep with awaiting the perf items, and widened the margin on the eager-dispatch wall-clock bound, since these now run in pre-merge. Happy to move it to a different stage if you'd prefer.

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

No further issues are apparent in the functional logic.

However, the test is unsound and is not included in the CI test list. In light of the ongoing code normalization effort (/cc @QiJune ), please evaluate whether to remove the invalid test or to correct it.

This file is not referenced by any test list, so CI has never collected
it and three defects went unnoticed since it was added:

- tensorrt_llm._tensorrt_engine was removed in NVIDIA#15918 and reached this
  branch through a main merge, so the module import failed at collection
  time and no test in the file could run.
- InferenceRequest.task_id is required with no default, so every request
  construction raised ValidationError.
- StatsKeeper keys its records by request id while the mock returned one
  shared id, so both completed requests collapsed into a single record.
  That merges their events and skews the recorded statistics, and it made
  the len(stats.requests) == 2 assertion unsatisfiable.

Also harden two timing-sensitive waits. The duration-not-exceeded test
slept a fixed 1.5s for work that takes 1.2s; it now awaits the perf items
themselves, which is both faster and immune to scheduling delay. The
eager-dispatch test now issues enough requests that an unenforced run
would take ~5s against a ~1s enforced run, so the wall-clock bound has a
wide margin in both directions instead of 0.3s.

Signed-off-by: Kuo Wei <weikuo@google.com>
CI collects unit tests only through the lists under
tests/integration/test_lists/test-db, so this file has never run since it
was added and its failures went unnoticed. Register it in the two
GPU-free pre-merge stages: the tests mock LLM entirely and need no
device.

Signed-off-by: Kuo Wei <weikuo@google.com>
@weikuo0506

Copy link
Copy Markdown
Author

@karljang @YihuiLu512 — pushed fixes for both of YihuiLu512's findings, plus a third defect that running the tests surfaced (_tensorrt_engine was removed in #15918 and reached this branch via a main merge, breaking collection). Verified locally against tensorrt-llm==1.3.0rc21: 4 failed → 4 passed, stable across 3 runs.

One change worth flagging: this test file was not in any test list, so CI has never collected it — that's how the defects survived. I registered it in l0_cpu_x86.yml / l0_cpu_arm.yml (both system_gpu_count: 0, tests are fully mocked). Please let me know if a different stage suits better. Ready for CI whenever convenient.

# Conflicts:
#	tests/integration/test_lists/test-db/l0_cpu_x86.yml
)
for task in self._tasks:
task.cancel()
else:

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.

When _raise_for_failed_tasks raises a ValueError, self._stop is not set (stop is only set by backend.stop() during normal shutdown). This causes execution to enter the else path, skip cancellation, and wait for all tasks to finish. As a result, runtime on failure may be longer than the pre-change baseline.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — this was a real regression. self._stop is only set by an explicit shutdown, so the failure path took the drain branch and the worker waited for every outstanding request before surfacing the error. Draining is only correct on duration expiry, where the point is to record the in-flight requests' statistics; on failure the run is aborting anyway.

Fixed by tracking the duration-triggered exit explicitly and cancelling in every other case, so a stop signal and a failure both behave as they did before duration support was added. Also added a regression test for the failure path, which had no coverage — verified it fails against the previous code (worker blocks on the hung requests until the test times out) and passes with the fix.

)
@optgroup.option(
"--duration",
type=int,

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.

0 and negative values are accepted. With duration=0, _duration_exceeded() becomes true immediately after start_time is first set for the initial request, effectively skipping almost all requests. Please evaluate whether this aligns with the intended design.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed — --duration 0 silently produced a run that skips nearly everything while looking like it worked, which is worse than an error.

I considered treating 0 as "no limit" for consistency with --num-requests, but "run for zero seconds" has no useful meaning the way "cap at the dataset length" does, so a silent reinterpretation seemed more surprising than a rejection. The option is now click.IntRange(min=1) on both throughput and latency, which also rejects negatives and reports the mistake at the command line. Leaving it unset still means no limit.

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

LGTM

Please address the remaining corner cases as appropriate.

Draining in-flight requests instead of cancelling them is only correct
when the duration limit is reached, where the point is to record their
statistics. The check used self._stop, which is only set by an explicit
shutdown, so a request failure took the drain path too: the worker waited
for every outstanding request before surfacing the error, making a failing
run slower than it was before duration support was added.

Track the duration-triggered exit explicitly and cancel in every other
case, restoring the original abort behaviour on failure while keeping the
drain semantics on expiry. Add a regression test covering the failure
path, which previously had no coverage.

Signed-off-by: Kuo Wei <weikuo@google.com>
--duration accepted 0 and negative values. With 0 the deadline is already
past once start_time is set for the first request, so every subsequent
request is skipped and the benchmark produces almost no data while
appearing to run normally.

Constrain the option to positive integers so the mistake is reported at
the command line instead. Leaving it unset still means no limit.

Signed-off-by: Kuo Wei <weikuo@google.com>
@weikuo0506
weikuo0506 requested a review from a team as a code owner July 28, 2026 12:21
@weikuo0506

Copy link
Copy Markdown
Author

Thanks @YihuiLu512 — both corner cases are pushed: the failure path now cancels in-flight requests (with a regression test), and --duration is click.IntRange(min=1) on both commands.

On your earlier point about the test not being in the CI list, I corrected it rather than removing it and registered it in l0_cpu_x86.yml / l0_cpu_arm.yml. Happy to move it if the normalization effort you mentioned prefers otherwise.

task.cancel()
logger.debug("Waiting for requests...")
if self._tasks:
await asyncio.wait(self._tasks)

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.

@weikuo0506 , thank you for addressing the review comments!
I believe this comment might be the last one :)

There may still be a failure-after-duration corner case. If the duration expires first, drain_in_flight becomes True and the finally block waits for every task. If an in-flight request then fails while another remains slow or hung, the failure is not checked until all tasks finish.

Could the drain path use asyncio.wait(..., return_when=asyncio.FIRST_EXCEPTION) and cancel the pending tasks when any completed task has an exception? If all tasks succeed, it would retain the intended duration-drain behavior. A regression test where duration expires before one request fails and another hangs would cover this ordering.

Thank you.

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

Labels

Community want to contribute PRs initiated from Community

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[None][feat] Add duration-based execution to trtllm-bench

9 participants