A computação paralela ou distribuída é em si uma coisa não muito trivial. E o ambiente de desenvolvimento deve suportar, e o especialista em DS deve ter as habilidades necessárias para conduzir a computação paralela, e a tarefa deve ser reduzida a um formato que possa ser dividido em partes, se houver. Mas com uma abordagem competente, é possível acelerar bastante a solução do problema com o R de thread único, se você tiver pelo menos um processador com vários núcleos (e quase todo mundo o possui agora), ajustado para o limite teórico de aceleração determinado pela lei de Amdal . No entanto, em alguns casos, mesmo isso pode ser contornado.
É uma continuação de publicações anteriores .
Abordagem típica
Como regra, quando um analista (especialista em DS, desenvolvedor ou escolher qualquer nome adequado para si mesmo) tenta acelerar a tarefa em um único computador e começa a passar do modo de thread único para multi-thread, ele faz isso de maneira padronizada. parApply
, foreach\%dopar%
etc. Você pode ver de forma compacta e inteligível, por exemplo, aqui: "Paralelismo em R" . 3 passos:
- faça rosca
core-1
- corra usando
foreach
, - colete as respostas e obtenha o resultado.
Para tarefas típicas de computação que ocupam 100% da CPU e não exigem a transferência de uma grande quantidade de informações de entrada, esta é a abordagem correta. O ponto principal que precisa de atenção é fornecer registro nos segmentos para poder controlar o processo. Sem registro, o voo continuará sem instrumentos.
No caso de tarefas "corporativas", quando são paralelas, surgem muitas dificuldades metodológicas adicionais que reduzem significativamente o efeito da abordagem direta acima:
- possível forte desequilíbrio da carga nos fluxos;
- Os requisitos de desempenho da CPU em uma única tarefa podem ser reduzidos com apenas algumas explosões bruscas;
- cada cálculo individual pode exigir uma quantidade significativa de memória para entrada e saída de resultados também de tamanho considerável;
- como parte de uma tarefa separada, pode haver uma mistura entre computação, trabalho com o disco e consulta de sistemas externos.
Esse é um cenário completamente típico quando, como parte do processo, você precisa obter um trabalho volumoso como entrada, ler dados do disco, pegar uma grande parte do banco de dados, solicitar sistemas externos e aguardar uma resposta deles (clássico - solicitação da API REST) e, em seguida, retornar N ao processo pai megabytes como resultado.
Map-reduce
por usuários, locais, documentos, endereços IP, datas, ... (adicione você mesmo). Nos casos mais tristes, a execução paralela pode ser maior que a de thread único. Problemas de falta de memória também podem ocorrer. Tudo se foi? Nem um pouco.
Maneira alternativa
Considere a tese de uma maneira de melhorar radicalmente a situação. Ao mesmo tempo, não esquecemos que vivemos no quadro de um zoológico completo. Circuito produtivo nos laptops *nix
e DS no Win * nix \ MacOS, mas é necessário que funcione uniformemente em todos os lugares.
- Micro tarefa: recebida na entrada do usuário, solicitou o banco de dados, solicitou 2 ICs externos via REST, baixou e analisou o diretório do disco, executou o cálculo, despejou o resultado em disk \ database. Usuários, por exemplo,
10^6
. - Passamos ao uso do pacote
future
e do adaptador universal doFuture
. - Se tarefas separadas são tais que, dentro de tarefas separadas, o tempo do processador é necessário em pequena quantidade (estamos aguardando respostas de sistemas de terceiros), o
doFuture
permite que você vá da divisão de doFuture
para a divisão em processos separados em uma linha (você pode ver os parâmetros de inicialização em *nix
no htop
) . - Esses processos podem ser criados muito mais que núcleos. Não ocorrerá um bloqueio porque os processos individuais estão no modo de espera a maior parte do tempo. Mas será necessário selecionar experimentalmente o número ideal de processos com base no ciclograma de um processo de processamento típico.
Resultado - a tarefa original é muitas vezes mais rápida. A aceleração pode ser ainda maior que o número de núcleos disponíveis.
Não há código conscientemente, pois a principal tarefa da publicação é compartilhar a abordagem e uma excelente família de pacotes future
.
PS
Existem algumas pequenas nuances que também precisam ser rastreadas:
- cada processo consumirá memória, incluindo dados recebidos e retornados. Um aumento no número de processos multiplicará os requisitos de RAM disponível.
doFuture
usa "magic" para determinar automaticamente a composição de variáveis e pacotes transferidos para o processo, mas você não deve deixar tudo correr sozinho, é melhor verificar.- nos processos, o controle
gc
explícito e a exclusão explícita de variáveis usando rm
não prejudicam. Isso não é uma panacéia e pode não funcionar , mas a indicação explícita de objetos excluídos ajudará. - após a conclusão do cálculo,
plan(sequential)
chamada plan(sequential)
. Isso fechará todos os processos e liberará a memória que eles ocupam. - Se você precisar transferir uma grande quantidade de dados para o processo, considere usar um armazenamento externo (disco, banco de dados). Não esqueça que os descritores não podem ser transferidos, a fonte deve ser aberta dentro do próprio processo.
Publicação anterior - “Processos de negócios em empresas: especulação e realidade. Nós lançamos luz com R " .