Disaggregert prefill og dekoding

Separering av prefill og dekoding på tvers av enheter gir raskere LLM-inferens og bedre ytelse

ForfatterePerplexity Research

For å generere utdatatokens fra en inndataprompt, deles LLM-inferens inn i to faser: prefill og dekoding. Prefill kjører på inndatatokenene og fyller KV-mellomlagre før den går over til dekodingsfasen som genererer tokens ett etter ett. Mens et enkelt dekodingstrinn vanligvis tar tivis av millisekunder, tar prefill betydelig lengre tid. Hvis det kjøres på de samme enhetene, vil blanding av prefill med dekoding svekke dekodingsytelsen. I denne artikkelen utforsker vi en etablert løsning i form av disaggregert prefill og dekoding, der de kjøres på separate enheter for å maksimere både prefill-gjennomstrømming og dekodingslatens.

Prefill vs. dekodingsytelse

I en typisk LLM-betjeningsmotor velger satsplanleggeren forespørsler som skal behandles i hvert kjøringstrinn for en modell. Ved kjøring på en enkelt enhet eller node blir både prefill- og dekodingsforespørsler samlet i satser. Kostnaden for attention, som aggregerer langs sekvenslengden, øker for både prefill og dekoding, proporsjonalt med lengden på oppføringene i KV-mellomlageret (kv_len). Dekodingsforespørsler videresender vanligvis et enkelt token (qo_len=1), til en minimal kostnad gjennom andre lag som opererer uavhengig på tokens i en sekvens. Prefill-forespørsler videresender tusenvis eller titusenvis av tokens til en betydelig kostnad gjennom tette lag (stor qo_len).

Latensen for en framoverpasning påvirkes sterkere av antall uavhengige tokens som sendes gjennom tette lag (qo_len) enn av antall tokens som hentes fra KV-mellomlageret under attention (kv_len). Attention kan parallellisere både på tvers av antall forespørsler og kv_len proporsjonalt med sekvenslengder, noe som gir god utnyttelse. Prefill er beregningsbundet: Siden qo_len er høy, kan GEMM-kjerner allokere tilstrekkelig med blokker langs M-dimensjonen til å utnytte beregningskapasiteten til moderne GPU-er fullt ut. Dekoding er minnebundet: På grunn av typisk lave satsstørrelser er antall inndata langs M vanligvis lite, nok til bare én blokk. Selv om Split-K GEMM-kjerner kan forbedre SM-utnyttelsen for lave token-satsstørrelser, forblir mellomlagrene og matrisemultiplikasjonsenhetene typisk underutnyttet.

Forespørselsbatching gjennom et transformerlag, som viser spørrings- og nøkkel/verdi-projeksjoner, oppmerksomhet og oppdateringer av KV-bufferen.

Når de blandes sammen, fører satser som inneholder forespørsler om prefill, til høyere latens gjennom framoverpasningen, noe som påvirker dekodingsgjennomstrømmingen for hele instansen negativt. Selv om det å blande prefill-forespørsler med dekodingsforespørsler eller bruke oppdelt prefill kan forbedre dekodingsytelsen noe, er det vanskelig å opprettholde tilstrekkelig prefill-gjennomstrømming til å behandle nok forespørsler på en instans til å maksimere dekodingsgjennomstrømmingen. Når det gjelder store modeller med typiske utdatalengder, må prefill utføres ofte nok til å opprettholde en stor satsstørrelse for dekoding, slik at den gjennomsnittlige latensen reduseres betydelig og det oppstår hakking i utdataene.

Blandet planlegging av prefill og decode for tre forespørsler, som viser prefill- og decode-intervaller på en delt tidslinje.
Separert planlegging av prefill og decode, med en dedikert prefiller som mater decode-arbeid for tre forespørsler.

