Frank de Alcantara
Frank de Alcantara
Pai, marido, professor e engenheiro.
Siga no Twitter

Fused Multiply-Add A Instrução que Dobra sua CPU

Fused Multiply-Add A Instrução que Dobra sua CPU

Se a esforçada leitora olhar a expressão a * b + c vai pensar que se trata de uma expressão banal. Talvez, aos olhos da aritmética básica, até seja. Mas esta banalidade mudou completamente o panorama da computação numérica na última década depois que o hardware passou a executar essa operação, fused multiply-add (FMA), com um único arredondamento ao final. A partir deste ponto, não só obtemos o resultado matematicamente correto (exatamente o que o padrão IEEE 754-2008 exige), como literalmente dobramos o throughput de ponto flutuante da CPU.

Em 2025–2026, ignorar FMA é o mesmo que rodar com meia CPU desligada.

Este artigo explica por que FMA é a instrução mais importante da aritmética de ponto flutuante moderna, como usá-la (ou deixar que o compilador use por você) em Python e C++23, e, principalmente, como o ecossistema MLIR em 2025–2026 transforma essa operação simples no coração absoluto da performance em GPUs, TPUs e nas novas unidades matriciais (AMX/SME).

Por Que FMA É Matematicamente Superior

Em aritmética de ponto flutuante tradicional, a operação a * b + c é realizada em duas etapas distintas, introduzindo dois pontos de injeção de erro:

  1. temp = a * b $\rightarrow$ arredonda para o formato (perde precisão).
  2. result = temp + c $\rightarrow$ arredonda novamente.

Formalmente, isso é representado como:

\[RN(RN(a \times b) + c)\]

Na qual $RN$ é a operação de Round to Nearest. Dois arredondamentos implicam um erro total de arredondamento que pode chegar a quase 1 ulp (0.5 + 0.5), enquanto o FMA garante um erro de no máximo 0.5 ulp (unit in the last place) em casos de cancelamento.

Com FMA, a operação é atômica:

  • A multiplicação $a \times b$ é realizada com precisão interna infinita (ou estendida o suficiente pelo hardware).
  • A adição com $c$ ocorre sobre esse produto exato.
  • Ocorre apenas um arredondamento no final.

Matematicamente:

\[RN(a \times b + c)\]

O resultado garante um erro máximo de 0.5 ulp. Isso é tão importante que o padrão IEEE 754-2008 tornou o FMA obrigatório para conformidade total. Em algoritmos sensíveis, como o Kahan Summation, produtos escalares longos ou resolução de sistemas lineares mal condicionados, o FMA previne o cancelamento catastrófico que destruiria a precisão em operações separadas.

O Salto Brutal de Desempenho nas CPUs (2013–2025)

A evolução do hardware foi ditada pela capacidade de executar FMAs.

  1. A Era Vetorial (AVX2/AVX-512): Uma instrução FMA AVX-512 realiza 32 operações de ponto flutuante (16 multiplicações + 16 adições) por ciclo em 512 bits. Antes do FMA, o pico teórico era metade disso.
  2. A Era Matricial (2025–2026 - AMX e SME): Em 2025–2026, com Intel Granite Rapids (AMX - Advanced Matrix Extensions) e ARM (SME - Scalable Matrix Extension), o FMA deixou de ser apenas vetorial para se tornar matricial. O hardware agora despacha “tiles” inteiros de operações FMA.

Em código denso (GEMM, DNNs, CFD), processadores modernos como o Apple M4 Max ou AMD EPYC Turin atingem 95–98% do pico teórico apenas porque suas unidades de execução são saturadas com instruções FMA. Quem não usa FMA em 2025–2026 está desperdiçando, no mínimo, 50% da capacidade de computação do silício.

Python em 2025–2026 – FMA Grátis (Via Bibliotecas)

Em Python, a estratégia é delegar. Bibliotecas como NumPy, PyTorch e JAX ligam-se a backends (OpenBLAS, MKL, oneDNN) que utilizam instruções FMA agressivamente.

import numpy as np

# O backend (MKL/Accelerate) detecta o padrão e usa instruções vetoriais **FMA**
# ou matriciais (AMX) dependendo do hardware.
def benchmark_fma():
    N = 10_000_000
    a = np.random.rand(N).astype(np.float32)
    b = np.random.rand(N).astype(np.float32)
    
    # Dot product é o caso clássico de uso de **FMA**
    # Resultado computado com acumulação de alta precisão
    return np.dot(a, b) 

O PyTorch 2.5+ com torch.compile(mode="max-autotune") utiliza o Torch-Inductor (baseado em MLIR) para gerar kernels que fundem operações de multiplicação e adição em instruções FMA nativas, seja em CPU (AVX-512) ou GPU (FFMA/HFMA).

Sob o Capô: Arquiteturas de **FMA**

Para entender o impacto no desempenho, é preciso distinguir como CPUs e GPUs implementam esta operação no silício:

1. CPU: AVX-512 (Vetorial) No ecossistema x86 (Intel/AMD), o AVX-512 utiliza registradores ZMM de 512 bits de largura. Como um número de ponto flutuante de precisão simples (float) ocupa 32 bits, o processador consegue empacotar 16 números lado a lado ($512 \div 32 = 16$).

  • Quando uma instrução FMA é executada, ela realiza 16 multiplicações e 16 adições simultaneamente.
  • Resultado: 32 operações de ponto flutuante (FLOPs) por ciclo de clock, por unidade de execução.

