Home · ASPLOS 2023 · GPUs & Parallel Hardware

MSCCLang: Microsoft Collective Communication Language

Meghan Cowan, Saeed Maleki, Madanlal Musuvathi, Olli Saarikivi and Yifan Xiong

Conference
ASPLOS 2023
Published
2023
Pages
502-514
Citations
37

Collective communication eats up to 79% of large-model training time yet ships as a take-it-or-leave-it algorithm menu inside NCCL — MSCCLang lets developers write custom GPU collectives as ~30 lines of chunk-routing Python, compiled through dependency DAGs into a fused, scheduled IR that an NCCL-compatible interpreter executes as a single kernel, race- and deadlock-free by construction — beating NCCL's AllReduce by up to 1.9×, a hand-tuned AllToAll by 1.3×, and serving Azure OpenAI's Copilot in production.

The argument in one glance

  1. Problem

    Communication grows faster than models

    Distributed training and inference synchronize GPUs through collectives — AllReduce, AllGather, AllToAll. As parameters outgrow single GPUs, the communication share explodes: ResNet-50 (~100 MB) spends 3% of its time communicating; DeepLight (~2 GB) spends 79% on the same cluster. Vendor libraries (NCCL, RCCL) supply hand-tuned implementations of only a FEW standard algorithms — Ring and Tree — regardless of your topology, buffer size, or communication pattern.

  2. What people did

    Custom algorithms — by hand, or by composition

    Research showed custom collectives tailored to interconnect topology and input size beat the standard menu (SCCL even synthesizes pareto-optimal algorithms with a constraint solver). To implement them, practitioners either write raw CUDA or compose sequences of existing NCCL calls — building a hierarchical AllReduce, say, out of four library invocations.

  3. Why it fell short

    Hand-writing is hazardous; composing is slow

    A collective kernel is concurrency at its nastiest: dozens of thread blocks on multiple GPUs exchanging data over NVLink and InfiniBand, where a missed dependency is a silent data race and a wrong schedule is a deadlock — and matching hand-tuned performance also demands fusion, pipelining, and channel-level parallelism that algorithm generators like SCCL don't implement (SCCL's own runtime only speaks NVLink). Composition avoids the danger but pays multiple kernel launches and forfeits every cross-kernel optimization — the composed hierarchical AllReduce loses badly to both NCCL and a fused implementation of the SAME algorithm.

  4. This paper's idea

    A language, a compiler, and an interpreter for chunk routing

    MSCCLang is a Python-embedded DSL where a collective is written as declarative CHUNK ROUTING — under 30 lines for every algorithm evaluated. The compiler traces it into a Chunk DAG, lowers to instructions, fuses receive-reduce-send chains into register-resident primitives, and schedules onto thread blocks and channels with deadlock-freedom guaranteed by topological ordering; pre/postconditions validate correctness before anything touches hardware. An NCCL-API-compatible interpreter runs the result as ONE cooperative kernel with tile pipelining, falling back to NCCL outside each algorithm's sweet spot. Results: up to 1.9× over NCCL's Ring AllReduce, 1.3× over hand-optimized AllToAll, a 14.5× custom collective MPI never named — and production wins at Azure OpenAI (~20% GPU time) and 256-GPU MoE training (1.10–1.89×).

Why this problem is worth solving

the standard multi-party communication operations from MPI: AllReduce (every GPU ends with the global sum), AllGather (every GPU ends with everyone’s data), AllToAll (a global transpose), ReduceScatter, and friends. Deep learning’s data, tensor, and expert parallelism are all built on them
are where distributed ML actually spends its network — and the gap between a good and a mediocre collective implementation is not a constant factor but a function of topology and size. A ring is bandwidth-optimal but pays 2R−2 latency steps; an all-pairs exchange pays maximal fan-out but only 2 steps; a hierarchical scheme matches multi-node clusters whose intra-node NVLink is 24× faster than inter-node InfiniBand. No fixed menu covers this space, and
NVIDIA’s Collective Communication Library — the de-facto standard GPU collectives runtime, with hand-tuned Ring and Tree algorithms, point-to-point transports for NVLink/PCIe/InfiniBand, and three latency/bandwidth protocols (LL, LL128, Simple). Fast, but a closed menu: adding an algorithm means writing CUDA inside NCCL
ships exactly two entries.

