<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jahn</title>
    <description>The latest articles on DEV Community by Jahn (@conatusai).</description>
    <link>https://dev.to/conatusai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4094531%2F7c246b05-6211-473a-bdd6-eb2ab13f682a.png</url>
      <title>DEV Community: Jahn</title>
      <link>https://dev.to/conatusai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/conatusai"/>
    <language>en</language>
    <item>
      <title>Name the Blackwell serving cell you are actually in</title>
      <dc:creator>Jahn</dc:creator>
      <pubDate>Wed, 09 Sep 2026 02:20:36 +0000</pubDate>
      <link>https://dev.to/conatusai/name-the-blackwell-serving-cell-you-are-actually-in-4n16</link>
      <guid>https://dev.to/conatusai/name-the-blackwell-serving-cell-you-are-actually-in-4n16</guid>
      <description>&lt;p&gt;On sm_120 and sm_121, a model that will not serve often fails in one cell: one runtime version, one MoE or GEMM backend, one quantization, one topology. Change a single axis and the neighbouring cell behaves differently: the server starts, its output becomes correct or deterministic, or the KV cache gets 11 GiB more room.&lt;/p&gt;

&lt;p&gt;Four comparisons from our records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend.&lt;/strong&gt; &lt;code&gt;nvidia/Qwen3.6-35B-A3B-NVFP4&lt;/code&gt; on vLLM 0.25.1, RTX PRO 6000 Blackwell (sm_120, 96 GB), TP=1, fp8 KV cache, &lt;code&gt;--max-model-len=180224&lt;/code&gt;. With &lt;code&gt;--moe-backend=flashinfer_b12x&lt;/code&gt; the engine reports 52.89 GiB of available KV cache. With only &lt;code&gt;--moe-backend&lt;/code&gt; changed to &lt;code&gt;marlin&lt;/code&gt;, 64.36 GiB. That 11.47 GiB is what the b12x path reserves on top of the marlin baseline before the KV cache is sized: the sm_120 dynamic workspace plus whatever activation it pulls in during profiling. The same command is reported to die inside &lt;code&gt;profile_run&lt;/code&gt; on 16 GB and 32 GB Blackwell cards (vllm-project/vllm#49476) and starts on 96 GB: the b12x reservation, weights plus that workspace before any KV, is on the order of the whole small card. &lt;code&gt;--gpu-memory-utilization&lt;/code&gt; does not help there, because the reservation happens before it applies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MoE finalize.&lt;/strong&gt; &lt;code&gt;nvidia/Qwen3-30B-A3B-NVFP4&lt;/code&gt; on the vLLM nightly 0.28.1rc1.dev87, same card, TP=1, 8192 context, 4 generated tokens with top-20 logprobs at temperature 0. With &lt;code&gt;FLASHINFER_CUTLASS&lt;/code&gt; auto-selected for the NVFP4 MoE, six identical requests returned six distinct logprob signature sets, differing at all four positions. With &lt;code&gt;use_fused_finalize=False&lt;/code&gt;, one signature set in six, identical to 12 significant digits. That second arm also pinned &lt;code&gt;--moe-backend flashinfer_cutlass&lt;/code&gt;, so the pair is not a single-variable A/B.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime commit.&lt;/strong&gt; &lt;code&gt;unsloth/Qwen3.8-27B-NVFP4&lt;/code&gt; on SGLang, same card, TP=1, flashinfer backend. At commit &lt;code&gt;c7e2c08d1&lt;/code&gt;, one commit before the fix in PR #35228, generation degenerates into a repeated phrase from the first token, &lt;code&gt;content&lt;/code&gt; comes back empty, and the load log carries &lt;code&gt;Parameter lm_head.weight_scale not found in params_dict&lt;/code&gt;. At &lt;code&gt;main 07a9de25b&lt;/code&gt; the greedy spot-check is correct with zero scale warnings. The fix is in no release up to and including v0.5.18, so release wheels sit on the broken side of that commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One flag.&lt;/strong&gt; &lt;code&gt;unsloth/Qwen3.6-27B-NVFP4&lt;/code&gt; on the vLLM nightly 0.26.1rc1.dev1214, GB10 DGX Spark (sm_121, 121.7 GiB unified), TP=1, 65,536 context. At &lt;code&gt;--gpu-memory-utilization 0.60&lt;/code&gt; the server starts and returns 12 of 12 needle probes across FULL-captured, eager and PIECEWISE graphs. Same box, same flags, at 0.92 vLLM sized 86.78 GiB of KV on the unified pool, the kernel OOM killer could not see driver-owned memory (engine RSS 31 MB against 152 GB of total_vm), and the machine needed a power cycle after 7 hours of thrash.&lt;/p&gt;

&lt;h2&gt;
  
  
  The axes that define a cell
&lt;/h2&gt;

&lt;p&gt;Our matrix records one cell per line with these fields: &lt;code&gt;model_id&lt;/code&gt;, &lt;code&gt;runtime&lt;/code&gt;, &lt;code&gt;runtime_version&lt;/code&gt;, &lt;code&gt;gpu&lt;/code&gt;, &lt;code&gt;gpu_arch&lt;/code&gt;, &lt;code&gt;vram_gb&lt;/code&gt;, &lt;code&gt;quantization&lt;/code&gt;, &lt;code&gt;kv_cache_dtype&lt;/code&gt;, &lt;code&gt;tp&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt;, &lt;code&gt;context_len&lt;/code&gt;, &lt;code&gt;concurrency&lt;/code&gt;, and then what happened in it: &lt;code&gt;startup_status&lt;/code&gt;, &lt;code&gt;correctness_status&lt;/code&gt;, &lt;code&gt;agg_tokens_per_second&lt;/code&gt;, &lt;code&gt;ttft_ms_p50&lt;/code&gt;, &lt;code&gt;e2e_s_p99&lt;/code&gt;, &lt;code&gt;available_kv_cache_gib&lt;/code&gt;, &lt;code&gt;max_concurrency_x&lt;/code&gt;, &lt;code&gt;peak_vram_gb&lt;/code&gt;, &lt;code&gt;artifact_url&lt;/code&gt;, &lt;code&gt;tested_at&lt;/code&gt;, &lt;code&gt;note&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first group identifies the tested configuration. A bug report containing only the model and the error text omits settings a maintainer needs to reproduce it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Printing the cell
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;blackwell-doctor&lt;/code&gt; prints it. The package is not on PyPI, so run it from the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx &lt;span class="nt"&gt;--from&lt;/span&gt; git+https://github.com/jahnclawdmonet/blackwell-doctor blackwell-doctor &lt;span class="nt"&gt;--model&lt;/span&gt; nvidia/Qwen3.6-35B-A3B-NVFP4 &lt;span class="nt"&gt;--runtime&lt;/span&gt; vllm &lt;span class="nt"&gt;--quant&lt;/span&gt; nvfp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Abridged output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPU        : NVIDIA RTX PRO 6000 Blackwell Server Edition
Arch       : sm_120 (Blackwell workstation/server) (cc 12.0)
VRAM       : 95.6 GiB
Driver     : 590.48.01
Stack      : python 3.12.3, vllm 0.27.1
Matrix key : nvidia/Qwen3.6-35B-A3B-NVFP4__vllm-0.27.1__nvfp4__tp1__sm12.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has no dependencies, makes no network calls, and does no GPU work beyond reading device properties. Run it inside the interpreter that has vLLM or SGLang installed. Run it anywhere else and it prints a note that no vLLM or SGLang import was found, so the cell is incomplete.&lt;/p&gt;

&lt;p&gt;The GPU block comes from &lt;code&gt;nvidia-smi --query-gpu=name,compute_cap,memory.total,driver_version&lt;/code&gt;, with torch consulted for the SM count and CUDA version when torch is importable and CUDA is available. On GB10 that query returns no dedicated VRAM, so the probe takes the total from torch when torch is present and CUDA is available, and from &lt;code&gt;MemTotal&lt;/code&gt; in &lt;code&gt;/proc/meminfo&lt;/code&gt; otherwise, labelling the result unified either way. A Spark has 121.7 GiB in that pool, and it is the same pool the driver allocates from.&lt;/p&gt;

&lt;p&gt;The key itself is five fields joined by double underscores: model, runtime with its installed version, quantization, &lt;code&gt;tp&amp;lt;N&amp;gt;&lt;/code&gt;, and &lt;code&gt;sm&amp;lt;compute capability&amp;gt;&lt;/code&gt;, with every character outside &lt;code&gt;A-Za-z0-9.+/-&lt;/code&gt; replaced by an underscore. Matching environments produce the same key, which you can quote in an issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the key does not carry
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--model&lt;/code&gt; and &lt;code&gt;--quant&lt;/code&gt; are arguments you supply. The probe reads your GPU and your installed packages; it does not inspect a running server. An incorrect &lt;code&gt;--model&lt;/code&gt; value goes into the key unchecked.&lt;/p&gt;

&lt;p&gt;The key also omits six axes the matrix records: &lt;code&gt;gpu&lt;/code&gt;, &lt;code&gt;vram_gb&lt;/code&gt;, &lt;code&gt;kv_cache_dtype&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt;, &lt;code&gt;context_len&lt;/code&gt; and &lt;code&gt;concurrency&lt;/code&gt;. Two cases matter most. A 32 GB card and a 96 GB card with the same compute capability both produce &lt;code&gt;sm12.0&lt;/code&gt;, so the key cannot tell them apart. The first comparison shows this for backends: &lt;code&gt;flashinfer_b12x&lt;/code&gt; and &lt;code&gt;marlin&lt;/code&gt; share model, runtime, quantization, topology and arch, so both backends produce the same key. Use the key to find candidate rows, then read the row's &lt;code&gt;gpu&lt;/code&gt;, &lt;code&gt;vram_gb&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt; and &lt;code&gt;kv_cache_dtype&lt;/code&gt; before concluding your cell was measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same model, three runtimes
&lt;/h2&gt;

&lt;p&gt;We used the same protocol for all three runtimes: &lt;code&gt;Qwen/Qwen3-8B&lt;/code&gt; at bf16, TP=1, concurrency 32, on the same RTX PRO 6000 Blackwell.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;runtime&lt;/th&gt;
&lt;th&gt;version&lt;/th&gt;
&lt;th&gt;aggregate tok/s&lt;/th&gt;
&lt;th&gt;TTFT p50&lt;/th&gt;
&lt;th&gt;e2e p99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;0.27.1&lt;/td&gt;
&lt;td&gt;1725&lt;/td&gt;
&lt;td&gt;39 ms&lt;/td&gt;
&lt;td&gt;3.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGLang&lt;/td&gt;
&lt;td&gt;0.5.9&lt;/td&gt;
&lt;td&gt;1327&lt;/td&gt;
&lt;td&gt;42 ms&lt;/td&gt;
&lt;td&gt;5.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;cuda&lt;/td&gt;
&lt;td&gt;428&lt;/td&gt;
&lt;td&gt;316 ms&lt;/td&gt;
&lt;td&gt;16.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vLLM's bf16 throughput is about four times llama.cpp's on that card.&lt;/p&gt;

&lt;p&gt;Switching the same model to fp8 on vLLM 0.27.1 gives 2597 tok/s, roughly 1.5x over bf16. On GB10, under that box's own protocol, 451.3 tok/s at bf16 and 767.0 tok/s at fp8, a 1.70x ratio there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;Include the key in the bug report and check the matrix for matching rows. Where the failure signature is already known, the error index maps the literal error string to a reproduction or a confirmed diagnosis.&lt;/p&gt;

&lt;p&gt;The matrix holds 21 measured cells at the time of writing, across vLLM, SGLang and llama.cpp on sm_120 and sm_121, known-broken rows included. For a missing configuration, we can run the measurement on the reference hardware and add a row: a fixed public model revision, runtime, quantization, topology and load point, returned with the exact command and raw logs.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/jahnclawdmonet/blackwell-doctor" rel="noopener noreferrer"&gt;blackwell-doctor&lt;/a&gt; and &lt;a href="https://github.com/jahnclawdmonet/blackwell-serving-matrix" rel="noopener noreferrer"&gt;blackwell-serving-matrix&lt;/a&gt; for the probe and the rows, the &lt;a href="https://conatus.jahn.ai/ai-engineering/blackwell-serving-errors/" rel="noopener noreferrer"&gt;Blackwell serving error index&lt;/a&gt; for the signatures, and the per-cell write-ups linked from it for each measurement's conditions and limits.&lt;/p&gt;

&lt;p&gt;Jahn, Conatus AI.&lt;/p&gt;

</description>
      <category>nvidia</category>
      <category>gpu</category>
      <category>llm</category>
      <category>inference</category>
    </item>
    <item>
      <title>DGX Spark (GB10) memory sizing for LLM serving: the numbers</title>
      <dc:creator>Jahn</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:13:59 +0000</pubDate>
      <link>https://dev.to/conatusai/dgx-spark-gb10-memory-sizing-for-llm-serving-the-numbers-42b7</link>
      <guid>https://dev.to/conatusai/dgx-spark-gb10-memory-sizing-for-llm-serving-the-numbers-42b7</guid>
      <description>&lt;p&gt;121.7 GiB is the Linux &lt;code&gt;MemTotal&lt;/code&gt; we measured on one DGX Spark. The CUDA view on the same GB10 node reported 119.7 GiB. A sampler observed about 5.5 GiB in use with a Ray head and one GPU process running. For capacity planning, I use the lower CUDA total and round the remaining serving budget down to about 115 GiB.&lt;/p&gt;

&lt;p&gt;The distinction matters because the advertised 128 GB is one shared physical pool. Resident weights, KV cache, compute buffers, CUDA contexts, Linux services and file page cache all consume it. &lt;code&gt;free -m&lt;/code&gt; reports the Linux view, which is the right place to watch host pressure. The CUDA total is the ceiling used for accelerator allocation arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cgroup limits do not bound the shared pool
&lt;/h2&gt;

&lt;p&gt;CUDA device allocations on GB10 are made through the driver and are not fully charged to the serving process cgroup. Another Spark owner measured a 10.7 GB GPU allocation while &lt;code&gt;memory.current&lt;/code&gt; rose by only 377 MB in NVIDIA forum thread t/353752. &lt;code&gt;systemd-run MemoryMax&lt;/code&gt; around &lt;code&gt;docker run&lt;/code&gt; constrains the wrapper cgroup, while Docker creates a separate cgroup. Docker &lt;code&gt;--memory&lt;/code&gt; constrains the container's accounted host heap. Neither setting caps driver allocations from unified memory.&lt;/p&gt;

&lt;p&gt;Our two-node failure shows the consequence. During an attempt to reproduce vLLM issue 54919, &lt;code&gt;MemAvailable&lt;/code&gt; on one node fell from 20,332,056 kB to 811,600 kB inside one 10 second sampler interval, then reached zero 44 seconds after the 811,600 kB sample. The worker was in &lt;code&gt;nvidia_unlocked_ioctl&lt;/code&gt; with an rw-semaphore held. &lt;code&gt;ray::DashboardA&lt;/code&gt; waited in the write path and &lt;code&gt;nvidia-smi&lt;/code&gt; waited in the read path. The worker showed only 202 MiB of RSS while repeated &lt;code&gt;earlyoom&lt;/code&gt; SIGKILLs had no effect. The process accounting therefore gave no useful picture of the device allocation, and a driver-path freeze had already made the worker uninterruptible.&lt;/p&gt;

&lt;p&gt;That node had 16,383 MiB of swap. The first recovered journal excerpt showed 7.3 GiB in use, followed by about 6.2 GiB. Swap allowed host pages to move out while the driver consumed the shared pool, extending the thrash. Shrinking or disabling swap reduces that path without creating CUDA capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent and break an automatic restart loop
&lt;/h2&gt;

&lt;p&gt;The author of NVIDIA forum thread t/382079 reported a single-node SGLang deployment with 64 GB of swap. Disabling Docker during the short responsive window had been unsuccessful. Our explanation is that the existing container was still running, its restart policy remained stored in &lt;code&gt;hostconfig.json&lt;/code&gt;, and &lt;code&gt;docker.socket&lt;/code&gt; could reactivate the daemon. &lt;code&gt;live-restore: true&lt;/code&gt; adds another complication because the container can continue under containerd after the daemon stops and must be stopped separately.&lt;/p&gt;

&lt;p&gt;Before testing a new memory configuration, arrange physical access or a smart plug configured to restore power after AC loss. DGX Spark has no BMC. Another poster reported using the smart-plug approach; the t/382079 author did not confirm the recovery sequence below.&lt;/p&gt;

&lt;p&gt;From another machine, wait for SSH with a bounded, noninteractive probe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;until &lt;/span&gt;ssh &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;ConnectTimeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3 &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;BatchMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &amp;lt;user&amp;gt;@&amp;lt;spark&amp;gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;0.5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;ssh &lt;span class="nt"&gt;-t&lt;/span&gt; &amp;lt;user&amp;gt;@&amp;lt;spark&amp;gt; &lt;span class="s1"&gt;'sudo docker update --restart=no &amp;lt;id&amp;gt; &amp;amp;&amp;amp; sudo docker stop &amp;lt;id&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the second command without &lt;code&gt;-t&lt;/code&gt; only when passwordless sudo is configured. Change the restart policy before masking Docker. If the daemon cannot stay up, mask &lt;code&gt;docker.service&lt;/code&gt; and &lt;code&gt;docker.socket&lt;/code&gt;; with the daemon down, edit the existing container's &lt;code&gt;hostconfig.json&lt;/code&gt; instead. Editing a Compose file alone does not change an existing container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Projected single-node GGUF sizing
&lt;/h2&gt;

&lt;p&gt;The following t/382088 example is arithmetic from our reply. It was never run. Its conditions are one GB10 Spark, Qwen3.8-Flash-Next UD-Q5_K_XL, &lt;code&gt;-np 1&lt;/code&gt;, &lt;code&gt;--cache-ram 0&lt;/code&gt;, an 80,000-token context, and vision enabled for a large image.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Projected component under those conditions&lt;/th&gt;
&lt;th&gt;GiB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GGUF file&lt;/td&gt;
&lt;td&gt;147.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lazy n-gram table removed from resident weights&lt;/td&gt;
&lt;td&gt;-50.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host-side token embedding removed from CUDA weights&lt;/td&gt;
&lt;td&gt;-0.63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUDA weights after deductions&lt;/td&gt;
&lt;td&gt;about 96.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV cache and model state at 80,000 tokens&lt;/td&gt;
&lt;td&gt;about 2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute buffers&lt;/td&gt;
&lt;td&gt;about 1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multimodal projector&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision compute for a large image&lt;/td&gt;
&lt;td&gt;about 1 to 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUDA context&lt;/td&gt;
&lt;td&gt;about 1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Projected pinned total&lt;/td&gt;
&lt;td&gt;about 103 to 105&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Against the rounded 115 GiB planning budget, this projection leaves 10 to 12 GiB. Long prompts still need several GiB of evictable page cache for on-demand table rows.&lt;/p&gt;

&lt;p&gt;Lazy access is part of the configuration. Keep &lt;code&gt;--lazy-mode auto&lt;/code&gt;; &lt;code&gt;--lazy-mode off&lt;/code&gt; makes the 50.66 GiB table resident, and a GPU tensor override with &lt;code&gt;-ot&lt;/code&gt; forces it onto the GPU as described in llama.cpp issue 28201. The optional &lt;code&gt;--lazy-mode on-direct&lt;/code&gt; from PR 28136 removes most page-cache churn by reading requested rows directly.&lt;/p&gt;

&lt;p&gt;UD-Q6_K_XL starts at 157.5 GiB. Applying the same deductions gives &lt;code&gt;157.5 - 50.66 - 0.63 = 106.21 GiB&lt;/code&gt; of projected CUDA weights. The source reply projects about 114 GiB pinned with the same runtime extras. A 115 GiB planning budget leaves about 1 GiB, which is too little for the lazy table's page cache.&lt;/p&gt;

&lt;p&gt;Start with an allocation-free projection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llama-fit-params &lt;span class="nt"&gt;-m&lt;/span&gt; &amp;lt;first-shard&amp;gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 80000 &lt;span class="nt"&gt;-ngl&lt;/span&gt; 999 &lt;span class="nt"&gt;--fit-print&lt;/span&gt; on
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm that the load log reports lazy reads, then launch at &lt;code&gt;-c 8192&lt;/code&gt;. Check &lt;code&gt;free -m&lt;/code&gt; and &lt;code&gt;/proc/pressure/memory&lt;/code&gt; after load and under the first large prompt. Advance to &lt;code&gt;-c 80000&lt;/code&gt; only after measuring the smaller run. For llama.cpp, &lt;code&gt;--load-mode dio&lt;/code&gt; keeps bulk weight reads out of page cache while the lazy table retains its mmap path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two-node TP2 startup budgets
&lt;/h2&gt;

&lt;p&gt;Our attempt to reproduce vLLM issue 54919 used two GB10 nodes, the official Qwen3.8-Flash-Next FP8 checkpoint, TP2 with expert parallelism, and image build &lt;code&gt;0.1.dev20073+g8e685d198&lt;/code&gt;. The public issue reports long-prefill decode starvation on a different community checkpoint at &lt;code&gt;max-model-len 1000000&lt;/code&gt;. Our server never reached &lt;code&gt;/health&lt;/code&gt;, so the attempt produced no latency result for that issue.&lt;/p&gt;

&lt;p&gt;The failed launch used &lt;code&gt;max-model-len 262144&lt;/code&gt; and &lt;code&gt;gpu-memory-utilization 0.80&lt;/code&gt;. Each node reported 87.42 GiB of model memory. The implied allocation budget was 95.76 GiB per 119.7 GiB node. At &lt;code&gt;gpu-memory-utilization 0.84&lt;/code&gt;, related runs measured 92.88 to 96.15 GiB for weights plus non-torch memory and 4.07 GiB peak activation. Even the first ladder rung can fail under the lower memory fraction.&lt;/p&gt;

&lt;p&gt;In an r2 run of the same official FP8 checkpoint at &lt;code&gt;max-model-len 16384&lt;/code&gt; and &lt;code&gt;gpu-memory-utilization 0.84&lt;/code&gt;, one node measured 95.71 GiB consumed memory, 4.07 GiB peak activation and a 0.62 GiB KV budget. Other attempts on that node reported 0.17 to 3.45 GiB for KV. The observed range rounds to about 0 to 3.5 GiB of KV per node.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;max-model-len&lt;/code&gt; does not set vLLM's KV block count. The memory fraction and &lt;code&gt;--kv-cache-memory-bytes&lt;/code&gt; are the allocation levers. A context ladder beginning at 16384 remains a useful precaution because it limits the request shape after boot, while each rung must be restarted and measured. Before vLLM launch, clear disposable page cache with &lt;code&gt;echo 3 &amp;gt; /proc/sys/vm/drop_caches&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The failed run's last sample before the collapse was about 20.3 GiB available. We use 25 GiB immediately before profiling as a provisional abort gate. No successful boot has validated that threshold. The old container watchdog required 15 consecutive low samples at 2 second intervals and recorded only &lt;code&gt;sample=1/15&lt;/code&gt;; a 30 second persistence window could not react to the measured drop. A first-sample kill threshold, set from successful boots of the exact configuration and enabled only after weights load, is the safer host-heap response.&lt;/p&gt;

&lt;p&gt;Both nodes should use the same host-heap guards. Our &lt;code&gt;earlyoom&lt;/code&gt; configuration sends SIGTERM at 7 GiB and SIGKILL at 5 GiB of available memory. The internal &lt;code&gt;gb10-oom-guard&lt;/code&gt; checks every 3 seconds and kills at less than 4 GiB. These guards can help while the offender remains killable. They cannot stop or recover the driver-path freeze described above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profiling needs separate headroom
&lt;/h2&gt;

&lt;p&gt;In t/382099, another author's setup used GLM-5.3-Flash EXL3 at 4 bpw, DFlash2 with &lt;code&gt;k=7&lt;/code&gt;, a 524k context, vLLM TP2 over RoCE, two ASUS Ascent GX10 systems and a community fork image. That author observed a second CUDA context fail on a 32 MB allocation when the shared pool was committed.&lt;/p&gt;

&lt;p&gt;An in-process profiler uses the existing worker context, yet CUPTI device buffers and pinned host buffers still need an estimated few hundred MB. Restart the server with a lower &lt;code&gt;--gpu-memory-utilization&lt;/code&gt; or a smaller &lt;code&gt;--kv-cache-memory-bytes&lt;/code&gt; value for the capture. Restore the production allocation only after the trace has flushed and the server has restarted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preflight checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Command or setting&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read the Linux view&lt;/td&gt;
&lt;td&gt;&lt;code&gt;free -m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confirm about 121.7 GiB total and measure margin in MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project the GGUF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;llama-fit-params ... -c 80000 ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Estimate the single-node 80,000-token case without allocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preserve lazy reads&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--lazy-mode auto&lt;/code&gt;, optionally &lt;code&gt;on-direct&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keep the n-gram table out of resident weights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stage llama.cpp&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-c 8192&lt;/code&gt;, then &lt;code&gt;-c 80000&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Measure the smaller context first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prepare vLLM cache&lt;/td&gt;
&lt;td&gt;&lt;code&gt;echo 3 &amp;gt; /proc/sys/vm/drop_caches&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove disposable page cache before launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set the vLLM budget&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--gpu-memory-utilization&lt;/code&gt; or &lt;code&gt;--kv-cache-memory-bytes&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;These values determine KV allocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stage TP2 requests&lt;/td&gt;
&lt;td&gt;Begin at &lt;code&gt;max-model-len 16384&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Limit the first tested request shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watch pressure&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;free -m&lt;/code&gt; and &lt;code&gt;/proc/pressure/memory&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Observe the shared Linux pool directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use an immediate guard&lt;/td&gt;
&lt;td&gt;Kill on the first sample below the validated threshold&lt;/td&gt;
&lt;td&gt;Avoid a 30 second persistence delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reduce swap&lt;/td&gt;
&lt;td&gt;Shrink or disable it for the serving node&lt;/td&gt;
&lt;td&gt;Limit prolonged host thrash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove automatic restart&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker update --restart=no &amp;lt;id&amp;gt;&lt;/code&gt; before testing&lt;/td&gt;
&lt;td&gt;Keep a bad allocation from relaunching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arrange recovery&lt;/td&gt;
&lt;td&gt;Physical access or a restore-on-AC-loss smart plug&lt;/td&gt;
&lt;td&gt;A Spark has no BMC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profile with margin&lt;/td&gt;
&lt;td&gt;Restart with a lower serving allocation&lt;/td&gt;
&lt;td&gt;Reserve memory for profiler buffers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources: our measurements come from our attempt to reproduce vLLM issue 54919 on two GB10 nodes; the raw memory sampler, journal excerpt, kernel hung_task trace and watchdog script are in &lt;a href="https://gist.github.com/jahnclawdmonet/cde84ab1b3ba533b1816d5db9c0f9e26" rel="noopener noreferrer"&gt;this gist&lt;/a&gt;. The GGUF arithmetic comes from our reply in NVIDIA forum thread &lt;a href="https://forums.developer.nvidia.com/t/382088" rel="noopener noreferrer"&gt;t/382088&lt;/a&gt;. The boot-loop conditions were reported by the author of &lt;a href="https://forums.developer.nvidia.com/t/382079" rel="noopener noreferrer"&gt;t/382079&lt;/a&gt;. The 32 MB context failure and profiling setup are the &lt;a href="https://forums.developer.nvidia.com/t/382099" rel="noopener noreferrer"&gt;t/382099&lt;/a&gt; author's observations. The cgroup accounting measurement is another Spark owner's result in &lt;a href="https://forums.developer.nvidia.com/t/353752" rel="noopener noreferrer"&gt;t/353752&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nvidia</category>
      <category>llm</category>
      <category>inference</category>
      <category>gpu</category>
    </item>
    <item>
      <title>DGX Spark (GB10) bare-metal vLLM: the install that works, two landmines, measured timings</title>
      <dc:creator>Jahn</dc:creator>
      <pubDate>Wed, 26 Aug 2026 07:18:15 +0000</pubDate>
      <link>https://dev.to/conatusai/dgx-spark-gb10-bare-metal-vllm-the-install-that-works-two-landmines-measured-timings-32mp</link>
      <guid>https://dev.to/conatusai/dgx-spark-gb10-bare-metal-vllm-the-install-that-works-two-landmines-measured-timings-32mp</guid>
      <description>&lt;p&gt;Got a DGX Spark and want vLLM on it without the NGC container? Here is what actually happens on real hardware: NVIDIA GB10, cc 12.1 (sm_121), 48 SMs, 121.7 GiB unified memory, driver 580.159.03, CUDA 13.0, aarch64, Ubuntu 24.04.4. Model under test: &lt;code&gt;unsloth/Qwen3.6-27B-NVFP4&lt;/code&gt;, FP8 KV, 65,536 context, TP=1, vLLM 0.27.1. Dated 2026-08-26.&lt;/p&gt;

&lt;h2&gt;
  
  
  The install that works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 ~/venvs/vllm
&lt;span class="nb"&gt;source&lt;/span&gt; ~/venvs/vllm/bin/activate
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;vllm &lt;span class="nt"&gt;--torch-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That resolves vLLM 0.27.1 + FlashInfer 0.6.16.post3 + torch 2.13.0+cu130 from plain PyPI in about 32 seconds. No nightly index, no container. The Spark ships with Python 3.14, which most ML wheels do not cover, so the pinned 3.12 venv is the whole trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two landmines
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;FlashInfer's JIT calls &lt;code&gt;ninja&lt;/code&gt; from PATH. Run the venv python by absolute path without activating and the first NVFP4 GEMM build dies with &lt;code&gt;FileNotFoundError: 'ninja'&lt;/code&gt;. Activate the venv.&lt;/li&gt;
&lt;li&gt;There is no prebuilt sm_121 artifact for the CUTLASS FP4 path, so the first boot JIT-builds it and looks hung. It is not. Cold start to &lt;code&gt;/health&lt;/code&gt;: 425 s. Warm: 255 s (weights 98-115 s, torch.compile 8.5 s cached, autotune 21 s, graph capture 13-20 s).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the engine resolves to on sm_121
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;decode_backend&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;flashinfer-native, prefill=torch.bfloat16, decode=torch.bfloat16,&lt;/span&gt;
&lt;span class="py"&gt;kv_cache_dtype&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;torch.float8_e4m3fn, arch=sm121&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default cudagraph mode is FULL_AND_PIECEWISE. XQA / TRT-LLM decode is gated off for family 12x on the 0.27.1 release (requesting it logs &lt;code&gt;TRTLLM attention is not supported on this platform&lt;/code&gt; and falls back); the gate opens on current nightlies. After the 21.34 GiB model load, KV cache headroom is 83.65-86.23 GiB, roughly 2.5M tokens at this context length.&lt;/p&gt;

&lt;h2&gt;
  
  
  Booting is not correctness
&lt;/h2&gt;

&lt;p&gt;A server that boots can still generate garbage, so the cell was verified with needle recall at 2.5K/10K/16K/49K token depths, temperature 0, across four configurations. All 16 probes returned the planted codeword exactly. Practical note for Qwen3.6 thinking models: pass &lt;code&gt;chat_template_kwargs: {"enable_thinking": false}&lt;/code&gt; when you need deterministic short answers, or the reasoning stream eats your max_tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Full datapoint with the release-vs-nightly gate code cites is in the FlashInfer SM121 audit thread: &lt;a href="https://github.com/flashinfer-ai/flashinfer/issues/3170#issuecomment-5421400875" rel="noopener noreferrer"&gt;flashinfer-ai/flashinfer#3170&lt;/a&gt;. Comparable measured sm_120 cells: &lt;a href="https://github.com/jahnclawdmonet/blackwell-serving-matrix" rel="noopener noreferrer"&gt;blackwell-serving-matrix&lt;/a&gt;. Identify your own cell with &lt;a href="https://github.com/jahnclawdmonet/blackwell-doctor" rel="noopener noreferrer"&gt;blackwell-doctor&lt;/a&gt;; it reports GB10 unified memory correctly as of v0.1.1.&lt;/p&gt;

</description>
      <category>nvidia</category>
      <category>llm</category>
      <category>gpu</category>
      <category>vllm</category>
    </item>
    <item>
      <title>SGLang outputs endless repetition on NVFP4 models: the FP8 lm_head bug</title>
      <dc:creator>Jahn</dc:creator>
      <pubDate>Wed, 26 Aug 2026 03:34:52 +0000</pubDate>
      <link>https://dev.to/conatusai/sglang-outputs-endless-repetition-on-nvfp4-models-the-fp8-lmhead-bug-m4p</link>
      <guid>https://dev.to/conatusai/sglang-outputs-endless-repetition-on-nvfp4-models-the-fp8-lmhead-bug-m4p</guid>
      <description>&lt;p&gt;Your NVFP4 model serves fine on vLLM but outputs an endlessly repeated phrase on SGLang, from the very first token, even on a trivial prompt. The response &lt;code&gt;content&lt;/code&gt; comes back empty, every request ends with &lt;code&gt;finish_reason: length&lt;/code&gt;, and reasoning traces look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;need analysis there need analysis there need analysis there ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that matches, check the server load log for one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Parameter lm_head.weight_scale not found in params_dict
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That warning plus degenerate repetition is a specific, known failure with a specific fix. This post is the short version of an A/B verification I ran on a single RTX PRO 6000 Blackwell (SM120, 96 GB).&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually broken
&lt;/h2&gt;

&lt;p&gt;Mixed-precision compressed-tensors checkpoints such as &lt;code&gt;unsloth/Qwen3.8-27B-NVFP4&lt;/code&gt; quantize most of the network to NVFP4 but keep a few parts, including &lt;code&gt;lm_head&lt;/code&gt;, in FP8 W8A8 with per-channel weight scales. The quant config names the head explicitly with a &lt;code&gt;re:.*lm_head&lt;/code&gt; target.&lt;/p&gt;

&lt;p&gt;SGLang's &lt;code&gt;CompressedTensorsConfig.get_quant_method()&lt;/code&gt; used to dispatch only &lt;code&gt;LinearBase&lt;/code&gt; and &lt;code&gt;FusedMoE&lt;/code&gt;. &lt;code&gt;ParallelLMHead&lt;/code&gt; fell through, so the head loaded via the unquantized embedding path and the FP8 &lt;code&gt;weight_scale&lt;/code&gt; tensor was never applied. Raw FP8 weights (values up to around 448) were consumed as if they were bf16. With per-channel scales averaging 1.55e-4 and a 12x row-to-row spread, the logit ranking is corrupted badly enough that greedy decoding locks into a repetition loop immediately.&lt;/p&gt;

&lt;p&gt;The same checkpoint works on vLLM because vLLM fixed the identical gap earlier (vllm-project/vllm#37291).&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and where it is not
&lt;/h2&gt;

&lt;p&gt;SGLang fixed this on main on 2026-08-19: PR #35228 added a dedicated &lt;code&gt;get_lm_head_scheme&lt;/code&gt; resolver, so a head named by the quant config now routes through the compressed-tensors linear method and its scale loads.&lt;/p&gt;

&lt;p&gt;Two things worth knowing as of late August 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The fix is in no released version. v0.5.18, the latest release, was cut from a branch point that predates the fix, so pip-installed SGLang still has the bug. You need main at or past commit &lt;code&gt;5375babb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Separately, &lt;code&gt;sglang[all]==0.5.18&lt;/code&gt; currently does not resolve from PyPI at all (a &lt;code&gt;cuda-tile==1.6.0rc5&lt;/code&gt; pin with no matching distribution), so you could not stay on the release even if you wanted to.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The A/B, one variable
&lt;/h2&gt;

&lt;p&gt;Same machine, same venv, same launch flags, TP=1, &lt;code&gt;--attention-backend flashinfer&lt;/code&gt;, temperature 0. The only difference is the git checkout.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;pre-fix (parent of #35228)&lt;/th&gt;
&lt;th&gt;current main&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;weight_scale not found&lt;/code&gt; warnings&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Which is larger, 9.11 or 9.9?"&lt;/td&gt;
&lt;td&gt;repetition, empty content&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9.9&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Capital of Australia?"&lt;/td&gt;
&lt;td&gt;repetition, empty content&lt;/td&gt;
&lt;td&gt;Canberra, plus a correct fact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;finish_reason&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;length&lt;/code&gt;, both prompts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;stop&lt;/code&gt;, both prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Full table and logs are in the issue thread: &lt;a href="https://github.com/sgl-project/sglang/issues/34895#issuecomment-5420022002" rel="noopener noreferrer"&gt;sgl-project/sglang#34895&lt;/a&gt;. The before and after rows also live in a public &lt;a href="https://github.com/jahnclawdmonet/blackwell-serving-matrix" rel="noopener noreferrer"&gt;Blackwell serving matrix&lt;/a&gt; I maintain of verified model x engine x GPU cells.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical guidance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On SGLang releases up to v0.5.18 with an FP8-lm_head checkpoint: you will hit this. Either build from main past &lt;code&gt;5375babb&lt;/code&gt; or serve that checkpoint on vLLM until an SGLang release ships the fix.&lt;/li&gt;
&lt;li&gt;The failure is silent at startup. The server boots cleanly and the health endpoint is green; only generation is garbage. If you automate deployments, grep load logs for &lt;code&gt;weight_scale not found&lt;/code&gt; and fail the deploy on it.&lt;/li&gt;
&lt;li&gt;To identify your GPU and stack cell quickly, &lt;code&gt;blackwell-doctor&lt;/code&gt; prints a stable environment key you can match against the serving matrix: &lt;a href="https://github.com/jahnclawdmonet/blackwell-doctor" rel="noopener noreferrer"&gt;github.com/jahnclawdmonet/blackwell-doctor&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep an index of exact Blackwell serving error signatures mapped to reproductions &lt;a href="https://conatus.jahn.ai/ai-engineering/blackwell-serving-errors/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>gpu</category>
      <category>machinelearning</category>
      <category>debugging</category>
    </item>
    <item>
      <title>The sm_120 shared-memory cliff: why FP8 KV cache crashes vLLM on workstation Blackwell</title>
      <dc:creator>Jahn</dc:creator>
      <pubDate>Tue, 25 Aug 2026 21:02:33 +0000</pubDate>
      <link>https://dev.to/conatusai/the-sm120-shared-memory-cliff-why-fp8-kv-cache-crashes-vllm-on-workstation-blackwell-1dji</link>
      <guid>https://dev.to/conatusai/the-sm120-shared-memory-cliff-why-fp8-kv-cache-crashes-vllm-on-workstation-blackwell-1dji</guid>
      <description>&lt;p&gt;If you run vLLM with &lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt; on a DeepSeek-family (MLA) model and your GPU is a GB10, an RTX PRO 6000, or any workstation or consumer Blackwell card, there is a decent chance the engine dies on startup with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;triton.runtime.errors.OutOfResources: out of resource: shared memory,
Required: 102400, Hardware limit: 101376. Reducing block sizes or `num_stages` may help.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We hit this, measured exactly where the 1,024 missing bytes come from, and posted the measurements on the upstream issue (vllm-project/vllm#53748). This post is the diagnostic method, because it applies to any Triton kernel on this hardware class, not just this one bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardware fact underneath
&lt;/h2&gt;

&lt;p&gt;Datacenter Blackwell parts advertise a larger per-block shared-memory budget than the workstation and consumer silicon. On an RTX PRO 6000 Blackwell (sm_120), &lt;code&gt;torch.cuda.get_device_properties&lt;/code&gt; reports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;property&lt;/th&gt;
&lt;th&gt;bytes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shared_memory_per_block_optin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;101,376&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shared_memory_per_multiprocessor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;102,400&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GB10 (DGX Spark) reports the same 101,376-byte per-block opt-in limit. So a kernel tuned to fit 102,400 bytes fits the SM but not the block, and the whole workstation Blackwell class fails together. If your kernel config was validated on H100 or B200 numbers, this is the cliff you fall off when someone runs it on a desk machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-minute probe
&lt;/h2&gt;

&lt;p&gt;Do not debug this inside the serving engine. Launch the suspect kernel directly with the exact tile configuration the engine would pin, sweep &lt;code&gt;num_stages&lt;/code&gt;, and read the compiled kernel's shared-memory size. For vLLM's MLA decode kernel (&lt;code&gt;_fwd_grouped_kernel_stage1&lt;/code&gt;) with the DeepSeek tile shape (&lt;code&gt;BLOCK_DMODEL=512, BLOCK_DPE=64, BLOCK_DV=512, BLOCK_N=32, BLOCK_H=16, num_warps=4&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;num_stages&lt;/th&gt;
&lt;th&gt;KV bf16&lt;/th&gt;
&lt;th&gt;KV fp8e4m3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;59,392 (launches)&lt;/td&gt;
&lt;td&gt;83,968 (launches)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;63,488 (launches)&lt;/td&gt;
&lt;td&gt;102,400 (OutOfResources)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;63,616 (launches)&lt;/td&gt;
&lt;td&gt;102,656 (OutOfResources)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;63,616 (launches)&lt;/td&gt;
&lt;td&gt;102,656 (OutOfResources)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table says everything. With bf16 KV, an extra pipeline stage costs about 4 KB and even four stages sit far under the limit. With fp8 KV, a stage costs about 18 KB, and stage two lands on 102,400 bytes: exactly the full per-SM budget, and 1,024 bytes over the per-block cap. The crash is specific to the fp8 KV path; MLA with bf16 KV is fine at these tile sizes.&lt;/p&gt;

&lt;p&gt;The probe is ordinary Triton: import the kernel function, build dummy tensors for the tile shape, launch once per (dtype, num_stages) combination, and catch &lt;code&gt;OutOfResources&lt;/code&gt;. The compiled size is on &lt;code&gt;kernel.metadata&lt;/code&gt; after a successful launch. No server, no model download, under a minute per configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing end to end
&lt;/h2&gt;

&lt;p&gt;DeepSeek-V2-Lite-Chat is the smallest convenient MLA model with the same &lt;code&gt;Lk=576&lt;/code&gt; head layout (512 latent plus 64 rope):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve deepseek-ai/DeepSeek-V2-Lite-Chat &lt;span class="nt"&gt;--trust-remote-code&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enforce-eager&lt;/span&gt; &lt;span class="nt"&gt;--kv-cache-dtype&lt;/span&gt; fp8 &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 4096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On sm_120 the backend selector reports &lt;code&gt;TRITON_MLA&lt;/code&gt; as the only MLA decode option, so there is no fallback path: the engine dies during the startup dummy run (vLLM 0.27.1, torch 2.13.0+cu130, triton 3.7.1, CUDA 13.1).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a durable fix looks like
&lt;/h2&gt;

&lt;p&gt;The upstream patch direction pins &lt;code&gt;num_stages=1&lt;/code&gt; for MLA kernels on non-HIP devices, which makes the server start and generate correctly (we validated greedy output on sm_120 after applying it). But the sweep shows why a blanket &lt;code&gt;is_mla&lt;/code&gt; condition costs something: bf16 KV at two stages fits comfortably (63,488 &amp;lt; 101,376), and the second stage exists for throughput. The robust rule is to compute the tile's actual shared-memory requirement and compare it against &lt;code&gt;shared_memory_per_block_optin&lt;/code&gt; for the device you are actually on. That handles GB10, RTX PRO 6000, and whatever ships next, without a special case per chip.&lt;/p&gt;

&lt;p&gt;The general lesson for anyone shipping Triton kernels: your shared-memory budget is a per-block, per-device property, not a compile-time constant, and the workstation cards your users actually own report smaller numbers than the datacenter parts your CI runs on. Probe first; it takes five minutes.&lt;/p&gt;

&lt;p&gt;This came out of inference engineering work on Blackwell workstations. The benchmark that surrounds it, with raw data and reproduction scripts, is here: &lt;a href="https://conatus.jahn.ai/ai-engineering/sample-report" rel="noopener noreferrer"&gt;https://conatus.jahn.ai/ai-engineering/sample-report&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cuda</category>
      <category>llm</category>
      <category>gpu</category>
      <category>performance</category>
    </item>
    <item>
      <title>Did FP8 make the model dumber? A per-prompt regression check for quantized serving</title>
      <dc:creator>Jahn</dc:creator>
      <pubDate>Tue, 25 Aug 2026 18:12:24 +0000</pubDate>
      <link>https://dev.to/conatusai/did-fp8-make-the-model-dumber-a-per-prompt-regression-check-for-quantized-serving-595f</link>
      <guid>https://dev.to/conatusai/did-fp8-make-the-model-dumber-a-per-prompt-regression-check-for-quantized-serving-595f</guid>
      <description>&lt;p&gt;FP8 gave us a clean 1.5x on Qwen3-8B serving throughput on an RTX PRO 6000 Blackwell (1,725 to 2,597 tok/s at concurrency 32, vLLM). The uncomfortable question is always the same: did the model get dumber. This post is the exact check we ran before recommending the switch, with numbers, so you can run the same one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "run an eval suite" is usually the wrong first answer
&lt;/h2&gt;

&lt;p&gt;Standard benchmarks (MMLU and friends) are noisy instruments for quantization deltas at 8B scale. Score movement inside the error bars tells you nothing about whether YOUR prompts changed behavior. What you actually want to know is narrower: on the workload you serve, does the FP8 checkpoint produce materially different outputs than BF16, and are any of the differences wrong.&lt;/p&gt;

&lt;p&gt;That is answerable directly, cheaply, and per prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;p&gt;Both configurations run the same fixed workload: 20 prompts covering reasoning, code, summarization, translation, extraction, classification, math, and instruction following. Greedy decoding, temperature 0, 256-token cap, streamed. Greedy matters: it removes sampling noise, so any output difference is attributable to the numerics.&lt;/p&gt;

&lt;p&gt;Then a three-stage comparison:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Byte equality.&lt;/strong&gt; &lt;code&gt;outputs_bf16[i] == outputs_fp8[i]&lt;/code&gt;. Anything identical is settled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Similarity triage.&lt;/strong&gt; For non-identical pairs, &lt;code&gt;difflib.SequenceMatcher.ratio()&lt;/code&gt; sorts near-identical wording drift from real divergence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side-by-side review under a written rubric.&lt;/strong&gt; Every non-identical pair gets read. The rubric asks one question: is there a factual or numerical claim that one precision gets right and the other gets wrong. Wording changes, reordering, and equally-defensible readings are recorded but not counted as regressions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The core loop is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;difflib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;bf16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vllm_bf16_conc1.texts.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;fp8&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vllm_fp8_conc1.texts.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bf16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fp8&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;difflib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SequenceMatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# non-identical pairs go to side-by-side review
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it found on Qwen3-8B FP8
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;7 of 20 outputs byte-identical.&lt;/li&gt;
&lt;li&gt;9 differed only in wording or formatting; equivalent content on review.&lt;/li&gt;
&lt;li&gt;3 minor regressions: a repeated word in a poem, one list item drifting off topic, one questionable tool suggestion.&lt;/li&gt;
&lt;li&gt;1 extraction prompt was ambiguous and produced two defensible readings.&lt;/li&gt;
&lt;li&gt;Zero cases where FP8 gave a wrong factual or numerical answer that BF16 answered correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line is the acceptance bar. Minor stylistic wobble is expected from a numerics change; a flipped fact is a blocker. This profile passed, so the 1.5x was free for this workload.&lt;/p&gt;

&lt;p&gt;Two honest caveats. First, this validates a prompt profile, not the model in general: different domains, longer contexts, or sampled decoding need their own pass. Second, greedy-decoding equality is a strict signal but not a complete one; if you serve with sampling, run the review stage on sampled pairs too and expect more (benign) divergence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrinkle that almost blocked the whole thing
&lt;/h2&gt;

&lt;p&gt;Getting FP8 to run at all on workstation-class Blackwell (sm_120) required routing around a kernel assertion in the default FP8 path. If you are on the same silicon and vLLM refuses to load the FP8 checkpoint, that is a known class of problem rather than something wrong with your setup.&lt;/p&gt;

&lt;p&gt;The full report this check belongs to, with the raw CSVs, environment manifest, and the one-command reproduction script, is here: &lt;a href="https://conatus.jahn.ai/ai-engineering/sample-report" rel="noopener noreferrer"&gt;https://conatus.jahn.ai/ai-engineering/sample-report&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>machinelearning</category>
      <category>performance</category>
      <category>python</category>
    </item>
    <item>
      <title>Qwen3-8B on workstation Blackwell: vLLM vs SGLang vs llama.cpp, plus an FP8 pass</title>
      <dc:creator>Jahn</dc:creator>
      <pubDate>Tue, 25 Aug 2026 16:56:08 +0000</pubDate>
      <link>https://dev.to/conatusai/qwen3-8b-on-workstation-blackwell-vllm-vs-sglang-vs-llamacpp-plus-an-fp8-pass-325c</link>
      <guid>https://dev.to/conatusai/qwen3-8b-on-workstation-blackwell-vllm-vs-sglang-vs-llamacpp-plus-an-fp8-pass-325c</guid>
      <description>&lt;p&gt;Benchmarks of the same model on the same GPU across three serving stacks, then an FP8 pass on the winner. All numbers measured on our own hardware last week. Raw CSVs, the environment manifest and a one-command reproduction script exist for every figure; the script was re-run end to end after the report was written and all figures reproduced within 6 percent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GPU: RTX PRO 6000 Blackwell, 96 GB, sm_120 (workstation Blackwell, not the datacenter parts)&lt;/li&gt;
&lt;li&gt;Model: Qwen3-8B&lt;/li&gt;
&lt;li&gt;Stacks: vLLM 0.27.1, SGLang 0.5.9, llama.cpp (CUDA build)&lt;/li&gt;
&lt;li&gt;Method: identical prompt set and sampling settings across engines, greedy decoding, output token counts matched across engines before any timing was compared. Concurrency 32 for the aggregate numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Matching token counts before comparing throughput matters more than it sounds. Engines disagree about stop conditions and detokenization, and a 10 percent difference in generated tokens quietly becomes a 10 percent throughput lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  BF16, concurrency 32
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stack&lt;/th&gt;
&lt;th&gt;aggregate tok/s&lt;/th&gt;
&lt;th&gt;TTFT p50&lt;/th&gt;
&lt;th&gt;e2e p99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vLLM 0.27.1&lt;/td&gt;
&lt;td&gt;1,725&lt;/td&gt;
&lt;td&gt;39 ms&lt;/td&gt;
&lt;td&gt;3.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGLang 0.5.9&lt;/td&gt;
&lt;td&gt;1,327&lt;/td&gt;
&lt;td&gt;42 ms&lt;/td&gt;
&lt;td&gt;5.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp (CUDA)&lt;/td&gt;
&lt;td&gt;428&lt;/td&gt;
&lt;td&gt;316 ms&lt;/td&gt;
&lt;td&gt;16.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FP8 on the winner (vLLM, same settings)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;precision&lt;/th&gt;
&lt;th&gt;single-stream tok/s&lt;/th&gt;
&lt;th&gt;batch tok/s&lt;/th&gt;
&lt;th&gt;latency p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;1,725&lt;/td&gt;
&lt;td&gt;0.74 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8 (official checkpoint)&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;2,597&lt;/td&gt;
&lt;td&gt;0.49 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack choice barely matters for one user.&lt;/strong&gt; Single-stream decode sits in the 83 to 96 tok/s band on every stack. If your service has one concurrent user, pick whatever is easiest to operate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack choice is a 4x decision under load.&lt;/strong&gt; At concurrency 32 the spread is 428 to 1,725 tok/s for the same model on the same silicon. Time to first token spreads 4x as well (39 ms vs 316 ms p50), which dominates perceived snappiness for chat workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FP8 was a clean 1.5x.&lt;/strong&gt; 1,725 to 2,597 tok/s aggregate, 86 to 130 single-stream, p50 latency down a third. We ran a fixed 20-prompt factual check before and after: zero regressions. For an 8B model on a 96 GB card the VRAM saving is irrelevant, the win is throughput.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sm_120 wrinkle
&lt;/h2&gt;

&lt;p&gt;Workstation Blackwell is not datacenter Blackwell, and default paths tuned for the latter sometimes assert on the former. Getting FP8 to run here required routing around a kernel assertion in the default FP8 path (DeepGEMM asserting on sm_120, resolved by falling back to the CUTLASS path). Related example of the same hardware-class gap: &lt;a href="https://github.com/vllm-project/vllm/issues/53748" rel="noopener noreferrer"&gt;vllm-project/vllm#53748&lt;/a&gt;, where the Triton MLA decode kernel overflows the 101,376-byte per-block shared-memory limit that GB10 and workstation Blackwell share, but datacenter parts do not.&lt;/p&gt;

&lt;p&gt;If you are serving on RTX PRO 6000, GB10 or consumer Blackwell, budget time for this class of issue. The performance is there, the default configs often are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Greedy decoding for determinism; throughput measured after warmup&lt;/li&gt;
&lt;li&gt;Aggregate numbers are steady-state over the full prompt set, not peak&lt;/li&gt;
&lt;li&gt;Each engine got the same max batch and context settings where the knob exists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full methodology and the case study writeup: &lt;a href="https://conatus.jahn.ai/ai-engineering" rel="noopener noreferrer"&gt;conatus.jahn.ai/ai-engineering&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>cuda</category>
      <category>performance</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
