Oddělený prefill a dekódování

Oddělení prefillu a dekódování napříč zařízeními urychluje inferenci LLM a zvyšuje výkon

AutořiPerplexity Research

Aby bylo možné generovat výstupní tokeny ze vstupního promptu, je inferenční fáze LLM rozdělena na dvě části: předběžné plnění (prefill) a dekódování. Prefill zpracovává vstupní tokeny a naplňuje KV mezipaměti (cache), než dojde k fázi dekódování, která generuje tokeny postupně jeden po druhém. Zatímco jeden krok dekódování obvykle trvá desítky milisekund, prefill zabírá podstatně déle. Pokud by probíhal na stejných zařízeních, kombinace prefillu a dekódování by snižovala výkon dekódování. V tomto článku se zabýváme osvědčeným řešením v podobě odděleného prefillu a dekódování, které běží na samostatných zařízeních s cílem maximalizovat propustnost prefillu i latenci dekódování.

Výkon prefillu vs. dekódování

V typickém obslužném enginu LLM vybírá dávkový plánovač (batch scheduler) požadavky ke zpracování v každém kroku provádění modelu. Při běhu na jediném zařízení nebo uzlu jsou požadavky na prefill i dekódování dávkovány společně. Náklady na mechanismus pozornosti (attention), které rostou s délkou sekvence, se u prefillu i dekódování zvyšují úměrně délce položek v KV mezipaměti (kv_len). Požadavky na dekódování obvykle zpracovávají jediný token (qo_len=1) s minimálními náklady v ostatních vrstvách, které pracují nezávisle na t okenech sekvence. Požadavky na prefill posílají tisíce až desítky tisíc tokenů s významnými náklady v hustých vrstvách (velké qo_len).

Latence průchodu vpřed (forward pass) je silněji ovlivněna počtem nezávislých tokenů procházejících hustými vrstvami (qo_len) než počtem tokenů načtených z KV mezipaměti během mechanismu pozornosti (kv_len). Pozornost lze paralelizovat jak napříč počtem požadavků, tak vůči kv_len úměrně délce sekvencí, čímž se dosahuje dobrého využití. Prefill je limitován výpočetním výkonem (compute bound): vzhledem k vysoké hodnotě qo_len mohou jádra GEMM alokovat dostatečné bloky v dimenzi M pro plné využití výpočetních schopností moderních GPU. Dekódování je limitováno pamětí (memory bound): kvůli obvykle nízkým velikostem dávek je počet vstupů v dimenzi M malý a stačí pouze pro jeden blok. Přestože jádra Split-K GEMM mohou zlepšit využití SM (streaming multiprocessor) u nízkých velikostí dávek tokenů, mezipaměti a jednotky pro násobení matic obvykle zůstávají podvyužité.

Dávkování požadavků přes vrstvu transformátoru, znázorňující projekce query a key/value, attention a aktualizace KV cache.

Při vzájemném promíchání vedou dávky obsahující požadavky na prefill k vyšší latenci průchodu vpřed, což negativně ovlivňuje propustnost dekódování celé instance. Přestože kombinace požadavků na prefill s požadavky na dekódování nebo použití rozděleného prefillu (chunked prefill) může výkon dekódování mírně zlepšit, je obtížné udržet dostatečnou propustnost prefillu ke zpracování dostatečného počtu požadavků na instanci tak, aby se maximalizovala propustnost dekódování. V případě velkých modelů s typickými délkami výstupu je nutné kvůli udržení velké velikosti dávky pro dekódování provádět prefill tak často, že to výrazně snižuje průměrnou latenci a způsobuje trhání výstupu.

Smíšené plánování prefill–decode pro tři požadavky, znázorňující intervaly prefill a decode na sdílené časové ose.
Oddělené plánování prefill–decode s vyhrazeným prefillerem, který zásobuje práci decode pro tři požadavky.

