Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Four experts receive [64, 192, 32, 128] token rows. There are 416 useful rows, but rounding each group up to a multiple of 128 reserves 640 rows. Should we pay for that padding or launch four smaller matrix multiplications? Separate launches avoid this particular packing requirement, but still have edge tiles and launch overhead. Counting useful arithmetic alone won't tell us which wins.
This is the feed-forward counterpart to the previous chapter's irregular attention workload. A mixture-of-experts (MoE) layer in a large language model (LLM) routes tokens to selected expert networks. A rank, the process controlling one GPU in our example, must compute each expert's projection with however many rows arrive.
DeepGEMM is a CUDA library for exploring that trade-off. It isn't a model server: it doesn't admit requests, own a token budget, or expose HTTP. It supplies dense and grouped GEMM kernels, low-bit layouts, multi-query attention (MQA) scoring for DeepSeek's lightning indexer, and Mega MoE, which overlaps expert-parallel communication with expert compute.[1] Other serving systems call it. A kernel can win its microbenchmark and still leave end-to-end latency unchanged if the surrounding runtime spends the saved time elsewhere.
We'll keep the public Flash benchmark shape in view: 256 routed experts, top-k 6, hidden size 4096, and intermediate size 2048. Under eight-way expert parallelism (EP8), a balanced placement gives 32 experts per rank. Our four groups are a small subset used to understand packing, not the complete rank workload.[2]
The implementation discussion is pinned to commit 559d79fb6994a58b8a15b4b93bf13ccc16edf247 (July 15, 2026), verified against the official repository on September 2, 2026.[3] Runnable examples below use the CPU and standard library. They check arithmetic and dependencies, not CUDA execution, FP8 rounding, synchronization, or GPU speed.
What problem does DeepGEMM solve, and what problem does it leave to a serving runtime?
Answer
DeepGEMM solves selected GPU tensor operations and their data movement. A serving runtime still owns request scheduling, batching, admission, model loading, and end-to-end latency policy.
Project identity and system boundary
The repository is maintained in the DeepSeek AI open-source ecosystem. Its README describes a unified tensor-core kernel library and lists DeepSeek contributors. The Mega MoE release notes name a larger group because communication kernels, layouts, scheduling, and benchmarking cross subsystem boundaries.[1][4]
| Field | Current project fact |
|---|---|
| Origin and steward | DeepSeek AI publishes and maintains DeepGEMM under the deepseek-ai organization.[1] |
| Named contributors | The repository citation names Chenggang Zhao, Zhean Xu, Liang Zhao, Jiashi Li, Chenhao Xu, Anyi Xu, Shengyu Liu, Kexing Zhou, and Kuai Yu.[1] |
| Contributor model | Development happens through the public repository and focused kernel pull requests. The current snapshot doesn't declare a foundation, TSC, or formal committer ladder.[1][4] |
| Source license | MIT, with DeepSeek copyright, for the pinned source snapshot.[5] |
| Commercial boundary | DeepGEMM is a kernel library, not the DeepSeek API or a model release. Its own Mega MoE announcement separates kernel development from internal model releases.[4] |
| Asset boundary | DeepSeek model versions, weights, tokenizers, and training data can use terms different from the kernel library. Check the exact model card and version. |
That boundary also explains DeepGEMM's relationship to CUTLASS and CuTe. It borrows ideas from both while keeping a smaller set of public kernel functions. The narrow surface makes one generated path easier to inspect, but callers still have to prepare layouts and scales. No fallback can make a malformed tensor fast.
| Layer | DeepGEMM owns | Caller still owns |
|---|---|---|
| Tensor math | GEMM, grouped GEMM, MQA logits, fused Mega MoE | Model graph and layer ordering |
| Device schedule | Tile sizes, persistent task order, barriers, ring buffers | Request batching and token admission |
| Data representation | Kernel-required FP8, FP4, BF16, and scale layouts | Casting, transposition, padding, and source quantization |
| Compilation | Shape and architecture-specific JIT modules | CUDA toolchain, process launch, cache lifecycle |
| Measurement | Correctness tests and shape-specific benchmarks | Product SLOs, traffic mix, and end-to-end comparison |
What callers actually use it for
Follow one routed token through the graph and four kernel families answer different bottlenecks:
- Dense projection layers. Attention and feed-forward layers reduce to matrix products. DeepGEMM exposes BF16 and low-bit GEMMs for these hot loops.
- Mixture-of-Experts. Grouped GEMMs process the variable number of tokens sent to each expert. Contiguous and masked layouts target prefill and decode differences.
- DeepSeek indexer scoring. MQA scoring turns query vectors, compressed key/value vectors, and per-head weights into token-to-token logits. The kernel has non-paged and paged forms for prefill and decode, with an FP4 indexer path in later releases.[6][4]
- Expert-parallel inference. Mega MoE combines dispatch, two expert projections, SwiGLU, and combine so NVLink traffic can overlap tensor-core work.[4]
The README also lists HyperConnection (HC) GEMMs. We won't trace every primitive: dense GEMM establishes the memory path, grouped GEMM adds uneven routing, and Mega MoE adds communication. The indexer is a useful contrast because its output isn't an expert activation or a full attention result.
Research foundations and source trail
No single paper explains every DeepGEMM kernel. Read the sources by the question they answer: DeepSeek-V3 motivates sparse expert routing, DeepSeek-V3.2 describes the lightning indexer, DeepSeek-V4 supplies the Flash and Pro shapes, and the repository plus release pull requests record the implementation.[7][6][8]
Use the sources in four layers:
| Layer | Source | Question it answers |
|---|---|---|
| Model architecture | DeepSeek-V3 and V4 technical reports | Why total expert storage and active-token compute diverge, and which M/N/K the kernels see |
| Indexer behavior | DeepSeek-V3.2 paper | Why MQA scoring needs range-aware, low-bit logits |
| Kernel implementation | DeepGEMM repository | Which layouts, dtypes, architectures, and APIs are supported |
| Fused distributed path | Mega MoE release and benchmark PRs | How overlap is implemented and how one harness measured it |
A paper can motivate a workload without proving a kernel speedup. A benchmark can show a speedup without proving model quality. Keep those evidence types separate as we trace the code.
GEMM first: the contract behind the speed
General matrix-matrix multiplication, or GEMM, computes a matrix product and optionally adds an existing output. DeepGEMM's public naming convention is , and the common NT interface (non-transposed A, transposed B) implements .[1]
For that NT layout, has shape , has shape , and has shape . Each output element is a dot product of one row from A and one row from B. The operation count is about floating-point operations because each multiply is paired with an add.
For one output, take A's row [1, 2] and B's row [3, 4]: their dot product is . Pair A's next row [0, -1] with the same B row and the result is . A matrix multiplication repeats this operation over every row pair. B is stored as [N,K]; the transpose in the formula makes the inner K dimensions meet.
Now take a whole GEMM with , , and . It carries multiply-accumulate pairs, or FLOPs when multiply and add each count as one. At a hypothetical 10 microseconds, that would be about 1,718 TFLOP/s. No timing was measured for this example. Also, these are whole-matrix dimensions, not one thread block's tile size.
The hardware sees more than arithmetic. It sees global-memory loads, shared-memory staging, scale-factor loads, register or tensor-memory pressure, barriers, and output stores. A kernel can have high arithmetic intensity and still lose if a layout forces extra transposes or if a small M dimension leaves tensor cores idle.
Why can two GEMMs with the same FLOP count have different latency?
Answer
Their tile shapes, memory layouts, scaling-factor loads, occupancy, and synchronization can differ. FLOP count describes arithmetic work, not data movement or parallelism.
Read the performance ladder before the kernel
Take the output from the previous example and implement it without a library. A naïve kernel gives one thread one output element. Each thread walks all values and repeatedly requests overlapping A and B data from global memory. The result can be correct, but redundant loads and launch overhead are now exposed.
A tiled kernel gives a thread block a rectangle of D. It loads matching A and B rectangles into shared memory, then reuses those values while the K loop advances. Threads keep one or more partial sums in registers, close to the arithmetic. Shared memory cuts repeated global loads; registers keep the accumulator nearby. Both are finite resources, so a larger tile or more accumulators can reduce the number of active blocks through shared-memory or register pressure. If the register budget is exceeded, values can spill to off-chip local memory and erase the reuse the tile was meant to create.[9]
Tensor cores change the inner loop again. The kernel feeds matrix fragments to specialized matrix-multiply-accumulate units, carries partial sums across K tiles, and converts the accumulator when it stores D. Tile shape, operand layout, input precision, and accumulator type must match the architecture. DeepGEMM's TMA, TMEM, and tcgen05 path later in this chapter is this third stage made explicit for SM100, not a way around shape and layout checks.[10][1]
Make one prediction before profiling. With , much of a large output tile has no useful row work, so launch and staging overhead matter more. With , a block can reuse more loaded data and keep more tensor-core work in flight. If a tile change raises register use, occupancy may fall without the kernel getting slower or faster in a predictable direction. Measure the change rather than treating occupancy as the target.[9]
The performance receipt belongs beside the number. Record hardware and software, workload shape and token distribution, precision and algorithm, exact baseline, and the correctness method. Without those five labels, “faster” describes a run but not a result another engineer can reproduce.
Correctness comes before the receipt is useful. Compare the same inputs against a PyTorch or legacy reference with an explicit tolerance, and include one-token, ragged, edge-tile, and scale-boundary cases. Check output order and routed-token indices as well as the numerical error. A high TFLOPS value with a wrong expert row is a faster bug. NVIDIA's Compute Sanitizer can then check out-of-bounds or misaligned access, shared-memory hazards, uninitialized reads, and invalid synchronization while the numerical reference checks the answer.[11]
Precision and scaling
DeepGEMM's low-bit paths trade input precision for bandwidth and tensor-core throughput. FP8 here is E4M3: four exponent bits and three mantissa bits, commonly used for activations. FP4 here is E2M1 packed two values per byte, used for expert weights on the SM100 path. BF16 is a 16-bit operand and output format whose exponent range matches FP32. DeepGEMM's BF16 tensor-core paths keep matrix accumulators in FP32 before converting them to the configured BF16 or FP32 output.[1]
The kernel consumes scale factors in a specific layout. For the low-bit GEMM paths discussed here, SM90 uses FP32 scales and SM100's native representation packs four UE8M0 scales into one 32-bit torch.int. UE8M0 encodes unsigned powers of two with eight exponent bits and no mantissa. The API can prepare some scale layouts, so distinguish the native kernel representation from accepted wrapper inputs. Other operations, including the older FP8 indexer, have different scale contracts.[1]
Group size isn't universal either. SM90 FP8 paths commonly use K-group 128 with FP32 scales. The SM100 FP8×FP4 Mega MoE path uses K-group 32 with packed UE8M0. For a per-32 K-group, a row of K values splits into groups of 32 and each group gets one scale. A correct numerical tensor with the wrong stride can still fail an assertion or trigger a slow preparation path. The Tensor Memory Accelerator (TMA) stages compatible multidimensional tiles into shared memory, so its alignment requirements become part of the kernel contract too.
| Input path | Typical value format | Scale representation | What to watch |
|---|---|---|---|
| SM90 FP8 GEMM | FP8 A and B | FP32 scales, often K-group 128 | NT layout and TMA alignment |
| SM100 FP8 × FP4 GEMM | FP8 E4M3 A, packed FP4 E2M1 B | Packed UE8M0 integer scales, K-group 32 or 128 | Major layout and packed width |
| BF16 GEMM | BF16 A and B | No low-bit scale tensor | Accumulator type and tile shape |
| Mega MoE, FP8 × FP4 | FP8 activations, FP4 weights | Per-32 UE8M0 scales | Symmetric buffer and expert alignment |
Treat casts and layout transforms as a pipeline stage before the kernel. DeepGEMM provides helpers such as transform_sf_into_required_layout, but the README warns that input transposition and FP8 casting may be slower utility operations. In a production path, fuse those operations into an earlier model kernel when profiling shows they dominate.
Why a scale belongs inside the K reduction
Suppose the stored rows are [1, 2, 3, 4] and [2, 1, -1, 2]. Use two K groups, each of width two, with A scales [0.5, 2] and B scales [2, 0.25]. The first group's raw dot is 4 and its scale product is 1. The second group's raw dot is 5 and its scale product is 0.5. Adding the scaled partial dots gives , not the unscaled dot 9. A single scale applied after the whole reduction loses that distinction.
This CPU reference checks the grouping rule by comparing groupwise accumulation with explicit dequantization. Group width two is deliberately small for inspection; it isn't a supported DeepGEMM low-bit recipe.
1from math import isclose
2
3def scaled_dot(a, b, sa, sb, group):
4 if group <= 0 or len(a) != len(b) or len(a) % group:
5 raise ValueError("equal row lengths must be divisible by group")
6 if len(sa) != len(a) // group or len(sb) != len(sa):
7 raise ValueError("one scale per K group is required")
8 return sum(
9 sum(a[k] * b[k] for k in range(g * group, (g + 1) * group))
10 * sa[g] * sb[g]
11 for g in range(len(sa))
12 )
13
14a, b = [1, 2, 3, 4], [2, 1, -1, 2]
15sa, sb = [0.5, 2.0], [2.0, 0.25]
16grouped = scaled_dot(a, b, sa, sb, 2)
17dequantized = sum(
18 (x * sa[k // 2]) * (y * sb[k // 2])
19 for k, (x, y) in enumerate(zip(a, b))
20)
21assert isclose(grouped, dequantized) and grouped == 6.5
22assert grouped != sum(x * y for x, y in zip(a, b))
23print("groupwise", grouped, "dequantized", dequantized)1groupwise 6.5 dequantized 6.5Two architectures, two execution contracts
The repository supports NVIDIA SM90 (Hopper) and SM100 (Blackwell). Treat those as different execution contracts, not interchangeable names for a single GPU. A path selected for one family may use different scale types, layouts, instructions, and compiler flags on the other.[10]
The dispatch layer checks the device architecture, input major layout, scale dtype, and output shape before selecting an implementation. On SM90, the current FP8 path uses FP32 scales and supports the NT layout for the primary GEMM interface. On SM100, the FP8 × FP4 path can select NT, TN, NN, or TT variants and uses packed UE8M0 scales.[1]
A single portable kernel could hide those differences behind extra branches or generalized iterators. DeepGEMM keeps architecture-specific implementations visible, then uses a thin API layer to check the contract and route to the matching implementation.
An architecture mismatch is a contract failure, not a tuning problem. If an SM100-only FP4 path is sent to SM90, no tile-size tweak can fix it. Check torch.cuda.get_device_capability(), CUDA version, scale dtype, and major layout before comparing benchmarks. At the pinned revision, the Mega MoE implementation dispatches to SM100 and its test contains a TODO about skipping SM90. That comment isn't an implemented automatic skip.

Inside an SM100 tile
The dispatch decision leaves one tile to follow. Global memory holds the source tensors, TMA stages a compatible rectangle, and the tensor-core path accumulates partial products before the epilogue stores D. Each handoff has a different lifetime and synchronization rule, so a correct matrix formula isn't enough.
TMA moves the right rectangle
TMA, the Tensor Memory Accelerator, copies multidimensional tensor regions between global memory and shared memory using a descriptor. A descriptor carries shape and stride information so the copy engine can move a tile without every thread computing addresses.
TMA reduces address instructions and stages a copy asynchronously while a previous tile is being consumed. That lets a kernel overlap global-memory movement with tensor-core math, provided barriers and shared-memory stages are sized correctly.[9][10]
The catch is alignment. DeepGEMM's API checks TMA-aligned strides for input and scale tensors. A caller who transposes a tensor with a view may produce mathematically correct values but a stride pattern that can't satisfy the descriptor. The safe path is to create the required major layout explicitly, then validate both shape and stride.
TMEM holds accumulators and block scales
TMEM, or tensor memory, is specialized SM100 storage used by the tcgen05 instruction family. This block-scaled implementation reserves TMEM columns for both FP32 accumulators and scale factors. After TMA stages scales in shared memory, a shared-to-TMEM copy supplies them to the matrix instruction. Treating TMEM as accumulator-only storage misses a real data dependency.[3][12]
Keep those lifetimes separate when reading the code. Global memory holds model tensors. Shared memory stages TMA input. TMEM holds the matrix accumulator and the scales needed by this instruction. Each handoff has an ordering requirement.
tcgen05 issues MMA work
The SM100 implementation calls tcgen05 matrix multiply-accumulate instructions through CUTLASS and local PTX helpers. One instruction consumes a selected tile from shared memory and accumulates into TMEM. The epilogue then applies output conversion, activation-specific work, or a store.
The issuing CUDA warps follow instruction-specific participation and synchronization rules. Ordinary registers are thread-local, not a shared storage tier. DeepGEMM coordinates TMEM allocation, TMA completion, scale copies, asynchronous matrix completion, and epilogue reads. Some ordering is explicit and some is supplied by commit instructions, so adding or removing a fence by analogy to ordinary loads is unsafe.
| Resource | Role in one tile | Typical failure symptom |
|---|---|---|
| Global memory | Source A, B, C, and scale tensors | Low bandwidth or wrong stride |
| Shared memory | TMA staging and epilogue buffers | TMA barrier timeout or overwritten stage |
| TMEM | tcgen05 accumulators and block scales | Race, invalid read, or incorrect output |
| Tensor core | Multiply-accumulate execution | Low utilization on bad tile shape |
| Host JIT cache | Compiled cubin and metadata | Recompile latency or stale artifact |
Those five resources explain the JIT. Tile sizes, scale granularity, and architecture flags change generated code, so the library compiles a kernel for the signature it actually received instead of shipping every combination.
The runtime JIT and shape signature
DeepGEMM compiles kernels at runtime instead of requiring a complete CUDA build for every possible shape. The C++ JIT compiler creates source for a selected implementation, builds a compiled GPU binary (CUBIN), and loads it through a runtime handle. Installation can therefore skip a giant matrix of prebuilt kernels.[1]
The default compiler is NVCC. NVRTC is available behind DG_JIT_USE_NVRTC=1 and can compile faster, with a possible performance cost on some shapes. The cache key is a digest of kernel name, compiler signature, compiler flags, and generated source. A digest maps that signature to a directory under $HOME/.deep_gemm by default, or under DG_JIT_CACHE_DIR when configured. Compilation writes into a temporary directory, flushes files, and atomically renames the completed directory. That publish step matters when multiple distributed ranks try to compile the same shape on a shared filesystem. It prevents a reader from loading a half-built published directory; it doesn't prevent duplicate concurrent compilation or repair a filesystem that lacks the required rename semantics.[3]
1kernel signature = name + compiler + flags + generated source
2cache key = digest(kernel signature)
3cache hit = load existing cubin
4cache miss = compile temporary cubin, publish atomicallyThe JIT can specialize block sizes, scale granularity, architecture, and optional features. New generated signatures can therefore produce a compile tail. A new request shape doesn't necessarily mean a new binary: the library also exposes set_ignore_compile_dims to keep selected dimensions dynamic. Warm the signatures actually produced by your workload, not every imaginable (M,N,K) tuple.
What belongs in a signature
The signature should include every value that changes generated code or the memory contract: architecture, M/N/K tile choices, scale granularity, grouped versus dense mode, activation, and feature flags such as Programmatic Dependent Launch (PDL). If a value affects code but is missing from the key, a cached cubin can be reused for an incompatible layout.
DG_JIT_DEBUG, DG_JIT_PRINT_COMPILER_COMMAND, DG_JIT_DUMP_PTX, and DG_JIT_DUMP_SASS expose the compilation path. DG_JIT_PTXAS_CHECK=1 asks the build to reject local-memory usage. These switches are useful during kernel bring-up, but production launchers should control them deliberately because logs and compilation artifacts can be large.
Why is the JIT cache key more than a shape tuple like (M, N, K)?
Answer
Compiler version, flags, architecture, generated code options, scale format, activation, and layout can change the binary even when M, N, and K stay constant. The key must distinguish all code or ABI changes.
Dense GEMM still assumes one M for the whole launch. MoE breaks that assumption because each expert sees a different number of tokens. The grouped family has to carry those uneven M values without paying for one launch per expert.
Grouped GEMM: the MoE shape problem
An MoE layer routes each token to a small set of experts. Each expert sees a different number of tokens, so the straightforward choices are one GEMM per expert or padding every expert to the same M. Uneven routing makes each choice pay a different tax.
DeepGEMM's contiguous grouped API concatenates expert token segments into one M axis. N and K stay fixed because experts share weight dimensions. A compact layout tensor describes each segment, allowing one kernel to schedule multiple expert blocks while reusing one compiled implementation.
The segment boundaries still need alignment. The README requires each expert segment to meet the M block alignment returned by get_mk_alignment_for_contiguous_layout(). Padding belongs in the caller's token packing, and the resulting output must be unpacked to the original token order.
| Grouped mode | Known at launch? | Layout | Intended workload |
|---|---|---|---|
| Contiguous | Expert token counts known | Concatenated M segments | Training and prefill |
| Masked | Counts hidden from CPU | Fixed M plus validity mask | Decode with CUDA graphs |
| K-grouped backward | K segments known | Fixed M/N, grouped K | MoE weight gradients |
Masked grouped GEMM solves a different problem. During decode, the CPU may not know how many tokens each expert receives, especially when a CUDA graph fixes tensor shapes. A mask marks valid rows, so the GPU launches one shape while skipping invalid portions. That avoids a host synchronization, but it may execute less efficiently than a perfectly packed contiguous layout.
Worked routing example
Return to four of a Flash-shaped rank's 32 local experts. They receive [64, 192, 32, 128] routed rows. With M alignment 128, the contiguous pack reserves [128, 256, 128, 128] rows. Valid rows stay 416; padded capacity becomes 640. The first gated projection has , . The second has , .
Padding isn't free. It increases memory traffic and can lower arithmetic utilization. It can still beat four separate launches if one persistent kernel amortizes launch overhead and keeps tiles in flight. Measure total kernel time and useful-token throughput together. A low microsecond number with high padding can mislead.
Here M counts token-expert assignments, not unique source tokens. A token routed to six experts contributes six rows across the distributed expert workload. With hidden width H and intermediate width I, linear 1 costs FLOPs and linear 2 costs . Together that's . The next snippet uses that arithmetic to compare useful rows with a full-padded computation model. Actual kernel instruction counts require profiling; some paths skip invalid tiles.
1def align_up(value: int, alignment: int) -> int:
2 if value < 0 or alignment <= 0:
3 raise ValueError("nonnegative row count and positive alignment required")
4 return ((value + alignment - 1) // alignment) * alignment
5
6alignment = 128
7valid_counts = [64, 192, 32, 128]
8padded_counts = [align_up(count, alignment) for count in valid_counts]
9assert padded_counts == [128, 256, 128, 128]
10
11valid_rows = sum(valid_counts)
12padded_rows = sum(padded_counts)
13assert valid_rows == 416
14assert padded_rows == 640
15
16hidden = 4096
17intermediate = 2048
18
19def routed_flops(token_rows: int) -> int:
20 return 2 * token_rows * hidden * intermediate * 3
21
22useful = routed_flops(valid_rows)
23padded = routed_flops(padded_rows)
24assert useful == 20_937_965_568
25assert padded == 32_212_254_720
26print("valid rows", valid_rows)
27print("padded rows", padded_rows)
28print("useful GFLOPs", round(useful / 1e9, 2))
29print("padded GFLOPs", round(padded / 1e9, 2))
30print("padding share", round((padded - useful) / padded, 3))
31print("overhead vs useful", round((padded - useful) / useful, 3))1valid rows 416
2padded rows 640
3useful GFLOPs 20.94
4padded GFLOPs 32.21
5padding share 0.35
6overhead vs useful 0.538Those 20.94 useful GFLOPs cover only the two expert GEMMs for 416 routed rows in our subset. They exclude dispatch, combine, SwiGLU, and the other local experts. Padding is 35% of reserved rows but 53.8% overhead relative to useful rows. These are different denominators, not conflicting results.
Grouped GEMM handles the expert projections, but it can't fuse the DeepSeek indexer's dequantization, ReLU, head weighting, and range mask. That different dataflow needs its own kernel.
MQA logits for the DeepSeek indexer
Multi-query attention uses many query heads with one shared key/value representation. DeepGEMM's indexer kernel computes a weighted ReLU score for each query token against a selected range of key/value tokens. For query i and key/value token j, the README gives this computation:
The shared key vector is dequantized with its scale, each head computes a dot product, ReLU clips negative head scores, and per-head weights combine them into one score. This is the README's FP8 path with no separate query-scale input, not a universal formula for every newer scaled-query variant. It matches the weighted-ReLU indexer in DeepSeek-V3.2.[6]
Walk one score by hand before looking at the paged path. Query heads are and , weights are and , the stored key is , and its scale is . Dequantized is . The dots are and . ReLU keeps and zeros the negative head, so the logit is .
1def relu(value: float) -> float:
2 return value if value > 0.0 else 0.0
3
4q_heads = ((1.0, 0.0), (0.0, 1.0))
5weights = (0.4, 0.6)
6kv = (2.0, -1.0)
7scale = 0.5
8dequant = tuple(value * scale for value in kv)
9assert dequant == (1.0, -0.5)
10
11dots = [sum(q * k for q, k in zip(head, dequant)) for head in q_heads]
12assert dots == [1.0, -0.5]
13score = sum(relu(dot) * weight for dot, weight in zip(dots, weights))
14assert abs(score - 0.4) < 1e-12
15print("dequant k", dequant)
16print("head dots", dots)
17print("logit", score)1dequant k (1.0, -0.5)
2head dots [1.0, -0.5]
3logit 0.4The hand calculation explains what the specialized path adds. DeepGEMM offers non-paged scoring for prefill and paged scoring for decode. Paged mode reads key/value blocks through a block table, which matches a serving runtime's fragmented cache. The indexer can therefore sit beside, not inside, the main attention kernel: it selects useful context positions before the model performs a larger attention operation. FlashInfer already taught the paged-KV layout. This kernel consumes a similar table and writes a logit matrix instead of an attention output.
The bounds are part of correctness. A dense result can contain unwritten positions outside each query's valid key range. clean_logits=True fills those positions with negative infinity; without cleanup, the caller must exclude them explicitly. At this revision, the compressed non-paged mode requires clean_logits=False, so its bounds and output-index mapping remain the caller's responsibility. A zero-filled invalid position isn't safe either: head weights can make valid scores negative, and top-k could then select the invalid zero.[3]
Check that failure without a GPU. Here only positions 1 and 2 are valid; position 0 must never win retrieval.
1scores = [0.0, -0.4, -0.2] # position 0 is an invalid, zero-filled slot
2start, end = 1, 3
3wrong = max(range(len(scores)), key=scores.__getitem__)
4clean = [score if start <= j < end else float("-inf")
5 for j, score in enumerate(scores)]
6chosen = max(range(start, end), key=clean.__getitem__)
7assert wrong == 0 and chosen == 2
8assert max(range(len(clean)), key=clean.__getitem__) == chosen
9print("unmasked choice", wrong, "valid choice", chosen)1unmasked choice 0 valid choice 2For an empty valid range, return no selected keys instead of applying top-k to a row of negative infinities. Neither this bounds check nor the earlier two-dimensional dot example tests an actual supported CUDA shape.
The MQA path makes the specialization boundary concrete. Generic GEMM can express a dot product, but it doesn't automatically fuse dequantization, ReLU, head weighting, range masking, and cache paging. The specialized path saves intermediate writes and keeps the score reduction close to the input tiles. The same question returns in Mega MoE: which data can move while the tensor cores are busy?
Mega MoE: overlap communication and compute
Mega MoE is DeepGEMM's most ambitious kernel. It fuses expert-parallel dispatch, linear 1, SwiGLU, linear 2, and expert-parallel combine into one SM100 kernel. It uses PyTorch symmetric memory so ranks can address each other's buffers and overlap NVLink traffic with tensor-core work.[4]
The computation still follows ordinary MoE semantics:
- Router selects top-k experts and weights for each token.
- Expert parallelism dispatches token rows to owning ranks.
- Linear 1 produces gate and up activations.
- SwiGLU applies the gated activation.
- Linear 2 projects back to the hidden size.
- Combine returns weighted expert outputs to the source rank.
Schedule and storage provide the core mechanism. A symmetric ring buffer holds token data, scale data, routing indices, weights, and intermediate expert activations. Persistent schedulers claim blocks, move remote data into reusable slots, run L1 and L2 work, and release slots only after all consumers finish.

In this simplified schedule, each chunk needs one dispatch slot, two compute slots, and one combine slot. Two serial chunks take eight slots. With independent communication and compute resources, dispatch B can run during compute A, and combine A during compute B. Completion takes six slots. If both resources contend for the same bottleneck, or no second chunk is ready, that saving needn't occur.
This overlap is specialized expert-parallel traffic over NVLink and symmetric memory, not a general collective. The next chapter's NCCL library supplies the broader all-reduce, all-gather, and all-to-all contract that training and serving stacks compose on top of.
Persistent scheduling and ring reuse
Persistent scheduling means a resident set of cooperative thread arrays repeatedly claims work instead of launching a new grid for every expert block. Communication and compute phases can interleave, but more state lives in registers and shared memory. The source comments call out warmup waves, ring-block reuse, and ordering rules that prevent an L2 task from consuming an unfinished L1 block.
The ring buffer is bounded. A new dispatch can't overwrite a slot until the previous consumer signals that all N blocks are complete. That rule is a correctness invariant, not a performance hint. A missing release or early reuse can produce data races that look like occasional model-quality regressions rather than a clean CUDA error.
Symmetric memory contract
The Python API allocates a SymmBuffer for a distributed process group. For one rank it can use a regular CUDA allocation; for multiple ranks it uses PyTorch's symmetric-memory rendezvous. The buffer exposes typed views for input tokens, scale factors, top-k indices, top-k weights, and intermediate activations.
The documented low-bit Mega MoE path uses PyTorch 2.9 or newer, FP8 activations with FP4 weights, SwiGLU, SM100 hardware, and aligned dimensions.[4] The pinned source also exposes a BF16 variant. Expert parallelism across ranks needs a multi-process launch; a single-process smoke test doesn't exercise remote buffer synchronization or NVLink overlap.
The pinned Python call accepts recipe=(1, 1, 32), and the C++ API asserts that exact recipe for the low-bit Mega MoE path. Callers pack activations and weights with K-group 32 UE8M0 scales, transform the weights, copy routing inputs into the symmetric buffer, then invoke fp8_fp4_mega_moe. Passing another group width isn't a tuning option.[3]
1# Partial API sketch. Allocate and fill SymmBuffer before this call.
2buffer = deep_gemm.get_symm_buffer_for_mega_moe(
3 group, num_experts, max_tokens_per_rank, topk, hidden, intermediate_hidden
4)
5transformed_l1, transformed_l2 = deep_gemm.transform_weights_for_mega_moe(
6 l1_weights, l2_weights
7)
8deep_gemm.fp8_fp4_mega_moe(y, transformed_l1, transformed_l2, buffer)This is an unexecuted API sketch, not a copy-runnable lab. The caller must initialize the process group, cast activations, pack scales, copy routing inputs into the symmetric buffer before each call, and size output tensors. The source's weight transform also interleaves gate/up rows. A buffer with the right byte count but the wrong row ordering isn't a valid input.
Read the Mega MoE benchmark as a bounded experiment
DeepGEMM's benchmark pull request answers a narrow question: how does Mega MoE compare with a legacy dispatch, GEMM, activation, and combine path under eight-way expert parallelism? Values are averaged across eight ranks and two named DeepSeek-V4 configurations. They are evidence for that harness, not a universal speedup.[2]
DeepSeek-V4-Flash shape
The reported Flash shape has 256 experts, top-k 6, hidden size 4096, and intermediate size 2048. Batch size means source tokens per rank, before each token's six expert assignments. These are historical results from PR #316, merged April 24, 2026, not measurements made for this lesson.[2]
| Tokens per rank | Mega MoE time | Compute | Global memory | Interconnect | Speedup vs legacy |
|---|---|---|---|---|---|
| 1 | 56.5 µs | 5 TFLOPS | 1311 GB/s | 1 GB/s | 1.96x |
| 512 | 146.5 µs | 1056 TFLOPS | 3192 GB/s | 266 GB/s | 1.73x |
| 8192 | 1283.1 µs | 1928 TFLOPS | 998 GB/s | 499 GB/s | 1.56x |
| 32768 | 4855.5 µs | 2038 TFLOPS | 794 GB/s | 529 GB/s | 1.62x |
DeepSeek-V4-Pro shape
The Pro benchmark shape uses 384 experts, top-k 6, hidden size 7168, and intermediate size 3072. Larger dimensions change both arithmetic work and communication volume.[2]
| Tokens per rank | Mega MoE time | Compute | Global memory | Interconnect | Speedup vs legacy |
|---|---|---|---|---|---|
| 1 | 108.1 µs | 7 TFLOPS | 1758 GB/s | 1 GB/s | 1.61x |
| 512 | 369.6 µs | 1098 TFLOPS | 4619 GB/s | 182 GB/s | 1.54x |
| 8192 | 2818.5 µs | 2304 TFLOPS | 1094 GB/s | 393 GB/s | 1.50x |
| 32768 | 10655.2 µs | 2438 TFLOPS | 692 GB/s | 417 GB/s | 1.54x |
At 512 tokens per rank under EP8, the node holds source tokens, or 24,576 token-expert assignments for top-k six. Balanced routing would average 3,072 received rows per rank; individual ranks can differ. The source-token denominator isn't the local expert-row denominator.[2]
The compute and bandwidth columns are work estimates divided by elapsed time, not hardware-counter measurements of traffic. For example, FLOPs divided by 146.5 microseconds gives about 1,055 TFLOP/s, close to the rounded 1,056 reported. The pinned harness estimates global-memory bytes and NVLink payload bytes; cache behavior, protocol traffic, and scale movement can make physical traffic differ.[3]
The PR gives shapes and EP8 rank averaging but doesn't provide a complete GPU SKU, driver/toolkit, clock, topology, warmup, and correctness receipt for those historical rows. Don't fill that gap by guessing a Blackwell model. The pinned test's legacy comparison can also be skipped when baseline dependencies fail to import. A printed timing alone isn't evidence that correctness checks ran.
What does the 1.96x Mega MoE result prove, and what does it not prove?
Answer
For one Flash-shaped token per rank, the reported baseline-to-Mega time ratio was 1.96. Mega time was about 51% of baseline, a 49% reduction. It doesn't establish speedup on another topology or in a complete serving workload, and the historical environment receipt is incomplete.
Turn timing into a diagnosis
Start with Nsight Systems when the question is where time goes across the process. Its CUDA API and GPU workload traces show JIT compilation, host launches, memory transfers, kernels, and streams, so they can reveal preparation or serialization that a kernel stopwatch hides. Move to Nsight Compute for one selected kernel: Launch Statistics describes the grid, SpeedOfLight compares achieved compute and memory throughput with hardware ceilings, Memory Workload Analysis identifies the busy memory unit, and Occupancy or scheduler sections show whether resource pressure leaves warps waiting.[13][14]
Read those counters as clues, not verdicts. High occupancy doesn't guarantee high performance, and a low tensor-core percentage may be expected for a tiny M. A roofline point below the compute ceiling can mean low arithmetic intensity, poor reuse, or a launch that never feeds the units. Enable DG_JIT_WITH_LINEINFO=1 when source correlation matters, and use a narrow, repeatable profile range. Nsight Compute may replay a kernel to collect metrics, so keep its report for diagnosis and keep synchronized, unprofiled timings for the benchmark table.[9][14]
For Mega MoE, the profile needs one more join: route histogram, padded rows, rank topology, dispatch and combine readiness, and complete-layer time. If compute looks busy while the layer gets slower, inspect whether communication actually overlaps or whether casts, packing, or a barrier sit on the critical path. That evidence tells you which contract to change next.
Strengths and weaknesses
The same choices that make DeepGEMM fast also define where it fits. Shape-specialized kernels reward a team that controls its GPU fleet and tensor layouts; they ask more from callers that need portability or arbitrary shapes.
Strengths
- Shape-aware performance. The JIT can specialize tile sizes, layouts, scale granularity, and architecture instead of forcing one generic iterator.
- Small public surface. A few kernel APIs make it practical to trace from Python call to C++ checks to generated CUDA.
- Model-specific fusion. MQA scoring and Mega MoE remove intermediate tensors that a generic graph would materialize.
- Distributed awareness. Symmetric memory, ring buffers, and persistent schedulers address expert-parallel traffic directly.
- Useful correctness references. Tests compare outputs against PyTorch or legacy paths across dense, grouped, paged, and fused operations.
Weaknesses
- Narrow hardware support. Current public paths target SM90 or SM100, and Mega MoE is SM100-first. A machine with another architecture needs a different kernel or fallback.
- Strict layout contracts. Scale dtype, stride, major order, and alignment are part of correctness. The caller must prepare them.
- Compilation latency. New signatures compile on first use. A wide shape distribution can create cold-start tail latency.
- Operational complexity. Mega MoE needs multi-process setup, symmetric memory, correct barriers, and a compatible PyTorch release.
- Limited portability. CUDA, CUTLASS,
{fmt}, NVCC or NVRTC, and PyTorch versions all affect the supported path. - Benchmark locality. PR tables compare one harness. They don't replace a workload-level test with real request lengths and rank placement.
DeepGEMM chooses explicit contracts and measured specialization over broad portability. That fits a model team that controls hardware, CUDA images, and tensor layouts, but fits less well as a drop-in library for arbitrary user tensors. Bring-up should make that boundary visible before timing begins.
Bring up the contract before timing
The README lists Python 3.8 or newer, C++20-capable compilers, PyTorch 2.1 or newer for the base library, CUTLASS 4.0 or newer, and CUDA 12.3 or newer for SM90. CUDA 12.9 or newer is recommended for best performance and required by current SM100 support.[1] Mega MoE adds a PyTorch 2.9 requirement. Record those versions with every benchmark receipt.
Before running a benchmark, record the environment and the shape it will exercise:
- GPU model and compute capability.
- CUDA toolkit and driver versions.
- PyTorch and CUTLASS versions.
- Kernel mode, scale format, and layout.
- M, N, K or expert dimensions and token distribution.
- Number of ranks, rank-to-GPU topology, and interconnect.
- Warmup count, JIT cache state, synchronization method, and measurement window.
- Correctness tolerance and reference implementation.
For a first local check, start with a dense BF16 or FP8 GEMM shape from the repository tests. Enable DG_JIT_DEBUG=1 only while tracing compilation. Use DG_JIT_CACHE_DIR on fast local storage, not a path with unreliable distributed-file semantics. For multi-rank Mega MoE, run the repository's distributed test harness and compare output to the non-fused baseline before reading timing output.
Shape constraints to expose
Make constraints part of the model runtime's validation layer. Check that hidden and intermediate dimensions meet the required multiples, expert count is divisible by rank count, token counts fit the symmetric buffer, and scale tensors have the required packed width. Reject a request with a clear error before it reaches a CUDA graph or a collective barrier.
| Constraint | Why it exists | Guardrail |
|---|---|---|
hidden % 128 == 0 on low-bit Mega paths | Scale and tile packing | Validate model config at startup |
intermediate_hidden % 128 == 0 | Expert weight and scale layout | Check every expert block |
| Expert count divisible by rank count | Even local ownership | Assert before process-group launch |
| Contiguous grouped M alignment | Stable tile boundaries | Pad and preserve segment map |
| K-group 32 UE8M0 scales for FP8 × FP4 Mega MoE | Supported scale granularity | Keep gran_k in config and logs |
| SM90 or SM100 device | Architecture-specific implementation | Route unsupported devices to fallback |
Diagnose failures from their first symptom
Wrong output, no crash
Numerically wrong output with no crash usually points to scale packing, tensor major order, or output accumulation dtype. Compare a small shape against the PyTorch reference after dequantization. If the error appears only on SM100, inspect UE8M0 packing and TMA strides. If it appears only in grouped mode, print segment offsets and padded M counts.
First request is slow
If the first request is slow, check whether the JIT cache missed. Log kernel name, signature, compile duration, and cache directory. Warm expected signatures during startup or provide an explicit fallback. Report cold and warm latency separately, while keeping real cold requests in the product latency distribution.
Mega MoE hangs
If Mega MoE hangs, treat it as a synchronization or collective contract failure until proven otherwise. Confirm every rank entered the same process-group call, buffer sizes match, top-k indices are valid, and symmetric memory rendezvous completed. Enable communication-kernel debug only in a controlled reproduction because it zeros the buffer and requires callers to recopy inputs before each invocation.
Speedup disappears
If the speedup disappears, check whether casts, transposes, token packing, or network transport sit outside the timed kernel. Then compare useful-token throughput against padded-token throughput. A fused kernel can be faster while the full graph regresses if preparation dominates.
Thermal or topology drift
If results drift thermally or by topology, repeat after warmup and pin rank placement. A benchmark that runs eight ranks on a different NVLink topology from production can report a misleading interconnect rate. Record clocks and power policy when comparing machines.
A code-reading route through the repository
Use this order when studying the clone:
README.mddefines public APIs, supported architectures, scale formats, and examples.deep_gemm/__init__.pyexposes Python functions and utility transforms.csrc/apis/gemm.hppchecks shapes and dispatches dense and grouped GEMMs.csrc/jit/compiler.hppshows signatures, compiler flags, cache publication, and NVCC or NVRTC paths.csrc/jit_kernels/impls/sm100_fp8_fp4_gemm_1d1d.hppis the host wrapper that JIT-compiles one SM100 tile path.deep_gemm/include/deep_gemm/impls/sm100_fp8_fp4_gemm_1d1d.cuhtraces TMA, TMEM, and tcgen05 stages for dense GEMM.deep_gemm/include/deep_gemm/scheduler/mega_moe.cuhexplains expert task order, warmup, and ring capacity.deep_gemm/include/deep_gemm/impls/sm100_fp8_fp4_mega_moe.cuhconnects remote loads, grouped GEMMs, barriers, and stores.tests/test_attention.pyandtests/test_mega_moe.pyshow reference calculations and shape sweeps.
Read one data structure at a time. First find the shape and stride checks. Then find where a scale tensor is transformed. Finally trace who writes each buffer and who releases its barrier. This route is more productive than starting with a 1,000-line CUDA kernel and guessing the invariants.
What strong answers show
- Derive NT output dimensions and count multiply-adds without confusing a whole GEMM with a block tile.
- Place each K-group's scale product inside the reduction and identify the required native scale layout.
- Trace A, B, scales, and accumulators through shared memory and TMEM, including completion dependencies.
- Distinguish source tokens, routed rows, padded capacity, and local versus node-wide denominators.
- Explain overlap across ready chunks while preserving each chunk's dispatch, compute, and combine order.
- Separate CPU reference agreement, GPU correctness, historical throughput estimates, and serving latency evidence.
Follow-up questions
Why might a faster grouped GEMM make the layer slower?
Packing, scale conversion, dispatch, and output restoration may cost more than the saved kernel time. Compare complete-layer latency on the same route histogram and include preparation inside the measured boundary. Useful-row throughput and padded capacity help explain the result; neither substitutes for elapsed time.
What if the benchmark prints a time but no correctness result?
Check whether the legacy dependencies imported and the correctness loop ran. At the pinned revision, the harness can continue when its baseline isn't available. Mark that run as timing-only, restore the reference path, and test routed row counts and numerical outputs before using the result to choose a production kernel.
Architectural summary
- DeepGEMM is a kernel library, not a serving runtime.
- Data layout, tensor-core instruction, schedule, and model shape are selected together. Change one without the others and the kernel can miss its path.
- SM90 and SM100 use different scale formats and implementation paths. Architecture checks are part of correctness.
- Runtime JIT makes shape specialization practical, but cold-start compilation and cache hygiene become operational concerns.
- Grouped GEMM handles uneven MoE token counts with contiguous or masked layouts, each with a different host-knowledge trade-off.
- MQA indexer scoring fuses dequantization, head weighting, ReLU, and range or page selection for DeepSeek-V3.2.
- Mega MoE fuses expert dispatch, FP8 × FP4 projections, SwiGLU, and combine, then overlaps NVLink traffic with tensor-core work on SM100.
- Benchmark numbers need shape, hardware, rank topology, warmup, and baseline details before they can guide a production choice.