Brza ugrađivanja na GPU-ovima

Brza i tačna pretraga je vitalna za ceo Perplexity, od pretrage i računara do naše API platforme. Iza kulisa, težak posao obavljaju modeli ugrađivanja i rangiranja, koji pomažu našim sistemima da identifikuju najrelevantnije rezultate za dati upit. Postižemo stanje

AutoriPerplexity Engineering

Brza i tačna pretraga je od vitalnog značaja za celokupan Perplexity, od pretrage i računara do naše API platforme. Iza kulisa, glavni teret nose modeli ugrađivanja i rangiranja, koji pomažu našim sistemima da identifikuju najrelevantnije rezultate za dati upit. Kvalitet i latenciju na nivou najsavremenijih rešenja postižemo obukom i posluživanjem sopstvenih modela, kao što je pplx-embed.

Ovaj članak predstavlja uvid ispod haube u Perplexity-jevu infrastrukturu za posluživanje za ovu specijalnu klasu modela. Razgovaramo o našim tehnikama za efikasno rešavanje potreba za zaključivanjem pretrage zasnovane na veštačkoj inteligenciji, omogućavajući brzu izradu prototipova i procenu modela dok istovremeno pokrećemo naš indeks pretrage eksabajtne skale. Ove tehnike kolektivno proširuju Pareto granicu kvaliteta i efikasnosti pretrage, omogućavajući nam da poslužujemo agente i korisnike sa najboljim mogućim rezultatima uz najniže troškove i latenciju.

Ugrađivanja za pretragu

U tipičnom podešavanju pretrage, indeksirani dokumenti se preslikavaju u višedimenzionalni vektorski prostor pomoću modela ugrađivanja i čuvaju u vektorskoj bazi podataka. Ugrađivanjem upita pomoću istog modela, slični dokumenti se mogu locirati pronalaženjem vektora koji su najbliži vektoru upita. Ovo dovodi do dva različita šablona saobraćaja koje mašina za zaključivanje treba da opslužuje:

  • Ugrađivanje u serijama: prilikom izgradnje, proširivanja ili reindeksiranja baze podataka, gomile dokumenata moraju biti ugrađene u vektorski prostor, čime se maksimizira protok radi minimiziranja troškova.

Nakon vektorske pretrage, velike serije dokumenata moraju biti bodovane, čime se postiže ravnoteža između protoka i latencije.

  • Ugrađivanje na mreži: prilikom upita baze podataka, kratak upit mora biti ugrađen radi pretrage, čime se minimizira latencija.

Izgradili smo našu infrastrukturu za zaključivanje tako da iskoristi što više zajedničkih komponenti u različitim slučajevima korišćenja. Pošto obično koristimo male Transformer modele za generisanje ugrađivanja, delimo veći deo implementacije sa našim kodom za LLM zaključivanje: ugrađivanja u serijama su slična popunjavanju ograničenom računanjem, dok su ugrađivanja na mreži, koja se često pokreću na nekoliko tokena, računski slična dekodiranju ograničenom memorijom. Stoga ponovo koristimo naša optimizovana jezgra za popunjavanje i dekodiranje za posluživanje modela ugrađivanja. Kao rezultat toga, možemo postići ogroman protok zaključivanja u serijama uz minimalan dodatni inženjerski rad, uz očuvanje niske latencije za radna opterećenja ugrađivanja na mreži.

Tulips, Roses, and some Ivy

Izlažemo zaključivanje kroz standardizovane API-je, kako interno tako i eksterno preko naše API platforme. Ispod haube, više servisa je uključeno u obradu zahteva za ugrađivanje:

  • Ivy je Rust HTTP mrežni prolaz koji Perplexity servisi pozivaju.

On upravlja radom na strani CPU-a za zahteve kao što su JSON parsiranje, tokenizacija, šabloni ulaza i deljenje serija, prevodeći zahteve u prilagođeni gRPC protokol za downstream servere. Ova separacija nam omogućava da konfigurišemo određene parametre u vezi sa tokenizacijom i formatiranjem ulaza bez potrebe za diranjem težih instanci za zaključivanje.

  • Tulip je interfejs servera za zaključivanje.

To je gRPC server implementiran sa Rust-om, tokio bibliotekom i `tonic` paketom. Tulip prima gRPC zahteve za zaključivanje, upravljajući zakazivanjem i grupisanjem u serije. Zatim šalje serije ROSE mašini, vraćajući završene odgovore klijentima.

  • **ROSE** (Runtime-Optimized Serving Engine) implementuje modelovanje zaključivanja.

Prvenstveno je definisan u Python-u, obezbeđujući jezgros, slojeve i definicije za širok spektar modela. ROSE implementira prolaze unapred kroz modele, obezbeđujući i upravljanje CUDA grafovima specijalizovano za ugrađivanja. Povezan je sa Tulip-om preko funkcije step(), koja uzima seriju i vraća referencu na izračunavanje koje vrši na akceleratoru.

