Inteligência artificial, sem ruído.
Infraestrutura7 min

Continuous Batching: o scheduler que mantém suas GPUs cheias na inferência de LLMs

Entenda como vLLM, TGI e SGLang usam continuous batching, PagedAttention e chunked prefill para maximizar throughput de GPUs em produção — e por que static batching já era.

Continuous Batching: o scheduler que mantém suas GPUs cheias na inferência de LLMs

O gargalo invisível da inferência

Servir um modelo de linguagem em produção é, no fundo, um problema de agendamento disfarçado de problema de hardware. Uma GPU moderna é uma máquina de throughput: ela quer milhares de operações aritméticas ao mesmo tempo. Mas as requisições chegam uma a uma, em momentos imprevisíveis, com prompts e respostas de tamanhos muito diferentes.

O trabalho do scheduler de inferência é casar uma máquina que brilha no trabalho em lote com uma carga que pinga irregularmente. Continuous batching — ou agendamento em lote contínuo — é a resposta que os principais sistemas de serving adotaram, e entender sua mecânica é essencial para quem opera LLMs em produção.

Uma requisição, uma GPU

A geração de texto com transformers acontece em duas fases. A primeira é o prefill: o modelo ingere o prompt inteiro em uma única passada forward, computando atenção sobre todos os tokens de uma vez e construindo o KV cache — as chaves e valores armazenados que os tokens futuros vão referenciar. O prefill é compute-bound: muita matemática paralela, e as unidades aritméticas da GPU são o gargalo.

A segunda fase é o decode: o modelo gera a resposta um token por vez, e cada novo token exige sua própria passada forward. O decode é memory-bound: cada passo faz uma fatia pequena de computação, mas precisa mover o conjunto inteiro de pesos do modelo e o KV cache crescente pela memória.

Quando você roda uma requisição por vez, a GPU fica quase ociosa durante o decode — move gigabytes de pesos para produzir a aritmética de um único token.

Static batching: a solução óbvia (e seus limites)

Se uma requisição desperdiça a GPU no decode, a correção intuitiva é rodar várias juntas. Como o decode é memory-bound, adicionar mais sequências ao mesmo passo tem custo marginal quase zero. Isso é o static batching.

O sistema coleta um grupo de N requisições, aplica padding em todas as sequências até o comprimento da mais longa (porque tensores exigem formato uniforme) e roda passadas forward até que todas terminem. Depois devolve as N respostas e recomeça.

O problema está na decisão de congelar a composição do lote do início ao fim. Isso produz três falhas:

  • Head-of-line blocking: uma requisição de 10 tokens que cai no mesmo lote que uma de 500 fica refém — terminou faz tempo, mas só é liberada quando o lote inteiro drena.
  • Slots ociosos: conforme sequências terminam antes, suas posições no lote ficam vazias, mas continuam alocadas. Você passa o resto da vida do lote computando em um tensor meio vazio.
  • Sem admissão no meio do voo: uma requisição que chega um passo depois do lote começar precisa esperar o lote inteiro terminar. Sob tráfego constante, isso produz picos de latência.

A raiz dos três problemas: o static batching decide uma vez por lote, mas a carga muda a cada token.

Continuous batching: reagendando a cada token

Introduzido pelo sistema Orca em 2022, o continuous batching move a decisão de agendamento do lote para o token individual. Em vez de escolher um lote e rodá-lo até o fim, o scheduler executa antes de cada passada forward e redefine quais requisições estão no lote.

O lote deixa de ser um grupo congelado e vira um elenco vivo que muda a cada token. A engine roda em loop: o scheduler escolhe o lote atual, o modelo executa uma passada, os tokens amostrados são anexados a cada sequência, e o scheduler atualiza sua contabilidade — aposentando qualquer requisição que acabou de emitir um token de fim de sequência e liberando seu KV cache imediatamente.

O resultado é que a capacidade liberada está disponível no passo seguinte. O head-of-line blocking desaparece (a requisição curta sai no instante em que termina). Os slots ociosos desaparecem (a capacidade liberada é preenchida imediatamente). E a admissão no meio do voo desaparece (uma requisição nova entra em um ou dois passos).

