Phase 11 - Lesson 08

Ajuste Fino com LoRA & QLoRA

O ajuste fino completo de um modelo de 7B requer 56GB de VRAM. Você não tem isso. A maioria das empresas também não. O LoRA permite que você faça o ajuste fino do mesmo modelo em 6GB, treinando menos de 1% dos parâmetros. Isso não é um meio-termo — ele iguala a qualidade do ajuste fino completo na maioria das tarefas. Todo o ecossistema de ajuste fino de código aberto funciona com base nesse único truque.

Tipo: Build Idiomas: Python Pré-requisitos: Fase 10, Lição 06 (Instruction Tuning / SFT) Tempo: ~75 minutos Relacionado: A Fase 10 cobre os loops SFT/DPO do zero. Esta lição integra-os com os toolkits de PEFT de 2026 (PEFT, TRL, Unsloth, Axolotl, LLaMA-Factory).

Objetivos de Aprendizado

  • Implementar o LoRA injetando matrizes adaptadoras de baixo posto (A e B) nas camadas de atenção de um modelo pré-treinado
  • Calcular a economia de parâmetros do LoRA vs ajuste fino completo: rank r com dimensões d_model treina 2rd parâmetros em vez de d^2
  • Fazer o ajuste fino de um modelo usando QLoRA (base quantizada em 4 bits + adaptadores LoRA) para caber na memória de GPU de consumo
  • Mesclar os pesos do LoRA de volta no modelo base para implantação e comparar a velocidade de inferência com e sem adaptadores

O Problema

Você tem um modelo base. Llama 3 8B. Você quer que ele responda a chamados de suporte ao cliente no tom de voz da sua empresa. O SFT é a solução. Mas o SFT tem um problema de custo.

O ajuste fino completo atualiza todos os parâmetros do modelo. O Llama 3 8B possui 8 bilhões de parâmetros. Em fp16, cada parâmetro ocupa 2 bytes. Isso dá 16GB apenas para carregar os pesos. Durante o treinamento, você também precisa de gradientes (16GB), estados do otimizador para Adam (32GB para momentum + variância) e ativações. Total: aproximadamente 56GB de VRAM para um único modelo 8B.

Uma A100 80GB mal consegue acomodar isso. Duas A100s custam $3-4/hora em provedores de nuvem. O treinamento por 3 épocas em 50.000 exemplos leva de 6 a 10 horas. Isso representa $30-40 por experimento. Execute 10 experimentos para acertar os hiperparâmetros e você terá gasto $400 antes de implantar qualquer coisa.

Dimensione isso para o Llama 3 70B e os números ficam absurdos. 140GB apenas para os pesos. Você precisa de um cluster. Mais de

00 por experimento.

Há também um problema mais profundo. O ajuste fino completo modifica todos os pesos do modelo. Se você fizer o ajuste fino em dados de suporte ao cliente, poderá degradar as capacidades gerais do modelo. Isso é chamado de esquecimento catastrófico (catastrophic forgetting). O modelo fica melhor na sua tarefa e pior em tudo o resto.

Você precisa de um método que treine menos parâmetros, use menos memória e não destrua o conhecimento existente do modelo.

O Conceito

LoRA: Low-Rank Adaptation

Edward Hu e seus colegas na Microsoft publicaram o LoRA em junho de 2021. O insight do artigo: as atualizações de peso durante o ajuste fino têm posto intrínseco baixo. Você não precisa atualizar todos os 16,7 milhões de parâmetros em uma matriz de pesos de 4096x4096. A informação útil na atualização pode ser capturada por uma matriz de posto 16 ou 32.

Aqui está a matemática. Uma camada linear padrão calcula:

y = Wx

Onde W é uma matriz d_out x d_in. Para uma projeção de atenção de 4096x4096, são 16.777.216 parâmetros.

O LoRA congela W e adiciona uma decomposição de baixo posto:

y = Wx + BAx

Onde B é (d_out x r) e A é (r x d_in). O posto (rank) r é muito menor que d — tipicamente 8, 16 ou 32.

