Skip to content
GPU Pro Tip
Source: developer.nvidia.com

CUDA Pro Tip: Aumente o Desempenho com Acesso à Memória Vetorizado

Sources: https://developer.nvidia.com/blog/cuda-pro-tip-increase-performance-with-vectorized-memory-access, developer.nvidia.com

TL;DR

  • Loads e stores vetorizados usam larguras de dados mais amplas (64 ou 128 bits) para ler ou gravar vários valores por instrução, melhorando a utilização da banda e reduzindo a contagem de instruções em relação a loads escalares.
  • O caminho mais fácil é usar tipos vetoriais do CUDA C++, como int2, int4, float2 e float4, fazendo casts de ponteiros para que o compilador emita instruções vetorizadas.
  • O alinhamento é fundamental: os dados precisam estar alinhados ao tamanho do tipo vetor; deslocamentos desalinhados invalidam loads vetorizados; deslocamentos alinhados são seguros (por exemplo, reinterpret_cast(d_in+2)).
  • A vetorização pode trazer grandes ganhos (2x menos instruções para cópia de dois elementos e até 4x com vector4), mas pode aumentar a pressão de registradores e reduzir o paralelismo em alguns kernels.
  • Nem toda carga de trabalho se beneficia de loads vetorizados; kernels já limitados por registradores ou com paralelismo baixo podem ter melhor desempenho com loads escalares.

Contexto e antecedentes

Muitos kernels CUDA são limitados pela largura de banda, e a relação crescente entre operações de ponto flutuante (flops) e largura de banda em GPUs modernas faz com que mais kernels sejam limitados pela largura de banda de memória. Nesse cenário, mitigar gargalos de banda de memória torna-se uma estratégia de otimização crucial. O CUDA Pro Tip destaca uma abordagem prática: usar loads e stores vetorizados no CUDA C++ para aumentar a utilização da banda enquanto reduz o número total de instruções. O post começa com um kernel simples de cópia de memória e usa a inspeção de montagem como base da otimização. Ferramentas como o cuobjdump, incluído no CUDA Toolkit, mostram como o SASS (assembly) gerado difere entre as versões escalar e vetorizada. No código escalar, o conjunto de instruções usa LDG.E e STG.E para ler e gravar dados de 32 bits da memória global. As versões vetorizadas substituem essas instruções por LDG.E.{64,128} e STG.E.{64,128}, que operam em unidades de 64 ou 128 bits. Essa mudança é a principal alavanca de desempenho: menos instruções e menor latência para a mesma operação de memória. Os loads vetorizados utilizam tipos de dados vetoriais definidos nos headers padrão do CUDA C++, como int2, int4, float2 e float4. Esses tipos representam múltiplos valores empacotados em uma única unidade de dados e podem ser usados por meio de casting de ponteiros. Ao reinterpretar um ponteiro escalar como um ponteiro vetorial, o compilador gera as instruções vetorizadas correspondentes. Existe uma ressalva importante: os carregamentos vetorizados requerem alinhamento. O dispositivo mantém o alinhamento automático para o tamanho do tipo, mas, se houver deslocamento, ele precisa ser alinhado para manter segurança e desempenho. Por exemplo, reinterpret_cast(d_in+1) é inválido, enquanto reinterpret_cast(d_in+2) é válido. Além disso, estruturas cujo tamanho é potência de dois bytes ajudam a manter o alinhamento adequado. A técnica apresentada também mostra que é possível gerar loads vetorizados usando estruturas desde que o tamanho total seja uma potência de dois. Tamanhos não potentes podem levar a alinhamento ineficiente e podem exigir padding adicional pelo compilador para alinhar corretamente na arquitetura típica. Quando os requisitos de alinhamento são atendidos, instruções vetorizadas podem ser geradas com sucesso.

O que há de novo

