RDMA punkt-till-punkt-kommunikation för LLM-system

Elegant verktyg för att hantera nya LLM-kommunikationsmönster

FörfattarePerplexity Research

LLM-system har traditionellt förlitat sig på kollektiv kommunikation för strukturerade parallella mönster vid träning och inferens. Nya arbetslaster, som disaggregerad inferens, Mixture-of-Experts (MoE)-routing och asynkron finjustering med förstärkningsinlärning, kräver dock flexibiliteten hos punkt-till-punkt-kommunikation.

Vår senaste forskningsartikel presenterar TransferEngine, ett portabelt RDMA-baserat kommunikationsbibliotek som fungerar över olika nätverksgränssnittskort (NIC), inklusive NVIDIA ConnectX-7 och AWS Elastic Fabric Adapter (EFA). Vi visar dess effektivitet i tre produktionssystem hos Perplexity AI:

Disaggregerad förfyllning och avkodning: Lager-för-lager-överföring av KvCache med sidinmatade skrivningar (paged writes) och UVM-övervakare, vilket gör att förfyllnings- och avkodningsinstanser kan skalas oberoende av varandra.

Viktöverföring för RL-efterträning på under 2 sekunder: Blixtsnabb viktsynkronisering mellan tränings- och inferensnoder.

Möjliggör biljonparametersmodeller på AWS EFA: Svarstidsoptimerade (low-latency) MoE-dispatch- och kombineringsoperationer med scatter- och barrier-primitiver, vilket ger branschledande svarsstider på ConnectX-7 och den första fungerande implementeringen som är kompatibel med AWS EFA.

Den här artikeln fokuserar på själva TransferEngine, det kommunikationsbibliotek som driver dessa system. Läs hela forskningsartikeln på arXiv och utforska koden på GitHub.

Motivation

Kollektiv kommunikation utmärker sig vid strukturerade parallella mönster som tensorparallelism och dataparallellism, men den medför begränsningar som begränsar dess tillämpbarhet på nya arbetslaster:

Fast medlemskap: Kollektiv kommunikation kräver att en global kommunikationsvärld upprättas vid initieringstiden, med synkroniserad inställning över alla deltagare. För disaggregerad inferens är detta problematiskt eftersom antalet förfyllnings- och avkodningsinstanser måste skalas dynamiskt som svar på trafikmönster.

Enhetliga tensorformer: Kollektiv kommunikation förutsätter att alla deltagare utbyter tensorer med identiska former och datatyper. Vid MoE-routing är antalet token som skickas till varje expert databeroende och upptäcks vid körtid, vilket tvingar täta all-to-all-kollektiv att utföra utfyllnad (padding) till värsta-fallet-buffertstorlekar.

Operationsordning: Kollektiva API:er upprätthåller synkroniserad semantik, även när applikationer bara behöver veta att en uppsättning operationer har slutförts. Till exempel kan alla sidor från förfyllningsinstansen till avkodningsinstansen i KvCache-överföring ankomma i valfri ordning. På samma sätt kan alla vikt-tensorer vid RL-viktuppdateringar överföras oberoende utan ordningsbegränsningar.

RDMA-baserad punkt-till-punkt-kommunikation hanterar naturligt dessa begränsningar, men tre hinder har förhindrat dess införande:

Biblioteksbegränsningar: Även om torch.distributed och NCCL tillhandahåller punkt-till-punkt-API:er som Send/Recv ärver dessa operationer fortfarande kollektiv semantik och begränsningar.

Hårdvarumångfald: NVIDIA ConnectX NIC med Reliable Connection (RC)-transport är den vanligaste RDMA-installationen, vilket garanterar ordnad leverans. Molnleverantörer som AWS använder annan hårdvara, Elastic Fabric Adapter (EFA) med Scalable Reliable Datagram (SRD)-transport levererar meddelanden i oordning. EFA kräver också aggregering av fyra 100 Gbps NIC (eller två 200 Gbps NIC) för att uppnå 400 Gbps bandbredd.

Programvaruportabilitet: Befintliga RDMA-bibliotek lider av leverantörslåsning. DeepEP kräver GPU-initierad RDMA (IBGDA) som endast är tillgänglig på ConnectX. NVSHMEM uppvisar allvarlig prestandaförsämring på EFA. Bibliotek som Mooncake och NIXL saknar EFA-stöd eller befinner sig i ett tidigt utvecklingsstadium. Som ett resultat finns det ingen livskraftig lösning över flera leverantörer för LLM-inferens.

TransferEngine

TransferEngine adresserar dessa hinder genom fyra kärnkapaciteter:

Gemensamma transportantaganden: Istället för att rikta in sig på ett specifikt NIC antar TransferEngine bara att den underliggande transporten är tillförlitlig men oordnad, d.v.s. skärningspunkten mellan de garantier som tillhandahålls av både RC- och SRD-transporter.

ImmCounter-synkronisering: En ny primitiv för meddelande om slutförande som fungerar utan att förlita sig på meddelandeordning, vilket gör att samma kod kan köras på både ordnade (RC) och oordnade (SRD) transporter.

Stöd för flera NIC: Transparent hantering av flera NIC per GPU, vilket automatiskt delar upp (sharder) överföringar för att maximera bandbredden.

Minimal yta för API: En liten uppsättning operationer som räcker för att bygga komplexa kommunikationsmönster.

TransferEngine-systemdesign som visar förfrågnings- och callback-vägar, CPU-arbetare, UVM-buffertar, GPU:er och NIC:ar över två GPU/NUMA-domäner.

TransferEngine skapar en arbetstråd per GPU. Varje tråd hanterar en DomainGroup som samordnar 1 till 4 RDMA-NIC, beroende på hårdvaruplattform (ett enda ConnectX-7 ger 400 Gbps, medan EFA kräver fyra 100 Gbps-adaptrar eller två 200 Gbps-adaptrar). Inom en DomainGroup är varje Domain hårdvaruspecifik och hanterar ett enda NIC, och hanterar köparhantering, inlämning av arbete och avläsning av slutföranden.

Varje TransferEngine-instans exponerar en enda main address för peer-identifiering och upptäckt. Fjärr-peer-datorer utbyter NetAddr-strukturer som kodar nätverksadresser och kan serialiseras för out-of-band-kommunikation. Alla noder i ett system måste använda samma antal NIC per GPU. Denna enhetliga konfiguration gör det möjligt för TransferEngine att dela upp överföringar intelligent: vid initiering av en överföring vet både källa och destination exakt vilka NIC som är tillgängliga, vilket möjliggör automatisk lastbalansering över alla adaptrar.

API-design

TransferEngine exponerar ett minimalt API som abstraherar bort komplexiteten i de underliggande RDMA-gränssnitten.

rust
#[serde] struct NetAddr(Bytes); #[serde] struct MrDesc{ ptr: u64, rkeys: Vec<(NetAddr, u64)> } struct MrHandle(NonNull<c_void>); type Offset = u64; struct Pages{ indices: Vec<u32>, stride: u64, offset: Offset } struct PeerGroupHandle(u64); struct ScatterDst{ len: u64, src: Offset, dst: (MrDesc,Offset)} enum OnDone { Callback(fn () -> ()), Flag(Atomic<bool>) } trait TransferEngine { fn main_address() -> NetAddr; // Memory Region Management fn reg_mr(ptr, len, device) -> (MrHandle, MrDesc); // Two-sided Send/Recv fn submit_send(addr: NetAddr, msg: &[u8], cb: fn () -> ()); fn submit_recvs(len: u64, cnt: u64, cb: fn (&[u8]) -> ()); // One-sided Write fn expect_imm_count(imm: u32, count: u32, cb: fn () -> ()); fn submit_single_write(len: u64, imm: Option<u32> src: (MrHandle, Offset), dst: (MrDesc, Offset), OnDone); fn submit_paged_writes(page_len: u64, imm: Option<u32>, src: (MrHandle, Pages), dst: (MrDesc, Pages), OnDone); // One-sided Write to a group of peers fn add_peer_group(addrs: Vec<NetAddr>) -> PeerGroupHandle; fn submit_scatter(h: Option<PeerGroupHandle>, OnDone, imm: Option<u32>, src: MrHandle, dst: Vec<ScatterDst>); fn submit_barrier(h: Option<PeerGroupHandle>, OnDone, imm: u32, dst: Vec<MrDesc>); // Watcher for CPU-GPU synchronization fn alloc_uvm_watcher(cb: fn(u64,u64) -> ()) -> NonNull<u64>; }

API:et består av flera nyckelkomponenter:

Minnesregistrering

RDMA-operationer kräver att minnesregioner registreras hos NIC före dataöverföring. Funktionen reg_mr tar en pekare, längd och enhet och returnerar två handtag:

MrHandle: Ett lokalt handtag som används som källa för överföringar

MrDesc: En serialiserbar deskriptor som kan utbytas med fjärr-peers för att göra det möjligt för dem att skriva till det registrerade minnet

Strukturen MrDesc innehåller en lista över fjärrnätverksadress och fjärrnyckel (en per NIC) som ger fjärråtkomst till minnesregionen. Denna design hanterar naturligt installationer med flera NIC.

