Brza ugrađivanja na GPU-ovima
Brzo i precizno pretraživanje ključno je za cijeli Perplexity, od pretraživanja i računala do naše API platforme. Iza kulisa, teški poslovi se obavljaju ugrađivanjem i modelima rangiranja, koji pomažu našim sustavima da identificiraju najrelevantnije rezultate za dati upit. Ostvarujemo vrhunsko stanje
Brzo i precizno pretraživanje od vitalne je važnosti za cijeli Perplexity, od pretraživanja i računala do naše API platforme. Iza kulisa, glavni dio posla obavljaju modeli ugrađivanja i rangiranja, koji pomažu našim sustavima da identificiraju najrelevantnije rezultate za dani upit. Vrhunsku kvalitetu i latenciju postižemo treniranjem i posluživanjem vlastitih modela, kao što je pplx-embed.
Ovaj članak predstavlja uvid iza kulisa u Perplexityjevu infrastrukturu posluživanja za ovu posebnu klasu modela. Raspravljamo o našim tehnikama za učinkovito rješavanje potreba inferencije pretraživanja izvornog za AI, omogućujući brzu izradu prototipova i procjenu modela dok pokrećemo naš indeks pretraživanja exabajtne skale. Ove tehnike zajedno proširuju Pareto granicu kvalitete i učinkovitosti pretraživanja, omogućujući nam da poslužujemo agente i korisnike s najboljim mogućim rezultatima uz najmanje troškove i latenciju.
Ugrađivanja za pretraživanje
U tipičnom postavljanju pretraživanja, indeksirani dokumenti se preslikavaju u višedimenzionalni vektorski prostor pomoću modela ugrađivanja i pohranjuju u vektorsku bazu podataka. Ugrađivanjem upita pomoću istog modela, slični se dokumenti mogu pronaći pronalaženjem vektora najbližih onima upita. To dovodi do dva različita uzorka prometa koje stroj za inferenciju mora posluživati:
- Skupno ugrađivanje: prilikom izgradnje, proširivanja ili ponovnog indeksiranja baze podataka, skupni dokumenti moraju biti ugrađeni u vektorski prostor, čime se maksimalno povećava propusnost kako bi se smanjili troškovi.
Nakon vektorskog pretraživanja, velike grupe dokumenata moraju biti ocijenjene, čime se postiže ravnoteža između propusnosti i latencije.
- Ugrađivanje na mreži: prilikom pretraživanja baze podataka, kratak upit mora biti ugrađen za dohvaćanje, uz minimalnu latenciju.
Izgradili smo našu infrastrukturu inferencije kako bismo iskoristili što više uobičajenih komponenti u različitim slučajevima upotrebe. Budući da obično koristimo male Transformer modele za izradu ugrađivanja, dijelimo veći dio implementacije s našim kodom za LLM inferenciju: skupna ugrađivanja slična su predpopunjavanju ograničenom računalnim resursima, dok ugrađivanja na mreži, koja se često izvode na nekoliko tokena, računalno sliče dekodiranju ograničenom memorijom. Stoga ponovno koristimo naše optimizirane jezgre predpopunjavanja i dekodiranja za posluživanje modela ugrađivanja. Kao rezultat toga, možemo postići masivnu propusnost skupne inferencije s minimalnim dodatnim inženjerskim radom, uz očuvanje niske latencije za radna opterećenja mrežnih ugrađivanja.
Tulipani, ruže i malo bršljana
Izlažemo inferenciju kroz standardizirane API-je, kako interno tako i eksterno kroz našu API platformu. Iza kulisa, više je usluga uključeno u obradu zahtjeva za ugrađivanjem:
- Ivy je Rust HTTP pristupnik koji Perplexityjeve usluge pozivaju.
Obrađuje rad na strani CPU-a za zahtjeve kao što su JSON analiza, tokenizacija, izrada predložaka unosa i dijeljenje grupa, prevodeći zahtjeve u prilagođeni gRPC protokol za poslužitelje u nastavku. Ova odvojenost omogućuje nam konfiguriranje određenih parametara oko tokenizacije i formatiranja unosa bez potrebe za diranjem težih instanci inferencije.
- Tulip je sučelje poslužitelja za inferenciju.
To je gRPC poslužitelj implementiran s Rustom, tokio i `tonic` bibliotekom. Tulip prima gRPC zahtjeve za inferenciju, upravljajući raspoređivanjem i grupiranjem. Zatim šalje grupe ROSE stroju, vraćajući dovršene odgovore klijentima.
- **ROSE** (Runtime-Optimized Serving Engine) implementira inferenciju modela.
Prvenstveno je definiran u Python-u, pružajući jezgre, slojeve i definicije za širok raspon modela. ROSE implementira prolaze unaprijed kroz modele, a također pruža upravljanje CUDA grafovima specijalizirano za ugrađivanja. Povezan je s Tulip-om putem funkcije step(), koja uzima grupu i vraća referencu na izračun koji izvodi na ubrzivaču.

