Greitos įdėtys GPU
Greita ir tikslu paieška yra gyvybiškai svarbi visai "Perplexity" – nuo paieškos ir kompiuterio iki mūsų API platformos. Užkulisiuose sunkų darbą atlieka įdėčių ir reitingavimo modeliai, kurie padeda mūsų sistemoms nustatyti tinkamiausius rezultatus pagal pateiktą užklausą. Aukščiausią kokybę pasiekiame
Greita ir tikslu paieška yra gyvybiškai svarbi visai "Perplexity" – nuo paieškos ir kompiuterio iki mūsų API platformos. Užkulisiuose sunkų darbą atlieka įdėčių ir reitingavimo modeliai, kurie padeda mūsų sistemoms nustatyti tinkamiausius rezultatus pagal pateiktą užklausą. Aukščiausią kokybę ir mažiausią delsą pasiekiame patys treniruodami ir teikdami savo modelius, tokius kaip pplx-embed.
Šiame straipsnyje pateikiamas "Perplexity" teikimo infrastruktūros šiai specialiai modelių klasei vaizdas "iš arti". Aptariame savo metodus, skirtus veiksmingai spręsti dirbtinio intelekto gimtosios paieškos inferencijos poreikius, leidžiančius greitai kurti modelių prototipus ir juos vertinti, kartu palaikant mūsų egsabitų masto paieškos indeksą. Šie metodai kartu išplečia paieškos kokybės ir efektyvumo "Pareto" ribą, todėl galime aptarnauti agentus ir vartotojus pateikdami geriausius įmanomus rezultatus su mažiausiomis sąnaudomis ir delsos laiku.
Įdėtys paieškai
Tipinėje paieškos sąrankoje indeksuoti dokumentai susiejami su didelių dimensijų vektorių erdve naudojant įdėties modelį ir saugomi vektorių duomenų bazėje. Įdedant užklausą naudojant tą patį modelį, panašius dokumentus galima rasti surandant vektorius, artimiausius užklausos vektoriui. Dėl to inferencijos varikliui tenka aptarnauti du skirtingus srauto modelius:
- Paketinė įdėtis (Batch Embedding): kuriant, plečiant arba iš naujo indeksuojant duomenų bazę, dideli dokumentų kiekiai turi būti įdėti į vektorių erdvę, maksimaliai padidinant pralaidumą, kad būtų sumažintos išlaidos.
Po vektorių paieškos dideli dokumentų paketai turi būti įvertinti balais, išlaikant balansą tarp pralaidumo ir delsos.
- Internetinė įdėtis (Online Embedding): užklausos duomenų bazei metu trumpa užklausa turi būti įdėta paieškai, sumažinant delsą.
Sukūrėme inferencijos infrastruktūrą, kad ji išnaudotų kuo daugiau bendrų komponentų įvairiais naudojimo atvejais. Kadangi įdėtims generuoti paprastai naudojame nedidelius "Transformer" modelius, didžiąją realizacijos dalį bendriname su LLM inferencijos kodu: paketinės įdėtys (batch embeddings) yra panašios į skaičiavimams imlų išankstinį užpildymą (prefill), o internetinės įdėtys, kurios dažnai veikia su keliais žetonais, skaičiavimo prasme yra panašios į atmintiai imlų dekodavimą (decode). Todėl pakartotinai naudojame savo optimizuotus išankstinio užpildymo ir dekodavimo branduolius, kad galėtume teikti įdėčių modelius. Dėl to galime pasiekti didžiulį paketinės inferencijos pralaidumą su minimaliomis papildomomis inžinerinėmis sąnaudomis, išlaikydami mažą delsą internetinių įdėčių darbo krūviams.
Tulipai, rožės ir šiek tiek gebenės
Inferenciją viešiname per standartizuotas API, tiek viduje, tiek išorėje per mūsų API platformą. Užkulisiuose įdėties užklausos apdorojime dalyvauja kelios paslaugos:
- Ivy yra "Rust" HTTP šliuzas (gateway), į kurį kreipiasi "Perplexity" paslaugos.
Ji tvarko CPU pusės darbą užklausoms, tokioms kaip JSON analizė, žetonizavimas, įvesties šablonų sudarymas ir paketų padalijimas, versdama užklausas į pasirinktinį gRPC protokolą pasroviui esantiems serveriams. Šis atskyrimas leidžia mums sukonfiguruoti tam tikrus parametrus, susijusius su žetonizavimu ir įvesties formatavimu, nereikalauja liesti sunkesnių inferencijos egzempliorių.
- Tulip yra inferencijos serverio sąsaja.
Tai gRPC serveris, realizuotas su "Rust", tokio ir `tonic`. "Tulip" gauna gRPC inferencijos užklausas, tvarkydama planavimą ir paketavimą. Tada ji siunčia paketus į ROSE variklį, grąžindama užbaigtus atsakymus klientams.
- **ROSE** (Runtime-Optimized Serving Engine) realizuoja modelio inferenciją.
Jis daugiausia apibrėžtas Python, teikiant branduolius (kernels), sluoksnius ir apibrėžimus įvairiems modeliams. ROSE realizuoja tiesioginius praėjimus (forward passes) per modelius, taip pat teikia CUDA grafikų valdymą, specializuotą įdėtims (embeddings). Su "Tulip" jis sujungiamas per step() funkciją, kuri paima paketą ir grąžina nuorodą į skaičiavimą, atliekamą greitintuve.

Kreipiant dėmesį į tai, kas už branduolio ribų
Tiek "Transformer" pagrindu sukurti modeliai, tiek apatinio lygio "Hopper/Blackwell" architektūros yra brandžios technologijos, todėl inferencijos integravimas GPU pusėje susiformavo į didžiąja dalimi optimalų įgyvendinimą įvairiuose inferencijos varikliuose. Nepaisant to, radome papildomų galimybių pagerinti vykdymo laiką ir valdymo priemones (harnesses), kurios atskleidžia modelius nuo pradžios iki galo kliento pusei. Visų pirma nustatėme, kad galime pagerinti delsmą kruopščiai valdydami CUDA grafikus ir sukurdami LazyTensor abstrakciją, skirtą asinkroniškai sekti GPU pusėje gaunamą rezultatą gimtajame "Rust" variklyje. Šias funkcijas įdiegėme "Tulip", kad ji galėtų efektyviai sąveikauti su ROSE modelių realizacijomis.
Tulip
Sukūrėme "Tulip", kad ji būtų kuo lengvesnė modelio teikimo sąsaja. Ji apdoroja gaunamas užklausas "Tokio" asinkroninėse užduotyse, palaikydama užklausų telkinį, kurį stebi, ir planuoja paketus išsiuntimui į greitintuvą. Planavimo mechanizmas "Tulip" yra labai paprastas: užklausos kaupiasi, kol "Tulip" siunčia darbą arba laukia rezultatų. Iš sukauptų užklausų sekos renkamos pirmumo tvarka ("kas pirmesnis, tas gudresnis"), kad būtų vykdomos per modelį.
Paprastą planavimo mechanizmą motyvuoja modelio našumo stebėjimas. Mažiesiems įdėčių modeliams, esant mūsų aptarnaujamiems sekų ilgiams, pastebėjome, kad linijinės tankiųjų sluoksnių sąnaudos dominuoja virš kvadratinių dėmesio sąnaudų. Todėl delsa iš esmės yra proporcinga žetonų skaičiui, o ne sekų skaičiui. Vadinasi, kai paketas yra pakankamai didelis, kad prisotintų GPU (tai yra apie 512 žetonų mažesniame nei vieno milijardo parametrų modelyje), daugiau sekų į jį pakavimas nepagerina efektyvumo.
Kad efektyviai sąveikautų su modeliu, "Tulip" remiasi CUDA grafikais ir tingiuoju rezultatų sekimu, kad persidengtų GPU ir CPU darbai ir pilnai išnaudotų turimus išteklius.
CUDA grafikų valdymas
Modelio tiesioginis praėjimas apima tiek CPU pusės, tiek GPU pusės darbą. CPU yra atsakingas už paketų planavimą ir branduolių paleidimą su atitinkamais parametrais, o GPU vykdo atitinkamus matricos daugybos, dėmesio, normos arba aktyvavimo branduolius. Didelio pralaidumo darbo krūviams, tokiams kaip mokymas ir pakartotinis indeksavimas, CPU pusės pridėtinė apyvarta yra nereikšminga, nes paketų dydžiai ir GPU pusės delsa yra dideli. Tačiau esant mažesniems paketų dydžiams, CPU pusės darbas gali nusverti GPU pusės darbą.

