We built a self-contained benchmark harness (vLLM + an async load generator) that pins input/output length exactly, sweeps concurrency, and records TTFT, inter-token latency (ITL), end-to-end latency, throughput, and goodput. Every configuration runs through the identical harness so the comparisons are honest.
| ID | Configuration | Prefill | Decode | KV path | Status |
|---|---|---|---|---|---|
| A | Aggregated (baseline) | GPU | same GPU | none (in-place) | measured |
| C | Same-vendor disaggregation | NVIDIA GPU 0 | NVIDIA GPU 1 | NIXL, host-staged | measured |
| E | CPU prefill → GPU decode | CPU (EPYC) | GPU | DRAM → PCIe | measured* |
| B | Aggregated AMD | AMD MI300X | — | no AMD stock | |
| D | Cross-vendor (NVIDIA→AMD) | NVIDIA | AMD | host-staged | no AMD stock |
* Config E was measured by emulation (CPU prefill timed directly, composed with GPU decode) — see the honest limitations at the end. AMD (B, D) was unavailable on our cloud, so the cross-vendor hypothesis stays open.
Before judging disaggregation we need the reference. On a single H100, gpt-oss-20b throughput rises almost linearly with concurrency while inter-token latency stays low — this is the number every split has to beat. The prefill-heavy signal is visible too: at 8K input, time-to-first-token climbs steeply with load.
We split prefill onto GPU 0 (producer) and decode onto GPU 1 (consumer), wired by NIXL. The catch: our two L40S GPUs were connected only through the host bridge (no NVLink, no direct peer-to-peer), so NIXL fell back to host-staged transfer — GPU → CPU DRAM → GPU. At scale, the decode engine's own logs measured KV transfers averaging ~2.4 seconds each. That overhead swamps any benefit.
The second idea (H2): move prefill off the GPU entirely, onto the CPU, freeing the GPU for pure decode. The appeal is real — the CPU→GPU path skips the network — but prefill is compute-bound, and a CPU has a fraction of a GPU's math throughput. We measured it on a 64-core EPYC 9554 against the same 8B model on an L40S.
The decision isn't "disaggregate or don't" — it's a function of your interconnect, your workload shape, and what you're optimizing for. Here is the guide our data supports.
| If your situation is… | Use | Because |
|---|---|---|
| One GPU is enough for the model + load | A · Aggregated default | No transfer cost, simplest, and it scales cleanly. The bar everything else must clear. |
| Multi-GPU with NVLink / RDMA, high concurrency, long sequences | C · Disaggregation worth it | A fast fabric makes KV transfer cheap, and at high load it overlaps with compute — then independent prefill/decode scaling wins. |
| Multi-GPU with only PCIe / host-bridge links | A · Aggregated don't split | Host-staged KV transfer (~2.4 s/req here) costs more than specialization saves — we measured up to 5.5× worse TTFT. |
| Latency-sensitive (chat, autocomplete) | A · Aggregated avoid E | Any prefill offload — CPU or cross-node — adds first-token latency you can't hide from a user. |
| Throughput-only SLO, GPU is decode-bound, spare CPU | E · CPU prefill niche win | Short inputs only. Frees GPU for decode; can raise tokens-per-GPU-dollar when TTFT doesn't matter. |
Our headline result — disaggregation lost — sounds like it contradicts how frontier inference is built. It doesn't. Our test is the control case that proves why their infrastructure exists: same mechanism, no fast fabric, so it loses. The value is that it quantifies exactly how much overhead the fabric has to beat. Give the KV cache a fast enough wire and enough concurrency to hide behind, and the sign flips.
| Factor | Our benchmark — it lost | Frontier-scale serving — it wins |
|---|---|---|
| Interconnect | PCIe / host bridge — no P2P, ~2.4 s per KV transfer | NVLink/NVSwitch in-node + InfiniBand/RoCE RDMA with GPUDirect between nodes |
| Concurrency | up to 32 — transfer is on the critical path | thousands — transfer overlaps compute and disappears under load |
| Fleets | 2 GPUs, fixed roles | independent prefill and decode pools, autoscaled; P:D ratio tuned to traffic |
| Routing | round-robin | KV-cache-aware — routes to the node already holding the prompt's prefix |
| KV path | point-to-point, host-staged | pooled distributed KV store (DRAM/SSD tier) with prefix reuse |
Disaggregation is an interconnect bet. The one thing that flips our result is a fast fabric — everything else is amplification.
Everything is one command against the open-source harness. Results, run manifests (git SHA, vLLM version, GPU/CPU info), and every plot are in the repo.
git clone https://github.com/anshubansal2000/Heterogeneous-Prefill-Decode-Disaggregation bash scripts/setup_runpod.sh gpt-oss-20b # install vLLM + weights python run.py --config A --model gpt-oss-20b # aggregated baseline python run.py --config C --model gpt-oss-20b # NIXL disaggregation (2 GPUs) python scripts/cpu_prefill_bench.py --model Qwen/Qwen3-8B # CPU prefill
Workload matrix: input/output ∈ {1K, 8K}², concurrency 1–32, output length pinned exactly, prefix caching off to isolate raw compute. Total cloud cost for every run in this report: ~$9.60 on RunPod (H100 + 2× L40S + EPYC).
Open threads: the cross-vendor path (NVIDIA→AMD, Config D) needs AMD hardware we couldn't source; a precise CPU-prefill ceiling needs the vLLM CPU backend build. Both are natural next steps.