Vulkan: amado por la velocidad, odiado por la complejidad

¿Te fue útil?

Vulkan es la API que eliges cuando ya te cansaste de discutir con los controladores y estás listo para discutir contigo mismo.
Puede ofrecer una sobrecarga de CPU brutalmente baja y un rendimiento predecible, pero te pide que te conviertas en el controlador.
Si alguna vez has enviado una compilación con Vulkan que corría a 300 FPS en el laboratorio y a 45 FPS en el portátil de un cliente, ya conoces la sensación.

Esta es una guía de campo orientada a producción: qué te compra Vulkan, qué te cuesta, cómo fallan los equipos en la práctica,
y cómo diagnosticar cuellos de botella rápido usando comandos reales y sus salidas. Sin folklore. Sin romanticismo. Solo los bordes filosos.

Por qué existe Vulkan (y por qué duele)

Vulkan no es “OpenGL pero más nuevo”. Es la industria reconociendo que la magia implícita del controlador se había convertido en un problema de fiabilidad.
Las APIs gráficas antiguas ocultaban transiciones de estado, tiempos de vida de recursos, sincronización y colocación de memoria detrás de un controlador que
hacía conjeturas con el mejor esfuerzo. Esas conjeturas a veces eran brillantes. También a veces eran catastróficas e inrepeatables
entre distintos proveedores, versiones de SO e incluso diferentes builds del mismo controlador en la misma máquina.

Vulkan invierte eso. La aplicación le dice a la GPU exactamente lo que quiere, exactamente cuándo, usando sincronización explícita y gestión explícita
de recursos. El controlador deja de jugar al lector de mentes y se parece más a una capa de traducción ligera. Ese cambio
desbloquea rendimiento—especialmente en renderizadores limitados por CPU—y hace el comportamiento más determinista. También hace mucho más fácil
arrancarte un pie con un lanzacohetes.

“Vulkan es difícil” no es un meme. Es el costo de tener control. La pregunta es si ese control se alinea con los
modos de fallo de tu producto. Si tu equipo no puede razonar de forma fiable sobre concurrencia, tiempos de vida y memoria, Vulkan no te premiará
con velocidad. Te castigará con bugs que desaparecen bajo un depurador, reaparecen en un modelo de GPU y arruinan un lanzamiento.

Datos e historia interesantes (breve y concretos)

  • Vulkan desciende de AMD Mantle. Mantle demostró que una “API ligera y control explícito” podía reducir la sobrecarga de CPU; Vulkan generalizó la idea.
  • Está gestionado por Khronos Group. El mismo consorcio detrás de OpenGL, OpenCL y varios estándares multimedia—lo que implica muchos interesados y evolución cuidadosa.
  • Vulkan 1.0 llegó en 2016. Ese momento fue importante: los CPUs multicore eran generalizados, pero las rutas clásicas de OpenGL con hilos de controlador a menudo se atascaban en un solo núcleo.
  • SPIR-V forma parte del trato. El formato de shaders de Vulkan es una representación intermedia diseñada para portabilidad y flexibilidad de toolchain, no para la felicidad humana.
  • Las capas de validación se volvieron cultura. El ecosistema de Vulkan normalizó “ejecutar con validación en CI”, algo que los usuarios de OpenGL mayormente no hacían de forma consistente.
  • Los descriptor sets se diseñaron para batching. Vulkan empujó el enlace de recursos hacia sets preconstruidos para evitar churn por dibujado, especialmente en cargas limitadas por CPU.
  • La sincronización se formalizó explícitamente. La especificación define dependencias de memoria, dependencias de ejecución y layouts en detalle—no más “el controlador probablemente lo hará”.
  • La portabilidad vino después. Vulkan nunca fue “escribe una vez ejecuta en todas partes” por defecto; iniciativas como MoltenVK y el subconjunto de portabilidad surgieron del dolor.
  • Las extensiones son la vía de crecimiento principal. Vulkan evoluciona vía extensiones y promoción a core, lo cual es excelente para características y terrible para matrices de compatibilidad simplistas.

De dónde viene la velocidad (y dónde no)

La verdadera victoria de Vulkan: menor sobrecarga de CPU y mejor paralelismo

La victoria emblemática es la sobrecarga de CPU. Vulkan te anima a construir command buffers con antelación, reutilizar pipelines,
minimizar cambios de estado y evitar validación del lado del controlador en builds de release. Cuando se hace bien, puedes repartir
el trabajo de envío entre núcleos: un hilo graba la UI, otro graba la geometría del mundo, otro graba pases de sombras, y los unes
con command buffers secundarios o con particionado cuidadoso del buffer primario.

Pero seamos francos: Vulkan no hace a la GPU más rápida en cálculo. Si tu frame está limitado por shaders o por ancho de banda,
la ventaja de Vulkan es mayormente indirecta: te ayuda a alimentar la GPU de forma más consistente, reducir tartamudeos por cambios de estado,
y evitar picos de CPU por compilación de controladores o transiciones de recursos ocultas.

