Transformers documentation
Arquitectura de lotes continuos
Arquitectura de lotes continuos
El procesamiento por lotes tradicional procesa un grupo fijo de solicitudes a la vez y espera a que la más lenta termine antes de comenzar con el siguiente grupo. Esto provoca que la GPU permanezca inactiva entre lotes.
Con el loteo continuo, en cada paso de generación, el programador verifica las solicitudes finalizadas y las reemplaza de inmediato por las que están en espera. Las solicitudes cortas se eliminan en cuanto terminan, y la GPU permanece ocupada todo el tiempo. El resultado es un aumento significativo del rendimiento y una reducción de la latencia media.
Ciclo de vida de la solicitud
Una solicitud pasa por cuatro estados desde su envío hasta su finalización.
WAIT IN QUEUE LOAD PROMPT INTO KV STREAM OUTPUT DONE
┌────────────────┐ ┌─────────────────────┐ ┌─────────────────┐ ┌────────┐
│ PENDING │───▶│ PREFILLING │───▶│ DECODING │───▶│FINISHED│
│ ○ ○ ○ · · · │ │ ████████░░░░░░░░░░ │ │ → → → → · · │ │ ✓ │
└────────────────┘ └─────────────────────┘ └─────────────────┘ └────────┘
others ahead prompt chunks +1 token/step- Pendiente: la solicitud está en cola y esperando para ser programada.
- Prefilling: los tokens del prompt se procesan en una pasada hacia adelante.
- Decodificación: los tokens de salida se generan uno a la vez.
- Completado: la generación ha finalizado y el resultado está disponible.
Una solicitud se mueve de pendiente a prellenado cuando el programador encuentra suficiente presupuesto de tokens y espacio en caché para admitir la solicitud. Si el prompt es demasiado largo para caber en un solo paso, el programador lo divide en varias pasadas hacia adelante (chunk prefill).
Prefill por bloques
Procesar un prompt largo en un solo paso es costoso porque bloquea la generación de otras solicitudes. El prefill por fragmentos (chunked prefill) resuelve esto dividiendo los prefills grandes en varios pasos.
Blocking (long prefill blocks the batch) Step: 1 2 3 4 Req A [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] ← one huge prefill Req B ················ wait ················ Req C ················ wait ················ Chunked (same prompt split and interleave with decode) Step: 1 2 3 4 Req A [▓▓ pre] [▓▓ pre] [▓▓ pre] [→ dec] Req B [→ dec] [→ dec] [▓▓ pre] [→ dec] Req C [→ dec] · idle · [→ dec] · idle ·
Cuando el prompt de una nueva solicitud supera el presupuesto de tokens disponible (establecido por max_batch_tokens en ContinuousBatchingConfig), el programador procesa tantos tokens como sea posible y retiene el resto. En los pasos posteriores, continúa desde donde lo dejó, intercalando el trabajo de prefill con los pasos de decodificación en curso. Esto mantiene el lote productivo y reduce el tiempo hasta el primer token para otras solicitudes.
Programador
El programador decide qué solicitudes se unen a cada pasada hacia adelante en función de dos presupuestos y un límite de solicitudes.
- Presupuesto de tokens: el número máximo de tokens de consulta procesados en una sola pasada hacia adelante, definido por
max_batch_tokensen ContinuousBatchingConfig. - Presupuesto de caché: el número total de páginas KV que se pueden leer en una sola pasada, limitado por el tamaño total de la caché.
- Límite de solicitudes: el número máximo de solicitudes en una sola pasada hacia adelante, definido por
max_requests_per_batch. El tensor de logits escala con el tamaño del vocabulario, por lo que este límite acota la memoria máxima para modelos con vocabulario grande.
FIFO
FIFOScheduler es el programador predeterminado. Rellena el lote en orden de prioridad. Primero se procesan las solicitudes de decodificación activas, luego las solicitudes de prellenado activas y, a continuación, las nuevas solicitudes en espera en orden de llegada. Esto maximiza el rendimiento al hacer avanzar las solicitudes existentes antes de incorporar nuevas.
PrefillFirst
PrefillFirstScheduler completa las operaciones de prefill por fragmentos antes de reanudar el decodificado. Esto reduce la fragmentación en cargas de trabajo dominadas por prompts largos.
Establezca el programador en ContinuousBatchingConfig.
cb_config = ContinuousBatchingConfig(scheduler_type="prefill_first")Gestión de memoria
La caché KV se gestiona por páginas para que las solicitudes de diferentes longitudes puedan compartir un pool de memoria fijo sin fragmentación. Un pool compartido de fragmentos de memoria, implementado como CachePool, proporciona dichos fragmentos a cada asignador de tipo de atención, que los divide en bloques y páginas.
Shared pool (first two chunks reserved for padding / never allocated)
chunk N [ ======================================== ]
| |
full attn: [ FULL BLOCK 0 | FULL BLOCK 1 | … ]
[ page L0 | page L0 | … ] (one page per full layer)
sliding: [ ----------- SLIDING BLOCK 0 ----------- ]
[ page L1 | page L2 | page L3 | … ] (one page per sliding layer)
page = page_size tokens of KV for one layer
block = one page per layer of that attention type
chunk = pool unit sized so full and sliding blocks tile the same bytesEl loteo continuo solo admite dos tipos de atención: full_attention (FullAttentionCacheAllocator) y sliding_attention (SlidingAttentionCacheAllocator). Los modelos híbridos obtienen un asignador de cada tipo. El tipo de atención proviene del modelo (layer_types / sliding_window), no de ContinuousBatchingConfig.
Las capas deslizantes conservan solo una ventana fija de tokens recientes mediante el reciclaje de un pequeño conjunto de páginas. No admiten tablas de bloques de kernel ni el intercambio de bloques.
La reserva de caché destina dos fragmentos por delante de los fragmentos de datos para las lecturas y escrituras de relleno que las solicitudes nunca utilizan. Esos fragmentos nunca se asignan a las solicitudes.
Dimensionamiento de la caché
El gestor infiere cuántos fragmentos caben al inicio a partir de la memoria GPU libre. Tiene en cuenta los tensores KV, las máscaras de atención, las activaciones y los índices de gestión, y luego dimensiona el pool para que quepa dentro de max_memory_percent de la memoria disponible. Cuando no se establece, toma un valor predeterminado según el hardware y los parámetros de lotes continuos. MPS reserva menos espacio para la caché, donde la CPU y la GPU comparten memoria, para evitar el intercambio. num_blocks expresa la capacidad como bloques completos del modelo de page_size tokens cuando se establece explícitamente.
Puede fijar los valores en ContinuousBatchingConfig.
cb_config = ContinuousBatchingConfig(
page_size=256,
num_blocks=4096,
max_batch_tokens=512,
max_memory_percent=0.8,
)Admisión
Antes de que una solicitud se una al lote, el programador verifica que exista capacidad libre suficiente para cada asignador de tipo de atención. Si algún asignador no contara con suficiente capacidad, la solicitud se rechaza y no se asigna nada.
Para evitar que la caché se llene hasta que el offloading sea la única opción, el programador aplica un safety_margin. Una vez que la capacidad libre de la caché cae por debajo de safety_margin, se retienen los nuevos prefills y solo continúan los decodes activos. El programador FIFO tiene un valor predeterminado de 0.15 (15% de la capacidad en reserva), mientras que prefill_first tiene un valor predeterminado de 0.0. Establezca safety_margin en ContinuousBatchingConfig para ajustarlo, donde 0 desactiva el margen.
Caché de prefijos
Cuando dos solicitudes comparten un prefijo de prompt, pueden compartir los bloques que contienen el KV para ese prefijo. Cada bloque completado se ha calculado su hash de contenido. Una solicitud posterior con un prefijo coincidente reutiliza el bloque y omite el prefill para esos tokens. Los bloques compartidos se cuentan por referencia y solo vuelven al pool de libres cuando todas las solicitudes que los utilizan han finalizado.
allow_block_sharing tiene un valor predeterminado de True. El uso compartido de prefijos está activo solo cuando todos los asignadores admiten el uso compartido, por lo que solo los modelos con atención completa lo utilizan. Los modelos de deslizamiento puro e híbridos lo desactivan. Establezca allow_block_sharing=False para cargas de trabajo con indicaciones cortas y generaciones largas, donde la gestión administrativa supera a los ahorros.
Eviction
Los bloques liberados no se borran y permanecen “inicializados” con su contenido y su hash intactos, de modo que una coincidencia de prefijo posterior pueda reutilizarlos. Cuando el pool no inicializado se agota, el gestor degrada de forma perezosa el bloque inicializado más reciente a no inicializado, descartando su hash. La caché de prefijo es una capa de esfuerzo máximo que el asignador descarta para seguir atendiendo nuevas solicitudes.
Reinicio suave
Un reinicio suave es la ruta de respaldo para el offloading. Cuando el gestor no puede copiar la caché KV de una solicitud al pool de intercambio de CPU, porque el offloading de CPU está desactivado o el pool está lleno, realiza un reinicio suave de la solicitud en su lugar. Añade los tokens generados de la solicitud a su prompt original, libera sus bloques de caché y la vuelve a añadir a la cola. La sección Offloading detalla qué solicitudes se eligen.
Cuando la caché vuelve a tener espacio, la solicitud se reanuda desde donde se interrumpió, con su historial de generación codificado en el prompt.
Gráficos de CUDA
Cada paso de generación implica una sobrecarga de CPU, desde la ensamblaje del lote hasta la ejecución de los kernels de GPU y la lectura de los resultados. Los gráficos de CUDA eliminan esta sobrecarga al registrar la secuencia completa de ejecución en la GPU una sola vez y reproducirla para lotes con formas coincidentes.
Como las formas del lote cambian en cada paso, el sistema de lotes continuos lo gestiona mediante relleno y almacenamiento en caché.
- Las longitudes de consulta se rellenan hasta el múltiplo más cercano de
q_padding_interval_size. - Las longitudes de KV se rellenan hasta el múltiplo más cercano de
kv_padding_interval_size. - Los grafos grabados se almacenan en una caché.
Cuando la forma rellena de un lote coincide con un gráfico en caché, el gráfico se reproduce sin ninguna asignación en la CPU. Las nuevas formas activan una nueva captura de gráfico.
Los gráficos de CUDA requieren un flujo de control estático y son incompatibles con las máscaras de atención. Se detectan de forma automática por defecto y se desactivan cuando no se cumplen las condiciones.
Loteo asíncrono
El loteo asíncrono utiliza dos pares de búferes de E/S y dos flujos de CUDA para superponer el trabajo de la CPU y la GPU.
Sequential CPU ── [prep N] ·········idle········· [prep N+1] ·········idle········· GPU ·········idle········· [compute N] ·········idle········· [compute N+1] Async CPU ── [prep N] [prep N+1] [prep N+2] ── GPU ── ········ [compute N] [compute N+1] ──
Mientras la GPU calcula el lote N, la CPU prepara el lote N+1. Sin embargo, superponer ambos requiere aproximadamente el doble de VRAM.
El loteo asíncrono requiere que los gráficos de CUDA estén activos, ya que la reproducción de gráficos proporciona las direcciones de tensor estables necesarias para que la superposición de flujos sea correcta.
Descarga
Las solicitudes que generan salidas largas pueden crecer hasta que la caché KV no tenga espacio para asignar el siguiente bloque. En lugar de fallar, el gestor devuelve las solicitudes activas a la cola de espera para liberar bloques para las solicitudes que permanecen.
El offloading se basa en la demanda. El programador informa sobre las solicitudes stalled, es decir, las solicitudes activas que no pudieron asignar los bloques que necesitan, junto con la cantidad de bloques que cada una exige. El gestor realiza el offloading de la menor cantidad de solicitudes necesaria para liberar suficientes bloques y cubrir esa demanda. Realiza el offloading comenzando por las más recientes, de modo que las solicitudes ya programadas en el lote actual conserven su caché, y la última solicitud activa restante nunca se descargue.
Dos situaciones activan el descargado.
- Se programa un lote, pero algunas solicitudes activas se estancan. El gestor descarga suficientes solicitudes estancadas para que el siguiente lote pueda asignar bloques para el resto.
- La caché está llena y no se puede programar ningún lote. El gestor descarga las solicitudes y reintenta la programación en un bucle dentro del mismo paso, hasta que quepa un lote o no se pueda descargar ninguna solicitud. Ya no espera al siguiente pase hacia adelante para recuperarse.
Una solicitud descargada sigue uno de dos caminos. Cuando cpu_offload_space es mayor que 0.0, el gestor copia los bloques de la caché KV de la solicitud a un búfer de CPU fijo preasignado, agrupando todas las solicitudes que caben en el pool de intercambio en una única transferencia. Una vez que hay espacio disponible en la caché de la GPU, el gestor copia los bloques de vuelta a la GPU y reanuda la solicitud sin recalcular su prompt ni los tokens generados. Las solicitudes que no caben en el pool, o todas las solicitudes descargadas cuando la descarga a CPU está desactivada, recurren a un soft reset.
Próximos pasos
- La entrada de blog sobre Continuous batching aborda el almacenamiento en caché de KV, el prefill por fragmentos y la programación dinámica, con cifras de rendimiento de las pruebas de referencia.
- Para ver ejemplos de uso, consulte la documentación de Continuous batching.