Local LLM Deployment (2026): vLLM vs Ollama vs llama.cpp, VRAM Sizing & Production Stack

Local LLM deployment in 2026 is no longer an experimental weekend project — it is an engineering decision driven by workload economics, data sovereignty, and multi-agent throughput. When you run autonomous agents, automated content pipelines, or internal code/document analysis around the clock, cloud API token meters and rate limits quickly become your largest operational bottleneck.

This guide is our complete production reference for local LLM deployment: how to calculate real VRAM budgets across 7B to 72B models, how vLLM, Ollama, and llama.cpp compare under real concurrent load, and the exact docker compose, systemd, pre-flight checklist, and watchdog scripts we run on our own hardware (including our self-hosting AI models on NVIDIA DGX Spark guide and DGX Spark local LLM deployment walkthrough).


Why Local LLM Deployment Beats Cloud APIs for Sustained Workloads

In 2026 the API-versus-local question comes down to the shape of your traffic. Local LLM deployment wins decisively when any of these four conditions hold:

  1. Data privacy and compliance are non-negotiable — customer PII, legal contracts, financial records, or proprietary repositories never leave your LAN or Tailscale mesh.
  2. Your workload is sustained rather than bursty — continuous pipelines and background jobs turn variable per-token bills ($350–$600+/month) into a flat ~$30/month electricity cost.
  3. Predictable latency and zero rate limits — no 429 throttling during batch runs, no silent model deprecations, and full control over quantization and context length.
  4. Multi-agent orchestration — agent frameworks issue dozens of verification, routing, and extraction calls per task; serving those locally makes iterative agent loops practically free.

Chapter 1: Hardware & VRAM Sizing Table (7B to 72B Models)

The single most important number in local LLM deployment is available VRAM (or unified memory) after accounting for model weights, the KV cache (--max-model-len), and OS headroom:

Model ClassFP16 / BF16 VRAMFP8 VRAM (Hopper / Blackwell)4-Bit VRAM (AWQ / GPTQ / GGUF Q4_K_M)+ 32K Context KV CacheRecommended Hardware TierBest Workload Fit
7B – 9B (Qwen 2.5/3 7B, Llama 3.1 8B)15–18 GB8–10 GB5–6.5 GB+2–4 GB12GB–16GB Consumer GPU (RTX 4070 Ti / 4080)Fast classification, routing, JSON extraction
14B (Phi-4 14B, Qwen 2.5 14B)28–30 GB15–16 GB8.5–10 GB+4–6 GB16GB–24GB GPU (RTX 4090 / 5080)General coding, bilingual summarization
30B – 35B (Qwen 3 / 2.5 32B, DeepSeek-R1 32B)64–68 GB34–36 GB18–21 GB+8–12 GB24GB–32GB GPU (tight Q4) or 64GB–128GB Unified MemorySweet spot: tool calling, agentic coding, complex synthesis
70B – 72B (Qwen 2.5/3 72B, Llama 3.3 70B)140–145 GB72–76 GB42–48 GB+16–22 GB128 GB NVIDIA DGX Spark (GB10), Mac Studio, or 2×24GB/48GB GPUsFrontier-grade local reasoning, long-context multi-agent pipelines

Critical unified-memory rule: On unified-memory architectures like the NVIDIA GB10 (DGX Spark) or Apple Silicon, CPU and GPU share one physical RAM pool. Always leave 12–16 GB of system RAM untouched for the Linux kernel, Docker runtime, and disk page cache, or page-cache thrashing will cut token generation speed in half.


Chapter 2: Runtime Comparison — vLLM vs Ollama vs llama.cpp

You do not need one runtime for everything; you need the runtime whose scheduling architecture matches your concurrency profile. Here is an indicative 2026 comparison across vLLM, Ollama, and llama.cpp. The speed rows are typical ranges, not a controlled benchmark; for numbers we actually measured on our DGX Spark (Qwen3.8-27B NVFP4 on SGLang: ~29 tok/s single-stream median, ~92 tok/s aggregate at concurrency 4, 24 prompts, 2026-11-03), see the self-hosting guide:

