Ver como markdown

Desempenho do treinamento

Três alavancas mudam a velocidade de um passo de treinamento: a precisão mista, a captura do forward e do backward da rede em grafos CUDA, e o que o profiler disser que está de fato segurando o passo.

Meça antes de mudar qualquer coisa

As três alavancas abaixo resolvem problemas diferentes, e aplicar a errada não muda nada. O profiler diz qual problema você tem.

Perfilar e continuar treinando
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt") # Perfila uma janela curta de passos reais, imprime um veredicto e# depois continua a execução com os hooks removidos.model.train(data="my-dataset.yaml", epochs=100, profile=True)
Só medir e parar
# Define no_aug_epochs=0 e roda só as épocas necessárias para encher a janela.libreyolo profile run coco128 --weights LibreYOLO9s.pt --size s
Detalhar o resultado
libreyolo profile summary runs/profile/prof/profile.jsonlibreyolo profile phases runs/profile/prof/profile.jsonlibreyolo profile kernels runs/profile/prof/profile.json --top 10

profile=True mede uma janela de passos reais de treinamento, cinco descartados e depois vinte medidos por padrão, imprime um relatório, grava seus artefatos e então segue treinando com os hooks removidos. Não custa nada quando está desligado, e é ignorado em treinamento distribuído.

O relatório termina em um de quatro veredictos:

VeredictoSignificadoAlavancas
dataloadera GPU espera pelos dados de entradamais workers, cache="ram" ou "disk", augmentation mais leve, batch maior
host / launcha GPU é alimentada devagar demais, muitos kernels minúsculosbatch maior, grafos CUDA, menos sincronizações com o host por passo
computea GPU está saturadaAMP ou bfloat16, ou aceitar
memory-pressurethrash do alocador, VRAM no limitereduza o batch; os números de utilização aqui não são confiáveis

O número de utilização é o tempo em que os kernels ficam ocupados dividido pelo tempo do passo sem sincronização. A janela é dividida de propósito: a primeira metade roda sem sincronização extra, para que o veredicto reflita a sobreposição real, e só a segunda metade cerca cada fase com um sync para atribuir o tempo de GPU. Sincronizar cada fase dá folga aos workers do dataloader e esconde a starvation, então os números de composição nunca são usados para escolher o veredicto.

Quatro arquivos aparecem no diretório da execução: timeline.html, que abre sozinho no navegador, profile_trace.json para o Perfetto ou o Nsight, profile_summary.json e profile.json, o autocontido, feito para copiar por aí e devolver aos subcomandos libreyolo profile.

Vale saber duas coisas sobre o profile run. Ele define no_aug_epochs=0, porque o profiler mede a época 0 e uma execução curta com o no_aug_epochs padrão perfilaria o dataloader mais leve, sem augmentation, em vez do que o treinamento de fato usa. E --repeat N reporta média e desvio padrão, o que importa porque um passo limitado por lançamento é ruidoso o bastante para que uma única execução engane; ele grava diretórios por tentativa, prof_1, prof_2 e assim por diante, além de um profile_repeat.json agregado.

Precisão mista

amp=True é o padrão para a maioria das famílias e roda o forward sob o autocast do CUDA. amp_dtype escolhe entre float16 e bfloat16.

Python
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")model.train(data="my-dataset.yaml", amp=True, amp_dtype="bfloat16")
CLI
libreyolo train model=LibreYOLO9s.pt data=my-dataset.yaml \  amp_dtype=bfloat16

O float16 precisa de escalonamento dinâmico da loss e recebe um gradient scaler ativo; a faixa de expoente mais larga do bfloat16 não precisa, então o scaler dele fica desativado. Quatro famílias vêm com amp=False, D-FINE, DEIM, YOLO-NAS e FOMO, e a configuração do DEIM se propaga para o RT-DETRv4 por herança. O D-FINE explica o motivo: seu decoder limita as ativações em 65504, o maior valor finito de float16.

A semântica dos argumentos, incluindo o que um pedido de bfloat16 faz em hardware sem suporte a bfloat16, está em Hiperparâmetros.

Grafos CUDA

cuda_graph=True captura o forward e o backward de treinamento da rede em um grafo CUDA, eliminando o overhead de lançamento de kernels a cada passo.

Python
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")model.train(data="my-dataset.yaml", epochs=100, cuda_graph=True)
CLI
libreyolo train model=LibreYOLO9s.pt data=my-dataset.yaml \  epochs=100 cuda_graph=true

A flag é sempre segura de passar. Uma família, tarefa ou configuração que não pode ser capturada registra uma linha de log e treina em eager, sem mudanças.

Só a rede é capturada. A loss fica em eager por design, porque as losses de detecção selecionam com máscaras booleanas, rodam matching húngaro e ramificam conforme o resultado da atribuição, e um grafo não consegue gravar nada disso. O passo do otimizador, o clipping de gradiente, a atualização do EMA e o schedule de learning rate também ficam em eager.

Isso limita o ganho à fatia do passo que é rede, e essa fatia varia muito. Medido em uma RTX 5070 Ti a 640 px, batch 8: 84 por cento de um passo do YOLOv9-t é rede, 44 por cento de um passo do YOLOv7-b, 31 por cento de um passo do YOLOX-t e 26 por cento de um passo do RTMDet-t. Os dois últimos passam a maior parte de um passo dentro de seus label assigners, então capturar a rede é o que menos os ajuda.

Quanto isso vale

Condições de todos os números abaixo: RTX 5070 Ti, Windows, AMP, um processo por braço a partir de um estado salvo compartilhado, reproduzindo um batch real para tirar o dataloader do caminho, o passo mais rápido de 24 depois do warmup. Detecção a 640 px, classificação a 224 px. O tamanho de batch é por linha.

FamíliaTamanhoBatchEagerCom grafoSpeedup
FOMOs167.0 ms1.9 ms3.63x
MobileNetV4s1614.5 ms5.3 ms2.74x
EfficientNetV2b01629.0 ms11.9 ms2.44x
YOLOv9t893.6 ms47.0 ms1.99x
NAFNets8132.5 ms105.5 ms1.26x
PicoDets8145.0 ms118.7 ms1.22x
D-FINEn4185.3 ms159.2 ms1.16x
RF-DETRn4276.3 ms239.8 ms1.15x
YOLOXt8102.2 ms90.5 ms1.13x
RTMDett8149.7 ms136.2 ms1.10x
YOLOv7b4102.5 ms98.0 ms1.05x

Esses números isolam o passo de GPU. Um fine-tuning completo também paga pelo dataloader e pela validação. YOLOv9-t em um conjunto de detecção de 406 imagens, 20 épocas, batch 8, 640 px, 4 workers de dataloader, na mesma máquina: 428.4 s de wall clock em eager contra 367.7 s com grafo, um ganho de 1.16x, com mAP50-95 de 0.6394 nos dois braços.

Três coisas mexem nesses números. Batches pequenos são limitados por lançamento e os grandes são limitados por computação, então o RT-DETR-r18 ganha 1.19x com batch 2 e 1.04x com batch 8. O overhead de lançamento é maior no Windows, e os ganhos no Linux ficam entre um terço e a metade dos valores da tabela. E uma execução limitada pelo dataloader não vê mudança nenhuma no wall clock, e é por isso que o profiler vem primeiro.

A captura entra em ação do mesmo jeito com amp=False, mas os kernels fp32 rodam por mais tempo, então o passo fica menos limitado por lançamento e a maioria das famílias ganha menos. No mesmo hardware, o MobileNetV4-s com batch 16 vai de 2.74x sob AMP para 3.61x em fp32, enquanto o YOLOv9-t com batch 8 vai de 1.99x para 1.69x e o RT-DETR-r18 com batch 4 vai de 1.12x para 0.99x.

Onde a captura se aplica

