Running a Full LLM Stack on DGX Spark GB10 (Your Application -> LiteLLM -> llama-swap -> vLLM / llama.cpp / Ollama)

Running a Full Multi-Model LLM Stack on DGX Spark (GB10) — With VRAM Orchestration

Hey Guys, I (and the AI) have been working on this for quite a while and this is what I have so far.
Once it is set up it shall make using the LLMs as easy as possible — the loading and unloading is done by llama-swap. No matter if you use vLLM, llama.cpp or Ollama.
LiteLLM is used to route the LLMs. You can also use it for fallbacks or adding remote models to the stack.

The biggest issue so far is speed. It works reliably but it is not fast.
If someone has a better, easier, faster, more reliable solution please let me know.

After working on this for weeks and having a working solution I asked the AI to help generate a tutorial for it and this is what came out.


Hardware: NVIDIA DGX Spark (Grace Blackwell GB10) — 128 GB unified CPU/GPU memory, SM12.1 GPU architecture, ARM64 (SBSA) CPU

What this stack gives you: A single OpenAI-compatible API endpoint that dynamically swaps 10+ models in and out of VRAM on demand — 4B GGUF models, 30B FP8, 120B+ MoE, VLMs — with no manual docker run required. Requests come in through LiteLLM, which routes to llama-swap, which spins up the right vLLM/llama.cpp container for that model and kills it again after idle timeout.

This guide builds directly on the outstanding work of:

  • @eugrspark-vllm-docker provides the pre-built vLLM + FlashInfer wheels compiled specifically for the GB10. Without this, building a working vLLM image from source takes 2–4 hours and frequently breaks on nightly. He also authored llama-benchy, the standardized benchmarking tool used throughout this guide. Massive thanks — this stack would not be usable in practice without both of those projects.
  • @christopherowenspark-vllm-mxfp4-docker and the associated forks of vLLM, FlashInfer, and CUTLASS that enable native MXFP4 quantization on GB10. This is what makes OpenAI GPT-OSS-120B actually run at ~57 tok/s on a single Spark.

GitHub repo (all Dockerfiles + configs): GitHub - mARTin-B78/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama: LLM Stack for nVidia DGX Spark containing LiteLLM, LamaSwap, vLLM, Llama.cpp and ollama · GitHub


Architecture Overview

Your Client like Open-WebUI / VSCode / OpenClaw / Homeassistant / etc...
        ↓
   LiteLLM :14000          ← unified API key, routing, model aliases
        ↓
  llama-swap :28080         ← VRAM orchestrator, loads/evicts on demand
     /    |    \
vLLM   vLLM   llama.cpp    ← one ephemeral container per model
(30B)  (120B)  (GGUF)

All containers share the dgx_net Docker bridge network. Model containers attach to llama-swap’s network namespace (--network container:llama-swap), so they reach localhost:PORT inside llama-swap’s own network — this is how llama-swap knows when a model is ready.

Key insight for the GB10’s unified memory: CUDA sees ~121.7 GiB of the 128 GB physical RAM. Every model you load eats into that shared pool. llama-swap’s gpu_memory_utilization setting is your lever — it tells vLLM “claim at most X% of 121.7 GiB”.


Benchmark Results (2026-04-22, single DGX Spark GB10)

Measured with llama-benchy by @eugr.
pp2048 = prompt processing 2048 tokens (tok/s, higher is better) · tg128 = token generation 128 tokens (tok/s, higher is better)

S tier — Small / fast

Model Engine pp2048 (tok/s) tg128 (tok/s) Notes
Nemotron-3-Nano-4B-FP8 vLLM 8179 39.8 Instant responder, great for orchestration tasks
Nemotron-3-Nano-30B-A3B-NVFP4 vLLM 7417 55.9 Fastest 30B generation on the Spark
Qwen3.5-35B-Uncensored-Q4_K_M llama.cpp 1798 57.1 GGUF via llama.cpp, competitive with FP8 vLLM

M tier — Medium

Model Engine pp2048 (tok/s) tg128 (tok/s) Notes
Qwen3.5-35B-A3B-FP8 vLLM 4439 49.1 Solid all-rounder with native reasoning
Qwen3.6-35B-A3B-FP8 vLLM 4969 49.5 Slightly faster prefill than 3.5
Qwen3-VL-30B-A3B-Instruct-FP8 vLLM 9217 51.9 Vision model, exceptional prefill speed
Qwen3-Omni-30B-A3B-Instruct vLLM 5227 30.1 Audio + image + text multimodal
Qwen3-Coder-Next-FP8-Dynamic vLLM 3946 32.9 Full-precision coder model
Qwen3-Coder-Next-int4-AutoRound vLLM 4425 66.7 INT4 quant — fastest generation in M tier
Mistral-Small-24B-Instruct-2501 vLLM 2064 4.5 Dense model, limited by memory bandwidth

L tier — Large (solo only, evicts all others)

Model Engine pp2048 (tok/s) tg128 (tok/s) Notes
Qwen3.5-122B-A10B-int4-AutoRound vLLM (tf5) 2048 23.8 Best reasoning, fits in ~90 GB
GPT-OSS-120B (MXFP4) vLLM (mxfp4) 4703 56.4 Exceptional speed for a 120B model
Nemotron-3-Super-120B-A12B-NVFP4 vLLM 1823 14.5 NVIDIA’s flagship reasoning model

GPT-OSS-120B at 56 tok/s is the standout — a 120B model generating tokens faster than most 35B models thanks to CUTLASS MXFP4 kernels on the Blackwell architecture.


Prerequisites

  • DGX Spark with Ubuntu 22.04/24.04
  • Docker + NVIDIA Container Runtime (nvidia-container-runtime as default runtime in /etc/docker/daemon.json)
  • Portainer (optional but recommended for managing the stack)
  • GitHub account with a Personal Access Token (PAT) for GHCR image publishing
  • huggingface-cli for model downloads
// /etc/docker/daemon.json
{
    "default-runtime": "nvidia",
    "runtimes": {
        "nvidia": {
            "path": "nvidia-container-runtime",
            "args": []
        }
    }
}

Create the shared Docker network (one-time):

docker network create dgx_net

Step 1 — Clone the Repo

git clone https://github.com/mARTin-B78/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama.git
cd dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama

Create your .env file:

cp .env.sample .env
# Edit .env and fill in:
#   GH_USER=your-github-username
#   GH_PAT=ghp_your_personal_access_token
#   LLM_ROOT_PATH=/home/YOUR_USER/LLMs
#   REPO_CONFIG_PATH=/home/YOUR_USER/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama
#   LITELLM_MASTER_KEY=sk-choose-a-secure-key
#   POSTGRES_PASSWORD=choose-a-db-password

Step 2 — Build and Push the Base Images

The build_and_push.sh script builds and pushes five images to your GitHub Container Registry:

Image Purpose Dockerfile
llama-cpp-spark llama.cpp compiled for SM12.1 (arch 121) llama-cpp/llama-cpp.Dockerfile
llama-swap-spark llama-swap proxy (ARM64 binary) llama-swap/llama-swap.Dockerfile
ollama-spark Ollama (pinned version mirror) ollama/ollama.Dockerfile
litellm-spark LiteLLM gateway (stable release mirror) LiteLLM/litellm.Dockerfile
vllm-spark vLLM from nightly wheels (lightweight) vllm/vllm.Dockerfile
bash build_and_push.sh

What the llama.cpp build does differently for the Spark: It uses nvidia/cuda:13.1.0-devel-ubuntu24.04 (CUDA 13.1 is required to compile for architecture 121), creates the missing libcuda.so.1 ARM64 stub, and compiles with -DCMAKE_CUDA_ARCHITECTURES="121". Pre-built ARM64 llama.cpp binaries from most distros will silently fall back to CPU-only — this build ensures you actually use the GPU.


Step 3 — Build the vLLM Model-Serving Images

Credit: @eugrspark-vllm-docker

