Ātras iegulšanas uz GPU
Ātra un precīza meklēšana ir vitāli svarīga visam Perplexity, no Search un Computer līdz mūsu API platformai. Aizkulisēs smago darbu veic iegulšanas un rangu noteikšanas modeļi, kas palīdz mūsu sistēmām identificēt visatbilstošākos rezultātus dotajam vaicājumam. Mēs sasniedzam vismodernāko
Ātra un precīza meklēšana ir vitāli svarīga visam Perplexity — no Search un Computer līdz mūsu API platformai. Aizkulisēs smago darbu veic iegulšanas un rangu noteikšanas modeļi, kas palīdz mūsu sistēmām identificēt visatbilstošākos rezultātus dotajam vaicājumam. Mēs sasniedzam vismodernāko kvalitāti un aizturi, apmācot un apkalpojot savus modeļus, piemēram, pplx-embed.
Šajā rakstā ir sngts ieskats Perplexity apkalpošanas infrastruktūrā šai īpašajai modeļu klasei. Mēs apspriežam savas metodes, lai efektīvi risinātu ar AI saistītās meklēšanas secinājumu vajadzības, nodrošinot ātru prototipu izveidi un modeļu novērtēšanu, vienlaikus darbinot mūsu eksabaitu mēroga meklēšanas indeksu. Šīs metodes kopumā paplašina meklēšanas kvalitātes un efektivitātes Pareto robežu, ļaujot mums apkalpot aģentus un lietotājus ar labākajiem iespējamiem rezultātiem par zemākajām izmaksām un aizturi.
Iegulšanas meklēšanai
Tipiskā meklēšanas iestatījumā indeksēti dokumenti tiek kartēti uz augstas dimensijas vektortelpu, izmantojot iegulšanas modeli, un glabāti vektoru datubāzē. Iegulstot vaicājumu, izmantojot to pašu modeli, līdzīgus dokumentus var atrast, atrodot vektorus, kas ir vistuvākie vaicājuma vektoram. Tas rada divus dažādus satiksmes modeļus secinājumu dzinēja apkalpošanai:
- Pakešu iegulšana (Batch Embedding): veidojot, paplašinot vai reindeksējot datubāzi, vairumdokuementi ir jāiegulst vektortelpā, maksimāli palielinot caurlaidspēju, lai samazinātu izmaksas.
Pēc vektoru meklēšanas liela apjoma dokumentu pakas ir jānovērtē (scored), panākot līdzsvaru starp caurlaidspēju un aizturi.
- Tiešsaistes iegulšana (Online Embedding): veicot vaicājumu datubāzei, ir jāiegulst īss vaicājums meklēšanai, samazinot aizturi līdz minimumam.
Mēs izveidojām savu secinājumu infrastruktūru, lai maksimāli izmantotu kopīgos komponentus dažādos lietošanas gadījumos. Tā kā mēs parasti izmantojam mazus Transformer modeļus, lai iegūtu iegulšanas, mēs kopīgojam lielāko daļu ieviešanas ar mūsu LLM secinājumu kodu: pakešu iegulšanas ir līdzīgas ar skaitļošanu saistītai sagatavošanai (prefill), savukārt tiešsaistes iegulšanas, kas bieži darbojas ar dažiem žetoniem, skaitļošanas ziņā ir līdzīgas ar atmiņu saistītai dekodēšanai. Tāpēc mēs atkārtoti izmantojam mūsu optimizētos sagatavošanas un dekodēšanas kodolus, lai apkalpotu iegulšanas modeļus. Rezultātā mēs varam sasniegt milzīgu pakešu secinājumu caurlaidspēju ar minimālu papildu inženiertehnisko darbu, vienlaikus saglabājot zemu aizturi tiešsaistes iegulšanas darba slodzēm.
Tulipes, rozes un nedaudz Ivy
Mēs nodrošinām secinājumus, izmantojot standartizētas API, gan iekšēji, gan ārēji, izmantojot mūsu API platformu. Aizkulisēs iegulšanas pieprasījuma apstrādē ir iesaistīti vairāki pakalpojumi:
- Ivy ir Rust HTTP vārteja, ko izsauc Perplexity pakalpojumi.
Tas apstrādā CPU puses darbu tādiem pieprasījumiem kā JSON parsēšana, tokenizācija, ievades sagatavošana un pakešu sadalīšana, tulkojot pieprasījumus uz pielāgotu gRPC protokolu pakārtotajiem serveriem. Šī nošķiršana ļauj mums konfigurēt noteiktus parametrus ap to tokenizāciju un ievades formatēšanu, nebūtiski skarot smagākas secinājumu instances.
- Tulip ir secinājumu servera saskarne.
Tas ir gRPC serveris, kas ieviests, izmantojot Rust, tokio un `tonic`. Tulip saņem gRPC secinājumu pieprasījumus, apstrādājot plānošanu un pakešu veidošanu. Pēc tam tas nosūta pakas uz ROSE dzinēju, atgriežot pabeigtās atbildes klientiem.
- **ROSE** (Runtime-Optimized Serving Engine) īsteno modeļa secinājumu (inference).
Tas galvenokārt ir definēts Python, nodrošinot kodolus (kernels), slāņus un definīcijas visdažādākajiem modeļiem. ROSE īsteno modeļu pārsūtīšanas gaitu (forward passes), nodrošinot arī CUDA grafiku pārvaldību, kas specializēta iegulšanai. Tas ir savienots ar Tulip, izmantojot step() funkciju, kas saņem paketi un atgriež atsauci uz aprēķinu, ko tā veic uz paātrinātāja.

