Sou um novato em aceleração de GPU. Apenas tentei uma ligação LWJGL básica em CUDA com um kernel simples, sem memória compartilhada, a assinatura da função é a seguinte
__global__ void compute(
unsigned int n,
unsigned long long int* timeMs,
double* a, double* b, double* c,
double *g_odata)
a função do kernel é basicamente recuperar dados para o id do thread dos arrays acima (timeMs, a, b, c etc.) e fazer algumas contas e colocar o resultado no array g_odata no id do thread apropriado. n sendo o número de threads a serem computados (ele verifica se o ID do thread ultrapassa n, é claro). Não há memória compartilhada ou redução.
Agora aqui está o caso curioso sobre n (tamanho total do thread/paralelismo) e tamanho do bloco quando meço o tempo TOTAL necessário para o kernel ser concluído (tenho uma GPU com 80 multiprocessadores)
Por meio disso clock64()
, adicionei carimbo de data / hora na função do kernel antes e depois e coletei o tempo total de cada thread, e é aparente que quanto mais threads houver, mais lentos eles demoram para a MESMA tarefa
Agora perguntas:
- Por que o tempo total diminui após cerca de 100 threads? Dados 80 multiprocessadores e mais de 10 mil núcleos cuda, esperaria que esse número fosse maior, então talvez algum problema de configuração?
- Por que a função do kernel está demorando mais para mais threads? a execução é entrelaçada (ou seja, o escalonador pode pausar um deles antes de concluir e executar outro)
- Por que há um comportamento de platô após um aumento de 100 threads? e por que ele decola novamente
- O desempenho variável com base no número do bloco. Eu li que grade/bloco é apenas a perspectiva do desenvolvedor e não tem influência (especialmente para meus threads totalmente segregados sem compartilhamento/redução). Então, por que isso é importante e como escolher o melhor tamanho de bloco?
Você não mostra o código relevante nem nos fornece o tipo de GPU. Isso torna difícil responder a detalhes. Comecemos pelo princípio: se esta for uma GPU de nível de consumidor, você não usa seus "núcleos cuda", pois eles são apenas para precisão única (
float
). Apenas verificando as especificações de uma GPU aleatória: pelo excelente banco de dados de GPU da TechPowerUp , o RTX-4090 tem 82,58 TFLOPS de precisão simples e apenas 1,29 TFLOPS de precisão dupla.Sim, é assim que uma GPU funciona. Se você olhar a tabela Compute Capabilities no CUDA Programming Guide, verá que normalmente um SM (multiprocessador de streaming) tem 1024-2048 threads, mas quando você compara com a tabela de instruções aritméticas , apenas uma taxa de transferência de cerca de 128 instruções de precisão única por ciclo de clock. Ocultar a latência comprometendo demais os recursos da GPU é como funciona.
Uma escala logarítmica é difícil de interpretar, mas parece que pode haver um aumento de 256 threads (?). Pode ser quando o escalonador não consegue encontrar uma unidade de execução de dupla precisão livre a cada ciclo de clock. O criador de perfil visual Nsight Compute deve ser capaz de informar você.
Observe que um único bloco sempre é executado em um único SM. Portanto, 256 threads com tamanho de bloco de 1.024 significa que todos os threads são executados no mesmo processador, deixando os recursos de computação nos outros processadores desocupados.
No geral, acho que essa métrica não tem sentido de qualquer maneira. 100-1000 threads é muito pouco e você precisa observar o rendimento de todos os threads, ou seja, o número de itens de trabalho dividido pelo tempo total de execução do kernel.
Isto é errado. O tamanho do bloco é importante. Novamente, se você observar os recursos de computação, há limites para o número de blocos por SM, bem como para threads por SM. Se o tamanho do seu bloco for inferior a 64, você não alcançará 100% de ocupação. Um tamanho de bloco que não seja múltiplo do tamanho do warp também desperdiçará recursos com threads desativados. E é claro que um tamanho de bloco de 1.024 nunca atingirá mais de 2/3 da ocupação em dispositivos CUDA 8.6-8.9 com 1.536 threads por SM.
Os blocos também têm sobrecarga de lançamento, portanto, menos blocos podem ser benéficos, mas torná-los muito grandes também pode ter efeitos negativos. Um novo bloco só pode começar quando todos os threads de um bloco antigo terminarem. Blocos grandes significam que no final do kernel (ou nas
__syncthreads()
barreiras), muitos threads podem esperar por poucos retardatários, ocupando recursos por mais tempo do que o necessário.Regra geral, use cerca de 128-256 threads por bloco. Compare tamanhos diferentes, se necessário. Mas você precisa torná-lo uma referência significativa. Observe o rendimento geral e ocupe totalmente a GPU.