<?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: Devashish</title>
    <description>The latest articles on DEV Community by Devashish (@ric03uec).</description>
    <link>https://dev.to/ric03uec</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%2F104592%2F5802344f-b876-4875-8d65-d8226b7c8598.jpeg</url>
      <title>DEV Community: Devashish</title>
      <link>https://dev.to/ric03uec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ric03uec"/>
    <language>en</language>
    <item>
      <title>Owning Inference - Qwen3.6 on DGX Spark for real coding</title>
      <dc:creator>Devashish</dc:creator>
      <pubDate>Tue, 07 Jul 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/ric03uec/owning-inference-qwen36-on-dgx-spark-for-real-coding-2aob</link>
      <guid>https://dev.to/ric03uec/owning-inference-qwen36-on-dgx-spark-for-real-coding-2aob</guid>
      <description>&lt;p&gt;If you spend as much time on Hacker News as I do, you've surely noticed an uptick in conversations around local models, skyrocketing inference costs, shifting focus on Qwen (DeepSeek, Mistral and the likes) for coding tasks and a general frustration around navigating the AI-coding-at-scale-without-going-broke problem.&lt;/p&gt;

&lt;p&gt;On the other hand, running local models is no small feat by itself. I'm no AI researcher and my (rather naive) assumption when I got my DGX Spark a few months back was that local inference setup would be akin to pulling a Docker image, typing &lt;code&gt;ollama serve&lt;/code&gt; and pointing my OpenCode to the endpoint. Once done, I'd have infinite tokens at my disposal and have agents completing work 24/7.&lt;/p&gt;

&lt;p&gt;Needless to say, I opened the Pandora's box there and now I'm deep into the rabbit hole of figuring out what a functional local LLM stack looks like.&lt;/p&gt;

&lt;p&gt;A recent &lt;a href="https://news.ycombinator.com/item?id=48721903" rel="noopener noreferrer"&gt;1,190-point HN thread on Qwen 3.6 27B&lt;/a&gt; provides a deep insight into the state of local models as of writing this post. Yes, we can now run dense models on a local setup but the true value remains to be seen. That's exactly what I've been trying to do. My goal is to get real, continuous and high quality work done by local models. This post covers the progress I've made so far on this journey.&lt;/p&gt;

