1) Introducción detallada
Desplegar un LLM “en local” en un VPS barato suena a contradicción: los modelos grandes nacieron en GPUs caras, con memoria abundante y redes rápidas. Sin embargo, en 2024–2026 el ecosistema cambió lo suficiente como para que un despliegue viable sea posible en entornos modestos (1–4 vCPU, 2–16 GB RAM, sin GPU). La clave no es “correr Llama 3 sin límites”, sino diseñar el sistema alrededor de restricciones duras: memoria, ancho de banda, throughput por token, latencia de primer token, concurrencia y seguridad. Con cuantización (GGUF), inferencia en CPU optimizada (llama.cpp), batching inteligente y límites explícitos de contexto y tokens, puedes exponer un endpoint OpenAI-compatible estable para tareas típicas: RAG ligero, clasificación, extracción de entidades, redacción breve, chat interno con políticas estrictas, y automatizaciones que no justifican una API externa.
Este artículo asume una realidad: un VPS de bajo coste casi siempre es CPU-only. Eso no significa que sea inútil. Con modelos pequeños/medianos (Llama 3.2 1B/3B o Llama 3 8B cuantizado) puedes lograr resultados correctos, con latencias aceptables si reduces el contexto, aplicas KV-cache, ajustas parámetros y controlas la concurrencia. Si tu objetivo es 50 chats simultáneos con 2k tokens de salida, un VPS barato no es el lugar. Pero si buscas privacidad, coste predecible, cumplimiento (datos sensibles), independencia del proveedor y control total de la cadena (desde TLS a logs), entonces el despliegue local en VPS encaja perfectamente.
La trampa más común es tratar el LLM como un microservicio “como cualquier otro”, ignorando que su consumo de recursos depende de: tamaño del modelo (pesos), tamaño de contexto (KV cache), longitud de salida, temperatura/top-p y concurrencia. Por eso la guía es “extrema”: incluye dimensionamiento, benchmarks, estrategias de cuantización, elección de runtime, arquitectura con reverse proxy, rate limiting, colas, observabilidad, despliegue con Docker/systemd, endurecimiento (AppArmor/ufw), y tuning de inferencia en CPU (AVX2/AVX512, threads, NUMA). El objetivo: un despliegue reproducible, mantenible y con degradación controlada bajo carga.
2) Contexto histórico o teórico
El despliegue local de LLMs en hardware commodity es el resultado de tres líneas de evolución. (1) Arquitectura Transformer y la “escala” como motor de calidad: más parámetros, más datos, mejores resultados, pero más coste. (2) Optimización de inferencia: kernels y runtimes especializados, y el reconocimiento de que la inferencia (no el entrenamiento) es el cuello de botella típico en producto. (3) Cuantización y formatos portables: pasar de FP16/BF16 a INT8/INT4 con pérdidas controladas, habilitando CPU y GPUs modestas.
En el mundo open-source, llama.cpp popularizó la inferencia eficiente en CPU con cuantización agresiva y un formato (GGUF) pensado para cargar rápido y mapear memoria. Paralelamente, runtimes como vLLM se enfocaron en throughput con GPUs (PagedAttention) y servidores OpenAI-compatible, mientras que soluciones integradas como Ollama simplificaron la operación a costa de menos control fino. En VPS baratos, la batalla se decide por eficiencia en CPU y RAM: llama.cpp domina por madurez y rendimiento sin GPU, especialmente con builds optimizados para AVX2/AVX512.
Teóricamente, el consumo de memoria se divide en: (a) pesos del modelo (dependen de parámetros y cuantización), (b) KV cache (escala con contexto, capas, heads, batch/concurrencia), (c) buffers temporales y overhead del runtime. En CPU-only, el throughput en tokens/s depende de: frecuencia, IPC, vectorización (AVX2/AVX512), número de threads efectivos, afinidad, y de si el modelo cabe en RAM sin swapping. En un VPS con almacenamiento lento o memoria insuficiente, el rendimiento cae de forma catastrófica por page faults.
El resultado práctico: para “bajo coste” conviene un modelo pequeño bien afinado antes que un modelo grande mal servido. Llama 3 (8B) cuantizado puede funcionar en 8–16 GB RAM, pero la experiencia real depende de contexto y concurrencia. Modelos 3B/1B ofrecen latencias mucho mejores con menos RAM, ideal para endpoints internos o tareas específicas. La estrategia moderna es multi-model: un “fast model” para la mayoría de peticiones y un “bigger model” (si tienes recursos) para casos premium, con enrutado por complejidad.
3) Sección técnica principal 1: selección de VPS, dimensionamiento y pruebas de capacidad
En VPS de bajo coste, el proveedor importa menos que las características concretas: tipo de CPU (y soporte AVX2/AVX512), ratio vCPU/RAM, límites de CPU sostenidos (throttling), y velocidad de disco. Para LLMs CPU-only, prioriza: RAM (para evitar swap), AVX2 mínimo (AVX512 mejora), y almacenamiento NVMe (carga de modelo y mmap). Un error clásico: elegir 2 vCPU/2GB porque “es barato” y luego intentar cargar un 8B: no cabe, o entra con swap y se vuelve inutilizable.
Regla práctica para pesos: un 8B en FP16 ronda ~16 GB solo en pesos; en cuantización 4-bit (Q4) puede bajar a ~4–6 GB según esquema y overhead. A esto súmale KV cache: si habilitas 8k de contexto y permites respuestas largas, el KV se come varios GB adicionales. Por eso para un 8B Q4 con contexto moderado (2k–4k) el “mínimo” realista suele ser 8–12 GB RAM, y 16 GB si quieres margen, concurrencia o contextos altos.
Dimensionamiento orientado a producto: define SLOs (p.ej., P95 TTFT < 2s, P95 tokens/s > 10 para salidas cortas) y limita el problema: (1) max_input_tokens, (2) max_output_tokens, (3) max_concurrency, (4) timeouts. Si no defines límites, el primer usuario con un prompt gigante te tumba el nodo.
Benchmark mínimo antes de desplegar: mide tokens/s en tu VPS con un prompt fijo y salida fija. Mide TTFT (time-to-first-token) y throughput. Con llama.cpp puedes ejecutar un test reproducible. Ejemplo (asumiendo modelo GGUF ya descargado):
# Ejemplo de benchmark simple con llama.cpp
./llama-bench -m /models/llama3-8b-q4_k_m.gguf -n 256 -p 128 -t 4
# TTFT aproximado con servidor (primer token)
curl -s http://127.0.0.1:8080/completion -d '{
"prompt": "Resume en 2 frases la teoría de colas.",
"n_predict": 64,
"temperature": 0.2
}' | jq -r '.content'
Qué VPS elegir (CPU-only): para Llama 3 8B Q4: 4 vCPU/8–16 GB RAM. Para Llama 3.2 3B Q4: 2 vCPU/4–8 GB RAM. Si tu presupuesto es extremadamente bajo, la opción realista es 1B/3B o tareas de baja concurrencia con colas.
Plan B si necesitas más: usa un VPS con GPU barata (algunos proveedores ofrecen T4/L4, pero ya no es “bajo coste” estricto) o monta un “hybrid”: VPS barato para API, auth, RAG, y un nodo de inferencia separado (spot/horas valle) para cargas grandes. También puedes mantener un pool local y fallback a API externa cuando se superen límites (circuit breaker).
4) Sección técnica principal 2: elección de runtime (llama.cpp vs Ollama vs vLLM) y formato del modelo
Para VPS baratos, la decisión crítica es el runtime. llama.cpp es el estándar de facto para CPU por rendimiento, control y capacidad de compilar optimizado. Ollama es excelente para “time-to-first-demo”, pero añade una capa de gestión que, en VPS pequeños, puede penalizar memoria/operación si no ajustas bien. vLLM brilla en GPU con batching y PagedAttention; en CPU suele no ser la mejor opción para bajo coste (además de complejidad y dependencias). Por tanto, mi recomendación directa para CPU-only: llama.cpp server (o un wrapper compatible OpenAI basado en llama.cpp).
Formato: GGUF es el formato recomendado para llama.cpp. Evita cargar pesos en HuggingFace Transformers en CPU para producción en VPS barato: el overhead, la memoria y la velocidad suelen ser peores. La ruta típica es: elegir el modelo base (p.ej., Llama 3 8B Instruct o Llama 3.2 3B Instruct), descargar una variante cuantizada en GGUF (Q4_K_M suele ser un buen equilibrio), y servirlo con llama.cpp.
Cuantización: Q4 (4-bit) es el punto dulce para CPU: reduce RAM y mejora cache locality. Q5/Q6 suben calidad algo, pero exigen más RAM y bajan rendimiento. En VPS barato, Q4_K_M o Q4_0 suele ser lo razonable. Si el output es “alucinación” o pierde precisión, primero mejora prompt/plantillas/temperatura y RAG, antes de subir a Q6 y reventar la RAM.
Contexto: no persigas 32k de contexto en CPU barato. El coste del KV cache escala linealmente con tokens de contexto y con concurrencia. Diseña el producto para 2k–4k tokens de entrada + salida, y para RAG usa chunking, top-k limitado, compresión de contexto (resúmenes) y re-ranking si puedes.
Servidor: llama.cpp incluye un servidor HTTP que puede exponer endpoints tipo /completion y (según versión) compatibilidad parcial con OpenAI. Si necesitas una API 100% OpenAI-compatible (chat/completions, embeddings, tool calls), puedes usar un gateway propio (FastAPI/Express) que traduzca formatos y aplique políticas (rate limits, auth, logging, redaction). Esto también evita acoplarte a la estructura de endpoints del runtime.
5) Sección técnica principal 3: instalación, compilación optimizada y despliegue con Docker/systemd
En VPS de bajo coste, compilar bien importa. Si tu CPU soporta AVX2, compila llama.cpp con flags adecuados. En Ubuntu/Debian: instala build-essential, cmake y dependencias. Si puedes, usa mmap para cargar el modelo sin duplicar memoria, y fija threads para evitar oversubscription. Evita correr como root; crea un usuario “llm”.
Instalación nativa (recomendado para máximo rendimiento):
# 1) Paquetes base
sudo apt-get update
sudo apt-get install -y build-essential cmake git pkg-config libssl-dev
# 2) Usuario dedicado
sudo useradd -m -s /bin/bash llm
sudo mkdir -p /opt/llama.cpp /var/lib/llm/models
sudo chown -R llm:llm /opt/llama.cpp /var/lib/llm
# 3) Build
sudo -u llm bash -lc '
cd /opt && git clone https://github.com/ggerganov/llama.cpp.git
cd /opt/llama.cpp
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j $(nproc)
'
# Binarios típicos
ls -la /opt/llama.cpp/build/bin
Descarga de modelos (GGUF): la forma más robusta es bajar el archivo GGUF desde un repositorio confiable (HuggingFace) y verificar checksum. Guarda en un directorio persistente (p.ej., /var/lib/llm/models). Recuerda que el acceso y uso del modelo debe cumplir su licencia.
# Ejemplo: descarga (URL ilustrativa; usa el repo/archivo concreto que elijas)
sudo -u llm bash -lc '
cd /var/lib/llm/models
wget -O llama3-8b-instruct-q4_k_m.gguf "https://huggingface.co/.../resolve/main/...gguf"
'
systemd para servicio persistente: un LLM server debe arrancar solo, reiniciarse, loguear y tener límites claros. Ejemplo de unidad (ajusta rutas/puerto/threads):
# /etc/systemd/system/llama-server.service
[Unit]
Description=llama.cpp HTTP server
After=network.target
[Service]
User=llm
Group=llm
WorkingDirectory=/opt/llama.cpp
ExecStart=/opt/llama.cpp/build/bin/llama-server \
-m /var/lib/llm/models/llama3-8b-instruct-q4_k_m.gguf \
--host 127.0.0.1 --port 8080 \
-t 4 \
-c 4096 \
--n-predict 256 \
--temp 0.2
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
MemoryMax=12G
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now llama-server
sudo journalctl -u llama-server -f
Docker (si priorizas reproducibilidad): Docker añade overhead pero facilita CI/CD. En VPS muy ajustados, prefiero nativo para exprimir CPU. Si usas Docker, limita memoria/cpus, monta modelos en volumen y evita imágenes gigantes. La build multi-stage compila llama.cpp y deja binarios en una imagen mínima.
6) Sección técnica principal 4: arquitectura de API, reverse proxy, autenticación y control de abuso
No expongas el puerto del runtime directamente a Internet. Coloca un reverse proxy (Nginx/Caddy) con TLS, auth y rate limiting. A nivel de aplicación, crea un gateway que traduzca la API, valide payloads, recorte inputs y aplique políticas. En VPS barato, el abuso se traduce en CPU al 100% y latencias enormes: necesitas controles duros.
Patrón recomendado:
Cliente → Nginx (TLS, WAF básico, rate limit, auth) → Gateway API (FastAPI/Node) → llama.cpp en localhost.
Nginx con rate limiting:
# /etc/nginx/sites-available/llm.conf
limit_req_zone $binary_remote_addr zone=llm_rl:10m rate=5r/s;
limit_conn_zone $binary_remote_addr zone=llm_conn:10m;
server {
listen 443 ssl http2;
server_name llm.tudominio.com;
ssl_certificate /etc/letsencrypt/live/llm.tudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/llm.tudominio.com/privkey.pem;
# Límite de conexiones y requests
limit_conn llm_conn 10;
limit_req zone=llm_rl burst=20 nodelay;
# Tamaño máximo de body para evitar prompts gigantes
client_max_body_size 256k;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300s;
}
}
Gateway en FastAPI (ejemplo): valida tokens, limita input/output, aplica timeouts, y llama al runtime. Aquí también puedes implementar “circuit breaker” si la cola crece o la CPU está saturada.
# app.py
import os, time
import httpx
from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel, Field
API_KEY = os.getenv("LLM_API_KEY", "")
LLAMA_URL = os.getenv("LLAMA_URL", "http://127.0.0.1:8080/completion")
MAX_PROMPT_CHARS = 8000
MAX_PREDICT = 256
app = FastAPI()
class CompletionIn(BaseModel):
prompt: str = Field(..., min_length=1)
temperature: float = 0.2
top_p: float = 0.9
n_predict: int = 128
@app.post("/v1/completions")
async def completions(payload: CompletionIn, x_api_key: str = Header(default="")):
if API_KEY and x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Unauthorized")
if len(payload.prompt) > MAX_PROMPT_CHARS:
raise HTTPException(status_code=413, detail="Prompt too large")
n_predict = min(payload.n_predict, MAX_PREDICT)
req = {
"prompt": payload.prompt,
"temperature": payload.temperature,
"top_p": payload.top_p,
"n_predict": n_predict
}
t0 = time.time()
async with httpx.AsyncClient(timeout=180) as client:
r = await client.post(LLAMA_URL, json=req)
if r.status_code != 200:
raise HTTPException(status_code=502, detail=f"Upstream error: {r.text}")
data = r.json()
return {
"id": "cmpl-local",
"object": "text_completion",
"created": int(time.time()),
"model": "llama3-local",
"choices": [{"text": data.get("content", ""), "index": 0, "finish_reason": "stop"}],
"usage": {
"prompt_tokens": data.get("tokens_evaluated", 0),
"completion_tokens": data.get("tokens_predicted", 0),
"total_tokens": data.get("tokens_evaluated", 0) + data.get("tokens_predicted", 0)
},
"latency_ms": int((time.time() - t0) * 1000)
}
Ejecución del gateway:
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn httpx
export LLM_API_KEY='cambia-esto'
uvicorn app:app --host 127.0.0.1 --port 3000
Este patrón te permite evolucionar sin tocar el runtime: añadir logs estructurados, trazas, caché de respuestas para prompts idénticos, o incluso enrutamiento a otro modelo si el local está saturado.
7) Sección técnica principal 5: optimización extrema (CPU, memoria, concurrencia), observabilidad y degradación controlada
En VPS barato, optimizar es sobrevivir. La prioridad es evitar swapping y controlar concurrencia. Si el modelo entra justo en RAM, cualquier pico (logs, buffers, otro proceso) puede forzar swap y destruir el throughput. Deja margen: si tienes 8 GB, no intentes usar 7.9 GB. Ajusta MemoryMax en systemd y configura el VPS para evitar overcommit agresivo.
Threads: más threads no siempre es mejor. Para CPU compartida, 2–4 threads suele ser el punto estable. Si saturas CPU con 8 threads en un 4 vCPU, aumentas la latencia por contención. Ajusta con pruebas: mide tokens/s y P95. En general, usa -t (n_cores_físicos o vCPU) pero valida con benchmark. Controla afinidad si el proveedor asigna cores inestables.
Contexto y KV cache: el contexto (parámetro -c) y la concurrencia multiplican memoria. Si quieres 4096 tokens de contexto y 4 peticiones concurrentes, el KV cache se multiplica. Si tu producto lo permite, baja a 2048 y compensa con RAG bien hecho (mejor recuperación, menos texto). Si necesitas conversaciones largas, implementa resumen incremental (cada N turnos, resume y reemplaza historial) para mantener el contexto efectivo bajo.
Batching y colas: en CPU, la concurrencia alta degrada. Una estrategia efectiva es serializar o limitar a 1–2 inferencias simultáneas y poner el resto en cola. Sí: aumenta la espera bajo carga, pero evita “meltdown” donde todos tardan minutos. Implementa un “queue length cap”: si hay > X peticiones esperando, responde 429 con retry-after. Esto es crucial para mantener SLOs.
Streaming: habilita streaming de tokens si tu runtime/gateway lo permite. El usuario percibe mejor rendimiento si ve tokens desde el inicio (reduce impacto del TTFT). Incluso si el throughput total es modesto, el streaming hace el sistema usable.
Observabilidad: sin métricas, vas a adivinar. Mínimo: latencia total, TTFT si puedes, tokens/s, tokens de entrada/salida, errores, saturación CPU, RSS, page faults, cola. Exporta métricas Prometheus desde el gateway y recopila con node_exporter. Ejemplo de métricas simples en FastAPI (pseudo): contador de requests, histograma de latencias, gauge de cola.
Logs y privacidad: no loguees prompts completos en producción si contienen datos sensibles. Implementa redacción: loguea hashes, longitudes, ids, pero no contenido. Si necesitas auditoría, cifra y restringe acceso. En VPS barato, además, el disco es limitado: rota logs (logrotate) y limita retención.
Degradación controlada: define modos: normal, high-load y emergency. En high-load reduces max_output_tokens, deshabilitas temperatura alta, reduces top_p, o fuerzas respuestas más cortas. En emergency rechazas prompts grandes o devuelves fallback (p.ej. “no disponible”). Esto evita que el nodo quede “muerto” durante minutos.
8) Ejemplos de código detallados
Aquí tienes piezas concretas que uso en despliegues reales para que el VPS no se hunda: (1) script de healthcheck, (2) rate limit por API key en gateway, (3) cliente Node.js con streaming, (4) endpoint de cola simple.
8.1 Healthcheck del runtime (bash + systemd)
#!/usr/bin/env bash
# /usr/local/bin/llm-healthcheck.sh
set -euo pipefail
URL="http://127.0.0.1:8080/health"
if curl -fsS --max-time 2 "$URL" > /dev/null; then
exit 0
fi
exit 1
# Fragmento para systemd (opcional)
# ExecStartPre=/usr/local/bin/llm-healthcheck.sh
8.2 Rate limiting por API key (FastAPI, en memoria)
Para un VPS barato, un rate limit in-memory es suficiente si no hay múltiples instancias. Si escalas horizontalmente, muévelo a Redis.
# rate_limit.py
import time
from collections import defaultdict, deque
WINDOW_S = 10
MAX_REQ = 20
buckets = defaultdict(lambda: deque())
def allow(key: str) -> bool:
now = time.time()
q = buckets[key]
while q and (now - q[0]) > WINDOW_S:
q.popleft()
if len(q) >= MAX_REQ:
return False
q.append(now)
return True
# En tu handler
from fastapi import HTTPException
from rate_limit import allow
if not allow(x_api_key or "anon"):
raise HTTPException(status_code=429, detail="Rate limit")
8.3 Cliente Node.js con streaming (SSE-like) hacia tu gateway
Si implementas streaming real dependerá del runtime. Este ejemplo asume que tu gateway expone un endpoint que va enviando chunks (puedes hacerlo con StreamingResponse en FastAPI).
// client.js
import fetch from "node-fetch";
const res = await fetch("https://llm.tudominio.com/v1/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.LLM_API_KEY
},
body: JSON.stringify({
prompt: "Escribe una función en JS para debounce y explica su complejidad.",
temperature: 0.2,
n_predict: 200
})
});
if (!res.ok) {
console.error(await res.text());
process.exit(1);
}
const data = await res.json();
console.log(data.choices[0].text);
8.4 Cola simple para limitar concurrencia (patrón semaphore)
En CPU-only, limitar inferencias concurrentes es lo que más estabilidad aporta.
import asyncio
SEM = asyncio.Semaphore(1) # solo 1 inferencia a la vez
@app.post("/v1/completions")
async def completions(payload: CompletionIn, x_api_key: str = Header(default="")):
async with SEM:
# llama al upstream (llama.cpp) aquí
...
Puedes sofisticarlo con una cola con timeout: si no adquiere semáforo en 2–3s, devuelve 429 para evitar acumulación.
9) Comparativa de pros y contras
Desplegar Llama 3 local en VPS barato (CPU-only)
- Pros
- Coste predecible: pagas el VPS, no tokens. Ideal para cargas constantes y presupuestos cerrados.
- Privacidad y control: datos no salen a terceros; puedes cumplir políticas internas y reducir riesgo legal.
- Independencia del proveedor: sin lock-in de API; controlas versiones, parámetros, logging y retención.
- Latencia estable en red: el tiempo de red es mínimo; útil para backend interno o edge cercano.
- Personalización operativa: rate limiting, colas, caching, y hardening a tu medida.
- Contras
- Rendimiento limitado: tokens/s bajos comparado con GPU; throughput pobre con concurrencia.
- RAM como cuello de botella: contextos largos o modelos mayores disparan KV cache y riesgo de swap.
- Mantenimiento: actualizaciones del runtime, parches de seguridad, rotación de logs, backups.
- Calidad vs tamaño: para tareas complejas, modelos pequeños pueden quedarse cortos.
- Operación bajo carga: sin límites y degradación, el sistema entra en meltdown fácilmente.
Comparativa rápida de runtimes en VPS barato:
- llama.cpp: mejor rendimiento CPU, máximo control, requiere más trabajo de instalación/tuning.
- Ollama: fácil de operar, buen DX, menos control fino; puede no exprimir al máximo un VPS pequeño.
- vLLM: excelente en GPU para throughput; en CPU/bajo coste normalmente no compensa.
10) Conclusión
Desplegar Llama 3 localmente en un VPS de bajo coste es viable si defines el problema correctamente: modelo cuantizado (GGUF), contexto limitado, concurrencia controlada y un gateway que aplique políticas duras. El éxito no depende de “hacer que funcione”, sino de hacer que sea predecible bajo carga: sin swap, con límites de entrada/salida, rate limiting, colas y observabilidad mínima. Para CPU-only, llama.cpp es la elección pragmática por rendimiento y control, y la cuantización Q4 suele ser el punto de equilibrio.
Si necesitas más calidad o concurrencia, no lo fuerces: o subes recursos (RAM/GPU) o rediseñas el flujo (RAG más agresivo, resumen incremental, enrutado multi-model, fallback externo). La arquitectura correcta en VPS barato no es “una API de chat sin límites”, es un servicio de inferencia con SLOs claros, degradación controlada y seguridad desde el perímetro. Ese enfoque convierte un VPS económico en una pieza útil y estable dentro de un sistema real.