3 pontos por GN⁺ 2025-03-13 | 2 comentários | Compartilhar no WhatsApp
  • Como melhorar o desempenho de algoritmos de ordenação com computação paralela usando CUDA
  • Explora o potencial de ganho de desempenho ao implementar o merge sort básico com CUDA

Merge sort recursivo básico (implementação em CPU)

  • Algoritmo de ordenação que divide o array em dois subarrays, ordena cada um deles e depois faz a mesclagem
  • Divide o array recursivamente e realiza a operação de mesclagem quando o tamanho chega a 1
  • Principais pontos da implementação
    • Uso de uint8_t → minimiza o uso de memória com valores pequenos (0~255)
    • Uso de long long → permite processar arrays grandes (até 10¹⁸)
    • Os resultados são validados com std::sort para comparação de desempenho
    • Complexidade de tempo: O(n log n) em média
    • Complexidade de espaço: O(n)

Merge sort recursivo básico em CUDA

  • Segue o mesmo padrão da implementação em CPU
  • A operação de mesclagem foi implementada para rodar em paralelo no CUDA
  • Principais pontos da implementação
    • Uso de cudaMalloc, cudaMemcpy, cudaFree → alocação de memória na GPU e transferência de dados
    • merge<<<1, 1>>>(...) → executa a operação de mesclagem em paralelo
    • cudaDeviceSynchronize() → sincroniza até a conclusão da mesclagem
    • Problema de desempenho → como o CUDA é ineficiente para recursão, é necessário uma abordagem iterativa

Comparação entre implementações em CPU e GPU

  • Há perda de desempenho porque as chamadas recursivas são executadas na CPU
  • Em CUDA, chamadas recursivas geram problemas de tamanho de pilha e overhead de execução de kernel
  • Forma de melhorar o desempenho: migrar para uma abordagem iterativa (bottom-up)

Merge sort iterativo bottom-up (implementação em CPU)

  • Faz a mesclagem gradualmente a partir de subarrays pequenos → mais eficiente no CUDA
  • Aumenta o tamanho do array de mesclagem em 1, 2, 4, 8, … enquanto executa as mesclagens
  • Estrutura principal do código
    MERGE_SORT(arr, temp, start, end)  
      FOR sub_size ← 1 TO end STEP 2 × sub_size DO  
          FOR left ← 0 TO end STEP 2 × sub_size DO  
              mid ← MIN(left + sub_size - 1, end)  
              right ← MIN(left + 2 * sub_size - 1, end)  
              MERGE(arr, temp, left, mid, right)  
          ENDFOR  
      ENDFOR  
    END MERGE_SORT  
    
  • Principais pontos da implementação
    • Quando o tamanho do array não é múltiplo de 2, o problema é resolvido limitando os índices
    • A operação de mesclagem é feita por meio de loops
    • Grande potencial de melhoria de desempenho

Merge sort iterativo bottom-up (implementação em CUDA)

  • Melhora o desempenho ao executar o merge sort iterativo em paralelo
  • A operação de mesclagem é executada após calcular a quantidade de threads e blocos para o paralelismo
  • Estrutura principal do código
      void mergeSort(uint8_t* arr, uint8_t* temp, long long n) {  
          bool flipflop = true;  
          long long size;  
          for (size = 1; size < n; size *= 2) {  
              numThreads = max(n / (2 * size), (long long)1);  
              gridSize = (numThreads + THREADS_PER_BLOCK - 1) / THREADS_PER_BLOCK;  
              mergeKernel<<<gridSize, THREADS_PER_BLOCK>>>(flipflop ? arr : temp, flipflop ? temp : arr, size, n);  
              CUDA_CHECK(cudaGetLastError());  
              CUDA_CHECK(cudaDeviceSynchronize());  
              flipflop = !flipflop;  
          }  
          if (!flipflop) CUDA_CHECK(cudaMemcpy(arr, temp, n * sizeof(uint8_t), cudaMemcpyDeviceToDevice));  
      }  
    
  • Principais pontos
    • flipflop → alterna o local onde o resultado da mesclagem é armazenado
    • gridSize, THREADS_PER_BLOCK → fazem a paralelização da operação de mesclagem
    • mergeKernel → atribui uma tarefa de mesclagem única a cada thread
    • Gerenciamento de índices por meio do cálculo dos índices de threads e blocos

