Skip to main content
UltraInstinct
Back to latest articles
Artificial Intelligence11 min read

Disaggregated LLM Serving: Architecting Asynchronous Prefill-Decode Engines with Zero-Copy RDMA KV-Cache Migration

A deep architectural guide to decoupling prefill and decode compute in distributed LLM serving using GPUDirect RDMA, PagedAttention block migration, and async pipelining.

Featured visual representing Disaggregated LLM Serving: Architecting Asynchronous Prefill-Decode Engines with Zero-Copy RDMA KV-Cache Migration
Advertisement
Google AdSense In-Article Contextual Slot

Introduction

Modern autoregressive Large Language Model (LLM) inference suffers from an inherent hardware-execution mismatch. The inference lifecycle is strictly bipartite, split across two distinct computational regimes:

  1. The Prefill (Context) Phase: Compute-bound and highly parallelized. It processes the prompt tokens concurrently, saturating GPU Tensor Cores with high arithmetic intensity ($\text{FLOPs}/\text{Byte} \gg \text{Hardware Ridge Point}$).
  2. The Decode (Generation) Phase: Memory-bandwidth bound and sequential. It generates one token per sequence step, constrained by the memory subsystem as the entire model weight matrix and the Key-Value (KV) cache must be loaded from High Bandwidth Memory (HBM) into SRAM for every individual token ($\text{FLOPs}/\text{Byte} \approx 1\text{--}2$).

Historically, serving engines such as early vLLM, TGI, and TensorRT-LLM colocated prefill and decode operations on the same physical accelerators. Under continuous batching, incoming prefill requests preempt or run alongside decoding sequences. This causes head-of-line blocking, severe Inter-Token Latency (ITL / P99 jitter) degradation, and mutual execution interference where neither Tensor Cores nor HBM bandwidth operate at peak efficiency.

Unified Serving Bottleneck (Interference):
GPU HBM/Tensor Core:
[Prefill (Compute-Bound)] ---> Stalls Decode Iterations (High ITL)
[Decode (Memory-Bound)]  ---> Underutilizes Tensor Cores (Low MFU)

The disaggregated serving architecture physically decouples prefill computation from decode execution onto heterogeneous GPU pools. Prefill nodes optimize exclusively for high Tensor Parallelism (TP) and dense matrix operations, while Decode nodes maximize memory capacity, batch size, and pipeline efficiency.

The primary architectural challenge of disaggregation shifts from local scheduling to the network fabric: how to migrate multi-gigabyte KV-cache state across distributed GPU memory spaces with sub-millisecond latency and zero CPU-bounce overhead.


Decoupled Prefill and Decode Topologies

In a disaggregated cluster, the prefill instance ($P$-Worker) and decode instance ($D$-Worker) operate as independent systems communicating over a non-blocking InfiniBand or RoCEv2 network fabric.

                                  DISAGGREGATED TOPOLOGY
 +---------------------------------------------------------------------------------------+
 |                                  INGRESS ROUTER / SCHEDULER                           |
 +---------------------------------------------------------------------------------------+
                | Prompt Tokens                                    | Token Generation Requests
                v                                                  v
 +-----------------------------+                  +--------------------------------------+
 |   PREFILL POOL (P-Workers)  |                  |       DECODE POOL (D-Workers)        |
 |  - High Tensor Parallel (TP8)                  |  - Low Tensor Parallel (TP1 / TP2)   |
 |  - High Clock, Dense FLOPs  |                  |  - High HBM Capacity, Max Batch Size |
 |  - Computes Initial KV      |                  |  - Sequential Token Generation       |
 +-----------------------------+                  +--------------------------------------+
                |                                                  ^
                |       GPUDirect RDMA (RoCEv2 / IB 400Gbps+)      |
                +==================================================+
                            Remote KV-Cache Block Migration

Mathematical Formulation of Latency Gains

Let the total request processing time in a unified system be $T_{\text{unified}}$:

$$T_{\text{unified}} = T_{\text{prefill}} + \sum_{i=1}^{M} (T_{\text{decode}, i} + \Delta_{\text{bubble}, i})$$

Where $\Delta_{\text{bubble}, i}$ is the execution stall induced by prefill preemption during decode step $i$, and $M$ is the number of generated tokens.

In a disaggregated architecture, this interference is completely eliminated. The end-to-end latency $T_{\text{disagg}}$ becomes:

$$T_{\text{disagg}} = T_{\text{prefill}} + T_{\text{xfer}} + \sum_{i=1}^{M} T_{\text{decode}, i}$$

Where $T_{\text{xfer}}$ is the network transfer time of the generated KV-cache:

$$T_{\text{xfer}} = \frac{2 \cdot L \cdot H_{\text{kv}} \cdot d_{\text{head}} \cdot N_{\text{ctx}} \cdot S_{\text{dtype}}}{B_{\text{RDMA}}} + \tau_{\text{prop}}$$

Here:

  • $L$ is the number of transformer layers.
  • $H_{\text{kv}}$ is the number of KV heads (e.g., under Grouped-Query Attention).
  • $d_{\text{head}}$ is the hidden dimension per head.
  • $N_{\text{ctx}}$ is the prompt sequence length.
  • $S_{\text{dtype}}$ is the bytes per element (e.g., 2 for FP16/BF16, 1 for FP8).
  • $B_{\text{RDMA}}$ is the effective cross-sectional RDMA bandwidth.
  • $\tau_{\text{prop}}$ is the network fabric propagation and queueing latency.

Because $T_{\text{xfer}}$ overlaps asynchronously with decode batch construction or is performed via pipeline chunking, $T_{\text{xfer}}$ is amortized, driving $\Delta_{\text{bubble}} \to 0$ and yielding deterministic P99 Inter-Token Latency.


Zero-Copy KV-Cache Transfer over RDMA/RoCE

The fundamental requirement for making $T_{\text{xfer}}$ negligible is bypassing host memory entirely. Conventional socket-based TCP/IP communication requires copying memory from GPU HBM $\to$ Host Pinned Memory $\to$ Kernel Network Buffers $\to$ NIC, introducing significant latency and saturating PCIe lanes.

Conventional CPU-Mediated Data Path (Inefficient):
[GPU HBM] --(PCIe)--> [Host RAM] --(OS Copy)--> [Socket Buffer] --(PCIe)--> [NIC]

GPUDirect RDMA Zero-Copy Data Path (Optimal):
[Prefill GPU HBM] --(PCIe / NVLink)--> [Local NIC] ===== (RDMA Network) =====> [Remote NIC] --(PCIe)--> [Decode GPU HBM]

Paged Memory Descriptors and Head-Sharding

In engines utilizing PagedAttention abstractions, the KV-cache is allocated as non-contiguous fixed-size blocks (e.g., 16 or 32 tokens per block). When migrating a context across nodes with different Tensor Parallelism degrees (e.g., Prefill node uses $TP_P = 8$, Decode node uses $TP_D = 1$), the KV heads must be dynamically redistributed.

Each physical block is mapped through a memory registration descriptor exposed to the Remote Direct Memory Access (RDMA) subsystem via OpenFabrics Enterprise Distribution (OFED) / InfiniBand verbs:

                  P-WORKER (TP=2)                        D-WORKER (TP=1)
            +-------------------------+            +-------------------------+
 GPU 0 HBM: | Block 0 (Heads 0..1)    |            | Block 0                 |
            +-------------------------+   RDMA     | - Heads 0..1 (from G0)  |
            | Block 1 (Heads 0..1)    | ---------> | - Heads 2..3 (from G1)  |
            +-------------------------+   Scatter  +-------------------------+
                                          Gather   | Block 1                 |
 GPU 1 HBM: | Block 0 (Heads 2..3)    | ---------> | - Heads 0..1 (from G0)  |
            +-------------------------+            | - Heads 2..3 (from G1)  |
            | Block 1 (Heads 2..3)    |            +-------------------------+
            +-------------------------+

The migration engine issues one-sided RDMA Write operations (IBV_WR_RDMA_WRITE) with immediate data flags directly targeting the pre-allocated physical page addresses of the $D$-Worker HBM.


Speculative Verification and Pipelined Micro-Batching