Pievēršot uzmanību ārpus kodola
Gan uz Transformer balstīti modeļi, gan pamatā esošās Hopper/Blackwell arhitektūras ir nobriedušas tehnoloģijas, tāpēc iegulšanas (embedding) secinājumi GPU pusē ir nonākuši pie galvenokārt optimālas ieviešanas dažādos secinājumu dzinējos. Tomēr mēs atklājām papildu iespējas uzlabojumiem izpildes laikā un rīkos, kas nodrošina modeļu tiešu saskarsmi ar klientu no sākuma līdz galam. Konkrēti, mēs atklājām, ka varam uzlabot aizturi, rūpīgi pārvaldot CUDA grafikus un izveidojot LazyTensor abstrakciju, lai asinhroni izsekotu GPU puses rezultātam vietējā Rust dzinējā. Mēs ieviesām šīs funkcijas Tulip, lai tas varētu efektīvi mijiedarboties ar ROSE modeļu implementācijām.
Tulip
Mēs izstrādājām Tulip tā, lai tā būtu pēc iespējas vieglāka saskarne virs mūsu modeļu apkalpošanas. Tas apstrādā ienākošos pieprasījumus Tokio asinhronajos uzdevumos, uzturot pieprasījumu baseinu, ko tas izseko un no kura plāno pakas nosūtīšanai uz paātrinātāju. Plānošanas mehānisms Tulip ir ļoti vienkāršs: pieprasījumi uzkrājas, kamēr Tulip nosūta darbu vai gaida rezultātus. No uzkrātajiem pieprasījumiem secības tiek atlasītas rindas kārtībā (first-come, first-served), lai tās palaistu caur modeli.
Vienkāršo plānošanas mehānismu motivē novērojums par modeļa veiktspēju. Maziem iegulšanas modeļiem pie apkalpotajiem secību garumiem mēs pamanījām, ka blīvo slāņu lineārās izmaksas dominē pār uzmanības kvadrātiskajām izmaksām. Tādējādi aizture galvenokārt ir proporcionāla žetonu skaitam, nevis secību skaitam. Līdz ar to, tiklīdz pakete ir pietiekami liela, lai piesātinātu GPU (kas ir ap 512 žetoniem modelī ar mazāk nekā miljardu parametru), vairāku secību ievietošana tajā neuzlabo efektivitāti.
Lai efektīvi mijiedarbotos ar modeli, Tulip paļaujas uz CUDA grafikiem un slinku rezultātu izsekošanu, lai pārklātu GPU un CPU darbu un pilnībā izmantotu pieejamos resursus.
CUDA grafiku pārvaldība
Modeļa pārsūtīšanas gaitas izpilde ietver gan CPU puses, gan GPU puses darbu. CPU ir atbildīgs par pakešu plānošanu un kodolu palaišanu ar atbilstošiem parametriem, savukārt GPU izpilda attiecīgos matrica reizināšanas, uzmanības, normas vai aktivizācijas kodolus. Augstas caurlaidspējas darba slodzēm, piemēram, apmācībai un reindeksēšanai, CPU puses izmaksas ir niecīgas, jo gan pakešu lielums, gan GPU puses aizture ir liela. Tomēr mazākiem pakešu lielumiem CPU puses darbs var pārsniegt GPU puses darbu.