The build system in vllm/build/spark-vllm-docker/ downloads pre-built vLLM + FlashInfer wheels from eugr’s GitHub releases. These wheels are compiled specifically for the GB10 (CUDA 13.1, SM12.1a, ARM64 SBSA) and updated regularly. Without them, every build requires compiling FlashInfer and vLLM from source — a 2–4 hour process. The build script automatically falls back to source compilation if the pre-built wheels aren’t available or if you pass --rebuild-vllm/--rebuild-flashinfer. Thank you @eugr for maintaining this — it makes iterating on the stack practical.

The build_and_push.sh’s vllm-spark image is a lightweight wrapper. For actually serving models you need the purpose-built images from vllm/build/spark-vllm-docker/:

cd vllm/build/spark-vllm-docker

Build the standard image (used for Nemotron-30B, Qwen3-VL, Qwen3-Omni, Mistral, etc.):

bash build-and-copy.sh
# produces: vllm-node

Build with Transformers 5.x (required for Qwen3.5-122B-MoE, Qwen3.6-35B, Qwen3-Coder-Next — models using the newer Mamba/hybrid architecture):

bash build-and-copy.sh --tf5
# produces: vllm-node-tf5

Build with experimental MXFP4 support (for OpenAI GPT-OSS-120B):

Credit: @christopherowenspark-vllm-mxfp4-docker

This build uses christopherowen’s forks of vLLM, FlashInfer, and CUTLASS that add native MXFP4 quantization support for the GB10’s CUTLASS kernels. The --mxfp4-backend CUTLASS + --mxfp4-layers moe,qkv,o,lm_head flags this enables are what push GPT-OSS-120B from ~35 tok/s to ~57 tok/s on a single Spark. This is not in upstream vLLM yet. Huge thanks to @christopherowen for this work.

bash build-and-copy.sh --exp-mxfp4
# produces: vllm-node-mxfp4
# takes ~1 hour (compiles FlashInfer + CUTLASS fork from source)

Build times on the GB10 using @eugr’s pre-built wheels: ~15 minutes. From source: 2–4 hours.


Step 4 — Download Models

Install the HuggingFace CLI:

pip install huggingface-hub

Download each model into the directory structure expected by the config. Replace $LLM_ROOT_PATH with your actual path (e.g. /home/YOUR_USER/LLMs):

BASE=$LLM_ROOT_PATH/vllm

# --- S tier ---
huggingface-cli download nvidia/Nemotron-3-Nano-4B-FP8 \
  --local-dir $BASE/Nvidia/Nemotron-3-Nano-4B-FP8
huggingface-cli download nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4 \
  --local-dir $BASE/Nvidia/Nemotron-3-Nano-30B-A3B-NVFP4
huggingface-cli download Intel/Qwen3-Coder-Next-int4-AutoRound \
  --local-dir $BASE/Alibaba/Qwen3-Coder-Next-int4-AutoRound

# --- M tier ---
huggingface-cli download Qwen/Qwen3.5-35B-A3B-FP8 \
  --local-dir $BASE/Alibaba/Qwen3.5-35B-A3B-FP8
huggingface-cli download Qwen/Qwen3-VL-30B-A3B-Instruct-FP8 \
  --local-dir $BASE/Alibaba/Qwen3-VL-30B-A3B-Instruct-FP8
huggingface-cli download Qwen/Qwen3-Omni-30B-A3B-Instruct \
  --local-dir $BASE/Alibaba/Qwen3-Omni-30B-A3B-Instruct
huggingface-cli download Qwen/Qwen3-Coder-Next-FP8-Dynamic \
  --local-dir $BASE/Alibaba/Qwen3-Coder-Next-FP8-Dynamic
huggingface-cli download mistralai/Mistral-Small-24B-Instruct-2501 \
  --local-dir $BASE/Mistral/Mistral-Small-24B-Instruct-2501

# --- L tier ---
huggingface-cli download Intel/Qwen3.5-122B-A10B-int4-AutoRound \
  --local-dir $BASE/Alibaba/Qwen3.5-122B-A10B-int4-AutoRound
huggingface-cli download nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 \
  --local-dir $BASE/Nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4
huggingface-cli download openai/gpt-oss-120b \
  --local-dir $BASE/OpenAI/GPT-OSS-120B

# --- GGUF (llama.cpp) ---
huggingface-cli download HauhauCS/Qwen3.5-35B-A3B-Uncensored-Aggressive \
  --include "*.gguf" --include "*.jinja" \
  --local-dir $LLM_ROOT_PATH/ollama/Alibaba/Qwen3.5-35B-A3B-Uncensored-HauhauCS-Aggressive

Step 5 — Prepare Config Files

REPO=/home/YOUR_USER/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama
mkdir -p $REPO/llama-swap/scripts

llama-swap/config.yaml — the heart of the stack. Copy and patch the sample:

cp llama-swap/config.yaml.sample llama-swap/config.yaml
sed -i "s|/path/to/LLMs|$LLM_ROOT_PATH|g" llama-swap/config.yaml
sed -i "s|/path/to/Docker|$HOME/Docker|g" llama-swap/config.yaml

Key concepts in the config:

host: "0.0.0.0"
port: 8080
readyTimeout: 3600

macros:
  host: "0.0.0.0"
  tensor_parallel: "1"

groups:
  # S group: small/fast models — swap:true means evict previous before loading next
  small-models:
    swap: true
    exclusive: true   # evict M and L groups when any S model loads

  # M group: 30B FP8 / 24B BF16
  medium-models:
    swap: true
    exclusive: true

  # L group: 120B+ MoE, always solo
  large-models:
    swap: true
    exclusive: true

models:
  MyModel-30B:
    ttl: 600          # evict after 600s idle
    readyTimeout: 600
    cmd: >
      docker run --rm --name vllm-mymodel-${PORT}
      --runtime nvidia --gpus all --ipc=host
      --network container:llama-swap
      -e NVIDIA_DISABLE_FORWARD_COMPATIBILITY=1
      -v /home/YOUR_USER/LLMs/vllm:/models/vllm
      vllm-node
      vllm serve /models/vllm/MyOrg/MyModel-30B
      --served-model-name MyModel-30B
      --host ${host} --port ${PORT}
      --gpu-memory-utilization 0.70
      --max-model-len 131072
      --kv-cache-dtype fp8
      --load-format fastsafetensors
      --enable-prefix-caching
      --enable-auto-tool-choice
      --tool-call-parser qwen3_coder
      --reasoning-parser qwen3
    cmdStop: "docker stop vllm-mymodel-${PORT}"

The --network container:llama-swap flag is the linchpin — it puts the model container inside llama-swap’s network namespace so it binds to localhost:${PORT}, which llama-swap proxies. Without it, llama-swap can’t reach the model.

See llama-swap/config.yaml.sample for the full annotated config with all models.

Critical memory math for 128 GB GB10:

CUDA-visible total: ~121.7 GiB

S tier (4B–30B quant):  0.50–0.65 × 121.7 = 61–79 GiB  → swap:true
M tier (30B–35B FP8):   0.60–0.75 × 121.7 = 73–91 GiB  → swap:true
L tier (120B+ MoE):     0.70–0.85 × 121.7 = 85–103 GiB → swap:true, solo

LiteLLM/config.yaml — copy the sample and update your master key:

cp LiteLLM/config.yaml.sample LiteLLM/config.yaml
sed -i "s|sk-your-litellm-master-key|YOUR_MASTER_KEY|g" LiteLLM/config.yaml

Each llama-swap model needs an entry pointing to http://llama-swap:8080/v1:

model_list:
  - model_name: MyModel-30B
    litellm_params:
      model: openai/MyModel-30B
      api_base: "http://llama-swap:8080/v1"
      api_key: "sk-your-master-key"
      supports_reasoning: true
      include_reasoning: true
      merge_reasoning_content_in_choices: true

See LiteLLM/config.yaml.sample for all models with their reasoning flags.


Step 6 — Dynamic VRAM Launcher for Large Models

Large MoE models (120B+ INT4/FP4) hit a recurring failure: after the previous model container exits, the CUDA memory allocator on the unified-memory GB10 doesn’t immediately return all memory to the free pool. vLLM’s startup check free_memory >= gpu_memory_utilization × total fails with a hardcoded high utilization value.

