DEV Community

Cover image for El ecosistema local de LLMs no necesita Ollama (y me incomodó descubrirlo)
Juan Torchia
Juan Torchia

Posted on • Originally published at juanchi.dev

El ecosistema local de LLMs no necesita Ollama (y me incomodó descubrirlo)

Ollama acaba de agregar soporte nativo para herramientas en más modelos y la comunidad está celebrando. Yo también lo usé, lo defiendo en Twitter cuando alguien se queja, y lo tengo corriendo en mi máquina hace más de un año. Pero tengo algo para decir que probablemente no sea lo que esperás de alguien que armó su primer MCP server local con Ollama como backend.

La semana pasada intenté sacarlo de la ecuación. Completamente. Y lo que encontré me incomodó lo suficiente como para escribir esto.

Local LLM sin Ollama: qué pasa cuando vas directo a llama.cpp

El contexto: estaba construyendo un pipeline que necesita correr inferencia desde un worker en Docker, sin interfaz, sin OpenAI-compatible API, sin nada que no sea necesario. Un modelo, una entrada, una salida. Punto.

Ollama en ese contexto se siente como llevar un camión a comprar el pan. Tiene un servidor HTTP, gestión de modelos, caching, una API REST, logs, actualizaciones... todo eso tiene un costo. No dramático, pero real.

Así que probé la alternativa obvia: llama.cpp directo, con un wrapper mínimo en Python.

# Instalar llama-cpp-python con soporte CUDA (si tenés GPU)
pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121

# O sin GPU, solo CPU
pip install llama-cpp-python
Enter fullscreen mode Exit fullscreen mode
from llama_cpp import Llama

# Cargás el modelo directo — sin servidor, sin magia intermedia
llm = Llama(
    model_path="./models/mistral-7b-instruct-q4_k_m.gguf",
    n_ctx=4096,          # contexto máximo
    n_threads=8,         # threads de CPU
    n_gpu_layers=35,     # capas en GPU (0 si no tenés)
    verbose=False        # silenciamos el ruido de llama.cpp
)

def inferencia(prompt: str) -> str:
    # Llamada directa — sin HTTP, sin JSON, sin overhead de red
    resultado = llm(
        prompt,
        max_tokens=512,
        temperature=0.7,
        stop=["</s>", "[INST]"],  # tokens de parada para Mistral
        echo=False
    )
    return resultado["choices"][0]["text"].strip()

# Probamos
respuesta = inferencia("[INST] Explicá qué es un índice compuesto en PostgreSQL [/INST]")
print(respuesta)
Enter fullscreen mode Exit fullscreen mode

Eso es todo. Sin servidor. Sin puerto 11434. Sin ollama pull. El modelo es un archivo .gguf que descargás de Hugging Face y lo apuntás directo.

Los números que no esperaba

En mi máquina (Ryzen 7, 32GB RAM, RTX 3060 12GB):

Setup Tiempo primera respuesta Uso de memoria extra
Ollama + modelo cargado ~180ms ~120MB overhead
llama-cpp-python directo ~95ms ~0MB overhead
Ollama cold start ~3.2s
llama-cpp-python cold start ~1.8s

No son diferencias que te van a cambiar la vida en uso interactivo. Pero en un pipeline que corre 500 inferencias por hora, empiezan a importar.

Dónde llama.cpp solo no alcanza y dónde Ollama realmente brilla

Aquí viene la parte incómoda. Después de dos días con el setup minimalista, empecé a extrañar cosas específicas de Ollama. No el servidor. No la API. Cosas concretas:

1. Gestión de modelos. ollama pull llama3.2 es una línea. Con llama-cpp-python tenés que ir a Hugging Face, encontrar el GGUF correcto para tu VRAM, descargarlo manualmente, y rezar para que el formato sea compatible. No es complicado, pero es fricción.

2. Compatibilidad de prompts automática. Ollama conoce el chat template de cada modelo. Con llama.cpp directo, tenés que formatear el prompt vos:

# Con Ollama — esto funciona para cualquier modelo
# ollama.chat(model="mistral", messages=[{"role": "user", "content": "hola"}])