Metric / CapabilityvLLM (Production API Standard)Ollama (Rapid Prototyping)llama.cpp (llama-server)
Scheduling ArchitectureContinuous batching + PagedAttention + prefix cachingGo wrapper over llama.cpp with dynamic model loadingNative C/C++ GGML/GGUF server with parallel slots (-np)
Single-User Speed (32B Q4/FP8 on GB10, indicative)22–28 tok/s (TTFT ~38ms)20–25 tok/s (TTFT ~45ms)21–26 tok/s (TTFT ~40ms)
8-Concurrent-Request Throughput (32B, indicative)115–145 tok/s total (scales cleanly)24–32 tok/s total (serializes/queues heavily)48–65 tok/s total (depends on -np KV split)
VRAM Allocation StrategyPre-allocates fixed pool (--gpu-memory-utilization 0.85)Loads on demand, unloads after OLLAMA_KEEP_ALIVE timerAllocates exact GGUF layers (-ngl) + fixed context buffer
Supported Quantization FormatsFP8, AWQ, GPTQ, NVFP4, BitsAndBytes, GGUFGGUF (Q4_K_M, Q5_K_M, Q8_0)GGUF (Q2_K through Q8_0, IQ4_XS)
Schema / Guided DecodingFirst-class (guided_json, response_format FSM)Basic JSON mode / tool callsGrammar-based GBNF + JSON schema
Ideal Use CaseMulti-agent systems, team APIs, 24/7 production servicesLocal CLI testing, IDE assistants, quick model swapsEdge hardware, CPU+GPU split offloading, minimal footprint

The Pragmatic Dual-Runtime Pattern

In our production environment, we run vLLM on port 8000 for our always-on production model (called by agents and web services) and Ollama on port 11434 for ad-hoc testing of newly released checkpoints before promoting them to production.


Chapter 3: Copy-Paste Production Setup (docker-compose.yml + systemd)

Option A: Docker Compose Stack (vLLM + Open WebUI / Local API)

Save this as docker-compose.yml for a reproducible local LLM deployment backed by local NVMe storage:

services:
  vllm-primary:
    image: vllm/vllm-openai:latest
    container_name: vllm-primary
    restart: unless-stopped
    ipc: host
    shm_size: "16g"
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      - HF_HOME=/mnt/nvme/hf
      - VLLM_API_KEY=${VLLM_API_KEY:-local-prod-key}
    volumes:
      - /mnt/nvme/hf:/mnt/nvme/hf
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    command: >
      --model Qwen/Qwen2.5-32B-Instruct-AWQ
      --served-model-name local-primary
      --dtype auto
      --max-model-len 32768
      --gpu-memory-utilization 0.85
      --enable-prefix-caching
      --port 8000
      --host 0.0.0.0
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8000/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 180s

Running a foreground container inside systemd gives you the prebuilt CUDA/vLLM container image plus native journald logging, boot ordering, and controlled restart backoff (StartLimitBurst):

# /etc/systemd/system/vllm.service
[Unit]
Description=vLLM OpenAI-Compatible Local LLM Server
After=network-online.target docker.service
Wants=network-online.target docker.service
StartLimitIntervalSec=300
StartLimitBurst=3

[Service]
Type=simple
Restart=on-failure
RestartSec=15
TimeoutStartSec=1800
Environment=HF_HOME=/mnt/nvme/hf
Environment=VLLM_API_KEY=local-prod-key
ExecStartPre=-/usr/bin/docker rm -f vllm
ExecStart=/usr/bin/docker run --rm --name vllm \
  --gpus all --ipc=host --shm-size=16g \
  --env HF_HOME=/mnt/nvme/hf \
  -v /mnt/nvme/hf:/mnt/nvme/hf \
  -p 127.0.0.1:8000:8000 \
  vllm/vllm-openai:latest \
  --model /mnt/nvme/hf/models/Qwen2.5-32B-Instruct-AWQ \
  --served-model-name local \
  --dtype auto \
  --max-model-len 32768 \
  --gpu-memory-utilization 0.85 \
  --api-key "${VLLM_API_KEY}" \
  --port 8000 --host 0.0.0.0
ExecStop=/usr/bin/docker stop vllm

[Install]
WantedBy=multi-user.target

