<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>swesty.net</title><description>Field notes on LLM serving, fine-tuning, GPU performance, and agentic AI — measured on real hardware.</description><link>https://inference.swesty.net/</link><language>en-us</language><item><title>vLLM on the NVIDIA DGX Spark (GB10 / sm_121): A Complete Technical Guide</title><link>https://inference.swesty.net/blog/vllm-dgx-spark-technical-report/</link><guid isPermaLink="true">https://inference.swesty.net/blog/vllm-dgx-spark-technical-report/</guid><description>An all-in-one technical reference for running and understanding vLLM on the DGX Spark: the hardware, why the platform has been hard for vLLM, CUTLASS / FlashInfer / Marlin / QuTLASS, NVFP4-MXFP4-FP8 quantization wiring, fused-MoE backend selection, enforce-eager, the development history, and containers vs. source builds.</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A companion, published after the field notes.&lt;/strong&gt; I published the field-notes post, &lt;a href=&quot;/blog/vllm-on-dgx-spark-gb10-field-notes&quot;&gt;&lt;em&gt;vLLM on the DGX Spark, the Hard Way&lt;/em&gt;&lt;/a&gt;, first: that’s the operational story, what broke, what we run in production, and the measured numbers. This post is the deep background behind it, the kernel, CUTLASS/FlashInfer, quantization, and backend-oracle detail I had to work through to understand &lt;em&gt;why&lt;/em&gt; those field observations happen at all. Researching it was most of the actual work, and it seemed worth putting in one place for anyone hitting the same walls, so I’m releasing it as a companion. Read the field notes for what to do today; read this for why it works underneath. One caveat: this is a dated implementation audit (2026-06-02 against vLLM &lt;code&gt;main&lt;/code&gt;), so treat the version-specific operational bits as a snapshot and defer to the field notes’ timeline for the current state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why this report exists.&lt;/strong&gt; On 2026-06-01 the vLLM team published its DGX Spark deep-dive. We implemented its guidance on our production fleet &lt;strong&gt;the next day&lt;/strong&gt;, and then ran it for seven weeks of real traffic. This report is what that produced: a commit-level audit of the implementation details the high-level recipe leaves out (SM121 arch targeting, backend oracle behavior, dense-vs-MoE split routing, FlashInfer cubin/JIT behavior, CUDA graph escape hatches), plus an honest &lt;a href=&quot;#the-scorecard-what-worked-what-didnt-what-we-found&quot;&gt;scorecard&lt;/a&gt; of what worked, what didn’t survive production, and what we had to discover ourselves. Where the official blog and repository behavior diverge, the repository behavior is treated as authoritative for the audited commit. The body is a dated snapshot of that audit; the &lt;a href=&quot;#whats-changed-since-the-audit&quot;&gt;update timeline&lt;/a&gt; records what has changed between the audit and this post’s publication.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scope.&lt;/strong&gt; This is an all-in-one technical reference for running and understanding vLLM on the NVIDIA DGX Spark. It covers the hardware, &lt;em&gt;why&lt;/em&gt; the platform has been hard for vLLM, the kernel libraries involved (CUTLASS, FlashInfer, Marlin, QuTLASS), how quantization (NVFP4 / MXFP4 / FP8) is wired up, how vLLM’s fused-MoE machinery selects backends, what &lt;code&gt;--enforce-eager&lt;/code&gt; is and why it has been a crutch, the development history, and concrete guidance on prebuilt containers vs. source builds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Provenance.&lt;/strong&gt; Code citations are against the vLLM &lt;code&gt;main&lt;/code&gt; checkout at commit &lt;code&gt;689e...&lt;/code&gt; (2026-06-02). Release-timing and the operational recommendations are reconciled with the official blog &lt;em&gt;“vLLM on NVIDIA DGX Spark”&lt;/em&gt; (&lt;a href=&quot;https://vllm.ai/blog/2026-06-01-vllm-dgx-spark&quot;&gt;https://vllm.ai/blog/2026-06-01-vllm-dgx-spark&lt;/a&gt;). Where the repo and field experience diverge, the repo wins and the divergence is called out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⏱ Freshness &amp;amp; maintenance: implementation audit dated 2026-06-02 (vLLM &lt;code&gt;main&lt;/code&gt; @ &lt;code&gt;689e…&lt;/code&gt;).&lt;/strong&gt; &lt;code&gt;v0.22.0&lt;/code&gt; is treated throughout as the &lt;strong&gt;first audited stable baseline&lt;/strong&gt; containing the Spark fixes discussed, &lt;strong&gt;not&lt;/strong&gt; as the latest release as of this post’s date (stable releases have continued since; see the update timeline). Re-audit backend selection and dependency pins against the exact image or tag you deploy. sm_121 support is a fast-moving target (vLLM stable cadence ≈ every 2–3 weeks), so claims carry inline date tags: &lt;strong&gt;&lt;code&gt;[verified YYYY-MM-DD]&lt;/code&gt;&lt;/strong&gt; = checked against the repo/blog on that date; &lt;strong&gt;&lt;code&gt;[since vX.Y.Z]&lt;/code&gt;&lt;/strong&gt; = release a fix first shipped in. &lt;strong&gt;Time-volatile sections to re-check every vLLM release:&lt;/strong&gt; §4.4 (the &lt;code&gt;b12x&lt;/code&gt;-is-opt-in exclusion, the single highest-churn fact here; it flips when flashinfer-ai/flashinfer#3134 lands upstream), §7.2–7.3 (backend auto-selection order), §9 (release→fix mapping), §10 (recommended image + dependency pins). The architectural material (§1, §3, §5–§6, §8) is structural and ages slowly. When you re-verify, bump the date tag even if nothing changed: a current date is the signal the claim still holds.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-changed-since-the-audit&quot;&gt;What’s changed since the audit&lt;/h2&gt;
&lt;p&gt;The body below is the 2026-06-02 audit snapshot. Between that audit and this post’s publication (body assembled 2026-07-20; this timeline last extended 2026-08-05), the following moved. Each entry is either verified in our own production fleet (details and receipts in &lt;a href=&quot;/blog/vllm-on-dgx-spark-gb10-field-notes/&quot;&gt;the GB10 field notes&lt;/a&gt;) or checked against the public release record on the date shown.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2026-06-05 → 2026-07-05: MTP under real concurrency&lt;/strong&gt; &lt;em&gt;[fleet-verified]&lt;/em&gt;: in-checkpoint MTP validated at 86.8% acceptance, then hit &lt;code&gt;CUDA error: an illegal memory access&lt;/code&gt; every few hours in production, only under concurrent load (upstream #41190/#42084).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-07-11/12: native FP4 confirmed working in production&lt;/strong&gt; &lt;em&gt;[fleet-verified]&lt;/em&gt;: a genuine W4A4 checkpoint (4-bit activations, &lt;code&gt;lm_head&lt;/code&gt; unquantized) put dense &lt;strong&gt;and&lt;/strong&gt; MoE layers on the &lt;code&gt;FLASHINFER_CUTLASS&lt;/code&gt; native-FP4 path, faster than forced Marlin on our boxes, and it eliminated the MTP crash (8-way soak, 400/400, zero IMA). §6’s checkpoint-format emphasis aged well; any lingering “Marlin is mandatory on GB10” reading of §5 did not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-07-14: vLLM &lt;code&gt;v0.25.1&lt;/code&gt; released&lt;/strong&gt; &lt;em&gt;[PyPI, checked 2026-07-20]&lt;/em&gt;: multiple stable releases now postdate the audited &lt;code&gt;v0.22.0&lt;/code&gt; baseline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-07-19: official cu129 stable still ships no sm_121 cubins&lt;/strong&gt; &lt;em&gt;[fleet-verified]&lt;/em&gt;: smoke-testing the v0.25.1 stable candidates reproduced the report’s central lesson: wheel/cubin coverage, not version number, decides whether a path works. Our fleet runs digest-pinned nightlies for exactly this reason.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-07-27: v0.26.0 released; cu129 aarch64 stable now HAS sm_121 NVFP4 cubins&lt;/strong&gt; &lt;em&gt;[fleet scratch-tested 2026-08-05]&lt;/em&gt;: &lt;code&gt;auto&lt;/code&gt; selects &lt;code&gt;FLASHINFER_CUTLASS&lt;/code&gt; for both dense and MoE with no &lt;code&gt;no kernel image is available&lt;/code&gt;, which breaks the 2026-07-19 lesson above (the 0.22.0 → 0.25.1 cu129 cubin drought is over). It still could not serve our unsloth W4A4 checkpoint, though: EngineCore init dies on a CUTLASS FP8 W8A8 GEMM (&lt;code&gt;cutlass_gemm_caller.cuh:62, Error Internal&lt;/code&gt;), a path the same checkpoint clears on our cu13-line nightly. The lesson refines rather than breaks: the failure moved from a missing NVFP4 kernel image to an FP8 CUTLASS error, still wheel/CUDA-line-specific, not version-specific.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-07-29: #41190 (MTP illegal-memory-access) fixed on &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; &lt;em&gt;[PR #48245/#49620]&lt;/em&gt;: root cause turned out to be a kernel-agnostic scheduler bug (async-decode output-placeholder underflow under KV-pressure preemption), not the drafter kernel. Not yet in a tagged stable; upstream marks it a probable “may fix.” Stable-wheel operators stay exposed until a nightly carries it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-08-05: #42084 (prefix-caching + spec-decode OOB) appears resolved on a v0.26.1-dev nightly&lt;/strong&gt; &lt;em&gt;[fleet scratch-tested]&lt;/em&gt;: prefix caching and MTP now run together via an experimental Mamba/SSM cache-&lt;code&gt;align&lt;/code&gt; mode (it aligns the attention block size to the linear-attention page size). Validated with a shared-prefix + MTP soak (40% cache hit, 86.9% KV, zero OOB or asserts). This lifts the prefix-caching-vs-spec-decode either/or on recent nightlies, with the caveat that vLLM marks the align path experimental. See the field notes’ 2026-08-05 update for the run detail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-08-05: #31424 (NGC container lag) closed &lt;code&gt;not_planned&lt;/code&gt;&lt;/strong&gt;: the vendor-container lag is now permanent-by-neglect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-verified 2026-08-05:&lt;/strong&gt; §4.4’s &lt;code&gt;b12x&lt;/code&gt; auto-selection exclusion still holds, checked in &lt;code&gt;_POSSIBLE_NVFP4_KERNELS&lt;/code&gt; (dense) and the MoE oracle at v0.26.0 and &lt;code&gt;main&lt;/code&gt;; b12x is excluded from &lt;code&gt;auto&lt;/code&gt; on both paths and the flashinfer-ai/flashinfer#3134 guard has not landed. &lt;strong&gt;Still not re-audited, re-check before relying on:&lt;/strong&gt; §7.2–7.3 (backend auto-selection order) and §10 (image and dependency pins).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-scorecard-what-worked-what-didnt-what-we-found&quot;&gt;The scorecard: what worked, what didn’t, what we found&lt;/h2&gt;
&lt;p&gt;We adopted the blog’s recipe in one commit (2026-06-02, one day after publication) and have run it under 24/7 agent traffic since. The verdict, with receipts:&lt;/p&gt;
&lt;h3 id=&quot;what-worked&quot;&gt;What worked&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;cu130-nightly&lt;/code&gt; image track&lt;/strong&gt;: adopted as a gitsha-pinned nightly (0.22.1rc1.dev26, CUDA 13.0.2), per the blog’s own “pin a digest” clause. It unlocked native FP4 immediately: &lt;code&gt;--moe-backend auto&lt;/code&gt; → &lt;code&gt;FLASHINFER_CUTLASS&lt;/code&gt;, &lt;strong&gt;~55.8–58.5 tok/s vs ~50 on forced Marlin&lt;/strong&gt; (2026-06-02, batch-1 decode). Still the fleet’s serving basis today.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trusting &lt;code&gt;auto&lt;/code&gt; backend selection, on that image track.&lt;/strong&gt; The blog’s claim that “recent FlashInfer CUTLASS paths are much stronger than older Spark guidance suggests” measured out on our boxes the same day.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CUDA graphs enabled.&lt;/strong&gt; We retired &lt;code&gt;--enforce-eager&lt;/code&gt; fleet-wide the same commit (it had been a v0.20.0 Mamba graph-capture workaround, §8) and never looked back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in &lt;code&gt;--reasoning-parser nemotron_v3&lt;/code&gt;&lt;/strong&gt; over NVIDIA’s HF-card parser plugin: simpler, coherent, no plugin file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The JIT pre-warm ping&lt;/strong&gt; (&lt;code&gt;max_tokens: 3&lt;/code&gt; at boot): we run the same trick the blog describes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-didnt-or-didnt-survive-production&quot;&gt;What didn’t, or didn’t survive production&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;auto&lt;/code&gt; on the images that predated the blog.&lt;/strong&gt; On cu129/v0.20.0 images, &lt;code&gt;auto&lt;/code&gt; crashed; our cards from that era read “cu129 stock: auto crashes; marlin correct.” The blog’s advice was true of the image track it tested and wrong for the installed base that preceded it. Reconciling that (blog vs. repo vs. our logs) is how we learned it was a &lt;strong&gt;version gap, not a dispute&lt;/strong&gt;, and it’s why every claim in this report carries a commit and date.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The demo-shaped numeric defaults.&lt;/strong&gt; &lt;code&gt;--gpu-memory-utilization 0.85&lt;/code&gt; matches our own single-model figure, but co-residency forced 0.3–0.4, and taught us the unified-memory rule the hard way: &lt;strong&gt;the GPU reservation is unswappable, so the reservation itself is the memory pressure&lt;/strong&gt; (0.6 ≈ 71 GiB got our 35B OOM-killed on 2026-07-03). &lt;code&gt;--max-num-seqs 4&lt;/code&gt; and &lt;code&gt;--max-model-len 131072&lt;/code&gt; became 4/8/16 and 32K/262K per box role. None of these are errors in the blog; they’re demo-shaped numbers that don’t transfer to a multi-model production box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MTP as “evaluate.”&lt;/strong&gt; We productionized &lt;code&gt;--speculative-config&lt;/code&gt; MTP and paid the tuition: 86.8% acceptance in validation, then &lt;code&gt;CUDA illegal memory access&lt;/code&gt; every few hours &lt;strong&gt;only under concurrent load&lt;/strong&gt; (upstream #41190/#42084). The configuration that avoided it, a genuine W4A4 checkpoint putting experts &lt;em&gt;and&lt;/em&gt; drafter on the CUTLASS path, is the report’s biggest self-discovered lesson (below). The blog’s “validate before relying on it” clause was fully vindicated; batch-1 validation was not enough.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The fp8 KV-cache caution: inverted by measurement.&lt;/strong&gt; The blog frames &lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt; as a memory-pressure concession (“may affect predictability… noticeable performance cost… avoid unless memory pressure requires it and quality checks pass”). We ran the quality check it prescribes (2026-07-20, A/B on the production 35B, temp-0 task-scored extraction + greedy probes): &lt;strong&gt;byte-identical extraction output across arms 10/10 docs, fp8 &lt;em&gt;more&lt;/em&gt; deterministic intra-arm (10/10 vs 9/10), and ~27% faster decode&lt;/strong&gt; (96.6 vs 76.3 tok/s, halved KV traffic on a bandwidth-bound box, with MTP re-reading KV every verification step), plus double the 262K-context capacity in the same pool (~3.2 vs ~1.6 sequences). Not an error in the blog (its escape clause is exactly what we executed), but on decode-bound long-context serving the cost-benefit framing runs the other way: fp8 KV is a &lt;em&gt;default with a per-checkpoint validation step&lt;/em&gt;, not a concession.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-we-found-that-the-blog-never-mentions&quot;&gt;What we found that the blog never mentions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The checkpoint’s quant format decides the FP4 backend, not your flags&lt;/strong&gt; (W4A4 → native CUTLASS; W4A16 → Marlin fallback). The load-bearing rule of this entire report (§6), and the actual fix for the MTP crashes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;b12x&lt;/code&gt; is opt-in, not auto&lt;/strong&gt; at the audited commit (§4.4): &lt;code&gt;auto&lt;/code&gt; cannot select the Spark-native MoE path the blog’s era shipped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend flags the recipe needs but the blog never names&lt;/strong&gt;: &lt;code&gt;--attention-backend&lt;/code&gt; (TRITON_ATTN / flashinfer per model), &lt;code&gt;--mamba-ssm-cache-dtype float32&lt;/code&gt; for hybrid-Mamba, &lt;code&gt;--max-num-batched-tokens&lt;/code&gt; prefill chunking, and &lt;code&gt;--async-scheduling&lt;/code&gt;, including the discovery that spec-decode configurations can silently disable async scheduling and tax prefill (§the n-gram lesson in our &lt;a href=&quot;/blog/vllm-on-dgx-spark-gb10-field-notes/&quot;&gt;field notes&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coordinator interactions&lt;/strong&gt;: behind a llamactl-style proxy, &lt;code&gt;--served-model-name&lt;/code&gt; 404s every request; the blog’s standalone recipe breaks unmodified behind a gateway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unified-memory operational arithmetic&lt;/strong&gt;: KV-pool math from the boot log (per-token cost, boot floor ≈ 0.24 at 262K ctx, ≥15 GiB host headroom) that turns &lt;code&gt;--gpu-memory-utilization&lt;/code&gt; from folklore into subtraction.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#1-the-dgx-spark-architecture&quot;&gt;The DGX Spark architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-why-dgx-spark-has-been-hard-for-vllm&quot;&gt;Why DGX Spark has been hard for vLLM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-cutlass-the-templated-gemm-engine&quot;&gt;CUTLASS: the templated GEMM engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-flashinfer-the-kernel-serving-layer&quot;&gt;FlashInfer: the kernel-serving layer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-marlin-the-universal-fallback&quot;&gt;Marlin: the universal fallback&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-nvfp4--mxfp4--fp8-quantization-on-spark&quot;&gt;NVFP4 / MXFP4 / FP8 quantization on Spark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#7-how-vllm-does-moe-and-how-it-lands-on-spark&quot;&gt;How vLLM does MoE, and how it lands on Spark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#8-cuda-graphs-and---enforce-eager&quot;&gt;CUDA graphs and &lt;code&gt;--enforce-eager&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#9-the-history-how-spark-support-was-built&quot;&gt;The history: how Spark support was built&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#10-prebuilt-containers-vs-rolling-your-own&quot;&gt;Prebuilt containers vs. rolling your own&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#11-appendix-glossary-and-file-index&quot;&gt;Appendix: glossary and file index&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-the-dgx-spark-architecture&quot;&gt;1. The DGX Spark architecture&lt;/h2&gt;
&lt;h3 id=&quot;11-what-the-box-is&quot;&gt;1.1 What the box is&lt;/h3&gt;
&lt;p&gt;The DGX Spark is built on the &lt;strong&gt;NVIDIA GB10 Grace Blackwell superchip&lt;/strong&gt;, a Grace &lt;strong&gt;ARM64/aarch64&lt;/strong&gt; CPU and a Blackwell GPU fused onto one package with a &lt;strong&gt;128 GB unified LPDDR5X memory pool&lt;/strong&gt; shared between CPU and GPU. This single fact drives most of the operational quirks: there is no separate “VRAM”; weights, KV cache, the OS page cache, and the container runtime all draw from the same 128 GB. Memory bandwidth (~273 GB/s class) is far below a datacenter card, which is why decode throughput is the binding constraint on Spark, not compute.&lt;/p&gt;
&lt;p&gt;vLLM recognizes the unified-memory nature of the platform explicitly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vllm/utils/mem_utils.py:110&lt;/code&gt;: &lt;em&gt;“On UMA (Unified Memory Architecture) platforms where CPU and GPU share physical memory (e.g. GH200, DGX Spark, Jetson Orin)…”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vllm/platforms/interface.py:799&lt;/code&gt;: &lt;code&gt;is_integrated_gpu()&lt;/code&gt; documents DGX Spark as a UMA device.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;12-the-compute-capability-that-matters-sm_121&quot;&gt;1.2 The compute capability that matters: &lt;code&gt;sm_121&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The GB10 GPU reports &lt;strong&gt;compute capability 12.1&lt;/strong&gt;, i.e. &lt;code&gt;sm_121&lt;/code&gt; (architecture-specific target &lt;code&gt;sm_121a&lt;/code&gt;). This is the single most important number in this entire document. It places Spark in the &lt;strong&gt;“SM12x family”&lt;/strong&gt; of consumer/workstation Blackwell parts:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Compute capability&lt;/th&gt;
&lt;th&gt;&lt;code&gt;nvcc&lt;/code&gt; target&lt;/th&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;12.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sm_120a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;consumer Blackwell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 6000&lt;/td&gt;
&lt;td&gt;12.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sm_120a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;workstation Blackwell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DGX Spark (GB10)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;sm_121a&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;workstation Blackwell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B200 / GB200&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sm_100a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;datacenter&lt;/strong&gt; Blackwell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;vLLM’s own source documents the mapping in two kernel generators:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;csrc/quantization/marlin/generate_kernels.py:18&lt;/code&gt; and &lt;code&gt;csrc/moe/marlin_moe_wna16/generate_kernels.py:18&lt;/code&gt;:
&lt;code&gt;# SM89 and the SM12x family (SM120 RTX 5090, SM121 DGX Spark GB10)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;13-the-trap-sm_121-is-not-sm_100&quot;&gt;1.3 The trap: &lt;code&gt;sm_121&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; &lt;code&gt;sm_100&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Datacenter Blackwell (&lt;code&gt;sm_100&lt;/code&gt;) and workstation Blackwell (&lt;code&gt;sm_120&lt;/code&gt;/&lt;code&gt;sm_121&lt;/code&gt;) are &lt;strong&gt;different architectures with different tensor-core instruction sets&lt;/strong&gt;, even though both are marketed as “Blackwell.” A kernel compiled and tuned for &lt;code&gt;sm_100&lt;/code&gt; will not run on &lt;code&gt;sm_121&lt;/code&gt;. In CUTLASS terms they require entirely separate kernel instantiations: different &lt;code&gt;BlockwiseScaleConfig&lt;/code&gt;, different warp-specialized kernel schedules, and different &lt;code&gt;cutlass::arch::Sm1xx&lt;/code&gt; tags (see §3). The practical consequence: &lt;strong&gt;almost all generic “Blackwell NVFP4” guidance on the internet is written for &lt;code&gt;sm_100&lt;/code&gt; and does not transfer to Spark.&lt;/strong&gt; This is the root cause of nearly every Spark gotcha.&lt;/p&gt;
&lt;h3 id=&quot;14-what-the-tensor-cores-natively-support&quot;&gt;1.4 What the tensor cores natively support&lt;/h3&gt;
&lt;p&gt;The Marlin generator records the key hardware fact about the SM12x family:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;csrc/quantization/marlin/generate_kernels.py:18-23&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# SM89 and the SM12x family (SM120 RTX 5090, SM121 DGX Spark GB10)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# fully support mma.sync.aligned.m16n8k32.row.col.f32.e4m3.e4m3.f32.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# SM90 and SM100 can use this PTX, but it&apos;s simulated&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# with FP16 MMA, so it cannot achieve any acceleration.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if arch == 89 or arch // 10 == 12:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    SUPPORT_FP8 = True&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;So &lt;code&gt;sm_121&lt;/code&gt; has &lt;strong&gt;native FP8 (&lt;code&gt;e4m3&lt;/code&gt;) tensor-core MMA&lt;/strong&gt;, and (via &lt;code&gt;OpClassBlockScaledTensorOp&lt;/code&gt;) native &lt;strong&gt;block-scaled FP4&lt;/strong&gt; tensor cores. The hardware is capable; the historical problems were entirely about &lt;em&gt;software coverage&lt;/em&gt; for this specific architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-why-dgx-spark-has-been-hard-for-vllm&quot;&gt;2. Why DGX Spark has been hard for vLLM&lt;/h2&gt;
&lt;p&gt;Four compounding reasons:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Architecture fragmentation + arch-specific cubins.&lt;/strong&gt; Modern quantized GEMM kernels (CUTLASS, FlashInfer’s CuteDSL kernels) are compiled into &lt;strong&gt;architecture-specific cubins&lt;/strong&gt;. A wheel that ships &lt;code&gt;sm_100a&lt;/code&gt; and &lt;code&gt;sm_120a&lt;/code&gt; cubins but &lt;em&gt;not&lt;/em&gt; &lt;code&gt;sm_121a&lt;/code&gt; will fail at runtime on Spark, not with a graceful fallback, but with a hard error or, worse, silent garbage. The canonical symptom:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;csrc/.../fp4/nvfp4_scaled_mm_entry.cu:66-68&lt;/code&gt; (runtime SM dispatch):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;cpp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;STD_TORCH_CHECK_NOT_IMPLEMENTED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;No compiled nvfp4 mm kernel for SM &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, sm,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;. Recompile with CUDA &amp;gt;= 12.8 and CC &amp;gt;= 100.&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;sm_121&lt;/code&gt; is the newest, least-covered target.&lt;/strong&gt; &lt;code&gt;sm_120&lt;/code&gt; (RTX 5090) support landed in vLLM in mid-2025; &lt;code&gt;sm_121&lt;/code&gt; is a strict latecomer that had to be threaded through every kernel’s arch list one at a time (see §9). For a long window, &lt;code&gt;sm_121&lt;/code&gt; was &lt;em&gt;partially&lt;/em&gt; covered: some kernels had it, others silently excluded it, producing inconsistent behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. aarch64.&lt;/strong&gt; Spark is ARM64. Historically the CUDA Python ecosystem was x86-first; aarch64 CUDA wheels were patchy. This is now largely solved (PyTorch ≥2.11.0 ships aarch64 CUDA wheels on PyPI, and vLLM publishes aarch64 wheels), but it added friction during the early Spark period and still bites anyone on older torch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. CUDA toolkit floor.&lt;/strong&gt; &lt;code&gt;sm_121&lt;/code&gt; kernels require &lt;strong&gt;CUDA ≥ 12.8&lt;/strong&gt; to even compile; CUDA 13.0 is preferred (it introduces &lt;em&gt;family&lt;/em&gt; targets, see §3.4). Building against an older toolkit silently drops Spark from the arch list:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;CMakeLists.txt:99-110&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;cmake&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(CMAKE_CUDA_COMPILER_VERSION &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VERSION_GREATER_EQUAL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; 13.0)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(CUDA_SUPPORTED_ARCHS &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;7.5;8.0;8.6;8.7;8.9;9.0;10.0;11.0;12.0&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# family targets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;elseif&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(CMAKE_CUDA_COMPILER_VERSION &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VERSION_GREATER_EQUAL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; 12.8)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(CUDA_SUPPORTED_ARCHS &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;7.5;8.0;8.6;8.7;8.9;9.0;10.0;10.1;10.3;12.0;12.1&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(CUDA_SUPPORTED_ARCHS &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;7.0;7.5;8.0;8.6;8.7;8.9;9.0&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)               &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# no sm_121 at all&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;endif&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;The rest of this guide explains the moving parts that these four problems touch.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-cutlass-the-templated-gemm-engine&quot;&gt;3. CUTLASS: the templated GEMM engine&lt;/h2&gt;
&lt;h3 id=&quot;31-what-cutlass-is-and-why-it-matters&quot;&gt;3.1 What CUTLASS is and why it matters&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CUTLASS&lt;/strong&gt; (CUDA Templates for Linear Algebra Subroutines) is NVIDIA’s open-source C++ template library for building high-performance GEMMs directly on the tensor cores. vLLM uses CUTLASS 3.x (“c3x”) to implement its &lt;em&gt;first-choice&lt;/em&gt; quantized matmuls: FP8 W8A8, NVFP4 W4A4, and the blockwise-scaled variants. When CUTLASS works for your arch + dtype, it is typically the fastest path, which is why the backend oracles try it before falling back to Marlin.&lt;/p&gt;
&lt;p&gt;Key files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;csrc/quantization/w8a8/cutlass/scaled_mm_c3x_sm120.cu&lt;/code&gt;: FP8/INT8 scaled-MM entry for SM120/121&lt;/li&gt;
&lt;li&gt;&lt;code&gt;csrc/quantization/w8a8/cutlass/c3x/scaled_mm_sm120_fp8_dispatch.cuh&lt;/code&gt;: per-tensor FP8, with M-shape-based tile selection&lt;/li&gt;
&lt;li&gt;&lt;code&gt;csrc/quantization/w8a8/cutlass/c3x/scaled_mm_blockwise_sm120_fp8_dispatch.cuh&lt;/code&gt;: blockwise FP8&lt;/li&gt;
&lt;li&gt;&lt;code&gt;csrc/quantization/fp4/nvfp4_scaled_mm_sm120_kernels.cu&lt;/code&gt;: NVFP4 GEMM&lt;/li&gt;
&lt;li&gt;&lt;code&gt;csrc/quantization/fp4/nvfp4_blockwise_moe_kernel.cu&lt;/code&gt;: NVFP4 blockwise MoE&lt;/li&gt;
&lt;li&gt;&lt;code&gt;csrc/quantization/fp4/nvfp4_scaled_mm_entry.cu&lt;/code&gt;: runtime SM dispatch (the source of the “No compiled nvfp4 mm kernel for SM …” error)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;32-why-cutlass-needs-separate-kernels-for-sm_100-vs-sm_120sm_121&quot;&gt;3.2 Why CUTLASS needs &lt;em&gt;separate&lt;/em&gt; kernels for &lt;code&gt;sm_100&lt;/code&gt; vs &lt;code&gt;sm_120&lt;/code&gt;/&lt;code&gt;sm_121&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;CUTLASS kernels are parameterized by an &lt;strong&gt;architecture tag&lt;/strong&gt; that selects the MMA atoms and the warp-specialized scheduling strategy. Datacenter and workstation Blackwell get different tags and different schedules:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;&lt;code&gt;sm_100&lt;/code&gt; (datacenter)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;sm_120&lt;/code&gt;/&lt;code&gt;sm_121&lt;/code&gt; (workstation)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Arch tag&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cutlass::arch::Sm100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cutlass::arch::Sm120&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blockwise scale config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Sm100BlockwiseScaleConfig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Sm120BlockwiseScaleConfig&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel schedule&lt;/td&gt;
&lt;td&gt;&lt;code&gt;KernelTmaWarpSpecializedBlockwise{1,2}SmSm100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;KernelTmaWarpSpecializedBlockwise{Pingpong,Cooperative}Sm120&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These are &lt;em&gt;distinct compiled kernels&lt;/em&gt;; there is no shared binary. That is exactly why “it works on my B200” tells you nothing about Spark.&lt;/p&gt;
&lt;h3 id=&quot;33-the-enable_sm120_family-guard-one-binary-for-sm120-and-sm121&quot;&gt;3.3 The &lt;code&gt;enable_sm120_family&lt;/code&gt; guard: one binary for SM120 &lt;em&gt;and&lt;/em&gt; SM121&lt;/h3&gt;
&lt;p&gt;The mechanism that lets a single compiled kernel serve both RTX-class (&lt;code&gt;sm_120&lt;/code&gt;) and Spark (&lt;code&gt;sm_121&lt;/code&gt;) is a thin device-side wrapper:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;csrc/cutlass_extensions/common.hpp&lt;/code&gt; (≈ lines 128-142):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;cpp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// SM12x family includes SM120 (RTX 5090) and SM121 (DGX Spark GB10)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;template&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;typename&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Kernel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;struct&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; enable_sm120_family&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; : Kernel {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  template&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;typename&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;... &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Args&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  CUTLASS_DEVICE &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; operator&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Args&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;... &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;#if&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; defined&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; __CUDA_ARCH__&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  #if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;__CUDA_ARCH__&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1200&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; __CUDA_ARCH__&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1300&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Kernel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;::operator()(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;std&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;::&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;forward&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Args&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;gt;(args)...);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  #else&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    printf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;This kernel only supports sm120f.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    asm&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;trap;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  #endif&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;#endif&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;The range check &lt;code&gt;__CUDA_ARCH__ &amp;gt;= 1200 &amp;amp;&amp;amp; &amp;lt; 1300&lt;/code&gt; admits 1200 (&lt;code&gt;sm_120&lt;/code&gt;) &lt;strong&gt;and&lt;/strong&gt; 1210 (&lt;code&gt;sm_121&lt;/code&gt;), the whole SM12x family. Several Spark bugs over the past months were precisely cases where a kernel used a narrower &lt;code&gt;sm_120&lt;/code&gt;-only guard and thus &lt;em&gt;trapped or fell through on &lt;code&gt;sm_121&lt;/code&gt;&lt;/em&gt;; the fixes consisted of routing those kernels through &lt;code&gt;enable_sm120_family&lt;/code&gt; (e.g. the per-tensor FP8 dispatch fix, PR #41215).&lt;/p&gt;
&lt;h3 id=&quot;34-the-a-vs-f-suffix-and-cuda_archs_loose_intersection&quot;&gt;3.4 The &lt;code&gt;a&lt;/code&gt; vs &lt;code&gt;f&lt;/code&gt; suffix, and &lt;code&gt;cuda_archs_loose_intersection&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;nvcc&lt;/code&gt; arch targets carry a suffix:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;12.1a&lt;/code&gt; (architecture-specific):&lt;/strong&gt; one cubin for exactly &lt;code&gt;sm_121&lt;/code&gt;. To cover the family you must list every member: &lt;code&gt;12.0a;12.1a&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;12.0f&lt;/code&gt; (family):&lt;/strong&gt; &lt;em&gt;one&lt;/em&gt; cubin that runs across the whole SM12x family: &lt;code&gt;sm_120&lt;/code&gt;, &lt;code&gt;sm_121&lt;/code&gt;, and future SM12x. &lt;strong&gt;Requires CUDA ≥ 13.0.&lt;/strong&gt; (See NVIDIA’s family-specific-architecture docs.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;vLLM’s CMake picks the right form by toolkit version. For example, scaled-MM:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;CMakeLists.txt:755-758&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;cmake&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;${CMAKE_CUDA_COMPILER_VERSION}&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; VERSION_GREATER_EQUAL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; 13.0)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  cuda_archs_loose_intersection(SCALED_MM_ARCHS &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;12.0f&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;${CUDA_ARCHS}&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  cuda_archs_loose_intersection(SCALED_MM_ARCHS &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;12.0a;12.1a&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;${CUDA_ARCHS}&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;endif&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;The same pattern appears for &lt;code&gt;FP4_ARCHS&lt;/code&gt;, &lt;code&gt;MLA_ARCHS&lt;/code&gt;, &lt;code&gt;MARLIN_*_ARCHS&lt;/code&gt;, etc. The helper &lt;code&gt;cuda_archs_loose_intersection&lt;/code&gt; (&lt;code&gt;cmake/utils.cmake:368-416&lt;/code&gt;) is what makes the &lt;em&gt;family&lt;/em&gt; form match a user’s specific arch: given source &lt;code&gt;12.0f&lt;/code&gt; and a target list containing &lt;code&gt;12.1a&lt;/code&gt;, it strips suffixes, compares the &lt;strong&gt;major version&lt;/strong&gt; (both &lt;code&gt;12&lt;/code&gt;), and matches, so a single &lt;code&gt;12.0f&lt;/code&gt; build covers Spark. This family-level matching (added in PR #38126, “Fix DGX Spark logic”) is why a CUDA-13 build with just &lt;code&gt;12.0f&lt;/code&gt; correctly serves &lt;code&gt;sm_121&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Operator takeaway:&lt;/strong&gt; if you build from source on CUDA 12.8/12.9 you must list &lt;code&gt;12.1a&lt;/code&gt; (or &lt;code&gt;12.0a;12.1a&lt;/code&gt;); on CUDA 13.0+ &lt;code&gt;12.0f&lt;/code&gt; suffices. The in-repo Docker Grace-Blackwell example uses &lt;code&gt;&apos;9.0 10.0+PTX&apos;&lt;/code&gt;. That is for GH200/GB200 and is &lt;strong&gt;wrong for GB10&lt;/strong&gt;; you must override it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-flashinfer-the-kernel-serving-layer&quot;&gt;4. FlashInfer: the kernel-serving layer&lt;/h2&gt;
&lt;h3 id=&quot;41-what-flashinfer-is-in-vllm&quot;&gt;4.1 What FlashInfer is in vLLM&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;FlashInfer&lt;/strong&gt; is a kernel library that vLLM leans on heavily for inference-optimized kernels. vLLM’s wrapper is &lt;code&gt;vllm/utils/flashinfer.py&lt;/code&gt; (~1000 lines). Surface area spans:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Attention&lt;/strong&gt;: paged/ragged batch decode &amp;amp; prefill, TRT-LLM attention backends, MLA (DeepSeek), FP4 KV cache.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fused MoE&lt;/strong&gt;: TRT-LLM BF16/FP8/FP4 block-scale MoE, CUTLASS fused MoE, CuteDSL NVFP4 MoE, and the SM12x &lt;strong&gt;&lt;code&gt;b12x&lt;/code&gt;&lt;/strong&gt; fused MoE.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quantized GEMM&lt;/strong&gt;: FP4/MXFP4 quantize, NVFP4 scaled MM, block-scale FP8.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RoPE&lt;/strong&gt; and &lt;strong&gt;collective all-reduce&lt;/strong&gt; (MNNVL/NVLink).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;42-how-flashinfer-ships-kernels-prebuilt-cubins-vs-jit&quot;&gt;4.2 How FlashInfer ships kernels: prebuilt cubins vs. JIT&lt;/h3&gt;
&lt;p&gt;This is the crux of FlashInfer’s relationship with Spark. FlashInfer kernels arrive one of two ways (&lt;code&gt;vllm/utils/flashinfer.py:27-63&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prebuilt cubins&lt;/strong&gt;: the &lt;code&gt;flashinfer-cubin&lt;/code&gt; package (pinned &lt;code&gt;flashinfer-cubin==0.6.11.post2&lt;/code&gt; in &lt;code&gt;requirements/cuda.txt:13&lt;/code&gt;) downloads precompiled cubins from an NVIDIA artifact repository. Fast, but &lt;strong&gt;only covers the arches NVIDIA chose to build.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JIT&lt;/strong&gt;: if cubins are missing, FlashInfer compiles kernels at first request, which &lt;strong&gt;requires &lt;code&gt;nvcc&lt;/code&gt; to be present&lt;/strong&gt; in the container:
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; has_flashinfer_cubin() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; shutil.which(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;nvcc&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # FlashInfer unavailable: no cubins and no nvcc to JIT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; False&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why Spark broke:&lt;/strong&gt; for a long while the prebuilt FlashInfer wheels shipped &lt;strong&gt;zero &lt;code&gt;sm_120&lt;/code&gt;/&lt;code&gt;sm_121&lt;/code&gt; cubins&lt;/strong&gt;, and stock inference images don’t carry &lt;code&gt;nvcc&lt;/code&gt; (it’s in the &lt;em&gt;devel&lt;/em&gt; image, not &lt;em&gt;runtime&lt;/em&gt;). So FlashInfer’s FP4 path either errored (&lt;code&gt;Failed to run cutlass FP4 gemm on sm120/sm121 … Error Internal&lt;/code&gt;) or (the dangerous case) produced incoherent tokens with no error. This is the original sin behind the field rule “force Marlin on Spark.”&lt;/p&gt;
&lt;h3 id=&quot;43-b12x-the-modern-sm12x-moe--fp4-path&quot;&gt;4.3 &lt;code&gt;b12x&lt;/code&gt;: the modern SM12x MoE / FP4 path&lt;/h3&gt;
&lt;p&gt;The newer, Spark-native path is &lt;strong&gt;&lt;code&gt;b12x&lt;/code&gt;&lt;/strong&gt; (FlashInfer’s SM12x block-scaled GEMM, built on &lt;strong&gt;CuteDSL&lt;/strong&gt;, the CUTLASS C++ tensor-layout DSL). It exists in two forms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MoE expert:&lt;/strong&gt; &lt;code&gt;vllm/model_executor/layers/fused_moe/experts/flashinfer_b12x_moe.py&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“FlashInfer CuteDSL fused MoE expert for SM12x (SM120/SM121, RTX Pro 6000 / DGX Spark). Uses &lt;code&gt;b12x_fused_moe&lt;/code&gt; … which fuses token dispatch, two GEMMs, SwiGLU activation, and topk-weight reduction into a single kernel call. Input quantization (BF16→FP4) is performed inside the kernel … Only NVFP4 … is supported.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Notably, it takes &lt;strong&gt;BF16 hidden states directly&lt;/strong&gt; and quantizes inside the kernel; scale factors are pre-converted to MMA layout once in &lt;code&gt;process_weights_after_loading&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Dense linear:&lt;/strong&gt; &lt;code&gt;vllm/model_executor/kernels/linear/nvfp4/flashinfer.py:221&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;class FlashInferB12xNvFp4LinearKernel(NvFp4LinearKernel): &quot;&quot;&quot;NVFP4 GEMM via FlashInfer&apos;s b12x CuTe DSL warp-level MMA kernel (SM120+).&quot;&quot;&quot;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both require CC ≥ 12.0. &lt;code&gt;b12x&lt;/code&gt; landed in vLLM in &lt;strong&gt;PR #40082&lt;/strong&gt; (first released in &lt;strong&gt;v0.22.0&lt;/strong&gt;).&lt;/p&gt;
&lt;h3 id=&quot;44-the-critical-nuance-b12x-is-opt-in-not-auto-verified-2026-06-02-highest-churn-fact-re-check-each-release&quot;&gt;4.4 The critical nuance: &lt;code&gt;b12x&lt;/code&gt; is &lt;strong&gt;opt-in&lt;/strong&gt;, not auto &lt;code&gt;[verified 2026-06-02: highest-churn fact; re-check each release]&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is the single most under-appreciated fact for Spark operators, and it refines the common “just leave backends on &lt;code&gt;auto&lt;/code&gt;” advice. The official blog recommends leaving backend selection on &lt;code&gt;auto&lt;/code&gt;, but &lt;strong&gt;at the audited vLLM &lt;code&gt;main&lt;/code&gt; commit (&lt;code&gt;689e…&lt;/code&gt;, 2026-06-02), &lt;code&gt;b12x&lt;/code&gt; is deliberately excluded from auto-selection&lt;/strong&gt; until an upstream CUTLASS &lt;code&gt;sm_121&lt;/code&gt; MMA-guard issue (flashinfer-ai/flashinfer#3134) is resolved:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;vllm/model_executor/layers/fused_moe/oracle/nvfp4.py:170-173&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;NOTE&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;: the kernels are selected in the following order.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# FLASHINFER_B12X is intentionally excluded from auto-selection until&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# the upstream CUTLASS SM121 MMA op guard is resolved; use&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# moe_backend=&quot;flashinfer_b12x&quot; to opt in explicitly.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and the matching note for dense linears in &lt;code&gt;vllm/model_executor/kernels/linear/__init__.py:383-395&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# FlashInferB12xNvFp4LinearKernel excluded from auto-selection until&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# upstream CUTLASS SM121 MMA op guard is resolved; use&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# VLLM_NVFP4_GEMM_BACKEND=flashinfer-b12x to opt in explicitly.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;So on that code path, &lt;code&gt;--moe-backend auto&lt;/code&gt; will &lt;strong&gt;not&lt;/strong&gt; pick the Spark-native &lt;code&gt;b12x&lt;/code&gt; kernel; it walks the list below (§7) and lands on whatever else supports &lt;code&gt;sm_121&lt;/code&gt;. To actually use &lt;code&gt;b12x&lt;/code&gt; you pass &lt;code&gt;--moe-backend flashinfer_b12x&lt;/code&gt; (and/or &lt;code&gt;--linear-backend flashinfer-b12x&lt;/code&gt;). This is why the audited-commit Spark recipe is &lt;strong&gt;either&lt;/strong&gt; “force Marlin,” &lt;strong&gt;“explicitly opt into &lt;code&gt;b12x&lt;/code&gt; and validate,”&lt;/strong&gt; or, as our production fleet later confirmed, get onto the &lt;code&gt;FLASHINFER_CUTLASS&lt;/code&gt; native path with a genuine W4A4 checkpoint. Re-check this against the exact release you deploy: it is expected to change once the upstream guard lands.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update &lt;code&gt;[verified 2026-08-10]&lt;/code&gt;:&lt;/strong&gt; the exclusion still holds at v0.26.0 and &lt;code&gt;main&lt;/code&gt;, but two details in the quoted source comments have since moved — treat the comments above as a 2026-06-02 snapshot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The env-var opt-in is gone.&lt;/strong&gt; &lt;code&gt;VLLM_NVFP4_GEMM_BACKEND&lt;/code&gt; was deprecated in v0.23 (&lt;code&gt;deprecated_env(..., &quot;v0.23&quot;, &quot;Use --linear-backend.&quot;)&lt;/code&gt;) and &lt;strong&gt;removed in v0.25&lt;/strong&gt;. On any current image, &lt;code&gt;VLLM_NVFP4_GEMM_BACKEND=flashinfer-b12x&lt;/code&gt; is a &lt;strong&gt;silent no-op&lt;/strong&gt; — stale recipes still hand it out. The only surviving opt-in is the CLI value: &lt;code&gt;--linear-backend flashinfer_b12x&lt;/code&gt; / &lt;code&gt;--moe-backend flashinfer_b12x&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The exclusion is global, not &lt;code&gt;sm_121&lt;/code&gt;-scoped.&lt;/strong&gt; &lt;code&gt;FLASHINFER_B12X&lt;/code&gt; is absent from &lt;code&gt;AVAILABLE_BACKENDS&lt;/code&gt; on &lt;em&gt;every&lt;/em&gt; GPU — an &lt;code&gt;sm_120&lt;/code&gt; RTX PRO 6000 also falls to Marlin under &lt;code&gt;auto&lt;/code&gt;. &lt;code&gt;sm_121&lt;/code&gt; is the &lt;em&gt;reason&lt;/em&gt; cited in the comment, not the &lt;em&gt;scope&lt;/em&gt;. The still-open &lt;a href=&quot;https://github.com/vllm-project/vllm/pull/47577&quot;&gt;PR #47577&lt;/a&gt; would re-add auto-selection for &lt;code&gt;sm_120&lt;/code&gt; only and deliberately keep &lt;code&gt;sm_121&lt;/code&gt; on the explicit opt-in — &lt;code&gt;auto&lt;/code&gt; keeps skipping &lt;code&gt;b12x&lt;/code&gt; and falling through to whatever the image + checkpoint actually support (&lt;code&gt;FLASHINFER_CUTLASS&lt;/code&gt; on a genuine W4A4 checkpoint with &lt;code&gt;sm_121&lt;/code&gt; cubins, Marlin on a W4A16 checkpoint — see §4.4), &lt;em&gt;not&lt;/em&gt; an unconditional Marlin default. So b12x remains &lt;strong&gt;opt-in-only, never auto-selected on a Spark, in every released version&lt;/strong&gt; — and forcing it currently carries a cluster of open bugs (activation memory roughly doubling → ~55% less KV cache, #49079; an Xid 31 MMU fault under concurrent chunked prefill, #50189).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;45-flashinfer_cutlass_moe-vs-flashinfer_b12x_moe&quot;&gt;4.5 &lt;code&gt;flashinfer_cutlass_moe&lt;/code&gt; vs &lt;code&gt;flashinfer_b12x_moe&lt;/code&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;flashinfer_cutlass_moe&lt;/code&gt; (older)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;flashinfer_b12x_moe&lt;/code&gt; (newer)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dispatch&lt;/td&gt;
&lt;td&gt;per-block GEMM&lt;/td&gt;
&lt;td&gt;single fused kernel (dispatch + 2 GEMMs + SwiGLU + reduce)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quant&lt;/td&gt;
&lt;td&gt;FP8 block, MXFP4, static NVFP4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;NVFP4 only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;pre-quantized activations&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;BF16&lt;/strong&gt; (quantize inside kernel)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SM target&lt;/td&gt;
&lt;td&gt;SM90 / SM100(+)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SM120+&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-selected?&lt;/td&gt;
&lt;td&gt;yes (in the list)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no, opt-in&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;PR #39825 (“Disable FlashInfer CUTLASS MoE on SM121”) was the stopgap that kept the &lt;em&gt;older&lt;/em&gt; path from misbehaving on Spark before &lt;code&gt;b12x&lt;/code&gt; existed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-marlin-the-universal-fallback&quot;&gt;5. Marlin: the universal fallback&lt;/h2&gt;
&lt;h3 id=&quot;51-what-marlin-is&quot;&gt;5.1 What Marlin is&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Marlin&lt;/strong&gt; (originating from IST-DASLab, integrated via Neural Magic; &lt;code&gt;csrc/quantization/marlin/marlin.cu&lt;/code&gt;) is a &lt;strong&gt;mixed-precision, weight-only quantized GEMM&lt;/strong&gt;: 16-bit activations (FP16/BF16) multiplied by low-bit weights. The “&lt;strong&gt;wna16&lt;/strong&gt;” naming = &lt;em&gt;weight n-bit, activation 16-bit&lt;/em&gt;. It supports a broad matrix of weight formats (&lt;code&gt;csrc/quantization/marlin/generate_kernels.py:29-100&lt;/code&gt;): AWQ-INT4, GPTQ-INT4/INT8, FP8 (&lt;code&gt;kFE4M3fn&lt;/code&gt;), &lt;strong&gt;NVFP4&lt;/strong&gt; (&lt;code&gt;kFE2M1f&lt;/code&gt; weights, &lt;code&gt;kFE4M3fn&lt;/code&gt; scales), MXFP4, and MXFP8.&lt;/p&gt;
&lt;h3 id=&quot;52-why-marlin-is-the-robust-fallback-on-spark&quot;&gt;5.2 Why Marlin is the robust fallback on Spark&lt;/h3&gt;
&lt;p&gt;Two structural reasons:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. It is template-generated, not arch-pinned.&lt;/strong&gt; Marlin emits kernels from Jinja2 templates parameterized by thread/tile/group config, so the &lt;em&gt;same&lt;/em&gt; source covers &lt;code&gt;sm_80&lt;/code&gt;, &lt;code&gt;sm_89&lt;/code&gt;, &lt;code&gt;sm_120&lt;/code&gt;, &lt;code&gt;sm_121&lt;/code&gt; without bespoke per-arch kernel files the way CUTLASS does. Its CMake arch lists explicitly include the SM12x family:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;CMakeLists.txt&lt;/code&gt;: &lt;code&gt;MARLIN_FP8_ARCHS&lt;/code&gt; = &lt;code&gt;&quot;8.9;12.0a;12.1a&quot;&lt;/code&gt; (CUDA&amp;lt;13) / &lt;code&gt;&quot;8.9;12.0f&quot;&lt;/code&gt; (CUDA≥13); &lt;code&gt;MARLIN_MOE_FP8_ARCHS&lt;/code&gt; = &lt;code&gt;&quot;8.9;12.0;12.1&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;2. It covers &lt;em&gt;both&lt;/em&gt; dense linears and MoE.&lt;/strong&gt; Dense: &lt;code&gt;csrc/quantization/marlin/&lt;/code&gt;. MoE: &lt;code&gt;csrc/moe/marlin_moe_wna16/&lt;/code&gt;. So forcing Marlin fixes the &lt;em&gt;whole&lt;/em&gt; model in one move, which is exactly why it became the Spark workaround when CUTLASS/FlashInfer FP4 paths were broken for &lt;em&gt;both&lt;/em&gt; the dense and MoE halves of a model.&lt;/p&gt;
&lt;h3 id=&quot;53-marlin-for-nvfp4&quot;&gt;5.3 Marlin for NVFP4&lt;/h3&gt;
&lt;p&gt;Marlin has a real FP4 path. The NVFP4 linear kernel is &lt;code&gt;MarlinNvFp4LinearKernel&lt;/code&gt; (“NVFP4 weight-only GEMM via Marlin (W4A16)”, &lt;code&gt;vllm/model_executor/kernels/linear/nvfp4/marlin.py:18&lt;/code&gt;). It performs a non-trivial scale repacking, converting the FP8-E4M3 block scales into Marlin’s internal “S0E5M3” dequant layout (&lt;code&gt;vllm/.../utils/marlin_utils_fp4.py:53-114&lt;/code&gt;). You route to it explicitly with &lt;code&gt;VLLM_NVFP4_GEMM_BACKEND=marlin&lt;/code&gt; (env, &lt;strong&gt;deprecated&lt;/strong&gt; per the blog) or, preferably, the CLI &lt;code&gt;--linear-backend marlin&lt;/code&gt; / &lt;code&gt;--moe-backend marlin&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;54-where-marlin-sits-in-selection-and-40923&quot;&gt;5.4 Where Marlin sits in selection, and #40923&lt;/h3&gt;
&lt;p&gt;In the NVFP4 MoE oracle (§7) Marlin sits near the &lt;strong&gt;end&lt;/strong&gt; of the auto list, a deliberate “always works, decent speed” backstop after the CUTLASS/FlashInfer attempts. PR #40923 (“Marlin MoE: include SM 12.x in default arch list”) added the SM12x family to the &lt;em&gt;default&lt;/em&gt; Marlin-MoE build arch list so stock builds carry Spark Marlin-MoE kernels out of the box (currently only in &lt;code&gt;v0.22.1rc0&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost of Marlin:&lt;/strong&gt; as a weight-only W*A16 kernel it dequantizes weights to 16-bit and runs the matmul in 16-bit, so it does not exploit Spark’s native FP4/FP8 tensor cores as fully as a true block-scaled CUTLASS/&lt;code&gt;b12x&lt;/code&gt; kernel can. On a bandwidth-bound box like Spark this is often acceptable, but it’s the reason Marlin is the &lt;em&gt;fallback&lt;/em&gt;, not the &lt;em&gt;goal&lt;/em&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;6-nvfp4--mxfp4--fp8-quantization-on-spark&quot;&gt;6. NVFP4 / MXFP4 / FP8 quantization on Spark&lt;/h2&gt;
&lt;h3 id=&quot;61-nvfp4-format&quot;&gt;6.1 NVFP4 format&lt;/h3&gt;
&lt;p&gt;NVFP4 is a &lt;strong&gt;4-bit float with two-level block scaling&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Elements:&lt;/strong&gt; &lt;code&gt;e2m1&lt;/code&gt; (2-bit exponent, 1-bit mantissa; max 6.0), packed 2-per-byte.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-block scale:&lt;/strong&gt; &lt;code&gt;FP8 e4m3&lt;/code&gt;, one per &lt;strong&gt;16&lt;/strong&gt; contiguous elements along K.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global scale:&lt;/strong&gt; an &lt;code&gt;fp32&lt;/code&gt; scalar (one for weights, one for activations); the kernel combines them as &lt;code&gt;alpha = 1 / (input_global_scale * weight_global_scale)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the effective value is &lt;code&gt;element × fp8_block_scale × fp32_global_scale&lt;/code&gt;. (See &lt;code&gt;vllm/.../quantization/modelopt.py&lt;/code&gt; around the NVFP4 method, and &lt;code&gt;csrc/.../fp4/nvfp4_utils.cuh&lt;/code&gt;.) This block structure is why NVFP4 needs &lt;em&gt;block-scaled&lt;/em&gt; tensor-core MMA (&lt;code&gt;OpClassBlockScaledTensorOp&lt;/code&gt;) to run fast, which only &lt;code&gt;sm_100&lt;/code&gt;/&lt;code&gt;sm_120&lt;/code&gt;/&lt;code&gt;sm_121&lt;/code&gt; provide, in arch-specific form.&lt;/p&gt;
&lt;h3 id=&quot;62-the-dense-vs-moe-split-the-gotcha-that-wasted-everyones-time&quot;&gt;6.2 The dense-vs-MoE split: the gotcha that wasted everyone’s time&lt;/h3&gt;
&lt;p&gt;NVFP4 weights appear in &lt;strong&gt;two independent code paths&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dense linear layers&lt;/strong&gt; → &lt;code&gt;ModelOptNvFp4LinearMethod&lt;/code&gt;, served by the &lt;em&gt;linear&lt;/em&gt; kernel oracle (&lt;code&gt;cutlass_scaled_fp4_mm&lt;/code&gt;, FlashInfer, or Marlin).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MoE expert weights&lt;/strong&gt; → the &lt;em&gt;fused-MoE&lt;/em&gt; oracle (§7).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are selected &lt;strong&gt;independently&lt;/strong&gt;. The historical Spark trap: the model-card env vars &lt;code&gt;VLLM_USE_FLASHINFER_MOE_FP4=1&lt;/code&gt; / &lt;code&gt;VLLM_FLASHINFER_MOE_BACKEND=…&lt;/code&gt; only reroute the &lt;strong&gt;MoE&lt;/strong&gt; half. The &lt;strong&gt;dense&lt;/strong&gt; NVFP4 linears keep going through the broken CUTLASS FP4 path and crash or emit garbage. &lt;strong&gt;The only single-switch fix that covers both halves is Marlin&lt;/strong&gt; (&lt;code&gt;--linear-backend marlin --moe-backend marlin&lt;/code&gt;), or, on current images, explicitly opting &lt;em&gt;both&lt;/em&gt; into &lt;code&gt;b12x&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;63-the-kernels-that-consume-nvfp4-and-the-runtime-sm-dispatch&quot;&gt;6.3 The kernels that consume NVFP4, and the runtime SM dispatch&lt;/h3&gt;
&lt;p&gt;The CUTLASS FP4 entry point dispatches on the &lt;em&gt;runtime&lt;/em&gt; SM and fails loudly if the matching cubin wasn’t compiled in:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;csrc/.../fp4/nvfp4_scaled_mm_entry.cu&lt;/code&gt; (≈ 48-68):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;cpp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; int32_t&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sm &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; get_sm_version_num&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;#if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; defined&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ENABLE_NVFP4_SM100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (sm &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sm &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 120&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) { &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cutlass_scaled_fp4_mm_sm100a&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(...); &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;#endif&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;#if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; defined&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ENABLE_NVFP4_SM120&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (sm &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 120&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sm &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 130&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) { &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cutlass_scaled_fp4_mm_sm120a&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(...); &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; }&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   // SM120/121&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;#endif&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  STD_TORCH_CHECK_NOT_IMPLEMENTED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;No compiled nvfp4 mm kernel for SM &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, sm, ...);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;This device guard + runtime dispatch was added/hardened in PR #29711. The lesson: a wheel built without &lt;code&gt;ENABLE_NVFP4_SM120&lt;/code&gt; / the &lt;code&gt;12.1a&lt;/code&gt; arch simply has no Spark FP4 kernel, and you hit the &lt;code&gt;NOT_IMPLEMENTED&lt;/code&gt; at first FP4 matmul.&lt;/p&gt;
&lt;h3 id=&quot;64-nvfp4-vs-mxfp4-and-who-uses-which&quot;&gt;6.4 NVFP4 vs MXFP4 (and who uses which)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;NVFP4&lt;/th&gt;
&lt;th&gt;MXFP4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Element&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e2m1&lt;/code&gt; (4-bit)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e2m1&lt;/code&gt; (4-bit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block size (K)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block scale&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FP8 e4m3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;e8m0&lt;/code&gt;&lt;/strong&gt; (exponent-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical models&lt;/td&gt;
&lt;td&gt;Qwen, &lt;strong&gt;Nemotron-3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GPT-OSS&lt;/strong&gt; (MoE)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MXFP4 (&lt;code&gt;vllm/.../quantization/mxfp4.py&lt;/code&gt;) is what the GPT-OSS family ships in. The block-scaled FP4/MXFP4 CUTLASS kernels for &lt;code&gt;sm_120&lt;/code&gt;/&lt;code&gt;sm_121&lt;/code&gt; are partly provided through &lt;strong&gt;QuTLASS&lt;/strong&gt; (&lt;code&gt;cmake/external_projects/qutlass.cmake&lt;/code&gt;, fetched from IST-DASLab; requires CUDA 12.8+ and CC &lt;code&gt;10.0f&lt;/code&gt;/&lt;code&gt;12.0f&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id=&quot;65-fp8-the-path-of-least-resistance-on-spark&quot;&gt;6.5 FP8: the path of least resistance on Spark&lt;/h3&gt;
&lt;p&gt;Because &lt;code&gt;sm_121&lt;/code&gt; has &lt;strong&gt;native &lt;code&gt;e4m3&lt;/code&gt; FP8 MMA&lt;/strong&gt; (§1.4) and FP8 is covered by both CUTLASS (&lt;code&gt;scaled_mm_sm120_fp8_dispatch&lt;/code&gt;) and Marlin, &lt;strong&gt;FP8 is usually far less fragile than NVFP4 on Spark&lt;/strong&gt;; on the audited paths it needed no Marlin dance. Treat that as image- and model-dependent rather than a law (Spark FP8 failures have surfaced in specific nightlies and models); smoke-test the exact model and container before relying on &lt;code&gt;auto&lt;/code&gt;. On a 128 GB unified box the memory saving of FP4 over FP8 is often irrelevant for the models people actually run, so a practical rule is: &lt;strong&gt;prefer FP8 (or BF16) unless a model is only published in NVFP4/MXFP4.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;7-how-vllm-does-moe-and-how-it-lands-on-spark&quot;&gt;7. How vLLM does MoE, and how it lands on Spark&lt;/h2&gt;
&lt;h3 id=&quot;71-the-modular-fused-moe-architecture&quot;&gt;7.1 The modular fused-MoE architecture&lt;/h3&gt;
&lt;p&gt;vLLM decomposes a fused-MoE layer into composable stages (&lt;code&gt;vllm/model_executor/layers/fused_moe/modular_kernel.py:45-79&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[Router] → [Quantize-Dispatch] → [Permute-Experts-Unpermute] → [Combine]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;implemented via three classes: &lt;code&gt;FusedMoEPrepareAndFinalizeModular&lt;/code&gt; (prep + finalize), &lt;code&gt;FusedMoEExpertsModular&lt;/code&gt; (the expert GEMMs + activation + optional quant), and &lt;code&gt;FusedMoEModularKernel&lt;/code&gt; (the glue). Each concrete backend is an “experts” implementation under &lt;code&gt;vllm/model_executor/layers/fused_moe/experts/&lt;/code&gt; (e.g. &lt;code&gt;flashinfer_b12x_moe.py&lt;/code&gt;, &lt;code&gt;marlin_moe.py&lt;/code&gt;, &lt;code&gt;triton_moe.py&lt;/code&gt;, &lt;code&gt;flashinfer_cutlass_moe.py&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id=&quot;72-the-oracle-how-a-backend-is-chosen-verified-2026-06-02&quot;&gt;7.2 The oracle: how a backend is chosen &lt;code&gt;[verified 2026-06-02]&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;For NVFP4 MoE, &lt;code&gt;select_nvfp4_moe_backend()&lt;/code&gt; (&lt;code&gt;oracle/nvfp4.py:160-333&lt;/code&gt;) walks a priority list and asks each backend whether it supports the current config/device:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;AVAILABLE_BACKENDS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NvFp4MoeBackend.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;FLASHINFER_TRTLLM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NvFp4MoeBackend.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;FLASHINFER_CUTEDSL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NvFp4MoeBackend.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;FLASHINFER_CUTEDSL_BATCHED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NvFp4MoeBackend.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;FLASHINFER_CUTLASS&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NvFp4MoeBackend.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;VLLM_CUTLASS&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NvFp4MoeBackend.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;MARLIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NvFp4MoeBackend.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;EMULATION&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# FLASHINFER_B12X intentionally excluded from auto-selection (opt in explicitly)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--moe-backend&lt;/code&gt; CLI value maps onto this enum in &lt;code&gt;map_nvfp4_backend()&lt;/code&gt; (&lt;code&gt;oracle/nvfp4.py:141-157&lt;/code&gt;), and the available options + their docstrings (including &lt;em&gt;“flashinfer_b12x: … for SM12x (RTX Pro 6000 / DGX Spark)”&lt;/em&gt;) live in &lt;code&gt;vllm/config/kernel.py:171-190&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;73-what-this-means-on-spark&quot;&gt;7.3 What this means on Spark&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--moe-backend auto&lt;/code&gt; on &lt;code&gt;sm_121&lt;/code&gt; will &lt;strong&gt;skip &lt;code&gt;b12x&lt;/code&gt;&lt;/strong&gt; (opt-in only, at the audited commit, §4.4) and try TRT-LLM / CuteDSL / CUTLASS paths, then &lt;code&gt;VLLM_CUTLASS&lt;/code&gt;, then &lt;strong&gt;Marlin&lt;/strong&gt;, then emulation. Whether the earlier entries actually run on &lt;code&gt;sm_121&lt;/code&gt; depends on the image’s cubins; this is why &lt;code&gt;auto&lt;/code&gt; behavior on Spark has historically been a coin-flip and why people pinned Marlin.&lt;/li&gt;
&lt;li&gt;For a &lt;strong&gt;predictable&lt;/strong&gt; Spark deployment you have two solid choices:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Force Marlin&lt;/strong&gt; for both linear and MoE: guaranteed-correct, slightly slower.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicitly opt into &lt;code&gt;b12x&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;--moe-backend flashinfer_b12x&lt;/code&gt;, &lt;code&gt;--linear-backend flashinfer-b12x&lt;/code&gt;) on a &lt;code&gt;v0.22.0+&lt;/code&gt; image (the fast native path) &lt;em&gt;and validate output coherence&lt;/em&gt; before trusting it (the &lt;code&gt;b12x&lt;/code&gt; exclusion-from-auto exists precisely because the upstream guard isn’t fully settled).&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;8-cuda-graphs-and---enforce-eager&quot;&gt;8. CUDA graphs and &lt;code&gt;--enforce-eager&lt;/code&gt;&lt;/h2&gt;
&lt;h3 id=&quot;81-what-cuda-graphs-do-and-what-enforce_eager-turns-off&quot;&gt;8.1 What CUDA graphs do, and what &lt;code&gt;enforce_eager&lt;/code&gt; turns off&lt;/h3&gt;
&lt;p&gt;vLLM captures the decode forward pass into a &lt;strong&gt;CUDA graph&lt;/strong&gt; so the GPU replays a recorded sequence of kernel launches with near-zero per-launch CPU overhead. &lt;code&gt;enforce_eager&lt;/code&gt; disables this entirely:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;vllm/config/model.py&lt;/code&gt;: &lt;code&gt;enforce_eager: bool = False&lt;/code&gt;: &lt;em&gt;“If True, we will disable CUDA graph and always execute the model in eager mode. If False, we will use CUDA graph and eager execution in hybrid for maximal performance and flexibility.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When set, vLLM forces &lt;code&gt;cudagraph_mode = NONE&lt;/code&gt; and clears the capture-size list.&lt;/p&gt;
&lt;h3 id=&quot;82-piecewise-vs-full-capture-and-the-v1-default&quot;&gt;8.2 Piecewise vs. full capture (and the V1 default)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;vllm/config/compilation.py:599-634&lt;/code&gt; defines the modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NONE&lt;/code&gt;: no capture.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PIECEWISE&lt;/code&gt;: capture everything &lt;em&gt;except&lt;/em&gt; graph-incompatible ops (e.g. some attention), which run outside the graph for flexibility.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FULL&lt;/code&gt; / &lt;code&gt;FULL_DECODE_ONLY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;FULL_AND_PIECEWISE&lt;/code&gt;&lt;/strong&gt;: full graphs for decode batches, piecewise for prefill/mixed. &lt;strong&gt;This is the V1 default&lt;/strong&gt; and the most performant for most models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Graphs are captured at a discrete set of batch sizes (&lt;code&gt;cudagraph_capture_sizes&lt;/code&gt;), auto-generated roughly as &lt;code&gt;[1,2,4] + range(8,256,8) + range(256, max, 16)&lt;/code&gt;, with &lt;code&gt;max_cudagraph_capture_size&lt;/code&gt; defaulting to &lt;code&gt;min(max_num_seqs*2, 512)&lt;/code&gt;. This is the mechanism behind the field trick of capping &lt;code&gt;--max-num-seqs&lt;/code&gt; to bound the capture set.&lt;/p&gt;
&lt;h3 id=&quot;83-why-mamba--hybrid-nemotron_h-models-need-eager-on-spark&quot;&gt;8.3 Why Mamba / hybrid (&lt;code&gt;nemotron_h&lt;/code&gt;) models need eager on Spark&lt;/h3&gt;
&lt;p&gt;First, the scope: the official blog’s benchmark path kept CUDA graphs &lt;strong&gt;enabled&lt;/strong&gt;; this section covers a separate field failure mode, not a blanket Spark rule. Hybrid SSM/Mamba models maintain &lt;strong&gt;recurrent state&lt;/strong&gt; and use &lt;strong&gt;Triton&lt;/strong&gt; kernels with persistent in-place state updates. Graph capture freezes kernel-launch semantics and pointer bindings, which collides with that dynamic state; hence Mamba models implement special &lt;code&gt;copy_inputs_before_cuda_graphs&lt;/code&gt; / &lt;code&gt;get_seqlen_agnostic_capture_inputs&lt;/code&gt; hooks (&lt;code&gt;vllm/model_executor/models/mamba.py:270-274&lt;/code&gt;, &lt;code&gt;mamba2.py&lt;/code&gt;, &lt;code&gt;jamba.py&lt;/code&gt;, &lt;code&gt;nemotron_h.py&lt;/code&gt;). On &lt;code&gt;sm_121&lt;/code&gt; specifically, graph capture of the Mamba-2 Triton path has surfaced an &lt;em&gt;“illegal instruction”&lt;/em&gt; during capture in our deployments, so the operational fix has been &lt;code&gt;--enforce-eager&lt;/code&gt;, applied only when a specific model/image combination shows graph-capture instability, and documented with the tested model, image, CUDA version, and error.&lt;/p&gt;
&lt;h3 id=&quot;84-why-eager-is-a-crutch-not-a-destination&quot;&gt;8.4 Why eager is a crutch, not a destination&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;enforce_eager&lt;/code&gt; pays for itself in launch overhead on every decode step; field measurements on the Nemotron hybrid models put the cost around &lt;strong&gt;~37%&lt;/strong&gt; throughput on Spark, and the broader range is roughly 5–30% depending on batch size and model depth. It’s an &lt;strong&gt;escape hatch&lt;/strong&gt; for a kernel/capture incompatibility, not a tuning choice. The right long-term fix is upstream: make the Mamba-2 Triton kernels capture-safe on &lt;code&gt;sm_121&lt;/code&gt; (or provide a CUDA-graph-compatible SSM path), at which point Spark hybrid models can drop &lt;code&gt;--enforce-eager&lt;/code&gt; and recover that throughput. Until then it remains model-specific: apply it to &lt;code&gt;nemotron_h&lt;/code&gt;/Mamba hybrids, &lt;strong&gt;not&lt;/strong&gt; to dense or standard-MoE models that capture fine.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;9-the-history-how-spark-support-was-built&quot;&gt;9. The history: how Spark support was built&lt;/h2&gt;
&lt;p&gt;Support arrived in two waves: RTX-class &lt;code&gt;sm_120&lt;/code&gt; first, then Spark &lt;code&gt;sm_121&lt;/code&gt; threaded in behind it. Release attributions below are verified with &lt;code&gt;git tag --contains&lt;/code&gt; &lt;code&gt;[verified 2026-06-02]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wave 1, &lt;code&gt;sm_120&lt;/code&gt; (RTX Blackwell) groundwork, mid-2025:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;#17280 CUTLASS W8A8 FP8 for &lt;code&gt;sm_120&lt;/code&gt;; #21309 NVFP4 W4A4 for &lt;code&gt;sm_120&lt;/code&gt;; #22131 block FP8; #29242 NVFP4 MoE CUTLASS; #29711 device guard + runtime SM dispatch for &lt;code&gt;cutlass_scaled_fp4_mm&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Wave 2, &lt;code&gt;sm_121&lt;/code&gt; (DGX Spark) era, 2026:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;First release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;#33517 (2026-02-06)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;enable_sm120_family&lt;/code&gt; template: first explicit DGX Spark commit; one binary for SM120+SM121&lt;/td&gt;
&lt;td&gt;early 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#38126 (2026-03-27)&lt;/td&gt;
&lt;td&gt;“Fix DGX Spark logic”: CMake family matching (&lt;code&gt;12.0f&lt;/code&gt;↔&lt;code&gt;12.1a&lt;/code&gt;), arch lists gain &lt;code&gt;12.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v0.19.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#38423 (2026-03-30)&lt;/td&gt;
&lt;td&gt;NVFP4 bugfixes for DGX Spark + RTX50&lt;/td&gt;
&lt;td&gt;(post-v0.19)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#39825 (2026-04-14)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Disable&lt;/strong&gt; FlashInfer CUTLASS MoE on SM121 (stopgap)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v0.20.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#35568 (2026-05-15)&lt;/td&gt;
&lt;td&gt;Fix incorrect SM121 exclusion from Marlin/CUTLASS FP8 paths&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v0.22.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#40082 (2026-05-20)&lt;/td&gt;
&lt;td&gt;Integrate FlashInfer &lt;strong&gt;&lt;code&gt;b12x&lt;/code&gt;&lt;/strong&gt; MoE + FP4 GEMM for SM120/121&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v0.22.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#41215 (2026-05-20)&lt;/td&gt;
&lt;td&gt;Route per-tensor FP8 through &lt;code&gt;enable_sm120_family&lt;/code&gt; on SM12.1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v0.22.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#40923 (2026-05-28)&lt;/td&gt;
&lt;td&gt;Marlin MoE: include SM12.x in default arch list&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v0.22.1rc0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The key takeaway:&lt;/strong&gt; the cluster of fixes that make Spark coherent end-to-end, the SM121 FP8 exclusion fix (#35568) and the &lt;code&gt;b12x&lt;/code&gt; integration (#40082), &lt;strong&gt;first ship together in &lt;code&gt;v0.22.0&lt;/code&gt; (2026-05-29).&lt;/strong&gt; Anything older (including the popular &lt;code&gt;v0.20.0&lt;/code&gt;) lacks &lt;code&gt;b12x&lt;/code&gt; and the exclusion fix; it can be made to work only by &lt;strong&gt;forcing Marlin&lt;/strong&gt;. Release cadence is ~2–3 weeks.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;10-prebuilt-containers-vs-rolling-your-own&quot;&gt;10. Prebuilt containers vs. rolling your own&lt;/h2&gt;
&lt;h3 id=&quot;101-decision-summary-verified-2026-06-02-image-tags--version-floors-drift-re-check-each-release&quot;&gt;10.1 Decision summary &lt;code&gt;[verified 2026-06-02: image tags &amp;amp; version floors drift; re-check each release]&lt;/code&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You are…&lt;/th&gt;
&lt;th&gt;Do this&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Running stock models via container (most people)&lt;/td&gt;
&lt;td&gt;Use a &lt;strong&gt;pinned digest of an image you have smoke-tested on Spark&lt;/strong&gt;. &lt;code&gt;v0.22.0&lt;/code&gt; is the &lt;em&gt;first stable baseline in this audit&lt;/em&gt; containing &lt;code&gt;b12x&lt;/code&gt; (#40082) + the SM121 FP8 exclusion fix (#35568), a historical minimum, not the July 2026 recommendation; newer stable or nightly images may be preferable but must be re-audited for backend selection and cubin coverage (see the update timeline: the cu129 v0.25.1 stable still shipped no sm_121 cubins on 2026-07-19).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tracking the newest kernels / want #40923 Marlin-MoE arch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/vllm-openai:v0.22.1rc0&lt;/code&gt; or the blog’s compatibility track &lt;code&gt;cu130-nightly&lt;/code&gt; (pin a digest; it’s CUDA-13).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modifying C++/CUDA, or need a kernel no image ships&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Build from source&lt;/strong&gt; with &lt;code&gt;TORCH_CUDA_ARCH_LIST=12.1a&lt;/code&gt;, CUDA ≥ 12.8 (13.0 preferred).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On an old image (≤ v0.21) you can’t change&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Force Marlin&lt;/strong&gt; (&lt;code&gt;--moe-backend marlin --linear-backend marlin&lt;/code&gt;); expect dense+MoE to need it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;You almost never need a from-scratch native compile on the Spark itself&lt;/strong&gt; unless you’re editing kernels; the published aarch64 images/wheels exist precisely so you don’t. From-source is the path for kernel work or when a needed fix is on &lt;code&gt;main&lt;/code&gt; but not yet released.&lt;/p&gt;
&lt;h3 id=&quot;102-if-you-build-your-own-the-arch-traps&quot;&gt;10.2 If you build your own: the arch traps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;List the right arch.&lt;/strong&gt; CUDA 12.8/12.9 → &lt;code&gt;12.1a&lt;/code&gt; (or &lt;code&gt;12.0a;12.1a&lt;/code&gt;). CUDA 13.0+ → &lt;code&gt;12.0f&lt;/code&gt; (family target covers Spark). Below 12.8, &lt;code&gt;sm_121&lt;/code&gt; cannot be built at all (&lt;code&gt;CMakeLists.txt:99-110&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not copy the in-repo Grace-Blackwell Docker example’s&lt;/strong&gt; &lt;code&gt;torch_cuda_arch_list=&apos;9.0 10.0+PTX&apos;&lt;/code&gt;. That targets GH200/GB200, &lt;strong&gt;not&lt;/strong&gt; GB10. Override it with &lt;code&gt;12.1a&lt;/code&gt;/&lt;code&gt;12.0f&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Match the dependency pins to the revision you build.&lt;/strong&gt; Current &lt;code&gt;main&lt;/code&gt; (&lt;code&gt;requirements/cuda.txt&lt;/code&gt;) pins &lt;code&gt;flashinfer-python==0.6.11.post2&lt;/code&gt;, &lt;code&gt;flashinfer-cubin==0.6.11.post2&lt;/code&gt;, and &lt;code&gt;nvidia-cutlass-dsl[cu13]==4.5.2&lt;/code&gt; &lt;code&gt;[verified 2026-06-02: these pins move; read the pin in the exact tag you build]&lt;/code&gt;. (Older field notes citing cutlass-dsl 4.4.2 are stale.) &lt;strong&gt;Update &lt;code&gt;[verified 2026-08-10]&lt;/code&gt;:&lt;/strong&gt; this pin has since diverged &lt;em&gt;by build path&lt;/em&gt;. The upstream &lt;code&gt;main&lt;/code&gt; pin is not the whole story — Luke Alonso’s &lt;code&gt;b12x&lt;/code&gt; kernel library (the one eugr’s experimental fork image builds against) now requires &lt;code&gt;nvidia-cutlass-dsl==4.6.0&lt;/code&gt;, while &lt;code&gt;4.5.0&lt;/code&gt; is known to emit PTX that &lt;code&gt;ptxas&lt;/code&gt; rejects on &lt;code&gt;sm_121&lt;/code&gt;/&lt;code&gt;sm_120&lt;/code&gt; (&lt;a href=&quot;https://github.com/NVIDIA/cutlass/issues/3227&quot;&gt;NVIDIA/cutlass#3227&lt;/a&gt;, fixed in 4.5.1/4.5.2). So “4.4.2 is stale” is now itself stale: read the cutlass-dsl pin in the exact tag &lt;strong&gt;and&lt;/strong&gt; build path you deploy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Include &lt;code&gt;nvcc&lt;/code&gt; if you rely on FlashInfer JIT&lt;/strong&gt;; otherwise FlashInfer disables itself when prebuilt cubins are missing for your arch (§4.2).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;103-the-non-negotiable-validate-on-the-image-you-deploy&quot;&gt;10.3 The non-negotiable: validate on the image you deploy&lt;/h3&gt;
&lt;p&gt;Because Spark correctness hinges on whether the &lt;em&gt;specific image&lt;/em&gt; carries &lt;code&gt;sm_121&lt;/code&gt; cubins and which backend &lt;code&gt;auto&lt;/code&gt; lands on, &lt;strong&gt;always smoke-test before trusting a config&lt;/strong&gt;, watching for &lt;em&gt;both&lt;/em&gt; the hard error and silent garbage:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Serve one NVFP4 &lt;strong&gt;MoE&lt;/strong&gt; model and one NVFP4-heavy &lt;strong&gt;dense&lt;/strong&gt; model on the candidate image.&lt;/li&gt;
&lt;li&gt;Watch logs for &lt;code&gt;Failed to run cutlass FP4 gemm on sm120/sm121 … Error Internal&lt;/code&gt; or &lt;code&gt;No compiled nvfp4 mm kernel for SM 121&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inspect actual generated text for coherence (the silent-garbage failure mode produces no error).&lt;/li&gt;
&lt;li&gt;Try, in order: (a) &lt;code&gt;auto&lt;/code&gt;, (b) explicit &lt;code&gt;--moe-backend flashinfer_b12x --linear-backend flashinfer-b12x&lt;/code&gt;, (c) forced Marlin. Adopt the fastest that is &lt;em&gt;coherent and stable&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Fire a warm-up request (&lt;code&gt;max_tokens=3&lt;/code&gt;, same model + chat template) at startup: the first request JIT-compiles Inductor/FlashInfer kernels (~25 s) and you don’t want a user to eat it.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;104-backend-selection-cheat-sheet-for-spark&quot;&gt;10.4 Backend selection cheat-sheet for Spark&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FP8 / BF16 model:&lt;/strong&gt; &lt;code&gt;auto&lt;/code&gt; is usually fine; FP8 uses native &lt;code&gt;sm_121&lt;/code&gt; tensor cores, so no Marlin dance in the common case. Treat it as image- and model-dependent, though: the v0.26.0 cu129 stable actually died on a CUTLASS FP8 W8A8 GEMM for our checkpoint (see the update timeline), so smoke-test the exact model and container before trusting &lt;code&gt;auto&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NVFP4 model, want guaranteed-correct:&lt;/strong&gt; &lt;code&gt;--moe-backend marlin --linear-backend marlin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NVFP4 model, want fastest native path:&lt;/strong&gt; &lt;code&gt;--moe-backend flashinfer_b12x --linear-backend flashinfer-b12x&lt;/code&gt; on &lt;code&gt;v0.22.0+&lt;/code&gt;, then validate coherence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hybrid Mamba (&lt;code&gt;nemotron_h&lt;/code&gt;):&lt;/strong&gt; add &lt;code&gt;--enforce-eager&lt;/code&gt; &lt;strong&gt;if&lt;/strong&gt; your model/image combination shows the §8.3 graph-capture failure; it is a documented per-model workaround, not a Spark default.&lt;/li&gt;
&lt;li&gt;Prefer &lt;strong&gt;CLI flags&lt;/strong&gt; over the &lt;code&gt;VLLM_*&lt;/code&gt; env vars: &lt;code&gt;VLLM_NVFP4_GEMM_BACKEND&lt;/code&gt; was deprecated in v0.23 and &lt;strong&gt;removed in v0.25&lt;/strong&gt; (a no-op on current images), and &lt;code&gt;VLLM_USE_FLASHINFER_MOE_FP4&lt;/code&gt; is deprecated. Use &lt;code&gt;--moe-backend&lt;/code&gt; / &lt;code&gt;--linear-backend&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;11-appendix-glossary-and-file-index&quot;&gt;11. Appendix: glossary and file index&lt;/h2&gt;
&lt;h3 id=&quot;glossary&quot;&gt;Glossary&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GB10 / Grace Blackwell&lt;/strong&gt;: the DGX Spark superchip (Grace ARM CPU + Blackwell GPU, unified memory).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sm_121&lt;/code&gt; / CC 12.1&lt;/strong&gt;: DGX Spark’s compute capability; workstation-Blackwell SM12x family, distinct from datacenter &lt;code&gt;sm_100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;a&lt;/code&gt; vs &lt;code&gt;f&lt;/code&gt; arch suffix&lt;/strong&gt;: &lt;code&gt;12.1a&lt;/code&gt; = arch-specific cubin (one SM); &lt;code&gt;12.0f&lt;/code&gt; = family cubin (whole SM12x, needs CUDA 13).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CUTLASS&lt;/strong&gt;: NVIDIA C++ template GEMM library; vLLM’s first-choice quantized matmul; needs arch-specific kernels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FlashInfer&lt;/strong&gt;: inference kernel library (attention, MoE, FP4/FP8 GEMM); ships prebuilt cubins or JITs at runtime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CuteDSL&lt;/strong&gt;: CUTLASS’s tensor-layout DSL; basis of the &lt;code&gt;b12x&lt;/code&gt; SM12x kernels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;b12x&lt;/code&gt;&lt;/strong&gt;: FlashInfer’s SM12x block-scaled fused-MoE / FP4-GEMM path; fast on Spark but &lt;strong&gt;opt-in&lt;/strong&gt; (not auto, at the audited commit, §4.4).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Marlin / wna16&lt;/strong&gt;: template-generated weight-only (W4/W8 ·A16) GEMM; broad arch coverage; Spark’s robust fallback for both dense and MoE.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;QuTLASS&lt;/strong&gt;: IST-DASLab CUTLASS extension providing FP4/MXFP4 kernels for &lt;code&gt;sm_100&lt;/code&gt;/&lt;code&gt;sm_120&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NVFP4&lt;/strong&gt;: 4-bit float (&lt;code&gt;e2m1&lt;/code&gt;), 16-element blocks, FP8-&lt;code&gt;e4m3&lt;/code&gt; block scales + FP32 global scale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MXFP4&lt;/strong&gt;: 4-bit float, 32-element blocks, &lt;code&gt;e8m0&lt;/code&gt; block scales; GPT-OSS format.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;enforce-eager&lt;/strong&gt;: disables CUDA-graph capture; needed for Mamba/hybrid on Spark; ~5–37% throughput cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;file-index-paths-under-the-vllm-repo-root&quot;&gt;File index (paths under the vLLM repo root)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SM12x family guard&lt;/td&gt;
&lt;td&gt;&lt;code&gt;csrc/cutlass_extensions/common.hpp&lt;/code&gt; (&lt;code&gt;enable_sm120_family&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUTLASS FP8 dispatch (SM120)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;csrc/quantization/w8a8/cutlass/c3x/scaled_mm_sm120_fp8_dispatch.cuh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUTLASS FP4 entry + SM dispatch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;csrc/quantization/fp4/nvfp4_scaled_mm_entry.cu&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUTLASS FP4 SM120 kernel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;csrc/quantization/fp4/nvfp4_scaled_mm_sm120_kernels.cu&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arch lists / family logic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CMakeLists.txt&lt;/code&gt; (&lt;code&gt;SCALED_MM_ARCHS&lt;/code&gt;, &lt;code&gt;FP4_ARCHS&lt;/code&gt;, &lt;code&gt;MARLIN_*_ARCHS&lt;/code&gt;), &lt;code&gt;cmake/utils.cmake&lt;/code&gt; (&lt;code&gt;cuda_archs_loose_intersection&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlashInfer wrapper / JIT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/utils/flashinfer.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b12x&lt;/code&gt; MoE expert&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/layers/fused_moe/experts/flashinfer_b12x_moe.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b12x&lt;/code&gt; linear kernel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/kernels/linear/nvfp4/flashinfer.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Older FlashInfer CUTLASS MoE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/layers/fused_moe/experts/flashinfer_cutlass_moe.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 MoE oracle (auto order, b12x exclusion)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/layers/fused_moe/oracle/nvfp4.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 linear oracle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/kernels/linear/__init__.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoE/linear backend options&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/config/kernel.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modular fused-MoE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/layers/fused_moe/modular_kernel.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marlin (dense)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;csrc/quantization/marlin/{marlin.cu,generate_kernels.py}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marlin MoE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;csrc/moe/marlin_moe_wna16/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marlin NVFP4 linear + scales&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/kernels/linear/nvfp4/marlin.py&lt;/code&gt;, &lt;code&gt;vllm/.../quantization/utils/marlin_utils_fp4.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVFP4 / ModelOpt quant method&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/layers/quantization/modelopt.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MXFP4 quant method&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/layers/quantization/mxfp4.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QuTLASS build&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cmake/external_projects/qutlass.cmake&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUDA graph / eager config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/config/compilation.py&lt;/code&gt;, &lt;code&gt;vllm/config/model.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mamba graph hooks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/model_executor/models/{mamba,mamba2,jamba,nemotron_h}.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UMA / integrated-GPU detection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vllm/utils/mem_utils.py&lt;/code&gt;, &lt;code&gt;vllm/platforms/interface.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install / aarch64 / Docker&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docs/getting_started/installation/gpu.cuda.inc.md&lt;/code&gt;, &lt;code&gt;docker/Dockerfile&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Compiled from the vLLM &lt;code&gt;main&lt;/code&gt; source tree (2026-06-02) and the official vLLM DGX Spark blog (2026-06-01). Backend selection details, especially the &lt;code&gt;b12x&lt;/code&gt; opt-in, reflect current &lt;code&gt;main&lt;/code&gt;; re-check the oracle comments in &lt;code&gt;oracle/nvfp4.py&lt;/code&gt; against the exact image you deploy, since the upstream CUTLASS sm_121 MMA-guard fix may flip &lt;code&gt;b12x&lt;/code&gt; into auto-selection in a future release.&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>vLLM on the DGX Spark, the Hard Way: Seven Months of GB10 Field Notes</title><link>https://inference.swesty.net/blog/vllm-on-dgx-spark-gb10-field-notes/</link><guid isPermaLink="true">https://inference.swesty.net/blog/vllm-on-dgx-spark-gb10-field-notes/</guid><description>Seven months of empirical GB10 (sm_121) vLLM findings: why &apos;Marlin is mandatory&apos; has a load-bearing exception (it&apos;s your checkpoint, not your GPU), the two-condition rule for native FP4, the MTP illegal-memory-access saga and how the W4A4/CUTLASS configuration avoided it, unified-memory OOM math, and measured numbers for every claim.</description><pubDate>Sun, 19 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Every performance number in this post was measured on our own two DGX Sparks and is labeled with the most precise identifiers we have for that run: image digest or pinned gitsha, checkpoint, and date. Where we cite someone else’s number, we say so. Where we were wrong earlier, the timeline says that too.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;why-this-post-exists&quot;&gt;Why this post exists&lt;/h2&gt;
&lt;p&gt;The DGX Spark’s serving ecosystem moved faster than anyone’s documentation could; NVIDIA’s launch guidance and the vLLM team’s Spark deep-dive were each accurate for the moment they were written, and the platform kept moving underneath them. What that left for operators to discover in production: which image builds carry &lt;code&gt;sm_121&lt;/code&gt; kernels and which don’t, why some NVFP4 checkpoints crash or emit garbage on paths that work fine on datacenter Blackwell, what &lt;code&gt;--gpu-memory-utilization&lt;/code&gt; really means on a unified 128 GB pool, and what happens to speculative decoding under &lt;em&gt;concurrent&lt;/em&gt; load rather than in a batch-1 demo. We wrote our findings down after each incident. This post is that document, cleaned up, with the receipts, offered as a snapshot with dates attached, since it will age the same way.&lt;/p&gt;
&lt;p&gt;Two boxes inform everything here: &lt;strong&gt;Spark-1&lt;/strong&gt; and &lt;strong&gt;Spark-2&lt;/strong&gt;, both GB10 (aarch64, &lt;code&gt;sm_121a&lt;/code&gt;, 128 GB unified, ~273 GB/s), serving production traffic for a homelab agent fleet (reasoning, tool-calling, and structured extraction), 24/7, behind a model gateway. This is not a benchmark rig; every lesson below came out of something breaking. For the kernel-level &lt;em&gt;why&lt;/em&gt; behind these notes (the CUTLASS/FlashInfer/Marlin mechanics, the backend oracle, and the arch details), see the companion &lt;a href=&quot;/blog/vllm-dgx-spark-technical-report&quot;&gt;technical report&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-five-minute-model-of-what-vllm-is-doing-on-this-box&quot;&gt;The five-minute model of what vLLM is doing on this box&lt;/h2&gt;
&lt;p&gt;Everything in this post follows from four pieces of vLLM machinery. If you already operate vLLM, skip ahead; if not, these four paragraphs are the vocabulary the rest of the post uses.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Serving has two phases with opposite bottlenecks.&lt;/strong&gt; &lt;em&gt;Prefill&lt;/em&gt; processes your whole prompt in parallel; it’s compute-bound, measured in thousands of tokens/second, and determines time-to-first-token. &lt;em&gt;Decode&lt;/em&gt; generates one token at a time, and each token requires re-reading the model’s active weights from memory; it’s memory-bandwidth-bound, which on a 273 GB/s unified-memory box is the budget everything competes for. This split explains most of the numbers here: why a 35B MoE with only 3B &lt;em&gt;active&lt;/em&gt; parameters decodes fast on a Spark (decode reads the active set, not the total), why a weight-only quantization like Marlin costs little at single-user decode but shows up at prefill and concurrency (those are compute-heavy), and why our tables always report the phases separately. Two caveats the rest of the post leans on. First, the &lt;em&gt;short-context limit&lt;/em&gt;: decode actually re-reads the active weights &lt;em&gt;and&lt;/em&gt; the KV cache of every token attended so far, so at long context the KV term grows and decode throughput falls with sequence length, which is why KV-pool sizing (§9) is a performance concern, not just a capacity one. Second, what &lt;em&gt;Marlin&lt;/em&gt; means throughout: it’s vLLM’s weight-only 4-bit kernel that keeps weights 4-bit in memory and dequantizes them to BF16/FP16 &lt;em&gt;inside&lt;/em&gt; the GEMM, so it wins on bandwidth-bound decode but runs the matmul in high precision, never touching the native FP4 tensor cores. That’s why its cost surfaces in compute-bound prefill and under concurrency, not at batch-1 decode.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Kernels are chosen by a chain with three links, and any link can break.&lt;/strong&gt; When vLLM loads a model it reads the checkpoint’s quantization config (&lt;code&gt;config.json&lt;/code&gt;), runs it through a backend “oracle” (&lt;code&gt;select_nvfp4_moe_backend()&lt;/code&gt; in &lt;code&gt;vllm/model_executor/layers/fused_moe/oracle/nvfp4.py&lt;/code&gt;) that maps &lt;em&gt;quant format + GPU architecture&lt;/em&gt; to a ranked list of candidate kernel backends. That ranked list is the oracle’s &lt;code&gt;AVAILABLE_BACKENDS&lt;/code&gt; constant: the &lt;code&gt;[&apos;FLASHINFER_TRTLLM&apos;, … &apos;FLASHINFER_CUTLASS&apos;, … &apos;MARLIN&apos;, &apos;EMULATION&apos;]&lt;/code&gt; line you’ll see in our boot logs. The winning backend then still has to find a &lt;em&gt;compiled kernel binary (cubin) for your GPU inside the wheel you actually installed&lt;/em&gt;. Three links: checkpoint declares, oracle selects, wheel delivers. Each fails differently: a W4A16 checkpoint means the oracle &lt;em&gt;correctly&lt;/em&gt; selects Marlin (nothing to fix); a wheel without &lt;code&gt;sm_121&lt;/code&gt; cubins means the selected backend dies at runtime (&lt;code&gt;no kernel image is available&lt;/code&gt;); and a version-gated integration mismatch can silently drop you onto slow fallbacks. Most of this post’s timeline is one of those three links breaking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. The KV cache is a pre-allocated pool, not a meter.&lt;/strong&gt; vLLM (PagedAttention) grabs one big block of memory at startup, sized by &lt;code&gt;--gpu-memory-utilization&lt;/code&gt;, and pages sequences into it. It’s &lt;em&gt;capacity&lt;/em&gt;; the boot log tells you the pool size in GiB and in tokens, and dividing them gives the per-token cost that makes context-length planning arithmetic instead of folklore (worked example in the unified-memory section). On a unified-memory box the grab itself is the danger: it can’t be swapped, so an oversized pool &lt;em&gt;is&lt;/em&gt; the OOM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Speculative decoding buys speed with a coupled scheduler.&lt;/strong&gt; A cheap drafter proposes k tokens; the full model verifies them in a single forward pass; accepted tokens cost almost nothing extra. Acceptance depends entirely on how &lt;em&gt;predictable&lt;/em&gt; your output is to that drafter (code and structured JSON accept well, free prose doesn’t) and the metric vLLM reports (“acceptance length”) includes a guaranteed bonus token, so read it carefully. The part nobody advertises: the drafter is wired into the scheduler, so turning speculation on can change scheduling behavior itself: disable async scheduling, cap the token budget per step. A “free” flag can therefore tax prefill and TTFT even while decode acceptance looks healthy. That coupling is exactly how our n-gram experiment lost (full numbers below).&lt;/p&gt;
&lt;h2 id=&quot;the-timeline-from-launch-day-optimism-to-a-config-that-survives&quot;&gt;The timeline: from launch-day optimism to a config that survives&lt;/h2&gt;
&lt;p&gt;This is the part most guides skip: &lt;em&gt;when&lt;/em&gt; each fact became true. sm_121 support moved so fast that advice written eight weeks apart contradicts itself, including our own. Dates below come from our git history, incident notes, and the vLLM release record.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2025-12-20: the origin.&lt;/strong&gt; NVIDIA’s launch-window vLLM container documentation (NGC &lt;code&gt;vllm:25.11-py3&lt;/code&gt; and its release notes) claimed clean “DGX Spark functional support” and NVFP4-on-Blackwell. We wrote our first Spark serving doc from those sources, taking the claims at face value. What wasn’t yet visible in that material: &lt;code&gt;sm_121&lt;/code&gt; is consumer Blackwell, not the &lt;code&gt;sm_100&lt;/code&gt; the FP4 kernel work had targeted, and none of the then-shipped FlashInfer wheels contained sm_121 cubins, details the launch-window docs preceded rather than concealed. Everything below is the gap between documentation written at launch and a working box, closing in real time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2026-02 → 2026-05: upstream catches up, in pieces.&lt;/strong&gt; The vLLM release record tells the real support story: &lt;code&gt;enable_sm120_family&lt;/code&gt; (#33517, Feb); “Fix DGX Spark logic” (#38126, → v0.19.0); disable the broken FlashInfer CUTLASS MoE on sm_121 as a stopgap (#39825, → v0.20.0); fix the wrong sm_121 &lt;em&gt;exclusion&lt;/em&gt; from Marlin/CUTLASS FP8 (#35568) and land the b12x sm_121 kernels (#40082), both only in &lt;strong&gt;v0.22.0 (2026-05-29)&lt;/strong&gt;. In our tested pre-v0.22 images and checkpoints, native NVFP4 paths crashed (&lt;code&gt;cutlass FP4 gemm ... sm120/sm121 ... Error Internal&lt;/code&gt;), silently emitted garbage, or required Marlin; behavior varies with the image build, packaged wheels, checkpoint, and backend, so treat this as our observed floor, not a universal law.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2026-05-31: the empirical break.&lt;/strong&gt; After weeks of intermittent failures we isolated the pattern on our own boxes: prebuilt FlashInfer wheels shipped &lt;strong&gt;zero&lt;/strong&gt; sm_120/121 cubins; the model-card env vars rerouted only MoE layers while dense NVFP4 linears still hit the broken CUTLASS path; and the reliable fix was forcing &lt;strong&gt;Marlin for both backends&lt;/strong&gt; (plus &lt;code&gt;--enforce-eager&lt;/code&gt; for hybrid-Mamba models, a graph-capture bug fixed later in v0.22.0). Forced-Marlin became our fleet default, correct at the time, and understandably carried forward by community guides (including ours) as if it were a permanent hardware rule.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2026-06-01/02: reconciliation, then past Marlin.&lt;/strong&gt; The vLLM team published its Spark deep-dive claiming &lt;code&gt;auto&lt;/code&gt; backends were fine, which contradicted our validated config. Reconciling blog-vs-repo-vs-our-logs showed the disagreement was a &lt;strong&gt;version gap&lt;/strong&gt;, not a dispute: our Marlin era ran on v0.20.0, which simply lacked the v0.22.0 kernels. On 2026-06-02 we moved to a digest-pinned cu130 nightly and &lt;code&gt;--moe-backend auto&lt;/code&gt; selected FLASHINFER_CUTLASS native FP4, coherent, no Marlin, no enforce-eager, and &lt;em&gt;faster&lt;/em&gt; (~50.2 → ~55.8/58.5 tok/s on our Nemotrons). Meanwhile stock &lt;code&gt;v0.22.0-cu129&lt;/code&gt; (same vLLM version, different wheel) still crashed on &lt;code&gt;auto&lt;/code&gt; because the cu129 wheel didn’t bake the sm_121 MoE cubin. That’s when “which vLLM version?” stopped being the right question and “does &lt;em&gt;this wheel&lt;/em&gt; carry the cubins?” replaced it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2026-06-05 → 2026-07-05: the MTP production saga.&lt;/strong&gt; We deployed the 35B Qwen with in-checkpoint MTP: 86.8% acceptance in validation, then &lt;code&gt;CUDA error: an illegal memory access&lt;/code&gt; crashes every few hours in production, only under concurrent load (upstream #41190/#42084; the failure signature is a broken draft path poisoning the accepted-token buffer). An image bump to a 0.23-era nightly fixed the &lt;em&gt;load-time&lt;/em&gt; bug (#35031 via #46316) but not the runtime crash, so spec-decode went off. Along the way, a second, unrelated checkpoint lesson: the W4A16 build’s 4-bit &lt;code&gt;lm_head&lt;/code&gt; produced &lt;code&gt;!!!!&lt;/code&gt; garbage on greedy structured-extraction output; quantization choices inside the checkpoint, not the engine, decided both failures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2026-07-11/12: the W4A4 resolution.&lt;/strong&gt; Swapping to a genuine W4A4 checkpoint (4-bit activations, &lt;code&gt;lm_head&lt;/code&gt; unquantized) moved experts &lt;em&gt;and&lt;/em&gt; drafter onto the FLASHINFER_CUTLASS path, and the crash was gone: 8-way soak, 400/400, zero IMA, then re-enabled in production and bumped to &lt;code&gt;num_speculative_tokens: 3&lt;/code&gt; (mean acceptance length ~3.1 output tokens per verification step, bonus token included; ~95% draft acceptance on real traffic). The upstream bugs remain open; we didn’t patch vLLM, we &lt;em&gt;routed around the crashing kernel path via configuration&lt;/em&gt;, checkpoint format and drafter backend together, so neither alone gets the credit. The transferable lesson stands regardless: on sm_121, the checkpoint’s quant format (W4A4 vs W4A16) decides which kernels you run, and therefore which bugs you hit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2026-07-19: today.&lt;/strong&gt; Both boxes on digest-pinned nightlies (0.22/0.23-era), native FP4, MTP where the model ships heads, everything below measured on this config. We smoke-tested the v0.25.1 stable candidates the same day this post was assembled, and the timeline’s lesson reproduced on cue: the official cu129 stable still ships no sm_121 cubins (dead at engine init), and the promising new cu13 tag turned out to be a mis-stamped dev artifact decoding 2.6× slow (details in the image table below). Seven months in, the rule hasn’t changed: &lt;strong&gt;there is still no viable aarch64 stable for GB10 native FP4; validate the wheel you actually pulled, then pin it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update: 2026-08-05 (post-publication).&lt;/strong&gt; Three developments the timeline’s own logic predicted: (1) &lt;strong&gt;v0.26.0&lt;/strong&gt; (2026-07-27): we smoke-tested its cu129 aarch64 stable on a scratch GB10 instance, and the &lt;code&gt;sm_121&lt;/code&gt; NVFP4 cubins are finally present. &lt;code&gt;auto&lt;/code&gt; selects &lt;code&gt;FLASHINFER_CUTLASS&lt;/code&gt; for both dense and MoE NVFP4 with no &lt;code&gt;no kernel image is available&lt;/code&gt;, breaking the three-release cu129 drought. It still could not serve our unsloth checkpoint, though: EngineCore init dies during the profiling run on a CUTLASS FP8 W8A8 GEMM (&lt;code&gt;cutlass_gemm_caller.cuh:62, Error Internal&lt;/code&gt;), a path the same checkpoint clears on our v0.23-era nightly. So the operator bottom line is unchanged (no cu129 stable serves this GB10 NVFP4+FP8 workload yet), but the failure moved from a missing NVFP4 kernel image to an FP8 CUTLASS error, worth re-testing per checkpoint. (Scratch-tested 2026-08-05; not yet root-caused or filed.) (2) &lt;strong&gt;#41190 (the MTP illegal-memory-access) was fixed on main 2026-07-29&lt;/strong&gt; (#48245/#49620), though not yet in a tagged stable release and marked a probable (“may fix”) rather than confirmed resolution, so stable-wheel operators remain exposed until a nightly carries it. (3) &lt;strong&gt;Prefix caching + MTP now coexist&lt;/strong&gt; on a v0.26.1-dev nightly: the either/or that #42084 forced looks resolved via an experimental Mamba/SSM cache-align mode (tested on our GDN-hybrid 35B: zero out-of-bounds under a shared-prefix + MTP soak). All are elaborated in the sections below; noted here to keep the timeline honest about its own aging.&lt;/p&gt;
&lt;h2 id=&quot;the-one-rule-that-explains-all-the-fp4-confusion&quot;&gt;The one rule that explains all the FP4 confusion&lt;/h2&gt;
&lt;p&gt;A widely-shared rule (reasonably derived from what its authors tested) says “GB10 has no native FP4 compute, so FP4 MoE must run on Marlin.” Our production logs show the rule has an important exception:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Using FlashInferCutlassNvFp4LinearKernel for NVFP4 GEMM&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Using &apos;FLASHINFER_CUTLASS&apos; NvFp4 MoE backend out of potential backends:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  [&apos;FLASHINFER_TRTLLM&apos;, &apos;FLASHINFER_CUTEDSL&apos;, &apos;FLASHINFER_CUTEDSL_BATCHED&apos;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   &apos;FLASHINFER_CUTLASS&apos;, &apos;VLLM_CUTLASS&apos;, &apos;MARLIN&apos;, &apos;EMULATION&apos;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s a Qwen3.6-35B-A3B NVFP4 MoE on &lt;code&gt;--moe-backend auto&lt;/code&gt;, coherent output, running for days under real load, on a GB10. No Marlin anywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Native FP4 on sm_121 needs two conditions, and both must hold:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The image’s FlashInfer wheel actually bakes &lt;code&gt;sm_121&lt;/code&gt; cubins.&lt;/strong&gt; Source support is not wheel support. Stock &lt;code&gt;v0.22.0-cu129&lt;/code&gt; has the sm_121 kernels &lt;em&gt;in source&lt;/em&gt; (#40082) but the cu129 wheel ships without the MoE cubin; &lt;code&gt;auto&lt;/code&gt; dies with &lt;code&gt;no kernel image is available&lt;/code&gt;. The cu130 nightlies bake it. This is an image property; smoke-test the exact digest you deploy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The checkpoint is genuine W4A4&lt;/strong&gt; (&lt;code&gt;nvfp4-pack-quantized&lt;/code&gt;: 4-bit weights &lt;em&gt;and&lt;/em&gt; activations). A &lt;strong&gt;W4A16 weight-only&lt;/strong&gt; “NVFP4” checkpoint has no FP4 activations, so there is no FP4 matmul to dispatch; &lt;code&gt;auto&lt;/code&gt; lands on Marlin &lt;em&gt;correctly&lt;/em&gt;, and no flag will change it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Condition 2 is the one nobody talks about, and it explains the contradictory community reports. We ran three quantizations of the &lt;em&gt;same model&lt;/em&gt; (Qwen3.6-35B-A3B) on the same image:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checkpoint&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;What &lt;code&gt;auto&lt;/code&gt; picks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nvidia/Qwen3.6-35B-A3B-NVFP4&lt;/code&gt; (modelopt)&lt;/td&gt;
&lt;td&gt;W4A16 weight-only&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MARLIN&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RedHatAI NVFP4 build&lt;/td&gt;
&lt;td&gt;W4A4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;FLASHINFER_CUTLASS&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unsloth/Qwen3.6-35B-A3B-NVFP4&lt;/code&gt; (deployed)&lt;/td&gt;
&lt;td&gt;W4A4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;FLASHINFER_CUTLASS&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So when a guide says Marlin is required, it’s worth checking which checkpoint was tested; most popular recipes use the W4A16 builds, which made the conclusion easy to reach. If it was an &lt;code&gt;nvidia/...&lt;/code&gt; modelopt W4A16 build (most of the popular recipes are), their observation was correct &lt;em&gt;for that checkpoint&lt;/em&gt;; it just doesn’t generalize into a hardware rule.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decision tree:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Is the checkpoint W4A4 (nvfp4-pack-quantized, activations num_bits: 4)?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├─ NO (W4A16 weight-only) → auto → Marlin. This is correct; don&apos;t fight it.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│    At batch-1 you barely pay for it: decode is bandwidth-bound and 4-bit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;│    weight storage already captures the win. Marlin costs you prefill + concurrency.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└─ YES → does your image&apos;s flashinfer wheel bake sm_121 cubins?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     ├─ NO (e.g. stock v0.22.0-cu129) → force --moe-backend marlin, or change image&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     └─ YES (cu130 nightlies, pinned by gitsha) → --moe-backend auto&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          → FLASHINFER_CUTLASS native FP4, dense AND MoE. Faster than Marlin,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            full cudagraphs, and (0.23-era) --attention-backend flashinfer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            works even on hybrid attention/Mamba/GDN models.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One more wrinkle from the release archaeology: the &lt;em&gt;fastest&lt;/em&gt; sm_121 path, &lt;code&gt;flashinfer_b12x&lt;/code&gt; (#40082), is intentionally excluded from &lt;code&gt;auto&lt;/code&gt;-selection pending an upstream CUTLASS MMA-guard fix, so &lt;code&gt;auto&lt;/code&gt; gives you a working-and-fast path, not necessarily the theoretical-fastest one. Precision note, since people assume this is MoE-only: it isn’t. The dense NVFP4 linear oracle (&lt;code&gt;_POSSIBLE_NVFP4_KERNELS&lt;/code&gt; in &lt;code&gt;vllm/model_executor/kernels/linear/__init__.py&lt;/code&gt;) comments b12x out of its auto candidate list with the same rationale as the MoE oracle, so &lt;code&gt;auto&lt;/code&gt; never even tests it on either path. Our boot log’s &lt;code&gt;FlashInferCutlassNvFp4LinearKernel&lt;/code&gt; for the dense GEMM is &lt;code&gt;auto&lt;/code&gt; walking past the policy-excluded b12x (and a higher-priority CuteDSL kernel our wheel can’t use) down to CUTLASS. Both dense and MoE land on CUTLASS for the same reason: b12x is opt-in only (&lt;code&gt;--linear-backend flashinfer_b12x&lt;/code&gt; / &lt;code&gt;moe-backend flashinfer_b12x&lt;/code&gt;), never auto-selected. We haven’t needed to &lt;em&gt;force&lt;/em&gt; &lt;code&gt;b12x&lt;/code&gt;; CUTLASS is already ahead of Marlin.&lt;/p&gt;
&lt;h2 id=&quot;which-images-actually-work-july-2026&quot;&gt;Which images actually work (July 2026)&lt;/h2&gt;
&lt;p&gt;The single most common failure mode on Spark is an image whose wheels predate &lt;code&gt;sm_121&lt;/code&gt;. Verified state as of &lt;strong&gt;2026-07-19&lt;/strong&gt; (Docker Hub API + our deployments):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;th&gt;sm_121 verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm/vllm-openai:nightly-&amp;lt;gitsha&amp;gt;&lt;/code&gt; (cu130 line)&lt;/td&gt;
&lt;td&gt;0.23.x-dev&lt;/td&gt;
&lt;td&gt;✅ our Spark-2 production: native FP4 + MTP, pinned by gitsha&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm/vllm-openai:nightly-4721bb3aa…&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.22.1rc1.dev26&lt;/td&gt;
&lt;td&gt;✅ our Spark-1 production (Nemotron ×2, native FP4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm/vllm-openai:v0.25.1-aarch64-cu129-ubuntu2404&lt;/code&gt; (stable, 2026-07-13)&lt;/td&gt;
&lt;td&gt;0.25.1&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;tested 2026-07-19: dead at EngineCore init&lt;/strong&gt;, &lt;code&gt;CUDA error: no kernel image is available&lt;/code&gt; in the FlashInfer alloc path. cu129 stable wheels have now shipped without sm_121 cubins for &lt;strong&gt;three consecutive releases&lt;/strong&gt; (0.22.0 → 0.25.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm/vllm-openai:vllm-arm64-cu13-0.25.1-&amp;lt;sha&amp;gt;&lt;/code&gt; (2026-07-17)&lt;/td&gt;
&lt;td&gt;says 0.25.1…&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;tested 2026-07-19: a trap.&lt;/strong&gt; Looks like the long-awaited cu13 aarch64 stable, and the sm_121 cubins ARE there (&lt;code&gt;auto&lt;/code&gt; → FLASHINFER_CUTLASS, coherent output), but &lt;code&gt;/version&lt;/code&gt; reports &lt;code&gt;0.1.dev18498&lt;/code&gt;, a &lt;strong&gt;mis-stamped CI artifact&lt;/strong&gt;, and decode ran &lt;strong&gt;2.6× slower&lt;/strong&gt; than our pinned 0.22-era nightly on the identical config (23.5 vs 62.0 tok/s, prefill halved too)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vllm/vllm-openai:cu130-nightly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~0.20-era&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;dormant since 2026-04-23&lt;/strong&gt;: it still appears in guides written before the track went quiet (easy to miss; we only caught it via the Docker Hub API); the cu13 line moved to &lt;code&gt;nightly-dev-arm64-cu13.0.1-&amp;lt;sha&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v0.22.0-aarch64-cu129&lt;/code&gt; + forced Marlin&lt;/td&gt;
&lt;td&gt;0.22.0&lt;/td&gt;
&lt;td&gt;✅ as a pinned stable &lt;em&gt;fallback&lt;/em&gt; only (weight-only path, ~50 tok/s on our 30B Nemotron)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Rules that survived contact:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Check &lt;code&gt;/version&lt;/code&gt; before anything else: a tag’s name is a claim, not a fact.&lt;/strong&gt; The cu13 “0.25.1” tag above passed every kernel check and would have shipped a 2.6× decode regression; the one-line &lt;code&gt;curl :PORT/version&lt;/code&gt; exposed it as a dev artifact instantly. Our best hypothesis for the slowdown: with a bogus version stamp, version-gated integrations (FlashInfer wheel matching, autotune caches, compat shims) can’t resolve, and the build silently takes fallback kernel paths; the uniform ~2× hit across &lt;em&gt;both&lt;/em&gt; prefill and decode fits systemic fallback, not one bad kernel. We didn’t profile further; a mis-stamped build is disqualified either way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pin &lt;code&gt;nightly-&amp;lt;gitsha&amp;gt;&lt;/code&gt; or a digest, never a moving tag.&lt;/strong&gt; We froze our production image as a locally-tagged derivative so upstream tag drift can’t change what’s running.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smoke-test the exact image before deploying&lt;/strong&gt;: boot the model on a scratch port, hit the raw &lt;code&gt;/v1/completions&lt;/code&gt; endpoint (bypasses chat template and reasoning parser), and watch for both the loud failure (&lt;code&gt;no kernel image is available&lt;/code&gt;, &lt;code&gt;cutlass FP4 gemm … Error Internal&lt;/code&gt;) and the quiet one (silent garbage tokens).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A reasoning model returning empty output is usually not broken.&lt;/strong&gt; If &lt;code&gt;finish_reason: length&lt;/code&gt; and both &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;reasoning_content&lt;/code&gt; are empty, the model hit &lt;code&gt;max_tokens&lt;/code&gt; inside an unclosed &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; block. Raise the budget or pass &lt;code&gt;chat_template_kwargs: {&quot;enable_thinking&quot;: false}&lt;/code&gt; before you blame the kernels.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-mtp-saga-speculative-decoding-that-survives-real-traffic&quot;&gt;The MTP saga: speculative decoding that survives real traffic&lt;/h2&gt;
&lt;p&gt;Qwen3.6 ships in-checkpoint MTP heads, and single-stream decode on a bandwidth-bound box is exactly where speculative decoding pays. Getting it to &lt;em&gt;stay up&lt;/em&gt; taught us the most expensive lesson in this post:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Batch-1 validation lies.&lt;/strong&gt; Our first MTP deployment (W4A16 checkpoint → Marlin experts, drafter forced to a Triton MoE backend) validated clean at batch-1 with 86.8% acceptance, then died in production with &lt;code&gt;CUDA error: an illegal memory access&lt;/code&gt; &lt;strong&gt;only under concurrent load&lt;/strong&gt;, taking both the coordinator tool-calling endpoint and the memory-extraction alias down with it. Upstream issues existed (#41190/#42084); an image bump didn’t fix it; we ran months with spec-decode off.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The crash was avoided by configuration, not a patch.&lt;/strong&gt; Moving to the W4A4 build routed both the experts &lt;em&gt;and&lt;/em&gt; the drafter through FLASHINFER_CUTLASS, a different kernel path than the crashing Marlin+Triton combination. The W4A4/CUTLASS configuration eliminated the crash in our concurrency soak, but checkpoint format and drafter backend changed together, so the result cannot be attributed to either change alone. Before re-enabling in production we soaked it at 8-way concurrency: 400/400 requests, &lt;strong&gt;zero&lt;/strong&gt; illegal-memory-access, 73% acceptance under load.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What MTP actually delivers on GB10&lt;/strong&gt; (our production numbers, &lt;code&gt;num_speculative_tokens: 3&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mean acceptance length ≈3.1 output tokens per verification step&lt;/strong&gt; under 8-way burst: note vLLM’s metric includes the guaranteed bonus token, so with &lt;code&gt;n=3&lt;/code&gt; that’s ~2.1 accepted &lt;em&gt;draft&lt;/em&gt; tokens per step (per-position draft acceptance 84.2% / 67.8% / 57.5%: the falling marginal value is why you keep n at 1–3, unlike block-verify methods that run 7–15).&lt;/li&gt;
&lt;li&gt;~&lt;strong&gt;94.8% cumulative acceptance over ~20 h of real coordinator + extraction traffic&lt;/strong&gt;, far above any synthetic number, because real agent workloads are predictable, structured output. Measure acceptance on &lt;em&gt;your&lt;/em&gt; traffic via &lt;code&gt;/metrics | grep -i spec_decode&lt;/code&gt;, not on a demo prompt.&lt;/li&gt;
&lt;li&gt;Thinking-mode decode is &lt;em&gt;faster&lt;/em&gt; than answer-mode with MTP on: structured reasoning accepts better than free prose.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;measured-native-fp4--mtp-vs-the-published-marlin-numbers&quot;&gt;Measured: native FP4 + MTP vs the published Marlin numbers&lt;/h3&gt;
&lt;p&gt;We ran the same single-stream methodology the community playbook published (prefix-cache-defeating nonces, forced full-length generation, server-side token counts, 3-run medians, ~6K-token prompt) against our &lt;strong&gt;production&lt;/strong&gt; 35B, native FLASHINFER_CUTLASS, W4A4, MTP-3, vLLM 0.23.1rc1-era, measured 2026-07-19:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config (same model, Qwen3.6-35B-A3B NVFP4)&lt;/th&gt;
&lt;th&gt;TTFT&lt;/th&gt;
&lt;th&gt;Prefill&lt;/th&gt;
&lt;th&gt;Decode (answer)&lt;/th&gt;
&lt;th&gt;Decode (thinking)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ours: W4A4 → native FP4, MTP-3, live prod box&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;955 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6,307 t/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;82.2 t/s (79–98 across runs)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;102.5 t/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Published: W4A16 → Marlin, MTP-3, idle box (vlaicu.io)&lt;/td&gt;
&lt;td&gt;~965 ms&lt;/td&gt;
&lt;td&gt;~6,240 t/s&lt;/td&gt;
&lt;td&gt;102 t/s&lt;/td&gt;
&lt;td&gt;117.4 t/s (6K ctx)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ours, historical: W4A16 → Marlin, MTP-3 (2026-06-06)&lt;/td&gt;
&lt;td&gt;~91 ms @ short prompt&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;~102.7 t/s&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The honest read: &lt;strong&gt;these measurements show no batch-1 native-FP4 decode advantage, and they don’t isolate the backend, either.&lt;/strong&gt; Checkpoint format, machine load, measurement date, and tooling all differ across the rows, so this is convergent evidence, not a controlled experiment. It is &lt;em&gt;consistent&lt;/em&gt; with batch-1 parity: decode is memory-bandwidth-bound and 4-bit weight storage is identical either way; our own W4A16-era measurement (102.7 t/s) matches the published Marlin number almost exactly, and our W4A4 medians land lower mostly because the box was serving background production traffic during the run (top of spread 97.6; window acceptance 76.4% vs ~80% idle). Calling it backend &lt;em&gt;equivalence&lt;/em&gt; would need a same-checkpoint, same-load A/B we haven’t run. Native FP4’s real wins are &lt;strong&gt;prefill/concurrency (compute-bound paths), full cudagraphs on the hybrid arch, and (decisively for us) the kernel path on which MTP stopped crashing.&lt;/strong&gt; The practical takeaway: don’t switch backends expecting a batch-1 decode win, and when evaluating Marlin, look at prefill and concurrent load: that’s where the difference actually lives.&lt;/p&gt;
&lt;p&gt;Patterns from this configuration (one model, one serving config, corroborated where noted, not universal laws): prefill held ~6.3K t/s in both modes on this Qwen3.6-35B setup (TTFT ≈ prompt_tokens ÷ 6300; the vlaicu measurement of the same model reports the same ~6.2K, and note our Nemotron prefills at 8.5K; ‘flat across modes’ generalizes, the absolute number doesn’t); &lt;strong&gt;thinking decoded ~25% faster than answering&lt;/strong&gt; under MTP for this workload and acceptance profile (102.5 vs 82.2); per-position draft acceptance decayed 86.6% → 77.2% → 65.5% across positions 0/1/2 in the same window.&lt;/p&gt;
&lt;p&gt;And the no-speculator baseline, same script, same day, our Nemotron-3-Nano-30B-A3B-NVFP4 (native FP4, no spec-decode, vLLM 0.22.1rc1-era):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Answer-only&lt;/th&gt;
&lt;th&gt;Thinking&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TTFT (~6K prompt)&lt;/td&gt;
&lt;td&gt;706 ms&lt;/td&gt;
&lt;td&gt;710 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefill&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8,530 t/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8,485 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decode&lt;/td&gt;
&lt;td&gt;64.2 t/s&lt;/td&gt;
&lt;td&gt;63.8 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two things worth noticing: thinking ≈ answering exactly as bandwidth-bound theory predicts when there’s no draft head (the Qwen thinking-speedup is &lt;em&gt;entirely&lt;/em&gt; MTP), and the Mamba-hybrid prefills ~35% faster than the attention-hybrid Qwen (8.5K vs 6.3K t/s). Also 64.2 t/s is ~10% above our own 2026-06-02 measurement of the same deployment (58.5), measured with a different tool; methodology moves numbers, which is exactly why every figure here states its method and date.&lt;/p&gt;
&lt;p&gt;Two spec-decode paths we evaluated but do &lt;strong&gt;not&lt;/strong&gt; run:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;n-gram lookup&lt;/strong&gt;: the best story in this post, because we got it wrong in public first. Our initial A/B showed n-gram &lt;em&gt;losing&lt;/em&gt; 14% on our extraction workload despite 62% acceptance, and we wrote it up as “the free flag isn’t free.” A reviewer pushed back: our explanation blended three effects, and the prefill penalty looked configuration-specific. The controlled retest (every arm with an explicit &lt;code&gt;--max-num-batched-tokens&lt;/code&gt;, prefix caching pinned off, k swept 1→4, token-equality checks) &lt;strong&gt;flipped the verdict entirely&lt;/strong&gt;: extract-workload decode went 61.5 → &lt;strong&gt;117.3 tok/s (+91%) at k=3&lt;/strong&gt;, with &lt;em&gt;no&lt;/em&gt; prefill penalty in any arm. The original loss was our configuration all along (an unset scheduler budget interacting with speculation settings), exactly what the boot warning had been trying to tell us. The k-sweep shows the real economics: acceptance decays with depth (80.8% at k=1 → 64% at k=4) while throughput saturates at k=3. Two caveats survived the retest: speculative and plain decode are &lt;em&gt;not&lt;/em&gt; token-identical at temperature 0 on this build (every spec arm diverged from the control’s greedy output at the same early position: deterministic batch-shape numerics, so validate output quality, don’t assume equivalence), and all of this is single-stream: speculation taxes throughput under concurrent load, so it ships with &lt;code&gt;--speculative-disable-by-batch-size&lt;/code&gt; or not at all. The meta-lesson outranks the numbers: &lt;strong&gt;a configuration confound can invert your benchmark’s conclusion, and the warning that would have told you is in the boot log.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;And then the story turned again. Before promoting, we ran the production gates (an 8-way soak plus an output-quality gate with a control arm), and the quality gate killed it: the no-spec control produced &lt;strong&gt;20/20&lt;/strong&gt; perfect JSON extractions, while ngram produced &lt;strong&gt;0/20&lt;/strong&gt;, with version strings losing their final component (&lt;code&gt;&quot;0.22&quot;&lt;/code&gt; for &lt;code&gt;&quot;0.22.0&quot;&lt;/code&gt;) and JSON keys mutating mid-document (&lt;code&gt;&quot;sentence&quot;&lt;/code&gt; → &lt;code&gt;&quot;sent&quot;&lt;/code&gt;). The GPU proposer failed identically, localizing the defect to the shared verification path. The temperature-0 divergence we’d politely called “batch-shape numerics” was token corruption all along. (Two more gate catches on the way: prefix caching × ngram hard-asserts in the Mamba-2 layers on this arch, and the burst-protection flag most guides recommend doesn’t exist on this build in either spelling.) Final ledger for n-gram on this box: +91% throughput, mechanically real, generating subtly wrong tokens, worthless &lt;strong&gt;on this hybrid Mamba/GDN architecture&lt;/strong&gt; (the corruption lives in the shared verification path, upstream #39273; CPU and GPU proposers fail identically, which localizes it there, and on a pure-attention target n-gram may well be fine). Three verdicts in three days, each overturned by a better-controlled experiment than the last. If there’s one sentence to take from this post, it’s the last gate’s: &lt;strong&gt;speculative-decoding throughput without output validation is not a result.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DFlash&lt;/strong&gt;: block-diffusion drafters, the biggest published single-stream wins (2.2–2.7× on GB10, per AEON-7’s measurements). The caveat that keeps it off our fleet: on &lt;strong&gt;quantized (NVFP4/FP8) targets under stock vLLM, acceptance collapses&lt;/strong&gt; (~4 of 15 tokens → a net slowdown); the working paths need a patched sm121 build or a BF16 target, and DFlash requires BF16 KV, incompatible with our fp8-KV recipes. MTP at ~95% real-traffic acceptance with zero extra memory sets a high bar.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;unified-memory-the-oom-you-get-with-free-ram-showing&quot;&gt;Unified memory: the OOM you get with “free” RAM showing&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--gpu-memory-utilization&lt;/code&gt; on Spark is a fraction of the whole 128 GB pool, shared with the OS, page cache, and every other container. Two facts make it dangerous:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The reservation is unswappable.&lt;/strong&gt; On unified memory, vLLM’s KV-pool grab can’t be paged out. Set 0.6 on a box that’s also running a gateway and observability stack and you’ve made the kernel OOM-killer’s decision for it: that exact misconfiguration cost us a 2h40m production outage (instance reserved ~71 GiB, host hit ~2 GiB free with swap exhausted, &lt;code&gt;oom_kill&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lowering it shrinks KV &lt;em&gt;capacity&lt;/em&gt;, not speed.&lt;/strong&gt; The pool is capacity, not a performance knob. Our 35B runs at &lt;strong&gt;0.3&lt;/strong&gt; (≈34 GiB) while keeping the model’s full 262K context, because the math works:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;KV cost  ≈ 12.4 KB/token          ← boot log: 7.15 GiB ÷ 617,494 pool tokens&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                     (Qwen3.6-35B-A3B, KV cache in fp8)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1 × 262K sequence ≈ 3.1 GiB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0.3 util → 7.15 GiB KV pool → 2.36x concurrency at full 262K context&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                     (vLLM prints this: Maximum concurrency 2.36x)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;real coordinator/extraction traffic touches &amp;lt; 2% of that&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A clarification that trips people up: &lt;strong&gt;two quantizations coexist in this server and they are different knobs.&lt;/strong&gt; The &lt;em&gt;weights&lt;/em&gt; are NVFP4 (that’s the checkpoint format the whole kernel-selection story is about); the &lt;em&gt;KV cache&lt;/em&gt; is fp8 because we pass &lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt;. The 12.4 KB/token figure is a property of the KV dtype and the model’s attention layout; the weight quantization plays no part in it. (A widely-quoted figure for this same model family is ~16.6 KB/token, measured on a different checkpoint, vLLM build, and config. Hybrid attention/SSM models are exactly where per-token KV accounting varies across versions, another reason the formula beats the folklore: your own boot log prints both numbers; divide them.)&lt;/p&gt;
&lt;p&gt;Read &lt;code&gt;Available KV cache memory&lt;/code&gt; and &lt;code&gt;GPU KV cache size ... tokens&lt;/code&gt; from your own boot log, and size &lt;code&gt;gpu-memory-utilization&lt;/code&gt; from &lt;code&gt;max-num-seqs × max-model-len&lt;/code&gt; plus slack, not from a recipe’s 0.85 default, which assumes the box serves nothing else. If a load fails with memory apparently free, NVIDIA’s documented &lt;em&gt;debugging&lt;/em&gt; workaround for UMA memory-reporting is to flush the page cache (&lt;code&gt;sync; echo 3 &amp;gt; /proc/sys/vm/drop_caches&lt;/code&gt;) and then restart the affected application; the cache holds unified memory CUDA can’t reclaim. Treat it as a diagnostic step, not routine ops.&lt;/p&gt;
&lt;h2 id=&quot;smaller-sharp-edges-quickly&quot;&gt;Smaller sharp edges, quickly&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool calling fails closed, loudly, and only at request time.&lt;/strong&gt; Without &lt;code&gt;--enable-auto-tool-choice --tool-call-parser &amp;lt;family&amp;gt;&lt;/code&gt;, the model loads fine and &lt;code&gt;/v1/models&lt;/code&gt; is green; then every agent request 400s (&lt;code&gt;&quot;auto&quot; tool choice requires…&lt;/code&gt;). The parser tracks the &lt;em&gt;model family&lt;/em&gt;, not your client: &lt;code&gt;qwen3_coder&lt;/code&gt;/&lt;code&gt;qwen3_xml&lt;/code&gt; for Qwen3.6, &lt;code&gt;nemotron_v3&lt;/code&gt;/nano parsers for Nemotron. The wrong parser fails &lt;em&gt;open&lt;/em&gt;: the call text sits in &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;tool_calls&lt;/code&gt; stays null, and your agent silently stalls after the think block.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hybrid-Mamba graph capture&lt;/strong&gt; (&lt;code&gt;nemotron_h&lt;/code&gt;): needed &lt;code&gt;--enforce-eager&lt;/code&gt; on v0.20.0 (~37% throughput tax), fixed by v0.22.0; drop the flag and take the free speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;max_tokens&lt;/code&gt; starvation on reasoning models&lt;/strong&gt; breaks tool-call parsing in ways that look like model bugs. Budget ~10K+ output tokens for reasoning models in agent pipelines. It presents &lt;em&gt;identically&lt;/em&gt; to the wrong-parser stall above (the agent hangs after the think block with no error), but the two are distinguishable: &lt;strong&gt;wrong parser&lt;/strong&gt; gives &lt;code&gt;tool_calls&lt;/code&gt; null with the call text sitting in &lt;code&gt;content&lt;/code&gt;; &lt;strong&gt;budget starvation&lt;/strong&gt; gives &lt;code&gt;finish_reason: length&lt;/code&gt; with an unclosed &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; block. Check which before blaming the model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ConnectX-7 clustering: the 200 Gb/s per QSFP cable arrives as two ~100 Gb/s interfaces: configure both or you get half.&lt;/strong&gt; Each port exposes two RoCE “twins” (each behind an x4 PCIe Gen5 link); our single-interface &lt;code&gt;ib_write_bw&lt;/code&gt; measured ~111 Gb/s, consistent with one twin, so full NCCL bandwidth needs &lt;code&gt;NCCL_IB_HCA=&amp;lt;twin1&amp;gt;,&amp;lt;twin2&amp;gt;&lt;/code&gt;. DGX Spark also doesn’t support GPUDirect RDMA; CUDA-memory traffic takes NVIDIA’s documented host-pinned fallback. And multi-node vLLM needs more than NCCL: a Ray cluster (or multi-node multiprocessing) with matching runtime environments and model paths, plus an explicitly selected distributed executor. TP=2 across two one-GPU Sparks works; PP=2 can reduce cross-node tensor-parallel collectives where the model supports it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cold start is real&lt;/strong&gt;: ~2.5 min weight load + ~25 s first-request JIT on our 30B-class models. Keep instances warm (no idle-timeout, no on-demand start in the request path) and fire a &lt;code&gt;max_tokens: 3&lt;/code&gt; warm-up ping at boot.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-wed-tell-you-to-deploy-today&quot;&gt;What we’d tell you to deploy today&lt;/h2&gt;
&lt;p&gt;For a 30–35B-class MoE NVFP4 reasoning model on one GB10, July 2026:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; a genuine W4A4 build (check &lt;code&gt;config.json&lt;/code&gt; for &lt;code&gt;nvfp4-pack-quantized&lt;/code&gt; with activation &lt;code&gt;num_bits: 4&lt;/code&gt;; confirm &lt;code&gt;lm_head&lt;/code&gt; is in the quant ignore-list if you do structured extraction; 4-bit lm_head gave us &lt;code&gt;!!!!&lt;/code&gt; garbage on greedy JSON output).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Image:&lt;/strong&gt; a cu130-line nightly pinned by gitsha (ours: 0.23.1rc1-era), or v0.25.1 stable &lt;em&gt;after&lt;/em&gt; an sm_121 smoke test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flags:&lt;/strong&gt; &lt;code&gt;--moe-backend auto&lt;/code&gt; (verify the boot log says FLASHINFER_CUTLASS), &lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt;, MTP at &lt;code&gt;num_speculative_tokens: 3&lt;/code&gt; &lt;strong&gt;after&lt;/strong&gt; an 8-way soak, tool-call + reasoning parsers matched to the model family, and &lt;code&gt;--gpu-memory-utilization&lt;/code&gt; computed from the KV math above, not defaulted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Process:&lt;/strong&gt; pin digests, smoke-test raw completions on every image change, watch &lt;code&gt;/metrics&lt;/code&gt; spec-decode acceptance and KV utilization, and never validate spec-decode at batch-1.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;still-open-what-were-waiting-on-and-why-it-matters&quot;&gt;Still open: what we’re waiting on, and why it matters&lt;/h2&gt;
&lt;p&gt;A snapshot of the upstream gaps that shape our config today. Each of these is a reason some recommendation above reads “pinned nightly” or “re-test later” instead of “just do X.”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vLLM #41190: illegal memory access in MTP speculative decoding (fixed on main 2026-07-29 via #48245/#49620; not yet in a tagged stable release, and upstream marks it a probable “may fix” rather than a confirmed resolution).&lt;/strong&gt; The crash that took our 35B down for a month. Failure signature: the drafter path poisons the accepted-token buffer (all &lt;code&gt;-1&lt;/code&gt;) and the engine dies with &lt;code&gt;CUDA error: an illegal memory access&lt;/code&gt;, &lt;em&gt;only under concurrent load&lt;/em&gt;, which is why batch-1 validation sails past it. The upstream fix locates the root cause deeper than our symptom: async-decode output-placeholder accounting underflows when KV-pressure preemption rolls back an in-flight request. That’s a scheduler bug, kernel-agnostic, which is precisely why concurrency (KV pressure, then preemption) triggers it and batch-1 never can. Why it’s pertinent: we didn’t patch it, we &lt;strong&gt;routed around it&lt;/strong&gt;. The W4A4/CUTLASS configuration avoids the crashing combination, but since the true cause is scheduler-level, we can’t yet say whether we &lt;em&gt;avoided&lt;/em&gt; or merely &lt;em&gt;masked&lt;/em&gt; it. We ran that soak: W4A4/CUTLASS held at 100% KV utilization under concurrent MTP load with zero illegal-memory-access, but with prefix caching off, vLLM’s admission control never actually preempted an in-flight request, so saturation alone did not reproduce the crash. That is strong evidence the config is robust in practice, and a hint that the #41190 rollback needs conditions this config avoids, but it is not a formal proof of avoided over masked. Anyone on a W4A16 NVFP4 checkpoint (the Marlin path) with MTP under real concurrency stayed exposed until a nightly carries #48245, one more reason to run digest-nightlies, not stables. Our rule stands regardless: no spec-decode config goes to production without an 8-way soak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vLLM #42084: out-of-bounds gather with prefix caching + spec decode on &lt;code&gt;sm_121a&lt;/code&gt; (open, but see the 2026-08-05 update below).&lt;/strong&gt; Constrains which feature &lt;em&gt;combinations&lt;/em&gt; are safe on this architecture. The mechanism is hybrid-architecture-specific: the linear-attention state block table (Mamba-2 layers in Nemotron, Gated DeltaNet layers in Qwen3.6, both managed by vLLM’s SSM cache, which it confusingly calls “Mamba” even for GDN) and the speculative extension disagree on how many blocks a sequence owns, so the &lt;code&gt;torch.gather&lt;/code&gt; indexes past the end; a pure-attention model has no second state block table to desync, so it is likely unaffected. Pertinent because prefix caching and speculation are both individually attractive for agent workloads: this is the reason we validate them together, not separately, before enabling both. (We did lose the production 35B to an illegal-memory-access around 2026-08-04 under concurrent extraction load, but with no logs and a coincident llamactl-restart incident on the same box we can’t attribute it — not to #42084, and not confidently to any particular feature combination — so we don’t count it as evidence either way.) &lt;strong&gt;Update 2026-08-05:&lt;/strong&gt; on a v0.26.1-dev nightly we ran prefix caching + MTP together on our GB10 Qwen3.6-35B (the Gated DeltaNet hybrid, &lt;em&gt;not&lt;/em&gt; classic Mamba) under a shared-prefix concurrent soak (40% cache hit, 86.9% KV): zero out-of-bounds, zero state-cache asserts, coherent output throughout. #42084 appears resolved via a new experimental cache-“align” mode that sets the attention block size to match the linear-attention page size. So the prefix-caching-vs-spec-decode either/or looks liftable on recent nightlies, with the caveat that vLLM still marks the align path experimental.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;b12x&lt;/code&gt; backend is deliberately excluded from &lt;code&gt;auto&lt;/code&gt; selection&lt;/strong&gt; pending an upstream CUTLASS sm_121 MMA-guard fix (the exclusion is a documented choice in vLLM’s NVFP4 oracle). &lt;code&gt;b12x&lt;/code&gt; (#40082) is the purpose-built sm_121 path and plausibly the fastest one, meaning some performance is intentionally left on the table today. When that guard fix lands, &lt;code&gt;auto&lt;/code&gt; may silently start selecting &lt;code&gt;b12x&lt;/code&gt;, which is both good news and a behavior change to catch in the boot log. We re-bench on every image bump partly for this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No aarch64 stable wheel has shipped &lt;code&gt;sm_121&lt;/code&gt; cubins in three consecutive releases&lt;/strong&gt; (v0.22.0 → v0.25.1, verified by us on the day each mattered), and the first CUDA-13 aarch64 “stable” tag turned out to be a mis-stamped dev artifact. This is the single reason our production images are digest-pinned &lt;em&gt;nightlies&lt;/em&gt;, not a preference for living dangerously, but the only track whose wheels currently carry the kernels. The day a properly version-stamped cu13 aarch64 stable appears (check &lt;code&gt;/version&lt;/code&gt; first), the calculus flips.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NGC container lag (vllm #31424).&lt;/strong&gt; NVIDIA’s official vLLM containers trail upstream by months: the newest NGC tag ships 0.22.1 while upstream stable is 0.25.1. Pertinent because “use the vendor container” is the default advice in most enterprise settings, and on this platform it currently can’t serve the newest model architectures or fixes. (NVIDIA closed #31424 as &lt;code&gt;not_planned&lt;/code&gt;, so the lag is effectively permanent-by-neglect, which is &lt;em&gt;why&lt;/em&gt; the vendor container can’t be the default on this platform.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speculative decoding’s scheduler coupling&lt;/strong&gt;: on this build, CPU n-gram speculation disables async scheduling, and the per-step token budget derives from your batch/speculation settings (vLLM says both in boot warnings). Mostly &lt;em&gt;mitigable from your side&lt;/em&gt;: our controlled retest showed that simply setting &lt;code&gt;--max-num-batched-tokens&lt;/code&gt; explicitly eliminated the penalty that sank our first experiment, and &lt;code&gt;ngram_gpu&lt;/code&gt; (present and working on our 0.22-era build) didn’t disable async scheduling at all. What remains open upstream, now upgraded from observation to confirmed defect: n-gram speculation on this build &lt;strong&gt;corrupts structured output&lt;/strong&gt; (control 20/20 clean vs ngram 0/20; dropped token components inside version strings, mutated JSON keys; identical signature from CPU and GPU proposers → shared verify-path bug), and prefix caching × ngram hard-asserts in &lt;code&gt;mamba_mixer2&lt;/code&gt; on the hybrid arch. Before filing, we duplicate-checked the tracker, and both bugs were already known, which is its own lesson: the assert is precisely open PR vllm#46424 (our data extends it from MTP to ngram), and the corruption belongs to the vllm#39273 hybrid-corruption cluster. We also tested vllm#40875’s config-only workaround (&lt;code&gt;prompt_lookup_min=8&lt;/code&gt;): it did not mitigate on this Mamba2 hybrid: corruption changed shape but persisted, 0/20. Our contribution upstream is corroborating data on existing issues, not new tickets, and until that cluster resolves, no n-gram config passes our quality gate regardless of its throughput.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One we owe upstream:&lt;/strong&gt; in our n-gram A/B, temperature-0 output &lt;em&gt;lengths diverged run-to-run&lt;/em&gt; with speculation on (502–1116 tokens) while the control was deterministic (529 every run). Speculative decoding is supposed to preserve the target distribution; numerics on this path apparently aren’t bit-stable on this build. We haven’t filed a reproducer yet; until then treat it as our observation, not a confirmed upstream bug.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;appendix-every-knob-this-post-mentions&quot;&gt;Appendix: every knob this post mentions&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What it actually controls&lt;/th&gt;
&lt;th&gt;If you get it wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--gpu-memory-utilization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fraction of the (unified!) pool pre-allocated for weights + KV&lt;/td&gt;
&lt;td&gt;Too high on a shared box = unswappable reservation = OOM-killer (our 2h40m outage)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-model-len&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Max prompt+output tokens per request; sizes one sequence’s KV claim&lt;/td&gt;
&lt;td&gt;Model-card maxima can reserve absurd KV on 128 GB: compute from the KV math&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-num-seqs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Concurrent sequences scheduled&lt;/td&gt;
&lt;td&gt;High values tax bandwidth-bound decode; also interacts with cudagraph capture sizes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--quantization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Overrides quant-format detection (&lt;code&gt;modelopt&lt;/code&gt; for nvidia ModelOpt checkpoints)&lt;/td&gt;
&lt;td&gt;Compressed-tensors checkpoints auto-detect; passing the flag wrongly breaks loading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--moe-backend&lt;/code&gt; / &lt;code&gt;--linear-backend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kernel backend for MoE / dense layers (&lt;code&gt;auto&lt;/code&gt; = let the oracle pick)&lt;/td&gt;
&lt;td&gt;Forcing the wrong one: crashes or silent garbage; &lt;code&gt;auto&lt;/code&gt; on a cubin-less wheel: &lt;code&gt;no kernel image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Halves KV cost vs fp16&lt;/td&gt;
&lt;td&gt;Model-specific; some models loop or degrade: validate per checkpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--speculative-config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enables spec decoding (method, drafter, k)&lt;/td&gt;
&lt;td&gt;Unsoaked configs crash under load (#41190); n-gram silently changes scheduler behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--enable-auto-tool-choice&lt;/code&gt; + &lt;code&gt;--tool-call-parser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lets agent clients send &lt;code&gt;tool_choice: auto&lt;/code&gt;; parser must match the &lt;em&gt;model family&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Missing: every agent request 400s. Wrong parser: calls stall silently in &lt;code&gt;content&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--reasoning-parser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Splits &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; into &lt;code&gt;reasoning_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing: chain-of-thought leaks into the visible reply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--enforce-eager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disables CUDA graphs&lt;/td&gt;
&lt;td&gt;Was a needed workaround (hybrid-Mamba, ≤v0.20); now just a ~37% decode tax: drop it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--enable-prefix-caching&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;KV reuse for repeated prompt prefixes&lt;/td&gt;
&lt;td&gt;On by default in V1, but some archs/features disable it: check, don’t assume (see #42084)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--enable-chunked-prefill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Interleaves prefill chunks with decode&lt;/td&gt;
&lt;td&gt;Community-reported ~9× MoE slowdown on some SSM+MoE combos: A/B per model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--attention-backend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pins the attention kernel&lt;/td&gt;
&lt;td&gt;Forcing one crashed hybrid models on some builds; current nightlies handle &lt;code&gt;flashinfer&lt;/code&gt; fine: test per image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--mamba-ssm-cache-dtype&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSM state cache precision on hybrid-Mamba models&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float32&lt;/code&gt; is the validated setting in our Nemotron recipe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;sources--receipts&quot;&gt;Sources &amp;amp; receipts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;vLLM team, “vLLM on NVIDIA DGX Spark”: &lt;a href=&quot;https://vllm.ai/blog/2026-06-01-vllm-dgx-spark&quot;&gt;https://vllm.ai/blog/2026-06-01-vllm-dgx-spark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NVIDIA DGX Spark playbooks: &lt;a href=&quot;https://build.nvidia.com/spark/vllm/instructions&quot;&gt;https://build.nvidia.com/spark/vllm/instructions&lt;/a&gt; · &lt;a href=&quot;https://github.com/NVIDIA/dgx-spark-playbooks&quot;&gt;https://github.com/NVIDIA/dgx-spark-playbooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;vLLM PRs referenced: #38126, #39825 (v0.20.0); #35568, #40082 (v0.22.0); #40923 (v0.22.1rc0). Upstream spec-decode IMA issues: #41190, #42084.&lt;/li&gt;
&lt;li&gt;Community playbook with the broadest recipe coverage, the most useful single Spark reference we’ve found, whose Marlin guidance this post extends with checkpoint-format context: &lt;a href=&quot;https://vlaicu.io/posts/dgx-vllm/&quot;&gt;https://vlaicu.io/posts/dgx-vllm/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Community reports illustrating the software-immaturity-vs-hardware-limitation distinction this post draws: a slow-tokens/s thread where the cause is corrected from hardware to an immature kernel stack (&lt;a href=&quot;https://forums.developer.nvidia.com/t/slow-performance-for-redhatai-qwen3-coder-next-nvfp4/363129&quot;&gt;https://forums.developer.nvidia.com/t/slow-performance-for-redhatai-qwen3-coder-next-nvfp4/363129&lt;/a&gt;), and the “SM121 software support is severely lacking” thread (&lt;a href=&quot;https://forums.developer.nvidia.com/t/dgx-spark-sm121-software-support-is-severely-lacking-official-roadmap-needed/357663&quot;&gt;https://forums.developer.nvidia.com/t/dgx-spark-sm121-software-support-is-severely-lacking-official-roadmap-needed/357663&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;AEON-7 DFlash GB10 measurements: &lt;a href=&quot;https://github.com/AEON-7/vllm-dflash&quot;&gt;https://github.com/AEON-7/vllm-dflash&lt;/a&gt; · Z-Lab DFlash: &lt;a href=&quot;https://github.com/z-lab/dflash&quot;&gt;https://github.com/z-lab/dflash&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Checkpoints: &lt;code&gt;unsloth/Qwen3.6-35B-A3B-NVFP4&lt;/code&gt; (deployed), &lt;code&gt;nvidia/Qwen3.6-35B-A3B-NVFP4&lt;/code&gt;, &lt;code&gt;nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4&lt;/code&gt;, &lt;code&gt;nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-NVFP4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bench methodology: single-stream prefill/decode script measuring TTFT, prefill tok/s, decode tok/s from server token counts, with prefix-cache-defeating nonces and forced full-length generation (&lt;code&gt;ignore_eos&lt;/code&gt;), adapted from the vlaicu.io playbook §10.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>