How Tokens Reach Their Experts During LLM Inference DeepSeek-V4.1-Flash, a Mixture-of-Experts model with 384 experts per MoE module that routes each token to 6 of them, requires tokens to travel between GPUs in every one of its 40 layers when served with expert parallelism, according to an analysis of SGLang's source code measured on one machine with eight B300 GPUs. With 48 experts per GPU and 4,096-token prefill chunks, about 177 MB of tokens arrive at each GPU per layer when tokens are sent only to GPUs holding their experts, versus 320 MB if every token goes to every GPU; a decode step with 64 requests per GPU moves under 3 MB per round trip. SGLang's `--moe-a2a-backend` flag accepts twelve values for this all-to-all step, yet the SGLang cookbook recipe for DeepSeek-V4.1-Flash does not pass the flag at all. Most of the large open models of the last two years are Mixture-of-Experts MoE models. In each layer, a small router looks at a token and picks a handful of experts for it, so only a fraction of the weights is used for any one token. DeepSeek-V4.1-Flash, for example, has 384 experts in every MoE module, and the router picks 6 of them per token. This works nicely on a single GPU. But once the model is spread over several GPUs with expert parallelism, each GPU holds only some of the experts, so a token is usually not on the GPU where its experts are. It has to travel there, and the result has to travel back, in every one of the 40 layers. Let’s say we serve this model on a machine with 8 GPUs and give each GPU 48 of the 384 experts. If every GPU is working on a prompt chunk of 4,096 tokens, then about 177 MB of tokens arrive at each GPU in every layer, provided that each token is only sent to the GPUs that hold one of its experts. If we simply send every token to every GPU, it is 320 MB. During decoding, a step with 64 requests per GPU moves less than 3 MB, but it still makes 40 of these round trips. So, how should the tokens be sent? In SGLang, the inference engine that I use as the reference in this series, the flag for this step, --moe-a2a-backend a2a is short for all-to-all , accepts twelve values. And the recipe that the SGLang cookbook recommends for this very model doesn’t pass the flag at all. Why are there so many ways to do this one step, what really differs between them, and when do we need one at all? In this article, I want to go through these ways one at a time, starting with the simplest one, and ask the same questions of each, which section 1.6 spells out. I will focus on what the GPUs send to each other and on what the receiving GPU has to do before its experts can run. I won’t cover the kernels that fuse the whole step with the expert computation DeepGEMM’s MegaMoE and its FlashInfer counterpart , since they get an article of their own. The mechanisms come from reading the source code, which I link along the way. The concrete numbers are for DeepSeek-V4.1-Flash https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash , and the measurements are my own, from one machine with eight B300 GPUs. 1. Why Tokens Have to Travel 1-why-tokens-have-to-travel If you have worked with expert parallelism before, you can skim this section. I would still read sections 1.3 and 1.4, since the rest of the article keeps coming back to two things from them: the number of copies of a token, and the problem that nobody knows in advance how many copies a GPU will receive. 1.1 Dispatch and Combine 11-dispatch-and-combine For every token, the router produces the ids of the chosen experts together with one weight per expert, and the output of the MoE module is the sum of the chosen experts’ outputs, each multiplied by its weight. With expert parallelism, each GPU holds a subset of the experts in full, which is 48 experts per GPU for DeepSeek-V4.1-Flash on 8 GPUs. A token then needs two trips. In the first one, called dispatch, its hidden state is sent to the GPUs that hold its experts. In the second one, called combine, the experts’ outputs come back to the GPU where the token started, and they are added up there. Figure 1 below follows a single token through both. Note in Figure 1 that the token needs three experts, but two of them E2 and E3 sit on the same GPU, so two copies of the token are enough. How large is one copy? The hidden state of DeepSeek-V4.1-Flash has 5,120 elements, and as 16-bit floats that’s 10,240 bytes, or 10 KB. I will call one copy of a token inside a tensor a row. I will call the kernel that runs all the experts of one GPU over their rows the expert kernel. And I will use two batch sizes as examples: a prefill batch, where the server processes a chunk of a prompt and each GPU has 4,096 rows to send, and a decode step, where each running request contributes one row and each GPU has 64. 1.2 Where the Tokens Are Before the MoE Module 12-where-the-tokens-are-before-the-moe-module Whether anything has to be sent at all depends on what happened in the attention module just before. If the attention module is split with tensor parallelism, the partial results of the GPUs are summed, and after that sum, every GPU holds the complete hidden state of every token. The alternative is called DP attention DP for data parallelism . Here, each GPU runs the complete attention module, but only for its own share of the requests, so it ends up holding only their tokens. Models with DeepSeek-style attention are often served this way because their KV cache would otherwise be stored on every GPU. Figure 2 below shows both situations. On the left side of Figure 2, every expert already has its tokens on its own GPU, whichever experts the router picks. On the right side, each token sits on one GPU, and most of its experts sit elsewhere. So the trips in Figure 1 are only needed in the second situation. When we ask SGLang for one of the all-to-all backends under tensor-parallel attention, it creates the second situation on purpose by letting each GPU keep only its slice of the batch. 1.3 How Many Copies 13-how-many-copies Now, to how many GPUs do we send a token? There are three natural answers, and they cost different amounts. The first is to send it to every GPU, which gives G copies on G GPUs. The second is to send one copy per expert, which gives 6 copies for our model, no matter how the experts are distributed. The third is the one from Figure 1: one copy for each GPU that holds at least one of the token’s experts. For the third answer, we can calculate the average. With E experts and k experts per token, a given GPU holds E/G experts, and the chance that none of the k chosen experts is among them is C E − E/G, k / C E, k , if the router picks uniformly. So, a token needs G × 1 − C E − E/G, k / C E, k GPUs on average. For DeepSeek-V4.1-Flash on 8 GPUs, this gives 4.43, so we have 8 copies, 6 copies, or 4.43 copies per token. All three numbers count the copy that stays on the token’s own GPU, which is one in eight on average. In bytes, a prefill batch with 4,096 rows per GPU delivers 320 MB, 240 MB, or 177 MB to each GPU in every layer. For a decode step with 64 rows per GPU, it’s 5.0 MB, 3.8 MB, or 2.8 MB. A real router doesn’t pick uniformly, so to see how far off the formula is, I recorded the router’s choices for a 7,700-token prompt in all 40 layers of the model. Figure 3 below shows the three answers for different numbers of GPUs, together with the recorded values. In Figure 3, the recorded values sit on the curve: with 8 GPUs, a token needed 4.41 GPUs on average, against the formula’s 4.43. Also, the gray line keeps growing with the number of GPUs, whereas the other two stay at or below 6. With 4 GPUs, sending a token everywhere costs 4 copies instead of 3.30. With 64 GPUs, it would cost 64 instead of 5.81. 1.4 Nobody Knows How Many Rows Will Arrive 14-nobody-knows-how-many-rows-will-arrive If every token goes to every GPU, each GPU knows that it will receive exactly G times the batch. As soon as the destination depends on the router, a GPU no longer knows how many rows it will receive. And the load is not even. In my recording, with 8 GPUs, the busiest GPU of a layer received on average 1.39 times as many rows as the average GPU, and in the worst layer 1.64 times. This is a practical problem, since the receiving side has to put the rows somewhere. Either the GPUs first tell each other how much they are going to send, and the receiver allocates a tensor of exactly the right size. Or the receiver reserves room for the worst case in advance, and a small counter next to the data says how many rows are valid. The first way is tidy, but it means that the CPU has to wait in the middle of the step, since it is the CPU that allocates tensors, and it can’t do so before it has read the count from the GPU. The wait itself is short, but it stops the CPU from queueing the following kernels ahead of the GPU. For a prefill batch, that hardly matters next to the work, but for decoding, it costs much more. A decode step is so small that the cost of launching its kernels from Python becomes noticeable, which is why inference engines record the sequence of launches once, with fixed shapes and fixed memory addresses, and replay it for every step. This is called a CUDA graph. A step in which the CPU reads a number from the GPU and then decides on the size of a tensor can’t be part of such a recording. So, the tidy way costs us a CPU round trip in each of the 40 layers, and the CUDA graph on top of it. 1.5 What the Expert Kernel Wants 15-what-the-expert-kernel-wants The receiving GPU also has to care about the order of the rows. The expert kernel multiplies each row by the weights of one particular expert, so it needs to know which rows belong to which expert. The kernels that we will meet do this in one of three ways, and Figure 4 below shows them for the same five tokens. From left to right in Figure 4, a contiguous kernel wants the rows of each expert next to each other, so a token that needs two experts appears twice. The contiguous kernel of DeepGEMM, DeepSeek’s kernel library, also wants each expert’s run padded to a multiple of 128 rows. A masked kernel gets a block of a fixed size for each expert, along with a count that tells it how many rows of the block to use. And a routed kernel takes the rows in whatever order they arrived, as long as each row comes with the ids of its experts. The first layout needs the exact count, so it belongs to the tidy way of the previous section. The other two have a fixed size, so they fit into a CUDA graph. 1.6 Summary 16-summary For each way of doing the round trip, I will ask the same three questions. How many copies of a token are sent? Does the CPU have to wait for a row count? And in what layout do the rows arrive, that is, what is left to do before the expert kernel can run? We will go through five answers: no backend at all, the two modes of the first version of DeepEP, DeepEP v2, and FlashInfer’s all-to-all. 2. No Backend: Every GPU Gets Every Token 2-no-backend-every-gpu-gets-every-token If we don’t pass --moe-a2a-backend at all, its value is none , and expert parallelism still works. So, what happens to the round trip? 2.1 Run Your Own Experts on Everything 21-run-your-own-experts-on-everything Nobody decides where a token should go. Every GPU gets every token, runs its own experts on the rows that need one of them, and skips the rest. Each GPU then holds a partial output for every token, which is zero where it had no expert to contribute. Adding up these partial outputs across the GPUs gives the output of the MoE module. With tensor-parallel attention, this needs no dispatch at all, because every GPU holds every token anyway. And the sum at the end is the same sum across the GPUs an all-reduce that a tensor-parallel MoE module would have needed. In this setup, SGLang’s dispatcher only translates the expert ids, so that the expert kernel can tell its own experts from the others simplified from here https://github.com/sgl-project/sglang/blob/f1065423/python/sglang/srt/layers/moe/token dispatcher/standard.py : Simplified from StandardDispatcher.dispatch local ids = torch.full num experts, , -1 -1: not on this GPU local ids first local : first local + num local = torch.arange num local topk ids = local ids topk ids the expert kernel skips -1 return hidden states nothing is sent This is exactly what the cookbook recipe for DeepSeek-V4.1 does. Its command line contains --tp 4 --ep-size 4 , which means tensor-parallel attention over 4 GPUs, with the experts divided among the same 4 GPUs. 2.2 With DP Attention: Gather and Reduce-Scatter 22-with-dp-attention-gather-and-reduce-scatter With DP attention, the tokens are not on every GPU, so they have to be brought there first. SGLang does this with the two collectives that the first article https://how-gpus-talk.vercel.app/ of this series described for a tensor-parallel MoE module: a gather before the experts, which gives every GPU the rows of all the GPUs, and a reduce-scatter after them, which adds up the partial outputs and returns to each GPU only the rows of its own tokens. Figure 5 below shows the whole pass. The faded rows in Figure 5 are the price of this approach, since the expert kernel of every GPU is handed all 8 rows. For a decode batch, the gather is usually an all-gather of rows that are padded to the same length on every GPU. When several GPUs are prefilling at once, SGLang instead lets every GPU write its rows at its own offset into a zero-filled buffer and sums the buffers with an all-reduce, which moves about twice as many bytes. 2.3 Why This Is Often Enough 23-why-this-is-often-enough So, why does the recipe for DeepSeek-V4.1 get away without a backend? The first reason is that it uses tensor-parallel attention, so there is nothing to dispatch. The second reason is the number of GPUs, and it would hold even with DP attention. On 4 GPUs, a gather delivers 4 copies of each token, whereas sending each token only where it is needed would deliver 3.30. That’s 21% more bytes, and in return, we need no extra buffers, no counting, and no library. On 8 GPUs, the gather delivers 8 copies instead of 4.43, which is 81% more. This is an argument about prefill. For a decode step, which moves a few megabytes, the number of kernels and handshakes per round trip matters more than the bytes. On our machine with 8 GPUs, a gather and a reduce-scatter for a decode step with 64 rows per GPU take {{agrs total 64 8}} microseconds together, and for a prefill batch with 4,096 rows per GPU, {{agrs total 4096 8}} milliseconds. We will compare these numbers with the alternatives in section 8. 2.4 Summary 24-summary To summarize in terms of our three questions, without a backend, a token is delivered to all G GPUs and under tensor-parallel attention, it is there already . The CPU never waits, because every GPU receives exactly G times the batch. And the rows arrive in the order of the senders with nothing left to do, but the expert kernel is handed all G times the batch and has to skip the rows it has no expert for. The obvious weakness is the number of copies, since with 8 GPUs, 3.6 of the 8 copies of a token go to GPUs that have no use for them. 3. DeepEP v1, Normal Mode: Exact Sizes, and the CPU Waits 3-deepep-v1-normal-mode-exact-sizes-and-the-cpu-waits The best-known library for sending tokens only where they are needed is DeepEP https://github.com/deepseek-ai/DeepEP , which DeepSeek open-sourced in February 2025. Its first version, which --moe-a2a-backend deepep selects in SGLang, has two modes that work quite differently. Let’s start with normal mode, which is the one for large batches. 3.1 Writing into Another GPU 31-writing-into-another-gpu All the remaining mechanisms in this article rely on one thing. Inside a machine with NVLink, a kernel that runs on one GPU can write directly into the memory of another GPU, as long as that memory was set up for it. Every GPU allocates a buffer at startup, and each buffer is mapped into the address space of all the other GPUs. After that, a kernel on GPU 0 can store a row into GPU 3’s buffer with an ordinary store instruction. This arrangement is called symmetric memory. Between machines, the network card does the same job, which is called RDMA. 3.2 Four Steps 32-four-steps With this in place, normal mode dispatches a batch in four steps. First, a small kernel goes through the expert ids of all the tokens and works out, for every token, the set of GPUs that hold at least one of its experts. This is where the 4.43 copies from section 1.3 come from: a token is sent once to each GPU in its set, however many of its experts live there. The same kernel counts how many rows each GPU will get. Second, the GPUs exchange these counts by writing them into each other’s buffers, and each GPU adds up how many rows it is about to receive. Third, the CPU waits for this number. The following excerpt is shortened from DeepEP’s dispatch function https://github.com/deepseek-ai/DeepEP/blob/567632d/csrc/deep ep.cpp L598-L656 : // Simplified from Buffer::intranode dispatch moe recv counter = -1; // memory that the CPU and the GPU share notify dispatch ... ; // a GPU kernel exchanges the counts while true { // the CPU spins here num recv tokens = static cast