Chapter 4: Pre-Flight Checklist & Watchdog Health Checks (Merged Operational Guide)

Consolidated from our GB10 Pre-Flight Checklist and Local LLM Watchdog operational runbooks.

A local inference server rarely dies cleanly. Often the process stays up, port 8000 stays open, and /health keeps returning 200 OK while the inference engine is wedged behind a fragmented KV cache. Relying on Restart=always alone will leave your server in a zombie state for days.

1. The 5-Point Pre-Flight Checklist (Especially on Unified-Memory / GB10 Boxes)

  1. Pick quantization before sizing context: On Hopper/Blackwell GPUs (including GB10), FP8 gives near-BF16 accuracy at half the memory; AWQ / NVFP4 / GGUF Q4_K_M quarters the weight footprint.
  2. Calculate --gpu-memory-utilization against post-reserve RAM: Run free -g on an idle box, subtract 12–16 GB for OS + container overhead, and allocate 80–88% of the remainder.
  3. Pin HF_HOME to local NVMe: Never load 40GB+ weights over NFS/SMB. Measure both cold start (disk read + CUDA graph capture) and warm start (page-cached) times and record them in your unit file.
  4. Enforce client-side timeouts and fallback routing: Every client calling http://127.0.0.1:8000/v1 should set a 20-second connect/first-token timeout and fail over cleanly if the local box is restarting.
  5. Test readiness with a real generation probe, not TCP liveness: Verify that the model actually decodes tokens at temperature: 0.

2. Exit-Code-Classified Generation Watchdog (healthcheck.py)

This lightweight Python probe sends a 16-token streaming request with a cache-busting UUID marker, measures Time-To-First-Token (TTFT), and classifies the state via exit code so your watchdog only restarts when a restart will actually fix the problem:

  • Exit 0: Healthy — token generated within latency budget.
  • Exit 2: Unreachable / hung socket — safe for watchdog to drain and restart.
  • Exit 3: Corrupt/wrong output — alert human, do not restart-loop.
  • Exit 4: Correct answer but slow TTFT (queue pressure / thermal throttling) — log warning, do not clear KV cache with a restart.
#!/usr/bin/env python3
# /opt/llm/healthcheck.py
import json, os, time, urllib.request, uuid

BASE = os.environ.get("LLM_BASE", "http://127.0.0.1:8000")
MODEL = os.environ.get("LLM_MODEL", "local")
API_KEY = os.environ.get("VLLM_API_KEY", "local-prod-key")
BUDGET_S = float(os.environ.get("HEALTH_BUDGET_S", "20"))
MARKER = "READY-" + uuid.uuid4().hex[:6]

def probe():
    payload = {
        "model": MODEL,
        "messages": [{"role": "user", "content": f"Reply with exactly this token: {MARKER}"}],
        "temperature": 0,
        "max_tokens": 16,
        "stream": True,
    }
    req = urllib.request.Request(
        BASE + "/v1/chat/completions",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}"},
    )
    start, ttft, chunks = time.monotonic(), None, []
    with urllib.request.urlopen(req, timeout=BUDGET_S) as resp:
        for raw in resp:
            line = raw.decode().strip()
            if not line.startswith("data:"):
                continue
            body = line[5:].strip()
            if body == "[DONE]":
                break
            delta = json.loads(body)["choices"][0]["delta"].get("content")
            if delta:
                if ttft is None:
                    ttft = time.monotonic() - start
                chunks.append(delta)
    return ttft or (time.monotonic() - start), time.monotonic() - start, "".join(chunks)

if __name__ == "__main__":
    try:
        ttft, total, answer = probe()
    except Exception as exc:
        print(f"unreachable: {exc}")
        raise SystemExit(2)      # Dead socket/hung engine -> restart
    if MARKER not in answer:
        print(f"wrong answer: {answer!r}")
        raise SystemExit(3)      # Corrupt weights/tokenizer -> page human
    print(f"ok ttft={ttft:.2f}s total={total:.2f}s")
    if ttft > BUDGET_S / 2:
        raise SystemExit(4)      # Slow under load -> alert only, do not restart
    raise SystemExit(0)

