← Todos los artículos

ARQUITECTURA

La capa de serving para LLM locales: vLLM, SGLang, llama.cpp y Ollama

Una vez elegido el modelo, la primera decisión arquitectónica es qué capa de serving lo va a ejecutar. Este artículo separa cuatro capas no por listas de características, sino por su enfoque arquitectónico en planificación, memoria, reutilización y empaquetado.

Cuando una organización ya ha elegido el modelo que va a ejecutar, lo que tiene en la mano es un archivo: los pesos, un vocabulario y el formato de prompt que el modelo espera. Convertir ese archivo en un servicio es otro software distinto. Encola las peticiones que llegan, reparte la memoria de la GPU, decide cuántos usuarios pueden mantener una conversación a la vez y expone una superficie de API.

Este artículo separa cuatro nombres — vLLM, SGLang, llama.cpp y Ollama — no con una tabla de características, sino con su enfoque arquitectónico a lo largo de cuatro ejes: planificación, memoria, reutilización y empaquetado. No establece entre ellos un orden de velocidad, y cada cifra que aparece más abajo se cita con la fecha y la base de comparación de su propia fuente. Los cálculos del lado del archivo del modelo — niveles de cuantización, aritmética de VRAM y valores por defecto de la ventana de contexto — pertenecen a la elección del modelo y no se repiten aquí.

Qué hace la capa de serving y qué hace el archivo del modelo

Se separan dos responsabilidades. El archivo del modelo lleva los pesos, el vocabulario y el formato de prompt. La capa de serving asume la planificación, la gestión de memoria, la concurrencia y la superficie de API. La separación tiene una consecuencia concreta: el mismo archivo de modelo puede ejecutarse sobre las cuatro capas, pero cuántos usuarios reciben respuesta a la vez sobre el mismo hardware lo decide la capa de serving, no el archivo.

En la práctica, esa capa suele tomar la forma de un servidor HTTP: la documentación del servidor de llama.cpp describe su propia herramienta como un servidor HTTP ligero, en C/C++ puro, construido sobre httplib y nlohmann::json, que ofrece API REST y una interfaz web. Los sustratos también se solapan: el 15 de mayo de 2025 Ollama anunció su propio motor sobre la biblioteca de tensores GGML para modelos multimodales, y en la misma publicación indicó que hasta entonces se había apoyado en el proyecto llama.cpp para el soporte de modelos.

Enfoques de batching: estático, dinámico y continuo

La primera decisión que toma una capa de serving es cómo agrupar las peticiones que llegan a la vez.

  • Batch fijo: las peticiones se reúnen en un conjunto, se ejecutan como un todo y el siguiente batch no arranca hasta que todas las peticiones del anterior han terminado. Esta etiqueta es solo descriptiva; no es un término tomado de ninguna fuente.
  • Batching dinámico: el batch lo forma el servidor en tiempo de ejecución. Las peticiones esperan un instante y se despachan en cuanto se alcanza el tamaño preferido o se agota el margen de espera.
  • Batching continuo: el contenido del batch se decide de nuevo en cada iteración. Una petición terminada sale, y otra que esperaba entra en cuanto concluye la iteración en curso.

La forma documentada del enfoque intermedio está en NVIDIA Triton (véase Triton Inference Server — Dynamic Batcher). El batching dinámico combina peticiones individuales en un único batch en el momento que elige el servidor. La espera está acotada por max_queue_delay_microseconds, y la configuración de ejemplo usa 100 microsegundos.

La fuente primaria del tercer enfoque es Orca (USENIX OSDI ’22, pp. 521-538). El artículo enuncia primero el hallazgo: hasta ese momento los sistemas planificaban la ejecución con granularidad de petición y mantenían un conjunto fijo de peticiones hasta que todas las del batch habían terminado, de modo que una petición acabada pronto no podía volver al cliente y otra que llegaba después esperaba a que el batch en curso se vaciara por completo. La respuesta de Orca consiste en bajar la planificación a granularidad de iteración: el planificador selecciona las peticiones que se van a ejecutar, llama al motor para una única iteración y recoge los resultados. De ahí viene lo que hoy se llama continuous batching.