Disse problemene kan løses ved å bruke eit separat sett med noder til å utføre prefill og dekoding. Ved å knytte en prefill-node til flere dekodingsnoder kan det planlegges et tilstrekkelig antall forespørsler for prefill til å maksimere gjennomstrømmingen og opprettholde et stort nok antall samtidige forespørsler på dekodingsnodene til også å maksimere dekodingsgjennomstrømmingen. Prefill-nodene fyller KV-mellomlagrene, som deretter overføres til dekodingsnodene. Siden dekoderne ikke lenger trenger å gjøre opphold for prefill, blir latensen mye mer deterministisk, ettersom den generelle innvirkningen av voksende kv_len for aktive forespørsler er mye mindre utpreget. Kostnaden betales i form av økt tid til første token (TTFT), siden overføringen av KV-mellomlagre over nettverket kan ta fra ti til hundrevis av millisekunder.

KV Messenger

Hos Perplexity er vår implementering for disaggregert prefill og dekoding bygget rundt en KV-meldingstjeneste som samhandler med LLM-motoren for å organisere overføringer av KV-mellomlager fra prefill-noder til dekodingsnodene via et nettverk. På prefill-siden godtar meldingstjenesten forespørsler fra dekodingsnoder, sender dem videre til satsplanleggeren og holder oversikt over kjøringen av framoverpasningen for å sende KV-mellomlagre med minst mulig latens. På dekodingssiden, etter at sider som ikke kan kastes ut, er allokert, blokkerer meldingstjenesten forespørselen fra å bli planlagt for dekoding til den blir varslet om at overføringene av KV-mellomlager og dekodingskontekst er fullført.

En separert forespørsel-livssyklus, som viser koordineringen mellom dekoderen og prefillereren og overføringen av KV-bufferdata.

Disaggregering av prefill krever tilkoblinger med høy gjennomstrømming og lav latens, og derfor er implementeringen vår skreddersydd for RDMA, med støtte for både EFA- og ConnectX-nettverksgrensesnittkort (NIC). KV Messenger er bygget på libfabric ved hjelp av våre fabric-lib-omslag for å tilby abstraksjoner på høyt nivå med lav latens over RDMA-primitiver (Remote Direct Memory Access), og implementerer effektiv side- og metadataoverføring samt signalisering med lav latens. I bakgrunnen koordinerer fabric-lib en GPU og dens direkte tilkoblede NIC-er for å kopiere data fra prefill-noden til dekodingsnoden.

Ved mottak allokerer prefill-noden et tilsvarende sett med kilde-KV-sider og planlegger forespørselen for prefill ved hjelp av sin lokale motor. For å minimere latens venter ikke overføringer på framoverpasningen: i stedet startes kopiering av KV-sider så snart modellen er ferdig med å legge til KV-mellomlager-oppføringer i KV-mellomlageret for individuelle lag. Siden prefill-forespørsler kan deles opp i deler, varsler satsplanleggeren KV-meldingstjenesten om de for øyeblikket planlagte delene før kjøring. For å støtte CUDA-grafer samtidig som man kan spore lag, holder meldingstjenesten en dedikert tråd som avspør en teller som økes etter utdataprojeksjonen for attention. Telleren opprettholdes bare på hovednoden i et sharded miljø: selv om KV-mellomlager-oppføringene er gyldige etter tilføyelse og før attention, reduseres utdataprojeksjonen på tvers av ranger, slik at de synkroniseres implisitt. Når en endring i telleren observeres, blir meldingstjenesten varslet og den ringer fabric-lib for å starte overføringen av et lag.

KV-bufferoverføringer lag for lag, som viser sideinndelte og enkeltstående skrivinger samt fullføringstellere mellom prefillereren og dekoderen.

Når overføringen av den siste delen er fullført, kopieres også eventuelle ekstra metadatakopier: spekulativ dekoding eller MTP krever at logits og skjulte tilstander flyttes til dekoderen. Disse kopieringene utføres også gjennom RDMA, til og fra forhåndsallokerte buffere.

Ved fullføring av alle ventende overføringer av den siste delen deallokerer prefill-noden KV-sidene og fullfører forespørselen. Dekodingsnoden varsles ikke eksplisitt: i stedet bruker den umiddelbare tellere til å holde oversikt over antall fullførte operasjoner. Antallet RDMA-operasjoner på prefill-siden er proporsjonalt med antall overførte sider. Ved fullføring av det kjente antallet side- og kontekstkopier ringer fabric-lib KV-meldingstjenesten for å indikere at en forespørsel er klar for dekoding. Meldingstjenesten deallokerer all kontekst og overleverer forespørselen til LLM-motoren.

