latentSource

Ejecutar Ollama correctamente: Modelos locales y tus propios fine-tunes en una sola GPU

Ollama es la forma menos complicada de servir modelos locales en una sola GPU, pero sus valores predeterminados están ajustados para demos, no para producción. Analizamos lo que realmente importa —quantization, memoria de contexto, keep-alive— y cómo ejecutar tu propio modelo con fine-tuning tras la misma API.

·6 min read
Compartir
Ejecutar Ollama correctamente: Modelos locales y tus propios fine-tunes en una sola GPU

La mayor parte de lo que ejecuto contra LLMs en el día a día va a APIs alojadas, y por una buena razón: ninguna GPU doméstica compite con un centro de datos. Pero hay un tipo de trabajo donde lo local gana: cualquier cosa que toque datos que no deban salir del edificio, cualquier cosa que quieras procesar en un bucle sin mirar el medidor de facturación, y cualquier cosa construida sobre un modelo al que le hayas hecho un fine-tuning tú mismo. Para ese tipo de tareas, en una sola GPU NVIDIA de consumo, Ollama es la herramienta que elijo. No porque sea el stack de servicio más rápido —no lo es—, sino porque hace que la parte operativa sea aburrida, y lo aburrido es lo que buscas en la infraestructura.

El truco es que los valores predeterminados de Ollama están ajustados para la experiencia de demo: escribes ollama run, obtienes tokens, te sientes bien. Ejecutarlo como una pieza de infraestructura de la que tus aplicaciones realmente dependan requiere un puñado de decisiones deliberadas. Aquí es donde pondría la atención.

Elige la quantization como eliges un índice

Cada modelo en el registro de Ollama viene en múltiples quantizations, y el tag que elijas (pull) importa más que casi cualquier otra cosa que configures. El compromiso tiene la misma forma que cualquier decisión de almacenamiento: los tipos más pequeños te dan capacidad y velocidad a costa de la precisión, y la pregunta es si la precisión que sacrificaste servía para algo útil.

Para la mayoría de los trabajos, 4-bit es la elección correcta; la variante q4_K_M de GGUF se ha convertido en el estándar de la comunidad porque está en el punto donde la pérdida de calidad es difícil de notar en el uso normal, pero el ahorro de memoria es enorme. Como regla general, un modelo de 4-bit necesita aproximadamente medio gigabyte de VRAM por cada mil millones de parámetros para los weights, por lo que un modelo de 8B cabe cómodamente en una tarjeta de gama media y un modelo de clase 30B se vuelve viable en una de 24 GB. Saltar a 8-bit casi duplica esa huella para una mejora de calidad que difícilmente medirás en la mayoría de las tareas; bajar de 4-bit empieza a mostrar una degradación real, especialmente en razonamiento y código. Mi regla: q4_K_M por defecto, q8_0 solo cuando he visto un fallo real que pueda atribuir a la quantization, lo cual es raro.

Lo que la gente olvida es que los weights no son toda la cuenta. El KV cache —la memoria que guarda tu ventana de contexto— crece con la longitud del contexto, y en contextos largos puede rivalizar con los weights mismos. Lo que nos lleva a los ajustes.

Los tres ajustes que importan

Primero, la longitud del contexto. Ollama viene con un contexto predeterminado pequeño —unos pocos miles de tokens— y truncará feliz y silenciosamente todo lo que lo supere. Si estás haciendo RAG o trabajo con agentes, ese silencio es veneno: tus pasajes recuperados se salen de la ventana y el modelo responde basado en "vibes". Configura num_ctx deliberadamente, ya sea por modelo en un Modelfile o por solicitud a través de la API. Pero hazlo teniendo en cuenta la cuenta de VRAM, porque un contexto grande en un modelo grande puede empujar el KV cache más allá de lo que soporta tu tarjeta, y Ollama empezará a dividir el modelo entre GPU y CPU. ollama ps te indica la división. Si ves algo que no sea 100% GPU, tus tokens por segundo acaban de caer en picado y deberías reducir el contexto o el modelo.

Segundo, keep_alive. Por defecto, un modelo se descarga cinco minutos después de la última solicitud, y cargar varios gigabytes de weights de nuevo en la VRAM no es instantáneo. Para una aplicación interactiva, un arranque en frío en medio de una sesión se interpreta como "el sitio no funciona". Yo configuro OLLAMA_KEEP_ALIVE con un valor largo —o en -1 para fijar el modelo— en cualquier máquina cuyo trabajo sea servir un solo modelo. La RAM ya está pagada; úsala.

Tercero, la concurrencia. OLLAMA_NUM_PARALLEL controla cuántas solicitudes sirve un modelo cargado a la vez, y OLLAMA_MAX_LOADED_MODELS controla cuántos modelos comparten la tarjeta. Ambos tienen valores predeterminados bajos, y en una sola GPU eso es correcto: cada slot paralelo multiplica el KV cache, y cada modelo extra cargado es otro conjunto de weights residentes. Resiste el impulso de aumentarlos. Si realmente necesitas un alto rendimiento concurrente (throughput), ya has superado la herramienta: ese es territorio de vLLM, con continuous batching y PagedAttention, y se gana su peso operativo extra a esa escala. Una GPU, un puñado de usuarios, modelos mixtos: Ollama. Muchos usuarios, un modelo, throughput como KPI: vLLM.

Ejecuta el servidor bajo systemd o Docker, no importa mucho, pero trata las variables de entorno como configuración bajo control de versiones, no como algo que exportaste en una shell una vez y olvidaste. Seis meses después, la pregunta de "¿por qué staging se comporta diferente?" siempre se responde con una variable de entorno.

Tu propio fine-tune, servido como cualquier otro modelo

Esta es la parte que justifica toda la configuración local, porque las APIs alojadas sirven sus modelos, no los tuyos. Ollama servirá cualquier cosa que puedas convertir a GGUF, lo que significa que un fine-tune que entrenaste tú mismo encaja junto a los modelos listos para usar tras la misma API.

El camino depende de lo que haya producido tu entrenamiento. Si hiciste un fine-tune completo y tienes weights en safetensors, los conviertes con el script de conversión de llama.cpp, haces la quantization del resultado a tu precisión de servicio y lo envuelves en un Modelfile. Si hiciste un LoRA —que en una sola GPU de consumo es casi seguro lo que hiciste, ya que el entrenamiento eficiente en parámetros es el único que cabe—, mantienes el adapter por separado y dejas que el Modelfile lo combine con la base:

FROM llama3.1:8b
ADAPTER ./my-lora-adapter.gguf
PARAMETER num_ctx 8192
PARAMETER temperature 0.3
SYSTEM You classify support tickets. Answer with the category only.

Luego ollama create tickets -f Modelfile y ya es un modelo de primera clase: ollama run tickets, la misma API REST, el mismo ciclo de vida que todo lo demás. El Modelfile es la pieza que más elogio de todo el sistema: es un Dockerfile para la configuración de modelos. Weights base, adapter, parámetros de decodificación, plantilla de chat, system prompt, todo en un pequeño archivo de texto que puedes comparar (diff) y registrar en git. La alternativa —dispersar esas decisiones en el código de la aplicación, donde cada llamador establece su propia temperatura— es como obtienes comportamientos que nadie puede reproducir.

Una advertencia basada en la experiencia con las guerras de formatos: asegúrate de que la plantilla de chat en tu Modelfile coincida con la que se usó para el fine-tune del modelo. Una plantilla desajustada es el clásico fallo silencioso: sin errores, sin caídas, solo un modelo que parece extrañamente más tonto de lo que era en tus evaluaciones de entrenamiento, porque los tokens del prompt están estructurados de forma diferente a lo que vio en el entrenamiento.

Y antes de hacer un fine-tune, sé honesto sobre si realmente lo necesitas. Un fine-tune fija un comportamiento: un formato, un tono, una habilidad de clasificación específica. No añade conocimiento de manera confiable y se queda obsoleto en el momento en que tus datos cambian. Si el problema es "el modelo no conoce mis documentos", eso es recuperación (retrieval), no entrenamiento; ya he escrito antes sobre esa distinción y sigo llegando a la misma conclusión. Los fine-tunes que han valido la pena para mí son los pequeños y aburridos: formatos de salida estrictos y clasificadores específicos, donde un modelo de 8B ajustado supera a un modelo con prompts tres veces más grande y se ejecuta en hardware propio.

Conéctalo a través del endpoint compatible con OpenAI

Ollama expone una API compatible con OpenAI en /v1 junto con la suya nativa, y ese es el punto de integración en el que yo estandarizaría. Apunta el SDK de OpenAI a http://localhost:11434/v1, establece el nombre del modelo y listo: cualquier base de código escrita para la API alojada ahora puede hablar con tu GPU con un cambio de una sola línea en la URL base. Eso convierte la decisión entre local o alojado en una cuestión de enrutamiento en lugar de una reescritura: el trabajo masivo barato y los datos privados van al modelo local, el razonamiento difícil va a un modelo de frontera (frontier model), y el código de la aplicación no nota la diferencia.

Esa es toda la propuesta, en realidad. El trabajo de Ollama es hacer que un modelo local —incluyendo uno que lleve tu propio adapter— parezca simplemente otro endpoint de API, en hardware que tú controlas, con un archivo de configuración que puedes leer. Haz bien la quantization, dimensiona el contexto contra tu VRAM con honestidad, fija el modelo en memoria y pon el Modelfile en git. Nada de esto es glamuroso. Es la misma disciplina de ejecutar cualquier otro servicio con estado, aplicada a un stack que la mayoría de la gente todavía trata como un juguete.