La otra victoria: rendimiento predecible gracias a la explicitud

La predictibilidad es el tipo de velocidad que a un SRE le encanta. Vulkan te obliga a definir transiciones de recursos y sincronización.
Eso significa menos misterios del tipo “funciona en el proveedor A” y más claridad del tipo “nuestra barrera está mal”. En producción, eso importa mucho.
El comportamiento determinista es lo que te permite establecer SLOs en tiempo de frame, detectar regresiones temprano y mantener la cadencia de lanzamientos.

Dónde Vulkan no te salvará

  • Contenido malo. Overdraw, permutaciones de shaders absurdas, texturas 8K en una GPU de gama baja—Vulkan no puede negociar con la física.
  • Caos de pipelines. Si compilas pipelines durante el juego, el tartamudeo es culpa tuya, no de la API.
  • Paranoia por sincronización. Sobre-barreras pueden serializar tu GPU como si fuera 2008. Vulkan no te detendrá.
  • Thrashing de memoria. Si asignas/liberas por frame, obtendrás fragmentación, sobrecarga del controlador y picos aleatorios. Vulkan facilita hacerlo mal.

Una cita para tener en un post-it:
«La esperanza no es una estrategia.» — General Gordon R. Sullivan.
Vulkan premia a los ingenieros que reemplazan la esperanza con instrumentación y flujos de trabajo repetibles.

Broma #1: Vulkan es como la transmisión manual—más rápido cuando sabes lo que haces, y espectacularmente ruidoso cuando no.

El impuesto de la complejidad: lo que realmente pagas

1) Ahora eres responsable de la sincronización

La sincronización explícita de Vulkan es a la vez el punto y la trampa. Debes razonar sobre:
orden de ejecución (qué ocurre antes de qué), visibilidad de memoria (qué escrituras son visibles para qué lecturas),
y layouts de imágenes (cómo interpreta la GPU la memoria para una operación dada).

Modo de fallo clásico: añades una barrera “por seguridad”, pero eliges una máscara de etapas que fuerza a la GPU
a esperar mucho más trabajo del necesario. Todo es correcto. Todo es lento. Esta es la tragedia más común de Vulkan: la corrección no implica rendimiento.

2) Ahora eres responsable de la colocación y tiempo de vida de la memoria

Vulkan expone heaps de memoria, tipos de memoria y la diferencia entre memoria visible por el host y memoria local al dispositivo.
Esto es fantástico porque puedes hacer lo correcto para tu carga de trabajo. También es agotador porque necesitas
políticas: quién asigna, quién libera, cómo subasignas y cómo evitas la fragmentación.

Si no haces nada más: usa un asignador bien diseñado (la mayoría de equipos usan VMA u otro similar) y estandariza
patrones de asignación. La gestión de memoria en Vulkan no es lugar para creatividad artesanal.

3) Los pipelines son caros, y Vulkan hace que sea tu problema

Los pipelines de Vulkan (gráficos y compute) pueden ser costosos de crear. Pueden desencadenar compilación de shaders y trabajo interno del controlador.
Si los creas en tiempo de ejecución durante el juego, verás picos “aleatorios” que se correlacionan con contenido nuevo o movimiento de cámara.
Parecerá recolección de basura, pero es peor porque ocurre dentro de las paredes del controlador.

La postura correcta es aburrida: precompilar, cachear, hacer warm up y enviar caches de pipeline cuando la plataforma lo permita. En Vulkan,
lo aburrido es una ventaja competitiva.

4) La matriz de extensiones es trabajo real

El ecosistema de Vulkan evoluciona mediante extensiones. Eso es buena ingeniería: las funciones pueden lanzarse sin esperar una revisión mayor,
y los proveedores pueden innovar. Pero en producción, cada extensión es un multiplicador de compatibilidad y QA.
Necesitas una base de datos de capacidades, sondeo en tiempo de ejecución y rutas de fallback que no sean embarazosas.

Guion de diagnóstico rápido: encuentra el cuello de botella

Cuando un frame con Vulkan es lento, tu trabajo no es “optimizar Vulkan”. Tu trabajo es localizar el limitador: CPU, GPU, memoria,
sincronización, compilación o presentación. Este guion es el orden que normalmente te da respuestas en minutos, no días.

1) Primera comprobación: ¿está limitado por CPU, GPU o presentación?

  • Limitado por CPU: un núcleo saturado, GPU infrautilizada, tiempo de frame se correlaciona con conteo de draw o complejidad de escena, no con resolución.
  • Limitado por GPU: alta utilización de GPU, el tiempo de frame escala con resolución, shaders pesados, ancho de banda o overdraw.
  • Limitado por presentación (vsync / compositor / swapchain): el tiempo de frame se ajusta a intervalos de refresco; la GPU puede quedarse inactiva esperando el present.

