Está a ter dificuldades com a IA ou com o desenvolvimento full-stack? Os nossos especialistas estão aqui para o orientar: aconselhamento personalizado, integração técnica e muito mais. Entre em contacto com [email protected].

Técnicas de otimização da inferência LLM

A otimização da inferência é uma parte essencial das aplicações de IA generativa implementadas na produção. Usar LLMs eficientemente em escala é um desafio e muitas técnicas foram desenvolvidas nos últimos anos para tornar a inferência mais rápida e barata. Vamos analisar essas técnicas neste artigo.

Técnicas de otimização da inferência LLM

A Arquitetura dos LLMs

Os modelos de linguagem de grande dimensão (LLM) baseiam-se todos na arquitetura transformadora inventada em 2017 por Vaswani et al. A arquitetura transformadora consegue uma precisão superior, uma aprendizagem de poucos disparos e capacidades quase humanas em diversas tarefas linguísticas. No entanto, estes modelos de base, muitas vezes compostos por dezenas a centenas de milhares de milhões de parâmetros, são dispendiosos de treinar e consomem muitos recursos durante a inferência. Os custos de inferência aumentam com contextos de entrada longos, que exigem um poder de processamento significativo devido aos grandes dados de entrada. Isto torna a inferência eficiente um desafio crítico, particularmente na gestão da memória e dos recursos de computação.

A arquitetura do transformador
A arquitetura do transformador

Mais especificamente, os LLM mais conhecidos são os LLM apenas de descodificação, como o GPT-3, GPT-4, LLaMA, Mistral, DeepSeek, etc. Estes modelos são pré-treinados numa tarefa de modelação causal, funcionando como preditores de palavras seguintes. Processam uma sequência de tokens como entrada e produzem tokens seguintes de forma auto-regressiva até ser atingida uma condição de paragem.

A inferência LLM em modelos só de descodificador envolve duas fases fundamentais: a fase de pré-preenchimento e a fase de descodificação. Na fase de pré-preenchimento, o modelo processa tokens de entrada para computar estados intermediários (chaves e valores) para gerar o primeiro novo token. Essa fase, semelhante a uma operação matriz-matriz, é altamente paralelizada e utiliza eficientemente os recursos da GPU. Por outro lado, a fase de descodificação gera tokens, um de cada vez, com base nos estados dos tokens anteriores. Esta operação matriz-vetor é limitada pela memória, uma vez que a transferência de dados para a GPU, e não a velocidade de computação, dita principalmente a latência, levando à subutilização da potência de computação da GPU.

A otimização da fase de descodificação é um ponto fulcral para enfrentar os desafios da inferência. As soluções incluem o desenvolvimento de mecanismos de atenção eficientes e um melhor gerenciamento de chaves e valores para reduzir os gargalos de memória. O post destaca abordagens práticas para melhorar o desempenho da inferência, assumindo que os leitores têm uma compreensão básica da arquitetura do transformador e dos mecanismos de atenção. Essas otimizações são cruciais para melhorar a taxa de transferência e reduzir a latência em implantações LLM do mundo real.

Uma outra complicação decorre da utilização de diferentes tokenizadores nos LLMs, o que afecta a comparabilidade dos tokens. Os tokens, aproximadamente equivalentes a quatro caracteres ingleses, variam em representação dependendo do tokenizador, o que torna as comparações diretas do rendimento da inferência (por exemplo, tokens por segundo) enganadoras. Esta variabilidade sublinha a necessidade de métricas de avaliação padronizadas para avaliar e comparar com precisão o desempenho do LLM durante a inferência.

Loteamento

O agrupamento é uma estratégia fundamental para melhorar a utilização e o rendimento da GPU em modelos de linguagem de grande porte (LLMs). Ao processar várias solicitações simultaneamente usando o mesmo modelo, o lote distribui o custo de memória dos pesos do modelo entre as solicitações, permitindo que lotes maiores aproveitem mais poder de computação da GPU. No entanto, há um limite para o tamanho do lote, pois lotes excessivamente grandes podem causar estouro de memória devido às demandas de memória dos LLMs, particularmente relacionadas ao cache de valor-chave (KV) (mais sobre isso mais tarde).

Técnicas de loteamento
Técnicas de loteamento