The fix is a small wrapper script that queries actual free VRAM at launch time and computes the safe utilization dynamically. Add it to llama-swap/scripts/ (this directory is mounted into the llama-swap container at /app/scripts/):

llama-swap/scripts/launch-large-model.sh — adapt MODEL_PATH, container name, and vllm flags for your model:

#!/bin/bash
# Dynamically sets --gpu-memory-utilization based on actually-free VRAM at launch time.
# Usage (from llama-swap cmd): /app/scripts/launch-large-model.sh PORT HOST
set -euo pipefail

PORT="${1}"
HOST="${2}"

# Query free/total VRAM via nvidia-smi inside the (already-cached) vllm image.
# Adds ~3s overhead — negligible vs. the several minutes this model takes to load.
MEM_LINE=$(docker run --rm --runtime nvidia --gpus all \
    vllm-node-tf5:latest \
    sh -c "nvidia-smi --query-gpu=memory.free,memory.total --format=csv,noheaders,nounits | head -1" \
    2>/dev/null || true)

FREE_MIB=$(echo "$MEM_LINE" | awk -F',' '{gsub(/ /,"",$1); print $1+0}')
TOTAL_MIB=$(echo "$MEM_LINE" | awk -F',' '{gsub(/ /,"",$2); print $2+0}')

# GB10: nvidia-smi reports 128 GiB (131072 MiB) unified memory;
# CUDA sees only 121.69 GiB (124610 MiB). Subtract that delta + 3 GiB safety margin
# so the computed value matches vLLM's view of available memory.
GMEM=$(awk -v f="$FREE_MIB" -v t_nv="$TOTAL_MIB" 'BEGIN {
    cuda_t   = 124610;
    safety   = 3072;
    overhead = (t_nv > cuda_t) ? t_nv - cuda_t : 0;
    cuda_free = f - overhead - safety;
    if (cuda_free < 0) cuda_free = 0;
    u = cuda_free / cuda_t;
    if (u > 0.85) u = 0.85;
    if (u < 0.60) u = 0.60;
    printf "%.2f", u;
}')

if [ -z "$GMEM" ] || [ "$FREE_MIB" = "0" ]; then
    echo "[auto-gmem] WARNING: VRAM query failed, using fallback 0.75"
    GMEM="0.75"
fi

echo "[auto-gmem] nvidia-smi free=${FREE_MIB}MiB / total=${TOTAL_MIB}MiB → gpu_memory_utilization=${GMEM}"

exec docker run --rm --name "vllm-mymodel-122b-${PORT}" \
    --runtime nvidia --gpus all --ipc=host --network container:llama-swap \
    -e NVIDIA_DISABLE_FORWARD_COMPATIBILITY=1 \
    -e VLLM_MARLIN_USE_ATOMIC_ADD=1 \
    -v /home/YOUR_USER/LLMs/vllm:/models/vllm \
    vllm-node-tf5:latest \
    vllm serve /models/vllm/MyOrg/MyModel-122B \
    --served-model-name MyModel-122B \
    --host "${HOST}" --port "${PORT}" \
    --gpu-memory-utilization "${GMEM}" \
    --max-model-len 131072 \
    --kv-cache-dtype fp8 \
    --load-format fastsafetensors \
    --attention-backend FLASHINFER \
    --mamba-ssm-cache-dtype float16 \
    --enable-prefix-caching \
    --enable-auto-tool-choice \
    --tool-call-parser qwen3_coder \
    --reasoning-parser qwen3
chmod +x llama-swap/scripts/launch-large-model.sh

Reference it from llama-swap/config.yaml:

  MyModel-122B:
    ttl: 3600
    readyTimeout: 1800
    cmd: /app/scripts/launch-large-model.sh ${PORT} ${host}
    cmdStop: "docker stop vllm-mymodel-122b-${PORT}"

Step 7 — Deploy the Stack via Portainer

In Portainer → Stacks → Add Stack → paste the following. Replace all YOUR_* placeholders before deploying.

version: '3.8'
services:

  # ==========================================
  # 1. LLAMA.CPP (GGUF Engine)
  # ==========================================
  llama-server:
    image: ghcr.io/martin-b78/llama-cpp-spark:latest
    container_name: llama.cpp
    restart: unless-stopped
    ulimits:
      memlock: -1
      stack: 67108864
    ipc: host
    security_opt:
      - seccomp:unconfined
    ports:
      - "18080:18080"
    volumes:
      - /home/YOUR_USER/LLMs/ollama:/models/ollama
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    environment:
      - GGML_CUDA_ENABLE_UNIFIED_MEMORY=1
    command:
      - --host
      - "0.0.0.0"
      - --port
      - "18080"
      - --parallel
      - "4"
      - --no-mmap
      - --context-shift
      - --models-dir
      - /models
      - --n-gpu-layers
      - "99"
      - --ctx-size
      - "16384"
    stdin_open: true
    tty: true
    networks:
      - dgx_net

  # ==========================================
  # 2. OLLAMA
  # ==========================================
  ollama:
    image: ghcr.io/martin-b78/ollama-spark:latest
    container_name: ollama
    restart: unless-stopped
    ports:
      - "11434:11434"
    volumes:
      - /home/YOUR_USER/LLMs/ollama:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
      - NVIDIA_DRIVER_CAPABILITIES=compute,utility
      - OLLAMA_HOST=0.0.0.0
      - OLLAMA_FLASH_ATTENTION=1
      - OLLAMA_NUM_PARALLEL=1
      - OLLAMA_LLM_LIBRARY=cuda_v13
    ipc: host
    ulimits:
      memlock: -1
      stack: 67108864
    networks:
      - dgx_net

  # ==========================================
  # 3. LLAMA-SWAP (VRAM Orchestrator)
  # Port 28080 (host) → 8080 (container)
  # LiteLLM reaches it via Docker DNS: http://llama-swap:8080
  # Model containers attach via: --network container:llama-swap
  # ==========================================
  llama-swap:
    image: ghcr.io/martin-b78/llama-swap-spark:latest
    container_name: llama-swap
    restart: unless-stopped
    ports:
      - "28080:8080"
    entrypoint: ["/usr/bin/llama-swap", "-config", "/app/config.yaml", "-listen", "0.0.0.0:8080"]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    volumes:
      - /home/YOUR_USER/Docker/REPO_DIR/llama-swap:/app
      - /var/run/docker.sock:/var/run/docker.sock
      - /home/YOUR_USER/LLMs:/models
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
      - NVIDIA_DRIVER_CAPABILITIES=compute,utility
      - GITHUB_TOKEN=YOUR_GITHUB_PAT
    networks:
      - dgx_net

  # ==========================================
  # 4. LITELLM DATABASE
  # ==========================================
  litellm-db:
    image: postgres:15-alpine
    container_name: litellm-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: litellm
      POSTGRES_USER: litellm_admin
      POSTGRES_PASSWORD: YOUR_DB_PASSWORD
    ports:
      - "15432:5432"
    volumes:
      - litellm_db_data:/var/lib/postgresql/data
    networks:
      - dgx_net

  # ==========================================
  # 5. LITELLM GATEWAY
  # ==========================================
  litellm:
    image: ghcr.io/martin-b78/litellm-spark:latest
    container_name: litellm
    restart: unless-stopped
    depends_on:
      - litellm-db
    ports:
      - "14000:4000"
    environment:
      - DATABASE_URL=postgresql://litellm_admin:YOUR_DB_PASSWORD@litellm-db:5432/litellm
      - LITELLM_MASTER_KEY=YOUR_MASTER_KEY
    volumes:
      - /home/YOUR_USER/Docker/REPO_DIR/LiteLLM/config.yaml:/app/config.yaml
    command:
      - "--config"
      - "/app/config.yaml"
      - "--port"
      - "4000"
    networks:
      - dgx_net

networks:
  dgx_net:
    external: true

volumes:
  litellm_db_data:

Step 8 — Verify the Stack

# llama-swap up and listing all configured models
curl http://localhost:28080/v1/models | python3 -m json.tool

# LiteLLM health
curl http://localhost:14000/health

# Trigger a model load (llama-swap starts the container on first request)
curl http://localhost:28080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"MyModel-30B","messages":[{"role":"user","content":"Hello"}]}'

# Watch llama-swap load the container in real time
docker logs -f llama-swap

Step 9 — Benchmarking

Credit: @eugrllama-benchy

llama-benchy is a standardized LLM benchmark tool that measures prompt-processing (pp) and token-generation (tg) throughput in a reproducible way specifically designed for comparing results across the DGX Spark community. The benchmark script in this repo is a wrapper around llama-benchy. Thank you @eugr for creating and maintaining it — the consistent output format makes it possible to compare results across different model configs and post meaningful numbers to the forums.

Install llama-benchy first:

pip install llama-benchy
# or: uvx llama-benchy  (no install needed with uv)

Then run the full benchmark across all configured models:

bash benchmark-models.sh --endpoint http://localhost:28080

The script tests each model sequentially, runs a coherence check to detect repetition loops, and writes a results summary to test-results/. The llama-benchy output table is formatted for direct copy-paste into forum posts.


Tips & Common Issues

Model containers fail with “port already in use”
llama-swap assigns ports dynamically from its pool. Make sure the port range in config.yaml doesn’t overlap with other services on the host.

vLLM startup check: free_memory < gpu_memory_utilization × total
After stopping one model container, the CUDA allocator on unified-memory systems can hold freed memory for several seconds. Use the dynamic launcher script from Step 6 instead of a hardcoded --gpu-memory-utilization value for any model over 100B parameters.

Mamba/hybrid models need the tf5 image and an extra flag
Models using the Mamba SSM layers (Qwen3.5-122B-A10B, Qwen3.6-35B, Qwen3-Coder-Next) require vllm-node-tf5:latest (built with --tf5) and --mamba-ssm-cache-dtype float16 in the vllm serve command.

--load-format fastsafetensors is strongly recommended
It loads weight shards in parallel and cuts startup time by ~40% for multi-shard models. Requires model.safetensors.index.json to be present alongside the weight files (all HuggingFace multi-shard models include it).

GPT-OSS-120B MXFP4: skip Ray
Do not use --distributed-executor-backend ray for single-GPU MoE models. Ray’s GCS server + dashboard add ~500 MB overhead, which pushes total allocation past Ray’s 95% OOM threshold. vLLM’s default mp (multiprocessing) executor is leaner and also re-enables async scheduling.

S/M/L group sizing on 128 GB

CUDA-visible: ~121.7 GiB

S (4B–30B quantized):  0.50–0.65 × 121.7 GiB = 61–79 GiB  → swap:true
M (30B–35B FP8):       0.60–0.75 × 121.7 GiB = 73–91 GiB  → swap:true
L (120B+ MoE):         0.70–0.85 × 121.7 GiB = 85–103 GiB → swap:true, solo

With swap: true on all groups, the active model is always evicted before the next one loads. exclusive: true evicts all other groups when a new group activates — essential for preventing OOM when transitioning between tiers.


Acknowledgements

This stack stands on the shoulders of several people’s work:


Repo: GitHub - mARTin-B78/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama: LLM Stack for nVidia DGX Spark containing LiteLLM, LamaSwap, vLLM, Llama.cpp and ollama · GitHub

This is exactly what I’ve been dreaming of!
Thank you for all your effort and for sharing!

I thought about letting the AI write a Setup / Installer Script..
where you have to put in some of the Variables like your Huggingface Key, some Ports you want to use or avoide, the location of your LLMs on the SSD…

Im trying this, but at step three you say cd vllm/build/spark-vllm-docker

But the folder doesnt exist.

Ive also raised some github issues about the tutorial

Unfortunately, this is pretty much a lot of new tools for me, I’m not experienced enough with docker, completely new to llama-swap, LiteLLM etc… I probably would need a lot more guidance from a tutorial.

It looks like I need to do a lot of edits to the compose.yml or compose.yml.sample file, and then run “docker compose up” at some point, but both files seem to have inconsistent paths in them.

Feeling a bit overwhelmed and out of my depth :| :D

Hey giles8,
thanks that you go through the tutorial.
Sorry that it has issues. - Told the AI to solve them and I think it did.

also went through the issues you reported on github and updated the tutorial

4. ✅ Fixed vllm/build/spark-vllm-docker/ Missing Directory Issue

Issue: Forum post mentioned the folder doesn’t exist after cloning. Users were confused about Step 3.

Fix: Added a clear note explaining this is a Git submodule:


# From the repo root, initialize submodules

git submodule update --init --recursive

# Then navigate to the build folder

cd vllm/build/spark-vllm-docker

This is now prominently displayed in Step 3 so users know what to do if the folder is missing.

Updated Tutorial

Thanks for the updates. I will give it another run in the next few days

Automated Setup Script (Now Available!)

I HAVE NOT TESTED THIS - CAUSE EVERYTHING IS SETUP AT MY SPARK ALREADY - USE AT YOUR OWN RISK

For users who find the manual setup overwhelming, the AI (Claude Haiku 4.5) has created an interactive setup wizard that automates the entire configuration process.

What It Does

The setup.sh script guides you through:

✅ Docker & NVIDIA runtime verification
✅ Service detection (Portainer, LiteLLM, llama.cpp, Ollama, llama-swap)
✅ Port conflict resolution (auto-detects and suggests alternatives)
✅ Credential collection (HuggingFace token, GitHub PAT)
✅ Model tier selection (S/M/L/GGUF)
✅ Automatic .env and docker-compose.yml generation
Time: ~5 minutes of guided prompts vs 45 minutes of manual configuration.

How to Use

#Get the latest code
git clone https://github.com/mARTin-B78/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama.git
cd dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama

# Run the interactive setup wizard
./setup/setup.sh

# After setup completes, download models based on your selections
./setup/download-models.sh

# Start the stack
docker compose up -d

Two Setup Paths

Automated (Recommended for beginners):

# 5 min guided wizard
./setup/setup.sh  
      
# Auto-download based on selections
./setup/download-models.sh

Manual (For full control):

  1. Follow TUTORIAL.md step-by-step
  2. Manually edit all configuration files
  3. Ideal if you need custom setups

Both approaches end with the same result — a fully configured, running stack!

The Companion download-models.sh

This script reads your tier selections from .env and automatically downloads models:

  • Uses hf download for efficient, resumable transfers
  • Filters GGUF variants to Q4_K_M only (saves 70+ GB!)
  • Shows progress and final disk usage

What’s in the setup folder

setup/
├── setup.sh              # Interactive installer
├── download-models.sh    # Automated model downloader
└── README.md             # Full documentation & troubleshooting

🚀 DGX Spark LLM Stack Update: Configuration Templates & Secure Setup

What’s New:

We’ve completed a comprehensive update to the configuration templates used by the DGX Spark LLM stack, bringing everything in sync with the latest optimized production configurations.

✅ What’s Been Updated

LiteLLM Gateway (config.yaml.sample)

  • 11 optimized model definitions including Qwen 3.5/3.6 series and Nemotron models
  • Reasoning model support with proper tool-calling configurations
  • Ready-to-use template with secure placeholders (no actual API keys exposed)

VRAM Orchestrator (config.yaml.sample)

  • Updated 11 vLLM model launchers with latest optimization flags (MTP-2, FLASHINFER, chunked prefill)
  • Model tier grouping for automatic memory management
  • All paths use generic placeholders (<LLM_ROOT_PATH>, <REPO_CONFIG_PATH>) for instant cross-environment deployment

🎯 Key Advantages

  1. Production-Ready - Models pre-configured with proven optimization flags from live DGX Spark benchmarks

  2. Easy Deployment - Setup scripts automatically substitute placeholders with your paths; templates work across different environments

  3. Latest Models - Includes Qwen 3.6 uncensored, Nemotron-3 Super 120B, and reasoning-enabled models

  4. Clear Documentation - Both automated (5-min setup) and manual (45-min detailed) configuration paths available in README.md

📝 Next Steps

  1. Users can now clone the repo and run bash setup/setup.sh for automated configuration
  2. Manual setup users have detailed, updated templates in each service folder
  3. All defaults reflect real-world optimizations tested on GB10 hardware

If you want to try it, I’m quite keen to build out key-issuance on llm-proxy - GitHub - wentbackward/llm-proxy: Transparent reverse proxy for LLM APIs — virtual models, parameter profiles, and OTel metrics for any OpenAI-compatible backend · GitHub - it’s super-fast, written in Go. The proxy itself is not contributing much to the overall speed, but there’s a nice feature which will help you.

llm-proxy (I probably need a different name) works different to LiteLLM - I’ve created this concept of virtual models where you can specify defaults or clamp the sampling parameters. Take Qwen3.6 for example. You can’t take a coding/thinking model and just turn thinking off, it’s highly sub-optimal. So you can create several virtual models from the same underlying model.

Concrete example from my 2-node spark setup

coder - typical 20t/s - cyankiwi/Qwen3.6-27B-AWQ:
temperature: 0.6, top_p: 0.95, top_k: 20, presence_penalty: 0.1, repetition_penalty: 1.1, min_p: 0.08, enable_thinking: true

coder-moe - typical 40t/s - Qwen/Qwen3.6-35B-A3B-FP8:
temperature: 0.6, top_p: 0.95, top_k: 20, presence_penalty: 0.0, repetition_penalty: 1.0, enable_thinking: true

general - typical 40t/s - Qwen/Qwen3.6-35B-A3B-FP8:
temperature: 1.0, top_p: 0.95, top_k: 20, min_p: 0.00, presence_penalty: 0.0, repetition_penalty: 1.0, enable_thinking: true

instruct - typical 40t/s - cyankiwi/Qwen3.6-27B-AWQ:
temperature: 0.7, top_p: 0.80, top_k: 20, presence_penalty: 1.5, repetition_penalty: 1.05, min_p: 0.00, enable_thinking: false

When I’m performing maintenance, I switch my users over to hugging face, by creating the virtual route in the config file and SIGHUP, they have no idea when I take my sparks down. I also have a bunch of other machines such as Jetson Nano’s with 8GB unified RAM where I run embed’s, classifiers and TTS services. This cheaply keeps the Sparks dedicated. I route the moe to primary on one spark, the coder model head is on the 2nd spark and distrubted. This means openclaw etc runs in it’s bursty nature, coding can get long runs. I have some overhead on 1 spark to play.

In real usage:
openclaw - general
OpenWebUI - general but you also get all the available models from llm-proxy
pi-coder etc - coder-moe for architecture, design, ui, planning, brainstorming
pi-coder etc - coder-moe one-shot coding tasks
pi-coder etc - coder precise coding
system admin - instruct running shell commands etc.
workflow agents - coder-moe or instruct depending on type of task. coder-moe is better for figuring stuff out from SOP’s, instruct is better for constructing tool calls or CLI usage.

So that’s the messy reality (at least it’s my messy reality) of running my own AI. The users can just choose the model they need and the sampling parameters are tuned for the role.

I hope that helps optimizing performance.

For llm-proxy I’m planning to build a bearer token plug-in. Issue a token, permitted to specific models (or unrestricted), rate limits (or unlimited). Let me know if you have any requirements for key management, I’d like to feed that into the design.
Paul

I tried the setup script, have raiised issue in github with the results. I did proceed with hardcoding the script responses, but still found issues.

I think the model downloads are fixed, still dont see anything other than dockerfile in the vllm folder.

I like your concept, and may use it for building my own, but I think the setup needs a few iterations before it can be used.

thanks for giving it another go. Your input is very valuable for me. Cause its really hard to bugfix something that you do not try yourself.
Took me so long to get my system running so I am worried messing it up by trying the installer.

Told the AI to fix it and it did and pushed the changes to github.

Script parses cleanly.

Summary of fixes in setup/setup.sh:

  1. NVIDIA runtime check (setup.sh:115-127) — replaced docker run --rm --runtime=nvidia nvidia/cuda:11.0-runtime nvidia-smi (no arm64 variant, far too old for Blackwell — false negative on the GB10) with a docker info parse. Detects three states: nvidia is the default runtime ✅, nvidia is registered but not default ⚠️, or not present.
  2. Syntax error at line 320 (setup.sh:320-334) — the unquoted (4B-30B...) and (y/n) inside $(echo -e ...) were being parsed by bash as subshells. Quoted the echo -e argument on all four read -p lines.

The reporter on issue #5 should now see “NVIDIA Container Runtime is configured (default runtime: nvidia)” given their daemon.json, and the script will run through Step 10 instead of dying at line 320.

Any chance you can add to this Tutorial alternative steps to operate on a 2 node Spark cluster? Eugr does it really well to cover both single node and cluster on his git page.

I can ask the AI to write it for a cluster as well but I have no option to test it.
Love to have a second spark too.
maybe the only thing you need to change is the number of Tensor_parallel_size in the lama-swap/config.yaml from 1 to 2.

#tensor-parallel-size
tensor_parallel: “1” #<- CHANGE THE VALUE TO THE NUMBER OF SPARKS IN THE CLUSTER like “2”

# llama-swap config.yaml
host: "0.0.0.0"
port: 8080
# Global safety: matching your local healthCheckTimeout (1 hour)
timeout: 3600
readyTimeout: 3600
healthCheckTimeout: 900
logLevel: "info" 
metricsMaxInMemory: 1000

# Global macros for cleaner model blocks
macros:
  host: "0.0.0.0"
  gpu_mem: "0.7"
  #tensor-parallel-size
  tensor_parallel: "2" 

# ===================================================
# 🔀 CONCURRENT MODEL GROUPS  (S / M / L tiers)
# ===================================================
...

Update me if that works or not.

I’ll give this a try tomorrow and get back on here with an update.

Ok I had some issues that the DGX crashed because the RAM was full. So I ask the AI to upgrade my Stack and dynamically calculate the gpu-memory-utilization based on the free RAM.

For benchmark-models.sh besides llama-benchy I told it to include
Tool-eval-bench / Introducing Tool Eval Bench CLI
So not only the speed is measured but also the tool use.

Here are the Changes - (the following text is AI generated)

🚀 Stack Update: All Models Now Benchmark Cleanly + One Launcher to Rule Them All

Quick update from a few days of fixing edge cases that were silently killing benchmark runs. Everything below is now in the repo:

✅ Adaptive --gpu-memory-utilization for every vLLM model

The same script — launch-vllm-auto.sh — is now wired into all vLLM model blocks and llama.cpp GGUF.

It:

  • Estimates required VRAM from weights (safetensors) + KV cache (config.json) + safety
  • Reads /proc/meminfo (works on GB10 where nvidia-smi --query-gpu=memory.* returns “Not Supported”)
  • Picks the smallest --gpu-memory-utilization that fits, clamped to [GMEM_MIN, GMEM_MAX]

Best part — one knob switches a model between adaptive and static:

GMEM_OVERRIDE=0.7069     # pin to this exact value
GMEM_OVERRIDE=adaptive   # (or unset) → compute dynamically

Useful when one specific model needs a hand-tuned thermal cap but the other 12 should adapt to free RAM.

🛡️ 126.5 GB system-RAM cap (the GB10 crash threshold)

I kept hitting random crashes when used RAM passed ~126.5 GB. The launcher now defaults SYSTEM_RAM_CEILING_GIB=117.81 (= 126.5 GB decimal) and always reserves (MemTotal − ceiling) GiB even when /proc/meminfo says more is free. Override per-model if your workload tolerates more pressure.

Plus a 5 GiB safety buffer between MemAvailable and cudaMemGetInfo, because vLLM’s startup check can race the kernel’s page-cache reclaim by ~1 GiB and fail with Free memory < desired.