Decisión: no toques shaders si estás limitado por CPU; no micro-optimices la grabación de command buffers si estás limitado por GPU.
Si estás limitado por presentación, deja de “optimizar” y arregla tu modo de swapchain o estrategia de timing.

2) Segunda comprobación: ¿tartamudeas por compilación de pipelines/shaders?

Busca picos cuando aparecen materiales, PSOs o efectos nuevos. Si los picos desaparecen tras unos minutos (caches calientes),
estás compilando en tiempo de ejecución.

Decisión: implementa una estrategia de cache de pipelines, pases de warm-up y/o envía caches derivadas cuando la plataforma lo permita.

3) Tercera comprobación: sincronización y barreras

Si el tiempo de GPU es alto pero la carga parece “demasiado pequeña”, revisa por sobre-sincronización: pipeline barriers que serializan
trabajo no relacionado, esperas de cola innecesarias, o un semaphore de timeline usado como un mutex global.

Decisión: ajusta máscaras de etapas, estrecha máscaras de acceso, pasa de esperas globales a dependencias por recurso,
y considera compute asíncrono solo si puedes demostrar solapamiento.

4) Cuarta comprobación: comportamiento de la memoria y presión de transferencias

Observa asignaciones por frame, churn de buffers de staging y copias excesivas de buffers/imágenes.
Picos en CPU pueden venir de map/unmap y flushes de caché; picos en GPU pueden venir de saturación de ancho de banda.

Decisión: adopta ring buffers, staging mapeado persistentemente y agrupa transferencias; aplica la regla “no asignaciones durante el frame”.

5) Quinta comprobación: presentación y pacing de frames

Si el pacing de frames es irregular, podrías estar persiguiendo el número equivocado. El FPS medio es vanidad; el tiempo de frame consistente es cordura.
Busca jitter por recreación de swapchain, interacciones con el compositor o envío de trabajo inconsistente.

Decisión: elige el modo de present intencionalmente, controla el pacing de frames (especialmente con VRR) y trata la recreación de swapchain como un evento planificado.

Tareas prácticas con comandos: qué ejecutar, qué significa, qué decides

Estos son los movimientos de “SRE para gráficos”: comandos reproducibles que puedes ejecutar en máquinas de desarrollo, agentes CI y cajas de repro de clientes.
Las salidas son ejemplos; tu entorno variará. El punto es la interpretación y la decisión siguiente.

Task 1: Confirmar que el loader y el driver ven tu GPU (Linux)

cr0x@server:~$ vulkaninfo --summary
Vulkan Instance Version: 1.3.275

Instance Extensions: count = 20
...

Devices:
========
GPU0:
    apiVersion         = 1.3.275
    driverVersion      = 550.54.14
    vendorID           = 0x10de
    deviceID           = 0x2684
    deviceType         = DISCRETE_GPU
    deviceName         = NVIDIA GeForce RTX 4070

Qué significa: El loader funciona, se encontró el ICD y tienes una GPU discreta expuesta vía Vulkan.

Decisión: Si la lista de dispositivos está vacía o muestra solo llvmpipe, arregla la instalación del driver/ICD antes de depurar tu app.

Task 2: Detectar rápidamente “estás ejecutando renderizado por software”

cr0x@server:~$ vulkaninfo --summary | grep -E "deviceType|deviceName"
    deviceType         = CPU
    deviceName         = llvmpipe (LLVM 17.0.6, 256 bits)

Qué significa: Estás en una implementación Vulkan basada en CPU. Las quejas de rendimiento están totalmente justificadas.

Decisión: Deja de optimizar. Arregla la selección de driver, PRIME offload, acceso a GPU en contenedor o passthrough en VM/remota.

Task 3: Verificar qué JSONs de ICD están instalados (común en fallos en contenedores)

cr0x@server:~$ ls -1 /usr/share/vulkan/icd.d/
nvidia_icd.json
intel_icd.x86_64.json

Qué significa: El sistema tiene múltiples ICDs; el loader elegirá según GPU y entorno.

Decisión: Si tu contenedor carece de estos archivos, móntalos o instala los paquetes de driver correctos; de lo contrario caerás en fallback silencioso.

Task 4: Forzar al loader a decirte qué está haciendo (debug del loader)

cr0x@server:~$ VK_LOADER_DEBUG=all vulkaninfo --summary
INFO:             Vulkan Loader Version 1.3.275
INFO:             Searching for ICDs
INFO:             Found ICD manifest file /usr/share/vulkan/icd.d/nvidia_icd.json
INFO:             Loading ICD library /usr/lib/x86_64-linux-gnu/libvulkan_nvidia.so
...

Qué significa: Puedes ver el descubrimiento y la selección de ICD. Esto es oro cuando “funciona en mi máquina” se encuentra con contenedores.

Decisión: Si carga el ICD equivocado, establece controles de entorno explícitos (o arregla el empaquetado) en lugar de adivinar.

