Monthly Archives: August 2026

Deploying Mistral Medium 3.5 (128B) GGUF on 4× NVIDIA V100: A Practical Guide

A field-tested walkthrough of running Mistral-Medium-3.5-128B in Q4_K_M GGUF on four Tesla V100-SXM2-32GB cards. Includes the reasoning, the dead ends, the patches, and the working OpenAI-compatible deployment — with 64K context, tool calling, reasoning, and API-key auth, served via llama-server.

Hardware: 4× Tesla V100-SXM2-32GB (NVLink mesh), CUDA 12.8, 565 GB RAM

TL;DR. We deployed a 128-billion-parameter open-weights model on commodity datacenter GPUs. The path was not vLLM (broken on Volta), not a Hugging Face AWQ (gated, awkward licensing), not a custom Triton patch (too risky for a production deployment). It was llama.cpp + a properly-merged single-file GGUF + a one-line chat-template patch. End-to-end: ~80 seconds to load the model, ~9 tok/s generation, full OpenAI API compatibility, served via a Cloudflare Quick Tunnel. The article below walks through every wrong turn, what we learned from each, and the exact commands that finally worked.

The goal

Run Mistral Medium 3.5 (128B) on the four V100-SXM2-32GB cards in our compute box, expose it as an OpenAI-compatible API on port 20000, accessible from the public internet via a Cloudflare tunnel. Required features: multi-turn chat, tool calling, native reasoning mode, API-key authentication.

The hardware constraint shaped everything that followed. V100 is Volta — compute capability 7.0. It has no tensor cores, no FP8, no BF16, no hardware attention. Any “modern” inference stack that assumes Ampere or later will refuse to run, or run with a degraded backend.

Why not vLLM?

vLLM is the obvious choice. It speaks the OpenAI API natively, supports Mistral’s tool-calling format, has a --reasoning-parser mistral flag for the new Magistral-style reasoning tokens, and integrates with Hugging Face tokenizers. On paper it should “just work” with a GGUF — vLLM has a gguf.py loader since 0.7.

On V100, it does not. Three issues, in order of encounter:

1. compressed-tensors and awq quantization loaders are gated by SM

vLLM’s auto-select quant loader checks each layer’s required minimum compute capability. For the compressed-tensors backend: Min capability: 80 (Ampere). For the legacy awq backend: 75 (Turing). V100 is 70. We hit this immediately when trying to serve cyankiwi/Mistral-Medium-3.5-128B-AWQ-INT4:

RuntimeError: Quantization scheme is not supported for the current GPU.
Min capability: 80. Current capability: 70.

That rules out every standard AWQ/GPTQ INT4 quant on this hardware. Which leaves either BF16/FP16 (won’t fit — 128B × 2 bytes = 256 GB, we have 128 GB total) or GGUF.

2. vLLM dropped Volta pre-built kernels at 0.9+

The last vLLM release with Volta-compatible pre-built CUDA binaries is 0.8.5. Newer versions may or may not work via PTX JIT fallback, but the community consensus is “don’t bother.” We pinned to vllm==0.8.5.

3. Triton 3.1/3.2 crashes on V100 with GGUF

This is the killer. vLLM successfully loads a GGUF model on V100, but the first inference call crashes:

LLVM ERROR: Failed to compute parent layout for slice layout.
ERROR [client.py:305] RuntimeError('Engine process (pid 24920) died.')

This is a known Triton bug specific to SM 7.0: Triton’s linear-layout code generator was reworked for Hopper (SM 9.0+) and emits a layout the V100 backend can’t decode. Confirmed by issues #17152, #17449, and the vLLM 0.9 release notes.

We tried downgrading Triton to 2.3.0 (the old layout-free version). That produced a different error: ImportError: cannot import name 'triton_key'. Triton 3.0.0 sits between the two bugs but wasn’t on PyPI in the form we needed. We could have patched the Triton source, but “patching the JIT compiler of a major framework for one model deployment” is the kind of technical debt that wakes you up at 3 AM six months later. Pass.

Decision: switch the runtime to llama.cpp. GGUF is llama.cpp’s native format. It supports Volta. It has tool-call parsers. It supports Mistral chat templates. It is what the GGUF was designed for.

Why llama.cpp

llama.cpp is the project that invented the GGUF format. Its llama-server binary speaks OpenAI-compatible HTTP, supports streaming, tool calling, multiple chat templates, and is the most thoroughly tested GGUF loader on consumer and datacenter hardware alike.

For our target hardware (4× V100-SXM2-32GB, NVLink), llama.cpp has the right answers baked in:

  • Tesla V100 support: SM 7.0 is a first-class target.
  • Multi-GPU: --tensor-split a,b,c,d distributes layers across GPUs.
  • Quantization support: native GGUF, including Q2_K through Q8_0 and the i-quants.
  • OpenAI-compatible API: /v1/chat/completions, /v1/models, /v1/embeddings.
  • Reasoning extraction: --reasoning-format deepseek strips [THINK]... blocks into a separate reasoning_content field.
  • Tool-call parsing: supports the Mistral tool format natively via the chat template.

Building it

git clone --depth=1 https://github.com/ggerganov/llama.cpp.git
cd llama.cpp
cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j$(nproc) --target llama-server

~5 minutes on the V100 box. GGML_CUDA=ON is the only flag you need; CUDA toolkit 12.8 was already in the base image. We also built llama-gguf-split (the tool that knows how to merge split GGUF shards correctly) with the same command.

The model: which GGUF?

Mistral Medium 3.5 ships as a 128-billion-parameter dense transformer. The HF ecosystem offers several quantizations:

Quant Size Quality Notes
BF16 (original) 248 GB Reference Won’t fit. Skip.
AWQ-INT4 (cyankiwi) 153 GB Good vLLM rejects (SM 80 required). Skip on V100.
Q2_K (unsloth, single file) 44 GB Degraded Too lossy for a 128B reasoning model.
Q4_K_M (unsloth, split) 71 GB Very good Sweet spot. Fits 4× V100 with KV cache headroom.
Q5_K_M, Q6_K 87 / 103 GB Mixed Tighter fit, less KV cache room.