O lote tradicional ou estático tem limitações porque os pedidos dentro de um lote geram frequentemente números diferentes de tokens de conclusão, levando a tempos de execução variados. Isso faz com que todas as solicitações aguardem a conclusão da mais lenta, o que pode ser problemático quando os comprimentos de geração variam significativamente. Para resolver este problema, foram desenvolvidas técnicas avançadas como o batching em voo para otimizar o desempenho.

O batching em voo, também conhecido como batching contínuo, aborda os desafios impostos pela natureza dinâmica das cargas de trabalho de LLM, que podem variar de respostas simples de chatbot a resumos de documentos complexos ou geração de código. Essas tarefas produzem resultados de tamanhos muito diferentes, dificultando o lote e a execução eficiente de solicitações em paralelo. Ao contrário do batching estático, o batching in-flight permite que o servidor remova imediatamente as sequências concluídas do batch e comece a processar novas solicitações enquanto outras ainda estão em andamento. Essa abordagem aumenta significativamente a utilização da GPU, adaptando-se aos tempos de execução variáveis das solicitações em cenários do mundo real.

Implementação multi-GPU com paralelização de modelos

A paralelização de modelos é uma estratégia crítica para gerir a memória e as exigências computacionais de modelos de aprendizagem automática em grande escala, distribuindo-os por várias GPUs. Essa abordagem permite o manuseio de modelos maiores ou lotes de entrada que excedem a capacidade de memória de um único dispositivo, tornando-a essencial para o treinamento e a inferência quando as restrições de memória são pequenas. Existem várias técnicas para dividir os pesos dos modelos, incluindo paralelismo de pipeline, paralelismo de tensor e paralelismo de sequência, cada uma abordando diferentes aspectos da distribuição de modelos. Ao contrário do paralelismo de dados, que se concentra na replicação de pesos de modelos entre dispositivos para processar lotes de entrada maiores durante o treinamento, esses métodos são mais relevantes para reduzir as pegadas de memória durante o treinamento e a inferência.

Várias GPUs NVIDIA
Várias GPUs NVIDIA

O paralelismo de pipeline divide o modelo verticalmente em partes sequenciais, com cada parte contendo um subconjunto de camadas atribuído a um dispositivo separado. Por exemplo, numa configuração de pipeline de quatro vias, cada dispositivo lida com um quarto das camadas do modelo, passando as saídas para o dispositivo seguinte em sequência. Embora isto reduza significativamente os requisitos de memória por dispositivo, introduz ineficiências conhecidas como "bolhas de pipeline", em que os dispositivos podem ficar inactivos enquanto aguardam as saídas das camadas anteriores. O microbatching, que divide os lotes de entrada em sub-lotes mais pequenos para processamento sequencial, pode reduzir estas bolhas, mas não as elimina totalmente, uma vez que os tempos de inatividade persistem durante as passagens para a frente e para trás.

O paralelismo tensorial, pelo contrário, divide horizontalmente as camadas individuais em blocos computacionais mais pequenos que podem ser executados independentemente entre dispositivos. Isto é particularmente eficaz para componentes de transformador como blocos de atenção e perceptrons multicamadas (MLPs), onde, por exemplo, diferentes cabeças de atenção podem ser atribuídas a dispositivos separados para computação paralela. No entanto, o paralelismo tensorial é menos eficaz para operações como LayerNorm e Dropout, que não podem ser facilmente divididas e têm de ser replicadas entre dispositivos, o que leva à utilização de memória redundante para armazenar activações. Esta limitação realça a necessidade de abordagens complementares para otimizar a eficiência da memória.

O paralelismo de sequência aborda as ineficiências de memória de operações como LayerNorm e Dropout, dividindo-as ao longo da dimensão da sequência de entrada, aproveitando a sua independência entre elementos da sequência. Este método reduz o espaço de memória das activações redundantes, tornando-o um complemento valioso do paralelismo tensorial. Estas técnicas de paralelização não são mutuamente exclusivas e podem ser combinadas para otimizar ainda mais os modelos de linguagem de grande dimensão (LLMs). Além disso, estratégias de otimização específicas para o módulo de atenção podem aumentar a escalabilidade e reduzir as exigências de memória por GPU, permitindo uma formação e inferência mais eficientes para modelos de grande dimensão.

Otimização da atenção

