24 pontos por GN⁺ 2025-06-25 | 1 comentários | Compartilhar no WhatsApp
  • GPUs têm velocidade de cálculo muito superior à velocidade de acesso à memória, por isso a hierarquia de memória se torna o gargalo de desempenho
  • De acordo com a intensidade aritmética (Arithmetic Intensity, AI), a execução pode ser classificada como limitada por memória ou limitada por computação, e o ponto crítico da GPU A100 é de cerca de 13 FLOPs/Byte
  • As principais estratégias de otimização de desempenho incluem fusão de operações (Fusion) e tiling; a Fusion reduz idas e vindas desnecessárias à memória, enquanto o Tiling maximiza a reutilização de dados
  • Entender características estruturais do hardware de GPU, como sincronização, Coalesced Load e resolução de conflitos de banco, é importante para escrever kernels de alto desempenho
  • Considerações adicionais, como ocupação (Occupancy), minimização de divergência entre threads e quantização (Quantization), também têm impacto importante no desempenho real

Hierarquia de computação e memória da GPU

  • Em geral, GPUs têm capacidade de processamento aritmético muito maior do que sua largura de banda de memória
  • Por exemplo, a NVIDIA A100 entrega cerca de 19.5 TFLOPS (ponto flutuante de 32 bits), enquanto a largura de banda de memória fica em torno de 1.5 TB/s
  • Como é possível executar dezenas de cálculos enquanto se lê 4 bytes de dados, a movimentação de dados é o gargalo de desempenho
  • A memória global (VRAM) é a memória off-chip lenta onde todos os dados residem, enquanto os Streaming Multiprocessors (SMs) são responsáveis pela computação
  • Cada SM possui Shared Memory (SRAM) on-chip de alta velocidade, que pode ser usada como um cache gerenciado diretamente pelo programa
  • Threads são a menor unidade de execução, e cada thread possui seu próprio conjunto de registradores
  • 32 threads formam um Warp, e um Block é uma grade de threads executada no mesmo SM

Regiões de desempenho: limitado por memória vs limitado por computação

  • O desempenho de um kernel é limitado por memória (restrito pela velocidade de movimentação de dados) ou por computação (restrito pela capacidade de cálculo do SM)
  • A intensidade aritmética (AI) é definida como Total FLOPs / Total Bytes Accessed, e esse valor é um indicador importante
  • Modelo Roofline: gráfico em que o eixo x é AI e o eixo y é FLOPS/s, representando o desempenho realizável do kernel
    • Se a AI é baixa e o kernel é limitado por memória, ele fica na diagonal (degrau da largura de banda de memória)
    • Se a AI é alta e o kernel é limitado por computação, ele fica na horizontal (degrau do desempenho máximo de cálculo)
  • O Ridge Point da A100 é 19.5 TFLOPS / 1.5 TB/s ≈ 13 FLOPs/Byte
  • Ao aumentar a AI, o desempenho melhora e o kernel pode atingir o regime limitado por computação

Estratégias para aumentar a intensidade aritmética

  • Modelo simples: 1 thread calcula 1 valor de C[i,j] → AI = 0.25 (muito baixa, limitado por memória)
  • Mesmo quando uma thread calcula um tile 2x2, AI = 0.5 (ainda baixa)
  • Para aumentar a AI, várias threads precisam carregar tiles grandes em Shared Memory no nível de bloco, maximizando a reutilização dos dados
  • Com cooperação entre as threads do bloco, é possível elevar AI > 13 e entrar no regime limitado por computação

Estado limitado por overhead

  • Pode haver overhead no processo em que a CPU (host) envia trabalho para a GPU
  • Se os kernels da GPU forem muito pequenos ou numerosos demais, a GPU pode acabar esperando por trabalho
  • Frameworks modernos introduzem execução assíncrona, enfileirando antecipadamente o stream de comandos para minimizar esse overhead

Duas estratégias centrais para ganho de desempenho: Fusion e Tiling

Operator Fusion (fusão de operações)

  • Em operações simples em cadeia, por exemplo y = relu(x + 1), se cada operação roda em um kernel separado, os dados ficam indo e voltando da memória global
  • A Fusion combina várias operações em um único kernel, sem armazenar valores intermediários na memória global; o processamento é feito em registradores e apenas o resultado final é gravado
  • Exemplos: compiladores JIT como Triton e torch.compile Inductor automatizam esse processo

Tiling

  • Em operações mais complexas, como multiplicação de matrizes, o modelo de thread única leva a uma AI baixa
  • Após dividir em tiles por bloco, todas as threads do bloco cooperam para carregar os tiles de dados em Shared Memory, permitindo grande reutilização dos dados
  • O padrão de execução segue 3 etapas: "Load (global -> Shared Memory) - Synchronize (sincronização) - Compute (cálculo)"

Coalesced Load e vetorização

  • Ao mover dados da memória global para a Shared Memory, Coalesced Access é importante (as 32 threads de um warp acessam uma região contígua de 128 bytes)
  • Com vetorização (por exemplo, float4) para carregar vários dados de uma vez, é possível economizar recursos de hardware e maximizar o uso da largura de banda
  • O alinhamento de dados (alignment) é essencial, e o valor K em bytes da matriz precisa ser múltiplo de 4 para ser eficiente

Bancos da Shared Memory e conflitos de banco

  • A Shared Memory é composta por 32 bancos independentes, então as 32 threads de um warp precisam acessar bancos diferentes para operar sem conflito
  • Acesso por linha não gera conflito, enquanto acesso por coluna gera conflito (acesso ao mesmo banco)
  • No tile B, a estratégia de "carregar e transpor" armazena os dados transpostos na Shared Memory, evitando conflitos de banco ao privilegiar acesso por linha durante o cálculo

Padrões de computação on-chip de alta velocidade

Estratégia básica 1: uma thread calcula uma saída

  • Sob a limitação de BLOCK_DIM=32, a AI máxima é 8, então não é possível entrar no regime limitado por computação

Estratégia 2: uma thread calcula várias saídas

  • Com BLOCK_DIM=16 e TILE_DIM=64, uma thread calcula uma saída 4x4 → AI=16
  • Como AI>13, é possível atingir desempenho compute-bound na A100
  • Também é possível calcular com eficiência usando cargas vetorizadas como float4 na Shared Memory

Limite prático do tiling: quantização de tiles

  • Se o tamanho da matriz não for múltiplo do tamanho do tile, os blocos de borda calculam uma região maior do que a real (trabalho desnecessário), com padding aplicado
  • As threads de borda impedem acessos desnecessários à memória com condições de guarda, mas o loop de cálculo continua igual, gerando operações inúteis (por exemplo, C += A * 0)

Fatores adicionais de ajuste de desempenho

Ocupação (Occupancy) e ocultação de latência

  • Quando um warp fica esperando por muito tempo, por exemplo em leituras de memória, o SM troca imediatamente para outro warp para reduzir o tempo ocioso (ocultação de latência, latency hiding)
  • Ao alocar vários Thread Blocks simultaneamente, é possível minimizar o tempo de espera com alta ocupação
  • Se o tamanho do bloco ou do tile for grande demais, o número de blocos residentes diminui, a ocupação cai e o desempenho piora

Minimização de divergência entre threads

  • Se ocorrer uma ramificação if-else dentro de um warp, os dois caminhos são executados em sequência, reduzindo o desempenho efetivo praticamente pela metade
  • É necessário minimizar ramificações com código sem branch, como min e max

Quantização (Quantization)

  • Ao reduzir a precisão de FP32 para FP16/BFP16, a quantidade de dados transferidos pela memória e de dados processáveis dobra em cada caso
  • Na A100, operações FP16 podem atingir 312 TFLOPS (até 16 vezes o desempenho relativo de 19.5 TFLOPS em FP32)
  • Com quantização, é possível avançar simultaneamente para a direita (eficiência de memória) e para cima (desempenho máximo de cálculo) no Roofline

Resumo geral

  • O limite fundamental do desempenho em GPU vem do desequilíbrio entre largura de banda de memória e capacidade de computação on-chip
  • O ganho de desempenho é obtido por maximização da reutilização de dados (Tiling) e minimização do tráfego intermediário de memória (Fusion)
  • É preciso entender as características do hardware (warps, bancos, acessos coalescidos, sincronização) para escrever e otimizar kernels de alto desempenho
  • Na prática, fatores adicionais como ocupação, minimização de divergência e quantização afetam diretamente a velocidade real
  • Projetar computação de alto desempenho em GPU exige considerar em conjunto o aumento teórico de AI, o aproveitamento das características do hardware e a adaptação ao layout e ao tamanho reais dos dados

1 comentários

 
GN⁺ 2025-06-25
Comentários no Hacker News
  • Curiosidade sobre o quão bem a otimização do programa inteiro está sendo feita no nível do compilador; a sensação é de que a abordagem atual, de otimizar cada arquitetura de LLM individualmente, está ficando para trás

  • Relato de uma experiência tentando rodar llama.cpp e vllm na mesma 4070 para processar mais prompts em lote; a partir de batch 8, o llama.cpp fica drasticamente mais lento e, embora o uso de GPU pareça aceitável, na prática há um gargalo; já o vllm lida com isso muito melhor

    • O vllm usa cache KV paginado e um layout totalmente coalescido, preferido pela GPU, entregando desempenho otimizado para batch; já o llama.cpp usa um layout flat bom para prompt único, então em cenários com batch o padrão de acesso à memória L2 se quebra e a velocidade cai

    • Compartilhamento de experiência: ao intercalar o tensor KV no llama.cpp de [seq, head, dim] para [head, seq, dim], acompanhando a forma como o vllm alimenta dados para o kernel de atenção fundido, houve melhora imediata de cerca de 2x no desempenho de computação

    • O gargalo não está na GPU em si, mas em como o acesso à shared memory e as leituras globais são projetados; o vllm ataca exatamente esse ponto com a mudança de layout

    • Levaram mais de 2 dias para analisar esse gargalo, não dava para perceber isso só olhando gráficos de utilização da GPU, e a maior parte foi descoberta por tentativa e erro

    • Pergunta se existe alguma forma de repetir esse tipo de experimento com mais facilidade, de modo iterativo, com hot reload

    • Observação de que, embora tenha sido dito que a GPU não era o gargalo, na prática a ineficiência do layout de memória acabou se tornando um gargalo por reduzir a eficiência computacional da GPU

    • Menção ao projeto nano-vllm, publicado ontem por um funcionário da DeepSeek; tem só 1.200 linhas, mas registrou desempenho melhor do que o vllm vanilla https://github.com/GeeeekExplorer/nano-vllm

    • Pergunta se o layout alterado no llama.cpp foi enviado como pull request; a opinião é que um ganho de 2x poderia trazer grande benefício para todos

    • Recomendação para também testar o projeto ik_llama.cpp https://github.com/ikawrakow/ik_llama.cpp

  • Comentário de que é um artigo com boas informações, e que o conteúdo trata das escolhas que a NVIDIA faz ao desenvolver arquitetura de GPU; reforça que não se deve interpretar isso errado como se fossem diferenças universais em relação a outras empresas

    • Por exemplo, a AMD Instinct MI300, com até 160 TFLOPS em FP32 e 6TB/s de largura de banda HBM3/3E, muda o ridge-point para 27 FLOPs/byte, o dobro dos 13 FLOPs/byte da A100; além disso, a grande capacidade de memória HBM (128~256GB) também altera os trade-offs práticos entre tiling depth e occupancy; por outro lado, essas GPUs são caras e têm o trade-off de não suportarem CUDA

    • Opinião de que, até a AMD dar mais atenção ao software de computação, só as GPUs da NVIDIA continuarão tendo relevância

  • Ênfase de que, como spoiler, o que realmente importa não é tanto como a GPU funciona em si, mas como ela é usada em cálculos de machine learning

    • Crítica de que, na prática, o conteúdo está mais próximo de um resumo geral de CUDA, e que, tirando o exemplo de relu e a menção ao torch, não há grande relação com machine learning
  • Opinião de que o uso de cores com contraste é indispensável, com ênfase na legibilidade

    • Relato de experiência com font-weight: 300: muitos designers de Mac desenvolvem ajustando para as opções de suavização de fonte, configurando para parecer "normal" em geral; como no Mac fontes finas acabam parecendo meio mais grossas, os designers tendem a usar fontes mais finas para transmitir sensação de "normal"; compartilha o link relacionado https://news.ycombinator.com/item?id=23553486

    • Suposição de que o autor talvez esteja editando e formatando em modo escuro; menciona que, ao aplicar edge://flags/#enable-force-dark, os links ficam mais visíveis

    • Aponta que os links e os comentários dentro dos blocos de código exigiram esforço extra para ler, sugere aumentar o contraste, mas avalia que a qualidade do conteúdo em si foi excelente

    • Crítica de que o site comete um grande erro ao usar transparência alfa no texto, reduzindo severamente o contraste

  • Sugestão de que um título mais adequado seria algo mais próximo de “Fatos básicos sobre GPUs Nvidia”; explicação de contexto de que o termo WARP também é característico das GPUs Nvidia modernas, e que as GPUs Nvidia por volta de 2003 eram hardware voltado apenas para renderização de videogames, completamente diferente das GPUs atuais de computação de propósito geral; em resumo, o post não traz uma explicação genérica aplicável a todas as GPUs

  • Comentário agradecendo por ser um material introdutório realmente muito bom; relato de que, ao montar pessoalmente um AI PC, passou vários dias pesquisando sobre GPUs, e este texto ajudou muito por organizar bem tanto os pontos essenciais quanto as áreas de aplicação de alto valor agregado, como IA generativa; em especial, o diagrama da hierarquia de memória da GPU A100 foi considerado muito útil

  • Estranhamento com o uso de diagramas ASCII