Tyto problémy lze vyřešit použitím samostatné sady uzlů pro provádění prefillu a dekódování. Propojením jednoho prefililačního uzlu s více dekodérovými uzly lze naplánovat dostatečný počet požadavků na prefill pro maximalizaci propustnosti a zároveň udržet dostatečně velký počet souběžných požadavků na dekodérových uzlech pro maximalizaci propustnosti dekódování. Prefililační uzly naplní KV mezipaměti, které jsou následně přeneseny do dekodérových uzlů. Jelikož dekodéry již nemusí přerušovat činnost kvůli prefillu, latence jsou mnohem determinističtější, protože celkový dopad rostoucího kv_len u aktivních požadavků je mnohem méně výrazný. Cenou za to je nárůst metriky Time to First Token (TTFT), protože přenos KV mezipamětí po síti může trvat desítky až stovky milisekund.

KV Messenger

V Perplexity je naše implementace odděleného prefillu a dekódování postavena kolem systému KV messenger, který interaguje s enginem LLM a řídí přenosy KV mezipamětí z prefililačních uzlů do dekodérových uzlů přes síť. Na straně prefillu přijímá messenger požadavky od dekodérových uzlů, předává je dávkovému plánovači a sleduje provádění průchodu vpřed, aby odeslal KV mezipaměti s co nejmenší latencí. Na straně dekodéru, po alokaci stránek, které nelze vyjmout z paměti, messenger blokuje plánování požadavku na dekódování, dokud není upozorněn na dokončení přenosů KV mezipaměti a kontextu dekodéru.

Oddělený životní cyklus požadavku, znázorňující koordinaci mezi decoderem a prefillerem a přenos dat KV cache.

Oddělení prefillu vyžaduje vysokou propustnost a připojení s nízkou latencí, proto je naše implementace přizpůsobena pro RDMA a podporuje síťová rozhraní EFA i ConnectX (NIC). Systém KV Messenger je postaven na knihovně libfabric a využívá naše obaly fabric-lib k poskytování abstraktních vrstev s nízkou latencí a vyšší úrovní nad primitivy RDMA (Remote Direct Memory Access), což zajišťuje efektivní přenosy stránek a metadat spolu s pohnutkami o nízké latenci. Na pozadí fabric-lib koordinuje GPU a jeho přímo připojená síťová rozhraní (NIC) za účelem kopírování dat z prefililačního uzlu do dekodérového uzlu.

Po přijetí alokuje prefililační uzel odpovídající sadu zdrojových stránek KV a naplánuje požadavek na prefill pomocí lokálního enginu. Aby se minimalizovala latence, přenosy nečekají na dokončení průchodu vpřed: místo toho se kopírování stránek KV spustí, jakmile model dokončí připojování záznamů KV mezipaměti pro jednotlivé vrstvy. Jelikož požadavky na prefill mohou být rozděleny na části (chunked), dávkový plánovač informuje KV messenger o aktuálně naplánovaných částech před jejich spuštěním. Kvůli podpoře grafů CUDA a současné schopnosti sledovat vrstvy udržuje messenger vyhrazené vlákno dotazující se na čítač, který se inkrementuje po výstupní projekci mechanismu pozornosti. Čítač je udržován pouze na hlavním uzlu v distribuovaném prostředí: přestože záznamy KV mezipaměti jsou platné po připojení a před mechanismem pozornosti, výstupní projekce je redukována napříč hodnostmi (ranks), čímž se implicitně synchronizují. Jakmile je zaznamenána změna v čítači, messenger je upozorněn a zavolá fabric-lib k zahájení přenosu vrstvy.

Přenosy KV cache po jednotlivých vrstvách, znázorňující stránkované a jednorázové zápisy a čítače dokončení mezi prefillerem a decoderem.

Po dokončení přenosu poslední části jsou zkopírována i veškerá dodatečná metadata: spekulativní dekódování nebo MTP vyžadují přesun logitů a skrytých stavů do dekodéru. Tyto kopie se rovněž provádějí prostřednictvím RDMA do a z předem alokovaných vyrovnávacích pamětí (buffers).

Po dokončení všech probíhajících přenosů poslední části uvolní prefililační uzel stránky KV a dokončí požadavek. Dekodérový uzel není výslovně upozorněn: místo toho používá okamžité čítače ke sledování počtu dokončených operací. Počet operací RDMA na straně prefillu je úměrný počtu přenesených stránek. Po dokončení známého počtu kopírování stránek a kontextu zavolá fabric-lib KV messenger, čímž signalizuje, že požadavek je připraven k dekódování. Messenger uvolní kontext a předá požadavek enginu LLM.

