Separar Attention de FFN: o plugin que extrai 11% mais throughput do vLLM sem mexer uma linha

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: