Skip to content

Requisitos de Hardware e Otimizações para LLMs ​

Trabalhar com Modelos de Linguagem de Grande Escala (LLMs) tradicionalmente exige recursos computacionais substanciais, especialmente em termos de memória de GPU. No entanto, com as técnicas certas de otimização, é possível construir, treinar e utilizar LLMs mesmo em hardware com limitações de memória, como GPUs de 4-6GB ou no ambiente do Google Colab. Nesta aula, exploraremos os requisitos de hardware para LLMs e as estratégias de otimização que tornam possível trabalhar com esses modelos em configurações mais modestas.

Entendendo as Limitações de Memória ​

Antes de discutirmos as otimizações, é importante compreender por que os LLMs consomem tanta memória e quais são os principais gargalos.

Principais Consumidores de Memória em LLMs ​

  1. Parâmetros do Modelo: Os pesos e biases que definem o modelo. Um LLM típico pode ter de milhões a bilhões de parâmetros.

  2. Estados de Ativação: Os valores intermediários calculados durante o forward pass, que precisam ser armazenados para o cálculo do gradiente durante o backward pass.

  3. Otimizadores: Algoritmos como Adam mantêm estatísticas adicionais para cada parâmetro (momentos de primeira e segunda ordem), efetivamente multiplicando o requisito de memória.

  4. Batch Size: O número de exemplos processados simultaneamente, que multiplica linearmente o consumo de memória das ativações.

  5. Precisão dos Parâmetros: O formato numérico usado para representar os parâmetros (FP32, FP16, INT8, etc.).

Cálculo Aproximado de Requisitos de Memória ​

Para um modelo Transformer com (n) parâmetros, usando precisão de ponto flutuante de 32 bits (FP32) e o otimizador Adam, podemos estimar o consumo de memória durante o treinamento:

  • Parâmetros: (n \times 4) bytes
  • Estados do otimizador: (n \times 4 \times 2) bytes (para Adam)
  • Gradientes: (n \times 4) bytes
  • Ativações: Depende da arquitetura, tamanho de entrada e batch size

Por exemplo, um modelo com 100 milhões de parâmetros em FP32 com Adam requer aproximadamente:

  • 100M × 4 bytes = 400MB para os parâmetros
  • 100M × 4 × 2 bytes = 800MB para os estados do otimizador
  • 100M × 4 bytes = 400MB para os gradientes Total: 1.6GB apenas para parâmetros, otimizador e gradientes, sem contar as ativações!

Técnicas de Otimização para Hardware Limitado ​

Felizmente, existem várias técnicas que podemos empregar para reduzir significativamente os requisitos de memória:

1. Redução de Precisão Numérica ​

Uma das técnicas mais eficazes é usar formatos numéricos de menor precisão:

  • FP16 (Half Precision): Usa 16 bits em vez de 32 bits por parâmetro, reduzindo o consumo de memória pela metade.
  • BF16 (Brain Floating Point): Similar ao FP16, mas com melhor alcance numérico.
  • INT8 Quantização: Usa inteiros de 8 bits, reduzindo o consumo de memória para 1/4 do FP32.
python
# Exemplo de conversão para FP16 em PyTorch
model = model.half()  # Converte o modelo para FP16

2. Gradient Checkpointing ​

Esta técnica sacrifica velocidade de computação para economizar memória. Em vez de armazenar todas as ativações intermediárias, armazenamos apenas algumas e recalculamos as outras durante o backward pass quando necessário.

python
# Exemplo de gradient checkpointing em PyTorch
from torch.utils.checkpoint import checkpoint

# Definindo um módulo que usa checkpointing
class CheckpointedModule(nn.Module):
    def __init__(self, submodule):
        super().__init__()
        self.submodule = submodule
        
    def forward(self, x):
        return checkpoint(self.submodule, x)

O gradient checkpointing pode reduzir o consumo de memória das ativações em até 80%, com um custo de aproximadamente 30% em tempo de computação.

3. Offloading para CPU ​

Podemos mover temporariamente partes do modelo ou estados do otimizador para a RAM da CPU quando não estão em uso ativo, liberando memória de GPU.

python
# Exemplo simplificado de offloading manual
# Mover parâmetros para CPU
cpu_params = [p.cpu() for p in model.parameters()]
# Liberar memória na GPU
for p in model.parameters():
    p.data = torch.empty(0, device='cuda')
# ... fazer algum processamento que requer memória ...
# Restaurar parâmetros para GPU
for i, p in enumerate(model.parameters()):
    p.data = cpu_params[i].cuda()