Obratite pozornost izvan jezgre
I modeli temeljeni na transformerima i temeljne arhitekture Hopper/Blackwell zrele su tehnologije, tako da je ugrađena inferencija na strani GPU-a konvergirala u uglavnom optimalnu implementaciju preko različitih strojeva za inferenciju. Unatoč tome, otkrili smo dodatne mogućnosti za poboljšanje u izvođenjima i sučeljima koja izlažu modele s kraja na kraj klijentu. Posebno smo otkrili da možemo poboljšati latencije pomnim upravljanjem CUDA grafovima i izgradnjom LazyTensor apstrakcije za asinkrono praćenje rezultata na strani GPU-a u izvornom Rust stroju. Ove smo značajke implementirali u Tulip-u, tako da može učinkovito komunicirati s implementacijama modela ROSE-a.
Tulip
Dizajnirali smo Tulip da bude što lakše sučelje preko posluživanja našeg modela. Upravlja dolaznim zahtjevima u Tokio asinkronim zadacima, održavajući skup zahtjeva koje prati i iz kojih raspoređuje grupe za prosljeđivanje na ubrzivač. Mehanizam raspoređivanja u Tulip-u vrlo je jednostavan: zahtjevi se akumuliraju dok Tulip prosljeđuje rad ili čeka rezultate. Iz akumuliranih zahtjeva, sekvence se biraju po načelu tko prvi dođe, njegovo (FIFO) kako bi se izvršile kroz model.
Jednostavan mehanizam raspoređivanja motiviran je zapažanjem o performansama modela. Za male modele ugrađivanja, pri duljinama sekvenci koje poslužujemo, primijetili smo da je linearni trošak gustih slojeva dominantan nad kvadratnim troškom pažnje. Stoga je latencija uglavnom proporcionalna broju tokena, a ne broju sekvenci. Posljedično, jednom kada je grupa dovoljno velika da zasićuje GPU, što je oko 512 tokena na modelu ispod jedne milijarde parametara, pakiranje više sekvenci u nju ne poboljšava učinkovitost.
Kako bi učinkovito komunicirao s modelom, Tulip se oslanja na CUDA grafove i praćenje lijenih rezultata kako bi preklopio rad GPU-a i CPU-a te u potpunosti iskoristio dostupne resurse.
Upravljanje CUDA grafom
Izvođenje prolaza modela unaprijed uključuje rad na strani CPU-a i na strani GPU-a. CPU je odgovoran za raspoređivanje grupa i pokretanje jezgri s odgovarajućim parametrima, dok GPU izvršava relevantne jezgre množenja matrica, pažnje, normiranja ili aktivacije. Za radna opterećenja s visokom propusnošću kao što su treniranje i ponovno indeksiranje, režijski troškovi na strani CPU-a su zanemarivi jer su veličine grupa i latencija na strani GPU-a velike. Međutim, na manjim veličinama grupa, rad na strani CPU-a može nadmašiti rad na strani GPU-a.

Za ublažavanje režijskih troškova, umjesto pokretanja neovisnih jezgri, CUDA graf se može izgraditi za snimanje metapodataka potrebnih za pokretanje svih jezgri prolaza unaprijed jednim pozivom CUDA upravljačkom programu. Time se uklanja potreba za ponovnim pokretanjem skupog Python i PyTorch koda za konfiguracije za koje se CUDA grafovi mogu snimiti.
Za svaki model pratimo točku infleksije, određujući minimalni broj tokena pri kojem je izvršavanje na GPU-u skuplje od pokretanja jezgre na strani CPU-a. Budući da su modeli ugrađivanja mali, primjećujemo da se ova točka infleksije javlja kod grupa od tisuća tokena i desetaka sekvenci. Neke implementacije pažnje oslanjaju se na dinamičke unose na strani hosta za konfiguriranje pokretanja jezgri, čime se sprječavaju CUDA grafovi punog modela/gustog predpopunjavanja. Prenijeli smo promjene na relevantne jezgre kako bismo ih omogućili u našem stroju za inferenciju.
Kako bismo riješili režijske troškove, gradimo CUDA grafove za cijeli model za sve modele ugrađivanja i preklapamo rad CPU-a s radom GPU-a. Budući da CUDA grafovi minimizirajo režijske troškove na strani CPU-a, jednom kada je graf pokrenut, imamo slobodno vrijeme za pokretanje i stavljanje u red čekanja izvršavanja sljedeće grupe kad god je dostupna. Rezultati grupe na čekanju prate se pomoću LazyTensor elementa, što omogućuje asinkronom zadatku u RUST-u da blokira dok prethodna grupa ne završi s izvršavanjem. CUDA grafovi pomažu posluživanju s niskom latencijom osiguravajući da nismo sputani troškom pokretanja jezgri i olakšavaju poboljšano raspoređivanje u slučaju visoke propusnosti jer oslobađaju CPU da ranije obavi posao na sljedećoj grupi.

