Reprodutibilidade
O que “Reprodutibilidade” significa em Aprendizado Profundo
Em aprendizado de máquina (machine learning), reprodutibilidade (reproducibility) é a capacidade de reexecutar um experimento e obter os mesmos resultados (ou resultados equivalentes de forma significativa). Em aprendizado profundo (deep learning) em escala, a reprodutibilidade é importante para:
- Depuração (debugging): isolar se uma mudança na acurácia veio do código, dos dados ou da aleatoriedade.
- Validade científica: sustentar comparações e ablações (ablations) críveis.
- Confiabilidade de engenharia: garantir que pipelines de treinamento se comportem de forma consistente entre máquinas e ao longo do tempo.
- Governança e conformidade: conseguir explicar como um modelo foi produzido.
Na prática, a reprodutibilidade vem em níveis, que vale a pena distinguir:
- Reprodutibilidade bit a bit (bitwise reproducibility) (exata): toda execução produz pesos, perdas e métricas idênticos. É a mais estrita e, muitas vezes, a mais difícil, especialmente em GPUs e em sistemas distribuídos.
- Reprodutibilidade estatística (statistical reproducibility): os resultados batem dentro da variação esperada (por exemplo, média/variância ao longo de sementes) mesmo que execuções individuais difiram.
- Reprodutibilidade qualitativa (qualitative reproducibility): as conclusões gerais se mantêm (por exemplo, método A supera o método B), mas os números exatos podem variar.
A maioria dos esforços de treinamento em larga escala busca reprodutibilidade estatística mais a capacidade de produzir reprodutibilidade bit a bit dentro de um ambiente fixo ao depurar.
Este artigo foca em sementes (seeds), limites de determinismo, e práticas práticas para tornar experimentos reproduzíveis, especialmente em cenários de treinamento em escala.
Por que o treinamento não é determinístico por padrão
Mesmo que você “defina a semente”, sistemas de aprendizado profundo frequentemente permanecem não determinísticos. Fontes comuns incluem:
Geração de números aleatórios (random number generation, RNG) em todo lugar
- Inicialização de parâmetros
- Embaralhamento de dados
- Aumento de dados (data augmentation)
- Dropout e regularização estocástica
- Amostragem (por exemplo, na avaliação ou decodificação)
- Alguns otimizadores e camadas com comportamento estocástico
Cada um desses pode usar diferentes RNGs (Python, NumPy, RNG do framework, RNG do CUDA).
O paralelismo muda a matemática
O treinamento de aprendizado profundo é dominado por operações paralelas de ponto flutuante. A soma em ponto flutuante não é associativa:
[ (a + b) + c \neq a + (b + c) ]
Quando as operações rodam em paralelo, a ordem das reduções (somas) pode mudar levemente entre execuções, levando a pequenas diferenças numéricas que podem se amplificar ao longo de um treinamento longo.
Isso fica mais visível em:
- Kernels de GPU que usam operações atômicas
- Operações de CPU multithread (OpenMP/MKL)
- Reduções distribuídas como all-reduce em Treinamento Distribuído
Kernels “determinísticos” nem sempre estão disponíveis (ou são rápidos)
Algumas operações têm implementações determinísticas em GPU; outras não têm, ou a versão determinística é mais lenta ou usa mais memória. Frameworks podem escolher kernels rápidos e não determinísticos, a menos que sejam instruídos explicitamente do contrário.
Variação de hardware e software
Diferenças em:
- Modelo de GPU e versão do driver
- Versões de CUDA/cuDNN
- Bibliotecas BLAS (MKL/OpenBLAS)
- Flags do compilador, conjuntos de instruções (AVX2 vs AVX512)
- Escalonamento de threads da CPU
podem mudar o comportamento numérico.
Precisão mista (mixed precision) aumenta a sensibilidade
Com FP16/BF16 e escalonamento de perda (loss scaling), perturbações minúsculas podem mudar comportamento de arredondamento e overflow. Veja Precisão Mista para detalhes sobre por que escolhas de precisão orientadas a desempenho podem afetar a repetibilidade.
Sementes: o que elas controlam (e o que não controlam)
Uma semente inicializa um estado de RNG. Se você reproduzir exatamente a mesma sequência de chamadas ao RNG na mesma ordem, você obterá os mesmos números aleatórios.
Na prática, a reprodutibilidade exige:
- Definir todas as sementes relevantes
- Garantir que aconteça a mesma ordem de chamadas (ordem dos dados, escalonamento paralelo)
- Garantir implementações determinísticas das operações
As sementes ajudam mais com:
- Inicialização e dropout
- Embaralhamento de dados em processo único
- Pipelines de aumento de dados determinísticos
As sementes ajudam menos quando:
- O carregamento de dados usa múltiplos workers com escalonamento não determinístico
- Kernels de GPU são não determinísticos
- Reduções distribuídas mudam a ordem entre execuções
Boa prática: trate a semente como parte da definição do experimento
Uma execução de treinamento normalmente é identificada por:
- versão do código + configuração + versão do conjunto de dados + semente
Se você registrar apenas “seed=42”, mas não o estado exato do código e dos dados, você não conseguirá reproduzir.
Determinismo: o que é necessário (e por que é difícil)
Uma execução de treinamento é determinística se:
- Todas as fontes de RNG começam em estados conhecidos
- A execução é funcionalmente idêntica (mesmas ramificações, mesmas formas)
- Todas as operações são determinísticas dados inputs idênticos
- O ambiente (versões, hardware) não introduz diferenças
Em treinamento em larga escala, (3) e (4) são os mais difíceis.
Operações não determinísticas comuns
Dependendo do framework e das versões, o não determinismo aparece com frequência em:
- Algumas passagens de retropropagação (backward passes) de convolução e pooling (seleção de algoritmo do cuDNN)
- Scatter/gather com atualizações atômicas
- Algumas reduções e reduções segmentadas
- Certas operações de incorporação (embedding)
- Alguns kernels fundidos (fused)
Frameworks frequentemente documentam quais ops são não determinísticas e oferecem um “modo determinístico (deterministic mode)” que pode lançar um erro se uma op não determinística for usada.
Treinamento distribuído torna o determinismo exato mais difícil
Em Treinamento Distribuído, gradientes são combinados usando operações coletivas (por exemplo, all-reduce). Mesmo que cada worker seja determinístico, a redução global pode variar devido a:
- Árvores de redução diferentes / ordem de anel (ring) diferente
- Sobreposição de comunicação e computação
- Não associatividade de ponto flutuante
Às vezes, você consegue repetibilidade dentro de uma topologia de cluster e pilha de software fixas, mas reprodutibilidade bit a bit entre clusters é significativamente mais difícil.
Definição prática de sementes (exemplo em PyTorch)
Abaixo está uma linha de base prática para PyTorch. Ela não resolverá todo o não determinismo, mas é um bom ponto de partida.
import os
import random
import numpy as np
import torch
def seed_everything(seed: int):
# Python
random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
# NumPy
np.random.seed(seed)
# PyTorch (CPU)
torch.manual_seed(seed)
# PyTorch (CUDA)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
seed_everything(42)
Forçando algoritmos determinísticos no PyTorch
import torch
torch.use_deterministic_algorithms(True) # may raise errors for nondeterministic ops
torch.backends.cudnn.benchmark = False # avoid autotuner selecting different algorithms
torch.backends.cudnn.deterministic = True
Observações:
cudnn.benchmark=Truepode mudar a seleção de algoritmo de convolução com base em tamanhos de entrada e heurísticas de runtime, o que pode introduzir não determinismo e variação entre execuções.- Habilitar algoritmos determinísticos pode reduzir o desempenho e pode exigir trocar camadas/ops.
Determinismo do DataLoader (PyTorch)
Pipelines de entrada com múltiplos workers são uma armadilha comum de reprodutibilidade. Um bom padrão é definir uma semente determinística para cada worker:
from torch.utils.data import DataLoader
def seed_worker(worker_id):
# Each worker gets a distinct, reproducible seed derived from the initial seed
worker_seed = torch.initial_seed() % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
g = torch.Generator()
g.manual_seed(42)
loader = DataLoader(
dataset,
batch_size=64,
shuffle=True,
num_workers=4,
worker_init_fn=seed_worker,
generator=g,
persistent_workers=True,
)
Para mais sobre como embaralhamento, pré-busca (prefetching), cache (caching) e o comportamento de workers afetam reprodutibilidade e desempenho, veja Carregamento de Dados e Pipelines de Entrada.
Notas sobre JAX e TensorFlow (alto nível)
Ecossistemas diferentes têm controles diferentes de reprodutibilidade:
JAX
- JAX usa chaves PRNG (PRNG keys) explícitas (uma grande vantagem para reprodutibilidade).
- Você precisa gerenciar a divisão de chaves de forma consistente para reproduzir operações estocásticas.
- A reprodutibilidade exata ainda pode ser afetada por diferenças de compilação XLA (XLA compilation), tipo de dispositivo e reduções paralelas.
Padrão típico:
import jax
import jax.random as jr
key = jr.PRNGKey(42)
key, subkey = jr.split(key)
# use subkey for dropout, augmentation, etc.
TensorFlow
- TensorFlow fornece sementes globais e em nível de operação.
- O determinismo pode exigir habilitar ops determinísticas e controlar o comportamento do cuDNN.
- Estratégias distribuídas podem introduzir diferenças na ordem de redução similares às do PyTorch.
Como os controles exatos variam por versão e plataforma, trate as configurações de determinismo do framework como parte do seu ambiente fixado (pinned environment).
Práticas de experimento reproduzível (o que registrar e congelar)
Reprodutibilidade é tanto uma disciplina de engenharia quanto uma propriedade numérica da matemática.
1) Fixe e registre o ambiente
No mínimo, registre:
- Sistema operacional e kernel
- Modelo(s) de GPU, versão do driver
- Versões de CUDA/cuDNN (se aplicável)
- Versão do framework (PyTorch/JAX/TF)
- Versão do Python
- Versões de bibliotecas-chave: NumPy, NCCL, BLAS/MKL, bibliotecas de tokenizador, etc.
Abordagem prática:
- Use
requirements.txtoupoetry.lock/uv.lock - Prefira contêineres (containers) para jobs de treinamento (Docker/Singularity) para congelar dependências de sistema
- Registre
pip freeze(ou equivalente) como um artefato (artifact)
2) Versione seu código *e* sua configuração
Registre:
- Hash do commit do Git (Git commit hash) (e se a árvore de trabalho estava “suja”)
- Configuração completa de treinamento (hiperparâmetros, arquitetura, caminhos de dados, aumento de dados, etc.)
Um padrão forte é “configuração como fonte da verdade (config-as-source-of-truth)”:
- Uma execução é lançada a partir de um único arquivo de configuração (YAML/JSON/Hydra)
- A configuração é salva junto com os artefatos da execução
3) Versione seus dados e pré-processamento
Problemas de dados são uma das causas mais comuns de irreprodutibilidade.
Registre:
- Identificador de versão do conjunto de dados (ID de snapshot, hash de manifesto, ou commit)
- Versão do código de pré-processamento
- Definições de split de treino/val/teste (idealmente como listas explícitas de IDs)
Se o conjunto de dados for grande, registre:
- Um arquivo de manifesto (manifest) estável (por exemplo, lista de caminhos de arquivos + checksums)
- O esquema de particionamento (sharding scheme) usado para treinamento distribuído
4) Torne a aleatoriedade explícita e centralizada
Em vez de deixar a aleatoriedade ocorrer implicitamente pelo código:
- Gere sementes em um único lugar
- Propague-as para componentes que precisam delas (aumentos de dados, dropout, amostragem)
- Registre o plano completo de sementes (semente global, sementes por worker, sementes por rank)
Em jobs distribuídos, garanta que cada processo/rank receba uma semente determinística, frequentemente baseada em:
global_seed + rank(simples)- ou um hash de (global_seed, rank, worker_id) (mais robusto)
5) Salve mais do que apenas os pesos do modelo: capture o estado completo de treinamento
Para reproduzir continuações (retomar o treinamento exatamente), um checkpoint (checkpoint) deve incluir:
- Parâmetros do modelo
- Estado do otimizador (por exemplo, momentos do Adam)
- Estado do agendador de taxa de aprendizado
- Estado do escalonador de gradientes (para precisão mista)
- Estados de RNG:
- Python
random - RNG do NumPy
- Estados de RNG do framework em CPU e CUDA
- Python
- Estado do amostrador de dados / posição do dataloader (quando possível)
Isso se sobrepõe fortemente a Checkpointing e Tolerância a Falhas: retomar após preempção é um problema de reprodutibilidade tanto quanto é um problema de confiabilidade.
6) Avaliação e relatório determinísticos
Mesmo que o treinamento seja estocástico, a avaliação geralmente deve ser determinística:
- Desabilite dropout
- Fixe parâmetros de decodificação (tamanho do feixe, temperatura)
- Use sementes fixas de avaliação para métricas estocásticas ou avaliação baseada em amostragem
- Registre o código exato de computação de métricas e sua versão
Também reporte resultados de forma estatisticamente responsável:
- Média ± desvio padrão ao longo de múltiplas sementes
- Intervalos de confiança quando apropriado
- Indique quando melhorias estão dentro do ruído
Determinismo vs desempenho: escolhendo o trade-off certo
Forçar determinismo frequentemente custa vazão (throughput):
- Kernels determinísticos podem ser mais lentos
- Algumas operações fundidas rápidas podem ser proibidas
- Reduzir não determinismo de paralelismo pode reduzir a utilização da GPU
Uma abordagem pragmática:
- Modo de depuração: determinismo total quando possível
- algoritmos determinísticos habilitados
- sementes fixas
- número fixo de workers
- sem ops não determinísticas (ou falhar rapidamente)
- Modo de treinamento de produção: reprodutibilidade estatística
- sementes fixas e registradas
- ambiente fixado
- permitir kernels não determinísticos por velocidade
- confiar em avaliação multi-semente para confiança
Para sistemas em escala, o mais importante geralmente é conclusões reproduzíveis, não pesos bit a bit idênticos.
Sutilezas e armadilhas comuns
A ordem dos dados muda apesar de sementes fixas
Causas:
- Número diferente de workers do dataloader
- Diferenças de pré-busca
- Limites de shard mudam quando o número de GPUs muda
- Ordem não determinística de listagem de arquivos em armazenamento remoto
Mitigações:
- Use manifestos explícitos
- Garanta ordenação estável de listas de arquivos
- Fixe a lógica de sharding e registre-a
- Registre o tamanho de batch global e o tamanho do mundo de paralelismo de dados (data-parallel world size)
“Mesma semente”, mas resultados diferentes após retomar
Se você retomar a partir de um checkpoint, mas não restaurar estados de RNG e estados de amostrador, o restante do treinamento irá divergir.
Se a continuação exata importa, garanta que o checkpoint restaure:
- Estados de RNG
- Estado do dataloader/amostrador
- Contagens de passos do agendador
Sensibilidade da precisão mista
Em Precisão Mista, pequenas diferenças numéricas podem empurrar valores através de limiares de arredondamento ou limites de overflow. Duas execuções podem divergir mais rápido do que em fp32 completo.
Mitigações:
- Fixe a estratégia de escalonamento de perda (estático vs dinâmico)
- Registre o estado do escalonador
- Considere fp32 completo para comparações de depuração
Não determinismo vindo de fontes “inofensivas”
- Tempo de logging afetando a ordem de execução assíncrona na GPU
- Contagens diferentes de threads (MKL/OMP) afetando pré-processamento na CPU
- Dropout ficando ativo inadvertidamente durante a avaliação
- Versões diferentes de bibliotecas de tokenização mudando IDs de entrada (impacto enorme)
Pegadinhas de treinamento distribuído
- Mapeamento diferente de rank-para-dispositivo muda a ordem de redução e a deriva numérica
- Passos de acumulação de gradiente mudam a estrutura efetiva de redução
- Sobreposição de comunicação/computação muda o escalonamento
Se você precisa de comparabilidade estrita, mantenha a topologia distribuída e a pilha de software fixas e registre:
- world size, mapeamento de ranks, versão do NCCL, detalhes do tecido de rede (quando relevante)
Exemplo: um template de execução de treinamento reproduzível
Abaixo está um esqueleto simples mostrando o que registrar e como estruturar uma execução.
import json, os, subprocess, time
import torch, numpy as np, random
def get_git_info():
try:
commit = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
status = subprocess.check_output(["git", "status", "--porcelain"]).decode().strip()
dirty = len(status) > 0
return {"commit": commit, "dirty": dirty, "status": status}
except Exception:
return {"commit": None, "dirty": None}
def seed_everything(seed):
os.environ["PYTHONHASHSEED"] = str(seed)
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
def capture_rng_state():
return {
"python_random": random.getstate(),
"numpy_random": np.random.get_state(),
"torch_cpu": torch.get_rng_state().tolist(),
"torch_cuda": [s.tolist() for s in torch.cuda.get_rng_state_all()],
}
def main(config):
seed_everything(config["seed"])
run_dir = config["run_dir"]
os.makedirs(run_dir, exist_ok=True)
meta = {
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
"git": get_git_info(),
"config": config,
"torch": torch.__version__,
"cuda": torch.version.cuda,
"cudnn": torch.backends.cudnn.version(),
}
with open(os.path.join(run_dir, "meta.json"), "w") as f:
json.dump(meta, f, indent=2)
# ... build dataset/model/optimizer ...
# Save an initial checkpoint including RNG state
ckpt = {
"model": model.state_dict(),
"optimizer": optimizer.state_dict(),
"rng_state": capture_rng_state(),
"step": 0,
}
torch.save(ckpt, os.path.join(run_dir, "ckpt_step0.pt"))
Em um sistema mais completo, você também:
- armazenaria estado do amostrador/dataloader
- armazenaria estados do agendador/escalonador
- escreveria checkpoints e métricas periodicamente
Checklist recomendado de reprodutibilidade (treinamento em escala)
Use isto como uma linha de base prática.
Obrigatório para experimentos críveis
- Registre a semente global e quaisquer sementes derivadas (rank/worker)
- Salve a configuração completa junto com a execução
- Registre a versão do código (commit do git) e as versões do ambiente
- Versione conjuntos de dados (manifesto/snapshot + pré-processamento)
- Execute múltiplas sementes para comparações; reporte média/desvio padrão
- Avaliação determinística (sem dropout; decodificação fixa)
Fortemente recomendado para reexecuções robustas
- Containerize ou, de outra forma, congele dependências do sistema
- Garanta ordenação estável de arquivos e sharding determinístico
- Salve checkpoints com estados de otimizador, agendador, escalonador e RNG
- Adicione um toggle de “modo determinístico de depuração” (falhar rapidamente em ops não determinísticas)
Necessário apenas para determinismo bit a bit (quando viável)
- Habilite algoritmos determinísticos no framework
- Desabilite kernels não determinísticos e autotuning
- Fixe o modelo de hardware e as versões de driver/toolkit
- Fixe a topologia distribuída e as configurações de comunicação
- Evite operações conhecidas por serem não determinísticas na sua pilha
Como a reprodutibilidade se conecta a outros tópicos de treinamento em escala
A reprodutibilidade não é isolada — ela fica na interseção de várias preocupações de sistemas:
- Carregamento de Dados e Pipelines de Entrada: embaralhamento, carregamento multi-worker, cache e ordenação de E/S não determinística.
- Treinamento Distribuído: operações coletivas, ordem de redução, mapeamento de ranks e deriva numérica dependente de topologia.
- Precisão Mista: sensibilidade a arredondamento/overflow e estado do escalonador.
- Checkpointing e Tolerância a Falhas: salvar/restaurar o estado de treinamento completo, incluindo RNG e posição do amostrador.
Resumo
Reprodutibilidade em aprendizado profundo é um espectro — de reexecuções exatas a resultados estatisticamente consistentes. Sementes são necessárias, mas não suficientes: reprodutibilidade real também exige controlar a execução determinística, registrar versões de ambiente e dados, e salvar o estado completo de treinamento.
Em treinamento em escala, o objetivo mais prático geralmente é:
- avaliação determinística
- execuções bem instrumentadas
- relato estatístico multi-semente
- determinismo em modo de depuração quando necessário
Essa combinação produz resultados nos quais você pode confiar, comparar e reproduzir — sem sacrificar os benefícios de desempenho de sistemas modernos de treinamento.