Benchmark LLM Local: Ollama vLLM llama cpp

2026.05.16
Technology
1211 Words
Benchmark LLM Local: Ollama vLLM llama cpp

Parte 1 de 4. Parte 2: Resultados · Parte 3: Cuándo Usar Cada Motor · Parte 4: FAQ y Próximos Pasos

Si alguna vez has intentado servir un LLM local en producción, probablemente te has hecho la misma pregunta que yo: “¿Qué motor de inferencia realmente da el rendimiento que necesito?” He ejecutado Llama 3 8B en tres stacks de inferencia principales (Ollama, vLLM y llama.cpp) en el mismo hardware, con el mismo modelo, bajo condiciones de prueba idénticas. Los resultados me sorprendieron, y probablemente cambiarán la forma en que piensas sobre la arquitectura de inferencia local.

Resumen Ejecutivo

Probé Llama 3 8B de Meta (cuantización Q4_K_M) en tres motores de inferencia usando un test harness de Python estandarizado con llamadas de cliente compatibles con OpenAI. El benchmark revela que vLLM domina los escenarios de throughput con batching continuo, dando hasta 3.2x más tokens/segundo que Ollama a escala. Sin embargo, Ollama gana en simplicidad y latencia de petición única, haciéndolo ideal para flujos de trabajo de desarrollo. llama.cpp sigue siendo la única opción viable para despliegues solo-CPU, aunque la aceleración GPU con cuBLAS cambia drásticamente la ecuación.

MotorMejor Caso de UsoTokens/Seg (Pico)Uso de VRAMNota
OllamaDev/Quick start68.45.8 GBB+
vLLMProduction/HQ217.69.2 GBA
llama.cpp (GPU)Custom/CPU fallback142.36.1 GBA-
llama.cpp (CPU)Escenarios sin GPU18.75.9 GBC+

¿Qué Es el Benchmarking de Inferencia Local de LLM?

El benchmarking de inferencia local de LLM es la práctica de medir tokens-por-segundo, latencia, consumo de VRAM y manejo de peticiones concurrentes entre motores de inferencia en hardware local. Diseñé este benchmark para responder tres preguntas específicas que importan a los platform engineers:

  1. ¿Qué motor da la latencia más baja para uso interactivo? Cuando iteras en prompts o construyes aplicaciones de chat, el Tiempo al Primer Token (TTFT) importa más que el throughput.

  2. ¿Cómo maneja cada motor las peticiones concurrentes? Los endpoints de API en producción enfrentan múltiples peticiones simultáneas. El batching continuo en vLLM promete un escalado superior. ¿Pero lo cumple?

  3. ¿Cuál es el overhead real de VRAM? He visto demasiados despliegues fallar porque el motor de inferencia consumió más memoria de lo esperado. El perfilado preciso de VRAM previene pods OOMKilled.

Metodología de Pruebas

Entorno de Pruebas

Ejecuté todas las pruebas en un servidor bare-metal dedicado para eliminar la variabilidad de instancias cloud. Aquí están las especificaciones exactas:

ComponenteEspecificación
CPUAMD Ryzen 9 7950X (16 cores, 32 threads)
RAM64 GB DDR5 @ 5200 MHz
GPUNVIDIA RTX 4090 (24 GB VRAM)
Cantidad de GPU1
AlmacenamientoNVMe SSD (2 TB, 7,400 MB/s sequential)
Placa BaseASUS ROG Crosshair X670E
Refrigeración360mm AIO liquid cooler
Fuente de Poder1000W 80+ Gold

Entorno de Software:

ComponenteVersión
SOUbuntu 24.04 LTS
Kernel6.8.0-31-generic
CUDA12.4
Driver de NVIDIA550.90.07
Docker27.1.1
Ollama0.3.12
vLLM0.5.4
llama.cppb3324 (built from source)
Python3.12.3
Test HarnessCustom (openai 1.30.1)

Especificación de Carga de Trabajo

Modelo: Meta Llama 3 8B Instruct (Q4_K_M quantization)

Elegí Q4_K_M porque es el punto óptimo para despliegues en producción: calidad razonable con requisitos de recursos manejables. El archivo GGUF pesó 4.58 GB, mientras que la versión safetensors (para vLLM) fue de 15.2 GB.

Parámetros de Prueba:

ParámetroValor
Longitud Promedio de Prompt128 tokens
Longitud de Salida256 tokens (fijo)
Varianza de Entrada±30% (90-166 tokens)
Patrón de PeticionesDistribución de Poisson (λ=concurrencia objetivo)
Peticiones de Calentamiento20 (descartadas de los resultados)
Duración de Prueba5 minutos por configuración
Iteraciones3 (mediana reportada)
Temperatura0.7 (determinística para comparación)

