Брза уграђивања на ГПУ-овима
Брза и тачна претрага је од виталног значаја за цијели Perplexity, од претраге и рачунара (Computer) до наше API платформе. Иза кулиса, тежак посао обављају модели уграђивања и рангирања, који помажу нашим системима да идентификују најрелевантније резултате за дати упит. Постижемо најсавременији стате-of-the
Брза и тачна претрага је од виталног значаја за цијели Perplexity, од претраге и рачунара (Computer) до наше API платформе. Иза кулиса, тежак посао обављају модели уграђивања и рангирања, који помажу нашим системима да идентификују најрелевантније резултате за дати упит. Постижемо најсавременији квалитет и кашњење обуком и опслуживањем сопствених модела, као што је pplx-embed.
Овај чланак представља поглед испод хаубе инфраструктуре за опслуживање компаније Perplexity за ову специјалну класу модела. Разматрамо наше технике за ефикасно рјешавање потреба за инференцијом претраге засноване на вјештачкој интелигенцији, омогућавајући брзо пројектовање и евалуацију модела док покрећемо наш индекс претраге егсабајт скале. Ове технике заједнички проширују Парето границу квалитета и ефикасности претраге, омогућавајући нам да опслужујемо агенте и кориснике са најбољим могућим резултатима уз најнижу цијену и кашњење.
Уграђивања за претрагу
У типичној поставци претраге, индексирани документи се мапирају у векторски простор високе димензије помоћу модела уграђивања и чувају се у векторској бази података. Уграђивањем упита коришћењем истог модела, слични документи се могу лоцирати проналажењем вектоа најближих онима из упита. Ово доводи до два различита образаца саобраћаја које мотор за инференцу треба да опслужи:
- Серијско уграђивање: приликом изградње, проширења или реиндексирања базе података, масивни документи морају бити уграђени у векторски простор, максимизирајући проток како би се минимизирали трошкови.
Након претраге вектора, велике серије докумената морају бити бодоване, постижући равнотежу између протока и кашњења.
- Онлајн уграђивање: приликом упита базе података, кратак упит мора бити уграђен ради претраге, минимизирајући кашњење.
Изградили смо нашу инфраструктуру за инференцу да искористи што више заједничких компонентa у различитим случајевима коришћења. Пошто обично користимо мале Transformer моделе за производњу уграђивања, дијелимо већину имплементације са нашим кодом за LLM инференцу: серијска уграђивања су слична препуњавању везаном за рачунање (compute-bound prefill), док су онлајн уграђивања, која се често извршавају на неколико токена, рачунски слична декодирању везаном за меморију (memory-bound decode). Стога поново користимо наше оптимизоване кернеле за препуњавање и декодирање за опслуживање модела уграђивања. Као резултат тога, можемо постићи масовни проток серијске инференце уз минималан додатни инжењерски рад, уз очување ниског кашњења за радне оптерећења онлајн уграђивања.
Tulips, Roses, и мало Ivy-ја
Излажемо инференцу кроз стандардизоване API-је, како интерно тако и екстерно преко наше API платформе. Иза кулиса, више сервиса је укључено у обраду захтјева за уграђивање:
- Ivy је Rust HTTP мрежни пролаз који сервиси компаније Perplexity позивају.
Рукује радом на страни ЦПУ-а за захтјеве као што су парсовање JSON-а, токенизација, шаблон улаза и дијељење серија, преводећи захтјеве на прилагођени gRPC протокол за низводне сервере. Ово раздвајање нам омогућава да конфигуришемо одређене параметре око токенизације и форматирања улаза без потребе да дирамо теже инстанце инференце.
- Tulip је интерфејс сервера за инференцу.
То је gRPC сервер имплементиран са Rust-ом, tokio-ом и `tonic`-ом. Tulip прима gRPC захтјеве за инференцу, руководећи заказивањем и серијањем. Затим шаље серије у ROSE мотор, враћајући завршене одговоре клијентима.
- **ROSE** (Runtime-Optimized Serving Engine) имплементира инференцу модела.
Првенствено је дефинисан у Python-у, пружајући кернеле, слојеве и дефиниције за широк спектар модела. ROSE имплементира пролазе унапријед кроз моделе, такође пружајући управљање CUDA графовима специјализовано за уграђивања. Повезан је са Тјулипом (Tulip) путем step() функције, која узима серију и враћа референцу на израчунавање које врши на акцелератору.

