Disaggregated LLM Inference That Scales
Compare unified, chunked-prefill, and split serving before paying the cache-transfer cost of separate prompt and generation pools.
Disaggregated LLM inference can protect both first-token speed and decode smoothness by placing prefill and generation on different workers. This guide shows when that separation beats a unified server and when transferring the cache costs more than it saves.
The intended reader operates a GPU-backed model service and can replay representative prompts. You will leave with a three-mode benchmark, a placement rule, and a rollback receipt that compare user-visible latency instead of raw accelerator utilization.
The operating vocabulary separates prefill decode separation, KV transfer, inference serving, and time to first token; each names one part of the same capacity decision.
- Request
- Prefill pool
- Cache link
- Decode pool
Disaggregated LLM inference starts with two clocks
disaggregated LLM inference begins with measuring request arrival-to-first-token separately from the spacing between later tokens. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision. The vLLM disaggregated-prefill documentation documents the supported separation and explicitly frames it as a way to control interference rather than improve total token throughput.
Work through four explicit moves:
- Record queue, prompt, handoff, and generation time
- Group results by prompt and output length
- Report medians beside p95 and p99
- Reject wins that move pain between clocks
In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.
The named failure mode is averaging the entire response into one latency number. Its consequence is a faster prompt path can hide visibly uneven generation.
Mitigate it with two explicit service objectives and per-cohort histograms. The release receipt is a paired latency report with request counts and percentile definitions. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.
Choose separation for interference, not fashion
A useful disaggregated LLM inference decision depends on proving that prompt-heavy work is blocking token generation on the same devices. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision. The DistServe research paper provides the original resource-separation argument and motivates evaluating prompt and generation objectives independently.
Work through four explicit moves:
- Replay the production length distribution
- Measure queue overlap on one worker pool
- Reserve an equivalent decode cohort
- Compare the same arrival trace after separation
In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.
The named failure mode is splitting a service that has no material phase interference. Its consequence is network hops and idle fragments add cost without improving experience.
Mitigate it with a unified control and a chunked-prompt alternative. The release receipt is one table showing all three modes under identical traffic. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.
Benchmark unified, chunked, and split modes
The worked disaggregated LLM inference fixture makes keeping models, tokenizers, prompts, outputs, concurrency, and warm-up conditions identical. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.
Work through four explicit moves:
- Freeze model revision and numerical settings
- Replay one versioned request corpus
- Sweep concurrency without changing arrivals
- Store raw samples for independent calculation
In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.
The named failure mode is giving the new topology a friendlier workload. Its consequence is the benchmark becomes a capacity demo rather than a causal comparison.
Mitigate it with one seeded trace and one harness for every mode. The release receipt is hashes for the corpus, configuration, and raw result file. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.
| Signal | Decision | Proof |
|---|---|---|
| Long prompts | Separate pools | First-token tail improves |
| Tiny prompts | Stay unified | Handoff cost exceeds gain |
| Decode pressure | Reserve generation | Token cadence stays stable |
Make the handoff a versioned contract
disaggregated LLM inference needs an explicit rule for binding every transferred block to request, model, tokenizer, adapter, layer range, and destination ownership. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.
Work through four explicit moves:
- Create an immutable transfer identifier
- Declare expected block count and byte length
- Acknowledge only after complete validation
- Abort to a safe route on mismatch
In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.
The named failure mode is continuing from incomplete or incompatible prompt state. Its consequence is the decoder can produce corrupt output or read another request's memory.
Mitigate it with length checks, identity fields, checksums, and destination-scoped leases. The release receipt is a transfer log that proves validation before continuation. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.
Runnable artifact. Save this as disaggregated-llm-inference.test.mjs and run node --test disaggregated-llm-inference.test.mjs. Expected result: PASS: choose split only after net TTFT gain. The checked-in copy lives with this batch's evidence.
import assert from "node:assert/strict";
import test from "node:test";
const modes = [
{ name: "unified", ttft: 820, itl: 32, transfer: 0 },
{ name: "chunked", ttft: 690, itl: 31, transfer: 0 },
{ name: "split", ttft: 510, itl: 29, transfer: 85 },
];
function choose(rows, minimumNetGain = 150) {
const unified = rows.find((row) => row.name === "unified");
const split = rows.find((row) => row.name === "split");
const netGain = unified.ttft - (split.ttft + split.transfer);
return netGain >= minimumNetGain && split.itl <= unified.itl ? "split" : "unified";
}
test("chooses the topology from user-visible clocks", () => {
assert.equal(choose(modes), "split");
assert.equal(choose(modes, 250), "unified");
console.log("PASS: choose split only after net TTFT gain");
});
Model bandwidth as serving capacity
In production, disaggregated LLM inference turns on treating the cache link as a finite shared resource rather than a free internal detail. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.
Work through four explicit moves:
- Measure state bytes per prompt token
- Cap concurrent handoffs per link
- Prioritize requests by deadline and size
- Expose saturation before queuing explodes
In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.
The named failure mode is sizing only the GPU pools. Its consequence is a congested fabric becomes the new serialized prompt queue.
Mitigate it with link budgets, bounded queues, and admission control. The release receipt is handoff throughput beside queue age and dropped work. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.
Route by workload, not a global toggle
Safe disaggregated LLM inference requires letting short prompts, long prompts, and latency-sensitive streams take different paths under one declared policy. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.
Work through four explicit moves:
- Define cohorts before inspecting results
- Assign a default and an exception route
- Log the chosen rule and reason
- Measure fallback traffic separately
In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.
The named failure mode is forcing every request through one fashionable architecture. Its consequence is small requests pay unnecessary transport while heavy ones still contend.
Mitigate it with a deterministic cohort router with conservative defaults. The release receipt is a routing confusion matrix and outcome by cohort. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.
- RouteRoute
Classify prompt size and service objective.
- PrefillPrefill
Produce the prompt state on the assigned pool.
- TransferTransfer
Move only the versioned blocks required to continue.
- DecodeDecode
Generate while reporting cadence and queue delay.
Roll out with a unified escape route
A disaggregated LLM inference rollout should preserve preserving a tested single-pool path when workers, links, or transfer metadata become unhealthy. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.
Work through four explicit moves:
- Probe every phase independently
- Drain rather than strand owned work
- Route new traffic to the control
- Rehearse recovery during representative load
In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.
The named failure mode is treating the split pool as mandatory hidden capacity. Its consequence is one degraded link can stop otherwise healthy generation.
Mitigate it with load-tested failover and bounded retry ownership. The release receipt is a fault-injection trace with no duplicate or orphaned request. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.
Decide with service-level evidence
The evidence for disaggregated LLM inference is strongest when requiring a meaningful latency gain for target cohorts without higher error rate, cost, or unfairness. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.
Work through four explicit moves:
- Set the improvement threshold in advance
- Compare equal successful-request populations
- Inspect slow and rejected cohorts
- Record the boundary where unified still wins
In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.
The named failure mode is shipping because one percentile moved in one run. Its consequence is noise or selective reporting becomes architecture.
Mitigate it with repeated trials, confidence intervals, and declared exclusions. The release receipt is a signed decision note linking samples to the rollout rule. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.
Put the decision into practice
Disaggregated LLM inference is valuable when prompt work measurably interferes with generation and a separate pool restores both clocks by more than the state-transfer cost. It is not a universal throughput trick; the method earns its complexity one workload cohort at a time.
Begin with the single seeded trace in the runnable artifact, then replace its toy samples with raw observations from unified, chunked, and split modes. Ship only the routing boundary that survives equal traffic, repeated trials, and a failed-handoff rehearsal.
The method connects to four existing Journal notes: KV cache optimization, LLM admission control, backpressure and flow control, SLOs that follow user journeys. Each link covers an adjacent boundary while this article stays focused on one outcome. Keep the fixture, visual evidence, command output, and release receipt together so the next review can test the claim against the same starting conditions.