Como vimos en el post anterior, Ollama nos permite interactuar con LLMs de una forma sencilla y transparente. Mucho más allá de realizar ciertas pruebas sencillas o iniciarnos en el mundo de los LLMs para comprobar su funcionamiento, en el mundo de desarrollo software se suelen necesitar opciones más avanzadas y personalizadas.

En este post exploraremos estas opciones avanzadas que nos ofrece Ollama para poder personalizar nuestros modelos, así como hacer uso de su API y poder sacarle todo el partido para crear aplicaciones de IA.

Conceptos previos

En el post anterior comentamos algunos aspectos más teóricos para la ejecución de los LLMs en local a través de Ollama. Al avanzar a casos de uso más complejos, también es necesario tener en cuenta nuevos conceptos.

Hay que aclarar que estos conceptos teóricos son transversales a los LLMs, independientemente de las herramientas que se analizan en los posts en las que puede que no siempre sea posible una implementación directa de dicho concepto (por ejemplo: Ollama no entrena modelos sino que aplica adaptadores LoRA/QLoRA para por ejemplo aplicar fine-tuning a un modelo).

Estos conceptos son:

Fine-tunning

El fine-tuning es el proceso en el que se adapta un modelo previamente entrenado a una tarea específica. Esto se realiza a través de un conjunto de datos ligados a esta tarea concreta, que podrán actualizar de múltiples formas (total o parcialmente) los pesos de la red neuronal. Este proceso permite adaptar un modelo a una tarea concreta para la que no ha sido entrenado (en la mayoría de las ocasiones los modelos se entrenan para un uso genérico). Esto mejora el desempeño del modelo en dicha tarea, sin necesidad de reentrenarlo desde el principio, lo que lleva a un ahorro de tiempo y recursos.

Como se comentó en el post anterior, el funcionamiento de los LLMs consiste en predecir la siguiente palabra en un contexto. Sin una guía muy concreta a través de prompts (prompt engineering), un LLM que no haya sido “tuneado” podría producir una respuesta coherente aunque no adecuada a la intención del usuario.

Por ejemplo, si se le solicita algo como: “¿Cómo puedo hacer un currículo?” un LLM podría responder “Usando Microsoft Word”. Esta respuesta es completamente válida pero no está alineada con lo que el usuario necesita, a pesar de que el propio LLM puede tener un conocimiento extenso sobre la creación de currículos.

El fine-tuning es especialmente útil para personalizar el estilo de un LLM para responder a los usuarios de una forma en concreto, por ejemplo en un chatbot, o para agregar conocimientos específicos de un dominio (por ejemplo, vocabulario especializado con el que no ha sido entrenado el modelo de origen), como en entornos legales, financieros o de salud.

Algunos tipos de fine-tuning que existen son:

  • Completo: se trataría de actualizar los pesos de toda la red neuronal. Es parecido al propio entrenamiento inicial del modelo, diferenciándose de él en el estado inicial de los pesos y el conjunto de datos usado.
  • Parcial: o también llamado selectivo, en el que generalmente se actualizan solo una parte de los pesos más críticos relacionados con las tareas a realizar posteriormente.
  • Aditivo: en lugar de modificar los pesos existentes, este método añade más pesos o capas de neuronas al modelo. Uno de los métodos que más de moda está entre la comunidad del fine-tuning pertenece a este tipo y es LoRa.

Otra catalogación de tipos de fine-tuning puede ser en base a si está supervisado por humanos (SFT o Supervised Fine-Tuning), semi supervisado o no supervisado.

Formatos de ficheros: Safetensors y GGUF

Partiendo de la base de que un tensor es un objeto matemático (básicamente un array que puede tener múltiples dimensiones) para la representación de datos, Safetensor es un formato de fichero desarrollado por Hugging Face que almacena dichos tensors de forma segura. A diferencia de otros formatos, Safetensor está habilitado en un formato de solo lectura (evitando la ejecución de código no deseado) y diseñado para la eficiencia y portabilidad de datos.

Scalar, vector, matriz, tensor

Un ejemplo de lo que es un tensor si lo trasladamos al mundo real, podría ser:

  • Scalar (tensor de dimensión 0): el valor de la intensidad de un pixel en una imagen en escala de grises.
  • Vector (tensor de dimensión 1): para una imagen en escala de grises, una línea de pixels puede ser representada como un vector.
  • Matriz (tensor de dimensión 2): una imagen en escala de grises entera se puede representar como una matriz.
  • Tensor de 3 dimensiones: una imagen a color. En una imagen a color, cada pixel no solo tiene un valor para la intensidad, tiene tres (RGB). En este caso, se puede pensar la imagen a color como tres matrices apiladas juntas, una para cada color.