Task 5: Comprobar que las capas de validación están instaladas y son detectables

cr0x@server:~$ vulkaninfo --layers | head
VK_LAYER_KHRONOS_validation (Khronos Validation Layer) Vulkan version 1.3.275, layer version 1
VK_LAYER_MESA_overlay (Mesa Overlay layer) Vulkan version 1.3.275, layer version 1

Qué significa: La validación está disponible en esta máquina; puedes activarla en builds de depuración.

Decisión: Si la validación no está presente, instálala o inclúyela en el repositorio. No vueles a ciegas.

Task 6: Ejecutar tu app con validación + mensajes verbosos (triage de corrección)

cr0x@server:~$ VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation VK_LAYER_SETTINGS_PATH=/etc/vk_layer_settings.d ./my_vulkan_app
VUID-vkCmdPipelineBarrier2-srcStageMask-03842: Validation Error: srcStageMask includes VK_PIPELINE_STAGE_2_HOST_BIT but no host access mask set.
...

Qué significa: La barrera está malformada: estás reclamando una dependencia de etapa host sin las máscaras de acceso correspondientes.

Decisión: Corrige la corrección primero. Cualquier perfilado de rendimiento hecho con sincronización inválida es una pérdida de tiempo.

Task 7: Capturar un frame con RenderDoc desde la línea de comandos (repro reproducible)

cr0x@server:~$ qrenderdoc --version
qrenderdoc v1.31

Qué significa: La UI de RenderDoc está instalada. Para automatización normalmente capturarás mediante inyección o triggers en la app.

Decisión: Estandariza un flujo de captura: misma escena, misma ruta de cámara, mismo índice de frame. Si no, tus comparaciones mienten.

Task 8: Revisar hotspots del lado CPU con perf (sobrecarga de grabación de command buffer)

cr0x@server:~$ perf stat -e cycles,instructions,context-switches,cpu-migrations -p $(pidof my_vulkan_app) -- sleep 5
 Performance counter stats for process id '24188':

     9,845,221,003      cycles
    12,110,993,551      instructions              # 1.23  insn per cycle
           8,214      context-switches
             112      cpu-migrations

       5.001233495 seconds time elapsed

Qué significa: Tienes una vista del coste de CPU y churn del scheduler mientras la app corre.

Decisión: Si context switches/migrations se disparan durante el tartamudeo, busca contención de hilos, logging o primitivas de sincronización actuando como locks globales.

Task 9: Identificar si tu app está silenciosamente bloqueada en present/vsync

cr0x@server:~$ strace -tt -p $(pidof my_vulkan_app) -e trace=futex,nanosleep,clock_nanosleep -s 0
12:44:10.112233 futex(0x7f2d3c00a1c0, FUTEX_WAIT_PRIVATE, 7, NULL) = 0
12:44:10.128901 clock_nanosleep(CLOCK_MONOTONIC, 0, {tv_sec=0, tv_nsec=8000000}, NULL) = 0

Qué significa: La app está esperando/durmiendo con regularidad; a menudo esto se correlaciona con limitación de frames, pacing de swapchain o throttling interno.

Decisión: Si crees que estás limitado por GPU pero ves sleeps regulares, revisa tu modo de present, limitador de frames y la cantidad de imágenes del swapchain.

Task 10: Inspeccionar relojes y utilización de GPU (ejemplo NVIDIA)

cr0x@server:~$ nvidia-smi dmon -s pucm -d 1 -c 5
# gpu   pwr gtemp mtemp    sm   mem   enc   dec  mclk  pclk
# Idx     W     C     C     %     %     %     %   MHz   MHz
    0    92    63     -    38    22     0     0  8001  2100
    0   165    67     -    97    75     0     0  8001  2520
    0   160    66     -    96    78     0     0  8001  2520
    0   158    66     -    95    77     0     0  8001  2520
    0    98    63     -    41    24     0     0  8001  2100

Qué significa: Cuando el frame es pesado, SM y utilización de memoria suben y los relojes aumentan. Cuando es ligero, bajan.

Decisión: Si SM se mantiene bajo pero FPS es bajo, probablemente estás limitado por CPU/present/sync. Si SM está al máximo, estás limitado por GPU.

Task 11: Comprobar presión de memoria GPU (uso de VRAM)

cr0x@server:~$ nvidia-smi --query-gpu=memory.total,memory.used,memory.free --format=csv
memory.total [MiB], memory.used [MiB], memory.free [MiB]
12282 MiB, 10840 MiB, 1442 MiB

Qué significa: Estás cerca del techo. Vulkan no te impedirá hacer paging o eviction; el comportamiento depende del driver/SO.

Decisión: Reduce residencia (mip bias, streaming de texturas, caches más pequeños), y evita asignar imágenes transitorias grandes en picos de carga.

Task 12: Detectar spam de compilación de shaders en logs (firma de tartamudeo)

