Phase 17 - Lesson 04

Componentes Internos de Serviço do vLLM: PagedAttention, Continuous Batching, Chunked Prefill

A dominância do vLLM em 2026 baseia-se em três configurações padrão combinadas, e não em um único truque. O PagedAttention está sempre ativado. O Continuous batching insere novas requisições no lote ativo entre as iterações de decodificação. O Chunked prefill divide prompts longos para que os tokens de decodificação nunca fiquem desprovidos de processamento. Ao ativar os três recursos, um Llama 3.3 70B FP8 em uma única H100 SXM5 atinge 2.200 a 2.400 tok/s com 128 conexões simultâneas — cerca de 25% acima do padrão do próprio vLLM e 3 a 4 vezes mais rápido do que um loop PyTorch ingênuo. Esta lição detalha o agendador e o kernel de atenção em um nível que você pode diagramar, e termina com um agendador simplificado de continuous batching em code/main.py que agenda o prefill e a decodificação da mesma forma que o vLLM faz.

Tipo: Aprender Linguagens: Python (stdlib, simulador simplificado de agendador de continuous batching) Pré-requisitos: Phase 17 · 01 (Model Serving), Phase 11 (LLM Engineering) Tempo: ~75 minutos

Objetivos de Aprendizagem

  • Explicar o PagedAttention como um alocador de KV cache: blocos, tabelas de blocos e por que a fragmentação permanece abaixo de 4% sob carga de produção.
  • Diagramar o continuous batching no nível de iteração: como sequências finalizadas deixam o lote e novas sequências entram sem a necessidade de esvaziá-lo.
  • Descrever o chunked prefill em uma única frase e nomear qual métrica de latência ele protege (dica: é a cauda do TTFT, não o rendimento médio).
  • Nomear a armadilha do vLLM v0.18.0 de 2026 que afeta equipes ao ativar todas as otimizações ao mesmo tempo.

O Problema

Um loop de serviço PyTorch ingênuo executa uma requisição por vez: tokenizar, prefill, decodificar até encontrar o token de fim de sequência (EOS), retornar. Com um único usuário, isso funciona. Com cem, gera-se uma fila de pessoas esperando pacientemente. A correção óbvia — static batching (lote estático) — preenche todas as requisições até o comprimento do prompt mais longo da janela de tempo, preenche toda decodificação até a saída mais longa esperada e trava todo o lote na sequência mais lenta. Você paga por um preenchimento (padding) que nunca usa, e requisições rápidas esperam pelas lentas.

O vLLM resolve três problemas de uma só vez. O PagedAttention impede que a fragmentação do KV cache consuma 60% a 80% da memória da GPU, o que ocorre no esquema clássico de alocação contígua. O continuous batching permite que requisições entrem e saiam do lote entre cada iteração de decodificação, mantendo o lote sempre ocupado com trabalho real. O chunked prefill divide um prompt de 32k tokens em fatias de ~512 tokens que se intercalam com a decodificação, evitando que um prompt longo congele todos os outros tokens de decodificação na GPU.

O padrão de produção para 2026 é manter os três ativados. Você precisa entender o funcionamento de cada um deles porque os modos de falha se encontram no agendador, não no modelo.

O Concept

PagedAttention como um sistema de memória virtual

Um KV cache consome num_layers × 2 × num_heads × head_dim × seq_len × bytes_per_element por sequência. Para o Llama 3.3 70B com 8192 tokens, isso equivale a cerca de 1,25 GB por sequência em BF16. Se você reservar previamente 8192 posições para cada requisição, mas a requisição média usar apenas 1500 tokens, você desperdiçará cerca de 82% da memória de alta largura de banda (HBM) reservada. O lote clássico paga por esse desperdício.

O PagedAttention empresta o conceito de memória virtual dos sistemas operacionais. O KV cache não é contíguo por sequência. Ele é alocado em blocos de tamanho fixo (padrão de 16 tokens). Cada sequência possui uma tabela de blocos que mapeia suas posições lógicas de tokens para IDs de blocos físicos. Conforme uma sequência cresce além dos blocos alocados, um novo bloco é adicionado. Quando ela finaliza, seus blocos retornam ao pool.

A fragmentação cai de 60-80% (clássico) para menos de 4% (PagedAttention). Você não ativa o PagedAttention com uma flag — ele é o único alocador que o vLLM oferece. O parâmetro de ajuste é --gpu-memory-utilization (padrão 0.9), que informa ao vLLM quanta HBM deve ser reservada para os blocos de KV após o carregamento dos pesos e ativações.

Continuous batching no nível de iteração

O antigo "dynamic batching" esperava uma janela de tempo (por exemplo, 10 ms) para preencher um lote, então executava prefill + decodificação + decodificação até que todas as sequências fossem finalizadas. As sequências rápidas terminavam antes e ficavam ociosas enquanto a GPU finalizava as lentas.

O continuous batching opera entre cada etapa de decodificação. Chame o conjunto de sequências em execução de lista RUNNING. A cada iteração:

  1. Qualquer sequência em RUNNING que acabou de atingir o EOS ou o limite máximo de tokens (max_tokens) é removida.
  2. O agendador analisa a fila de espera. Se houver blocos de KV livres, ele admite novas sequências (seja para prefill ou retomada).
  3. A passagem forward é executada para tudo o que está na lista RUNNING, gerando um novo token por sequência.

O tamanho do lote nunca é ajustado para um número fixo de preenchimento. Sequências em posições diferentes de suas saídas compartilham uma única passagem forward fundida. No vLLM de 2026, isso é chamado de V1 scheduler. A invariante fundamental: o agendador roda uma vez por iteração de decodificação, não uma vez por requisição.

Chunked prefill protege a cauda do TTFT

A etapa de prefill é limitada por computação (compute-bound). Um prompt de 32k tokens no Llama 3.3 70B leva cerca de 800 ms de prefill puro em uma H100. Enquanto o prefill é executado, os tokens de decodificação de todas as outras sequências no lote esperam. Em um loop de serviço, a latência até o primeiro token (TTFT) de um único prompt longo se torna o pico de latência entre tokens (ITL) para dezenas de outros usuários.

O chunked prefill divide o prefill em fatias de tamanho fixo (padrão de 512 tokens) e agenda cada fatia como uma unidade. Entre as fatias, o agendador pode avançar as sequências de decodificação em um token. Você troca um pequeno impacto absoluto na latência de prefill (alguns ms por fatia) por uma oscilação (jitter) muito menor no tempo de decodificação. O ITL P99 sob carga mista cai de ~50 ms para ~15 ms em benchmarks publicados.

Os três padrões interagem

Todos os três recursos dependem uns dos outros. O PagedAttention fornece ao agendador um recurso granular de KV para gerenciar. O continuous batching precisa desse recurso granular para que a admissão de uma nova sequência não force uma reorganização global. O chunked prefill é uma decisão que o agendador toma na mesma lista RUNNING — trata-se de mais uma política do agendador, não de um sistema separado.

Você não precisa conhecer todas as flags. Precisa saber o que o agendador otimiza: o goodput sob o orçamento de blocos de KV, sujeito ao particionamento do chunked prefill.

A armadilha da versão v0.18.0 de 2026

No vLLM v0.18.0, você não pode combinar --enable-chunked-prefill com decodificação especulativa por modelo de rascunho (--speculative-model). A exceção documentada é a decodificación especulativa baseada em N-gram na GPU no agendador V1. Equipes que ativam todas as flags sem ler as notas de lançamento encontram um erro de execução na inicialização, e não uma regressão sutil de desempenho. Se o ganho especulativo justificava desativar o chunked prefill, reavalie a escolha — a resposta certa em 2026 costuma ser o EAGLE-3 sem chunked prefill, em vez de um modelo de rascunho mais chunked prefill que não compila.

Números que você deve lembrar

  • Llama 3.3 70B FP8, H100 SXM5, 128 conexões simultâneas, os três recursos ativos: 2.200 a 2.400 tok/s.
  • Mesmo modelo, vLLM padrão (sem chunked prefill): ~1.800 tok/s.
  • Mesmo modelo, loop forward PyTorch ingênuo: ~600 tok/s.
  • Desperdício por fragmentação de KV sob PagedAttention sob carga de produção: <4%.
  • ITL P99 sob carga mista: ~15 ms com chunked prefill, ~50 ms sem.