Sharded KV-mellomlageroverføringer

Hvis prefilleren og dekoderen er avhengige av tensor-parallelle (TP) oppsett og shardet eller replikerer KV-mellomlagrene identisk, koordinerer en enkelt overføringsmotor flere enheter for å sende og motta sidene for alle replikaene. For å kunne bruke en enkelt meldingstjeneste og overføringsmotor til tross for at modellens eksekutor er replikert på tvers av flere enheter og prosesser, brukes cuMem og cuMemImportFromShareableHandle til å allokere enhetsminnet som ligger til grunn for KV-mellomlagrene, og til å tilordne det til hovedprosessen. Overføringsmotoren inspiserer nodens topologi for å finne NIC-ene og CPU-ene i nærmeste NUMA-node som skal brukes til overføringene for hver av KV-mellomlagerskivene.

Hvis kilden og destinasjonen shardes identisk, er overføringer enkle, siden det er en-til-en-tilordning fra kildens og destinasjonens enheter og sider. I denne situasjonen bidrar sharding implisitt til overføringslatenser: ved å bruke flere GPU-er kan flere tilknyttede NIC-er tas i bruk, slik at man kommer nærmere full utnyttelse av båndbredden. Hvis det imidlertid er et avvik, må overføringsmotoren dele opp eller rekonstruere sider avhengig av forholdet mellom kilde- og destinasjonsskiver.

Topologi for KV-bufferoverføring mellom en TP/EP-prefiller og DP/EP-dekodere, som viser NIC-tilkoblingene.

Hvis prefilleren deler opp KV-mellomlageret på flere enheter, rekonstrueres hele sider på dekoderen ved å sende de tilsvarende halvdelene ut fra prefill-enhetene. Hvis dekoderen har flere sharder, mottar den sider fra flere kilder. Dekoderen må kjenne til sharding-skjemaet for prefilleren for å kunne beregne antall RDMA-skrivinger den forventer å motta. Hvis replikering er involvert, grupperer prefilleren enhetene i replikasett som replikerer hele KV-mellomlageret seg imellom. Destinasjonsreplikasett blir tilfeldig tildelt et av kildesettene for å kunne bruke alle tilgjengelige enheter til å starte RDMA-skrivinger.

NHD- og HND-oppsett for KV-buffer, som kontrasterer rekkefølgen av tokens og hoder innenfor hvert lag og hver side.

Sharded overføringer krever en liten justering av KV-mellomlagre. Som standard er FlashInfer avhengig av NHD-oppsettet, som ordner tokene i en side innenfor hodene. Siden mellomlagrene mest sannsynlig er shardet langs antall attention-hoder, skaper dette diskontinuitet innenfor hodet. RDMA-overføringer støtter ikke implisitt strided skrivinger, noe som krever én operasjon per hode for å utføre overføringen. For å redusere antall interaksjoner med libfabric organiserer vi i stedet KV-mellomlagre ved hjelp av HND-oppsettet, som plasserer hodedimensjonen før antall tokens. Dette sikrer kontinuitet og gjør det mulig å kopiere en side med en enkelt skriving.

Spekulativ dekoding

Spekulativ dekoding krever små justeringer av disaggregert prefill-dekoding. I vår implementering har ikke prefill-noder lov til å sample tokens. Siden Sonar-modellene til Perplexity støtter strukturert utdata, ønsker vi ikke å pådra oss kompleksiteten med å synkronisere skjemaprosessorimplementeringene på tvers av prefill-noder og dekodere. I MTP- og de spekulative dekodingsmekanismene innebærer det å forhåndsfylle utkastmodellen opp til det siste tokenet, å sample tokens fra målmodellen.

Spekulativ dekoding på tvers av prefillereren og dekoderen, som viser lengden på KV-bufferen, logits og prøvetaking av tokens.