Lai mazinātu pieskaitāmās izmaksas, tā vietā, lai palaistu neatkarīgus kodolus, var izveidot CUDA grafiku, lai uztvertu metadatus, kas nepieciešami visu pārsūtīšanas gaitas kodolu palaišanai ar vienu izsaukumu uz CUDA draiveri. Tas novērš nepieciešamību atkārtoti palaist dārgu Python un PyTorch kodu tām konfigurācijām, kurām var uztvert CUDA grafikus.
Katram modelim mēs izsekojam inflexijas punktu, nosakot minimālo žetonu skaitu, kurā GPU izpilde ir dārgāka nekā CPU puses kodola palaišana. Tā kā iegulšanas modeļi ir mazi, mēs novērojam, ka šis inflexijas punkts rodas pie tūkstošiem žetonu un desmitiem secību pakešiem. Dažas uzmanības ieviešanas paļaujas uz dinamiskiem resursdatora puses ievades datiem, lai konfigurētu kodolu palaišanu, novēršot pilna modeļa sagatavošanas/blīvos CUDA grafikus. Mēs augšupstraumējām (upstreamed) izmaiņas attiecīgajos kodolos, lai iespējotu tos mūsu secinājumu dzinējā.
Lai novērstu pieskaitāmās izmaksas, mēs izveidojam pilna modeļa CUDA grafikus visiem iegulšanas modeļiem un pārklājam CPU darbu ar GPU darbu. Tā kā CUDA grafiki samazina CPU puses pieskaitāmās izmaksas, kad grafiks ir palaists, mums ir brīvs laiks, lai palaistu un pievienotu rindai nākamās paketes izpildi, tiklīdz tā ir pieejama. Gaidāmās paketes rezultāti tiek izsekoti ar LazyTensor, kas ļauj asinhronajam uzdevumam Rust bloķēt, līdz iepriekšējā pakete pabeidz izpildi. CUDA grafiki palīdz zemas aiztures apkalpošanā, nodrošinot, ka mūs nebremzē kodolu palaišanas izmaksas, un veicina uzlabotu plānošanu augstas caurlaidspējas gadījumā, jo tie atbrīvo CPU, lai agrāk paveiktu darbu pie nākamās paketes.

CUDA grafiki ir jāuztver katrai atsevišķai konfigurācijai, kas iegulšanai nozīmē grafiku katrai secību skaita un žetonu skaita kombinācijai. Tā kā šis režģis ir plašs, mēs papildinām žetonu skaitu līdz blokiem (buckets), kas ir 64 vai 256 reizinājumi. Tas joprojām rada tūkstošiem grafiku, kuru uztveršana tipiskam modelim var aizņemt vairākas minūtes. Uztveršanas izmaksas rodas no diviem avotiem: eager pārsūtīšanas gaitas, kas ir jāizpilda, lai apkopotu kodolus un iestatītu buferus dažādiem kodoliem, kuriem tie nepieciešami, kam seko uztveršanas brauciens, kurā atkārtoti tiek izpildīts Python kods.
Mēs mazinām palaišanas izmaksas, slinki uztverot CUDA grafikus, kamēr dzinējs veic apkalpošanu. Mēs izsekojam katru konfigurāciju un nodrošinām, ka tā iziet cauri eager iesildīšanas braucienam, pirms tiek aktivizēta grafika uztveršana un atskaņošana otrajā reizē. Visi nākamie tās pašas grafika konfigurācijas izpildījumi pēc tam iziet cauri CUDA grafika atskaņošanai. Slinkai grafika uztveršanai ir ietekme uz p99 aizturi palaišanas laikā; tomēr tā ir vērtīga, sadalot vairāku minūšu eager darbu vairākās stundās. Ātrāks palaišanas laiks ļauj mums labāk mērogot un pārvaldīt iegulšanas izvietošanu.
Tari (Tensors)
Caur CUDA GPU darbs ir asinhrons. Tā kā kodola palaide asinhroni pievieno to plūsmai, resursdatora kodam ir skaidri jāsinhronizējas, lai nolasītu iegūtos vektorus. Lai veicinātu augstāku paralēlisma pakāpi un spētu palaist turpmākās pakas, gaidot iepriekšējās paketes pabeigšanu ierīcē, mēs paļaujamies uz LazyTensor abstrakciju, lai izsekotu vērtībām.
LazyTensor izseko resursdatora buferi lapu bloķētā atmiņā un cudaMemcpyAsync operāciju, izmantojot notikumu, kas kopē datus no ierīces. Tas tiek palaists pēc pārsūtīšanas gaitas palaišanas tajā pašā plūsmā. Tā kā kopēšanas operācijai ir jāgaida, kamēr tiek izpildīti visi iepriekšējie plūsmas kodoli, saistītais notikums izseko gan pārsūtīšanas gaitas pabeigšanu, gan rezultāta pieejamību CPU.

