Otimizando memória para inferência e ajuste fino de modelos de linguagem grande
![DALL·E 2024 05 02 11.25.49 Create a minimalist article banner in a 16 9 aspect ratio in a futuristic anime style about Memory for Large Language Model Inference. The image f](https://i3.wp.com/www.unite.ai/wp-content/uploads/2024/05/DALL·E-2024-05-02-11.25.49-Create-a-minimalist-article-banner-in-a-16_9-aspect-ratio-in-a-futuristic-anime-style-about-Memory-for-Large-Language-Model-Inference.-The-image-f-1000x600.webp?w=780&resize=780,470&ssl=1)
Grandes modelos de linguagem (LLMs) como GPT-4, Bloom e LLaMA alcançaram capacidades notáveis ao escalar até bilhões de parâmetros. No entanto, a implantação desses modelos massivos para inferência ou ajuste fino é um desafio devido aos seus imensos requisitos de memória. Neste weblog técnico, exploraremos técnicas para estimar e otimizar o consumo de memória durante a inferência LLM e o ajuste fino em várias configurações de {hardware}.
Compreendendo os requisitos de memória
A memória necessária para carregar um LLM é determinada principalmente pelo número de parâmetros e pela precisão numérica usada para armazenar os parâmetros. Uma regra simples é:
- Carregar um modelo com X bilhões de parâmetros requer aproximadamente 4X GB de VRAM em 32 bits precisão flutuante
- Carregar um modelo com X bilhões de parâmetros requer aproximadamente 2X GB de VRAM em 16 bits precisão bfloat16/float16
Por exemplo, carregar o modelo GPT-3 de parâmetro 175B exigiria aproximadamente 350 GB de VRAM com precisão bfloat16. Atualmente, as maiores GPUs disponíveis comercialmente, como NVIDIA A100 e H100, oferecem apenas 80 GB de VRAM, necessitando de paralelismo de tensor e técnicas de paralelismo de modelo.
Durante a inferência, o consumo de memória é dominado pelos parâmetros do modelo e pelos tensores de ativação temporários produzidos. Uma estimativa de alto nível para o pico de uso de memória durante a inferência é a soma da memória necessária para carregar os parâmetros do modelo e a memória para ativações.
Quantificando a memória de inferência
Vamos quantificar os requisitos de memória para inferência usando o modelo OctoCode, que possui cerca de 15 bilhões de parâmetros no formato bfloat16 (~31GB). Usaremos a biblioteca Transformers para carregar o modelo e gerar texto:
</pre> from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline import torch mannequin = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", torch_dtype=torch.bfloat16, device_map="auto", pad_token_id=0) tokenizer = AutoTokenizer.from_pretrained("bigcode/octocoder") pipe = pipeline("text-generation", mannequin=mannequin, tokenizer=tokenizer) immediate = "Query: Please write a Python perform to transform bytes to gigabytes.nnAnswer:" consequence = pipe(immediate, max_new_tokens=60)(0)("generated_text")(len(immediate):) def bytes_to_gigabytes(bytes): return bytes / 1024 / 1024 / 1024 bytes_to_gigabytes(torch.cuda.max_memory_allocated()) <pre>
Saída:
O pico de uso de memória da GPU é de cerca de 29 GB, o que se alinha com nossa estimativa de 31 GB para carregar os parâmetros do modelo no formato bfloat16.
Otimizando Memória de Inferência com Quantização
Embora bfloat16 seja a precisão comum usada para treinar LLMs, os pesquisadores descobriram que quantizar os pesos do modelo para tipos de dados de menor precisão, como inteiros de 8 bits (int8) ou inteiros de 4 bits, pode reduzir significativamente o uso de memória com perda mínima de precisão para tarefas de inferência como geração de texto.
Vamos ver a economia de memória com a quantização de 8 e 4 bits do modelo OctoCode:
</div> # 8-bit quantization mannequin = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", load_in_8bit=True, pad_token_id=0) pipe = pipeline("text-generation", mannequin=mannequin, tokenizer=tokenizer) consequence = pipe(immediate, max_new_tokens=60)(0)("generated_text")(len(immediate):) bytes_to_gigabytes(torch.cuda.max_memory_allocated())</pre>
Output:
# 4-bit quantization mannequin = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", load_in_4bit=True, low_cpu_mem_usage=True, pad_token_id=0) pipe = pipeline("text-generation", mannequin=mannequin, tokenizer=tokenizer) consequence = pipe(immediate, max_new_tokens=60)(0)("generated_text")(len(immediate):) bytes_to_gigabytes(torch.cuda.max_memory_allocated()) </pre> <pre>
Saída:
Com a quantização de 8 bits, o requisito de memória cai de 31 GB para 15 GB, enquanto a quantização de 4 bits reduz ainda mais para apenas 9,5 GB! Isso permite executar o modelo OctoCode de 15B de parâmetro em GPUs de consumo como o RTX 3090 (24GB VRAM).
No entanto, observe que uma quantização mais agressiva, como a de 4 bits, às vezes pode levar à degradação da precisão em comparação com a precisão de 8 bits ou bfloat16. Há uma compensação entre economia de memória e precisão que os usuários devem avaliar para seu caso de uso.
A quantização é uma técnica poderosa que pode permitir a implantação de LLM em ambientes com recursos limitados, como instâncias de nuvem, dispositivos de borda ou até mesmo telefones celulares, reduzindo drasticamente o consumo de memória.
Estimando memória para ajuste fino
Embora a quantização seja usada principalmente para inferência eficiente, técnicas como paralelismo de tensores e paralelismo de modelos são cruciais para gerenciar requisitos de memória durante o treinamento ou ajuste fino de grandes modelos de linguagem.
O pico de consumo de memória durante o ajuste fino é normalmente 3 a 4 vezes maior que o inferido devido aos requisitos adicionais de memória para:
- Gradientes
- Estados do otimizador
- Ativações do passe direto armazenadas para retropropagação
Uma estimativa conservadora é que o ajuste fino de um LLM com X bilhões de parâmetros requer cerca de 4 * (2X) = 8XGB de VRAM com precisão bfloat16.
Por exemplo, o ajuste fino do modelo LLaMA de parâmetro 7B exigiria aproximadamente 7 * 8 = 56 GB de VRAM por GPU com precisão bfloat16. Isso excede a capacidade de memória das GPUs atuais, necessitando de técnicas distribuídas de ajuste fino.
Técnicas de ajuste fino distribuídas
Vários métodos de ajuste fino distribuído foram propostos para superar as restrições de memória da GPU para modelos grandes:
- Paralelismo de dados: a abordagem clássica de paralelismo de dados reproduction todo o modelo em várias GPUs enquanto divide e distribui os lotes de dados de treinamento. Isso reduz o tempo de treinamento linearmente com o número de GPUs, mas não reduz o pico de exigência de memória em cada GPU.
- ZeRO Estágio 3: uma forma avançada de paralelismo de dados que particiona os parâmetros do modelo, gradientes e estados do otimizador entre GPUs. Reduz a memória em comparação com o paralelismo de dados clássico, mantendo apenas os dados particionados necessários em cada GPU durante as diferentes fases de treinamento.
- Paralelismo tensorial: em vez de replicar o modelo, o paralelismo tensorial divide os parâmetros do modelo em linhas ou colunas e os distribui pelas GPUs. Cada GPU opera em um conjunto particionado de parâmetros, gradientes e estados de otimização, levando a economias substanciais de memória.
- Paralelismo de pipeline: esta técnica particiona as camadas do modelo em diferentes GPUs/employees, com cada dispositivo executando um subconjunto de camadas. As ativações são passadas entre trabalhadores, reduzindo o pico de memória, mas aumentando a sobrecarga de comunicação.
Estimar o uso de memória para esses métodos distribuídos não é trivial, pois a distribuição de parâmetros, gradientes, ativações e estados do otimizador varia entre as técnicas. Além disso, diferentes componentes, como o corpo do transformador e o cabeçote de modelagem da linguagem, podem apresentar diferentes comportamentos de alocação de memória.
A solução LLMem
Os pesquisadores propuseram recentemente o LLMem, uma solução que estima com precisão o consumo de memória da GPU ao aplicar métodos de ajuste fino distribuídos a LLMs em várias GPUs.
Estimando o uso de memória GPU para LLM pré-treinado de ajuste fino
O LLMem considera fatores como recombinação de parâmetros antes da computação (ZeRO Estágio 3), coleta de saída na passagem para trás (paralelismo de tensor) e as diferentes estratégias de alocação de memória para o corpo do transformador e cabeçote de modelagem de linguagem.
Resultados experimentais mostram que o LLMem pode estimar o pico de uso de memória da GPU para ajuste fino de LLMs em uma única GPU com taxas de erro de até 1,6%, superando a taxa de erro média do DNNMem de última geração de 42,6%. Ao aplicar métodos de ajuste fino distribuídos a LLMs com mais de um bilhão de parâmetros em múltiplas GPUs, o LLMem atinge uma impressionante taxa de erro média de 3,0%.