cr0x@server:~$ journalctl --user -n 200 | grep -i -E "shader|pipeline|spirv" | head
Jan 13 12:41:02 workstation my_vulkan_app[24188]: pipeline: creating graphics pipeline for material=WetConcrete variant=Skinned
Jan 13 12:41:02 workstation my_vulkan_app[24188]: shader: compiling fragment SPIR-V module hash=9c2f...
Jan 13 12:41:02 workstation my_vulkan_app[24188]: pipeline: cache miss, building pipeline key=0x7f1a...

Qué significa: Estás compilando pipelines/shaders en tiempo de ejecución. Eso genera tartamudeos.

Decisión: Añade un paso de build offline, warm-up de escenas o una fase determinista “compila todos los pipelines necesarios antes del juego”.

Task 13: Confirmar modo de swapchain y cantidad de imágenes (logging en la app + comprobaciones)

cr0x@server:~$ grep -E "presentMode|minImageCount|imageCount" /var/log/my_vulkan_app.log | tail -n 5
swapchain: presentMode=VK_PRESENT_MODE_FIFO_KHR
swapchain: minImageCount=2 chosenImageCount=2
swapchain: format=VK_FORMAT_B8G8R8A8_SRGB colorSpace=VK_COLOR_SPACE_SRGB_NONLINEAR_KHR

Qué significa: FIFO es vsync; dos imágenes es doble-buffering. Esto puede aumentar latencia y reducir tolerancia a picos.

Decisión: Considera triple buffering (3 imágenes) para un pacing más suave, o MAILBOX cuando sea apropiado—después de medir las necesidades de latencia.

Task 14: Validar que no estés ejecutando con overhead de debug en release

cr0x@server:~$ env | grep -E "VK_INSTANCE_LAYERS|VK_LOADER_DEBUG|VK_LAYER"
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation
VK_LOADER_DEBUG=all

Qué significa: La validación y el debug del loader están habilitados. Genial para depurar, terrible para métricas de rendimiento.

Decisión: En ejecuciones de rendimiento, sanea las variables de entorno y asegúrate de que tu build desactive validación y marcadores de debug salvo que sean necesarios explícitamente.

Tres micro-historias corporativas desde las trincheras

Micro-historia 1: El incidente causado por una suposición errónea

Un estudio mediano lanzó una actualización de su renderer Vulkan para reducir la sobrecarga de CPU en escenas de mundo abierto.
Los datos internos de rendimiento se veían fantásticos en las máquinas de prueba principales del equipo: GPUs discretas más nuevas, mayormente del mismo proveedor.
QA dio su visto bueno. El lanzamiento fue limpio—hasta que la cola de soporte empezó a llenarse con “texturas parpadeantes aleatorias” en portátiles.

El bug era intermitente y difícil de reproducir. Los frames se veían bien la mayor parte del tiempo, luego unos pocos objetos parpadeaban con
texturas obsoletas, como si la GPU estuviera muestreando memoria de ayer. El equipo asumió que era “un bug de driver”, porque los artefactos eran específicos
de vendor y modelo.

No lo era. La app tenía una suposición incorrecta sobre transiciones de layout de imagen alrededor de una operación de copia.
En sus GPUs principales, el driver aparentemente toleraba la ruta de transición descuidada. En los sistemas fallidos, la ejecución de la GPU
era más paralela y la dependencia faltante realmente importaba. Las capas de validación habrían avisado—excepto que la validación estaba deshabilitada
en los builds de repro porque “ralentiza”.

La corrección fue directa: arreglar la barrera, estrechar y ajustar máscaras de etapa/acceso, y asegurar que las transiciones de layout
estaban definidas para el rango exacto de subrecursos. La corrección real fue cultural: las capas de validación se volvieron obligatorias en CI y
en builds de repro de QA, y los ingenieros dejaron de tratar las comprobaciones de corrección como adorno opcional.

También aprendieron la lección peligrosa que Vulkan enseña temprano: si dependes de comportamiento indefinido, no tienes un renderer.
Tienes un rumor.

Micro-historia 2: La optimización que salió mal

Una empresa que construía una herramienta de visualización tipo CAD decidió “ir a full Vulkan” y agrupar trabajo agresivamente.
Redujeron actualizaciones de descriptores creando descriptor sets enormes con miles de entradas, actualizadas raramente,
e indexando dinámicamente. En papel, menos actualizaciones equivalen a menos ciclos de CPU. Todos asintieron.

La primera pasada de rendimiento se veía bien en GPUs de gama alta. Luego probaron en una variedad de máquinas y vieron caídas extrañas:
algunas GPUs se volvieron dramáticamente más lentas al mover la cámara, a pesar de menos draw calls y menos llamadas a la API.
El tiempo de GPU se disparó, y no era obvio por qué. El equipo inicialmente culpó a la complejidad de los fragment shaders.