Para r=16 em uma camada 4096x4096:

  • Parâmetros originais: 4096 x 4096 = 16.777.216
  • Parâmetros do LoRA: (4096 x 16) + (16 x 4096) = 65.536 + 65.536 = 131.072
  • Redução: 131.072 / 16.777.216 = 0,78%

Você está treinando 0,78% dos parâmetros e obtendo de 95% a 100% da qualidade.

graph LR
    X["Entrada x"] --> W["W Congelado (d x d)"]
    X --> A["A (r x d)"]
    A --> B["B (d x r)"]
    W --> Plus["+ (mesclar)"]
    B --> Plus
    Plus --> Y["Saída y"]

    style W fill:#1a1a2e,stroke:#e94560,color:#fff
    style A fill:#0f3460,stroke:#16213e,color:#fff
    style B fill:#0f3460,stroke:#16213e,color:#fff

A é inicializado com uma Gaussiana aleatória. B é inicializado como zero. Isso significa que a contribuição do LoRA começa em zero — o modelo inicia o treinamento a partir do seu comportamento original e gradualmente aprende a adaptação.

O Fator de Escala: Alpha

O LoRA introduz um fator de escala alpha que controla o quanto a atualização de baixo posto afeta a saída:

y = Wx + (alpha / r) * BAx

Quando alpha = r, a escala é de 1x. Quando alpha = 2r (o padrão comum), a escala é de 2x. Esse hiperparametro controla a taxa de aprendizado do caminho do LoRA independentemente da taxa de aprendizado base.

Diretrizes práticas:

  • alpha = 2 * rank é uma convenção comum da comunidade (o artigo original usou alpha = rank na maioria dos experimentos)
  • alpha = rank fornece escala de 1x, conservadora mas estável
  • Um alpha mais alto significa atualizações maiores por passo, o que pode acelerar a convergência ou causar instabilidade

Onde Aplicar o LoRA

Um transformer possui muitas camadas lineares. Você não precisa adicionar o LoRA a todas elas. O artigo original testou diferentes combinações:

Camadas Alvo Parâmetros Treináveis (7B) Qualidade
apenas q_proj 4.7M Boa
q_proj + v_proj 9.4M Melhor
q_proj + k_proj + v_proj + o_proj 18.9M Melhor para atenção
Todas lineares (atenção + MLP) 37.7M Ganho marginal, 2x parâmetros

O ponto ideal para a maioria das tarefas: q_proj + v_proj. Isso visa as projeções de consulta (query) e valor (value) na auto-atenção, que controlam para onde o modelo direciona a atenção e quais informações ele extrai. Adicionar camadas MLP ajuda em tarefas complexas, como geração de código, mas dobra a contagem de parâmetros para retornos decrescentes em tarefas mais simples.

Seleção de Posto (Rank)

O posto r controla a expressividade da adaptação:

Posto (Rank) Parâmetros Treináveis (por camada) Ideal Para
4 32,768 Classificação simples, sentimento
8 65,536 P&R de domínio único, sumarização
16 131,072 Tarefas de múltiplos domínios, seguimento de instruções
32 262,144 Raciocínio complexo, geração de código
64 524,288 Retornos decrescentes para a maioria das tarefas
128 1,048,576 Raramente justificado

Hu et al. mostraram que r=4 já captura a maior parte da adaptação para tarefas simples. r=8 e r=16 são as escolhas mais comuns na prática. Ir além de r=64 raramente melhora a qualidade e começa a perder a vantagem de memória do LoRA.

QLoRA: Quantização de 4 Bits + LoRA

Tim Dettmers e seus colegas na Universidade de Washington publicaram o QLoRA em maio de 2023. A ideia: quantizar o modelo base congelado para a precisão de 4 bits e, em seguida, adicionar adaptadores LoRA em fp16 por cima.

Isso altera a equação de memória drasticamente:

Método Memória dos Pesos (7B) Memória de Treinamento (7B) GPU Necessária
Ajuste fino completo (fp16) 14GB ~56GB 1x A100 80GB
LoRA (base fp16) 14GB ~18GB 1x A100 40GB
QLoRA (base 4 bits) 3.5GB ~6GB 1x RTX 3090 24GB

O QLoRA traz três contribuições técnicas:

NF4 (Normal Float 4-bit): Um novo tipo de dados projetado especificamente para pesos de redes neurais. Os pesos de redes neurais seguem uma distribuição aproximadamente normal. O NF4 posiciona seus 16 níveis de quantização nos quantis de uma distribuição normal padrão. Isso é ideal do ponto de vista da teoria da informação para dados distribuídos normalmente. Ele perde menos informação do que a quantização uniforme de 4 bits (INT4) ou Float4 padrão.

Double quantization (Quantização dupla): As próprias constantes de quantização ocupam memória. Cada bloco de 64 pesos precisa de um fator de escala fp32 (4 bytes). Para um modelo de 7B, isso representa 0,4GB extras. A quantização dupla quantiza essas constantes para fp8, reduzindo o overhead para 0,1GB. É pouco, mas faz diferença no total.

Paged optimizers (Otimizadores paginados): Durante o treinamento, os estados do otimizador (momentum e variância do Adam) podem exceder a memória da GPU em sequências longas. Os otimizadores paginados usam a memória unificada da NVIDIA para paginar automaticamente os estados do otimizador para a memória RAM da CPU quando a memória da GPU estiver esgotada, e trazê-los de volta quando necessário. Isso evita travamentos por falta de memória (OOM), ao custo de alguma taxa de processamento (throughput).

A Questão da Qualidade

Reduzir parâmetros ou quantizar a base prejudica a qualidade? Os resultados de múltiplos artigos:

Método MMLU (5-shot) MT-Bench HumanEval
Ajuste fino completo (Llama 2 7B) 48.3 6.72 14.6
LoRA r=16 47.9 6.68 14.0
QLoRA r=16 (NF4) 47.5 6.61 13.4
QLoRA r=64 (NF4) 48.1 6.70 14.2

O LoRA com r=16 fica a menos de 1% do ajuste fino completo na maioria dos benchmarks. O QLoRA com r=16 perde mais uma fração de percentual. O QLoRA com r=64 essencialmente iguala o ajuste fino completo enquanto usa 90% menos memória.

Custos no Mundo Real

Ajuste fino do Llama 3 8B em 50.000 exemplos (3 épocas):

Método GPU Tempo Custo
Ajuste fino completo 2x A100 80GB 8 horas ~$32
LoRA r=16 1x A100 40GB 4 horas ~$8
QLoRA r=16 1x RTX 4090 24GB 6 horas ~$5
QLoRA r=16 (Unsloth) 1x RTX 4090 24GB 2.5 horas ~
QLoRA r=16 1x T4 16GB 12 hours ~$4

O QLoRA em uma única GPU de consumo custa menos que um almoço. É por isso que a comunidade de ajuste fino de pesos abertos explodiu em 2023 e por que cada framework de treinamento abaixo vem com suporte ao QLoRA por padrão em 2026.

O ecossistema de PEFT em 2026

Framework O que é Escolha quando
Hugging Face PEFT A biblioteca canônica para LoRA/QLoRA/DoRA/IA3 Você quer controle bruto e seu loop de treinamento já está no transformers.Trainer
TRL Treinadores de aprendizado por reforço baseados em feedback da HF (SFT, DPO, GRPO, PPO, ORPO) Você precisa de DPO/GRPO após SFT; construído sobre o PEFT
Unsloth Reescrita em kernel Triton das passagens forward/backward Você quer aceleração de 2-5x + metade da VRAM sem perda de precisão; famílias Llama/Mistral/Qwen
Axolotl Wrapper de configuração YAML sobre PEFT + TRL + DeepSpeed + Unsloth Você quer execuções de treinamento reproduzíveis e com controle de versão
LLaMA-Factory GUI/CLI/API sobre PEFT + TRL Você deseja ajuste fino sem código; mais de 100 famílias de modelos suportadas
torchtune Receitas PyTorch nativas, sem dependência de transformers Você quer o mínimo de dependências e sua organização já padroniza no PyTorch

Regra geral: pesquisa ou experimento único → PEFT. Pipeline de produção repetível → Axolotl com kernels Unsloth habilitados. Prototipagem rápida e descartável → LLaMA-Factory.

Mesclando Adaptadores

Após o treinamento, você tem duas coisas: o modelo base congelado e um pequeno adaptador LoRA (tipicamente 10-100MB). Você pode:

  1. Mantê-los separados: Carregar o modelo base e carregar o adaptador por cima. Trocar adaptadores para tarefas diferentes. É assim que você serve múltiplas variantes com ajuste fino a partir de um único modelo base.

  2. Mesclá-los permanentemente: Calcular W' = W + (alpha/r) * BA e salvar o resultado como um novo modelo completo. O modelo mesclado tem o mesmo tamanho do original. Sem overhead de inferência. Sem adaptador para gerenciar.

Para atender a múltiplas tarefas (adaptador de suporte ao cliente, adaptador de código, adaptador de tradução), mantenha-os separados. Para implantar um único modelo especializado, mescle-os.

Técnicas avançadas de mesclagem para combinar múltiplos adaptadores:

  • TIES-Merging (Yadav et al. 2023): Remove parâmetros de pequena magnitude, resolve conflitos de sinal e então faz a mesclagem. Reduz a interferência entre adaptadores.
  • DARE (Yu et al. 2023): Descarta aleatoriamente parâmetros do adaptador antes de mesclar e redimensiona o restante. Surpreendentemente eficaz para combinar capacidades.
  • Aritmética de tarefas (Task arithmetic): Simplesmente somar ou subtrair pesos de adaptadores. Somar um adaptador de "código" e um adaptador de "matemática" frequentemente gera um modelo bom em ambos.

Quando NÃO Fazer Ajuste Fino

O ajuste fino é a terceira opção, não a primeira.

Primeiro: engenharia de prompt (prompt engineering). Escreva um prompt de sistema melhor. Adicione exemplos de poucos disparos (few-shot). Use cadeia de pensamento (chain-of-thought). Isso não custa nada e leva minutos. Se o prompt levar você a 80% do caminho, você provavelmente não precisa de ajuste fino.

Segundo: RAG. Se o modelo precisar conhecer seus dados específicos (documentos, base de conhecimento, catálogo de produtos), a recuperação é mais barata e mais fácil de manter do que gravar essas informações nos pesos do modelo. Veja a Lição 06.

Terceiro: ajuste fino. Use isso quando você precisar que o modelo adote um estilo, formato ou padrão de raciocínio específico que não possa ser alcançado por meio de prompts. Quando precisar de saída estruturada de forma consistente. Quando precisar destilar um modelo maior em um menor. Quando a latência for importante e você não puder arcar com o custo de tokens extras de prompts few-shot.

graph TD
    Start["Precisa de melhor comportamento do modelo?"] --> PE["Tente engenharia de prompt"]
    PE -->|"Funciona"| Done["Publique-o"]
    PE -->|"Não é suficiente"| RAG["Precisa de conhecimento externo?"]
    RAG -->|"Sim"| RAGBuild["Construa pipeline RAG"]
    RAG -->|"Não, preciso mudar estilo/formato"| FT["Ajuste fino com LoRA/QLoRA"]
    RAGBuild -->|"Funciona"| Done
    RAGBuild -->|"Também preciso mudar estilo"| FT
    FT --> Done

    style Start fill:#1a1a2e,stroke:#e94560,color:#fff
    style Done fill:#0f3460,stroke:#16213e,color:#fff

Construa

Implementamos o LoRA do zero em PyTorch puro. Sem bibliotecas. Sem mágica. Você construirá a camada LoRA, a injetará em um modelo, o treinará e mesclará os pesos de volta.

Passo 1: A Camada LoRA

import torch
import torch.nn as nn
import math

class LoRALayer(nn.Module):
    def __init__(self, in_features, out_features, rank=8, alpha=16):
        super().__init__()
        self.rank = rank
        self.alpha = alpha
        self.scaling = alpha / rank

        self.A = nn.Parameter(torch.randn(in_features, rank) * (1 / math.sqrt(rank)))
        self.B = nn.Parameter(torch.zeros(rank, out_features))

    def forward(self, x):
        return (x @ self.A @ self.B) * self.scaling

A é inicializado com valores aleatórios escalados. B é inicializado como zero. O produto BA começa em zero, então o modelo inicia com seu comportamento original.