Entre otros formatos de ficheros, también se puede encontrar GGUF (GPT-Generated Unified Format), que posibilita la gestión de LLMs de forma optimizada. Este formato permite, además de codificar los tensors, incluir un conjunto de metadatos y dar soporte a diversos tipos de cuantización y fine-tuning.

GGUF está pensado para la extensibilidad y versatilidad, pudiendo incorporar nueva información sin romper la compatibilidad con los modelos antiguos además de los que puedan venir en el futuro.

Modelo GGUF
Modelo GGUF

Parámetros del modelo

Así como las aplicaciones tienen parámetros comunes que son configurables según ciertos escenarios (multidivisa, multiidioma), los modelos también contienen parámetros similares.

Más allá de que cada modelo puede implementar múltiples parámetros concretos, existen algunos comunes y que tal vez sean los más relevantes de cara a su optimización. Entre estos parámetros se encuentran:

  • temperature: permite escalar la distribución de probabilidades para las salidas de un LLM, es decir, controla la “creatividad” del modelo. Valores más bajos hacen que el modelo actúe de forma más determinista y predecible, mientras que valores más altos producen salidas más aleatorias o “creativas”. Los valores típicamente están entre 0 y 1, encontrándose con valores por defecto en 0.7 o 0.8 según el modelo.
  • top_p: selecciona las opciones para la salida cuyas probabilidades sumen, al menos, la probabilidad acumulada indicada. Posibles valores entre 0 y 1.
  • top_k: selecciona las k mejores opciones de salida del LLM, ordenadas según las probabilidades de cada una.
  • num_predict: número máximo de tokens que generará el modelo en una sola respuesta. El valor “-1” permite responder con un número ilimitado de tokens hasta que se encuentre una condición de parada.
  • stop: una lista de textos que, en el caso de que el modelo genere alguno de ellos en su salida, parará de generar salidas inmediatamente. Útil para asegurar que el modelo deje de responder en un punto deseado.
  • num_ctx: parámetro que define el tamaño de la ventana de contexto (context-window) del modelo. Como es uno de los más importantes, incluso dentro de estos parámetros, se detalla mejor en una sección propia a continuación.

Los anteriores parámetros tal vez sean los más relevantes, pero existen más, aunque se debe tener en cuenta que parece que no todos se pueden configurar según las distintas opciones disponibles de interacción con Ollama (API, cli, Modelfile).

Longitud de contexto

Este es uno de los parámetros a personalizar en los modelos más importantes. Se trata del número máximo de tokens que el modelo puede procesar al mismo tiempo. Hay que indicar que este número de tokens comprende:

  • Tokens de entrada del usuario: el prompt con las preguntas o peticiones del usuario.
  • Tokens de memoria o históricos de la conversación del usuario: en la típica interacción en forma de chat con un modelo, los usuarios realizan múltiples preguntas que, en muchos casos, están relacionadas. Ollama suele guardar en la memoria las preguntas-respuestas anteriores para poder responder de forma adecuada a nuevas preguntas.
  • Tokens de salida: las respuestas producidas por el modelo.
  • Tokens que componen el prompt del sistema: tokens en el que se le indica al LLM cómo debe actuar y responder. Un ejemplo de prompt de sistema sencillo sería: “Eres un agente de viajes que responde con recomendaciones de ciudades a las que viajar en base a las preferencias recibidas. Responde indicando por qué recomiendas esas ciudades y ordénalas en una lista según sea más recomendable viajar a ellas o no”.

La longitud del contexto es crítica, puesto que afectará directamente a la calidad de la respuesta del modelo. Por ejemplo, en conversaciones largas o en las que se aporta una cantidad muy grande de información (adjuntando documentos), si se supera la longitud del contexto del modelo, empezará a responder de una forma errática (puede responder con repeticiones de respuestas en una conversación de varias preguntas-respuestas o no responder a lo que se pregunta porque no tiene toda la información disponible).

Aunque sea un parámetro configurable, los LLMs vienen preentrenados con una longitud de contexto máxima (se pueden ver valores como 2048, 4096, 8192, …, 128K, aunque también existen modelos con contextos de hasta ¡2 millones! de tokens como Gemini 1.5 Pro).

