Field note · vLLM
Which NVFP4 MoE backend actually works on SM121 (it depends on the model)
One model needs --moe-backend cutlass and is slow under marlin. Another needs VLLM_SCALED_MM_BACKEND=marlin and crashes without it. NVIDIA officially recommends marlin. All three statements are true.
- Hardware
- NVIDIA DGX Spark · GB10 (SM121) · aarch64 · 128 GB unified
- CUDA
- 13.0
- Software
- vLLM 0.18.x — stock nightly and spark-fixes builds
Three answers, apparently in conflict
Three sources, three different answers to what sounds like one question — which NVFP4 MoE backend should vLLM use on SM121:
| Model | Working setting | Evidence |
|---|---|---|
| Mistral Small 4 119B NVFP4 | --moe-backend cutlass, stable 9+ hours under load. Marlin is FP4→FP16 decompression and very slow; flashinfer_cutlass / auto hit the TMA descriptor crash. | My own testing |
| Nemotron-3-Super 120B-A12B NVFP4 | VLLM_SCALED_MM_BACKEND=marlin on the stock nightly, which flips the NvFp4 MoE backend from FLASHINFER_CUTLASS to MARLIN. Full async speed, 14.1–14.8 tok/s sequential. | My own testing and write-up |
| Not model-specific | --moe-backend marlin | NVIDIA's official DGX Spark guidance |
Read quickly, that looks like it can't all be true. It's all true. Two things are easy to
conflate here, and both need to be said plainly: --moe-backend and VLLM_SCALED_MM_BACKEND
are different knobs, not two names for the same setting — and the right answer between them
is model-dependent, not hardware-dependent. Both rows above ran on the same DGX Spark GB10
hardware. What changed was the model sitting on top of it.
Two knobs, not one disagreement
--moe-backend is an explicit vLLM CLI flag. Set it to cutlass or marlin and vLLM uses
that MoE kernel implementation directly — that's the flag in the first and third rows above.
VLLM_SCALED_MM_BACKEND is not that flag. It's an environment variable that governs
quantized-GEMM backend selection more broadly, and in the middle row's config — where
--moe-backend was never set at all — setting it to marlin had the side effect of flipping
the resolved NvFp4 MoE backend from FLASHINFER_CUTLASS to MARLIN. Same destination,
different road. The two are easy to conflate because they share a value (marlin) and end up
choosing the same kind of thing.
It's worth being concrete about how easy this is to get wrong: the working Nemotron config
already had three other Marlin-flavored environment variables set —
VLLM_NVFP4_GEMM_BACKEND=marlin, VLLM_TEST_FORCE_FP8_MARLIN=1,
VLLM_MARLIN_USE_ATOMIC_ADD=1 — and all three were already in place while the server was
still crashing on the third request, every time. None of them touch MoE backend selection.
The one that stopped the crashes was VLLM_SCALED_MM_BACKEND=marlin, added on its own.
The diagnostic that ruled out broken kernels
Before that fix was found, the working theory could easily have been "these kernels are
fundamentally broken on SM 12.1." The diagnostic that ruled that out was
CUDA_LAUNCH_BLOCKING=1, set on a hunch.
That variable forces every CUDA kernel launch to be synchronous — no overlap between streams. With it set, every crash disappeared: ten sequential requests, then a full benchmark including 4- and 8-concurrent runs, zero failures. Throughput dropped from the ~11–13 tok/s seen on the first couple of successful async requests to about 7–8 tok/s — exactly the cost you'd expect from losing kernel overlap. Rock solid, just slower.
That result is the whole diagnosis in miniature. If serializing the kernel launches makes a
crash disappear, the kernels are not fundamentally incompatible with the hardware — something
about their overlap, on this architecture specifically, is unsafe. Before this test, crash
traces had landed in a different place almost every run — FlashInfer attention prefill,
scaled_fp8_quant, FlashInferFP8ScaledMMLinearKernel — which looks like three unrelated bugs
until you know CUDA reports these errors asynchronously, and any of those call sites could
simply be wherever the host happened to check next. CUDA_LAUNCH_BLOCKING=1 turned "three bugs
in three places" into "one race condition, observed in three places."
VLLM_SCALED_MM_BACKEND=marlin fixed the actual race at full async speed, so
CUDA_LAUNCH_BLOCKING was never needed in production here. But as a diagnostic step, before
you know what the fix is, it's the cheapest way I know of to tell "broken kernel" apart from
"async race."
Test your own model
I don't have a root cause that explains why Mistral Small 4 wants cutlass and Nemotron-3-
Super wants marlin. Nemotron-3-Super is a Mamba-2 + attention hybrid, with about 12B of its
120B parameters active per token through its MoE layer — a different architecture, a different
vLLM build, tested days apart. That may be enough to explain the divergence. It may not be. I
genuinely don't know yet, which is why this note is filed as "investigating," not "resolved."
What I am confident of is the shape of the mistake to avoid: don't take any single row of that
table — mine included — and apply it to a model it wasn't tested on. If you landed here from a
search for an SM121 MoE crash, the useful next step isn't copying a flag off this page. It's
setting CUDA_LAUNCH_BLOCKING=1 against your own model, confirming whether the crash
disappears, and then testing --moe-backend cutlass against VLLM_SCALED_MM_BACKEND=marlin
against NVIDIA's --moe-backend marlin on your own hardware — before trusting any of the
three, including this one.