&lt;p&gt;My previous post in this series (&lt;a href="https://devashish.me/p/two-qwen3-models-on-one-dgx-spark" rel="noopener noreferrer"&gt;Two Qwen3 Models on One DGX Spark&lt;/a&gt;) was about setting up a two model stack on the DGX. This post answers the follow-up: does this stack write code that ships? At present I have &lt;code&gt;Qwen/Qwen3.6-27B-FP8&lt;/code&gt; as the sole active model on my DGX Spark, serving reasoning + tools + MTP at 256K context, and shipping work into my open-source (OSS) project - &lt;a href="https://github.com/ric03uec/clawrium" rel="noopener noreferrer"&gt;Clawrium&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is broken down into two sections. The first section talks about the journey of getting the stack working and second one shows how this stack is put to work on a real project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final stack (for the impatient)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host:&lt;/strong&gt; DGX Spark GB10 (Grace Blackwell ARM64 (aarch64), &lt;code&gt;sm_121&lt;/code&gt; compute capability), ~119 GiB unified memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;code&gt;Qwen/Qwen3.6-27B-FP8&lt;/code&gt; at 262,144-token context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image:&lt;/strong&gt; &lt;code&gt;nvcr.io/nvidia/vllm:26.06-py3&lt;/code&gt; with one pip pin (see Wall 3)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proxy:&lt;/strong&gt; LiteLLM v1.88.1 at &lt;code&gt;:4000&lt;/code&gt; with &lt;code&gt;drop_params: true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runner:&lt;/strong&gt; Model Runner &lt;strong&gt;V1&lt;/strong&gt; (V2 breaks — see Wall 2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags:&lt;/strong&gt; &lt;code&gt;--enable-prefix-caching --enforce-eager --enable-auto-tool-choice --tool-call-parser qwen3_xml --reasoning-parser qwen3 --speculative-config '{"method":"mtp","num_speculative_tokens":1}'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clients:&lt;/strong&gt; pi.dev (editor) and Hermes agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz30xefrxw52am81hzez.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz30xefrxw52am81hzez.jpg" alt="Local agentic stack " width="767" height="773"&gt;&lt;/a&gt;&lt;br&gt;
Now the walls, in the order I hit them to get this working.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Change Models?
&lt;/h2&gt;

&lt;p&gt;Before that, a brief overview of the problems I was facing. My &lt;a href="https://www.devashish.me/p/two-qwen3-models-on-one-dgx-spark" rel="noopener noreferrer"&gt;previous setup&lt;/a&gt; on the same hardware was running two models:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Qwen3-Next-80B-Instruct-FP8&lt;/li&gt;
&lt;li&gt;Qwen3-4B-Instruct&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And before I moved to the current model, I also tried &lt;a href="https://huggingface.co/Qwen/Qwen3.6-35B-A3B" rel="noopener noreferrer"&gt;Qwen3.6-35B-A3B&lt;/a&gt;. Pretty much same problem with each of these — 4B-Instruct was anyway not suited for large tasks but I tried solving simple bugs with the other two models. I hoped to get better quality with 35B but since it was not a reasoning model and with only 3B active parameters, it failed on these tasks.&lt;/p&gt;

&lt;p&gt;I hooked these up with Hermes and OpenCode and tried with different prompt sizes, system prompts and other tweaks but they didn't do much beyond basic summarization tasks.&lt;/p&gt;

&lt;p&gt;The Hacker News post I mentioned earlier triggered my curiosity to use a dense model for the same task with the same rig.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wall 1 — NGC image too old for &lt;code&gt;qwen3_5&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Dropped the model YAML onto the existing &lt;code&gt;nvcr.io/nvidia/vllm:25.11-py3&lt;/code&gt; image that had been serving Qwen3-Next-80B fine for weeks. The container crashed on start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pydantic_core._pydantic_core.ValidationError: 1 validation error for ModelConfig
  Value error, The checkpoint you are trying to load has model type `qwen3_5`
  but Transformers does not recognize this architecture.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Qwen3.6 is &lt;code&gt;Qwen3_5ForConditionalGeneration&lt;/code&gt; — a dense 27B with hybrid attention plus Mamba/GDN layers. The &lt;code&gt;25.11-py3&lt;/code&gt; image ships a Transformers old enough to predate &lt;code&gt;qwen3_5&lt;/code&gt;. Upgrading Transformers alone breaks the CUDA + vLLM + Transformers version matrix that NGC pins together.&lt;/p&gt;

&lt;p&gt;Bumped the image to &lt;code&gt;nvcr.io/nvidia/vllm:26.06-py3&lt;/code&gt; (vLLM &lt;code&gt;0.22.1&lt;/code&gt;, Transformers &lt;code&gt;5.6.0&lt;/code&gt;, arm64 manifest, FP8 kernels for &lt;code&gt;sm_121&lt;/code&gt;). The pull took 35 minutes and looked stuck — 20 GiB across ~9 concurrent CloudFront connections is just the actual wall time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: NGC image tags bundle a version matrix. Don't upgrade individual components inside a running container. Bump the whole tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 2 — the red herring that hid the real crash
&lt;/h2&gt;

&lt;p&gt;New image, &lt;code&gt;qwen3_5&lt;/code&gt; recognized, weights loaded, torch.compile ran, engine died:&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="n"&gt;ERROR&lt;/span&gt; &lt;span class="n"&gt;EngineCore&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;torch/_ops.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;1408&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_get_packet&lt;/span&gt;
    &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overload_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_C&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_jit_get_operation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qualname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;UnicodeDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="n"&gt;codec&lt;/span&gt; &lt;span class="n"&gt;can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t decode byte 0xad in position 181
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every guide on the internet ties this UnicodeDecodeError to torch.compile on aarch64. I ran with that hypothesis and added &lt;code&gt;--enforce-eager&lt;/code&gt; to skip torch.compile entirely. Same crash. &lt;code&gt;--enforce-eager&lt;/code&gt; was working — the log confirmed &lt;code&gt;Enforce eager set, disabling torch.compile and CUDAGraphs&lt;/code&gt; — but the crash was unaffected.&lt;/p&gt;

&lt;p&gt;Read the traceback more carefully. The crash was inside &lt;code&gt;torch.library._del_library&lt;/code&gt; — that's &lt;strong&gt;Python interpreter shutdown code&lt;/strong&gt;, not init.&lt;/p&gt;

&lt;p&gt;Something else killed the engine, then the shutdown-cleanup path fired the visible error. Tightened the grep and found the real primary error earlier in the log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AssertionError: Model Runner V2 has not yet supported mamba_cache_mode='align'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chained together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qwen3.6-27B is a hybrid attention + Mamba model.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--enable-prefix-caching&lt;/code&gt; forces &lt;code&gt;mamba_cache_mode='align'&lt;/code&gt; on hybrid models — the only cache layout the linear-attention layers support with prefix caching.&lt;/li&gt;
&lt;li&gt;Model Runner V2 in vLLM 0.22 is a rewrite that hasn't reimplemented align-mode prefix caching for hybrids (&lt;a href="https://github.com/vllm-project/vllm/issues/26201" rel="noopener noreferrer"&gt;vllm#26201&lt;/a&gt;, &lt;a href="https://github.com/vllm-project/vllm/issues/38041" rel="noopener noreferrer"&gt;vllm#38041&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The NGC 26.06-py3 compose template ships &lt;code&gt;VLLM_USE_V2_MODEL_RUNNER: "1"&lt;/code&gt; as a default. That flips the switch that trips the assertion.&lt;/li&gt;
&lt;li&gt;The engine crashes during &lt;code&gt;_initialize_kv_caches&lt;/code&gt;, the interpreter shuts down, &lt;code&gt;torch.library._del_library&lt;/code&gt; iterates a garbled JIT op registry — UnicodeDecodeError, fully downstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix: delete one env line from the compose template. V1 is the documented fallback and matches the vLLM Recipes reference command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: when Python shuts down mid-crash, the visible error is a shutdown artifact. &lt;code&gt;grep -B2 -A20 "ERROR .* failed to start"&lt;/code&gt; before you form any hypothesis. I lost about half a day to the wrong one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 3 — HTTP 500 on every request (Prometheus + FastAPI 0.137)
&lt;/h2&gt;

&lt;p&gt;Engine came up clean. But every chat completion returned 500:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"'_IncludedRouter' object has no attribute 'path'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"InternalServerError"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FastAPI 0.137 (May 2026) restructured &lt;code&gt;include_router()&lt;/code&gt; to wrap routers in &lt;code&gt;_IncludedRouter&lt;/code&gt; objects that don't expose &lt;code&gt;.path&lt;/code&gt;. &lt;code&gt;prometheus-fastapi-instrumentator &amp;lt;= 8.0.0&lt;/code&gt; unconditionally reads &lt;code&gt;.path&lt;/code&gt; in its middleware. Every request through the middleware raises &lt;code&gt;AttributeError&lt;/code&gt;, which bubbles to a 500. NGC 26.06-py3 bundles &lt;code&gt;fastapi &amp;gt;= 0.137&lt;/code&gt; with &lt;code&gt;prometheus-fastapi-instrumentator == 8.0.0&lt;/code&gt; — the exact broken pair.&lt;/p&gt;

&lt;p&gt;Upstream fixes exist (&lt;a href="https://github.com/trallnag/prometheus-fastapi-instrumentator/releases" rel="noopener noreferrer"&gt;instrumentator 8.0.1&lt;/a&gt;, vLLM PRs &lt;a href="https://github.com/vllm-project/vllm/pull/45594" rel="noopener noreferrer"&gt;#45594&lt;/a&gt; / &lt;a href="https://github.com/vllm-project/vllm/pull/45629" rel="noopener noreferrer"&gt;#45629&lt;/a&gt;). None of them are in the &lt;code&gt;26.06-py3&lt;/code&gt; build. No newer NGC tag exists. No vLLM CLI flag disables the FastAPI middleware.&lt;/p&gt;

&lt;p&gt;Fix: a two-line derived Dockerfile that pins the instrumentator forward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nvcr.io/nvidia/vllm:26.06-py3&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="s2"&gt;"prometheus-fastapi-instrumentator&amp;gt;=8.0.1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Built locally, tagged &lt;code&gt;vllm-inx:26.06-py3-patched&lt;/code&gt;. Model YAML &lt;code&gt;image:&lt;/code&gt; swapped. Chat completion returned 200.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: NGC bundles a whole ecosystem tightly. When FastAPI does a routing refactor, middlewares break inside the container even if they've shipped a fix. A thin derived Dockerfile with a single pip pin is cheap enough to be the standard workaround.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 4 — &lt;code&gt;hermes&lt;/code&gt; parser doesn't recognize Qwen3.6's tool emission
&lt;/h2&gt;

&lt;p&gt;Baseline serve closed. In the previous post (&lt;a href="https://www.devashish.me/p/two-qwen3-models-on-one-dgx-spark" rel="noopener noreferrer"&gt;Two Qwen3 Models on One DGX Spark&lt;/a&gt;), &lt;code&gt;hermes&lt;/code&gt; worked with Qwen3-Next-80B-Instruct; carrying that forward here, I added tool calling with the same parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--enable-auto-tool-choice&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--tool-call-parser&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hermes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ran a &lt;code&gt;calculator&lt;/code&gt; tool test. &lt;code&gt;tool_calls&lt;/code&gt; was &lt;code&gt;null&lt;/code&gt;. The call text was in &lt;code&gt;.content&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;tool_call&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;function=calculator&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;parameter=expression&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;42 * 17&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/parameter&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/function&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/tool_call&amp;gt;"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model was emitting a tool call — in &lt;strong&gt;Qwen3-native XML&lt;/strong&gt; (&lt;code&gt;&amp;lt;function=…&amp;gt;&amp;lt;parameter=…&amp;gt;&lt;/code&gt;), not &lt;strong&gt;Hermes JSON&lt;/strong&gt; (&lt;code&gt;&amp;lt;tool_call&amp;gt;{"name":"calculator","arguments":{...}}&amp;lt;/tool_call&amp;gt;&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The parser doesn't match, everything falls through as raw text, the harness silently thinks the model refused the tool.&lt;/p&gt;

&lt;p&gt;NGC 26.06-py3 ships two Qwen3 parsers registered as &lt;code&gt;qwen3_xml&lt;/code&gt; and &lt;code&gt;qwen3_coder&lt;/code&gt;. Qwen3.6 emits the XML form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- - hermes
&lt;/span&gt;&lt;span class="gi"&gt;+ - qwen3_xml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-deploy. Structured tool call recognized, &lt;code&gt;finish_reason: tool_calls&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt; mostly clean (reasoning still leaked — the next phase's target).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: tool parsers map to emission formats, not model families. Qwen 2.5 Coder, Qwen3-Coder, and Qwen3.6 all pick different parsers. Run a &lt;code&gt;calculator&lt;/code&gt;-tool test on raw output before you wire any agent harness. The failure is silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reasoning + tools together
&lt;/h2&gt;

&lt;p&gt;Added &lt;code&gt;--reasoning-parser qwen3&lt;/code&gt; to route &lt;code&gt;&amp;lt;think&amp;gt;...&amp;lt;/think&amp;gt;&lt;/code&gt; into &lt;code&gt;.reasoning_content&lt;/code&gt;. The critical simultaneous-use test asks the model to think, then call a tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"reasoning_content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The user wants to know 42 times 17. I need to use the calculator tool..."&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;expression&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;42 * 17&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"calculator"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"finish_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_calls"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reasoning populated, &lt;code&gt;content: null&lt;/code&gt; (no leakage), &lt;code&gt;tool_calls[0]&lt;/code&gt; populated, &lt;code&gt;finish_reason: tool_calls&lt;/code&gt;. That's the shape a coding-agent harness wants.&lt;/p&gt;

&lt;h2&gt;
  
  
  MTP — the risk that turned out safe
&lt;/h2&gt;

&lt;p&gt;MTP (Multi-Token Prediction speculative decoding) was the highest-risk phase because a draft head that doesn't respect structured-output boundaries corrupts either the tool XML or the &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; tags. Startup log after adding the flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resolved architecture: Qwen3_5MTP
Loading drafter model...
Detected MTP model. Sharing target model {embedding,lm_head} weights with the draft model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Architecture flipped from &lt;code&gt;Qwen3_5ForConditionalGeneration&lt;/code&gt; to &lt;code&gt;Qwen3_5MTP&lt;/code&gt; — the FP8 checkpoint ships the draft head embedded. Re-ran the reasoning + tool acceptance test. Same clean shape. MTP did not corrupt tool tags or &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: read the resolved architecture line in the startup log. &lt;code&gt;Qwen3_5MTP&lt;/code&gt; vs &lt;code&gt;Qwen3_5ForConditionalGeneration&lt;/code&gt; tells you whether MTP is actually active. The flag can be accepted without the checkpoint having the head — silent no-op.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall 5 — pi sends &lt;code&gt;reasoning_effort&lt;/code&gt;, LiteLLM 400s
&lt;/h2&gt;

&lt;p&gt;Wired the pi.dev agent harness with a new provider extension pointed at &lt;code&gt;http://&amp;lt;host&amp;gt;:4000/v1&lt;/code&gt;, model &lt;code&gt;Qwen3.6-27B&lt;/code&gt;, &lt;code&gt;reasoning: true&lt;/code&gt;. First call:&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="mi"&gt;400&lt;/span&gt; &lt;span class="n"&gt;litellm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnsupportedParamsError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="n"&gt;does&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;support&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reasoning_effort&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;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Qwen&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Qwen3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;FP8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;To&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt; &lt;span class="n"&gt;these&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="sb"&gt;`litellm.drop_params=True`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pi's &lt;code&gt;reasoning: true&lt;/code&gt; flag emits an OpenAI-style &lt;code&gt;reasoning_effort&lt;/code&gt; param with every request. The LiteLLM route maps to a generic &lt;code&gt;openai/*&lt;/code&gt; upstream, and OpenAI's SDK strictly refuses &lt;code&gt;reasoning_effort&lt;/code&gt; for non-o1 models. LiteLLM propagates the strict validation as a 400. This is orthogonal to vLLM's own reasoning behavior — &lt;code&gt;--reasoning-parser qwen3&lt;/code&gt; operates on the model's &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; output tokens server-side; &lt;code&gt;reasoning_effort&lt;/code&gt; is a client-facing param LiteLLM doesn't route by default.&lt;/p&gt;

&lt;p&gt;One-line fix in the LiteLLM config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;litellm_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;drop_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LiteLLM now silently strips unsupported params before forwarding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: &lt;code&gt;drop_params: true&lt;/code&gt; is load-bearing for any LiteLLM deployment that serves multiple client SDKs. pi, aider, opencode, Continue, and Cursor each pass different reasoning-related params (&lt;code&gt;reasoning_effort&lt;/code&gt;, &lt;code&gt;thinking&lt;/code&gt;, &lt;code&gt;reasoning&lt;/code&gt;, &lt;code&gt;thinking_budget&lt;/code&gt;). Without &lt;code&gt;drop_params&lt;/code&gt;, every new client is a fresh 400 to debug.&lt;/p&gt;

&lt;p&gt;Also worth a mention: &lt;strong&gt;GB10's unified memory pool doesn't report the way discrete GPUs do.&lt;/strong&gt; &lt;code&gt;nvidia-smi --query-gpu=memory.used&lt;/code&gt; returns &lt;code&gt;[N/A]&lt;/code&gt;. Use &lt;code&gt;nvidia-smi --query-compute-apps=used_memory --format=csv&lt;/code&gt; and sum. Same intent, different query. Update any monitoring or pre-flight gates written against discrete-VRAM semantics.&lt;/p&gt;

&lt;h3&gt;
  
  
  The generalized rules
&lt;/h3&gt;

&lt;p&gt;Five lessons that generalize past this box:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The visible error is not the primary error when Python shuts down.&lt;/strong&gt; Grep for &lt;code&gt;EngineCore failed to start&lt;/code&gt; first; the &lt;code&gt;_del_library&lt;/code&gt; UnicodeDecodeError is downstream noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGC image tags bundle a version matrix.&lt;/strong&gt; Don't upgrade individual components. Bump the whole tag or write a thin derived Dockerfile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Runner V2 doesn't support hybrid-attention align-mode prefix caching yet.&lt;/strong&gt; If you're serving Qwen3.5/3.6, MiniMax M2, or any Mamba/GDN model, unset &lt;code&gt;VLLM_USE_V2_MODEL_RUNNER&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool parsers map to emission formats, not model families.&lt;/strong&gt; Run a &lt;code&gt;calculator&lt;/code&gt;-tool test on raw output before wiring any agent harness. The failure is silent — the call lands in &lt;code&gt;.content&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;drop_params: true&lt;/code&gt; in LiteLLM is load-bearing.&lt;/strong&gt; Client SDKs pass different reasoning params; the OpenAI upstream rejects unknown ones. &lt;code&gt;drop_params&lt;/code&gt; decouples clients from upstream opinion.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;This work only matters if the stack survives contact with a real repo. &lt;a href="https://github.com/ric03uec/clawrium" rel="noopener noreferrer"&gt;Clawrium&lt;/a&gt; is my open-source agent fleet manager where this stack is routing to. I'm using this project to benchmark the stack with most of the new changes. This is a non-trivial project with real users so the objective is to dogfood my stack against something I have an incentive to keep stable. I started with small PRs and some documentation tasks. I'll give it a few weeks to settle down before graduating to more long running tasks.&lt;/p&gt;

&lt;p&gt;Additionally, I've also onboarded a Hermes agent to use the same stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Evidence: code that shipped into Clawrium
&lt;/h3&gt;

&lt;p&gt;Here are concrete Clawrium pull requests that went through the local-Qwen path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ric03uec/clawrium/pull/859" rel="noopener noreferrer"&gt;#859 — fix(gui): return 502 for lifecycle failures, surface errors in frontend (#712)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ric03uec/clawrium/pull/855" rel="noopener noreferrer"&gt;#855 — fix(#753): validate agent_name at sync boundary before path interpolation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ric03uec/clawrium/pull/861" rel="noopener noreferrer"&gt;#861 — fix(cli): fail fast when api_url env is invalid&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One important nuance: not every artifact is an execution artifact. Some are planning artifacts that support the same workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ric03uec/clawrium/pull/858" rel="noopener noreferrer"&gt;#858 — docs(#712): implementation plan for GUI lifecycle 502 fix (plan-only)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Hermes agent using this setup self-published a release notes blog: &lt;a href="https://ric03uec.github.io/clawrium/blog/v26.7.0-release" rel="noopener noreferrer"&gt;https://ric03uec.github.io/clawrium/blog/v26.7.0-release&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;As much as this model seems promising, the rule of thumb is: Don't ask it for any non-trivial planning. Ask it to write the change once the plan is concrete. This also requires a change in approach. Granularity and precision of the plan defines the success of a local model.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Plan with Codex/Claude/GLM.&lt;/strong&gt; Feed the issue, the file tree, and the acceptance criteria. Output: an ordered task list with file paths, function signatures, and test expectations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute with local Qwen.&lt;/strong&gt; Feed the task list to pi.dev or OpenCode pointed at the LiteLLM proxy. The model reads the codebase (tools), applies diffs (tools), runs tests (tools). All at 256K context with MTP speculative decoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review locally, then ship.&lt;/strong&gt; PR draft, review pass, merge. The frontier is called for planning, not for typing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harness choice matters.&lt;/strong&gt; OpenCode remains my daily driver but as much as it makes model switching easier, it comes with real bloat. Going for local LLMs means obsessing over keeping your stack lean. Starting with the harness. I'm getting good results with &lt;a href="https://pi.dev" rel="noopener noreferrer"&gt;pi.dev&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code without validation is useless.&lt;/strong&gt; I haven't ported my validation harness yet to use the local model (yet). For true cost efficiencies &lt;em&gt;both&lt;/em&gt; generation and validation need to be localised.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the actual split I'm aiming for: frontier models plan, local Qwen executes, and the repo accumulates both kinds of output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trends so far
&lt;/h3&gt;

&lt;p&gt;The stack usage is around 50M tokens so far over the past weekend. I'll start adding more tasks to the queue this week and report updated numbers soon.&lt;/p&gt;

&lt;p&gt;Qwen3.6-27B-FP8 on the DGX Spark sustained about &lt;strong&gt;14-16 tok/s decode throughput on active two-request workloads&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prompt ingest showed bursty but high prefill throughput, with aggregate effective prefill around 12.6k tok/s.&lt;/li&gt;
&lt;li&gt;Prefix caching was highly effective at roughly 95% hit rate.&lt;/li&gt;
&lt;li&gt;Average TTFT was about 8.5s, and average end-to-end latency was about 42s, driven mostly by long decode phases on very large prompts.&lt;/li&gt;
&lt;li&gt;The observed average prompt size was huge at about 78.8k tokens/request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ql6sezy6ymjthxsznkk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ql6sezy6ymjthxsznkk.jpg" alt="local llm usage over past 1 week with LiteLLM dashboard" width="799" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Local inference is finally getting interesting because it can survive contact with real work. It has to reason, call tools, survive a real repo, and ship code I'd actually merge.&lt;/p&gt;

&lt;p&gt;That's what this DGX Spark stack is starting to do. Frontier models still do the heavy planning. But once the task is concrete, local Qwen is getting close to being the machine I trust to execute it. That's the future I want: owned inference, owned context, owned workflow. And this is the first setup I've had that feels like it might actually get there.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nvidia</category>
      <category>coding</category>
    </item>
    <item>
      <title>Two Qwen3 Models on One DGX Spark: The Residency Math for Local LLM Coding</title>
      <dc:creator>Devashish</dc:creator>
      <pubDate>Tue, 16 Jun 2026 20:23:10 +0000</pubDate>
      <link>https://dev.to/ric03uec/two-qwen3-models-on-one-dgx-spark-the-residency-math-for-local-llm-coding-5bpj</link>
      <guid>https://dev.to/ric03uec/two-qwen3-models-on-one-dgx-spark-the-residency-math-for-local-llm-coding-5bpj</guid>
      <description>&lt;p&gt;My agent stack with Hermes runs on a workstation. The models run on a DGX Spark on the same LAN. The split is deliberate: the workstation stays responsive, the Spark does the GPU work, and they talk over an HTTP proxy.&lt;/p&gt;

&lt;p&gt;Since I started managing the agent fleet through &lt;a href="//github.com/ric03uec/clawrium/"&gt;Clawrium&lt;/a&gt;, the Hermes count has climbed. More agents on more hosts, more concurrent traffic, all hitting the same Spark. What was a one-laptop, one-model setup is now a small fleet against a single backend — and the shape of the load is exactly what a single-model server can't serve.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Febx9lrmv2zjy9b7mlx8x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Febx9lrmv2zjy9b7mlx8x.png" alt="Fleet management using Clawrium" width="799" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Spark served models through ollama for months. It worked. One model up, single config, easy to bring down.&lt;/p&gt;

&lt;p&gt;But ollama owns the card. There's no per-process memory budget, no &lt;code&gt;gpu_memory_utilization&lt;/code&gt; knob, no straightforward way to coresident a heavy model for reasoning and a fast model for quick turns. KV cache management is whatever the underlying llama.cpp backend gives you. PagedAttention isn't there.&lt;/p&gt;

&lt;p&gt;vLLM fixes all of that.&lt;/p&gt;

&lt;p&gt;PagedAttention reclaims KV blocks instead of contiguous-pinning them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gpu_memory_utilization&lt;/code&gt; gives you a per-container budget.&lt;/p&gt;

&lt;p&gt;One Spark (GB10, 119.67 GiB unified memory) can run multiple vLLM containers behind a LiteLLM proxy on &lt;code&gt;:4000&lt;/code&gt;, and Hermes hits one URL to route to either model. The promise: serve Qwen3-Next-80B-Instruct-FP8 for the heavy work and Qwen3-4B-Instruct-2507 for fast turns, coresident, both reachable from a single endpoint.&lt;/p&gt;

&lt;p&gt;That's the why. What follows is what it took to make the promise hold.&lt;/p&gt;

&lt;p&gt;Spark hardware will happily hold two Qwen3 models if the numbers line up. They didn't, for several days. That's where my last weekend went.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt one: trust the target
&lt;/h2&gt;

&lt;p&gt;First 80B config: &lt;code&gt;gpu_memory_utilization: 0.75&lt;/code&gt;, &lt;code&gt;max_model_len: 65536&lt;/code&gt;, &lt;code&gt;max_num_seqs: 4&lt;/code&gt;. vLLM's KV cache init crashed with &lt;em&gt;"No available memory for the cache blocks."&lt;/em&gt; Qwen3-Next is mostly Mamba; the per-block page alignment pushes KV pool demand higher than the ~14 GiB residue after weights.&lt;/p&gt;

&lt;p&gt;Bumped to 0.85. Now the free-memory check crashed: &lt;em&gt;"Free memory on device (98.51/119.67 GiB) is less than desired GPU memory utilization (0.85, 101.72 GiB)."&lt;/em&gt; The 4B was already resident at ~16 GiB. The 80B's 0.85 target was reading the whole card, not what was free.&lt;/p&gt;

&lt;p&gt;That's the first lesson. &lt;code&gt;gpu_memory_utilization&lt;/code&gt; is a fraction of total GPU memory, not free memory.&lt;/p&gt;

&lt;p&gt;Two co-resident vLLM processes need their fractions to sum below ~0.95 to leave room for CUDA framework overhead. If your math assumes free, you'll oscillate between OOMs and silent KV starvation.&lt;/p&gt;

&lt;p&gt;Settled at 0.80 / 32k / 2 for the 80B. Loaded clean. KV pool ~20.8 GiB after weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt two: point Hermes at it
&lt;/h2&gt;

&lt;p&gt;Then Hermes came online and tool calls came back as plain text. &lt;code&gt;&amp;lt;tool_call&amp;gt;&lt;/code&gt; JSON sitting inside &lt;code&gt;content&lt;/code&gt;. &lt;code&gt;tool_calls: []&lt;/code&gt;. &lt;code&gt;finish_reason: stop&lt;/code&gt;. Hermes never executed it.&lt;/p&gt;

&lt;p&gt;A day of parser triage produced nothing actionable. Both &lt;code&gt;hermes_tool_parser.py&lt;/code&gt; and &lt;code&gt;qwen3xml_tool_parser.py&lt;/code&gt; look for &lt;code&gt;&amp;lt;tool_call&amp;gt;&lt;/code&gt; (singular). The &lt;code&gt;&amp;lt;tools&amp;gt;&lt;/code&gt; plural tag is the system-prompt definition, not the output. The parser wasn't wrong. The model wasn't emitting.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tool_choice: "required"&lt;/code&gt; worked. &lt;code&gt;tool_choice: "auto"&lt;/code&gt; came back empty: &lt;code&gt;tool_calls: []&lt;/code&gt;, &lt;code&gt;content: ""&lt;/code&gt;, 619 characters of reasoning inside &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; concluding &lt;em&gt;"Alright, that's it"&lt;/em&gt; without emitting the call.&lt;/p&gt;

&lt;p&gt;Qwen's own model card states it plainly: Qwen3-Next-80B-Thinking supports only thinking mode. &lt;code&gt;enable_thinking: false&lt;/code&gt; is a structural no-op on this checkpoint. &lt;code&gt;/no_think&lt;/code&gt; in the prompt is ignored. The model reasons inside &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt;, decides, and never emits.&lt;/p&gt;

&lt;p&gt;That's an unrecoverable failure for any agent SDK that defaults to &lt;code&gt;tool_choice: "auto"&lt;/code&gt;. The fix wasn't a parser flag. It was swapping the whole 80B backbone from Thinking to Instruct.&lt;/p&gt;

&lt;p&gt;77 GiB pre-pull. Drain GPU. Bring up with &lt;code&gt;--enable-auto-tool-choice --tool-call-parser hermes&lt;/code&gt;, no &lt;code&gt;--reasoning-parser&lt;/code&gt;. Three LiteLLM aliases (writer / reviewer / sources) all passed &lt;code&gt;tool_choice: "auto"&lt;/code&gt; cleanly with &lt;code&gt;finish_reason: tool_calls&lt;/code&gt;. Trade accepted: reviewer loses native &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; traces. Reasoning moved into the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt three: the bump that broke coresidency
&lt;/h2&gt;

&lt;p&gt;Reviewer agent (running on Hermes) needed 64k context. Bumped the 80B to &lt;code&gt;0.85 / 65536 / 2&lt;/code&gt;. 80B loaded healthy. The 4B's restart loop kicked in 19 times: &lt;em&gt;"Free memory on device (12.58/119.67 GiB) is less than desired GPU memory utilization (0.12, 14.36 GiB)."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;80B's actual residency at 0.85 was 101.5 GiB. Plus ~5 GiB CUDA framework overhead. That left ~12.5 GiB free. The 4B needed 14.36 GiB. No room.&lt;/p&gt;

&lt;p&gt;Toned the 80B back to 0.80, dropped the 4B to &lt;code&gt;0.10 / 16384 / 8&lt;/code&gt;. Both came up healthy. The 4B's &lt;code&gt;max_model_len&lt;/code&gt; had to drop because the 0.10 allocation leaves only ~3.5 GiB for KV pool — 32k single-seq KV demand (~4.8 GiB) doesn't fit; 16k (~2.4 GiB) does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The residency math
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8m4mdv6o8nvvkdmpw4in.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8m4mdv6o8nvvkdmpw4in.png" alt=" " width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the table I wish I'd built on day one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Allocation target&lt;/th&gt;
&lt;th&gt;Actual resident&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-Next-80B-Instruct-FP8 at 0.80&lt;/td&gt;
&lt;td&gt;~95 GiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;87.8 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-4B-Instruct at 0.10&lt;/td&gt;
&lt;td&gt;~12 GiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13.8 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~107 GiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;101.6 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free headroom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~12 GiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~18 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three observations from the actuals.&lt;/p&gt;

&lt;p&gt;The 80B's actual residency at 0.80 ran 8 GiB &lt;em&gt;under&lt;/em&gt; allocation. That cushion is the only reason the 4B's restart variability doesn't break the deployment. At 0.85, the cushion went negative — same hardware, same models, same vLLM build.&lt;/p&gt;

&lt;p&gt;The 4B at 0.10 actually resides at 13.8 GiB, not the 12 GiB the target implies. CUDA framework overhead doesn't disappear at small allocations.&lt;/p&gt;

&lt;p&gt;On Qwen3-Next specifically, &lt;code&gt;max_model_len × max_num_seqs&lt;/code&gt; is dominated by Mamba state alignment, not attention KV. Halving &lt;code&gt;max_model_len&lt;/code&gt; doesn't halve KV pool demand the way it does on a pure attention model. Plan KV against Mamba page sizes, not against intuition from Llama-class models.&lt;/p&gt;

&lt;p&gt;Once the wiring was complete, LiteLLM showed all the aliases for the same two models running on the Spark.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw17gyy0bpuec3ykwvq0t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw17gyy0bpuec3ykwvq0t.png" alt=" " width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The insight
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gpu_memory_utilization&lt;/code&gt; is a snapshot vLLM takes at process start, against total card memory. It is not a target against free memory. CUDA contexts from prior failed attempts can transiently inflate residency and trip the check spuriously. Co-resident processes don't negotiate — they race.&lt;/p&gt;

&lt;p&gt;The only number that matters is actual residency after both processes have stabilized, measured against the headroom the harder-to-restart model needs to come back from a crash. Target allocations are a planning input; actuals are the ground truth.&lt;/p&gt;

&lt;p&gt;For a two-model Spark deployment, the playbook is: load the bigger model first, let it settle, run &lt;code&gt;nvidia-smi&lt;/code&gt; to read actual residency, then size the smaller model's &lt;code&gt;gpu_memory_utilization&lt;/code&gt; against the free pool minus ~5 GiB for its own framework overhead. Recheck after both restart cleanly twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 24-hour action
&lt;/h2&gt;

&lt;p&gt;If you have a vLLM deployment running right now, pull this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvidia-smi &lt;span class="nt"&gt;--query-gpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;memory.used &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the actual number to what your &lt;code&gt;gpu_memory_utilization&lt;/code&gt; target implies. If the two diverge by more than 10%, your sizing model is wrong. Fix it before you ship anything that depends on coresidency — agent stacks, parallel workers, fallback chains. The math has to be empirical, not aspirational.&lt;/p&gt;




&lt;p&gt;If you're standing up a similar local-LLM stack — DGX Spark (or other hardware), vLLM, multiple coresident models, or wiring a remote agent fleet to a single inference backend — I'd love to compare notes.&lt;/p&gt;

</description>
      <category>localllm</category>
      <category>vllm</category>
      <category>ai</category>
      <category>nvidia</category>
    </item>
    <item>
      <title>https://www.devashish.me/p/aie-code-2025-wrapup</title>
      <dc:creator>Devashish</dc:creator>
      <pubDate>Tue, 23 Dec 2025 06:56:57 +0000</pubDate>
      <link>https://dev.to/ric03uec/httpswwwdevashishmepaie-code-2025-wrapup-365l</link>
      <guid>https://dev.to/ric03uec/httpswwwdevashishmepaie-code-2025-wrapup-365l</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.devashish.me/p/aie-code-2025-wrapup" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JZD1%21%2Cw_1200%2Ch_600%2Cc_fill%2Cf_jpg%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Cg_auto%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5eccc34d-86fe-482c-9ae9-86e3b2fc0a0d_1252x970.jpeg" height="400" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.devashish.me/p/aie-code-2025-wrapup" rel="noopener noreferrer" class="c-link"&gt;
            AIE Code 2025 Wrapup - devashish.me
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Leadership and engineering takeaways from AIE CODE 2025.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21fEfd%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F0d92f0f4-93b1-4725-8c15-c140459d9507%252Ffavicon.ico" width="64" height="64"&gt;
          devashish.me
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
    </item>
  </channel>
</rss>
