SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

Prefill vs decode: por que o primeiro token demora

Entenda por que o prefill define o tempo até o primeiro token e o decode define tokens por segundo. Meça as duas fases separadamente na sua GPU.

Prefill versus decode, num parágrafo

A distinção entre prefill e decode explica a maioria das questões de latência num LLM (large language model) alojado localmente. O prefill lê todo o prompt numa única passagem e é limitado pela capacidade de computação. O decode escreve a resposta um token de cada vez e é limitado pela largura de banda da memória. O tempo até ao primeiro token é uma métrica do prefill. O número de tokens por segundo é uma métrica do decode.

Ambas as fases executam na mesma GPU (graphics processing unit), com os mesmos pesos e dentro do mesmo processo, pelo que é natural tratá-las como uma única carga de trabalho. Na prática, comportam-se como dois programas diferentes que partilham o mesmo dispositivo. Quando as separa, deixa de ser confusa uma longa lista de resultados aparentemente contraditórios.

Por que o prefill é limitado pelo processamento?

O prefill envia o prompt completo por todas as camadas uma vez. Um prompt com 2,000 tokens fornece 2,000 linhas de trabalho a cada multiplicação de matrizes, por isso a GPU executa muitos cálculos para cada byte de pesos carregado. Essa relação, chamada intensidade aritmética, corresponde à quantidade de operações aritméticas por byte transferido. O prefill tem uma intensidade aritmética elevada. O dispositivo funciona próximo do limite de processamento, enquanto o barramento de memória ainda tem capacidade disponível.

O prefill produz duas coisas: o cache KV (os tensores de chave e valor) para cada token do prompt e o primeiro token de saída. Nada chega ao leitor enquanto essa passagem não termina. Por isso, o tempo de prefill e o tempo até ao primeiro token (TTFT) têm valores muito próximos.

O custo do prefill aumenta com o comprimento do prompt. A parte linear corresponde ao trabalho das multiplicações de matrizes em cada camada. A parte quadrática corresponde à atenção, em que cada token presta atenção a todos os tokens anteriores. Essa parte começa a ter impacto em contextos longos. Por isso, duplicar o prompt duplica, no mínimo, o TTFT.

Pode observar isto num minuto. Envie ao servidor um prompt com 200 tokens e depois um prompt com 2,000 tokens, pedindo o mesmo número de tokens de saída em ambos os casos. O TTFT aumenta acentuadamente. A velocidade de geração após o primeiro token quase não se altera.

Por que o decode é limitado pela largura de banda da memória?

O decode produz um token por etapa. Para produzir esse único token, a GPU tem de ler todos os pesos do modelo da memória, usar cada peso em algumas operações e descartá-lo. A intensidade aritmética é próxima de 1, por isso as unidades de computação passam a maior parte do tempo à espera.

O decode é lento porque cada token exige ler todo o modelo da memória. Assim, o barramento de memória define o ritmo e as unidades de computação ficam ociosas.

Isto define o limite teórico da velocidade de decode de um único fluxo. Divida a largura de banda da memória pelo número de bytes ocupados pelos pesos.

ChartPublished memory bandwidth and the decode ceiling it implies for a 16 GB model
The data behind this chart
[
  {
    "device": "CPU, dual channel DDR5-5600",
    "mem_bandwidth_gb_s": 90,
    "decode_ceiling_tok_s": 6
  },
  {
    "device": "NVIDIA A10G",
    "mem_bandwidth_gb_s": 600,
    "decode_ceiling_tok_s": 38
  },
  {
    "device": "NVIDIA L40S",
    "mem_bandwidth_gb_s": 864,
    "decode_ceiling_tok_s": 54
  },
  {
    "device": "NVIDIA RTX 4090",
    "mem_bandwidth_gb_s": 1008,
    "decode_ceiling_tok_s": 63
  },
  {
    "device": "NVIDIA A100 80GB SXM",
    "mem_bandwidth_gb_s": 2039,
    "decode_ceiling_tok_s": 127
  },
  {
    "device": "NVIDIA H100 SXM",
    "mem_bandwidth_gb_s": 3350,
    "decode_ceiling_tok_s": 209
  }
]

A coluna de largura de banda contém o valor publicado nas especificações de cada fabricante. A coluna de limite contém esse valor dividido por 16 GB, o tamanho de um modelo com 8 mil milhões de parâmetros armazenado com precisão de 16 bits. É um cálculo aritmético, não o resultado de um benchmark. A taxa medida ficará abaixo desse valor. Saber quão abaixo ela está é útil porque indica se deve corrigir a stack de serving ou o hardware.

