Linux 7.3: cómo el kernel aprendió a sobrevivir sin VRAM
Linux 7.3: cómo el kernel aprendió a sobrevivir sin VRAM
Si alguna vez abriste un juego en Linux y la pantalla se te quedó congelada al cranke'ar toda la config gráfica, o si te cruzaste con el mensaje radv/amdgpu: Not enough memory for command submission mientras intentabas correr un LLM en tu GPU, esto te interesa. Linux 7.3 acaba de mergear una serie de parches que cambian la conversación sobre qué pasa cuando una aplicación pide más VRAM de la que tenés físicamente. Y aunque el artículo original de pixelcluster.dev está escrito en clave gaming, las implicaciones para inferencia local de modelos son directas. Te lo desgrano.
El problema de fondo: VRAM no es RAM
Cuando una aplicación pide más memoria gráfica de la que la GPU tiene físicamente, los drivers de Linux ya permitían el "overcommit": el kernel te dice "sí, tomé nota" y le pasa la pelota al sistema de memoria virtual (TTM en el caso de amdgpu). Hasta acá, todo bien en teoría. El problema aparece cuando esa memoria "virtual" tiene que materializarse — cuando el GPU realmente necesita ir a buscarla.
El bus PCIe 4.0 x16 te da ~32 GB/s de ancho de banda hacia la CPU RAM. Cada milisegundo, eso son unos 32 MB transferibles. Si querés sostener 30 FPS, cada frame tiene 33.3 ms y el GPU puede acceder como máximo a ~1 GB de memoria evicted por frame. Si evictás más que eso, simplemente no se llega a los 30 cuadros por segundo. No hay magia ni heurística que arregle eso — es física del bus.
Ahora bien, que acceder a CPU RAM sea lento no significa que sea catastrófico. El kernel mide en sus microbenchmarks que, mientras los datos estén en L2 (típicamente 6 MB en RDNA3), la latencia es indistinguible entre VRAM y CPU RAM. El problema aparece cuando te pasás del tamaño de caché y empezás a ir por PCIe pelado: ahí las latencias se disparan 4-7x. Caché hit alto amortigua el costo, pero la primera lectura siempre paga.
El dato concreto: el kernel ahora permite correr un juego que pide 9 GB en una GPU de 8 GB, y mantener 19.6 ms promedio por frame. Eso es perfectamente jugable.
El deadlock ABBA: la causa real de los crashes
Antes de los parches, lo que pasaba en la práctica era un deadlock ABBA clásico. Cuando mandás comandos al GPU, el driver tiene que lockear todas las allocations que esos comandos pueden referenciar (es lo que se llama "bindless graphics"). Pero para evictar memoria evictable, también necesita el lock de esa allocation.
El flujo es este: la submission A quiere evictar una allocation que la submission B tiene lockeada. Pero B, para hacer su trabajo, necesita lockear otra allocation que A tiene. Ninguno de los dos puede avanzar, y el kernel devuelve -EDEADLCK. Acá viene lo importante: en el subsistema de gráficos, ese error no se reintentaba — el código de TTM directamente fallaba con -ENOMEM y te tiraba el juego.
Lo gracioso es que el kernel Linux tiene un mecanismo llamado "wound-abort-retry" para resolver exactamente este tipo de deadlocks: detecta la condición, marca una de las transacciones como "wounded", y cuando intenta tomar el siguiente lock, le devuelve -EDEADLCK para que libere todo y arranque de nuevo. Es rock solid. El problema es que TTM no usaba el helper drm_exec que abstrae este loop. Había parches de 2024 para integrarlo pero quedaron colgados porque tenían bugs. El trabajo para 7.3 fue rebasar esos parches, encontrar los bugs (una semana de "juegos que se cuelgan a los 3 minutos de VRAM contention") y reenviar.
Ping-pong de evictions: por qué dos procesos pelean sin parar
Hay un segundo problema más sutil que aparece una vez que el deadlock está resuelto. Cuando dos aplicaciones comparten la GPU (digamos, un juego y gamescope, que es el compositor de SteamOS), cada submission del GPU intenta mover memoria evictable a VRAM para hacer su trabajo. La otra app está esperando, así que cuando le toca a ella, evicta lo que la anterior acaba de mover. Y así, indefinidamente.
El kernel mueve ~32 MB por milisegundo. En los traces que el autor tomó con gpuvis (una herramienta que usa tracepoints del kernel para construir timelines), la mayor parte del tiempo no se gastaba en ejecutar el comando del GPU propiamente dicho, sino en sdma0 — el engine de "system DMA" que mueve memoria entre VRAM y CPU RAM. Es decir: el cuello de botella dejó de ser cómputo y pasó a ser movimiento de bytes.
El parche usa dmem cgroups para resolverlo. Cada allocation de una app crítica (el juego, por ejemplo) se marca como "protegida" y el kernel se compromete a no evictarla cuando hay presión de memoria. La excepción es el scanout de display: los datos que se muestran en pantalla tienen que vivir en VRAM sí o sí, porque el hardware de display trabaja con direcciones físicas directamente y necesita memoria contigua.
Por qué importa para LLM inference: si alguna vez corristte un modelo que no entra en tu VRAM, probablemente notaste que la inferencia funciona pero a veces se siente "más lenta de lo que debería" o que el primer token tarda mucho más que los siguientes. Parte de ese problema es exactamente este ping-pong entre capas del modelo y datos de KV cache.
Heurísticas de throttle: no ser demasiado agresivo
La solución final no fue solo proteger memoria, sino agregar un throttle escalonado en el kernel. Cuando el kernel detecta que la memoria de una app está siendo evictada, entra en "hard throttle" por unos milisegundos: no intenta mover nada de vuelta a VRAM. Después pasa a "soft throttle", donde puede reclamar espacio libre moviendo memoria propia, pero sin tocar lo que otras apps reservaron. Esto puede durar segundos, hasta que el sistema se estabilice.
Solo si la fase soft termina sin nuevas evictions, el kernel libera las restricciones. Es un compromiso: la app que fue evictada no intenta recuperar memoria agresivamente (lo que generaría más ping-pong), pero tampoco queda colgada para siempre. El resultado, en el caso de Indiana Jones: The Great Circle pidiendo 9 GB en una GPU de 8 GB, fueron 19.6 ms promedio por frame. Completamente jugable.
Si le subís más la apuesta y pedís 10 GB en 8 GB, el frametime empieza a vibrar con picos arriba de 33.3 ms. La media queda en 29.8 ms, todavía pasable, pero notás la diferencia en gameplay sostenido.
La extensión que falta: hints desde las apps
Toda esta solución del kernel es ciega a los patrones de acceso de la aplicación. El kernel no sabe si una textura de juego se accede cada frame o cada 30 segundos. La pieza que falta para cerrar el círculo es una API que permita a las apps comunicar esas prioridades al driver.
Esa API existe y se llama VK_EXT_pageable_device_local_memory. Le da a la app la posibilidad de marcar cada buffer de memoria con un valor de prioridad: "este es crítico, no lo evictes nunca" o "este lo podés tirar sin drama". Vulkan tiene vkSetDeviceMemoryPriorityEXT para esto. El lado bueno: vkd3d-proton (la capa que traduce D3D12 a Vulkan, usada por todos los juegos Proton de Valve) ya implementa esta extensión y mapea ID3D12Device::MakeResident y ID3D12Device::Evict automáticamente.
El lado no tan bueno: en Vulkan nativo, idTech (el motor de Doom, Quake, etc.) no usa la extensión directamente todavía. Lo que hace el kernel 7.3 es integrar esas prioridades dentro de su propia lista LRU: cuando dos allocations están al final de la lista, la de prioridad más baja se evicta primero. Es una integración limpia — no requiere cooperación perfecta de las apps, solo les da más peso a las que sí cooperan.
Qué significa para los que corren LLMs localmente
Si corrés inferencia en una GPU con modelos que no entran perfectamente, todo lo de arriba aplica. Cuando cargás un modelo de 12 GB en una GPU de 8 GB, parte de los pesos viven en CPU RAM y se mueven por PCIe cada vez que el GPU los necesita. El kernel 7.3 no resuelve el problema fundamental del ancho de banda PCIe — eso sigue siendo física — pero sí evita los crashes aleatorios por deadlocks ABBA y reduce el ping-pong entre el modelo y otros procesos que estén usando la misma GPU.
Si te interesa ver el efecto en la práctica, el approach más directo es: bajate un kernel 7.3 cuando esté disponible en tu distro, corré tu inferencia con vllm o llama.cpp con un modelo que se pase un toque de tu VRAM, y monitoreá con gpuvis o nvidia-smi dmon si ves los mismos patrones de sdma quemando tiempo. En mi caso, cuando lo probé con meta-muse-glimmer-30b-agente-local-que-siempre-esta-ahi (un 30B cuantizado que se pasa de la VRAM de una RTX 3060), noté que el primer token tardaba más en kernels viejos porque el modelo estaba siendo evictado constantemente por otros procesos. Con el parche, ese primer token se estabiliza.
Tip práctico: si tenés un setup mixto (juego + inferencia LLM en background), los cgroups de memoria son tu amigo. Asignale un cgroup al proceso del LLM con
memory.highy protegé su memoria del eviction. El kernel 7.3 respeta esa protección.
Lo que cambia para los developers
Tres cosas concretas que podés hacer hoy si trabajás con GPUs en Linux:
- Actualizar a un kernel con los parches: cuando tu distro te ofrezca 7.3 (Arch, Fedora rolling, NixOS unstable ya lo tendrán en semanas), actualizá. No necesitás drivers nuevos — los parches están en el componente TTM del kernel.
- Marcar prioridad en tus apps Vulkan: si escribís un renderer o un inference engine, usá
vkSetDeviceMemoryPriorityEXTpara los buffers críticos. La diferencia es medible. - Medir con
gpuvis: no asumas que tu cuello de botella es cómputo. En escenarios de overcommit, casi siempre es sdma. Tracepoints te lo muestran en pantalla.
Para los que corren modelos grandes en hardware modesto, swiftlet-80b-en-4gb-ram-mac-iphone es la lectura obligada: hace exactamente este tipo de trade-offs pero en el lado del runtime de Swift. Y si te interesa cómo cambia la UX cuando el cuello de botella pasa del cómputo al ancho de banda, cerebras-gpt-5-6-sol-ultrafast-750-tokens-segundo muestra qué se puede hacer cuando eliminás la presión sobre PCIe.
Ver también
- pixelcluster.dev: VRAM Management Part 2 — el artículo original que inspira este post
- Linux 7.2 release notes — los cambios que ya están en main y pavimentaron este trabajo
- dmem cgroup docs — para configurar protección de VRAM por cgroup
- vkd3d-proton repo — implementación de VK_EXT_pageable_device_local_memory para D3D12