We picked unsloth/Mistral-Medium-3.5-128B-GGUF in Q4_K_M. The unsloth quantizer splits it into three shards (because GGUF metadata + tensors exceeds HF’s per-file recommendation). The breakdown:

  • Shard 1 (8 MB): tokenizer metadata + the output.weight tensor (kept at high precision Q6_K because Mistral intentionally preserves the output projection).
  • Shard 2 (47 GB): roughly the first half of the transformer blocks.
  • Shard 3 (24 GB): the second half of the blocks.

The merge: the one hard part

llama-server needs a single GGUF file. The unsloth repo gives you three. Two options:

  1. llama-gguf-split --merge (official llama.cpp tool). Knows how to merge split GGUFs while preserving all KV metadata, including the tokenizer.ggml.merges ARRAY (a list of 538,891 BPE merge rules that the tokenizer needs).
  2. Roll your own merger in Python using the gguf PyPI library. Do not do this.

We learned option 2 the hard way. Our first attempt used a custom Python script that read each shard with gguf.GGUFReader and wrote a merged file with gguf.GGUFWriter. The tensors copied fine, the scalar metadata (architecture, hyperparameters) copied fine. But the script silently dropped every ARRAY-typed KV (tokenizer merges, token list, token types). The resulting GGUF loaded into vLLM but crashed inside llama.cpp’s tokenizer init with the cheerful error: “cannot find tokenizer merges in model file.”

Switching to llama-gguf-split --merge fixed it in one command:

cd /workspace/models/mistral-medium-3.5-gguf/Q4_K_M
llama-gguf-split --merge Mistral-Medium-3.5-128B-Q4_K_M-00001-of-00003.gguf merged.gguf
# gguf_merge: merged.gguf merged from 3 split with 795 tensors.

Verifying the merge

The gguf Python library is a useful sanity check:

from gguf import GGUFReader
r = GGUFReader("merged.gguf")
for k in ["tokenizer.ggml.merges", "tokenizer.ggml.tokens", "tokenizer.ggml.token_type"]:
    f = r.fields[k]
    print(f"{k}: {len(f.parts)} parts, type {f.types[0].name}")
# tokenizer.ggml.merges: 538891 parts, type ARRAY
# tokenizer.ggml.tokens: 262149 parts, type ARRAY
# tokenizer.ggml.token_type: 131077 parts, type ARRAY

~538K BPE merge rules, ~262K vocab tokens, ~131K token types — all there. The official Mistral tokenizer (tekken v15, vocab size 131,072) is the one baked into this GGUF.

Server flags: what worked

The complete llama-server invocation:

llama-server \
  --model /workspace/models/mistral-medium-3.5-gguf/Q4_K_M/Mistral-Medium-3.5-128B-Q4_K_M.gguf \
  --host 0.0.0.0 \
  --port 20000 \
  --api-key "sk-..." \
  --ctx-size 65536 \
  --n-gpu-layers 99 \
  --tensor-split "8,8,8,8" \
  --parallel 1 \
  --jinja \
  --chat-template-file /workspace/models/mistral-medium-3.5-tokenizer/chat_template_patched.jinja \
  --reasoning on \
  --reasoning-format deepseek \
  --temp 0.7 \
  --top-p 0.95 \
  --top-k 40 \
  --alias mistral-medium-3.5

Notes on each flag:

Flag Value Why
--model merged GGUF Single file the server can mmap directly.
--ctx-size 65536 64K context per slot. Lower if you need more slots.
--n-gpu-layers 99 99 All layers to GPU; no CPU offload.
--tensor-split 8,8,8,8 Distribute evenly: each V100 gets 1/4 of every linear layer’s weight matrix.
--parallel 1 1 One slot, full 64K context. Increase for concurrency at the cost of context per slot.
--jinja flag Use the Jinja chat template (vs. llama.cpp’s built-in C++ template).
--chat-template-file patched.jinja Custom template (see next section).
--reasoning on   Enable reasoning mode.
--reasoning-format deepseek   Strip [THINK]... into a separate reasoning_content field.
--alias mistral-medium-3.5 The model name clients use.

The chat template patch (the one thing nobody told us)

The Mistral-Medium-3.5 chat template that ships in the unsloth GGUF (and the one in the official mistralai repo) requires the client to pass reasoning_effort: "none" or "high". Any other value, or omitting the parameter entirely, throws:

Jinja Exception: reasoning_effort must be either "none" or "high"