The deeper issue is who can afford to explore. Every candidate algorithm — a different chunking, a different channel layout, a different aggregation granularity — is days of expert CUDA work carrying race and deadlock risk, so the design space stays unexplored and the community defaults to the vendor menu. The analogy the paper leans on is compilation itself: nobody hand-writes assembly to try out an algorithm; a language plus an optimizing compiler makes exploration cheap enough that better algorithms get found. MSCCLang is that stack for collectives — and the evaluation’s most telling detail is not any single speedup but the authoring cost: every algorithm in the paper is under 30 lines and took 15 minutes to an hour to write and tune, including one (All Pairs) the authors discovered because exploration had become cheap.

What people did before, and where it broke

  • Vendor libraries. NCCL/RCCL: excellent Ring and Tree implementations with protocol selection by size — and nothing else. Topology-specialized or pattern-specialized algorithms simply aren’t expressible without forking the library’s CUDA.
  • Algorithm generators. SCCL synthesizes pareto-optimal algorithms from topology descriptions; tree-packing work similarly generates schedules. But generation isn’t execution: SCCL’s runtime is NVLink-only with its own protocol, and none of these systems implement the fusion, pipelining, and parallelization that separate an algorithm on paper from a fast kernel.
  • Composing library calls. Building hierarchical algorithms from NCCL invocations (as BlueConnect, PLink, Horovod-style systems do) works everywhere — and pays for it: every phase is a separate kernel launch, and no optimization can cross the boundary. The paper’s measurement is stark: the same hierarchical AllReduce, composed from NCCL collectives, is dramatically slower than the single-kernel MSCCLang version — the composition can’t pipeline the intra-node phase of one tile against the inter-node phase of another.
  • In-network aggregation. Programmable switches (SwitchML, ATP, SHArP) attack the same bottleneck with hardware; MSCCLang is the pure-software counterpart, squeezing the existing links.

How MSCCLang works

From Python to a single GPU kernel

  1. The DSL: declarative chunk routing new

    A collective is written as Python that routes CHUNKS (uniform contiguous spans of a buffer): chunk(rank, buf, idx) picks up a reference; .copy(dst,...) and .reduce(other) move and combine them, chaining fluently. Only the LATEST reference to any location is usable — staleness is a compile error, making programs data-race-free by construction. Each program pairs with its collective's pre/postcondition (e.g., AllReduce: every rank ends with every chunk's global sum), enabling automatic correctness validation before any hardware runs.

  2. Tracing → Chunk DAG new

    Executing the Python sequentially traces a DAG of copy/reduce operations with true dependencies (chunk movement) and false ones (buffer reuse) — the global picture of the algorithm's parallelism, independent of any hardware mapping.

  3. Instruction generation + fusion new

    Operations expand into per-GPU instructions (send, recv, local copy/reduce); remote reduces become send + recvReduceCopy pairs linked by communication edges. Peephole passes then fuse chains: a receive followed by a send becomes recvCopySend; receive-reduce-send becomes recvReduceSend when the local result is never reused — fused instructions pass values through REGISTERS, eliminating round-trips to global memory.

  4. Scheduling: thread blocks, channels, and guaranteed no deadlock new

    Every instruction is assigned to a thread block (each with at most ONE send peer and ONE receive peer, so blocks never serialize on shared connections) and every communication edge to a channel. Assignment follows a global topological order with depth-based priorities, which provably cannot introduce cycles — deadlock-freedom by construction. Cross-thread-block dependencies become explicit semaphore waits in the IR. User directives tune the schedule: ch= pins operations to parallel channels, parallelize(N) splits transfers N ways (one A100 thread block cannot saturate an NVLink), and multi-count chunks aggregate sends to amortize per-message startup cost.

  5. MSCCL-IR + interpreter runtime new

    The scheduled program serializes as MSCCL-IR: per-GPU, per-thread-block instruction lists. An NCCL-extension runtime interprets the IR inside a SINGLE cooperative kernel launch (all thread blocks resident simultaneously — required for the semaphores, and capping thread blocks at the SM count), reusing NCCL's point-to-point transports (NVLink, PCIe, InfiniBand with GPUDirect RDMA) and its three protocols (LL/LL128/Simple). Chunks bigger than a FIFO slot split into TILES whose execution pipelines across phases — intra-node and inter-node links run concurrently instead of taking turns.

  6. Deployment posture

    API-compatible with NCCL: existing frameworks swap it in unchanged. Each algorithm registers for a buffer-size range; anything outside falls back to NCCL's built-ins. Open-sourced as microsoft/msccl; running under Azure OpenAI's Copilot and 256-GPU Mixture-of-Experts training.

