Hurtige indlejringer på GPU'er
Hurtig og nøjagtig søgning er afgørende for hele Perplexity, lige fra Search og Computer til vores API-platform. Bag kulisserne udføres det tunge arbejde af indlejrings- og rangeringsmodeller, som hjælper vores systemer med at identificere de mest relevante resultater for en given forespørgsel. Vi opnår state-of-the
Hurtig og nøjagtig søgning er afgørende for hele Perplexity, lige fra Search og Computer til vores API-platform. Bag kulisserne udføres det tunge arbejde af indlejrings- og rangeringsmodeller, som hjælper vores systemer med at identificere de mest relevante resultater for en given forespørgsel. Vi opnår state-of-the-art kvalitet og latenstid ved at træne og betjene vores egne modeller, såsom pplx-embed.
Denne artikel præsenterer et kig bag kulisserne på Perplexitys serveringsinfrastruktur for denne særlige klasse af modeller. Vi diskuterer vores teknikker til effektivt at imødekomme inferensbehovene for AI-native søgning, hvilket muliggør hurtig prototyperangering og evaluering af modeller, samtidig med at det driver vores eksabyt-skala søgeindeks. Disse teknikker udvider kollektivt Pareto-grænsen for søgekvalitet og effektivitet, hvilket gør det muligt for os at betjene agenter og brugere med de bedst mulige resultater til de laveste omkostninger og latenstid.
Indlejringer til søgning
I en typisk søgeopsætning knyttes indekserede dokumenter til et højfimensionelt vektorrum ved hjælp af en indlejringsmodel og gemmes i en vektordatabase. Ved at indlejre en forespørgsel ved hjælp af den samme model kan lignende dokumenter findes ved at finde de vektorer, der er tættest på forespørgselens. Dette giver anledning til to forskellige trafiksønstre for en inferensmotor at betjene:
- Bunker-indlejring: ved opbygning, udvidelse eller genindeksering af databasen skal store mængder dokumenter indlejres i vektorrummet, hvilket maksimerer gennemløbet for at minimere omkostningerne.
Efter vektorsøgning skal store bunker af dokumenter scores, hvilket skaber en balance mellem gennemløb og latenstid.
- Online Indlejring: ved forespørgsel på databasen skal en kort forespørgsel indlejres til opslag, hvilket minimerer latenstiden.
Vi opbyggede vores inferensinfrastruktur til at udnytte så mange fælles komponenter som muligt på tværs af use cases. Da vi typisk bruger små Transformer-modeller til at producere indlejringer, deler vi hovedparten af implementeringen med vores LLM-inferenskode: bunkeindlejringer ligner beregningsbundet prefill, hvorimod onlineindlejringer, som ofte kører på få tokens, er beregningsmæssigt svarende til hukommelsesbundet dekodning. Vi genbruger således vores optimerede prefill- og dekodnings-kernels til at betjene indlejringsmodeller. Som et resultat kan vi opnå massiv bunkeinferensgenløb med minimalt ekstra ingeniørarbejde, samtidig med at vi opretholder lav latenstid for onlineindlejrings-arbejdsbelastninger.
Tulips, Roses og lidt Ivy
Vi eksponerer inferens gennem standardiserede API'er, både internt og eksternt gennem vores API-platform. Bag kulisserne er flere tjenester involveret i behandlingen af en indlejringsanmodning:
- Ivy er en Rust-HTTP-gateway, som Perplexity-tjenester kalder.
Den håndterer CPU-side-arbejdet for anmodninger såsom JSON-parsing, tokenisering, input-templating og bunkeopdeling, og oversætter anmodninger til en tilpasset gRPC-protokol til downstream-servere. Denne adskillelse giver os mulighed for at konfigurere visse parametre omkring tokenisering og inputformatering uden at skulle røre ved de tungere inferensinstanser.
- Tulip er inferensservergrænsefladen.
Det er en gRPC-server implementeret med Rust, tokio og `tonic`. Tulip modtager gRPC-inferensanmodninger og håndterer planlægning og bunkedannelse. Den sender derefter bunkerne til ROSE-motoren og returnerer fuldførte svar til klienter.
- **ROSE** (Runtime-Optimized Serving Engine) implementerer modelinferens.
Den er primært defineret i Python og leverer kernels, lag og definitioner for en lang række modeller. ROSE implementerer fremadrettede gennemløb gennem modeller og leverer også CUDA-grafadministration, der er specialiseret til indlejringer. Den er forbundet til Tulip via en step()-funktion, som tager en bunke og returnerer en reference til den beregning, den udfører på acceleratoren.