This breaks any client that defaults to "low" or "medium" (the Mistral API itself accepts those). We patched the template to:

  • Default reasoning_effort to "high" when the client omits it.
  • Normalize any non-standard value ("low", "medium", etc.) to "high" instead of raising.
{#- if reasoning_effort is not defined or reasoning_effort is none %}
    {%- set reasoning_effort = 'high' %}            {# was: 'none' #}
{%- endif %}
{%- if reasoning_effort not in ['none', 'high'] %}
    {%- set reasoning_effort = 'high' %}            {# was: raise_exception(...) #}
{%- endif %}

The patch is at /workspace/models/mistral-medium-3.5-tokenizer/chat_template_patched.jinja and is loaded via --chat-template-file. After the patch, the API accepts any reasoning_effort value (or none) and the server always runs in reasoning mode. To disable reasoning, clients must explicitly send "reasoning_effort": "none".

Putting it under supervisor

Supervised by the existing supervisord in the base image. The wrapper script waits for the merged GGUF and the binary to be present, then execs llama-server with the right flags. startsecs=3600 (1 hour) gives the 128B model plenty of time to load without supervisor giving up on it.

The relevant files on the host:

/opt/supervisor-scripts/vllm-mistral-medium.sh    # wrapper
/etc/supervisor/conf.d/vllm-mistral-medium.conf  # supervisor service
/workspace/.vllm_credentials.env                  # API key + paths (mode 600)
/workspace/models/mistral-medium-3.5-gguf/Q4_K_M/Mistral-Medium-3.5-128B-Q4_K_M.gguf  # 70 GB
/workspace/models/mistral-medium-3.5-tokenizer/chat_template_patched.jinja
/workspace/llama.cpp/build/bin/llama-server       # binary
/var/log/portal/vllm-mistral-medium.log           # logs

Performance numbers (V100-SXM2-32GB, Q4_K_M, 4× TP, 64K ctx)

Metric Value
Model load time ~80 seconds
VRAM per card (idle) 23-25 GB (out of 32 GB)
Prompt eval (cold) ~127 tok/s
Prompt eval (KV cache hit) ~9 MS total / ~370 tokens cached
Generation ~9 tok/s
Time to first token (TTFT) ~3 seconds for ~370-token prompt

The 9 tok/s generation speed is the V100 tax. No tensor cores, no FlashAttention-2, no FP8. The model is doing honest FP16 GEMMs with XFormers attention. For comparison, an A100-40GB hits ~25 tok/s on the same model, an H100 hits ~50 tok/s. On V100, 9 tok/s is the ceiling for Q4_K_M Mistral-128B.

If you need faster throughput on V100, the trade-off is quantization. Q5_K_M (~87 GB) gets you a noticeable quality bump but uses more VRAM. Q3_K (~58 GB) loses some coherence on hard reasoning tasks but doubles decode speed. For our use case (reasoning-heavy agentic workflows), Q4_K_M was the right balance.

Talk to it

Once running, the API is a standard OpenAI-compatible endpoint. Example with curl:

curl -sS -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  https://crossing-exhibit-mlb-compression.trycloudflare.com/v1/chat/completions \
  -d '{
    "model": "mistral-medium-3.5",
    "messages": [{"role": "user", "content": "What is 17 * 23?"}],
    "max_tokens": 2048
  }'

Returns:

{
  "choices": [{
    "finish_reason": "stop",
    "message": {
      "role": "assistant",
      "content": "17 multiplied by 23 is **391**.",
      "reasoning_content": "The user is asking for a simple multiplication: 17 * 23..."
    }
  }],
  "usage": {"completion_tokens": 124, "prompt_tokens": 373, "total_tokens": 497}
}

Tool calls work the same way as the OpenAI API: send tools: [...], the model emits tool_calls, you respond with role: "tool" + tool_call_id. Multi-turn with tool results composes naturally.

What I’d do differently next time

  1. Skip vLLM entirely on V100. llama.cpp is the right tool for this hardware. Save yourself the day of debugging Triton’s slice-layout bug.
  2. Use llama-gguf-split --merge from the start. Don’t try to write your own GGUF merger in Python. The official tool knows about every KV metadata format, including the ARRAY-typed ones that look like they should be trivial but aren’t.
  3. Patch the chat template before the first client hits the API. The reasoning_effort validation is a footgun; better to relax it up front than to debug it during an incident.
  4. Set --parallel 1 for the full 64K context. More slots means smaller per-slot context, which defeats the point of having a 256K-context model. One slot at full context is the right default for single-tenant deployments.

Conclusion

128-billion-parameter LLMs on commodity datacenter hardware (V100) are a solved problem if you pick the right stack. The stack is llama.cpp + a properly-merged single-file GGUF + a chat-template patch for client leniency. vLLM is the wrong default for Volta — it will fight you at every quantization step and lose to the Triton bug on inference.

How to run Nemotron 3 Super 120B FP8 on vLLM on 2 x Nvidia H200 143GB VRAM

Running a 120-billion-parameter reasoning model across two GPUs is no longer an exotic exercise — in 2026, with NVIDIA’s FP8 quantization and the Hopper architecture’s native FP8 tensor cores, it is an afternoon project that delivers genuinely production-grade speed. This article walks through the complete, end-to-end deployment of Nemotron-3-Super-120B-A12B on a server with 2× NVIDIA H200 (143 GB each), served by vLLM behind a standard OpenAI-compatible API, with a 1-million-token context window, thinking/reasoning mode, tool calling, multi-turn conversations, and API-key authentication.

There is already an excellent guide for running this model on a single Blackwell RTX PRO 6000 using the NVFP4 (4-bit floating point) checkpoint. This article is the companion piece for the Hopper / H200 case — and as you’ll see, the model variant and several flags must change, because Hopper and Blackwell have different native low-precision capabilities. They are a different hardware, so the installation process turned out to be quite different.

The Target Hardware

Component Specification
GPU 2× NVIDIA H200
VRAM (per GPU) 143 GB (143,771 MiB)
Total VRAM ~287 GB
Compute Capability sm_90 (Hopper)
Driver 580.105.08 / CUDA 13.0
System RAM 440 GB
Storage 1.5 TB NVMe
OS Ubuntu 24.04.3 LTS

The Critical Decision: FP8, Not NVFP4

This is the single most important thing to get right, and it is where a blind copy of the Blackwell guide will bite you.

Nemotron-3-Super-120B-A12B ships in three quantization variants on Hugging Face:

  • NVFP4 — 4-bit floating point weights (~80 GB). Requires Blackwell architecture (B200/B300, RTX PRO 6000 Blackwell) with native FP4 tensor cores. On Hopper, FP4 has no native tensor-core support — it would be emulated and slow.
  • FP8 — 8-bit floating point weights (~128 GB). This is the native sweet spot for Hopper (H100/H200). The H200’s FP8 tensor cores run this at full speed.
  • BF16 — 16-bit, ~240 GB. Overkill and a waste of VRAM unless you need the absolute maximum accuracy.

For 2× H200, the correct choice is nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8. At ~128 GB it fits comfortably across both GPUs with tensor parallelism, leaving roughly 160 GB of headroom for a large FP8 KV cache — which is exactly what lets you run the full 1M-token context.

Rule of thumb: Blackwell (sm_120+) ? NVFP4. Hopper (sm_90) ? FP8. Do not mix these up — the performance difference is dramatic.

The Model: Nemotron-3-Super-120B-A12B

A quick refresher on why this model is special and how it achieves its speed:

  • 120 billion total parameters, only 12 billion active per token — a hybrid Latent Mixture-of-Experts design (22 experts routed out of 512 per layer). This is why it is fast despite its size.
  • Mamba-2 hybrid layers interleaved with MoE and attention layers. The Mamba layers have O(1) recurrent state (no growing KV cache), which is precisely how the model supports a 1,048,576-token (1M) context.
  • Configurable reasoning/thinking mode via the chat template’s enable_thinking flag — the model produces a reasoning trace, then a final answer.
  • Native tool calling (function calling) and multi-turn conversation support.
  • Multi-Token Prediction (MTP) layers for faster speculative generation.

On Hugging Face it lives at nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8. It is public and ungated — no token or license acceptance required to download.

Step 1 — Connect to the Server

ssh root@YOUR-SERVER-IP

Verify the GPUs are visible and idle:

nvidia-smi

You should see two H200s with ~143 GB each and 0 MiB used.

Step 2 — Install Docker and GPU Passthrough

Unlike the Blackwell guide (where Docker was pre-installed), our H200 server had no Docker at all — only the nvidia-container-toolkit package. Install Docker first:

apt-get update
apt-get install -y docker.io
systemctl enable docker
systemctl start docker

Then configure the NVIDIA container runtime so Docker can pass GPUs through to containers:

nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

Verify both GPUs are visible inside a container:

docker run --rm --runtime=nvidia --gpus all ubuntu:22.04 \
  nvidia-smi --query-gpu=name,memory.total --format=csv,noheader

You should see:

NVIDIA H200, 143771 MiB
NVIDIA H200, 143771 MiB

Step 3 — Create a Python Environment

We need a small venv on the host for downloading the model. On Ubuntu 24.04, the python3-venv package must be installed explicitly:

apt-get install -y python3.12-venv

python3 -m venv /opt/nemotron-venv
/opt/nemotron-venv/bin/pip install --upgrade pip wheel setuptools
/opt/nemotron-venv/bin/pip install "huggingface_hub[cli]" hf_transfer

Step 4 — Download the Model (128 GB)

mkdir -p /opt/models

HF_HUB_CACHE=/opt/models /opt/nemotron-venv/bin/python - <<'PY'
from huggingface_hub import snapshot_download
snapshot_download(
    "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8",
    cache_dir="/opt/models",
    max_workers=16,
)
PY

This pulls 26 safetensors shards (~128 GB) plus the chat template, custom modeling code (modeling_nemotron_h.py), and a reasoning parser (super_v3_reasoning_parser.py). With 16 parallel workers on a fast connection, expect roughly 10–15 minutes.

Note the snapshot path — you’ll need it later:

find /opt/models -type d -path "*Nemotron-3-Super*FP8*/snapshots/*"

Step 5 — Generate an API Key

Create a random API key that clients must present as a Bearer token:

API_KEY="nm-$(openssl rand -hex 24)"
echo "$API_KEY"

mkdir -p /opt/nemotron-serve
echo "API_KEY=$API_KEY" > /opt/nemotron-serve/.api_key
chmod 600 /opt/nemotron-serve/.api_key

Keep this key secret — every request to the server must carry it.

Step 6 — Pull the vLLM Image

The model’s README pins vLLM 0.20.0. Use the official Docker image — it bundles a matched CUDA 13.x toolchain so all the FP8 kernels compile and load cleanly. Do not be tempted by pip install vllm; the pip path hits JIT compiler/header mismatches on Hopper that the Docker image avoids entirely.

docker pull vllm/vllm-openai:v0.20.0

Step 7 — The Launch Script

This is the heart of the deployment. Create the launch script:

cat > /opt/nemotron-serve/launch_docker.sh <<'EOF'
#!/bin/bash
set -euo pipefail

API_KEY="$(grep -oP 'nm-[0-9a-f]+' /opt/nemotron-serve/.api_key)"
# Path as it appears INSIDE the container (the HF_HOME mount point)
MODEL_PATH="/root/.cache/huggingface/models--nvidia--NVIDIA-Nemotron-3-Super-120B-A12B-FP8/snapshots/SNAPSHOT_HASH"

echo "Using model path: $MODEL_PATH"
echo "Using API key:    $API_KEY"

docker rm -f nemotron-serve 2>/dev/null || true

exec docker run --name nemotron-serve \
  --runtime nvidia --gpus all \
  --network host \
  --shm-size 16g \
  --restart unless-stopped \
  -e VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 \
  -e HF_HUB_OFFLINE=1 \
  -e HF_HOME=/root/.cache/huggingface \
  -e HF_MODULES_CACHE=/root/.cache/hf_modules \
  -v /opt/models:/root/.cache/huggingface:ro \
  -v /opt/hf-cache/modules:/root/.cache/hf_modules:rw \
  vllm/vllm-openai:v0.20.0 \
    --model "$MODEL_PATH" \
    --served-model-name nvidia/nemotron-3-super \
    --host 0.0.0.0 \
    --port 20000 \
    --api-key "$API_KEY" \
    --async-scheduling \
    --dtype auto \
    --kv-cache-dtype fp8 \
    --tensor-parallel-size 2 \
    --enable-expert-parallel \
    --trust-remote-code \
    --gpu-memory-utilization 0.90 \
    --enable-chunked-prefill \
    --max-model-len 1048576 \
    --max-num-seqs 32 \
    --mamba-ssm-cache-dtype float32 \
    --reasoning-parser nemotron_v3 \
    --enable-auto-tool-choice \
    --tool-call-parser qwen3_coder
EOF

chmod +x /opt/nemotron-serve/launch_docker.sh
mkdir -p /opt/hf-cache/modules

Replace SNAPSHOT_HASH with the actual hash from Step 4.

What each key flag does

  • --tensor-parallel-size 2 — splits the model across both H200s. Essential for a 128 GB checkpoint.
  • --enable-expert-parallel — distributes the MoE experts across GPUs (NVIDIA’s recommended setting for multi-GPU Hopper).
  • --max-model-len 1048576 + VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 — unlocks the full 1M-token context. The model’s max_position_embeddings is 262144, but the Mamba/attention hybrid legitimately extends to 1M; the env var lets vLLM exceed the conservative default.
  • --kv-cache-dtype fp8 — FP8 KV cache, halving attention memory and fitting longer contexts in VRAM.
  • --mamba-ssm-cache-dtype float32 — the Mamba-2 recurrent state in fp32 (required for the hybrid layers).
  • --reasoning-parser nemotron_v3 — loads the parser that splits reasoning traces into the reasoning field of the response.
  • --enable-auto-tool-choice --tool-call-parser qwen3_coder — enables function-calling in the OpenAI-compatible API.
  • --max-num-seqs 32 — allows up to 32 concurrent sequences for batching.
  • --enable-chunked-prefill — processes long prompts in chunks for smoother scheduling.

Launch it

nohup /opt/nemotron-serve/launch_docker.sh > /opt/nemotron-serve/docker.log 2>&1 &

Startup takes about 2 minutes: ~30 s to load 128 GB of weights across two GPUs, then ~90 s for torch.compile and CUDA graph capture. The server is ready when the log shows:

INFO ... Application startup complete.

Follow along with:

tail -f /opt/nemotron-serve/docker.log

The Two Pitfalls (and Their Fixes)

I hit both of these during bring-up. The launch script above already avoids them, but they are worth understanding.

Pitfall #1 — --swap-space not recognized

Symptom: vllm: error: unrecognized arguments: --swap-space 0.

Cause: The model’s README (written for a newer vLLM) includes --swap-space 0, but vLLM 0.20.0’s argument parser does not accept this flag.

Fix: Simply remove --swap-space 0 from the launch script.

Pitfall #2 — Model path resolution inside the container

Symptom: HFValidationError: Repo id must be in the form 'repo_name' or 'namespace/repo_name': '/opt/models/...' or LocalEntryNotFoundError: Cannot find an appropriate cached snapshot folder.

Cause: There are two traps here. First, if you pass the host path (/opt/models/...), it does not exist inside the container (the weights are mounted at /root/.cache/huggingface/...). Second, if you pass the repo ID (nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8) with HF_HUB_OFFLINE=1, vLLM’s get_model_path() tries to look up a specific revision and fails because offline mode prevents the lookup.

Fix: Pass the path as it appears inside the container — i.e., under the HF_HOME mount point (/root/.cache/huggingface/models--nvidia--.../snapshots/HASH), not the host path. The launch script above does exactly this.

Step 8 — Verify Everything Works

Load the API key into a variable:

API_KEY=$(grep -oP 'nm-[0-9a-f]+' /opt/nemotron-serve/.api_key)

List models — confirms the 1M context

curl -s http://localhost:20000/v1/models \
  -H "Authorization: Bearer $API_KEY" | python3 -m json.tool

The response includes "max_model_len": 1048576 — your 1M context is active.

Thinking / reasoning mode

Pass "reasoning": {"effort": "auto"} in the request body to activate the reasoning parser. The model’s thinking trace appears in the reasoning field, and the final answer in content:

curl -s http://localhost:20000/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nvidia/nemotron-3-super",
    "messages": [{"role":"user","content":"A bat and ball cost $1.10 total. The bat costs $1.00 more than the ball. How much is the ball?"}],
    "max_tokens": 500,
    "temperature": 0.6,
    "reasoning": {"effort": "auto"}
  }'

You can also toggle thinking per-request via the chat template:

"chat_template_kwargs": {"enable_thinking": true}

Tool calling

curl -s http://localhost:20000/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nvidia/nemotron-3-super",
    "messages": [{"role":"user","content":"What is the weather in Tokyo?"}],
    "max_tokens": 400,
    "tools": [{"type":"function","function":{
      "name":"get_weather",
      "description":"Get current weather for a city",
      "parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}
    }}]
  }'

The model returns "finish_reason": "tool_calls" with a structured call to get_weather({"city": "Tokyo"}).

Multi-turn conversations

Simply pass the full message history — the model retains context across turns:

curl -s http://localhost:20000/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nvidia/nemotron-3-super",
    "messages": [
      {"role":"user","content":"My name is Alice. Remember it."},
      {"role":"assistant","content":"Nice to meet you, Alice!"},
      {"role":"user","content":"What is my name?"}
    ],
    "max_tokens": 200
  }'

API key enforcement

# Wrong key ? HTTP 401
curl -o /dev/null -w "%{http_code}" http://localhost:20000/v1/models \
  -H "Authorization: Bearer wrong-key"

# No key ? HTTP 401
curl -o /dev/null -w "%{http_code}" http://localhost:20000/v1/models

# Correct key ? HTTP 200
curl -o /dev/null -w "%{http_code}" http://localhost:20000/v1/models \
  -H "Authorization: Bearer $API_KEY"

Benchmarks

Measured against the live server with single requests (unless noted), using the OpenAI-compatible API:

Test Prompt Output Wall time Throughput
Short generation 20 tok ~50 tok 0.35 s 144 tok/s
Medium generation 26 tok ~300 tok 1.87 s 160 tok/s
Long generation 37 tok ~800 tok 4.91 s 163 tok/s
Large prefill 8,827 tok 132 tok 8.16 s ~4,507 tok/s (prefill+decode)
Concurrent (8 parallel) 200 tok 1,600 tok 5.69 s 281 tok/s aggregate

