Local LLM Deployments: Methods, Quantization, and OpenAI-Compatible Serving¶
Objective: A ruthless field guide to running modern LLMs on your own hardware. CPU to GPU, tiny to obnoxious, with real benchmarks and a path to RAG.
Mental Model: What You're Actually Choosing¶
Inference Stack Components¶
- Model Format: GGUF, GPTQ, AWQ, FP16/BF16
- Runtime: llama.cpp, vLLM, TGI, Ollama
- Serving API: OpenAI-compatible, native REST
- Orchestration: Docker Compose profiles, resource management
Three Canonical Paths¶
| Path | Format | Runtime | Best For | Memory | Throughput |
|---|---|---|---|---|---|
| Ollama | GGUF | Ollama | Easy setup, Mac M-series | 5-16GB | Medium |
| llama.cpp | GGUF | llama.cpp | CPU-only, edge deployment | 4-12GB | Low |
| vLLM/TGI | HF Models | vLLM/TGI | GPU serving, high throughput | 8-24GB | High |
Other Worthwhile Options¶
- LM Studio: Desktop UI for GGUF models
- KoboldCpp: Roleplay and longform generation
- MLC: WebGPU inference for browsers
- ONNX Runtime/OpenVINO: Intel CPU/iGPU optimization
- TensorRT-LLM: NVIDIA maximum speed (enterprise)
Model Formats & Quantization Crash Kit¶
GGUF Quantization (Safest Local Bet)¶
| Quantization | Quality | Memory | Use Case |
|---|---|---|---|
| Q4_K_M | Good | 5-6GB (8B) | Chat, tooling, minimal VRAM |
| Q5_K_M | Better | 6-8GB (8B) | Reasoning, writing |
| Q6_K | High | 8-10GB (8B) | Code, analysis |
| Q8_K | Near-FP16 | 12-14GB (8B) | Best quality, heavy |
GPU-Optimized Formats¶
- GPTQ: 4-bit weight-only, exllama kernels
- AWQ: 4-bit weight-only, AWQ kernels
- FP16/BF16: Best quality, needs big VRAM
Memory Planning Quick Sheet¶
# 8B Model Memory Requirements
Q4_K_M: ~5-6 GB RAM/VRAM
Q6_K: ~8-10 GB RAM/VRAM
Q8_K: ~12-14 GB RAM/VRAM
FP16: ~16 GB VRAM
# 13B Model Memory Requirements
Q4_K_M: ~8-10 GB RAM/VRAM
Q6_K: ~12-14 GB RAM/VRAM
FP16: ~24 GB VRAM
# 70B Model Memory Requirements
Q4_K_M: ~40-45 GB RAM/VRAM
FP16: "Don't."
Docker Compose: Profile the Zoo¶
docker-compose.yml¶
version: "3.9"
x-hc: &hc { interval: 5s, timeout: 3s, retries: 40 }
services:
# --- OLLAMA + UI ---
ollama:
image: ollama/ollama:latest
profiles: ["ollama"]
environment:
- OLLAMA_KEEP_ALIVE=24h
volumes:
- ollama:/root/.ollama
ports:
- "11434:11434"
healthcheck:
test: ["CMD","curl","-sf","http://localhost:11434/api/tags"]
<<: *hc
openwebui:
image: ghcr.io/open-webui/open-webui:latest
profiles: ["ollama"]
depends_on:
ollama:
condition: service_healthy
environment:
- OLLAMA_BASE_URL=http://ollama:11434
ports:
- "8080:8080"
# --- llama.cpp OpenAI-compatible server (CPU/GPU) ---
llamacpp:
image: ghcr.io/ggerganov/llama.cpp:server
profiles: ["llamacpp"]
command:
- "-m"
- "/models/model.gguf"
- "-c"
- "4096"
- "--host"
- "0.0.0.0"
- "--port"
- "8001"
- "--api"
volumes:
- "./models:/models"
ports:
- "8001:8001"
# --- vLLM (GPU) ---
vllm:
image: vllm/vllm-openai:latest
profiles: ["vllm"]
shm_size: "2g"
deploy:
resources:
reservations:
devices:
- capabilities: ["gpu"]
command: >
--model meta-llama/Meta-Llama-3-8B-Instruct
--dtype bfloat16
--max-model-len 8192
--gpu-memory-utilization 0.85
ports:
- "8002:8000"
# --- TGI (GPU) ---
tgi:
image: ghcr.io/huggingface/text-generation-inference:latest
profiles: ["tgi"]
deploy:
resources:
reservations:
devices:
- capabilities: ["gpu"]
environment:
- MODEL_ID=NousResearch/Hermes-3-Llama-3.1-8B
ports:
- "8003:80"
# --- RAG bits: Qdrant + embeddings + demo API ---
qdrant:
image: qdrant/qdrant:latest
profiles: ["rag"]
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant:/qdrant/storage
e5-small:
image: ghcr.io/huggingface/text-embeddings-inference:cpu-0.4
profiles: ["rag"]
environment:
- MODEL_ID=intfloat/e5-small-v2
ports:
- "8808:80"
rag-api:
build: ./rag-api
profiles: ["rag"]
environment:
OPENAI_BASE_URL: http://vllm:8000
QDRANT_URL: http://qdrant:6333
EMB_URL: http://e5-small:80
ports:
- "8010:8010"
# --- bench pod ---
bench:
build: ./bench
profiles: ["bench"]
command: ["bash","-lc","python bench.py"]
environment:
OPENAI_ENDPOINT: http://localhost:8002/v1
MODEL: meta-llama/Meta-Llama-3-8B-Instruct
volumes:
qdrant:
ollama:
Operator Recipes¶
# 1) One-command UX (Ollama + OpenWebUI)
docker compose --profile ollama up -d
# Pull & run a model
curl http://localhost:11434/api/pull -d '{"name":"llama3.1:8b"}'
# 2) OpenAI-compatible llama.cpp
docker compose --profile llamacpp up -d
curl http://localhost:8001/v1/models
# 3) GPU throughput (vLLM)
docker compose --profile vllm up -d
curl http://localhost:8002/v1/models
# 4) TGI alternative
docker compose --profile tgi up -d
# 5) Minimal RAG (Qdrant + embeddings + demo FastAPI)
docker compose --profile rag --profile vllm up -d
# 6) Benchmark
docker compose --profile bench up --build
OpenAI-Compatible Calls (Switch Backends Freely)¶
vLLM (High Throughput)¶
curl http://localhost:8002/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Meta-Llama-3-8B-Instruct",
"messages":[{"role":"user","content":"Explain locality-sensitive hashing."}],
"temperature": 0.2,
"max_tokens": 512
}'
llama.cpp Server (CPU/GPU)¶
curl http://localhost:8001/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "model.gguf",
"messages":[{"role":"user","content":"Explain locality-sensitive hashing."}],
"temperature": 0.2,
"max_tokens": 512
}'
Ollama (Native API)¶
curl http://localhost:11434/api/generate \
-d '{"model":"llama3.1:8b","prompt":"haiku about cache misses","stream":false}'
Methodologies: Choose Your Poison¶
A) Ollama (GGUF; Easiest)¶
Pros: Single endpoint, model zoo, CPU/GPU, fast setup, Mac M-series optimization Cons: Less tunable than raw vLLM/TGI; mixed plugin ecosystem Use when: Prototyping, local UX apps (OpenWebUI), mixed OS fleet
B) llama.cpp (GGUF; Absolute Portability)¶
Pros: Smallest memory footprint per quality; insane quant variety; CPU-only feasible Cons: Lower throughput vs GPU servers; long context windows can get slow Use when: Edge/air-gapped, CPU-only nodes, deterministic packaging
C) vLLM (GPU; High Throughput)¶
Pros: PagedAttention = large context + strong throughput; OpenAI API out of the box Cons: GPU required; model weights usually FP16/BF16 or GPTQ/AWQ variants Use when: Serving, multi-tenant, latency targets
D) TGI (GPU; Rock-Solid Production)¶
Pros: Battle-tested with HF models, tensor parallel, tokenizer server Cons: Bigger footprint, config verbosity Use when: Enterprise serving with HF ecosystem
Prompting & Sampling Defaults¶
Sane Defaults (Don't Sabotage Yourself)¶
Use Case Specific Settings¶
// Coding/Agents
{
"temperature": 0.0,
"top_p": 0.95,
"max_tokens": 2048
}
// Creative Writing
{
"temperature": 0.7,
"top_p": 0.9,
"max_tokens": 1024
}
// Reasoning/Analysis
{
"temperature": 0.2,
"top_p": 0.9,
"max_tokens": 1024
}
RAG Mini-Stack (Works with Any Server)¶
rag-api/Dockerfile¶
FROM python:3.11-slim
RUN pip install fastapi uvicorn requests qdrant-client
WORKDIR /app
COPY main.py .
CMD ["python", "main.py"]
rag-api/main.py¶
from fastapi import FastAPI
import requests, uuid, json
from qdrant_client import QdrantClient
from qdrant_client.http import models
APP = FastAPI()
QDRANT = QdrantClient(url="http://qdrant:6333")
COL = "docs"
EMB = "http://e5-small:80/embed"
OAI = "http://vllm:8000/v1"
MODEL = "meta-llama/Meta-Llama-3-8B-Instruct"
def embed(texts):
r = requests.post(EMB, json={"inputs": texts})
return r.json()["embeddings"]
@APP.post("/ingest")
def ingest(doc: dict):
chunks = [doc["text"][i:i+800] for i in range(0, len(doc["text"]), 800)]
vecs = embed(chunks)
QDRANT.upsert(collection_name=COL, points=[
models.PointStruct(id=str(uuid.uuid4()), vector=v, payload={"text": c})
for v, c in zip(vecs, chunks)
])
return {"ok": True}
@APP.post("/ask")
def ask(q: dict):
qvec = embed([q["question"]])[0]
hits = QDRANT.search(collection_name=COL, query_vector=qvec, limit=4)
context = "\n\n".join([h.payload["text"] for h in hits])
prompt = f"Answer using only the context.\n\nContext:\n{context}\n\nQ: {q['question']}\nA:"
r = requests.post(f"{OAI}/chat/completions", json={
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.2
})
return r.json()
if __name__ == "__main__":
import uvicorn
uvicorn.run(APP, host="0.0.0.0", port=8010)
Benchmarks (Latency, Throughput, Token-per-Second)¶
bench/Dockerfile¶
FROM python:3.11-slim
RUN pip install requests
WORKDIR /app
COPY bench.py .
CMD ["python", "bench.py"]
bench/bench.py¶
import os, time, requests
ENDPOINT = os.getenv("OPENAI_ENDPOINT", "http://localhost:8002/v1")
MODEL = os.getenv("MODEL", "meta-llama/Meta-Llama-3-8B-Instruct")
prompt = "Summarize: " + "lorem ipsum " * 200
print(f"Benchmarking {ENDPOINT} with model {MODEL}")
# Latency test
t0 = time.time()
r = requests.post(f"{ENDPOINT}/chat/completions", json={
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.2,
"max_tokens": 512
}, timeout=120)
dt = time.time() - t0
out = r.json()["choices"][0]["message"]["content"]
tps = 512 / dt
print(f"Latency: {dt:.2f}s ~{tps:.1f} tok/s len={len(out)}")
# Throughput test (concurrent requests)
import concurrent.futures
import threading
def single_request():
t0 = time.time()
r = requests.post(f"{ENDPOINT}/chat/completions", json={
"model": MODEL,
"messages": [{"role": "user", "content": "Hello"}],
"temperature": 0.2,
"max_tokens": 100
}, timeout=60)
return time.time() - t0
# Run 5 concurrent requests
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(single_request) for _ in range(5)]
times = [f.result() for f in futures]
avg_time = sum(times) / len(times)
print(f"Concurrent avg: {avg_time:.2f}s per request")
Benchmark Interpretation Rules¶
- If tps < 8 on CPU: Step down quantization or batch size; consider GPU
- If GPU OOM: Reduce max context, use quantized weights (GPTQ/AWQ), or try paged KV (vLLM)
- For multi-client serving: Prefer vLLM or TGI over Ollama/llama.cpp
Security, Licensing, and Tribal Scars¶
Model Licenses¶
- Llama 3.1: Meta license (commercial use allowed)
- Code Llama: Meta license (commercial use allowed)
- Mistral: Apache 2.0 (permissive)
- Qwen: Tongyi Qianwen license (check restrictions)
- Always verify: Don't ship proprietary weights where you shouldn't
Security Best Practices¶
# Don't expose your local server to the internet naked
# Use reverse proxy + auth
nginx -c nginx.conf
# Keep prompt/response logs off shared disks
# If you run agents, rate-limit tools and cap budgets
Production Considerations¶
- Rate Limiting: Implement request throttling
- Authentication: Add API keys or OAuth
- Monitoring: Log usage and performance metrics
- Backup: Model weights and configurations
Failure Modes & Fixes¶
Common Issues and Solutions¶
| Problem | Cause | Solution |
|---|---|---|
| Garbage output | Wrong chat template or over-quantized weights | Check model compatibility, try higher quant |
| Stalls/timeout | KV cache thrash | Cut max tokens or context; upgrade VRAM |
| GPU OOM on first token | Model too large for VRAM | Use --gpu-memory-utilization 0.75, smaller batch |
| Bad RAG answers | Chunking too big; embedding mismatch | Re-rank with small cross-encoder |
Debugging Commands¶
# Check GPU memory usage
nvidia-smi
# Monitor llama.cpp server
docker logs llamacpp
# Test vLLM health
curl http://localhost:8002/health
# Check Ollama models
curl http://localhost:11434/api/tags
Performance Tuning¶
# vLLM memory optimization
--gpu-memory-utilization 0.85
--max-model-len 8192
--dtype bfloat16
# llama.cpp CPU optimization
--threads 8
--batch-size 512
--ctx-size 4096
# Ollama optimization
OLLAMA_NUM_PARALLEL=2
OLLAMA_MAX_LOADED_MODELS=1
Trade-off Tables¶
Memory vs Quality vs Speed¶
| Method | Memory (8B) | Quality | Speed | Use Case |
|---|---|---|---|---|
| Q4_K_M | 5-6GB | Good | Fast | Chat, tooling |
| Q6_K | 8-10GB | Better | Medium | Reasoning |
| Q8_K | 12-14GB | High | Slow | Analysis |
| FP16 | 16GB | Best | Fast | Production |
Backend Comparison¶
| Backend | Setup | Memory | Throughput | API | Best For |
|---|---|---|---|---|---|
| Ollama | Easy | Medium | Medium | Native | Prototyping |
| llama.cpp | Medium | Low | Low | OpenAI | Edge/CPU |
| vLLM | Hard | High | High | OpenAI | Serving |
| TGI | Hard | High | High | Custom | Enterprise |
TL;DR Runbook¶
# Easiest path: Ollama + OpenWebUI
docker compose --profile ollama up -d
curl http://localhost:11434/api/pull -d '{"name":"llama3.1:8b"}'
# Open http://localhost:8080
# Need OpenAI API + throughput: vLLM (GPU)
docker compose --profile vllm up -d
curl http://localhost:8002/v1/models
# CPU-only edge: llama.cpp with GGUF Q4_K
docker compose --profile llamacpp up -d
# Place model.gguf in ./models/
# Add RAG: Qdrant + embeddings + demo API
docker compose --profile rag --profile vllm up -d
curl http://localhost:8010/docs
# Benchmark, then raise quant quality until speed and output are both acceptable
docker compose --profile bench up --build
Quick Start Checklist¶
- Choose your poison: Ollama (easy), llama.cpp (CPU), vLLM (GPU)
- Pick quantization: Q4_K_M (start), Q6_K (better), Q8_K (best)
- Test with benchmarks: Latency, throughput, quality
- Add RAG if needed: Qdrant + embeddings + FastAPI
- Productionize: Security, monitoring, scaling
This tutorial provides the complete machinery for running LLMs locally. Each component is production-ready, copy-paste runnable, and designed for real-world deployment with proper trade-offs and failure handling.