Como o agendador se comporta

while True:
    finished = [s for s in RUNNING if s.is_done()]
    for s in finished: release_blocks(s); RUNNING.remove(s)

    while WAITING and have_free_blocks_for(WAITING[0]):
        s = WAITING.pop(0)
        allocate_initial_blocks(s)
        RUNNING.append(s)

    # agenda fatias de prefill + decode em um único lote
    batch = []
    for s in RUNNING:
        if s.in_prefill:
            batch.append(next_prefill_chunk(s))   # ex: 512 tokens
        else:
            batch.append(decode_one_token(s))     # 1 token

    run_forward(batch)                            # uma chamada única e fundida na GPU

O arquivo code/main.py contém exatamente esse loop em Python padrão com contagens de tokens e latências de forward simuladas. Executá-lo demonstra como o chunked prefill mantém as sequências de decodificação ativas durante um prefill longo.

Use It

O arquivo code/main.py simula um agendador no estilo do vLLM com recursos que podem ser ativados ou desativados. Execute-o para visualizar:

  • Modo NAIVE: uma requisição por vez, sem loteamento.
  • Modo STATIC: preenchimento e espera, loteamento clássico.
  • Modo CONTINUOUS: admissão e liberação no nível da iteração.
  • Modo CONTINUOUS + CHUNKED: fatias de prefill intercaladas com decodificação.

A saída mostra o rendimento total (tokens por segundo virtual), TTFT médio e ITL P99. A linha CONTINUOUS + CHUNKED deve se destacar no tráfego misto.

Ship It

Esta lição produz outputs/skill-vllm-scheduler-reader.md. Dada uma configuração de serviço (tamanho do lote, utilização de memória KV, tamanho do chunked prefill, configuração especulativa), ela gera um diagnóstico do agendador identificando qual das três configurações padrão está limitando o desempenho e o que ajustar.

Exercises

  1. Execute code/main.py. Compare STATIC com CONTINUOUS em uma carga de trabalho com uma mistura de requisições curtas e longas. De onde vem a diferença de rendimento — eficiência de prefill, eficiência de decodificação ou latência de cauda?
  2. Modifique o agendador simplificado para adicionar --max-num-batched-tokens. Qual é o valor correto para uma H100 executando Llama 3.3 70B FP8? (Dica: é uma função do tamanho do bloco de KV e do número de blocos livres, não do HBM bruto.)
  3. Leia novamente as notas de lançamento do vLLM v0.18.0. Quais combinações de flags são mutuamente exclusivas? Liste-as.
  4. Calcule o desperdício por fragmentação do KV cache para um histórico de 1.000 requisições com média de 1.500 tokens de saída, desvio padrão de 600 tokens, sob (a) alocação contígua por requisição limitando a 8192 máximo, (b) PagedAttention com blocos de 16 tokens.
  5. Explique em um parágrafo por que o chunked prefill ajuda a latência ITL P99, mas não o rendimento de forma isolada. De onde vem o ganho de rendimento na prática?

Key Terms

Term What people say What it actually means
PagedAttention "o truque de KV" Alocador de blocos de tamanho fixo para KV cache; fragmentação <4%
Block table "a tabela de páginas" Mapa por sequência da posição lógica do token para o bloco físico de KV
Continuous batching "batching dinâmico, mas feito direito" Decisões de admissão/liberação feitas a cada iteração de decodificação
Chunked prefill "divisão de prefill" Divide um prefill longo em fatias de 512 tokens intercaladas com decodificação
TTFT "tempo até o primeiro token" Prefill + fila + rede; dominado pelo prefill em prompts longos
ITL "latência entre tokens" Tempo entre tokens de decodificação consecutivos; dominado pelo tamanho do lote
Goodput "rendimento que atende ao SLO" Tokens/segundo em que cada requisição ainda atinge os objetivos de TTFT e ITL
V1 scheduler "o novo agendador" Agendador do vLLM de 2026; a decodificação especulativa por N-gram é o caminho compatível com chunked prefill
--gpu-memory-utilization "o botão de memória" Fração de HBM reservada para blocos de KV após pesos e ativações

Further Reading

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).