~160 tokens/second steady-state decode from a 120B-parameter model — and remarkably flat across output lengths (144 ? 163), confirming the FP8 KV cache and Mamba SSM state are healthy. This is roughly 1.7× faster than the ~94 tok/s achieved on a single Blackwell RTX PRO 6000, thanks to the H200’s higher memory bandwidth plus tensor parallelism across two GPUs.

Under 8-way concurrency, aggregate output throughput reaches 281 tok/s, demonstrating that the batching engine (--max-num-seqs 32) scales well. For context: a full 800-token coding response generates in under 5 seconds.

Using the Server

The server speaks the standard OpenAI API. Point any OpenAI-compatible client at it:

from openai import OpenAI

client = OpenAI(
    base_url="http://YOUR-SERVER-IP:20000/v1",
    api_key="nm-YOUR-API-KEY",
)

resp = client.chat.completions.create(
    model="nvidia/nemotron-3-super",
    messages=[{"role": "user", "content": "Design a REST API for a todo app."}],
    max_tokens=2000,
    temperature=0.6,
    extra_body={"reasoning": {"effort": "auto"}},
)
print(resp.choices[0].message.reasoning)  # thinking trace
print(resp.choices[0].message.content)     # final answer

Tool calling works the same way — pass tools=[...] and the model emits structured function calls. Multi-turn conversations work as expected (pass the full message history).

