Embedding Cepat di GPU
Pencarian cepat dan akurat sangat penting bagi seluruh Perplexity, dari Search dan Computer hingga Platform API kami. Di balik layar, pekerjaan berat dilakukan oleh model embedding dan ranking, yang membantu sistem kami mengidentifikasi hasil yang paling relevan untuk kueri tertentu. Kami mencapai status-of-the
Pencarian yang cepat dan akurat sangat penting bagi seluruh Perplexity, mulai dari Search dan Computer hingga Platform API kami. Di balik layar, pekerjaan berat dilakukan oleh model *embedding* dan peringkat, yang membantu sistem kami mengidentifikasi hasil yang paling relevan untuk kueri tertentu. Kami mencapai kualitas dan latensi mutakhir dengan melatih dan menyajikan model kami sendiri, seperti pplx-embed.
Artikel ini menyajikan pandangan mendalam tentang infrastruktur penyajian Perplexity untuk kelas khusus model ini. Kami membahas teknik kami untuk secara efisien mengatasi kebutuhan inferensi pencarian asli AI, memungkinkan pembuatan prototipe dan evaluasi model yang cepat sekaligus mendukung indeks pencarian berskala eksabita kami. Teknik-teknik ini secara kolektif memperluas batasan Pareto dari kualitas dan efisiensi pencarian, memungkinkan kami melayani agen dan pengguna dengan hasil terbaik pada biaya dan latensi terendah.
Embedding untuk Pencarian
Dalam pengaturan pencarian tipikal, dokumen yang diindeks dipetakan ke ruang vektor berdimensi tinggi menggunakan model *embedding* dan disimpan dalam basis data vektor. Dengan menyematkan kueri menggunakan model yang sama, dokumen serupa dapat ditemukan dengan menemukan vektor yang paling dekat dengan kueri tersebut. Hal ini memunculkan dua pola lalu lintas yang berbeda untuk dilayani oleh mesin inferensi:
- *Embedding* Batch: saat membangun, memperluas, atau mengindeks ulang basis data, dokumen massal harus di-*embed* ke dalam ruang vektor, memaksimalkan *throughput* untuk meminimalkan biaya.
Mengikuti pencarian vektor, sekumpulan besar dokumen harus dinilai, menciptakan keseimbangan antara *throughput* dan latensi.
- *Embedding* Daring: saat membuat kueri basis data, kueri pendek harus di-*embed* untuk pencarian, meminimalkan latensi.
Kami membangun infrastruktur inferensi kami untuk memanfaatkan sebanyak mungkin komponen umum di berbagai kasus penggunaan. Karena kami biasanya menggunakan model Transformer kecil untuk menghasilkan *embedding*, kami membagikan sebagian besar implementasi dengan kode inferensi LLM kami: *batch embedding* mirip dengan *prefill* yang terikat komputasi, sedangkan *embedding* daring, yang sering kali berjalan pada beberapa token, secara komputasi mirip dengan *decode* yang terikat memori. Oleh karena itu, kami menggunakan kembali kernel prefill dan decode yang dioptimalkan untuk melayani model *embedding*. Hasilnya, kami dapat mencapai *throughput* inferensi batch besar dengan pekerjaan teknik tambahan minimal, sambil mempertahankan latensi rendah untuk beban kerja *embedding* daring.
Tulip, Rose, dan beberapa Ivy
Kami mengekspos inferensi melalui API standar, baik secara internal maupun eksternal melalui Platform API kami. Di balik layar, beberapa layanan terlibat dalam pemrosesan permintaan *embedding*:
- Ivy adalah gateway HTTP Rust yang dipanggil oleh layanan Perplexity.
Ini menangani pekerjaan sisi CPU untuk permintaan seperti penguraian JSON, tokenisasi, pembuatan templat masukan, dan pemecahan batch, menerjemahkan permintaan ke protokol gRPC kustom untuk server hilir. Pemisahan ini memungkinkan kami mengonfigurasi parameter tertentu seputar tokenisasi dan pemformatan masukan tanpa harus menyentuh instans inferensi yang lebih berat.
- Tulip adalah antarmuka server inferensi.
Ini adalah server gRPC yang diimplementasikan dengan Rust, tokio, dan `tonic`. Tulip menerima permintaan inferensi gRPC, menangani penjadwalan dan *batching*. Kemudian mengirimkan batch ke mesin ROSE, mengembalikan respons yang lengkap ke klien.
- **ROSE** (Runtime-Optimized Serving Engine) mengimplementasikan inferensi model.
Ini terutama didefinisikan dalam Python, menyediakan kernel, lapisan, dan definisi untuk berbagai macam model. ROSE mengimplementasikan *forward pass* melalui model, juga menyediakan manajemen grafik CUDA yang dikhususkan untuk *embedding*. Ini dijembatani ke Tulip melalui fungsi step(), yang mengambil batch dan mengembalikan referensi ke komputasi yang dilakukannya pada akselerator.

Memperhatikan Lebih Jauh dari Kernel
Baik model berbasis Transformer dan arsitektur Hopper/Blackwell yang mendasarinya adalah teknologi yang matang, sehingga inferensi *embedding* di sisi GPU telah menyatu ke implementasi yang sebagian besar optimal di berbagai mesin inferensi. Meskipun demikian, kami menemukan peluang tambahan untuk peningkatan pada *runtime* dan *harness* yang mengekspos model secara *end-to-end* ke klien. Khususnya, kami menemukan bahwa kami dapat meningkatkan latensi dengan mengelola grafik CUDA secara cermat dan membangun abstraksi LazyTensor untuk melacak hasil di sisi GPU secara asinkron dalam mesin Rust asli. Kami mengimplementasikan fitur-fitur ini di Tulip, sehingga dapat berinteraksi secara efektif dengan implementasi model ROSE.
Tulip
Kami merancang Tulip agar menjadi antarmuka yang ringan di atas penyajian model kami. Ini menangani permintaan yang masuk dalam tugas asinkron Tokio, mempertahankan kumpulan permintaan yang dilacaknya dan menjadwalkan batch untuk dikirim ke akselerator. Mekanisme penjadwalan di Tulip sangat sederhana: permintaan terakumulasi saat Tulip sedang mengirimkan pekerjaan atau menunggu hasil. Dari permintaan yang terakumulasi, sekuens dipilih berdasarkan siapa yang datang pertama, dilayani terlebih dahulu untuk dijalankan melalui model.
Mekanisme penjadwalan yang sederhana dimotivasi oleh pengamatan pada performa model. Untuk model *embedding* kecil, pada panjang sekuens yang kami layani, kami menyadari bahwa biaya linier dari lapisan padat lebih dominan daripada biaya kuadrat dari *attention*. Dengan demikian, latensi sebagian besar sebanding dengan jumlah token, bukan jumlah sekuens. Akibatnya, begitu sebuah batch cukup besar untuk menjenuhkan GPU, yaitu sekitar 512 token pada model di bawah satu miliar parameter, mengemas lebih banyak sekuens ke dalamnya tidak meningkatkan efisiensi.
Untuk berinteraksi secara efektif dengan model, Tulip mengandalkan grafik CUDA dan pelacakan hasil lambat (*lazy*) untuk menumpang tindih pekerjaan GPU dan CPU serta memanfaatkan sumber daya yang tersedia sepenuhnya.
Manajemen Grafik CUDA
Menjalankan *forward pass* dari sebuah model melibatkan pekerjaan di sisi CPU dan sisi GPU. CPU bertanggung jawab untuk menjadwalkan batch dan meluncurkan kernel dengan parameter yang sesuai, sementara GPU mengeksekusi perkalian matriks, *attention*, norm, atau kernel aktivasi yang relevan. Untuk beban kerja *throughput* tinggi seperti pelatihan dan pengindeksan ulang, *overhead* sisi CPU dapat diabaikan karena ukuran batch dan latensi sisi GPU sama-sama besar. Namun, pada ukuran batch yang lebih kecil, pekerjaan sisi CPU dapat lebih besar daripada pekerjaan sisi GPU.

Untuk memitigasi *overhead*, alih-alih meluncurkan kernel independen, grafik CUDA dapat dibangun untuk menangkap metadata yang diperlukan guna meluncurkan semua kernel dari *forward pass* dengan satu panggilan tunggal ke driver CUDA. Ini menghilangkan kebutuhan untuk menjalankan kembali kode Python dan PyTorch yang mahal untuk konfigurasi grafik CUDA yang dapat ditangkap.
Di setiap model, kami melacak titik infleksi, menentukan jumlah minimum token di mana eksekusi GPU lebih mahal daripada peluncuran kernel di sisi CPU. Karena model *embedding* berukuran kecil, kami mengamati bahwa titik infleksi ini terjadi pada batch ribuan token dan puluhan sekuens. Beberapa implementasi *attention* mengandalkan masukan sisi *host* yang dinamis untuk mengonfigurasi peluncuran kernel, mencegah grafik CUDA *prefill* / padat model penuh. Kami melakukan upstream perubahan pada kernel terkait untuk mengaktifkannya di mesin inferensi kami.
Untuk mengatasi *overhead*, kami membangun grafik CUDA model-penuh untuk semua model *embedding* dan menumpang tindih pekerjaan CPU dengan pekerjaan GPU. Karena grafik CUDA meminimalkan *overhead* sisi CPU, begitu sebuah grafik diluncurkan, kami memiliki waktu luang untuk memulai dan memasukkan antrean eksekusi batch berikutnya setiap kali tersedia. Hasil dari batch yang tertunda dilacak dengan LazyTensor, yang memungkinkan tugas asinkron di Rust untuk memblokir hingga batch sebelumnya selesai dieksekusi. Grafik CUDA membantu penyajian latensi rendah dengan memastikan bahwa kita tidak tertahan oleh biaya peluncuran kernel dan memfasilitasi penjadwalan yang ditingkatkan dalam kasus *throughput* tinggi karena mereka membebaskan CPU untuk melakukan pekerjaan pada batch berikutnya lebih cepat.

Grafik CUDA harus ditangkap untuk setiap konfigurasi yang berbeda, yang untuk *embedding* berarti satu grafik per kombinasi jumlah sekuens dan jumlah token. Karena kisi ini luas, kami mengisi jumlah token ke dalam ember yang merupakan kelipatan 64 atau 256. Ini masih menghasilkan ribuan grafik yang mungkin membutuhkan waktu beberapa menit untuk ditangkap untuk model tipikal. Biaya penangkapan berasal dari dua sumber: *eager forward pass* yang harus dieksekusi untuk mengompilasi kernel dan menyiapkan buffer untuk berbagai kernel yang membutuhkannya, diikuti oleh proses penangkapan yang mengeksekusi ulang kode Python.
Kami memitigasi biaya permulaan dengan menangkap grafik CUDA secara lambat (*lazy*) saat mesin melayani. Kami melacak setiap konfigurasi dan memastikan bahwa konfigurasi tersebut melalui uji pemanasan yang rajin (*eager*) sebelum memicu penangkapan dan pemutaran ulang grafik pada hit kedua. Semua eksekusi berikutnya dari konfigurasi grafik yang sama kemudian melalui pemutaran ulang grafik CUDA. Penangkapan grafik lambat berdampak pada latensi p99 selama permulaan; namun, ini sangat berharga dalam menyebarkan pekerjaan rajin selama beberapa menit ke dalam beberapa waktu. Waktu permulaan yang lebih cepat memungkinkan kami menskalakan dan mengelola penerapan *embedding* dengan lebih baik.
Lazy Tensor
Melalui CUDA, pekerjaan GPU bersifat asinkron. Karena meluncurkan kernel secara asinkron memasukkannya ke dalam antrean pada aliran, kode *host* harus disinkronkan secara eksplisit untuk membaca vektor yang dihasilkan. Untuk memfasilitasi tingkat paralelisme yang lebih tinggi dan dapat memulai batch masa depan sambil menunggu yang sebelumnya selesai pada perangkat, kami mengandalkan abstraksi LazyTensor untuk melacak nilai.
LazyTensor melacak buffer *host* dalam memori yang dikunci halaman dan operasi cudaMemcpyAsync melalui peristiwa penyalinan data dari perangkat. Ini dimulai setelah peluncuran *forward pass* pada aliran yang sama. Karena operasi penyalinan harus menunggu semua kernel sebelumnya pada aliran untuk dieksekusi, peristiwa terkait melacak penyelesaian *forward pass* dan ketersediaan hasil pada CPU.

Kami memanfaatkan LazyTensor di mesin *encoder* ROSE kami untuk tumpang tindih pekerjaan GPU dan CPU. Alih-alih setiap panggilan step() menjalankan grafik CUDA dan menunggu hingga selesai, step() mengembalikan LazyTensor untuk melacak hasilnya secara asinkron. Dipasangkan dengan grafik CUDA, ini membantu kami mencapai latensi rendah dan *throughput* yang lebih baik.

ROSE
Kami mengadaptasi mesin ROSE kami, yang awalnya kami bangun untuk penyajian LLM, untuk juga menangani eksekusi model *embedding*. Untuk meminimalkan upaya yang diperlukan untuk mendukung model *embedding*, ROSE secara agresif menggunakan kembali kode antara LLM dan *embedding*. Sebagai contoh, penyajian pplx-embed dan *dekode* LLM Qwen3.5 semuanya melewati kernel yang sama. Pembagian ini memungkinkan kami untuk dengan mudah melayani model *embedding* yang awalnya disetel secara halus dari LLM untuk pembuatan prototipe, evaluasi, dan inferensi produksi.
Untuk lapisan padat, inferensi *embedding* dan LLM identik karena vektor token diproses secara independen. Dalam lapisan *attention*, perbedaan ditangani dengan menambahkan dukungan untuk masukan tidak beraturan (*ragged*), di samping pengaturan *prefill* dan *decode* bertingkat (*paged*) yang diperlukan oleh LLM. Saat menyajikan model *embedding*, kami tidak membuat tembolok KV dan mendistribusikan ke variasi kernel *attention* yang mendukung format tidak beraturan untuk menghindari padding. Konversi pendukung dan rutin kalibrasi juga dibagikan dengan LLM.
Ivy
Ivy, lapisan proksi HTTP inferensi kami, juga memainkan peran penting dalam performa. Karena muatan permintaan bervariasi dalam produksi, merutekan permintaan individu ke replika individu dapat menyebabkan ketidakseimbangan beban. Ivy memecah permintaan batch besar menjadi beberapa bagian dan menyeimbangkan beban di antara replika, meningkatkan pemanfaatan dan meratakan latensi. Pekerjaan terbaru kami pada tokenisasi unigram buatan sendiri, yang diluncurkan sepenuhnya di Ivy, secara drastis meningkatkan latensi dibandingkan dengan tokenizer siap pakai.
...tetapi Kernel Tetap Penting
ROSE mendukung berbagai backend *attention*. Kernel yang berbeda mungkin cocok untuk ukuran masalah tertentu. Seiring waktu, kami mengintegrasikan kernel FlashInfer 2, FlashInfer 3, and FlashAttention 4 untuk mengimplementasikan *ragged attention*.

Secara umum, kami mengamati bahwa FlashAttention 4 lebih cepat. Namun, FlashInfer 3 mengunggulinya pada model berbasis Qwen pada panjang sekuens yang sangat panjang. Karena performa dan penyetelan dapat bervariasi dengan jumlah dan dimensi kepala *attention*, kami mempertahankan dukungan untuk berbagai konfigurasi dan membuat keputusan kasus per kasus saat melayani.
Tolok Ukur
Kami melakukan tolok ukur terhadap vLLM v0.22.0, menjalankan inferensi pada presisi BF16 pada bobot model aktual dan masukan yang berasal dari kumpulan data evaluasi. Semua uji waktu didahului oleh uji pemanasan yang memverifikasi bahwa divergensi dalam kemiripan kosinus berada dalam 0,1%.
*Embedding* Latensi Rendah (p50 / p90 / p99 / maks ms)
Kami melaporkan *runtime* untuk ukuran batch permintaan yang telah di-tokenisasi sebelumnya 1, permintaan berurutan sepenuhnya, panjang sekuens 128, 512, dan 4096 token.

Penilaian Latensi Rendah (p50 / p90 / p99 / maks ms)
Ukuran batch permintaan yang telah di-tokenisasi sebelumnya 5, 25, dan 50, panjang sekuens 512 token.

*Embedding* Throughput Tinggi (emb/d)
Ukuran batch permintaan 100, empat proses konkuren yang mengirimkan permintaan, panjang sekuens 512, 1024, dan 4096 token.

*Embedding* Konurensi Tinggi (p50 / p90 / p99 / maks ms)
Panjang sekuens 512, ukuran batch 1, tetapi kami mengirimkan 1, 2, 4, 8, dan 16 permintaan konkuren. Tolok ukur ini juga mencakup biaya tokenisasi melalui Ivy, beserta overhead jaringan antara Ivy dan Tulip.

Kesimpulan dan Pekerjaan Masa Depan
Infrastruktur penyajian yang terdiri dari Ivy, Tulip, dan ROSE memungkinkan kami menyajikan *embedding* untuk Perplexity dengan latensi yang lebih rendah dan *throughput* yang lebih baik, menghasilkan pencarian yang lebih akurat dengan biaya yang lebih rendah dibandingkan dengan solusi siap pakai.
Dengan berfokus pada model tertentu dan mengambil kepemilikan seluruh *stack*, kami mendapatkan kebebasan yang diperlukan untuk mencapai keseimbangan yang efektif antara performa dan fleksibilitas, mencampurkan primitif Rust yang sangat dapat digunakan kembali dan berkinerja tinggi bersama dengan kode pemodelan Python yang lebih generik. Banyak mesin inferensi sumber terbuka, seperti vLLM, SGLang, dan TokenSpeed, mengintegrasikan bahasa seperti Rust dan C++ ke dalam *stack* mereka. Kami telah berinvestasi dalam Rust selama dua tahun terakhir dan telah memetik hasil yang luar biasa dalam hal performa dan kemudahan pemeliharaan. Dengan membagikan sebagian besar implementasi *embedding* dengan *stack* penyajian LLM kami, kami juga memperoleh peningkatan *throughput*, tanpa memerlukan upaya teknik yang signifikan untuk dihabiskan pada pemeliharaan model *embedding*.
Seiring berkembangnya model, kami akan terus meningkatkan setiap lapisan *stack* kami untuk mengurangi latensi yang terikat CPU maupun GPU. Protokol berbasis gRPC kustom kami di dalam Ivy and Tulip memungkinkan kami mengubah komunikasi untuk mengurangi latensi jaringan, sementara ROSE menyediakan fondasi untuk meningkatkan *throughput* komputasi. Selain itu, seiring berkembangnya dukungan untuk Python berulir bebas di seluruh ekosistem, kami akan dapat lebih meningkatkan interoperabilitas Python-Rust untuk mengurangi *overhead*.