Siekiant sušvelninti pridėtinę apyvartą, vietoj nepriklausomų branduolių paleidimo galima sukurti CUDA grafiką, kuris fiksuotų metaduomenis, reikalingus visiems tiesioginio praėjimo branduoliams paleisti vienu CUDA tvarkyklės (driver) iškvietimu. Tai pašalina poreikį iš naujo vykdyti brangų Python ir PyTorch kodą konfigūracijoms, kurioms galima užfiksuoti CUDA grafikus.
Kiekviename modelyje sekame lūžio tašką, nustatydami minimalų žetonų skaičių, ties kuriuo GPU vykdymas tampa brangesnis nei CPU pusės branduolio paleidimas. Kadangi įdėčių modeliai yra maži, pastebime, kad šis lūžio taškas pasiekiamas esant tūkstančių žetonų ir dešimčių sekų paketams. Kai kurios dėmesio realizacijos remiasi dinaminiais pagrindinio kompiuterio pusės įvesties duomenimis branduolio paleidimams sukonfigūruoti, o tai neleidžia naudoti viso modelio išankstinio užpildymo / tankiųjų CUDA grafikų. Mes patobulinome (upstreamed) atitinkamų branduolių pakeitimus, kad juos būtų galima įjungti mūsų inferencijos variklyje.
Siekdami spręsti pridėtinės apyvartos problemas, visiems įdėčių modeliams kuriame viso modelio CUDA grafikus ir persidengiame CPU darbą su GPU darbu. Kadangi CUDA grafikai sumažina CPU pusės pridėtinę apyvarta, paleidus grafiką turime laisvo laiko inicijuoti ir įtraukti į eilę kito paketo vykdymą, kai tik jis tampa prieinamas. Laukiančio paketo rezultatai sekami naudojant LazyTensor, kuris leidžia asinkroninei užduočiai "Rust" blokuoti, kol baigiasi ankstesnio paketo vykdymas. CUDA grafikai padeda mažos delsos teikimui, užtikrindami, kad mūsų nestabdytų branduolių paleidimo kaina, ir palengvina geresnį planavimą didelio pralaidumo atveju, nes atlaisvina CPU atlikti darbą su kitu paketu anksčiau.

CUDA grafika turi būti užfiksuota kiekvienai skirtingai konfigūracijai, o tai įdėtims reiškia po grafiką kiekvienam sekų skaičiaus ir žetonų skaičiaus deriniui. Kadangi šis tinklelis yra didelis, žetonų skaičių suapvaliname iki kibirų (buckets), kurie yra 64 arba 256 kartotiniai. Dėl to vis tiek gaunasi tūkstančiai grafikų, kurių fiksavimas tipiniam modeliui gali užtrukti kelias minutes. Fiksavimo kaina kyla iš dviejų šaltinių: sparčiojo vykdymo tiesioginio praėjimo, kuris turi būti įvykdytas norint sukompiliuoti branduolius ir nustatyti buferius įvairiems jiems reikalingiems branduoliams, po kurio seka fiksavimo vykdymas, iš naujo vykdantis Python kodą.
Pradinės išlaidos sušvelninamos tingiai fiksuojant CUDA grafikus, kai variklis teikia paslaugas. Sekame kiekvieną konfigūraciją ir užtikriname, kad prieš inicijuojant grafiko fiksavimą ir atkūrimą antrojo užklausos metu ji pereitų sparčiojo vykdymo apšilimo paleidimą. Visi tolesni to paties grafiko konfigūracijos vykdimai tada vyksta per CUDA grafiko atkūrimą. Tingusis grafiko fiksavimas turi įtakos p99 delsai paleidimo metu; tačiau jis naudingas paskirstant kelių minučių spartiojo vykdymo darbą per kelias valandas. Greitesnis paleidimo laikas leidžia mums geriau mastelkeisti ir valdyti įdėčių diegimus.
Tingieji tenzoriai (Lazy Tensors)
Per CUDA GPU darbas yra asinkroninis. Kadangi branduolio paleidimas asinkroniškai įtraukia jį į eilę sraute, pagrindinio kompiuterio kodas turi aiškiai sinchronizuoti, kad nuskaitytų gaunamus vektorius. Siekdami palengvinti didesnį lygiagretumą ir suteikti galimybę paleisti būsimus paketus laukiant, kol ankstesnis baigsis įrenginyje, reikšmėms sekti pasikliaujame LazyTensor abstrakcija.
LazyTensor seka pagrindinio kompiuterio buferį puslapyje užfiksuotoje atmintyje ir cudaMemcpyAsync operaciją per įvykį, nukopijuojantį duomenis iš įrenginio. Jis paleidžiamas po tiesioginio praėjimo paleidimo tame pačiame sraute. Kadangi kopijavimo operacija turi palaukti, kol bus įvykdyti visi ankstesni srauto branduoliai, susijęs įvykis seka tiek tiesioginio praėjimo užbaigimą, tiek rezultato prieinamumą CPU.

