Startups and small dev teams are facing high per-token costs from commercial LLM APIs during R&D. Self-hosting open-source LLMs on local or small cloud infrastructure can dramatically reduce cost while giving you privacy and control.
Introduction
This guide helps engineers and technical decision-makers choose, configure, and deploy open-source LLMs on constrained hardware — from CPU-only laptops or servers to single small GPUs (8–16GB). It catalogs practical options and trade-offs, shows quick-start commands, and provides a decision framework so you can pick the right stack for your team.
Key goals:
- Minimize API costs during experimentation and internal usage
- Keep latency and throughput acceptable for R&D and low-volume production
- Preserve data privacy and avoid vendor lock-in
Hardware profiles and expectations 🔧
Pick the right target class first. Here are the common scenarios and what to expect:
- Localhost / CPU-only (8–32 vCPU, 16–64 GB RAM)
- Best for prototyping, batch jobs, and low-concurrency tooling
- Can run 1–4B parameter models with quantization (GGUF / GPTQ / AWQ)
- Small GPU (8–16 GB VRAM, consumer RTX or equivalent)
- Good for interactive apps and small team usage
- Ideal for 3–13B models with 4-bit/8-bit quantization and optimized runtimes
- Cloud micro GPU (T4, A10, etc.)
- Elastic option for short bursts or staging environments
- Consider spot instances for cost savings
Hardware decision quick rules:
- If you need sub-second latency across multiple users, go small-GPU + vLLM/TGI.
- If you mostly do offline batch processing or experimentation, CPU-only + llama.cpp/OpenVINO is fine.
Deployment methods and frameworks (what to use and when)
This section catalogs the most practical, battle-tested tools and where they fit.
Inference engines and runtimes
-
llama.cpp (GGUF) — Extremely portable, C/C++ runtime optimized for CPU and lightweight GPU offload. Great for local testing and CPU-only servers. Works well with quantized GGUF models.
-
vLLM — Designed for low-latency, high-concurrency serving on GPUs. Implements memory-efficient attention and batching (PagedAttention) to maximize throughput on limited VRAM.
-
TextGenerationInference (TGI) — NVIDIA-backed server optimized for GPU inference with support for multi-instance GPU serving and model optimizations. Good when you can run NVIDIA drivers and want a production-focused server.
-
Ollama — Developer-friendly, OpenAI-compatible API wrapper that simplifies running many GGUF models locally or on a single host.
-
LocalAI — Lightweight model server with built-in support for multiple backends (llama.cpp, ggml, etc.) and an OpenAI-compatible API, useful for self-hosting with minimal ops.
-
OpenVINO / OVMS (OpenVINO Model Server) — Intel-optimized inference and model server for CPU-first deployments, often outperforming generic runtimes on Intel hardware.
Rust ecosystem & native inference
Rust is increasingly a practical choice for deploying LLMs, especially when you value low-overhead, single-binary distribution, memory safety, and predictable performance on CPU-first hosts. The Rust ecosystem now offers native bindings and runtimes that can load quantized GGUF/ggml models for CPU inference, run LibTorch-backed models for GPU inference, or consume ONNX artifacts for optimized CPU paths.
-
Key crates & tools
llm/llama-rs/ggml-rs— Native GGUF / ggml loaders for CPU-optimized inference and quantized models.tch-rs(LibTorch bindings) — Use when you need CUDA-backed GPU inference inside a Rust binary.rust-bert— Transformer utilities and examples built ontch-rs.onnxruntime/tract— Stable ONNX runtimes for CPU and (where supported) CUDA/ONNX-TRT acceleration.huggingface-tokenizers— High-performance tokenizers in Rust for low-latency preprocessing.
-
Deployment patterns
- CPU-only service: Convert / quantize your model with Python tools (GPTQ / AWQ → GGUF) then load it in a Rust service (e.g.,
llm/llama-rs) and expose an HTTP API withaxum/hyper. - GPU-enabled Rust binary: Use
tch-rswith LibTorch to run models on CUDA if you prefer a single Rust process (note: introduces LibTorch/CUDA dependencies). - ONNX route: Convert model to ONNX and use
onnxruntime/tractfor CPU-optimized inference on server CPUs. - Hybrid architecture: Keep GPU-optimized Python services (vLLM/TGI) for heavy lifting and use a Rust gateway for routing, caching, authentication, and low-overhead pre/post-processing.
- CPU-only service: Convert / quantize your model with Python tools (GPTQ / AWQ → GGUF) then load it in a Rust service (e.g.,
-
Caveats & practical notes
- Most model conversion and cutting-edge quantization tooling still lives in the Python ecosystem; a common workflow is: convert/quantize in Python → ship GGUF/ONNX → load in Rust.
- GPU-specific, advanced quantization (e.g., bitsandbytes INT8 workflows) is more mature in Python; use Rust where operational simplicity and small binary size matter.
- The Rust ML ecosystem is rapidly maturing; expect more direct tooling and wrappers over the next year.
Example: high-level Rust server sketch (conceptual)
Below is an illustrative sketch showing the pieces (tokenizer + GGUF model loader + HTTP endpoint). Refer to the specific crate docs for exact APIs — this is intentionally high-level.
Cargo.toml (deps):
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
huggingface-tokenizers = "0.*"
llm = "*" # or use the specific crate you choose (llama-rs / ggml-rs)
main.rs (sketch):
use axum::{routing::post, Json, Router};
use serde::{Deserialize, Serialize};
use tokenizers::Tokenizer;
// use llm::Model; // pseudo import — consult crate docs for exact types
#[derive(Deserialize)]
struct InferenceRequest { prompt: String }
#[derive(Serialize)]
struct InferenceResponse { output: String }
#[tokio::main]
async fn main() {
// Load tokenizer and model (pseudo-code)
let tokenizer = Tokenizer::from_file("./tokenizer.json").unwrap();
// let model = llm::load_model("./model.gguf").unwrap();
let app = Router::new().route("/v1/generate", post(move |Json(req): Json<InferenceRequest>| async move {
// Tokenize, run model, decode – pseudocode
let tokens = tokenizer.encode(req.prompt, true).unwrap();
// let out = model.generate(&tokens);
Json(InferenceResponse { output: "<model output>".to_string() })
}));
axum::Server::bind(&"0.0.0.0:8080".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
This pattern gives you a compact, production-ready artifact: a multi-threaded Rust binary with minimal runtime overhead. For deployment, use a multi-stage Docker build (compile on a builder, copy the static binary into a minimal runtime image) to keep images small and secure.
Quantization and compression techniques
-
GGUF — A model file format often used with llama.cpp for compact CPU-friendly inference. Usually combined with 4-bit or 8-bit quantization.
-
GPTQ — Post-training quantization that produces high-quality 4-bit-aware models; commonly used to fit larger models into small GPUs.
-
AWQ (Approximate Weight Quantization) — A newer quantization technique that often improves quality for 3–13B models when using 4-bit formats.
-
bitsandbytes (bnb) — A PyTorch extension that enables 8-bit / 4-bit model loading and training on GPUs. Frequently used with Transformers-based stacks (
load_in_8bit=True,device_map='auto').
Notes on quality vs size: 8-bit quantization tends to preserve model quality better than aggressive 4-bit methods, but 4-bit gains you more memory savings. Test your downstream tasks (LLM reasoning, instruction following) — quantization effects often vary by model and task.
Memory requirements & optimization strategies 🧠
-
Model size baseline (floating point)
- 3B model (FP16): ~6–8 GB
- 7B model (FP16): ~12–16 GB
- 13B model (FP16): ~24–30 GB
-
After quantization (rough expectations)
- 4-bit (GPTQ/AWQ): ~25–35% of FP16 size (very rough)
- 8-bit / 16-bit: intermediate savings
-
Optimization techniques
- Offload / CPU+GPU hybrid: Keep hot layers on GPU, offload embeddings or blocks to RAM
- Sharding: Split model across devices if you have multiple small GPUs
- KV cache management: Use runtimes that support streaming/eviction for long contexts to avoid blowing VRAM
- Batching / dynamic batching: Aggregate requests to increase throughput but watch latency
Practical trade-offs: speed, memory, and quality
- The smaller the model, the faster and cheaper it is, but with diminishing returns for complex reasoning tasks.
- 4-bit quantization reduces memory the most but can introduce subtle quality regressions; evaluate per-task.
- CPU deployments save cost but will be slower; use OpenVINO or llama.cpp with GGUF to get reasonable latency.
Quick-starts & example commands (actionable) ⚡
1) CPU-only: Running a GGUF model with llama.cpp
- Convert an HF model to GGUF (check
llama.cpprepo conversions for your model format).
# Example: run a local gguf file with llama.cpp
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make
./main -m /path/to/model.gguf -p "Write a short summary of deployment options."
- Expose an HTTP API with a small wrapper or use
LocalAI/Ollamafor an OpenAI-compatible server.
2) Small GPU: vLLM (Docker)
# Pull image and run (example, verify image/tags in docs)
docker run --gpus all -p 8000:8000 ghcr.io/vllm/vllm:latest \
vllm serve --model /models/your-quantized-model
vLLM provides a fast HTTP inference endpoint and can manage batching and multiple concurrent sessions efficiently.
3) OpenVINO CPU optimization
# Convert with Optimum-Intel and run the OpenVINO Model Server
python -m optimum.intel.openvino.convert --model_name <HF_MODEL> --output_dir ./ov_model
# Then configure OVMS to serve the model
(OpenVINO tool names and flags change over time—consult the Optimum-Intel docs for up-to-date commands.)
4) Using bitsandbytes in a Transformers app (GPU INT8)
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(
'your-model',
load_in_8bit=True,
device_map='auto'
)
5) Quick LocalAPI: Ollama / LocalAI
- Ollama:
ollama run <model>then callhttp://localhost:11434/v1/chat/completions - LocalAI: run its binary or Docker image and point at your GGUF models; it exposes an OpenAI-compatible API
Decision Tree for Runtime Selection
Hardware available?
├── GPU (NVIDIA)
│ ├── >100 concurrent users → vLLM or SGLang
│ ├── 10-100 users → vLLM (default)
│ └── <10 users / prototyping → Ollama
├── CPU (Intel)
│ ├── Intel Xeon with AMX → OpenVINO
│ └── AMD/ARM → llama.cpp (GGUF)
├── Apple Silicon
│ └── llama.cpp (via Ollama or MLX)
└── Edge device
└── llama.cpp (GGUF Q4_K_M or smaller)
Comparison matrix — method suitability
| Method / Stack | CPU-only | Small GPU (8–16GB) | Multi-user | Ease-of-use | Best for |
|---|---|---|---|---|---|
| llama.cpp (GGUF) | ✅ Good | ✅ Ok (with offload) | ⚠️ Limited | ⭐️⭐️ | Local prototyping, CPU servers |
| OpenVINO + OVMS | ✅ Very good (Intel) | ⚠️ | ⚠️ | ⭐️⭐️ | CPU-first production on Intel servers |
| vLLM | ⚠️ | ✅ Excellent | ✅ Excellent | ⭐️⭐️⭐️ | Small GPU production, multi-user APIs |
| TGI (NVIDIA) | ⚠️ | ✅ Excellent (NVIDIA) | ✅ | ⭐️⭐️ | GPU production with NVIDIA stack |
| Ollama / LocalAI | ✅ Good | ✅ Good | ⚠️ Depends | ⭐️⭐️⭐️ | Rapid prototyping & internal APIs |
Legend: ✅ Good, ⚠️ Possible but constrained
LLM Serving Engine Comparison (2026)
| Engine | Quantization | Throughput | GPU Memory | Multi-GPU | Best For |
|---|---|---|---|---|---|
| vLLM | AWQ, GPTQ, FP8, NVFP4 | Highest | Efficient (PagedAttention) | Yes | Production, 100+ concurrent users |
| SGLang | FP8, AWQ | Higher than vLLM (~29%) | Efficient | Yes | Multi-turn agent workflows |
| TGI | INT8, FP8 | Moderate | Moderate | Yes | HuggingFace ecosystem |
| TensorRT-LLM | INT8, FP8, INT4, NVFP4 | Highest | Efficient | Yes | Enterprise, Blackwell optimized |
| llama.cpp | GGUF (2-8 bit) | Moderate | CPU + GPU offload | Limited | CPU, Mac, edge devices |
| Ollama | GGUF (via llama.cpp) | Low-Moderate | CPU + GPU | No | Local dev, single user |
Production Decision Flow
Need production serving (100+ users)?
├── Yes → GPU available?
│ ├── Yes → vLLM (default) or SGLang (multi-turn agents)
│ └── No → OpenVINO (Intel) or llama.cpp (AMD/ARM)
└── No → Local/single user?
├── Ollama (simplest)
└── llama.cpp (maximum control)
vLLM Production Configuration
# vLLM production config (config.yaml)
model: /models/llama-3.3-70b-awq
max_model_len: 131072
gpu_memory_utilization: 0.90
tensor_parallel_size: 4 # 4 GPUs for 70B
pipeline_parallel_size: 1
enable_prefix_caching: true
max_num_seqs: 256
max_num_batched_tokens: 65536
quantization: awq
# Performance settings
block_size: 16
swap_space: 4 # GB for KV cache offloading
enable_chunked_prefill: true
# Serving
host: 0.0.0.0
port: 8000
served_model_name: llama-70b
response_role: assistant
Startup command
python3 -m vllm.entrypoints.openai.api_server \
--config config.yaml
Multi-GPU Deployment Patterns
| Pattern | Description | Best For |
|---|---|---|
| Tensor Parallelism | Split model layers across GPUs | Fitting large models |
| Pipeline Parallelism | Split layers by stage | Deep models, reduced communication |
| Data Parallelism | Replicate model, split requests | Higher throughput |
| Context Parallelism | Split sequence across GPUs | Long-context (>100K tokens) |
For most deployments, tensor parallelism with 2-4 GPUs provides the best balance of model capacity and throughput.
# Launch vLLM with tensor parallelism
docker run --gpus all -p 8000:8000 \
-v /models:/models \
vllm/vllm:latest \
vllm serve /models/llama-70b-awq \
--tensor-parallel-size 4 \
--quantization awq \
--max-model-len 32768
Serving Engine Benchmarks (Qwen2.5-32B on H200)
| Engine | Throughput (tok/s) | TTFT (p50) | TTFT (p95) | Memory Efficiency |
|---|---|---|---|---|
| vLLM | 461 | 0.8s | 2.1s | Best (PagedAttention) |
| SGLang | 595 | 0.6s | 1.5s | Good (RadixAttention) |
| TGI | 210 | 0.4s | 1.1s | Moderate |
| TensorRT-LLM | 520 | 0.5s | 1.3s | Best (optimized kernels) |
| llama.cpp | 85 | 1.5s | 3.5s | Good (CPU+GPU) |
| Ollama | 65 | 2.0s | 4.0s | Moderate |
SGLang offers the highest throughput for multi-turn agent workflows. vLLM is the best general-purpose choice. TGI excels at long-context prompts (200K+ tokens) with 13x faster prefill.
Performance benchmarks (realistic ranges)
Actual numbers depend heavily on model architecture, quantization, batch size, prompts, and runtime. Expect this order of magnitude for interactive workloads:
- CPU-only (8–16 cores)
- 1–3B model: tens to low hundreds of tokens/second (very dependent on quantization and SIMD optimizations)
- 7B model: often single-digit to low tens of tokens/second
- Small GPU (8–16GB) + 4-bit quantization
- 3B: hundreds of tokens/second
- 7B: tens to low hundreds tps
- 13B: tens of tps (if properly quantized and with efficient runtime)
Note: These are broad ranges. Run microbenchmarks for your model and workload; measuring real-world prompt/response cycles is essential.
Memory Optimization Techniques
| Technique | Memory Savings | Quality Impact | Implementation Complexity |
|---|---|---|---|
| PagedAttention (vLLM) | 80-95% KV cache | None | Built-in (vLLM) |
| Prefix caching | 50-90% on repeated prefixes | None | Built-in (vLLM) |
| KV cache quantization (NVFP4) | 50% KV cache | Minimal | B200+ GPUs |
| Continuous batching | 2-4x throughput | None | Built-in (vLLM) |
| Chunked prefill | 30% peak memory | None | Built-in (vLLM) |
| CPU offloading | 50-80% GPU memory | Latency increase | llama.cpp |
| FlashAttention-3 | 2-3x attention speed | None | H100+ GPUs |
Decision framework — how to choose
- Start with the use case: prototyping, internal tooling, or low-volume production?
- Identify latency tolerance and concurrency requirements.
- Choose a model size that fits your memory constraints after quantization.
- Pick a runtime aligned with your hardware (llama.cpp/OpenVINO for CPU, vLLM/TGI for GPU).
- Validate with a small benchmark and an A/B test between quantization settings (4-bit vs 8-bit).
- Automate CI for model conversion, test prompts, and integration tests before deploying.
Auto-Scaling Production Deployments
# Kubernetes auto-scaling for LLM serving
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: llm-server
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-server
minReplicas: 2
maxReplicas: 10
metrics:
- type: Pods
pods:
metric:
name: vllm_gpu_cache_usage
target:
type: AverageValue
averageValue: 85
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Monitoring and Observability
| Metric | Tool | Warning | Critical |
|---|---|---|---|
| GPU memory usage | nvidia-smi, Prometheus | >85% | >95% |
| P50/P95 latency | vLLM metrics, Grafana | P95 > 5s | P95 > 10s |
| Throughput (tok/s) | vLLM metrics | <100 | <50 |
| Error rate | Application logs | >1% | >5% |
| Queue depth | vLLM metrics | >50 | >200 |
| KV cache usage | vLLM metrics | >80% | >95% |
| Model loading time | Startup logs | >5 min | >15 min |
Prometheus Metrics from vLLM
# Prometheus scrape config for vLLM
scrape_configs:
- job_name: 'vllm'
static_configs:
- targets: ['localhost:8000']
metrics_path: /metrics
scrape_interval: 15s
Cost Analysis
Self-Hosted vs API Cost Comparison
| Volume | Model | Self-Hosted (Monthly) | API (Monthly) | Break-Even |
|---|---|---|---|---|
| 100K queries | Llama-3.1-8B | $1,200 (1x A100) | $2,400 (GPT-4.1 Mini) | 6 months |
| 1M queries | Llama-3.1-70B | $12,000 (4x A100) | $85,000 (GPT-4.1) | 2 months |
| 10M queries | Llama-3.3-70B | $45,000 (8x A100) | $850,000 (GPT-4.1) | 1 month |
Self-hosting becomes cost-effective at >1M queries/month for 70B-class models.
Hardware Requirements by Model Size
| Model Size | Min GPU Memory | Recommended GPU | Quantization | Throughput (tok/s) |
|---|---|---|---|---|
| 1-3B | 4GB | RTX 3060, T4 | GGUF Q4_K_M | 100-500 |
| 7-8B | 8GB | RTX 4090, A10 | AWQ INT4 | 50-200 |
| 13B | 12GB | A100 40GB | AWQ INT4 | 30-100 |
| 34B | 24GB | 2x A100 | AWQ INT4 | 20-60 |
| 70B | 48GB | 4x A100 | AWQ INT4 | 10-40 |
| 120B+ | 80GB | 8x A100 | FP8/INT4 | 5-20 |
Prompt Engineering for Self-Hosted Models
Self-hosted models often need different prompting than API-based models:
| Aspect | API Model (GPT-4, Claude) | Self-Hosted (Llama, Qwen) |
|---|---|---|
| System prompt | Strongly followed | May need reinforcement in user message |
| Few-shot examples | Effective with 1-2 | May need 3-5 examples |
| Output format | Reliable JSON mode | May need schema in prompt |
| Temperature | 0.7 typical | 0.3-0.5 for deterministic |
| Max tokens | Set generously | Set conservatively (speed) |
Troubleshooting Common Issues
| Symptom | Cause | Solution |
|---|---|---|
| CUDA OOM | Model too large for GPU | Use smaller quant (INT4), enable swap, reduce batch size |
| Slow first token | Cold start, no prefix cache | Enable prefix caching, warm up with dummy requests |
| Low throughput | Poor batching | Increase max_num_seqs, enable continuous batching |
| High latency | CPU offloading | Move more layers to GPU, reduce CPU offload |
| Model fails to load | Wrong quantization format | Verify format compatibility with serving engine |
| Memory leak | vLLM version bug | Upgrade to latest vLLM, check GitHub issues |
| Inconsistent responses | Temperature too high | Reduce temperature to 0.1-0.3 for deterministic output |
Final notes and best practices ✅
- Always evaluate quality after quantization on real tasks — not just loss numbers
- Use batch inference where latency allows to benefit from GPU throughput
- Automate model conversions and deploy reproducible images (Docker) for predictable behavior
- Monitor memory, latency, and error rates; define fallback behavior for out-of-memory situations
- Implement prefix caching for repeated prompts to reduce time-to-first-token
- Start with Ollama for prototyping, migrate to vLLM for production
- Use AWQ quantization for best quality on NVIDIA GPUs
- Test with realistic concurrency before going live
Security Considerations
| Concern | Mitigation |
|---|---|
| Model access control | Use API keys, network policies, and authentication middleware |
| Data privacy | Self-hosting keeps data on-premise — no data sent to third parties |
| Model theft | Store models on encrypted volumes, restrict file system access |
| Prompt injection | Implement input validation and output filtering |
| Rate limiting | Protect against abuse with per-user rate limits |
| Audit logging | Log all inference requests and responses for compliance |
| Container security | Run with minimal privileges, scan images for vulnerabilities |
Decision Framework: When to Self-Host vs. Use API
| Factor | Self-Host | API Provider |
|---|---|---|
| Monthly volume | >1M queries | <100K queries |
| Latency requirement | <100ms p50 | <500ms p50 acceptable |
| Data privacy | Sensitive data, compliance | Public or anonymized data |
| Model customization | Fine-tuning, custom architectures | Standard models only |
| Team expertise | DevOps/MLOps team available | Minimal ops overhead |
| Cost sensitivity | Predictable costs at high volume | Variable, pay-per-use |
| Time to market | Longer (infrastructure setup) | Immediate |
Tipping Points
| Model Size | Self-Host Cost (Monthly) | API Cost (Monthly) | Tipping Point |
|---|---|---|---|
| 7B | $1,200 | $2,400 at 100K queries | ~50K queries/month |
| 13B | $3,500 | $6,000 at 100K queries | ~60K queries/month |
| 34B | $7,000 | $15,000 at 100K queries | ~50K queries/month |
| 70B | $12,000 | $85,000 at 1M queries | ~150K queries/month |
GPU Memory Planning
Calculate memory requirements for deployment:
def estimate_gpu_memory(model_size_b: float, quantization: str,
context_length: int, batch_size: int) -> dict:
"""Estimate GPU memory for LLM serving."""
weight_bytes = {"fp16": 2, "int8": 1, "int4": 0.5, "fp8": 1}
kv_bytes = 2 * 80 * context_length * 8192 / 64 * 128 # 70B model assumption
weight_gb = model_size_b * weight_bytes.get(quantization, 2)
kv_cache_gb = kv_bytes * batch_size / (1024**3)
overhead_gb = 2 # CUDA context, activations
total = weight_gb + kv_cache_gb + overhead_gb
return {
"weights_gb": round(weight_gb, 1),
"kv_cache_gb": round(kv_cache_gb, 1),
"overhead_gb": overhead_gb,
"total_gb": round(total, 1),
"recommended_gpu": f"{int(total * 1.2)}+ GB"
}
print(estimate_gpu_memory(70, "int4", 32768, 16))
# Output: {'weights_gb': 35.0, 'kv_cache_gb': 10.5, 'overhead_gb': 2, 'total_gb': 47.5, 'recommended_gpu': '57+ GB'}
Getting Started: 5-Minute Quickstart
# Option 1: Ollama (simplest)
curl -fsSL https://ollama.com/install.sh | sh
ollama run llama3.2:3b
curl http://localhost:11434/api/chat -d '{"model":"llama3.2:3b","messages":[{"role":"user","content":"Hello"}]}'
# Option 2: vLLM Docker (production)
docker run --gpus all -p 8000:8000 \
vllm/vllm:latest \
vllm serve Qwen/Qwen2.5-7B-Instruct \
--quantization awq
# Test
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"Hello"}]}'
Model Recommendation by Use Case
| Use Case | Recommended Model | Size | Quantization | Runtime |
|---|---|---|---|---|
| Chatbot, general QA | Llama 3.2 8B | 8B | AWQ INT4 | vLLM |
| Code generation | Qwen2.5-Coder 7B | 7B | AWQ INT4 | vLLM |
| Document analysis | Llama 3.3 70B | 70B | AWQ INT4 | vLLM, 4x GPU |
| RAG embedding | BGE-M3 | 568M | FP16 | FastEmbed |
| Classification | Llama 3.2 3B | 3B | GGUF Q4 | llama.cpp |
| Edge/on-device | Phi-3.5 mini | 3.8B | GGUF Q4 | Ollama |
Production Operational Tasks
| Frequency | Task | Impact |
|---|---|---|
| Daily | Check GPU memory usage, latency p95 | Prevent OOM, detect degradation |
| Weekly | Review error rates, update models | Maintain reliability |
| Monthly | Rotate API keys, audit access logs | Security compliance |
| Quarterly | Upgrade serving engine, re-evaluate model choice | Stay current |
| On-deploy | Run load test, verify quantization quality, check compatibility | Prevent regressions |
Conclusion
Self-hosting open-source LLMs can dramatically reduce the per-token costs of R&D and internal tooling while giving you full data control. For most startups:
- Start with Ollama or llama.cpp on CPU for rapid prototyping
- Move to vLLM or SGLang when you need interactive latency and multi-user throughput
- Use AWQ INT4 quantization for best quality on NVIDIA GPUs
- Implement prefix caching and continuous batching for production throughput
- Monitor GPU memory, latency, and error rates continuously
With careful model selection, quantization, and the right runtime, you can run capable LLMs with predictable costs and acceptable performance. The self-hosted ecosystem in 2026 is mature enough for production workloads — the key is matching the serving engine to your hardware and concurrency requirements.
Deployment Architecture Patterns
Single GPU (7B-13B models)
Client → Load Balancer → vLLM (1x GPU) → Model (7B/13B AWQ)
└── Redis Cache (prefix caching)
Multi-GPU (34B-70B models)
Client → Load Balancer → vLLM (4x GPU, TP=4) → Model (70B AWQ)
├── Redis Cache
└── Prometheus + Grafana (monitoring)
High-Availability (production)
Client → Load Balancer → vLLM Replica 1 (2x GPU)
vLLM Replica 2 (2x GPU)
vLLM Replica 3 (2x GPU)
└── Shared Redis Cache + PostgreSQL (request log)
Batch Inference for Throughput
For non-real-time workloads, batch inference maximizes GPU utilization:
import asyncio
from vllm import AsyncLLMEngine, SamplingParams
async def batch_inference(engine, prompts: list[str]) -> list[str]:
"""Run batch inference for maximum throughput."""
sampling_params = SamplingParams(
temperature=0.1,
max_tokens=512,
top_p=0.9
)
requests = [engine.generate(p, sampling_params) for p in prompts]
results = await asyncio.gather(*requests)
return [r.outputs[0].text for r in results]
# Usage: process 100 prompts in a single batch
engine = AsyncLLMEngine.from_vllm_config(vllm_config)
responses = asyncio.run(batch_inference(engine, prompts))
Batch inference can achieve 3-5x higher throughput than sequential processing for batch workloads like document classification, summarization, and data extraction.
Upgrade Path: Ollama → vLLM
| Stage | Tool | Users | GPUs | Setup Time |
|---|---|---|---|---|
| 1: Prototype | Ollama | 1-5 | 0-1 | Minutes |
| 2: Internal tool | Ollama + Docker | 5-50 | 1 | Hours |
| 3: Production | vLLM | 50-1000 | 1-8 | Days |
| 4: Scale | vLLM + K8s | 1000+ | 8-64 | Weeks |
Migrate from Ollama to vLLM when you exceed 50 concurrent users or need >100 tok/s throughput.
Serving Engine Configuration Reference
# vLLM: Production configuration
# Hardware: 4x A100 80GB, Llama-3.3-70B AWQ
engine:
model: meta-llama/Llama-3.3-70B-Instruct-AWQ-INT4
quantization: awq
tensor_parallel_size: 4
max_model_len: 32768
gpu_memory_utilization: 0.90
enable_prefix_caching: true
max_num_seqs: 256
block_size: 16
swap_space: 4
# SGLang: Multi-turn agent configuration
engine:
model: Qwen/Qwen2.5-32B-Instruct-AWQ
quantization: awq
tensor_parallel_size: 2
max_model_len: 65536
enable_mixed: true
radix_cache_size: 16384
# llama.cpp: CPU inference configuration
engine:
model: /models/llama-3.2-3b-q4_k_m.gguf
n_gpu_layers: -1 # All on GPU if available
n_ctx: 8192
n_batch: 512
n_threads: 8
no_kv_offload: false
Related Articles
- Local First Ai Ollama Open Webui
- Deepseek Complete Guide
- Llm Api Comparison Openai Anthropic Open Source
Open-Source Model Recommendations
| Category | Best Model (2026) | Size | License | Why |
|---|---|---|---|---|
| General purpose | Llama 3.3 70B | 70B | Llama 3 | Best overall quality |
| General purpose (small) | Qwen2.5 32B | 32B | Apache 2.0 | Strong quality, fits 2x GPU |
| Code generation | Qwen2.5-Coder 32B | 32B | Apache 2.0 | Best open code model |
| Reasoning | DeepSeek-R1 distill 7B | 7B | MIT | Strong reasoning in small package |
| Edge/mobile | Phi-3.5 Mini | 3.8B | MIT | Tiny, runs on phone NPU |
| Embeddings | BGE-M3 | 568M | MIT | Best general embedding quality |
| Vision | LLaVA-NeXT 34B | 34B | Apache 2.0 | Strong multimodal |
Migration Checklist: Ollama → vLLM
- Convert model to AWQ format (or download pre-quantized)
- Set up vLLM server with tensor parallelism if multi-GPU
- Configure prefix caching and continuous batching
- Update client code to use OpenAI-compatible API
- Set up Prometheus metrics and Grafana dashboards
- Implement load testing with realistic concurrency
- Configure auto-scaling based on GPU memory utilization
- Set up Redis cache layer for repeated prompt prefixes
- Deploy with Docker/Kubernetes for reproducibility
- Monitor for 48 hours before cutting over production traffic
Frequently Asked Questions
Q: Can I run Llama 3 70B on a single GPU? A: Yes, with AWQ INT4 quantization a 70B model requires ~35GB — it fits on a single H100 (80GB) or A100 (80GB). For production throughput, 4x GPUs are recommended.
Q: Is self-hosting cheaper than API? A: At >1M queries/month, yes. Below that, API may be more cost-effective when accounting for engineering time and infrastructure management.
Q: What’s the best quantization for self-hosting? A: AWQ INT4 for NVIDIA GPUs (best quality-to-speed). GGUF Q6_K for CPU inference (best quality on CPU). GGUF Q4_K_M for edge deployment (memory-constrained).
Q: How do I handle concurrent users? A: Use vLLM with continuous batching. A single A100 serving a 7B AWQ model can handle 50-100 concurrent users with good latency.
Q: Can I serve multiple models from one server?
A: Yes, vLLM supports model multiplexing. Set --serve-model-name and use separate endpoints per model. TensorRT-LLM also supports multi-model serving.
Resources
- vLLM Documentation
- Ollama Documentation
- llama.cpp GitHub
- SGLang Documentation
- Hugging Face Text Generation Inference
- TensorRT-LLM Documentation
- OpenVINO AI Tools
- 13 Open-Source Tools for Foundation Model Deployment
- vLLM vs TGI vs SGLang Benchmarks (2026)
Comments