Přenosy distribuované KV mezipaměti (Sharded KV Cache)

Pokud prefill a dekodér spoléhají na paralelismus tenzorů (Tensor Parallelism – TP) a rozdělují nebo replikují KV mezipaměti totožně, jediný přenosový engine koordinuje více zařízení pro odesílání a přijímání stránek všech replik. Aby bylo možné použít jeden messenger a přenosový engine navzdory tomu, že vykonavatel modelu je replikován na více zařízeních a procesech, používají se cuMem a cuMemImportFromShareableHandle k alokaci paměti zařízení, která tvoří KV mezipaměť, a k jejímu namapování do hlavního procesu. Přenosový engine prozkoumá topologii uzlu, aby našel síťová rozhraní (NIC) a procesory (CPU) v nejbližším uzlu NUMA, které se použijí pro přenos jednotlivých řezů KV mezipaměti.

Pokud se zdroj a cíl rozdělují identicky, jsou přenosy triviální, protože existuje vzájemné přiřazení 1:1 mezi zařízeními a stránkami zdroje a cíle. V této situaci sharding implicitně pomáhá latenci přenosu: použitím více GPU lze zapojit více souvisejících síťových rozhraní (NIC), čímž se dosáhne bližšího plného využití propustnosti. Pokud však dojde k neshodě, přenosový engine musí stránky rozdělit nebo rekonstruovat v závislosti na poměru mezi zdrojovými a cílovými řezy.

Topologie přenosu KV cache mezi TP/EP prefillerem a DP/EP decodery, znázorňující propojení NIC.

Pokud prefill rozděluje KV mezipaměť napříč více zařízeními, úplné stránky jsou na dekodéru rekonstruovány odesláním odpovídajících polovin ze zařízení prefillu. Pokud má dekodér více šardů, přijímá stránky z více zdrojů. Dekodér musí znát schéma rozdělení prefillu, aby mohl spočítat počet zápisů RDMA, které má obdržet. Pokud dochází k replikaci, prefill seskupí zařízení do sad replik, které v sobě replikují celou KV mezipaměť. Cílovým sadám replik je náhodně přiřazena jedna ze zdrojových sad, aby se k zahájení zápisů RDMA využila všechna dostupná zařízení.

Rozvržení KV cache NHD a HND, porovnávající uspořádání tokenů a hlav v rámci každé vrstvy a stránky.

Distribuované přenosy vyžadují mírnou úpravu KV mezipamětí. FlashInfer standardně spoléhá na rozvržení NHD, které řadí tokeny v rámci stránky v hlavičkách (heads). Jelikož jsou mezipaměti nejčastěji rozděleny podle počtu hlav pozornosti, vytváří to nespojitost uvnitř hlavičky. Přenosy RDMA implicitně nepodporují zápisy s krokováním (strided writes), což vyžaduje jednu operaci na hlavičku k provedení přenosu. Místo toho, abychom omezili počet interakcí s libfabric, organizujeme KV mezipaměti pomocí rozvržení HND, které umisťuje dimenzi hlavičky před počet tokenů. To zajišťuje spojitost a umožňuje zkopírovat stránku jediným zápisem.

Spekulativní dekódování

Spekulativní dekódování vyžaduje drobné úpravy odděleného prefillu a dekódování. V naší implementaci nemají prefililační uzly povoleno vzorkovat tokeny. Jelikož modely Sonar od Perplexity podporují strukturovaný výstup, nechceme podstupovat složitost synchronizace implementace procesoru schémat napříč prefillery a dekodéry. V mechanismech MTP a spekulativního dekódování zahrnuje předběžné plnění návrhového modelu (draft model) až do posledního tokenu vzorkování tokenů z cílového modelu.

Spekulativní dekódování napříč prefillerem a decoderem, znázorňující délku KV cache, logits a vzorkování tokenů.

Abychom se těmto problémům vyhnuli, prefill nezahrnuje poslední token vstupní sekvence. Místo toho jsou přeneseny skryté stavy nebo logity z prefillu předcházejícího poslednímu tokenu a v dalším kroku na dekodéru je s ním zacházeno jako s tokenem dekódování. Ačkoli to mírně zvyšuje latenci, protože po prefillu je nutné provést plný krok dekódování k vydání prvního tokenu, složitost implementace se tím výrazně snižuje.

Oddělená nasazení

Nasadili jsme nebo experimentovali s více oddělenými konfiguracemi s různými modely, abychom podpořili buď produkční provoz, nebo interní vyhodnocovací zátěže. Na základě velikosti a mechanismu pozornosti modelů jsme zvolili vhodná schémata shardingu pro prefililační a dekodérové uzly, abychom co nejlépe využili GPU.

DeepSeek-R1

U modelu DeepSeek jsme zvažovali nasazení pomocí paralelismu tenzorů (TP) i datového paralelismu (DP). Jak bylo diskutováno v předchozích příspěvcích na blogu, nasazení TP poskytuje lepší latenci za cenu nižší propustnosti a vyžaduje více GPU pro obsluhu vysoké zátěže. Nasazení DP se škáluje s rostoucí zátěží mnohem lépe, avšak jeho špičková propustnost je nižší kvůli nákladům na komunikaci mezi zařízeními nebo uzly.

DeepSeek spoléhá na mechanismus Multi-Head Latent Attention, který komprimuje KV mezipaměti. Vzhledem k tomu, že všechny KV hlavy jsou zkomprimovány do jediného latentního vektoru, nemůže TP rozdělit KV mezipaměti pomocí shardingu, ale musí latentní vektory replikovat na všech zařízeních. Sharding probíhá až po dekompresi, protože každá úroveň může extrahovat různé hlavy ze stejné latentní reprezentace. V důsledku toho jsou všechny šardy KV mezipaměti identické napříč šardy prefillu i dekodéru.

V případě vnitrouzlového nastavení TP jsou prefillery i dekodéry rozděleny identicky. Přenosy jsou spouštěny ze všech pozic, aby se plně využila všechna dostupná síťová rozhraní (NIC). U nasazení s DP, kde je velikost hodnosti TP menší nebo je každé hodnosti DP přiřazeno jediné GPU, však může přenos odeslat libovolné zařízení prefillu, které drží replikovanou kopii KV mezipaměti. Pro vyvážení požadavků napříč všemi dostupnými síťovými rozhraními náhodně vybíráme GPU a síťové rozhraní pro odeslání KV mezipaměti z prefillu do dekodéru.

S kombinovaným prefillem a dekódováním se naše nasazení R1 potýkalo s problémem trvale překročit 50 TPS kvůli častým přerušením prefillu v řádu stovek milisekund. Naproti tomu oddělením prefillu nám vznikla penalizace přibližně 100 ms na TTFT pro každý požadavek, avšak jediný prefililační uzel dokázal udržet konzistentní velikosti dávek na 3 dekodérových uzlech a poskytnout propustnost přesahující 90 TPS při zvládání zátěže přibližně 1 QPS na dekodérový uzel. U nasazení s datovým paralelismem byla propustnost TPS o něco nižší, kolem 50, avšak instance dokázaly zpracovat zátěž 1 QPS na jednu hodnost, přičemž na jeden uzel připadalo 8 hodností.

Qwen3-Coder

Tento model se 480 miliardami parametrů využívá mechanismus Grouped-Query Attention (GQA), takže pozornost lze snadno rozdělit a těžit z paralelismu tenzorů bez obětování paměti pro KV mezipaměti. V důsledku toho jsme mohli model rozdělit mezi 8 GPU pro prefill i dekódování a spárovat přibližně 3 dekodérové uzly s jedním prefililačním uzlem. Jelikož je pozornost rozdělena, spoléháme na rozvržení KV mezipaměti HND pro rozdělení KV mezipamětí prefillu a dekodéru, přičemž párujeme pozice prefillu s pozicemi dekodéru a plně využíváme všechna síťová rozhraní k paralelnímu přenosu řezů.

Citace

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}
}