Savo ROSE kodavimo variklyje naudojame LazyTensor, kad persidengtų GPU ir CPU darbai. Vietoj to, kad kiekvienas step() iškvietimas vykdytų CUDA grafiką ir lauktų jo pabaigos, step() grąžina LazyTensor, kuris asinkroniškai seka jo rezultatą. Kartu su CUDA grafikais tai padeda mums pasiekti mažą delsą ir geresnį pralaidumą.

ROSE
Pritaikėme savo ROSE variklį, kurį iš pradžių sukūrėme LLM teikimui, kad jis taip pat atliktų įdėčių modelių vykdymą. Siekiant sumažinti pastangas, reikalingas įdėčių modeliams palaikyti, ROSE agresyviai pakartotinai naudoja kodą tarp LLM ir įdėčių. Pavyzdžiui, pplx-embed teikimas ir Qwen3.5 LLM dekodavimas – viskas vyksta per tuos pačius branduolius. Šis bendrinimas leidžia mums lengvai teikti įdėčių modelį, kuris iš pradžių buvo patikslintas (fine-tuned) iš LLM prototipų kūrimui, vertinimui ir gamybinei inferencijai.
Tankiesiems sluoksniams įdėčių ir LLM inferencija yra identiška, nes žetonų vektoriai apdorojami nepriklausomai. Dėmesio sluoksniuose skirtumai sprendžiami pridedant palaikymą netolygiai paskirstytoms įvestims (ragged inputs), kartu su LLM reikalaujamomis suskaidytomis (paged) išankstinio užpildymo ir dekodavimo sąrankomis. Teikdami įdėčių modelį, nekuriame KV spartiosios atminties egzemplioriaus ir persiunčiame į dėmesio branduolių variantus, kurie palaiko netolygų formatą, kad būtų išvengta papildymo (padding). Palaikomos konversijos ir kalibravimo procedūros taip pat yra bendros su LLM.
Ivy
"Ivy", mūsų inferencijos HTTP tarpinio serverio (proxy) sluoksnis, taip pat vaidina svarbų vaidmenį užtikrinant našumą. Kadangi gamyboje užklausų naudingieji duomenys skiriasi, atskirų užklausų nukreipimas į atskiras kopijas (replicas) gali sukelti apkrovos nesubalansavimą. "Ivy" padalija didelių paketų užklausas į dalis ir subalansuoja jų apkrovą tarp kopijų, taip pagerindama išnaudojimą ir sušvelnindama delsą. Neseniai "Ivy" visiškai įdiegtas darbas prie nuosavo vienaradžių (unigram) žetonizavimo drastiškai pagerina delsą lyginant su paruoštais naudoti žetonizatoriais.
...tačiau branduoliai vis tiek svarbūs
ROSE palaiko įvairius dėmesio foninius elementus (backends). Skirtingi branduoliai gali būti pritaikyti konkretiems uždavinio dydžiams. Laikui bėgant integravome "FlashInfer 2", "FlashInfer 3" ir "FlashAttention 4" branduolius, kad realizuotume netolygų dėmesį (ragged attention).