Arhitektura posluživanja od zahteva za ugrađivanje preko Ivy-ja do replikovanih Tulip servera

Obratite pažnju i van jezgra

I modeli zasnovani na Transformer-u i osnovne Hopper/Blackwell arhitekture su zrele tehnologije, tako da je ugrađivanje zaključivanja na strani GPU-a konvergiralo ka uglavnom optimalnoj implementaciji na različitim mašinama za zaključivanje. I pored toga, otkrili smo dodatne mogućnosti za poboljšanje u izvršnim okruženjima i okruženjima koja izlažu modele kraj-uz-kraj klijentu. Konkretno, otkrili smo da možemo poboljšati latencije pažljivim upravljanjem CUDA grafovima i izgradnjom LazyTensor apstrakcije za asinhrono praćenje rezultata na strani GPU-a u izvornom Rust okruženju. Ove funkcije smo implementirali u Tulip-u, tako da može efikasno da se poveže sa implementacijama modela u ROSE-u.

Tulip

Dizajnirali smo Tulip da bude što lakši interfejs preko našeg posluživanja modela. On upravlja dolaznim zahtevima u Tokio asinhronim zadacima, održavajući skup zahteva koje prati i iz kojih zakazuje serije za prosleđivanje akceleratoru. Mehanizam zakazivanja u Tulip-u je vrlo jednostavan: zahtevi se akumuliraju dok Tulip prosleđuje rad ili čeka rezultate. Iz akumuliranih zahteva, sekvence se biraju po principu „prvi dođe, prvi uslužen“ da bi se izvršile kroz model.

Jednostavan mehanizam zakazivanja motivisan je zapažanjem o performansama modela. Za male modele ugrađivanja, pri dužinama sekvenci koje poslužujemo, primetili smo da linearni trošak gustih slojeva dominira nad kvadratnim troškom pažnje. Stoga je latencija uglavnom proporcionalna broju tokena, a ne broju sekvenci. Posledično, kada je serija dovoljno velika da zasiti GPU, što je oko 512 tokena na modelu ispod jedne milijarde parametara, pakovanje više sekvenci u nju ne poboljšava efikasnost.

Da bi se efikasno povezao sa modelom, Tulip se oslanja na CUDA grafove i praćenje lenjih rezultata kako bi preklopio rad GPU-a i CPU-a i u potpunosti iskoristio raspoložive resurse.

Upravljanje CUDA grafom

Pokretanje prolaza unapred modela uključuje rad na strani CPU-a i na strani GPU-a. CPU je odgovoran za zakazivanje serija i pokretanje jezgara sa odgovarajućim parametrima, dok GPU izvršava relevantne operacije množenja matrica, pažnje, norme ili aktivaciona jezgra. Za radna opterećenja visokog protoka kao što su obuka i reindeksiranje, opterećenja na strani CPU-a su zanemarljiva jer su veličine serija i latencija na strani GPU-a velike. Međutim, kod manjih veličina serija, rad na strani CPU-a može prevagnuti nad radom na strani GPU-a.

Eager prolaz unapred: pozivi hosta prepleteni sa jezgrima uređaja

Da bi se ublažila opterećenja, umesto pokretanja nezavisnih jezgara, može se izgraditi CUDA graf da snimi metapodatke potrebne za pokretanje svih jezgara prolaza unapred jednim pozivom CUDA drajveru. Ovo eliminiše potrebu za ponovnim pokretanjem skupog Python i PyTorch koda za konfiguracije za koje se CUDA grafovi mogu snimiti.

Kod svakog modela pratimo tačku inflekcije, utvrđujući minimalan broj tokena pri kojem je izvršavanje na GPU-u skuplje od pokretanja jezgra na strani CPU-a. Pošto su modeli ugrađivanja mali, primetujemo da se ova tačka inflekcije javlja kod serija od hiljada tokena i desetina sekvenci. Neke implementacije pažnje se oslanjaju na dinamičke ulaze na strani hosta za konfigurisanje pokretanja jezgara, što sprečava popunjavanje celog modela / guste CUDA grafove. Uneli smo promene u relevantna jezgra kako bismo ih omogućili u našoj mašini za zaključivanje.

