An inference scheduler deadlocks under sustained load and stops issuing work
A continuous-batching scheduler admits requests against a token budget it must not exceed. Under sustained concurrency the accounting can reach a state where no admitted request can advance and no new one can be admitted, so the server stops issuing work while remaining connected and apparently healthy.
The scheduler is wedged, not overloaded. Lower the maximum batched token budget and the concurrent request limit so it runs with headroom, and alert on zero completions with a non-empty queue.
- Symptom
assert total_num_tokens <= self.max_num_tokens, (- Root cause
- Admission is a resource-allocation decision made repeatedly against a shared budget. If a request's demand can grow after it has been admitted, the scheduler can reach a state where the total exceeds what it promised itself, with every admitted request needing more budget to finish and none able to release any. That is a deadlock in the classical sense rather than an overload: no participant can proceed and none will yield.
- Recommended fix
- Reduce the maximum batched token budget so the scheduler operates with headroom rather than at its ceiling, which removes the tight contention the state requires.
- How Denpex helps
- Denpex matches An inference scheduler deadlocks under sustained load and stops issuing work across every rank in a distributed run and reports which rank failed first, so you act on the initiating node instead of the loudest one.
What this failure is
A scheduling deadlock in a continuous-batching inference server where admitted requests collectively claim more of the token budget than it holds, leaving none able to progress and none able to release, so the server stops issuing work without failing.
Is this what broke your run? Paste your log.
You're reading about An inference scheduler deadlocks under sustained load and stops issuing work. Paste your own crash log or traceback below and get the real root cause for YOUR run, not this generic entry. No account, no card. Logs are masked at ingress and never saved to account history.
Why it happens (the mechanism)
Continuous batching trades simplicity for utilisation: requests join and leave a running batch, and a shared budget decides who is admitted. Any admitted request whose demand can grow turns that budget from a bound into a claim, and claims that cannot be satisfied or withdrawn are the definition of deadlock. High concurrency is what makes the interleaving that reaches it likely.
What you'll observe
- Throughput reaches zero while the process stays up and keeps accepting connections
- It only appears above a certain request rate, so it never reproduces in a quiet environment
- An assertion about token accounting fires, which reads as an internal bug rather than saturation
- Restarting clears it until the load returns
Common symptoms and what they mean
| Symptom | Why it happens |
|---|---|
| assert total_num_tokens <= self.max_num_tokens failing inside the engine's request-preparation path | Admission is a resource-allocation decision made repeatedly against a shared budget. If a request's demand can grow after it has been admitted, the scheduler can reach a state where the total exceeds what it promised itself, with every admitted request needing more budget to finish and none able to release any. |
| [TRT-LLM] [E] Encountered an error in forward function, followed by an error in the event loop reporting that sampling failed | That is a deadlock in the classical sense rather than an overload: no participant can proceed and none will yield. It is why the server neither completes work nor sheds it, and why nothing times out on its own. |
| The reported total exceeding the configured maximum by a handful of tokens rather than by a large margin | It is load-dependent because the state requires a particular interleaving of admissions and completions. At low concurrency the budget is never contended tightly enough to reach it, which is why it survives testing and appears in production. |
| The overshoot varying between runs, sometimes a few tokens and occasionally by thousands | Admission is a resource-allocation decision made repeatedly against a shared budget. If a request's demand can grow after it has been admitted, the scheduler can reach a state where the total exceeds what it promised itself, with every admitted request needing more budget to finish and none able to release any. |
| Disabling partial reuse making the failure arrive sooner rather than preventing it | That is a deadlock in the classical sense rather than an overload: no participant can proceed and none will yield. It is why the server neither completes work nor sheds it, and why nothing times out on its own. |
| An assertion comparing a running token total against the configured maximum, firing under load | It is load-dependent because the state requires a particular interleaving of admissions and completions. At low concurrency the budget is never contended tightly enough to reach it, which is why it survives testing and appears in production. |
Which systems are affected
- Continuous and in-flight batching schedulers that admit against a global token budget
- Deployments running near their configured maximum batched tokens rather than comfortably below it
- Speculative decoding and other features that make a request's token demand vary during its lifetime
- Long-context traffic, where a single request can consume a large share of the budget
How to confirm this is the problem
Use this checklist to test the hypothesis against a small reproduction. No single line proves the root cause, so preserve the preceding events and compare one variable at a time.
- ✓Compare the reported total against the configured maximum. An overshoot of a few tokens points at the admission arithmetic rather than at a request that is genuinely too large.
- ✓Raise the configured maximum and re-run the same traffic. Failing again at the new ceiling confirms the accounting rather than the value.
- ✓Turn partial reuse off. Reaching the assertion sooner rather than later shows it is not the cause and rules out one variable.
- ✓Check whether completions have stopped while the queue is non-empty; a wedged scheduler shows both at once, whereas an overloaded one still completes work slowly.
- ✓Correlate onset with request rate rather than with any deployment change. A threshold that appears above a particular concurrency and not below it indicates a state reachable only under contention.
- ✓Reduce the batched token budget and replay the same load. Surviving with a smaller ceiling confirms budget contention rather than a fault in any single request.
Root cause
- Admission is a resource-allocation decision made repeatedly against a shared budget. If a request's demand can grow after it has been admitted, the scheduler can reach a state where the total exceeds what it promised itself, with every admitted request needing more budget to finish and none able to release any.
- That is a deadlock in the classical sense rather than an overload: no participant can proceed and none will yield. It is why the server neither completes work nor sheds it, and why nothing times out on its own.
- It is load-dependent because the state requires a particular interleaving of admissions and completions. At low concurrency the budget is never contended tightly enough to reach it, which is why it survives testing and appears in production.
The fix and how to prevent it
Searchable error signature
assert total_num_tokens <= self.max_num_tokens, (
[TRT-LLM] [E] Encountered an error in forward function:
[TRT-LLM] [E] Error in event loop: Sampling failed
total_num_tokens=4118 > 4096
File ".../model_engine.py", line 2678
AssertionError: assert total_num_tokens <= self.max_num_tokens
[TRT-LLM] [E] Encountered an error in forward function
[TRT-LLM] [E] Error in event loopUse this text as a lookup key in logs and upstream issue trackers. It is not presented as a captured customer log. Confirm the cause from your own preceding events, versions, configuration and the cited references.
The fix and the prevention pattern
The root cause is on this page and stays free. A free account adds the exact remediation steps, saved history, and the fix on every entry in the encyclopedia.
Sign up free. Unlock the full analysisNo credit card. Daily allowance follows verified trust tier. Instant access.
Why the recommended fix works
Operating below the ceiling means an admitted request's growth can be absorbed rather than contended, so the state is never reached. Reducing concurrency does the same from the other side by limiting how many claims exist at once. Neither improves peak throughput on paper, and both are what keep the measured throughput above zero.
Best practices by model family
| Model / Stack | Recommendation | Notes |
|---|---|---|
| Wedged under peak load | Lower the maximum batched token budget | Removes the tight contention the deadlock state requires. |
| High concurrency traffic | Lower the concurrent request limit | Bounds how many outstanding claims exist against the budget. |
| Speculative decoding enabled | Disable it while diagnosing | It lets an admitted request's demand grow after admission. |
| Any production pool | Alert on zero completions with a non-empty queue | Liveness and readiness both pass while the server does nothing. |
With the fix vs without the fix
| Dimension | With the fix | Without the fix |
|---|---|---|
| Overloaded versus wedged | Wedged completes nothing at all | Assumed to be slow rather than stopped |
| What health checks report | Healthy, because the process answers | Expected to fail and evict the instance |
| Effect of restarting | Restores service until load returns | Taken as evidence the problem is fixed |
Diagnostic note
“Every health check will report this instance as fine. The process is running, the port answers, and readiness passes, so an orchestrator keeps routing to a server that completes nothing. If you take one thing from this entry, make it the alert on completions rather than on liveness, that gap is what turns a scheduler bug into a silent outage across a whole pool.”
Visual fingerprint
budget: max_num_tokens
admitted A needs more to finish ---+
admitted B needs more to finish ---+--> total > budget
admitted C needs more to finish ---+
none can complete -> none releases budget
none can be admitted -> queue grows, completions = 0Diagnose this failure in VS Code
Select the traceback or open the failed terminal, then run Denpex locally to see the initiating rank, collateral failures, exact fix, and verification command without uploading the log.
Install the free VS Code extensionRelated failures to investigate next
Frequently asked questions
Questions engineers and on-call staff commonly ask about this failure.
Is the server just overloaded?
Why do health checks pass?
It only happens in production.
Restarting fixes it. Is that enough?
Don't just read the fix, diagnose your run
The encyclopedia tells you what went wrong. Denpex tells you what went wrong in YOUR training run. With your logs, your config, and your stack.