Paralelização em R e bash

Considere um caso em que você queira executar um mesmo script várias vezes, com dois argumentos fixos e outro que varia conforme os itens de uma lista. Uma opção é fazer um loop usando “for”. No entanto, o computador vai processar somente uma chamada de cada vez. Para não precisar esperar um script rodar para chamar o outro, você pode rodar tudo em paralelo.

Um núcleo (ou “core”) é geralmente a unidade de computação básica da CPU (ou “processor”), podendo executar um único contexto de programa (ou vários se ele suportar threads de hardware, como no caso de hyperthreading nos processadores da Intel). Uma CPU pode ter um ou mais núcleos para executar tarefas em um determinado momento. Veja mais sobre esses conceitos no post Paralelismo em python. Para saber quantos processadores e núcleos tem seu computador, basta usar o comando “lspcu” e olhar as linhas correspondentes, ou também usar o comando “htop” para ver o funcionamento de cada CPU.

O GNU parallel é uma ferramenta de shell para executar trabalhos em paralelo usando um ou mais processadores. Um trabalho pode ser um único comando ou um pequeno script, que será executado para cada uma das entradas de uma lista. Para instalação, use “apt-get install parallel”.

Veja esse script em R, que recebe três argumentos:

args = commandArgs(trailingOnly = TRUE)
input_cte1 = args[1]
input_cte2 = args[2]
input = args[3]
print(paste("1a entrada comum:",input_cte1,";","2a entrada comum:",input_cte2,";","Entrada variável:",input))

Chamando a rotina no terminal, é preciso informar os três argumentos conforme segue:

Rscript script.R 'arg_cte1' 'arg_cte2' 'arg1'

Caso queira rodar o mesmo script alterando o último argumento para ‘arg2’ e ‘arg3’ também – tudo em paralelo – execute o comando desse modo:

parallel --no-notice Rscript script.R 'arg_cte1' 'arg_cte2' ::: 'arg1' 'arg2' 'arg3'

Cada sinal de “dois pontos” indica o recebimento de uma entrada; se não informar nada, o parallel fica esperando uma entrada via digitação no terminal, e se informar argumentos a mais do que o número de “dois pontos”, os argumentos serão desconsiderados. A opção “–no-notice” evita a impressão de uma mensagem padrão ao inciar a ferramenta.

A saída deve ser impressa desse modo:

[1] "1a entrada comum: arg_cte1 ; 2a entrada comum: arg_cte2 ; Entrada variável: arg1"
[1] "1a entrada comum: arg_cte1 ; 2a entrada comum: arg_cte2 ; Entrada variável: arg2"
[1] "1a entrada comum: arg_cte1 ; 2a entrada comum: arg_cte2 ; Entrada variável: arg3"

Vários outros exemplos podem ser encontrados no tutorial do parallel, disponível no link.

Outro programa para dividir um processamento em vários núcleos é o MPI (Message Passing Interface). O MPICH e seus derivados formam as implementações amplamente utilizadas nos computadores mais rápidos do mundo. Ele pode ser instalado pelo repositório através do comando “sudo apt-get install mpich”. Veja um exemplo de uso para um script já preparado para trabalhar dividindo processamento (caso contrário, ele vai simplesmente copiar o processe para cada núcleo fazer a mesma coisa):

# Número de processadores
cpuinfo=$(cat /proc/cpuinfo | grep processor | wc -l)
# Execução do script
mpirun -np ${cpuinfo} ./script.sh

O comando “mpirun” é implementado por muitas implementações MPI. Nunca foi, no entanto, padronizado e sempre houve diferenças, muitas vezes sutis, entre as implementações. O “mpiexec” é definido no padrão MPI, nesse caso um link direto para “mpiexec.hydra” – Hydra é um determinado sistema de gerenciamento de processos.

Alternativas

Caso monte um loop com shell script e execute um script em R dentro, ele só vai executar o próximo elemento do loop quando finalizar o atual. Além disso, esse processamento vai usar apenas um processador, a não ser que nada dentro do script em R diga o contrário.

Uma tática para tornar esse script mais rápido, dividindo o processamento em vários núcleos, é o de colocar cada uma das rodadas do loop em background. Desse modo, um script não espera o outro terminar para iniciar, e o gerenciador de processamentos do sistema operacional designa outro(s) núcleo(s) para esse processamento. Isso pode ser feito acrescentando-se um e comercial no fim da linha, conforme segue:

#!/bin/bash
# Script para executar loop com processos em paralelo

for p in {1..5}; do
	Rscript teste.R $p &
done
wait

echo "Todos os processos foram finalizados"

O comando “wait” serve para fazer o script principal esperar a execução de TODOS os scripts rodando em paralelo para seguir em frente e imprimir a mensagem final. Se existirem mais processos executando em paralelo do que núcleos, o gerenciador do sistema operacional se preocupa em ficar trocando os processos de núcleo. O número de núcleos de seu computador pode ser visto através do seguinte comando:

ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w

O número de núcleos pode ser visto facilmente através do comando htop, que permite também monitorar o uso de cada um. Caso tenha muitos processos em execução e prefira limitar pelo número de núcleos de sua máquina, veja algumas sugestão nessa página do StackOverflow – Parallelize Bash script with maximum number of processes.

Com relação ao script em R utilizado, aqui vai uma sugestão simples com um loop infinito recebendo o número do processo como argumento:

#!/usr/bin/Rscript
# Script de loop infinito

# Receber argumento de entrada
args = commandArgs(trailingOnly = TRUE)
arg1 = args[1]

while (1 == 1) {
    nprocesso = arg1
    print(nprocesso)
    #q()
}

Outra alternativa é a de abrir uma screen (instalável usando “sudo apt-get install screen”) para cada elemento do loop, conforme segue:

#!/bin/bash
# Script para executar loop com processos em paralelo

for p in {1..5}; do
	screen -dmS s$p bash -c 'Rscript teste.R; exec bash'
done

Os parâmetros utilizado são:

  • -S: cria a screen com nome “s1, s2, …” conforme o valor da variável “p”
  • -d: desanexa a screen (deixa em “segundo plano”)
  • -m: para impor a criação da tela, independentemente de a tela ser chamada de outra tela ou não

Ainda não acertei para fazer o script em R receber argumentos dessa forma. Durante ou após a execução, você pode abrir a janela desejada para acompanhar o processo através do comando “screen -s1” (por exemplo, para abrir a sessão s1). Veja mais sobre o comando screen clicando no link.

Compartilhe :)

One comment

Leave a Reply

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.