Apskritai pastebime, kad "FlashAttention 4" veikia greičiau. Tačiau "FlashInfer 3" lenkia jį naudojant Qwen pagrindu sukurtus modelius esant labai ilgoms sekoms. Kadangi našumas ir derinimas gali skirtis priklausomai nuo dėmesio (attention) galvučių skaičiaus ir dimensijos, teikiame palaikymą kelioms konfigūracijoms ir teikdami paslaugas sprendimą priimame kiekvienu atveju atskirai.
Etaloniniai tyrimai (Benchmarks)
Atliekame palyginimą su vLLM v0.22.0, vykdydami inferenciją BF16 tikslumu su tikrais modelio svoriais ir įvestimis, gautomis iš vertinimo duomenų rinkinių. Prieš visus laiko matavimo paleidimus buvo atliekami apšilimo paleidimai, kuriais buvo patikrinta, ar kosinusinio panašumo skirtumas neviršija 0,1 %.
Mažos delsos įdėtys (p50 / p90 / p99 / maks. ms)
Pateikiame vykdymo laiką iš anksto žetonizuotam 1 dydžio užklausų paketui, visiškai nuoseklioms užklausoms, 128, 512 ir 4096 žetonų sekų ilgiams.

Mažos delsos vertinimas (p50 / p90 / p99 / maks. ms)
Iš anksto žetonizuoti užklausų paketų dydžiai 5, 25 ir 50, sekos ilgis 512 žetonų.

Didelio pralaidumo įdėtys (emb/s)
Užklausų paketo dydis 100, keturi vienu metu veikiantys procesai, teikiantys užklausas, 512, 1024 ir 4096 žetonų sekų ilgiai.

Didelio konkurentabilumo įdėtys (p50 / p90 / p99 / maks. ms)
Sekos ilgis 512, paketo dydis 1, tačiau siunčiame 1, 2, 4, 8 ir 16 vienalaikių užklausų. Šis etalonas (benchmark) taip pat apima žetonizavimo sąnaudas per "Ivy", kartu su tinklo pridėtine apyvarta (overhead) tarp "Ivy" ir "Tulip".

Išvados ir būsimi darbai
Iš "Ivy", "Tulip" ir ROSE sudaryta teikimo infrastruktūra leidžia teikti "Perplexity" įdėtis su mažesne delsa ir geresniu pralaidu, todėl paieška tampa tikslesnė, o sąnaudos mažesnės lyginant su paruoštais naudoti sprendimais.
Sutelkdami dėmesį į konkrečius modelius ir perimdami viso steko valdymą, įgyjam laisvę, kurios reikia norint rasti veiksmingą balansą tarp našumo ir lankstumo, derinant labai pakartotinai naudojamus ir našius "Rust" primityvus su bendresniu Python modeliavimo kodu. Daugelis atvirojo kodo inferencijos variklių, tokių kaip vLLM, SGLang ir TokenSpeed, į savo steką integravimo tokias kalbas kaip "Rust" ir C++. Per pastaruosius dvejus metus investavome į "Rust" ir sulaukėme didelės naudos tiek našumo, tiek priežiūros srityse. Bendrindami didžiąją dalį įdėčių realizacijos su mūsų LLM teikimo steku, taip pat padidiname pralaidumą, nereikalaudami skirti didelių inžinerinių išteklių įdėčių modelių priežiūrai.
Modeliams tobulėjant, ir toliau tobulinsime kiekvieną savo steko sluoksnį, kad sumažintume tiek CPU, tiek GPU apribotą delsą. Mūsų pasirinktiniai gRPC pagrindu sukurti protokolai "Ivy" ir "Tulip" viduje leidžia koreguoti ryšį, kad būtų sumažinta tinklo delsa, o ROSE suteikia pagrindą skaičiavimo pralaidumui pagerinti. Be to, augant laisvųjų gijų (free-threaded) Python palaikymui visoje ekosistemoje, galėsime dar labiau pagerinti Python ir "Rust" sąveiką, kad sumažintume pridėtinę apyvartą.