CUDA grafovi moraju se snimiti za svaku zasebnu konfiguraciju, što za ugrađivanja znači graf po kombinaciji broja sekvenci i broja tokena. Budući da je ova mreža opsežna, dopunjujemo brojeve tokena do spremnika koji su višekratnici broja 64 ili 256. To još uvijek rezultira tisućama grafova čije snimanje za tipičan model može trajati nekoliko minuta. Trošak snimanja dolazi iz dva izvora: eager prolaza unaprijed koji se mora izvršiti za kompiliranje jezgri i postavljanje međuspremnika za razne jezgre koje ih trebaju, nakon čega slijedi izvođenje snimanja koje ponovno izvršava Python kod.
Ublažavamo troškove pokretanja lijenim snimanjem CUDA grafova kako stroj poslužuje. Pratimo svaku konfiguraciju i osiguravamo da prolazi kroz eager probno pokretanje prije pokretanja snimanja i ponovnog izvođenja grafa pri drugom pozivu. Sva naknadna izvršavanja iste konfiguracije grafa tada prolaze kroz ponovno izvođenje CUDA grafa. Lijeno snimanje grafova ima utjecaj na p99 latencije tijekom pokretanja; međutim, vrijedno je u raspodjeli višeminutnog eager rada na više sati. Brže vrijeme pokretanja omogućuje nam bolje skaliranje i upravljanje primjenama ugrađivanja.
Lijeni tenzori
Putem CUDA-e, rad GPU-a je asinkron. Budući da asinkrono pokretanje jezgre stavlja je u red čekanja na toku, kod hosta mora izričito izvršiti sinkronizaciju kako bi pročitao rezultirajuće vektore. Kako bismo olakšali viši stupanj paralelizma i mogli pokrenuti buduće grupe dok čekamo da prethodna završi na uređaju, oslanjamo se na apstrakciju LazyTensor za praćenje vrijednosti.
LazyTensor prati međuspremnik hosta u memoriji zaključanoj na stranicu i cudaMemcpyAsync operaciju putem događaja koji kopira podatke s uređaja. Pokreće se nakon pokretanja prolaza unaprijed na istom toku. Budući da operacija kopiranja mora čekati izvršenje svih prethodnih jezgri na toku, povezani događaj prati i završetak prolaza unaprijed i dostupnost rezultata na CPU-u.

Koristimo LazyTensor elemente u našem ROSE stroju za kodiranje kako bismo preklopili rad GPU-a i CPU-a. Umjesto da svaki poziv step() pokreće CUDA graf i čeka njegov završetak, step() vraća LazyTensor za asinkrono praćenje svog rezultata. U kombinaciji s CUDA grafovima, ovo nam pomaže postići niske latencije i bolju propusnost.

ROSE
Prilagodili smo naš ROSE stroj, koji smo izvorno izgradili za posluživanje LLM-a, kako bi također upravljao izvršavanjem modela ugrađivanja. Kako bi se smanjio napor potreban za podršku modelima ugrađivanja, ROSE agresivno ponovno koristi kod između LLM-ova i ugrađivanja. Na primjer, pplx-embed posluživanje i Qwen3.5 LLM dekodiranje prolaze kroz iste jezgre. Ovo dijeljenje nam omogućuje da jednostavno poslužujemo model ugrađivanja koji je izvorno fino podešen iz LLM-a za izradu prototipova, procjenu i inferenciju u proizvodnji.
Za guste slojeve, ugrađivanje i LLM inferencija su identični budući da se vektori tokena obrađuju neovisno. U slojevima pažnje, razlike se rješavaju dodavanjem podrške za nazubljene unose, uz stranicionirana postavljanja predpopunjavanja i dekodiranja koja zahtijevaju LLM-ovi. Prilikom posluživanja modela ugrađivanja, ne instanciramo KV predmemoriju i ne prosljeđujemo varijacijama jezgri pažnje koje podržavaju nazubljeni format kako bismo izbjegli dopunjavanje. Podržane rutine konverzije i kalibracije također se dijele s LLM-ovima.
Ivy
Ivy, naš HTTP proxy sloj za inferenciju, također igra važnu ulogu u performansama. Budući da se korisni tereti zahtjeva razlikuju u proizvodnji, usmjeravanje pojedinačnih zahtjeva na pojedinačne replike može uzrokovati neravnotežu opterećenja. Ivy dijeli zahtjeve velikih grupa u komade i ravnomjerno raspoređuje opterećenje između replika, poboljšavajući iskorištenost i ublažavajući latenciju. Naš nedavni rad na vlastitoj unigram tokenizaciji, u potpunosti implementiranoj u Ivyju, drastično poboljšava latencije u odnosu na gotove tokenizatore.
...ali jezgre su i dalje važne
ROSE podržava različite pozadinske sustave pažnje. Različite jezgre mogu biti prikladne za određene veličine problema. S vremenom smo integrirali FlashInfer 2, FlashInfer 3 i FlashAttention 4 jezgre za implementaciju nazubljene pažnje.