From Microsoft Research (ASPLOS 2023) — the same group behind CoCoNet (Maleki, Musuvathi, Saarikivi). Evaluated on Azure NDv4 nodes (8×A100, 12 NVLinks/GPU into 6 NVSwitches = 600 GB/s; 8×25 GB/s InfiniBand NICs) and DGX2 nodes (16×V100).

The idea

Describe where chunks go; let the compiler make it safe and fast. The load-bearing abstraction is chunk-oriented programming: the developer writes the algorithm’s data movement — chunk 0 rides the ring, gets reduced at each hop, then rides back — as straight-line Python whose sequential semantics define correctness. Everything concurrent is derived: the DAG exposes parallelism the trace implies, the reference-staleness rule outlaws races at the language level, and the topological scheduler cannot create deadlocks because a traced program always has a global order to respect. Correctness stops being the developer’s problem at exactly the moment — multi-GPU, multi-transport concurrency — where developers are worst at it, and the pre/postcondition check catches algorithmic (not just concurrency) bugs before hardware runs.

The performance is in the lowering, not the language. What separates a working collective from a fast one is a stack of low-level decisions the DSL surfaces as directives and the compiler executes mechanically: fusing receive-reduce-send chains so intermediate values never touch global memory; spreading a logical ring across multiple channels so sends and receives overlap (the trick behind the 1.9× AllReduce win — NCCL’s schedule is roughly one-channel-per-ring, parallelized 24×, and simply occupies a different point in a space MSCCLang can sweep); parallelizing transfers because one thread block can’t fill an NVLink; aggregating contiguous chunks to amortize the α in the α–β cost model; and tiling with cross-phase pipelining so intra-node and inter-node links work simultaneously. Each is folklore to NCCL’s authors; MSCCLang’s contribution is making them composable knobs on any algorithm, tunable per topology and size in minutes.

An interpreter, not a code generator — for deployability. MSCCL-IR is interpreted by one CUDA kernel rather than compiled into per-algorithm binaries. That choice buys the production posture: one runtime binary serves every algorithm; algorithms select dynamically by buffer size with NCCL fallback for everything else; and the whole system inherits NCCL’s transport matrix (the precise gap in SCCL, whose bespoke NVLink-only protocol can’t touch InfiniBand). The comparison with SCCL is honest about the price: SCCL’s direct-copy protocol beats MSCCLang’s FIFO-buffered Simple protocol at mid sizes — an interpreter overhead the authors acknowledge and defer.

From 30 lines of Python to the wire

The hierarchical AllReduce, end to end

Chunk = uniform contiguous span of buffer (N×G of them here: nodes × GPUs/node). Ring ReduceScatter/AllGather = each chunk circles the ring being reduced/copied. rrs = fused receiveReduceSend instruction. Channel = one of several parallel connections between a GPU pair. Numbers from the paper's 2-node A100 evaluation.

Step 1 of 7

Write the algorithm as chunk routes