Passo 2: Camada Linear Envelopada com LoRA

class LinearWithLoRA(nn.Module):
    def __init__(self, linear, rank=8, alpha=16):
        super().__init__()
        self.linear = linear
        self.lora = LoRALayer(
            linear.in_features, linear.out_features, rank, alpha
        )

        for param in self.linear.parameters():
            param.requires_grad = False

    def forward(self, x):
        return self.linear(x) + self.lora(x)

A camada linear original é congelada. Apenas os parâmetros do LoRA (A e B) são treináveis.

Passo 3: Injetar LoRA em um Modelo

def inject_lora(model, target_modules, rank=8, alpha=16):
    for param in model.parameters():
        param.requires_grad = False

    lora_layers = {}
    for name, module in model.named_modules():
        if isinstance(module, nn.Linear):
            if any(t in name for t in target_modules):
                parent_name = ".".join(name.split(".")[:-1])
                child_name = name.split(".")[-1]
                parent = dict(model.named_modules())[parent_name]
                lora_linear = LinearWithLoRA(module, rank, alpha)
                setattr(parent, child_name, lora_linear)
                lora_layers[name] = lora_linear
    return lora_layers

Primeiro, congele todos os parâmetros do modelo. Em seguida, percorra a árvore do modelo, encontre as camadas lineares correspondentes aos seus nomes de destino e substitua-as por versões envelopadas com LoRA. As matrizes LoRA A e B são os únicos parâmetros treináveis em todo o modelo.

Passo 4: Contar Parâmetros

def count_parameters(model):
    total = sum(p.numel() for p in model.parameters())
    trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
    frozen = total - trainable
    return {
        "total": total,
        "trainable": trainable,
        "frozen": frozen,
        "trainable_pct": 100 * trainable / total if total > 0 else 0
    }

Passo 5: Mesclar Pesos de Volta

def merge_lora_weights(model):
    for name, module in model.named_modules():
        if isinstance(module, LinearWithLoRA):
            with torch.no_grad():
                merged = (
                    module.lora.A @ module.lora.B
                ) * module.lora.scaling
                module.linear.weight.data += merged.T
            parent_name = ".".join(name.split(".")[:-1])
            child_name = name.split(".")[-1]
            if parent_name:
                parent = dict(model.named_modules())[parent_name]
            else:
                parent = model
            setattr(parent, child_name, module.linear)

Após a mesclagem, as camadas LoRA desaparecem. O modelo passa a ter o mesmo tamanho do original, com a adaptação gravada diretamente nos pesos. Sem overhead de inferência.

Passo 6: Quantização QLoRA Simulada

def quantize_to_nf4(tensor, block_size=64):
    blocks = tensor.reshape(-1, block_size)
    scales = blocks.abs().max(dim=1, keepdim=True).values / 7.0
    scales = torch.clamp(scales, min=1e-8)
    quantized = torch.round(blocks / scales).clamp(-8, 7).to(torch.int8)
    return quantized, scales

def dequantize_from_nf4(quantized, scales, original_shape):
    dequantized = quantized.float() * scales
    return dequantized.reshape(original_shape)

Isso simula a quantização de 4 bits mapeando os pesos em 16 níveis discretos dentro de blocos de 64. O QLoRA em produção utiliza a biblioteca bitsandbytes para obter NF4 real em GPU.

Passo 7: Loop de Treinamento

def train_lora(model, data, epochs=5, lr=1e-3, batch_size=4):
    optimizer = torch.optim.AdamW(
        [p for p in model.parameters() if p.requires_grad], lr=lr
    )
    criterion = nn.MSELoss()

    losses = []
    for epoch in range(epochs):
        epoch_loss = 0.0
        n_batches = 0
        indices = torch.randperm(len(data["inputs"]))

        for i in range(0, len(indices), batch_size):
            batch_idx = indices[i:i + batch_size]
            x = data["inputs"][batch_idx]
            y = data["targets"][batch_idx]

            output = model(x)
            loss = criterion(output, y)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            epoch_loss += loss.item()
            n_batches += 1

        avg_loss = epoch_loss / n_batches
        losses.append(avg_loss)

    return losses