Leia as 6 linhas pela ordem apresentada. O padrão é claro. Uma CPU com DDR5 de dois canais transfere cerca de 90 GB/s, o que limita o decode a cerca de 6 tokens por segundo para esse modelo. Uma L40S chega a cerca de 54. Uma H100 SXM, com a largura de banda publicada de 3350 GB/s, fica perto de 209.

É também por isso que a quantização é o fator isolado com maior impacto na velocidade de decode. Armazene o mesmo modelo com 8 bits em vez de 16 e reduzirá para metade os bytes lidos por token. Assim, o limite aproximadamente duplica. Não adicionou computação. Moveu menos memória.

Como medir cada fase no meu próprio servidor?

Ollama devolve a divisão no corpo da resposta. Peça uma conclusão sem streaming e leia os contadores.

curl -s http://localhost:11434/api/generate -d '{
  "model": "llama3.2",
  "prompt": "Explain memory bandwidth in two sentences.",
  "stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'

Use uma tag de modelo que tenha realmente obtido, que ollama list apresenta. prompt_eval_count e prompt_eval_duration correspondem ao prefill: a contagem de tokens do prompt e o tempo gasto nessa etapa. eval_count e eval_duration correspondem ao decode. As durações estão em nanossegundos, por isso a velocidade de decode é eval_count / eval_duration * 1e9 e a velocidade de prefill é prompt_eval_count / prompt_eval_duration * 1e9. É normal a taxa de prefill ser muito superior à taxa de decode no mesmo pedido. É essa diferença que o restante deste texto explica.

Num servidor compatível com OpenAI, como o vLLM, o curl pode medir o tempo até ao primeiro byte.

curl -N -s -o /dev/null \
  -w 'pretransfer %{time_pretransfer}s  first_byte %{time_starttransfer}s\n' \
  http://localhost:8000/v1/completions \
  -H 'Content-Type: application/json' \
  -d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'

time_starttransfer é o momento em que chegou o primeiro byte do corpo da resposta. Com "stream": true, esse valor corresponde ao TTFT mais a configuração da ligação. Subtraia time_pretransfer para remover o custo da configuração. Execute duas vezes e mantenha o segundo resultado, porque a primeira chamada pode incluir o carregamento a frio do modelo.

O vLLM também publica a divisão como métricas Prometheus em /metrics. Execute curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' para obter os histogramas vllm:time_to_first_token_seconds e vllm:inter_token_latency_seconds. Adicione vllm:num_requests_running e vllm:num_requests_waiting para a profundidade da fila, e vllm:kv_cache_usage_perc para a pressão da cache. Esses cinco nomes constituem todo o dashboard.

Sob carga, vllm bench serve --model <name> --num-prompts 200 --request-rate 4 gera carga no servidor em execução e apresenta o tempo até ao primeiro token e a latência por token de saída com percentis. Esta é a única forma de observar as duas fases a competir entre si. Antes de ajustar qualquer parâmetro, obtenha uma baseline limpa: o método em medir tokens por segundo num LLM local fornece uma baseline que permanece válida após um reboot.

Por que um prompt de sistema longo atrasa o primeiro token, mas não a velocidade de transmissão?

Porque o prompt de sistema é trabalho de prefill, tal como o restante do prompt. Ele é processado uma vez, na mesma passagem que o restante do prompt, antes de o primeiro token aparecer. Depois dessa passagem, existe apenas como entradas na KV cache, e o decode lê essas entradas juntamente com tudo o resto. Assim, um prompt de sistema com 3,000 tokens aumenta o TTFT em todos os pedidos, mas deixa os tokens por segundo praticamente inalterados.

Praticamente, não exatamente. Essas entradas adicionais da KV cache são lidas novamente em cada etapa de decode, por isso um prompt muito longo também reduz ligeiramente a velocidade do decode. A secção seguinte aborda este ponto.

A solução é deixar de recalcular o mesmo prefixo. Um servidor com prefix caching mantém a KV cache de um prefixo partilhado e reutiliza-a. Assim, o segundo pedido que contém o mesmo prompt de sistema ignora totalmente essa parte do prefill. O vLLM chama a isto automatic prefix caching; consulte vllm serve --help na sua versão, porque o valor predefinido mudou entre releases. Essa KV cache na GPU é diferente da prompt cache pela qual um fornecedor de API lhe cobra, e vale a pena ler a diferença entre uma KV cache e uma prompt cache antes de ajustar qualquer uma delas.

Por que a decodificação fica mais lenta à medida que o contexto aumenta?

Há dois motivos, ambos relacionados com o KV cache.

