Fine-Tuning de LLMs Open-Source com QLoRA e Unsloth: O Guia Completo 2026
Você construiu um protótipo com GPT-4 ou Claude. Funciona que é uma beleza. Aí chega a fatura: $12.000 de chamadas API no mês passado. E tá crescendo 40% mês a mês.
Todo engenheiro de IA bate nesse muro. O abismo entre protótipo e produção onde os custos de API ficam insustentáveis, os requisitos de latência apertam, e você percebe que precisa de um modelo que realmente entenda o seu domínio — não todo o conhecimento da internet.
Fine-tuning de um LLM open-source é a resposta. E graças ao QLoRA (Quantized Low-Rank Adaptation) e ferramentas como Unsloth, você não precisa mais de um cluster de GPUs A100 nem de um doutorado em machine learning pra fazer isso. Uma única GPU de consumo com 24GB de VRAM — uma RTX 4090 ou até uma T4 gratuita do Google Colab — é suficiente pra fazer fine-tuning de um modelo com bilhões de parâmetros ativos que supera o GPT-4 na sua tarefa específica.
Este guia cobre tudo do zero à produção: por que fine-tuning funciona, como o QLoRA torna viável em hardware de consumo, como preparar seu dataset, o código exato de treinamento, estratégias de avaliação e deploy. Todos os exemplos de código são testados em produção.
Quando fazer Fine-Tuning ao invés de Prompting?
Antes de entrar no código, bora ser precisos sobre quando fine-tuning é a escolha certa. Nem sempre é.
Use prompting / RAG quando:
- Sua tarefa é de propósito geral (resumo, tradução, Q&A sobre documentos)
- Seus dados mudam frequentemente (bases de conhecimento, tickets de suporte)
- Você ainda tá explorando o que o modelo deve fazer
- Precisa fazer deploy em dias, não semanas
Use fine-tuning quando:
- O modelo precisa aprender um estilo, formato ou comportamento específico que prompting não consegue produzir de forma confiável
- Você tem uma tarefa bem definida com padrões de entrada/saída consistentes
- Latência e custo em escala importam (um modelo 7B fine-tunado é 10-50x mais barato por token que GPT-4)
- O modelo precisa entender profundamente terminologia específica do domínio
- Você quer reduzir alucinações em fatos específicos do domínio
Os casos de uso mais comuns em produção:
| Caso de Uso | Onde o Prompting Falha | Vantagem do Fine-Tuning |
|---|---|---|
| Geração de código pra APIs internas | O modelo não conhece seu SDK | Aprende seus padrões e convenções |
| Análise de documentos médicos/jurídicos | Modelos genéricos são cautelosos demais | Saídas seguras e específicas do domínio |
| Extração de dados estruturados | Formatação baseada em prompt é frágil | Aderência consistente ao schema |
| Matching de tom do suporte ao cliente | System prompts desviam em conversas longas | Voz e personalidade internalizadas |
| Geração de SQL pra schemas customizados | Schema no contexto consome tokens | Conhecimento internalizado do schema |
O insight principal: fine-tuning não ensina ao modelo conhecimento novo per se. Ensina comportamentos novos. Um modelo fine-tunado não memoriza seu banco de dados — aprende como raciocinar sobre os padrões do seu domínio, produzir outputs no seu formato específico e aplicar as convenções da sua organização de forma consistente.
Entendendo LoRA e QLoRA
O problema: Fine-Tuning completo é caro demais
Fine-tuning completo tradicional atualiza cada parâmetro do modelo. Pra um modelo de 7B parâmetros:
- Memória: ~28GB só pros pesos do modelo em FP32, mais ~28GB pra estados do otimizador, mais ~28GB pra gradientes. Total: ~84GB de VRAM no mínimo.
- Hardware: Múltiplas GPUs A100 de 80GB.
- Custo: $10-50/hora em instâncias GPU na nuvem, treinamentos de horas a dias.
- Risco: Esquecimento catastrófico — o modelo perde capacidades gerais enquanto aprende sua tarefa específica.
LoRA: a grande sacada
LoRA (Low-Rank Adaptation) teve um insight fundamental: você não precisa atualizar todos os parâmetros. Quando faz fine-tuning de um modelo pré-treinado, as mudanças de peso tendem a ter um rank intrínseco baixo. A matriz de atualização pode ser decomposta em duas matrizes muito menores.
Em vez de atualizar uma matriz de pesos W de dimensões d × k, LoRA congela W e treina duas matrizes pequenas A (d × r) e B (r × k), onde r (o rank) é muito menor que d e k:
Original: W (4096 × 4096) → 16.7M parâmetros pra atualizar
LoRA: A (4096 × 16) + B (16 × 4096) → 131K parâmetros pra atualizar
Redução: 99.2% menos parâmetros treináveis
O forward pass vira: output = W·x + α·B·A·x, onde α é um fator de escala. Na inferência, você pode fundir B·A de volta no W, então tem zero latência adicional em comparação com o modelo original.
# Ilustração conceitual do LoRA import torch import torch.nn as nn class LoRALayer(nn.Module): def __init__(self, original_layer: nn.Linear, rank: int = 16, alpha: float = 32): super().__init__() self.original = original_layer self.original.weight.requires_grad = False # Congelar pesos originais d_in = original_layer.in_features d_out = original_layer.out_features # Matrizes de decomposição de baixo rank self.lora_A = nn.Parameter(torch.randn(d_in, rank) * 0.01) self.lora_B = nn.Parameter(torch.zeros(rank, d_out)) self.scale = alpha / rank def forward(self, x): # Computação original (congelada) + atualização de baixo rank original_output = self.original(x) lora_output = (x @ self.lora_A @ self.lora_B) * self.scale return original_output + lora_output def merge(self): """Fundir pesos LoRA no original pra inferência sem custo.""" self.original.weight.data += (self.lora_A @ self.lora_B).T * self.scale
QLoRA: acessível pra todo mundo
QLoRA (Quantized LoRA) adicionou três inovações que tornaram o fine-tuning acessível em hardware de consumo:
-
Quantização NormalFloat de 4 bits (NF4): O modelo base é quantizado pra 4 bits usando um esquema de quantização consciente da distribuição. Um modelo de 7B vai de ~14GB (FP16) pra ~3.5GB.
-
Quantização Dupla: As constantes de quantização também são quantizadas, economizando mais 0.37 bits por parâmetro (~325MB num modelo de 7B).
-
Otimizadores Paginados: Estados do otimizador são descarregados pra RAM da CPU quando a memória GPU fica curta. Isso evita crashes por OOM durante picos de treinamento.
O resultado: fine-tuning de um modelo 7B numa única GPU de 24GB, ou um modelo 13B numa GPU de 48GB. O breakdown de memória:
Fine-Tuning Completo (modelo 7B):
Pesos do modelo (FP32): ~28 GB
Estados do otimizador: ~28 GB
Gradientes: ~28 GB
Total: ~84 GB → Precisa de 2x A100 80GB
Fine-Tuning com QLoRA (modelo 7B):
Pesos do modelo (NF4): ~3.5 GB
Adaptadores LoRA (FP16): ~0.1 GB
Estados do otimizador: ~0.4 GB
Gradientes + ativações: ~4.0 GB
Total: ~8.0 GB → Cabe na RTX 4090 (24GB) com folga
A diferença de qualidade entre fine-tuning completo e QLoRA? Na maioria dos benchmarks, fica dentro de 1-2% — um tradeoff insignificante pra uma redução de 10x nos requisitos de hardware.
Configurando o ambiente
Requisitos de Hardware
| GPU | VRAM | Tamanho máximo do modelo | Velocidade de treinamento |
|---|---|---|---|
| T4 (Colab Free) | 16GB | 7B (apertado) | ~1.5 horas/epoch em 10K amostras |
| RTX 3090/4090 | 24GB | 7B (confortável), 13B (apertado) | ~45 min/epoch em 10K amostras |
| A100 40GB | 40GB | 13B (confortável), 34B (apertado) | ~20 min/epoch em 10K amostras |
| A100 80GB | 80GB | 70B com quantização agressiva | ~15 min/epoch em 10K amostras |
Instalação
Usando Unsloth (recomendado pra 2-5x de speedup sobre HuggingFace padrão):
# Criar um ambiente limpo conda create -n finetune python=3.11 -y conda activate finetune # Instalar PyTorch com suporte CUDA pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # Instalar Unsloth (cuida do bitsandbytes, transformers, peft, trl automaticamente) pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git" pip install --no-deps xformers trl peft accelerate bitsandbytes # Pra avaliação pip install rouge-score nltk scikit-learn
Escolhendo um modelo base (Março 2026)
A escolha do modelo base importa mais do que a maioria pensa. O panorama atual:
| Modelo | Parâmetros | Contexto | Ideal pra | Licença |
|---|---|---|---|---|
| Llama 4 Scout | 109B total / 17B ativos (MoE) | 10M | Flagship da Meta, contexto massivo, precisa de H100 | Llama 4 Community |
| Llama 3.1 8B | 8B | 128K | Melhor ratio qualidade/tamanho pra iniciantes | Llama 3.1 Community |
| Mistral Small 4 | 32B | 128K | Multilíngue, raciocínio forte, licença Apache | Apache 2.0 |
| Gemma 3 27B | 27B | 128K | Multimodal, coding forte, ecossistema Google | Gemma License |
| Qwen 2.5 7B | 7B | 128K | Melhor pra bilíngue chinês + inglês | Apache 2.0 |
| Phi-4 14B | 14B | 16K | Compacto, raciocínio forte | MIT |
Recomendação pra quem tá começando: Comece com Llama 3.1 8B. Tem a maior comunidade, mais tutoriais e o melhor ratio qualidade/recursos. Quando seu pipeline funcionar, troque por modelos maiores como Llama 4 Scout ou Gemma 3 27B. O Llama 4 Scout usa uma arquitetura Mixture-of-Experts (MoE) — só 17B parâmetros ficam ativos por token, o que o torna surpreendentemente eficiente apesar dos 109B parâmetros totais.
Preparando seu Dataset
Aqui é onde a maioria dos projetos de fine-tuning tem sucesso ou fracassa. O modelo é tão bom quanto os dados que você dá pra ele. 500 exemplos de qualidade excepcional vão superar 50.000 exemplos mediocres.
Formato de dados: o Chat Template
Fine-tuning moderno usa o formato conversacional:
{ "conversations": [ {"role": "system", "content": "Você é um engenheiro backend sênior especializado em otimização de PostgreSQL."}, {"role": "user", "content": "Essa query tá demorando 30 segundos numa tabela com 50M de linhas:\n\nSELECT * FROM orders WHERE customer_id = 12345 AND status = 'pending' ORDER BY created_at DESC LIMIT 10;"}, {"role": "assistant", "content": "O problema provavelmente é um índice composto faltando. Bora pra análise:\n\n**Problema:** Sem um índice em `(customer_id, status, created_at)`, o PostgreSQL faz um sequential scan em 50M de linhas, depois ordena os resultados.\n\n**Solução:**\n```sql\nCREATE INDEX CONCURRENTLY idx_orders_customer_status_created\nON orders (customer_id, status, created_at DESC);\n```\n\n**Por que funciona:**\n1. O índice cobre as três colunas do WHERE e ORDER BY\n2. `created_at DESC` no índice casa com seu ORDER BY, eliminando a operação de sort\n3. O PostgreSQL pode usar um Index Only Scan se você trocar `SELECT *` por colunas específicas\n4. `CONCURRENTLY` evita bloquear a tabela durante a criação\n\n**Melhora esperada:** De ~30s pra <10ms pra esse padrão de query."} ] }
Checklist de qualidade dos dados
Antes de treinar, audite seu dataset contra estes critérios:
def audit_dataset(dataset): issues = [] for i, example in enumerate(dataset): conversations = example['conversations'] # Check 1: Comprimento mínimo da conversa if len(conversations) < 2: issues.append(f"Exemplo {i}: Menos de 2 turnos") # Check 2: Qualidade da resposta (proxy por comprimento) assistant_msgs = [c for c in conversations if c['role'] == 'assistant'] for msg in assistant_msgs: if len(msg['content']) < 50: issues.append(f"Exemplo {i}: Resposta muito curta ({len(msg['content'])} chars)") if len(msg['content']) > 8000: issues.append(f"Exemplo {i}: Resposta muito longa ({len(msg['content'])} chars)") # Check 3: Sem mensagens vazias for c in conversations: if not c['content'].strip(): issues.append(f"Exemplo {i}: Mensagem vazia de {c['role']}") # Check 4: Alternação correta de roles roles = [c['role'] for c in conversations if c['role'] != 'system'] for j in range(1, len(roles)): if roles[j] == roles[j-1]: issues.append(f"Exemplo {i}: Mensagens consecutivas de {roles[j]}") # Check 5: Sem vazamento de dados for c in conversations: if any(phrase in c['content'].lower() for phrase in ['as an ai', 'i am an ai', 'language model']): issues.append(f"Exemplo {i}: Possível vazamento de identidade em mensagem de {c['role']}") return issues
Quantos exemplos você precisa?
Depende da sua tarefa:
| Tipo de tarefa | Mínimo | Ponto ideal | Retornos decrescentes |
|---|---|---|---|
| Adaptação de estilo/tom | 50–100 | 200–500 | >1,000 |
| Q&A específico do domínio | 200–500 | 1,000–3,000 | >10,000 |
| Geração de código (SDK específico) | 500–1,000 | 2,000–5,000 | >15,000 |
| Cadeias de raciocínio complexas | 1,000–2,000 | 5,000–10,000 | >20,000 |
A regra 80/10/10:
- 80% pra treinamento
- 10% pra validação (monitorado durante o treinamento pra prevenir overfitting)
- 10% pra avaliação final (nunca visto durante o treinamento)
from datasets import load_dataset, DatasetDict def prepare_splits(dataset_path): dataset = load_dataset("json", data_files=dataset_path, split="train") dataset = dataset.shuffle(seed=42) # Split 80/10/10 train_test = dataset.train_test_split(test_size=0.2, seed=42) val_test = train_test['test'].train_test_split(test_size=0.5, seed=42) return DatasetDict({ 'train': train_test['train'], 'validation': val_test['train'], 'test': val_test['test'], })
Gerando dados de treinamento sintéticos
Se você não tem exemplos suficientes, pode fazer bootstrap do seu dataset usando um modelo forte (GPT-4, Claude) pra gerar dados de treinamento pra um modelo menor. Essa técnica se chama destilação de conhecimento via dados sintéticos e é muito usada em produção.
import openai import json SYSTEM_PROMPT = """Você está gerando dados de treinamento para um modelo fine-tunado que vai atuar como especialista em otimização de PostgreSQL. Gere perguntas realistas de usuários sobre problemas de performance do PostgreSQL e forneça respostas de nível especialista. Inclua: - Queries SQL específicas com nomes de tabelas e tamanhos realistas - Interpretação de output do EXPLAIN ANALYZE - Recomendações concretas de índices com statements CREATE INDEX - Estimativas de melhoria de performance Cada resposta deve ter 200-500 palavras com exemplos de código.""" async def generate_training_examples(n_examples: int = 500): client = openai.AsyncOpenAI() examples = [] topics = [ "queries JOIN lentas em tabelas grandes", "problemas de N+1 queries em ORMs", "full table scans em colunas indexadas", "contenção de locks em cenários de alta escrita", "regressão de query plan depois de VACUUM", "esgotamento do connection pool", "detecção e remediação de index bloat", ] for i in range(n_examples): topic = topics[i % len(topics)] response = await client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": f"Gere um exemplo de treinamento sobre: {topic}. " f"Varie a complexidade e os schemas das tabelas."} ], temperature=0.9, response_format={"type": "json_object"}, ) example = json.loads(response.choices[0].message.content) examples.append(example) return examples
Aviso crítico: Sempre revise manualmente uma amostra dos seus dados sintéticos. LLMs podem gerar conselhos técnicos que parecem plausíveis mas tão errados. Reserve tempo pra revisão humana de pelo menos 10-20% dos exemplos sintéticos.
Treinamento com Unsloth
Agora vem a parte boa. Aqui vai o script de treinamento completo:
from unsloth import FastLanguageModel from trl import SFTTrainer from transformers import TrainingArguments from datasets import load_dataset # ───────────────────────────────────────── # 1. Carregar modelo com quantização de 4 bits # ───────────────────────────────────────── model, tokenizer = FastLanguageModel.from_pretrained( model_name="unsloth/Meta-Llama-3.1-8B-Instruct", max_seq_length=4096, dtype=None, load_in_4bit=True, ) # ───────────────────────────────────────── # 2. Configurar adaptadores LoRA # ───────────────────────────────────────── model = FastLanguageModel.get_peft_model( model, r=32, lora_alpha=64, target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", ], lora_dropout=0.05, bias="none", use_gradient_checkpointing="unsloth", random_state=42, ) model.print_trainable_parameters() # ───────────────────────────────────────── # 3. Carregar e formatar dataset # ───────────────────────────────────────── dataset = load_dataset("json", data_files="training_data.jsonl", split="train") def format_chat(example): """Formatar conversas no chat template do Llama 3.""" messages = example['conversations'] text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=False, ) return {"text": text} dataset = dataset.map(format_chat, remove_columns=dataset.column_names) # ───────────────────────────────────────── # 4. Configurar treinamento # ───────────────────────────────────────── training_args = TrainingArguments( output_dir="./outputs", per_device_train_batch_size=4, gradient_accumulation_steps=4, num_train_epochs=3, learning_rate=2e-4, lr_scheduler_type="cosine", warmup_ratio=0.05, weight_decay=0.01, logging_steps=10, save_strategy="steps", save_steps=100, eval_strategy="steps", eval_steps=100, fp16=not torch.cuda.is_bf16_supported(), bf16=torch.cuda.is_bf16_supported(), optim="adamw_8bit", seed=42, max_grad_norm=0.3, report_to="wandb", ) # ───────────────────────────────────────── # 5. Inicializar trainer e começar # ───────────────────────────────────────── trainer = SFTTrainer( model=model, tokenizer=tokenizer, train_dataset=dataset, args=training_args, max_seq_length=4096, dataset_text_field="text", packing=True, ) print("Iniciando fine-tuning...") stats = trainer.train() print(f"Treinamento concluído em {stats.metrics['train_runtime']:.0f} segundos") print(f"Loss final: {stats.metrics['train_loss']:.4f}")
Guia de ajuste de hiperparâmetros
Os valores padrão acima funcionam bem pra maioria dos casos, mas aqui vai como ajustá-los:
Rank LoRA (r):
r=8: Adaptação mínima. Bom pra mudanças simples de estilo.r=16: Padrão. Funciona pra maioria das tarefas.r=32: Maior capacidade. Bom pra adaptação de domínio complexa.r=64+: Próximo da capacidade de fine-tuning completo. Raramente necessário.
Regra geral: comece com r=16. Se a loss de validação estagnar cedo, suba pra 32. Se fizer overfitting rápido, abaixe pra 8.
Learning Rate:
2e-4: Padrão QLoRA. Comece aqui.1e-4: Mais conservador. Use se o treinamento estiver instável.5e-5: Muito conservador. Use pra datasets muito pequenos (<200 exemplos).
Número de Epochs:
- 1–2: Datasets grandes (>10K exemplos)
- 2–4: Datasets médios (1K–10K exemplos)
- 4–8: Datasets pequenos (<1K exemplos)
- Cuidado com overfitting: Se a loss de validação começar a subir enquanto a de treinamento continua caindo, é overfitting. Pare o treinamento.
from transformers import EarlyStoppingCallback trainer = SFTTrainer( model=model, tokenizer=tokenizer, train_dataset=train_dataset, eval_dataset=val_dataset, args=training_args, callbacks=[ EarlyStoppingCallback( early_stopping_patience=3, early_stopping_threshold=0.01 ), ], max_seq_length=4096, dataset_text_field="text", packing=True, )
Como a curva de loss deve parecer
Um treinamento saudável tem essa cara:
Loss
4.0 |X
| X
3.0 | X
| X
2.0 | X X
| X X X
1.0 | X X X X X X ← plateau (bom, modelo convergiu)
|
0.0 +─────────────────────────────────────
0 200 400 600 800 1000
Steps
Sinais de alerta:
- Loss não abaixa → Learning rate muito baixo ou problemas com os dados
- Loss cai perto de 0 → Overfitting severo, reduza epochs ou aumente dados
- Loss muito ruidosa → Batch size muito pequeno ou learning rate muito alto
- Loss salta repentinamente → Explosão de gradiente, reduza learning rate
Avaliando seu modelo
O treinamento é só metade da batalha. A avaliação te diz se o modelo fine-tunado realmente melhora em relação ao modelo base pra sua tarefa específica.
Avaliação automatizada
import torch from rouge_score import rouge_scorer import json def evaluate_model(model, tokenizer, test_dataset, max_samples=100): """Avaliação abrangente do modelo fine-tunado.""" model.eval() scorer = rouge_scorer.RougeScorer(['rouge1', 'rougeL'], use_stemmer=True) results = { 'rouge1_scores': [], 'rougeL_scores': [], 'format_compliance': [], 'avg_response_length': [], 'examples': [], } for i, example in enumerate(test_dataset.select(range(min(max_samples, len(test_dataset))))): conversations = example['conversations'] prompt_messages = [] expected_response = "" for msg in conversations: if msg['role'] == 'assistant' and msg == conversations[-1]: expected_response = msg['content'] else: prompt_messages.append(msg) inputs = tokenizer.apply_chat_template( prompt_messages, tokenize=True, add_generation_prompt=True, return_tensors="pt" ).to(model.device) with torch.no_grad(): outputs = model.generate(inputs, max_new_tokens=1024, temperature=0.1, do_sample=False) generated = tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokens=True) rouge_scores = scorer.score(expected_response, generated) results['rouge1_scores'].append(rouge_scores['rouge1'].fmeasure) results['rougeL_scores'].append(rouge_scores['rougeL'].fmeasure) results['avg_response_length'].append(len(generated)) expected_has_code = '```' in expected_response generated_has_code = '```' in generated results['format_compliance'].append(expected_has_code == generated_has_code) if i < 10: results['examples'].append({ 'prompt': prompt_messages[-1]['content'][:200], 'expected': expected_response[:300], 'generated': generated[:300], 'rouge1': rouge_scores['rouge1'].fmeasure, }) summary = { 'avg_rouge1': sum(results['rouge1_scores']) / len(results['rouge1_scores']), 'avg_rougeL': sum(results['rougeL_scores']) / len(results['rougeL_scores']), 'format_compliance_rate': sum(results['format_compliance']) / len(results['format_compliance']), 'avg_response_length': sum(results['avg_response_length']) / len(results['avg_response_length']), 'examples': results['examples'], } return summary
Avaliação LLM-as-Judge
Pra avaliação subjetiva de qualidade, use um modelo mais forte como juiz:
async def llm_judge_evaluation(examples, judge_model="gpt-4o"): """Usar um LLM forte pra avaliar a qualidade das respostas.""" client = openai.AsyncOpenAI() scores = [] for ex in examples: response = await client.chat.completions.create( model=judge_model, messages=[{ "role": "system", "content": """Você está avaliando a qualidade da resposta de um modelo fine-tunado. Avalie cada resposta nestas dimensões (escala 1-5): 1. Precisão Técnica: As afirmações técnicas estão corretas? 2. Completude: Aborda todos os aspectos da pergunta? 3. Conformidade de Formato: Segue o formato de saída esperado? 4. Acionabilidade: O usuário pode aplicar isso diretamente? Responda como JSON: {"accuracy": N, "completeness": N, "format": N, "actionability": N, "reasoning": "..."}""" }, { "role": "user", "content": f"Pergunta: {ex['prompt']}\n\nResposta: {ex['finetuned_response']}" }], response_format={"type": "json_object"}, ) score = json.loads(response.choices[0].message.content) scores.append(score) return scores
Salvando e fazendo deploy do seu modelo
Opções de salvamento
Depois do treinamento, você tem três opções:
# Opção 1: Salvar só os adaptadores LoRA (~100-300MB) model.save_pretrained("./my-lora-adapter") tokenizer.save_pretrained("./my-lora-adapter") # Opção 2: Fundir e salvar modelo completo em 16-bit (~14GB pra 7B) model.save_pretrained_merged("./my-model-merged", tokenizer, save_method="merged_16bit") # Opção 3: Exportar como GGUF pra llama.cpp / Ollama model.save_pretrained_gguf("./my-model-gguf", tokenizer, quantization_method="q4_k_m") # Opção 4: Push pro Hugging Face Hub model.push_to_hub("your-username/my-fine-tuned-model", token="hf_...") tokenizer.push_to_hub("your-username/my-fine-tuned-model", token="hf_...")
Deploy com vLLM (Recomendado pra produção)
vLLM é o padrão pra serving de LLMs em produção:
import openai client = openai.OpenAI(base_url="http://localhost:8000/v1", api_key="dummy") response = client.chat.completions.create( model="./my-model-merged", messages=[ {"role": "system", "content": "Você é um especialista em otimização de PostgreSQL."}, {"role": "user", "content": "Minha query tá fazendo sequential scan em 100M de linhas..."}, ], temperature=0.3, max_tokens=1024, ) print(response.choices[0].message.content)
Deploy com Ollama (Local/Edge)
Pra desenvolvimento local ou deploy no edge, exporte pra GGUF e use Ollama:
cat > Modelfile << 'EOF' FROM ./my-model-gguf/unsloth.Q4_K_M.gguf TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|> {{ .System }}<|eot_id|>{{ end }}<|start_header_id|>user<|end_header_id|> {{ .Prompt }}<|eot_id|><|start_header_id|>assistant<|end_header_id|> """ PARAMETER temperature 0.3 PARAMETER num_ctx 4096 SYSTEM "Você é um especialista em otimização de PostgreSQL." EOF ollama create my-pg-expert -f Modelfile ollama run my-pg-expert "Como otimizar uma query lenta com GROUP BY?"
Checklist de produção
Pré-Deploy
- Scores de avaliação superam baseline: O modelo fine-tunado supera o modelo base no seu test set por uma margem significativa
- Sem esquecimento catastrófico: Teste em tarefas gerais pra garantir que o modelo não perdeu capacidades básicas
- Guardrails implementados: Teste pra outputs prejudiciais, enviesados ou fora do escopo e implemente filtragem de conteúdo
- Benchmarks de latência: Meça P50, P95 e P99 de latência sob carga realista
- Projeção de custos: Calcule custo por request incluindo compute GPU e compare com alternativas baseadas em API
Monitoramento pós-deploy
- Rastrear qualidade do output ao longo do tempo: Configure avaliação automatizada em uma amostra aleatória de requests de produção
- Monitorar distribution drift: Se as queries dos usuários começarem a diferir dos dados de treinamento, a qualidade vai cair
- Versione seus modelos: Use semantic versioning (v1.0.0, v1.1.0) e mantenha capacidade de rollback
- Cadência de retreinamento: Planeje retreinamento periódico conforme acumula mais dados de produção
Erros comuns e como evitá-los
Erro 1: Treinar com dados ruins
Sintoma: O modelo gera respostas que parecem plausíveis mas são tecnicamente incorretas.
Causa: Dados de treinamento sintéticos sem verificação humana.
Solução: Sempre tenha especialistas do domínio revisando pelo menos 10% dos seus dados. Um exemplo incorreto pode contaminar centenas de outputs relacionados.
Erro 2: Overfitting em datasets pequenos
Sintoma: A loss de treinamento se aproxima de 0, mas os outputs são formulaicos e parecem memorizar exemplos literalmente.
Causa: Epochs demais com exemplos de menos.
Solução: Reduza epochs, aumente LoRA dropout pra 0.1, use um rank LoRA mais baixo, ou aumente seu dataset.
Erro 3: Mismatch de Chat Template
Sintoma: O modelo gera lixo, tokens repetidos ou ignora o system prompt.
Causa: Usar um chat template diferente durante treinamento vs inferência.
Solução: Use sempre tokenizer.apply_chat_template() tanto pra formatação de dados de treinamento quanto pra construção de prompts de inferência. Nunca construa chat prompts manualmente.
Erro 4: Esquecimento catastrófico
Sintoma: O modelo performa bem na sua tarefa mas não consegue mais fazer tarefas básicas que fazia antes.
Causa: Fine-tuning agressivo que sobrescreve conhecimento geral.
Solução: Use um learning rate mais baixo (5e-5 em vez de 2e-4), menos epochs, ou misture dados de propósito geral (10-20% do seu training set deve ser exemplos gerais).
Erro 5: Ignorar efeitos da quantização
Sintoma: O modelo fine-tunado funciona bem em FP16 mas degrada significativamente depois de quantização GGUF.
Causa: Quantização agressiva (Q2_K, Q3_K) em modelos que não foram projetados pra isso.
Solução: Use Q4_K_M ou Q5_K_M pra quantização de deploy. Sempre faça benchmark depois de quantizar pra verificar que a qualidade se mantém.
Comparação de custos: Fine-Tuning vs API
Bora fazer as contas com um cenário realista de produção:
Cenário: 100.000 requests/mês, média de 500 tokens de entrada + 500 tokens de saída por request.
| Abordagem | Custo mensal | Latência (P50) | Controle |
|---|---|---|---|
| GPT-4o API | ~$1,500 | 800ms | Baixo |
| Claude 3.5 Sonnet API | ~$1,800 | 600ms | Baixo |
| GPT-4o-mini API | ~$30 | 400ms | Baixo |
| Self-hosted Llama 3.1 8B (A10G) | ~$350 | 120ms | Total |
| Self-hosted Fine-Tuned 8B (A10G) | ~$350 | 120ms | Total + Expertise do domínio |
O modelo fine-tunado custa o mesmo pra operar que o modelo base — mas produz outputs de maior qualidade específicos do domínio.
Ponto de equilíbrio: Pra maioria dos times, self-hosting fica mais barato que chamadas API a partir de 30.000-50.000 requests/mês.
O que vem por aí: técnicas emergentes em 2026
Unsloth Studio (Março 2026): Unsloth lançou uma interface open-source, local e sem código que cuida de todo o ciclo de fine-tuning — preparação de dados, treinamento e deploy — numa única GUI. Promete 70% menos VRAM e 2x mais velocidade. Se os scripts Python acima te intimidam, Studio é um game-changer.
DoRA (Weight-Decomposed Low-Rank Adaptation): Separa magnitude e direção nas atualizações de pesos, superando LoRA consistentemente por 1-3% com overhead insignificante. Já integrado na biblioteca PEFT.
GaLore (Gradient Low-Rank Projection): Promete qualidade de fine-tuning completo a custos de memória de LoRA. Ainda experimental mas promissor.
LoRA Merging e Model Soups: Combinar múltiplos adaptadores LoRA (cada um treinado em tarefas diferentes) num único modelo via média de pesos. Permite especialização multi-tarefa sem deploys separados.
GRPO (Group Relative Policy Optimization): Uma técnica emergente pra treinar modelos de "IA de raciocínio" que fazem lógica multi-passo e chain-of-thought. Unsloth suporta GRPO com apenas 5GB de VRAM. É assim que a próxima geração de modelos de raciocínio como DeepSeek-R1 estão sendo treinados.
Fine-Tuning com Reward Model (RLHF/DPO): Depois do SFT, uma segunda passada de treinamento usando Direct Preference Optimization (DPO) alinha os outputs do modelo com preferências humanas.
from trl import DPOTrainer, DPOConfig dpo_config = DPOConfig( output_dir="./dpo-output", beta=0.1, learning_rate=5e-6, per_device_train_batch_size=2, num_train_epochs=1, ) dpo_trainer = DPOTrainer( model=sft_model, ref_model=None, args=dpo_config, train_dataset=preference_dataset, tokenizer=tokenizer, ) dpo_trainer.train()
Resumo final
Fine-tuning de LLMs open-source passou de atividade de laboratório pra workflow de engenharia padrão. Com QLoRA e Unsloth, você pode fine-tunar um modelo que supera o GPT-4 na sua tarefa específica — numa única GPU de consumo, em menos de uma hora.
Os princípios-chave:
- Qualidade acima de quantidade. 500 exemplos excelentes superam 50.000 mediocres.
- Comece pequeno. Use Llama 3.1 8B com QLoRA no seu menor dataset viável. Faça o pipeline funcionar antes de escalar.
- Avalie rigorosamente. Métricas automatizadas (ROUGE, conformidade de formato) mais LLM-as-judge mais revisão manual. Os três.
- Monitore em produção. A performance do modelo vai driftar conforme as queries dos usuários evoluem. Planeje retreinamento periódico.
- Saiba quando NÃO fazer fine-tuning. Se RAG ou melhor prompting resolve seu problema, é mais barato e rápido.
A distância entre modelos closed-source e open-source diminui a cada mês. Com as técnicas deste guia, você pode construir sistemas de IA em produção que são mais baratos, mais rápidos, mais privados e mais especializados do que qualquer coisa que uma API possa oferecer.
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit