Szétválasztott előkitöltés és dekódolás
Az előkitöltés és a dekódolás eszközök közötti szétválasztása felgyorsítja az LLM-következtetést és javítja a teljesítményt
Annak érdekében, hogy a bemeneti promptból kimeneti tokeneket generáljunk, az LLM-következtetés két szakaszra oszlik: előkitöltésre (prefill) és dekódolásra. Az előkitöltés a bemeneti tokeneken fut, feltöltve a KV-gyorsítótárakat, mielőtt belépne a dekódolási szakaszba, amely tokenenként generál. Míg egyetlen dekódolási lépés általában néhány tíz milliszekundumig tart, az előkitöltés lényegesen hosszabb ideig. Ugyanazon az eszközön futtatva az előkitöltés és a dekódolás keverése rontja a dekódolási teljesítményt. Ebben a cikkben egy bevált megoldást vizsgálunk meg a szétválasztott előkitöltés és dekódolás formájában, amelyek külön eszközökön futnak, hogy maximalizálják az előkitöltési áteresztőképességet és a dekódolási késleltetéseket.
Előkitöltés vs. dekódolási teljesítmény
Egy tipikus LLM-kiszolgáló motorban a kötegelő-ütemező kiválasztja a feldolgozandó kéréseket a modell egyes végrehajtási lépéseiben. Egyetlen eszközön vagy csomóponton futtatva mind az előkitöltési, mind a dekódolási kérések kötegelve vannak. A szekvenciahossz mentén összesítő figyelem költsége mind az előkitöltés, mind a dekódolás esetében nő, arányosan a KV-gyorsítótár bejegyzéseinek hosszával (kv_len). A dekódolási kérések általában egyetlen tokent továbbítanak (qo_len=1), minimális költséggel a többi rétegen keresztül, amelyek függetlenül működnek a szekvencia tokenein. Az előkitöltési kérések ezer vagy tízezer tokent továbbítanak jelentős költséggel a sűrű rétegeken keresztül (nagy qo_len).
Az előrehaladó menet (forward pass) késleltetését erősebben befolyásolja a sűrű rétegeken átadott független tokenek száma (qo_len), mint a figyelem során a KV-gyorsítótárból lekért tokenek száma (kv_len). A figyelem képes párhuzamosítani mind a kérések száma, mind a szekvenciahosszal arányos kv_len szerint, jó kihasználtságot elérve. Az előkitöltés számítási korlátos (compute bound): mivel a qo_len magas, a GEMM-magok elegendő blokkot tudnak lefoglalni az M dimenzió mentén a modern GPU-k számítási képességeinek teljes kihasználásához. A dekódolás memóriakorlátos (memory bound): a jellemzően alacsony kötegméretek miatt az M mentén lévő bemenetek száma általában kicsi, ami csak egy blokkhoz elegendő. Míg a Split-K GEMM-magok javíthatják az SM kihasználtságát alacsony token-kötegméretek esetén, a gyorsítótárak és a mátrixszorzó egységek általában alulhasználtak maradnak.
Összekeverésük esetén az előkitöltési kéréseket tartalmazó kötegek nagyobb késleltetést okoznak az előrehaladó meneten keresztül, negatívan befolyásolva a teljes példány dekódolási áteresztőképességét. Míg az előkitöltési kérések és a dekódolási kérések keverése vagy a darabolt előkitöltés (chunked prefill) alkalmazása kissé javíthatja a dekódolási teljesítményt, nehéz elegendő előkitöltési áteresztőképességet fenntartani ahhoz, hogy elég kérést dolgozzunk fel egy példányon a dekódolási áteresztőképesség maximalizálásához. Nagy modellek és tipikus kimeneti hosszak esetén a dekódoláshoz szükséges nagy kötegméret fenntartásához az előkitöltést elég gyakran kell elvégezni ahhoz, hogy az jelentősen rontsa az átlagos késleltetést, és akadozást okozzon a kimenetben.
Ezeket a problémákat különálló csomópontkészlet használatával lehet kezelni az előkitöltés és a dekódolás elvégzéséhez. Ha egy előkitöltő csomópontot több dekódoló csomóponttal társítunk, elegendő kérés ütemezhető az előkitöltéshez az áteresztőképesség maximalizálása és a dekódoló csomópontokon elég sok egyidejű kérés fenntartása érdekében a dekódolási áteresztőképesség maximalizálásához is. Az előkitöltő csomópontok feltöltik a KV-gyorsítótárakat, amelyeket aztán átvisznek a dekódoló csomópontokra. Mivel a dekódolóknak többé nem kell megszakítaniuk a folyamatot az előkitöltés miatt, a késleltetések sokkal determinisztikusabbá válnak, mivel az aktív kérések növekvő kv_len-ének összhatása sokkal kevésbé szembetűnő. Ennek ára az első token generálásáig eltelt idő (TTFT) növekedése, mivel a KV-gyorsítótárak hálózaton keresztüli átvitele tíz- vagy százmilliszekundumot is igénybe vehet.
KV Messenger
A Perplexitynél a szétválasztott előkitöltés és dekódolás megvalósítása egy KV-üzenetküldő (messenger) köré épül, amely interakcióba lép az LLM-motorral, hogy hálózaton keresztül vezérelje a KV-gyorsítótár-átviteleket az előkitöltő csomópontokról a dekódoló csomópontokra. Az előkitöltő oldalon az üzenetküldő elfogadja a kéréseket a dekódoló csomópontoktól, átadja azokat a kötegelő-ütemezőnek, és nyomon követi az előrehaladó menet végrehajtását, hogy a lehető legkisebb késleltetéssel indítsa el a KV-gyorsítótárakat. A dekódoló oldalon, a ki nem lakoltatható lapok lefoglalása után, az üzenetküldő blokkolja a kérés dekódolásra történő ütemezését mindaddig, amíg értesítést nem kap a KV-gyorsítótár és a dekódoló kontextus átvitelének befejezéséről.
Az előkitöltés szétválasztása nagy áteresztőképességű, alacsony késleltetésű kapcsolatokat igényel, ezért megvalósításunkat RDMA-ra szabtuk, támogatva mind az EFA, mind a ConnectX hálózati illesztővezérlőket (NIC). A KV Messenger a libfabric-re épül, a fabric-lib burkolóinkat (wrapper) használva, hogy magasabb szintű, alacsony késleltetésű absztrakciókat biztosítson a távoli közvetlen memóriaelérés (RDMA) primitívjei felett, megvalósítva a hatékony lap- és metaadat-átviteleket, valamint az alacsony késleltetésű jelzést. A háttérben a fabric-lib koordinálja a GPU-t és a közvetlenül csatlakoztatott NIC-eket az adatok előkitöltő csomópontról a dekódoló csomópontra történő másolásához.
A kézhezvétel után az előkitöltő csomópont lefoglalja a forrás KV-lapok megfelelő készletét, és a helyi motor használatával ütemezi a kérést előkitöltésre. A késleltetés minimalizálása érdekében az átvitelek nem várják meg az előrehaladó menetet: ehelyett a KV-lapok másolása azonnal elindul, amint a modell befejezi a KV-gyorsítótár bejegyzések hozzáfűzését az egyes rétegek KV-gyorsítótárához. Mivel az előkitöltési kérések darabolhatók, a kötegelő-ütemező végrehajtás előtt értesíti a KV-üzenetküldőt az aktuálisan ütemezett darabokról. Annak érdekében, hogy támogatni tudja a CUDA-gráfokat, miközben képes nyomon követni a rétegeket, az üzenetküldő fenntart egy erre a célra szolgáló szálat, amely egy számlálót kérdez le (polling), amelyet a figyelem kimeneti vetülete után növelnek. A számláló csak a sharded környezet fő csomópontján van karbantartva: annak ellenére, hogy a KV-gyorsítótár bejegyzések érvényesek a hozzáfűzés után és a figyelem előtt, a kimeneti vetület csökkentve van a rangok között, implicit módon szinkronizálva azokat. Amikor a számláló változása észlelhető, az üzenetküldő értesítést kap, és meghívja a fabric-lib-et egy réteg átvitelének elindításához.
Az utolsó darab átvitelének befejezése után a további metaadatok másolása is megtörténik: a spekulatív dekódolás vagy az MTP megköveteli a logitek és rejtett állapotok átmozgatását a dekódolóra. Ezeket a másolatokat szintén RDMA-n keresztül hajtják végre, az előre lefoglalt pufferekbe és azokból.
Az utolsó darab összes függőben lévő átvitelének befejezésekor az előkitöltő csomópont felszabadítja (de-allocates) a KV-lapokat, és befejezi a kérést. A dekódoló csomópont nem kap explicit értesítést: ehelyett azonnali számlálókat használ a befejezett műveletek számának nyomon követésére. Az RDMA-műveletek száma az előkitöltő oldalon arányos átvitt lapok számával. A lap- és kontextusmásolások ismert számának befejezésekor a fabric-lib hívja a KV-üzenetküldőt, jelezve, hogy egy kérés készen áll a dekódolásra. Az üzenetküldő felszabadít minden kontextust, és átadja a kérést az LLM-motornak.
Sharded KV-gyorsítótár-átvitelek
Ha az előkitöltő és a dekódoló tenzorpárhuzamosságra (Tensor Parallelism - TP) támaszkodik, és azonos módon shardolja vagy replikálja a KV-gyorsítótárakat, egyetlen átviteli motor koordinálja több eszköz küldését és fogadását az összes replika lapjaihoz. Annak érdekében, hogy egyetlen üzenetküldő és átviteli motor legyen használható annak ellenére, hogy a modell végrehajtója több eszközön és folyamaton van replikálva, a cuMem és a cuMemImportFromShareableHandle szolgál a KV-gyorsítótárakat alátámasztó eszközmemória lefoglalására és a fő folyamatba való leképezésére. Az átviteli motor megvizsgálja a csomópont topológiáját, hogy megtalálja a KV-gyorsítótár-szeletek mindegyikének átviteléhez használható, a legközelebbi NUMA-csomópontban lévő NIC-eket és CPU-kat.
Ha a forrás és a cél azonos módon shardolt, az átvitelek triviálisak, mivel egy-az-egyhez leképezés létezik a forrás és a cél eszközei és lapjai között. Ebben a helyzetben a sharding implicit módon segíti az átviteli késleltetéseket: több GPU használatával több társított NIC alkalmazható, ami közelebb kerül a teljes sávszélesség-kihasználtsághoz. Ha azonban eltérés van, az átviteli motornak szét kell osztania vagy rekonstruálnia kell a lapokat a forrás- és célszeletek közötti arány függvényében.
Ha az előkitöltő több eszköz között osztja szét a KV-gyorsítótárat, a teljes lapok a dekódolón rekonstruálódnak a megfelelő feleknek az előkitöltő eszközökről történő elküldésével. Ha a dekódolónak több shardja van, akkor több forrástól fogad lapokat. A dekódolónak ismernie kell az előkitöltő sharding sémaszerkezetét ahhoz, hogy ki tudja számítani a várhatóan fogadandó RDMA-írások számát. Replikáció esetén az előkitöltő replikakészletekbe csoportosítja az eszközöket, amelyek magukon belül replikálják a teljes KV-gyorsítótárat. A cél-replikakészletekhez véletlenszerűen hozzárendelik az egyik forráskészletet, hogy az összes elérhető eszközt fel tudják használni az RDMA-írások elindításához.
A sharded átvitelek enyhe módosítást igényelnek a KV-gyorsítótárakban. Alapértelmezés szerint a FlashInfer az NHD elrendezésre támaszkodik, amely a fejeken belül rendezi a tokeneket egy lapon belül. Mivel a gyorsítótárak valószínűleg a figyelemfejek száma mentén vannak shardolva, ez egyenetlenséget hoz létre a fejen belül. Az RDMA-átvitelek nem támogatják implicit módon a lépésközös (strided) írásokat, fejenként egy műveletet igényelve az átvitel elvégzéséhez. Ehelyett, a libfabric-kel való interakciók számának csökkentése érdekében a KV-gyorsítótárakat az HND elrendezés használatával szervezzük meg, amely a fejdimenziót a tokenek száma elé helyezi. Ez biztosítja a folytonosságot, lehetővé téve egy lap átmásolását egyetlen írással.
Spekulatív dekódolás
A spekulatív dekódolás enyhe módosításokat igényel a szétválasztott előkitöltés-dekódolásban. Megvalósításunkban az előkitöltő csomópontok nem mintavételezhetnek tokeneket. Mivel a Perplexity Sonar modelljei támogatják a strukturált kimenetet, nem akarjuk felvállalni a sémaprocesszor-implementációk előkitöltők és dekódolók közötti szinkronizálásának összetértségét. Az MTP- és spekulatív dekódolási mechanizmusokban a vázlatmodell (draft model) utolsó tokenig történő előkitöltése magában foglalja a tokenek mintavételezését a célymodelltől.
Ezen problémák áthidalása érdekében az előkitöltés nem tartalmazza a bemeneti szekvencia utolsó tokenjét. Ehelyett az utolsó tokent megelőző előkitöltésből származó rejtett állapotok vagy logitek kerülnek átvitelre, és a dekódolón a következő lépésben dekódolási tokenként van kezelve. Bár ez kissé növeli a késleltetéseket, mivel az első token kiadásához teljes dekódolási lépést kell végrehajtani az előkitöltés után, a megvalósítás összetértsége nagymértékben lecsökken.
Szétválasztott üzembe helyezések
Több szétválasztott konfigurációt is üzembe helyeztünk, vagy kísérleteztünk velük különböző modellekkel, hogy támogatjuk a éles (production) adatforgalmat vagy a házon belüli kiértékelési munkaterheléseket. A modellek mérete és figyelemmechanizmusa alapján megfelelő sharding sémákat választottunk az előkitöltő és dekódoló csomópontok számára a GPU-k legjobb kihasználása érdekében.
DeepSeek-R1
A DeepSeek esetében mind a tenzorpárhuzamos (TP), mind az adatpárhuzamos (DP) üzembe helyezést megfontoltuk. Ahogy a korábbi blogbejegyzésekben tárgyaltuk, a TP üzembe helyezések jobb késleltetést biztosítanak az alacsonyabb áteresztőképesség árával, és több GPU-t igényelnek a nagy terhelés kiszolgálásához. A DP üzembe helyezések sokkal jobban skálázódnak a terheléssel, azonban a csúcs-áteresztőképességük alacsonyabb az eszközök vagy csomópontok közötti kommunikáció költsége miatt.
A DeepSeek többfejű látens figyelmre (Multi-Head Latent Attention) támaszkodik, tömörítve a KV-gyorsítótárakat. Mivel az összes KV-fej egyetlen látens vektorba van tömörítve, a TP nem tudja shardolni a KV-gyorsítótárakat, mivel ehelyett replikálnia kell a látens vektorokat az összes rangon. A sharding a dekompresszió után történik, mivel minden rang különböző fejeket tud kinyerni ugyanabból a látens reprezentációból. Következésképpen az összes KV-gyorsítótár-shard azonos az előkitöltő és a dekódoló shardok között.
Csomóponton belüli (intranode) TP-beállítással mind az előkitöltők, mind a dekódolók azonos módon vannak shardolva. Az átvitelek az összes rangról elindulnak az összes elérhető NIC teljes kihasználása érdekében. DP üzembe helyezés esetén azonban, ahol a TP-rang mérete kisebb, vagy minden DP-rang egyetlen GPU-hoz van rendelve, bármely olyan előkitöltő eszköz elküldheti, amely rendelkezik a KV-gyorsítótár replikált másolatával. A kérések összes elérhető NIC közötti kiegyensúlyozásához véletlenszerűen kiválasztunk egy GPU-t és egy NIC-et a KV-gyorsítótárnak az előkitöltőről a dekódolóra történő küldéséhez.
Vegyes előkitöltés-dekódolás mellett az R1 üzembe helyezésünk azzal küzdött, hogy következetesen túllépje az 50 TPS-t a százmilliszekundumos nagyságrendű, gyakori előkitöltési megszakítások miatt. Ezzel szemben az előkitöltés szétválasztásával kérésenként körülbelül 100 ms TTFT-büntetést szenvedtünk el, de egyetlen előkitöltő csomópont képes volt fenntartani a konzisztens kötegméreteket 3 dekódoló csomóponton, 90 TPS-t meghaladó áteresztőképességet biztosítva, miközben dekódoló csomópontonként körülbelül 1 QPS terhelést kezel. Adatpárhuzamos üzembe helyezések esetén a TPS kicsit alacsonyabb volt, 50 körül, azonban a példányok rangonként 1 QPS terhelést tudtak kezelni, csomópontonként 8 ranggal.
Qwen3-Coder
Ez a 480B-s modell csoportos lekérdezési figyelmet (Grouped-Query Attention - GQA) használ, így a figyelem könnyen shardolható, és élvezheti a tenzorpárhuzamosság előnyeit anélkül, hogy a KV-gyorsítótárak memóriáját feláldozná. Következésképpen a modellt 8 GPU között shardolhatjuk mind az előkitöltéshez, mind a dekódoláshoz, és körülbelül 3 dekódoló csomópontot párosíthatunk egyetlen előkitöltő csomóponttal. Mivel a figyelem shardolva van, az HND KV-gyorsítótár-elrendezésre támaszkodunk az előkitöltő és a dekódoló KV-gyorsítótárak shardolásához, párosítva az előkitöltő rangokat a dekódoló rangokkal, és teljesen kihasználva az összes NIC-et a szeletek párhuzamos átviteléhez.
Idézet
@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}
}