fix: fail fast on endpoint response stalls - #462
Conversation
|
MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅ |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #462 +/- ##
=======================================
Coverage ? 81.31%
=======================================
Files ? 151
Lines ? 20528
Branches ? 0
=======================================
Hits ? 16693
Misses ? 3835
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
02eeec7 to
a5b80ac
Compare
|
|
||
| 1. Publish `SessionEventType.STARTED` | ||
| 2. Start receiver coroutine (`_receive_responses`) | ||
| 2. Start receiver coroutine (`_receive_responses`). When the no-progress timeout is set, each active in-flight cohort arms a liveness watchdog; it exits as soon as the cohort drains. The watchdog fails the session only when an in-flight request has no streamed chunk or final result for the configured interval. |
There was a problem hiding this comment.
Lets not use cohort here.
| help=( | ||
| "Fail a run when in-flight requests make no " | ||
| "response progress for this many seconds" | ||
| ), | ||
| ), | ||
| ] = Field( | ||
| None, | ||
| gt=0, | ||
| description=( | ||
| "Fail a run when requests are in flight but no " | ||
| "response chunk or completion arrives for this many seconds. Disabled " | ||
| "by default; set above the longest expected interval between response " | ||
| "progress (full request latency for non-streaming endpoints) and, for " | ||
| "TensorRT-LLM disaggregated serving, match hang_detection_timeout." | ||
| ), |
There was a problem hiding this comment.
Lets keep a single copy in the help/description so there isn't drift over time.
arekay-nv
left a comment
There was a problem hiding this comment.
The opt-in liveness guard is useful, but this implementation adds avoidable work and state to the load generator's hot paths. The most serious issue is retaining every completed UUID in a second phase-lifetime container, which is an O(total requests) memory regression at the repository's 50k+ QPS target. The disabled path also performs work for every response, and low-concurrency workloads can create/cancel a watchdog task per request. Please keep the disabled path inert and use bounded active-request state plus a single event/deadline-driven watchdog.
Add a no-progress deadline for in-flight endpoint requests. An accepted request that never produces a response chunk or final result would otherwise block the benchmark until its outer wall-time limit, because the phase drain waits on responses that never arrive. Disabled by default (`settings.timeouts.no_progress_timeout_s`, `null`). Rebased onto main and moved the setting into the Timeouts model added by mlcommons#409, as requested in review. Review feedback addressed: - [P1] Do not retain every completed UUID. `completed_uuids` now only holds synthetic completions from `register_skipped`, so it no longer grows O(total requests) alongside `uuid_to_index`. - [P2] Keep the disabled path inert. Activity tracking is gated on the feature being enabled, and the receiver reuses the arrival timestamp it already takes rather than reading the clock a second time. Measured cost when disabled: +3.5 ns per response, +9.4 ns per request. - [P2] Replace the per-transition watchdog task with a single self-rearming `loop.call_later` TimerHandle. No task, no Event, no per-iteration `wait_for`. Under the production eager task factory this cuts arm+cancel from 2205 ns to 535 ns per request (4.1x). - [P2] Compute the remaining deadline from the last progress stamp instead of sleeping a full interval each iteration, which could delay detection by up to 2x the configured timeout. - Drop `cohort` from the docs and from the local variable in `issue()`. - Keep a single copy of the tuning guidance: the schema description is the short form, docs/config/DESIGN.md holds the detail. Also fixed while here: a receiver transport error could overwrite an earlier NoProgressError, masking the real diagnosis when a stalled endpoint also drops its connection. First error now wins. Validation: - `pytest tests/unit/config/test_schema.py tests/unit/commands/test_benchmark.py tests/unit/load_generator/test_async_session.py` (425 passed) - `pre-commit run --all-files` (mypy reports 3 pre-existing Darwin-only `os.sched_*affinity` errors in files this change does not touch) - `python scripts/regenerate_templates.py --check` - AGA disaggregated held-response canary, job 578946: failed as intended with `Endpoint made no response progress for 10.0s with 1 request(s) in flight`, 10.002 s after the phase started against a 10 s deadline. The 10 s value is fault-injection coverage, not the deployment recommendation. Not adopted: the suggestion to use one phase/session-lifetime watchdog driven by an activity event. The guard is still armed on the 0-to-1 in-flight transition and retired on drain, now with a TimerHandle rather than a task. This keeps the timer heap clear once a phase drains and measured 4.1x cheaper than the reviewed version; happy to revisit if a resident watchdog is preferred. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
a5b80ac to
6d6f42a
Compare
|
Thanks for the comments @viraatc and @arekay-nv . All issues resovled other than one. For the per transition watchdog, I choosed a different approche than your suggested ,so flagging it here. the guard is still armed on the 0→1 in-flight transition and retired on drain — but it's now a single Also measured arm+cancel per request, under the eager task factory this repo uses in production:
It also leaves the timer heap clear once a phase drains, which covers the "stale timer work" part of your comment. let me know if that make sense to you, Thanks a lot! |
Summary
settings.timeouts.no_progress_timeout_splus a--no-progress-timeoutCLI aliasWhen this is useful
Use this for automated runs where a request can be accepted but the endpoint then becomes silent — for example, a TensorRT-LLM disaggregated executor or KV-transfer stall that never reaches the normal terminal-error path. It is engine-agnostic: it catches the same client-visible silent failure through vLLM, a frontend, or transport. Without it the benchmark blocks until its outer wall-time limit, because the phase drain waits on responses that never arrive and
drain_timeoutdefaults to unlimited.Disabled by default. It starts after work is issued, runs only while requests are in flight, and resets on an observed stream chunk or final result. It does not diagnose or restart the backend — it makes the benchmark fail with a clear error. For non-streaming endpoints, configure it above the full expected request latency. For TensorRT-LLM disaggregated serving the documented starting value is 300 s, matching the executor
hang_detection_timeout.Changes since the last review
Rebased onto latest main and moved the setting into the
Timeoutsmodel added by #409 (@viraatc).completed_uuidsretained every completed UUIDregister_skippedadds tombstones now, so it no longer grows O(total requests) alongsideuuid_to_indexloop.call_laterTimerHandle — no task, noEvent, no per-iterationwait_forcohortwordingissue()docs/config/DESIGN.mdholds the tuning detailMeasured cost when disabled (A/B against the base commit): +3.5 ns per response, +9.4 ns per request. With
stream_all_chunks: false(the default) the main process sees 2 messages per request, not one per token.Measured cost of the arming change under the production eager task factory: arm+cancel per request went from 2205 ns to 535 ns (4.1x).
Also fixed while here
A receiver transport error could overwrite an earlier
NoProgressError. A stalled endpoint often drops its connection too, so the real diagnosis was being masked by a generic "receiver failed" message. First error now wins, with a regression test.Not adopted
The suggestion to use one phase/session-lifetime watchdog driven by an activity event. The guard is still armed on the 0→1 in-flight transition and retired on drain — now with a
TimerHandlerather than a task. This keeps the timer heap clear once a phase drains, and measured 4.1x cheaper than the reviewed version. Happy to switch to a resident watchdog if that is preferred.Validation
pytest tests/unit/config/test_schema.py tests/unit/commands/test_benchmark.py tests/unit/load_generator/test_async_session.py— 425 passedpre-commit run --all-files— all hooks pass except mypy, which reports 3 pre-existingos.sched_*affinityerrors on macOS incpu_affinity.py/token_metrics.py; neither file is touched by this PR and the symbols exist on Linuxpython scripts/regenerate_templates.py --checkEndpoint made no response progress for 10.0s with 1 request(s) in flight, 10.002 s after the phase started against a 10 s deadline. The 10 s value is fault-injection coverage, not the deployment recommendation.Timer lifecycle is covered in both directions: the perf issue cap (
stop_current_phase) deliberately leaves the guard armed so a stall during drain is still caught, while drain completion,stop(), a phase change, and session teardown each retire it.