Komunikasi point-to-point RDMA untuk sistem LLM
Alat elegan untuk mengatasi pola komunikasi LLM baru
Secara tradisional, sistem LLM mengandalkan komunikasi kolektif untuk pola paralel terstruktur dalam pelatihan dan inferensi. Namun, beban kerja baru, seperti inferensi terdisagregasi, perutean Mixture-of-Experts (MoE), dan fine-tuning reinforcement learning asinkron, menuntut fleksibilitas komunikasi point-to-point.
Makalah penelitian terbaru kami memperkenalkan TransferEngine, pustaka komunikasi berbasis RDMA portabel yang bekerja di berbagai Network Interface Controllers (NIC), termasuk NVIDIA ConnectX-7 dan AWS Elastic Fabric Adapter (EFA). Kami mendemonstrasikan efektivitasnya dalam tiga sistem produksi di Perplexity AI:
Prefill dan Decode Terdisagregasi: Transfer KvCache lapis demi lapis menggunakan paged writes dan UVM watchers, yang memungkinkan instans prefill dan decode untuk diskalakan secara independen.
Transfer Bobot untuk Pascapelatihan RL dalam waktu di bawah 2 detik: Sinkronisasi bobot yang sangat cepat antara node pelatihan dan inferensi.
Mengaktifkan Model Berparameter Triliunan pada AWS EFA: Operasi pengiriman dan penggabungan MoE latensi rendah menggunakan primitif scatter dan barrier, mencapai latensi canggih pada ConnectX-7 dan implementasi layak pertama yang kompatibel dengan AWS EFA.
Artikel ini berfokus pada TransferEngine itu sendiri, pustaka komunikasi yang mendukung sistem-sistem ini. Baca makalah penelitian lengkap di arXiv dan jelajahi kodenya di GitHub.
Motivasi
Komunikasi kolektif unggul dalam pola paralel terstruktur seperti Tensor Parallelism dan Data Parallelism, tetapi komunikasi ini menimbulkan batasan yang membatasi penerapannya pada beban kerja yang muncul:
Keanggotaan tetap: Komunikasi kolektif mengharuskan penetapan dunia komunikasi global pada waktu inisialisasi, dengan penyiapan yang disinkronkan di semua peserta. Untuk inferensi terdisagregasi, ini menjadi masalah karena jumlah instans prefill dan decode harus diskalakan secara dinamis sebagai respons terhadap pola lalu lintas.
Bentuk tensor seragam: Komunikasi kolektif mengasumsikan semua peserta bertukar tensor dengan bentuk dan tipe data yang identik. Dalam perutean MoE, jumlah token yang dikirim ke setiap pakar bergantung pada data dan ditemukan saat runtime, memaksa komunikasi kolektif all-to-all yang padat untuk melakukan padding ke ukuran buffer terburuk.
Pengurutan operasi: API kolektif memberlakukan semantik tersinkronisasi, bahkan ketika aplikasi hanya perlu mengetahui bahwa sekumpulan operasi telah selesai. Misalnya, dalam transfer KvCache, semua halaman dari instans prefill ke instans decode dapat tiba dalam urutan apa pun. Demikian pula, dalam pembaruan bobot RL, semua tensor bobot dapat ditransfer secara independen tanpa batasan pengurutan.
Komunikasi point-to-point berbasis RDMA secara alami mengatasi keterbatasan ini, tetapi tiga hambatan telah mencegah adopsinya:
Keterbatasan pustaka: Meskipun torch.distributed dan NCCL menyediakan API point-to-point seperti Send/Recv, operasi ini masih mewarisi semantik dan batasan kolektif.
Keberagaman perangkat keras: NIC NVIDIA ConnectX dengan transpor Reliable Connection (RC) adalah penyiapan RDMA yang paling umum, yang menjamin pengiriman sesuai urutan. Penyedia cloud seperti AWS menggunakan perangkat keras yang berbeda, Elastic Fabric Adapter (EFA) dengan transpor Scalable Reliable Datagram (SRD) mengirimkan pesan tidak berurutan. EFA juga memerlukan penggabungan empat NIC 100 Gbps (atau dua NIC 200 Gbps) untuk mencapai bandwidth 400 Gbps.
Portabilitas perangkat lunak: Pustaka RDMA yang ada mengalami penguncian vendor. DeepEP memerlukan RDMA yang dimulai GPU (IBGDA) yang hanya tersedia di ConnectX. NVSHMEM menunjukkan penurunan performa yang parah pada EFA. Pustaka seperti Mooncake dan NIXL tidak memiliki dukungan EFA atau masih dalam tahap pengembangan awal. Akibatnya, tidak ada solusi lintas penyedia yang layak untuk inferensi LLM.
TransferEngine
TransferEngine mengatasi hambatan ini melalui empat kemampuan inti:
Asumsi transpor umum: Alih-alih menargetkan NIC tertentu, TransferEngine hanya berasumsi bahwa transpor yang mendasarinya andal tetapi tidak berurutan, yaitu perpotongan jaminan yang disediakan oleh transpor RC dan SRD.
Sinkronisasi ImmCounter: Primitif baru untuk notifikasi penyelesaian yang bekerja tanpa mengandalkan pengurutan pesan, memungkinkan kode yang sama berjalan pada transpor terurut (RC) dan tidak terurut (SRD).
Dukungan multi-NIC: Manajemen transparan dari beberapa NIC per GPU, secara otomatis melakukan sharding transfer untuk memaksimalkan bandwidth.
Permukaan API minimal: Sekumpulan kecil operasi yang cukup untuk membangun pola komunikasi yang kompleks.
TransferEngine menghasilkan satu utas pekerja per GPU. Setiap pekerja mengelola DomainGroup yang mengkoordinasikan 1 hingga 4 NIC RDMA, bergantung pada platform perangkat keras (satu ConnectX-7 menyediakan 400 Gbps, sementara EFA memerlukan empat adaptor 100 Gbps atau dua adaptor 200 Gbps). Dalam DomainGroup, setiap Domain bersifat khusus perangkat keras dan mengelola satu NIC, menangani manajemen pasangan antrean, pengiriman kerja, dan polling penyelesaian.
Setiap instans TransferEngine mengekspos satu alamat utama untuk identifikasi dan penemuan rekan. Rekan jarak jauh bertukar struktur NetAddr yang mengenkode alamat jaringan dan dapat diserialisasikan untuk komunikasi out-of-band. Semua rekan dalam sistem harus menggunakan jumlah NIC yang sama per GPU. Konfigurasi seragam ini memungkinkan TransferEngine untuk melakukan sharding transfer secara cerdas: saat memulai transfer, sumber dan tujuan tahu persis NIC mana yang tersedia, memungkinkan penyeimbangan beban otomatis di semua adaptor.
Desain API
TransferEngine mengekspos API minimal yang mengabstraksi kompleksitas antarmuka RDMA yang mendasarinya.
#[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 terdiri dari beberapa komponen kunci:
Registrasi Memori
Operasi RDMA memerlukan wilayah memori untuk didaftarkan ke NIC sebelum transfer data. Fungsi reg_mr mengambil pointer, panjang, dan perangkat, mengembalikan dua handel:
MrHandle: Handel lokal yang digunakan sebagai sumber untuk transfer
MrDesc: Deskriptor yang dapat diserialisasikan yang dapat ditukar dengan rekan jarak jauh agar mereka dapat menulis ke memori yang terdaftar
Struktur MrDesc berisi daftar alamat jaringan jarak jauh dan kunci jarak jauh (satu per NIC) yang memberikan akses jarak jauh ke wilayah memori. Desain ini secara alami menangani penyiapan multi-NIC.
Kirim/Terima Dua Sisi (Two-Sided Send/Recv)
Untuk pesan kecil dan pesan bidang kontrol bergaya RPC, TransferEngine menyediakan submit_send dan submit_recvs:
submit_send: Mengirim pesan ke rekan jarak jauh yang diidentifikasi oleh NetAddr. Pesan disalin secara internal, sehingga pemanggil dapat langsung menggunakan kembali atau melepaskan buffer.
submit_recvs: Mengeposkan kumpulan buffer penerima. Ketika sebuah pesan tiba, callback dipanggil dengan data yang diterima. Buffer secara otomatis dikembalikan ke kumpulan setelah callback selesai.
Penulisan Sisi Tunggal (One-Sided Write)
Untuk transfer data massal, TransferEngine menyediakan operasi penulisan sisi tunggal tanpa salin (zero-copy):
submit_single_write: Mentransfer wilayah yang bersebelahan dari sumber ke tujuan
submit_paged_writes: Mentransfer halaman yang tidak bersebelahan yang ditentukan oleh indeks, stride, dan offset
Kedua operasi menerima nilai langsung 32-bit opsional yang dikirimkan ke penerima secara atomik setelah selesai.
Mesin ini secara otomatis melakukan sharding transfer besar di beberapa NIC untuk memaksimalkan bandwidth.
Grup Rekan dan Operasi Massal
Untuk operasi yang menargetkan beberapa rekan (umum dalam perutean MoE), TransferEngine menyediakan primitif yang dioptimalkan:
add_peer_group: Pra-registrasi sekumpulan alamat rekan untuk pencarian cepat
submit_scatter: Mengirim irisan data berbeda ke setiap rekan dalam grup
submit_barrier: Mengirim notifikasi khusus langsung ke semua rekan (transfer nol-byte)
Operasi ini dioptimalkan untuk latensi rendah dengan menyimpan struktur data internal dalam cache dan membuat templat permintaan kerja RDMA, menghindari overhead penyiapan yang berulang.
Pengamat UVM: Pemberi Sinyal GPU-ke-CPU
Tantangan utama dalam sistem LLM adalah mengoordinasikan transfer RDMA dengan kemajuan GPU. Idealnya, transfer data harus dimulai segera setelah GPU menghasilkan keluaran, tetapi melakukan polling GPU dari CPU tidak efisien.
TransferEngine mengatasi hal ini dengan UVM Watchers: lokasi memori virtual bersatu yang dapat diperbarui oleh kernel GPU (bahkan dalam grafik CUDA) dan di-polling secara efisien oleh CPU menggunakan GDRCopy. Saat pengamat UVM dialokasikan, callback didaftarkan yang dipanggil setiap kali GPU memperbarui nilai.
Hal ini memungkinkan pola seperti transfer KvCache lapis demi lapis: setelah setiap lapisan transformer selesai, GPU menaikkan pengamat, memicu transfer RDMA dari KvCache lapisan tersebut sementara lapisan berikutnya terus menghitung.
Notifikasi Penyelesaian
TransferEngine menyediakan mekanisme notifikasi penyelesaian untuk sisi inisiator (pemanggil submit_write) dan sisi penerima (target operasi Write).
Notifikasi sisi inisiator: Ketika operasi penulisan dikirimkan, pemanggil dapat memilih untuk diberi tahu setelah selesai melalui fungsi callback atau flag atomik. Notifikasi ini mengonfirmasi bahwa data telah berhasil ditransfer dari buffer sumber.
Notifikasi sisi penerima: Inovasi utamanya adalah ImmCounter, komponen yang melacak penghitung yang diindeks oleh angka langsung 32-bit. Ketika operasi penulisan membawa nilai langsung, ImmCounter penerima dinaikkan secara atomik saat pengiriman data. Fungsi expect_imm_count memungkinkan penerima mendaftarkan callback yang menyala ketika nilai langsung tertentu mencapai hitungan target. Penghitung dapat disinkronkan secara transparan dengan GPU melalui GDRCopy, di-polling secara langsung, atau dipantau melalui callback.
Mekanisme sisi penerima ini menyediakan sinkronisasi yang andal tanpa memerlukan pengurutan pesan. Mekanisme ini bekerja dengan baik pada NIC ConnectX (yang menjamin pengiriman sesuai urutan melalui transpor RC) dan NIC EFA (yang mengirimkan pesan tidak berurutan melalui transpor SRD).
Implementasi
TransferEngine diimplementasikan dalam Rust, mengoptimalkan alokasi, pengutasan, dan sinkronisasi dengan cermat untuk meminimalkan latensi dan mencapai throughput yang tinggi. Mesin ini menghasilkan satu utas pekerja per DomainGroup, masing-masing disematkan ke inti CPU pada node NUMA tempat perangkat dilampirkan, meminimalkan latensi penjadwalan dan akses memori. Struktur data yang khusus untuk domain dialokasikan setelah penyematan untuk memastikan bahwa memori dicadangkan di node NUMA yang benar. Satu utas pekerja menangani hingga 4 Domain (masing-masing mengelola satu NIC), sementara utas berdedikasi lainnya melakukan polling GPU untuk memperbarui pengamat UVM. Komunikasi antar-utas dilakukan melalui antrean bebas kunci.
API meneruskan permintaan ke DomainGroup yang melayani perangkat yang sesuai, dengan perangkat pertama juga melayani host. Dalam loop ketat, pekerja domain melakukan polling untuk pekerjaan baru, memprioritaskan pengiriman permintaan baru. Bergantung pada perangkat keras dan konfigurasi, permintaan di-shard dan diseimbangkan bebannya di seluruh Domain yang tersedia. Penulisan pertama dari permintaan komposit langsung diposkan ke antrean kirim NIC. Setelah permintaan baru habis, pekerja melanjutkan untuk memproses operasi yang tertunda, mengeposkan penulisan untuk mengisi pipeline perangkat keras. Selanjutnya, antrean penyelesaian di-polling untuk menanyakan transfer yang selesai dan kenaikan penghitung langsung. Peristiwa dikumpulkan untuk menyampaikan notifikasi per transfer, menyerahkan transfer ke utas callback berdedikasi yang dibagikan oleh semua grup.
Sharding di dalam DomainGroup bersifat fleksibel. Transfer dapat menargetkan NIC tertentu berdasarkan indeks. Penulisan tunggal dapat dibagi. Transfer berhalaman, scatter, dan operasi barrier, yang semuanya diterjemahkan ke beberapa Penulisan, dapat di-shard di semua NIC.
Pengoptimalan Khusus Perangkat Keras
Domain di dalam TransferEngine dikhususkan dan dioptimalkan untuk perangkat keras di bawah kendali mereka:
AWS EFA: Implementasi EFA menggunakan libfabric, mengelola domain fabric per NIC dalam DomainGroup. Keputusan implementasi dan pengoptimalan utama meliputi:
Penulisan berukuran nol: EFA berbeda dari spesifikasi RDMA dengan mengharuskan deskriptor target yang valid bahkan untuk penulisan berukuran nol. Oleh karena itu, TransferEngine memberlakukan deskriptor yang valid untuk semua transfer.
Pembuatan templat permintaan kerja: Pra-mengisi dan mempertahankan bidang umum struktur libfabric sebelum mengeposkan, mengurangi overhead per operasi untuk transfer massal dan operasi grup rekan
NVIDIA ConnectX-7: Implementasi ConnectX-7 menggunakan libibverbs dengan transpor RC. Keputusan implementasi dan pengoptimalan utama meliputi:
Pasangan antrean UD untuk jabat tangan: Setiap Domain menggunakan pasangan antrean Unreliable Datagram (UD) untuk bertukar jabat tangan koneksi RC.
Pasangan antrean RC terpisah: Membuat 2 pasangan antrean RC per rekan, satu untuk operasi Kirim/Terima dua sisi dan satu lagi untuk operasi Tulis dan WriteImm sisi tunggal. Pemisahan ini diperlukan karena penyelesaian Recv dan WriteImm menghabiskan permintaan kerja dalam urutan pengeposan, memungkinkan semantik Recv tingkat tinggi sambil mendukung WriteImm tanpa gangguan.
Pembuatan templat permintaan kerja: Pra-mengisi bidang umum struktur permintaan kerja untuk transfer massal dan grup rekan.
Pengantaian permintaan kerja: Menghubungkan hingga 4 permintaan kerja melalui pointer next dari ibv_send_wr, mengurangi jumlah dering bel pintu ke NIC.
Pengurutan longgar: Mengaktifkan IBV_ACCESS_RELAXED_ORDERING untuk mengizinkan transaksi PCIe tidak berurutan antara NIC dan memori GPU.
Kesimpulan
Dengan menargetkan perpotongan jaminan di berbagai transpor RDMA dan memperkenalkan sinkronisasi independen urutan melalui ImmCounter, TransferEngine menyediakan fondasi portabel untuk komunikasi point-to-point dalam sistem LLM. Pustaka ini bersifat sumber terbuka dan tersedia di GitHub, dan kami mendorong komunitas untuk menjelajahi dan membangunnya untuk pola komunikasi baru dalam sistem pembelajaran mesin berskala besar.
Sitasi
@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}
}