Preenchimento e descodificación desagregados
A separación do preenchimento e a descodificación entre dispositivos acelera a inferencia de LLM e mellora o rendemento
Para xerar tokens de saída a partir dun aviso de entrada, a inferencia de LLM divídese en dúas etapas: preenchimento e descodificación. O preenchimento execútase nos tokens de caché, enchendo as cachés KV, antes de entrar na etapa de descodificación que xera tokens un a un. Aínda que un único paso de descodificación adoita executarse durante decenas de milisegundos, o preenchimento leva substancialmente máis tempo. Se se executa nos mesmos dispositivos, mesturar o preenchimento coa descodificación degrade o rendemento da descodificación. neste artigo exploramos unha solución establecida en forma de preenchimento e descodificación desagregados, executándoos en dispositivos separados para maximizar tanto o rendemento de preenchimento como as latencias de descodificación.
Rendemento de preenchimento vs descodificación
Nun motor de servizo de LLM típico, o planificador de lotes selecciona solicitudes para procesar en cada paso de execución dun modelo. Cando se executa nun único dispositivo ou nodo, tanto as solicitudes de preenchimento como de descodificación agrúpanse en lotes. O custo da atención, que se agrega ao longo da lonxitude da secuencia, crece tanto para o preenchimento como para a descodificación, proporcionalmente á lonxitude das entradas na caché KV (kv_len). As solicitudes de descodificación adoitan enviar un único token (qo_len=1), cun custo mínimo a través doutras capas que operan de forma independente nos tokens dunha secuencia. As solicitudes de preenchimento envían miles ou decenas de miles de tokens cun custo significativo a través de capas densas (qo_len grande).
A latencia dun pase de avance está máis fortemente influenciada polo número de tokens independentes pasados a través de capas densas (qo_len) que polo número de tokens recuperados da caché KV durante a atención (kv_len). A atención pode paralizarse tanto a través do número de solicitudes como do kv_len proporcionalmente ás lonxitudes das secuencias, acadando unha boa utilización. O preenchimento está limitado polo cálculo: ao ser alto qo_len, os núcleos GEMM poden asignar suficientes bloques ao longo da dimensión M para utilizar plenamente as capacidades de cálculo das GPUs modernas. A descodificación está limitada pola memoria: debido a tamaños de lote normalmente baixos, o número de entradas ao longo de M adoita ser pequeno, suficiente para un só bloque. Aínda que os núcleos Split-K GEMM poden mellorar a utilización de SM para tamaños de lote de tokens baixos, as cachés e as unidades de multiplicación de matriz adoitan permanecer infrautilizadas.
Cando se mesturan, os lotes que conteñen solicitudes de preenchimento incorren en latencias máis altas a través do pase de avance, afectando negativamente ao rendemento de descodificación de toda a instancia. Aínda que mesturar solicitudes de preenchimento con solicitudes de descodificación ou empregar o preenchimento fragmentado pode mellorar lixeiramente o rendemento da descodificación, é difícil manter un rendemento de preenchimento suficiente para procesar suficientes solicitudes nunha instancia para maximizar o rendemento de descodificación. No caso de modelos grandes, con lonxitudes de saída típicas, para manter un tamaño de lote grande para a descodificación, o preenchimento debe realizarse con suficiente frecuencia como para degradar significativamente a latencia media e causar intermitencias na saída.
Estes problemas pódense solucionar empregando un conxunto separado de nodos para realizar o preenchimento e a descodificación. Ao asociar un nodo de preenchimento con varios nodos de descodificación, pódense programar suficientes solicitudes para o preenchimento para maximizar o rendemento e manter un número suficientemente grande de solicitudes concorrentes nos nodos de descodificación para maximizar tamén o rendemento de descodificación. Os nodos de preenchimento enchen as cachés KV, que logo se transfiren aos nodos de descodificación. Xa que os descodificadores xa non teñen que deterse para o preenchimento, as latencias fanse moito máis deterministas, xa que o impacto xeral do crecente kv_len das solicitudes activas é moito menos pronunciado. O custo pagar en forma dun incremento no tempo ata o primeiro token (TTFT), xa que a transferencia de cachés KV a través da rede pode levar de decenas a centos de milisegundos.
Mensaxeiro KV
En Perplexity, a nosa implementación para o preenchimento e a descodificación desagregados está construída arredor dun mensaxeiro KV que interactúa co motor LLM para orquestrar as transferencias de caché KV desde os nodos de preenchimento ata os nodos de descodificación a través dunha rede. No lado do preenchimento, o mensaxeiro acepta solicitudes dos nodos de descodificación, entregándollas ao planificador de lotes e facendo un seguimento da execución do pase de avance para enviar as cachés KV coa menor latencia posible. No lado do descodificador, despois de que se asignen páxinas non desaloxables, o mensaxeiro bloquea a solicitude para que non sexa programada para a descodificación ata que se lle notifique a finalización da caché KV e das transferencias de contexto do descodificador.
A desagregación do preenchimento require conexións de alto rendemento e baixa latencia, polo que a nosa implementación está adaptada para RDMA, admitindo tanto controladores de interface de rede (NIC) EFA como ConnectX. O mensaxeiro KV está construído sobre libfabric, utilizando os nosos contenedores fabric-lib para proporcionar abstraccións de maior nivel e baixa latencia sobre as primitivas de acceso remoto directo a memoria (RDMA), implementando transferencias eficientes de páxinas e metadatos, xunto con sinalización de baixa latencia. En segundo plano, fabric-lib coordina unha GPU e os seus NICs conectados directamente para copiar datos desde o nodo de preenchimento ao nodo de descodificador.
Tras a recepción, o nodo de preenchimento asigna un conxunto correspondente de páxinas KV de orixe e programa a solicitude de preenchimento utilizando o seu motor local. Para minimizar a latencia, as transferencias non agardan polo pase de avance: no seu lugar, as copias de páxinas KV inícianse tan pronto como o modelo remata de engadir entradas de caché KV á caché KV para capas individuais. Dado que as solicitudes de preenchimento pódense fragmentar, o planificador de lotes notifica ao mensaxeiro KV dos fragmentos programados actualmente antes da execución. Para admitir gráficos CUDA á vez que se poden rastrear as capas, o mensaxeiro mantén un fío adicado que sondea un contador incrementado despois da proxección de saída da atención. O contador mantíxese só no nodo principal nun ambiente fragmentado: malia que as entradas de caché KV son válidas despois da adición e antes da atención, a proxección de saída redúcese entre rangos, sincronizándoos implicitamente. Unha vez que se observa un cambio no contador, notifícase ao mensaxeiro e este chama a fabric-lib para iniciar a transferencia dunha capa.
Despois de que se complete a transferencia do último fragmento, tamén se copian os metadatos adicionais: a descodificación especulativa ou MTP requiren que os logits e os estados ocultos se movan ao descodificador. Estas copias tamén se realizan a través de RDMA, cara e desde búferes preasignados.
Tras a finalización de todas as transferencias pendentes do último fragmento, o nodo de preenchimento desasigna as páxinas KV e completa a solicitude. O nodo do descodificador non se notifica explícitamente; no seu lugar, usa contadores inmediatos para facer un seguimento do número de operacións completadas. O número de operacións RDMA no lado do preenchimento é proporcional ao número de páxinas transferidas. Tras a finalización do número coñecido de copias de páxinas e contextos, fabric-lib chama ao mensaxeiro KV para indicar que unha solicitude está lista para a descodificación. O mensaxeiro desasigna calquera contexto e entrega a solicitude ao motor LLM.
Transferencias de caché KV fragmentadas
Se o preenchimento e o descodificador confían no paralelismo de tensores (TP) e fragmentan ou replican as cachés KV de forma idéntica, un único motor de transferencia coordina varios dispositivos para enviar e recibir as páxinas de todas as réplicas. Para poder usar un único mensaxeiro e motor de transferencia a pesar de que o executor do modelo está replicado en múltiples dispositivos e procesos, úsanse cuMem e cuMemImportFromShareableHandle para asignar a memoria do dispositivo que sustenta as cachés KV e mapeala no proceso principal. O motor de transferencia inspecciona a topoloxía do nodo para atopar os NICs e as CPUs no nodo NUMA máis próximo para usar nas transferencias de cada unha das porcións da caché KV.
Se a orixe e o destino se fragmentan de forma idéntica, as transferencias son triviais xa que hai unha asignación un a un desde os dispositivos e páxinas da orixe e do destino. Nesta situación, a fragmentación axuda implicitamente ás latencias de transferencia: mediante o uso de máis GPUs, pódense empregar máis NICs asociados, achegándose á utilización total da ancho de banda. Con todo, se hai unha discrepancia, o motor de transferencia debe dividir ou reconstruír páxinas dependendo da proporción entre as porcións de orixe e destino.
Se o preenchimento divide a caché KV entre máis dispositivos, as páxinas completas reconstrúense no descodificador enviando as metades correspondentes desde os dispositivos de preenchimento. Se o descodificador ten máis fragmentos, recibe páxinas de múltiples fontes. O descodificador necesita coñecer o esquema de fragmentación do preenchimento para poder calcular o número de escrituras RDMA que se agarda recibir. Se hai implicada replicación, o preenchimento agrupa os dispositivos en conxuntos de réplicas que replican a caché KV completa dentro de si mesmos. Os conxuntos de réplicas de destino asínganse aleatoriamente un dos conxuntos de orixe para usar todos os dispositivos dispoñibles para iniciar escrituras RDMA.
As transferencias fragmentadas requiren un lixeiro axuste nas cachés KV. Por defecto, FlashInfer confía no deseño NHD, que ordena os tokens dentro dunha páxina dentro das cabezas. Dado que é moi probable que as cachés estean fragmentadas ao longo do número de cabezas de atención, isto crea discontinuidade dentro da cabeza. As transferencias RDMA non admiten implicitamente escrituras con saltos, o que require unha operación por cabeza para realizar a transferencia. No seu lugar, para reducir o número de interaccións con libfabric, organizamos as cachés KV usando o deseño HND que coloca a dimensión da cabeza antes do número de tokens. Isto garante a continuidade, permitindo que unha páxina se copie cunha única escritura.
Descodificación especulativa
A descodificación especulativa require lixeiros axustes para o preenchimento-descodificación desagregados. Na nosa implementación, aos nodos de preenchimento non se lles permite mostras de tokens. Dado que os modelos Sonar de Perplexity admiten a saída estruturada, non queremos incorrer na complexidade de sincronizar as implementacións do procesador de esquemas entre preenchidores e descodificadores. Nos mecanismos MTP e de descodificación especulativa, preencher o modelo de borrador ata o último token implica mostrar tokens do modelo de destino.
Para evitar estes problemas, o preenchimento non inclúe o último token da secuencia de entrada. No seu lugar, transfírense os estados ocultos ou logits do preenchimento anteriores ao último token e trátase como un token de descodificación no seguinte paso do descodificador. Aínda que isto aumenta lixeiramente as latencias, xa que se debe realizar un paso de descodificación completo despois do preenchimento para emitir o primeiro token, a complexidade da implementación redúcese considerablemente.
Despregamentos desagregados
Implementamos ou experimentamos con múltiples configuracións desagregadas con diferentes modelos, para admitir tráfico de produción ou cargas de traballo de avaliación interna. Baseándonos no tamaño e no mecanismo de atención dos modelos, eliximos esquemas de fragmentación axeitados para os nodos de preenchimento e descodificador para utilizar da mellor maneira as GPUs.
DeepSeek-R1
Con DeepSeek, consideramos tanto os despregamentos paralelos de tensores (TP) como os paralelos de datos (DP). Como se comentou en publicacións de blogs anteriores, os despregamentos TP ofrecen unha mellor latencia a costa dun menor rendemento, o que require máis GPUs para atender tráfico pesado. Os despregamentos DP escalan moito mellor coa carga, con todo o seu rendemento máximo é inferior debido ao custo da comunicación entre dispositivos ou entre nodos.
DeepSeek baséase na atención latente multi-cabeza, comprimindo as cachés KV. Dado que todas as cabezas KV están comprimidas nun único vector latente, o TP non pode fragmentar as cachés KV, xa que debe replicar os vectores latentes en todos os rangos. A fragmentación ocorre despois da descompresión, xa que cada rango pode extraer diferentes cabezas da mesma representación latente. En consecuencia, todos os fragmentos de caché KV son idénticos tanto nos fragmentos de preenchimento como nos de descodificación.
Cun configuración TP intrarnodo, tanto os preenchidores como os descodificadores están fragmentados de forma idéntica. As transferencias envíanse desde todos os rangos para utilizar plenamente todos os NICs dispoñibles. Con todo, cun despregamento DP, onde o tamaño do rango TP é menor ou cada rango DP está asignado a unha única GPU, calquera dispositivo de preenchimento que manteña unha copia replicada da caché KV pode enviala. Para equilibrar as solicitudes en todos os NICs dispoñibles, seleccionamos aleatoriamente unha GPU e un NIC para enviar a caché KV desde o preenchidor ao descodificador.
Co preenchimento-descodificación mesturado, o noso despregamento R1 tiña dificultades para superar consistentemente os 50 TPS debido a frecuentes interrupcións de preenchimento na orde de centos de milisegundos. Pola contra, ao separar o preenchimento, incorremos nunha penalización de preto de 100 ms para o TTFT para cada solicitude, pero un único nodo de preenchimento podía manter tamaños de lote consistentes en 3 nodos de descodificador, ofrecendo un rendemento superior a 90 TPS mentres manexaba unha carga de preto de 1 QPS por nodo de descodificador. Cos despregamentos paralelos de datos, o TPS foi lixeiramente inferior, arredor de 50, con todo as instancias podían manexar unha carga de 1 QPS por rango, con 8 rangos nun único nodo.
Qwen3-Coder
Este modelo de 480B usa atención de consultas agrupadas (GQA), polo que a atención pódese fragmentar facilmente e pode beneficiarse do paralelismo de tensores sen sacrificar memoria para as cachés KV. En consecuencia, poderiamos fragmentar o modelo en 8 GPUs tanto para o preenchimento como para a descodificación, emparellando arredor de 3 nodos de descodificador cun único nodo de preenchimento. Dado que a atención está fragmentada, confiamos no deseño de caché KV HND para fragmentar as cachés KV de preenchimento e descodificador, emparellando os rangos de preenchimento cos rangos de descodificador e utilizando plenamente todos os NICs para transferir porcións en paralelo.
Cita
@online{perplexity2025DisaggregatedPrefillAndDecode,
author = {{Perplexity} and Nandor Licker and Lequn Chen and Kevin Hu and Erin McKnight and Phi Hoang},
title = {{Disaggregated prefill and decode}},
date = {2025-08-01},
url = {https://perplexity.coolkeypoint.com/hub/blog/disaggregated-prefill-and-decode}
}