Cuando se ejecutan LLM a escala, la verdadera limitación es la memoria de GPU en lugar de la computación, principalmente porque cada solicitud requiere una caché KV para almacenar datos a nivel de token. En las configuraciones tradicionales, se reserva un gran bloque de memoria fija por solicitud en función de la longitud máxima de la secuencia, lo que genera un espacio no utilizado significativo y limita la simultaneidad. Paged Attention mejora esto al dividir el caché KV en fragmentos más pequeños y flexibles que se asignan solo cuando es necesario, de manera similar a cómo funciona la memoria virtual. También permite que múltiples solicitudes con el mismo mensaje inicial compartan memoria y solo la dupliquen cuando sus resultados comiencen a diferir. Este enfoque mejora enormemente la eficiencia de la memoria, permitiendo un rendimiento significativamente mayor con muy poca sobrecarga.
En este artículo, simulamos el ingenuo asignador de caché KV, creamos una implementación funcional de Atención paginada con una tabla de bloques y uso compartido de prefijos de copia en escritura, y medimos la brecha de utilización en tamaños de lotes de 10 a 200 solicitudes simultáneas.
Antes de simular algo, necesitamos saber cuánta memoria de GPU cuesta realmente un solo token. Esto depende completamente de la arquitectura del modelo. Usamos una configuración estilo GPT: 32 capas, 32 cabezas de atención, 128 dimensiones por cabeza, almacenadas en fp16. El factor de 2 al principio representa las proyecciones de clave y valor (no hay caché Q; las consultas se recalculan en cada paso). Multiplicarlos nos da 524.288 bytes, o 512 KB, por token. Esta es la unidad fundamental sobre la que se construye todo lo demás: los tamaños de preasignación, el recuento de páginas y la memoria desperdiciada se escalan directamente a partir de este número.
El enfoque ingenuo es simple: cuando llega una solicitud, se asigna un bloque contiguo de memoria de GPU con el tamaño de la longitud máxima de secuencia (2048 tokens en este caso). Esto sucede porque la duración de la respuesta se desconoce desde el principio, por lo que se reserva el peor de los casos.
AVG_RESPONSE está establecido en 500, que es un promedio realista para un chatbot de producción. Multiplicar por KV_MB_PER_TOKEN da lo que realmente está escrito versus lo que estaba bloqueado. La brecha es el desperdicio.
Los números concretan el problema. Cada solicitud preasigna 1024 MB, pero utiliza solo 250 MB (24,4 % de utilización). Los 774 MB restantes permanecen reservados durante toda la duración de la solicitud y no están disponibles para ninguna otra solicitud. Entre 100 usuarios simultáneos, son 75 GB de memoria GPU que no hacen nada. Este no es un caso extremo: es el comportamiento predeterminado de todo sistema que no implementa la asignación paginada, y es exactamente la razón por la que los sistemas de servicio ingenuos chocan contra un muro OOM mucho antes de que la GPU se sature computacionalmente.
Aquí se presentan dos clases para simular cómo funciona realmente la atención paginada en el nivel de administración de memoria.
PagePool representa el grupo de memoria física de la GPU: una matriz plana de páginas del mismo tamaño, cada una con 16 tokens. Mantiene una lista gratuita y un recuento de referencias por página. Cuando el recuento de referencias de una página cae a cero, se devuelve inmediatamente a la lista gratuita y queda disponible para cualquier nueva solicitud. Esta es la diferencia clave con la asignación ingenua: no hay agujeros reservados, ni fragmentación, ni memoria vinculada a una solicitud finalizada.
PagedRequest representa una única solicitud de inferencia. Contiene un block_table, una lista que asigna índices de páginas lógicas a identificadores de páginas físicas en el grupo. Cada vez que se llama a generate_token() y el recuento de tokens cruza el límite de una página, se reclama una nueva página física del grupo. Ningún recuerdo se toca antes de que sea necesario.
Se ejecutan cinco solicitudes con recuentos de tokens de 320, 48, 160, 96 y 272. El resultado muestra páginas asignadas proporcionalmente al uso real: req-1 con 48 tokens obtiene 3 páginas, req-0 con 320 tokens obtiene 20. Cuando se libera req-1, sus 3 páginas regresan directamente al grupo y se pueden reutilizar inmediatamente. La utilización del grupo del 10,9% parece baja solo porque se aprovisionaron 512 páginas para 5 solicitudes pequeñas; en un grupo de producción completamente cargado, se ubicaría cerca del rango del 98% visto en la Sección 4. "0 tokens desperdiciados" en la columna de la última página es un artefacto inicial: los cinco recuentos de tokens resultan ser múltiplos exactos de 16. En la práctica, el desperdicio promedio de la última página es PAGE_SIZE / 2 = 8 tokens por solicitud.
En producción, casi todas las solicitudes a un LLM implementado llevan el mismo mensaje del sistema: las instrucciones que definen el comportamiento del modelo. Bajo una asignación ingenua, cada una de esas solicitudes almacena su propia copia completa del caché KV del indicador del sistema. Con 10 solicitudes simultáneas y un aviso del sistema de 200 tokens, es decir, 10 copias idénticas de los mismos datos que ocupan regiones de memoria separadas.
Aquí se reutiliza el mismo PagePool de la Sección 2, ampliado con dos métodos: share() incrementa el recuento de referencias de una página sin asignar nada nuevo, y cow_copy() asigna una página nueva y disminuye el recuento de referencias en la original. Se crea una instancia de un nuevo grupo y el mensaje del sistema se codifica en 13 páginas: math.ceil(200/16). Cada una de las 10 solicitudes de usuario llama a share() en las 13 páginas, apuntando sus tablas de bloques a la misma memoria física. No se asignan páginas nuevas. El recuento de árbitros en cada página compartida simplemente aumenta a 11.
Los ahorros son inmediatos: la asignación ingenua requeriría 130 páginas en 10 solicitudes. Con CoW, sólo existen 13 páginas físicas. Son 936 MB guardados desde un único prefijo compartido.
Cuando req-3 genera su primer token único, se llama a cow_copy() en su última página compartida: la página 12. Se asigna una nueva página 13 como copia privada de req-3 y el recuento de referencias en la página 12 se reduce en uno. Las otras 9 solicitudes continúan apuntando a la página 12, sin verse afectadas en absoluto. Este es el contrato CoW: compartido hasta la divergencia, privado sólo cuando sea necesario.
Se definen dos funciones para medir la utilización bajo cada enfoque en diferentes tamaños de lotes.
naive_utilisation extrae recuentos de tokens de una distribución normal con avg=500 y std=200, recortados a [200, 2048]. Esto refleja una distribución de producción realista: la mayoría de las respuestas oscilan entre 200 y 800 tokens, aunque ocasionalmente hay respuestas largas. Para cada solicitud, el bloque completo de 2048 ranuras se preasigna independientemente. La utilización es entonces actual_tokens_sum / (2048 × n): la proporción entre lo que se escribió y lo que se reservó.
paged_utilisation toma el mismo recuento de tokens real pero calcula cuántas páginas necesitaría cada solicitud: ceil(tokens/16). El único desperdicio es la cola sin completar de la última página de cada solicitud, que tiene un promedio de 8 tokens. La utilización es suma_tokens_actual / (páginas_asignadas × 16).
Los resultados se analizan en tamaños de lote de 10, 25, 50, 100 y 200. La utilización ingenua ronda el 24 % en todos los tamaños de lote, con cierta variación en lotes más pequeños debido al ruido del muestreo, que es exactamente avg/max_seq = 500/2048. No mejora con la escala porque el desperdicio es estructural, no estadístico.
La utilización paginada se mantiene estable en ~98,5 % independientemente del tamaño del lote, porque el desperdicio por solicitud está limitado por una única página parcial y no escala con max_seq_len en absoluto. La brecha entre los dos números (aproximadamente 74 puntos porcentuales) es directamente lo que permite a vLLM acomodar entre 2 y 4 veces más solicitudes simultáneas en la misma memoria de GPU.
Consulta el cuaderno completo aquí. Además, no dude en seguirnos en Twitter y no olvide unirse a nuestro SubReddit de más de 120.000 ML y suscribirse a nuestro boletín. ¡Esperar! estas en telegrama? Ahora también puedes unirte a nosotros en Telegram.

Soy graduado en ingeniería civil (2022) de Jamia Millia Islamia, Nueva Delhi, y tengo un gran interés en la ciencia de datos, especialmente las redes neuronales y su aplicación en diversas áreas.