Curso / Aplicado a tus proyectos / Fixes en vLLM
🔧 basado en tu código real

Aplicado a tus proyectos · vLLM (fork propio)

Aguas arriba: tres bugs que arreglaste vos, no NVIDIA ni Google

Servir Gemma-4 en una RTX 5090 te puso en la intersección de hardware nuevo (Blackwell, SM 12.x) y una arquitectura de modelo nueva (atención heterogénea por capa). En esa intersección, casi nadie más pisó estos bugs antes que vos.

Hardware: RTX 5090 (Blackwell, sm_120) 3 fixes propios en tu fork de vLLM Uno con revisión real de un maintainer (hmellor)

0. El patrón: sos el primero en pisar esto

vLLM se testea principalmente contra GPUs de datacenter (A100, H100) con meses de uso acumulado. La RTX 5090 es Blackwell de consumo (SM 12.0), una combinación mucho menos transitada. Y Gemma-4 introdujo algo que casi ningún modelo anterior tenía: geometría de atención distinta por capa (las capas de ventana deslizante usan head_dim=256 con 8 cabezas KV; las de atención completa usan head_dim=512 con una sola cabeza KV compartida). Cada uno de los tres bugs de esta página vive exactamente en esa intersección — hardware de punta + arquitectura nueva — donde el código existente simplemente nunca fue ejercitado.

1. FlashInfer: un error real disfrazado de otro

El síntoma: al arrancar vLLM en la RTX 5090, el sampler de FlashInfer (activado por default) tira "FlashInfer requires GPUs with sm75 or higher" — un mensaje que no tiene sentido para una GPU sm_120. El motor muere durante el profiling de arranque, antes de servir ni un token.

La causa real, encontrada leyendo el código de FlashInfer: su detección de arquitectura (check_cuda_arch()) traga el error real cuando el toolkit de CUDA es anterior al 12.9 sobre SM 12.x — deja un set de arquitecturas objetivo vacío, y ese set vacío es lo que después dispara el mensaje genérico "sm75 or higher" en cualquier intento de compilar un kernel JIT. El mensaje que ves no es el error — es el síntoma de un error que ya fue silenciado un paso antes.

🔧 El fix real

En vez de confiar en el mensaje genérico, el fix llama a check_cuda_arch() directamente en el gate del sampler, y si falla, vuelve a llamar al método interno de FlashInfer que sabe derivar la razón real (CompilationContext._normalize_cuda_arch(major, minor)) para recuperar el mensaje verdadero — algo como "SM 12.x requires CUDA >= 12.9". Con esa razón real en mano: si el usuario no pidió FlashInfer explícitamente, vLLM cae a sampling nativo con un warning claro; si lo pidió a propósito (VLLM_USE_FLASHINFER_SAMPLER=1), lanza el error real — no el genérico — para que sepa exactamente qué actualizar.