El artículo define además una segunda técnica: el batching selectivo. La siguiente iteración de dos peticiones no siempre se puede fusionar: si ambas están en la fase de iniciación con distinto número de tokens de entrada, o si cada una está en una fase, las formas de los tensores de atención no coinciden. Por eso el batching no se aplica a todas las operaciones, sino a las seleccionadas. Orca registra un rendimiento 36,9 veces mayor que el de NVIDIA FasterTransformer sobre GPT-3 175B con el mismo nivel de latencia: una medición de 2022, frente a la base de comparación de entonces.

Los términos no se delimitan igual en todos los documentos. La documentación del servidor de llama.cpp define el flag --cont-batching como «continuous batching (a.k.a dynamic batching)» y lo activa por defecto. La separación conceptual de arriba pertenece a la terminología de vLLM y SGLang.

PagedAttention: dividir la caché KV en páginas

En cuanto la planificación baja a granularidad de iteración, la gestión de memoria pasa a ser el factor decisivo. La razón está en el comportamiento de la caché KV: es grande para cada petición y crece y mengua a medida que avanza la generación. El artículo de PagedAttention lo dice de forma directa: ese comportamiento determina el tamaño del batch (véase Efficient Memory Management for Large Language Model Serving with PagedAttention, arXiv:2309.06180, SOSP ’23).

Lo que el artículo mide es la proporción: solo entre el 20,4 % y el 38,2 % de un bloque de memoria contigua reservado de antemano contiene estados de token reales, frente al 96,3 % de vLLM en la misma medición. El resto se va en huecos reservados para tokens futuros y en el espacio que produce reservar contra la longitud máxima de secuencia.

La respuesta viene de los sistemas operativos. PagedAttention divide la caché KV de una petición en bloques, cada uno con los vectores de clave y valor de un número fijo de tokens, y no exige que esos bloques sean contiguos en la memoria física. La analogía del artículo sostiene toda la sección: «one can think of blocks as pages, tokens as bytes, and requests as processes».

La contabilidad se deriva de ahí. Cada entrada de la tabla de bloques guarda un número de bloque físico y el número de huecos ocupados en ese bloque; la caché de una petición es una secuencia de bloques lógicos que se rellena de izquierda a derecha, y solo se reserva un bloque físico nuevo cuando los anteriores están llenos. Cada bloque físico lleva un contador de referencias y el copy-on-write se aplica con granularidad de bloque: dos salidas que comparten un prompt mantienen una sola copia del estado del prompt, y una escritura reserva un bloque nuevo.

Lo que aporta compartir está medido: en beam search, compartir bloques produce un ahorro de memoria de hasta el 55 %. El artículo registra un rendimiento entre 2 y 4 veces mayor que el de FasterTransformer y Orca con el mismo nivel de latencia: una medición de 2023 frente a las bases de comparación de entonces.

RadixAttention y caché de prefijos: reutilizar el contexto compartido

El tercer cambio de granularidad está en el lado de la reutilización. PagedAttention abarató compartir dentro de una misma petición; SGLang apunta a compartir entre peticiones (véase SGLang: Efficient Execution of Structured Language Model Programs, arXiv:2312.07104).

Lo que distingue a RadixAttention es sencillo: al terminar la generación conserva la caché en un árbol radix en lugar de descartarla. Un árbol radix es una forma del árbol de prefijos eficiente en espacio, cuyas aristas pueden etiquetarse con secuencias de longitud variable en vez de con un solo elemento. El árbol asocia secuencias de tokens a sus tensores de caché KV, y se almacenan en caché tanto los prompts como las generaciones.

Esto es una capa, no una alternativa. El artículo dice con claridad que RadixAttention es compatible con continuous batching, paged attention y paralelismo de tensores, y que, cuando no hay acierto de caché, la sobrecarga de memoria y de tiempo que introduce es insignificante.

El desalojo sigue una política LRU y empieza por las hojas: sale primero la hoja usada hace más tiempo, y los ancestros compartidos siguen siendo reutilizables hasta que ellos mismos pasan a ser hojas. No se reserva de antemano ningún pool de caché de tamaño fijo; los tokens en caché y las peticiones en curso comparten el mismo pool de memoria.

La planificación también tiene en cuenta la caché: las peticiones se ordenan por la longitud del prefijo coincidente. El teorema 3.1 establece que, cuando el tamaño de la caché no es menor que la longitud máxima de petición, ordenar primero por el prefijo compartido más largo equivale a un recorrido en profundidad del árbol radix y da la tasa de aciertos óptima.

