Precisão mista

Visão geral

Precisão mista (mixed precision) é uma técnica de treinamento em que você usa mais de um formato de ponto flutuante (floating-point) durante o passe direto (forward pass), o passe reverso (backward pass) e a etapa de otimização (optimization step) de um modelo. O objetivo típico é:

  • Aumentar o throughput (throughput) (mais operações por segundo via hardware especializado como NVIDIA Tensor Cores ou unidades de matriz (matrix units) de TPUs)
  • Reduzir o uso de memória (ativações/gradientes menores; tamanhos de lote maiores)
  • Manter a acurácia próxima ao treinamento completo em FP32

No aprendizado profundo (deep learning) moderno, “precisão mista” geralmente significa treinar com FP16 (meia precisão IEEE) ou BF16 (bfloat16) enquanto mantém alguns valores (frequentemente atualizações de pesos e certas reduções) em FP32 para estabilidade numérica.

Este artigo foca em treinamento FP16/BF16, escalonamento da perda (loss scaling) e trade-offs de desempenho, com orientações práticas para execuções reais de treinamento—especialmente no cenário de “treinamento em escala (training at scale)”.

Tópicos de base relacionados: Retropropagação, Descida do Gradiente, Treinamento Distribuído, e Paralelismo de Dados/Modelo/Pipeline.

Formatos de ponto flutuante usados no aprendizado profundo

O treinamento de redes neurais é basicamente a aplicação repetida de álgebra linear densa (multiplicações de matrizes (matmuls)/convoluções (convolutions)) mais operações elemento a elemento (elementwise ops) e reduções (reductions). O formato numérico afeta tanto a acurácia quanto o desempenho.

FP32 (precisão simples)

  • 1 bit de sinal, 8 bits de expoente, 23 bits de mantissa
  • Boa precisão e faixa para treinamento
  • Formato “seguro” de referência, mas mais lento e mais exigente em memória do que formatos de 16 bits

FP16 (meia precisão IEEE)

  • 1 bit de sinal, 5 bits de expoente, 10 bits de mantissa
  • Prós: alta velocidade em hardware compatível, 2× menor que FP32
  • Contras: faixa dinâmica (dynamic range) muito menor (é fácil ter overflow/underflow), o que pode quebrar o treinamento se não for tratado com cuidado

FP16 é o formato em que o escalonamento da perda é mais comumente necessário.

BF16 (bfloat16)

  • 1 bit de sinal, 8 bits de expoente, 7 bits de mantissa
  • Prós: faixa dinâmica semelhante à do FP32 (mesma largura de expoente), risco muito menor de overflow/underflow do que FP16
  • Contras: menos precisão do que FP16 na mantissa (menos bits fracionários), mas em geral “bom o suficiente” para aprendizado profundo

Como o BF16 mantém uma faixa semelhante à do FP32, o escalonamento da perda muitas vezes é desnecessário.

Uma nota sobre “TensorFloat-32” (TF32)

Em GPUs NVIDIA Ampere e posteriores, muitas multiplicações de matrizes “FP32” podem rodar internamente em TF32 (mantissa de 10 bits, expoente de 8 bits) por velocidade. TF32 não é o mesmo que precisão mista, mas faz parte do mesmo cenário de desempenho/acurácia: você pode obter acelerações sem trocar seus tensores para FP16/BF16. A precisão mista geralmente vai além ao também reduzir memória e comunicação.

O que o “treinamento em precisão mista” realmente faz

Uma receita comum e eficaz é assim:

  1. Armazenar os pesos do modelo em FP32 (“pesos mestre (master weights)”).
  2. Durante o passe direto/passe reverso:
    • Converter muitas ativações/pesos para FP16 ou BF16
    • Executar matmuls/convoluções em 16 bits para usar caminhos de hardware rápidos
  3. Acumular certos valores em FP32, especialmente:
    • Reduções de gradiente (somas)
    • Algumas estatísticas de normalização (ex.: LayerNorm)
    • Estado do otimizador (optimizer) (momentos do Adam (Adam moments)) tipicamente em FP32
  4. Aplicar a atualização do otimizador em FP32 nos pesos mestre.
  5. Opcionalmente manter uma “cópia do modelo” em FP16/BF16 para o passe direto.

Isso é “misto” porque nem tudo é 16 bits—apenas as partes que mais se beneficiam e que toleram precisão reduzida.

Na prática, frameworks implementam isso com conversão automática de tipo (autocasting) e kernels fundidos (fused kernels), então você não precisa converter manualmente cada tensor.

Por que funciona: fontes de erro e onde o FP32 importa