El fix viene con un test que monkey-parchea check_cuda_arch y _normalize_cuda_arch para simular exactamente esta condición sin necesitar hardware SM12x real en CI, y referencia el issue público (#42393) — la forma correcta de mandar un fix: reproducible, con test, y atado a un reporte real.

🟢 No fue la única vez con FlashInfer

serve_lacan_vllm.sh ya documenta otra fragilidad de la misma pareja FlashInfer/CUDA-en-sm_120: forzar CUDA_HOME=/usr/local/cuda-13.1 explícitamente porque "ninja recompiló el kernel FP4 con 12.8 y falló" (lección propia, 2026-08-01). Es el mismo patrón de fondo — FlashInfer asume una alineación de versiones de CUDA que en sm_120 todavía no está bien probada — resuelto una vez operativamente (fijando la versión) y otra vez en el código fuente (arreglando la detección).

2. Gemma-4 en el backend de Transformers

Este es el caso más largo — cinco commits propios, con revisión real de un maintainer de vLLM (hmellor) en el medio — y conecta directo con algo que ya viste en la página de cuantización: la geometría de atención heterogénea de Gemma-4 (head_dim=256 en capas de ventana, head_dim=512 en capas completas) es la misma razón por la que tuviste que forzar el backend TRITON_ATTN al servir el modelo cuantizado. Acá es la causa de fondo, atacada en el código en vez de sorteada en runtime.

El síntoma inicial: el backend nativo de Transformers en vLLM construía cada instancia de atención con el head count y head size globales del config. Un modelo con geometría homogénea nunca lo nota. Gemma-4 sí: crash inmediato en el primer forward pass, shape mismatch en o_proj.

🔧 Dos bugs silenciosos que aparecieron al generalizar

El primer fix funcionó, pero hmellor pidió en review que se generalizara — el código tenía hardcodeados los nombres de atributo específicos de Gemma-4 ({"head_dim", "num_key_value_heads"}), una asunción que se rompe con el próximo modelo heterogéneo. Al generalizarlo aparecieron dos bugs que nadie había visto:

  • Un get_total_num_kv_heads() sin override propio para configs con attention_k_eq_v=True — cualquier variante de Gemma-4 con ese flag hacía que flash_attn.py, triton_attn.py y los conectores de transferencia de KV leyeran un valor global degradado en vez del máximo real entre capas. Sin crashear — simplemente corriendo con la cabeza KV equivocada.
  • La detección de layout viejo vs. nuevo del config estaba atada a la presencia de un mixin que Transformers agrega a todos los configs desde su versión 5.15 — no a si el modelo realmente usa el layout nuevo. Resultado: cualquier checkpoint publicado de Gemma-4 (que todavía usa el layout viejo) se enviaba por el camino nuevo, donde silenciosamente devolvía head_dim=256 para capas que necesitaban 512.

Ambos son la misma clase de bug que ya viste en la página de cuantización: no truenan, simplemente producen el número equivocado. La diferencia es que acá el "número equivocado" es la geometría misma del modelo, no un peso cuantizado.

El mismo trabajo destapó, de yapa, una regresión de performance real: resolver la geometría por capa de la forma ingenua agregaba 8.3 segundos al arranque de un modelo de 48 capas — indexar por tipo de capa copiaba y comparaba cada config de ese tipo en cada llamada. Resuelto indexando por posición en vez de por tipo: 350 milisegundos.

El cierre de la cadena es tan prolijo como el resto: un commit final borra por completo un flag de compatibilidad temporal (allow_global_per_layer_attribute_access) después de instrumentar un arranque completo de Gemma-4 y confirmar que quedaba exactamente un lugar que todavía lo necesitaba — y arreglar ese lugar en vez de mantener el flag indefinidamente.

3. NVFP4 en SM12x: cuando el bug no truena

El tercer caso es el más inquietante de los tres, precisamente porque no hay ningún error en pantalla. En SM 12.x, el kernel NVFP4 que vLLM auto-selecciona por default (FlashInferCutlassNvFp4LinearKernel) corre sin quejarse — y produce basura. Medido en un benchmark real: GSM8K 0.0 con ese kernel, contra 0.8867 con Marlin (otra implementación de kernel NVFP4), mismo checkpoint, mismo hardware.

🔧 El fix

Excluir ambos kernels basados en CUTLASS (el auto-seleccionado y el nativo — los dos dan mal en SM12x) de la auto-selección en esa arquitectura, para que la selección automática caiga en Marlin — que sí es correcto ahí. Un --linear-backend explícito sigue permitiendo forzar cualquiera de los dos para debug.

Esto es la versión más extrema de una lección que ya apareció dos veces en este curso: un log limpio, o incluso una respuesta que suena coherente, no prueba nada. Acá ni siquiera hace falta un log sospechoso — hace falta correr un eval real para descubrir que el 100% de las respuestas están mal.

4. Qué hace bueno a un fix aguas arriba

Los tres casos comparten la misma forma, y vale la pena nombrarla:

  1. Diagnóstico hasta la causa real, no hasta el primer síntoma — el mensaje de FlashInfer, el crash de shape, el score de GSM8K son todos síntomas; el fix ataca lo que hay debajo.
  2. El cambio mínimo que corrige la causa, sin tocar nada que ese diagnóstico no justifique.
  3. Un test que reproduce el bug sin depender de tener el hardware exacto a mano (el monkeypatch de FlashInfer corre en cualquier CI).
  4. Atado a un issue o número de PR real, nunca "arreglado porque sí".
  5. Abierto a la revisión — el caso de Gemma-4 es mejor código después de que hmellor pidió generalizarlo, no a pesar de eso.

5. Resumen

Tres bugs, un mismo origen: hardware de consumo de última generación corriendo una arquitectura de modelo con una geometría que el código existente nunca anticipó. Ninguno de los tres se resolvió "esperando a que NVIDIA o Google lo arreglaran" — se diagnosticaron y se mandaron aguas arriba, con tests, con referencia al issue real, y en el caso más grande, iterando sobre revisión real de un maintainer del proyecto.