
O treino que acordou lento — a herança escondida dentro do checkpoint
A retomada que parecia perfeita
A rodada anterior da Fase 4 tinha morrido — a história do crash silencioso e da memória de GPU órfã já tá contada aqui no blog. Reiniciei a máquina virtual, limpei o fantasma do driver e relancei o treino do último checkpoint bom.
E aí veio aquela sensação boa de retomada sã: os steps voltaram a passar, o gradiente dentro da faixa, o loss descendo devagar mas descendo. Nenhum traceback. Nenhum CUDA out of memory. O processo vivo, telemetria fluindo. Deixei rolando e fui cuidar da vida.
Foi só quando abri a telemetria com calma que o detalhe apareceu. O número da taxa de aprendizado que o treino estava usando era ~5e-7. O valor que eu tinha configurado na linha de comando era 1e-4 — três ordens de grandeza acima. O treino não tinha falhado em nada. Ele só tinha acordado aprendendo mil vezes mais devagar do que deveria.
A metáfora mais honesta: era como dar fisioterapia a alguém que precisa reaprender a andar, e descobrir que o instrutor está corrigindo os exercícios pela ficha de um paciente outro — a ficha de alguém que já estava no fim do tratamento.
A aritmética do plano
O que de fato aconteceu, reconstruído depois:
| Camada | Valor que chega ao treino |
|---|---|
CLI (--lr 1e-4) |
1e-4 |
| Scheduler fresco | baseia os patamares no CLI — correto |
scheduler.load_state_dict(ckpt) |
baseia os patamares no checkpoint — 1e-6 |
| LR efetivo no step seguinte | ~5e-7 |
O scheduler é um plano de redução do LR ao longo dos steps: divisões por 10 em marcos definidos. O CLI declara o teto inicial; o checkpoint guarda uma cópia do plano da época em que foi salvo. Se o checkpoint é de uma run que começou com LR seis ordens menor, restaurar o estado dele sobrescreve o teto da sessão atual — e nenhuma mensagem avisa. Todo pipeline de treino que guarda o scheduler no checkpoint convive com esse risco; resta saber se vai descobrir isso lendo telemetria ou lendo código.
O conserto de duas linhas
Depois de restaurar o estado, reimpor os patamares declarados na sessão atual:
if tstate.get("scheduler") is not None and scheduler is not None:
try:
scheduler.load_state_dict(tstate["scheduler"])
scheduler.base_lrs = [args.lr for _ in scheduler.base_lrs] # fix LR~0: ckpt carregava base_lrs de run antigo (1e-6)
print(f" base_lrs reforcados para CLI: {args.lr:.2e}")
except Exception as e:
print(f" scheduler.load_state_dict falhou ({e}); usando scheduler fresco")
Duas linhas: a reimposição e um print de confirmação. O resto do bloco já existia — só ninguém tinha percebido que o load_state_dict derrubava o valor logo abaixo do try. E uma decisão de projeto embutida ali: a sessão atual é quem manda no LR. Um checkpoint antigo carrega o progresso dos steps, não a política de LR de uma época que já passou. Se um dia eu quiser retomar com outro valor de CLI, o pipeline obedece o CLI.
O que a telemetria quase não mostrou
A parte que me deixa sem jeito: se eu tivesse olhado só o loss, teria demorado semanas. Loss com LR três ordens menor desce mais devagar — e na fase em que a rodada estava, a curva já era lenta por natureza. Uma lentidão dentro de uma curva lenta é quase indistinguível do normal a olho nu.
O que denunciou foi justamente a métrica operacional: o LR impresso a cada step. Ele era o único número que podia estar “errado” sem afetar imediatamente qualquer outro — e por isso era o único que contava a verdade completa: você pediu 1e-4 e está rodando 5e-7.
Isso fecha um padrão com o crash anterior. Lá, o processo morreu e a memória ficou; a lição foi que log parado não é morte. Aqui, o processo viveu e o LR encolheu; a lição é que treino vivo não é treino saudável. Os dois juntos desenham a regra que eu levo pra qualquer pipeline de treino daqui pra frente: o watchdog vigia a vida do processo; a telemetria de parâmetros vigia a sanidade. São dois sistemas, e um não substitui o outro.
Quanto custou não ter visto antes
Dá pra estimar. A rodada tinha sido retomada no fim de semana; o fix entrou na segunda à tarde. Todas as steps nesse intervalo avançaram com o LR do checkpoint antigo — talvez um dia inteiro e pouco de GPU rodando a um milésimo da velocidade de aprendizado pretendida. A conta honesta: um dia de eletricidade e VRAM produzindo progresso que umas poucas horas teriam produzido.
O alívio é que nada foi perdido de verdade: o checkpoint guardava os pesos de onde a run antiga parou, e o progresso real da otimização foi preservado. O que ficou de lição é que a “retomada perfeita” tinha um defeito silencioso — e defeito silencioso é o único tipo que não dispara alerta.
O que fica
- Checkpoint carrega progresso, não política. O que é da sessão atual — LR, limites, decisões de configuração — deve ser reafirmado a cada retomada.
- Toda carga de estado é uma superfície de override. Restaurar
state_dictde qualquer componente (scheduler, optimizer, AMP) traz consigo os valores de outra época. - A métrica da configuração é parte da telemetria. Imprimir o LR efetivo a cada step não é verbosidade — é a única forma de ver divergência entre o pedido e o entregue.
- Custo de retomada com defeito silencioso é tempo diluído. Nada quebra; tudo demora mil vezes mais.
O próximo marco da Fase 4 continua sendo atravessar a zona onde a rodada anterior morreu — com o scheduler agora obedecendo a sessão, e o watchdog duplo cobrindo tanto morte quanto lentidão.