La realidad: la estrategia de “un descriptor set gigantesco” causó mala localidad de caché y aumentó la presión en las vías de indexado de descriptores.
Algunos drivers lo manejaban bien; otros pagaban más por acceso. Peor aún, su descriptor set se convirtió en un punto caliente de sincronización porque las actualizaciones
requerían fencing cuidadoso para evitar modificar descriptores en uso.

Tuvieron que pausar el despliegue. Reestructuraron bindings en descriptor sets más pequeños por material y por pase,
usaron descriptor indexing solo donde era claramente beneficioso, e introdujeron un ring por frame para recursos dinámicos.
La sobrecarga de CPU subió ligeramente. La estabilidad del tiempo de frame mejoró masivamente y la varianza entre GPUs cayó.

Vulkan aceptará felizmente tu “optimización”. También aceptará felizmente tu regresión de rendimiento.
La API no impone buen gusto.

Micro-historia 3: La práctica aburrida pero correcta que salvó el día

Un equipo que lanzaba un motor de juego basado en Vulkan tenía una regla aburrida: cada PR que tocara render necesitaba una captura de frame
y dos métricas: desglosado de tiempo de frame (CPU/GPU) y tasa de aciertos del cache de pipeline. No a veces. Cada vez.
Fastidiaba a la gente. También prevenía incidentes.

Cerca del final de una ciclo de lanzamiento, un ingeniero fusionó un cambio que introdujo una nueva variante de post-proceso.
La característica era pequeña y visualmente sutil. El PR incluyó la captura requerida, y el revisor notó algo extraño:
la tasa de aciertos del cache de pipeline bajó en un escenario que debería haber permanecido sin cambios. El tiempo de GPU también mostró nuevos picos.

Revirtieron, luego hicieron bisect y descubrieron la raíz: una clave de pipeline incluía un campo no determinista (un hash derivado de un puntero),
así que pipelines que deberían ser idénticos se trataron como distintos entre ejecuciones. Eso significaba creación frecuente de pipelines,
y tartamudeo que solo ocurría en instalaciones nuevas o tras invalidación de cache—exactamente el tipo de bug que se desliza.

La corrección fue aburrida: hacer las claves de pipeline deterministas, añadir tests unitarios sobre la generación de claves, e incluir estadísticas de cache
en puertas automáticas de rendimiento. Nada heroico. Sin drama nocturno. El tipo de ingeniería que no se vuelve viral.

Broma #2: La mejor optimización de Vulkan es la que no envías porque la lista de revisión la detectó.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: texturas que parpadean o píxeles basura ocasionales

Causa raíz: Sincronización faltante o incorrecta, transiciones de layout de imagen equivocadas, o desajuste en el rango de subrecursos en barreras.

Solución: Ejecuta con capas de validación; verifica que las barreras usen máscaras de etapa/acceso correctas; asegura que las transiciones de layout cubran exactamente los mip/array layers usados; evita usar “ALL_COMMANDS” como muleta.

2) Síntoma: FPS estable pero tartamudeos periódicos al girar la cámara

Causa raíz: Creación de pipelines o compilación de shaders durante el juego; misses en pipeline cache; explosión de permutaciones de PSO.

Solución: Preconstruye pipelines offline; haz warm up en carga; usa pipeline caches; reduce permutaciones; registra creación de pipelines y trátala como un error en tiempo de juego.

3) Síntoma: baja utilización de GPU, núcleo de CPU al máximo, draw calls altos

Causa raíz: Envío/recording limitado por CPU; actualizaciones de descriptores excesivas; demasiados command buffers pequeños; demasiado trabajo por draw.

Solución: Agrupa draws; reduce cambios de estado; usa command buffers secundarios apropiadamente; graba en múltiples hilos; prealoca pools de descriptores; pasa a bindless/indexing donde esté probado.

4) Síntoma: tiempo de GPU inexplicablemente alto después de “hacer barreras seguras”

Causa raíz: Sobre-sincronización: máscaras de etapa amplias, idle innecesario de colas, fences globales, serialización de pases que podrían solaparse.

Solución: Ajusta máscaras de etapa; usa barreras por recurso; prefiere semáforos de timeline para dependencias estructuradas; valida con trazas GPU que recuperaste solapamiento.

5) Síntoma: crash o device lost bajo carga intensa

Causa raíz: OOM, acceso ilegal a memoria por bugs de tiempo de vida, o timeouts/watchdog por shaders/dispatches de larga ejecución.

Solución: Sigue presupuestos de memoria; añade ownership robusto de tiempos de vida; trocea trabajo de compute largo; reduce el peor tiempo de frame; captura volcados de crash GPU cuando estén disponibles.

6) Síntoma: funciona en un proveedor, falla en otro

Causa raíz: Depender de comportamiento indefinido; suposiciones incorrectas sobre coherencia de memoria; uso de extensiones sin comprobación de capacidades.