Disaggregated serving enables a powerful architectural synergy with Speculative Decoding. In a unified system, running a small draft model on the same accelerator as the target model disrupts memory bandwidth utilization and induces context switching in CUDA streams.

In a disaggregated engine, speculative execution is pipelined across three distinct components:

  1. Draft Worker: Executes $K$ steps of autoregressive generation using an ultra-lightweight draft model or an early-exit head, producing a speculative verification tree.
  2. Prefill/KV Worker: Computes and maintains context state, asynchronously streaming KV blocks for both draft and target models over RDMA.
  3. Target Verifier (Decode Node): Executes a single forward pass over all $K$ speculative tokens concurrently, verifying token acceptance using a modified parallel tree mask.
                           SPECULATIVE PIPELINE
 TIME -->
 Draft Model:   [Draft K tokens] ---------> [Draft K tokens] --------->
                       \                           \
 RDMA Sync:             \ (Async Tree Push)         \ (Async Tree Push)
                         v                           v
 Target Verifier:        [Verify Tokens 1..K]        [Verify Tokens 1..K]
                         [Emit Accepted + 1 ]        [Emit Accepted + 1 ]

Pipelined Chunked Prefill with Streaming Ingestion

For ultra-long context windows (e.g., $128\text{K}+$ tokens), waiting for the entire prefill phase to complete before beginning network transfer creates an idle "decode bubble."

To resolve this, the prefill engine divides the prompt into discrete chunks of size $C$ (e.g., $C = 4096$). As chunk $C_j$ finishes its forward pass:

  1. A non-blocking CUDA event signals the RDMA driver.
  2. The KV-cache slice for chunk $C_j$ is pushed via an RDMA Write directly to the destination node.
  3. Computation for chunk $C_{j+1}$ proceeds concurrently on the $P$-Worker.

By the time the final chunk $C_{\text{final}}$ completes, $(N-1)/N$ percent of the KV-cache has already migrated, reducing the apparent transfer latency $T_{\text{xfer}}$ to near zero.


Implementation: Custom CUDA Kernel and RDMA Ring-Buffer Orchestration

Below is an implementation of a disaggregated KV-cache transfer framework. It consists of a high-performance CUDA kernel for packing non-contiguous PagedAttention memory into an RDMA-registered contiguous intermediate staging buffer (or direct HBM-pinned memory), alongside the C++/libibverbs host orchestration code.

1. KV-Cache Resharding & Packing CUDA Kernel

#include 
#include 

// Struct representing the layout of a single block in PagedAttention
struct PagedKVBlock {
    // Shape: [num_heads, tokens_per_block, head_dim]
    half* k_ptr;
    half* v_ptr;
};

__global__ void pack_paged_kv_for_rdma(
    const PagedKVBlock* __restrict__ block_table,
    const int32_t* __restrict__ physical_block_ids,
    half* __restrict__ rdma_staging_buffer,
    const int tokens_per_block,
    const int num_heads,
    const int head_dim,
    const int num_blocks_to_pack
) {
    // 3D grid: x -> head_dim, y -> token within block, z -> block index
    int dim_idx = blockIdx.x * blockDim.x + threadIdx.x;
    int token_idx = blockIdx.y * blockDim.y + threadIdx.y;
    int block_seq_idx = blockIdx.z;

    if (block_seq_idx >= num_blocks_to_pack || 
        token_idx >= tokens_per_block || 
        dim_idx >= head_dim) {
        return;
    }

    int physical_block_id = physical_block_ids[block_seq_idx];
    PagedKVBlock src_block = block_table[physical_block_id];

    // Compute dense contiguous offsets for network egress
    // Target Layout: [num_blocks, 2 (K/V), num_heads, tokens_per_block, head_dim]
    size_t elements_per_block = 2 * num_heads * tokens_per_block * head_dim;
    half* dst_block_base = rdma_staging_buffer + (block_seq_idx * elements_per_block);

    for (int h = 0; h < num_heads; ++h) {
        // Source offsets inside non-contiguous HBM page
        size_t src_offset = (h * tokens_per_block * head_dim) + (token_idx * head_dim) + dim_idx;
        
        // Destination offsets in contiguous transfer buffer
        size_t k_dst_offset = (0 * num_heads * tokens_per_block * head_dim) + 
                              (h * tokens_per_block * head_dim) + 
                              (token_idx * head_dim) + dim_idx;
                              
        size_t v_dst_offset = (1 * num_heads * tokens_per_block * head_dim) + 
                              (h * tokens_per_block * head_dim) + 
                              (token_idx * head_dim) + dim_idx;

        // Perform vectorized or direct read/write
        dst_block_base[k_dst_offset] = src_block.k_ptr[src_offset];
        dst_block_base[v_dst_offset] = src_block.v_ptr[src_offset];
    }
}

2. RDMA Out-of-Band Registration and Verbs Dispatcher

#include 
#include 
#include 
#include 

class RDMAServingEngine {
private:
    struct ibv_context* context;
    struct ibv_pd* pd;
    struct ibv_cq* cq;
    struct ibv_qp* qp;
    struct ibv_mr* gpu_mr; // GPUDirect RDMA Memory Region

public:
    RDMAServingEngine(struct ibv_context* dev_ctx, void* gpu_hbm_buffer, size_t buffer_size) 
        : context(dev_ctx) {
        
        // Allocate Protection Domain
        pd = ibv_alloc_pd(context);
        if (!pd) throw std::runtime_error("Failed to allocate Protection Domain");

        // Create Completion Queue
        cq = ibv_create_cq(context, 1024, nullptr, nullptr, 0);
        if (!cq) throw std::runtime_error("Failed to create Completion Queue");

        // Initialize Queue Pair with GPUDirect access flags
        struct ibv_qp_init_attr qp_init_attr = {};
        qp_init_attr.send_cq = cq;
        qp_init_attr.recv_cq = cq;
        qp_init_attr.qp_type = IBV_QPT_RC; // Reliable Connection
        qp_init_attr.cap.max_send_wr = 512;
        qp_init_attr.cap.max_recv_wr = 512;
        qp_init_attr.cap.max_send_sge = 1;
        qp_init_attr.cap.max_recv_sge = 1;

        qp = ibv_create_qp(pd, &qp_init_attr);
        if (!qp) throw std::runtime_error("Failed to create Queue Pair");

        // Register GPUDirect HBM Pointer with the RDMA Subsystem
        // IBV_ACCESS_RELAXED_ORDERING increases PCIe throughput on NVIDIA Hopper / Blackwell
        int access_flags = IBV_ACCESS_LOCAL_WRITE | 
                           IBV_ACCESS_REMOTE_WRITE | 
                           IBV_ACCESS_REMOTE_READ |
                           IBV_ACCESS_RELAXED_ORDERING;

        gpu_mr = ibv_reg_mr(pd, gpu_hbm_buffer, buffer_size, access_flags);
        if (!gpu_mr) throw std::runtime_error("Failed to register GPU HBM for GPUDirect RDMA");
    }

    // Post an asynchronous zero-copy write to remote Decode node
    void post_rdma_kv_transfer(
        uint64_t local_gpu_offset, 
        uint64_t remote_gpu_addr, 
        uint32_t rkey, 
        size_t transfer_length,
        uint32_t imm_data
    ) {
        struct ibv_sge sge;
        sge.addr = (uintptr_t)((uint8_t*)gpu_mr->addr + local_gpu_offset);
        sge.length = transfer_length;
        sge.lkey = gpu_mr->lkey;

        struct ibv_send_wr wr = {};
        struct ibv_send_wr* bad_wr = nullptr;

        wr.wr_id = 1;
        wr.sg_list = &sge;
        wr.num_sge = 1;
        wr.opcode = IBV_WR_RDMA_WRITE_WITH_IMM;
        wr.send_flags = IBV_SEND_SIGNALED;
        wr.imm_data = htonl(imm_data); // Signals block index to decode receiver
        wr.wr.rdma.remote_addr = remote_gpu_addr;
        wr.wr.rdma.rkey = rkey;

        if (ibv_post_send(qp, &wr, &bad_wr) != 0) {
            throw std::runtime_error("ibv_post_send failed to issue zero-copy KV transfer");
        }
    }