TarefaFamílias
detectyolo9, yolo9_p2, yolo9_e2e, yolox, yolo7, yolonas, picodet, rtmdet, rfdetr, dfine, deim, deimv2, rtdetr, rtdetrv2, rtdetrv4, ec
classifyresnet, convnext, mobilenetv4, efficientnetv2
semanticsegformer, lingbotvision
pointfomo
restorenafnet

Todo o resto cai para eager com uma linha de log: outras tarefas nessas famílias, famílias que não estão na lista, execuções distribuídas e execuções de destilação. Uma falha de captura em runtime também derruba o resto da execução para eager, em vez de quebrar.

Nos detectores encoder-decoder, D-FINE, DEIM, DEIMv2, RT-DETR v1, v2 e v4, e EC, só o backbone e o encoder são capturados. O decoder deles lê o ground truth para montar as queries de contrastive denoising, e o número dessas queries acompanha a maior contagem de ground truth do batch, então sua contagem de tokens muda de batch para batch.

Shapes

Um grafo é válido exatamente para o shape de entrada com que foi capturado. O trainer conta os shapes de batch e captura assim que um shape se repete três vezes. Batches em qualquer outro shape rodam em eager: os batches multi-escala e o último batch parcial de uma época.

Essa é a armadilha das famílias DETR, que redimensionam todo batch por padrão. Com multi_scale=True, uma execução curta pode nunca ver um shape com frequência suficiente para chegar a capturar. Passe multi_scale=False quando o objetivo for o speedup.

O YOLOX muda o que a região capturada calcula no meio de uma execução, ligando seu ramo de regressão L1 quando o mosaico se encerra em no_aug_epochs. O trainer invalida a captura nesse ponto e recaptura assim que o novo shape se estabiliza.

Numérica e memória

A maioria das famílias reproduz bit a bit a trajetória da loss em eager sob AMP. FOMO e LingBot-Vision diferem no último bit do float32 por causa de uma ordem de soma diferente. Os detectores de deformable attention, D-FINE, DEIM, DEIMv2, RT-DETR, RF-DETR e EC, também não reproduzem suas próprias execuções em eager, porque esse backward acumula com atômicos e as convoluções TF32 escolhem uma ordem de redução a cada lançamento; a execução com grafo fica dentro dessa dispersão. O RTMDet difere em cerca de 3e-4 relativo em dois de 139 gradientes, porque compartilha convoluções da cabeça entre os níveis da pirâmide e os dois caminhos de backward somam três contribuições em ordem diferente. O SegFormer tem stochastic depth dentro da região capturada, então um grafo reproduzido puxa seu próprio fluxo aleatório e é estatisticamente equivalente ao eager, não idêntico; o gerenciador registra isso uma vez no momento da captura.

Com amp=False, resultado bit a bit idêntico não está disponível para nada neste hardware, com ou sem captura. Duas execuções eager idênticas do YOLOv9-t com a mesma semente divergem 36 por cento relativo ao longo de 20 passos, e o YOLOX-t, 2.6 por cento, porque o cuDNN escolhe um algoritmo não determinístico de gradiente de pesos para alguns shapes de convolução fp32.

Um grafo capturado fixa buffers estáticos de entrada, saída e workspace, então o pico de VRAM sobe mais ou menos um conjunto extra de ativações. Nas famílias acima, a alocação de pico variou entre -5 e +19 por cento. O custo relativo é maior nos modelos pequenos de classificação, cujas ativações já são pequenas de início: o ResNet-18 a 224 px, batch 16, foi de 0.48 GB em eager para 0.57 GB com grafo. Se isso empurrar uma execução além do limite, reduza o batch ou deixe a flag desligada.

Relacionados

  • Hiperparâmetros para batch, nbs, cache e workers.
  • Treinamento multi-GPU, onde nem os grafos CUDA nem o profiler estão disponíveis.
  • Grafos CUDA para a matriz de suporte combinada de inferência e treinamento, os pontos de divisão da captura e o contrato numérico.

Verificado com o LibreYOLO v1.5.0.