Ringkasan Utama
NixlPushConnector milik vLLM memindahkan transfer KV dari prefill ke decode keluar dari jalur token pertama. Decode mendaftarkan block tujuannya saat prefill masih berjalan, lalu prefill menulis KV dengan NIXL WRITE. Pada Qwen3-32B dengan AWS EFA, vLLM mengukur TTFT sekitar 540 ms dibanding 1.744 ms untuk pull pada 8 QPS dan input 16k.
Angka yang membuat saya membuka postingan ini adalah 1.744 ms. Itu time to first token yang dilaporkan vLLM untuk connector pull default pada 8 request per detik dengan prompt 16k token, di mesin dengan jaringan 3,2 Tb/s. Tidak ada yang tampak lambat di hardware itu, artinya waktunya habis di tempat lain, bukan di kabel.
Tulisan ini hasil membaca, bukan benchmark. Saya tidak menjalankan connector-nya. Saya membaca pengumuman vLLM tanggal 14 Oktober 2026, dokumen desain push connector, panduan penggunaan NixlConnector, sebuah pull request yang masih terbuka, dan postingan bidirectional transfer, dan saya hanya mengutip angka yang benar-benar saya baca di sana. Kalau saya menafsirkan, bukan mengutip, saya katakan.
Mengapa transfer KV ada di jalur TTFT
Pada deployment yang di-disaggregate, prefill dan decode berjalan sebagai instance vLLM terpisah, jadi KV cache yang dihitung prefill harus sampai ke decode sebelum token pertama bisa dihasilkan. Efisiensi transfer dengan demikian bagian dari TTFT. Dokumen vLLM sendiri memberi alasan memisahkannya: tiap sisi bisa memakai strategi paralel sendiri, dan tail inter-token latency lebih mudah dikendalikan dibanding chunked prefill saja.
Connector default berbasis pull. Setelah prefill, router meneruskan metadata sumber, yaitu ID block prefill, ke instance decode. Decode lalu mengalokasikan block tujuan, menyiapkan transfer, dan menjalankan NIXL READ. Pengamatan utama di postingan itu: langkah-langkah ini berjalan berurutan, setelah prefill selesai, di jalur kritis menuju token pertama.
# Pull (default NixlConnector): everything after prefill is serial.
#
# prefill compute --> router relays prefill block IDs to decode
# --> decode allocates destination blocks
# --> decode prepares the transfer (descriptors, inline)
# --> decode issues NIXL READ
# --> first token
#
# Push (NixlPushConnector): decode's side is done before prefill ends.
#
# decode allocates blocks --> PUSH_REG notification to prefill (early)
# prefill compute ---------------------------------------------+
# registration + prefill done -+--> NIXL WRITE
# --> first tokenDua catatan sebelum masuk detail. Dokumen vLLM menyatakan jelas bahwa disaggregated prefill tidak meningkatkan throughput, dan fitur ini masih experimental serta bisa berubah. Push connector juga baru berusia tiga hari saat saya menulis ini, jadi anggap semua di bawah sebagai desain untuk dievaluasi, bukan default yang sudah mapan.
Apa yang berubah di push: daftar lebih awal, tulis saat siap
Push mengatur ulang pekerjaan yang sama. Ketika decode mengalokasikan block untuk request remote-prefill, ia langsung mengirim registrasi ke instance prefill sebagai notifikasi NIXL, berisi identitasnya dan lokasi block yang dipesan. Itu terjadi sebelum prefill selesai. Dokumen desain menyebut pesan itu PUSH_REG, dengan payload msgpack berisi request ID, engine decode, host, port, TP size, dan ID block.
| Langkah | Pull (NixlConnector default) | Push (NixlPushConnector) |
|---|---|---|
| Siapa yang memindahkan data | Decode menjalankan NIXL READ setelah prefill | Prefill menjalankan NIXL WRITE ke memori terdaftar milik decode |
| Kapan decode memilih block tujuan | Setelah prefill, begitu router meneruskan metadata sumber | Saat alokasi, didaftarkan ke prefill sebelum prefill selesai |
| Kapan router memilih instance decode | Setelah prefill selesai | Lebih awal, supaya setup bisa tumpang tindih dengan prefill |
| Di mana descriptor work berjalan | Inline di decode worker, di dalam engine loop-nya | Di background thread khusus pada tiap prefill worker |
| Kapan prefill melepas block KV | Setelah decode membacanya, atau saat lease habis | Setelah WRITE selesai, atau saat lease habis; menurut postingan bisa lebih cepat |
Registrasi dan selesainya prefill bisa datang dalam urutan apa pun, dan yang datang kedua memicu penulisan. Dokumen menjelaskan pencocokannya: cocok persis pada request ID dulu, lalu fallback yang membuang suffix acak per engine di belakang ID, karena proxy memberi kedua sisi X-Request-Id yang sama dan suffix itu satu-satunya perbedaan. Notifikasi completion memakai format pull mode, jadi kode accounting di decode tidak berubah.
Penulisan langsung masuk ke memori terdaftar milik decoder, dialamatkan per tensor-parallel rank. Tiap sisi memetakan block ke layout-nya sendiri, sehingga ukuran block prefill dan decode boleh berbeda, dan decode menghitung satu notifikasi completion per rank penulis sebelum mulai.
Ke mana waktu habis: descriptor work dan writer thread
Postingan itu menyalahkan satu biaya spesifik. Di pull mode, descriptor NIXL diposting inline di decode worker, secara sinkron di dalam engine loop-nya, sehingga pekerjaan itu menahan pembuatan token. Itu juga sebabnya postingan melaporkan perbaikan TPOT dan bukan hanya TTFT: decode worker mengerjakan urusan transfer di sela token.
Di push mode, tiap prefill worker, satu per TP rank, menjalankan thread bernama nixl-push-writer. Thread ini membuat atau memakai ulang koneksi, memetakan ID block ke descriptor, menggabungkan block yang berurutan, lalu mengirim WRITE. Dokumen desain menyebut thread tidur pada threading.Event, dibangunkan oleh registrasi baru atau block selesai, oleh setiap langkah get_finished, dan oleh callback handshake, serta polling sendiri tiap 1,0 ms selama block selesai menunggu registrasi yang belum datang.
Ada pull request terbuka, nomor 45211, yang menerapkan offload yang sama ke pull mode dengan memindahkan pengiriman READ ke thread nixl-pull-reader. Penulisnya melaporkan mean TTFT turun dari 215,88 ms ke 133,46 ms pada Qwen3-32B dengan input 2.048 token di 8 QPS. Menurut bacaan saya, offload thread bisa dipisahkan dari protokol push, jadi sebagian keuntungan push mungkin bisa dicapai tanpa push. Itu kesimpulan saya sendiri, dan beban kerjanya berbeda: input 2k di sana, 16k di sini. PR-nya masih terbuka dan bertanda needs-rebase saat saya membacanya.
Apakah selisih sisanya berasal dari registrasi yang tumpang tindih atau dari threading tidak bisa dipisahkan oleh angka yang dipublikasikan. Postingan tidak memuat ablation, dan saya tidak akan berpura-pura memuatnya.
Angka yang dipublikasikan vLLM
Pengujian memakai Qwen3-32B dengan topologi 1P1D di node p5en.48xlarge, masing-masing 8 GPU H200 dan 16 interface EFA 200 Gb/s, dalam satu availability zone, dengan vllm 0.29.0 dan nixl 1.4.1. Decode berjalan dengan prefix caching dimatikan sehingga setiap request mengambil seluruh KV cache-nya, kasus terberat untuk jalur transfer. Sweep mencakup input 1k sampai 16k token, 1 sampai 8 QPS, dan 128 token output.
| Metrik | Pull | Push | Selisih |
|---|---|---|---|
| TTFT, input 16k, 8 QPS | sekitar 1.744 ms | sekitar 540 ms | sekitar 3x lebih rendah (3,2x menurut hitungan saya) |
| TPOT, input 16k, 4 sampai 8 QPS | sekitar 10,4 ms | sekitar 7 ms | hingga sekitar 30% lebih rendah |
| TPOT, beban ringan | sekitar 5,6 sampai 5,8 ms | sekitar 5,6 sampai 5,8 ms | tidak ada beda berarti |
| TTFT, beban rendah dan prompt pendek | hampir identik | hampir identik | jangan berharap ada keuntungan |
Baca tabelnya untuk bentuknya, bukan hanya angka headline. Keuntungan membesar seiring beban dan panjang prompt, dan hilang saat transfernya kecil. Itu cocok dengan penjelasan berbasis pekerjaan serial: setup inline yang tetap lebih menyakitkan ketika request antre di belakangnya. Postingan itu memuat grafik, bukan tabel, jadi setiap nilai di atas adalah angka perkiraan yang disebut di teksnya.
Ini juga satu model di satu keluarga hardware, dan postingannya sendiri bilang hasil bergantung pada workload dan konfigurasi. Jangan membaca 3x sebagai janji untuk cluster Anda.
Mengaktifkannya: konfigurasi yang berubah
Yang berubah adalah kelas connector di kedua sisi. Baseline pull di bawah mengikuti perintah launch di postingan untuk node prefill dan nilai kv_role dari panduan penggunaan. Untuk push, contoh konfigurasi prefill di postingan menambahkan kv_parallel_size dan memilih backend LIBFABRIC, backend yang dipakai setup EFA di postingan itu.
# Pull baseline, prefill node. Shape follows the post's launch command.
# Set VLLM_NIXL_SIDE_CHANNEL_HOST to this node's IP when P and D are on
# different machines; the default is localhost and the port defaults to 5600.
VLLM_NIXL_SIDE_CHANNEL_PORT=5600 \
vllm serve Qwen/Qwen3-32B --served-model-name Qwen --dtype bfloat16 \
--tensor-parallel-size 8 --port 8100 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_producer"}'
# Pull baseline, decode node. The benchmark turned prefix caching off here
# "to force every request to fetch full KV from prefill".
vllm serve Qwen/Qwen3-32B --served-model-name Qwen --dtype bfloat16 \
--tensor-parallel-size 8 --port 8200 --no-enable-prefix-caching \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_consumer"}'
# Push, prefill node: swap the class, and (as the post's example does)
# add kv_parallel_size and the LIBFABRIC backend used with EFA.
vllm serve Qwen/Qwen3-32B --served-model-name Qwen --dtype bfloat16 \
--tensor-parallel-size 8 --port 8100 \
--kv-transfer-config '{"kv_connector":"NixlPushConnector","kv_role":"kv_producer","kv_parallel_size":2,"kv_connector_extra_config":{"backends":["LIBFABRIC"]}}'
# Push, decode node: same class swap, plus the PREFILL_* coordinates
# (engine id, host, side-channel port, TP and PP size). Take the exact
# JSON from the post's serve_pd.sh; I could not retrieve it.
# Wrong: NixlConnector on one node and NixlPushConnector on the other.
# The post's symptom is a server crash right after the first request.
# Right: the same class on prefill and decode.Saya tidak bisa mengambil serve_pd.sh dari postingan karena URL asetnya mengembalikan halaman 404, jadi saya belum melihat JSON push sisi decode yang persis maupun cara koordinat prefill dilewatkan. Postingan menyebut namanya: PREFILL_ENGINE_ID, PREFILL_KV_HOST, PREFILL_SIDE_CHANNEL_PORT, PREFILL_TP_SIZE, dan PREFILL_PP_SIZE. Ambil launch decode dari skrip itu, bukan dari sketsa saya.
Connector harus sama di kedua sisi. Bagian troubleshooting postingan mencantumkan server crash tepat setelah request pertama sebagai gejala pull di satu node dan push di node lain, dan menyarankan memeriksa PREFILL_TP_SIZE terhadap tensor-parallel size server prefill saat handshake error.
Yang rusak: kegagalan yang hanya diketahui decode lewat timeout
Bagian failure di dokumen desain adalah bagian paling berguna dari seluruh cerita ini, karena menunjukkan bagaimana push mengubah siapa yang menyadari masalah. Di pull, decode yang menggerakkan transfer dan melihatnya gagal. Di push, prefill yang menggerakkan, dan beberapa kegagalan di sisi prefill tidak pernah menghasilkan failed receive di decode.
| Kegagalan | Yang dilakukan connector | Bagaimana decode tahu |
|---|---|---|
| Handshake prefill gagal | Mencatat push_handshake_failed dan membuang request tanpa menandai receive gagal | Hanya saat registration watchdog berbunyi |
| Pengiriman WRITE prefill gagal | Melepas handle, menaikkan failure counter, membuang WRITE | Completion tidak datang; lease watchdog yang menanganinya |
| Handshake atau pengiriman notifikasi decode gagal | Menandai block decode invalid dan melaporkan failed receive | Segera, accounting sama dengan pull |
| Registrasi tidak pernah cocok | Watchdog membuangnya setelah push_registration_timeout dan mencatat warning | Request tetap antre sampai abort path atau timeout pemanggil menggagalkannya |
Timeout watchdog default-nya decoder_kv_blocks_ttl, dan panduan penggunaan menyebutnya 480 detik secara default. Jika kedua default itu berlaku, registrasi yang hilang bisa diam delapan menit sebelum dibuang, sehingga timeout klien Anda adalah pendeteksi kegagalan yang sebenarnya. Mengatur push_registration_timeout secara eksplisit adalah tuas yang jelas; dokumen menyebut namanya tetapi tidak menyarankan nilai, dan saya pun tidak.
Ada juga batas keras. Pipeline parallelism di sisi decode tidak didukung karena completion dihitung per consumer rank, hybrid Mamba dan SSM tidak didukung di bawah PP, dan packed PP push mensyaratkan cache MLA serta ukuran block prefill dan decode yang sama.
Kegagalan push di sisi prefill sengaja sunyi. Pasang timeout di sisi klien pada setiap request lewat jalur disaggregated, dan beri alert pada counter vllm:nixl_num_notifications_after_expiry, yang menurut panduan penggunaan menghitung notifikasi terlambat di connector push maupun pull.
Kapan pull masih menang
vLLM tidak menyajikan push sebagai pengganti. Panduannya sendiri: pilih push ketika memilih instance decode lebih awal bisa diterima dan TTFT lebih rendah jadi prioritas, dan pertahankan pull pada tiga situasi.
- Load-aware routing. Pull membiarkan router memilih instance decode setelah prefill, dengan informasi beban terbaru. Push berkomitmen lebih awal, harga dari setup yang tumpang tindih dengan prefill.
- Bidirectional KV transfer. Panduan penggunaan mengaktifkannya dengan bidirectional_kv_xfer pada deployment NixlConnector, dan postingan menunjuk pull untuk itu. Postingan terkait menjelaskan node prefill menjalankan NIXL READ D ke P untuk turn yang sudah dihitung, dan melaporkan TTFT hingga sekitar 1,6x lebih rendah pada Qwen3-32B di 24k token. Saya tidak menemukan yang menyebut push mendukungnya.
- Apa pun yang dikecualikan batas push: pipeline parallelism sisi decode, hybrid SSM di bawah PP, atau ukuran block berbeda dengan packed PP.
Ini tuas yang berbeda untuk TTFT. Push memendekkan jalur yang dilalui KV pada tiap request. Bidirectional transfer menghindari hitung ulang riwayat pada percakapan multi-turn. Produk chat dengan konteks bersama yang panjang dan peringkas batch dengan prompt 16k sekali jalan tidak seharusnya memilih connector yang sama dengan alasan yang sama.
Yang dilakukan hari Senin
Jika Anda menjalankan vLLM disaggregated di jaringan yang mendukung RDMA, uji push sebagai perubahan terkontrol, bukan rollout.
- Pastikan workload Anda mirip pengujian: prompt panjang dan beban berkelanjutan, dengan decode mengambil sebagian besar KV. Jika prompt pendek dan trafik ringan, postingan menyebut jangan berharap ada beda.
- Verifikasi fabric lebih dulu. Di AWS jalankan fi_info -p efa, dan pastikan port side-channel NIXL (5600 secara default) terbuka antar node.
- Ambil baseline pull pada panjang prompt asli Anda dengan skrip di bawah sebelum mengubah apa pun.
- Ganti connector di kedua sisi, pertahankan model, proxy, dan trafik yang sama, lalu bandingkan TTFT dan TPOT pada beberapa request rate, bukan satu.
- Atur timeout klien dan alert untuk kegagalan sunyi di atas, dan siapkan konfigurasi pull jika Anda butuh load-aware routing.
# Step 2: is the fabric there, and is the NIXL side channel reachable?
fi_info -p efa | head
nc -zv "$PREFILL_IP" 5600
# Step 5: late notifications on either connector (name from the usage guide).
curl -s "http://$DECODE_IP:8200/metrics" | grep nixl_num_notifications_after_expiry# probe_ttft.py - rough time to first streamed chunk through the toy proxy.
import json
import random
import time
import urllib.request
URL = "http://localhost:8192/v1/completions" # toy proxy from the usage guide
MODEL = "Qwen" # --served-model-name in the post's launch command
N_NUMBERS = 5000 # tune until usage.prompt_tokens is near 16k
RUNS = 5
def ttft_ms() -> float:
# Random content: a repeated prompt would hit the prefill prefix cache
# and flatter both modes.
prompt = " ".join(str(random.randint(0, 99999)) for _ in range(N_NUMBERS))
body = json.dumps(
{"model": MODEL, "prompt": prompt, "max_tokens": 128, "stream": True}
).encode()
req = urllib.request.Request(
URL, data=body, headers={"Content-Type": "application/json"}
)
start = time.perf_counter()
with urllib.request.urlopen(req, timeout=120) as resp:
for line in resp:
if line.startswith(b"data: ") and b"[DONE]" not in line:
return (time.perf_counter() - start) * 1000
raise RuntimeError("stream ended without a data chunk")
samples = sorted(ttft_ms() for _ in range(RUNS))
print("TTFT ms:", [round(s) for s in samples], "median:", round(samples[RUNS // 2]))Skripnya adalah probe kasar untuk waktu ke chunk streaming pertama lewat toy proxy dari panduan penggunaan, yang di contohnya mendengarkan di 8192. Saya baru memeriksa sintaksnya. Angka acak menghindari prefix-cache hit pada request berulang, dan Anda perlu menyetel jumlah kata sampai request non-streaming melaporkan sekitar 16k prompt token.
Push KV transfer lebih merupakan perubahan penjadwalan daripada perubahan jaringan: beri tahu producer ke mana data akan pergi sebelum datanya ada, dan transfer berhenti antre di belakang prefill. Ukur pada panjang prompt dan rate Anda sendiri, pertahankan pull untuk load-aware routing dan bidirectional transfer, dan anggap kegagalan sunyi di sisi prefill sebagai hal yang hanya tertangkap oleh timeout.
Sumber dan bacaan lanjutan
- vLLM blog: Push-Based KV Transfer to Improve TTFT and TPOT for Disaggregated Inference
- vLLM docs: desain NixlPushConnector
- vLLM docs: panduan penggunaan NixlConnector
- vLLM docs: disaggregated prefilling
- vLLM pull request 45211: offload pengiriman NIXL READ di pull mode
- vLLM blog: bidirectional KV transfer untuk workload agentic multi-turn
- NVIDIA NIXL di GitHub
- AWS docs: Elastic Fabric Adapter

