Transformers documentation

Arquitetura de loteamento contínuo

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Arquitetura de loteamento contínuo

O processamento por lotes tradicional processa um grupo fixo de solicitações em conjunto e aguarda a conclusão da mais lenta antes de iniciar o próximo grupo. Isso faz com que a GPU fique ociosa entre os lotes.

Com o loteamento contínuo, a cada etapa de geração, o agendador verifica as solicitações finalizadas e as substitui imediatamente por solicitações em espera. As solicitações curtas são removidas assim que terminam, e a GPU permanece ocupada o tempo todo. O resultado é um aumento significativo no throughput e uma redução na latência média.

Ciclo de vida da requisição

Uma requisição passa por quatro estados, do envio à conclusão.

       WAIT IN QUEUE          LOAD PROMPT INTO KV       STREAM OUTPUT           DONE
    ┌────────────────┐    ┌─────────────────────┐    ┌─────────────────┐    ┌────────┐
    │    PENDING     │───▶│     PREFILLING      │───▶│    DECODING     │───▶│FINISHED│
    │  ○ ○ ○ · · ·   │    │ ████████░░░░░░░░░░  │    │  → → → → · ·    │    │   ✓    │
    └────────────────┘    └─────────────────────┘    └─────────────────┘    └────────┘
         others ahead            prompt chunks              +1 token/step
  1. Pendente — a solicitação está na fila e aguardando agendamento.
  2. Preenchimento inicial — os tokens do prompt são processados em uma passagem direta.
  3. Decodificação — os tokens de saída são gerados um de cada vez.
  4. Concluído — a geração está completa e o resultado está disponível.

Uma solicitação é movida de pendente para pré-preenchimento quando o agendador encontra orçamento de tokens e espaço em cache suficientes para admitir a solicitação. Se o prompt for longo demais para caber em uma única etapa, o agendador o divide em múltiplas passadas de frente (chunk prefill).

Prefill em blocos

Processar um prompt longo em uma única etapa é caro, pois impede que outras solicitações gerem. O prefill em blocos (chunked prefill) resolve isso dividindo grandes prefills em várias etapas.

Blocking (long prefill blocks the batch)
  Step:   1        2        3        4
  Req A  [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓]   ← one huge prefill
  Req B  ················ wait ················
  Req C  ················ wait ················

Chunked (same prompt split and interleave with decode)
  Step:   1          2          3          4
  Req A  [▓▓ pre]    [▓▓ pre]   [▓▓ pre]   [→ dec]
  Req B  [→ dec]     [→ dec]    [▓▓ pre]   [→ dec]
  Req C  [→ dec]     · idle ·   [→ dec]    · idle ·

Quando o prompt de uma nova solicitação excede o orçamento de tokens disponível (definido por max_batch_tokens em ContinuousBatchingConfig), o agendador processa o máximo de tokens possível e retém o restante. Nas etapas subsequentes, ele continua de onde parou, intercalando o trabalho de prefill com as etapas de decode em andamento. Isso mantém o lote produtivo e reduz o tempo até o primeiro token para as outras solicitações.

Agendador

O scheduler decide quais solicitações entram em cada forward pass com base em dois orçamentos e um limite de solicitações.

  • Orçamento de tokens — o número máximo de tokens de consulta processados em uma única passagem direta, definido por max_batch_tokens em ContinuousBatchingConfig.
  • Orçamento de cache — o número total de páginas de KV que podem ser lidas em uma única passagem, limitado pelo tamanho total do cache.
  • Limite de solicitações — o número máximo de solicitações em uma única passagem de frente, definido por max_requests_per_batch. O tensor de logits escala com o tamanho do vocabulário, portanto esse limite define a memória de pico para modelos com vocabulário grande.

FIFO

FIFOScheduler é o agendador padrão. Ele preenche o lote em ordem de prioridade. As solicitações de decodificação ativas são processadas primeiro, seguidas pelas solicitações de pré-preenchimento ativas e, em seguida, pelas novas solicitações em espera na ordem de chegada. Isso maximiza a taxa de transferência ao processar as solicitações existentes antes de incorporar novas.

PrefillFirst

PrefillFirstScheduler conclui as operações de prefill em blocos antes de retomar o decode. Isso reduz a fragmentação em cargas de trabalho dominadas por prompts longos.

Defina o scheduler em ContinuousBatchingConfig.

cb_config = ContinuousBatchingConfig(scheduler_type="prefill_first")

Gerenciamento de memória

