Ringkasan Utama
Disaggregated prefill dan decode di vLLM v0.31.0 menjalankan prefill dan decode sebagai instance vllm serve terpisah yang dihubungkan NixlConnector, sehingga prompt panjang tidak lagi menghambat streaming token. Tes L40S vLLM menurunkan p99 inter-token latency dari 169 ms ke 29 ms, tetapi tanpa akses GPU peer-to-peer, median time to first token naik dari 0,7 detik ke 2,2 detik.
Kesalahan paling mahal pada split prefill dan decode adalah mem-benchmark setup yang diam-diam rusak. Kalau dua GPU tidak bisa saling menjangkau secara langsung, split tetap jalan, tetap mengembalikan teks yang benar, dan tetap menampilkan grafik inter-token latency yang bersih, padahal setiap request membayar lebih dari satu detik untuk memindahkan KV cache. Anda baru sadar ketika ada yang bertanya kenapa time to first token jadi tiga kali lipat.
Tulisan ini adalah pembacaan teliti panduan disaggregated serving dari vLLM sendiri yang terbit pada 29 September 2026, bersama dokumentasi NixlConnector, halaman fitur disaggregated prefilling, dan release notes v0.31.0. Saya tidak menjalankan setup ini sendiri, jadi semua pengukuran di bawah adalah milik vLLM dan ditandai demikian, dan perintahnya adalah yang dicetak halaman-halaman tersebut. Kalau sebuah angka adalah hitungan saya sendiri dan bukan hasil pengukuran, saya sebutkan.
Kenapa satu proses vllm serve berebut dengan dirinya sendiri
Satu proses vllm serve mengerjakan tiga pekerjaan berbeda. Prefill membaca seluruh prompt dalam satu ledakan yang terikat compute dan menentukan time to first token. Decode menghasilkan satu token per langkah, terikat memory bandwidth, dan menentukan inter-token latency. Frontend CPU mengurus templating, tokenisation, dan parsing output. Di satu GPU, dua yang pertama bergantian memakai hardware yang sama, sehingga prefill panjang jatuh di tengah aliran token request lain.
Ukuran keberhasilan di panduan itu adalah goodput: jumlah request per detik yang memenuhi target time to first token sekaligus target inter-token latency, bukan token per detik puncak. Halaman fitur jujur soal sisi lainnya: 'Disaggregated prefill DOES NOT improve throughput.' Yang didapat adalah kemampuan menyetel kedua latency secara terpisah, dengan pengaturan paralel berbeda per instance, dan mencegah prefill menyela decode. Halaman yang sama menandai fitur ini sebagai experimental dan bisa berubah.
Idenya lebih tua dari implementasi vLLM. Paper DistServe menempatkan prefill dan decoding di GPU berbeda dan melaporkan bisa melayani 7,4x lebih banyak request, atau memenuhi target latency 12,6x lebih ketat, dibanding sistem pembandingnya. Anggap itu batas atas riset, bukan prakiraan untuk model Anda. Sisa tulisan ini membahas seberapa banyak yang bertahan ketika bertemu interconnect sungguhan.
Hitung biaya transfer KV sebelum membangun apa pun
Setiap request dalam split harus memindahkan KV cache prompt-nya dari GPU prefill ke GPU decode sebelum token pertama dihasilkan. Ukurannya ditentukan model, jadi tagihannya bisa diperkirakan dari model card. Panduan itu menyebut Llama-3.1-70B dalam BF16 memakai 320 KiB KV cache per token. Prompt 10.000 token berarti sekitar 3 GB, dan pada 400 Gb/s waktu di kabel saja sekitar 64 ms. Itu hitungan saya: 3,2 GB dikali 8 bit, dibagi 400 Gb/s, sebelum overhead protokol.
| Kasus | Volume KV | Waktu sampai ke decode | Asal angka |
|---|---|---|---|
| Llama-3.1-70B BF16, prompt 10.000 token | Sekitar 3 GB (320 KiB per token) | Sekitar 64 ms pada 400 Gb/s, sebelum overhead | KV per token dari panduan; waktunya hitungan saya |
| Qwen2.5-7B, prompt 8k token, dua L40S tanpa peer-to-peer | Sekitar 470 MB | Sekitar 1,3 detik | Diukur panduan di mesin ujinya |
Baris kedua adalah pelajarannya. Data yang kurang dari seperenam ukuran baris pertama memakan waktu sekitar dua puluh kali lebih lama, karena jalur antar-GPU bukan peer to peer. Saya membandingkan waktu kabel teoretis dengan waktu terukur di hardware berbeda, jadi baca ini sebagai orde besaran, bukan rasio. Intinya, bandwidth bukan satu-satunya variabel: rutenya juga. Panduan itu meminta dua pengecekan sebelum benchmark apa pun.
# 1. Topology. On one box this must say OK between the prefill GPU and
# the decode GPU. Anything else means KV moves by a slower path.
nvidia-smi topo -p2p r
# 2. Send a few long prompts ONE AT A TIME through the proxy, then read the
# decode instance's log for its "KV Transfer metrics" line. If
# "Avg xfer time" is in the hundreds of milliseconds, fix the transport
# before you measure anything else.
grep "KV Transfer metrics" decode.log | tail -n 5Yang pertama memastikan topologi, yang kedua membaca biaya transfer yang sebenarnya. Keduanya tidak butuh harness benchmark dan hanya memakan semenit. Kalau salah satunya janggal, apa pun yang Anda ukur sesudahnya tidak berarti.
Jalankan tiga proses
Deployment minimal di panduan itu adalah dua instance vllm serve dan sebuah proxy, dengan asumsi vLLM v0.31.0 atau lebih baru. Prefiller memakai kv_role kv_producer, decoder memakai kv_consumer, dan keduanya memuat NixlConnector. NIXL dipasang dengan uv pip install nixl di NVIDIA; platform lain membangunnya dari source terhadap UCX. Saya mengganti contoh wiring Qwen3-0.6B dari panduan dengan Qwen2.5-7B-Instruct, karena panduan menyebut model 0,6B melakukan prefill begitu cepat sampai hampir tidak mengganggu decode, jadi tidak ada yang perlu diperbaiki oleh split.
# Requires vLLM v0.31.0 or later, and NIXL: uv pip install nixl
# Prefiller on GPU 0. kv_producer = this instance computes KV and hands it off.
CUDA_VISIBLE_DEVICES=0 UCX_NET_DEVICES=all VLLM_NIXL_SIDE_CHANNEL_PORT=5600 \
vllm serve Qwen/Qwen2.5-7B-Instruct --port 8100 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_producer","kv_load_failure_policy":"fail"}' \
2>&1 | tee prefill.log
# Decoder on GPU 1. Its side-channel port MUST differ from the prefiller's
# whenever both run on one host: each worker needs a unique port per host.
CUDA_VISIBLE_DEVICES=1 UCX_NET_DEVICES=all VLLM_NIXL_SIDE_CHANNEL_PORT=5601 \
vllm serve Qwen/Qwen2.5-7B-Instruct --port 8200 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_consumer","kv_load_failure_policy":"fail"}' \
2>&1 | tee decode.log
# Development proxy from the vLLM repository. Clients talk to :8192.
python tests/v1/kv_connector/nixl_integration/toy_proxy_server.py --port 8192 \
--prefiller-hosts localhost --prefiller-ports 8100 \
--decoder-hosts localhost --decoder-ports 8200Contoh di dokumentasi NixlConnector juga memakai --enforce-eager. Saya tidak menyertakannya agar benchmark menjalankan engine seperti di production. Proxy-nya adalah toy_proxy_server.py dari repository vLLM, dan panduan menyebutnya hanya untuk development, dengan llm-d dan Dynamo sebagai pilihan production. Setelan yang penting adalah berikut.
| Setelan | Letak | Default | Fungsi |
|---|---|---|---|
| VLLM_NIXL_SIDE_CHANNEL_PORT | Environment, setiap worker | 5600 | Port handshake. Harus unik per worker di satu host; dengan TP atau DP nilainya base port ditambah dp_rank |
| VLLM_NIXL_SIDE_CHANNEL_HOST | Environment | localhost | Isi dengan alamat yang bisa dijangkau saat prefill dan decode berjalan di mesin berbeda |
| kv_role | kv-transfer-config | Isi secara eksplisit | kv_producer untuk prefill, kv_consumer untuk decode. kv_both sudah deprecated untuk NixlConnector |
| kv_lease_duration | kv_connector_extra_config | 30 detik | Berapa lama prefill menahan block yang sudah selesai sambil menunggu decode membacanya; heartbeat memperpanjangnya |
| kv_load_failure_policy | kv-transfer-config | fail | fail menggagalkan request saat KV load gagal; recompute membangun ulang block yang gagal di decode |
| backends | kv_connector_extra_config | UCX | Memilih transport lain, misalnya LIBFABRIC; ketersediaannya bergantung pada cara NIXL dibangun |
Lintas mesin, beri setiap instance VLLM_NIXL_SIDE_CHANNEL_HOST sendiri dan berikan ke proxy alamat dan port setiap instance. Untuk multi-host data parallel, dokumentasi menyebut hanya host dan port instance head yang dibutuhkan.
Apa yang dilakukan proxy pada setiap request
Proxy adalah tempat split mulai terlihat, jadi layak mengenal empat langkahnya. Client hanya bicara ke port-nya, 8192 pada contoh, dan tidak pernah langsung ke prefill atau decode.
- Proxy mengirim request ke prefill dengan max_tokens bernilai 1 dan kv_transfer_params berisi do_remote_decode true.
- Prefill menghitung KV cache, menahan block-nya, dan mengembalikan kv_transfer_params yang menunjuk ke block tersebut.
- Proxy meneruskan request asli ke decode dengan params itu terlampir.
- Decode menarik block lewat NIXL, lalu generate.
# Step 1: what the proxy sends to the PREFILL instance (completions API).
# max_tokens is 1 because prefill only has to produce the KV cache.
{
"model": "Qwen/Qwen2.5-7B-Instruct",
"prompt": "<the full long prompt>",
"max_tokens": 1,
"kv_transfer_params": {"do_remote_decode": true}
}
# Step 2: prefill answers with kv_transfer_params describing the blocks it is
# holding. Step 3: the proxy sends the ORIGINAL request to the DECODE instance
# with those params attached. Step 4 happens inside decode: it pulls the
# blocks over NIXL, then generates.
# Chat completions variant: call prefill with "return_token_ids": true, then
# give decode its kv_transfer_params plus "prompt_token_ids" from prefill.Untuk chat completions, varian di panduan meminta token ID dari prefill dan memberikannya ke decode. Halaman fitur menandai pemakaian ulang token ID itu experimental: ID diabaikan jika messages berisi konten non-teks atau echo diaktifkan, dan ID yang tidak valid menghasilkan HTTP 400. Konsekuensi praktisnya, bug di proxy terlihat seperti bug vLLM. Kirim satu request pendek lewat proxy dan pastikan completion normal sebelum menambah beban.
Benchmark goodput, bukan token per detik
Panduan itu mengukur dengan vllm bench serve dan definisi goodput time to first token dalam 2000 ms dan time per output token dalam 30 ms. Workload-nya dataset acak dengan input 8.192 token dan output 256 token serta ignore-eos, sehingga setiap request berjalan sampai panjang penuh. Kalau Anda memakai ulang seed di berbagai request rate, panduan meminta prefix caching dimatikan, kalau tidak run berikutnya bisa dilayani dari cache run sebelumnya.
# Goodput = requests per second that meet BOTH targets: TTFT 2000 ms, TPOT 30 ms.
# Run it against the collocated server and against the proxy on :8192 with the
# same hardware, then compare. Repeat per --request-rate.
vllm bench serve --model Qwen/Qwen2.5-7B-Instruct --port 8192 \
--dataset-name random --random-input-len 8192 --random-output-len 256 --ignore-eos \
--request-rate 0.4 --num-prompts 100 \
--percentile-metrics ttft,tpot,itl --metric-percentiles 50,99 \
--goodput ttft:2000 tpot:30| Pengukuran (Qwen2.5-7B, dua L40S, prompt sekitar 8k token) | Collocated | Split prefill/decode |
|---|---|---|
| p99 inter-token latency pada 0,4 req/s | 169 ms | 29 ms |
| p99 inter-token latency pada 2 req/s | 263 ms | 25 sampai 52 ms di semua beban |
| Median inter-token latency sampai 1 req/s | 21 sampai 24 ms | 21 sampai 24 ms |
| Median time to first token pada 0,2 req/s | 0,7 detik | 2,2 detik |
Baca tabel itu untuk apa yang tidak dikatakannya. Median inter-token latency sama di kedua setup, jadi split tidak melakukan apa pun untuk token yang tipikal. Yang dihilangkan adalah tail: p99 pada 0,4 req/s turun dari 169 ms ke 29 ms, dan panduan menyebut split tidak pernah melewati 52 ms. Harganya ada di baris terakhir, ketika mesin L40S tidak punya akses peer-to-peer dan median time to first token naik dari 0,7 detik ke 2,2 detik. Split hanya menang jika target layanan Anda lebih peduli pada tail daripada waktu token pertama.
Dua hasil eksternal dikutip panduan itu, dan saya belum memeriksa benchmark aslinya. Pada satu node 8xMI300X yang menjalankan Qwen3-235B-A22B-FP8, connector MoRI-IO dari AMD memenuhi kedua target untuk 73 dari 100 request dibanding 30 dari 100 pada collocated, sekitar 2,4x goodput. llm-d melaporkan sekitar 59% lebih rendah untuk mean end-to-end latency dan 67% lebih rendah untuk P95 pada gpt-oss-120b di 16 H200. Keduanya memakai model lebih besar di akselerator kelas data center, bukan pasangan L40S di atas, jadi jangan membacanya sebagai prediksi untuk mesin kecil.
Bagaimana split yang rusak tampak seperti yang sehat
Sebuah split bisa salah dengan cara yang tetap mengembalikan teks. Berikut empat kegagalan yang dijelaskan sumber, beserta pengecekan yang membongkarnya. Baris pertama yang paling sering menjebak benchmark: grafik inter-token tampak sempurna sementara token pertama merangkak.
| Gejala | Penyebab yang mungkin | Pengecekan |
|---|---|---|
| Inter-token latency bagus, median time to first token beberapa kali lebih buruk | Tidak ada jalur peer-to-peer antar GPU, sehingga KV merangkak lewat rute yang lambat | nvidia-smi topo -p2p r, lalu Avg xfer time di baris KV Transfer metrics milik decode |
| Request error saat beban naik | kv_load_failure_policy bernilai fail dan sebuah transfer gagal | Log decode di sekitar request yang gagal |
| Block hilang sebelum dibaca decode | kv_lease_duration 30 detik habis saat request mengantre di decode | Metrik vllm:nixl_num_kv_expired_reqs; naikkan lease jika nilainya tinggi |
| Output multi-turn dari reasoning model lancar tetapi salah | bidirectional_kv_xfer dengan chat template yang membuang thinking trace giliran sebelumnya, sehingga block prefill dan decode tidak lagi cocok | Issue vLLM 43094; panduan menyebut belum ada yang menangkap ketidakcocokan ini di vLLM saat ini |
Ada pilihan sengaja pada failure policy. Default-nya, fail, langsung menggagalkan request dan menghindari prefill ulang di instance decode. Alternatifnya, recompute, membangun ulang block yang gagal secara lokal di decode, itulah cara request bertahan dari transfer yang gagal. Dokumentasi memperingatkan ini bisa menyebabkan jitter performa decode dan tail latency lebih tinggi, dan menyebutnya tidak efisien untuk production.
Jangan aktifkan recompute selagi Anda memvalidasi setup baru. Ia mengubah transfer yang gagal menjadi keberhasilan yang lambat, persis kegagalan yang hendak dibongkar pengecekan di atas. Pertahankan fail sampai jalur transfer lolos, supaya link yang rusak muncul sebagai error, bukan sebagai angka latency yang menyenangkan.
Kasus multi-turn pantas mendapat kalimat tersendiri. Dengan bidirectional_kv_xfer di kedua instance, prefill menarik kembali block giliran sebelumnya dari decode sehingga hanya token baru yang dihitung. Itu penghematan nyata, dan itu pula sebabnya template yang menulis ulang riwayat menghasilkan output yang salah, bukan sekadar lambat. Release notes v0.31.0 juga mencantumkan dead peer state dilepas tanpa menunggu TTL, salah satu alasan memakai versi itu dan bukan yang lebih lama.
Render tier tanpa GPU, dan apa yang masih terbuka
Split kedua memindahkan pekerjaan CPU, bukan GPU. vllm launch render menjalankan templating dan parsing tanpa GPU, dan vllm serve --tokens-only menjalankan engine hanya pada token ID. Render, prefill, dan decode lalu bisa berjalan sebagai tier terpisah, dengan kode Anda sendiri yang mengatur serah terimanya.
# CPU-only frontend: templating, tokenisation, parsing. No GPU needed.
# The parser flags must match the engine's configuration, or the split into
# content / reasoning / tool_calls will differ from an in-process server.
vllm launch render Qwen/Qwen3-0.6B --port 8100 \
--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser hermes
# Engine on token IDs only. Rejects stop strings; cannot return text itself.
vllm serve Qwen/Qwen3-0.6B --tokens-only --port 8200
# The request travels three hops:
# POST :8100/v1/chat/completions/render -> token IDs
# POST :8200/inference/v1/generate -> generated token IDs
# POST :8100/v1/chat/completions/derender -> text, reasoning, tool calls
# Wrong: calling derender with only the generated tokens. It needs
# chat_request, the original request, passed back in. Without it: HTTP 400.Angka di panduan kecil, dan itulah daya tariknya. Me-render prompt 9.000 token memakan sekitar 15 ms CPU, dan satu server render menangani 73 request per detik pada sedikit lebih dari satu core. Jumlah worker diatur dengan --renderer-num-workers dengan default 1. Panduan juga memperingatkan bahwa konfigurasi render dan engine harus cocok, kalau tidak pemisahan content, reasoning, dan tool_calls akan berbeda dari server in-process.
Issue yang masih terbuka adalah bagian yang perlu dibaca sebelum mengadopsinya. Streaming derender dengan parser memutar ulang seluruh riwayat token di setiap chunk, yang diukur panduan sekitar 9x CPU in-process, dengan lapisan caching dilacak di issue 57571. Prompt panjang dikirim ulang di setiap chunk streaming. Batch derender bisa melewatkan finish_reason tool_calls (issue 47931), dan logprobs kembali sebagai string placeholder (issue 57574). Saya akan memperlakukan 73 request per detik sebagai batas atas yang perlu diukur terhadap traffic Anda sendiri.
Mulailah dengan split prefill dan decode saja. Render tier memindahkan pekerjaan CPU dari host GPU, tetapi issue terbuka di panduan itu menunjukkan streaming memakan sekitar 9x CPU in-process, jadi baru layak dipakai ketika CPU frontend terbukti jadi bottleneck.
Yang dikerjakan hari Senin
Inilah urutan yang akan saya ikuti, berhenti di langkah pertama yang gagal. Pengecekan murah dan benchmark mahal, jadi pengecekan didahulukan.
- Pastikan vLLM v0.31.0 atau lebih baru di kedua host dan pasang NIXL.
- Jalankan nvidia-smi topo -p2p r dan syaratkan OK antara GPU prefill dan decode. Di mesin yang tidak bisa menunjukkan OK, harapkan sesuatu seperti transfer 1,3 detik yang diukur panduan.
- Jalankan prefill dan decode dengan side-channel port berbeda dan kv_load_failure_policy fail, lalu kirim satu request pendek lewat proxy.
- Kirim beberapa prompt panjang satu per satu dan baca Avg xfer time di baris KV Transfer metrics milik decode. Ratusan milidetik berarti perbaiki transport lebih dulu.
- Baru setelah itu jalankan benchmark goodput, dengan model kelas 7B, terhadap baseline collocated di hardware yang sama, dengan prefix caching dimatikan jika Anda memakai ulang seed.
#!/usr/bin/env bash
# preflight.sh: stop at the first check that fails.
set -euo pipefail
# 1. Version. The guide assumes v0.31.0 or later.
python -c "import vllm; print(vllm.__version__)"
# 2. Peer-to-peer between the GPUs. Read the matrix: you want OK between
# the prefill and decode devices.
nvidia-smi topo -p2p r
# 3. One short request end to end through the proxy before any load.
curl -s localhost:8192/v1/completions \
-H "Content-Type: application/json" \
-d '{"model":"Qwen/Qwen2.5-7B-Instruct","prompt":"Say hello.","max_tokens":16}'
# 4. A few long prompts at a low rate, then read the transfer cost.
vllm bench serve --model Qwen/Qwen2.5-7B-Instruct --port 8192 \
--dataset-name random --random-input-len 8192 --random-output-len 64 \
--ignore-eos --request-rate 0.1 --num-prompts 5
grep "KV Transfer metrics" decode.log | tail -n 5
# 5. Is the decode side losing blocks to lease expiry? (kv_lease_duration)
curl -s localhost:8200/metrics | grep nixl_num_kv_expired_reqsTulis aturan go atau no-go sebelum menjalankan benchmark. Kalau p99 inter-token latency Anda sudah memenuhi target saat collocated, split hanya menambah satu peran deployment dan jalur transfer tanpa manfaat, karena dokumentasi tegas bahwa ia tidak meningkatkan throughput. Kalau tail yang menyakitkan, dan pengecekan transfer lolos, angka di atas menunjukkan layak dicoba.
Disaggregation adalah alat pembentuk latency dengan tagihan transfer, bukan percepatan gratis. Aturan yang dibawa: ukur jalur transfer KV lebih dulu, benchmark goodput terhadap baseline collocated di hardware identik, dan biarkan kv_load_failure_policy tetap fail sampai angkanya nyata.
Sumber dan bacaan lanjutan
- Blog vLLM: panduan praktis disaggregated serving (29 September 2026), sumber semua angka benchmark di tulisan ini
- Dokumentasi vLLM: panduan penggunaan NixlConnector, sumber perintah, environment variable, dan failure policy
- Dokumentasi vLLM: halaman fitur disaggregated prefilling, termasuk pernyataan soal throughput dan daftar connector
- Release notes vLLM: v0.31.0, versi minimum yang diasumsikan panduan
- Paper DistServe (arXiv 2401.09670): memisahkan prefill dan decoding untuk serving yang dioptimalkan goodput
- NIXL di GitHub (ai-dynamo/nixl), library transfer yang ditunjuk dokumentasi NixlConnector