O artigo de 2017 *Attention Is All You Need* de Vaswani et al. introduziu o modelo Transformer, com a auto-atenção como sua pedra angular. A auto-atenção permite que o modelo avalie a relevância de diferentes palavras em uma frase em relação umas às outras, melhorando a compreensão contextual para tarefas como o processamento de linguagem natural. O documento formalizou a auto-atenção, particularmente através do mecanismo de atenção escalonada ponto-produto (SDPA), que mapeia a consulta e os pares de valores-chave para uma saída, tornando-a um componente essencial nas redes neurais modernas. Aqui estão algumas das técnicas mais importantes para otimizar os cálculos de atenção:

O papel da atenção
O papel da atenção

A atenção multi-cabeças (MHA) baseia-se na SDPA, executando várias operações de atenção em paralelo, cada uma com projecções distintas de matrizes de consulta, chave e valor. Estas operações paralelas, ou "cabeças", concentram-se em diferentes subespaços de representação, enriquecendo a compreensão da entrada pelo modelo. Os resultados destas cabeças são concatenados e projectados linearmente, mantendo uma eficiência computacional comparável à atenção de uma única cabeça, reduzindo a dimensionalidade de cada cabeça (por exemplo, dividindo a dimensão do modelo pelo número de cabeças, como 8).

A atenção a múltiplas consultas (MQA) optimiza a MHA para inferência através da partilha de projecções de chave e valor entre múltiplas cabeças de atenção, mantendo simultaneamente múltiplas projecções de consulta. Isso reduz as demandas de largura de banda de memória e o tamanho do cache de valor-chave (KV), permitindo tamanhos de lote maiores e melhor utilização de computação. No entanto, o MQA pode reduzir ligeiramente a precisão, e os modelos que o utilizam requerem treino ou afinação com o MQA ativado para manter o desempenho.

A atenção a consultas agrupadas (GQA) equilibra a MHA e a MQA agrupando cabeças de consulta e partilhando projecções de valores-chave dentro de cada grupo, alcançando uma qualidade próxima da MHA com uma eficiência computacional mais próxima da MQA. Modelos como Llama 2 70B utilizam GQA, e os modelos treinados com MHA podem ser adaptados para GQA com um mínimo de treinamento adicional. Tanto o MQA como o GQA reduzem as necessidades de memória cache da KV, embora sejam necessárias mais optimizações na gestão da cache.

O FlashAttention aprimora os mecanismos de atenção reordenando os cálculos para aproveitar as hierarquias de memória da GPU com mais eficiência. Ao contrário do processamento tradicional camada por camada, o FlashAttention funde operações e usa "tiling" para computar pequenas partes da matriz de saída de uma só vez, minimizando as operações de leitura/escrita na memória. Este algoritmo de atenção exacta e com reconhecimento de E/S integra-se perfeitamente nos modelos existentes sem modificações, oferecendo aumentos de velocidade significativos através da otimização da movimentação de dados.

Armazenamento em cache de valores-chave

A memorização de KV é uma técnica de otimização fundamental utilizada durante a fase de descodificação de modelos de linguagem de grande dimensão (LLMs) para melhorar a eficiência dos cálculos de auto-atenção. Nesta fase, cada token gerado depende dos tensores de chave (K) e de valor (V) de todos os tokens anteriores, incluindo os calculados durante a fase de pré-preenchimento e as etapas de descodificação subsequentes. Em vez de recomputar esses tensores para cada token em cada etapa de tempo, o cache KV os armazena na memória da GPU, acrescentando novos tensores ao cache à medida que são computados. Normalmente, é mantida uma cache KV separada para cada camada do modelo, reduzindo significativamente os cálculos redundantes e acelerando o processo de descodificação.

Armazenamento em cache de valores-chave
Armazenamento em cache de valores-chave

Os requisitos de memória para LLMs em GPUs são principalmente impulsionados por dois componentes: pesos do modelo e a cache KV. Os pesos do modelo, que consistem nos parâmetros do modelo, ocupam uma memória substancial; por exemplo, um modelo de 7 biliões de parâmetros como o Llama 2 7B em precisão de 16 bits requer aproximadamente 14 GB. A cache KV, por outro lado, armazena tensores de auto-atenção para evitar a recomputação, sendo o seu tamanho determinado por factores como o número de camadas, cabeças de atenção, dimensões das cabeças e precisão. Para cada token, o tamanho da cache é calculado como 2 * num_layers * (num_heads * dim_head) * precision_in_bytes, em que o fator 2 é responsável pelas matrizes K e V. Para um lote de entradas, o tamanho total da cache KV aumenta com o tamanho do lote e o comprimento da sequência, podendo atingir tamanhos significativos, como ~2 GB para um modelo Llama 2 7B com um comprimento de sequência de 4.096 e tamanho de lote de 1.