Općenito primjećujemo da je FlashAttention 4 brži. Međutim, FlashInfer 3 ga nadmašuje na modelima temeljenim na Qwen-u pri vrlo dugačkim duljinama sekvenci. Budući da performanse i ugađanje mogu varirati ovisno o broju i dimenziji glava pažnje, održavamo podršku za višestruke konfiguracije i donosimo odluku od slučaja do slučaja prilikom posluživanja.
Računalna mjerila
Izvodimo računalna mjerila u usporedbi s vLLM v0.22.0, pokrećući inferenciju na BF16 preciznosti na stvarnim težinama modela i unosima izvedenim iz skupova podataka za procjenu. Svim mjerenjima vremena prethodila su probna pokretanja koja su potvrdila da je divergencija u kosinusnoj sličnosti unutar 0,1%.
Ugrađivanja niske latencije (p50 / p90 / p99 / max ms)
Izvješćujemo o vremenima izvođenja za unaprijed tokeniziranu veličinu grupe zahtjeva 1, u potpunosti sekvencijalne zahtjeve, duljine sekvenci od 128, 512 i 4096 tokena.

Bodovanje niske latencije (p50 / p90 / p99 / max ms)
Unaprijed tokenizirane veličine grupa zahtjeva 5, 25 i 50, duljina sekvence od 512 tokena.

Ugrađivanja visoke propusnosti (emb/s)
Veličina grupe zahtjeva 100, četiri istovremena procesa šalju zahtjeve, duljine sekvenci od 512, 1024 i 4096 tokena.

Ugrađivanja visoke współlokacije (p50 / p90 / p99 / max ms)
Duljina sekvence 512, veličina grupe 1, ali šaljemo 1, 2, 4, 8 i 16 istovremenih zahtjeva. Ova računalna mjerila također uključuju troškove tokenizacije kroz Ivy, uz mrežne režijske troškove između Ivyja i Tulipa.

Zaključak i budući rad
Infrastruktura posluživanja sastavljena od Ivyja, Tulipa i ROSE-a omogućuje nam posluživanje ugrađivanja za Perplexity s manjom latencijom i boljom propusnošću, što rezultira preciznijim pretraživanjem uz smanjene troškove u usporedbi s gotovim rješenjima.
Fokusiranjem na specifične modele i preuzimanjem vlasništva nad cijelim stogom, dobivamo slobodu potrebnu za postizanje učinkovite ravnoteže između performansi i fleksibilnosti, miješajući visoko višekratno iskoristive i performantne Rust primitive uz generičniji Python kod za modeliranje. Mnogi strojevi za inferenciju otvorenog koda, kao što su vLLM, SGLang i TokenSpeed, integriraju jezike poput Rusta i C++-a u svoj stog. Uložili smo u Rust u protekle dvije godine i ubrali velike nagrade u performansama i mogućnosti održavanja. Dijeljenjem većine implementacije ugrađivanja s našim stogom za posluživanje LLM-a, također ostvarujemo dobitke u propusnosti, bez potrebe za trošenjem značajnog inženjerskog napora na održavanje modela ugrađivanja.
Kako se modeli razvijaju, nastavit ćemo poboljšavati svaki sloj našeg stoga kako bismo smanjili latencije ograničene i CPU-om i GPU-om. Naši prilagođeni protokoli temeljeni na gRPC-u unutar Ivyja i Tulipa omogućuju nam prilagodbu komunikacije radi smanjenja mrežnih latencija, dok ROSE pruža temelj za poboljšanje računalne propusnosti. Dodatno, kako podrška za Python bez zaključaka niti raste u cijelom ekosustavu, moći ćemo dodatno poboljšati interoperabilnost između Pythona i Rusta kako bismo smanjili režijske troškove.