Passo 8: Demonstração Completa

def demo():
    torch.manual_seed(42)
    d_model = 256
    n_classes = 10

    model = nn.Sequential(
        nn.Linear(d_model, 512),
        nn.ReLU(),
        nn.Linear(512, 512),
        nn.ReLU(),
        nn.Linear(512, n_classes),
    )

    n_samples = 500
    x = torch.randn(n_samples, d_model)
    y = torch.randint(0, n_classes, (n_samples,))
    y_onehot = torch.zeros(n_samples, n_classes).scatter_(1, y.unsqueeze(1), 1.0)

    data = {"inputs": x, "targets": y_onehot}

    params_before = count_parameters(model)

    lora_layers = inject_lora(
        model, target_modules=["0", "2"], rank=8, alpha=16
    )

    params_after = count_parameters(model)

    losses = train_lora(model, data, epochs=20, lr=1e-3)

    merge_lora_weights(model)
    params_merged = count_parameters(model)

    return {
        "params_before": params_before,
        "params_after": params_after,
        "params_merged": params_merged,
        "losses": losses,
    }

A demonstração cria um modelo pequeno, injeta o LoRA em duas camadas, treina-o e mescla os pesos de volta. A contagem de parâmetros cai de totalmente treinável para aproximadamente 1% treinável durante o treinamento do LoRA, e depois retorna à arquitetura original após a mesclagem.

Use

Com o ecossistema da Hugging Face, aplicar o LoRA em um modelo real leva cerca de 20 linhas:

from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    target_modules=["q_proj", "v_proj"],
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

Para o QLoRA, adicione a quantização do bitsandbytes:

from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    quantization_config=bnb_config,
    device_map="auto",
)

model = get_peft_model(model, lora_config)

É isso. Mesmo loop de treinamento. Mesmo pipeline de dados. O modelo base agora vive em 4 bits, os adaptadores LoRA são treinados em fp16 e todo o conjunto cabe em 6GB.

Para treinamento com o Hugging Face Trainer:

from transformers import TrainingArguments, Trainer
from datasets import load_dataset

dataset = load_dataset("tatsu-lab/alpaca", split="train[:5000]")

training_args = TrainingArguments(
    output_dir="./lora-llama",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    fp16=True,
    logging_steps=10,
    save_strategy="epoch",
    optim="paged_adamw_8bit",
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
)

trainer.train()

model.save_pretrained("./lora-adapter")

O adaptador salvo possui entre 10 e 100MB. O modelo base permanece inalterado. Você pode compartilhar adaptadores no Hugging Face Hub sem redistribuir o modelo completo.

Publique

Esta lição produz:

  • outputs/prompt-lora-advisor.md — um prompt que ajuda a decidir o posto (rank) do LoRA, módulos de destino e hiperparâmetros para sua tarefa específica
  • outputs/skill-fine-tuning-guide.md — uma habilidade que ensina aos agentes a árvore de decisão de quando e como fazer o ajuste fino

Exercícios

  1. Estudo de ablação do posto (Rank ablation study). Execute a demonstração com os postos 2, 4, 8, 16, 32 e 64. Plote a perda final vs. posto. Encontre o ponto de retornos decrescentes onde dobrar o posto não reduz mais a perda pela metade. Para uma tarefa de classificação simples em recursos de 256 dimensões, isso deve ser em torno de r=8-16.

  2. Comparação de módulos de destino. Modifique inject_lora para segmentar apenas a camada "0", apenas a camada "2", apenas a camada "4", e todas as três. Treine cada variante por 20 épocas. Compare a velocidade de convergência e a perda final. Isso reflete a decisão real de atingir q_proj vs v_proj vs todas as camadas lineares.

  3. Análise de erro de quantização. Obtenha as matrizes de pesos do modelo treinado antes e depois de quantize_to_nf4 / dequantize_from_nf4. Calcule o erro quadrático médio, o erro absoluto máximo e a correlação entre os pesos originais e reconstruídos. Experimente com valores de block_size de 32, 64, 128 e 256.

  4. Atendimento com múltiplos adaptadores (Multi-adapter serving). Treine dois adaptadores LoRA em diferentes subconjuntos de dados (índices pares vs. ímpares). Salve ambos os adaptadores. Carregue o modelo base uma vez, depois troque os adaptadores e verifique se cada um produz saídas diferentes para a mesma entrada. É assim que sistemas em produção servem múltiplos modelos com ajuste fino a partir de uma única base.

  5. Inferência mesclada vs. não mesclada. Compare a saída do modelo LoRA antes e depois de merge_lora_weights nas mesmas 100 entradas. Verifique se as saídas são idênticas (dentro da tolerância de ponto flutuante de 1e-5). Em seguida, avalie a velocidade de inferência para ambos — a versão mesclada deve ser ligeiramente mais rápida, pois é uma multiplicação de matriz única em vez de duas.

Termos-Chave

Termo O que dizem O que realmente significa
LoRA "Ajuste fino eficiente" Low-Rank Adaptation: congela os pesos da base, treina duas matrizes pequenas A e B cujo produto aproxima a atualização completa dos pesos
QLoRA "Ajuste fino em um laptop" Quantized LoRA: carrega o modelo base em NF4 de 4 bits, treina adaptadores LoRA em fp16 por cima, permitindo o ajuste fino de 7B em 6GB de VRAM
Posto / Rank (r) "O quanto o modelo pode aprender" A dimensão interna das matrizes A e B; controla a expressividade vs. contagem de parâmetros
Alpha "Taxa de aprendizado do LoRA" Fator de escala aplicado à saída do LoRA; alpha/r dimensiona a contribuição da adaptação para a saída final
NF4 "Quantização de 4 bits" Normal Float 4: um tipo de dados de 4 bits com níveis de quantização nos quantis da distribuição normal, ideal para pesos de redes neurais
Adaptador "A parte pequena treinada" As matrizes LoRA A e B salvas como um arquivo separado (10-100MB), carregáveis por cima de qualquer cópia do modelo base
Módulos de destino "Quais camadas aplicar LoRA" As camadas lineares específicas (q_proj, v_proj, etc.) onde os adaptadores LoRA são injetados
Mesclagem (Merging) "Gravar no modelo" Calcular W + (alpha/r) * BA e substituir o peso original, eliminando o overhead do adaptador na inferência
Otimizadores paginados "Evitar OOM durante o treino" Descarregar os estados do otimizador (momentum e variância do Adam) para a CPU quando a memória da GPU estiver esgotada
Esquecimento catastrófico "O ajuste fino quebrou tudo" Quando a atualização de todos os pesos faz com que o modelo perca capacidades aprendidas anteriormente

Leituras Adicionais

  • Hu et al., "LoRA: Low-Rank Adaptation of Large Language Models" (2021) — o artigo original que introduz o método de decomposição de baixo posto, testado no GPT-3 175B com posto tão baixo quanto 4
  • Dettmers et al., "QLoRA: Efficient Finetuning of Quantized Language Models" (2023) — apresenta NF4, quantização dupla e otimizadores paginados, permitindo o ajuste fino de 65B em uma única GPU de 48GB
  • Documentação da biblioteca PEFT (huggingface.co/docs/peft) — a biblioteca padrão para LoRA, QLoRA e outros métodos de eficiência de parâmetros no ecossistema Hugging Face
  • Yadav et al., "TIES-Merging: Resolving Interference When Merging Models" (2023) — técnicas para combinar múltiplos adaptadores LoRA sem degradação de qualidade
  • Rafailov et al., "Direct Preference Optimization: Your Language Model is Secretly a Reward Model" (NeurIPS 2023) — derivação do DPO; a etapa de ajuste de preferências que ocorre após o SFT, sem necessidade de modelo de recompensa.
  • Documentação do TRL — referência oficial para SFTTrainer, DPOTrainer, KTOTrainer e a superfície de integração com PEFT/bitsandbytes/Unsloth.
  • Documentação do Unsloth — kernels fundidos que dobram o rendimento de ajuste fino e reduzem a memória pela metade; a camada de desempenho sob o TRL.
  • Documentação do Axolotl — treinador de SFT/DPO/QLoRA multi-GPU configurado por YAML; a alternativa de configuração como código para scripts escritos à mão.
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).