Dónde compensa es concreto: prompts few-shot, historiales de chat de varios turnos y el contexto compartido de un pipeline RAG. En esos benchmarks la tasa de aciertos va del 50 % al 99 %, y la planificación consciente de la caché se acerca de media al 96 % de la tasa de aciertos óptima. En un despliegue de un mes en Chatbot Arena se observaron tasas de aciertos del 52,4 % para LLaVA-Next-34B y del 74,1 % para Vicuna-33B, y el tiempo medio hasta el primer token de Vicuna-33B se redujo 1,7 veces: una observación de 2024, específica de esa carga de trabajo.

vLLM ofrece hoy la misma función; la diferencia está en la estructura. En vLLM la identidad del bloque es por hash: el hash del bloque se forma con el hash del bloque padre y los tokens de ese bloque, solo se almacenan en caché los bloques completos, y un cache salt entra en el hash para separar cachés en entornos multiinquilino. El algoritmo de hash por defecto es sha256 desde la v0.11, y el prefix caching está activo por defecto. Del lado de SGLang, la misma función se construye con un árbol radix que desaloja las hojas por LRU: la misma función, otra estructura de datos.

GGUF y el binario único: empaquetado y modelo de operación

El cuarto eje se aparta de la planificación y la memoria: el empaquetado. GGUF es un formato de archivo para almacenar modelos destinados a la inferencia con GGML y con ejecutores basados en GGML. La especificación enumera sus objetivos de diseño en orden: despliegue en un solo archivo, ya que los archivos se distribuyen y se cargan con facilidad y no necesitan archivos externos; extensibilidad; compatibilidad con mmap; facilidad de uso; e información completa — todo lo necesario para cargar un modelo está en el archivo.

Ese último objetivo no es abstracto. Las claves generales de metadatos están definidas como general.architecture, general.quantization_version, general.file_type y general.alignment. El tokenizador viaja entero dentro del archivo: vocabulario, reglas de fusión, tipos de token e identificadores de los tokens especiales. La clave más llamativa es tokenizer.chat_template — una plantilla Jinja que describe el formato de entrada que el modelo espera, alojada en el mismo archivo que los pesos.

En un despliegue on-premise conviene distinguir dos «archivos únicos» distintos. El primero es el archivo GGUF único del modelo. El segundo es el propio ejecutor: el primer punto de la lista de características de llama.cpp es una implementación en C/C++ puro sin dependencias, y la unidad de distribución es un binario descargable, no una pila de contenedores.

La unidad de empaquetado de Ollama es el Modelfile, que la documentación describe como el plano para crear y compartir un modelo. Sus instrucciones son FROM, PARAMETER, TEMPLATE, SYSTEM, ADAPTER, LICENSE y MESSAGE; FROM acepta un nombre de modelo existente, un directorio Safetensors o la ruta a un archivo GGUF. La cuantización se ata al paso de creación, en la forma documentada ollama create --quantize q4_K_M, y el modelo se comparte después con ollama push y se ejecuta al otro lado con ollama run. Lo que significa un nivel de cuantización en el lado de la calidad y la memoria pertenece a la elección del modelo; aquí el tema es el empaquetado en sí.

Dónde convergió el ecosistema: el repositorio de TGI pasa a archivo

La señal más reciente de por qué estos cuatro nombres se mencionan juntos viene de un proyecto que se cerró. El repositorio Text Generation Inference de Hugging Face se archivó el 21 de marzo de 2026; la fecha figura en el banner de la página del repositorio. El README lleva un aviso de modo de mantenimiento.

Lo que importa es la segunda frase de ese mismo aviso. Dice que TGI inició el movimiento para que los motores de inferencia optimizados se apoyaran en las arquitecturas de modelo de transformers, y remite al lector por su nombre a vLLM, SGLang y a los motores de ejecución local con intercompatibilidad como llama.cpp o MLX. Tres de las cuatro capas de este artículo aparecen nombradas en el texto del propio proyecto que cierra.

La cronología es corta: la última versión etiquetada fue la v3.3.7, el 19 de diciembre de 2025. La lista de características del repositorio muestra además lo que se daba por estándar en esa fecha: paralelismo de tensores en varias GPU, continuous batching de las peticiones entrantes, una Messages API compatible con la OpenAI Chat Completion API y código de inferencia que usa Flash Attention y Paged Attention.

Qué capa encaja en cada escenario en una instalación on-premise