Operations Cheatsheet

# check container status
docker ps --filter name=nemotron-serve

# tail logs
docker logs -f nemotron-serve

# restart (picks up the launch script)
docker restart nemotron-serve

# GPU usage
nvidia-smi

# rotate the API key
API_KEY="nm-$(openssl rand -hex 24)"
echo "API_KEY=$API_KEY" > /opt/nemotron-serve/.api_key
docker restart nemotron-serve

GPU Memory Breakdown

With the 128 GB FP8 checkpoint split across two H200s at 90% memory utilization:

GPU 0: 129,757 MiB / 143,771 MiB used
GPU 1: 129,757 MiB / 143,771 MiB used

That is ~65 GB of weights per GPU plus a large FP8 KV cache and Mamba SSM state — leaving healthy headroom for the 1M-token context window.

Conclusion

The era of running a 120-billion-parameter reasoning model on affordable datacenter GPUs is firmly here. With two H200s and vLLM, you get a 1M-context reasoning model at ~160 tok/s, with native tool calling and thinking mode, behind a standard OpenAI-compatible API secured with an API key.

The key insight — and the reason this article exists alongside the Blackwell guide — is matching the quantization variant to your architecture: NVFP4 for Blackwell, FP8 for Hopper. Get that one decision right, and the rest of the deployment is straightforward infrastructure work: Docker GPU passthrough, the correct model path inside the container, and the right cache/memory flags. Once those are solved, the server is stable, fast, and ready for production traffic.


Model: NVIDIA-Nemotron-3-Super-120B-A12B-FP8, governed by the NVIDIA Nemotron Open Model License. Serving: vLLM 0.20.0 on 2× NVIDIA H200. All benchmarks are real, measured live on the described hardware.

How to setup Nemotron 3 Super 120B + vLLM as local LLM on Nvidia RTX6000 96GB VRAM

Introduction

Running a 120-billion-parameter reasoning model on a single workstation GPU used to be impossible. In 2026, thanks to NVIDIA’s NVFP4 quantization and the Blackwell architecture’s native FP4 tensor cores, it is now a practical afternoon project. This article walks through the complete, end-to-end deployment of nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 on a single RTX PRO 6000 Blackwell (96 GB) — every command, every pitfall, and the real benchmarks at the end.

We will cover: connecting to the server, choosing the right model artifact, installing the serving stack, fixing the four problems you will hit on Blackwell, and measuring throughput.

The Target Hardware

Component Specification
GPU NVIDIA RTX PRO 6000 Blackwell Workstation Edition
VRAM 96 GB (97,887 MiB)
Compute Capability sm_120 (Blackwell)
Driver 580.95.05 / CUDA 13.0
System RAM 147 GB
Storage 485 GB NVMe
OS Ubuntu 22.04

The Model: Nemotron-3-Super-120B-A12B

This is the star of the show. Nemotron-3-Super-120B-A12B is a hybrid Latent Mixture-of-Experts model:

  • 120 billion total parameters, but only 12 billion active per token (22 experts routed out of 512 per layer). This is why it runs fast despite its size.
  • Architecture: interleaved Mamba-2 state-space layers, MoE layers, and select attention layers. The Mamba layers have O(1) recurrent state — they do not grow a KV cache — which is exactly how the model supports a 1,048,576-token (1M) context on a single GPU.
  • NVFP4 quantization: experts are stored in 4-bit floating point (group size 16); attention and shared layers remain FP8. This shrinks the weights to ~80 GB, fitting comfortably in 96 GB VRAM with room for a large KV cache.
  • Capabilities: configurable reasoning/thinking mode (via the chat template’s enable_thinking flag), native tool calling, and a 1M-token context window.

On Hugging Face it lives at nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4. It is public and ungated — no token or license-acceptance gate is required to download it.

Important naming note: There is no model literally called “Nemotron 3 Super NVFP4.” The official repository id is nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4. An older, unrelated family member (nvidia/Llama-3.3-Nemotron-Super-49B-v1) is a DeciLM/NAS model — make sure you grab the right one.

Step 1 — Create a Python Environment

We do not need a full CUDA toolkit on the host; vLLM ships with everything inside its Docker image. We do need a small venv on the host for downloading the model and running benchmark scripts.

python3 -m venv /opt/nemotron-venv
/opt/nemotron-venv/bin/pip install --upgrade pip wheel setuptools
/opt/nemotron-venv/bin/pip install "huggingface_hub[cli]" hf_transfer

Step 2 — Download the Model (80 GB)

export HF_HUB_CACHE=/opt/models
mkdir -p /opt/models

