Group-GEMM in XLA: Making Mixture-of-Experts Fast and Memory-Efficient XLA now lowers JAX's `ragged-dot` operation to Group-GEMM through two GPU backends: an AMD-specific backend built on hipBLASLt for ROCm and a new portable Triton code-generation backend that runs on both CUDA and ROCm. The work targets mixture-of-experts layers, which decompose into many small matrix multiplications with runtime-determined row counts, and the post evaluates both backends against a padded dense-batched-matmul baseline on AMD MI300 and MI350 GPUs using MaxText MoE models. The Triton backend emits one kernel per `ragged-dot` variant, giving a portable alternative to the vendor library. Group-GEMM in XLA: Making Mixture-of-Experts Fast and Memory-Efficient group-gemm-in-xla-making-mixture-of-experts-fast-and-memory-efficient Mixture-of-Experts MoE https://arxiv.org/abs/1701.06538 models have become a standard in most recent LLMs DeepSeek, Grok-1, Llama-4, Gemini . But under the hood, an MoE layer turns into something that GPUs and math libraries have historically been bad at: a large collection of small, independent matrix multiplications, each with a different, data-dependent shape. In JAX, users can describe this collection of matrix multiplications using an operation called ragged-dot , as shown in the following code snippet: php lhs shape: 5, 4 - 5 total input rows/tokens lhs = jnp.array 1.0, 0.5, 0.0, 1.0 , 0.2, 1.0, 0.5, 0.0 , 1.0, 1.0, 1.0, 1.0 , 0.1, 0.2, 0.3, 0.4 , 0.5, 0.5, 0.5, 0.5 , dtype=jnp.float32 rhs shape: 2, 4, 3 - 2 expert weight matrices of shape 4, 3 rhs = jnp.array 1.0, 0.0, 0.1 , 0.1, 1.0, 0.0 , 0.0, 0.1, 1.0 , 1.0, 1.0, 0.0 , Expert 0 0.5, 0.5, 0.5 , 0.2, 0.2, 0.2 , 0.3, 0.3, 0.3 , 0.4, 0.4, 0.4 Expert 1 , dtype=jnp.float32 Group sizes: Expert 0 processes 3 rows, Expert 1 processes 2 rows 3 + 2 = 5 = M group sizes = jnp.array 3, 2 , dtype=jnp.int32 Compute ragged dot product output = jax.lax.ragged dot lhs, rhs, group sizes Hence, even though the number of groups i.e., the number of matrix multiplications is known at compile time since Dimension 0 of rhs and group sizes are equal to the number of matrix multiplications , the number of rows i.e., the number of tokens to be extracted from lhs for each group is determined by the contents of group sizes and is not known until runtime. In other words, while the sizes k and n of each m x k x n matrix multiplication are known at compile time, the size m is not known until runtime, which is highly impractical for efficient computation and optimization. The naive way to run this operation is to pad everything into a single dense batched matmul — which wastes both compute on the padding and memory on the copies . The better way is a primitive called Group-GEMM : a batched matmul in which each matrix in the batch can have a different shape, computed in a single operation. This post walks through how XLA now lowers the ragged-dot operation to real Group-GEMM through two GPU backends: - an AMD-specific backend built on hipBLASLt AMD ROCm , and - a new portable code-generation backend built on Triton that runs on both CUDA and ROCm. By the end, you’ll understand what MoE looks like to a compiler, what ragged-dot is, why Group-GEMM is the natural solution, and how each of the two backends makes it work on the GPU. The post is organized in five parts: - Part 1 — Why MoE Turns Into a “Ragged” Matmul Problem part-1-why-moe-turns-into-a-ragged-matmul-problem sets up the problem: how an MoE layer becomes a ragged matmul, why the padded baseline is wasteful, and why Group-GEMM is the right primitive. - Part 2 — Backend 1: hipBLASLt Group-GEMM AMD ROCm part-2-backend-1-hipblaslt-group-gemm-amd-rocm covers the vendor-library backend, and how it bridges XLA’s compile-time planning with runtime group sizes. - Part 3 — Backend 2: Triton Ragged-Dot Portable CUDA + ROCm part-3-backend-2-triton-ragged-dot-portable-cuda-rocm covers the portable code-generation backend, with one emitted kernel per ragged-dot variant. - Part 4 — Performance Evaluation part-4-performance-evaluation measures both backends against the padded baseline on MI300 and MI350, using MaxText MoE models. - Part 5 — Conclusion and Next Steps part-5-conclusion-and-next-steps wraps up and outlines what comes next. Part 1 — Why MoE Turns Into a “Ragged” Matmul Problem part-1-why-moe-turns-into-a-ragged-matmul-problem A Quick Refresher: Dense FFN vs. Mixture-of-Experts a-quick-refresher-dense-ffn-vs-mixture-of-experts In a standard Transformer, every token passes through the same Feed-Forward Network FFN . If you want the model to “know more,” you make that FFN bigger — but then every token pays for the extra parameters, and compute grows with model size. Mixture-of-Experts breaks that link. Instead of one big FFN, an MoE layer has many smaller FFNs called experts . A small router a gating network looks at each token and picks the top-k experts that should process it often k=1 or k=2 . Only those experts run for that token. The result: you can add a lot of parameters many experts while keeping the per-token compute roughly constant, because each token only visits a couple of experts. Figure 1 below summarizes this flow. The Consequence for a Compiler: Variable-Size Groups the-consequence-for-a-compiler-variable-size-groups Here’s the crucial systems detail. Because the router assigns each token to experts based on the token’s content , the number of tokens that land on each expert is different, and it is only known at runtime. One expert might receive 2,000 tokens this step; its neighbor might receive 50; the split changes with every batch. If we gather all the tokens routed to the same expert into a contiguous block, an MoE layer becomes: - A stack of token rows, partitioned into G contiguous groups one per expert , where group g has m g rows and the m g values vary and are runtime-dependent. - Each expert g has its own weight matrix, so we need to compute output g = tokens g × W g , i.e. a matmul of shape m g, k × k, n . Figure 2 illustrates groups with unequal sizes: each color represents an expert. The mental model to keep for the rest of this post: An MoE layer is a batch of matmuls whose left-hand-side rows are split into contiguous, variable-size groups. That “variable-size groups” property is what we call ragged . The Naive Support, and Why It Hurts: Padded-Dot the-naive-support-and-why-it-hurts-padded-dot GPUs are well suited to regular, dense shapes. So the naive solution is to force the ragged problem back into a dense one: 1. Pick a fixed capacity C a maximum number of tokens per expert . 2. Pad every expert’s token block up to C rows. 3. Now every block has the same shape, so we can run one big batched GEMM over E × C, k × k, n . Figure 3 illustrates this padding process: every expert’s block is padded to the same C rows, where the grey padding is compute the GPU performs for nothing. This works, but it is wasteful in two ways: - Wasted compute. Any expert that received fewer than C tokens still does a full C -row matmul; the padding rows are pure overhead. - Wasted memory and bandwidth. The compact ragged input has to be expanded into E padded matrices, and the outputs have to be compacted again afterwards. Those scatter/gather and pre/post-processing steps cost extra memory and memory traffic. This “expand into padded matrices” approach is exactly the XLA baseline we’ll compare against later. The Ragged-Dot Operation the-ragged-dot-operation Rather than having users replace their MoE with a dense problem, JAX lets you describe the ragged computation directly with ragged dot . The basic form: jax.lax.ragged dot lhs, rhs, group sizes, ... with the shapes: - lhs is 2D: m, k — all the tokens, stacked, in group order. - rhs is 3D: group, k, n — one weight matrix per group per expert . - group sizes is 1D: group — how many rows of lhs belong to each group. - the output is 2D. The group sizes operand is the key: it is a small vector, known only at runtime, that partitions the m rows of lhs into contiguous groups. For example group sizes = m g0, m g1, m g2 means the first m g0 rows are group 0, the next m g1 rows are group 1, and so on. Note that jax.lax.ragged dot is a JAX-level operation. When the JAX program is compiled, it is converted into the HLO ragged-dot operation kRaggedDot , which is the actual input XLA receives and lowers. Figure 4 illustrates the operands of a ragged-dot. JAX also offers a more general form: jax.lax.ragged dot general lhs, rhs, group sizes, ragged dimension, ... which lets you choose which dimension is the ragged one. That choice yields the three variants listed in Table 1 and drawn in Figure 5: Table 1: The three ragged-dot variants exposed by jax.lax.ragged dot general . | Variant | Ragged dimension | What it means | Group index in output? | |---|---|---|---| | Non-contracting m | the “rows” dimension | the classic MoE case | No | | Contracting k | the reduction dimension | groups along the contracted axis | Yes | | Batch b | a batch dimension | effectively a regular batched GEMM | — | Inside XLA these correspond to the kRaggedDot variants kRaggedNonContracting , kRaggedContracting , and kRaggedBatch . For MoE, the one that matters most is non-contracting , where the rows tokens are ragged. Why Group-GEMM Is the Suitable Solution why-group-gemm-is-the-suitable-solution Group-GEMM is an API for matrix multiplication that generalizes the usual batched GEMM: instead of requiring every matrix in the batch to share the same shape, it lets each one differ — in size, transposition, activation, and scaling factors. In other words: Group-GEMM = a set of independent GEMMs of different shapes, computed in a single operation. That is a perfect match for ragged-dot , as ragged-dot is nothing more than a set of independent GEMMs of different shapes where the pointer to the input data can be calculated from the base pointer of the operand buffer + an offset derived at runtime from the group-size operands. Hence, Group-GEMM can take care of “unpacking” the ragged operand and computing all of the per-expert products in one kernel launch, with no padding, as shown in Figure 6. Compared to the padded-dot baseline, this directly removes both costs described in The Naive Support, and Why It Hurts: Padded-Dot the-naive-support-and-why-it-hurts-padded-dot : - No padding FLOPs — each group does exactly the work it needs. - No expansion/copies — the compact ragged layout is consumed as-is. The rest of this post is about the two backends XLA uses to turn ragged-dot into real Group-GEMM for AMD GPUs. Part 2 — Backend 1: hipBLASLt Group-GEMM AMD ROCm part-2-backend-1-hipblaslt-group-gemm-amd-rocm Overview overview AMD’s hipBLASLt library has supported Group-GEMM https://rocm.docs.amd.com/projects/hipBLASLt/en/develop/reference/ext-reference.html grouped-gemm since version 1.2.2. XLA already had a hipBLASLt backend for ordinary dot operations, so the natural move was to extend that existing backend to also lower ragged-dot down to hipBLASLt Group-GEMM. Figure 7 shows the lowering path of a plain dot , and Figure 8 the ragged-dot path added alongside it. The implementation reuses the existing matmul backend machinery and simply derives a GroupedMatmulPlan class from MatmulPlan , including the additional parameters required to run the hipBLASLt Group-GEMM. struct GroupedGemmConfig { uint64 t m, n, k; uint64 t batch count; uint64 t group count; int64 t lhs leading dim stride; int64 t rhs leading dim stride; int64 t c leading dim stride; int64 t output leading dim stride; blas::Transpose trans a, trans b; bool must swap operands; xla::complex128 alpha; double beta; blas::DataType type a, type b, type c, type d; int64 t stride ragged dim; int64 t stride group dim; int64 t c stride ragged dim; int64 t output stride ragged dim; // PrecisionConfig-level algorithm xla::PrecisionConfig::Algorithm precision algorithm; int64 t compute precision; RaggedDotMode ragged mode; std::optional