Solución: Activa validación; prueba en múltiples vendors temprano; habilita características según lo consultado en runtime; evita lógica de “parece funcionar” en sync y layouts.

7) Síntoma: uso de memoria crece lentamente con el tiempo

Causa raíz: Fugas de VkImage/VkBuffer/VkDeviceMemory; crecimiento de pools de descriptores; asignaciones por frame no reclamadas; caches sin eviction.

Solución: Añade seguimiento de asignaciones; implementa auditorías de tiempos de vida de recursos; limita caches; usa ring buffers para asignaciones por frame; asegura que pools de descriptores se reseteen/reciclen.

8) Síntoma: tormentas de recreación de swapchain (frames negros, glitches al redimensionar)

Causa raíz: Manejo incorrecto de VK_ERROR_OUT_OF_DATE_KHR / SUBOPTIMAL; carreras en lógica de resize; presentar con imágenes de swapchain obsoletas.

Solución: Centraliza el ciclo de vida del swapchain; pausa rendering durante la recreación; reconstruye recursos dependientes; valida que todos los frames en vuelo se vacíen de forma segura.

Listas de verificación / plan paso a paso para Vulkan sano en producción

Plan paso a paso: de prototipo a enviable

  1. Define tu cuello de botella objetivo.
    Si ya estás limitado por GPU, Vulkan no lo arreglará mágicamente. Decide si compras cabeza de CPU, determinismo o portabilidad.
  2. Elige un modelo de sincronización y documéntalo.
    Decide cómo manejarás recursos por frame, frames en vuelo y dependencias entre colas. Escríbelo como un runbook de oncall.
  3. Adopta un asignador de memoria y establece políticas.
    Estandariza estrategia de subasignación, reglas de alineamiento y la regla de “no asignaciones durante el frame”.
  4. Haz de la creación de pipelines una fase controlada.
    Implementa caches de pipeline, claves de pipeline estables y una ruta de warm-up. Rastrea la tasa de aciertos del cache como KPI.
  5. Construye una base de datos de capacidades.
    Consulta versión Vulkan, características del dispositivo, límites y extensiones en runtime; regístralo; úsalo para toggles de características y fallbacks.
  6. Haz obligatoria la validación en CI y builds de QA.
    Envía con validación desactivada en release, pero no aceptes PRs de render que no pasen limpios con validación.
  7. Instrumenta tiempo de frame, no FPS.
    Rastrea tiempo de frame de CPU, GPU, tiempo de espera de present y percentiles de picos (p95/p99).
  8. Estandariza flujos de captura.
    Una escena de repro fija, ruta de cámara determinista, índice de frame fijo y una toolchain conocida de captura.
  9. Prueba en múltiples vendors desde temprano.
    No dejes la primera prueba en AMD/Intel para después de que tu arquitectura se haya cristalizado.
  10. Envia con guardrails.
    Comprobaciones en runtime para creación inesperada de pipelines, asignaciones durante el frame, agotamiento de pools de descriptores y errores de swapchain. Falla ruidosamente en debug; degrada con gracia en release.

Lista operacional: qué recolectar en un bug de rendimiento de cliente

  • Modelo de GPU, versión de driver, versión de SO, compositor/sistema de ventanas (especialmente en Linux).
  • Versión de instancia/dispositivo Vulkan y extensiones/características habilitadas (regístralo una vez al inicio).
  • Histograma de tiempo de frame (no solo FPS promedio), con descripción de la escena repro.
  • Estadísticas de cache de pipeline: hits/misses, número de pipelines creados durante el juego.
  • Uso de VRAM en reposo y en pico; si se aproxima al presupuesto.
  • Configuración del swapchain: modo de present, conteo de imágenes, estado de vsync, ventana vs pantalla completa.
  • Una captura de un frame del punto repro (con ajustes consistentes).

Lista de ingeniería: “¿Estamos usando Vulkan a propósito?”

  • ¿Podemos explicar nuestro modelo de sincronización a un ingeniero nuevo en 10 minutos?
  • ¿Tenemos una política contra asignaciones por frame y creación de pipelines en runtime?
  • ¿Probamos al menos dos vendors de GPU semanalmente?
  • ¿Tenemos detección automatizada de device lost y telemetría de crash accionable?
  • ¿Podemos reproducir regresiones de rendimiento de forma determinista (seed fijo, ruta de cámara fija)?

Preguntas frecuentes

1) ¿Siempre es Vulkan más rápido que OpenGL o Direct3D?

No. Vulkan suele ser más rápido cuando estás limitado por CPU debido a la sobrecarga del driver, cambios de estado y coste de envío de draw calls.
Si estás limitado por GPU, las ganancias pueden ser pequeñas o inexistentes a menos que Vulkan permita mejor scheduling y menos stalls.

2) ¿Por qué Vulkan se siente tan verboso comparado con otras APIs?