/opt/nemotron-venv/bin/python - <<'PY'
from huggingface_hub import snapshot_download
snapshot_download(
    "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
    cache_dir="/opt/models",
    max_workers=16,
)
PY

This pulls 17 safetensors shards (~80 GB) plus the chat template and a custom reasoning parser (super_v3_reasoning_parser.py). Copy the parser somewhere stable — we will mount it into the container:

MODEL_PATH=$(find /opt/models -type d -path "*Nemotron-3-Super*/snapshots/*" | head -1)
cp "$MODEL_PATH/super_v3_reasoning_parser.py" /opt/super_v3_reasoning_parser.py

Step 3 — Generate an API Key

API_KEY="nm-$(openssl rand -hex 24)"
echo "$API_KEY"
mkdir -p /opt/nemotron-serve
echo "API_KEY=$API_KEY" > /opt/nemotron-serve/.api_key
chmod 600 /opt/nemotron-serve/.api_key

Keep this key secret — every request to the server must carry it as a Bearer token.

Step 4 — Fix Docker GPU Passthrough (Pitfall #1)

The host had Docker installed but no nvidia-container-toolkit, so --gpus all failed with could not select device driver "" with capabilities: [[gpu]]. The NVIDIA apt repository was already configured, so the fix was a one-liner:

apt-get install -y nvidia-container-toolkit
nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

Verify the GPU is visible inside a container:

docker run --rm --runtime=nvidia --gpus all ubuntu:22.04 \
  nvidia-smi --query-gpu=name,memory.total --format=csv,noheader

# NVIDIA RTX PRO 6000 Blackwell Workstation Edition, 97887 MiB

Step 5 — Pull the vLLM Image

The model README pins vLLM 0.20.0 with a purpose-built Blackwell image. Do not be tempted by a newer pip install vllm — the pip path hit two more problems (see the pitfalls below). Use the official image, which bundles a matched CUDA 13.x toolkit:

docker pull vllm/vllm-openai:v0.20.0

Step 6 — Launch the Server

Create the launch script. The flags come straight from the model’s official README, with two critical additions discovered during bring-up (explained in the pitfalls section).

cat > /opt/nemotron-serve/launch_docker.sh <<'EOF'
#!/bin/bash
set -euo pipefail

API_KEY="$(grep -oP 'nm-[0-9a-f]+' /opt/nemotron-serve/.api_key)"
MODEL_PATH="/root/.cache/huggingface/models--nvidia--NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4/snapshots/4f0cf9daaeb7a4d5e23f80a00e7ed15f0e03caf6"

docker rm -f nemotron-serve 2>/dev/null || true

exec docker run --name nemotron-serve \
  --runtime nvidia --gpus all \
  --network host \
  --shm-size 16g \
  --restart unless-stopped \
  -e VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 \
  -e HF_HUB_OFFLINE=1 \
  -e HF_HOME=/root/.cache/huggingface \
  -e HF_MODULES_CACHE=/root/.cache/hf_modules \
  -v /opt/models:/root/.cache/huggingface:ro \
  -v /opt/hf-cache/modules:/root/.cache/hf_modules:rw \
  -v /opt/super_v3_reasoning_parser.py:/app/super_v3_reasoning_parser.py:ro \
  vllm/vllm-openai:v0.20.0 \
    --model "$MODEL_PATH" \
    --served-model-name nvidia/nemotron-3-super \
    --host 0.0.0.0 \
    --port 20000 \
    --api-key "$API_KEY" \
    --async-scheduling \
    --dtype auto \
    --kv-cache-dtype fp8 \
    --tensor-parallel-size 1 \
    --trust-remote-code \
    --gpu-memory-utilization 0.90 \
    --enable-chunked-prefill \
    --max-model-len 1048576 \
    --max-num-seqs 32 \
    --mamba-ssm-cache-dtype float16 \
    --reasoning-parser-plugin /app/super_v3_reasoning_parser.py \
    --reasoning-parser super_v3 \
    --enable-auto-tool-choice \
    --tool-call-parser qwen3_coder
EOF
chmod +x /opt/nemotron-serve/launch_docker.sh

# writable HF modules dir for trust-remote-code
mkdir -p /opt/hf-cache/modules

# launch
nohup /opt/nemotron-serve/launch_docker.sh > /opt/nemotron-serve/docker.log 2>&1 &

What each key flag does

  • --max-model-len 1048576 + VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 — unlocks the full 1M-token context (the model’s max_position_embeddings is 262144, but the Mamba/attention hybrid legitimately extends to 1M; the env var lets vLLM exceed the conservative default).
  • --kv-cache-dtype fp8 — FP8 KV cache, halving attention memory and fitting longer contexts in VRAM.
  • --mamba-ssm-cache-dtype float16 — the Mamba-2 recurrent state in fp16 (required for the hybrid layers).
  • --reasoning-parser super_v3 + --reasoning-parser-plugin — loads the custom parser that splits reasoning traces into the reasoning_content field.
  • --enable-auto-tool-choice --tool-call-parser qwen3_coder — enables function-calling in the OpenAI-compatible API.
  • --max-num-seqs 32 — see Pitfall #4.

Startup takes about 90 seconds: ~25 s to load 80 GB of weights, then ~60 s for torch.compile and CUDA graph capture. The server is ready when the log shows:

INFO ... Application startup complete.

Step 7 — Verify It Works

API_KEY=$(grep -oP 'nm-[0-9a-f]+' /opt/nemotron-serve/.api_key)

# list models — confirms the 1M context
curl -s http://localhost:20000/v1/models \
  -H "Authorization: Bearer $API_KEY" | python3 -m json.tool

# chat
curl -s http://localhost:20000/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nvidia/nemotron-3-super",
    "messages": [{"role":"user","content":"What is 17 * 23?"}],
    "max_tokens": 500,
    "temperature": 1.0,
    "top_p": 0.95
  }'