Pair /opt/llm/healthcheck.py with a 60-second systemd timer (SuccessExitStatus=0 4) and a drain wrapper that checks vllm:num_requests_running on /metrics before executing systemctl restart vllm.service:

# /etc/systemd/system/llm-healthcheck.service
[Unit]
Description=Local LLM generation readiness probe
After=vllm.service

[Service]
Type=oneshot
Environment=LLM_MODEL=local
Environment=LLM_BASE=http://127.0.0.1:8000
Environment=HEALTH_BUDGET_S=20
ExecStart=/opt/llm/healthcheck.py
SuccessExitStatus=0 4
OnFailure=llm-drain-restart.service

Chapter 5: Production Gotchas We’ve Hit First-Hand

  1. KV-cache memory balloons at 64K–128K context — A 32K context window handles 99% of agent and coding tasks while leaving double the VRAM for concurrent batching.
  2. Setting --gpu-memory-utilization above 0.92 — Looks great on a single prompt, then crashes under a 4-agent parallel burst. Keep it at 0.85–0.88.
  3. Silent CPU fallback in Ollama — If a model exceeds VRAM by even 1 GB, Ollama spills layers to system RAM and throughput drops from 40 tok/s to 4 tok/s. Always verify ollama ps (100% GPU) or nvidia-smi after startup.
  4. Prompt cache misses from dynamic timestamps — Put static instructions first and dynamic timestamps/UUIDs at the end of the user message so vLLM’s --enable-prefix-caching hits >80% cache reuse.

The Complete Local LLM Deployment Checklist

  • Hardware VRAM budget computed (weights + 32K KV cache + 16 GB OS reserve)
  • Runtime matched to workload (vLLM for concurrent APIs/agents, Ollama for local prototyping)
  • Quantization (FP8, AWQ, or GGUF Q4_K_M) tested against real structured JSON prompts
  • Model cache pinned to local NVMe (HF_HOME=/mnt/nvme/hf)
  • Foreground Docker container managed by systemd with StartLimitBurst=3
  • Generation health-check probe (healthcheck.py) running on a 60s systemd timer
  • Client-side timeout (20s) + fallback API route configured

If you want all of these systemd units, dual-model vLLM configs, memory-planning worksheets, and watchdog scripts pre-built and tested for NVIDIA GB10 / Linux servers, grab our DGX Spark LLM Deployment Kit ($79) (direct Gumroad download).


Related Guides:

🎁 Want to try before you buy?

Both samples are 100% free. Enter your email to unlock the download links and get our 3-day AI toolkit drip in your inbox.

No spam. Unsubscribe anytime. We respect your privacy.

Or browse the full catalogue in the Slashman Tools storefront.

About the author
Published by slashman413 — writing practical, evergreen guides on money, productivity, developer tooling and the web. More about this site →

Frequently Asked Questions

What is the best runtime for local LLM deployment in 2026: vLLM, Ollama, or llama.cpp?

Use vLLM for production multi-agent pipelines and shared team APIs where continuous batching and high concurrent throughput matter. Use Ollama for single-user interactive prototyping, and llama.cpp for CPU+GPU hybrid inference or Apple Silicon/heterogeneous hardware.

How much VRAM do I need for local LLM deployment?

A 7B–9B model in Q4/FP8 needs 6–10 GB VRAM; a 14B model needs 10–16 GB; a 32B–35B model needs 20–24 GB in 4-bit AWQ/GGUF (or 36 GB in FP8); and a 70B–72B model requires 42–48 GB for weights plus 12–20 GB for a 32K KV cache, fitting comfortably on a 128 GB unified-memory box like the NVIDIA DGX Spark (GB10).

Why isn't systemd Restart=always enough for a local LLM server?

Restart=always only triggers when the OS process exits. When a local LLM server wedges due to KV-cache exhaustion or a CUDA lockup, the port stays open and /health still returns 200. You need a generation watchdog probe that sends a short completion request and classifies exit codes before draining and restarting.

🎁 Recommended Tools

📚 Related Articles

🎁 Free AI Productivity Toolkit

50+ curated prompts + tools comparison + workflow templates. Free download — sent to your inbox instantly.

Free forever · No spam · Unsubscribe anytime · Sent instantly

🎁 Get Free Toolkit