Memória e Largura de Banda

Por que “memória & largura de banda” importa em IA

Em cargas de trabalho modernas de IA, computação bruta (FLOPs) muitas vezes não é o fator limitante. Em vez disso, o tempo de execução e o custo são dominados por movimentar dados:

  • Da HBM/DRAM para os núcleos do acelerador
  • Entre caches da GPU e memória compartilhada
  • Através de interconexões GPU–GPU (NVLink) ou redes nó–nó (InfiniBand/Ethernet)
  • Do armazenamento para a memória do host durante pipelines de entrada e checkpointing

É por isso que dois modelos com contagens de FLOPs semelhantes podem ter throughput muito diferente, e por que “mais TFLOPs” não significa automaticamente treinamento/inferência mais rápidos.

Um modelo mental útil é:

Computação é barata; movimentação de dados é cara.
O desempenho muitas vezes é limitado por largura de banda (bytes/segundo), não por computação (FLOPs/segundo).

Este artigo explica a teoria por trás de gargalos de memória e como raciocinar sobre eles em sistemas reais de aprendizado de máquina (machine learning).

A hierarquia de memória (e por que você continua perdendo o “pico”)

Aceleradores de IA são construídos em torno de uma hierarquia de memórias, cada uma com diferentes capacidade, largura de banda e latência:

  • Registradores (minúsculos, mais rápidos)
  • Caches on-chip / SRAM (L1/L2, memória compartilhada; rápida, porém limitada)
  • HBM (Memória de Alta Largura de Banda, High Bandwidth Memory) em GPUs/TPUs (relativamente grande, alta largura de banda)
  • DRAM do host (maior, mais lenta da perspectiva da GPU)
  • Rede / interconexão entre dispositivos (Interconexões (NVLink/InfiniBand))
  • Armazenamento (object store, SSDs) (Armazenamento)

Duas consequências importantes:

  1. A maioria dos tensores não cabe on-chip, então kernels buscam repetidamente na HBM.
  2. A largura de banda da HBM é finita, e muitas operações de aprendizado de máquina são “streaming”: leem e escrevem grandes arrays com pouca reutilização.

Mesmo que uma GPU anuncie TFLOPs de pico enormes, você só obtém esses TFLOPs se as unidades matemáticas permanecerem alimentadas com dados na taxa necessária.

Largura de banda vs latência: não confunda

Ambas importam, mas prejudicam de maneiras diferentes:

  • Latência é o tempo para começar a obter dados (por exemplo, pointer chasing, acessos aleatórios pequenos).
  • Largura de banda é a taxa que você consegue sustentar ao fazer streaming (GB/s).

Aprendizado profundo tende a pressionar largura de banda, porque muitas operações acessam memória em grandes blocos contíguos. No entanto, algumas partes (buscas de embeddings, indexação irregular, inferência com batch pequeno) podem se tornar dominadas por latência.

Um modo comum de falha em discussões de desempenho é otimizar computação (por exemplo, usar uma GEMM mais rápida) quando a carga de trabalho está, na verdade, gargalada por transferências de memória ou sincronização.

O modelo central: bytes, FLOPs e intensidade operacional

Para raciocinar sobre gargalos, reduza uma operação a duas quantidades:

  • Trabalho: quantas operações de ponto flutuante (FLOPs)
  • Tráfego: quantos bytes movidos de/para memória “lenta” (frequentemente HBM)

Então defina:

  • Intensidade operacional (IO) (operational intensity, OI) = FLOPs / Bytes
    (também chamada de intensidade aritmética)

Isso se conecta ao modelo Roofline (Roofline model), um limite de desempenho simples, porém poderoso:

  • Throughput de computação de pico: P_peak (FLOPs/s)
  • Largura de banda de memória de pico: B_peak (Bytes/s)

Então o melhor desempenho possível alcançado é aproximadamente:

  • P_achievable ≤ min(P_peak, B_peak * OI)

Interpretação:

  • Se B_peak * OI < P_peak, você está limitado por memória
  • Se B_peak * OI ≥ P_peak, você está limitado por computação

Um exemplo rápido: por que ops elemento a elemento são limitadas por largura de banda

Considere uma ativação elemento a elemento y = gelu(x) em N elementos fp16.

  • FLOPs: digamos ~10 FLOPs/elemento (ordem de grandeza)
  • Bytes: ler x (2 bytes) + escrever y (2 bytes) = ~4 bytes/elemento (ignorando caches)

Então:

  • OI ≈ 10 / 4 = 2.5 FLOPs/byte

Em uma GPU com (ilustrativo) B_peak = 1.5 TB/s, a taxa de computação limitada por largura de banda é:

  • B_peak * OI ≈ 1.5e12 bytes/s * 2.5 FLOPs/byte = 3.75e12 FLOPs/s = 3.75 TFLOPs/s

