Neste post do blog, a equipe de engenharia da MosaicML compartilha as práticas recomendadas sobre como aproveitar os modelos de linguagem grandes (LLMs) de código aberto populares para uso em produção. Também fornecemos diretrizes para a implantação de serviços de inferência criados em torno desses modelos para ajudar os usuários na seleção de modelos e hardware de implantação. Trabalhamos com vários back-ends baseados em PyTorch em produção; estas diretrizes são extraídas de nossa experiência com FasterTransformers, vLLM, TensorRT-LLM da NVIDIA, que será lançado em breve, e outros.
Entendendo a geração de texto do LLM
Os modelos de linguagem grandes (LLMs) geram texto em um processo de duas etapas: "preenchimento", onde os tokens no prompt de entrada são processados em paralelo, e "decodificação", onde o texto é gerado um 'token' por vez de maneira autorregressiva. Cada token gerado é anexado à entrada e realimentado no modelo para gerar o próximo token. A geração para quando o LLM produz um token de parada especial ou quando uma condição definida pelo usuário é atendida (por exemplo, algum número máximo de tokens foi gerado). Se você quiser mais informações sobre como os LLMs usam blocos de decodificador, confira este post do blog.
Os tokens podem ser palavras ou subpalavras; as regras exatas para dividir o texto em tokens variam de modelo para modelo. Por exemplo, você pode comparar como os modelos Llama tokenizam o texto com a forma como os modelos OpenAI tokenizam o texto. Embora os provedores de inferência de LLM geralmente falem sobre o desempenho em métricas baseadas em token (por exemplo, tokens/segundo), esses números nem sempre são comparáveis entre os tipos de modelo, dadas essas variações. Para um exemplo concreto, a equipe da Anyscale descobriu que a tokenização do Llama 2 é 19% mais longa do que a tokenização do ChatGPT (mas ainda tem um custo geral muito menor). E os pesquisadores da HuggingFace também descobriram que o Llama 2 exigia ~20% mais tokens para treinar sobre a mesma quantidade de texto que o GPT-4.
Métricas importantes para o serviço de LLM
Então, como exatamente devemos pensar sobre a velocidade de inferência?
Nossa equipe usa quatro métricas principais para o serviço de LLM:
- Tempo até o primeiro token (TTFT): a rapidez com que os usuários começam a ver a saída do modelo após inserir sua consulta. Baixos tempos de espera por uma resposta são essenciais em interações em tempo real, mas menos importantes em cargas de trabalho offline. Essa métrica é impulsionada pelo tempo necessário para processar o prompt e, em seguida, gerar o primeiro token de saída.
- Tempo por token de saída (TPOT): tempo para gerar um token de saída para cada usuário que está consultando nosso sistema. Essa métrica corresponde a como cada usuário perceberá a "velocidade" do modelo. Por exemplo, um TPOT de 100 milissegundos/tok seria de 10 tokens por segundo por usuário, ou ~450 palavras por minuto, o que é mais rápido do que uma pessoa comum consegue ler.
- Latência: o tempo total que o modelo leva para gerar a resposta completa para um usuário. A latência geral da resposta pode ser calculada usando as duas métricas anteriores: latência = (TTFT) + (TPOT) * (o número de tokens a serem gerados).
- Taxa de transferência: o número de tokens de saída por segundo que um servidor de inferência pode gerar em todos os usuários e solicitações.
Nosso objetivo? O tempo mais rápido para o primeiro token, a maior taxa de transferência e o tempo mais rápido por token de saída. Em outras palavras, queremos que nossos modelos gerem texto o mais rápido possível para o maior número de usuários que pudermos suportar.
Notavelmente, há uma compensação entre taxa de transferência e tempo por token de saída: se processarmos 16 consultas de usuário simultaneamente, teremos uma taxa de transferência maior em comparação com a execução das consultas sequencialmente, mas levaremos mais tempo para gerar tokens de saída para cada usuário.
Se você tiver metas gerais de latência de inferência, aqui estão algumas heurísticas úteis para avaliar modelos:
- O comprimento da saída domina a latência geral da resposta: para a latência média, você geralmente pode apenas pegar o comprimento esperado/máximo do token de saída e multiplicá-lo por um tempo médio geral por token de saída para o modelo.
- O comprimento da entrada não é significativo para o desempenho, mas importante para os requisitos de hardware: a adição de 512 tokens de entrada aumenta a latência menos do que a produção de 8 tokens de saída adicionais nos modelos MPT. No entanto, a necessidade de suportar entradas longas pode tornar os modelos mais difíceis de servir. Por exemplo, recomendamos usar o A100-80GB (ou mais recente) para servir o MPT-7B com seu comprimento de contexto máximo de 2048 tokens.
- A latência geral é dimensionada de forma sublinear com o tamanho do modelo: no mesmo hardware, modelos maiores são mais lentos, mas a taxa de velocidade não corresponderá necessariamente à taxa de contagem de parâmetros. A latência do MPT-30B é ~2,5x a da latência do MPT-7B. A latência do Llama2-70B é ~2x a da latência do Llama2-13B.
Os clientes em potencial geralmente nos pedem para fornecer uma latência de inferência média. Recomendamos que, antes de se fixar em metas de latência específicas ("precisamos de menos de 20 ms por token"), você gaste algum tempo caracterizando seu comprimento de entrada esperado e o comprimento de saída desejado.
Desafios na inferência de LLM
A otimização da inferência de LLM se beneficia de técnicas gerais, como:
- Fusão de operador: combinar diferentes operadores adjacentes geralmente resulta em melhor latência.
- Quantização: as ativações e os pesos são compactados para usar um número menor de bits.
- Compressão: Esparsidade ou Destilação.
- Paralelização: paralelismo de tensor em vários dispositivos ou paralelismo de pipeline para modelos maiores.
Além desses métodos, existem muitas otimizações importantes específicas do Transformer. Um excelente exemplo disso é o cache KV (chave-valor). O mecanismo de Atenção em modelos baseados em Transformer somente decodificador é computacionalmente ineficiente. Cada token atende a todos os tokens vistos anteriormente e, portanto, recalcula muitos dos mesmos valores à medida que cada novo token é gerado. Por exemplo, ao gerar o Nth token, o (N-1)th token atende aos tokens (N-2)th, (N-3)th … 1st. Da mesma forma, ao gerar o token (N+1)th, a atenção para o Nth token novamente precisa examinar os tokens (N-1)th, (N-2)th, (N-3)th, … 1st. O cache KV, ou seja, o salvamento de chaves/valores intermediários para as camadas de atenção, é usado para preservar esses resultados para reutilização posterior, evitando a computação repetida.
A largura de banda da memória é fundamental
Os cálculos em LLMs são dominados principalmente por operações de multiplicação matriz-matriz; essas operações com pequenas dimensões são normalmente limitadas pela largura de banda da memória na maioria dos hardwares. Ao gerar tokens de maneira autorregressiva, uma das dimensões da matriz de ativação (definida pelo tamanho do lote e o número de tokens na sequência) é pequena em tamanhos de lote pequenos. Portanto, a velocidade depende de quão rápido podemos carregar os parâmetros do modelo da memória da GPU para caches/registradores locais, em vez de quão rápido podemos computar os dados carregados. A largura de banda de memória disponível e alcançada no hardware de inferência é um melhor preditor da velocidade de geração de token do que seu desempenho computacional máximo.
A utilização do hardware de inferência é muito importante em termos de custos de serviço. As GPUs são caras e precisamos que elas façam o máximo de trabalho possível. Os serviços de inferência compartilhados prometem manter os custos baixos, combinando cargas de trabalho de muitos usuários, preenchendo lacunas individuais e agrupando solicitações sobrepostas. Para modelos grandes como o Llama2-70B, só alcançamos um bom custo/desempenho em tamanhos de lote grandes. Ter um sistema de serviço de inferência que possa operar em tamanhos de lote grandes é fundamental para a eficiência de custos. No entanto, um lote grande significa um tamanho de cache KV maior e isso, por sua vez, aumenta o número de GPUs necessárias para servir o modelo. Há uma disputa aqui e os operadores de serviços compartilhados precisam fazer algumas compensações de custo e implementar otimizações de sistemas.
Utilização da largura de banda do modelo (MBU)
Quão otimizado é um servidor de inferência de LLM?
Como explicado brevemente anteriormente, a inferência para LLMs em tamanhos de lote menores — especialmente no tempo de decodificação — é limitada por quão rápido podemos carregar os parâmetros do modelo da memória do dispositivo para as unidades de computação. A largura de banda da memória determina a rapidez com que o movimento de dados acontece. Para medir a utilização do hardware subjacente, apresentamos uma nova métrica chamada Utilização da Largura de Banda do Modelo (MBU). O MBU é definido como (largura de banda de memória alcançada) / (largura de banda de memória de pico), onde a largura de banda de memória alcançada é ((tamanho total do parâmetro do modelo + tamanho do cache KV) / TPOT).
Por exemplo, se um parâmetro de 7B em execução com precisão de 16 bits tiver TPOT igual a 14ms, ele estará movendo 14 GB de parâmetros em 14ms, traduzindo para 1 TB/seg de uso de largura de banda. Se a largura de banda de pico da máquina for de 2 TB/seg, estamos executando em um MBU de 50%. Para simplificar, este exemplo ignora o tamanho do cache KV, que é pequeno para tamanhos de lote menores e comprimentos de sequência mais curtos. Valores de MBU próximos a 100% implicam que o sistema de inferência está utilizando efetivamente a largura de banda de memória disponível. O MBU também é útil para comparar diferentes sistemas de inferência (hardware + software) de forma normalizada. O MBU é complementar à métrica Model Flops Utilization (MFU; introduzida no artigo PaLM), que é importante em configurações limitadas por computação.
A Figura 1 mostra uma representação pictórica do MBU em um gráfico semelhante a um gráfico de linha de teto. A linha inclinada sólida da região sombreada em laranja mostra a taxa de transferência máxima possível se a largura de banda da memória estiver totalmente saturada em 100%. No entanto, na realidade, para tamanhos de lote baixos (ponto branco), o desempenho observado é menor do que o máximo – o quão menor é uma medida do MBU. Para tamanhos de lote grandes (região amarela), o sistema é limitado por computação e a taxa de transferência alcançada como uma fração da taxa de transferência máxima possível é medida como a Utilização de Flops do Modelo (MFU).
MBU e MFU determinam quanto mais espaço está disponível para aumentar ainda mais a velocidade de inferência em uma determinada configuração de hardware. A Figura 2 mostra o MBU medido para diferentes graus de paralelismo de tensor com nosso servidor de inferência baseado em TensorRT-LLM. A utilização máxima da largura de banda da memória é alcançada ao transferir grandes blocos de memória contíguos. Quando modelos menores como o MPT-7B são distribuídos em várias GPUs, observamos um MBU menor, pois estamos movendo blocos de memória menores em cada GPU.
A Figura 3 mostra o MBU observado empiricamente para diferentes graus de paralelismo de tensor e tamanhos de lote nas GPUs NVIDIA H100. O MBU diminui à medida que o tamanho do lote aumenta. No entanto, à medida que dimensionamos as GPUs, a diminuição relativa no MBU é menos significativa. Também vale a pena notar que escolher hardware com maior largura de banda de memória pode aumentar o desempenho com menos GPUs. No tamanho do lote 1, podemos alcançar um MBU mais alto de 60% em 2xH100-80GBs em comparação com 55% em GPUs 4xA100-40GB (Figura 2).
Resultados de benchmark
Latência
Medimos o tempo até o primeiro token (TTFT) e o tempo por token de saída (TPOT) em diferentes graus de paralelismo de tensor para os modelos MPT-7B e Llama2-70B. À medida que os prompts de entrada se alongam, o tempo para gerar o primeiro token começa a consumir uma parte substancial da latência total. O paralelismo de tensor em várias GPUs ajuda a reduzir essa latência.
Ao contrário do treinamento de modelo, o dimensionamento para mais GPUs oferece retornos decrescentes significativos para a latência de inferência. Por exemplo, para o Llama2-70B, passar de 4x para 8x GPUs diminui a latência em apenas 0,7x em tamanhos de lote pequenos. Uma razão para isso é que o paralelismo mais alto tem um MBU mais baixo (como discutido anteriormente). Outra razão é que o paralelismo de tensor introduz sobrecarga de comunicação em um nó de GPU.
| Tempo até o primeiro token (ms) | ||||
|---|---|---|---|---|
| Modelo | 1xA100-40GB | 2xA100-40GB | 4xA100-40GB | 8xA100-40GB |
| MPT-7B | 46 (1x) | 34 (0,73x) | 26 (0,56x) | - |
| Llama2-70B | Não cabe | 154 (1x) | 114 (0,74x) | |
Tabela 1: Tempo até o primeiro token, dado que as solicitações de entrada têm 512 tokens de comprimento com tamanho de lote de 1. Modelos maiores como o Llama2 70B precisam de pelo menos 4xA100-40B GPUs para caber na memória
Em tamanhos de lote maiores, um paralelismo de tensor mais alto leva a uma diminuição relativa mais significativa na latência do token. A Figura 4 mostra como o tempo por token de saída varia para o MPT-7B. No tamanho do lote 1, passar de 2x para 4x reduz a latência do token em apenas ~12%. No tamanho do lote 16, a latência com 4x é 33% menor do que com 2x. Isso está de acordo com nossa observação anterior de que a diminuição relativa no MBU é menor em graus mais altos de paralelismo de tensor para o tamanho do lote 16 em comparação com o tamanho do lote 1.
A Figura 5 mostra resultados semelhantes para o Llama2-70B, exceto que a melhoria relativa entre 4x e 8x é menos pronunciada. Também comparamos o dimensionamento de GPU em dois hardwares diferentes. Como o H100-80GB tem 2,15x de largura de banda de memória da GPU em comparação com o A100-40GB, podemos ver que a latência é 36% menor no tamanho do lote 1 e 52% menor no tamanho do lote 16 para sistemas 4x.
Taxa de transferência
Podemos trocar a taxa de transferência e o tempo por token agrupando as solicitações. Agrupar consultas durante a avaliação da GPU aumenta a taxa de transferência em comparação com o processamento de consultas sequencialmente, mas cada consulta levará mais tempo para ser concluída (ignorando os efeitos de enfileiramento).
Existem algumas técnicas comuns para agrupar solicitações de inferência:
- Agrupamento estático: o cliente empacota vários prompts em solicitações e uma resposta é retornada após a conclusão de todas as sequências no lote. Nossos servidores de inferência suportam isso, mas não exigem.
- Agrupamento dinâmico: os prompts são agrupados dinamicamente dentro do servidor. Normalmente, esse método tem um desempenho pior do que o agrupamento estático, mas pode chegar perto do ideal se as respostas forem curtas ou de comprimento uniforme. Não funciona bem quando as solicitações têm parâmetros diferentes.
- Agrupamento contínuo: a ideia de agrupar solicitações à medida que chegam foi introduzida neste excelente artigo e é atualmente o método SOTA. Em vez de esperar que todas as sequências em um lote terminem, ele agrupa as sequências no nível da iteração. Ele pode alcançar uma taxa de transferência 10x-20x melhor do que o agrupamento dinâmico.
O agrupamento contínuo geralmente é a melhor abordagem para serviços compartilhados, mas há situações em que os outros dois podem ser melhores. Em ambientes de baixo QPS, o agrupamento dinâmico pode superar o agrupamento contínuo. Às vezes, é mais fácil implementar otimizações de GPU de baixo nível em uma estrutura de agrupamento mais simples. Para cargas de trabalho de inferência de lote offline, o agrupamento estático pode evitar sobrecarga significativa e alcançar uma melhor taxa de transferência.
Tamanho do lote
O quão bem o agrupamento funciona depende muito do fluxo de solicitação. Mas podemos obter um limite superior em seu desempenho comparando o agrupamento estático com solicitações uniformes.
| Tamanho do lote | |||||||
|---|---|---|---|---|---|---|---|
| Hardware | 1 | 4 | 8 | 16 | 32 | 64 | 128 |
| 1 x A10 | 0,4 (1x) | 1,4 (3,5x) | 2,3 (6x) | 3,5 (9x) | Erro OOM (Sem memória) | ||
| 2 x A10 | 0,8 | 2,5 | 4,0 | 7,0 | 8,0 | ||
| 1 x A100 | 0,9 (1x) | 3,2 (3,5x) | 5,3 (6x) | 8,0 (9x) | 10,5 (12x) | 12,5 (14x) | |
| 2 x A100 | 1,3 | 3,0 | 5,5 | 9,5 | 14,5 | 17,0 | 22,0 |
| 4 x A100 | 1,7 | 6,2 | 11,5 | 18,0 | 25,0 | 33,0 | 36,5 |
Tabela 2: Taxa de transferência máxima do MPT-7B (req/seg) com agrupamento estático e um back-end baseado em FasterTransformers. Solicitações: 512 tokens de entrada e 64 tokens de saída. Para entradas maiores, o limite de OOM estará em tamanhos de lote menores.
Compensação de latência
A latência da solicitação aumenta com o tamanho do lote. Com uma GPU NVIDIA A100, por exemplo, se maximizarmos a taxa de transferência com um tamanho de lote de 64, a latência aumenta em 4x, enquanto a taxa de transferência aumenta em 14x. Os serviços de inferência compartilhados normalmente escolhem um tamanho de lote equilibrado. Os usuários que hospedam seus próprios modelos devem decidir a compensação de latência/taxa de transferência apropriada para seus aplicativos. Em alguns aplicativos, como chatbots, a baixa latência para respostas rápidas é a principal prioridade. Em outros aplicativos, como o processamento em lote de PDFs não estruturados, podemos querer sacrificar a latência para processar um documento individual para processar todos eles rapidamente em paralelo.
A Figura 7 mostra a curva de taxa de transferência vs. latência para o modelo 7B. Cada linha nesta curva é obtida aumentando o tamanho do lote de 1 para 256. Isso é útil para determinar o quão grande podemos tornar o tamanho do lote, sujeito a diferentes restrições de latência. Relembrando nosso gráfico de linha de teto acima, descobrimos que essas medições são consistentes com o que esperaríamos. Após um determinado tamanho de lote, ou seja, quando passamos para o regime limitado por computação, cada duplicação do tamanho do lote apenas aumenta a latência sem aumentar a taxa de transferência.