O primeiro é a largura de banda. Em cada passo de decodificação, a atenção lê as chaves e os valores de todos os tokens anteriores. Os pesos têm um custo fixo por token. O KV cache cresce. Pode calcular o seu tamanho a partir do config.json do modelo: os bytes por token são iguais a 2 multiplicado por num_hidden_layers, por num_key_value_heads, pela dimensão da cabeça (hidden_size dividida por num_attention_heads), pelos bytes por elemento. O 2 inicial conta uma chave e um valor.

Numa arquitetura comum de 8 billion parâmetros, com 32 camadas, 8 cabeças de chaves e valores em GQA (grouped query attention), dimensão da cabeça 128 e precisão de 16 bits, o cálculo é 2 x 32 x 8 x 128 x 2 = 131,072 bytes, cerca de 128 KiB por token. Uma conversa com 8,000 tokens transporta, portanto, aproximadamente 1 GB de KV cache por pedido.

O segundo é a capacidade. Esse 1 GB é memória que não pode armazenar pesos nem o contexto de outro utilizador. O servidor dimensiona o seu pool de KV uma vez no arranque, no vLLM através de --gpu-memory-utilization, e, quando o pool fica cheio, os novos pedidos aguardam. vllm:num_requests_waiting a aumentar enquanto vllm:kv_cache_usage_perc permanece próximo de 1 é a assinatura exata desse estado. Algumas stacks interrompem um pedido em execução e recalculam o seu cache mais tarde, em vez de o colocar numa fila. Para o utilizador, isto aparece como uma pausa a meio do fluxo.

Um contexto longo custa duas vezes: mais trabalho de prefill no início e mais leituras de memória por token durante o resto da resposta.

Por que o batching aumenta o throughput e prejudica a latência de cauda?

Como o decode é limitado pela largura de banda, os pedidos adicionais têm um custo computacional reduzido. Uma leitura dos pesos pode produzir um token para cada sequência do batch, por isso o throughput total aumenta quase linearmente com o tamanho do batch, até o pool de KV ficar sem espaço ou o batch crescer o suficiente para voltar a ficar limitado pela capacidade de processamento. O continuous batching reconstrói o batch em cada passo. Assim, um pedido concluído sai e outro pedido em fila entra sem esperar pelos restantes.

O custo aparece nos percentis. O próximo token de cada utilizador passa a esperar pela parte mais lenta de um passo partilhado. Assim, o p50, a mediana, mantém-se aceitável, enquanto o p99, o 1 pedido mais lento em 100, aumenta. É o p99 que os utilizadores notam, porque corresponde à pausa a meio de uma frase.

O prefill torna este efeito mais acentuado. Quando um prompt grande chega durante o streaming, ocupa o dispositivo durante um passo longo e todos os pedidos que estão a transmitir veem uma interrupção. O chunked prefill reduz este efeito ao dividir um prompt longo em partes e misturar cada parte nos batches de decode. Em agosto de 2026, o motor vLLM V1 faz isto por predefinição e expõe o equilíbrio através de --max-num-batched-tokens. A documentação de otimização do vLLM descreve claramente este compromisso: valores menores, próximos de 2048, proporcionam uma melhor latência entre tokens (ITL), porque menos operações de prefill interrompem os decodes; valores maiores proporcionam um melhor TTFT, porque cabem mais tokens de prefill num único batch. Essa flag expõe o equilíbrio entre prefill e decode como um número que pode ajustar. O ponto em que o p99 deixa de ser aceitável é uma questão de capacidade, e quantos utilizadores simultâneos um LLM autoalojado consegue servir analisa essa questão com as mesmas métricas.

Por que uma GPU maior às vezes não muda nada?

Porque maior normalmente significa mais capacidade de computação, e o decode não precisa de mais computação.

Compare duas linhas do gráfico acima. A A100 80GB tem uma largura de banda publicada de 2039 GB/s, contra 864 GB/s da L40S, e o limite de decode acompanha exatamente esses valores: 127 tokens por segundo contra 54. A RTX 4090 é uma placa muito rápida na maioria dos critérios, e os seus 1008 GB/s colocam o seu limite em 63. Independentemente das outras diferenças entre duas placas, o decode de um único fluxo acompanha a largura de banda indicada na ficha técnica.

Por isso, há duas formas de tornar o decode mais rápido: ler menos bytes por token, quantizando os pesos ou executando um modelo menor, ou comprar mais largura de banda. O prefill é o caso oposto. Precisa de capacidade de computação, por isso uma placa mais rápida reduz efetivamente o TTFT em prompts longos. Se o problema é o primeiro token demorar quatro segundos, hardware melhor pode resolver a situação. Se o problema é o texto aparecer lentamente, provavelmente não resolverá.

