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:
temp = a * b$\rightarrow$ arredonda para o formato (perde precisão).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.
- 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.
- 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:
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.- Contração de FP (Otimização): Permite que o compilador funda
a * b + cem 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:
- Protótipo: Python/JAX/Torch.compile (FMA + aceleração automática).
- Performance Crítica em CPU: C++ com
-march=native -ffp-contract=fast. - 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.