Da bismo rešili problem opterećenja, gradimo CUDA grafove za cele modele za sve modele ugrađivanja i preklapamo rad CPU-a sa radom GPU-a. Pošto CUDA grafovi minimiziraju opterećenja na strani CPU-a, kada se graf pokrene, imamo slobodno vreme da pokrenemo i stavimo u red čekanja izvršavanje sledeće serije kad god je dostupna. Rezultati serije na čekanju se prate pomoću LazyTensor objekta, što omogućava asinhronom zadatku u RUST-u da blokira dok prethodna serija ne završi izvršavanje. CUDA grafovi pomažu posluživanju niske latencije tako što osiguravaju da nas troškovi pokretanja jezgara ne usporavaju i olakšavaju bolje zakazivanje u slučaju visokog protoka jer oslobađaju CPU da ranije obavi posao na sledećoj seriji.

Prolaz unapred za Cudagraph

CUDA grafovi moraju biti snimljeni za svaku različitu konfiguraciju, što za ugrađivanja znači graf po kombinaciji broja sekvenci i broja tokena. Pošto je ova mreža opsežna, popunjavamo broj tokena do kofera koji su umnošci broja 64 ili 256. Ovo i dalje rezultira hiljadama grafova čije snimanje za tipičan model može potrajati više minuta. Trošak snimanja potiče iz dva izvora: eager prolaz unapred koji se mora izvršiti da bi se kompajlirala jezgra i podesili baferi za različita jezgra kojima su potrebni, praćen pokretanjem snimanja koje ponovo izvršava Python kod.

Ublažavamo troškove pokretanja lenjim snimanjem CUDA grafova dok mašina obavlja posluživanje. Pratimo svaku konfiguraciju i osiguravamo da prođe kroz eager rad zagrevanja pre pokretanja snimanja i reprize grafa pri drugom pogotku. Sva naknadna izvršavanja iste konfiguracije grafa zatim prolaze kroz reprizu CUDA grafa. Lenje snimanje grafa ima uticaj na p99 latencije tokom pokretanja; međutim, dragoceno je u raspoređivanju više minuta eager rada na više sati. Kraća vremena pokretanja nam omogućavaju da bolje skaliramo i upravljamo primenama ugrađivanja.

Leni tenzori

Kroz CUDA, rad GPU-a je asinhroni. Pošto pokretanje jezgra asinhrono stavlja u red čekanja na tok, kod hosta mora eksplicitno da se sinhronizuje da bi pročitao rezultujuće vektore. Da bismo olakšali viši stepen paralelizma i omogućili pokretanje budućih serija dok čekamo da se prethodna završi na uređaju, oslanjamo se na LazyTensor apstrakciju za praćenje vrednosti.

LazyTensor prati bafer hosta u memoriji zaključanoj na stranicu i cudaMemcpyAsync operaciju preko događaja koji kopira podatke sa uređaja. Pokreće se nakon pokretanja prolaza unapred na istom toku. Pošto operacija kopiranja mora da sačeka da se sva prethodna jezgra na toku izvrše, povezani događaj prati i završetak prolaza unapred i dostupnost rezultata na CPU-u.

Izvršavanje LazyTensor-a

Koristimo LazyTensor objekte u našem ROSE enkoder okruženju da preklopimo rad GPU-a i CPU-a. Umesto da svaki poziv funkcije step() pokreće CUDA graf i čeka da se završi, step() vraća LazyTensor da asinhrono prati svoj rezultat. U kombinaciji sa CUDA grafovima, ovo nam pomaže da postignemo niske latencije i bolji protok.

Vremenska linija koja prikazuje pripremu CPU-a i sinhronizaciju koja preklapa uzastopne GPU serije

ROSE

Prilagodili smo našu ROSE mašinu, koju smo prvobitno napravili za LLM posluživanje, tako da upravlja i izvršavanjem modela ugrađivanja. Da bi se minimizovao napor potreban za podršku modelima ugrađivanja, ROSE agresivno ponovo koristi kod između LLM-ova i ugrađivanja. Na primer, pplx-embed posluživanje i Qwen3.5 LLM dekodiranje prolaze kroz ista jezgra. Ovo deljenje nam omogućava da lako poslužujemo model ugrađivanja koji je prvobitno fino podešen iz LLM-a radi izrade prototipova, procene i zaključivanja u proizvodnji.

Za guste slojeve, zaključivanje ugrađivanja i LLM-a je identično pošto se vektori tokena obrađuju nezavisno. U slojevima pažnje, razlike se rešavaju dodavanjem podrške za nazubljene ulaze, zajedno sa podešavanjima paginiranog popunjavanja i dekodiranja koja zahtevaju LLM-ovi. Prilikom posluživanja modela ugrađivanja, ne instanciramo KV keš i ne prosleđujemo varijacijama jezgara pažnje koja podržavaju nazubljeni format kako bismo izbegli popunjavanje. Povezane rutine konverzije i kalibracije se takođe dele sa LLM-ovima.

Ivy

Ivy, naš sloj HTTP proksi servera za zaključivanje, takođe igra važnu ulogu u performansama. Pošto se korisni tereti zahteva razlikuju u proizvodnji, usmeravanje pojedinačnih zahteva na pojedinačne replike može izazvati neravnomernost opterećenja. Ivy deli zahteve velikih serija na delove i balansira njihovo opterećenje između replika, čime poboljšava iskorišćenost i ublažava latenciju. Naš nedavni rad na unigram tokenizaciji u sopstvenoj režiji, u potpunosti implementiranoj u Ivy-ju, drastično poboljšava latencije u odnosu na gotove tokenizatore.

...ali jezgra su i dalje važna

ROSE podržava različite pozadinske sisteme pažnje. Različita jezgra mogu biti pogodna za specifične veličine problema. Vremenom smo integrisali FlashInfer 2, FlashInfer 3 i FlashAttention 4 jezgra radi implementacije nazubljene pažnje.

Performanse jezgra pažnje prema obliku modela i veličini problema

Generalno primetujemo da je FlashAttention 4 brži. Međutim, FlashInfer 3 ga nadmašuje na modelima zasnovanim na Qwen-u pri vrlo dugačkim dužinama sekvenci. Pošto performanse i podešavanje mogu varirati u zavisnosti od broja i dimenzije glava pažnje, održavamo podršku za više konfiguracija i donosimo odluku od slučaja do slučaja prilikom posluživanja.

Benčmark analize

Vršimo benčmarking u odnosu na vLLM v0.22.0, pokrećući zaključivanje u BF16 preciznosti na stvarnim težinama modela i ulazima izvedenim iz skupova podataka za procenu. Svi vremenski ciklusi su bili prethodjeni probnim radovima zagrevanja koji su potvrdili da je divergencija u kosinusnoj sličnosti unutar 0,1%.

Ugrađivanja niske latencije (p50 / p90 / p99 / maks. ms)

Izveštavamo o vremenima izvršavanja za veličinu serije unapred tokenizovanog zahteva 1, potpuno sekvencijalne zahteve, dužine sekvenci od 128, 512 i 4096 tokena.

Rezultati benčmarka ugrađivanja niske latencije

Bodovanje niske latencije (p50 / p90 / p99 / maks. ms)

Veličine serija unapred tokenizovanih zahteva 5, 25 i 50, dužina sekvence od 512 tokena.

Rezultati benčmarka bodovanja niske latencije

Ugrađivanja visokog protoka (emb/s)

Veličina serije zahteva 100, četiri konkurentna procesa šalju zahteve, dužine sekvenci 512, 1024 i 4096 tokena.

Rezultati benčmarka ugrađivanja visokog protoka

Ugrađivanja visoke konkurentnosti (p50 / p90 / p99 / maks. ms)

Dužina sekvence 512, veličina serije 1, ali šaljemo 1, 2, 4, 8 i 16 konkurentnih zahteva. Ovaj benčmark takođe uključuje troškove tokenizacije kroz Ivy, zajedno sa mrežnim opterećenjem između Ivy-ja i Tulip-a.

Rezultati benčmarka ugrađivanja visoke konkurentnosti

Zaključak i budući rad

Infrastruktura za posluživanje koju čine Ivy, Tulip i ROSE nam omogućava da poslužujemo ugrađivanja za Perplexity uz manju latenciju i bolji protok, što rezultira preciznijom pretragom uz smanjene troškove u poređenju sa gotovim rešenjima.

Fokusiranjem na specifične modele i preuzimanjem vlasništva nad celim stekom, dobijamo slobodu neophodnu da postignemo efikasnu ravnotežu između performansi i fleksibilnosti, mešajući visoko višekratno iskoristive i performantne Rust primitive uz generički kod za modelovanje u Python-u. Mnoge mašine za zaključivanje otvorenog koda, kao što su vLLM, SGLang i TokenSpeed, integrišu jezike poput Rusta i C++ u svoj stek. Uložili smo u Rust u protekle dve godine i ubrali velike nagrade u pogledu performansi i mogućnosti održavanja. Deljenjem većine implementacije ugrađivanja sa našim stekom za posluživanje LLM-a, takođe ostvarujemo dobitke u protoku, bez potrebe za trošenjem značajnog inženjerskog napora na održavanje modela ugrađivanja.

Kako se modeli razvijaju, nastavićemo da poboljšavamo svaki sloj našeg steka kako bismo smanjili latencije ograničene i CPU-om i GPU-om. Naši prilagođeni gRPC protokoli unutar Ivy-ja i Tulip-a omogućavaju nam da prilagodimo komunikaciju radi smanjenja mrežnih latencija, dok ROSE pruža osnovu za poboljšanje računskog protoka. Pored toga, kako podrška za Python sa slobodnim nitima raste širom ekosistema, moći ćemo dodatno da poboljšamo interoperabilnost između Python-a i Rusta kako bismo smanjili opterećenja.

Reference