    ~RDMAServingEngine() {
        if (gpu_mr) ibv_dereg_mr(gpu_mr);
        if (qp) ibv_destroy_qp(qp);
        if (cq) ibv_destroy_cq(cq);
        if (pd) ibv_dealloc_pd(pd);
    }
};

Performance Benchmarks and Production Topologies

Evaluating disaggregated serving against monolithic colocation reveals substantial throughput and latency Pareto frontier improvements, particularly as context windows expand and traffic models exhibit bursty arrival patterns.

Production Setup

  • Workload: ShareGPT + synthetic multi-turn conversations (Context lengths: 4,096 to 32,768 tokens; Output: 512 tokens).
  • Baseline (Unified): 8 $\times$ NVIDIA H100 SXM5 (80GB), running vLLM v0.6+ with continuous batching and PagedAttention ($TP=8$).
  • Disaggregated Topology:
    • Prefill Engine: 4 $\times$ NVIDIA H100 ($TP=4$).
    • Decode Engine: 4 $\times$ NVIDIA H100 ($TP=1, DP=4$).
    • Interconnect: NVIDIA Quantum-2 InfiniBand (400 Gbps per GPU) with GPUDirect RDMA.
+-------------------------------------------------------------------------------+
|                      LATENCY & THROUGHPUT BENCHMARK COMPARISON                |
+------------------------------------+--------------------+---------------------+
| Metric                             | Unified (vLLM v0.6)| Disaggregated Engine|
+------------------------------------+--------------------+---------------------+
| Time-To-First-Token (TTFT) P50     | 420 ms             | 185 ms              |
| Time-To-First-Token (TTFT) P99     | 2,150 ms           | 210 ms              |
| Inter-Token Latency (ITL) Mean     | 18.2 ms            | 7.8 ms              |
| Inter-Token Latency (ITL) P99      | 84.5 ms            | 8.9 ms              |
| Max Token Throughput (tokens/s/GPU)| 1,240              | 3,180               |
| GPU Model FLOPs Utilization (MFU)  | 28.4%              | 56.8%               |
+------------------------------------+--------------------+---------------------+
LATENCY JITTER UNDER HIGH LOAD:

Unified (Colocated) System:
ITL Latency (ms)
 100 |          /\        /\          /\   (Preemption by Prefill requests)
  50 |         /  \      /  \        /  \
  10 |________/____\____/____\______/____\_______
      0        10        20        30        40   (Tokens Generated)

Disaggregated Architecture:
ITL Latency (ms)
 100 |
  50 |
  10 |-----------------------------------------   (Flat, deterministic P99)
      0        10        20        30        40   (Tokens Generated)

Architectural Analysis of Results

  1. Elimination of Latency Jitter (Flat P99): In the unified system, the P99 ITL spikes to $84.5\text{ ms}$ whenever a batch-scheduling cycle admits a large prefill request. The disaggregated system holds ITL P99 at $8.9\text{ ms}$ because $D$-Workers never execute compute-dense prefill forward passes.
  2. TTFT Determinism: $P$-Workers process prompts without context switches, driving TTFT P99 down from $2,150\text{ ms}$ to $210\text{ ms}$.
  3. MFU Amplification: By eliminating low-batch GEMM calls on $P$-Workers (enforcing compute-saturating batches) and avoiding memory serialization bubbles on $D$-Workers, Model FLOPs Utilization (MFU) doubles from $28.4%$ to $56.8%$.

Conclusion

The disaggregated LLM serving paradigm addresses the fundamental physical trade-off in modern AI compute: the divergence between compute-bound matrix prefilling and memory-bound autoregressive decoding. By treating the KV-cache as an ephemeral, globally routable distributed data layer rather than local GPU state, systems architects can achieve:

  • Isolation of compute and memory domains, leading to near-zero P99 ITL jitter.
  • Asymmetrical cluster scaling, provisioning cheap memory-dense accelerators for decode pools while reserving top-tier Tensor Core accelerators for prefill bursts.
  • Seamless integration with advanced inference optimizations, including streaming chunked prefill, speculative verification trees, and hierarchical caching across NVMe/CXL fabrics.

As models scale past 100-billion parameters and context lengths push into the millions, disaggregation transforms from an optimization technique into a foundational requirement for high-throughput, low-latency AI infrastructure.


References