LLMModule 09 · Problems 6, 7, 21

Batching and scheduling: continuous batching, chunked prefill, fair multi-tenancy

A GPU serving a language model is only cheap when it works on many requests at once. The scheduler decides which requests share each step. Get it wrong and you either waste the GPU or make users wait. This page works through three interview problems about that scheduler, with real numbers and the trade-offs.

Problems covered6 (continuous batching under deadlines), 7 (chunked prefill for a 100K-token prompt), 21 (multi-tenant fairness, admission control, priorities)
Interview levelMid (6, 7) to senior (21)
Key ideasIteration-level scheduling, step-time model, token budgets, prefill/decode interference, weighted fair sharing, load shedding
Time to prep3–4 h: 1 h reading, 1 h with the demo, 1–2 h for the vLLM exercise

Source: Vishal Singh's 25 inference-engineering interview problems · saved in sources/09-vishal-inference-25.md

Start here (no maths needed)

A model server is like a kitchen with one very fast cook who can stir many pots at once. The trick is deciding which orders go on the stove, when, and making sure one giant order or one greedy customer doesn't make everyone else wait.

Think of it like this

Think of a ski lift. A bad way to run it is the tour-bus way: wait until 8 people are ready, send them all up together, and don't let anyone else on until the last of the 8 comes back down. Most chairs ride up empty while one slow skier finishes. The ski-lift way is better: the moment a chair is free, the next person in the queue hops on. That's continuous batching. Now imagine a school group of 100 arrives at once. If they all board before anyone else, the people behind wait ages. So you let the group board a few at a time, mixed in with everyone else. That's chunked prefill. And if three schools share the lift, you take turns between schools so one big school can't hog it. That's fair scheduling.

How it works

  1. The model writes one word (token) per step for every request in the batch.
  2. Old way: a fixed group starts together and the group ends when its longest answer ends.
  3. New way: after every step, finished requests leave and waiting ones join straight away.
  4. Reading a long question is done in slices, so short questions can slip in between slices.
  5. A gatekeeper decides who gets in, in what order, and turns some away politely when it's too busy.

Why robots care

A robot asking a cloud model "what should I do next?" needs the answer within a fixed time, say 300 milliseconds, or the robot has already moved on. If the server is busy reading someone's 300-page document, the robot's tiny question can wait seconds. Good scheduling is what keeps that small, urgent question fast.

Try it

Words to know

Batch
The group of requests the GPU works on in one step.
Prefill
Reading the whole prompt at once, before the first word comes out.
Decode
Writing the answer, one token per step.
TTFT
Time to first token: how long until the first word appears.
Tenant
One customer or team sharing the same servers with others.

Going deeper: everything below is the full version, with some maths. On a first read, skim the headings, the interview cards and the "Why it matters for robotics" box, and come back for the rest later.

0. One cost model for the whole page

All three problems use the same picture of one engine step. A step runs one forward pass over every token scheduled in it: prompt tokens being prefilled, plus one token per decoding request.

t_step ≈ t_0 + c_p × (prefill tokens in step) + c_d × (decoding requests in step)

t_0  : fixed cost per step, mostly reading all weights from HBM once
c_p  : cost per prompt token (compute-bound matrix multiplies)
c_d  : cost per decoding request (its KV-cache reads; grows with its context)

TPOT (time per output token) for every decoding request = t_step
throughput (output tokens/s)                            = decoding requests / t_step

Working assumptions used throughout, for a Llama-style 70B model served on one 8-GPU tensor-parallel node. These are round numbers for practice, not measurements: t_0 = 20 ms, c_p = 0.1 ms per prompt token (10,000 prompt tokens/s), c_d = 0.2 ms per decoding request at a few thousand tokens of context. In an interview, say your numbers are assumptions and show how the answer changes if they move. The demo uses exactly these values.

Problem 6 · Continuous batching under latency deadlines, and why it beats static batching

What the interviewer is testing: that you know decode is memory-bound so batching is nearly free, that requests finish at very different times, and that you can turn latency targets into a concrete admission rule.

The concepts

Static (request-level) batching groups B requests, runs them together until the longest finishes, then takes the next group. Two kinds of waste follow. Finished requests keep their slot (often still computing padding) until the longest one ends. And new arrivals wait for the whole group, however long it takes.

static slot utilisation = Σ out_i / (B × max_i out_i)