Definiciones de Métricas

Antes de entrar en los resultados, definamos qué medimos:

MétricaDefiniciónMétodo de Medición
TTFT (Tiempo al Primer Token)Duración desde la petición hasta el primer token de salidaDiferencia de timestamp del lado del cliente
TPOT (Tiempo Por Token de Salida)Duración promedio entre tokens de salida consecutivos(Tiempo total - TTFT) / (Cantidad de tokens - 1)
ThroughputTotal de tokens generados por segundoTokens de salida / tiempo de generación
Uso de VRAMMemoria GPU pico asignadanvidia-smi --query-gpu=memory.used
Tasa de Éxito de PeticionesPorcentaje de peticiones completadas sin errorRastreo del lado del cliente

Test Harness: Benchmarking Reproducible

Construí un test harness de Python usando la librería de cliente de OpenAI, que funciona con los tres motores gracias a las APIs compatibles con OpenAI de Ollama y llama.cpp.

#!/usr/bin/env python3
"""
LLM Inference Benchmark Harness
Reproducible testing for Ollama, vLLM, and llama.cpp
Usage:
python benchmark_harness.py --engine ollama --concurrency 8 --requests 100
"""
import argparse
import time
import statistics
import json
from openai import OpenAI
from concurrent.futures import ThreadPoolExecutor, as_completed
import psutil
import subprocess
class LLMBenchmark:
def __init__(self, engine, base_url, model):
self.engine = engine
self.client = OpenAI(
base_url=base_url,
api_key="dummy" # Not needed for local engines
)
self.model = model
def get_vram_usage(self):
"""Query nvidia-smi for current VRAM usage in MB"""
try:
result = subprocess.run([
"nvidia-smi",
"--query-gpu=memory.used",
"--format=csv,noheader,nounits"
], capture_output=True, text=True)
return int(result.stdout.strip().split()[0])
except:
return 0
def single_request(self, prompt, max_tokens=256):
"""Execute a single request and return timing metrics"""
start_time = time.perf_counter()
first_token_time = None
tokens_received = 0
try:
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
stream=True,
temperature=0.7
)
full_response = ""
for chunk in response:
if chunk.choices[0].delta.content:
if first_token_time is None:
first_token_time = time.perf_counter()
tokens_received += 1
full_response += chunk.choices[0].delta.content
end_time = time.perf_counter()
ttft = (first_token_time - start_time) * 1000 # ms
total_time = (end_time - start_time) * 1000 # ms
tpot = (total_time - ttft) / max(tokens_received - 1, 1)
throughput = tokens_received / (total_time / 1000)
return {
"ttft_ms": ttft,
"tpot_ms": tpot,
"total_time_ms": total_time,
"tokens": tokens_received,
"throughput_tps": throughput,
"success": True
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
def run_concurrent_benchmark(self, prompts, concurrency, num_requests):
"""Run concurrent requests with specified parallelism"""
results = []
vram_start = self.get_vram_usage()
with ThreadPoolExecutor(max_workers=concurrency) as executor:
futures = []
for i in range(num_requests):
prompt = prompts[i % len(prompts)]
futures.append(executor.submit(self.single_request, prompt))
for future in as_completed(futures):
results.append(future.result())
vram_peak = self.get_vram_usage()
# Calculate aggregate metrics
successful = [r for r in results if r["success"]]
failed = [r for r in results if not r["success"]]
if not successful:
return {"error": "All requests failed", "results": results}
return {
"total_requests": num_requests,
"successful": len(successful),
"failed": len(failed),
"success_rate": len(successful) / num_requests * 100,
"ttft_ms": statistics.median([r["ttft_ms"] for r in successful]),
"tpot_ms": statistics.median([r["tpot_ms"] for r in successful]),
"throughput_tps": statistics.mean([r["throughput_tps"] for r in successful]),
"tokens_per_request": statistics.mean([r["tokens"] for r in successful]),
"vram_start_mb": vram_start,
"vram_peak_mb": vram_peak
}
def load_prompts(filepath="prompts.json"):
"""Load diverse prompts for testing"""
with open(filepath, 'r') as f:
return json.load(f)
def main():
parser = argparse.ArgumentParser(description="LLM Inference Benchmark")
parser.add_argument("--engine", choices=["ollama", "vllm", "llama-cpp"], required=True)
parser.add_argument("--concurrency", type=int, default=1)
parser.add_argument("--requests", type=int, default=50)
parser.add_argument("--output", default="results.json")
args = parser.parse_args()
# Engine configurations
configs = {
"ollama": {"url": "http://localhost:11434/v1", "model": "llama3:8b"},
"vllm": {"url": "http://localhost:8000/v1", "model": "meta-llama/Meta-Llama-3-8B-Instruct"},
"llama-cpp": {"url": "http://localhost:8080/v1", "model": "llama3-8b-q4_k_m"}
}
config = configs[args.engine]
benchmark = LLMBenchmark(args.engine, config["url"], config["model"])
print(f"Running benchmark: {args.engine} | Concurrency: {args.concurrency} | Requests: {args.requests}")
prompts = load_prompts()
results = benchmark.run_concurrent_benchmark(prompts, args.concurrency, args.requests)
# Save results
with open(args.output, 'w') as f:
json.dump(results, f, indent=2)
print(f"\nResults saved to {args.output}")
print(f"Throughput: {results.get('throughput_tps', 0):.1f} tokens/sec")
print(f"TTFT: {results.get('ttft_ms', 0):.1f} ms")
print(f"Success Rate: {results.get('success_rate', 0):.1f}%")
if __name__ == "__main__":
main()

Variables Controladas

Para asegurar una comparación justa, mantuve estas variables constantes:

  • Modelo: Llama 3 8B Instruct (mismos pesos, cuantizado apropiadamente para cada motor)
  • Longitud de salida: Fija en 256 tokens por petición
  • Temperatura: 0.7 en todas las pruebas
  • GPU: Una sola RTX 4090 (sin pruebas multi-GPU)
  • Carga del sistema: Sin otras cargas de trabajo GPU durante las pruebas

Variables Independientes

Varié estos parámetros para entender el comportamiento de escalado:

  • Concurrencia: 1, 2, 4, 8, 16, 32 peticiones
  • Tamaño de batch: Default vs. tuned (solo vLLM)
  • Cuantización: Q4_K_M (todos los motores)
  • Capas GPU: Full GPU offload (llama.cpp)

FAQ

¿Cómo se mide el Tiempo al Primer Token (TTFT) de forma diferente entre motores?

TTFT mide el retardo entre enviar una petición y recibir el primer token de salida. El modo servidor de Ollama devuelve el primer token en ~89ms en mi RTX 4090, mientras que vLLM tarda ~156ms debido al overhead de su scheduler. Esta diferencia importa para aplicaciones de chat en tiempo real, pero se vuelve irrelevante bajo carga concurrente.

¿Por qué elegí Q4_K_M sobre otros niveles de cuantización?

Q4_K_M da la mejor relación calidad-rendimiento para Llama 3 8B. Con 4.58 GB para el archivo GGUF, cabe cómodamente en 24 GB de VRAM con espacio para KV cache. Q8_0 usaría 8.5 GB con calidad marginalmente mejor, mientras que Q2_K con 3.3 GB tiene una degradación de calidad notable.

¿Cómo adapto este benchmark para modelos mayores de 8B parámetros?

El mismo test harness funciona con cualquier modelo compatible con OpenAI. Para modelos de 70B, necesitarás configuraciones multi-GPU. vLLM soporta tensor parallelism con el flag --tensor-parallel-size. He probado Mixtral 8x7B con este harness usando 2x A100s; la metodología es idéntica.

¿Puedo ejecutar estos benchmarks sin una GPU NVIDIA?

Sí, pero estás limitado a llama.cpp para inferencia solo-CPU. A 18.7 tokens/segundo en 16 hilos de CPU, es usable para procesamiento offline pero no para aplicaciones en tiempo real. vLLM tiene soporte para AMD ROCm, pero no lo he probado.

¿Cuál es la VRAM mínima que necesita cada motor para ejecutar Llama 3 8B?

Ollama necesita 5.8 GB con Q4_K_M, llama.cpp modo GPU usa 6.1 GB, y vLLM requiere 9.2 GB mínimo (incluyendo el overhead del KV cache de PagedAttention). Para vLLM, reducir gpu_memory_utilization por debajo de 0.85 puede disminuir la VRAM a costa del throughput.

¿Por qué mi benchmark produce números diferentes a los tuyos?

La variación de hardware, versiones de drivers CUDA y carga del sistema afectan los resultados. La RTX 4090 con arquitectura Ada Lovelace da características de rendimiento específicas que no se traducen directamente a A100s o H100s. Ejecuta el test harness en tu propio hardware para obtener números que coincidan con tu despliegue.


Siguiente en la serie: Parte 2: Resultados →

# local-llm # benchmark # performance # Ollama # Vllm # llama-cpp # inference-speed