- Com a Lei de Moore chegando aos seus limites, o hardware evolui do foco em acelerar um único núcleo para múltiplos núcleos e processamento paralelo
- O processamento paralelo se divide em várias formas, como paralelismo de dados, paralelismo de modelo e paralelismo de pipeline, sendo usado de forma combinada em sistemas modernos de deep learning
- A paralelização acontece em diversas camadas, como SIMD (paralelismo de dados no nível de instrução), paralelismo de threads/cores e paralelismo massivo em GPU
- As linguagens, bibliotecas e ferramentas para processamento paralelo em sua maioria são extensões “acopladas” a linguagens sequenciais já existentes, por isso ganha atenção a tendência de integrar o paralelismo nativamente à linguagem (como no Mojo)
- A otimização prática de desempenho, como redução de compartilhamento de linhas de cache (interações desnecessárias), particionamento eficiente de memória e vetorização automática, é um desafio importante
Motivação do paralelismo e evolução do hardware
- No início, o desempenho melhorava naturalmente com a miniaturização dos transistores e o aumento do clock, mas chegou a limites físicos por causa de restrições térmicas e de processo de fabricação
- Depois disso, a arquitetura multicore virou padrão, com dezenas a centenas de núcleos em uma única CPU
Formas gerais de paralelismo
- Paralelismo de dados: aplicar a mesma operação simultaneamente a muitos dados (ex.: soma de vetores)
- Paralelismo de modelo: distribuir um modelo entre vários dispositivos
- Paralelismo de pipeline: dividir o cálculo em várias etapas, com cada etapa operando ao mesmo tempo
SIMD (single instruction, multiple data) e vetorização
- SIMD é uma forma de processar vários dados (vetores) com uma única instrução, com suporte em vários ISAs como ARM NEON e x86 SSE/AVX
- Por meio de intrinsics em C/C++, é possível controlar explicitamente operações vetoriais; compiladores também oferecem vetorização automática, embora com limitações
- Na prática, processa-se primeiro o volume correspondente ao comprimento do vetor e, em seguida, os dados restantes são tratados com operações escalares
Paralelização em CPU
- Usa-se threads para execução paralela em múltiplos núcleos, com APIs por linguagem e suporte do escalonador do sistema operacional
- Como o custo de criação e destruição de threads é alto, é mais eficiente dividir o trabalho em uma quantidade adequada de threads (próxima ao número de núcleos) em relação ao tamanho dos dados
- É importante otimizar o "false sharing" de linha de cache (queda de desempenho quando threads diferentes acessam variáveis independentes na mesma linha de cache), usando por exemplo
std::hardware_destructive_interference_size do C++17
- É necessário aplicar padding/alinhamento para que cada thread escreva em uma área de dados separada
Paralelização em GPU
- A GPU é especializada em processamento paralelo massivo de dados por meio de milhares de pequenos núcleos
- CUDA/OpenCL: executam funções kernel em unidades de dezenas a dezenas de milhares de threads/blocos; internamente usam o modelo SIMT (single instruction, multiple threads)
- O funcionamento por work groups/warps e a minimização de divergência de ramificação (branch divergence) são extremamente importantes para o desempenho
- Hierarquia de memória: é necessário otimizar de forma hierárquica registradores de thread, memória compartilhada por bloco e memória global
Triton: DSL de kernels GPU baseada em Python
- Triton é uma DSL embutida em Python, com suporte a compilação JIT e múltiplos backends (MLIR/LLVM/PTX etc.)
- Permite escrever código de kernel em Python de alto nível, com suporte a paralelização, particionamento e masking automáticos
- Entrega de 75% a 90% do desempenho do NVIDIA cuDNN, reduzindo drasticamente a complexidade de desenvolvimento
Tornando o paralelismo nativo na linguagem: Mojo
- Mojo é uma nova linguagem criada por Chris Lattner, desenvolvedor de LLVM/MLIR, com suporte em nível de linguagem para paralelismo e compilação especializada em hardware
- Tipos vetoriais SIMD, funções vetorizadas e distinção entre memória de host/device mostram que o paralelismo está embutido no sistema de tipos e na estrutura da linguagem
- Até loops em estilo Python podem ser vetorizados automaticamente, permitindo desempenho sem necessidade de controle explícito de baixo nível
Conclusão e perspectivas
- A programação paralela moderna é composta pela combinação de vários hardwares e modelos de paralelismo, e o suporte da própria linguagem vem se tornando cada vez mais importante
- Com a ascensão de linguagens e ferramentas de próxima geração para paralelismo, como Mojo, Triton e JAX, a paralelização está evoluindo para algo mais intuitivo e produtivo
- Na programação paralela, o desempenho máximo só pode ser alcançado quando arquitetura de hardware, otimização de memória e suporte da linguagem se combinam de forma orgânica
Ainda não há comentários.