# Con llama-cpp-python — tenés que saber el formato de cada modelo
def formato_mistral(mensaje: str) -> str:
    return f"[INST] {mensaje} [/INST]"

def formato_llama3(mensaje: str) -> str:
    # Llama 3 tiene un formato completamente diferente
    return f"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n{mensaje}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"

def formato_qwen(mensaje: str) -> str:
    # Y Qwen otro
    return f"<|im_start|>user\n{mensaje}<|im_end|>\n<|im_start|>assistant\n"

# Podés usar el chat handler built-in de llama-cpp-python
# pero requiere configuración adicional por modelo
Enter fullscreen mode Exit fullscreen mode

Esto parece menor hasta que querés cambiar de modelo y tu pipeline se rompe porque olvidaste actualizar el template.

3. La API OpenAI-compatible. Si estás enchufando tu LLM local en un agente, en LangChain, en un MCP server, en cualquier cosa que hable OpenAI, Ollama te lo da gratis. Con llama-cpp-python tenés que levantar vos el servidor:

# llama-cpp-python SÍ tiene servidor OpenAI-compatible
# pero tenés que levantarlo explícitamente
python -m llama_cpp.server --model ./models/mistral-7b.gguf --port 8000

# O programáticamente
from llama_cpp.server.app import create_app
from llama_cpp.server.settings import ModelSettings, ServerSettings

# Esto es lo que Ollama hace por vos, con mejor DX
Enter fullscreen mode Exit fullscreen mode

En algún punto, si necesitás API compatible y multi-modelo, estás reinventando Ollama. Y eso me recuerda algo que escribí sobre no reimplementar lo que ya existe cuando estás construyendo agentes.

Los errores que cometí y los gotchas que nadie te dice

Gotcha #1: el modelo en memoria no se libera solo.

Con Ollama, los modelos se descargan de memoria después de un timeout configurable. Con llama-cpp-python directo, el modelo vive mientras viva tu objeto Python. En un pipeline long-running, esto importa:

import gc
from llama_cpp import Llama

class GestorModelo:
    def __init__(self, ruta_modelo: str):
        self.ruta = ruta_modelo
        self._modelo = None

    def cargar(self):
        if self._modelo is None:
            self._modelo = Llama(model_path=self.ruta, n_gpu_layers=35)

    def descargar(self):
        """Liberar VRAM explícitamente — Ollama hace esto automático"""
        if self._modelo is not None:
            del self._modelo
            self._modelo = None
            gc.collect()  # forzar garbage collection

    def inferencia(self, prompt: str) -> str:
        self.cargar()
        return self._modelo(prompt, max_tokens=512)["choices"][0]["text"]
Enter fullscreen mode Exit fullscreen mode

Gotcha #2: el tamaño del contexto y la VRAM.

Ollama gestiona esto con defaults razonables. Con llama-cpp-python, si ponés n_ctx=8192 y tu modelo más el contexto no caben en VRAM, el proceso muere silenciosamente o llama.cpp offloadea a CPU sin avisarte bien. Siempre verificá:

# Verificar si el modelo cargó en GPU o cayó a CPU
llm = Llama(model_path="./model.gguf", n_gpu_layers=35, verbose=True)
# Buscá en los logs: "llm_load_tensors: offloaded X/Y layers to GPU"
# Si Y < n_gpu_layers, algo no entró en VRAM
Enter fullscreen mode Exit fullscreen mode

Gotcha #3: la imagen Docker.

Ollama tiene imagen oficial. Con llama-cpp-python tenés que construir la tuya, y si necesitás CUDA, la imagen base pesa fácil 6GB antes de agregar nada. Aprendí esto de la peor manera cuando estaba optimizando imágenes Docker — el mismo principio aplica acá: multi-stage, solo lo que necesitás:

# Imagen base CUDA — esto ya pesa
FROM nvidia/cuda:12.1-devel-ubuntu22.04 AS builder

RUN apt-get update && apt-get install -y python3-pip git cmake

# Compilar llama-cpp-python con CUDA desde fuente
ENV CMAKE_ARGS="-DLLAMA_CUDA=on"
RUN pip install llama-cpp-python --no-cache-dir

