Artikel

Pramuat dan Dekode Terpisah

Partikel data geometris berkecepatan tinggi, melesat melewati

Untuk menghasilkan token output dari sebuah input prompt, inferensi LLM dibagi menjadi dua tahap: prefill dan decode. Prefill berjalan pada token input, mengisi cache KV, sebelum memasuki tahap decode yang menghasilkan token satu per satu.

Sementara satu langkah decode biasanya berjalan selama puluhan milidetik, prefill memakan waktu lebih lama. Jika dijalankan pada perangkat yang sama, mencampur prefill dengan decode akan mengurangi kinerja decode. Dalam artikel ini kami menjelajahi solusi yang telah ada dalam bentuk prefill dan decode yang disetujui, menjalankannya pada perangkat terpisah untuk memaksimalkan throughput prefill dan latensi decode.

Kinerja Prefill vs Decode

Dalam mesin penyajian LLM khas, penjadwal batch memilih permintaan untuk diproses pada setiap langkah eksekusi model. Ketika berjalan pada satu perangkat atau node, permintaan prefill dan decode digabungkan. Biaya perhatian, yang mengakumulasi sepanjang panjang urutan, meningkat untuk prefill dan decode, sebanding dengan panjang entri dalam cache KV (kv_len). Permintaan decode biasanya meneruskan satu token (qo_len=1), dengan biaya minimal melalui lapisan lain yang beroperasi secara independen pada token urutan. Permintaan prefill meneruskan ribuan atau puluhan ribu token dengan biaya signifikan melalui lapisan padat (besar qo_len).

Latensi dari sebuah lintasan maju lebih dipengaruhi oleh jumlah token independen yang dilewati lapisan padat (qo_len) dibandingkan jumlah token yang diambil dari cache KV selama perhatian (kv_len). Perhatian dapat memparalelkan jumlah permintaan dan kv_len sebanding dengan panjang urutan, mencapai pemanfaatan yang baik. Prefill terikat pada komputasi: qo_len yang tinggi, kernel GEMM dapat mengalokasikan blok yang cukup sepanjang dimensi M untuk sepenuhnya memanfaatkan kemampuan komputasi GPU modern. Decode terikat memori: karena ukuran batch yang biasanya rendah, jumlah input sepanjang M biasanya kecil, hanya cukup untuk satu blok. Sementara kernel GEMM Split-K dapat meningkatkan pemanfaatan SM untuk ukuran batch token rendah, cache dan unit perkalian matriks biasanya tetap kurang dimanfaatkan.

Ketika dicampur bersama, batch yang berisi permintaan untuk prefill menimbulkan latensi lebih tinggi melalui lintasan maju, yang secara negatif mempengaruhi throughput decode seluruh instance. Sementara mencampur permintaan prefill dengan permintaan decode atau menggunakan prefill yang dipotong dapat sedikit meningkatkan kinerja decode, sulit untuk mempertahankan throughput prefill yang memadai untuk memproses cukup permintaan pada instance untuk memaksimalkan throughput decode. Dalam kasus model besar, dengan panjang output yang khas, untuk mempertahankan ukuran batch besar untuk decode, prefill harus dilakukan cukup sering yang secara signifikan mengurangi latensi rata-rata dan menyebabkan stutter dalam output.

Masalah-masalah ini dapat diatasi dengan menggunakan satu set node terpisah untuk melakukan prefill dan decode. Dengan mengasosiasikan node prefiller dengan banyak node decoder, permintaan yang cukup dapat dijadwalkan untuk prefill untuk memaksimalkan throughput dan mempertahankan jumlah permintaan bersamaan yang cukup pada node decoder untuk juga memaksimalkan throughput decode. Node prefiller mengisi cache KV, yang kemudian dipindahkan ke node decoder. Karena decoder tidak lagi harus berhenti untuk prefill, latensi menjadi jauh lebih deterministik, karena dampak keseluruhan dari peningkatan kv_len dari permintaan aktif jauh lebih tidak menonjol. Biayanya dibayar dengan peningkatan Waktu ke Token Pertama (TTFT), karena transfer cache KV melalui jaringan dapat memakan waktu puluhan hingga ratusan milidetik.

Pengirim KV

Di Perplexity, implementasi kami untuk prefill dan decode yang disetujui dibangun di sekitar pengirim KV yang berinteraksi dengan mesin LLM untuk mengatur pemindahan cache KV dari node prefiller ke node decoder melalui jaringan. Di sisi prefiller, messenger menerima permintaan dari node decoder, menyerahkannya kepada penjadwal batch dan melacak eksekusi lintasan maju untuk mengirimkan cache KV dengan latensi seminimal mungkin. Di sisi decoder, setelah halaman yang tidak dapat diusir dialokasikan, messenger memblokir permintaan dari dijadwalkan untuk decode sampai diberitahukan bahwa transfer cache KV dan konteks decoder selesai.

Memisahkan prefill memerlukan koneksi throughput tinggi, latensi rendah, sehingga implementasi kami disesuaikan untuk RDMA, mendukung baik Pengontrol Antarmuka Jaringan EFA dan ConnectX (NIC). Pengirim KV dibangun di atas libfabric, menggunakan pembungkus fabric-lib kami untuk menyediakan abstraksi latensi rendah tingkat tinggi di atas primitif Akses Memori Langsung Jarak Jauh (RDMA), mengimplementasikan transfer halaman dan metadata yang efisien, bersama dengan sinyal latensi rendah. Di latar belakang, fabric-lib mengoordinasikan GPU dan NIC yang langsung terhubung untuk menyalin data dari node prefiller ke node decoder.

Setelah diterima, node prefiller mengalokasikan serangkaian halaman sumber KV dan menjadwalkan permintaan untuk prefill menggunakan mesinnya sendiri. Untuk meminimalkan latensi, transfer tidak menunggu lintasan maju: sebagai gantinya, salinan halaman KV dimulai segera setelah model selesai menambahkan entri cache KV ke cache KV untuk layer individual. Karena permintaan prefill dapat dipotong, penjadwal batch memberi tahu pengirim KV tentang potongan yang saat ini dijadwalkan sebelum eksekusi. Untuk mendukung grafik CUDA sambil dapat melacak layer, messenger menjaga utas khusus yang terus-menerus memantau penghitung yang bertambah setelah proyeksi output dari perhatian. Penghitung hanya dipertahankan pada node utama dalam lingkungan yang dibagi bersama: meskipun entri cache KV tetap valid setelah ditambahkan dan sebelum perhatian, proyeksi output dikurangi melintasi peringkat, secara implisit menyinkronkan mereka. Setelah perubahan dalam penghitung terdeteksi, messenger diberitahu dan memanggil fabric-lib untuk memulai transfer satu layer.

Setelah transfer potongan terakhir selesai, metadata tambahan juga disalin: decoding spekulatif atau MTP memerlukan perpindahan logit dan status tersembunyi ke decoder. Salinan ini juga dilakukan melalui RDMA, ke dan dari buffer yang sudah dialokasikan sebelumnya.

Setelah penyelesaian semua transfer yang tertunda dari potongan terakhir, node prefiller membatalkan pengalokasian halaman KV dan melengkapi permintaan. Node decoder tidak diberitahu secara eksplisit: sebaliknya, ia menggunakan penghitung segera untuk melacak jumlah operasi yang telah selesai. Jumlah operasi RDMA di sisi prefiller sebanding dengan jumlah halaman yang ditransfer. Setelah penyelesaian jumlah halaman dan salinan konteks yang diketahui, fabric-lib memanggil pengirim KV untuk menunjukkan bahwa sebuah permintaan siap untuk decoding. Messenger membatalkan alokasi konteks apapun dan menyerahkan permintaan tersebut kepada mesin LLM.

Transfer Cache KV Terpecah

Jika prefiller dan decoder mengandalkan Paralelisme Tensor (TP) dan membagikan atau mereplikasi cache KV secara identik, satu mesin transfer mengoordinasikan beberapa perangkat untuk mengirim dan menerima halaman dari semua replika. Untuk dapat menggunakan satu pengirim dan mesin transfer meskipun eksekutor model direplikasi di beberapa perangkat dan proses, cuMem dan cuMemImportFromShareableHandle digunakan untuk mengalokasikan memori perangkat yang mendukung cache KV dan memetakannya ke dalam proses utama. Mesin transfer memeriksa topologi node untuk menemukan NIC dan CPU di node NUMA terdekat untuk digunakan dalam transfer setiap bagian dari cache KV.

Jika sumber dan tujuan terpisah secara identik, transfer adalah sepele karena ada pemetaan satu-ke-satu dari perangkat dan halaman sumber dan tujuan. Dalam situasi ini, pemisahan secara implisit membantu latensi transfer: dengan menggunakan lebih banyak GPU, lebih banyak NIC yang terkait dapat digunakan, mendekati pemanfaatan bandwidth penuh. Namun, jika ada ketidakcocokan, mesin transfer harus memecah atau merekonstruksi halaman tergantung pada rasio antara bagian sumber dan tujuan.

Jika prefiller membagi cache KV di lebih banyak perangkat, halaman penuh direkonstruksi pada decoder dengan mengirimkan bagian yang sesuai dari perangkat prefiller. Jika decoder memiliki lebih banyak bagian, ia menerima halaman dari beberapa sumber. Decoder perlu mengetahui skema pembagian dari prefiller untuk dapat menghitung jumlah penulisan RDMA yang diharapkan diterimanya. Jika replikasi terlibat, prefiller mengelompokkan perangkat ke dalam set replika yang mereplikasi cache KV penuh di dalamnya. Set replika tujuan secara acak ditugaskan satu set sumber untuk menggunakan semua perangkat yang tersedia untuk memulai penulisan RDMA.

Transfer yang terpecah memerlukan penyesuaian kecil pada cache KV. Secara default, FlashInfer mengandalkan tata letak NHD, yang menyusun token dalam halaman dalam kepala. Karena cache kemungkinan besar terpecah di sepanjang jumlah kepala perhatian, ini menciptakan diskontinuitas dalam kepala. Transfer RDMA tidak secara implisit mendukung penulisan berpola, memerlukan satu operasi per kepala untuk melakukan transfer. Sebaliknya, untuk mengurangi jumlah interaksi dengan libfabric, kami menyusun ulang cache KV menggunakan tata letak HND yang menempatkan dimensi kepala sebelum jumlah token. Ini memastikan kontinuitas, memungkinkan halaman untuk disalin dengan sekali penulisan.

Decoding Spekulatif

Decoding spekulatif memerlukan sedikit penyesuaian pada prefill-decode yang disetujui. Dalam implementasi kami, node prefiller tidak diperbolehkan untuk mengambil sampel token. Karena model Sonar Perplexity mendukung keluaran terstruktur, kami tidak ingin menimbulkan kompleksitas dalam menyinkronkan implementasi prosesor skema di antara prefiller dan decoder. Dalam mekanisme MTP dan decoding spekulatif, prefill model draf hingga token terakhir melibatkan pengambilan sampel token dari model target.

Untuk mengatasi masalah ini, prefill tidak menyertakan token terakhir dari urutan input. Sebagai gantinya, status tersembunyi atau logit dari prefill sebelum token terakhir dipindahkan dan diperlakukan sebagai token decode pada langkah berikutnya di decoder. Meskipun ini sedikit meningkatkan latensi, karena langkah decode penuh harus dilakukan setelah prefill untuk mengeluarkan token pertama, kompleksitas implementasi sangat berkurang.

Pengaturan Deployment Terpisah

Kami telah mengerahkan atau bereksperimen dengan beberapa konfigurasi terpisah dengan model yang berbeda, untuk mendukung lalu lintas produksi atau beban kerja evaluasi internal. Berdasarkan ukuran dan mekanisme perhatian model, kami memilih skema pemisahan yang cocok untuk node prefiller dan decoder untuk memanfaatkan GPU secara optimal.

