Communication point à point RDMA pour les systèmes LLM
Outil élégant pour répondre aux nouveaux schémas de communication des LLM
Les systèmes de grands modèles de langage (LLM) se sont traditionnellement appuyés sur des communications collectives pour les modèles parallèles structurés lors de l'entraînement et de l'inférence. Cependant, les charges de travail émergentes, telles que l'inférence désagrégée, le routage par mélange d'experts (MoE) et le réglage fin par apprentissage par renforcement asynchrone, exigent la flexibilité de la communication point à point.
Notre dernier document de recherche présente TransferEngine, une bibliothèque de communication portable basée sur l'accès direct en mémoire à distance (RDMA) qui fonctionne sur différents contrôleurs d'interface réseau (NIC), notamment NVIDIA ConnectX-7 et AWS Elastic Fabric Adapter (EFA). Nous démontrons son efficacité dans trois systèmes de production chez Perplexity AI :
Préchargement et décodage désagrégés : transfert de KvCache couche par couche à l'aide d'écritures paginées et de moniteurs UVM, permettant aux instances de préchargement et de décodage d'évoluer de manière indépendante.
Transfert de poids pour le post-entraînement par apprentissage par renforcement en moins de 2 secondes : synchronisation ultra-rapide des poids entre les nœuds d'entraînement et d'inférence.
Activation de modèles à mille milliards de paramètres sur AWS EFA : opérations de répartition et de combinaison MoE à faible latence utilisant des primitives de dispersion et de barrière, atteignant une latence de pointe sur ConnectX-7 et la première implémentation viable compatible avec AWS EFA.
Cet article se concentre sur TransferEngine lui-même, la bibliothèque de communication qui alimente ces systèmes. Lisez le document de recherche complet sur arXiv et explorez le code sur GitHub.
Motivation
Les communications collectives excellent dans les modèles parallèles structurés tels que le parallélisme de tenseurs et le parallélisme de données, mais elles imposent des contraintes qui limitent leur applicabilité aux charges de travail émergentes :
Appartenance fixe : les collectifs nécessitent l'établissement d'un univers de communication global au moment de l'initialisation, avec une configuration synchronisée entre tous les participants. Pour l'inférence désagrégée, cela pose problème puisque le nombre d'instances de préchargement et de décodage doit s'adapter dynamiquement en fonction des schémas de trafic.
Formes de tenseurs uniformes : les collectifs supposent que tous les participants échangent des tenseurs ayant des formes et des types de données identiques. Dans le routage MoE, le nombre de jetons envoyés à chaque expert dépend des données et est découvert à l'exécution, ce qui oblige les collectifs « all-to-all » denses à effectuer un remplissage (« padding ») pour atteindre les tailles de tampon du pire des cas.
Ordonnancement des opérations : les interfaces de programmation (API) collectives imposent des sémantiques synchronisées, même lorsque les applications ont seulement besoin de savoir qu'un ensemble d'opérations est terminé. Par exemple, lors du transfert de KvCache, toutes les pages de l'instance de préchargement vers l'instance de décodage peuvent arriver dans n'importe quel ordre. De même, lors des mises à jour des poids par apprentissage par renforcement, tous les tenseurs de poids peuvent être transférés indépendamment sans contrainte d'ordonnancement.
La communication point à point basée sur le RDMA répond naturellement à ces limites, mais trois obstacles ont empêché son adoption :
Limitations de la bibliothèque : bien que torch.distributed et NCCL fournissent des API point à point telles que Send/Recv, ces opérations héritent toujours de sémantiques et de limitations collectives.
Diversité matérielle : les cartes réseau (NIC) NVIDIA ConnectX avec transport de connexion fiable (RC) constituent la configuration RDMA la plus courante, garantissant une livraison dans l'ordre. Les fournisseurs de cloud comme AWS utilisent un matériel différent ; l'adaptateur EFA (Elastic Fabric Adapter) avec transport de datagrammes fiables évolutifs (SRD) livre les messages dans le désordre. L'EFA nécessite également l'agrégation de quatre cartes réseau de 100 Gbit/s (ou de deux cartes réseau de 200 Gbit/s) pour atteindre une bande passante de 400 Gbit/s.
Portabilité logicielle : les bibliothèques RDMA existantes souffrent d'un verrouillage propriétaire. DeepEP nécessite un accès RDMA initié par le GPU (IBGDA) disponible uniquement sur ConnectX. NVSHMEM présente une grave dégradation des performances sur l'EFA. Des bibliothèques telles que Mooncake et NIXL ne prennent pas en charge l'EFA ou en sont à leurs débuts de développement. Par conséquent, aucune solution inter-fournisseur viable n'existe pour l'inférence de grands modèles de langage.
TransferEngine
TransferEngine surmonte ces obstacles grâce à quatre fonctionnalités clés :
Hypothèses de transport communes : plutôt que de cibler une carte réseau spécifique, TransferEngine suppose uniquement que le transport sous-jacent est fiable mais non ordonné, c'est-à-dire l'intersection des garanties fournies par les transports RC et SRD.
Synchronisation ImmCounter : une nouvelle primitive de notification de fin de tâche qui fonctionne sans dépendre de l'ordre des messages, permettant au même code de s'exécuter sur des transports ordonnés (RC) et non ordonnés (SRD).
Prise en charge de plusieurs cartes réseau (Multi-NIC) : gestion transparente de plusieurs cartes réseau par GPU, répartissant automatiquement les transferts pour maximiser la bande passante.
Surface d'API minimale : un petit ensemble d'opérations suffisant pour créer des modèles de communication complexes.
TransferEngine génère un fil d'exécution (thread) ouvrier par GPU. Chaque ouvrier gère un DomainGroup qui coordonne 1 à 4 cartes réseau RDMA, selon la plateforme matérielle (une seule carte ConnectX-7 fournit 400 Gbit/s, tandis que l'EFA nécessite quatre adaptateurs de 100 Gbit/s ou deux adaptateurs de 200 Gbit/s). Au sein d'un DomainGroup, chaque Domain est spécifique au matériel et gère une seule carte réseau, s'occupant de la gestion des paires de files d'attente, de la soumission des travaux et de l'interrogation de fin de tâche.
Chaque instance de TransferEngine expose une seule adresse principale pour l'identification et la découverte des pairs. Les pairs distants échangent des structures NetAddr qui encodent les adresses réseau et peuvent être sérialisées pour une communication hors bande. Tous les pairs d'un système doivent utiliser le même nombre de cartes réseau par GPU. Cette configuration uniforme permet à TransferEngine de répartir intelligemment les transferts : lors de l'initiation d'un transfert, la source et la destination savent exactement quelles cartes réseau sont disponibles, ce qui permet un équilibrage de charge automatique sur tous les adaptateurs.
Conception de l'API
TransferEngine expose une API minimale qui abstrait la complexité des interfaces RDMA sous-jacentes.
#[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>;
}L'API se compose de plusieurs composants clés :
Enregistrement de la mémoire
Les opérations RDMA nécessitent l'enregistrement des régions de mémoire auprès de la carte réseau avant le transfert des données. La fonction reg_mr prend un pointeur, une longueur et un périphérique, et renvoie deux descripteurs :
MrHandle : un descripteur local utilisé comme source pour les transferts
MrDesc : un descripteur sérialisable qui peut être échangé avec des pairs distants pour leur permettre d'écrire dans la mémoire enregistrée
La structure MrDesc contient une liste d'adresses réseau distantes et de clés distantes (une par carte réseau) qui accordent un accès distant à la région de mémoire. Cette conception gère naturellement les configurations à plusieurs cartes réseau.
Envoi/Réception bidirectionnels (Two-Sided Send/Recv)
Pour les petits messages et les messages de plan de contrôle de style RPC, TransferEngine fournit submit_send et submit_recvs :
submit_send : envoie le message à un pair distant identifié par NetAddr. Le message est copié en interne, de sorte que l'appelant peut immédiatement réutiliser ou libérer le tampon.
submit_recvs : publie un groupe de tampons de réception. Lorsqu'un message arrive, une fonction de rappel (callback) est invoquée avec les données reçues. Le tampon est automatiquement restitué au groupe une fois la fonction de rappel terminée.
Écriture unidirectionnelle (One-Sided Write)
Pour les transferts de données en volume, TransferEngine propose des opérations d'écriture unidirectionnelles sans copie :
submit_single_write : transfère une région contiguë de la source vers la destination
submit_paged_writes : transfère des pages non contiguës spécifiées par des indices, des foulées (« strides ») et des décalages
Les deux opérations acceptent une valeur immédiate optionnelle de 32 bits qui est transmise au récepteur de manière atomique dès la fin de l'opération.
Le moteur répartit automatiquement les grands transferts sur plusieurs cartes réseau pour maximiser la bande passante.
Groupes de pairs et opérations en volume
Pour les opérations ciblant plusieurs pairs (ce qui est courant dans le routage MoE), TransferEngine fournit des primitives optimisées :
add_peer_group : pré-enregistre un ensemble d'adresses de pairs pour une recherche rapide
submit_scatter : envoie différentes tranches de données à chaque pair du groupe
submit_barrier : envoie une notification immédiate uniquement à tous les pairs (transfert de zéro octet)
Ces opérations sont optimisées pour une faible latence en mettant en cache les structures de données internes et en modélisant les demandes de travail RDMA, ce qui évite les surcharges de configuration répétitives.
Moniteur UVM (UVM Watcher) : signalisation du GPU vers le CPU
Un défi majeur dans les systèmes de grands modèles de langage est de coordonner les transferts RDMA avec la progression du GPU. Idéalement, le transfert de données devrait commencer dès que le GPU produit un résultat, mais interroger le GPU à partir du CPU est inefficace.
TransferEngine résout ce problème grâce aux moniteurs UVM (UVM Watchers) : des emplacements de mémoire virtuelle unifiée (UVM) qui peuvent être mis à jour par les noyaux GPU (même au sein de graphes CUDA) et interrogés efficacement par le CPU à l'aide de GDRCopy. Lorsqu'un moniteur UVM est alloué, une fonction de rappel est enregistrée et s'exécute chaque fois que le GPU met à jour la valeur.
Cela permet des schémas tels que le transfert de KvCache couche par couche : après l'achèvement de chaque couche du transformateur, le GPU incrémente le moniteur, déclenchant un transfert RDMA du KvCache de cette couche pendant que les couches suivantes continuent de calculer.
Notification de fin de tâche
TransferEngine fournit des mécanismes de notification de fin de tâche pour le côté initiateur (l'appelant de submit_write) et le côté récepteur (la cible des opérations d'écriture).
Notification côté initiateur : lorsqu'une opération d'écriture est soumise, l'appelant peut choisir d'être notifié de son achèvement par le biais d'une fonction de rappel ou d'un drapeau (flag) atomique. Cette notification confirme que les données ont été transférées avec succès depuis le tampon source.
Notification côté récepteur : l'innovation clé est le composant ImmCounter, qui suit les compteurs indexés par des nombres immédiats de 32 bits. Lorsqu'une opération d'écriture transporte une valeur immédiate, l'ImmCounter du récepteur est incrémenté de manière atomique lors de la livraison des données. La fonction expect_imm_count permet au récepteur d'enregistrer une fonction de rappel qui se déclenche lorsqu'une valeur immédiate spécifique atteint un compte cible. Les compteurs peuvent être synchronisés de manière transparente avec le GPU via GDRCopy, interrogés directement ou surveillés par le biais de fonctions de rappel.
Ce mécanisme côté récepteur offre une synchronisation fiable sans exiger d'ordonnancement des messages. Il fonctionne aussi bien sur les cartes réseau ConnectX (qui garantissent une livraison dans l'ordre sur le transport RC) que sur les cartes réseau EFA (qui livrent les messages dans le désordre sur le transport SRD).
Implémentation
TransferEngine est implémenté en Rust, en optimisant soigneusement les allocations, les fils d'exécution (threads) et la synchronisation pour minimiser la latence et atteindre un débit élevé. Le moteur génère un fil d'exécution ouvrier par DomainGroup, chacun étant épinglé à un cœur de processeur (CPU) sur le nœud NUMA auquel les périphériques sont connectés, ce qui minimise à la fois la latence de planification et d'accès à la mémoire. Les structures de données spécifiques à un domaine sont allouées après l'épinglage pour garantir que la mémoire est réservée dans le bon nœud NUMA. Un fil d'exécution ouvrier gère jusqu'à 4 Domains (chacun gérant une seule carte réseau), tandis qu'un autre fil d'exécution dédié interroge le GPU pour mettre à jour les moniteurs UVM. La communication inter-threads s'effectue par le biais de files d'attente sans verrou (lock-free).
L'API achemine les requêtes vers le DomainGroup desservant le périphérique approprié, le premier servant également l'hôte. Dans une boucle serrée, l'ouvrier de domaine interroge les nouveaux travaux, en priorisant la soumission des nouvelles requêtes. Selon le matériel et la configuration, les requêtes sont fragmentées et réparties par équilibrage de charge sur les Domains disponibles. La première écriture d'une requête composite est immédiatement publiée dans la file d'attente d'envoi de la carte réseau. Une fois les nouvelles requêtes épuisées, l'ouvrier procède au traitement des opérations en attente, en publiant des écritures pour remplir le pipeline matériel. Ensuite, les files d'attente de fin de tâche sont interrogées pour s'informer des transferts terminés et des incréments de compteurs immédiats. Les événements sont agrégés pour fournir des notifications par transfert, confiant le transfert à un fil d'exécution de rappel dédié partagé par tous les groupes.
Le découpage au sein d'un DomainGroup est flexible. Les transferts peuvent cibler des cartes réseau spécifiques par indice. Une seule écriture peut être divisée. Les transferts paginés, les opérations de dispersion et de barrière, qui se traduisent tous par de multiples écritures, peuvent être répartis sur toutes les cartes réseau.
Optimisations spécifiques au matériel
Les Domains au sein de TransferEngine sont spécialisés et optimisés pour le matériel sous leur contrôle :
AWS EFA : l'implémentation EFA utilise libfabric, gérant un domaine de fabric par carte réseau au sein du DomainGroup. Les principales décisions d'implémentation et optimisations comprennent :
Écritures de taille nulle : l'EFA s'écarte de la spécification RDMA en exigeant des descripteurs de cible valides, même pour les écritures de taille nulle. Par conséquent, TransferEngine impose des descripteurs valides pour tous les transferts.
Modélisation des requêtes de travail : pré-remplit et conserve les champs communs des structures libfabric avant la publication, ce qui réduit la surcharge par opération pour les transferts en volume et les opérations de groupes de pairs.
NVIDIA ConnectX-7 : l'implémentation ConnectX-7 utilise libibverbs avec le transport RC. Les principales décisions d'implémentation et optimisations comprennent :
Paires de files d'attente UD pour les poignées de main (handshakes) : chaque Domain utilise une paire de files d'attente de datagrammes non fiables (UD) pour échanger des poignées de main de connexion RC.
Paires de files d'attente RC séparées : crée 2 paires de files d'attente RC par pair, l'une pour les opérations bidirectionnelles Send/Recv et l'autre pour les opérations unidirectionnelles Write et WriteImm. Cette séparation est nécessaire car les fins de tâche Recv et WriteImm consomment des requêtes de travail dans l'ordre de publication, ce qui permet une sémantique Recv de haut niveau tout en prenant en charge WriteImm sans interférence.
Modélisation des requêtes de travail : pré-remplit les champs communs des structures de requêtes de travail pour les transferts en volume et les groupes de pairs.
Chaînage des requêtes de travail : relie jusqu'à 4 requêtes de travail via le pointeur next de ibv_send_wr, ce qui réduit le nombre d'appels de sonnerie (« doorbell rings ») vers la carte réseau.
Ordonnancement assoupli (Relaxed ordering) : active IBV_ACCESS_RELAXED_ORDERING pour autoriser les transactions PCIe hors-ordre entre la carte réseau et la mémoire du GPU.
Conclusion
En ciblant l'intersection des garanties entre les différents transports RDMA et en introduisant une synchronisation indépendante de l'ordre via ImmCounter, TransferEngine fournit une base portable pour la communication point à point dans les systèmes de grands modèles de langage. La bibliothèque est en open source et disponible sur GitHub ; nous encourageons la communauté à l'explorer et à l'utiliser pour concevoir de nouveaux schémas de communication dans les systèmes d'apprentissage automatique à grande échelle.
Citation
@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}
}