Para un solo desarrollador en un portátil, use Ollama: se instala en minutos, descarga modelos por nombre y los carga bajo demanda. Para un equipo pequeño, un Mac, un servidor sin GPU o una tarjeta gráfica de consumo, use llama-server de llama.cpp, que sirve modelos GGUF con control explícito sobre slots paralelos, claves de API y métricas de Prometheus. Para tráfico de producción en GPU de centro de datos, use vLLM, cuyo continuous batching y PagedAttention mantienen un rendimiento alto a medida que crece la concurrencia. Los tres exponen una API compatible con OpenAI, así que puede empezar con uno y pasar a otro cambiando una base URL y un nombre de modelo.
Para qué está optimizado cada servidor
Ollama optimiza el tiempo hasta la primera respuesta. Funciona como aplicación de escritorio o servicio en segundo plano, descarga modelos por nombre, carga un modelo con la primera petición y lo descarga tras un periodo de inactividad, cinco minutos por defecto según las preguntas frecuentes de Ollama. Unas pocas variables de entorno OLLAMA_* sustituyen al ajuste fino del planificador, y esa sencillez es precisamente lo que ofrece.
llama.cpp optimiza la portabilidad. Es un motor de inferencia en C/C++ construido sobre la biblioteca ggml, con backends Metal para Apple Silicon, CUDA, HIP para AMD, Vulkan, SYCL y rutas de CPU optimizadas para AVX, AVX2, AVX-512, AMX y ARM NEON. Cuando un modelo no cabe en la VRAM, puede dejar parte de las capas en la GPU y el resto en la memoria del sistema. Su servidor HTTP, llama-server, se reduce a un binario, un archivo GGUF y unas cuantas opciones. El README del proyecto muestra ahora un comando unificado llama serve en su inicio rápido, mientras que la documentación de llama-server y las imágenes Docker siguen distribuyendo el ejecutable llama-server que se usa en este artículo.
vLLM optimiza el rendimiento en aceleradores. Su README enumera PagedAttention para gestionar la caché KV, continuous batching, chunked prefill, prefix caching, servicio de varios LoRA, paralelismo tensorial y de pipeline y una larga lista de formatos de cuantización. Admite GPU de NVIDIA, AMD e Intel y CPU x86, ARM y PowerPC, y otros aceleradores mediante plugins de hardware.
En resumen: Ollama y llama.cpp encajan con hardware modesto y unos pocos usuarios simultáneos, mientras que vLLM compensa su configuración más exigente en cuanto llegan muchas peticiones a la vez. La parte de hardware de la decisión, incluida la memoria que necesitan los pesos y la caché KV, se trata en la guía de hardware para LLM locales.
Un cliente de OpenAI, tres base URL
Los tres servidores implementan /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/models y /v1/responses. La página de compatibilidad con OpenAI de Ollama aclara que su soporte de Responses es sin estado. llama-server ofrece además un /v1/messages compatible con Anthropic, y vLLM enumera Anthropic Messages, transcripción de audio y API de pooling junto a las rutas de OpenAI.
Mantenga la elección del servidor en la configuración para que el código del cliente no cambie nunca:
import os
from openai import OpenAI
# Ollama: http://127.0.0.1:11434/v1
# llama-server: http://127.0.0.1:8080/v1
# vLLM: http://127.0.0.1:8000/v1
client = OpenAI(
base_url=os.environ["LLM_BASE_URL"],
api_key=os.environ.get("LLM_API_KEY", "not-used"),
)
reply = client.chat.completions.create(
model=os.environ["LLM_MODEL"],
messages=[{"role": "user", "content": "Explain HTTP 429 in two sentences."}],
temperature=0.2,
)
print(reply.choices[0].message.content)
El SDK de OpenAI exige una cadena de clave, así que pase un valor de relleno cuando el servidor no tenga ninguna. Ollama ignora la clave en local, mientras que llama-server y vLLM solo la comprueban si se arrancaron con una.
Los servidores difieren en el campo model. Ollama espera una etiqueta de su biblioteca, como qwen3:8b. vLLM espera el nombre del repositorio de Hugging Face o el valor de --served-model-name. Un llama-server con un solo modelo sirve lo que haya cargado, --alias fija el nombre que devuelve /v1/models y, en modo router, el nombre selecciona el modelo.
La compatibilidad termina en los bordes de la especificación de OpenAI. vLLM acepta parámetros adicionales como top_k mediante extra_body y, por defecto, aplica el generation_config.json del modelo, lo que puede cambiar los valores de muestreo por defecto si no pasa --generation-config vllm. Las plantillas de chat y los tokenizadores también pueden diferir entre una conversión GGUF y el checkpoint original, así que antes de cambiar de servidor ejecute el mismo conjunto de evaluación en ambos, como se describe en la guía de evaluación de agentes de IA.
Formatos de modelo y de dónde salen los pesos
Los tres servidores no leen los mismos archivos, y eso suele decidir la cuestión antes que el rendimiento.
Ollama descarga modelos de su biblioteca por nombre, ejecuta repositorios GGUF de Hugging Face con ollama run hf.co/{user}/{repo}:{quant} e importa archivos GGUF locales o directorios Safetensors mediante un Modelfile cuya línea FROM apunta a los pesos. No cuantiza los archivos GGUF al importarlos, así que cuantícelos antes con las herramientas de llama.cpp.
llama.cpp lee GGUF. Se convierte un checkpoint de Hugging Face con convert_hf_to_gguf.py y se cuantiza, o se descarga un GGUF listo con -hf user/repo:quant. El README enumera cuantización entera de 1,5 a 8 bits. Los modelos de visión necesitan además un archivo de proyector, que -hf descarga automáticamente.
vLLM lee repositorios de modelos de Hugging Face con pesos Safetensors, además de checkpoints cuantizados como GPTQ, AWQ, FP8, INT8, INT4 y compressed-tensors. Su documentación califica el soporte de GGUF de muy experimental, y ese soporte se ha trasladado a un plugin externo, vllm-gguf-plugin.
# Ollama: a library model, or a GGUF repository on Hugging Face
ollama pull qwen3:8b
ollama run hf.co/bartowski/Llama-3.2-3B-Instruct-GGUF:Q8_0
# llama.cpp: download a GGUF and serve it
llama-server -hf ggml-org/Qwen3.5-0.8B-GGUF --port 8080
# vLLM: serve a Hugging Face repository
vllm serve Qwen/Qwen3-0.6B --host 127.0.0.1 --port 8000
Concurrencia, batching y memoria de la caché KV
La concurrencia cuesta memoria. Cada secuencia activa guarda las claves y valores de atención de cada token de su contexto, así que la memoria crece con el número de secuencias simultáneas multiplicado por su longitud, además de los pesos. Para un transformer estándar, esta estimación resulta útil:
KV bytes per token = 2 × layers × kv_heads × head_dim × bytes_per_value
Qwen3-8B, 16-bit cache: 2 × 36 × 8 × 128 × 2 = 147,456 bytes = 144 KiB
One 32,768-token sequence: 144 KiB × 32,768 = 4.5 GiB
El número de capas, el número de cabezas KV y la dimensión de cabeza salen del config.json del modelo. Cuatro secuencias así necesitan 18 GiB de caché antes de contar los pesos. Los modelos con atención de ventana deslizante o híbrida necesitan menos, por lo que para ellos la fórmula da una cota superior.
Ollama
OLLAMA_NUM_PARALLEL fija cuántas peticiones procesa a la vez cada modelo cargado, y las preguntas frecuentes indican un valor por defecto de 1. OLLAMA_MAX_LOADED_MODELS vale por defecto tres veces el número de GPU, o tres en inferencia por CPU. OLLAMA_MAX_QUEUE admite por defecto 512 peticiones en cola, tras lo cual el servidor responde con un error 503. Las preguntas frecuentes también advierten de que la memoria necesaria crece con OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH. La longitud de contexto por defecto se describe de forma distinta según la página y depende de la VRAM disponible, así que fíjela explícitamente y compruébela en la columna CONTEXT de ollama ps.
OLLAMA_HOST=127.0.0.1:11434 \
OLLAMA_NUM_PARALLEL=4 \
OLLAMA_CONTEXT_LENGTH=16384 \
OLLAMA_KEEP_ALIVE=30m \
ollama serve
llama-server
llama-server divide el trabajo en slots. Cada slot sostiene una conversación, -np fija el número de slots y el valor por defecto -1 significa automático. El continuous batching está activado por defecto, de modo que las peticiones nuevas se unen al batch en curso entre pasos de decodificación. Cuando el número de slots es automático, todos comparten un búfer KV unificado. Cuando usted fija -np, el búfer unificado queda desactivado por defecto y -c se reparte entre los slots: -c 32768 -np 4 da a cada conversación 8192 tokens. Compruebe el resultado con GET /slots, que muestra n_ctx de cada slot.
La descarga a la GPU se ajusta con -ngl, que acepta un número, auto (por defecto) o all. Si el modelo no cabe, las capas que quedan en la CPU dependen del ancho de banda de la memoria del sistema, así que un modelo parcialmente descargado funciona, pero suele generar tokens más despacio.
llama-server -m /models/qwen3-8b-q4_k_m.gguf \
--host 127.0.0.1 --port 8080 \
-c 32768 -np 4 -ngl all \
--api-key-file /etc/llama/api-keys.txt \
--metrics
vLLM
El continuous batching de vLLM admite secuencias nuevas en cada iteración de decodificación mientras haya bloques libres en la caché KV, y PagedAttention asigna esa caché en bloques de tamaño fijo a medida que crecen las secuencias, en lugar de reservar de antemano la longitud máxima. Al arrancar, vLLM reserva la fracción de memoria de GPU que indica --gpu-memory-utilization y convierte lo que queda tras los pesos y el espacio de trabajo de las activaciones en bloques de caché KV. Cuando los bloques se agotan bajo carga, desaloja algunas peticiones y las recalcula cuando vuelve a haber espacio, algo que se aprecia en la métrica vllm:num_preemptions y en la latencia de cola.
Las palancas principales son --max-model-len para el contexto más largo que acepta, --max-num-seqs para el máximo de secuencias por iteración, --max-num-batched-tokens, --gpu-memory-utilization y --tensor-parallel-size para repartir un modelo entre varias GPU. Reducir --max-model-len a lo que la aplicación necesita de verdad suele ser la forma más barata de acomodar más peticiones simultáneas.
export VLLM_API_KEY="$(openssl rand -hex 32)"
vllm serve Qwen/Qwen3-8B \
--host 127.0.0.1 --port 8000 \
--max-model-len 32768 \
--max-num-seqs 64 \
--gpu-memory-utilization 0.90
Salida estructurada y llamadas a herramientas
Los tres servidores pueden restringir la salida a un esquema JSON mediante el campo response_format de OpenAI, lo que hace portable esta petición:
schema = {
"type": "object",
"properties": {
"severity": {"type": "string", "enum": ["low", "medium", "high"]},
"summary": {"type": "string"},
},
"required": ["severity", "summary"],
"additionalProperties": False,
}
reply = client.chat.completions.create(
model=os.environ["LLM_MODEL"],
messages=[{"role": "user", "content": "Triage this log line: disk /var is 97% full"}],
response_format={
"type": "json_schema",
"json_schema": {"name": "triage", "schema": schema},
},
)
La API nativa de Ollama también acepta "json" o un esquema en su campo format, y su documentación recomienda repetir el esquema en el prompt. llama-server convierte los esquemas JSON a su formato de gramática GBNF y acepta además una gramática en bruto para otras formas de salida. vLLM admite response_format y un objeto structured_outputs en extra_body con las claves json, regex, choice, grammar y structural_tag. Los antiguos parámetros guided_* se eliminaron en la v0.12.0, como señala la página de structured outputs de vLLM. La decodificación restringida garantiza la sintaxis, no la veracidad, y una respuesta cortada por max_tokens sigue siendo JSON inválido, así que valide cada resultado contra el esquema en su propio código.
Las llamadas a herramientas funcionan en los tres, con una configuración distinta:
- Ollama acepta
toolsen/api/chaty en/v1/chat/completions, incluidas las llamadas paralelas, para modelos cuyas plantillas admiten herramientas. - llama-server admite herramientas al estilo OpenAI mediante plantillas de chat Jinja, activadas por defecto. Las notas de llama.cpp sobre function calling enumeran manejadores nativos para varias familias de modelos y un modo genérico de respaldo que consume más tokens. Las llamadas paralelas siguen desactivadas mientras la petición no indique
"parallel_tool_calls": true. - vLLM necesita
--enable-auto-tool-choicey un--tool-call-parseracorde con la familia del modelo, por ejemplohermespara Qwen2.5 ollama3_jsonpara Llama 3.1 y 3.2. La página de tool calling de vLLM explica que las funciones nombradas ytool_choice="required"usan structured outputs, mientras queautono garantiza que los argumentos se puedan analizar.
vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 127.0.0.1 --port 8000 \
--enable-auto-tool-choice --tool-call-parser hermes
Trate los argumentos de las herramientas como entrada no fiable. Los elige el modelo, y el texto de un documento recuperado puede condicionar esa elección, como explica la guía sobre prompt injection y seguridad MCP.
Ejecución en Docker con acceso a la GPU
En Linux con GPU de NVIDIA, instale primero NVIDIA Container Toolkit para que funcione --gpus. Los comandos siguientes reproducen la imagen y las opciones documentadas de cada proyecto con un único cambio: cada puerto se publica solo en 127.0.0.1. La documentación de Docker sobre publicación de puertos indica que los puertos mapeados sin dirección de host se publican en todas las direcciones del host, y las notas de Docker sobre cortafuegos añaden que el tráfico de los puertos publicados se desvía antes de que lo vean las reglas de ufw.
# Ollama with NVIDIA GPUs
docker run -d --name ollama --gpus=all \
-v ollama:/root/.ollama \
-p 127.0.0.1:11434:11434 \
ollama/ollama
# llama-server, CUDA build
docker run -d --name llama --gpus all \
-v /srv/models:/models:ro \
-p 127.0.0.1:8080:8080 \
ghcr.io/ggml-org/llama.cpp:server-cuda \
-m /models/qwen3-8b-q4_k_m.gguf --host 0.0.0.0 --port 8080 -ngl all
# vLLM with NVIDIA GPUs
docker run -d --name vllm --runtime nvidia --gpus all --ipc=host \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--env "HF_TOKEN=$HF_TOKEN" \
-p 127.0.0.1:8000:8000 \
vllm/vllm-openai:latest \
--model Qwen/Qwen3-8B
Dentro de un contenedor, el servidor debe escuchar en 0.0.0.0; de lo contrario, el puerto publicado no llega a él. Lo que lo mantiene privado es el 127.0.0.1 del lado del host. El comando documentado de vLLM añade --ipc=host porque PyTorch comparte datos entre procesos mediante memoria compartida, sobre todo en la inferencia con paralelismo tensorial. Para GPU de AMD, use ollama/ollama:rocm con --device /dev/kfd --device /dev/dri, las imágenes server-rocm o server-vulkan de llama.cpp, o vllm/vllm-openai-rocm. En producción, fije etiquetas o digests de imagen en lugar de latest.
Seguridad: escuchar en local y autenticar en el proxy
Trate un puerto de inferencia como un puerto de base de datos. Cualquiera que llegue a él puede gastar su tiempo de GPU, leer todo lo que devuelve el modelo y, en algunas configuraciones, invocar rutas administrativas.
Empiece por la dirección de escucha. Ollama escucha por defecto en 127.0.0.1:11434 y llama-server en 127.0.0.1:8080. vLLM es distinto: si no se indica --host, su lanzador escucha en todas las interfaces, así que fuera de un contenedor pase siempre --host 127.0.0.1.
Después compruebe qué protege realmente la clave de cada servidor:
- La API local de Ollama no tiene ajuste de clave de API. Cualquiera que alcance el puerto puede usarla, así que compártala solo a través de un proxy que autentique.
- llama-server comprueba
--api-keyo--api-key-fileen su API, mientras que/healthsigue siendo público por diseño. Su README advierte de que el CORS refleja por defecto cualquierOriginy recomienda--cors-originsen redes locales. - El
--api-keyde vLLM cubre solo unos pocos prefijos de ruta, entre ellos/v1. La guía de seguridad de vLLM enumera rutas sin protección como/invocations, que llega a las mismas funciones de inferencia, o/pausey/abort_requests, y recomienda un proxy inverso que solo permita una lista de endpoints.
Un pequeño frontal de Nginx cubre ambas necesidades. Termina TLS, comprueba un token bearer, reenvía solo /v1/ y devuelve 404 para todo lo demás.
map $http_authorization $llm_client {
default "";
"Bearer REPLACE_WITH_A_LONG_RANDOM_TOKEN" "team";
}
server {
listen 443 ssl;
server_name llm.example.internal;
ssl_certificate /etc/nginx/tls/llm.crt;
ssl_certificate_key /etc/nginx/tls/llm.key;
client_max_body_size 10m;
location /v1/ {
if ($llm_client = "") { return 401; }
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 600s;
}
location / {
return 404;
}
}
proxy_buffering off permite que los tokens en streaming lleguen al cliente a medida que se generan. Si el servidor de origen tiene su propia clave, asígnele el mismo token para que una petición que se salte el proxy siga fallando. Para Ollama, apunte proxy_pass al puerto 11434 y añada proxy_set_header Host localhost:11434, como en el ejemplo de las preguntas frecuentes. Añada limit_req cuando varios usuarios compartan una GPU. En el portátil de un desarrollador, recuerde que OLLAMA_ORIGINS=* permite que cualquier página web que visite llame al servidor local a través de su navegador.
Monitorización y comprobaciones de salud
Empiece por la disponibilidad. El /health de llama-server devuelve 503 mientras carga el modelo y 200 cuando está listo, y vLLM también expone /health. Úselos para los health checks de contenedores y las sondas del balanceador, no como prueba de que la calidad de generación es buena.
vLLM expone métricas de Prometheus en /metrics por defecto. La página de métricas de vLLM enumera vllm:num_requests_running, vllm:num_requests_waiting, vllm:kv_cache_usage_perc, histogramas del tiempo hasta el primer token, de la latencia entre tokens y de la latencia de extremo a extremo, y un contador de desalojos. llama-server solo expone /metrics si se arranca con --metrics, e incluye llamacpp:requests_processing, llamacpp:requests_deferred, llamacpp:prompt_tokens_seconds y llamacpp:predicted_tokens_seconds, mientras que GET /slots muestra el estado de cada slot.
scrape_configs:
- job_name: vllm
static_configs:
- targets: ["127.0.0.1:8000"]
- job_name: llama-server
static_configs:
- targets: ["127.0.0.1:8080"]
La API documentada de Ollama no tiene endpoint de Prometheus. Use GET /api/ps para ver los modelos cargados, su uso de VRAM, su longitud de contexto y cuándo se descargarán, y lea los campos de tiempos de cada respuesta. Las duraciones están en nanosegundos, así que la velocidad de generación es eval_count / eval_duration × 10^9 tokens por segundo:
curl -s http://127.0.0.1:11434/api/generate \
-d '{"model": "qwen3:8b", "prompt": "Say ready.", "stream": false}' |
jq '{tokens: .eval_count, tokens_per_second: (.eval_count / .eval_duration * 1e9)}'
Configure alertas sobre las señales de que hay usuarios esperando: una cola waiting o deferred que no baja de cero, un uso de caché KV cercano a 1, un contador de desalojos en aumento y un tiempo hasta el primer token que sube en el p95. Las métricas del servidor describen capacidad. No dicen qué prompt, qué llamada a herramienta o qué paso de recuperación ralentizó una petición, así que conéctelas con las trazas de las peticiones, como se describe en la guía de observabilidad de agentes de IA.
Comparación lado a lado
| Ollama | llama.cpp llama-server |
vLLM | |
|---|---|---|---|
| Optimizado para | Instalación rápida y gestión de modelos | Portabilidad en CPU, Apple Silicon y GPU de consumo | Rendimiento en GPU de centro de datos |
| Formatos de modelo | Modelos de la biblioteca, GGUF, importación Safetensors | GGUF | Safetensors de Hugging Face, GPTQ, AWQ, FP8 y otros; GGUF experimental |
| Dirección de escucha por defecto | 127.0.0.1:11434 |
127.0.0.1:8080 |
Todas las interfaces, puerto 8000 |
| Clave de API integrada | Ninguna | --api-key, --api-key-file |
--api-key o VLLM_API_KEY, prefijos de ruta limitados |
| Concurrencia | OLLAMA_NUM_PARALLEL por modelo, 1 por defecto |
Slots (-np) con continuous batching |
Continuous batching con PagedAttention |
| Salida estructurada | format, response_format |
response_format, esquema JSON, GBNF |
response_format, structured_outputs |
| Llamadas a herramientas | tools en rutas nativas y de OpenAI |
Plantillas Jinja, manejadores nativos o genéricos | --enable-auto-tool-choice con un parser |
| Métricas | /api/ps y tiempos de respuesta |
/metrics con --metrics, /slots |
/metrics por defecto |
| Uso ideal | Un desarrollador, prototipos | Equipos pequeños, hardware modesto o mixto | Muchos usuarios simultáneos, SLO de producción |
Una ruta de decisión para desarrollo, equipos y producción
Recorra estas preguntas en orden y deténgase en la primera respuesta clara.
- ¿Es una sola persona probando modelos en un portátil o una estación de trabajo? Use Ollama. Cambie cuando necesite métricas, autenticación o control por petición.
- ¿Trabaja con un Mac, un servidor sin GPU o una tarjeta de consumo en cuya VRAM el modelo apenas cabe o no cabe en absoluto? Use llama-server. La cuantización GGUF y la descarga parcial le permiten funcionar donde vLLM no puede cargar el modelo.
- ¿Es un equipo pequeño con unos pocos usuarios simultáneos en una sola máquina? Use llama-server con
-npigual a la concurrencia máxima, una clave de API,--metricsy un proxy. Ollama también sirve si todos comparten uno o dos modelos y el proxy se encarga de la autenticación. - ¿Espera tráfico concurrente sostenido, objetivos de latencia, varias GPU o formatos de cuantización para GPU como FP8 o AWQ? Use vLLM, ligado a localhost detrás de una pasarela, con imágenes fijadas y recogida de métricas de Prometheus desde el primer día.
Muchos equipos acaban usando dos: Ollama o llama-server en las máquinas de desarrollo, vLLM en staging y producción, y el mismo código cliente de OpenAI en todas partes. Eso solo funciona si la batería de evaluación se ejecuta contra el artefacto de producción, porque una cuantización GGUF y un checkpoint FP8 del mismo modelo se comportan en la práctica como modelos distintos. La guía de arquitectura de agentes de IA en producción cubre la pasarela, los timeouts, los reintentos y las alternativas de respaldo que deben situarse delante de cualquiera de estos servidores.
Lista de comprobación para el despliegue
- Elija el servidor con la ruta de decisión y anote el archivo de modelo exacto, la cuantización y la plantilla de chat que va a servir.
- Calcule la memoria de la caché KV para la concurrencia máxima y el contexto máximo, y ajuste en consecuencia
OLLAMA_CONTEXT_LENGTH,-cy-np, o--max-model-leny--max-num-seqs. - Escuche en
127.0.0.1, pase--host 127.0.0.1a vLLM de forma explícita y publique los puertos de Docker como127.0.0.1:port:port. - Ponga TLS, autenticación, una lista de endpoints permitidos y limitación de peticiones en un proxy inverso. No exponga nunca un puerto de inferencia sin autenticación.
- Defina una clave en llama-server o vLLM como segunda capa y manténgala fuera del historial del shell y de las capas de la imagen.
- Recoja
/metricsde llama-server y vLLM, y consulte/api/psde Ollama, a través de una red privada. - Haga pruebas de carga con sus propios prompts a la concurrencia esperada y registre el tiempo hasta el primer token, los tokens por segundo, la longitud de la cola y la memoria.
- Valide la salida estructurada contra su esquema en el código y trate los argumentos de las herramientas como entrada no fiable.
- Fije versiones y digests de imagen, y vuelva a ejecutar la batería de evaluación cada vez que cambien el servidor, el archivo de modelo, la cuantización o la imagen.