Separar Attention de FFN: el plugin que le saca 11% más throughput a vLLM sin tocarle una línea

Separar Attention de FFN: el plugin que le saca 11% más throughput a vLLM sin tocarle una línea

Por Devy · Categoría: Open Source / AI & LLMs

Dentro de un modelo MoE ocurren dos cosas en cada token que no tienen casi nada en común. La attention es memory-bound: arrastra el KV cache de un lado a otro y su apetito crece con la longitud del contexto. La FFN —en un Mixture-of-Experts, el conjunto de experts— es compute-bound: quiere FLOPs puros y le importa muy poco cuán larga sea tu conversación. Servimos ambas con la misma topología de workers, en el mismo proceso, sobre las mismas GPUs, porque así se construyó el engine. Y entonces dimensionamos el cluster completo según cuál de las dos se está ahogando primero, y sobreaprovisionamos la otra.

El plugin AFD de vLLM, publicado el 23 de julio de 2026 bajo la organización del proyecto vLLM, rompe ese supuesto. Separa Attention y FFN en dos servicios desplegados de forma independiente, cada uno escalando sobre su propio eje. La cifra de titular es +11,3% de throughput por die y, antes de que sigas leyendo, conviene que sepas que ese número se midió sobre NPUs Ascend 910C ejecutando DeepSeek-V3.2 W8A8, no sobre hardware NVIDIA. El plugin sí soporta CUDA. Los benchmarks publicados no lo cubren. Esa distinción importa y volveremos sobre ella.

Lo que quiero mostrarte acá es por qué la idea es sólida, cómo se engancha a vLLM sin necesidad de un fork, y cómo ejecutarlo en una sola máquina con un modelo que realmente entre.

La asimetría que nadie estaba explotando

Piensa en cómo se ve tu cluster de serving cuando subes la longitud de contexto. La attention escala con la longitud de secuencia: más tokens, más KV cache, más ancho de banda de memoria quemado por forward pass. A la FFN no le importa: un request de 16K tokens y uno de 2K encienden los mismos experts con el mismo costo de cómputo por token. Los dos componentes divergen bajo carga, y divergen más cuanto más largos son tus contextos.

Cuando ambos viven en el mismo worker, no puedes responder a esa divergencia. Agregas GPUs y obtienes más de los dos, necesites o no más de los dos. El expert parallelism (EP) ya reparte los experts entre dispositivos, pero la attention viene de acompañante: en un despliegue EP64 tienes 64 ranks haciendo los dos trabajos.

AFD te permite decir algo distinto: dame 64 ranks de attention y 16 de FFN. Esa es la configuración 64A16F de los benchmarks, y de ahí sale el 11,3%. El mismo silicio, distinta proporción, más tokens de salida. No estás comprando performance: estás dejando de desperdiciar capacidad de attention que aprovisionaste como capacidad de FFN que no necesitabas.

Cómo se engancha sin forkear vLLM

Esta es la parte que me parece más elegante, y es la razón por la que esta historia importa incluso si nunca llegas a desplegarlo.

El plugin se registra mediante vllm.general_plugins, un entry point estándar que vLLM expone exactamente para esto. Sin árbol de fuentes parcheado, sin un fork vendorizado que se va alejando del upstream, sin dolor de rebase en cada release. Instalas un paquete, vLLM lo descubre al arrancar, y el runtime AFD queda activo.

Ese runtime tiene tres piezas:

  • un servicio de Attention que se encarga del scheduling y del KV cache,
  • un servicio de FFN que corre como daemon en segundo plano,
  • una capa de connector que mueve las activaciones entre ambos.

Hoy vienen tres connectors: P2pNcclAFDConnector para decode en CUDA, CAMP2pAFDConnector para decode en NPU Ascend sobre HCCL, y CAMAsyncAFDConnector para el path de prefill asíncrono en NPU.

La lectura estratégica: en el tracker de vLLM hay RFCs abiertos sobre attention-FFN disaggregation desde hace casi un año —#21644, #22799 y, más recientemente, #27584 sobre AFD elástico—. Un cambio tan invasivo sobre el modelo de ejecución es difícil de aterrizar en un engine core del que dependen miles de despliegues. Publicarlo como plugin externo bajo la propia organización del proyecto esquiva el merge por completo: la feature puede ser experimental e iterar rápido, y el core se mantiene aburrido. Si mantienes infraestructura, ese patrón vale la pena robarlo, te interese o no el serving de MoE.

Los números, desarmados

Hay dos cifras que se están citando juntas. Vienen de experimentos distintos y merecen leerse por separado.

Decode síncrono — DeepSeek-V3.2 W8A8 sobre Ascend 910C:

Longitud de input Baseline EP64 64A16F Delta
16K 232,6 tok/s/die 258,9 tok/s/die +11,3%
32K 168,2 tok/s/die 183,3 tok/s/die +9,0%

Prefill asíncrono — mismo modelo, 2× Ascend 910C: el time-to-first-token mediano baja de 15,1s a 8,0s con 12 requests por segundo, aproximadamente −47%.

Tres lecturas honestas. Primera: son mediciones de los propios autores del plugin, sin reproducción independiente hasta ahora. Segunda: la ganancia de throughput se achica a medida que crece el contexto, de 11,3% en 16K a 9,0% en 32K, algo que vale la pena masticar considerando que el contexto largo es justamente donde más esperarías que la disaggregation rindiera. Tercera: el número de TTFT corresponde a una configuración completamente distinta —prefill asíncrono sobre dos dispositivos, no el path de decode síncrono en un dispositivo que produjo las cifras de throughput—. No son un combo que consigas activando un solo flag.

Y otra vez: Ascend. Si estás sobre H100, el mecanismo te aplica y el connector CUDA existe, pero los porcentajes específicos no son tuyos hasta que alguien los mida.

Ejecutarlo en una sola máquina

No necesitas un modelo de 671B ni un rack de NPUs para ver la cosa funcionando. El propio ejemplo del repositorio apunta a DeepSeek-V2-Lite —un MoE de unos 16B con cerca de 2,4B de parámetros activos— con DP=1, TP=1. Eso son dos procesos en un solo host.

Instalación. Requiere Python 3.10–3.13 y uv.

git clone https://github.com/vllm-project/afd-plugin.git
cd afd-plugin
uv sync --group dev

En Linux con CUDA, agrega el extra de vLLM, que fija vllm==0.19.1:

uv sync --group dev --extra vllm

Levanta el servicio de FFN en el puerto 18001:

vllm serve /path/to/DeepSeek-V2-Lite \
  --served-model-name deepseek-v2-lite-afd-ffn \
  --data-parallel-size 1 \
  --tensor-parallel-size 1 \
  --enable-expert-parallel \
  --enforce-eager \
  --host 127.0.0.1 \
  --port 18001 \
  --additional-config '{"afd":{"role":"ffn","connector":"P2pNcclAFDConnector","host":"127.0.0.1","port":6239,"num_attention_ranks":1,"num_ffn_ranks":1}}'

Levanta el servicio de Attention en el puerto 18000, apuntando al mismo puerto de rendezvous del connector, el 6239:

vllm serve /path/to/DeepSeek-V2-Lite \
  --served-model-name deepseek-v2-lite-afd-attention \
  --data-parallel-size 1 \
  --tensor-parallel-size 1 \
  --enable-expert-parallel \
  --enforce-eager \
  --host 127.0.0.1 \
  --port 18000 \
  --additional-config '{"afd":{"role":"attention","connector":"P2pNcclAFDConnector","host":"127.0.0.1","port":6239,"num_attention_ranks":1,"num_ffn_ranks":1}}'

Todo lo específico de AFD vive dentro de --additional-config. El resto son flags que ya conoces. Los dos roles se diferencian exactamente en un campo.

Manda el tráfico solamente al servidor de Attention, el puerto 18000. Esta es la trampa que te va a morder primero:

curl http://127.0.0.1:18000/v1/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-v2-lite-afd-attention","prompt":"Explica el routing de MoE en un párrafo.","max_tokens":128}'

Los workers de FFN son connector-driven. No son un segundo endpoint entre el que balancear carga. Las llamadas a execute_model() dirigidas por el scheduler del lado FFN fallan de inmediato por diseño, y está bien que así sea: te enteras al instante en vez de depurar rarezas silenciosas.

Con qué te vas a tropezar

El orden de los ranks es una restricción dura. Los ranks de FFN van ordenados antes que los de attention, y num_attention_ranks debe ser mayor o igual que num_ffn_ranks y divisible por él. Así que 64A16F es legal, 16A64F no lo es, y 64A24F tampoco. Planifica la proporción antes de planificar los hosts.

La versión de vLLM está fijada en 0.19.1. No es “0.19.1 o superior”. Si tu plataforma está en otra versión, esto es un entorno aparte, no un drop-in.

La cobertura de modelos es estrecha. Las familias DeepSeek V2/V3, incluida V3.2, más GLM MoE DSA. Tu despliegue de Qwen o de Mixtral no está cubierto hoy.

Es explícitamente experimental. El repositorio dice que necesita más testing a gran escala en distintos backends de hardware, con limitaciones conocidas en soporte de graph (solo eager y FULL_DECODE_ONLY) y en la cantidad de ubatches. Esto es algo para prototipar, no para poner frente a tus clientes este trimestre.

Dónde aterrizo

Los porcentajes se van a mover: son self-reported, de un solo proveedor, sobre un solo silicio, y se verán distintos en tu hardware. Lo que no se va a mover es la observación que hay debajo: attention y FFN son cargas de trabajo diferentes vestidas con el mismo uniforme, y servirlas como una sola unidad significa pagar de más por una de las dos de manera permanente. Esa idea sobrevive a lo que diga la tabla de benchmarks de este plugin en particular dentro de seis meses.

Si ejecutas modelos MoE a cualquier escala, levanta el ejemplo con V2-Lite y observa dos procesos sirviendo un solo modelo. Toma veinte minutos y cambia la forma en que piensas la arquitectura de un cluster de inferencia. Y si estás sobre NVIDIA y lo mides, publica lo que te dé: ese número hoy no existe públicamente, y a la comunidad le vendría muy bien.

¿Y tú, estás sirviendo modelos MoE en producción? ¿Te tocó sobreaprovisionar GPUs por el lado de attention sabiendo que los experts quedaban a media máquina? Cuéntame en los comentarios cómo lo estás resolviendo hoy.


Fuentes: