The question of whether to self-host llama vs gpt-5 isn’t philosophical — it’s a capacity planning exercise. If you control the hardware, you control the latency tail, the data residency, and the marginal cost per token. If you don’t, you’re buying someone else’s SLA. This guide walks through the decision framework, hardware sizing, model selection, and a production-ready deployment path you can run today.
Step 1: Define your constraints before picking a model
Write down the non-negotiables first. Every team that skips this step ends up re-architecting six months later.
Latency budget. What’s your p99 target? If it’s sub-200ms for first token, you need GPU memory bandwidth that consumer cards struggle to deliver at scale. If 2-3 seconds is acceptable, a single 24GB or 48GB GPU opens many doors.
Throughput requirement. Requests per second at peak determines GPU count more than model size. A single H100 80GB serves roughly 120-180 tokens/second on Llama-3-70B at FP8. Multiply by your concurrent users and add 40% headroom.
Data residency and compliance. If prompts or completions cannot leave your VPC, the decision is made for you. Self-hosting is the only option.
Operational maturity. Do you have on-call for GPU drivers, CUDA version drift, and kernel OOM kills? If not, factor in 0.5-1 FTE for platform work.
Cost horizon. Compare 3-year TCO: GPU depreciation + power + rack space + engineering time vs. API spend at projected volume. At 50M tokens/month, API often wins. At 500M+, self-hosting usually wins — but only if utilization stays above 60%.
Step 2: Choose the right Llama 3 variant and quantization
Llama 3 comes in 8B and 70B parameter sizes. The 405B model exists but requires 8x H100 for reasonable throughput — treat it as a research target, not a starting point.
| Model | FP16 VRAM | 4-bit (GPTQ/AWQ) VRAM | 8-bit VRAM | Typical use case |
|---|---|---|---|---|
| Llama-3-8B | 16 GB | 6 GB | 10 GB | Classification, extraction, routing, low-latency chat |
| Llama-3-70B | 140 GB | 40 GB | 72 GB | Reasoning, coding, long-context, quality-critical tasks |
Quantization guidance:
- 4-bit (GPTQ/AWQ): Negligible quality loss on 8B; measurable but acceptable on 70B for most tasks. Use AWQ for vLLM, GPTQ for TGI/ExLlamaV2.
- 8-bit: Near-FP16 quality, 2x memory savings. Good compromise if 4-bit degrades your eval set.
- FP8 (H100/H200 only): Native datatype, best throughput per watt. Requires model pre-quantized to FP8 (available on Hugging Face as
meta-llama/Meta-Llama-3-70B-Instruct-FP8).
Context length. Llama 3 supports 8K natively. For 128K, use llama-3-8b-128k or llama-3-70b-128k fine-tunes (e.g., from Gradient, Together, or NVIDIA). RoPE scaling adds ~15% KV cache overhead per doubling.
Step 3: Size the hardware
Single-GPU targets (consumer/prosumer):
- RTX 3090/4090 (24GB): Llama-3-8B at 4-bit, batch size 1-4. ~45 tok/s.
- RTX 6000 Ada (48GB): Llama-3-70B at 4-bit, batch size 1-2. ~25 tok/s.
- Dual 3090/4090 (48GB combined via NVLink): Llama-3-70B at 4-bit, batch size 4-8. ~55 tok/s.
Multi-GPU targets (datacenter):
- 4x A100 80GB (320GB): Llama-3-70B FP16, tensor parallel 4. ~180 tok/s.
- 8x H100 80GB (640GB): Llama-3-70B FP8, tensor parallel 8. ~1,200 tok/s.
- 2x H100 80GB (160GB): Llama-3-70B FP8, tensor parallel 2. ~300 tok/s.
CPU offload (Mac/Unified memory / Linux + system RAM):
- Apple M2/M3 Ultra (192GB): Llama-3-70B 4-bit at ~8 tok/s. Viable for dev/test, not production throughput.
- Linux + 256GB DDR5 + 24GB GPU: Offload layers to CPU via
llama.cpporexllama_v2. Expect 3-5 tok/s on 70B. Use only when GPU budget is zero.
Rule of thumb: VRAM_needed = model_size_bytes * 1.2 (KV cache) * 1.1 (overhead) / quantization_factor. For 70B at 4-bit: 70e9 * 0.5 * 1.32 ≈ 46 GB. Round up to nearest GPU boundary.
Step 4: Pick an inference server
Three production-grade options dominate. Pick one and standardize.
Option A: vLLM (recommended for throughput)
Best-in-class PagedAttention kernel, continuous batching, OpenAI-compatible API, active development.
# Install
pip install vllm==0.6.3
# Run Llama-3-70B 4-bit AWQ on 2x H100 (tensor parallel 2)
vllm serve meta-llama/Meta-Llama-3-70B-Instruct-AWQ \
--tensor-parallel-size 2 \
--dtype half \
--max-model-len 8192 \
--gpu-memory-utilization 0.9 \
--port 8000 \
--api-key $VLLM_API_KEY
Key flags:
--max-model-len: Controls KV cache allocation. Set to your actual max context + 10%.--gpu-memory-utilization: Leave 10% for OS/CUDA context. OOM kills happen at 0.98+.--enable-prefix-caching: Turn on if you have repetitive system prompts (RAG, few-shot).--kv-cache-dtype fp8: On H100, cuts KV cache memory in half with minimal quality loss.
Option B: Text Generation Inference (TGI)
Hugging Face’s server. Strong on speculative decoding, grammar-constrained generation, and safetensors loading.
docker run --gpus all --shm-size 32g \
-v $HF_HOME:/data ghcr.io/huggingface/text-generation-inference:2.3.1 \
--model-id meta-llama/Meta-Llama-3-70B-Instruct-GPTQ \
--quantize gptq \
--max-input-length 4096 \
--max-total-tokens 8192 \
--port 8080
TGI shines when you need:
- Guided JSON/Regex via
--grammar - Speculative decoding with a draft model (
--speculate 5) - Faster cold starts (smaller container, no Python overhead)
Option C: Ollama (simplest for single-node dev)
ollama run llama3:70b-instruct-q4_K_M
# API at http://localhost:11434/v1/chat/completions
Not built for multi-GPU tensor parallel or high-concurrency production. Use for local dev, eval harnesses, and edge deployments.
Step 5: Deploy with observability from day one
You cannot operate what you cannot measure. Deploy the stack with these components:
Prometheus metrics. All three servers expose /metrics. Scrape:
vllm:request_latency_seconds(histogram)vllm:gpu_cache_usage_perc(gauge)vllm:requests_running/requests_waiting(concurrency)tgi:queue_size,tgi:batch_total_token_throughput
Grafana dashboard. Import dashboard 15861 (vLLM) or 16203 (TGI). Alert on:
- p99 latency > 2x baseline for 5m
- GPU memory > 90% for 2m
- Queue depth > 2x GPU count
Structured logging. Log every request with: request_id, model, prompt_tokens, completion_tokens, latency_ms, finish_reason, user_id. Ship to Loki or Elastic.
# middleware example for vLLM OpenAI-compatible endpoint
import time, uuid
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
req_id = request.headers.get("x-request-id", str(uuid.uuid4()))
start = time.perf_counter()
response = await call_next(request)
duration_ms = (time.perf_counter() - start) * 1000
# Extract token counts from response headers if present
prompt_toks = response.headers.get("x-prompt-tokens", "?")
completion_toks = response.headers.get("x-completion-tokens", "?")
log.info(
"llm_request",
request_id=req_id,
model=request.path_params.get("model", "unknown"),
prompt_tokens=prompt_toks,
completion_tokens=completion_toks,
latency_ms=round(duration_ms, 1),
status=response.status_code,
)
response.headers["x-request-id"] = req_id
return response
Health checks. Kubernetes liveness/readiness probes:
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /v1/models
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
vLLM’s /health returns 200 only when the engine is ready to accept requests (model loaded, workers healthy).
Step 6: Implement routing and fallback
Production traffic needs resilience. Two patterns work well:
Pattern 1: Client-side routing with weighted fallbacks
import openai
import random
class RoutedClient:
def __init__(self):
self.primary = openai.OpenAI(
base_url="https://llm-prod.internal/v1",
api_key=os.getenv("PRIMARY_KEY"),
)
self.fallback = openai.OpenAI(
base_url="https://api.openai.com/v1",
api_key=os.getenv("OPENAI_KEY"),
)
def chat(self, messages, **kwargs):
try:
return self.primary.chat.completions.create(
model="llama-3-70b-instruct",
messages=messages,
timeout=30,
**kwargs
)
except Exception as e:
log.warning("primary_failed", error=str(e))
# Fallback to GPT-4o-mini for cost control
return self.fallback.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
**kwargs
)
Pattern 2: Gateway-level routing (n4n.ai style)
If you run a gateway in front of multiple backends, implement:
- Provider health checks every 10s
- Automatic failover on 5xx or latency > p99 threshold
- Per-request routing headers (
x-prefer-provider: self-hosted) - Usage metering per backend for cost allocation
# Example gateway config snippet
routes:
- match: "*"
backends:
- name: vllm-70b-primary
weight: 90
health_check: /health
- name: vllm-70b-secondary
weight: 5
health_check: /health
- name: openai-gpt4o-mini
weight: 5
health_check: /v1/models
fallback_policy: "sequential"
timeout_ms: 30000
Step 7: Validate with an eval harness
Before cutting traffic, run your task-specific eval set. Do not rely on MMLU or GSM8K — they don’t correlate with your use case.
# eval_harness.py
import json
from openai import OpenAI
from datasets import load_dataset
client = OpenAI(base_url="http://localhost:8000/v1", api_key="local")
def evaluate(split="test", n=200):
ds = load_dataset("your-org/your-eval-set", split=split).shuffle(seed=42).select(range(n))
results = []
for row in ds:
completion = client.chat.completions.create(
model="llama-3-70b-instruct",
messages=[{"role": "user", "content": row["prompt"]}],
temperature=0.0,
max_tokens=512,
)
pred = completion.choices[0].message.content
results.append({
"id": row["id"],
"expected": row["expected"],
"predicted": pred,
"latency_ms": completion.usage.total_tokens * 1000 / 150, # rough
})
# Your task-specific scorer here
score = your_scorer(results)
print(f"Score: {score:.3f}")
return results
if __name__ == "__main__":
evaluate()
Acceptance criteria examples:
- Classification F1 > 0.92 on your taxonomy
- Code generation pass@1 > 0.65 on your internal bench
- RAG answer faithfulness > 0.88 (use LLM-as-judge with a rubric)
- p99 latency < 3s at 50 concurrent requests
If the self-hosted model fails, quantify the gap. Sometimes a smaller model + better prompting beats a larger model zero-shot.
Step 8: Iterate on serving optimizations
Once baseline works, apply these in order of ROI:
-
Prefix caching. Enable
--enable-prefix-cachingin vLLM. Measure cache hit rate. If > 40%, you’re saving significant prefill compute. -
Speculative decoding. Add a draft model (Llama-3-8B) for 70B:
vllm serve meta-llama/Meta-Llama-3-70B-Instruct-AWQ \ --speculative-model meta-llama/Meta-Llama-3-8B-Instruct \ --num-speculative-tokens 5 \ --tensor-parallel-size 2Typical 1.5-2x throughput gain with zero quality loss.
-
FP8 KV cache. On H100/H200:
--kv-cache-dtype fp82x KV capacity, enabling larger batch sizes or longer contexts.
-
Chunked prefill. For long contexts (>4K), vLLM 0.6+ supports
--enable-chunked-prefillwith--max-num-batched-tokens. Prevents OOM on large prefill batches. -
Quantization re-eval. Every 3 months, re-run eval on newer quantization methods (AWQ-GEMM, HQQ, QuaRot). 3-bit may become viable for your task.
Step 9: Plan for model updates
Llama 3.1, 3.2, 4.0 will arrive. Your deployment must support zero-downtime model swaps.
Blue-green with vLLM:
# Start new model on port 8001
vllm serve meta-llama/Meta-Llama-3.1-70B-Instruct-AWQ \
--port 8001 --tensor-parallel-size 2 &
# Run smoke tests against :8001
python smoke_test.py --port 8001
# Flip gateway traffic (or DNS, or load balancer weight)
# Drain old workers on :8000
Version pinning. Never pull latest. Pin to digest:
image: vllm/vllm-openai@sha256:abc123...
Rollback window. Keep previous model loaded for 15 minutes after swap. If error rate spikes, flip back instantly.
Verification checklist
Run these before declaring production readiness:
- Load test:
hey -c 50 -n 1000 -m POST -H "Content-Type: application/json" -d '{"model":"llama-3-70b","messages":[{"role":"user","content":"Hello"}]}' http://localhost:8000/v1/chat/completions— verify p99 < target, no OOM, no 5xx. - Soak test: 24h at 80% expected peak QPS. Check for memory leaks (GPU memory monotonic increase), log growth, metric cardinality explosion.
- Failure injection: Kill one GPU process (
kill -9 <worker_pid>). Verify request completes on remaining workers, queue drains, no stuck connections. - Cold start: Scale deployment to 0, send request. Measure time-to-first-token. Should be < 60s for 70B on H100 (model load + warmup).
- Cost accounting: Run 1M tokens through the pipeline. Verify per-token cost matches your TCO model within 15%.
- Eval regression: Re-run Step 7 harness. Score must not degrade vs. baseline.
When to stay on the API
Self-hosting makes sense when you have: sustained high volume (>100M tokens/month), strict data residency, latency tail requirements APIs can’t meet, or need custom model modifications (LoRA, continued pretraining, architecture changes).
It does not make sense when: traffic is bursty and unpredictable, team lacks GPU ops experience, you need frontier-model reasoning (GPT-5/Claude-Opus class) today, or compliance requires SOC2/HIPAA on the inference layer itself — in which case a dedicated deployment via a gateway that handles fallbacks and metering (like n4n.ai) may be the faster path to production.
The hardware is the easy part. The operational discipline — capacity planning, eval-driven upgrades, incident response for CUDA driver regressions — is where teams succeed or fail. Start with the eval harness. Let the numbers decide.