Phase 00 - Lesson 12
Depuração e Profiling
Os piores bugs de IA não quebram. Eles treinam silenciosamente em lixo e reportam uma linda curva de loss.
Tipo: Construir Linguagem: Python Pré-requisitos: Lição 1 (Ambiente de Dev), familiaridade básica com PyTorch Tempo: ~60 minutos
Objetivos de Aprendizagem
- Usar
breakpoint()condicional edebug_printpara inspecionar formatos de tensores, dtypes e valores NaN durante o treinamento - Fazer profiling de loops de treinamento com
cProfile,line_profileretracemallocpara encontrar gargalos - Detectar bugs comuns de IA: incompatibilidade de formatos, loss NaN, vazamento de dados e tensores no dispositivo errado
- Configurar o TensorBoard para visualizar curvas de loss, histogramas de pesos e distribuições de gradientes
O Problema
Código de IA falha de maneira diferente do código comum. Um app web quebra com um stack trace. Um loop de treinamento mal configurado roda por 8 horas, queima US$ 200 em tempo de GPU e produz um modelo que prediz a média de toda entrada. O código nunca deu erro. O bug era um tensor no dispositivo errado, um .detach() esquecido ou rótulos vazando para dentro das features.
Você precisa de ferramentas de depuração que peguem essas falhas silenciosas antes que elas desperdicem seu tempo e sua computação.
O Conceito
A depuração de IA opera em três níveis:
graph TD
L3["3. Training Dynamics<br/>Loss curves, gradient norms, activations"] --> L2
L2["2. Tensor Operations<br/>Shapes, dtypes, devices, NaN/Inf values"] --> L1
L1["1. Standard Python<br/>Breakpoints, logging, profiling, memory"]
A maioria das pessoas pula direto para o nível 3 (ficar encarando o TensorBoard). Mas 80% dos bugs de IA vivem nos níveis 1 e 2.
Construa
Parte 1: Depuração com Print (Sim, Funciona)
A depuração com print é desprezada. Não deveria ser. Para código de tensores, um print direcionado supera passar por um depurador, porque você precisa ver formatos, dtypes e faixas de valores, tudo de uma vez.
def debug_print(name, tensor):
print(f"{name}: shape={tensor.shape}, dtype={tensor.dtype}, "
f"device={tensor.device}, "
f"min={tensor.min().item():.4f}, max={tensor.max().item():.4f}, "
f"mean={tensor.mean().item():.4f}, "
f"has_nan={tensor.isnan().any().item()}")
Chame isso depois de cada operação suspeita. Quando o bug for encontrado, remova os prints. Simples.
Parte 2: Depurador do Python (pdb e breakpoint)
O depurador embutido é subestimado para trabalho com IA. Coloque breakpoint() no seu loop de treinamento e inspecione tensores interativamente.
def training_step(model, batch, criterion, optimizer):
inputs, labels = batch
outputs = model(inputs)
loss = criterion(outputs, labels)
if loss.item() > 100 or torch.isnan(loss):
breakpoint()
loss.backward()
optimizer.step()
Quando o depurador te levar para dentro, comandos úteis:
p outputs.shapepara verificar formatosp loss.item()para ver o valor do lossp torch.isnan(outputs).sum()para contar NaNsp model.fc1.weight.gradpara verificar gradientescpara continuar,qpara sair
Isso é depuração condicional. Você só para quando algo parece errado. Para uma execução de treinamento de 10.000 passos, isso importa.
Parte 3: Logging em Python
Substitua as instruções de print por logging quando sua depuração for além de uma verificação rápida.
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("training.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
logger.info("Starting training: lr=%.4f, batch_size=%d", lr, batch_size)
logger.warning("Loss spike detected: %.4f at step %d", loss.item(), step)
logger.error("NaN loss at step %d, stopping", step)
O logging te dá timestamps, níveis de severidade e saída em arquivo. Quando uma execução de treinamento falha às 3 da manhã, você quer um arquivo de log, não uma saída de terminal que rolou para fora da tela.
Parte 4: Medindo o Tempo de Trechos de Código
Saber para onde o tempo vai é o primeiro passo para a otimização.
import time
class Timer:
def __init__(self, name=""):
self.name = name
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, *args):
elapsed = time.perf_counter() - self.start
print(f"[{self.name}] {elapsed:.4f}s")
with Timer("data loading"):
batch = next(dataloader_iter)
with Timer("forward pass"):
outputs = model(batch)
with Timer("backward pass"):
loss.backward()
Descoberta comum: o carregamento de dados leva 60% do tempo de treinamento. A correção é num_workers > 0 no seu DataLoader, não uma GPU mais rápida.
Parte 5: cProfile e line_profiler
Quando você precisa de mais do que timers manuais:
python -m cProfile -s cumtime train.py
Isso mostra cada chamada de função ordenada pelo tempo cumulativo. Para profiling linha a linha:
pip install line_profiler
@profile
def train_step(model, data, target):
output = model(data)
loss = F.cross_entropy(output, target)
loss.backward()
return loss
# Run with: kernprof -l -v train.py
Parte 6: Profiling de Memória
Memória de CPU com tracemalloc
import tracemalloc
tracemalloc.start()
# your code here
model = build_model()
data = load_dataset()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
for stat in top_stats[:10]:
print(stat)
Memória de CPU com memory_profiler
pip install memory_profiler
from memory_profiler import profile
@profile
def load_data():
raw = read_csv("data.csv") # watch memory jump here
processed = preprocess(raw) # and here
return processed
Execute com python -m memory_profiler your_script.py para ver o uso de memória linha a linha.
Memória de GPU com PyTorch
import torch
if torch.cuda.is_available():
print(torch.cuda.memory_summary())
print(f"Allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
print(f"Cached: {torch.cuda.memory_reserved() / 1e9:.2f} GB")
Quando você atinge OOM (Out of Memory):
- Reduza o tamanho do batch (primeira coisa a tentar, sempre)
- Use
torch.cuda.empty_cache()para liberar a memória em cache - Use
del tensorseguido detorch.cuda.empty_cache()para grandes intermediários - Use precisão mista (
torch.cuda.amp) para reduzir o uso de memória pela metade - Use gradient checkpointing para modelos muito profundos
Parte 7: Bugs Comuns de IA e Como Pegá-los
Incompatibilidade de Formato (Shape Mismatch)
O bug mais frequente. Um tensor tem formato [batch, features] quando o modelo espera [batch, channels, height, width].
def check_shapes(model, sample_input):
print(f"Input: {sample_input.shape}")
hooks = []
def make_hook(name):
def hook(module, inp, out):
in_shape = inp[0].shape if isinstance(inp, tuple) else inp.shape
out_shape = out.shape if hasattr(out, "shape") else type(out)
print(f" {name}: {in_shape} -> {out_shape}")
return hook
for name, module in model.named_modules():
hooks.append(module.register_forward_hook(make_hook(name)))
with torch.no_grad():
model(sample_input)
for h in hooks:
h.remove()
Rode isso uma vez com um batch de exemplo. Ele mapeia cada transformação de formato no seu modelo.
Loss NaN
Loss NaN significa que algo explodiu. Causas comuns:
- Taxa de aprendizado alta demais
- Divisão por zero em loss customizado
- Log de zero ou de número negativo
- Gradientes explodindo em RNNs
def detect_nan(model, loss, step):
if torch.isnan(loss):
print(f"NaN loss at step {step}")
for name, param in model.named_parameters():
if param.grad is not None:
if torch.isnan(param.grad).any():
print(f" NaN gradient in {name}")
if torch.isinf(param.grad).any():
print(f" Inf gradient in {name}")
return True
return False
Vazamento de Dados (Data Leakage)
Seu modelo atinge 99% de acurácia no conjunto de teste. Parece ótimo. É um bug.
def check_data_leakage(train_set, test_set, id_column="id"):
train_ids = set(train_set[id_column].tolist())
test_ids = set(test_set[id_column].tolist())
overlap = train_ids & test_ids
if overlap:
print(f"DATA LEAKAGE: {len(overlap)} samples in both train and test")
return True
return False
Verifique também o vazamento temporal: usar dados futuros para prever o passado. Ordene por timestamp antes de dividir.
Dispositivo Errado
Tensores em dispositivos diferentes (CPU vs GPU) causam erros em tempo de execução. Mas às vezes um tensor fica silenciosamente na CPU enquanto todo o resto está na GPU, e o treinamento simplesmente roda devagar.
def check_devices(model, *tensors):
model_device = next(model.parameters()).device
print(f"Model device: {model_device}")
for i, t in enumerate(tensors):
if t.device != model_device:
print(f" WARNING: tensor {i} on {t.device}, model on {model_device}")
Parte 8: Fundamentos do TensorBoard
O TensorBoard mostra o que está acontecendo dentro do treinamento ao longo do tempo.
pip install tensorboard
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("runs/experiment_1")
for step in range(num_steps):
loss = train_step(model, batch)
writer.add_scalar("loss/train", loss.item(), step)
writer.add_scalar("lr", optimizer.param_groups[0]["lr"], step)
if step % 100 == 0:
for name, param in model.named_parameters():
writer.add_histogram(f"weights/{name}", param, step)
if param.grad is not None:
writer.add_histogram(f"grads/{name}", param.grad, step)
writer.close()
Inicie-o:
tensorboard --logdir=runs
O que observar:
- Loss não diminui: Taxa de aprendizado baixa demais, ou problema de arquitetura do modelo
- Loss oscilando descontroladamente: Taxa de aprendizado alta demais
- Loss vai para NaN: Instabilidade numérica (veja a seção de NaN acima)
- Loss de treino diminuindo, loss de validação aumentando: Overfitting
- Histogramas de pesos colapsando para zero: Gradientes que somem (vanishing gradients)
- Histogramas de gradientes explodindo: Precisa de clipping de gradientes
Parte 9: Depurador do VS Code
Para depuração interativa, configure o VS Code com um launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Training",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false
}
]
}
Defina breakpoints clicando na margem (gutter). Use o painel Variables para inspecionar as propriedades dos tensores. O Debug Console permite executar expressões Python arbitrárias durante a execução.
Útil para passar por pipelines de pré-processamento de dados onde você quer ver cada transformação.
Use na Prática
Aqui está o fluxo de depuração que pega a maioria dos bugs de IA:
- Antes do treinamento: Rode
check_shapescom um batch de exemplo. Verifique se as dimensões de entrada e saída correspondem às expectativas. - Primeiros 10 passos: Use
debug_printno loss, nas saídas e nos gradientes. Confirme que nada é NaN e que os valores estão em faixas razoáveis. - Durante o treinamento: Registre o loss, a taxa de aprendizado e as normas dos gradientes. Use o TensorBoard para visualização.
- Quando algo quebra: Coloque
breakpoint()no ponto da falha. Inspecione os tensores interativamente. - Para desempenho: Meça o tempo do seu carregamento de dados vs o forward vs o backward pass. Faça profiling de memória se estiver perto de OOM.
Entregue
Execute o script do toolkit de depuração:
python phases/00-setup-and-tooling/12-debugging-and-profiling/code/debug_tools.py
Veja outputs/prompt-debug-ai-code.md para um prompt que ajuda a diagnosticar bugs específicos de IA.
Exercícios
- Rode
debug_tools.pye leia a saída de cada seção. Modifique o modelo dummy para introduzir um NaN (dica: divida por zero no forward pass) e observe o detector pegá-lo. - Faça profiling de um loop de treinamento com
cProfilee identifique a função mais lenta. - Use
tracemallocpara descobrir qual linha do seu pipeline de carregamento de dados aloca mais memória. - Configure o TensorBoard para uma execução simples de treinamento e identifique se o modelo está em overfitting.
- Use
breakpoint()dentro de um loop de treinamento. Pratique inspecionar formatos de tensores, dispositivos e valores de gradientes a partir do prompt do depurador.