2. GPU: FFMA vs. HFMA (Massivamente Paralelo) No universo CUDA (NVIDIA) e ROCm (AMD), a distinção ocorre pela precisão dos dados, impactando diretamente a densidade de cálculo:

  • FFMA (Float **FMA):** A instrução padrão para precisão simples (fp32). É a base para gráficos 3D e simulações científicas tradicionais.
  • HFMA (Half **FMA):** A instrução para precisão média (fp16). Como os dados ocupam metade dos bits (16 vs 32), o hardware pode duplicar o número de unidades de execução ou executar o dobro de operações no mesmo espaço de silício.

Nota: As modernas Tensor Cores ou Matrix Cores são, essencialmente, arranjos densos especializados em executar HFMA matricial, sacrificando flexibilidade para atingir trilhões de operações por segundo (TOPS) em cargas de Inteligência Artificial.

C++23 – Tipos Novos e Controle Total: Instrução vs. Contração

Em C++, embora a função std::fma exista desde o C++11, o padrão C++23 trouxe uma atualização vital para o hardware moderno: os tipos de ponto flutuante de precisão estendida e reduzida no cabeçalho <stdfloat>. Isso é fundamental porque as unidades matriciais modernas (AMX, Tensor Cores) frequentemente operam FMA sobre tipos como bfloat16 (std::bfloat16_t) ou fp16 (std::float16_t), e não apenas float ou double.

É necessário distinguir entre forçar a instrução pela biblioteca padrão e permitir a otimização pelo compilador:

  1. std::fma (Instrução Explícita): Força o comportamento de um único arredondamento. É indispensável para precisão numérica estrita, mas pode inibir otimizações de pipeline se o compilador não conseguir vetorizar a chamada intrínseca eficientemente.
  2. Contração de FP (Otimização): Permite que o compilador funda a * b + c em uma instrução FMA quando seguro, ou quando flags agressivas são usadas.
#include <cmath>
#include <vector>
#include <stdfloat> // Novidade do C++23: std::float16_t, std::bfloat16_t
                    // C++23 (GCC 14+, Clang 18+)

// 1. Abordagem Explícita (Foco em Precisão Numérica)
// Garante RN(a*b + c). Útil para Math Libraries que exigem conformidade estrita.
void compute_precise(const std::vector<float>& a, const std::vector<float>& b, std::vector<float>& c) {
    for(size_t i = 0; i < a.size(); ++i) {
        c[i] = std::fma(a[i], b[i], c[i]); 
    }
}

// 2. Abordagem de Performance (HPC & AI em 2025–2026)
// Use flags: -O3 -march=native -ffp-contract=fast
// O compilador (GCC 15/Clang 19) detecta o padrão e emite:
// - vfmadd231ps (AVX/AVX-512) para float
// - tdpbf16ps (AMX) se os tipos fossem std::bfloat16_t
float dot_product_fast(const float* a, const float* b, size_t n) {
    float s = 0.0f;
    for (size_t i = 0; i < n; ++i) {
        s += a[i] * b[i]; // O compilador funde e vetoriza agressivamente
    }
    return s;
}

Em 2025–2026, para High Performance Computing (HPC), a recomendação é utilizar a contração automática (-ffp-contract=fast) para permitir que o autovectorizador do LLVM ou GCC utilize todo o registro vetorial ou as unidades matriciais. Use std::fma apenas quando a precisão do último bit for mais valiosa que o throughput bruto.

MLIR – A Mágica do Compilador Modular (2025–2026)

No ecossistema moderno (Mojo, IREE, XLA, Triton), o FMA é tratado como uma operação de primeira classe dentro da representação intermediária (IR). O MLIR permite elevar o nível de abstração, desacoplando a matemática da arquitetura.

O pipeline de compilação típico para um acelerador de IA segue o fluxo: linalg (álgebra) $\rightarrow$ vector (tiling) $\rightarrow$ llvm/ptx (hardware).

É no dialeto vector que a fusão se torna explícita antes de tocar o assembly:

// Exemplo: Dialeto Vector do MLIR
// Representa a contração que será mapeada para o hardware (FMA/AMX)
func.func @dot_product_fma(%a: vector<16xf32>, %b: vector<16xf32>, %c: f32) -> f32 {
  
  // vector.contract é a representação canônica de um **FMA** n-dimensional
  %result = vector.contract {
      indexing_maps = [
        affine_map<(d0) -> (d0)>,   // a
        affine_map<(d0) -> (d0)>,   // b  
        affine_map<(d0) -> ()>      // c (acumulador)
      ],
      iterator_types = ["reduction"],
      kind = #vector.kind<add>
    } %a, %b, %c : vector<16xf32>, vector<16xf32>, f32 into f32

  return %result : f32
}

Compiladores como o Triton usam essa infraestrutura para mapear operações de matriz diretamente para Tensor Cores em GPUs NVIDIA (instruções mma.sync) ou unidades AMX em CPUs Intel, sem que o programador precise escrever assembly ou intrínsecos complexos.

Veredito – Recomendações para em 2025–2026

Se você escreve código numérico hoje:

  1. Protótipo: Python/JAX/Torch.compile (FMA + aceleração automática).
  2. Performance Crítica em CPU: C++ com -march=native -ffp-contract=fast.
  3. Hardware Exótico/Custom: Use stacks baseadas em MLIR.

FMA não é mais uma otimização. É a unidade atômica sobre a qual a infraestrutura de IA e simulação física em 2025–2026 foi construída.