<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Scheduling on Synaptic Radio</title><link>https://synapticradio.com/tags/scheduling/</link><description>Recent content in Scheduling on Synaptic Radio</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>anshuman264@gmail.com (Anshuman Sahoo)</managingEditor><webMaster>anshuman264@gmail.com (Anshuman Sahoo)</webMaster><copyright>Anshuman Sahoo</copyright><lastBuildDate>Mon, 20 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://synapticradio.com/tags/scheduling/index.xml" rel="self" type="application/rss+xml"/><item><title>Continuous Batching Needs Admission Control</title><link>https://synapticradio.com/post/continuous-batching-admission-control/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0000</pubDate><author>anshuman264@gmail.com (Anshuman Sahoo)</author><guid>https://synapticradio.com/post/continuous-batching-admission-control/</guid><description>&lt;img src="https://synapticradio.com/images/continuous-batching-admission-control/cover.svg" alt="Featured image of post Continuous Batching Needs Admission Control" />&lt;p>Continuous batching is often described as a scheduling win: instead of waiting for an entire static batch to finish, the server inserts new requests between decoding steps. That description is correct, but incomplete. A production scheduler is not merely choosing which tokens to compute next. It is also deciding which requests are allowed to become memory obligations.&lt;/p>
&lt;p>That distinction matters because the compute budget and the KV-cache budget evolve differently. A scheduler may have room for another prompt chunk in the current iteration while lacking enough cache capacity to carry the request through its full input and subsequent decode. Admitting it can look locally efficient and still create a system-level failure: cache exhaustion, preemption, recomputation, and sharply worse tail latency.&lt;/p>
&lt;p>This article reconstructs that failure with a deterministic discrete-event simulator. The simulator is deliberately small. It does not estimate GPU kernel time, network overhead, or exact vLLM internals. It isolates one architectural question:&lt;/p>
&lt;blockquote>
&lt;p>What information must an admission decision reserve before a waiting request joins the running set?&lt;/p>&lt;/blockquote>
&lt;h2 id="one-scheduler-step-has-two-budgets">One scheduler step has two budgets
&lt;/h2>&lt;p>Consider a server with block-granular KV-cache allocation. At every engine step, the scheduler must respect at least two limits:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>A token execution budget.&lt;/strong> This caps how many prompt or decode tokens may be processed in the next model invocation.&lt;/li>
&lt;li>&lt;strong>A persistent cache budget.&lt;/strong> This caps how many KV blocks the running requests may own after the invocation.&lt;/li>
&lt;/ol>
&lt;p>vLLM exposes related controls including &lt;code>max_num_batched_tokens&lt;/code>, &lt;code>max_num_seqs&lt;/code>, a full-input reservation option, and a free-block watermark. Its documentation explicitly describes full-input reservation as protection against over-admission and cache thrashing under chunked prefill. It also describes preemption and recomputation when KV-cache space becomes insufficient.&lt;/p>
&lt;p>The important asymmetry is this: the token budget resets every step; allocated KV state persists across steps.&lt;/p>
&lt;p>Suppose a request has a 320-token prompt and the cache block size is 16 tokens. The first prefill chunk might consume only 64 scheduler tokens, but completing the input requires 20 cache blocks. An admission check that asks only whether the first chunk fits treats a 20-block obligation as a one-block decision.&lt;/p>
&lt;p>The request is cheap &lt;em>now&lt;/em> and expensive &lt;em>over its lifetime&lt;/em>.&lt;/p>
&lt;p>&lt;img src="https://synapticradio.com/images/continuous-batching-admission-control/throughput.svg"
loading="lazy"
alt="Throughput remains similar across policies while cache behavior changes"
>&lt;/p>
&lt;h2 id="the-mechanism-model">The mechanism model
&lt;/h2>&lt;p>The package includes &lt;code>code/simulate_admission.py&lt;/code>. Each synthetic request has:&lt;/p>
&lt;ul>
&lt;li>an arrival step;&lt;/li>
&lt;li>prompt length;&lt;/li>
&lt;li>requested output length;&lt;/li>
&lt;li>prefilled and decoded token counters;&lt;/li>
&lt;li>block-granular KV ownership;&lt;/li>
&lt;li>timestamps for admission, first token, and completion;&lt;/li>
&lt;li>a preemption counter.&lt;/li>
&lt;/ul>
&lt;p>Each scheduler step performs five operations:&lt;/p>
&lt;ol>
&lt;li>Move newly arrived requests into the waiting queue.&lt;/li>
&lt;li>Admit waiting requests while sequence slots and the admission policy permit.&lt;/li>
&lt;li>Spend the token budget on one-token decode work first.&lt;/li>
&lt;li>Spend the remaining budget on chunked prefills.&lt;/li>
&lt;li>If physical cache use exceeds capacity, preempt the newest running requests and reset their computed state.&lt;/li>
&lt;/ol>
&lt;p>The fifth operation models recomputation-based recovery. It is intentionally punitive because the request loses its previously computed prefix. That is not an arbitrary penalty; it is the direct cost of treating temporary scheduler progress as durable capacity.&lt;/p>
&lt;p>Three policies are compared:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Policy&lt;/th>
&lt;th>Admission test&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>naive&lt;/code>&lt;/td>
&lt;td>Admit when one new block and a sequence slot are available&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>reserve&lt;/code>&lt;/td>
&lt;td>Admit only when the full prompt can fit&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>reserve_watermark&lt;/code>&lt;/td>
&lt;td>Reserve the full prompt and retain 12.5% cache headroom&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>All three use the same FCFS workload, token budget, cache size, sequence limit, and random seed. The outputs are synthetic observations from this model, not claimed measurements of vLLM or any GPU.&lt;/p>
&lt;h2 id="what-the-synthetic-run-shows">What the synthetic run shows
&lt;/h2>&lt;p>The committed output in &lt;code>outputs/metrics.json&lt;/code> reports:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Policy&lt;/th>
&lt;th style="text-align: right">Completed&lt;/th>
&lt;th style="text-align: right">Preemptions&lt;/th>
&lt;th style="text-align: right">P95 TTFT&lt;/th>
&lt;th style="text-align: right">Throughput&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>naive&lt;/td>
&lt;td style="text-align: right">30&lt;/td>
&lt;td style="text-align: right">1923&lt;/td>
&lt;td style="text-align: right">393 steps&lt;/td>
&lt;td style="text-align: right">0.060 req/step&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>reserve&lt;/td>
&lt;td style="text-align: right">30&lt;/td>
&lt;td style="text-align: right">115&lt;/td>
&lt;td style="text-align: right">399 steps&lt;/td>
&lt;td style="text-align: right">0.060 req/step&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>reserve + watermark&lt;/td>
&lt;td style="text-align: right">30&lt;/td>
&lt;td style="text-align: right">77&lt;/td>
&lt;td style="text-align: right">416 steps&lt;/td>
&lt;td style="text-align: right">0.060 req/step&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;img src="https://synapticradio.com/images/continuous-batching-admission-control/preemptions.svg"
loading="lazy"
alt="Preemptions by policy"
>&lt;/p>
&lt;p>The naive policy admits more aggressively because it prices only the first cache block. During bursts, several long prompts enter the running set together. Their later chunks expand cache ownership until the set no longer fits. The simulator then preempts recently admitted work, returns it to the queue, and recomputes it later.&lt;/p>
&lt;p>The guarded policies appear more conservative at admission time, but they prevent the running queue from accumulating obligations the cache cannot satisfy. In this workload, that reduces repeated work and improves the stability of time-to-first-token.&lt;/p>
&lt;p>&lt;img src="https://synapticradio.com/images/continuous-batching-admission-control/p95-ttft.svg"
loading="lazy"
alt="P95 TTFT by policy"
>&lt;/p>
&lt;p>The watermark adds a different kind of protection. Full-input reservation asks, “Can these prompts fit?” A watermark asks, “Should the server consume the last available blocks even when a fit is technically possible?” Headroom gives decode growth, allocator granularity, and workload estimation error somewhere to go.&lt;/p>
&lt;p>The model therefore overturns a common intuition: maximizing the number of active sequences is not equivalent to maximizing useful throughput. A sequence that will soon be preempted is not productive concurrency.&lt;/p>
&lt;h2 id="admission-control-is-a-contract-not-a-heuristic">Admission control is a contract, not a heuristic
&lt;/h2>&lt;p>A robust admission decision needs an explicit resource contract. For a request \(r\), define:&lt;/p>
&lt;ul>
&lt;li>\(P_r\): prompt tokens not yet represented in cache;&lt;/li>
&lt;li>\(O_r\): remaining output-token allowance or estimate;&lt;/li>
&lt;li>\(B\): cache block size;&lt;/li>
&lt;li>\(K_{\text{free}}\): currently free blocks;&lt;/li>
&lt;li>\(W\): reserved watermark blocks.&lt;/li>
&lt;/ul>
&lt;p>A minimum prompt-safe admission condition is:&lt;/p>
\[
\left\lceil \frac{P_r}{B} \right\rceil \le K_{\text{free}} - W
\]&lt;p>This does not reserve the full decode lifetime. Doing so from &lt;code>max_tokens&lt;/code> can be far too conservative, especially when clients specify loose upper bounds. But the equation makes the policy boundary visible: the scheduler commits to completing the input without relying on future eviction.&lt;/p>
&lt;p>A production policy can extend the contract in several directions:&lt;/p>
&lt;ul>
&lt;li>reserve the full input but let decode grow incrementally;&lt;/li>
&lt;li>estimate output length from request class or historical data;&lt;/li>
&lt;li>route long-context requests to a separate capacity pool;&lt;/li>
&lt;li>cap concurrent long prefills;&lt;/li>
&lt;li>reject requests that can never fit;&lt;/li>
&lt;li>expose queueing and preemption as service-level signals.&lt;/li>
&lt;/ul>
&lt;p>The correct choice depends on the service objective. Interactive chat prioritizes TTFT and fairness differently from offline generation. A single FCFS queue may be acceptable for one and harmful for the other.&lt;/p>
&lt;h2 id="what-to-measure-before-changing-the-policy">What to measure before changing the policy
&lt;/h2>&lt;p>Admission control should not be tuned from average throughput alone. At minimum, record:&lt;/p>
&lt;ul>
&lt;li>waiting and running queue depth;&lt;/li>
&lt;li>free and used KV blocks;&lt;/li>
&lt;li>admitted prompt-token obligations;&lt;/li>
&lt;li>preemptions and recomputed tokens;&lt;/li>
&lt;li>TTFT and end-to-end latency by prompt-length bucket;&lt;/li>
&lt;li>completion throughput;&lt;/li>
&lt;li>rejection counts;&lt;/li>
&lt;li>cache-watermark violations;&lt;/li>
&lt;li>sequence-slot utilization versus token-budget utilization.&lt;/li>
&lt;/ul>
&lt;p>A useful diagnostic is &lt;strong>recomputed tokens per completed request&lt;/strong>. When that value rises, the server may still show high GPU utilization while wasting work. Another is the correlation between long-prompt admission bursts and subsequent preemption. The timeline CSVs in this package make both patterns inspectable.&lt;/p>
&lt;p>Rollout should begin with shadow decisions: compute what the guarded policy would admit without changing live scheduling. Compare predicted queueing, headroom, and rejected admissions against actual preemptions. Then enable the policy for a small traffic slice with rollback thresholds on P95 TTFT, throughput, and rejection rate.&lt;/p>
&lt;h2 id="the-boundary-of-this-experiment">The boundary of this experiment
&lt;/h2>&lt;p>This simulator proves only a mechanism-level claim: under block-limited cache capacity, first-chunk admission can create future memory obligations that trigger recomputation; reserving full prompt capacity prevents that planted failure in the modeled workload.&lt;/p>
&lt;p>It does not prove the best vLLM configuration, a universal watermark, or a GPU-level speedup. Real serving adds prefix caching, speculative decoding, heterogeneous attention types, tensor parallelism, asynchronous scheduling, CUDA graphs, host overhead, and model-specific cache geometry. Those factors can change the optimal policy.&lt;/p>
&lt;p>The durable lesson is narrower and more useful: continuous batching needs an admission layer that prices persistent state, not only immediate compute. Without that contract, the scheduler can stay busy while the system moves backward.&lt;/p>
&lt;h2 id="references">References
&lt;/h2>&lt;ul>
&lt;li>Woosuk Kwon et al., &lt;a class="link" href="https://arxiv.org/abs/2309.06180" target="_blank" rel="noopener"
>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/a>.&lt;/li>
&lt;li>vLLM documentation on &lt;a class="link" href="https://docs.vllm.ai/en/stable/api/vllm/config/scheduler/" target="_blank" rel="noopener"
>scheduler configuration&lt;/a>, including token and sequence limits, full-input reservation, and watermarking.&lt;/li>
&lt;li>vLLM documentation on &lt;a class="link" href="https://docs.vllm.ai/en/latest/configuration/optimization/" target="_blank" rel="noopener"
>optimization and tuning&lt;/a>, including KV-cache preemption and recomputation under cache pressure.&lt;/li>
&lt;/ul></description></item></channel></rss>