A gestão eficiente da cache KV apresenta desafios devido ao seu crescimento linear com o tamanho do lote e o comprimento da sequência, o que pode limitar o rendimento e complicar o tratamento de entradas de contexto longo. Uma ineficiência comum surge do excesso de provisionamento estático, em que a memória é reservada para o comprimento máximo de sequência suportado (por exemplo, 2.048 tokens), independentemente do tamanho real da entrada. Isso leva a um desperdício significativo de memória ou fragmentação, já que grande parte do espaço reservado geralmente permanece sem uso durante todo o tempo de vida da solicitação, ocupando recursos valiosos de memória da GPU.

Para resolver estas ineficiências, o algoritmo PagedAttention introduz uma nova abordagem inspirada na paginação do sistema operativo. Ele divide a cache KV em blocos de tamanho fixo, cada um representando um número definido de tokens, que podem ser armazenados de forma não contígua na memória. Uma tabela de blocos rastreia esses blocos, buscando-os conforme necessário durante os cálculos de atenção. À medida que são geradas novas fichas, são atribuídos dinamicamente blocos adicionais. Este método minimiza o desperdício de memória, eliminando a necessidade de alocação contígua e de sobre-provisionamento, permitindo tamanhos de lote maiores e melhorando o rendimento, o que o torna um avanço significativo na gestão da memória cache KV para LLMs.

Otimização de modelos

Nesta secção, discutimos várias técnicas de otimização de modelos de linguagem de grande dimensão (LLMs) para reduzir o seu consumo de memória e melhorar o desempenho em GPUs. Os principais métodos incluem a quantização, a esparsidade e a destilação, cada um visando diferentes aspectos da eficiência do modelo. Estas técnicas modificam os pesos dos modelos, tiram partido da aceleração do hardware da GPU e transferem conhecimentos para modelos mais pequenos, permitindo que modelos maiores sejam executados em hardware limitado, mantendo o desempenho. Estes métodos podem degradar a precisão do modelo, pelo que devem ser utilizados com precaução.

A quantização reduz a precisão dos pesos e activações de um modelo, normalmente de 32 ou 16 bits para 8 ou menos bits, permitindo que os modelos ocupem menos memória e transfiram dados de forma mais eficiente. Enquanto a quantização de pesos é simples devido à sua natureza fixa pós-treinamento, a quantização de activações é mais complexa devido aos outliers que expandem a sua gama dinâmica. Técnicas como LLM.int8() resolvem este problema aplicando seletivamente uma maior precisão a certas activações, ou reutilizando a gama dinâmica de pesos quantizados para activações, embora as GPUs possam exigir a conversão dos pesos de volta para uma maior precisão para as operações.

A esparsidade envolve a poda de valores de modelo próximos de zero, criando matrizes esparsas que requerem menos memória. As GPUs suportam a esparsidade estruturada, como a representação de dois em cada quatro valores como zeros, o que acelera os cálculos. A combinação de esparsidade com quantização pode aumentar ainda mais a velocidade de execução. A investigação continua a explorar representações esparsas óptimas para LLMs, indicando uma via promissora para melhorar as velocidades de inferência.

A destilação transfere o conhecimento de um modelo "professor" maior para um modelo "aluno" mais pequeno, comprimindo o tamanho e preservando o desempenho. Por exemplo, o DistilBERT consegue uma redução de 40% no tamanho e um aumento de 60% na velocidade em comparação com o BERT, mantendo 97% das suas capacidades. A destilação pode envolver a imitação dos resultados do professor ou a utilização de dados gerados pelo professor para formação, com métodos como "Destilação passo a passo!" que incorporam fundamentos para uma aprendizagem eficiente. No entanto, as licenças restritivas de muitos LLMs avançados limitam a disponibilidade de modelos de professores adequados para a destilação.

Inferência especulativa

A inferência especulativa, também conhecida como amostragem especulativa ou geração assistida, é um método para paralelizar a execução de modelos autoregressivos de grandes linguagens (LLMs), como os modelos do tipo GPT, que normalmente geram texto token a token. Na execução padrão, cada token depende de todos os tokens anteriores para o contexto, tornando a geração paralela impossível, uma vez que o n-ésimo token deve ser gerado antes do (n+1)th. A inferência especulativa resolve este problema utilizando um modelo de rascunho "mais barato" para prever vários tokens futuros em simultâneo, que são depois verificados ou rejeitados em paralelo pelo modelo principal, permitindo uma geração de texto mais rápida.

O processo envolve a geração de um projeto de continuação de várias fichas utilizando um método menos intensivo em termos de recursos, seguido de uma verificação paralela pelo modelo principal utilizando o projeto como contexto especulativo. Se o modelo de verificação corresponder às fichas de rascunho, estas são aceites; caso contrário, as fichas não correspondentes e as subsequentes são rejeitadas e o processo repete-se com um novo rascunho. Os tokens de rascunho podem ser gerados usando várias abordagens, como treinar vários modelos, ajustar várias cabeças num modelo pré-treinado para prever tokens futuros, ou empregar um modelo de rascunho mais pequeno juntamente com um modelo de verificação maior e mais capaz, cada um com as suas próprias compensações.

Inferência desagregada

A inferência desagregada é uma técnica em que as tarefas computacionais são divididas por hardware diferente para otimizar o desempenho, o custo e a utilização de recursos. Especificamente, separa as fases de pré-preenchimento e descodificação. Ao desagregar estas fases, cada uma pode ser atribuída ao hardware mais adequado às suas exigências computacionais, melhorando a eficiência e a escalabilidade.

Inferência desagregada
Inferência desagregada

O pré-preenchimento é computacionalmente intensivo, exigindo multiplicações matriciais significativas para processar todo o prompt de entrada e produzir caches KV. Esta fase beneficia de hardware de alto desempenho, como GPUs ou TPUs, que se destacam em computações paralelas. Uma vez que o pré-preenchimento é uma tarefa única por pedido de inferência, pode ser transferido para um nó de computação centralizado e potente, optimizado para este tipo de cargas de trabalho. Essa configuração permite o processamento mais rápido de solicitações grandes e reduz a carga sobre dispositivos menos capazes, tornando-a ideal para ambientes baseados em nuvem ou de data center onde há hardware de alto rendimento disponível.

A descodificação, pelo contrário, está ligada à memória e envolve a geração iterativa de tokens, dependendo fortemente do acesso às caches KV. Requer menos poder computacional, mas precisa de acesso rápido à memória, o que a torna adequada para hardware menos potente e optimizado para memória, como CPUs ou dispositivos de ponta. Ao mover a descodificação para hardware separado - potencialmente mais próximo do utilizador final, como servidores locais ou dispositivos de ponta - a inferência desagregada reduz a latência e as exigências de largura de banda da rede. Esta separação permite uma implementação flexível, em que o pré-preenchimento é executado em servidores de nuvem topo de gama e a descodificação ocorre em dispositivos locais ou de ponta, optimizando a atribuição de recursos e permitindo um dimensionamento eficiente para aplicações como chatbots em tempo real ou sistemas de IA interactivos.

Conclusão

Recentemente, foram inventadas muitas técnicas de otimização da inferência para melhorar o desempenho das LLM.

A implementação destas técnicas requer um conhecimento profundo da arquitetura LLM e do hardware que está a utilizar, pelo que é geralmente mais fácil utilizar um motor de inferência existente que já tenha implementado estas técnicas, como o vLLM, o TensorRT-LLM, o LMDeploy, etc. Na verdade, implementámos estas técnicas no nosso próprio motor de inferência no NLP Cloud e escrevemos uma publicação no blogue sobre motores de inferência se quiser implementar os seus próprios modelos: pode lê-lo aqui.

Se não puder ou não quiser implementar os seus próprios LLMs, pode utilizar o NLP Cloud e tirar partido de modelos rápidos de IA generativa em escala na produção. Experimente agora a inferência rápida no NLP Cloud!

Se tiver dúvidas sobre os motores de inferência em geral, não hesite em perguntar-nos, é sempre um prazer aconselhá-lo!

Julien
CTO na NLP Cloud