Mēs izmantojam LazyTensor objektus mūsu ROSE kodētāja dzinējā, lai pārklātu GPU un CPU darbu. Tā vietā, lai katrs step() izsaukums palaistu CUDA grafiku un gaidītu tā pabeigšanu, step() atgriež LazyTensor, lai asinhroni izsekotu tā rezultātu. Apvienojumā ar CUDA grafikiem tas palīdz mums sasniegt zemu aizturi un labāku caurlaidspēju.

ROSE
Mēs pielāgojām mūsu ROSE dzinēju, ko sākotnēji izveidojām LLM apkalpošanai, lai apstrādātu arī iegulšanas modeļu izpildi. Lai samazinātu pūles, kas nepieciešamas iegulšanas modeļu atbalstam, ROSE agresīvi izmanto kodu starp LLM un iegulšanām. Piemēram, pplx-embed apkalpošana un Qwen3.5 LLM dekodēšana izmanto vienus un tos pašus kodolus. Šī koplietošana ļauj mums viegli apkalpot iegulšanas modeli, kas sākotnēji tika precīzi noregulēts (fine-tuned) no LLM prototipu veidošanai, novērtēšanai un ražošanas secinājumiem.
Blīvajiem slāņiem iegulšanas un LLM secinājumi ir identiski, jo žetonu vektori tiek apstrādāti neatkarīgi. Uzmanības slāņos atšķirības tiek risinātas, pievienojot atbalstu nelīdzenām ievadēm (ragged inputs), kā arī LLM prasītajām lapu sagatavošanas un dekodēšanas iestatījumiem. Apkalpojot iegulšanas modeli, mēs neveidojam KV kešatmiņu un novirzām uz uzmanības kodolu variācijām, kas atbalsta nelīdzeno formātu, lai izvairītos no papildināšanas (padding). Atbilstošās konversijas un kalibrēšanas rutīnas tiek kopīgotas arī ar LLM.
Ivy
Ivy, mūsu secinājumu HTTP starpniekservera (proxy) slānis, arī spēlē svarīgu lomu veiktspējā. Tā kā pieprasījumu datu apjomi ražošanā atšķiras, atsevišķu pieprasījumu maršrutēšana uz atsevišķām kopijām (replicas) var izraisīt slodzes nelīdzsvarotību. Ivy sadala lielo pakešu pieprasījumus segmentos un sabalansē to slodzi starp kopijām, uzlabojot resursu izmantošanu un izlīdzinot aizturi. Mūsu nesenais darbs pie iekšējas unigrammu tokenizācijas, kas pilnībā ieviesta Ivy, krasi uzlabo aizturi salīdzinājumā ar gataviem tokenizatoriem.
...bet kodoliem joprojām ir nozīme
ROSE atbalsta dažādas uzmanības aizmugursistēmas (backends). Dažādi kodoli var būt piemēroti konkrētiem problēmas izmēriem. Laika gaitā mēs integrējām FlashInfer 2, FlashInfer 3 un FlashAttention 4 kodolus, lai ieviestu nelīdzeno uzmanību (ragged attention).