Dubbelsidig Send/Recv

För små meddelanden och meddelanden i RPC-stil i kontrollplanet tillhandahåller TransferEngine submit_send och submit_recvs:

submit_send: Skickar meddelandet till en fjärr-peer som identifieras av NetAddr. Meddelandet kopieras internt så att anroparen omedelbart kan återanvända eller frige bufferten.

submit_recvs: Publicerar en pool av mottagningsbuffertar. När ett meddelande anländer anropas ett anropsobjekt (callback) med mottagen data. Bufferten återförs automatiskt till poolen när anropet har slutförts.

Ensidig skrivning (One-Sided Write)

För bulkdataöverföringar tillhandahåller TransferEngine nollkopieringsoperationer för ensidig skrivning:

submit_single_write: Överför en sammanhängande region från källa till destination

submit_paged_writes: Överför icke-sammanhängande sidor som specificeras av index, steglängder (strides) och förskjutningar (offsets)

Båda operationerna accepterar ett valfritt 32-bitars omedelbart värde som levereras till mottagaren atomiskt vid slutförande.

Motorn delar automatiskt upp stora överföringar över flera NIC för att maximera bandbredden.

Peer-grupper och bulkoperationer

För operationer som riktar sig till flera peers (vanligt vid MoE-routing) tillhandahåller TransferEngine optimerade primitiver:

add_peer_group: Förregistrerar en uppsättning peer-adresser för snabb sökning

submit_scatter: Skickar olika datasegment till varje peer i gruppen

submit_barrier: Skickar en notifiering som endast innehåller omedelbart värde till alla peers (nollbyte-överföring)

Dessa operationer är optimerade för låg latens genom att cacha interna datastrukturer och skapa mallar för RDMA-arbetsförfrågningar, vilket undviker repetitiva omkostnader för inställning.

UVM-övervakare: GPU-till-CPU-signalering

En utmaning i LLM-system är att samordna RDMA-överföringar med GPU-förlopp. Helst bör dataöverföringen starta så snart GPU:n producerar utdata, men att avläsa (poll) GPU:n från CPU:n är ineffektivt.

TransferEngine adresserar detta med UVM Watchers: enhetliga virtuella minnesplatser som kan uppdateras av GPU-kärnor (även inom CUDA-grafer) och effektivt avläsas av CPU:n med hjälp av GDRCopy. När en UVM-övervakare allokeras registreras ett anropsobjekt (callback) som anropas när GPU:n uppdaterar värdet.

Detta möjliggör mönster som lager-för-lager-överföring av KvCache: efter att varje transformerlager har slutförts inkrementerar GPU:n övervakaren, vilket utlöser en RDMA-överföring av det lagrets KvCache medan efterföljande lager fortsätter att beräkna.

Meddelande om slutförande

TransferEngine tillhandahåller mekanismer för meddelande om slutförande för både initiatörssidan (anroparen av submit_write) och mottagarsidan (målet för skrivoperationer).

Initiatörsidans notifiering: När en skrivoperation skickas in kan anroparen välja att få meddelande när den har slutförts genom antingen en anropsfunktion eller en atomisk flagga. Denna notifiering bekräftar att datan har överförts framgångsrikt från källbufferten.

Mottagarsidans notifiering: Nyckelinnovationen är ImmCounter, en komponent som spårar räknare indexerade av 32-bitars omedelbara nummer. När en skrivoperation bär på ett omedelbart värde inkrementeras mottagarens ImmCounter atomiskt vid dataleverans. Funktionen expect_imm_count gör det möjligt för mottagaren att registrera ett anropsobjekt som aktiveras när ett specifikt omedelbart värde når ett målräknarvärde. Räknarna kan synkroniseras transparent med GPU:n via GDRCopy, avläsas direkt eller övervakas via anropsobjekt.

Denna mekanism på mottagarsidan ger tillförlitlig synkronisering utan att kräva meddelandeordning. Den fungerar lika bra på ConnectX-NIC (som garanterar ordnad leverans över RC-transport) och EFA-NIC (som levererar meddelanden i oordning över SRD-transport).

Implementering

TransferEngine är implementerat i Rust, där allokeringar, trådhantering och synkronisering noggrant optimeras för att minimera latens och uppnå hög genomströmning. Motorn skapar en arbetstråd per DomainGroup, där var och en är låst (pinned) till en CPU-kärna på den NUMA-nod som enheterna är anslutna till, vilket minimerar både schemaläggnings- och minnesåtkomstlatens. Datastrukturer som är specifika för en domän allokeras efter låsning för att säkerställa att minne reserveras i rätt NUMA-nod. En arbetstråd hanterar upp till 4 Domain-domäner (där var och en hanterar ett enda NIC), medan en annan dedikerad tråd avläser GPU:n för att uppdatera UVM-övervakare. Trådoberoende kommunikation sker genom låsfria köer.