The Four Pitfalls (and Their Fixes)

I hit all four of these. If you follow the script above you will avoid them — but they are worth understanding.

Pitfall #1 — Docker GPU passthrough missing

Symptom: could not select device driver "" with capabilities: [[gpu]].

Cause: nvidia-container-toolkit not installed.

Fix: apt-get install -y nvidia-container-toolkit && nvidia-ctk runtime configure --runtime=docker && systemctl restart docker.

Pitfall #2 — Read-only filesystem for trust-remote-code

Symptom: OSError: [Errno 30] Read-only file system: '/root/.cache/huggingface/modules'.

Cause: The model uses trust-remote-code, which writes its custom nemotron_h Python module into a cache directory. I had mounted the HF cache as :ro.

Fix: Keep the weights mount read-only, but add a separate writable volume for HF_MODULES_CACHE: -e HF_MODULES_CACHE=/root/.cache/hf_modules -v /opt/hf-cache/modules:/root/.cache/hf_modules:rw.

Pitfall #3 — max_num_seqs exceeds available Mamba blocks

Symptom: ValueError: max_num_seqs (1024) exceeds available Mamba cache blocks (685). Each decode sequence requires one Mamba cache block, so CUDA graph capture cannot proceed.

Cause: The hybrid Mamba-2 layers allocate one fixed-size state block per concurrent sequence. vLLM’s default max_num_seqs of 1024 was too high for the VRAM left after the 69 GB of weights.

Fix: Add --max-num-seqs 32 (the model README suggests 4 for DGX Spark, but 32 fits comfortably in the 685 available blocks and gives better batch throughput).

Pitfall #4 — Why not just pip install vllm?

I tried a native pip install vllm (which pulled v0.26.0 with torch 2.11 / CUDA 13). It hit two Blackwell-specific JIT problems that the Docker image avoids entirely:

  1. Missing nvcc at runtime: vLLM’s CUTLASS NVFP4 kernels need to JIT-compile. The host had no CUDA toolkit, only the driver. Fixable by pointing CUDA_HOME at the pip-installed nvidia/cu13 package — but that led to problem #2.
  2. CUDA compiler / header mismatch: FlashInfer’s sm_120 FP4/NVFP4 GEMM JIT (gen_gemm_sm120) failed with #error "CUDA compiler and CUDA toolkit headers are incompatible". The pip nvidia-cuda-nvcc (13.3) disagreed with the bundled CCCL/libcudacxx headers.

The purpose-built vllm/vllm-openai:v0.20.0 image contains a coherently-matched CUDA 13.x toolchain, so all the NVFP4 kernels compile and load cleanly on the first try. Use the Docker image.

Benchmarks

Measured against the live server with a single concurrent request:

Test Prompt Output Wall time Throughput
Short generation 25 tok 60 tok 0.66 s 90.3 tok/s
Medium generation 28 tok 300 tok 3.20 s 93.7 tok/s
Long generation 38 tok 800 tok 8.51 s 94.0 tok/s
Large prefill 2,427 tok 120 tok 1.43 s ~1,780 tok/s (prefill+decode)

~94 tokens/second steady-state decode from a 120B-parameter model on a single workstation GPU. That is remarkable, and it comes from three things working together: NVFP4 weights (4× compression with minimal accuracy loss), Blackwell’s native FP4 tensor cores, and the 12B-active Mamba/MoE hybrid architecture that keeps the per-token FLOPs low. Throughput is flat across output lengths, confirming the KV and Mamba caches are healthy.

Using the Server

The server speaks the standard OpenAI API. Point any OpenAI-compatible client at it:

from openai import OpenAI

client = OpenAI(
    base_url="http://175.28.230.22:20000/v1",
    api_key="nm-<your-key>",
)

resp = client.chat.completions.create(
    model="nvidia/nemotron-3-super",
    messages=[{"role": "user", "content": "Design a REST API for a todo app."}],
    max_tokens=2000,
    temperature=1.0,
    top_p=0.95,
)
print(resp.choices[0].message.content)

Tool calling works the same way — pass tools=[...] and the model emits structured function calls. Multi-turn conversations work as expected (pass the full message history).

Thinking mode

The model’s chat template defaults to enable_thinking=True. When thinking is on, the model first produces a reasoning trace inside <think>...</think> tags, then the final answer. The super_v3 reasoning parser splits these into the response’s reasoning_content and content fields. To toggle it per-request:

resp = client.chat.completions.create(
    model="nvidia/nemotron-3-super",
    messages=[{"role": "user", "content": "Prove the Pythagorean theorem."}],
    extra_body={"chat_template_kwargs": {"enable_thinking": True}},
    ...
)

Operations Cheatsheet

# check container status
docker ps --filter name=nemotron-serve

# tail logs
docker logs -f nemotron-serve

# restart (picks up the launch script)
docker restart nemotron-serve

# GPU usage
nvidia-smi

# rotate the API key
API_KEY="nm-$(openssl rand -hex 24)"
echo "API_KEY=$API_KEY" > /opt/nemotron-serve/.api_key
docker restart nemotron-serve

Conclusion

The era of “a 120-billion-parameter model on one GPU” has arrived. The Nemotron-3-Super-120B-A12B design — LatentMoE with Mamba-2 hybrid layers, trained natively in NVFP4 — is purpose-built for exactly this hardware moment. With an RTX PRO 6000 Blackwell you get a 1M-context reasoning model at 94 tok/s, behind a standard OpenAI-compatible API, for the cost of the electricity to run one card.

The tricky parts are all infrastructure: getting Docker GPU passthrough working, matching the CUDA toolchain for the NVFP4 JIT kernels, and sizing the Mamba/sequence caches correctly. Once those are solved (and the launch script above solves them), the deployment is stable and fast.


Model: NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4, governed by the NVIDIA Nemotron Open Model License. Serving: vLLM 0.20.0. All benchmarks are real, measured live on the described hardware.