Multimodal on-premise: servir un VLM con vLLM (visión + lenguaje)
Tercera tanda de una serie operativa sobre exprimir un cluster LLM on-premise genérico de 4×H100 SXM. Sus piezas hermanas en esta tanda son FinOps y multi-tenancy de GPU con LiteLLM —que pone precio a cada token, y un token visual cuesta lo mismo que uno de texto— y Acelerar el cold start con Tensorizer —que importa el doble aquí, porque un VLM carga dos modelos (encoder de visión y LLM)—. Si el VLM acaba alimentando un asistente documental, ese montaje end-to-end con LibreChat y RAG es otra historia (en preparación).
TL;DR
Un modelo de visión-lenguaje (VLM) no es magia: es un LLM normal al que se le ha conectado un órgano sensorial delante. Tres piezas en serie: un encoder de visión (un ViT) que mira la imagen, un proyector o conector (típicamente un MLP) que traduce la salida del encoder al espacio de embeddings del LLM, y el LLM de siempre, que recibe esos embeddings como si fueran tokens más. La consecuencia que gobierna todo el diseño operativo es brutal en su simplicidad: una imagen se convierte en tokens visuales, y esos tokens cuestan exactamente lo mismo que tokens de texto. Una página de documento a alta resolución no son “un par de tokens de imagen”: son cientos o miles de tokens que entran al contexto, inflan el prefill (compute-bound) y ocupan KV-cache (memory-bound) igual que si hubieras pegado mil palabras. En Qwen2.5-VL la cuenta es literal: el número de tokens visuales es $H\times W / (14\times14\times4)$, de modo que una imagen de $896\times896$ son 1024 tokens y una página A4 escaneada a resolución decente se va fácil a 1500–2500 tokens solo de imagen. Este post abre la anatomía, trabaja las matemáticas del coste en tokens y las traduce a TTFT y a VRAM sobre una H100 80GB, explica el soporte multimodal de vLLM (--limit-mm-per-prompt, presupuesto de píxeles vía mm_processor_kwargs, chat template) y responde a la pregunta que de verdad importa: OCR clásico o VLM. La respuesta corta: usa Tesseract/PaddleOCR para el grueso de documentos con texto limpio, y reserva el VLM —caro en tokens y en GPU— para el subconjunto que de verdad lo necesita: layout complejo, tablas anidadas, manuscritos, sellos, gráficos. Sobre el cluster genérico 4×H100 SXM, con FP8 para que entre.
La analogía: el lector que ve la página frente al que solo oye la transcripción
Imagina dos peritos a los que les pides revisar un contrato escaneado. Al primero le lees el contrato en voz alta —una transcripción a ciegas, palabra por palabra—. Es rápido, eficiente, y para el 90 % de las cláusulas le basta: el texto es el texto. Pero hay cosas que no le puedes leer: que la firma del final está manuscrita y no coincide con el nombre mecanografiado; que hay un sello de “PAGADO” estampado en diagonal sobre el importe; que la tabla de la página 3 tiene una celda combinada que cambia a quién aplica cada fila; que en el margen hay una anotación a bolígrafo. La transcripción a ciegas pierde todo eso o lo aplana a un galimatías.
Al segundo perito le das la página entera, para que la mire. Ve la firma, ve el sello, entiende la estructura de la tabla porque percibe las líneas y las celdas, lee la anotación al margen. Capta el documento como un objeto visual con layout, no como un río de caracteres. Es estrictamente más capaz.
¿Por qué no usar siempre al segundo, entonces? Porque ver cuesta más ancho de banda mental. Al primer perito le entra el contrato como un flujo de palabras: barato, lineal. Al segundo le entra como una imagen que su cerebro tiene que parcelar, atender región por región, y reconstruir. Procesa muchísima más información por página —la mayoría de la cual, en un documento de texto limpio, es redundante con lo que la transcripción ya le habría dado—. Si solo necesitas leer las cláusulas, contratar al perito-que-ve para cada página es pagar de más por capacidad que no usas.
Esa es la tesis entera del post. El VLM es el perito que ve; el OCR es la transcripción a ciegas; y “el ancho de banda mental” son tokens. La ingeniería consiste en mandar al perito-que-ve solo las páginas donde ver cambia la respuesta, y resolver el resto con la transcripción barata.
Anatomía de un VLM: encoder, proyector, LLM
Un VLM moderno de la familia Qwen-VL (model card Qwen2.5-VL; Qwen3-VL Technical Report) se compone de tres bloques en serie:
Encoder de visión (ViT). Un Vision Transformer que recibe la imagen, la trocea en parches y produce un embedding por parche (o por grupo de parches). En Qwen2.5-VL es un ViT de resolución dinámica: acepta imágenes de tamaño arbitrario redimensionándolas a múltiplos de 28 y partiéndolas en parches de 14×14 píxeles, con window attention para acelerar el procesado de imágenes grandes. No hay un “tamaño canónico” al que se aplaste todo: una imagen grande produce más parches que una pequeña.
Proyector / conector. El espacio de embeddings del ViT no es el del LLM. El proyector hace de traductor. En Qwen2.5-VL es elegante y barato: agrupa cada bloque 2×2 de parches adyacentes (cuatro tokens del ViT), los concatena y los proyecta con un MLP de dos capas a un único token fusionado en la dimensión del LLM. Ese factor 4 de fusión es la razón del $4$ que aparece en el denominador de la fórmula de tokens más abajo: cuatro parches del ViT colapsan en un token visual que ve el LLM.
El LLM. El transformer de lenguaje de siempre. Recibe una secuencia de embeddings que es una mezcla de tokens de texto (de tu prompt) y tokens visuales (de la imagen), todos en el mismo espacio, y hace lo que sabe hacer: atención sobre la secuencia completa y generación autorregresiva. Para el LLM, un token visual y un token de texto son indistinguibles en coste: ambos pasan por las mismas proyecciones QKV, ambos ocupan una entrada en el KV-cache, ambos participan en la atención $O(C^2)$.
La idea que hay que interiorizar: el encoder y el proyector son el órgano sensorial, pero el coste de inferencia vive en el LLM, y ese coste se paga en tokens. El ViT añade un sobrecoste fijo de prefill (procesar la imagen una vez), pero la factura recurrente —prefill del LLM y KV-cache durante toda la generación— la determina el número de tokens visuales que el proyector escupe. Por eso la palanca de control no es “qué encoder”, sino cuántos píxeles dejas pasar.
Las matemáticas: cuánto cuesta ver una página
Pongamos números a “una imagen cuesta muchos tokens”, con la fórmula real de Qwen2.5-VL (Qwen team, blog Qwen2.5-VL):
$$N_{\text{tok}} = \frac{H \times W}{14 \times 14 \times 4} = \frac{H \times W}{784}$$
donde $H$ y $W$ son alto y ancho en píxeles (redondeados al múltiplo de 28 que impone el ViT). El $14\times14$ es el tamaño del parche; el $\times4$ es la fusión 2×2 del proyector. Comprobación de cordura con el dato del model card: una imagen de $896\times896$ da
$$N_{\text{tok}} = \frac{896 \times 896}{784} = \frac{802816}{784} = 1024 \text{ tokens.}$$
Cuadra. Ahora una página A4 escaneada a una resolución razonable para leer texto pequeño, digamos $1120 \times 1568$ px (alto $\times$ ancho aproximados, ya redondeados a múltiplos de 28):
$$N_{\text{tok}} = \frac{1120 \times 1568}{784} = \frac{1,756,160}{784} \approx 2240 \text{ tokens.}$$
Una sola página son ~2240 tokens visuales antes de escribir una palabra de prompt. Para contextualizar: eso es aproximadamente lo que ocuparían 1700 palabras de texto en español (a ~1,3 tokens/palabra). Si esa página A4 contiene 600 palabras de texto real, el VLM está pagando casi 4× en tokens por verla en lugar de leer su transcripción. Si la página es una tabla densa o un manuscrito que el OCR no sabe leer, ese 4× está justificado. Si es un párrafo de texto limpio, es despilfarro puro.
Qwen2.5-VL deja controlar esto con min_pixels / max_pixels: el número de tokens por imagen es dinámico y va de unos 4 hasta 16384 por defecto, ajustable con esos parámetros (HF docs Qwen2.5-VL). Bajar max_pixels recorta tokens a cambio de resolución —y de capacidad de leer la letra pequeña—. Es exactamente la palanca “cuánto ancho de banda mental le doy al perito”.
Del coste en tokens al TTFT
Los tokens visuales no son gratis en latencia. El prefill —procesar todo el contexto antes del primer token— es compute-bound y su coste crece con la longitud del contexto $C$ (lineal en las proyecciones, cuadrático en la atención); esto está trabajado en detalle en el backend de atención de vLLM. Cada token visual entra en ese $C$.
Modelemos el TTFT como el tiempo de procesar los tokens de prefill a un throughput de prefill dado. Tomemos un VLM de unos 7–8B sobre una H100 con un throughput de prefill ilustrativo de ~12 000 tok/s (cifra de ejemplo; mídela, no la asumas, y depende de cuánto pese además el forward del ViT). Para una petición con prompt de texto de 80 tokens más una página a 2240 tokens visuales:
$$\text{TTFT} \approx \frac{80 + 2240}{12,000 \text{ tok/s}} \approx \frac{2320}{12,000} \approx 0{,}19 \text{ s}$$
más el coste fijo del forward del encoder de visión sobre los ~9000 parches del ViT (otra fracción de prefill compute-bound). Ahora cinco páginas en la misma petición (un PDF corto):
$$\text{TTFT} \approx \frac{80 + 5 \times 2240}{12,000} = \frac{11,280}{12,000} \approx 0{,}94 \text{ s}$$
El TTFT se multiplica casi por cinco, porque los tokens visuales dominan por completo el contexto. Y la atención escala $O(C^2)$: cinco páginas no son “5× más caras” en la componente de atención, sino ~25× respecto a una. La lección operativa: el número de imágenes y su resolución son tu presupuesto de TTFT, no un detalle. Bajar max_pixels de 2240 a, digamos, 1100 tokens/página (resolución más baja, suficiente para texto grande) recorta el prefill casi a la mitad.
VRAM: pesos del VLM y lo que queda para KV
Sobre una H100 SXM de 80 GB, presupuestemos un VLM de la familia Qwen-VL. Tomemos un 8B (LLM) más el encoder de visión (un ViT de unos cientos de millones de parámetros; pongamos el conjunto en ~8,5B de parámetros efectivos a efectos de pesos). En BF16 (2 bytes/parámetro):
$$M_{\text{BF16}} \approx 8{,}5 \times 10^9 \times 2 \text{ B} \approx 17 \text{ GB.}$$
En FP8 (1 byte/parámetro), los pesos caen a la mitad (Qwen3-VL-8B-Instruct-FP8, con calidad reportada casi idéntica al BF16):
$$M_{\text{FP8}} \approx 8{,}5 \times 10^9 \times 1 \text{ B} \approx 8{,}5 \text{ GB.}$$
Para un modelo de 8B esto cabe holgado en BF16 en una H100; FP8 importa más por dejar VRAM libre para KV-cache —y aquí el KV es el problema, porque los tokens visuales lo inflan—. El detalle de FP8 end-to-end (pesos y KV) está en FP8 de punta a punta. Lo relevante: una página son ~2240 entradas de KV; servir varias sesiones que mandan documentos largos consume KV a un ritmo que un caso de solo-texto nunca vería. Con un VLM de 32B en FP8 (~32 GB de pesos) sobre una H100 de 80 GB quedan ~45 GB para KV y activaciones —que con contextos multimodales largos se agotan antes de lo que la intuición de “solo-texto” sugiere—. La regla: con VLM, planifica la VRAM contando los tokens visuales en el KV, no solo los pesos.
Servir un VLM con vLLM
vLLM soporta entrada multimodal de imágenes de forma nativa para la familia Qwen-VL y muchos otros VLM (Multimodal Inputs, vLLM docs). Los engranajes que importan:
--limit-mm-per-prompt. Limita cuántos ítems multimodales acepta cada prompt, por modalidad. Por defecto el límite es generoso (999 por modalidad), pero en producción conviene fijarlo bajo —--limit-mm-per-prompt '{"image": 2}'— porque cada imagen aceptada reserva presupuesto de tokens y de KV. Si pones un límite de 8 imágenes y alguien manda 8 páginas A4, son ~18 000 tokens visuales en una sola petición: revienta el max_model_len o el KV. El límite es un cortafuegos de presupuesto, no una restricción cosmética. (Caso curioso: ponerlo a 0 para una modalidad permite pasar embeddings precomputados sin cargar el módulo del encoder, ahorrando VRAM — útil si haces el encoding de visión fuera de vLLM.)
Presupuesto de píxeles vía mm_processor_kwargs. Aquí se conecta la fórmula de tokens con la configuración. Al servir Qwen2.5-VL se pasa, por ejemplo, --mm-processor-kwargs '{"min_pixels": 12544, "max_pixels": 254016}' (discusión vLLM Qwen2.5-VL). Con $254016$ píxeles máximos por imagen, el techo de tokens por imagen es $254016 / 784 \approx 324$ tokens — un recorte agresivo que mantiene el coste a raya a cambio de no poder leer la letra muy pequeña. Este es el dial que de verdad gobierna tu factura de tokens: súbelo para documentos densos, bájalo para imágenes donde el detalle fino no importa.
El chat template y el placeholder de imagen. El processor del modelo (cargado vía AutoProcessor) inserta tokens especiales que marcan dónde va la imagen en la secuencia: en Qwen2.5-VL la secuencia <|vision_start|><|image_pad|><|vision_end|>, donde <|image_pad|> se expande al número exacto de tokens visuales que la imagen produjo. vLLM aplica este template automáticamente; conviene saber que existe un detalle conocido —el bloque de imagen tiende a colocarse antes del texto del usuario en el orden de la secuencia, independientemente de dónde lo pongas en el contenido (issue #15125, vllm-project/vllm)—, lo que rara vez molesta pero conviene tener presente si el orden imagen/texto es semánticamente importante para tu prompt.
Modelos vigentes. A junio de 2026, la familia abierta de referencia es Qwen3-VL (variantes densas 2B/4B/8B/32B y MoE 30B-A3B / 235B-A22B, contextos intercalados de hasta 256K tokens combinando texto, imagen y vídeo), con checkpoints FP8 publicados de calidad casi idéntica al BF16 (Qwen3-VL Technical Report, arXiv 2511.21631; Qwen3-VL Usage Guide, vLLM recipes). Qwen2.5-VL sigue siendo perfectamente servible y su fórmula de tokens es la que hemos usado por ser pública y limpia; las cifras de coste son del mismo orden en ambas generaciones.
OCR clásico vs VLM: cuándo cada uno
Aquí está la decisión que paga la factura. Parsear documentos para meterlos en un pipeline de ingesta documental tiene dos caminos, y elegir bien el reparto entre ellos es la diferencia entre un cluster sano y uno ahogado.
OCR clásico (Tesseract, PaddleOCR). Motores de reconocimiento de texto que corren en CPU, son baratos, rápidos y maduros. Tesseract es el caballo de batalla portable (edge, kioscos offline, escáneres embebidos). PaddleOCR es más capaz en layout y en escrituras asiáticas, con su módulo PP-Structure para preservar estructura, extraer tablas y detectar campos clave-valor (PaddleOCR vs Tesseract, CodeSOTA 2026). En texto limpio y bien escaneado, el OCR clásico es difícil de batir en coste por página: cero GPU, latencia de milisegundos, throughput altísimo. Su debilidad es el documento “difícil”: layout complejo de múltiples columnas, tablas con celdas combinadas, manuscritos, sellos, gráficos, baja calidad de escaneo. Ahí el OCR pipeline-based o falla o devuelve un galimatías que destruye la estructura.
VLM. Toma la imagen del documento y genera markdown o HTML estructurado en una sola pasada end-to-end, entendiendo el layout porque lo ve. Brilla justo donde el OCR clásico sufre: tablas anidadas, lectura de gráficos, manuscritos, comprensión de la relación espacial entre elementos. El precio es el que llevamos todo el post calculando: caro en tokens y en GPU. Conviene notar un matiz de 2026: la frontera se ha movido. Han aparecido modelos VLM especializados en parsing de documentos —compactos (~1–3B), de código abierto— que puntúan muy alto en benchmarks como OmniDocBench (PaddleOCR-VL en la franja de 94–96 puntos; dots.ocr 3B en ~88 con markdown completo incluyendo tablas) (LLM-OCR vs Traditional OCR, Parsli 2026; Open-Source OCR Models, E2E 2025). Es decir: para parsing puro, hoy quizá no quieras un VLM generalista de 32B, sino un VLM-OCR especializado y pequeño. Pero sigue siendo GPU y sigue pagando tokens por imagen: el cálculo de coste no desaparece, solo se abarata el modelo.
La regla de reparto. No es “OCR o VLM” como elección global, sino triaje por documento. Pasa todo por un primer filtro barato: el OCR clásico ya devuelve una puntuación de confianza; si es alta y el documento no tiene tablas ni elementos visuales, la transcripción del OCR vale y has gastado CPU. Si la confianza es baja, hay tablas, hay manuscrito o hay elementos gráficos, escala esa página al VLM. En un corpus típico, el 80–90 % de páginas se resuelve con OCR barato y solo el resto consume GPU. Esto es lo que hace que un VLM on-premise sea sostenible: no sirves todo con él, lo usas como el especialista caro al que solo le mandas los casos difíciles.
Aplicado al cluster genérico 4×H100
Cómo encaja esto en un cluster de 4×H100 SXM 80GB:
Dónde colocar el VLM. Un VLM merece una H100 entera o un slice grande, por una razón específica de los VLM que la intuición de solo-texto no anticipa: el coste de tokens por imagen hace que no quepan muchas sesiones concurrentes. Si cada petición trae una o varias páginas, cada una consume miles de tokens de KV; el KV-cache se agota con pocas sesiones simultáneas. Comparado con un servicio de chat de solo-texto donde caben docenas de conversaciones cortas por GPU, un servicio de documentos satura el KV con un puñado de peticiones pesadas. Por eso no tiene sentido trocear finamente la GPU para el VLM con MIG agresivo (el reparto de GPU está en compartir GPU: time-slicing, MPS y MIG): el cuello no es el cómputo ocioso sino el KV, y un slice pequeño se queda sin KV antes de aprovechar el cómputo.
FP8 para que entre y para liberar KV. Sirve el VLM en FP8: a igualdad de calidad reportada, los pesos ocupan la mitad y dejas el máximo de VRAM para el KV-cache, que es el recurso escaso con contextos multimodales. Para un 32B en FP8 (~32 GB) sobre una H100, esos ~45 GB libres para KV son lo que determina cuántas páginas concurrentes aguantas.
El VLM solo para el subconjunto que lo necesita. La pieza de arquitectura más importante: no pongas el VLM en el camino crítico de todos los documentos. El triaje de la sección anterior vive aguas arriba; el grueso (texto limpio) se resuelve con OCR/PaddleOCR en CPU —que en un cluster con 4×H100 probablemente sobra CPU para correr en paralelo a las GPUs—, y solo el subconjunto duro llega al VLM. Esto libera las H100 para lo que de verdad las necesita y mantiene un coste por documento razonable. Conecta directamente con FinOps y multi-tenancy de GPU con LiteLLM: cuando pones precio a cada token, un token visual cuesta lo mismo que uno de texto, así que una página por el VLM puede costar 4× lo que su transcripción —y eso debe verse en el chargeback del equipo que decide mandarlo todo al VLM por comodidad—.
Cold start doble. Un detalle operativo que muerde: un VLM carga dos modelos a memoria —el encoder de visión y el LLM—, así que su cold start es más pesado que el de un LLM equivalente. Si vas a hacer model-swap o escalado bajo demanda, la carga rápida importa el doble; ahí entra acelerar el cold start con Tensorizer. Si en cambio sirves varios modelos en una misma GPU rotándolos, el patrón de swap+sleep (servir varios modelos en una GPU) tiene que contar con que el VLM ocupa más y carga más lento.
GPU de consumo, de referencia. Si alguien quiere prototipar fuera del cluster, una RTX 5090 (Blackwell, 32 GB) sirve holgada un VLM de 7–8B en FP8 (~8,5 GB de pesos) con KV de sobra para unas pocas páginas por petición; un 32B en FP8 (~32 GB) ya no cabe con KV útil en esa tarjeta y pide la H100. La 5090 es buena para validar el pipeline de triaje y el prompt; el servicio real, sobre las H100.
Lo que no hemos cubierto
- Vídeo. Qwen3-VL maneja vídeo con muestreo dinámico de FPS; cada fotograma es una imagen y el coste en tokens se multiplica por el número de fotogramas muestreados. Un minuto de vídeo puede ser decenas de miles de tokens. Es el coso multimodal más caro y merece su propio análisis.
- Prefix caching de imágenes. Si la misma imagen (un logo, una plantilla) aparece en muchas peticiones, ¿se puede cachear su KV? Depende del backend y de la estabilidad del placeholder; conecta con el backend de atención.
- Evaluación. Cómo medir que el VLM extrae la tabla correctamente (no basta con “parece bien”): faithfulness sobre un set con ground truth estructurado.
- Fine-tuning del VLM para un dominio documental concreto (facturas, formularios específicos), que puede meter parte del “saber ver” del dominio en los pesos y reducir lo que el prompt tiene que explicar.
Ver también
- FinOps y multi-tenancy de GPU con LiteLLM — pieza hermana de esta tanda: cuando pones precio a cada token, un token visual cuesta lo mismo que uno de texto, y una página por el VLM puede costar 4× su transcripción. El chargeback es lo que disciplina el uso del VLM.
- Acelerar el cold start con Tensorizer — pieza hermana: un VLM carga dos modelos (encoder + LLM), así que su arranque es más pesado y la carga rápida importa el doble.
- Ingesta documental: del PDF al chunk indexado — el pipeline donde encaja el triaje OCR/VLM: el grueso por OCR barato, el subconjunto duro por el VLM.
- Compartir una GPU: time-slicing, MPS y MIG — por qué un slice pequeño no le sirve al VLM: el cuello es el KV que comen los tokens visuales, no el cómputo ocioso.
- Servir varios modelos en una GPU: swap + sleep — rotar el VLM con otros modelos contando con que ocupa más VRAM y carga más lento por su doble modelo.
- El backend de atención de vLLM — el prefill compute-bound que cada token visual engorda; por qué cinco páginas no cuestan 5× sino ~25× en la atención.
- FP8 de punta a punta: pesos y KV — servir el VLM en FP8 para que entre y, sobre todo, para liberar el KV-cache que los contextos multimodales agotan antes de lo previsto.
Referencias
- vLLM, Multimodal Inputs (
limit_mm_per_prompt, embeddings precomputados): https://docs.vllm.ai/en/stable/features/multimodal_inputs/ - vLLM, Vision Language Multi Image (
mm_processor_kwargs, min/max pixels): https://docs.vllm.ai/en/latest/examples/offline_inference/vision_language_multi_image/ - vLLM, Qwen3-VL Usage Guide (recipes): https://docs.vllm.ai/projects/recipes/en/latest/Qwen/Qwen3-VL.html
- Qwen team, Qwen2.5-VL (blog, fórmula de tokens, resolución dinámica): https://qwenlm.github.io/blog/qwen2.5-vl/
- Qwen2.5-VL (Transformers docs, min/max pixels, rango de tokens): https://huggingface.co/docs/transformers/model_doc/qwen2_5_vl
- Qwen3-VL Technical Report, arXiv 2511.21631: https://arxiv.org/abs/2511.21631
- Qwen3-VL-8B-Instruct-FP8 (FP8 de calidad casi idéntica a BF16): https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct-FP8
- vLLM issue #15125 (orden del placeholder de imagen): https://github.com/vllm-project/vllm/issues/15125
- PaddleOCR vs Tesseract, CodeSOTA 2026: https://www.codesota.com/ocr/paddleocr-vs-tesseract
- LLM-Based OCR vs Traditional OCR, Parsli 2026: https://parsli.co/blog/llm-ocr-vs-traditional-ocr
- Open-Source OCR Models 2025, E2E Networks: https://www.e2enetworks.com/blog/complete-guide-open-source-ocr-models-2025