# Imagen final — solo runtime
FROM nvidia/cuda:12.1-runtime-ubuntu22.04
COPY --from=builder /usr/local/lib/python3.*/dist-packages /usr/local/lib/python3.10/dist-packages

# Agregás tu código, no el compilador
COPY ./src /app
WORKDIR /app
Enter fullscreen mode Exit fullscreen mode

FAQ: local LLM sin Ollama — las preguntas que me hicieron esta semana

¿Vale la pena reemplazar Ollama por llama.cpp directo?
Depende del contexto. Para desarrollo, exploración, o cualquier cosa que necesite cambiar modelos seguido: no. Ollama gana en DX por goleada. Para pipelines de producción donde el modelo está fijo, la latencia importa y no necesitás API: sí, tiene sentido evaluar llama-cpp-python o incluso llama.cpp binario directo.

¿Cuánto más rápido es llama.cpp sin el overhead de Ollama?
En mis pruebas, entre 10% y 40% dependiendo del modelo y el hardware. La diferencia más grande está en cold start (~45% más rápido) y en overhead de red local. En tokens por segundo con el modelo ya cargado, la diferencia es mucho menor — el cuello de botella es la inferencia en sí.

¿llama-cpp-python soporta los mismos modelos que Ollama?
Todos los modelos GGUF que funcionen en llama.cpp funcionan en llama-cpp-python. Que es básicamente todo — Llama, Mistral, Qwen, Phi, Gemma, DeepSeek. La diferencia es que con Ollama hacés ollama pull nombre y con llama.cpp tenés que descargar el .gguf manualmente de Hugging Face o usar huggingface_hub.

¿Qué pasa con las tool calls / function calling sin Ollama?
Esta es la parte donde Ollama todavía tiene ventaja. Las tool calls requieren que el modelo soporte el formato correcto Y que el runtime lo maneje bien. llama-cpp-python tiene soporte básico con grammar-based sampling, pero es más manual. Si tu pipeline depende de function calling, Ollama (o LM Studio para desktop) todavía es más cómodo. Justamente es lo que más extrañé cuando estaba probando integraciones para automatizar workflows repetitivos.

¿Tiene sentido usar las dos cosas en el mismo proyecto?
Absolutamente. Ollama para desarrollo local y experimentación, llama-cpp-python directo para el worker de producción con modelo fijo. No son mutuamente excluyentes y tampoco es sobreingeniería — son herramientas distintas para casos distintos.

¿Hay otras alternativas además de llama.cpp?
Sí: LM Studio tiene API compatible (pero es desktop), GPT4All tiene bindings Python, vLLM es la opción seria para multi-GPU y alto throughput (pero requiere CUDA y pesa más). Para uso embebido en código Python sin servidor, llama-cpp-python es la opción más madura hoy.

Ollama resuelve UX. Eso no es poco, pero tampoco es todo.

Aca está la conclusión que me costó aceptar después de dos días con el setup minimalista: Ollama es una herramienta de developer experience, no de infraestructura. Y eso está perfectamente bien. Resuelve un problema real — hacer que correr un LLM local sea accesible para cualquiera con una GPU decente.

Pero cuando empezás a enchufar modelos en pipelines reales, en workers de Docker, en sistemas que no tienen una persona mirando una terminal, la abstracción de Ollama puede estar resolviendo problemas que no tenés mientras agrega overhead que no querés.

La query de 40 segundos que bajé a 80ms con un índice compuesto me enseñó que la mayoría de las optimizaciones no son sobre usar otra herramienta — son sobre entender qué hace la herramienta que ya usás y cuándo esa abstracción te cuesta más de lo que te da.

Ollama es buenísimo. Seguí usándolo. Pero si estás construyendo algo en producción con LLMs locales, vale la pena entender qué hay abajo. Aunque lo que encontres te incomode un poco.

¿Estás corriendo LLMs locales en producción? ¿Con qué setup? Me interesa saber si alguien más llegó a la misma conclusión por otro camino.


Este artículo fue publicado originalmente en juanchi.dev

Top comments (0)