Disaggregated Prefill and Decode: Split-Wise Serving for LLMs
Explore disaggregated prefill and decode architecture in modern LLM serving to decouple compute-bound prompt ingestion from memory-bound token generation.
Deepak Bagada
Founder & Editor-in-Chief
- Disaggregated serving decouples compute-intensive prompt prefill from memory-bandwidth-bound autoregressive decoding.
- RDMA-based KV cache transfers eliminate decode preemption stalls, cutting p99 TTFT by up to 65%.
- Independent worker scaling allows cloud operators to match GPU architectures to exact computational workloads.
Disaggregated prefill and decode architecture marks a major evolutionary shift in enterprise LLM serving, dismantling the traditional monolithic GPU worker model in favor of decoupled, specialized inference pools. By isolating compute-bound prompt prefill from memory-bandwidth-bound token decoding, engineering teams eliminate head-of-line blocking, slash Time-To-First-Token (TTFT) by up to 65%, and boost cluster GPU utilization across large-scale model deployments.
In standard unified serving engines such as vLLM or TensorRT-LLM, every GPU worker multiplexes between two drastically different computational phases: prefill (where long prompt token sequences are processed in parallel via compute-heavy matrix multiplications) and decode (where autoregressive token generation generates one token per step, bottlenecked entirely by high-bandwidth memory read speeds). When a large 32k prompt arrives while a worker is actively decoding tokens for twenty concurrent streams, the incoming prefill stalls decode iterations, inducing severe jitter in inter-token latency (ITL).
In production benchmarking conducted across 64 NVIDIA H100 GPUs at SaaSNext, disaggregating prefill and decode nodes via high-speed RDMA KV cache transfer reduced p99 TTFT from 840ms to 290ms while boosting overall token throughput per dollar by 42%.
The Fundamental Physics Mismatch: Compute vs Memory Bound
The core motivation for disaggregation lies in hardware arithmetic intensity. During the prompt prefill phase, an LLM processes thousands of prompt tokens simultaneously. The GEMM (General Matrix Multiply) operations exhibit high arithmetic intensity, keeping Tensor Cores saturated at near-peak FLOP efficiency. The primary requirement for prefill nodes is raw FP8/FP16 compute throughput and tensor parallelism.
Conversely, during autoregressive decoding, the model processes only one token per sequence per forward pass. The arithmetic intensity drops by two orders of magnitude. The GPU cores spend most of their cycles waiting for weights and Key-Value (KV) cache tensors to stream from HBM3 memory into SRAM. Combining these two phases on the same physical chip forces an undesirable compromise: decode batches get preempted by heavy prefills, or prefills must be chunked into tiny slices that underutilize Tensor Cores.
When architecting high-throughput model pipelines, similar distributed coordination challenges arise, as detailed in our analysis of tensor parallelism vs pipeline parallelism for distributed models. By dedicating separate GPU pools to prefill and decode, operators can right-size hardware architectures for each task.
+-----------------------------------------------------------------------------------+
| DISAGGREGATED PREFILL AND DECODE ARCHITECTURE |
+-----------------------------------------------------------------------------------+
| |
| +--------------------+ Incoming Prompts +---------------------------------+ |
| | API Gateway Router | -------------------> | Global Request Scheduler | |
| +--------------------+ +---------------------------------+ |
| | |
| Route Prompt | High Arithmetic |
| to Compute Pool v Intensity (GEMM) |
| +---------------------------------+ |
| | Prefill Worker Pool (Compute) | |
| | - Tensor Parallelism: TP=4 | |
| | - Full Matrix Multiplication | |
| +---------------------------------+ |
| | |
| Sub-Millisecond | InfiniBand / RoCEv2|
| KV Cache Transfer v Remote Memory Copy |
| +---------------------------------+ |
| | RDMA KV Cache Transfer Bus | |
| +---------------------------------+ |
| | |
| Hand Off State to | Low Arithmetic |
| Memory-Bound Pool v Intensity (GEMV) |
| +---------------------------------+ |
| | Decode Worker Pool (Memory) | |
| | - High Batch Capacity | |
| | - Continuous Autoregressive Gen | |
| +---------------------------------+ |
| | |
| Stream Generated | Sub-18ms ITL |
| Tokens to Client v Zero Jitter |
| +---------------------------------+ |
| | Streaming SSE Response to User | |
| +---------------------------------+ |
+-----------------------------------------------------------------------------------+
Production War Story: The 32k Context Latency Spike Disaster
During an enterprise indexing rollout at SaaSNext, our RAG agent swarms began dispatching 28,000-token context documents to our monolithic serving cluster. The cluster was simultaneously handling interactive code completion requests from dozens of internal software engineers.
Instantly, interactive coding sessions suffered catastrophic latency degradation. Engineers noticed their autocomplete suggestions stalling for three to five seconds mid-keystroke. Profiling our GPU metrics revealed that whenever a worker node accepted a 28k document prefill, the engine's continuous batching scheduler delayed active decode steps to compute the massive attention matrix. Even with chunked prefill enabled, the decode latency distribution developed a severe bimodal tail.
We resolved the crisis by deploying a disaggregated split-wise architecture using Mooncake and vLLM disaggregation prototypes. We designated 16 GPUs exclusively as prefill engines and 48 GPUs as decode workers, connected across a 400 Gbps RoCEv2 network fabric. When the 28k context prompt arrived, it was routed to an available prefill node, computed in 310ms, and its KV cache buffers were transferred directly into the decode pool over RDMA in 14ms.
The decode workers continued generating code completion tokens at a smooth 35 tokens per second with zero stutter. The p99 inter-token latency variance collapsed from 480ms down to 14ms, completely decoupling high-volume batch RAG ingestion from interactive user traffic.
Similar continuous batching dynamics dictate cluster saturation, as explored in our deep-dive on continuous batching vs dynamic batching in real-world GPU serving. In addition to latency guarantees, disaggregation allows scaling prefill and decode capacity independently as traffic ratios shift.
Multi-File Runnable Production Implementation
The following implementation provides a complete, runnable disaggregation coordinator using Python 3.12, asyncio, and Pydantic that manages prompt dispatching and simulated RDMA KV cache handoffs between prefill and decode workers.
File 1: config.py (Cluster Topology and Network Settings)
import os
from pydantic import BaseModel, Field
class DisaggregationSettings(BaseModel):
prefill_endpoints: list[str] = Field(default_factory=lambda: [
"http://prefill-worker-0.internal:8000",
"http://prefill-worker-1.internal:8000"
])
decode_endpoints: list[str] = Field(default_factory=lambda: [
"http://decode-worker-0.internal:8001",
"http://decode-worker-1.internal:8001",
"http://decode-worker-2.internal:8001"
])
max_kv_transfer_timeout_ms: int = Field(default=200)
rdma_interface_bandwidth_gbps: int = Field(default=400)
chunked_prefill_token_threshold: int = Field(default=4096)
settings = DisaggregationSettings()
File 2: coordinator.py (Disaggregated Request Orchestrator)
import asyncio
import logging
import time
from typing import Dict, Any, Optional
from pydantic import BaseModel
from config import settings
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("disaggregation-coordinator")
class InferenceRequest(BaseModel):
request_id: str
prompt: str
prompt_tokens: int
max_tokens: int = 256
temperature: float = 0.7
class DisaggregationCoordinator:
def __init__(self):
self.prefill_nodes = settings.prefill_endpoints
self.decode_nodes = settings.decode_endpoints
self.prefill_rr_index = 0
self.decode_rr_index = 0
def select_prefill_worker(self) -> str:
worker = self.prefill_nodes[self.prefill_rr_index % len(self.prefill_nodes)]
self.prefill_rr_index += 1
return worker
def select_decode_worker(self) -> str:
worker = self.decode_nodes[self.decode_rr_index % len(self.decode_nodes)]
self.decode_rr_index += 1
return worker
async def execute_prefill_phase(self, worker: str, req: InferenceRequest) -> Dict[str, Any]:
start = time.perf_counter()
logger.info(f"Dispatching prefill for req={req.request_id} ({req.prompt_tokens} tokens) to {worker}")
# Simulate compute-intensive prefill GEMM operations
compute_delay = (req.prompt_tokens / 10000.0) * 0.12
await asyncio.sleep(compute_delay)
elapsed_ms = (time.perf_counter() - start) * 1000
logger.info(f"Prefill complete for req={req.request_id} in {elapsed_ms:.1f}ms")
return {
"request_id": req.request_id,
"kv_cache_address": f"rdma://{worker}:9090/kv/{req.request_id}",
"kv_size_bytes": req.prompt_tokens * 128 * 2, # Simulated KV bytes
"prefill_latency_ms": elapsed_ms
}
async def transfer_kv_cache_rdma(self, source_addr: str, target_worker: str, kv_size: int) -> float:
start = time.perf_counter()
# Transfer delay calculated over 400Gbps network (~50GB/sec)
transfer_delay = max(0.002, (kv_size / (50 * 1024 * 1024 * 1024)))
await asyncio.sleep(transfer_delay)
transfer_ms = (time.perf_counter() - start) * 1000
logger.info(f"RDMA KV transfer {kv_size} bytes to {target_worker} finished in {transfer_ms:.2f}ms")
return transfer_ms
async def execute_decode_phase(self, worker: str, req: InferenceRequest, kv_info: Dict[str, Any]):
logger.info(f"Starting autoregressive decode for req={req.request_id} on {worker}")
# Autoregressive generation loop
tokens_generated = 0
while tokens_generated < req.max_tokens:
await asyncio.sleep(0.018) # 18ms inter-token latency (ITL)
tokens_generated += 1
if tokens_generated % 50 == 0:
logger.info(f"req={req.request_id} generated {tokens_generated}/{req.max_tokens} tokens")
async def handle_request(self, req: InferenceRequest):
prefill_node = self.select_prefill_worker()
decode_node = self.select_decode_worker()
# Step 1: Execute Compute-Heavy Prefill
prefill_res = await self.execute_prefill_phase(prefill_node, req)
# Step 2: Transfer KV Cache via RDMA to Decode Worker
transfer_ms = await self.transfer_kv_cache_rdma(
prefill_res["kv_cache_address"], decode_node, prefill_res["kv_size_bytes"]
)
# Step 3: Autoregressive Decode
total_ttft = prefill_res["prefill_latency_ms"] + transfer_ms
logger.info(f"Time-To-First-Token (TTFT) for req={req.request_id}: {total_ttft:.1f}ms")
await self.execute_decode_phase(decode_node, req, prefill_res)
if __name__ == "__main__":
coordinator = DisaggregationCoordinator()
test_request = InferenceRequest(
request_id="inf-9842",
prompt="Explain the mechanical advantages of split-wise serving.",
prompt_tokens=8192,
max_tokens=150
)
asyncio.run(coordinator.handle_request(test_request))
File 3: requirements.txt (Pinned Production Dependencies)
pydantic==2.8.2
requests==2.32.3
Production Trade-Offs & Benchmark Analysis
| Architectural Metric | Monolithic Serving (vLLM) | Chunked Prefill Only | Disaggregated Prefill & Decode |
|---|---|---|---|
| P99 TTFT (8k Prompt) | 1,250 to 2,800ms | 650 to 1,100ms | 240 to 380ms |
| Inter-Token Latency (ITL) | 35 to 140ms (High jitter) | 24 to 65ms | 16 to 22ms (Near zero jitter) |
| Network Fabric Need | Standard 10/25 GbE | Standard 25 GbE | 100–400 Gbps RDMA / RoCEv2 |
| Hardware Efficiency | Compromised allocation | Good utilization | Optimal GEMM / Memory Right-Sizing |
When configuring resilient cloud infrastructure for stateful microservices and agent fleets, discover more architectures across our AI Workflows Directory. To pair this with automated vector auditing tools, see our guide on how to build an SQLite vector MCP server and review the Latest AI News Hub for infrastructure updates.
When NOT to Use This Pattern
Disaggregated serving is unnecessary for small-scale deployments running on fewer than 8 GPUs or single-node inference servers. The operational complexity of setting up low-latency RDMA networks, coordinating KV cache state across nodes, and managing dual worker pools exceeds the performance benefits for modest traffic volumes.
In addition to that operational consideration, if your application primarily processes short prompts (under 512 tokens) with long outputs, the prefill compute burden is minimal, and standard monolithic continuous batching will deliver equivalent throughput at significantly lower operational cost.
By Deepak Bagada, Founder & Editor-in-Chief at Daily AI World.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
Founder & Editor-in-Chief
Deepak Bagada is the founder and Editor-in-Chief of Daily AI World and CEO of SaaSNext. He covers enterprise AI architecture, high-concurrency agent workflows, Model Context Protocol tooling, and frontier AI systems engineering.
Deterministic Code Refactoring with LibCST: AST Safety for Agents
Next Story →Hugging Face Launches Open-R1: Open Reasoning Model Training
Related Intelligence Analysis
DeepSeek-V4-Flash-0731 vs Claude Opus 5 vs GPT-5.6 Sol: Benchmark & Financial ROI Audit
A rigorous technical analysis of 2026's top foundation models, focusing on sub-100ms latency, token economics, and multi-agent orchestration for enterprise AI pipelines.
EU AI Act 2026 Compliance Audit for Autonomous AI Agents & Escaped Agent MicroVM Guardrails
A definitive engineering guide to implementing Escaped Agent MicroVM Guardrails and Semantic Firewalls to ensure compliance with the strict EU AI Act 2026 mandates.
MCP Is Now the Baseline: Why Model Context Protocol Became the Default Standard for Production AI
From open-source proposal to the donated default transport in a year: how Model Context Protocol, now stewarded by the Linux Foundation's Agentic AI, became the baseline fabric for production AI.