DeepSeek-R1

Dengan DeepSeek, kami mempertimbangkan baik penyebaran Paralel Tensor (TP) dan Paralel Data (DP). Seperti yang dibahas dalam blog sebelumnya, penyebaran TP memberikan latensi yang lebih baik dengan biaya throughput yang lebih rendah, membutuhkan lebih banyak GPU untuk melayani lalu lintas berat. Penyebaran DP meningkatkan skala lebih baik dengan beban, namun throughput puncaknya lebih rendah karena biaya komunikasi antar perangkat atau antar node.

DeepSeek mengandalkan Perhatian Laten Multi-Kepala, yang memampatkan cache KV. Karena semua kepala KV dipadatkan menjadi vektor laten tunggal, TP tidak dapat membagi cache KV, tetapi harus mereplikasi vektor laten di semua peringkat. Pemisahan terjadi setelah dekompresi, karena setiap peringkat dapat mengekstrak kepala yang berbeda dari representasi laten yang sama. Konsekuensinya, semua pemisahan cache KV identik di antara pemisahan cache prefiller dan decoder.

Dengan pengaturan TP intranode, baik prefiller dan decoder dibagi identik. Transfer dikirim dari semua peringkat untuk sepenuhnya memanfaatkan semua NIC yang tersedia. Namun, dengan penyebaran DP, di mana ukuran peringkat TP lebih rendah atau setiap peringkat DP ditugaskan ke satu GPU, perangkat prefiller mana pun yang memiliki salinan cache KV yang direplikasi dapat mengirimkannya. Untuk menyeimbangkan permintaan di seluruh NIC yang tersedia, kami secara acak memilih GPU dan NIC untuk mengirim cache KV dari prefiller ke decoder.

Dengan prefill-decode campuran, penyebaran R1 kami berjuang untuk secara konsisten melebihi 50 TPS karena seringnya gangguan prefill dalam urutan ratusan milidetik. Sebaliknya, dengan memisahkan prefill, kami menerima penalti sekitar 100 ms ke TTFT untuk setiap permintaan, tetapi satu node prefiller dapat mempertahankan ukuran batch yang konsisten di 3 node decoder, memberikan throughput lebih dari 90 TPS sambil menangani beban sekitar 1 QPS per node decoder. Dengan penyebaran paralel data, TPS sedikit lebih rendah sekitar 50, tetapi instance dapat menangani beban 1 QPS per peringkat, dengan 8 peringkat untuk satu node.

Qwen3-Coder

Model 480B ini menggunakan Perhatian Kuery Tergrup (GQA), jadi perhatian dapat dengan mudah dibagi dan dapat memanfaatkan paralelisme tensor tanpa mengorbankan memori untuk cache KV. Akibatnya, kami dapat membagi model di seluruh 8 GPU untuk kedua prefill dan decode, memasangkan sekitar 3 node decoder dengan satu node prefiller. Karena perhatian dibagi, kami mengandalkan tata letak cache KV HND untuk membagi cache KV prefiller dan decoder, memasangkan peringkat prefiller dengan peringkat decoder dan sepenuhnya memanfaatkan semua NIC untuk mentransfer potongan secara paralel.

Tertarik membentuk masa depan Platform API kami? Kami sedang membuka lowongan.

Bergabunglah dengan komunitas pengembang kami untuk tetap mendapatkan informasi terbaru tentang rilis, fitur, dan pembaruan.

Tertarik membentuk masa depan Platform API kami? Kami sedang membuka lowongan.

Bergabunglah dengan komunitas pengembang kami untuk tetap mendapatkan informasi terbaru tentang rilis, fitur, dan pembaruan.

Tertarik membentuk masa depan Platform API kami? Kami sedang membuka lowongan.

Bergabunglah dengan komunitas pengembang kami untuk tetap mendapatkan informasi terbaru tentang rilis, fitur, dan pembaruan.