Skip to main content
BlogAI/ML

vLLM NixlPushConnector: Push vs Pull KV Transfer dan TTFT

Push-based KV transfer di vLLM mendaftarkan block decode lebih awal dan membiarkan prefill menulisnya, memangkas TTFT sekitar 3x di pengujian vLLM sendiri. Bedah mekanisme, angka, failure mode, dan kapan pull masih menang.

Matthews Wong

Software Engineer

Oktober 202612 menit baca
vLLM NixlPushConnector: Push vs Pull KV Transfer dan TTFT

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 token

Dua 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.

LangkahPull (NixlConnector default)Push (NixlPushConnector)
Siapa yang memindahkan dataDecode menjalankan NIXL READ setelah prefillPrefill menjalankan NIXL WRITE ke memori terdaftar milik decode
Kapan decode memilih block tujuanSetelah prefill, begitu router meneruskan metadata sumberSaat alokasi, didaftarkan ke prefill sebelum prefill selesai
Kapan router memilih instance decodeSetelah prefill selesaiLebih awal, supaya setup bisa tumpang tindih dengan prefill
Di mana descriptor work berjalanInline di decode worker, di dalam engine loop-nyaDi background thread khusus pada tiap prefill worker
Kapan prefill melepas block KVSetelah decode membacanya, atau saat lease habisSetelah 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.

MetrikPullPushSelisih
TTFT, input 16k, 8 QPSsekitar 1.744 mssekitar 540 mssekitar 3x lebih rendah (3,2x menurut hitungan saya)
TPOT, input 16k, 4 sampai 8 QPSsekitar 10,4 mssekitar 7 mshingga sekitar 30% lebih rendah
TPOT, beban ringansekitar 5,6 sampai 5,8 mssekitar 5,6 sampai 5,8 mstidak ada beda berarti
TTFT, beban rendah dan prompt pendekhampir identikhampir identikjangan 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.

KegagalanYang dilakukan connectorBagaimana decode tahu
Handshake prefill gagalMencatat push_handshake_failed dan membuang request tanpa menandai receive gagalHanya saat registration watchdog berbunyi
Pengiriman WRITE prefill gagalMelepas handle, menaikkan failure counter, membuang WRITECompletion tidak datang; lease watchdog yang menanganinya
Handshake atau pengiriman notifikasi decode gagalMenandai block decode invalid dan melaporkan failed receiveSegera, accounting sama dengan pull
Registrasi tidak pernah cocokWatchdog membuangnya setelah push_registration_timeout dan mencatat warningRequest 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.

  1. 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.
  2. Verifikasi fabric lebih dulu. Di AWS jalankan fi_info -p efa, dan pastikan port side-channel NIXL (5600 secara default) terbuka antar node.
  3. Ambil baseline pull pada panjang prompt asli Anda dengan skrip di bawah sebelum mengubah apa pun.
  4. Ganti connector di kedua sisi, pertahankan model, proxy, dan trafik yang sama, lalu bandingkan TTFT dan TPOT pada beberapa request rate, bukan satu.
  5. 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.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Apa itu NixlPushConnector di vLLM?

Ini adalah KV connector untuk disaggregated prefill dan decode yang membiarkan instance prefill menulis block KV langsung ke memori terdaftar milik instance decode dengan NIXL WRITE. Connector ini memakai ulang handshake dan setup agent NIXL yang ada, sementara NixlConnector default tetap berbasis pull. vLLM mengumumkannya pada 14 Oktober 2026.

Apa beda push dan pull KV transfer di vLLM?

Di pull mode, decode mengalokasikan block dan menjalankan NIXL READ setelah prefill selesai. Di push mode, decode mendaftarkan block tujuannya ke prefill saat alokasi, dan prefill menjalankan NIXL WRITE begitu registrasi dan prefill sama-sama selesai. Hasilnya, setup di sisi decode tumpang tindih dengan prefill, bukan menyusul sesudahnya.

Seberapa besar push KV transfer menurunkan TTFT?

Pada tes vLLM sendiri dengan Qwen3-32B, satu instance prefill dan satu decode di AWS P5en dengan EFA, TTFT pada 8 QPS dan input 16k token sekitar 1.744 ms dengan pull dan sekitar 540 ms dengan push. Pada beban rendah dan prompt pendek, kedua mode hampir identik. Hasil ini dari satu model dan satu setup hardware, jadi ukur workload Anda sendiri.

Kapan saya sebaiknya tetap memakai pull mode?

Tetap pakai pull jika router perlu memilih instance decode setelah prefill untuk load-aware routing, atau jika Anda memakai bidirectional KV transfer yang didokumentasikan vLLM pada NixlConnector. Push juga belum mendukung pipeline parallelism di sisi decode. Jika prompt pendek dan trafik ringan, push kecil kemungkinan membantu.

Apa yang terjadi jika transfer push gagal?

Sebagian kegagalan di sisi prefill tidak menghasilkan failed receive di decode. Handshake atau WRITE prefill yang gagal dicatat lalu dibuang, dan decode baru tahu saat registration watchdog atau lease-nya habis. Pasang timeout di sisi klien dan pantau counter vllm:nixl_num_notifications_after_expiry.