Continuous (iteration-level) batching re-forms the batch after every step. Orca (Yu et al., OSDI 2022) introduced this as iteration-level scheduling, together with selective batching: the big matrix multiplies run over all tokens of all requests flattened together, while attention runs per request, so requests of different lengths can share a step without padding. vLLM, SGLang and TensorRT-LLM (where it's called in-flight batching) all work this way. PagedAttention (Page 02) makes it practical, because KV memory is allocated in blocks as requests grow and freed the moment they finish.

Why batching is nearly free during decode. At small batch each step is dominated by t_0, the weight read. Adding a request adds only c_d. So throughput rises almost linearly with batch size while TPOT rises slowly, until either the KV cache is full or the step becomes compute-bound.

TPOT(B)       = t_0 + c_d × B
throughput(B) = B / (t_0 + c_d × B)    → approaches 1/c_d as B grows

Worked answer with numbers

Static vs continuous on one batch. Batch of 8: seven requests want 50 output tokens, one wants 1,000.

static utilisation = (7 × 50 + 1000) / (8 × 1000) = 1350 / 8000 = 16.9 %
static step        = 20 + 8 × 0.2 = 21.6 ms
static batch time  = 1000 steps × 21.6 ms = 21.6 s
  → the seven short requests finish after ~1.1 s, but their slots sit idle
    for 20.5 s and nothing new can start for 21.6 s.
continuous         : each freed slot is refilled on the next step,
                     so utilisation stays near 100 % while the queue is non-empty.

The demo shows the system-level effect with Poisson arrivals (2 requests/s, 32 slots, output lengths log-normal with median 150 tokens, capped at 1,000): static batching sustains about 117–182 output tokens/s and the queue keeps growing; continuous batching sustains about 380 tokens/s (it keeps up with all the traffic) with a mean TTFT of about 0.2 s.

Adding latency deadlines. Say the SLO is TTFT ≤ 1 s and TPOT ≤ 50 ms (20 tokens/s, faster than people read). Two limits cap the batch:

1. TPOT limit:  t_0 + c_d × B ≤ 50 ms  →  B ≤ (50 − 20) / 0.2 = 150 requests

2. KV limit (assume 200 GB of HBM left for KV after weights and activations;
   Llama-70B shape: 80 layers, 8 KV heads, head dim 128, fp16):
   KV per token = 2 × 80 × 8 × 128 × 2 B = 327,680 B (320 KiB)
   tokens that fit = 200e9 / 327,680 ≈ 610,000
   at 4,096 tokens average context  →  610,000 / 4,096 ≈ 149 requests

throughput at B = 150 ≈ 150 / 0.050 = 3,000 output tokens/s
throughput at B = 32  ≈ 32 / 0.0264 ≈ 1,212 output tokens/s

Here both limits land near 150, which is a useful thing to notice out loud: buying more KV memory (FP8 cache, fewer KV heads) only helps if TPOT also has room. The scheduler then does this every step:

  1. Keep running requests running (one decode token each). They already paid for their KV.
  2. Admit waiting requests in order of deadline (earliest TTFT deadline first) while the predicted step time stays ≤ the TPOT target and KV blocks remain.
  3. Bound prefill per step with a token budget so admissions don't blow the TPOT of everyone else (this is Problem 7).
  4. If KV runs out mid-flight, preempt the lowest-priority or newest request: free its blocks and later recompute its prefill, or swap blocks to CPU memory. vLLM does recompute-based preemption by default.
  5. If a request will miss its deadline anyway (waited longer than its TTFT SLO), consider rejecting it early rather than serving it late and slowing others.

Trade-offs

ChoiceGainCost
Larger max batchMore throughput, lower cost per tokenHigher TPOT for everyone, more KV pressure, more preemption
Continuous vs staticNo padding waste, new requests start next stepMore complex scheduler, needs paged KV memory and per-request attention kernels
Deadline-ordered admissionFewer SLO missesLong-waiting low-priority requests can starve; needs aging
Recompute vs swap on preemptionRecompute: no CPU memory or PCIe trafficRecompute redoes prefill; swap is better for very long contexts

How to say it in the interview

Decode is memory-bound: each step reads all the weights once, so adding requests to the batch is almost free until the KV cache fills or the step gets slow. Static batching wastes that, because a group runs until its longest request ends; with seven 50-token answers and one 1,000-token answer, slot utilisation is under 17%. Continuous batching re-forms the batch every iteration, so finished requests leave and new ones join on the next step. To respect deadlines I'd model step time as a fixed weight-read cost plus a per-request cost, cap the batch so predicted TPOT stays under target and KV fits, admit by earliest deadline, and cap prefill tokens per step. With my assumed numbers both limits come out around 150 concurrent requests and about 3,000 tokens/s per node.

Common mistakes

Interactive: static vs continuous batching, with and without chunked prefill

Each row is one GPU batch slot over the first 30 s of a 60 s simulation. Teal: prefill. Blue: decode. Amber: the 100K-token prompt. Gaps are idle slots. Arrivals are Poisson with a fixed seed; output lengths are log-normal (median 150, capped at 1,000); prompts are 200–2,000 tokens. Step time uses the cost model in section 0.

Output tokens/s
–
Mean TTFT
–
p95 TTFT
–
Worst decode gap
–
Still waiting at 60 s
–
Long prompt TTFT
–

Try this: at the defaults (2 requests/s, 32 slots), Static gives about 182 tokens/s, a mean TTFT near 25 s and 62 requests still queued; Continuous gives about 378 tokens/s and a mean TTFT of 0.19 s. Now turn the 100K-token prompt on with Continuous: mean TTFT rises to about 1.6 s, p95 to about 9.7 s, and the worst decode gap is about 10 s, because every running chat froze during one giant prefill step. Switch to Continuous + chunked at 512: mean TTFT about 0.25 s, p95 about 0.5 s, worst gap about 74 ms. The price: the long prompt's own TTFT grows from 10 s to about 23 s. At a 2,048 budget it's about 15 s, with a worst gap of about 228 ms. TTFTs are for the short requests; the model ignores attention's quadratic cost, which makes the real long prefill slower.

Problem 7 · A 100K-token prompt stalls short chats: fix TTFT with chunked prefill and scheduling

What the interviewer is testing: that you can see prefill/decode interference, size a chunk from a latency target, and know the other levers (ordering, isolation, caching) when chunking alone isn't enough.

The concepts

Why one prompt stalls everyone. Without chunking, an engine prefills a whole prompt in one step. Every other request in the batch waits for that step to finish before getting its next token, and requests in the queue wait for their own first token. Prefill cost is roughly linear in prompt length for the matrix multiplies, plus a quadratic term for attention:

prefill FLOPs ≈ 2 × N × T  +  2 × L × d_model × T²      (causal attention)
N = parameters, T = prompt tokens, L = layers

Chunked prefill splits a long prompt into pieces and processes one piece per step, mixed with the decode tokens of other requests. Sarathi (Agrawal et al., 2023) proposed chunked prefills with "decode-maximal batching": fill each step with all ongoing decodes, then top it up with a prefill chunk. Sarathi-Serve (Agrawal et al., OSDI 2024) adds stall-free scheduling under a per-step token budget chosen from the TPOT target. vLLM exposes this as --enable-chunked-prefill with the budget set by --max-num-batched-tokens; recent versions turn chunked prefill on by default, so check the docs for your version.

choose budget C so that   t_0 + c_p × C + c_d × B_decode ≤ TPOT target
→ C ≤ (TPOT target − t_0 − c_d × B_decode) / c_p

Worked answer with numbers

Assumptions: the 70B model from section 0; a 100,000-token prompt; 32 chats decoding.

Size of the job
  linear part   : 2 × 70e9 × 1e5            = 1.4e16 FLOPs
  attention part: 2 × 80 × 8192 × (1e5)²    = 1.31e16 FLOPs
  → at 100K tokens attention roughly doubles the work.
  At the assumed 10,000 prompt tokens/s (linear part only): 10 s;
  with the attention term, closer to 19 s.

No chunking
  one step ≈ 10 s  → every running chat's next token is 10 s late,
  every queued chat's TTFT includes up to 10 s of waiting.

Chunking (step = 20 ms + C × 0.1 ms + 32 × 0.2 ms, linear model)
  C      step     chunks   long prompt prefill   decode gap for others
  512    77.6 ms  196      ≈ 15.2 s              ≈ 78 ms
  2,048  231 ms   49       ≈ 11.3 s              ≈ 231 ms
  8,192  846 ms   13       ≈ 11.0 s              ≈ 846 ms

Budget from a 100 ms TPOT target:
  C ≤ (100 − 20 − 6.4) / 0.1 = 736 tokens  → pick 512

The pattern to say out loud: small chunks protect everyone else's latency, but every chunk pays the fixed t_0 again, so the long prompt itself finishes later. Later chunks also cost more than early ones, because each chunk attends to all the prompt tokens before it.

Chunking is half the fix. Scheduling is the other half.

Trade-offs

LeverHelpsHurts
Small chunk budgetTPOT and TTFT of short requestsLong prompt's TTFT; more per-step overhead; lower peak prefill efficiency
Large chunk budgetPrefill efficiency, long prompt TTFTDecode stalls return
Shortest-prefill-firstMedian and p95 TTFTFairness to long prompts unless aged
Separate long-context poolIsolation, predictable SLOsFragmented capacity, lower utilisation, routing logic

How to say it in the interview

The stall happens because the engine prefills the 100K prompt in one step, roughly 10–20 seconds on a 70B model, and every other request waits for that step. I'd turn on chunked prefill with a per-step token budget: each step carries all running decodes plus at most C prompt tokens. I pick C from the TPOT target; with a 20 ms weight-read floor, 0.1 ms per prompt token and 32 decoders, a 100 ms target gives about 700 tokens, so 512. Inside the budget I'd serve short prompts first with aging, limit long prefills to one at a time, and if long prompts are common, route them to a separate pool or disaggregate prefill. The cost is that the long prompt itself finishes later, which I'd state as an explicit SLO for long-context requests.

Common mistakes

Problem 21 · Multi-tenant serving with fair scheduling, admission control and priority queues

What the interviewer is testing: that you measure fairness in tokens, not requests; that you reject early instead of serving late; and that priorities come with starvation protection and preemption.

The concepts

The cost unit is tokens. One request can be 50 tokens or 100,000. Counting requests lets a tenant with long prompts take most of the GPU while looking "fair". Prompt and output tokens also cost different amounts (prefill is cheap per token, decode is expensive per token), so weight them.

cost(request) = w_in × prompt tokens + w_out × output tokens     (w_out > w_in)

Weighted fair queuing. Each tenant i has a weight w_i (its share). Classic weighted fair queuing gives each request a virtual finish tag and serves the smallest tag first:

F_new = max(V_now, F_prev_of_tenant) + cost / w_i

LLM serving has a twist: the output length is unknown when a request starts. The Virtual Token Counter (VTC; Sheng et al., OSDI 2024) handles this by keeping a counter per tenant, adding weighted tokens as they're actually processed, and always scheduling from the backlogged tenant with the smallest counter. When a tenant becomes active again, its counter is raised to the smallest counter among active tenants, so idle time can't be banked into a later burst.

Weighted max-min fairness describes the target allocation: nobody gets more than they ask for, and spare capacity is shared by weight among those who want more.

Admission control sits before the queue: per-tenant token buckets (a rate and a burst size in tokens per minute) and a global check of predicted wait. Priority classes (interactive, standard, batch) sit on top of fair sharing: strict priority between classes with aging, fair sharing between tenants inside a class.

Worked answer with numbers

Assume the pool serves 20,000 weighted tokens/s. Three tenants: A (free tier, weight 1, wants 15,000 tokens/s), B (paid, weight 3, wants 4,000), C (internal, weight 1, wants 10,000). Demand is 29,000, so someone gets less.

Round 1: shares by weight (1 : 3 : 1) of 20,000
  A 4,000   B 12,000   C 4,000
  B only wants 4,000 → give B 4,000, return 8,000 to the pool.

Round 2: split remaining 16,000 between A and C (1 : 1)
  A 8,000   C 8,000      both still want more → stop.

Final: A 8,000 (53% of its demand), B 4,000 (100%), C 8,000 (80%).

Admission check with Little's law. If the queue already holds requests expected to need 60,000 weighted tokens, the expected wait for a new arrival is about 60,000 / 20,000 = 3 s. With a 2 s TTFT SLO for interactive traffic, admitting more interactive work just creates late answers. Shed load instead: reject batch-class work with HTTP 429 and a Retry-After header, or cap max_tokens for the free tier, until predicted wait drops below the SLO.

expected wait ≈ queued work / service rate      (Little's law: L = λ × W)
admit if   expected wait + own prefill time ≤ TTFT SLO   (per class)

The design, front to back:

  1. Gateway: authenticate, find tenant and class, apply the tenant's token bucket. Over limit → 429 immediately.
  2. Admission: estimate wait per class from queued work and recent throughput. Reject or degrade when it exceeds the SLO; reject lowest class first.
  3. Queues: one queue per (class, tenant). Pick the class by strict priority with aging (a batch request waiting over, say, 10 minutes is promoted). Inside the class, pick the tenant by VTC.
  4. Engine: the continuous batcher from Problem 6 pulls from this picker. When KV runs out, preempt the lowest class first.
  5. Isolation for big customers: dedicated replicas or reserved capacity for tenants with contracts; shared pool for the rest. Per-tenant LoRA adapters can share one base model.
  6. Observe: per-tenant tokens served, rejected, TTFT and TPOT percentiles, preemptions. Fairness bugs show up only in per-tenant views.

Trade-offs

ChoiceGainCost
Fair by tokens (VTC) vs by requestsLong-prompt tenants can't hog the GPUNeeds token accounting inside the engine, not just at the gateway
Strict priority between classesInteractive SLOs protectedBatch starves without aging or a minimum share
Reject early (429)Accepted requests meet SLO; clients can retry elsewhereVisible errors; needs good client retry with backoff
Preempt low class on KV pressureHigh class keeps latencyRecomputation wastes GPU time; batch jobs take longer
Dedicated vs shared capacityPredictability for big tenantsIdle reserved GPUs; lower utilisation

How to say it in the interview

I'd measure fairness in weighted tokens, not requests, because one 100K-token prompt costs as much as hundreds of chats. At the gateway each tenant gets a token bucket, and an admission check estimates queue wait from queued work divided by throughput; if that exceeds the class's TTFT SLO I return 429 with Retry-After, shedding batch traffic first. Inside, I keep strict priority between interactive and batch classes, with aging so batch can't starve, and fair sharing between tenants within a class using a virtual token counter, which handles unknown output lengths. Under KV pressure the engine preempts the lowest class first. For example, with 20,000 tokens/s and weights 1:3:1, a paid tenant asking for 4,000 gets all of it, and the two others split the rest 8,000 each.

Common mistakes

Practise it

  1. Run the simulator yourself. Copy this page's script into a Python or Node file, add a second "tenant" field to requests, and implement VTC in the admission step. Measure per-tenant tokens/s when one tenant sends 10× the traffic.
  2. vLLM on a free Colab GPU. Serve a small instruct model (0.5–1.5B parameters) with vLLM's OpenAI-compatible server. Write an async Python client that sends 50 short chats (200-token prompts) at a steady rate and, at t = 10 s, one prompt as long as the model's context allows.
  3. Measure TTFT and inter-token latency of the short chats with streaming. Run three settings: chunked prefill off (if your version allows), on with --max-num-batched-tokens 512, and on with 4096. Plot short-chat p50/p95 TTFT and the worst inter-token gap, plus the long prompt's TTFT.
  4. Vary --max-num-seqs (the batch cap) from 8 to 256 at a high request rate and plot throughput against median TPOT. Find the knee.
  5. Write down your assumed t_0, c_p and c_d by fitting a line to step times from the server logs or your measurements, and check whether the section 0 model predicts your results.

In this course

Why it matters for robotics

Self-check

Seven requests want 50 tokens and one wants 1,000. What fraction of slot-steps does a static batch of 8 use?

(7 × 50 + 1000) / (8 × 1000) = 1350 / 8000 ≈ 16.9%.

Why does throughput rise almost linearly with batch size during decode, at first?

Each step reads all the weights once regardless of batch size, and that fixed cost dominates. Each extra request adds only its own KV reads and a little compute, so you get more tokens per step for almost the same step time, until KV memory fills or the step becomes compute-bound.

With t_0 = 20 ms, c_p = 0.1 ms per prompt token and 32 decoders at 0.2 ms each, what chunk budget meets a 100 ms TPOT target?

C ≤ (100 − 20 − 6.4) / 0.1 = 736 tokens. Round down to a convenient size such as 512.

Does chunked prefill make the 100K-token prompt faster?

No. It makes it slower, because each chunk pays the fixed step cost again and shares the step with decodes. It makes every other request faster.

Why measure tenant fairness in tokens rather than requests?

Requests vary from tens to hundreds of thousands of tokens. Request counts let a tenant with long prompts or long outputs take most of the GPU while looking equal.

Capacity 20,000 tokens/s, queued work 60,000 tokens, TTFT SLO 2 s. Admit a new interactive request?

Expected wait ≈ 60,000 / 20,000 = 3 s, above the 2 s SLO. Shed lower-priority work or reject with 429 and Retry-After until the predicted wait falls below the SLO.

Resources