Resultados de desempenho

  • Merge sort bottom-up (CUDA) → a melhora de desempenho é clara
    • Arrays pequenos → a CPU é mais rápida
    • Arrays grandes → o CUDA tem vantagem de desempenho
  • thrust::sortdesempenho superior da GPU em arrays grandes
  • O ganho de desempenho com CUDA é limitado pelo overhead de transferência de dados

Conclusão e trabalhos futuros

  • Houve sucesso em melhorar o desempenho do merge sort baseado em CUDA
  • Principais aprendizados:
    • Aprendizado dos conceitos de processamento paralelo do CUDA e de estratégias de ajuste de desempenho
    • Merge sort iterativo → mais eficaz no CUDA do que a abordagem recursiva
    • Identificação de gargalos de desempenho específicos do CUDA, como sincronização de threads e transferência de memória
  • Próximas melhorias:
    • Separação e otimização das tarefas entre CPU e GPU
    • Testes de desempenho com arrays ainda maiores
    • Combinação de thrust::sort com código implementado pelo usuário
    • Otimização de desempenho com uso de memória compartilhada

2 comentários

 
kandk 2025-03-14

Parece que isso foi implementado em CUDA com radix sort; eu também já tive a experiência de implementar exatamente da mesma forma como referência.

 
GN⁺ 2025-03-13
Comentários do Hacker News
  • Não é uma forma rápida de ordenar na GPU. O algoritmo mais rápido em CUDA é o Onesweep, que usa técnicas complexas para aproveitar o paralelismo da GPU e superar limitações

    • A Linebender está trabalhando para aplicar essas ideias de forma mais portável para GPUs
    • Materiais relacionados podem ser encontrados na página wiki da Linebender
  • Como em outras opiniões, este algoritmo não é adequado. Algoritmos como o Onesweep são legais, mas difíceis de entender

    • Se você olhar para o algoritmo central, o radix sort, fica mais fácil de entender
    • O radix sort pode ser implementado de forma muito simples para paralelização e é uma abordagem bonita e elegante
  • É um problema de brinquedo interessante para explorar. Usar opções de ajuste de threads pode trazer ganhos de desempenho

    • Também é divertido usar o Nsight para identificar os fatores que afetam o desempenho
    • Também é preciso considerar outros algoritmos de ordenação. Ordenações em rede como bitonic sort exigem mais trabalho, mas podem rodar mais rápido em hardware paralelo
    • Fiz uma implementação simples no H100 que ordena 10M em cerca de 10ms. Com mais trabalho, dá para deixá-la ainda mais rápida
  • A linguagem Futhark torna mais conveniente usar esses algoritmos na GPU

    • É uma linguagem de altíssimo nível que compila para instruções de GPU e pode ser acessada como biblioteca Python
    • O site tem um exemplo de implementação de merge sort
  • Isso me lembrou de um pequeno projeto da época da faculdade em que implementei bitonic sort em CUDA

    • A implementação de bitonic sort pode ser vista no GitHub
  • As notas que explicam o conceito de indexação de threads na GPU são boas

    • Apresenta um post de blog pessoal sobre os benefícios de desempenho da ordenação vetorizada
  • Gosto de implementações rápidas de radix sort

    • Funciona facilmente com a Cuda driver API e, ao contrário do CUB, não fica limitada à runtime API
    • Também inclui Onesweep, mas não consegui fazê-lo funcionar
  • Tentei usar com Unity, mas não consegui superar o gargalo de transferência de dados

    • Mesmo ao usar compute shaders, há overhead, mas não é tão grande
  • Para valer a pena ordenar na GPU, é preciso ter arrays grandes

    • A transferência de dados entre a RAM e a GPU leva tempo
  • Para economizar seu tempo, em resumo: alguém escreveu um algoritmo de ordenação para GPU, mas ele era lento

    • Não é estado da arte, e não está claro se o autor sabe usar a GPU de forma eficaz
    • É apenas um experimento pessoal de programação em GPU