Mas o continuous batching introduz dois problemas novos. Primeiro, uma requisição recém-admitida não pode simplesmente começar a decodificar — seu prompt precisa passar pelo prefill, uma rajada compute-bound que pode travar o decode de todas as outras. Segundo, como você está livre para admitir requisições a cada passo, pode supercomprometer o KV cache e ficar sem memória no meio da geração.

Intercalando prefill e decode

O prefill de uma requisição nova é uma rajada pesada de computação, enquanto os passos de decode são uma corrente de atualizações leves. Cada engine precisa encaixar essa rajada no fluxo de decode sem engasgar.

As três respostas são: prefill-first (pausa os decodes, roda o prefill — simples mas produz solavancos visíveis), chunked prefill (fatia o prefill longo em pedaços menores e os entrelaça com os decodes, para que nenhum passo seja dominado pelo prefill) e disaggregation (roda prefill e decode em pools separados de GPUs, transferindo o KV cache entre eles).

O chunked prefill virou padrão na maioria das engines modernas porque suaviza a latência sem hardware extra. A desagregação é uma abordagem mais pesada que compensa em escala muito grande.

Quando o KV cache transborda

O segundo problema é memória. Como o continuous batching continua admitindo requisições enquanto houver espaço, e o KV cache de cada requisição ativa cresce a cada token gerado, um lote que cabia há um instante pode transbordar a memória da GPU.

O PagedAttention, introduzido pelo vLLM, toma emprestada a ideia de memória virtual e paginação dos sistemas operacionais. O KV cache é quebrado em blocos pequenos de tamanho fixo, e a sequência logicamente contígua de uma requisição é mapeada, via tabela de blocos, para blocos físicos espalhados pela memória da GPU. Isso elimina fragmentação, permite alocar memória sob demanda conforme as sequências crescem, e torna preemption e compartilhamento de prefixo baratos.

Quando nem o PagedAttention dá conta, entra a preemption: o scheduler recupera memória de uma requisição já admitida para que as demais continuem. Há dois métodos: recomputation (descarta o KV cache e reconstrói depois refazendo o prefill — barato em memória, caro em computação) e swapping (copia os blocos para memória CPU e os traz de volta quando a requisição for retomada — preserva a computação, mas paga em largura de banda PCIe e RAM do host).

Onde vLLM, TGI e SGLang diferem

As técnicas acima — chunked prefill, prefix caching e memória paginada — já foram em grande parte consolidadas em um kit de ferramentas compartilhado. O que ainda difere é menos sobre quais técnicas cada engine tem e mais sobre ênfases e arquitetura.

vLLM é a referência memory-first. O PagedAttention é sua contribuição-assinatura. A engine V0 usava prefill-first (com os solavancos de latência), enquanto a V1 torna o chunked prefill o padrão.

TGI (Text Generation Inference, da Hugging Face) divide o serving em duas partes: um router rápido em Rust que gerencia filas e decisões de batching, e workers separados que só executam passadas forward do modelo. A Hugging Face investiu pesado em prefix caching, tornando o TGI especialmente forte em prompts longos e repetidos.

SGLang levou o reúso de prefixo mais longe. Seu RadixAttention organiza prefixos cacheados em uma árvore, permitindo que requisições que compartilham system prompts, exemplos few-shot ou definições de ferramentas reutilizem o KV cache umas das outras. A engine é particularmente forte em cargas com grande sobreposição de prompts, como agentes e pipelines de geração estruturada.

O que isso significa para quem opera LLMs

Para o profissional brasileiro que está colocando modelos em produção — seja numa GPU Droplet da DigitalOcean, numa instância AWS ou on-premise — a escolha da engine de serving e a compreensão do continuous batching afetam diretamente latência, throughput e custo.

O continuous batching mantém a GPU ocupada. O PagedAttention estica a memória. E o chunked prefill evita os solavancos. Juntos, esses três mecanismos são o que separa um serviço de inferência que funciona de um que derrete sob carga real.

Enquanto isso, a fronteira está se movendo para fora (prefill e decode em hardware separado, como no NVIDIA Dynamo) e para baixo (técnicas como speculative decoding e quantização de KV cache para extrair mais trabalho de cada passo memory-bound).


Descubra mais sobre noticiAI

Assine para receber nossas notícias mais recentes por e-mail.

R
Sobre o autorRedação Noticiai

Equipe editorial dedicada a explicar inteligência artificial com clareza, independência e contexto.