Kopumā mēs novērojam, ka FlashAttention 4 darbojas ātrāk. Tomēr FlashInfer 3 pārspēj to Qwen modeļos ar ļoti garām secību garumiem. Tā kā veiktspēja un regulēšana var mainīties atkarībā no uzmanības (attention) galviņu skaita un dimensijas, mēs saglabājam atbalstu vairākām konfigurācijām un pieņemam lēmumu katrā gadījumā atsevišķi, nodrošinot apkalpošanu.
Etalontesti (Benchmarks)
Mēs veicam etalontestus pret vLLM v0.22.0, veicot secinājumu izpildi ar BF16 precizitāti uz faktiskajiem modeļa svariem un ievadēm, kas iegūtas no novērtēšanas datu kopām. Visiem laika mērījumu braucieniem pirms tam tika veikti iesildīšanas braucieni, kas apliecināja, ka kosinusa līdzības novirze nepārsniedz 0,1%.
Zemas aiztures iegulšanas (p50 / p90 / p99 / max ms)
Mēs ziņojam par izpildes laikiem iepriekš tokenizēta pieprasījuma pakešu lielumam 1, pilnībā secīgiem pieprasījumiem, secību garumiem 128, 512 un 4096 žetoni.

Zemas aiztures vērtēšana (p50 / p90 / p99 / max ms)
Iepriekš tokenizēti pieprasījumu pakešu lielumi 5, 25 un 50, secības garums 512 žetoni.

Augstas caurlaidspējas iegulšanas (emb/s)
Pieprasījumu pakešu lielums 100, četri vienlaicīgi procesi, kas iesniedz pieprasījumus, secību garumi 512, 1024 un 4096 žetoni (tokens).

Augstas vienlaicības iegulšanas (p50 / p90 / p99 / max ms)
Secības garums 512, pakešu lielums 1, bet mēs nosūtām 1, 2, 4, 8 un 16 vienlaicīgus pieprasījumus. Šis etalontests (benchmark) ietver arī tokenizācijas izmaksas, izmantojot Ivy, kā arī tīkla pieskaitāmās izmaksas starp Ivy un Tulip.

Secinājumi un turpmākais darbs
Apkalpošanas infrastruktūra, ko veido Ivy, Tulip un ROSE, ļauj mums apkalpot iegulšanas Perplexity ar zemāku aizturi un labāku caurlaidspēju, nodrošinot precīzāku meklēšanu par samazinātām izmaksām salīdzinājumā ar gataviem risinājumiem.
Koncentrējoties uz konkrētiem modeļiem un uzņemoties atbildību par visu steku, mēs iegūstam brīvību, kas nepieciešama, lai panāktu efektīvu līdzsvaru starp veiktspēju un elastību, apvienojot augsti atkārtoti izmantojamus un veiktspējīgus Rust primitīvus ar vispārīgāku Python modelēšanas kodu. Daudzi atvērtā pirmkoda secinājumu dzinēji, piemēram, vLLM, SGLang un TokenSpeed, savā stekā integrē tādas valodas kā Rust un C++. Mēs esam ieguldījuši Rust pēdējo divu gadu laikā un guvuši lieliskus ieguvumus gan veiktspējas, gan uzturējamības ziņā. Kopīgojot lielāko daļu iegulšanas ieviešanas ar mūsu LLM apkalpošanas stekanu, mēs iegūstam arī caurlaidspējas pieaugumu, neprasot ieguldīt ievērojamus inženiertehniskos spēkus iegulšanas modeļu uzturēšanā.
Modelim attīstoties, mēs turpināsim uzlabot katru mūsu steka slāni, lai samazinātu gan ar CPU, gan ar GPU saistīto aizturi. Mūsu pielāgotie uz gRPC balstītie protokoli Ivy un Tulip ietvaros ļauj mums pielāgot saziņu, lai samazinātu tīkla aizturi, savukārt ROSE nodrošina pamatu skaitļošanas caurlaidspējas uzlabošanai. Turklāt, augot atbalstam brīvplūsmas (free-threaded) Python visā ekosistēmā, mēs varēsim vēl vairāk uzlabot Python-Rust sadarbību, lai samazinātu pieskaitāmās izmaksas.