Configurar este parámetro más allá de los valores preentrenados para cada modelo también producirá respuestas incoherentes, además de que valores altos afectarán al consumo de recursos de la máquina como la RAM, pudiendo ocasionar problemas de rendimiento.

Este parámetro tiene múltiples formas de cambiarlo en cada modelo:

  • Cli: cuando se ejecuta el comando ollama run mediante el comando interactivo /set parameter num_ctx < value >.
Longitud de contexto: cli
  • API: en la petición correspondiente mediante el campo options:
curl http://localhost:11434/api/generate -d '{
  "model": "llama3.2:1b",
  "prompt": "¿A que velocidad gira la Tierra?",
  "options": {
    "num_ctx": 1024
   }
}'
Longitud de contexto: api
  • Modelfile: esta forma se introducirá más adelante, pero simplemente se debe informar la instrucción: PARAMETER num_ctx < value > en el Modelfile.

API

Además de los comandos interactivos, Ollama proporciona un API para poder interactuar con los LLMs. Este API es útil sobre todo en casos en los que se quiera realizar una integración con otras aplicaciones (como por ejemplo desde Spring AI), puesto que lo único que habría que hacer es realizar una llamada al endpoint Rest correspondiente.

Por defecto, el API está expuesta en el puerto 11434 (http://localhost:11434 o http://127.0.0.1:11434), aunque es configurable. Los endpoints que expone el API son:

  • /api/generate: método POST. Endpoint para obtener respuestas del LLM con un único prompt que no sea con una secuencia de pregunta-respuesta. Óptimo para tareas como predicción de textos o traducción.
Ollama api generate
  • /api/chat: método POST. Endpoint para interacción pregunta-respuesta conversacional.
Ollama api chat
  • /api/embeddings: método POST. Genera embeddings (representaciones vectoriales numéricas) del input.
Ollama api embeddings
  • /api/tags: método GET. Equivalente al comando interactivo ollama list con el que se muestra un listado de modelos existentes en el sistema.
Ollama api tags
  • /api/show: método POST. Equivalente al comando interactivo ollama show con el que se obtiene información sobre un modelo concreto como parámetros, plantillas, licencias, etc.
Ollama api show
  • /api/delete: método DELETE. Equivalente al comando interactivo ollama rm para eliminar un modelo del sistema local.
ollama api delete
  • /api/pull: método POST. Equivalente al comando interactivo ollama pull para descargar un modelo desde el repositorio remoto al sistema local.
ollama api pul
  • /api/create: método POST. Equivalente al comando interactivo ollama create con el que se crea un nuevo modelo personalizado a partir de un Modelfile.
ollama api create
  • /api/copy: método POST. Equivalente al comando interactivo ollama cp para copiar un modelo en el sistema local.
ollama api copy
  • /api/push: método POST. Endpoint para subir un modelo al repositorio remoto.

Compatibilidad con API OpenAI

Como es bien conocido, OpenAI es uno de los primeros y más importantes impulsores de esta nueva ola de IA-LLMs, por lo que es lógico pensar que muchas aplicaciones hacen uso de sus APIs. Para mantener las integraciones con el resto de aplicaciones de la forma más sencilla posible, Ollama añade una capa para que las APIs sean compatibles con el formato de OpenAI.

En base, lo que sucede es que, además de los endpoints comentados anteriormente, se exponen otros sobre el path /v1/ (http://localhost:11434/v1/) con el formato del API de OpenAI. Algunos de los endpoints compatibles actualmente disponibles son:

  • /v1/chat/completions: sería el equivalente al endpoint /api/chat.
v1 chat completions
  • /v1/embeddings: sería el equivalente al endpoint /api/embeddings.
v1 embeddings
  • /v1/models: sería el equivalente al endpoint /api/tags.
v1 models

Con esto en mente, muchas de las librerías que se integran con el API de OpenAI solo necesitarían adaptar algunos parámetros de configuración para usar Ollama. Algunos de estos parámetros son:

  • base_url: simplemente indicar el endpoint v1 de Ollama: http://localhost:11434/v1/
  • api_key: simplemente informar cualquier valor no vacío, si bien es cierto que dependiendo de la librería, podría ser necesario indicar algún valor más específico como “nokey” o el correspondiente. En Ollama en realidad no se valida este parámetro.

Modelfiles

Ya en las primeras etapas de Ollama se estaba pensando en la modularidad y extensibilidad (en el equipo fundador de Ollama ha participado gente que había trabajado previamente en Docker). Es por esto que existen los Modelfiles, que no son más que ficheros de configuración que actúan como receta para la definición por capas de los modelos en Ollama. Este fichero sería una combinación de los pesos del modelo en formato GGUF, la plantilla, el system prompt y otras instrucciones.

Esta información está dividida en distintas instrucciones en el Modelfile (como lo está un Dockerfile), siendo las siguientes algunas de las más importantes:

  • FROM: instrucción obligatoria. Indica la base sobre la que construir el modelo. Existen varias formas de definición:
    • Modelo existente: referencia a un modelo existente en local o en el registro oficial de Ollama. Por ejemplo: FROM llama3.2:1b.
    • Pesos de modelo en formato Safetensors: ruta relativa o absoluta en donde se encuentran los ficheros de pesos y configuración necesarios. Indicar que por el momento de esta forma solo estarían soportados Safetensors con arquitecturas llama, mistral, gemma o phi3. Un ejemplo de instrucción sería: FROM ./models/llama_safetensors/.
    • Modelo en formato GGUF: ruta relativa o absoluta en donde se encuentra el fichero GGUF. Por ejemplo: FROM ./models/llama-3-2.gguf.
  • PARAMETER: instrucción que configura el valor del parámetro indicado de forma predeterminada para la ejecución del modelo. Por ejemplo: PARAMETER temperature 0.3; PARAMETER num_ctx 8192. Algunos de los parámetros más importantes a configurar son:
Parámetro Descripción Tipo Ejemplo
num_ctx Define el tamaño de la ventana de contexto int 4096
temperature Valor indicativo de la “creatividad” del modelo float 0.6
stop Posibles marcas de stop para el ciclo del LLM. Cuando el LLM encuentre este patrón, parará y devolverá la salida. string "AI assistant:"
top_k Filtrado de opciones de salida a las k con más probabilidades int 40
top_p Filtrado de opciones de salida con probabilidades acumuladas float 0.9
  • TEMPLATE: instrucción para definir el formato de estructuración del prompt de entrada para el LLM. Es una instrucción muy importante para modelos de chat o instrucciones, puesto que esta plantilla indica la delimitación de mensajes de sistema, turnos de usuario, turnos de asistente, etc. para el buen funcionamiento del ciclo pregunta-respuesta. Se escriben en la sintaxis de template de Go. Las variables clave serían:
Variable Descripción
{{ .System }} El prompt del sistema para configurar su comportamiento.
{{ .Prompt }} El prompt del usuario
{{ .Response }} La respuesta del LLM

Un ejemplo de instrucción template podría ser:

TEMPLATE """{{ if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}{{ if .Prompt }}<|im_start|>user
{{ .Prompt }}<|im_end|>
{{ end }}<|im_start|>assistant
"""
  • SYSTEM: configura un prompt de sistema por defecto. Con esto se consigue definir cómo se tiene que comportar el LLM (agente de viajes, agente financiero, profesor/a de matemáticas, etc). Un ejemplo sería:
SYSTEM """ Eres un agente de viajes que proporciona información útil como leyes, precios, lugares a visitar importantes, sitios de comida, alojamientos y demás información turística importante. """
  • ADAPTER: a través de una ruta de ficheros permite indicar un adaptador LoRa o QLoRa a la base del modelo indicado en la instrucción FROM. Es importante mencionar que el modelo base de la instrucción FROM debe ser el mismo que el modelo base sobre el que se creó el adaptador puesto que, sino, se podrían tener comportamientos erráticos. La ruta puede apuntar a:
    • Un único fichero en formato GGUF: modelo-lora.gguf.
    • Una carpeta en donde se encuentran los pesos en formato Safetensor: adaptador-modelo.safetensors, adaptador-config.json.
ADAPTER ./llama3.2-lora.gguf
  • LICENSE: concreta la licencia sobre la que se distribuye el modelo. Por ejemplo:
LICENSE """ MIT License """
  • MESSAGE: instrucción con ejemplos de turnos de conversación para que el LLM pueda seguir un estilo o formato en concreto. Los roles posibles son:
    • System: una forma alternativa de indicar el prompt del sistema.
    • User: ejemplos de lo que puede preguntar el usuario.
    • Assistant: ejemplos de cómo debe responder el modelo.

Un ejemplo de instrucciones message podría ser:

MESSAGE user ¿Está Madrid en España?
MESSAGE assistant si
MESSAGE user ¿Está Paris en España?
MESSAGE assistant no
MESSAGE user ¿Está Barcelona en España?
MESSAGE assistant si

Hay que indicar que el Modelfile no es case sensitive, simplemente las instrucciones se indican con mayúsculas por claridad y, además, las instrucciones no tienen por qué seguir un orden estricto (igualmente se indica la instrucción FROM de primera por claridad).

Teniendo en cuenta lo descrito anteriormente, un ejemplo sencillo de Modelfile podría ser:

FROM llama3.2:1b
PARAMETER temperature 0.3
PARAMETER num_ctx 8192
SYSTEM Eres un agente de viajes que proporciona información útil como leyes, precios, lugares a visitar importantes, sitios de comida, alojamientos y demás información turística importante.

Después de guardar dicho fichero, se ejecutaría el siguiente comando para su creación:

ollama create custom-model -f ./ModelfileCustom

Una vez creado, se puede comprobar que ya se lista junto al resto de modelos, pudiendo interactuar con él:

ejemplo modelfile

Importando modelos externos

Ollama permite la ejecución de modelos externos a su hub de modelos que sean distribuidos en formatos estándar como GGUF o Safetensors, o la inclusión de adaptadores para el fine-tuning. Algunos ejemplos de este proceso son:

GGUF

  1. Descargar el fichero en formato .gguf. Se puede encontrar un ejemplo en este enlace.
  2. Generar el Modelfile con la referencia al fichero descargado.
FROM ./external-models/gguf/llama-3.2-1b-instruct-q4_k_m.gguf
PARAMETER num_ctx 4096
SYSTEM "Eres un chatbot que sólo cuenta chistes"
  1. Crear el modelo para posteriormente poder ejecutarlo:
ollama create custom-gguf-model -f ./Modelfile_gguf
importar modelo externo gguf

Safetensors

  1. Descargar los ficheros necesarios en la misma carpeta. Se puede encontrar un ejemplo en este enlace.
  2. Generar el Modelfile con la referencia a la ruta en donde se encuentran los ficheros.
FROM ./external-models/safetensors/llama3_2_instruct_st/
PARAMETER num_ctx 4096
PARAMETER temperature 0.7
  1. Crear el modelo para posteriormente poder ejecutarlo:
ollama create custom-st-model -f ./modelfile

Adaptadores

  1. Descargar los ficheros necesarios en la misma carpeta. Se puede encontrar un ejemplo en este enlace.
  2. Generar el Modelfile con la referencia a la ruta en donde se encuentran los ficheros.
FROM llama-3.2-3b-instruct-bnb-4bit 
ADAPTER ./adapters/llama3_2_lora/llama3.2_LoRA_Spanish.gguf
PARAMETER temperature 0.5
SYSTEM "Ahora respondes en el estilo enseñado por el LoRA."
  1. Crear el modelo para posteriormente poder ejecutarlo:
ollama create custom-lora-model -f ./modelfile

Cuantización con Ollama

Ollama también incluye la opción de poder cuantizar (reducción de la precisión numérica en los pesos del modelo) un modelo en el momento de su creación. Básicamente consiste en indicar el parámetro -q (o –quantize) en el comando create de la siguiente forma:

ollama create llama-quantized -f ./Modelfile -q q4_K_M

En base al siguiente Modelfile, se puede comprobar el resultado de la cuantización (se puede observar como el modelo resultante tiene un tamaño menor):

FROM llama3.2:1b-instruct-fp16
PARAMETER temperature 0.9
SYSTEM """ Eres un agente de viajes que te llamas Mr Travel"""
cuantización ollama ejemplo

Realmente, por debajo se utilizarán las instrucciones de cuantización de la librería llama.cpp para realizar este proceso, guardando el modelo con el nombre correspondiente. Algunas de las opciones posibles de cuantización son: q4_0, q5_0, q6_K, q8_0, q4_K_S, q4_K_M, etc.

Conclusiones

En este post se han visto opciones de interacción con Ollama más avanzadas. Se ha probado el funcionamiento del API que expone Ollama, así como ciertos parámetros y configuraciones importantes para un buen rendimiento de los modelos.

También se ha podido probar una de las funcionalidades más importantes de Ollama: personalizar los modelos para que podamos adaptarlo lo mejor posible a nuestro caso de uso y poder tener una mejor experiencia de usuario en nuestras aplicaciones de IA.

En los siguientes posts introduciremos otras herramientas de ejecución de LLMS en local: LM Studio, Llamafile y Docker.

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.