Separar Attention de FFN: o plugin que tira 11% a mais de throughput do vLLM sem tocar uma linha
Por Devy · Categoria: Open Source / AI & LLMs
Dentro de um modelo MoE acontecem duas coisas em cada token que não têm quase nada em comum. A attention é memory-bound: arrasta o cache KV de um lado para outro e seu apetite cresce com o comprimento do contexto. A FFN — em um Mixture-of-Experts, o conjunto de experts — é compute-bound: quer FLOPs puros e se importa muito pouco com o quão longa seja sua conversa. Servimos ambas com a mesma topologia de workers, no mesmo processo, sobre as mesmas GPUs, porque assim foi construído o engine. E então dimensionamos o cluster completo conforme qual das duas está se afogando primeiro, e sobreaprovisionamos a outra.
O plugin AFD do vLLM, publicado em 23 de julho de 2026 sob a organização do projeto vLLM, quebra esse pressuposto. Separa Attention e FFN em dois serviços implantados de forma independente, cada um escalando sobre seu próprio eixo. O número em destaque é +11,3% de throughput por die e, antes de você continuar lendo, convém saber que esse número foi medido sobre NPUs Ascend 910C executando DeepSeek-V3.2 W8A8, não sobre hardware NVIDIA. O plugin sim suporta CUDA. Os benchmarks publicados não o cobrem. Essa distinção importa e voltaremos a ela.
O que quero mostrar aqui é por que a ideia é sólida, como se conecta ao vLLM sem necessidade de um fork, e como executá-lo em uma única máquina com um modelo que realmente caiba.
A assimetria que ninguém estava explorando
Pense em como fica seu cluster de serving quando você aumenta o comprimento do contexto. A attention escala com o comprimento da sequência: mais tokens, mais cache KV, mais largura de banda de memória queimada por forward pass. À FFN não importa: um request de 16K tokens e um de 2K acionam os mesmos experts com o mesmo custo de computação por token. Os dois componentes divergem sob carga, e divergem mais quanto mais longos seus contextos.
Quando ambos vivem no mesmo worker, você não consegue responder a essa divergência. Agrega GPUs e obtém mais dos dois, precise ou não de mais dos dois. O expert parallelism (EP) já distribui os experts entre dispositivos, mas a attention vem de acompanhante: em uma implantação EP64 você tem 64 ranks fazendo os dois trabalhos.
AFD permite dizer algo distinto: me dê 64 ranks de attention e 16 de FFN. Essa é a configuração 64A16F dos benchmarks, e daí sai o 11,3%. O mesmo silício, proporção distinta, mais tokens de saída. Você não está comprando performance: está deixando de desperdiçar capacidade de attention que aprovisionou como capacidade de FFN que não precisava.
Como se conecta sem forkar vLLM
Essa é a parte que me parece mais elegante, e é a razão pela qual essa história importa mesmo se você nunca chegue a implantá-la.
O plugin se registra por vllm.general_plugins, um entry point padrão que vLLM expõe exatamente para isso. Sem árvore de fontes parcheada, sem um fork vendorizado que se afasta do upstream, sem dor de rebase a cada release. Você instala um pacote, vLLM o descobre ao iniciar, e o runtime AFD fica ativo.
Esse runtime tem três peças:
- um serviço de Attention que cuida do scheduling e do cache KV,
- um serviço de FFN que roda como daemon em segundo plano,
- uma camada de connector que move as ativações entre ambos.
Hoje vêm três connectors: P2pNcclAFDConnector para decode em CUDA, CAMP2pAFDConnector para decode em NPU Ascend sobre HCCL, e CAMAsyncAFDConnector para o path de prefill assíncrono em NPU.
A leitura estratégica: no tracker do vLLM há RFCs abertos sobre attention-FFN disaggregation há quase um ano — #21644, #22799 e, mais recentemente, #27584 sobre AFD elástico. Uma mudança tão invasiva no modelo de execução é difícil de aterrissar em um engine core do qual dependem milhares de implantações. Publicá-lo como plugin externo sob a própria organização do projeto esquiva o merge completamente: a feature pode ser experimental e iterar rápido, e o core se mantém entediante. Se você mantém infraestrutura, esse padrão vale a pena roubar, se le interesse ou não o serving de MoE.
Os números, desarmados
Há duas cifras que estão sendo citadas juntas. Vêm de experimentos distintos e merecem ser lidas separadamente.
Decode síncrono — DeepSeek-V3.2 W8A8 sobre Ascend 910C:
| Comprimento 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 assíncrono — mesmo modelo, 2× Ascend 910C: o time-to-first-token mediano cai de 15,1s para 8,0s com 12 requests por segundo, aproximadamente −47%.
Três leituras honestas. Primeira: são medições dos próprios autores do plugin, sem reprodução independente até agora. Segunda: o ganho de throughput fica menor à medida que cresce o contexto, de 11,3% em 16K para 9,0% em 32K, algo que vale a pena mastigar considerando que o contexto longo é justamente onde você mais esperaria que a disaggregation rendesse. Terceira: o número de TTFT corresponde a uma configuração completamente distinta — prefill assíncrono sobre dois dispositivos, não o path de decode síncrono em um dispositivo que produziu as cifras de throughput. Não é um combo que você consegue ativando um único flag.
E de novo: Ascend. Se você está sobre H100, o mecanismo se aplica e o connector CUDA existe, mas os percentuais específicos não são seus até que alguém os meça.
Executá-lo em uma única máquina
Você não precisa de um modelo de 671B nem de um rack de NPUs para ver a coisa funcionando. O próprio exemplo do repositório aponta para DeepSeek-V2-Lite — um MoE de uns 16B com cerca de 2,4B de parâmetros ativos — com DP=1, TP=1. São dois processos em um único host.
Instalação. Requer Python 3.10–3.13 e uv.
git clone https://github.com/vllm-project/afd-plugin.git
cd afd-plugin
uv sync --group dev
Em Linux com CUDA, adiciona o extra do vLLM, que fixa vllm==0.19.1:
uv sync --group dev --extra vllm
Levanta o serviço de FFN na porta 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 o serviço de Attention na porta 18000, apontando para a mesma porta de rendezvous do connector, a 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}}'
Tudo específico de AFD vive dentro de --additional-config. O resto são flags que você já conhece. Os dois roles se diferenciam exatamente em um campo.
Manda o tráfego somente para o servidor de Attention, a porta 18000. Esse é o truque que vai morder você primeiro:
curl http://127.0.0.1:18000/v1/completions \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-v2-lite-afd-attention","prompt":"Explica o routing de MoE em um parágrafo.","max_tokens":128}'
Os workers de FFN são connector-driven. Não são um segundo endpoint entre o qual balancear carga. As chamadas para execute_model() direcionadas pelo scheduler do lado FFN falham imediatamente por design, e está bem que assim seja: você fica sabendo imediatamente em vez de depurar esquisitices silenciosas.
Com o que você vai se tropezar
A ordem dos ranks é uma restrição rígida. Os ranks de FFN vêm ordenados antes dos de attention, e num_attention_ranks deve ser maior ou igual a num_ffn_ranks e divisível por ele. Então 64A16F é legal, 16A64F não é, e 64A24F também não. Planeje a proporção antes de planejar os hosts.
A versão do vLLM está fixada em 0.19.1. Não é “0.19.1 ou superior”. Se sua plataforma está em outra versão, este é um ambiente separado, não um drop-in.
A cobertura de modelos é estreita. As famílias DeepSeek V2/V3, incluindo V3.2, mais GLM MoE DSA. Sua implantação de Qwen ou de Mixtral não é coberta hoje.
É explicitamente experimental. O repositório diz que precisa de mais testes em larga escala em diferentes backends de hardware, com limitações conhecidas no suporte de graph (apenas eager e FULL_DECODE_ONLY) e na quantidade de ubatches. Isto é algo para prototipagem, não para colocar na frente de seus clientes neste trimestre.
Onde eu chego
Os percentuais vão se mover: são auto-reportados, de um único fornecedor, sobre um único silício, e parecerão diferentes no seu hardware. O que não vai se mover é a observação por baixo: attention e FFN são cargas de trabalho diferentes vestidas com o mesmo uniforme, e servi-las como uma unidade única significa pagar mais por uma das duas de forma permanente. Essa ideia sobrevive ao que a tabela de benchmarks deste plugin em particular disser dentro de seis meses.
Se você executa modelos MoE em qualquer escala, levante o exemplo com V2-Lite e observe dois processos servindo um único modelo. Leva vinte minutos e muda a forma como você pensa a arquitetura de um cluster de inferência. E se você está na NVIDIA e o mede, publique o que der: esse número hoje não existe publicamente, e a comunidade se beneficiaria muito.
E você, está servindo modelos MoE em produção? Teve que fazer over-provisioning de GPUs pelo lado de attention sabendo que os experts ficavam com a máquina na metade? Conta nos comentários como você está resolvendo isto hoje.
Fontes: