Vida útil de uma solicitação de inferência (vLLM V1) - como os LLMs são atendidos com eficiência em escala
Ubicloud é uma alternativa de código aberto à AWS. Oferecemos serviços de nuvem gerenciados que se baseiam em PostgreSQL, Kubernetes, vLLM e outros.
O vLLM é um mecanismo de inferência de código aberto que atende a grandes modelos de linguagem. Implantamos várias instâncias do vLLM em GPUs e carregamos modelos de peso aberto, como o Llama 4, nelas. Em seguida, balanceamos a carga do tráfego entre as instâncias do vLLM, executamos verificações de integridade e fazemos atualizações. Nossos clientes consomem nosso serviço gerenciado enviando seus prompts para nossos endpoints de API. Esse endpoint também determina a instância do vLLM que atende ao prompt.
O vLLM fica na interseção da IA e da programação de sistemas, então pensamos que mergulhar em seus detalhes poderia interessar a alguns de nossos leitores. Nesta postagem do blog, descrevemos como uma solicitação de inferência viaja pelo servidor de API compatível com OpenAI do vLLM e pelo mecanismo principal. Também fornecemos ponteiros de código chave.
Presumimos que os leitores já estejam familiarizados com a arquitetura do transformador e os grandes modelos de linguagem. Se você não estiver, recomendamos este vídeo do cofundador da OpenAI, Andrej Karpathy. Vamos nos concentrar na nova arquitetura V1 do vLLM e como ela alcança o desempenho de geração de texto de última geração. Se você estiver procurando o comportamento V0 ou a inferência multimodal, consulte outra documentação do vLLM.
Terminologia
Usamos os seguintes termos ao longo deste blog. Esses termos também se alinham com o que é usado na base de código e na documentação do vLLM:
- Solicitação: uma mensagem de conclusão de bate-papo recebida do cliente, formatada no formato compatível com OpenAI.
- Sequência: o fluxo combinado de tokens de prompt e resposta associados a uma solicitação. Exceto em casos especiais, uma única solicitação normalmente corresponde a uma sequência. Às vezes, Sequence é usado de forma intercambiável com Request na base de código e nesta postagem do blog.
- Envio em lote: o processo de agrupar várias solicitações em uma única passagem direta pelo modelo para maximizar a utilização da GPU.
- Pré-preenchimento: o estágio no qual o modelo processa os tokens de prompt de uma nova solicitação. Ele calcula a atenção do transformador e armazena os tensores Key (K) Value (V) da atenção para cada token e cada camada nos blocos de cache KV.
- Decodificar: o estágio no qual o modelo gera o próximo token de saída ou o tensor de saída para camadas intermediárias repetidamente para solicitações contínuas. Ele reutiliza o cache KV armazenado de todos os tokens anteriores e calcula os tensores de Consulta (Q) com base no token mais recente.
- Cache KV: A região de memória da GPU usada para armazenar as chaves e valores de atenção do transformador para cada token em uma solicitação. Ele é gerenciado globalmente em todas as solicitações e dispositivos no vLLM.
- Bloco KV: Uma unidade de memória de tamanho fixo no cache KV que contém chaves e valores para um número fixo de tokens. O cache KV é segmentado em blocos para minimizar leituras/gravações de memória.
Arquitetura de alto nível
Antes de mergulhar no fluxo de solicitações, vamos primeiro dar uma olhada nos principais componentes da arquitetura de serviço do vLLM V1:
- AsyncLLM: um wrapper assíncrono em torno do mecanismo principal. O servidor compatível com OpenAI usa AsyncLLM para enviar solicitações e recuperar saídas do EngineCore de forma assíncrona. Ele se comunica com o EngineCore via AsyncMPClient usando IPC (comunicação entre processos) assíncrona. Ele também lida com tokenização e destokenização.
- EngineCore: o mecanismo principal que realiza a inferência. Ele é responsável pelo agendamento, o que significa agrupar e alocar tokens para solicitações. Ele também executa o modelo, processando tokens de prompt ou gerando novos. É o coração do vLLM onde reside a maior parte da lógica. No núcleo, há um loop ocupado que extrai dados de uma fila de entrada interna e executa uma etapa do mecanismo. Uma etapa do mecanismo inclui o agendamento e a execução de uma passagem para frente do modelo. Os resultados são enviados primeiro para uma fila de saída interna e, em seguida, enviados de volta para o AsyncLLM e o servidor de API. Dois threads adicionais em segundo plano gerenciam a transferência de dados assíncrona entre as duas filas internas de entrada/saída e o AsyncLLM sobre IPC.
- Agendador: um componente dentro do mecanismo que lida com a forma como várias solicitações são agrupadas em lote. O agendador mantém filas de solicitações e decide em cada etapa quantos tokens processar para cada solicitação.
- ModelExecutor: um componente que coordena o carregamento e a execução do modelo em um ou mais processos de trabalho da GPU. Internamente, ele usa a biblioteca de computação distribuída ray e inicia um processo de trabalho por dispositivo GPU.
- ModelRunner: cada trabalhador da GPU tem um ModelRunner que carrega o modelo e executa as passagens para frente. O ModelRunner recebe solicitações em lotes do agendador. Ele processa essas solicitações por meio do modelo em todos os núcleos CUDA ao mesmo tempo. Ele cuida da montagem de tensores de entrada e pode usar otimizações como captura de gráfico CUDA para execuções repetidas.
- KVCacheManager: módulos de gerenciamento de memória GPU do vLLM, responsáveis por gerenciar o espaço para os tensores KV do transformador na GPU para todas as solicitações. Ele trata a memória da GPU como um sistema de paginação. O cache KV é armazenado em blocos de tamanho fixo ou páginas. Isso permite uma alocação dinâmica eficiente e evita a necessidade de grandes reservas de memória contígua. O Agendador usa o KVCacheManager para alocar esses blocos e anexa as IDs de bloco a cada solicitação. Em seguida, o ModelRunner usa essas IDs ao preparar o contexto de execução.
Recebendo a solicitação – Servidor de API e IPC assíncrono
A jornada começa quando uma solicitação HTTP chega ao servidor vLLM (por exemplo, um POST para /v1/chat/completions). O servidor de API compatível com OpenAI lida com a comunicação e autenticação HTTP (por meio da chave de API, se configurada). Esse servidor geralmente é iniciado executando o comando vllm serve definido por vllm/entrypoints/cli/serve.py.
Após a validação, o servidor invoca o mecanismo AsyncLLM para lidar com a solicitação. No código, isso é feito chamando o método do mecanismo AsyncLLM generate(). O método recebe o prompt e um request_id recém-gerado para rastrear essa solicitação. Quando o generate() é chamado, o AsyncLLM executa a tokenização com o tokenizer do modelo (geralmente do Hugging Face) para converter texto em IDs de token. Algum processamento de entrada multimodal também acontece neste estágio, mas para simplificar, vamos nos concentrar em prompts de texto aqui. Em seguida, ele envia a solicitação para o EngineCore via AsyncMPClient usando chamadas IPC assíncronas.
Em seguida, um loop em segundo plano no EngineCore pega a solicitação do canal IPC e a coloca em uma fila de entrada interna para agendamento.
Observe que o AsyncLLM e o EngineCore são executados em processos diferentes. Isso ignora o Global Interpreter Lock (GIL) do Python. Ele permite que tarefas com uso intensivo de CPU, como tokenização e comunicação HTTP, sejam executadas junto com a tarefa com uso intensivo de GPU, a execução do modelo. Isso maximiza a taxa de transferência geral.
Agendamento e envio contínuo em lote
O módulo Scheduler é fundamental para a capacidade do vLLM de obter alta taxa de transferência. Ele acompanha todas as solicitações e orquestra seu progresso. O agendador mantém um deque de espera para solicitações que estão prontas para serem processadas (novas ou retomadas) e uma lista em execução para solicitações atualmente em geração ativa. Em cada iteração (ciclo) do mecanismo, ele primeiro examina a fila de entrada interna e adiciona novas solicitações ao dispositivo de espera do agendador. Em seguida, ele escolhe um conjunto de solicitações para avançar, examinando primeiro as solicitações da lista em execução e, em seguida, as solicitações do deque em espera.
O vLLM usa um algoritmo de envio em lote contínuo. Isso ajuda a maximizar a utilização da GPU, mantendo uma carga de trabalho completa dentro de um orçamento de token fixo, também conhecido como max_num_batched_tokens. Ele também garante a justiça na ordem da solicitação e limita o lote a apenas uma passagem para frente do modelo.
Aqui está um exemplo de envio em lote contínuo:
Suponha que tenhamos três solicitações com 3, 5 e 12 tokens de prompt, respectivamente, e um orçamento de token de 10. Em um servidor tradicional, eles podem ser tratados sequencialmente ou em lotes de tamanho fixo. No vLLM, no entanto, o agendador pode decidir em uma iteração (Etapa 0) alimentar, por exemplo, 3 tokens de prompt de R1, 5 de R2 e 2 de R3 em uma única passagem de encaminhamento. Observe que ele escolhe apenas dois tokens de prompt de R3 devido ao orçamento de token de 10. Na próxima iteração (Etapa 1), ele pode continuar com os 8 tokens de prompt restantes de R3. Ao mesmo tempo, ele começará a gerar 1 token cada para R1 e R2. Eles terminaram de preencher seus tokens de prompt e agora estão na fase de decodificação.
Observe que, durante a fase de preenchimento prévio, todos os tokens de prompt de uma solicitação podem ser processados em um lote. Isso é possível porque os tensores de consulta (Q), calculados a partir dos tokens imediatamente anteriores a eles, estão disponíveis para cada posição de token de prompt. Na fase de decodificação, no entanto, o token imediatamente anterior deve ser decodificado iterativamente, portanto, só podemos processar um token por vez por solicitação. O agendador garante que todas as solicitações concluam a fase de preenchimento prévio antes de entrar na fase de decodificação.
Durante a fase de agendamento, os blocos de cache KV também são determinados. O Gerenciador de Cache KV agrupa tokens em partes de tamanho fixo e aloca um novo bloco KV ou recupera um bloco KV existente para cada parte dos tokens. Observe que a alocação real de memória da GPU ocorre durante a inicialização do vLLM. O Gerenciador de Cache KV determina as IDs de bloco para os caches KV de cada solicitação e anexa essas IDs à solicitação, para que possam ser usadas posteriormente pelos ModelRunners ao executar o modelo.
Execução do modelo - Passagem para frente na GPU
Depois que o agendador determina quais solicitações devem ser avançadas, ele invoca os ModelRunners por meio do ModelExecutor baseado em ray. Isso executa a passagem para frente do modelo, processando os tokens de prompt ou gerando novos tokens.
Todos os tokens das solicitações selecionadas são combinados em um único tensor grande, também conhecido como lote, e são processados camada por camada por meio das matrizes de peso do transformador. Em cada camada, os três tensores de atenção, Chave (K), Valor (V) e Consulta (Q), de cada cabeça de atenção são calculados. Em seguida, um tensor de saída de atenção final para a camada é calculado a partir desses tensores. Os tensores K e V são armazenados para uso futuro e o tensor de saída se torna a entrada para a próxima camada. É aqui que as GPUs entram em ação, pois são ótimas em cálculos de matrizes grandes. Cada tensor inclui dados de todas as solicitações em lote. Isso permite que cada operação de matriz dessas solicitações seja processada na GPU em paralelo usando SIMD(T) (instrução única, vários dados e vários threads) em todos os núcleos CUDA.
Para solicitações na fase de decodificação, o tensor de saída da camada final do transformador produz os logits, que representam as probabilidades previstas para o próximo token. O vLLM então aplica a estratégia de amostragem ou decodificação. Para cada sequência, ele analisa os logits e seleciona o token superior (greedy ou determinístico) ou amostras de acordo com os parâmetros fornecidos, por exemplo, temperatura. Esse processo produz o próximo token para cada solicitação em lote. Esses tokens são colocados em uma fila de saída interna, onde um loop em segundo plano os pega e os envia de volta ao AsyncLLM via IPC para processamento de saída.
Este diagrama de linha do tempo mostra uma parte da passagem para frente para o modelo Qwen 32B destilado DeepSeek R1 que atendemos.
Dentro do loop ocupado do EngineCore, o método forward do modelo é executado. Esse método chama os métodos de encaminhamento de cada camada do transformador. Em seguida, o método forward de cada camada chama o módulo FlashAttention. FlashAttention é uma função altamente otimizada para calcular a atenção do transformador. Ele usa o algoritmo FlashAttention-3. Este modelo contém um total de 64 camadas de transformador e este diagrama de linha do tempo mostra 3 delas na parte inferior. O corredor do modelo repete a passagem para frente através de todas as 64 camadas do transformador em cada etapa do motor.
Processamento de saída – Tokens de streaming de volta
O AsyncLLM obtém os novos tokens do canal IPC. Em seguida, ele processa esses tokens destokenizando-os e colocando-os em uma fila de saída interna. O chamador original, a função generate() do AsyncLLM, irá pegá-los e passá-los de volta para o servidor de API.
No modo sem streaming, o manipulador de API acumularia tokens internamente até que a solicitação fosse concluída e, em seguida, retornaria a saída final. No modo de streaming, o manipulador enviará cada saída parcial imediatamente para o cliente em partes como dados: {…}
Conclusão
Nesta postagem do blog, analisamos toda a vida útil de uma solicitação de inferência vLLM. A jornada começa com o servidor de API entregando a solicitação ao AsyncLLM, que tokeniza e envia solicitações ao EngineCore. No EngineCore, o agendador agrupa solicitações. O executor trabalha com o ModelRunner para executar passagens para frente pelas camadas de atenção em GPUs. Em seguida, os tokens são transmitidos de volta aos clientes. Cada componente desempenha um papel distinto nesse ciclo de vida e, juntos, permitem que o vLLM atinja um desempenho de serviço de última geração.
Para engenheiros de IA que desejam implantar LLMs, esperamos que entender esse ciclo de vida ajude a ajustar e personalizar o vLLM. Para outros, esperamos que esse detalhamento forneça informações sobre o funcionamento interno do vLLM. Ao fazer isso, pretendemos desmistificar como grandes modelos de linguagem são atendidos de forma eficiente e em escala.