O KV cache é paginado, permitindo que solicitações de diferentes comprimentos compartilhem um pool de memória fixo sem fragmentação. Um pool compartilhado de blocos de memória, implementado como CachePool, fornece esses blocos a cada alocador de tipo de atenção, que os divide em blocos e páginas.

Shared pool (first two chunks reserved for padding / never allocated)

  chunk N  [ ======================================== ]
               |                            |
    full attn: [ FULL BLOCK 0 | FULL BLOCK 1 | … ]
               [ page L0 | page L0 | … ]   (one page per full layer)

    sliding:   [ ----------- SLIDING BLOCK 0 ----------- ]
               [ page L1 | page L2 | page L3 | … ]  (one page per sliding layer)

  page  = page_size tokens of KV for one layer
  block = one page per layer of that attention type
  chunk = pool unit sized so full and sliding blocks tile the same bytes

O continuous batching suporta apenas dois tipos de atenção: full_attention (FullAttentionCacheAllocator) e sliding_attention (SlidingAttentionCacheAllocator). Modelos híbridos recebem um alocador de cada tipo. O tipo de atenção vem do modelo (layer_types / sliding_window), e não de ContinuousBatchingConfig.

As camadas deslizantes mantêm apenas uma janela fixa de tokens recentes, reutilizando um pequeno conjunto de páginas. Elas não suportam tabelas de blocos de kernel nem compartilhamento de blocos.

O cache reserva dois blocos à frente dos blocos de dados para leituras e gravações de preenchimento que as solicitações nunca utilizam. Esses blocos nunca são alocados para solicitações.

Dimensionamento do cache

O manager infere, na inicialização, quantos chunks cabem com base na memória GPU livre. Ele considera os tensores de KV, as máscaras de atenção, as ativações e os índices de controle, e dimensiona o pool para caber dentro de max_memory_percent da memória disponível. Quando não definido, ele assume um valor padrão de acordo com o hardware e os parâmetros de continuous batching. No MPS, reserve menos espaço para o cache, já que a CPU e a GPU compartilham a memória, a fim de evitar swapping. num_blocks expressa a capacidade em blocos inteiros do modelo de page_size tokens quando definido explicitamente.

Você pode fixar os valores em ContinuousBatchingConfig.

cb_config = ContinuousBatchingConfig(
    page_size=256,
    num_blocks=4096,
    max_batch_tokens=512,
    max_memory_percent=0.8,
)

Admissão

Antes de uma solicitação entrar no lote, o agendador verifica se há capacidade livre suficiente para todos os alocadores de cada tipo de atenção. Se qualquer alocador ficar sem espaço, a solicitação é rejeitada e nada é alocado.

Para evitar que o cache seja preenchido até que o offloading seja a única opção, o scheduler impõe uma safety_margin. Quando a capacidade livre do cache fica abaixo de safety_margin, novos prefills são retidos e apenas os decodes ativos continuam. O scheduler FIFO usa 0.15 por padrão (15% da capacidade mantida em reserva), enquanto prefill_first usa 0.0 por padrão. Defina safety_margin em ContinuousBatchingConfig para ajustá-la, onde 0 desativa a margem.

Cache de prefixo

Quando duas solicitações compartilham um prefixo de prompt, elas podem compartilhar os blocos que armazenam o KV para esse prefixo. Cada bloco concluído é identificado por um hash de conteúdo. Uma solicitação posterior com um prefixo correspondente reutiliza o bloco e ignora o pré-processamento (prefill) para esses tokens. Os blocos compartilhados são contados por referência e só retornam ao pool de blocos livres quando todas as solicitações que os utilizam tiverem sido concluídas.

allow_block_sharing tem o valor padrão True. O compartilhamento de prefixo fica ativo apenas quando todos os alocadores suportam compartilhamento, de modo que apenas modelos com atenção total o utilizam. Modelos puramente deslizantes e híbridos o desativam. Defina allow_block_sharing=False para cargas de trabalho com prompts curtos e gerações longas, em que o controle de alocação supera as economias.

Evasão

Os blocos liberados não são apagados e permanecem “inicializados” com seu conteúdo e hash intactos, para que uma correspondência de prefixo posterior possa reutilizá-los. Quando o pool não inicializado fica baixo, o gerenciador rebaixa preguiçosamente o bloco inicializado mais recente para não inicializado, descartando seu hash. O cache de prefixo é uma camada de melhor esforço que o alocador descarta para continuar atendendo novas solicitações.

Reset suave

O soft reset é o caminho de fallback para offloading. Quando o gerenciador não consegue copiar o KV cache de uma requisição para o pool de swap da CPU, porque o offloading de CPU está desativado ou o pool está cheio, ele realiza um soft reset na requisição. Ele anexa os tokens gerados pela requisição ao seu prompt original, libera seus blocos de cache e a adiciona novamente à fila. A seção Offloading detalha quais requisições são selecionadas.

Quando o cache tem espaço novamente, a solicitação retoma de onde parou, com o histórico de geração codificado no prompt.

Gráficos CUDA

Cada etapa de geração envolve sobrecarga de CPU, desde a montagem do lote até o despacho dos kernels de GPU e a leitura dos resultados. Os CUDA graphs eliminam essa sobrecarga ao gravar a sequência completa de execução na GPU uma única vez e reproduzi-la para lotes com formatos compatíveis.

Como as formas do lote mudam a cada etapa, o sistema de loteamento contínuo lida com isso por meio de preenchimento e cache.

  1. Os comprimentos das consultas são preenchidos até o múltiplo mais próximo de q_padding_interval_size.
  2. Os comprimentos de KV são preenchidos até o múltiplo mais próximo de kv_padding_interval_size.
  3. Os grafos gravados são armazenados em um cache.

Quando a forma com padding de um lote corresponde a um gráfico em cache, o gráfico é reproduzido sem nenhum despacho de CPU. Novas formas acionam a captura de um novo gráfico.

Os gráficos CUDA exigem fluxo de controle estático e são incompatíveis com máscaras de atenção. Eles são detectados automaticamente por padrão e desativados quando as condições não são atendidas.

Loteamento assíncrono

O loteamento assíncrono usa dois pares de buffers de E/S e dois fluxos CUDA para sobrepor o trabalho da CPU e da GPU.

Sequential
  CPU  ── [prep N] ·········idle········· [prep N+1] ·········idle·········
  GPU  ·········idle········· [compute N] ·········idle········· [compute N+1]

Async
  CPU  ── [prep N] [prep N+1] [prep N+2] ──
  GPU  ── ········ [compute N] [compute N+1] ──

Enquanto a GPU calcula o lote N, a CPU prepara o lote N+1. No entanto, sobrepor as duas operações requer aproximadamente o dobro de VRAM.

O loteamento assíncrono requer que os gráficos CUDA estejam ativos, pois a reprodução do gráfico fornece os endereços de tensor estáveis necessários para que a sobreposição de streams seja correta.

Offloading

Requisições que geram saídas longas podem crescer até que o KV cache não tenha mais espaço para alocar o próximo bloco. Em vez de falhar, o gerenciador transfere as requisições ativas de volta para a fila de espera, liberando blocos para as requisições que permanecem.

O offloading é orientado pela demanda. O agendador relata as solicitações stalled, ou seja, as solicitações ativas que não conseguiram alocar os blocos de que precisam, juntamente com a quantidade de blocos que cada uma exige. O gerenciador faz o offloading do menor número de solicitações necessário para liberar blocos suficientes para cobrir essa demanda. Ele faz o offloading das mais recentes primeiro, de modo que as solicitações já agendadas no lote atual mantenham seu cache, e a última solicitação ativa restante nunca é submetida a offloading.

Duas situações acionam o offloading.

  • Um lote é agendado, mas algumas solicitações ativas ficaram paradas. O gerenciador descarrega o suficiente das solicitações paradas para que o próximo lote possa alocar blocos para as demais.
  • O cache está cheio e nenhum lote pode ser agendado. O gerenciador descarrega as solicitações e tenta agendar novamente em um loop dentro da mesma etapa, até que um lote caiba ou até que nenhuma solicitação possa ser descarregada. Ele não espera mais pelo próximo passo de propagação para frente para se recuperar.

Uma solicitação descarregada segue um de dois caminhos. Quando cpu_offload_space é maior que 0.0, o gerenciador copia os blocos do cache KV da solicitação para um buffer de CPU fixo pré-alocado, agrupando todas as solicitações que cabem no pool de troca em uma única transferência. Depois que o espaço do cache da GPU fica disponível, o gerenciador copia os blocos de volta para a GPU e retoma a solicitação sem recalcular seu prompt e os tokens gerados. Solicitações que não cabem no pool, ou todas as solicitações descarregadas quando o descarregamento para a CPU está desativado, recorrem a um soft reset.

Próximas etapas

Update on GitHub