mozak.tech Engenharia de IA Corporativa 53%

Parte II — Especialização de Modelos: Dados, Ajuste e Avaliação

2.4 — Ajuste Eficiente de Parâmetros: LoRA e a Matemática por Trás (LoRA / PEFT)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Entender a matemática de LoRA: W_novo = W + B × A com r << d

Explicar por que 0,1% dos parâmetros treináveis é suficiente para especialização

Comparar LoRA vs full fine-tuning em custo, qualidade e velocidade

Entender QLoRA e quando usar em hardware limitado

Decidir quando LoRA é a ferramenta certa vs API-based fine-tuning

Por que isso importa

Fine-tuning completo de um modelo de 7B parâmetros requer ~112GB de VRAM (modelo + gradientes + otimizador). Isso é inacessível para a maioria dos times. LoRA (Low-Rank Adaptation) permite fazer o mesmo com ~14GB — habilitando fine-tuning em consumer GPUs ou instâncias menores de cloud.

Conceitos Fundamentais

A Matemática de LoRA

Fundamento: LoRA e PEFT

Fine-tuning completo ajusta todos os pesos do modelo — para um modelo de 7B parâmetros, são 7 bilhões de números a atualizar, exigindo ~112 GB de VRAM. LoRA parte da hipótese de que a mudança necessária nos pesos tem "rank baixo" — pode ser aproximada pelo produto de duas matrizes pequenas (B × A). Em vez de atualizar W diretamente, você treina B e A (que juntas têm 0,1% dos parâmetros de W) e soma: W_novo = W + B × A. QLoRA adiciona quantização (NF4 4-bit) ao modelo base congelado, reduzindo VRAM de ~112 GB para ~6 GB para um modelo de 7B. Para o arquiteto: LoRA é a decisão que torna fine-tuning viável numa GPU de consumidor em vez de exigir cluster de H100.

# Sem LoRA: toda a matrix W é atualizada durante treinamento

# W tem forma [d_in × d_out], ex: [4096 × 4096] = 16M parâmetros



# Com LoRA: decomposição de baixo rank

# W_novo = W_original + B × A

# onde:

#   B tem forma [d_in × r]    ex: [4096 × 8]  = 32.768 parâmetros

#   A tem forma [r × d_out]   ex: [8 × 4096]  = 32.768 parâmetros

#   r = rank (tipicamente 4, 8, 16, 32)



# Parâmetros treináveis: B + A = 2 × r × d

# vs Full FT: d × d



# Economia para d=4096, r=8:

total_params = 4096 * 4096           # 16.7M

lora_params = 2 * 8 * 4096          # 65.5K

pct_treinavel = lora_params / total_params  # 0.39%

print(f"LoRA usa {pct_treinavel*100:.2f}% dos parâmetros")

Por que baixo rank funciona?

A hipótese de LoRA é que as atualizações de peso durante fine-tuning têm “low intrinsic rank” — ou seja, a mudança necessária no modelo pode ser representada em um espaço de dimensão muito menor.

import numpy as np



# Simulação: delta_W de um treinamento real tende a ser low-rank

# Verificar com SVD (Singular Value Decomposition):

def verificar_rank_efetivo(delta_W: np.ndarray, threshold: float = 0.01) -> int:

    U, S, Vt = np.linalg.svd(delta_W)

    # Valores singulares normalizados

    S_norm = S / S[0]

    # Rank efetivo: quantos valores singulares são > threshold

    return int((S_norm > threshold).sum())



# Tipicamente: rank efetivo << dimensão da matrix

# Para uma camada de atenção de GPT-2: rank efetivo ≈ 8-32 de 768

Comparativo LoRA vs Full Fine-Tuning

Modelo: LLaMA-2 7B



Full Fine-Tuning:

  VRAM: ~112 GB (modelo 14GB × 8 fp32)

  Parâmetros treináveis: 7B

  Custo na AWS: ~$50/hora (8× A100)

  

LoRA (r=8):

  VRAM: ~14 GB (modelo 7GB quantizado + adaptadores)

  Parâmetros treináveis: ~4M (0.06%)

  Custo na AWS: ~$1.5/hora (RTX 3090 ou T4)

  

QLoRA (4-bit + LoRA):

  VRAM: ~6 GB (!!)

  Qualidade: ~95% do Full FT em tarefas de instrução

  Custo: roda em laptop com RTX 3060

QLoRA: Quantização + LoRA

# QLoRA combina:

# 1. NF4 quantization: comprime pesos de 16-bit para 4-bit (4x menor)

# 2. Double quantization: comprime os parâmetros de quantização também

# 3. Paginação de memória: move estados do otimizador para CPU quando necessário



# Em código (HuggingFace):

from transformers import BitsAndBytesConfig

from peft import LoraConfig, get_peft_model



bnb_config = BitsAndBytesConfig(

    load_in_4bit=True,

    bnb_4bit_compute_dtype=torch.float16,

    bnb_4bit_use_double_quant=True,

    bnb_4bit_quant_type="nf4",

)



model = AutoModelForCausalLM.from_pretrained(

    "meta-llama/Llama-2-7b-hf",

    quantization_config=bnb_config,

)



lora_config = LoraConfig(

    r=8,

    lora_alpha=16,           # escala: alpha/r = fator de escala do update

    target_modules=["q_proj", "v_proj"],  # quais layers aplicar LoRA

    lora_dropout=0.05,

    bias="none",

    task_type="CAUSAL_LM",

)



model = get_peft_model(model, lora_config)

model.print_trainable_parameters()

# → trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.0622

Aprofundamento Técnico

Escolha do Rank

r=1: muito pouco → underfitting

r=4: adequado para tarefas simples (classificação)

r=8: padrão para instrução → mais comum

r=16: para tarefas complexas de geração

r=64+: começa a se aproximar de full FT em custo/memória

Quais Camadas Aplicar LoRA?

# Mínimo efetivo (uso comum):

target_modules=["q_proj", "v_proj"]  # query e value da atenção



# Mais completo (melhor qualidade):

target_modules=["q_proj", "k_proj", "v_proj", "o_proj"]  # todas as projeções



# Máximo (quase full FT):

target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]

Exemplos Anotados

Exemplo 1: Calculo de Memória LoRA

def calcular_mem_lora(d: int, r: int, n_layers: int, n_target_modules: int, bits: int = 16) -> dict:

    bytes_por_param = bits / 8

    

    # Parâmetros LoRA (A + B por layer por module)

    lora_params = 2 * r * d * n_layers * n_target_modules

    lora_mb = (lora_params * bytes_por_param) / 1e6

    

    return {

        'lora_params': lora_params,

        'lora_vram_mb': round(lora_mb, 1),

        'pct_do_total': round(lora_params / (d * d * n_layers) * 100, 3),

    }



# LLaMA-2 7B: d=4096, n_layers=32, target_modules=2 (q,v)

resultado = calcular_mem_lora(d=4096, r=8, n_layers=32, n_target_modules=2)

# → {'lora_params': 4194304, 'lora_vram_mb': 8.4, 'pct_do_total': 0.195}

# LoRA adiciona apenas 8.4 MB ao modelo de 14GB!

Padrões e Armadilhas

Padrões

Padrão 1: lora_alpha = 2 × r como ponto de partida

r=8, lora_alpha=16  # escala efetiva = alpha/r = 2.0

Padrão 2: Salvar apenas os adaptadores LoRA, não o modelo completo

model.save_pretrained("meu-lora-adapter")  # salva apenas os 8MB de LoRA

# Para usar: carregar modelo base + aplicar adaptador

Armadilhas

⚠️ Armadilha 1: LoRA não acelera inferência LoRA acelera treinamento, não inferência. Para inferência mais rápida, use quantização separada.

⚠️ Armadilha 2: lora_alpha muito alto pode destabilizar treinamento

alpha=128 com r=8 → escala 16.0 → updates grandes → gradiente explode

Mantenha alpha/r ≤ 4 para início

⚠️ Armadilha 3: Não testar em hardware alvo antes de treinar

GPU: RTX 3060 12GB

Modelo 7B quantizado: 4GB

LoRA + optimizer: 6GB  ← OOM!

Solução: gradient_checkpointing=True economiza ~30% de VRAM
⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

Esta unidade é conceitual — não há starter para executar. Os exercícios em exercicios.md pedem que você: 1. Calcule memória necessária para diferentes configurações de LoRA 2. Explique a matemática de W_novo = W + B × A com exemplos numéricos 3. Decida quando usar QLoRA vs fine-tuning via API

Agora você está pronto para o lab.