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.
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.
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 |
|---|---|
| An assertion comparing a running token total against the configured maximum, firing under load | 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. |
| The scheduler making no forward progress while requests remain queued | 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. |
| Throughput collapsing to zero without any request completing or failing | 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 condition appearing only above a threshold request rate and clearing on restart | 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. |
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
Apply the following checklist to a small reproduction: each box below is a positive signal that you are looking at this exact failure rather than a sibling in the same taxonomy.
- ✓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.
Example training logs (fingerprint)
AssertionError: assert total_num_tokens <= self.max_num_tokens
[TRT-LLM] [E] Encountered an error in forward function
[TRT-LLM] [E] Error in event loopTimestamps and exact values vary across runs, but the pattern. An info-level start, an early WARN, an ERROR carrying the symptom. Is the actual fingerprint you should alert on. The Denpex platform flags this combination automatically.
Root cause, fix & prevention, signed in
Sign up free to see why this failure really happens, the exact remediation steps, and the production-grade prevention pattern. You also get 3 free full diagnoses for your own training logs.
Sign up free. Unlock the full analysisNo credit card · 3 free diagnoses · 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.
Code examples
// Typical pattern:
import torch.optim as optim
optimizer = optim.AdamW(model.parameters(), lr=3e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_training_steps)
for step in range(num_training_steps):
optimizer.zero_grad()
loss = model(batch)
loss.backward()
optimizer.step()
scheduler.step()Adapt the snippet to your framework. The same pattern holds for PyTorch Lightning, Hugging Face Trainer, DeepSpeed, Megatron-LM, and vLLM training wrappers. Where the wrapper exposes a config flag (for examplelr_scheduler_type in Trainer), prefer the flag over the imperative API to keep the schedule declarative and reproducible.
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 |
Real engineering notes
“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 = 0Related failures to investigate next
Root cause, fix & prevention
Frequently asked questions
Twelve targeted questions that engineers and on-call staff most 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.