A estabilidade do treinamento é sensível a:

  • Underflow: valores ficam tão pequenos que são arredondados para zero (comum em gradientes FP16)
  • Overflow: valores ficam grandes demais e viram inf/nan (a faixa do FP16 é limitada)
  • Cancelamento catastrófico (catastrophic cancellation): subtrair números semelhantes perde precisão (comum em reduções/estatísticas)
  • Erro de acumulação (accumulation error): somar muitos valores pequenos em baixa precisão pode desviar significativamente

A precisão mista tenta:

  • Usar 16 bits onde as operações são “bem condicionadas” e aceleradas por hardware (matmul/conv)
  • Manter 32 bits onde o condicionamento numérico é pior (reduções, normalização, matemática do otimizador)

Escalonamento da perda (principalmente para FP16)

O problema: underflow de gradientes em FP16

Durante a Retropropagação, as magnitudes dos gradientes podem ser muito pequenas—especialmente em redes profundas, camadas de atenção, ou com certas perdas. Em FP16, valores pequenos frequentemente sofrem underflow para zero, fazendo os gradientes sumirem e o treinamento estagnar ou divergir.

A solução: escalar a perda para escalar os gradientes

Se você multiplicar a perda por uma constante ( S ), então, pela regra da cadeia (chain rule), todos os gradientes também são multiplicados por ( S ):

  • Calcule: ( L' = S \cdot L )
  • A retropropagação produz: ( \nabla_\theta L' = S \cdot \nabla_\theta L )
  • Antes da etapa do otimizador, divida os gradientes por ( S ) para restaurar a magnitude correta.

Isso empurra os gradientes para a faixa representável do FP16 durante a retropropagação, reduzindo underflow.

Escalonamento estático vs dinâmico da perda

Escalonamento estático da perda (static loss scaling)

  • Escolha uma escala constante (ex.: 1024, 8192)
  • Simples, mas frágil: grande demais → overflow; pequena demais → underflow continua

Escalonamento dinâmico da perda (dynamic loss scaling)

  • Comece com uma escala (ex.: 2^16)
  • Se os gradientes tiverem overflow (inf/nan), pule a etapa do otimizador e reduza a escala
  • Se tudo ficar estável por algumas etapas, aumente a escala

O escalonamento dinâmico é o padrão em muitas implementações de AMP porque se adapta a diferentes fases do treinamento.

BF16 tipicamente não precisa de escalonamento da perda

A faixa de expoente do BF16 é semelhante à do FP32, então underflow/overflow por limitações de faixa é muito mais raro. Você ainda pode ver instabilidade por outras causas (otimização, arquitetura), mas o escalonamento da perda geralmente não é necessário para BF16.

Exemplo prático: precisão mista no PyTorch (FP16)

Abaixo está o padrão canônico usando Precisão Mista Automática (Automatic Mixed Precision, AMP):

import torch
from torch.cuda.amp import autocast, GradScaler

model = model.cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)

scaler = GradScaler()  # dynamic loss scaling

for batch in loader:
    optimizer.zero_grad(set_to_none=True)

    x, y = batch[0].cuda(), batch[1].cuda()

    with autocast(dtype=torch.float16):
        pred = model(x)
        loss = torch.nn.functional.cross_entropy(pred, y)

    scaler.scale(loss).backward()

    # Optional: unscale before clipping so clipping uses true grad magnitudes
    scaler.unscale_(optimizer)
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

    scaler.step(optimizer)   # skips step if overflow detected
    scaler.update()

Pontos-chave:

  • autocast escolhe FP16 para operações elegíveis (como matmul/conv) e mantém outras em FP32.
  • GradScaler lida com escalonamento da perda e detecção de overflow.
  • Se você fizer clipping de gradientes, chame unscale_() primeiro para recortar corretamente.

Exemplo prático: precisão mista no PyTorch (BF16)

Em GPUs compatíveis (tipicamente Ampere+), você pode usar BF16:

import torch
from torch.cuda.amp import autocast

model = model.cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)

for batch in loader:
    optimizer.zero_grad(set_to_none=True)

    x, y = batch[0].cuda(), batch[1].cuda()

    with autocast(dtype=torch.bfloat16):
        pred = model(x)
        loss = torch.nn.functional.cross_entropy(pred, y)

    loss.backward()
    optimizer.step()

Frequentemente:

  • Nenhum GradScaler é necessário.
  • Você ainda se beneficia de matmuls mais rápidas e menor pressão sobre largura de banda de memória.

O que tipicamente permanece em FP32 (e por quê)

Mesmo com AMP, você frequentemente vai querer estes itens computados/acumulados em FP32:

  • Reduções (soma/média sobre muitos elementos), porque o erro de acumulação é pior em 16 bits
  • Softmax + entropia cruzada (ou padrões de log-sum-exp), porque exponenciais amplificam problemas numéricos
  • Camadas de normalização (BatchNorm/LayerNorm/RMSNorm): estatísticas e cálculos de variância
  • Matemática do otimizador, especialmente momentos do Adam/AdamW (exp_avg, exp_avg_sq), que são sensíveis à precisão

A maioria dos frameworks já lida com muitos desses casos via “listas de operações” do autocast (ops que rodam em FP16/BF16 vs FP32). Você ainda pode sobrescrever se necessário, mas normalmente é melhor seguir os padrões do framework a menos que você esteja depurando.

Trade-offs de desempenho e quando a precisão mista mais ajuda

A precisão mista não é apenas um recurso de “ligar um botão e ficar mais rápido”; os ganhos dependem do que limita seu treinamento.

Benefícios

  1. Maior throughput via hardware especializado
    • Matmuls em FP16/BF16 frequentemente rodam em Tensor Cores / unidades de matriz com FLOPs muito mais altos do que FP32.
  2. Menor footprint de memória
    • Ativações e gradientes frequentemente dominam o uso de memória. Usar 16 bits pode permitir:
      • tamanhos de lote maiores
      • sequências mais longas (importante para Arquitetura Transformer)
      • modelos maiores por dispositivo
  3. Comunicação mais rápida em treinamento distribuído
    • Em Treinamento Distribuído, a largura de banda de all-reduce de gradientes pode ser um limitador.
    • Comunicar gradientes FP16/BF16 pode reduzir o tráfego em ~2× (embora muitos sistemas já comprimam/quantizem de outras formas).

Custos e armadilhas

  1. Instabilidade numérica
    • FP16 pode sofrer overflow/underflow sem escalonamento da perda.
    • Alguns modelos/hiperparâmetros são mais sensíveis (ex.: redes muito profundas, taxas de aprendizado grandes).
  2. Overhead de conversões e gerenciamento de tipos
    • O autocast introduz conversões; tipicamente é pequeno comparado a matmuls, mas pode importar em modelos muito pequenos.
  3. Complexidade de depuração
    • Overflows, etapas puladas (escalonamento dinâmico da perda) e incompatibilidades de dtype podem ser confusos.
    • O não determinismo pode ficar mais perceptível; veja Reprodutibilidade.
  4. Estado do otimizador ainda é pesado
    • Se você usar AdamW, o otimizador mantém tensores de momentos em FP32, que podem dominar a memória em modelos muito grandes.
    • A precisão mista ajuda bastante, mas não elimina magicamente a memória do otimizador (você pode precisar de sharding do otimizador ou otimizadores alternativos—coberto em outros tópicos de treinamento em escala).

Regra prática: limitado por computação vs limitado por largura de banda

  • Se o treinamento é limitado por computação (compute-bound) (matmuls grandes, alta intensidade aritmética), FP16/BF16 pode trazer grandes acelerações.
  • Se o treinamento é limitado por memória/largura de banda (memory/bandwidth-bound) (operações pequenas, muitos kernels elemento a elemento, limites do pipeline de entrada), os ganhos podem ser menores. Veja Carregamento de Dados e Pipelines de Entrada.

Considerações de acurácia: meu modelo vai igualar FP32?

A precisão mista geralmente atinge a mesma qualidade final que FP32 para muitas arquiteturas (CNNs, Transformers, LLMs), mas há cenários em que diferenças aparecem:

  • Tamanhos de lote extremamente pequenos ou gradientes ruidosos
  • Taxas de aprendizado muito altas ou configurações instáveis do otimizador
  • Tarefas que exigem alta fidelidade numérica (alguns cenários científicos/de regressão)
  • Modelos com normalização sensível ou ops customizadas que não são seguras para AMP

Orientação prática:

  • Comece com BF16 se seu hardware suportar; ele tende a ser mais estável.
  • Se usar FP16, use escalonamento dinâmico da perda (comportamento padrão do AMP).
  • Compare curvas de validação cedo; se a precisão mista divergir, tente:
    • reduzir a taxa de aprendizado
    • clipping de gradientes
    • manter operações específicas em FP32 (ex.: softmax, normalização)
    • BF16 em vez de FP16

Interação com otimizadores e truques de treinamento

Adam/AdamW e pesos mestre em FP32

A maioria das implementações mantém:

  • pesos: FP16/BF16 para o passe direto, mais uma cópia mestre em FP32
  • gradientes: FP16/BF16 (às vezes), acumulados/reduzidos em FP32
  • momentos do Adam: FP32

Isso é uma grande parte do motivo pelo qual a precisão mista permanece estável: atualizações do otimizador acontecem em FP32 mesmo se o passe direto/passe reverso usou 16 bits.

Acumulação de gradientes

Com acumulação de gradientes (múltiplos microbatches antes de uma etapa do otimizador), você deve ter cuidado com:

  • Acumular gradientes em FP16 (pode perder pequenas atualizações)
  • Comportamento do autocast ao longo dos microbatches
  • Escalonamento da perda ao longo das etapas de acumulação