Deve executar o prefill e o decode em workers separados?

As grandes plataformas de serving fazem exatamente isto, e a técnica chama-se desagregação de prefill e decode. Um pool de workers executa apenas o prefill, um segundo pool executa apenas o decode, e a KV cache criada pelo primeiro é transferida para o segundo através de uma interligação rápida. Isto funciona porque as fases exigem hardware e escalonamento diferentes. O prefill exige capacidade de computação e lotes grandes de tokens. O decode exige largura de banda e muitas sequências concorrentes. Separá-los permite que cada pool escale de forma independente e impede que um prompt muito grande bloqueie todos os streams ativos.

Num único VPS (virtual private server) com uma GPU, quase nunca compensa. Estaria a dividir um único dispositivo contra si próprio e transformaria um ponteiro numa transferência de rede de gigabytes de cache. A técnica compensa quando existem aceleradores suficientes para dedicar máquinas inteiras a cada fase e tráfego constante suficiente para manter ambos os pools ocupados. Abaixo desse nível, o prefill em blocos oferece praticamente o mesmo isolamento com uma única flag.

O que alterar quando o número está errado

Quando o TTFT é demasiado elevado:

  • Encurte o prompt. O custo do prefill acompanha o número de tokens do prompt, e o system prompt é contabilizado em todos os pedidos.
  • Ative o cache de prefixos para que um prefixo repetido seja calculado uma vez, em vez de ser recalculado em cada pedido.
  • Aumente --max-num-batched-tokens para que mais trabalho de prefill seja processado em cada passo.
  • Verifique a fila antes de culpar o modelo. vllm:num_requests_waiting acima de zero significa que o pedido ainda não tinha começado, o que indica um problema de capacidade.

Quando o número de tokens por segundo é demasiado baixo:

  • Quantize os pesos. Menos bytes por peso significa menos bytes lidos por token.
  • Compare a largura de banda de memória publicada para a sua placa com o gráfico acima e verifique quão perto está do limite.
  • Reduza --max-num-batched-tokens para que os prefills interrompam o decode com menos frequência.
  • Verifique o comprimento do contexto. Uma conversa que cresceu para milhares de tokens lê uma cache KV muito maior em cada passo.

O runtime também é importante, porque Ollama e vLLM agendam o prefill e o decode de forma diferente, e uma definição que ajuda um deles pode não ter qualquer efeito no outro. Faça primeiro as medições, em ambas as fases, e depois altere uma coisa de cada vez.

FAQ

Por que o primeiro token demora segundos, mas os restantes são transmitidos rapidamente?

A espera corresponde ao prefill e a transmissão corresponde ao decode. O prefill processa todo o prompt numa única passagem limitada pela capacidade de computação, antes de existir qualquer saída. Por isso, o custo aumenta com o tamanho do prompt. O decode emite depois um token por etapa, a uma velocidade definida pela largura de banda da memória, que é quase independente do tamanho do prompt. Um system prompt longo em todos os pedidos é a causa habitual. O cache de prefixos elimina a repetição dessa parte do custo.

Um prompt mais longo reduz os tokens por segundo?

Reduz ligeiramente, mas por uma razão diferente do TTFT. Em cada etapa de decode, o sistema lê as chaves e os valores de todos os tokens anteriores. Por isso, um cache KV maior significa mais bytes lidos por token. Numa configuração comum com 8 mil milhões de parâmetros, o cache ocupa cerca de 128 KiB por token. Assim, um contexto de 8,000 tokens corresponde a aproximadamente 1 GB que é lido em cada etapa. O efeito mais significativo de um prompt longo continua a ocorrer no TTFT, não na velocidade de transmissão.

Que especificação da GPU prevê a velocidade de decode?

A largura de banda da memória. Divida a largura de banda publicada pelo tamanho dos pesos na memória para obter o limite teórico de uma única sequência. Uma placa com mais capacidade de computação, mas com a mesma largura de banda, não terá uma transmissão mais rápida. É também por isso que quantizar para 8 bits aumenta aproximadamente para o dobro a velocidade de decode: reduz para metade os bytes lidos por token sem alterar a computação.

Por que o throughput aumenta quando adiciono utilizadores, mas cada utilizador sente um desempenho mais lento?

Uma leitura dos pesos processa um token para cada sequência do batch. Por isso, o total de tokens por segundo aumenta com o tamanho do batch. Cada token individual passa então a esperar por uma etapa partilhada, pelo que a latência por utilizador aumenta ao mesmo tempo. Monitorize a latência inter token p99, não o valor agregado de throughput, e verifique vllm:num_requests_waiting para saber se os pedidos estão em fila em vez de estarem a ser executados.