- 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
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.cppevllmna mesma 4070 para processar mais prompts em lote; a partir de batch 8, ollama.cppfica drasticamente mais lento e, embora o uso de GPU pareça aceitável, na prática há um gargalo; já ovllmlida com isso muito melhorO
vllmusa cache KV paginado e um layout totalmente coalescido, preferido pela GPU, entregando desempenho otimizado para batch; já ollama.cppusa 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 caiCompartilhamento de experiência: ao intercalar o tensor KV no
llama.cppde[seq, head, dim]para[head, seq, dim], acompanhando a forma como ovllmalimenta dados para o kernel de atenção fundido, houve melhora imediata de cerca de 2x no desempenho de computaçãoO gargalo não está na GPU em si, mas em como o acesso à shared memory e as leituras globais são projetados; o
vllmataca exatamente esse ponto com a mudança de layoutLevaram 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 ovllmvanilla https://github.com/GeeeekExplorer/nano-vllmPergunta se o layout alterado no
llama.cppfoi enviado como pull request; a opinião é que um ganho de 2x poderia trazer grande benefício para todosRecomendação para também testar o projeto
ik_llama.cpphttps://github.com/ikawrakow/ik_llama.cppComentá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
relue a menção aotorch, não há grande relação com machine learningOpiniã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=23553486Suposiçã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íveisAponta 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