API:et vidarebefordrar förfrågningar till den DomainGroup som betjänar lämplig enhet, där den första också betjänar hosten. I en tät loop avläser domänarbetaren efter nytt arbete, med prioritet för att skicka in nya förfrågningar. Beroende på hårdvara och konfiguration delas förfrågningar upp och lastbalanseras över tillgängliga Domain-domäner. Den första skrivningen i en sammansatt förfrågan skickas omedelbart till NIC:ets sändningskö. När nya förfrågningar är slut fortsätter arbetaren med att bearbeta väntande operationer och skickar skrivningar för att fylla hårdvarurören. Därefter avläses slutförandeköer för att fråga efter slutförda överföringar och omedelbara räknarökningar. Händelser aggregeras för att leverera notifieringar per överföring, vilket överlämnar överföringen till en dedikerad anropstråd som delas av alla grupper.

Uppdelning (sharding) inom en DomainGroup är flexibel. Överföringar kan rikta in sig på specifika NIC efter index. En enskild skrivning kan delas upp. Sidinmatade överföringar, scatter- och barrier-operationer, som alla översätts till flera skrivningar, kan delas upp över alla NIC.

Hårdvaruspecifika optimeringar

Domain-domänerna i TransferEngine är specialiserade och optimerade för den hårdvara de kontrollerar:

AWS EFA: EFA-implementeringen använder libfabric och hanterar en fabric-domän per NIC inom DomainGroup. Viktiga implementeringsbeslut och optimeringar inkluderar:

Nollbyteskrivningar: EFA avviker från RDMA-specifikationen genom att kräva giltiga måldeskriptorer även för nollbyteskrivningar. Därför framtvingar TransferEngine giltiga deskriptorer för alla överföringar.

Mallskapande för arbetsförfrågningar: Fyller i i förväg och behåller de vanliga fälten i libfabric-strukturer före publicering, vilket minskar omkostnaderna per operation för bulköverföringar och peer-gruppsoperationer

NVIDIA ConnectX-7: ConnectX-7-implementeringen använder libibverbs med RC-transport. Viktiga implementeringsbeslut och optimeringar inkluderar:

UD-köpar för handskakningar: Varje Domain använder ett Unreliable Datagram (UD)-köpar för att utbyta RC-anslutningshandskakningar.

Separata RC-köpar: Skapar 2 RC-köpar per peer, en för dubbelsidiga Send/Recv-operationer och en annan för ensidiga Write- och WriteImm-operationer. Denna separation är nödvändig eftersom både Recv- och WriteImm-slutföranden konsumerar arbetsförfrågningar i publiceringsordning, vilket möjliggör Recv-semantik på hög nivå samtidigt som WriteImm stöds utan störningar.

Mallskapande för arbetsförfrågningar: Fyller i gemensamma fält för arbetsförfrågningsstrukturer för bulköverföringar och peer-grupper i förväg.

Kedjning av arbetsförfrågningar: Länkar upp till 4 arbetsförfrågningar via next-pekaren i ibv_send_wr, vilket minskar antalet dörrklockssignaler (doorbell rings) till NIC.

Avslappnad ordning (Relaxed ordering): Aktiverar IBV_ACCESS_RELAXED_ORDERING för att tillåta PCIe-transaktioner i oordning mellan NIC och GPU-minne.

Slutsats

Genom att rikta in sig på skärningspunkten mellan garantier över olika RDMA-transporter och introducera ordningsoberoende synkronisering via ImmCounter tillhandahåller TransferEngine en portabel grund för punkt-till-punkt-kommunikation i LLM-system. Biblioteket är öppen källkod och tillgängligt på GitHub, och vi uppmuntrar gemenskapen att utforska och bygga vidare på det för nya kommunikationsmönster i storskaliga maskininlärningssystem.

Citering

BibTeX
@online{perplexity2025RdmaPointToPointCommunicationForLlmSystems,
  author = {{Perplexity} and Nandor Licker and Kevin Hu and Vladimir Zaytsev and Lequn Chen},
  title = {{RDMA point-to-point communication for LLM systems}},
  date = {2025-11-05},
  url = {https://www.perplexity.ai/hub/blog/rdma-point-to-point-communication-for-llm-systems}
}