For å omgå disse problemene inkluderer ikke prefill det siste tokenet i inndatasekvensen. I stedet overføres skjulte tilstander eller logits fra prefill foran det siste tokenet, og det behandles som et dekodings-token i neste trinn på dekoderen. Selv om dette øker latensen noe, ettersom det må utføres et fullt dekodingstrinn etter prefill for å utstede det første tokenet, reduseres kompleksiteten i implementeringen betraktelig.

Disaggregerte distribusjoner

Vi har distribuert eller eksperimentert med flere disaggregerte konfigurasjoner med forskjellige modeller for å støtte enten produksjonstrafikk eller interne evalueringsarbeidsbelastninger. Basert på størrelsen og attention-mekanismen til modellene valgte vi egnede sharding-skjemaer for prefill- og dekodingsnoder for å utnytte GPU-er best mulig.

DeepSeek-R1

Med DeepSeek vurdert vi både tensor-parallelle (TP) og dataparallelle (DP) distribusjoner. Som diskutert i tidligere blogginnlegg gir TP-distribusjoner bedre latens på bekostning av lavere gjennomstrømming, noe som krever flere GPU-er for å betjene tung trafikk. DP-distribusjoner skalerer mye bedre med lasten, men toppgjennomstrømmingen deres er lavere på grunn av kostnaden for kommunikasjon mellom enheter eller noder.

DeepSeek baserer seg på Multi-Head Latent Attention, som komprimerer KV-mellomlagre. Siden alle KV-hodene er komprimert til en enkelt latent vektor, kan ikke TP sharde KV-mellomlagrene, ettersom den i stedet må replikere de latente vektorene på alle ranger. Sharding skjer etter dekompresjon, siden hver rang kan trekke ut forskjellige hoder fra den samme latente representasjonen. Følgelig er alle KV-mellomlagersharder identiske på tvers av både prefill- og dekodingssharder.

Med et intranode TP-oppsett er både prefillere og dekodere shardet identisk. Overføringer sendes ut fra alle ranger for å utnytte alle tilgjengelige NIC-er fullt ut. Men med en DP-distribusjon, der TP-rangstørrelsen er lavere eller hver DP-rang er tilordnet en enkelt GPU, kan en hvilken som helst prefill-enhet som har en replikert kopi av KV-mellomlageret, sende det ut. For å balansere forespørsler på tvers av alle tilgjengelige NIC-er velger vi tilfeldig en GPU og et NIC for å sende KV-mellomlageret fra prefilleren til dekoderen.

Med blandet prefill-dekoding slet R1-distribusjonen vår med å konsekvent overgå 50 TPS på grunn av hyppige prefill-avbrudd i størrelsesorden hundrevis av millisekunder. Til sammenligning pådro vi oss en straff på ca. 100 ms til TTFT for hver forespørsel ved å separere prefill, men en enkelt prefill-node kunne opprettholde konsekvente satsstørrelser på 3 dekodingsnoder, noe som ga en gjennomstrømming på over 90 TPS samtidig som den håndterte en last på ca. 1 QPS per dekodingsnode. Med dataparallelle distribusjoner var TPS litt lavere på rundt 50, men instansene kunne håndtere en last på 1 QPS per rang, med 8 ranger per node.

Qwen3-Coder

Denne 480B-modellen bruker Grouped-Query Attention (GQA), slik at attention enkelt kan shardes og dra nytte av tensor-parallelisme uten å offere minne for KV-mellomlagre. Følgelig kunne vi sharde modellen på tvers av 8 GPU-er for både prefill og dekoding, og koble rundt 3 dekodingsnoder med en enkelt prefill-node. Siden attention er shardet, stoler vi på HND KV-mellomlager-oppsettet for å sharde prefill- og dekodings-KV-mellomlagre, koble prefill-ranger med dekodingsranger og utnytte alle NIC-er fullt ut til å overføre skiver parallelt.

Sitat

BibTeX
@online{perplexity2025DisaggregatedPrefillAndDecode,
  author = {{Perplexity} and Nandor Licker and Lequn Chen and Kevin Hu and Erin McKnight and Phi Hoang},
  title = {{Disaggregated prefill and decode}},
  date = {2025-08-01},
  url = {https://perplexity.coolkeypoint.com/hub/blog/disaggregated-prefill-and-decode}
}