At være opmærksom ud over kernelen
Både Transformer-baserede modeller og de underliggende Hopper/Blackwell-arkitekturer er modne teknologier, så indlejringsinferens på GPU-siden er konvergeret mod en stort set optimal implementering på tværs af forskellige inferensmotorer. Alligevel opdagede vi yderligere muligheder for forbedring i køretider og rammer, der eksponerer modellerne ende-til-ende for en klient. Især fandt vi ud af, at vi kan forbedre latenstider ved omhyggeligt at administrere CUDA-grafer og ved at bygge en LazyTensor-abstraktion til asynkront at spore et GPU-side-resultat i den oprindelige Rust-motor. Vi implementerede disse funktioner i Tulip, så den effektivt kunne grænseflade med ROSE's modelimplementeringer.
Tulip
Vi designede Tulip til at være en så let grænseflade over vores modelservering som muligt. Den håndterer indgående anmodninger i Tokio-asynkrone opgaver, opretholder en pulje af anmodninger, som den sporer og planlægger bunker fra for at udsende til acceleratoren. Planlægningsmekanismen i Tulip er meget simpel: anmodninger ophobes, mens Tulip udsender arbejde eller venter på resultater. Fra de ophobede anmodninger vælges sekvenser efter først-til-mølle-princippet til at blive kørt gennem modellen.
Den enkle planlægningsmekanisme er motiveret af en observation om modelydeevne. For små indlejringsmodeller, ved de sekvenslængder vi betjener, bemærkede vi, at den lineære omkostning for tætte lag dominerer over den kvadratiske omkostning for attention. Derfor er latenstiden mest proportionale med antallet af tokens, ikke antallet af sekvenser. Følgelig, når en bunke er stor nok til at mætte GPU'en, hvilket er omkring 512 tokens på en model under en milliard parametre, forbedrer det ikke effektiviteten at pakke flere sekvenser ind i den.
For effektivt at interagere med modellen er Tulip afhængig af CUDA-grafer og lade-resultatsporing for at overlappe GPU- og CPU-arbejde og udnytte de tilgængelige ressourcer fuldt ud.
CUDA-grafadministration
Kørsel af en models forward-gennemløb involverer både CPU-side- og GPU-side-arbejde. CPU'en er ansvarlig for at planlægge bunker og starte kernels med de relevante parametre, mens GPU'en udfører de relevante matrixmultiplikations-, attention-, norm- eller aktiverings-kernels. For arbejdsbelastninger med højt gennemløb, såsom træning og genindeksering, er CPU-side-overheads ubetydelige, fordi bunkenstørrelser og GPU-side-latenstid begge er store. På mindre bunkenstørrelser kan CPU-side-arbejde dog opveje GPU-side-arbejdet.

For at afbøde overheads, i stedet for at starte uafhængige kernels, kan der bygges en CUDA-graf til at indfange de metadata, der kræves for at starte alle kernels i et forward-gennemløb med et enkelt kald til CUDA-driveren. Dette eliminerer behovet for at genkøre dyre Python- og PyTorch-koder for de konfigurationer, som CUDA-grafer kan indfanges for.
På tværs af hver model sporer vi et inflektionspunkt, der bestemmer det minimale antal tokens, hvor GPU-udførelse er dyrere end CPU-side-kernel-start. Da indlejringsmodeller er små, observerer vi, at dette inflektionspunkt kommer ved bunker på tusindvis af tokens og tivis af sekvenser. Nogle attention-implementeringer er afhængige af dynamiske værts-side-inputs til at konfigurere kernel-starter, hvilket forhindrer fuld-model prefill/tætte CUDA-grafer. Vi har upstreamet ændringer til relevante kernels for at aktivere dem i vores inferensmotor.
For at imødegå overheads bygger vi fuld-model CUDA-grafer for alle indlejringsmodeller og overlapper CPU-arbejde med GPU-arbejde. Da CUDA-grafer minimerer CPU-side-overheads, har vi, når først en graf startes, fri tid til at starte og sætte udførelsen af den næste bunke i kø, når den er tilgængelig. Resultaterne af den afventende bunke spores med en LazyTensor, hvilket giver en asynkron opgave i Rust mulighed for at blokere, indtil den forrige bunke er færdig med at udføre. CUDA-grafer hjælper servering med lav latenstid ved at sikre, at vi ikke holdes tilbage af omkostningerne ved kernel-starter, og letter forbedret planlægning i tilfældet med højt gennemløb, da de frigør CPU'en til at udføre arbejde på den næste bunke før.