A ideia central é modificar um kernel de cópia de memória para processar múltiplos elementos por iteração usando loads vetorizados. As mudanças-chave são:

  • O laço passa a executar N/2 vezes para processar dois elementos por iteração, reduzindo pela metade o número de iterações em relação ao kernel escalar.
  • Os ponteiros de entrada e saída são convertidos para ponteiros de tipos vetoriais (por exemplo, int2*) usando reinterpret_cast, permitindo loads e stores vetorizados.
  • Elementos remanescentes (quando N não é divisível por 2) são tratados para manter a correção.
  • A configuração de lançamento é ajustada para usar metade do número de threads do kernel escalar, acompanhando o processamento de dois elementos por iteração. A inspeção do SASS gerado mostra LDG.E.64 e STG.E.64 para a versão de 64 bits e LDG.E.128 e STG.E.128 para a versão de 128 bits. Os ganhos observados incluem uma redução de até 2x no número de instruções para uma cópia de dois elementos e até 4x para a versão vector4. Em geral, loads vetorizados apresentam melhor desempenho em cenários limitados pela largura de banda, mas os ganhos reais dependem do kernel e do hardware. A postagem também enfatiza trade-offs práticos. Embora loads vetorizados aumentem a utilização da banda e reduzam a contagem de instruções, eles podem elevar a pressão de registradores e reduzir o paralelismo global. Em kernels que já são limitados por registradores ou que possuem paralelismo de thread baixo, cargas escalares podem ser mais vantajosas. Além disso, para usar loads vetorizados, os dados devem estar alinhados e o tamanho do tipo deve ser potência de dois; caso contrário, a vetorização não pode ser aplicada. Loads vetorizados são apresentados como uma otimização fundamental do CUDA que deve ser utilizada sempre que possível, dada a sua capacidade de aumentar a largura de banda, reduzir a contagem de instruções e reduzir a latência. O post observa ainda que a abordagem apresentada serve como um caminho prático, com mudanças relativamente pequenas em kernels existentes, para complementar outras otimizações de acesso à memória.

Por que isso importa (impacto para desenvolvedores/empresas)

Para desenvolvedores de software CUDA, a capacidade de aumentar a utilização da largura de banda de memória sem aumentar o número de instruções executadas se traduz em maior throughput e menor latência. Em domínios como computação científica de grande escala, inferência de machine learning ou pipelines gráficos em tempo real, onde a largura de banda de memória é frequentemente o gargalo, a técnica de acesso vetorizado oferece um caminho de otimização prático que pode produzir melhorias mensuráveis em desempenho. Do ponto de vista empresarial, adotar loads vetorizados onde apropriado pode levar a melhor desempenho de aplicações em GPUs, permitindo processamento de dados mais rápido, redução do tempo de computação e potencialmente menor consumo de energia por concluir tarefas mais rapidamente. No entanto, os desenvolvedores devem avaliar a vetorização em seu hardware alvo, já que os ganhos dependem das características do kernel, como uso de registradores, padrões de acesso à memória e o grau de paralelismo.

Detalhes técnicos ou Implementação (orientação prática)

  • Comece com um kernel simples de cópia de memória e compare o caminho escalar com o vetorizado.
  • Habilite loads vetorizados convertendo ponteiros para tipos vetoriais, como int2, int4, float2 ou float4 (por exemplo, reinterpret_cast(d_in)). Isso faz com que o compilador emita LDG.E.{64,128} e STG.E.{64,128} conforme apropriado.
  • Garanta o alinhamento. Deslocamentos devem respeitar o alinhamento ao tamanho do vetor. Deslocamentos não alinhados invalidam loads vetorizados.
  • Preste atenção aos tamanhos de tipos. Loads vetorizados funcionam melhor quando os tamanhos são potências de dois; tamanhos não potentes podem introduzir padding que prejudica o desempenho.
  • Trate o remainder. Se N não for múltiplo do largura do vetor, implemente um caminho de correção para processar os elementos restantes com loads escalares.
  • Ajuste a configuração de lançamento. Ao usar two-element processing, você pode lançar metade dos threads do caminho escalar, pois cada iteração processa dois elementos.
  • Considere as trade-offs. Loads vetorizados aumentam a largura de banda e reduzem a contagem de instruções, mas podem elevar a pressão de registradores e reduzir o paralelismo. Em kernels limitados por registradores ou com paralelismo baixo, loads escalares podem ser preferíveis.
  • Utilize ferramentas para verificar e analisar. O cuobjdump, parte do CUDA Toolkit, ajuda a inspecionar o SASS gerado para as versões escalar e vetorizada, observando a transição de LDG.E para LDG.E.{64,128} e STG.E.{64,128}.
  • Consulte o CUDA Pro Tip sobre Acesso à Memória Vetorizado para contexto adicional e exemplos e para ver alterações de desempenho medidas entre kernels. Abaixo há uma comparação concisa para orientar decisões. Observe que ganhos reais dependem do kernel e do hardware: | Abordagem | Largura do vetor | Tendência típica de contagem de instruções | Requisito de alinhamento | Observações |--- |--- |--- |--- |--- |Loads escalares | 32 bit | Linha de base | Alinhado automaticamente pela memória do dispositivo | Portabilidade ampla; segura para todas as disposições de dados |Loads vetorizados 2 elementos | 64 bit | Instruções reduzidas; possível redução de latência | Deslocamentos devem estar alinhados à largura do vetor | Pode duplicar o throughput para caminhos limitados por banda; aumenta a pressão de registradores |Loads vetorizados 4 elementos | 128 bit | Instruções ainda mais reduzidas | Deslocamentos devem estar alinhados à largura do vetor | Pode oferecer ganhos maiores; maior risco de pressão de registradores | Principais observações
  • O alinhamento é essencial; se o deslocamento não for alinhado, loads vetorizados não podem ser usados.
  • O tamanho e a forma dos dados devem favorecer potências de dois para evitar padding.
  • Vetorização nem sempre traz ganhos; confirme com profiling e análise de desempenho.

Principais conclusões

  • Acessos de memória vetorizados podem reduzir significativamente a contagem de instruções e melhorar a utilização da largura de banda em kernels CUDA limitados pela memória.
  • Usar tipos vetoriais por meio de casting de ponteiros permite instruções mais amplas sem exigir mudanças estruturais grandes no código.
  • O alinhamento correto é crítico; descolamentos desalinhados podem invalidar a vetorização.
  • Avalie o trade-off entre ganhos de largura de banda e pressão de registradores; kernel com alto uso de registradores pode não se beneficiar.
  • Sempre profile seu código no hardware de destino para confirmar ganhos; utilize ferramentas para inspecionar a montagem gerada quando possível.

FAQ

  • O que são loads e stores vetorizados no CUDA C++?

    São operações de memória que carregam gravam unidades de dados mais largas (64 ou 128 bits) em uma única instrução, geradas ao usar tipos vetoriais e casting de ponteiros para esses tipos.

  • Como habilito o acesso à memória vetorizado em um kernel?

    Converta os ponteiros de entrada e saída para tipos vetoriais como int2, int4, float2 ou float4 (por exemplo, reinterpret_cast(d_in)). O compilador emitirá LDG.E.{64,128} e STG.E.{64,128} conforme apropriado.

  • uais são os requisitos de alinhamento para loads vetorizados?

    Os dados precisam estar alinhados ao tamanho do vetor; deslocamentos alinhados são seguros. Deslocamentos desalinhados invalidam loads vetorizados.

  • uais são as desvantagens potenciais da vetorização?

    Podem aumentar a pressão de registradores e reduzir o paralelismo; kernels com alto nível de paralelismo podem se beneficiar menos. Avalie caso a caso.

  • ual é o ganho de desempenho típico com loads vetorizados?

    Pode haver até 2x menos instruções para uma cópia de dois elementos e até 4x menos instruções para uma versão vector4; ganhos reais variam conforme o kernel e o hardware.

References

More news