El par de cifras más concreto que separa estas capas por su postura operativa está en la documentación de Ollama. OLLAMA_NUM_PARALLEL es el número de peticiones simultáneas por modelo y vale 1 por defecto; OLLAMA_MAX_LOADED_MODELS es el número de modelos que pueden estar cargados a la vez y vale 3 por GPU por defecto. Esos valores por defecto describen una postura: poca concurrencia, muchos modelos.

Los valores por defecto de vLLM y SGLang describen lo contrario: un modelo, mucha concurrencia. vLLM planifica por orden de llegada; como los bloques de una secuencia se acceden juntos, el desalojo se aplica todo o nada. Admite paralelismo de tensores al estilo de Megatron-LM y, como cada fragmento del modelo procesa el mismo conjunto de tokens de entrada, dentro del planificador central se mantiene un único gestor de caché KV y un único mapeo de bloques lógicos a físicos.

SGLang se describe a sí mismo como un framework de serving que escala desde una sola GPU hasta grandes clústeres distribuidos. Los valores por defecto de sus argumentos de servidor confirman esa postura: caché radix activada, lru como política de desalojo, tamaño de página 1, tamaño de paralelismo de tensores 1.

La postura de llama.cpp es la instalación mínima: una lista de backends que va de BLAS a CUDA y de Metal a Vulkan, e inferencia híbrida CPU+GPU que acelera parcialmente modelos mayores que la capacidad total de VRAM. Del lado de Ollama, los directorios de modelos están en ubicaciones documentadas y se mueven con la variable de entorno OLLAMA_MODELS; allí donde la localidad de los datos es una condición escrita, esa ruta también forma parte del documento de despliegue.

La superficie de API estandariza el acceso al modelo; la capa que estandariza el acceso del modelo a los sistemas corporativos es otra distinta, y se trata en el artículo «Qué es un servidor MCP y qué no es».

El equivalente arquitectónico de la pregunta de quién puede ver qué contexto en una instalación compartida se desarrolla en el artículo «Control de acceso en RAG: quién puede ver qué».

El ritmo de versiones es rápido. A 8 de agosto de 2026, la versión etiquetada de vLLM es la v0.26.0 (27 de julio de 2026) y la de SGLang, la v0.5.17 (8 de agosto de 2026). Los nombres de flags y los valores por defecto de este artículo corresponden a esa misma fecha; contrástalos con la documentación de tu propia versión antes de desplegar.

Fuentes

  • Orca: A Distributed Serving System for Transformer-Based Generative Models — USENIX OSDI ’22, pp. 521-538 (planificación a nivel de iteración, batching selectivo, la medición de 36,9 veces)
  • Efficient Memory Management for Large Language Model Serving with PagedAttention — arXiv:2309.06180, SOSP ’23 (tabla de bloques, copy-on-write, proporción de estados de token)
  • SGLang: Efficient Execution of Structured Language Model Programs — arXiv:2312.07104 (RadixAttention, planificación consciente de la caché, teorema 3.1)
  • Documentación de NVIDIA Triton Inference Server — Dynamic Batcher (max_queue_delay_microseconds)
  • vLLM — repositorio de GitHub, README.md y vllm/config/cache.py (lista de características, valor por defecto del prefix caching)
  • Documentación de vLLM — Automatic Prefix Caching, Structured Outputs, OpenAI-Compatible Server, Parallelism and Scaling
  • SGLang — repositorio de GitHub y documentación, Server Arguments (caché radix por defecto, lru, tamaño de página 1)
  • Text Generation Inference — repositorio de GitHub (archivado el 21 de marzo de 2026; última versión v3.3.7)
  • GGUF Specification — ggml-org/ggml, docs/gguf.md (objetivos de diseño, tokenizer.chat_template)
  • llama.cpp — repositorio de GitHub y tools/server/README.md (implementación sin dependencias, tabla de backends)
  • Documentación de Ollama — Modelfile Reference, Import a model, OpenAI compatibility y FAQ (valores por defecto de concurrencia, directorios de modelos)
  • Ollama’s new engine for multimodal models — Ollama Blog, 15 de mayo de 2025
  • GitHub Releases — vllm-project/vllm v0.26.0 y sgl-project/sglang v0.5.17

Más artículos

Tienes un sistema que construir, o uno que llevar más lejos.

No deck needed. 20 minutes. The rest is up to you.