Four phases in ~25 lines of Python: intra-node ring ReduceScatter (each GPU's chunks circle its node being summed), inter-node ring ReduceScatter (same-index GPUs sum across nodes), then inter- and intra-node AllGathers distributing the results. The code literally reads as "take chunk at (rank, buffer, index), reduce it into the neighbor, continue around the ring."

every algorithm in the paper: <30 LoC, 15–60 min to write and tune
no CUDA, no synchronization code, no transport code

Why it works

  • Sequential chunk-routing semantics define the algorithm; all concurrency is compiler-derived → races are excluded by the reference-staleness rule and deadlocks by topological scheduling of a traceable program → the failure modes that make hand-written collectives expert-only vanish, and postcondition checking catches algorithmic bugs pre-hardware.
  • Fusion keeps forwarded values in registers; channels overlap sends and receives; parallelization fills links one thread block can’t; aggregation amortizes per-send startup; tiling pipelines phases across fabrics → the optimizations that separate paper algorithms from fast kernels become mechanical passes and per-algorithm knobs → DSL programs MATCH or beat hand-tuned CUDA (1.3× on AllToAll, in one-fifth the code).
  • One cooperative kernel executes the whole collective → no launch overheads or kernel-boundary optimization walls → the composed-NCCL baseline’s exact losses become the headroom, and cross-phase tile pipelining becomes possible at all.
  • The runtime extends NCCL rather than replacing it → every transport (NVLink, PCIe, InfiniBand/GPUDirect), every protocol, and API compatibility are inherited → algorithms register per size range with automatic fallback — the difference between a research prototype (SCCL’s NVLink-only runtime) and something Azure ships.
  • Cheap authoring changes what gets explored → All Pairs (a 2-step latency-optimal AllReduce) and AllToNext (a collective MPI never defined, exploiting all 8 NICs of a node for pipeline-parallel handoffs, 14.5×) exist because trying an idea costs an afternoon.

What it costs

  • Scheduling is still on the human. The compiler guarantees safety, not optimality: channels, parallelization factors, aggregation, protocol, and tile size are hand-tuned per topology and size range (15–60 minutes per algorithm in practice). Auto-tuning the directive space is left open.
  • Interpreter overhead is real. SCCL’s direct-copy protocol beats MSCCLang’s FIFO-buffered transport at mid sizes; register pressure and instruction decode are the price of the one-binary deployment model.
  • Cooperative-launch constraints. All thread blocks must be resident simultaneously, capping a program’s thread blocks at the SM count and claiming those SMs for the collective’s duration — compute kernels can’t share them.
  • Communication only. MSCCLang schedules data movement; fusing computation INTO collectives (its sibling CoCoNet’s territory) is explicitly future work, as is any latency-hiding overlap with the model’s compute stream.
  • Per-size-range curation. The 1.9× wins come from hyper-specialized configurations valid in specific buffer ranges; someone must profile, register, and maintain the algorithm table per cluster type.

The evaluation

Two clusters (Azure NDv4: 8×A100 per node, 600 GB/s NVLink + 8 InfiniBand NICs; DGX2: 16×V100), NCCL and hand-written CUDA baselines, sizes swept from 1KB to 4GB, 50-iteration averages. The pattern across experiments: MSCCLang matches hand-tuned performance where it exists, beats it where the compiler’s scheduling outruns manual effort, and enables algorithms nobody would have hand-built at all — with production deployments as the closing argument.

Three ways to get a custom collective

PropertyNCCL menu (Ring/Tree)Hand-written CUDA / composed NCCL callsMSCCLang
Algorithm coverage
SafetyVendor-tested
PerformanceThe bar: heavily optimized
Runs onEverything NCCL supportsWhatever the author implements (SCCL's generated algorithms: NVLink only)
Production standingThe universal defaultBespoke, per-team
Clusters: Azure NDv4 (8×A100/node, 12 NVLinks→6 NVSwitches = 600 GB/s bidirectional; 8×25 GB/s HDR InfiniBand) up to 16 nodes/256 GPUs, and DGX2 (16×V100/node) up to 4 nodes. Baselines: NCCL 2.8.4, hand-optimized CUDA where available, best-effort kernels otherwise. 50-iteration averages after 20 warmups; AllToAll mid-size fluctuations attributed to shared-tenant InfiniBand congestion. SCCL comparison: MSCCLang faster at small sizes (LL protocol), slower mid-size (FIFO vs direct-copy transport).Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. A hierarchical AllReduce can be built by calling NCCL four times (intra-node ReduceScatter, inter-node ReduceScatter, inter-node AllGather, intra-node AllGather). Predict why this composition loses badly to the SAME algorithm in MSCCLang.

  2. 2. MSCCLang claims its programs are data-race-free and deadlock-free BY CONSTRUCTION. What two mechanisms deliver these guarantees?

  3. 3. MSCCLang beats NCCL's own Ring AllReduce — same algorithm — by up to 1.9× at mid sizes. Where does the gain come from?

  4. 4. AllToNext — GPU i sends its buffer to GPU i+1 — is not an MPI collective at all, yet MSCCLang's version beats the naive point-to-point implementation by up to 14.5×. What does this example demonstrate?

Sources

Related papers on GPUs & Parallel Hardware

See all 80 →