Saltar a contenido

LLaMA.cpp: Compilación, cuantización y benchmarking

llama.cpp es el motor de inferencia de LLMs escrito en C/C++ que hace posible ejecutar modelos como LLaMA, Mistral, Qwen o Phi en tu portátil, en un servidor sin GPU o incluso en una Raspberry Pi. Es la pieza de bajo nivel sobre la que se construyen herramientas como Ollama o LM Studio: si entiendes llama.cpp, entiendes lo que ocurre por debajo.

Esta guía es práctica y dedicada: compilación por backend, el formato GGUF y su cuantización, el uso real del CLI y del servidor, y cómo medir el rendimiento. Si buscas una comparativa entre frameworks, revisa primero Ecosistema de Modelos Locales.

¿Por qué compilar en lugar de usar Ollama?

Con llama.cpp tienes control total: eliges flags de compilación, backend de GPU, tipo de cuantización y parámetros de inferencia. Es la opción para producción en hardware limitado, edge devices y cuando necesitas exprimir cada token/segundo.

🧩 Arquitectura en un vistazo

flowchart LR
    A[Modelo original<br/>PyTorch / safetensors] -->|convert_hf_to_gguf.py| B[GGUF F16]
    B -->|llama-quantize| C[GGUF cuantizado<br/>Q4_K_M, Q5_K_M...]
    C --> D{Backend ggml}
    D -->|CPU| E[llama-cli / llama-server]
    D -->|CUDA / Metal / Vulkan| E
    E --> F[Inferencia<br/>API OpenAI-compatible]

El núcleo es ggml, la librería de tensores. Los modelos se distribuyen en GGUF, un formato de fichero único que incluye pesos, tokenizer y metadatos.

📦 Obtener el código

git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp

El repositorio se movió

El proyecto vive ahora en ggml-org/llama.cpp (antes ggerganov/llama.cpp). El make manual está deprecado: la vía oficial es CMake.

🔨 Compilación por backend

CPU (por defecto)

# Configuración y build en Release (optimizado)
cmake -B build
cmake --build build --config Release -j $(nproc)

# Los binarios quedan en build/bin/
ls build/bin/
# llama-cli  llama-server  llama-quantize  llama-bench ...

En CPU, llama.cpp usa automáticamente las instrucciones SIMD disponibles (AVX2, AVX-512, NEON en ARM). Para forzar optimización nativa del host:

cmake -B build -DCMAKE_C_FLAGS="-march=native" -DCMAKE_CXX_FLAGS="-march=native"
cmake --build build --config Release -j $(nproc)

CUDA (NVIDIA)

# Requiere el CUDA Toolkit instalado (nvcc en el PATH)
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j $(nproc)

Compilación más rápida y binario más ligero

Limita las arquitecturas de GPU a la tuya con -DCMAKE_CUDA_ARCHITECTURES=86 (86 = Ampere/RTX 30xx, 89 = Ada/RTX 40xx, 90 = Hopper). Reduce mucho el tiempo de build.

Metal (Apple Silicon)

# En macOS con chips M1/M2/M3/M4, Metal se activa por defecto.
cmake -B build
cmake --build build --config Release -j $(sysctl -n hw.ncpu)

Metal aprovecha la memoria unificada del Apple Silicon, así que puedes cargar modelos grandes sin GPU dedicada. Para forzarlo explícitamente: -DGGML_METAL=ON.

Vulkan (GPU multiplataforma: AMD, Intel, NVIDIA)

# Requiere el Vulkan SDK (headers y glslc)
cmake -B build -DGGML_VULKAN=ON
cmake --build build --config Release -j $(nproc)

Vulkan es la mejor opción para GPUs AMD/Intel sin depender de ROCm, y funciona en Windows, Linux y ChromeOS.

Backend Flag CMake Hardware objetivo
CPU (por defecto) Cualquier x86_64 / ARM
CUDA -DGGML_CUDA=ON GPUs NVIDIA
Metal -DGGML_METAL=ON Apple Silicon (M1+)
Vulkan -DGGML_VULKAN=ON AMD / Intel / NVIDIA
HIP/ROCm -DGGML_HIP=ON GPUs AMD (ROCm)
SYCL -DGGML_SYCL=ON Intel GPU / oneAPI

🗜️ GGUF y cuantización

GGUF (GPT-Generated Unified Format) es el formato de modelo de llama.cpp. La cuantización reduce la precisión de los pesos (de 16 bits a 4-8 bits) para bajar el uso de RAM/VRAM y acelerar la inferencia, a cambio de una pérdida controlada de calidad.

Descargar un modelo ya en GGUF

La forma más rápida: descargar un GGUF cuantizado directamente. llama-cli y llama-server aceptan -hf para tirar de Hugging Face:

# Descarga automática desde Hugging Face (repo:quant)
./build/bin/llama-cli -hf ggml-org/gemma-3-1b-it-GGUF

Convertir y cuantizar tú mismo

# 1. Convertir el modelo Hugging Face a GGUF en F16
python convert_hf_to_gguf.py ./models/mi-modelo --outfile modelo-f16.gguf --outtype f16

# 2. Cuantizar a Q4_K_M (el mejor equilibrio calidad/tamaño)
./build/bin/llama-quantize modelo-f16.gguf modelo-q4_k_m.gguf Q4_K_M

Tipos de cuantización habituales

Tipo Bits aprox. Uso de RAM Calidad Recomendación
Q8_0 8 Alta Casi idéntica a F16 Máxima calidad práctica
Q6_K 6.5 Media-alta Excelente Muy buena opción
Q5_K_M 5.5 Media Muy buena Alternativa de calidad
Q4_K_M 4.5 Baja Buena Por defecto recomendado
Q3_K_M 3.5 Muy baja Aceptable Hardware muy limitado
Q2_K 2.6 Mínima Degradada Solo si no hay otra opción