✅ Three previously-failing models now load reliably

  • Qwen3-Omni-30B-A3B-Instruct — was set to --gpu-memory-utilization 0.40 (~49 GiB), but BF16 weights are ~60 GiB. Could never have worked. Now adaptive.
  • Qwen3-Coder-Next-FP8-Dynamic & int4-AutoRound — needed the patch-revert mod (bash run.sh) before vLLM starts. Launcher now supports PRE_LAUNCH_CMD for in-container pre-vllm steps.
  • Nemotron-3-Nano-Omni-30B-A3B-Reasoning-NVFP4 — uses vllm/vllm-openai image whose ENTRYPOINT is already vllm serve. Launcher now supports VLLM_SERVE_PREFIX="" to skip prepending it.

🧪 Tool-eval-bench integrated into benchmark-models.sh

Big credit to @SeraphimSerapis for tool-eval-bench. The benchmark script now has a --quality flag that runs llama-benchy (speed) and tool-eval-bench (tool-call quality) in a single load cycle per model — since loading is the slowest step, doing both back-to-back saves hours.

bash benchmark-models.sh --quality                          # 15 scenarios, ~2-5 min/model extra
bash benchmark-models.sh --quality --quality-mode full      # 69 scenarios
bash benchmark-models.sh --quality --quality-categories "K A J"   # specific categories

The summary table grows a Quality /100 column. Top performers from the last full run: Qwen3.5-122B-A10B-int4-AutoRound: 94, Qwen3.5-35B-A3B-FP8: 93, Qwen3-Omni: 85, Qwen3-Coder-Next-FP8: 85.

🐛 Hidden Continue.dev gotcha (worth checking your config!)

If you use Continue.dev in VS Code/JetBrains and configured a heavy coding model with role autocomplete, the IDE pings that model on every keystroke pause. Through LiteLLM → llama-swap, that triggers a full cold load every few minutes — for an 80B model, that’s brutal. Move autocomplete to a small dedicated model (Nemotron-3-Nano-4B-FP8 is perfect: TTFT ~321 ms). My setup was silently slamming Qwen3-Coder-Next-FP8 every IDE pause for hours before I noticed.

Final benchmark — all green ✅

Date: 2026-05-03 14:28  |  Mode: quick  |  Runs: 1
Models tested: 13  |  Passed: 13  Failed: 0
Model tg tok/s Quality
Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive 71 83
Qwen3-Coder-Next-int4-AutoRound 65 82
Qwen3.6-35B-A3B-FP8 59 82
Nemotron-3-Nano-Omni-30B-A3B-Reasoning-NVFP4 57
GPT-OSS-120B 56 74
Nemotron-3-Nano-30B-A3B-NVFP4 55 75
Qwen3-VL-30B-A3B-Instruct-FP8 50 82
Qwen3.5-35B-A3B-FP8 48 93
Nemotron-3-Nano-4B-FP8 38 80
Qwen3-Coder-Next-FP8-Dynamic 31 85
Qwen3-Omni-30B-A3B-Instruct 29 85
Qwen3.5-122B-A10B-int4-AutoRound 27 94
Nemotron-3-Super-120B-A12B-NVFP4 14 78

Next steps

Pull the latest, and if you have an existing llama-swap/config.yaml, regenerate against the new config.yaml.sample (or just copy the env … /app/scripts/launch-vllm-auto.sh … pattern to your model blocks). The README and TUTORIAL now have the full env-var reference.

Repo: GitHub - mARTin-B78/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama: LLM Stack for nVidia DGX Spark containing LiteLLM, LamaSwap, vLLM, Llama.cpp and ollama · GitHub

Happy to take questions or PRs! 🙌

[UPDATE] DGX Spark LLM Stack — Recent Changes (Late April – May 2026)

Hey everyone, just dropping a summary of everything that has landed in the repo over the past few weeks. Quite a bit has changed, so here’s the breakdown by area.


Private Registry Support

The biggest new feature: you can now pull model images from private container registries (GitLab Registry, Harbor, Nexus, and others) instead of only Docker Hub. The setup script and config.yaml.sample have been updated to reflect this. The README was also expanded with a dedicated section explaining the workflow. (#9, #10)


Launcher — Smarter VRAM Management

A lot of work went into making the vLLM launcher more robust across hardware configurations:

  • GMEM_OVERRIDE env var — lets you hard-pin --gpu-memory-utilization instead of relying on auto-detection. Useful when the heuristic makes a wrong call.
  • 126.5 GB system RAM ceiling — the launcher now caps its VRAM estimate against the GB10’s known 126.5 GB unified memory ceiling to prevent over-allocation.
  • Better MemAvailable bridging — fixed a mismatch between /proc/meminfo’s MemAvailable and cudaMemGetInfo, adding a 5 GiB safety buffer to prevent OOM at load time.
  • VLLM_SERVE_PREFIX fix — images built with entrypoint=vllm-serve were missing the prefix; this is now wired in correctly.
  • Adaptive GPU memory utilization for mod-script models — models that use modifier scripts (e.g. for chat template patching) now get the same adaptive gmem logic as native ones, plus the prompt-processing token display was corrected.
  • GB10 /proc/meminfo fix — the shell-only auto-gmem path now correctly reads from /proc/meminfo on GB10 instead of trying a CUDA call that isn’t available in the launcher shell.
  • gpu_memory_utilization floor raised for 122B — bumped from 0.60 to 0.82 to avoid the 122B model failing to load due to insufficient VRAM headroom.

Benchmark Tool — Major Overhaul

The benchmark script (spark-bench) received multiple rounds of improvements:

  • Interactive wizard — instead of juggling flags, the benchmark now walks you through model selection, group size, and quality options interactively.
  • tool-eval-bench integration — tool-calling quality is now scored automatically as part of a benchmark run, with output visible in the terminal and captured in the report.
  • Quality detail report — benchmark results now include a richer quality breakdown (coherence, tool-call score, latency) in addition to raw throughput numbers.
  • Load-time tracking — model cold-start time is now measured and reported alongside TTFT.
  • Robust unload — fixed cases where a model container would not be fully evicted before the next model loaded, causing ghost VRAM usage.

Bug Fixes (Issues #6, #7, #8)

Three reported issues with the setup and build scripts were addressed:

  • Syntax error in tier-selection prompts
  • False-negative NVIDIA runtime detection (the check was incorrectly reporting the runtime as missing even when it was present)
  • Additional setup/build script fixes from community reports

Model Config — Qwen3.6-Uncensored

The Qwen3.6-Uncensored block in the config was updated:

  • Thinking disabled — the enable_thinking: false flag is now set explicitly to prevent unexpected <think> token chains.
  • Context window expanded to 64K — up from the previous default, letting the model handle much longer conversations without truncation.

Docs & Sample Configs

  • config.yaml.sample now clearly distinguishes between the two vLLM image families (vllm/vllm-openai vs. the project’s patched images) and when to use each.
  • TUTORIAL.md was updated with corrected setup paths, fixed huggingface-cli usage, and the 2026-04-23 benchmark run results (including TTFT and deep-context numbers).
  • The launcher reference docs were expanded to cover GMEM_OVERRIDE, the RAM ceiling, and all plumbing environment variables.
  • checkEndpoint: /health was added to all vLLM model blocks in the sample config so llama-swap can properly detect readiness.

docker-compose.yml

Did some changes to the docker-compose cause there were issues using the LLMs when not sending a API Key. (home-assistant addon did not provide any way to add a api key)

here is the full Sample docker-compose.yml

# ==============================================================================
# DGX SPARK / GRACE-BLACKWELL OPTIMIZED AI ORCHESTRATION STACK
#
# SETUP INSTRUCTIONS:
# 1. Copy this file to `docker-compose.yml`
# 2. Run `setup/setup.sh` (or hand-create a `.env`) so the following are set:
#      GH_USER              — image-registry user (also docker-login user, default ghcr.io)
#      LLM_ROOT_PATH        — host path that holds the model files
#      REPO_CONFIG_PATH     — absolute path to this repo on the host
#      DATABASE_URL         — postgres URL for LiteLLM
#                             e.g. postgresql://litellm_admin:PW@litellm-db:5432/litellm
#      LITELLM_MASTER_KEY   — optional: enforce API key auth; omit for open API / UI-only auth
#      POSTGRES_PASSWORD    — Postgres password for the LiteLLM DB
#      LITELLM_UI_USERNAME  — admin username for the LiteLLM web UI
#      LITELLM_UI_PASSWORD  — admin password for the LiteLLM web UI
#      GITHUB_TOKEN         — PAT for pulling images from ghcr.io (or your private REGISTRY)
#
#    Optional, for non-ghcr.io registries (GitLab, Harbor, Nexus, …):
#      REGISTRY             — defaults to "ghcr.io"
#      IMAGE_NAMESPACE      — path under the registry; defaults to ${GH_USER}.
#                             For GitLab use "<group>/<project>".
#    All image references below already expand ${REGISTRY} and
#    ${IMAGE_NAMESPACE}, so just set them in .env and run as usual.
# 3. `docker network create dgx_net`  (if you didn't already)
# 4. `docker compose up -d`
# ==============================================================================


services:

  # ==========================================
  # 1. VLLM (Primary Safetensors Engine)
  # ==========================================
  # NOTE: In practice, vLLM instances are typically managed by llama-swap
  # (service 3) as ephemeral on-demand containers. Keep this block only if
  # you want a *persistent* vLLM sidecar running alongside llama-swap.
  vllm:
    image: ${REGISTRY:-ghcr.io}/${IMAGE_NAMESPACE:-${GH_USER}}/vllm-spark:latest
    container_name: vllm
    restart: unless-stopped
    ipc: host
    ports:
      - "18000:8000"  # vLLM internal is always 8000; exposed as 18000
    volumes:
      - ${LLM_ROOT_PATH}/safetensors:/model
    deploy:
      resources:
        reservations:
          devices: [{driver: nvidia, count: all, capabilities: [gpu]}]
    command: >
      --model /model/Qwen3.5-7B-Instruct --host 0.0.0.0 --port 8000
    networks: [dgx_net]


  # ==========================================
  # 2. LLAMA-SERVER (Persistent GGUF Engine)
  # ==========================================
  # Persistent llama.cpp instance for GGUF models; stays loaded between requests.
  # llama-swap can also spin *ephemeral* llama.cpp containers on demand — use
  # this block for a model you always want hot (e.g. a fast chat fallback).
  llama-server:
    image: ${REGISTRY:-ghcr.io}/${IMAGE_NAMESPACE:-${GH_USER}}/llama-cpp-spark:latest
    container_name: llama.cpp
    restart: unless-stopped
    ipc: host                          # Required for shared memory with GPU driver
    security_opt:
      - seccomp:unconfined             # Allows low-level CUDA calls
    ulimits:
      memlock: -1                      # Unlimited locked memory — needed for unified VRAM
      stack: 67108864
    ports:
      - "19000:19000"
    volumes:
      - ${LLM_ROOT_PATH}/ollama:/models/ollama
      # Mount individual GGUF files if you only want specific models:
      # - ${LLM_ROOT_PATH}/ollama/YourModel/model-Q4_K_M.gguf:/models/model-Q4_K_M.gguf
      # Multi-file GGUF shards (e.g. large 106B models split into two files):
      # - ${LLM_ROOT_PATH}/ollama/BigModel/model-Q4_K_M-00001-of-00002.gguf:/models/model-00001-of-00002.gguf
      # - ${LLM_ROOT_PATH}/ollama/BigModel/model-Q4_K_M-00002-of-00002.gguf:/models/model-00002-of-00002.gguf
    deploy:
      resources:
        reservations:
          devices: [{driver: nvidia, count: all, capabilities: [gpu]}]
    environment:
      - GGML_CUDA_ENABLE_UNIFIED_MEMORY=1  # Required for Grace-Blackwell unified memory
    command:
      - --host
      - "0.0.0.0"
      - --port
      - "19000"
      - --parallel
      - "4"          # Simultaneous request slots
      - --no-mmap    # Disable memory-mapped I/O (required for unified memory on GB10)
      - --context-shift
      - --models-dir
      - /models
      - --chat-template
      - chatml
      - --n-gpu-layers
      - "99"         # Offload all layers to GPU
      - --models-max
      - "1"          # Keep only one model loaded at a time
      - --ctx-size
      - "16384"      # Context window in tokens
      - --timeout
      - "600"        # Request timeout in seconds
    stdin_open: true
    tty: true
    networks: [dgx_net]


  # ==========================================
  # 3. LLAMA-SWAP (The VRAM Manager)
  # ==========================================
  # Orchestrates all inference containers: spawns them on demand via docker.sock,
  # evicts them when idle so 128 GB unified memory is never wasted.
  # LiteLLM routes to it via Docker DNS: http://llama-swap:8080
  # Spawned model containers join its network namespace
  # (--network container:llama-swap) so they are reachable as localhost:PORT
  # inside llama-swap, even though they run in separate containers.
  llama-swap:
    image: ${REGISTRY:-ghcr.io}/${IMAGE_NAMESPACE:-${GH_USER}}/llama-swap-spark:latest
    container_name: llama-swap
    restart: unless-stopped
    ports:
      - "28080:8080"  # Host 28080 → container 8080 (avoid conflict with other services)
    entrypoint: ["/usr/bin/llama-swap", "-config", "/app/config.yaml", "-listen", "0.0.0.0:8080"]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    volumes:
      - ${REPO_CONFIG_PATH}/llama-swap:/app        # llama-swap config directory
      - /var/run/docker.sock:/var/run/docker.sock  # Required to spawn/evict model containers
    # - /usr/bin/docker:/usr/bin/docker             # Uncomment if docker CLI is not bundled in the image
      - ${LLM_ROOT_PATH}:/models                   # Model files — used by mod-script models
    deploy:
      resources:
        reservations:
          devices: [{driver: nvidia, count: all, capabilities: [gpu]}]
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
      - NVIDIA_DRIVER_CAPABILITIES=compute,utility
      - GITHUB_TOKEN=${GITHUB_TOKEN}  # PAT for pulling images when spawning model containers
    networks: [dgx_net]


  # ==========================================
  # 4. OLLAMA (GGUF via Ollama modelfile format)
  # ==========================================
  # Handles models pulled or defined via Ollama's modelfile system.
  # The 15-second startup delay gives the GPU driver time to settle
  # before Ollama tries to claim VRAM — avoids init-time OOM on GB10.
  ollama:
    image: ${REGISTRY:-ghcr.io}/${IMAGE_NAMESPACE:-${GH_USER}}/ollama-spark:latest
    container_name: ollama
    restart: unless-stopped
    ipc: host
    ulimits:
      memlock: -1
      stack: 67108864
    ports:
      - "11434:11434"
    volumes:
      - ${LLM_ROOT_PATH}/ollama:/root/.ollama
    deploy:
      resources:
        reservations:
          devices: [{driver: nvidia, count: all, capabilities: [gpu]}]
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
      - NVIDIA_DRIVER_CAPABILITIES=compute,utility
      - OLLAMA_HOST=0.0.0.0          # Listen on all interfaces inside the container
      - OLLAMA_FLASH_ATTENTION=1     # Enable flash attention — significant speedup on GB10
      - OLLAMA_NUM_PARALLEL=1        # Concurrent request slots (keep 1 for large models)
      - OLLAMA_LLM_LIBRARY=cuda_v13  # Force CUDA 13 backend for Blackwell SM121
      - OLLAMA_DEBUG=1               # Verbose logging; set to 0 in production
    entrypoint: ["/bin/sh", "-c", "sleep 15 && ollama serve"]
    networks: [dgx_net]


  # ==========================================
  # 5. LITELLM (The Unified API Gateway)
  # ==========================================
  # Single OpenAI-compatible endpoint (:14000) in front of all inference services.
  # Internal port is 4000; mapped to 14000 externally to avoid the DGX
  # NoMachine port conflict on 4000.
  # The master key is intentionally disabled by default — the stack runs with
  # open API access and UI-only authentication. Uncomment LITELLM_MASTER_KEY
  # to require sk-* tokens on every API call.
  litellm:
    image: ${REGISTRY:-ghcr.io}/${IMAGE_NAMESPACE:-${GH_USER}}/litellm-spark:latest
    container_name: litellm
    restart: unless-stopped
    depends_on: [litellm-db]
    ports:
      - "14000:4000"  # External 14000 → internal 4000 (avoids NoMachine conflict)
    environment:
      - DATABASE_URL=${DATABASE_URL}        # postgresql://litellm_admin:PW@litellm-db:5432/litellm
      - UI_USERNAME=${LITELLM_UI_USERNAME}  # Admin username for the LiteLLM web UI
      - UI_PASSWORD=${LITELLM_UI_PASSWORD}  # Admin password for the LiteLLM web UI
    # - LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}  # Uncomment to enforce API key auth
    volumes:
      - ${REPO_CONFIG_PATH}/LiteLLM/config.yaml:/app/config.yaml
    command:
      - "--config"
      - "/app/config.yaml"
      - "--port"
      - "4000"   # Keep this at 4000 internally (mapped to 14000 externally)
    networks: [dgx_net]


  # ==========================================
  # 6. LITELLM DATABASE (PostgreSQL)
  # ==========================================
  # Stores LiteLLM's virtual keys, usage logs, and team/budget data.
  # Exposed on 15432 to avoid conflict with any host Postgres on 5432.
  litellm-db:
    image: postgres:15-alpine
    container_name: litellm-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: litellm
      POSTGRES_USER: litellm_admin
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "15432:5432"
    volumes:
      - litellm_db_data:/var/lib/postgresql/data
    networks: [dgx_net]


# ==========================================
# GLOBAL NETWORKS & VOLUMES
# ==========================================
networks:
  dgx_net:
    external: true  # Create once with: docker network create dgx_net

volumes:
  litellm_db_data:  # Persistent Postgres data for LiteLLM (keys, usage logs, budgets)

here are just the changes:

1. LiteLLM — removed master key, fixed network, fixed volume path:

environment:
  - DATABASE_URL=postgresql://litellm_admin:LiteLLM-PostGres-PW@litellm-db:5432/litellm
# - LITELLM_MASTER_KEY=...   ← commented out intentionally
  - UI_PASSWORD=YOUR-LITE-LLM-PASSWORD
  - UI_USERNAME=YOUR-LITE-LLM-USERNAME

# Port changed from 14000:8000 → 14000:4000 (avoids NoMachine conflict on DGX)
ports:
  - "14000:4000"

# Network changed from network_mode: host → dgx_net
# Required for Docker DNS to work (litellm → llama-swap, litellm → ollama)
networks:
  - dgx_net

# Volume mount now uses absolute path instead of ${REPO_CONFIG_PATH}
# (env var was pointing to an old directory, causing Docker to create a
#  directory named config.yaml instead of mounting the file → IsADirectoryError)
volumes:
  - /home/sparky/Docker/dgx-spark_.../LiteLLM/config.yaml:/app/config.yaml

2. Ollama — fixed crash loop:

# BEFORE (broken): passes "sh" as an ollama subcommand → "unknown command sh"
command: >
  sh -c "sleep 15 && ollama serve"

# AFTER (fixed): overrides the entrypoint so the shell actually runs
entrypoint: ["/bin/sh", "-c", "sleep 15 && ollama serve"]

The ollama image’s entrypoint is the ollama binary itself, so command args get passed to it as subcommands. Using entrypoint bypasses this and gives you a real shell.


Net result

  • curl http://<host>:14000/v1/models — returns model list, no API key needed
  • curl http://<host>:14000/v1/chat/completions — works unauthenticated
  • Admin UI at :14000/ui — still requires YOUR LITE-LLM USERNAME / YOUR LITE-LLM PASSWORD
  • All 5 containers healthy: litellm, litellm-postgres, llama-swap, llama.cpp, ollama

Image Tagging Standardization (Issue #5)

Issue: Users were confused about having to manually retag their built vLLM images to spark-vllm:Version_1 or vllm-node:Version_1 to match the default configuration in llama-swap/config.yaml.

Fix:

  • Completely standardized all configuration files to use the default out-of-the-box tags produced by the upstream build scripts (perfectly matching the vllm-node from eugr’s spark-vllm-docker):
    • vllm-node:latest (Standard)
    • vllm-node-tf5:latest (Transformers >= 5.0)
    • vllm-node-mxfp4:latest (Experimental MXFP4)
  • Replaced all legacy references to Version_1 in llama-swap/config.yaml, llama-swap/config.yaml.sample, and benchmark-models.sh.

Now, building the ephemeral vLLM containers is purely “plug-and-play” with absolutely no manual docker tag commands required!


Repo: GitHub - mARTin-B78/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama: LLM Stack for nVidia DGX Spark containing LiteLLM, LamaSwap, vLLM, Llama.cpp and ollama · GitHub

Hi Martin, is it possible you didn’t push the changes to Github yet? It lists the last commit as e799ce1 · last week.

oh yes.. you are right - will push it

ok updated

Config sample and the adaptive launcher are updated in the repo: GitHub - mARTin-B78/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama: LLM Stack for nVidia DGX Spark containing LiteLLM, LamaSwap, vLLM, Llama.cpp and ollama · GitHub

[Update] Qwen3.6-27B now working — two bugs fixed + configs updated

Both the PrismaSCOUT NVFP4 (vLLM) and the Heretic v2 NVFP4-MLP (llama.cpp) variants are now loading cleanly on my GB10. Took some digging to find the root causes — posting the full breakdown in case anyone else hits this.


Bug 1 — YAML blank line crashing vLLM (Qwen3.6-27B PrismaSCOUT)

The model kept OOMing the system (~30 minutes into startup, after the FlashInfer autotuner). The crash was caused by a blank line inside the cmd: > block in llama-swap/config.yaml:

cmd: >
  env
  MODEL_PATH=...
  MAX_MODEL_LEN=65536 MAX_NUM_SEQS=4
  GMEM_MIN=0.45 GMEM_MAX=0.65
              ← blank line here ← THE BUG
  EXTRA_DOCKER_ARGS='...'
  /app/scripts/launch-vllm-auto.sh ...

YAML folded block scalar (>) preserves blank lines as a literal \n. The shell sees two separate commands: the env VAR=val … prefix runs as a no-op (env without a trailing command just prints the environment and exits), and none of MAX_MODEL_LEN, MAX_NUM_SEQS, or GMEM_MAX ever reach the launch script.

Result: vLLM auto-detects max_model_len from config.json262144 tokens, allocates 81 GiB KV cache, and the GB10 crashes. With the blank line removed and MAX_MODEL_LEN=65536, the model loads at gpu_memory_utilization=0.49, KV cache 44 GiB, and serves cleanly.

If you use launch-vllm-auto.sh with EXTRA_DOCKER_ARGS, make sure there is no blank line between the env var block and the script call.


Bug 2 — Negative CUDA graph memory estimate (vLLM 0.21.1rc1)

Even with the blank line fixed, the log shows:

Estimated CUDA graph memory: -12.10 GiB total

This is a vLLM bug: speculative decoding forces cudagraph_mode=PIECEWISE, and the memory estimator overflows. With num_speculative_tokens=3 the resulting over-allocation still caused a crash; reducing to num_speculative_tokens=1 keeps the PIECEWISE graph captures small (0.27 GiB actual) and the model loads fine.


Qwen3.6-27B architecture note — only 16/64 layers use standard KV cache

full_attention_interval: 4 means 1 in 4 layers is a full GDN attention layer; the other 48 are linear/Mamba layers with fixed-size recurrent states. If your KV-cache size estimator counts all 64 layers you’ll overshoot by 4×. Set GMEM_MAX=0.65 (not 0.80) to leave headroom for the torch.compile + FlashInfer autotuner spikes during startup.


Heretic v2 NVFP4-MLP (llama.cpp)

The 28 GiB file is BF16 attention + NVFP4 MLP-only, so it’s heavier than a pure NVFP4 quant. Two changes made it stable:

  • Remove --no-mmap: forced the entire file into heap upfront, causing a RAM spike. With mmap the kernel pages lazily.
  • --parallel 2 instead of 4: halves the KV cache (~17 GiB → ~8.5 GiB at 65K context).