Обраћање пажње изван кернела
И модели засновани на Трафорима (Transformer) и основне Hopper/Blackwell архитектуре су зреле технологије, тако да је инференција уграђивања на страни ГПУ-а конвергирала ка углавном оптималној имплементацији на различитим машинама за инференцу. Ипак, отkrili смо додатне могућности за побољшање у рајнтајмима и окружењима која излажу моделе од краја до краја клијенту. Конкретно, утврдили смо да можемо побољшати кашњења пажљивим управљањем CUDA графовима и изградњом LazyTensor апстракције за асинхроно праћење резултата на страни ГПУ-а у изворном Rust језику. Имплементирали смо ове карактеристике у Тјулипу (Tulip), тако да може ефикасно да се повеже са имплементацијама модела ROSE-а.
Tulip
Дизајнирали смо Tulip да буде што лакши интерфејс над опслуживањем наших модела. Рукује долазним захтјевима у Tokio асинхроним задацима, одржавајући базен захтјева које прати и из којих заказује серије за прослеђивање на акцелератор. Механизам заказивања у Тјулипу је врло једноставан: захтјеви се акумулирају док Тјулип прослеђује рад или чека резултате. Из акумулираних захтјева, секвенце се бирају по принципу први дошао, први услужен да би се извршиле кроз модел.
Једноставан механизам заказивања је мотивисан запажањем о перформансама модела. За мале моделе уграђивања, при дужинама секвенци које опслужујемо, приметили смо да је линеарни трошак густих слојева доминантнији над квадратним трошком пажње. Стога је кашњење углавном пропорционално броју токена, а не броју секвенци. Као резултат тога, када је серија довољно велика да засити ГПУ, што је око 512 токена на моделу испод милијарду параметара, паковање више секвенци у њу не побољшава ефикасност.
Да би ефикасно комуницирао са моделом, Tulip се ослања на CUDA графове и праћење лијених резултата како би преклопио рад ГПУ-а и ЦПУ-а и у потпуности искористио расположиве ресурсе.
Управљање CUDA графовима
Извршавање пролаза унапријед модела укључује и рад на страни ЦПУ-а и на страни ГПУ-а. ЦПУ је одговоран за заказивање серија и покретање кернела са одговарајућим параметрима, док ГПУ извршава релевантне кернеле множења матрица, пажње, норме или активације. За радна оптерећења са високим протоком као што су обука и реиндексирање, режијски трошкови на страни ЦПУ-а су занемарљиви јер су величине серија и кашњење на страни ГПУ-а велики. Међутим, код мањих величина серија, рад на страни ЦПУ-а може надмашити рад на страни ГПУ-а.

Да би се ублажили режијски трошкови, умјесто покретања независних кернела, CUDA граф се може изградити да ухвати метаподатке потребне за покретање свих кернела пролаза унапријед са једним позивом CUDA драјверу. Ово елиминише потребу за поновним извршавањем скупог Python и PyTorch кода за конфигурације за које се CUDA графови могу ухватити.
Код сваког модела пратимо тачку превоја, утврђујући минимални број токена при којем је извршавање на ГПУ-у скупље од покретања кернела на страни ЦПУ-а. Пошто су модели уграђивања мали, прихватамо да ова тачка превоја долази код серија од хиљада токена и десетина секвенци. Неке имплементације пажње се ослањају на динамичке улазе на страни домаћина да конфигуришу покретање кернела, спречавајући пуне CUDA графове модела за препуњавање/густе моделе. Узводно смо интегрисали промјене у релевантне кернеле како бисмо их омогућили у нашем мотору за инференцу.
Да бисмо ријешили режијске трошкове, градимо CUDA графове за цијели модел за све моделе уграђивања и преклапамо ЦПУ рад са ГПУ радом. Пошто CUDA графови минимизирају режијске трошкове на страни ЦПУ-а, када се граф покрене, имамо слободно вријеме да покренемо и ставимо у ред чекања извршавање сљедеће серије кад год је доступна. Резултати серије на чекању прате се помоћу LazyTensor-а, што омогућава асинхronom задатку у Rust-у да блокира док се претходна серија не заврши са извршавањем. CUDA графови помажу опслуживању са ниским кашњењем тако што осигуравају да нас не коче трошкови покретања кернела и олакшавају побољшано заказивање у случају високог протока јер ослобађају ЦПУ да раније ради на сљедећој серији.

CUDA графови морају бити ухваћени за сваку различиту конфигурацију, што за уграђивања значи граф по комбинацији броја секвенци и броја токена. Пошто је ова мрежа обимна, попуњавамо бројеве токена до корпи које су умношци од 64 или 256. Ово и даље резултира хиљадама графова чије хватање може трајати више минута за типичан модел. Трошак хватања потиче из два извора: агилног пролаза унапријед који се мора извршити ради компајлирања кернела и подешавања бафера за различите кернеле којима су потребни, након чега слиједи покретање хватања које поново извршава Python код.
Ублажавамо трошкове покретања тако што лијено хватамо CUDA графове док мотор ради. Пратимо сваку конфигурацију и осигуравамо да прође кроз агилно загревање прије покретања хватања и репризе графа при другом поготку. Сва накнадна извршавања исте конфигурације графа затим пролазе кроз репризу CUDA графа. Лијено хватање графа има утицај на p99 кашњења током покретања; међутим, вриједно је у ширењу више минута агилног рада на више сати. Бржа времена покретања нам омогућавају да боље скалирамо и управљамо распоређивањима уграђивања.
Лијени тензори (Lazy Tensors)
Кроз CUDA, рад ГПУ-а је асинхрон. Пошто покретање кернела асинхроно ставља га у ред чекања на стриму, код домаћина мора изричито да се синхронизује да би прочитао добијене векторе. Да бисмо омогућили већи степен паралелизма и били у стању да покренемо будуће серије док чекамо да се претходна заврши на уређају, ослањамо се на LazyTensor апстракцију за праћење вриједности.
LazyTensor прати бафер домаћина у меморији закључаној на страницу и cudaMemcpyAsync операцију путем догађаја који копира податке са уређаја. Покреће се након покретања пролаза унапријед на истом стриму. Пошто операција копирања мора да чека да се изврше сви претходни кернели на стриму, повезани догађај прати и завршетак пролаза унапријед и доступност резултата на ЦПУ-у.

Користимо LazyTensor објекте у нашем ROSE енџину за енкодер како бисмо преклопили рад ГПУ-а и ЦПУ-а. Умјесто да сваки step() позив покреће CUDA граф и чека да се заврши, step() враћа LazyTensor да асинхроно прати његов резултат. У комбинацији са CUDA графовима, ово нам помаже да постигнемо мала кашњења и бољи проток.

ROSE
Прилагодили смо наш ROSE мотор, који смо првобитно изградили за опслуживање LLM-а, тако да такође рукује извршавањем модела уграђивања. Да бисмо минимизирали напор потребан за подршку модела уграђивања, ROSE агретивно поново користи код између LLM-а и уграђивања. На примјер, pplx-embed опслуживање и декодирање за Qwen3.5 LLM пролазе кроз исте кернеле. Ово дијељење нам омогућава да лако опслужујемо модел уграђивања који је првобитно фино подешен из LLM-а за прототипирање, евалуацију и производну инференцу.
За густе слојеве, уграђивање и LLM инференција су идентични пошто се векторски токени обрађују независно. У слојевима пажње, разлике се рјешавају додавањем подршке за разбарушене улазе, заједно са подешавањима пејџованог препуњавања и декодирања које захтијевају LLM-ови. Приликом опслуживања модела уграђивања, не инстанцирамо KV кеш и прослеђујемо варијацијама кернела пажње који подржавају разбарушени формат како бисмо избјегли попуњавање. Подржавајуће рутине за конверзију и калибрацију такође се дијеле са LLM-овима.
Ivy
Ivy, наш слој HTTP проксија за инференцу, такође игра важну улогу у перформансама. Пошто се кориснички садржаји захтјева разликују у производњи, усмјеравање појединачних захтјева ка појединачним репликама може изазвати неравнотежу оптерећења. Ivy дијели захтјеве великих серија на дијелове и балансира оптерећење између реплика, побољшавајући искоришћење и углађујући кашњење. Наш недавни рад на сопственој униграмској токенизацији, која је у потпуности примењена у Ivy-ју, драстично побољшава кашњења у поређењу са готовима токенизаторима.
...али кернели су и даље важни
ROSE подржава низ позадина пажње. Различити кернели могу бити прикладни за специфичне величине проблема. Временом смо интегрисали FlashInfer 2, FlashInfer 3 и FlashAttention 4 кернеле за имплементацију разбарушене пажње.

Уопштено говорећи, прихватамо да је FlashAttention 4 бржи. Међутим, FlashInfer 3 га надмашује на моделима заснованим на Qwen моделима при веома дугим дужинама секвенци. Пошто перформансе и подешавање могу варирати у зависности од броја и димензије глава пажње, одржавамо подршку за више конфигурација и доносимо одлуку од случаја до случаја при опслуживању.
Бенчмаркови
Вршимо бенчмарковање у односу на vLLM v0.22.0, извршавајући инференцу на BF16 прецизности на стварним тежинама модела и улазима изведеним из скупова података за евалуацију. Свим временским покретањима претходила су загревања која су потврдила да је дивергенција у косинусној сличности унутар 0,1%.
Уграђивања са ниским кашњењем (p50 / p90 / p99 / max ms)
Извјештавамо о рајнтајмима за величину серије унапријед токенизованог захтјева 1, потпуно секвенцијалне захтјеве, дужине секвенци од 128, 512 и 4096 токена.

Бодовање са ниским кашњењем (p50 / p90 / p99 / max ms)
Величности серија унапријед токенизованих захтјева 5, 25 и 50, дужина секвенце од 512 токена.

Уграђивања са високим протокoм (emb/s)
Величина серије захтјева 100, четири истовремена процеса шаљу захтјеве, дужине секвенци од 512, 1024 и 4096 токена.

Уграђивања са високом конкрентношћу (p50 / p90 / p99 / max ms)
Дужина секвенце 512, величина серије 1, али шаљемо 1, 2, 4, 8 и 16 истовремених захтјева. Овај бенчмарк такође укључује трошкове токенизације кроз Ivy, заједно са мрежним режијским трошковима између Ivy-ја и Тјулипа (Tulip).

Закључак и будући рад
Инфраструктура за опслуживање коју чине Ivy, Tulip и ROSE омогућава нам да опслужујемо уграђивања за Perplexity са мањим кашњењем и бољим протоком, што резултира тачнијом претрагом уз смањене трошкове у поређењу са готовима рјешењима.
Фокусирањем на одређене моделе и преузимањем власништва над цијелим стоком, стичемо слободу неопходну за успостављање ефикасне равнотеже између перформанси и флексибилности, мијешајући висок ниво поновно искористивих и перформативних Rust примитива заједно са генеричнијим Python кодom за моделирање. Многи мотори за инференцу отвореног кода, као што су vLLM, SGLang и TokenSpeed, интегришу језике попут Rust-а и C++ у свој сток. Уложили смо у Rust у протекле двије године и остварили смо велике користи у погледу перформанси и одрживости. Дијељењем већине имплементације уграђивања са нашим стоком за опслуживање LLM-а, такође остварујемо добитке у протоку, без потребе за значајним инжењерским напором за одржавање модела уграђивања.
Како се модели развијају, наставићемо да побољшавамо сваки слој нашег стока како бисмо смањили и кашњења везана за ЦПУ и кашњења везана за ГПУ. Наши прилагођени протоколи засновани на gRPC-у у оквиру Ivy-ја и Тјулипа (Tulip) омогућавају нам да подесимо комуникацију како бисмо смањили мрежна кашњења, док ROSE пружа основу за побољшање рачунског протока. Поред тога, како подршка за Python са слободним нитима буде расла широм екосистема, моћи ћемо да додатно побољшамо интероперабилност између Python-а и Rust-а како бисмо смањили режијске трошкове.