Porque te hace especificar cosas que antes los controladores inferían: sincronización, layouts, asignación de memoria y estado de pipeline.
La verbosidad es el precio del determinismo y del control. Pagas en código para reducir conjeturas en los controladores.

3) ¿Debería habilitar capas de validación para pruebas de rendimiento?

No. Usa capas de validación para corrección, luego desactívalas para ejecuciones de rendimiento. La validación cambia el timing y puede ocultar o crear artefactos.
Trata “validación limpia” como una puerta antes del perfilado.

4) ¿Cuál es la causa número uno de tartamudeo en apps con Vulkan enviadas?

Creación de pipelines en tiempo de ejecución y compilación de shaders. El pico suele aparecer cuando aparece contenido nuevo.
Soluciona con compilación offline, warm-up, caches de pipeline y una estrategia de claves de pipeline estables.

5) ¿Realmente necesito un asignador como VMA?

Si vas a enviar algo no trivial, sí. La gestión manual de VkDeviceMemory es propensa a errores y tiende a degenerar en fragmentación,
fugas y políticas inconsistentes. Un asignador te da subasignación estandarizada, pooling y diagnósticos.

6) ¿Cómo sé si mis barreras son demasiado conservadoras?

Síntomas: el tiempo de GPU aumenta después de “hacer la sincronización segura”, o ves huecos donde los pases deberían solaparse.
Confirma con una línea temporal GPU en un profiler/captura: busca largos gaps de inactividad o serialización de colas. Luego ajusta máscaras de etapa/acceso y el alcance de dependencias.

7) ¿Qué modo de present debería usar?

FIFO es el más ampliamente soportado y se comporta como vsync. MAILBOX puede reducir latencia y tartamudeo cuando está disponible, pero puede cambiar el comportamiento de pacing.
Elige según latencia y pacing medidos, no por ideología. También considera el conteo de imágenes del swapchain: el doble buffering es frágil frente a picos.

8) ¿Por qué mi app funciona en Windows pero falla en Linux (o viceversa)?

A menudo es empaquetado del loader/ICD, distinto comportamiento de WSI, o suposiciones implícitas sobre coherencia de memoria y sincronización.
En Linux, detalles del compositor y del sistema de ventanas importan más de lo que los equipos esperan. Usa VK_LOADER_DEBUG y registra capacidades al inicio.

9) ¿Es “bindless” vía descriptor indexing siempre una ganancia?

No. Puede reducir la sobrecarga de CPU y simplificar el binding, pero puede estresar caches y crear variación entre vendors si se abusa.
Úsalo donde reduzca costes reales y organiza descriptores para localidad. Mide en tus GPUs objetivo.

10) ¿Qué significa “device lost” en la práctica?

Puede ser un fallo real de GPU, pero comúnmente es un bug de aplicación (acceso ilegal a memoria, sincronización inválida),
caos por OOM/eviction, o un timeout de watchdog por trabajo GPU de larga duración. Trátalo como un crash: captura telemetría y reproduce bajo validación.

Próximos pasos que realmente puedes tomar

La propuesta de valor de Vulkan es simple: control de bajo nivel predecible a cambio de asumir la responsabilidad que antes tenía el controlador.
Si ese intercambio se alinea con tu producto—renderer limitado por CPU, requisitos estrictos de pacing, determinismo multi-vendor—entra con los ojos abiertos.
Si no, elige algo de más alto nivel y gasta tu presupuesto de ingeniería en contenido y herramientas.

Pasos prácticos siguientes, en orden:

  1. Haz hábito correr capas de validación. Ejecútalas en CI y convierte “validación limpia” en una puerta de merge para cambios de render.
  2. Instrumenta tiempo de frame y comportamiento de caches. Rastrea tiempo de CPU/GPU/present por separado; registra hits/misses de cache de pipeline y creación de pipelines en runtime.
  3. Establece una política de memoria. Adopta un asignador, prohíbe asignaciones por frame y rastrea uso de VRAM frente al presupuesto.
  4. Codifica patrones de sincronización. Documenta convenciones de barreras, tiempos de vida de recursos y el modelo de frames en vuelo como un runbook de oncall.
  5. Construye un arnés de repro determinista. Escena fija, ruta de cámara fija, capturas de frame fijas. Si no puedes reproducir, no puedes mejorar.
  6. Prueba en múltiples vendors desde temprano y de forma continua. El comportamiento indefinido es una deuda que se acumula en el peor momento: justo antes del lanzamiento.

Vulkan es amado por la velocidad porque elimina excusas. Es odiado por la complejidad porque elimina excusas.
Si quieres el beneficio, acepta la responsabilidad—y construye la musculatura operacional para mantenerlo estable.

← Anterior
Correo: «451 temporary local problem» — encuentra rápido el problema del servidor
Siguiente →
Velocidad de restauración: MariaDB vs PostgreSQL — cómo lograr RTO inferior a 15 minutos

Deja un comentario