Se o pico de computação de tensor fp16 da GPU for, digamos, 200+ TFLOPs/s, você nunca vai chegar perto nesse kernel — não porque a matemática seja difícil, mas porque você não consegue mover bytes rápido o suficiente.

É por isso que compiladores e bibliotecas de aprendizado profundo tentam agressivamente fundir ops elemento a elemento: você quer ler/escrever cada tensor uma vez, não repetidamente.

Lendo o gargalo em camadas reais de aprendizado de máquina

Multiplicação de matrizes (GEMM): frequentemente limitada por computação (se grande o bastante)

Para C = A * B com tamanhos (M×K) * (K×N):

  • FLOPs ≈ 2*M*N*K
  • Bytes (grosseiramente): ler A + ler B + escrever C
    2*(M*K + K*N + M*N) bytes para fp16 (2 bytes/elem), ignorando reutilização

Para matrizes grandes, GEMM tem alta reutilização: cada elemento de A e B é usado muitas vezes. Isso eleva a IO, muitas vezes o suficiente para se tornar limitada por computação, por isso aceleradores focam tanto em GEMM rápida via núcleos tensoriais e arrays sistólicos (ver Fundamentos de Hardware).

Mas “GEMM é limitada por computação” tem ressalvas importantes:

  • Tamanhos de batch pequenos (especialmente inferência) reduzem tamanhos das matrizes → menos reutilização → menor IO
  • Tiling ruim ou conversões de layout não fundidas podem adicionar tráfego de memória
  • Atenção e MoE introduzem padrões que não são GEMM pura

LayerNorm / RMSNorm: um gargalo clássico de largura de banda

Camadas de normalização tipicamente fazem:

  1. Ler um vetor
  2. Calcular média/variância (redução)
  3. Ler novamente para normalizar (a menos que seja cuidadosamente fundido)
  4. Escrever a saída

Elas são dominadas por tráfego de memória e reduções. Muitas vezes se tornam hotspots em blocos de transformers, especialmente com batch pequeno.

Kernels modernos fundem etapas agressivamente e usam memória on-chip para reduzir passagens extras — mas a operação continua com IO relativamente baixa comparada a GEMM.

Atenção: não é “apenas FLOPs”, frequentemente limitada por largura de banda

Atenção ingênua calcula softmax(QKᵀ)V. Além das GEMMs, ela materializa grandes matrizes intermediárias (os scores de atenção). Esses intermediários custam largura de banda enorme.

É por isso que algoritmos como FlashAttention importam: eles reestruturam o cálculo para evitar escrever grandes intermediários na HBM, aumentando a IO efetiva ao melhorar a reutilização de dados on-chip.

Se você está aprendendo desempenho de transformers, combine este tema com Arquitetura Transformer: a matemática é simples, mas o comportamento de memória é onde a engenharia vive.

Embeddings e buscas esparsas: podem ser limitadas por latência

Buscas de embeddings podem envolver acesso quase aleatório a tabelas grandes:

  • Acesso menos contíguo → pior utilização de cache
  • Prefetching menos previsível
  • Potencialmente dominadas por latência, especialmente com batch pequeno

Esta é uma razão pela qual modelos de recomendação e alguns sistemas com recuperação aumentada (retrieval-augmented) têm perfis de desempenho bem diferentes de transformers densos.

“Movimentação de memória domina” também no treinamento multi-GPU

Quando você distribui o treinamento, introduz uma nova classe de tráfego:

  • All-reduce de gradientes (treinamento com paralelismo de dados)
  • All-gather / reduce-scatter (otimizadores sharded)
  • Ativações em paralelismo de pipeline
  • Coletivas de paralelismo tensorial

Essas transferências competem com computação e com a largura de banda da HBM e podem gargalar o escalonamento. O roofline efetivo agora tem múltiplos tetos de largura de banda:

  • Teto de largura de banda da HBM (no dispositivo)
  • Teto de largura de banda do NVLink (intra-nó)
  • Teto de InfiniBand/Ethernet (inter-nó)

Veja Interconexões (NVLink/InfiniBand) para como topologia e largura de banda do link restringem a eficiência de escalonamento.

Um jeito prático de raciocinar sobre gargalos

Passo 1: Estime se uma op é limitada por memória

Calcule uma IO aproximada. Para um kernel que:

  • lê X bytes
  • escreve Y bytes
  • faz F FLOPs

Então OI = F / (X+Y).

Se você conhece especificações de pico aproximadas (ou números sustentados medidos):

  • Compare B_peak * OI com P_peak.

Isso não vai prever o runtime exato, mas vai dizer em que direção otimizar.

Passo 2: Procure passagens extras de memória (impostos ocultos de largura de banda)