Muitos sistemas acumulam internamente em FP32 ou recomendam BF16 para um comportamento de acumulação mais suave.

Clipping de gradientes

Se estiver usando escalonamento da perda (FP16), faça clipping depois de desfazer a escala (unscaling); caso contrário, os limiares de clipping são efetivamente multiplicados pela escala.

Precisão mista em treinamento distribuído e paralelo

Em cenários de grande escala, a precisão mista afeta tanto computação quanto comunicação:

  • Paralelismo de dados (data parallelism): comunicar gradientes FP16/BF16 reduz a carga de largura de banda, potencialmente melhorando o escalonamento.
  • Paralelismo de modelo/pipeline (model/pipeline parallelism): a memória de ativações e tamanhos de transferência caem com ativações em 16 bits, o que pode reduzir bolhas de pipeline e pressão na interconexão. Veja Paralelismo de Dados/Modelo/Pipeline.
  • Operações coletivas e reduções: muitos sistemas fazem redução em FP32 mesmo se gradientes estiverem armazenados em FP16/BF16 para melhorar a acurácia numérica.

Ao fazer checkpoint (checkpointing) (veja Checkpointing e Tolerância a Falhas), em geral você quer salvar:

  • pesos mestre em FP32 (por fidelidade)
  • estado do otimizador (frequentemente FP32)
  • estado do scaler do AMP (se estiver usando escalonamento dinâmico da perda), para que retomar não desestabilize o treinamento

Modos de falha comuns e dicas de depuração

Sintomas: a perda vira NaN/Inf

Causas prováveis:

  • Overflow em FP16 devido a escala de perda grande demais ou treinamento instável
  • Taxa de aprendizado ou inicialização ruim
  • Uma operação CUDA customizada que não lida corretamente com FP16/BF16

Ações:

  • Se FP16: garanta que o escalonamento dinâmico da perda esteja habilitado; observe se há etapas puladas com frequência
  • Tente BF16
  • Reduza a taxa de aprendizado, habilite clipping de gradientes
  • Desabilite temporariamente o AMP para isolar o problema
  • Force ops sensíveis para FP32 (ex.: softmax/logits, normalização)

Sintomas: o treinamento é estável, mas mais lento do que o esperado

Causas prováveis:

  • O modelo não é pesado em matmul (muitos kernels pequenos)
  • Gargalo no pipeline de entrada
  • Lançamentos de kernel/conversões dominam

Ações:

  • Faça profiling para identificar gargalos
  • Aumente o tamanho do lote (se a memória permitir)
  • Garanta que você está usando kernels de atenção fundida / MLP fundida quando disponíveis (comum em stacks de Transformers)
  • Verifique a vazão do dataloader; veja Carregamento de Dados e Pipelines de Entrada

Sintomas: a acurácia final é ligeiramente pior do que em FP32

Ações:

  • Use BF16
  • Mantenha mais ops em FP32 (normalização, logits, reduções)
  • Verifique se a avaliação roda em FP32 para uma comparação justa
  • Garanta que não há truncamento de dtype não intencional no pré-processamento ou no cálculo da perda

Quando escolher FP16 vs BF16

Escolha BF16 quando:

  • Seu hardware o suporta com eficiência (TPUs; NVIDIA A100/H100 e mais novas; muitos aceleradores modernos)
  • Você quer menos problemas numéricos e complexidade mínima de escalonamento da perda
  • Você está treinando Transformers/LLMs grandes onde a faixa (range) importa

Escolha FP16 quando:

  • BF16 não está disponível ou não tem bom desempenho no seu hardware
  • Você depende de caminhos maduros de Tensor Core em FP16 e das ferramentas de AMP
  • Você está confortável em usar escalonamento da perda (normalmente automático)

Na prática por volta de 2025, BF16 está se tornando cada vez mais o padrão para treinamento de modelos grandes quando disponível, com FP16 ainda amplamente usado e eficaz—especialmente em GPUs mais antigas e em alguns pipelines de inferência.

Resumo

O treinamento em precisão mista usa FP16 ou BF16 para as partes críticas em desempenho do treinamento, enquanto retém FP32 onde a estabilidade numérica importa (pesos mestre, reduções, estado do otimizador). A principal diferença prática entre os dois formatos de 16 bits é:

  • FP16: mais rápido e comum, mas frequentemente requer escalonamento da perda para evitar underflow/overflow de gradientes.
  • BF16: faixa semelhante à do FP32, geralmente sem escalonamento da perda, frequentemente mais robusto para modelos grandes.

Em cenários de treinamento em escala, a precisão mista pode melhorar não apenas a velocidade bruta de computação, mas também o footprint de memória e a eficiência de comunicação distribuída—frequentemente tornando-se um habilitador-chave para treinar modelos modernos grandes de forma confiável e com boa relação custo-benefício.