CUDA-grafer skal indfanges for hver enkelt konfiguration, hvilket for indlejringer betyder en graf pr. kombination af sekvensantal og tokenantal. Da dette gitter er omfattende, udfører vi padding af tokenantal til beholdere (buckets), der er multipla af 64 eller 256. Dette resulterer stadig i tusindvis af grafer, som kan tage flere minutter at indfange for en typisk model. Omkostningerne ved indfangning kommer fra to kilder: et ivrigt forward-gennemløb, der skal udføres for at kompilere kernels og oprette buffers til forskellige kernels, der har brug for dem, efterfulgt af indfangningskørslen, som genudfører Python-kode.
Vi afbøder opstartsomkostninger ved at indfange CUDA-grafer lade, efterhånden som motoren betjener. Vi holder styr på hver konfiguration og sikrer, at den gennemgår en ivrig opvarmningskørsel, før vi udløser grafindfangning og genafspilning på det andet hit. Alle efterfølgende udførelser af den samme grafkonfiguration går derefter igennem CUDA-grafgenafspilning. Lade grafindfangning har en indvirkning på p99-latenstider under opstart; dog er det værdifuldt til at sprede flere minutters ivrigt arbejde over flere timer. Hurtigere opstartstider giver os mulighed for bedre at skalere og administrere indlejrings-udrulninger.
Lade Tensors
Gennem CUDA er GPU-arbejde asynkront. Da start af en kernel asynkront sætter den i kø på en strøm, skal værtskoden synkronisere eksplicit for at læse de resulterende vektorer ud. For at muliggøre en højere grad af parallelitet og være i stand til at starte fremtidige bunker, mens man venter på, at den forrige bliver færdig på enheden, stoler vi på en LazyTensor-abstraktion til at spore værdier.
LazyTensor sporer en værtsbuffer i sidelåst hukommelse og en cudaMemcpyAsync-operation via en hændelse, der kopierer data fra enheden. Den startes efter lanceringen af forward-gennemløbet på den samme strøm. Da kopieringsoperationen skal vente på, at alle tidligere kernels på strømmen udføres, sporer den tilhørende hændelse både afslutningen af forward-gennemløbet og tilgængeligheden af resultatet på CPU'en.

Vi udnytter LazyTensorer i vores ROSE-encoder-motor til at overlappe GPU- og CPU-arbejde. I stedet for at hvert step()-kald kører CUDA-grafen og venter på, at den bliver færdig, returnerer step() en LazyTensor til asynkront at spore dens resultat. Kombineret med CUDA-grafer hjælper dette os med at opnå lave latenstider og bedre gennemløb.

ROSE
Vi tilpassede vores ROSE-motor, som vi oprindeligt byggede til LLM-servering, til også at håndtere udførelsen af indlejringsmodeller. For at minimere den indsats, der kræves for at understøtte indlejringsmodeller, genbruger ROSE aggressivt kode mellem LLM'er og indlejringer. For eksempel går pplx-embed-servering og Qwen3.5 LLM-dekodning igennem de samme kernels. Denne deling giver os mulighed for nemt at betjene en indlejringsmodel, der oprindeligt blev finjusteret fra en LLM til prototyping, evaluering og produktionsinferens.
For tætte lag er indlejring og LLM-inferens identiske, da tokensureer behandles uafhængigt. I attention-lag håndteres forskelle ved at tilføje understøttelse af uensartede (ragged) inputs sammen med de paged prefill- og dekodnings-opsætninger, der kræves af LLM'er. Når vi betjener en indlejringsmodel, instansierer vi ikke en KV-cache og sender til varianter af attention-kernels, der understøtter det uensartede format for at undgå padding. De understøttende konverterings- og kalibreringsrutiner deles også med LLM'erne.
Ivy
Ivy, vores inferens-HTTP-proxylag, spiller også en vigtig rolle for ydeevnen. Da anmodningsnyttelast varierer i produktionen, kan routing af individuelle anmodninger til individuelle replikaer forårsage ubalancerede belastninger. Ivy opdeler store bunke-anmodninger i bidder og fordeler dem mellem replikaer, hvilket forbedrer udnyttelsen og udjævner latenstiden. Vores seneste arbejde med internt udviklet unigram-tokenisering, som er fuldt udrullet i Ivy, forbedrer drastisk latenstiderne i forhold til standard-tokeniserere.
...men kernels betyder stadig noget
ROSE understøtter en række attention-backends. Forskellige kernels kan være velegnede til specifikke problemstørrelser. Over tid integrerede vi FlashInfer 2-, FlashInfer 3- og FlashAttention 4-kernels for at implementere uensartet attention.

Generelt observerer vi, at FlashAttention 4 er hurtigere. Dog overgår FlashInfer 3 den på Qwen-baserede modeller ved meget lange sekvenslængder. Da ydeevne og finjustering kan variere med antallet og dimensionen af attention-hoveder, opretholder vi understøttelse af flere konfigurationer og træffer en beslutning fra sag til sag under servering.
Benchmarks
Vi foretager benchmarks mod vLLM v0.22.0, der kører inferens på BF16-præcision på faktiske modelvægte og inputs afledt af evalueringsdatasæt. Alle tidsforløb blev foregået af opvarmningskørsler, som verificerede, at afvigelsen i cosinus-lignhed er inden for 0,1%.
Indlejringer med lav latenstid (p50 / p90 / p99 / maks. ms)
Vi rapporterer køretider for pre-tokeniseret anmodningsbunkestørrelse 1, fuldstændig sekventielle anmodninger, sekvenslængder på 128, 512 og 4096 tokens.

Scoring med lav latenstid (p50 / p90 / p99 / maks. ms)
Pre-tokeniserede anmodningsbunke-størrelser 5, 25 og 50, sekvenslængde på 512 tokens.

Indlejringer med højt gennemløb (emb/s)
Anmodningsbunstørrelse 100, fire samtidige processer, der indsender anmodninger, sekvenslængder på 512, 1024 og 4096 tokens.

Indlejringer med høj samtidighed (p50 / p90 / p99 / maks. ms)
Sekvenslængde 512, bunkenstørrelse 1, men vi sender 1, 2, 4, 8 og 16 samtidige anmodninger. Denne benchmark inkluderer også tokeniseringsomkostninger gennem Ivy sammen med netværksoverheadet mellem Ivy og Tulip.

Konklusion og fremtidigt arbejde
Serveringsinfrastrukturen bestående af Ivy, Tulip og ROSE gør det muligt for os at betjene indlejringer for Perplexity med lavere latenstid og bedre gennemløb, hvilket resulterer i mere nøjagtig søgning til en reduceret omkostning sammenlignet med standardløsninger.
Ved at fokusere på specifikke modeller og tage ansvar for hele stakken opnår vi den nødvendige frihed til at skabe en effektiv balance mellem ydeevne og fleksibilitet ved at blande stærkt genbrugelige og performante Rust-primitiver sammen med mere generisk Python-modelleringskode. Mange open source-inferensmotorer, såsom vLLM, SGLang og TokenSpeed, integrerer sprog som Rust og C++ i deres stak. Vi har investeret i Rust over de sidste to år og har høstet store gevinster i både ydeevne og vedligeholdelse. Ved at dele det meste af indlejringsimplementeringen med vores LLM-serveringsstak opnår vi også gevinster i gennemløb uden at kræve, at der bruges væsentlig ingeniørarbejde på vedligeholdelse af indlejringsmodeller.
Efterhånden som modeller udvikler sig, vil vi fortsætte med at forbedre hvert lag i vores stak for at reducere både CPU-bundne og GPU-bundne latenstider. Vores tilpassede gRPC-baserede protokoller i Ivy og Tulip giver os mulighed for at finjustere kommunikationen for at reducere netværkslatenstider, mens ROSE giver et fundament til at forbedre beregningsgennemløbet. Derudover, efterhånden som understøttelsen af trådfri Python vokser i økosystemet, vil vi være i stand til yderligere at forbedre Python-Rust-interoperabiliteten for at reducere overheads.