Impostos comuns de largura de banda em stacks de aprendizado de máquina:

  • Cadeias elemento a elemento não fundidas (bias + gelu + dropout + residual)
  • Conversões de layout (por exemplo, NHWC↔NCHW, transposes)
  • Materialização de intermediários (matrizes de scores de atenção, máscaras)
  • Casts para lá e para cá (fp16↔fp32) quando não necessário
  • Salvamentos excessivos de ativações (treinamento) que forçam leituras/escritas extras

Compiladores e runtimes (XLA, TorchInductor, TensorRT) focam fortemente em reduzir esses impostos. Veja Compiladores & Runtimes.

Passo 3: Diferencie causas de “computação subutilizada”

Se você observa TFLOPs alcançados baixos, pode ser:

  • Limitado por memória (mais comum para ops pequenas/simples)
  • Paralelismo insuficiente (batch pequeno ou tensores pequenos)
  • Overhead de lançamento de kernel / gargalo na CPU (muitos kernels minúsculos)
  • Sincronização / stalls (coletivas, cadeias de dependência)
  • Mistura de instruções (funções especiais, conversões de tipo)

É por isso que profiling importa: “TFLOPs baixo” é um sintoma, não um diagnóstico.

Exemplo concreto: fusão vs sem fusão

Suponha que você tenha um sub-bloco MLP de transformer (simplificado):

  1. y = x @ W1
  2. y = gelu(y)
  3. y = y @ W2

Se gelu estiver separado, você pode:

  • Escrever y na HBM após a matmul
  • Ler y da HBM para aplicar gelu
  • Escrever y de volta na HBM
  • Ler y novamente para a segunda matmul

Isso são duas passagens extras completas do tensor por HBM para y.

Se você fundir gelu no epílogo da primeira GEMM (comum em kernels otimizados), você pode evitar essas leituras/escritas extras e manter o intermediário em registradores/memória on-chip o máximo possível. Isso frequentemente gera um ganho de velocidade maior do que ajustar instruções matemáticas — porque reduz a pressão de largura de banda.

Problemas de largura de banda específicos de treinamento

Tráfego de memória de ativações e checkpointing

Durante o treinamento, você armazena ativações para Retropropagação. Isso aumenta:

  • Demanda de capacidade de HBM
  • Demanda de largura de banda de HBM (escrever ativações, depois lê-las)

Checkpointing de ativações (activation checkpointing) reduz as ativações armazenadas ao recomputá-las no passe backward. Ele troca:

  • Menos tráfego / capacidade de memória
  • Mais computação (recomputação extra do forward)

Em aceleradores modernos, essa troca costuma ser favorável porque computação é relativamente abundante em comparação com capacidade/largura de banda de memória.

Estados do otimizador: multiplicadores de largura de banda e capacidade

Otimizadores como Adam mantêm múltiplos tensores de estado (por exemplo, m, v) junto com parâmetros e gradientes. Isso aumenta:

  • Pegada de memória (capacidade)
  • Tráfego de memória por passo (largura de banda)

Esta é uma razão pela qual estratégias de sharding de otimizador e parâmetros podem melhorar throughput mesmo quando a computação não muda: elas reduzem a pressão de memória por dispositivo e às vezes melhoram localidade.

Problemas de largura de banda específicos de inferência

Largura de banda do cache KV na decodificação autorregressiva

Na decodificação de transformers, cada novo token atende a todos os tokens anteriores via o cache KV. À medida que o comprimento da sequência cresce:

  • Você lê mais dados K/V por token
  • FLOPs crescem, mas o tráfego de memória pode crescer mais rápido dependendo da implementação

Com batch pequeno (comum em serving interativo), a inferência pode se tornar fortemente limitada por largura de banda — às vezes limitada por leituras de memória do cache KV em vez do throughput de GEMM.

Esta é uma razão pela qual técnicas como atenção paginada (paged attention), quantização do cache KV e melhores layouts de memória podem melhorar tokens/s sem mudar a matemática do modelo.

Quantização reduz largura de banda (frequentemente o verdadeiro ganho)

Quantização é frequentemente apresentada como “matemática mais rápida”, mas um grande benefício é reduzir bytes movidos:

  • int8 reduz pela metade os bytes vs fp16
  • int4 reduz pela metade novamente

Menor demanda de largura de banda pode gerar grandes acelerações em ops limitadas por memória, mesmo que computação não seja o limitador. Veja Quantização.

Como medir gargalos de largura de banda na prática

As métricas-chave a observar

Ao fazer profiling de um passo de treinamento ou de uma requisição de inferência, sinais úteis incluem:

  • Largura de banda de HBM alcançada (GB/s) vs pico teórico
  • Utilização de SM/núcleos tensoriais (ocupação de computação, não apenas “utilização de GPU”)
  • Quebra por kernel: muitos kernels pequenos geralmente implicam overhead e tráfego de memória
  • Throughput de leitura/escrita de DRAM (em profilers de GPU)
  • Tempo em coletivas (NCCL all-reduce/all-gather)
  • Stalls no pipeline de entrada da CPU (data loader não mantendo a GPU alimentada)

As ferramentas variam por plataforma, mas o fluxo de trabalho é consistente: identificar os kernels que mais consomem tempo e classificar cada um como limitado por computação ou por largura de banda.

Se você é novo em conceitos de GPU como warps, ocupação e memória compartilhada, Noções Básicas de CUDA é o artigo companheiro adequado.

Uma calculadora minúscula “estilo roofline” (brinquedo)

O trecho a seguir mostra como você pode fazer um sanity-check de um kernel se conseguir estimar FLOPs e bytes:

def roofline_bound(flops, bytes_moved, peak_flops_per_s, peak_bytes_per_s):
    oi = flops / bytes_moved
    perf_bw_limited = peak_bytes_per_s * oi
    achievable = min(peak_flops_per_s, perf_bw_limited)
    bound = "memory-bound" if perf_bw_limited < peak_flops_per_s else "compute-bound"
    return oi, achievable, bound

# Example: elementwise op over N fp16 elements
N = 1_000_000
flops = 10 * N
bytes_moved = 4 * N  # read + write fp16

oi, achievable, bound = roofline_bound(
    flops, bytes_moved,
    peak_flops_per_s=200e12,  # 200 TFLOPs/s
    peak_bytes_per_s=1.5e12,  # 1.5 TB/s
)

print("OI:", oi, "FLOPs/byte")
print("Bound:", bound)
print("Upper bound:", achievable/1e12, "TFLOPs/s")

Isso não substitui profiling, mas evita perseguir o alvo de otimização errado.

Estratégias comuns de otimização (mapeadas ao gargalo)

Se você está limitado por largura de banda de memória

Concentre-se em reduzir bytes movidos e melhorar localidade:

  • Fundir operações (reduzir leituras/escritas de intermediários)
  • Usar kernels melhores (tiling no estilo FlashAttention; LayerNorm fundida; embeddings otimizados)
  • Preferir layouts contíguos e evitar transposes/materializações
  • Aumentar intensidade aritmética aumentando reutilização (tiling, blocking)
  • Quantizar para reduzir tamanhos de tensores (Quantização)
  • Evitar casts de precisão desnecessários
  • Usar batches maiores quando isso aumenta reutilização (nem sempre possível em serving)

Se você está limitado por latência (comum em inferência pequena)

  • Agrupar requisições (micro-batching) para amortizar overhead
  • Reduzir contagem de kernels (fusão, compilação)
  • Usar CUDA graphs / execução estática onde disponível
  • Minimizar sincronização CPU↔GPU e overhead de Python
  • Manter shapes estáticos quando possível para ajudar compiladores (Compiladores & Runtimes)

Se você está limitado por interconexão (escalonamento multi-GPU)

  • Reduzir volume de comunicação (acumulação de gradientes, estratégias de sharding)
  • Sobrepor comunicação com computação (mudanças de pipeline e escalonamento)
  • Garantir alocação consciente de topologia (ilhas NVLink, localidade IB)
  • Usar coletivas eficientes e configurações corretas de NCCL
    (Ver Interconexões (NVLink/InfiniBand))

Checklist mental para depurar modelos “lentos”

Quando o throughput está pior do que o esperado, pergunte:

  1. Quais são os principais kernels por tempo? São GEMMs ou ops pesadas em largura de banda (norms, elemento a elemento, bookkeeping de atenção)?
  2. O modelo está limitado pela largura de banda da HBM? (Alta utilização de largura de banda, baixa utilização de computação.)
  3. Estamos pagando por passagens extras de memória? (Ops não fundidas, mudanças de layout, intermediários materializados.)
  4. O batch está pequeno demais? (Subutilização e overhead de latência.)
  5. A comunicação multi-GPU está dominando? (Tempo de all-reduce/all-gather alto.)
  6. O pipeline de entrada está faminto a GPU? (Gargalo de CPU/data loader/armazenamento — ver Armazenamento.)

Resumo

“Memória & largura de banda” é a lente de desempenho que explica por que:

  • Ops aparentemente simples podem dominar o tempo de execução
  • TFLOPs de pico raramente são atingidos de ponta a ponta
  • Fusão e tiling podem vencer “matemática mais rápida”
  • O escalonamento de treinamento distribuído pode travar na largura de banda da interconexão
  • Quantização ajuda não apenas a computação, mas a movimentação de dados

A estrutura mais prática é o modelo Roofline: estime a intensidade operacional (FLOPs/byte), compare contra os tetos de computação e largura de banda e então otimize o verdadeiro limitador — geralmente movendo menos bytes e reutilizando dados on-chip o máximo possível.