O erro de resumir performance a um número
Não adianta o monitor de recursos mostrar que o cluster bate 200 tokens por segundo se o usuário fica encarando uma tela em branco por oito segundos antes de a primeira palavra aparecer. Esse é o erro mais comum de quem começa a servir modelos de linguagem: resumir performance a um número só. A experiência percebida não é governada por uma velocidade, e sim por duas fases matematicamente distintas, com gargalos físicos diferentes, que respondem a otimizações diferentes e que frequentemente competem entre si.
Eu demorei para internalizar isso, e só internalizei olhando a telemetria do nosso próprio servidor. Então vou começar pelas duas fases e terminar nos números.
Fase 1, prefill: processar o prompt inteiro de uma vez
Quando você envia uma mensagem, o modelo precisa processar todos os tokens de entrada para construir o estado inicial: as matrizes de chave e valor de cada camada, que vão alimentar toda a geração seguinte. O tempo até o primeiro token aparecer é o TTFT, Time To First Token.
A característica decisiva dessa fase é que ela é massivamente paralelizável. Todos os tokens do prompt entram juntos na GPU e viram multiplicações de matrizes grandes, exatamente o formato que os Tensor Cores foram construídos para devorar. O custo aproximado é:
\[ \text{FLOPs}_{\text{prefill}} \approx 2 \cdot P \cdot n_{\text{tokens}} \]
em que \(P\) é o número de parâmetros do modelo. O trabalho cresce linearmente com o tamanho do prompt, mais a parcela da atenção, que cresce com o quadrado do comprimento e passa a dominar em contextos realmente longos. Por isso o prefill é limitado por computação: o teto é a capacidade bruta de cálculo do chip. Prompt gigantesco ou hardware sem TFLOPs suficientes, e o TTFT explode. É a sensação de congelamento.
Fase 2, decode: um token de cada vez
Depois que o primeiro token sai, o jogo muda por completo. A geração é autorregressiva: para produzir o token seguinte, a rede precisa percorrer todos os seus pesos de novo. Uma passagem inteira pelo modelo para produzir uma única palavra. A métrica aqui é o TPOT, Time Per Output Token, também chamado de latência entre tokens.
E aqui está o ponto que muita gente não internaliza: nessa fase, a GPU quase não calcula. Ela lê. Cada token gerado exige transportar todos os pesos da VRAM até os núcleos de cálculo e usar cada peso em uma única operação antes de descartá-lo. A intensidade aritmética, FLOPs por byte lido, despenca, e o chip fica ocioso esperando memória. O teto de velocidade de uma única sequência é, portanto, uma conta de banda:
\[ \text{TPOT}_{\min} = \frac{P \cdot b}{\text{BW}} \]
com \(b\) sendo os bytes por parâmetro (2 em FP16, 1 em INT8 ou FP8, cerca de 0,5 em INT4) e \(\text{BW}\) a largura de banda da VRAM.
Uma RTX 3060 de 12 GB entrega cerca de 360 GB/s. Um modelo quantizado ocupando 10,8 GB precisa de 10,8 GB lidos por token, o que dá um teto de aproximadamente 33 tokens por segundo, por sequência, no melhor cenário imaginável. Distribuindo o modelo em duas 3060 com paralelismo de tensor, cada placa carrega metade e as bandas somam: o teto sobe para perto de 66, e na prática fica abaixo por causa da sincronização entre as placas. Nenhum ajuste de software quebra esse limite. Ele é física.
A diferença em números medidos
A melhor ilustração que já vi dessa assimetria é a telemetria do nosso servidor vLLM em operação. No mesmo instante, com o mesmo modelo, nas mesmas GPUs:
- Lendo prompt (prefill): 12.968,6 tokens por segundo.
- Gerando (decode): 95 tokens por segundo.
Cerca de 136 vezes de diferença entre processar entrada e produzir saída. Não são duas velocidades do mesmo processo. São dois regimes físicos distintos rodando no mesmo silício.
| Dimensão | Prefill (TTFT) | Decode (TPOT) |
|---|---|---|
| Tokens por passo | Todos os do prompt | Um por sequência |
| Gargalo dominante | Computação (TFLOPs) | Banda de memória (GB/s) |
| Intensidade aritmética | Alta | Baixíssima |
| Efeito de prompt longo | Cresce muito, até quadrático | Cresce pouco, só o KV cache |
| Efeito de batch maior | Satura rápido | Ganho quase linear |
| O que melhora | Prefix caching, chunked prefill, mais TFLOPs | Mais banda, quantização, batching, decodificação especulativa |
| Medido no painel | 12.968,6 tok/s | 95 tok/s |
As alavancas que mexem o ponteiro
Prefix caching
Prompt de sistema fixo, instruções de ferramenta, histórico da conversa: tudo isso é recalculado do zero a cada requisição, a menos que o motor guarde o KV cache do prefixo comum. Com cache, o prefill só processa o sufixo novo. No mesmo painel, uma taxa de acerto de 97% no cache apareceu ao lado de um primeiro token em 0,87 segundo. Numa janela com 85,5% de acerto, o primeiro token foi a 1,72 segundo. A relação é direta e mensurável, e é a alavanca mais barata de todas.
Continuous batching
Em vez de montar um lote, processá-lo inteiro e só então aceitar o próximo, o escalonador insere requisições novas no ciclo de geração que já está rodando e remove as que terminaram. Como o decode é limitado por memória, rodar várias sequências ao mesmo tempo custa quase a mesma leitura de pesos: o throughput agregado sobe muito sem penalizar o TPOT individual na mesma proporção. É a única forma honesta de anunciar "160 tokens por segundo" sem mentir sobre a latência de cada usuário.
Chunked prefill
Um prompt de 40 mil tokens chegando no meio da geração de outra pessoa trava o ciclo e produz engasgos visíveis para quem já estava recebendo texto. Fatiar o prefill em pedaços e intercalá-los com passos de decode protege o TPOT de quem está no meio da resposta, ao custo de um TTFT um pouco maior para quem acabou de chegar.
Decodificação especulativa
Um modelo pequeno propõe vários tokens, o modelo grande verifica todos de uma vez. Como a verificação é uma operação paralela, ela ataca exatamente o gargalo do decode: amortiza uma leitura de pesos por vários tokens aceitos.
Desagregação de prefill e decode
Em escala, a solução mais radical: colocar as duas fases em nós diferentes, porque uma quer FLOPs e a outra quer banda, e otimizar hardware para as duas no mesmo lugar significa pagar caro nas duas pontas.
O trade-off de que ninguém escapa
Aumentar o batch melhora o throughput total e piora a latência individual. Priorizar quem acabou de chegar melhora o TTFT médio e piora o TPOT de quem já estava gerando. Não existe configuração ótima universal. Existe a configuração certa para o seu padrão de uso, e ela muda quando o padrão muda.
A métrica que uso para decidir é a vazão de requisições que respeitaram seus limites de latência, não a vazão bruta. Um servidor que faz 300 tokens por segundo violando o TTFT de metade dos usuários tem goodput pior que um que faz 180 respeitando todo mundo. E meça P95 e P99, não a média. A média esconde justamente o caso que faz o usuário reclamar.
Antes de comprar mais GPU
Se o seu problema é TTFT alto, comprar banda de memória não resolve. Investigue prefix caching, o tamanho real dos prompts e chunked prefill. Se o problema é texto saindo devagar, comprar TFLOPs não resolve. O caminho é banda, quantização e batching. Entender a divisão entre processar entrada e produzir saída é o primeiro passo para não gastar dinheiro no gargalo errado.
Resumo da conversa
- Prefill é paralelo e limitado por computação; decode é sequencial e limitado por banda de memória. Duas físicas, duas métricas: TTFT e TPOT.
- O teto do decode por sequência é pesos divididos por banda. Numa RTX 3060 com um modelo de 10,8 GB, cerca de 33 tokens por segundo, e nenhum software muda isso.
- No painel real: 12.968 tokens por segundo lendo, 95 gerando. 136 vezes.
- Prefix caching derruba o TTFT (97% de acerto, 0,87 s); continuous batching multiplica o throughput de decode sem multiplicar a latência; chunked prefill protege quem já está gerando.
- Batch maior e prioridade de chegada são trade-offs, não melhorias. Decida por goodput e por P99.
Igor Nascimento