Bibliotecas como accelerate da Hugging Face automatizam esse processo:

python
from accelerate import Accelerator

accelerator = Accelerator(cpu_offload_model=True)
model, optimizer, dataloader = accelerator.prepare(model, optimizer, dataloader)

4. Acumulação de Gradientes ​

Em vez de aumentar o batch size diretamente (o que aumenta o consumo de memória), podemos acumular gradientes ao longo de vários mini-batches menores antes de atualizar os parâmetros.

python
# Exemplo de acumulação de gradientes
accumulation_steps = 8  # Número de passos para acumular
optimizer.zero_grad()

for i, batch in enumerate(dataloader):
    outputs = model(batch)
    loss = loss_function(outputs)
    # Normalizar a perda pelo número de passos de acumulação
    loss = loss / accumulation_steps
    loss.backward()
    
    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

Esta técnica permite simular batches maiores sem o correspondente aumento no consumo de memória.

5. Arquiteturas Eficientes ​

Algumas variantes de arquiteturas Transformer foram projetadas especificamente para eficiência:

  • Reformer: Usa hashing sensível à localidade para reduzir a complexidade da atenção.
  • Linformer: Reduz a complexidade da atenção de O(n²) para O(n) usando projeções de baixo rank.
  • Performer: Usa aproximações de kernel para o mecanismo de atenção.
python
# Exemplo de uso do Reformer (usando a biblioteca Hugging Face)
from transformers import ReformerConfig, ReformerModel

config = ReformerConfig(
    attention_head_size=64,
    axial_pos_embds=True,
    axial_pos_shape=[64, 64],
    axial_pos_embds_dim=[64, 192],
    chunk_size_feed_forward=0,
    eos_token_id=2,
    feed_forward_size=512,
    hash_seed=None,
    hidden_size=256,
    num_attention_heads=2,
    num_buckets=None,
    num_hashes=1,
    pad_token_id=0,
    vocab_size=320
)

model = ReformerModel(config)

6. Sharding e Paralelismo ​

Para modelos maiores, podemos dividir o modelo entre múltiplos dispositivos:

  • Paralelismo de Dados: Cada dispositivo tem uma cópia completa do modelo, mas processa diferentes batches de dados.
  • Paralelismo de Modelo: O modelo é dividido entre dispositivos, com cada um responsável por diferentes camadas.
  • Paralelismo de Tensor: Os parâmetros individuais são divididos entre dispositivos.
python
# Exemplo de paralelismo de dados com PyTorch
model = torch.nn.DataParallel(model)

7. Técnicas Específicas para Treinamento vs. Inferência ​

Durante o treinamento, precisamos armazenar ativações para o backward pass, mas na inferência podemos descartar ativações imediatamente após o uso.

Para inferência, técnicas adicionais incluem:

  • Pruning: Remover conexões menos importantes do modelo.
  • Quantização Pós-Treinamento: Quantizar o modelo após o treinamento para inferência mais eficiente.
  • Distilação de Conhecimento: Treinar um modelo menor para imitar um modelo maior.

Configuração do Ambiente no Google Colab vs. Máquina Local ​

Vamos comparar as configurações para trabalhar com LLMs em diferentes ambientes:

Google Colab ​

Vantagens:

  • Acesso gratuito a GPUs (T4, P100, ocasionalmente V100)
  • Ambiente pré-configurado com bibliotecas essenciais
  • Integração com Google Drive para armazenamento persistente
  • Sem necessidade de configuração de hardware

Limitações:

  • Tempo de execução limitado (12h para Colab Pro)
  • Desconexões ocasionais
  • Alocação de GPU não garantida
  • Memória de GPU limitada (geralmente 12-16GB)

Configuração Recomendada:

python
# Verificar GPU disponível
!nvidia-smi

# Instalar bibliotecas adicionais
!pip install transformers accelerate bitsandbytes

# Montar Google Drive para armazenamento persistente
from google.colab import drive
drive.mount('/content/drive')

# Configurar para economia de memória
import torch
from accelerate import Accelerator

# Usar mixed precision
accelerator = Accelerator(mixed_precision='fp16')

# Verificar memória disponível
!nvidia-smi

Máquina Local com GPU de 4-6GB ​

Vantagens:

  • Controle total sobre o ambiente
  • Sem limitações de tempo de execução
  • Acesso consistente aos recursos
  • Possibilidade de otimizações específicas para o hardware

Limitações:

  • Memória de GPU muito limitada
  • Pode exigir configuração mais complexa
  • Treinamento mais lento comparado a GPUs profissionais

Configuração Recomendada:

python
# Configuração para GPU com memória limitada
import torch
import os

# Definir GPU a ser usada (se houver múltiplas)
os.environ["CUDA_VISIBLE_DEVICES"] = "0"

# Limitar cache CUDA
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:128"

# Configurar para economia de memória
torch.backends.cudnn.benchmark = True
torch.backends.cuda.matmul.allow_tf32 = True

# Usar mixed precision
scaler = torch.cuda.amp.GradScaler()

# Função para monitorar uso de memória
def print_gpu_memory():
    if torch.cuda.is_available():
        print(f"GPU memory allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
        print(f"GPU memory reserved: {torch.cuda.memory_reserved() / 1e9:.2f} GB")

Estratégias Práticas para Trabalhar com LLMs em Hardware Limitado ​

Combinando as técnicas discutidas, aqui estão algumas estratégias práticas para diferentes cenários:

Para Treinamento de Modelos Pequenos a Médios (até 100M parâmetros) ​

  1. Use precisão mista (FP16/BF16)
  2. Implemente gradient checkpointing
  3. Use batch sizes pequenos (1-4) com acumulação de gradientes
  4. Considere arquiteturas eficientes como Reformer ou Linformer
  5. Monitore cuidadosamente o uso de memória

Para Fine-tuning de Modelos Pré-treinados ​

  1. Use técnicas de Parameter-Efficient Fine-Tuning (PEFT) como LoRA ou Adaptadores
  2. Congele a maioria dos parâmetros do modelo
  3. Use offloading para CPU quando necessário
  4. Considere quantização de 8 bits para os parâmetros congelados

Para Inferência ​

  1. Quantize o modelo para INT8 ou menor
  2. Use geração em lotes pequenos
  3. Considere modelos destilados ou pruned
  4. Implemente técnicas de cache para tokens já processados

Exemplo Prático: Configurando um Ambiente Otimizado ​

Vamos concluir com um exemplo prático de configuração para treinar um modelo de linguagem pequeno em uma GPU com 6GB de memória:

python
import torch
import transformers
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
from datasets import load_dataset
from accelerate import Accelerator

# 1. Configurar acelerador com mixed precision
accelerator = Accelerator(mixed_precision='fp16')

# 2. Carregar modelo pequeno (GPT-2 pequeno tem ~124M parâmetros)
model_name = "gpt2"  # ~124M parâmetros
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)

# 3. Habilitar gradient checkpointing
model.gradient_checkpointing_enable()

# 4. Preparar dataset (exemplo com um dataset pequeno)
dataset = load_dataset("wikitext", "wikitext-2-raw-v1")
tokenized_dataset = dataset.map(
    lambda examples: tokenizer(examples["text"], truncation=True, max_length=128),
    batched=True,
    remove_columns=["text"]
)

# 5. Configurar treinamento com batch size pequeno e acumulação de gradientes
training_args = TrainingArguments(
    output_dir="./results",
    per_device_train_batch_size=1,
    gradient_accumulation_steps=8,  # Simula batch size de 8
    learning_rate=5e-5,
    num_train_epochs=3,
    save_strategy="epoch",
    fp16=True,  # Usar mixed precision
    report_to="none",  # Desativar relatórios para economizar memória
)

# 6. Preparar trainer
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset["train"],
)

# 7. Treinar o modelo
trainer.train()

# 8. Salvar o modelo
model.save_pretrained("./modelo_otimizado")
tokenizer.save_pretrained("./modelo_otimizado")

Conclusão ​

Trabalhar com LLMs em hardware com limitações de memória é desafiador, mas totalmente viável com as técnicas de otimização adequadas. Ao combinar estratégias como redução de precisão, gradient checkpointing, offloading e acumulação de gradientes, podemos construir, treinar e utilizar modelos surpreendentemente poderosos mesmo em GPUs com 4-6GB de memória ou no Google Colab.

Na próxima aula, exploraremos os fundamentos matemáticos e teóricos por trás dos LLMs, estabelecendo a base conceitual necessária para implementar esses modelos do zero.

Referências e Leituras Adicionais ​

  1. Rajbhandari, S., et al. (2020). "ZeRO: Memory Optimizations Toward Training Trillion Parameter Models".
  2. Kitaev, N., et al. (2020). "Reformer: The Efficient Transformer".
  3. Wang, S., et al. (2020). "Linformer: Self-Attention with Linear Complexity".
  4. Dettmers, T., et al. (2022). "8-bit Optimizers via Block-wise Quantization".
  5. Hu, E., et al. (2021). "LoRA: Low-Rank Adaptation of Large Language Models".

Todos os direitos reservados.