Expert parallelism and systems
DeepSeek-V3's routed experts hold 654 billion parameters, about 1.3 TB in bfloat16. No GPU holds that, so the experts must live on many GPUs, and that changes the problem completely. On one device an MoE layer is a gather, some matrix products, and a scatter (§13.2). Across 64 devices, the gather and the scatter become network traffic. Every token has to travel to the GPUs that hold its experts and its results have to travel back, twice in the forward pass and twice more in the backward pass, in every MoE layer.
This section is about that traffic and the machinery around it. We lay out expert parallelism and check a dispatch by hand, derive how many bytes each GPU sends, find the ratio of compute to communication that decides whether the network or the arithmetic sets the speed, place the experts on a real cluster, see how an unbalanced router turns into idle GPUs and how redundant experts fix it, build the permutations and grouped matrix products that make the local work fast, and then turn to serving, where the same model needs a very different layout.
Expert parallelism
Expert parallelism (EP), made standard by GShard (Lepikhin et al. 2020), splits an MoE layer's experts over a group of GPUs, experts each. Everything else in the layer (attention, norms, the router) is data parallel. Each GPU runs attention on its own tokens with a full copy of those weights. Only the experts are sharded. (The placement note at the end of §10.6 shows where the expert-parallel group sits among the other axes of the device mesh, and this section works the placement out in detail below.) One MoE layer's forward pass then runs these steps on every GPU at once.
- Route. Compute the router and the top- choices for the local tokens.
- Permute. Sort the assignments by destination GPU (expert lives on GPU ) and count how many go to each.
- Exchange counts. A tiny all-to-all of integers, so each GPU knows how many rows it will receive from each peer and can allocate buffers.
- Dispatch. An all-to-all (§10.2) sends each block of rows to its destination.
- Compute. Each GPU sorts what it received by local expert and runs its experts, one grouped matrix product.
- Combine. A second all-to-all, the exact reverse of the dispatch, returns every output row to the GPU its token came from.
- Unpermute and weight. Undo the sort and scatter-add each row into its token with its gate.
The backward pass runs the same two all-to-alls in reverse order, carrying gradients. With capacity limits, step 2 also drops or pads, so every buffer has a fixed size and step 3 can be skipped. Dropless systems (MegaBlocks, DeepSeek-V3) keep step 3 and handle variable sizes.
A dispatch you can check by hand
Take two GPUs and experts, with experts 0 and 1 on GPU 0 and experts 2 and 3 on GPU 1, top-2 routing, and three tokens per GPU. GPU 0's tokens choose the expert pairs , , and . Four of its six assignments (to experts 0, 1, 0, 1) stay home and two (to experts 2 and 3) leave. GPU 1's tokens choose , , and , so it keeps four assignments and sends two (to experts 0 and 1). Write entry of a matrix as the number of rows GPU sends to GPU ,
Each row sums to , and always will, because every GPU ships each of its tokens times. Each column sums to the rows one GPU's experts must process, here 6 and 6, and the columns are what the router controls. Only the off-diagonal entries cross the network, and the combine all-to-all moves the same matrix transposed, since every row returns to where it came from. Now suppose every token had chosen . The matrix becomes , GPU 0 processes all 12 rows while GPU 1 sits idle, and the layer takes twice as long as with balanced routing. Hold on to that picture. It returns, with numbers, when we come to load imbalance below.
The cell simulates the whole forward pass with four "GPUs" as entries of a Python list, checks the result against a single-device MoE, and prints the same send matrix for a random router.
The row sums are all 20, while the column sums wander, even for this random router. The widget below lets you skew the router and watch the columns, and with them the busiest GPU, pull away from the mean.
Expert-parallel dispatch and combine
This is the expert-parallel coding question. Implement ep_moe(X, top, gate, experts, n_ep, all_to_all). GPU g holds tokens X[g] of shape (n_g, d), their chosen experts top[g] of shape (n_g, k), and gates gate[g] of the same shape. experts(e, rows) applies expert e to a 2-D array of rows. There are E = n_ep * experts.per_gpu experts, and expert e lives on GPU e // experts.per_gpu. Use exactly two calls to all_to_all(send), which takes send[src][dst] (any Python object) and returns recv[dst][src], one to dispatch and one to combine. On each destination GPU, run each local expert once on all the rows it received for that expert, from every source together (grouped, not row by row or source by source). Return (Y, counts), where Y[g] of shape (n_g, d) holds for each token and counts[s][q] is the number of rows GPU s sent to GPU q, the matrix of the worked example.
How many bytes move
Count the dispatch for one GPU. It holds tokens, each with assignments, and each assignment carries the token's -dimensional vector at bytes per number. If routing is balanced and an assignment's expert is equally likely to be on any of the GPUs, a fraction of the assignments leave the GPU. So the bytes each GPU sends in one dispatch, and by symmetry receives, are
and the combine sends the same number of rows back, . In plain words, every token ships copies of itself out and gets results back, and almost all of them cross the network once the EP group is large.
A training step does this twice per MoE layer forward (dispatch and combine) and twice backward (the gradient of the combine is dispatched, and the gradient of the dispatch is combined), so about per layer per step, assuming the backward uses the same precisions. Note what does not appear. The number of experts and the expert width do not change the bytes, only the compute.
A worked example at DeepSeek-V3's shape
Take and , a micro-batch of tokens per GPU, and EP over GPUs, as DeepSeek-V3 used in training. DeepSeek-V3 dispatches in FP8 () and combines in BF16 (). Then
Now the compute those bytes feed. Each GPU's experts process, on average, assignments, and each costs forward FLOPs for a SwiGLU expert with . That is FLOPs, or 2.9 ms at a bfloat16 peak of 989 TFLOP/s. Moving 693 MB over a 50 GB/s InfiniBand link, the cross-node bandwidth DeepSeek reports per H800 GPU, takes 13.9 ms. Over NVLink at the 160 GB/s they report, 4.3 ms. Either way, the communication takes longer than the arithmetic it serves.
That one example explains most of the engineering in DeepSeek-V3's MoE layer, and we can make it general. Divide the FLOPs by the bytes, and , , , and (for large groups) all cancel,
In plain words, the only thing that buys compute per byte is a wider expert. A GPU that does 989 TFLOP/s over a 50 GB/s link needs about 19,800 FLOPs per byte to keep the link from being the bottleneck. DeepSeek-V3's fine-grained experts give . Mixtral's wide experts (, both directions in BF16) give . Fine-grained experts, which §13.5 shows are better for quality, are exactly the ones that make communication hard.
The same ratio, written as time, is what a profiler shows. With a GPU doing FLOP/s and a link of bytes per second (the symbols of §10.2),
ignoring the factor , which is here. For DeepSeek-V3 over InfiniBand at peak FLOP/s this is , the 13.9 ms against 2.9 ms above. In plain words, a token travels the same distance whatever the expert does with it, so the narrower the expert, the more of the layer's time is spent on the wire.
A training step improves the ratio but does not change the story. The backward pass costs twice the forward FLOPs ( per assignment) and adds the same two all-to-alls, so over a whole step the ratio is , which is for DeepSeek-V3, still about a third of the 19,800 the link needs.
What DeepSeek-V3 did about it
Four techniques bring the problem under control, and each maps to a term above.
- Lower precision on the wire. Dispatching in FP8 halves . The combine stays in BF16 for accuracy.
- Node-limited routing. Each token may use experts on at most of the 8 nodes in the EP group. The router scores each node by the sum of the token's two highest affinities there, keeps the best four nodes, and picks its 8 experts among them. A token is sent across InfiniBand at most once per destination node, and then forwarded to the right GPUs inside the node over the faster NVLink. Because NVLink is about 3.2 times faster than InfiniBand (160 against 50 GB/s), the paper notes that one InfiniBand arrival can feed about 3.2 experts in that node at no extra cost, so the same traffic could serve up to 13 routed experts per token (4 nodes times 3.2), though the model uses 8.
- Overlap. Communication runs while other work computes. DeepSeek's DualPipe schedule interleaves the forward of one micro-batch with the backward of another, so one's all-to-all hides behind the other's matrix products, and custom kernels use only 20 of the GPU's 132 streaming multiprocessors to drive the network.
- No tensor parallelism for experts. Each expert lives whole on one GPU, so no all-reduce is needed inside it.
How much of the problem is left after the first two? The report says that cross-node expert parallelism still gave DeepSeek-V3 a computation-to-communication ratio of about one to one, and the formula above reproduces that order of magnitude in two steps. First, the 4.8 above sent every one of a token's nearly 8 remote copies over InfiniBand. With the limit, a token crosses InfiniBand once per destination node, at most 4 nodes, and if nodes were picked evenly its own node would be among them half the time, so about 3.5 copies. That cuts the InfiniBand bytes, and the ratio, by more than half, from 4.8 to about at peak FLOP/s. Second, the arithmetic does not run at peak. The whole run achieved about 35 percent of the BF16 peak, some 350 TFLOP/s (§8.9 derives it from the reported GPU hours). If the expert products ran at that rate, compute would take about three times as long as at peak, and the ratio would fall to about . In plain words, even with FP8 and the node limit, moving tokens takes about as long as computing on them, which is why overlap, the third technique, is not optional.
Bytes are not the whole cost. An all-to-all over GPUs is separate messages from each GPU, so in the latency and bandwidth model of §10.2 it pays a fixed latency per message on top of the bytes divided by the bandwidth. In training, with thousands of tokens per GPU, the bandwidth term dominates. In decoding, with a handful of tokens per GPU per step, each message is a few kilobytes and latency dominates. That is why DeepSeek's open-source DeepEP library ships two kinds of kernels, high-throughput ones for training and prefill that cross InfiniBand once and forward over NVLink, and low-latency ones for decoding that send directly over RDMA.
All-to-all bytes per GPU
Implement alltoall_bytes(T_loc, k, d, n_ep, s_disp, s_comb, training=False) returning the bytes one GPU sends over the network in one MoE layer, assuming balanced routing with experts spread evenly over the GPUs of the EP group (assignments to a local expert cost nothing). Count the dispatch and the combine of the forward pass, and if training is true, the two all-to-alls of the backward pass as well, at the same precisions. Return a float.
Node-limited top-k routing
Implement DeepSeek-V3's group-limited selection, node_limited_topk(scores, k, n_groups, top_groups). The E experts in scores of shape (T, E) (already bias-adjusted affinities) are split into n_groups consecutive groups of E // n_groups experts, one group per node. For each token, score every group by the sum of its two largest expert scores, keep the top_groups best groups (ties to the smaller group index), and choose the token's k experts as the largest scores among experts in the kept groups only (ties to the smaller expert index). Return idx of shape (T, k), in descending order of score. No loop over tokens.
Where the experts sit on the cluster
Expert parallelism needs no GPUs of its own. Take the data-parallel GPUs that hold one pipeline stage (§10.5). For attention, norms, and every other dense weight they are ordinary replicas. For the experts they are cut into expert-parallel groups of GPUs, and each group holds one full set of experts, of each MoE layer per GPU. Every expert therefore exists in
copies, one per group, and those copies form that expert's own data-parallel group. In plain words, a GPU is a full replica for the dense weights and one shard of for the expert weights, so dense gradients are averaged over all GPUs and an expert's gradients only over the GPUs that hold a copy of it.
Getting that group wrong is a silent bug. With 4 experts per GPU, GPU 0 of an expert-parallel group holds experts 0 to 3 and GPU 1 holds experts 4 to 7 in the same memory slots, so an all-reduce over all GPUs would add expert 0's gradient to expert 4's. Frameworks build separate process groups for the two cases, and ZeRO-style sharding of optimizer states (§10.3) follows the same split, the dense states over GPUs and the expert states over .
DeepSeek-V3's layout, counted
DeepSeek-V3 trained on 2,048 H800 GPUs with 16 pipeline stages, 64-way expert parallelism spanning 8 nodes, and ZeRO-1 data parallelism, with no tensor parallelism at all. Each stage has GPUs, which form two expert-parallel groups of 64, so and each GPU holds routed experts of every MoE layer in its stage. The routed experts are B parameters (one expert is , §13.1), about 40.9B per stage and 0.64B per GPU. Everything else (attention, the shared experts, the three dense layers, routers, norms, embeddings) is the remaining 17.1B, about 1.07B per stage, and it is replicated on all 128 GPUs of the stage. So on average a GPU holds 1.71B parameters, 3.4 GB in BF16, of a model that is 1.3 TB in BF16. Without expert parallelism, the same 16-stage layout would put 41.9B parameters on every GPU, 84 GB of weights alone. (Stages do not split the layers exactly evenly, so these are averages.)
Expert parallelism has a ceiling. With whole experts on each GPU, , so Mixtral's 8 experts fill at most 8 GPUs. Past that, each expert must be split too, with the column and row split of §10.4 applied inside every expert (Megatron calls it expert tensor parallelism). That adds an all-reduce of each expert's output and makes every expert's matrices thinner, so it suits models with a few wide experts. Models with hundreds of narrow experts, like DeepSeek-V3, use pure expert parallelism, because their experts are already small and there are enough of them to give every GPU a few.
An expert-parallel layout
Implement ep_layout(n_gpu, n_stages, n_ep, E, n_moe_layers, expert_params, other_params) for a cluster of n_gpu GPUs split into n_stages pipeline stages, with expert parallelism of degree n_ep inside each stage's data-parallel GPUs and no tensor parallelism. The model has n_moe_layers MoE layers of E routed experts with expert_params parameters each, and other_params parameters that are not routed experts. Return a dict with n_dp (GPUs per stage), n_edp (copies of each expert), experts_per_gpu (routed experts of one MoE layer per GPU), and params_per_gpu (the average parameters held by one GPU, as a float, assuming layers split evenly over stages). Raise ValueError if n_stages does not divide n_gpu, or if n_ep does not divide both n_dp and E.
Load imbalance becomes idle time
The all-to-alls synchronize the expert-parallel group, so the expert phase of an MoE layer lasts as long as the busiest GPU's. Write for the rows GPU 's experts process (a column sum of the dispatch matrix) and for the mean over the group. The fraction of the group's expert compute that does useful work is
the maximal violation of §13.3 measured per GPU instead of per expert. In plain words, a GPU that receives 50% more than its share makes the whole group run at two thirds of its speed. It also receives the most bytes, so the all-to-all slows by the same factor. This is what a capacity factor (§13.2) buys in expert-parallel training. Capping every expert at times its fair share caps every GPU at times the balanced load, so a step is never more than times slower than with perfect balance, and the price is paid in dropped tokens. Dropless systems give up that guarantee and rely on balancing instead.
Holding several experts per GPU helps, for two reasons you can derive in a line each. A GPU's load is a sum, over its experts, where is expert 's load. First, the busiest GPU carries at most while the mean is , so , never worse than the busiest expert. Second, if the expert loads fluctuate independently around with standard deviation , a sum of of them has standard deviation (variances of independent terms add) and mean , so its relative spread is . DeepSeek-V3's 4 experts per GPU halve the relative spread of one expert. Mixtral at EP8, with one expert per GPU, gets no averaging at all.
What averaging cannot fix is an expert that is hot every step. Its GPU is the straggler every time, and no amount of randomness evens it out. The fix DeepSeek-V3 uses in serving (below) is redundant experts. Give every GPU a few spare slots, put extra copies of the hottest experts in them, and split each expert's tokens evenly among its copies. Choosing the copies and placing them is a small scheduling problem, and a greedy method does well.
- Replicate. Hand out the spare slots one at a time, each to the expert whose load per copy, with its number of copies, is currently largest.
- Pack. Sort all copies by their load per copy, largest first, and put each on the least-loaded GPU that still has a free slot.
Step 2 is the longest processing time first rule from scheduling, which Graham (1969) proved finishes within of the best possible makespan on identical machines when there is no slot limit. DeepSeek's open-source expert load balancer, EPLB, is built on the same replicate-then-pack pattern.
Here is a case to check by hand. Eight experts with loads sit on 4 GPUs with 3 slots each, so there are 4 spare slots. The plain layout puts experts and on GPU , and GPU 0 carries against a mean of , so . The greedy plan gives all four spare slots to expert 0 (its load per copy goes , always above 10), packs the five copies of 18 and the seven tens, and ends with GPU loads , so . It is not optimal. Four copies of expert 0 (22.5 each) plus a second copy of one cold expert (5 and 5) give and . Greedy is a heuristic, a good one, and the widget lets you watch it work on realistic loads.
Redundant experts and balanced placement
Implement place_experts(load, n_gpu, slots) for expert loads load of shape (E,) (tokens per expert in a recent window), n_gpu GPUs, and slots expert slots per GPU, with n_gpu * slots >= E. Every slot must hold one copy of some expert, every expert must have at least one copy, and an expert's tokens are split evenly over its copies. Return (copies, placement), where copies of shape (E,) counts each expert's copies and placement is a list of n_gpu lists of expert ids, slots per GPU. Use the greedy method of the text (ties to the smaller expert index, then the smaller GPU index), or anything better. The tests check the layout is valid and that its busiest GPU is no busier than the greedy plan's. Raise ValueError if the slots cannot hold every expert.
Token permutation and grouped GEMMs
Inside each GPU, the received rows have to be grouped by local expert before the expert products can run, and the outputs ungrouped afterward. Both are permutations. If order is the stable argsort of the expert ids, then X[order] puts the rows in expert order, and the inverse permutation, inv[order] = arange(n), puts them back, Y_sorted[inv]. Production kernels fuse the permutation into the all-to-all or into the GEMM's loads, because a separate gather of a tensor costs a full read and write of HBM, the same reason for fusion as in §11.7.
Then come the products themselves. Expert has rows, and the differ from expert to expert and from step to step. There are three ways to run matrix products of different heights.
- A loop of GEMMs. One kernel launch per expert. Simple, but the launches add overhead and small give small, inefficient products.
- Pad to capacity and batch. Make every block exactly rows, so one batched GEMM of shape does everything. Fast and static, but padding rows cost real FLOPs and memory, and overflowing rows are dropped.
- A grouped GEMM. One kernel that receives a list of problem sizes and computes all products of different heights in one launch, scheduling the output tiles of §11.5 from all of them across the GPU. No padding beyond the tile size, no dropping. CUTLASS provides grouped GEMMs, and MegaBlocks (Gale et al. 2022) reached the same goal with block-sparse kernels, reporting training up to 40% faster than the Tutel MoE library while never dropping tokens.
The cell measures what padding costs on a realistic, slightly unbalanced load.
With the busiest expert at about 1.5 times the mean, capacity padding forces a bad choice. At it drops 7% of assignments and still pads the idle experts' buffers, and at it drops half a percent while computing a quarter more rows than needed. A grouped GEMM with 128-row tiles pads under 3% and drops nothing. That is why modern training stacks are dropless.
Permute and unpermute
Implement permute(expert_ids, E) for a 1-D array of the expert id of each of rows. Return (order, counts, offsets, inverse). order is the stable sort of the rows by expert id, counts of shape (E,) the rows per expert (zeros included), offsets of shape (E + 1,) the start of each expert's block with offsets[E] = n, and inverse the permutation that undoes order, so that X[order][inverse] equals X.
In PyTorch
On real hardware the two all-to-alls are calls to torch.distributed.all_to_all_single, which takes a flat send buffer and per-destination split sizes. The sketch below shows the dispatch and combine for one EP group. It cannot run in a notebook without several GPUs, so it is shown as static code.
The runnable version simulates an EP group inside one process. Each "rank" is a slot in a list, the all-to-all is a transpose of a list of lists, and autograd flows through it as it would through the real collective, because all_to_all_single is differentiable in the same way (its backward is the reverse all-to-all).
Serving an MoE model
Inference flips the cost structure. A decoding step processes one new token per sequence, so the matrix products are thin and the step is limited by reading weights from memory, not by arithmetic (§6.5, §11.2). For a dense model, every step reads every weight once, and a batch of sequences shares that read. For an MoE model, a step reads only the experts that some token in the batch chose, and each chosen expert sees only the tokens that chose it.
Count the arithmetic intensity of one expert's product. With tokens routed to it, the product does FLOPs per weight matrix and reads weights of 2 bytes, so the intensity is about FLOPs per byte. An H100 needs about FLOPs per byte (its ridge point, §11.2) to stop being memory-bound. So each expert needs roughly 300 tokens per step, and with experts and choices that means a batch of about tokens. For DeepSeek-V3, tokens per step per model replica. No single server with a few dozen users comes close, which is why MoE serving is organized around very large batches spread over many GPUs.
DeepSeek-V3's own deployment shows the pattern, with prefill and decoding served by separate deployments, each laid out for its own shape of work. For prefill (processing prompts, many tokens per request), the minimum unit is 4 nodes of 8 GPUs. Attention runs with 4-way tensor parallelism plus sequence parallelism and 8-way data parallelism, the MoE layers with 32-way expert parallelism, and 32 redundant experts (extra copies of the most heavily loaded experts, chosen from serving statistics and periodically rearranged, the replicate-and-pack problem of the load-imbalance section above) are added so each GPU hosts its 8 experts plus one duplicate. Prefill also runs two micro-batches of similar size at once, so the attention and expert computation of one hides the dispatch and combine of the other. For decoding, the unit is 40 nodes, 320 GPUs. Attention is 4-way tensor parallel with 80-way data parallelism, and the MoE layers use 320-way expert parallelism with one expert per GPU, and 64 GPUs hosting redundant and shared experts. One expert per GPU means each GPU's weight read is shared by every token in the whole 320-GPU batch that chose that expert.
At the other extreme, a single-GPU or laptop deployment has to hold all the experts somewhere. Mixtral in bfloat16 is 93 GB, so it needs two 80 GB GPUs, or 4-bit quantization (about 23 GB), or keeping most experts in CPU memory and copying them in when chosen. Offloading works better than you might expect because consecutive tokens often reuse experts (the temporal locality Mixtral's authors measured, §13.6), but it is slow, since a PCIe link moves a few tens of GB/s against an HBM's few TB/s.
| layout | how experts are split | communication per MoE layer | strengths | weaknesses |
|---|---|---|---|---|
| tensor parallel | every expert split across GPUs | all-reduce of | no load imbalance across GPUs, simple | every GPU touches every expert, all-reduce grows with |
| expert parallel | whole experts per GPU | two all-to-alls of | each expert's weights read once per batch, scales to hundreds of GPUs | load imbalance becomes GPU idle time, needs large batches |
| EP plus redundant experts | hot experts duplicated | same, better balanced | evens out real traffic | more memory, placement must track load |
| offloading | experts in CPU memory | PCIe copies of chosen experts | runs on small hardware | very slow, latency depends on cache hits |
Read more
- Lepikhin et al. (2020). GShard: Scaling Giant Models with Conditional Computation and Automatic Sharding. Where expert parallelism and its all-to-all came from.
- DeepSeek-AI (2024). DeepSeek-V3 Technical Report. Section 2.1 covers node-limited routing, Section 3.2 DualPipe and the all-to-all kernels, and Section 3.4 the inference deployment.
- Gale et al. (2022). MegaBlocks: Efficient Sparse Training with Mixture-of-Experts. Dropless training with block-sparse kernels.
- Hwang et al. (2023). Tutel: Adaptive Mixture-of-Experts at Scale. Switching parallelism strategies at runtime as routing changes.
- Rajbhandari et al. (2022). DeepSpeed-MoE: Advancing Mixture-of-Experts Inference and Training to Power Next-Generation AI Scale. Training and inference systems for MoE, including expert-parallel serving.
- Graham (1969). Bounds on Multiprocessing Timing Anomalies. The 4/3 guarantee for largest-first scheduling, the rule behind balanced expert placement.
- DeepSeek-AI (2025). EPLB: Expert Parallelism Load Balancer. The open-source replicate-and-pack placement of redundant experts.
- Hugging Face (2025). The Ultra-Scale Playbook: Training LLMs on GPU Clusters. The expert parallelism chapter places EP among the other parallelisms with diagrams.