Regla práctica

Q4_K_M es el punto dulce para la mayoría de casos. Baja a Q3/Q2 solo si no te entra en memoria, y sube a Q6/Q8 si te sobra VRAM y quieres máxima fidelidad.

💻 Uso del CLI: llama-cli

Chat interactivo

./build/bin/llama-cli -m modelo-q4_k_m.gguf

Un prompt único (modo no interactivo)

./build/bin/llama-cli -m modelo-q4_k_m.gguf \
  -p "Explica qué es Kubernetes en 3 líneas" \
  -n 256 \
  --no-display-prompt

Flags esenciales

Flag Descripción
-m Ruta al fichero GGUF
-p Prompt de entrada
-n Número máximo de tokens a generar (-1 = infinito)
-c Tamaño del contexto (por defecto 4096; 0 = el del modelo)
-ngl Capas descargadas a la GPU (n-gpu-layers)
-t Número de hilos de CPU
--temp Temperatura de muestreo (0 = determinista)
-cnv Fuerza modo conversación

Descarga a GPU (GPU offload)

# -ngl 99 intenta cargar todas las capas en la GPU
./build/bin/llama-cli -m modelo-q4_k_m.gguf -ngl 99 -p "Hola"

¿Cuántas capas caben?

Empieza con -ngl 99 (todas). Si te quedas sin VRAM, baja el número hasta que cargue. Las capas no descargadas se ejecutan en CPU (más lento pero funcional).

🌐 Servidor con API compatible con OpenAI: llama-server

llama-server levanta un servidor HTTP con endpoints compatibles con la API de OpenAI, además de una interfaz web en http://localhost:8080.

./build/bin/llama-server \
  -m modelo-q4_k_m.gguf \
  -c 8192 \
  -ngl 99 \
  --host 0.0.0.0 \
  --port 8080

Llamar al endpoint de chat (OpenAI-compatible)

curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "local",
    "messages": [
      {"role": "system", "content": "Eres un asistente DevOps conciso."},
      {"role": "user", "content": "Dame un script bash para backup de PostgreSQL"}
    ],
    "temperature": 0.7
  }'

Con el SDK oficial de OpenAI en Python

from openai import OpenAI

# Apunta al servidor local; la api_key es un placeholder
client = OpenAI(base_url="http://localhost:8080/v1", api_key="sk-no-key-required")

resp = client.chat.completions.create(
    model="local",
    messages=[{"role": "user", "content": "Explica Docker en 3 líneas"}],
)
print(resp.choices[0].message.content)

Compatibilidad drop-in

Como el endpoint imita a OpenAI, cualquier librería o herramienta que hable con la API de OpenAI (LangChain, LlamaIndex, continue.dev...) funciona cambiando solo base_url. Igual que LM Studio, que también expone un servidor OpenAI-compatible.

📊 Benchmarking con llama-bench

llama-bench mide el rendimiento de forma reproducible, separando dos métricas clave:

  • pp (prompt processing): velocidad de ingesta del prompt (tokens/s).
  • tg (text generation): velocidad de generación de tokens (tokens/s).
# Benchmark básico: 512 tokens de prompt, 128 de generación
./build/bin/llama-bench -m modelo-q4_k_m.gguf -p 512 -n 128
# Barrido de niveles de GPU offload para encontrar el óptimo
./build/bin/llama-bench -m modelo-q4_k_m.gguf -ngl 0,20,40,99

Salida típica (resumida):

| model            | size   | backend | ngl | test  |    t/s |
| ---------------- | ------ | ------- | --- | ----- | ------ |
| llama 7B Q4_K_M  | 3.8GiB | CUDA    |  99 | pp512 | 2450.3 |
| llama 7B Q4_K_M  | 3.8GiB | CUDA    |  99 | tg128 |  118.7 |

Compara peras con peras

Para comparar cuantizaciones o backends, fija siempre los mismos -p y -n. Repite (-r 5) para promediar y reducir ruido.

⚡ Optimización CPU/GPU

En CPU

# Ajusta los hilos al número de núcleos físicos (no lógicos)
./build/bin/llama-cli -m modelo.gguf -t 8 -p "..."

# En NUMA (servidores multi-socket)
./build/bin/llama-cli -m modelo.gguf --numa distribute -p "..."
  • Usa núcleos físicos, no hilos lógicos: el hyperthreading rara vez ayuda en inferencia.
  • Compila con -march=native para aprovechar AVX-512 si tu CPU lo soporta.
  • Cuantizaciones _K (K-quants) están optimizadas para CPU moderna.

En GPU

  • Maximiza -ngl hasta llenar la VRAM disponible.
  • Usa flash attention para ahorrar memoria de contexto: -fa on.
  • Para contextos largos, activa la cache KV cuantizada: --cache-type-k q8_0 --cache-type-v q8_0.
./build/bin/llama-server -m modelo.gguf -ngl 99 -fa on \
  --cache-type-k q8_0 --cache-type-v q8_0 -c 16384

Contexto = memoria

El tamaño de contexto (-c) consume VRAM de forma proporcional. Un contexto de 32k puede duplicar el uso de memoria de la cache KV. Cuantízala o reduce -c si te quedas corto.

🎯 Cuándo elegir llama.cpp

  • Edge / hardware limitado: Raspberry Pi, mini-PCs, servidores sin GPU.
  • Producción con control fino: eliges cuantización, backend y sampling.
  • Máximo rendimiento por vatio: kernels optimizados a mano.
  • Si quieres una experiencia plug and play, usa Ollama (que usa llama.cpp por debajo) o LM Studio para una interfaz gráfica.

🔗 Recursos adicionales