| | | | |

3.3 Comunicações coletivas

Ajude a manter o site livre, gratuito e sem propagandas. Colabore!

Em revisão

Nesta seção, vamos discutir sobre rotinas de comunicações MPI coletivas. Basicamente, rotinas de sincronização, envio e recebimento de dados envolvendo múltiplas instâncias de processamento ao mesmo tempo.

3.3.1 Barreira de sincronização

Em revisão

Podemos forçar a sincronização de todos os processos em um determinado ponto do código utilizando a rotina de sincronização MPI_Barrier

int MPI_Barrier (MPI_Comm comm)

Quando um processo encontra esta rotina ele aguarda todos os demais processos. No momento em que todos os processo tiverem alcançados esta rotina, todos são liberados para seguirem com suas computações.

Exemplo 3.3.1.

No Código barrier.cc, abaixo, cada instância de processamento aguarda randomicamente até 3 segundos para alcançar a rotina de sincronização MPI_Barrier na linha 35. Em seguida, elas são liberadas juntas. Estude o código.

Código: barrier.cc
1#include <stdio.h>
2#include <stdlib.h>
3#include <unistd.h>
4
5// API MPI
6#include <mpi.h>
7
8int main(int argc, char** argv) {
9
10  // Inicializa o MPI
11  MPI_Init(NULL, NULL);
12
13  // numero total de processos
14  int world_size;
15  MPI_Comm_size(MPI_COMM_WORLD, &world_size);
16
17  // ID (rank) do processo
18  int world_rank;
19  MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
20
21  // cronometro
22  time_t init = time (NULL);
23
24  // semente do gerador randomico
25  srand (init + world_rank);
26
27  // max. of 3 segundos
28  size_t espera = rand() % 3000000;
29
30  usleep (espera);
31
32  printf ("%d chegou na barreira: %d s.\n",
33          world_rank, (time (NULL) - init));
34
35  MPI_Barrier (MPI_COMM_WORLD);
36
37  printf ("%d saiu da  barreira: %d s.\n",
38          world_rank, (time (NULL) - init));
39
40
41  // Finaliza o MPI
42  MPI_Finalize();
43
44  return 0;
45}

Vamos observar o seguinte teste de rodagem

$ mpic++ barrier.cc
$ mpirun -np 2 a.out
1 chegou na barreira: 1 s.
0 chegou na barreira: 3 s.
0 saiu da  barreira: 3 s.
1 saiu da  barreira: 3 s.

Neste caso, o processo 1 foi o primeiro a alcançar a barreira de sincronização e permaneceu esperando aproximadamente 2 segundos até que o processo 0 alcançasse a barreira. Imediatamente após o processo 1 chegar a barreira, ambos seguiram suas computações. Rode várias vezes este código e analise as saídas!

Observação 3.3.1.

No Código barrier.cc acima, o gerador de números randômicos é inicializado com a semente

srand (init + world_rank);

onde, init é o tempo colhido pela rotina time no início do processamento (veja as linhas 22-25). Observamos que somar o identificado rank garante que cada processo inicie o gerador randômico com uma semente diferente.

3.3.2 Transmissão coletiva

Em revisão

A rotina de transmissão de dados MPI_Bcast permite o envio de dados de um processo para todos os demais. Sua sintaxe é a seguinte

int MPI_Bcast(
  void *buffer,
  int count,
  MPI_Datatype datatype,
  int root,
  MPI_Comm comm)

O primeiro argumento buffer aponta para o endereço da memória do dado a ser transmitido. O argumento count é a quantidade de dados sucessivos que serão transmitidos (tamanho do buffer). O tipo de dado é informado no argumento datatype. Por fim, root é o identificador rank do processo que está transmitindo e comm é o comunicador.

Exemplo 3.3.2.

No seguinte Código bcast.cc, o processo 0 inicializa a variável de ponto flutuante x=π (linhas 22-23) e, então, transmite ela para todos os demais processos (linhas 25-26). Por fim, cada processo imprime no terminal o valor alocado na sua variável x (linhas 28-29).

Código: bcast.cc
1#include <stdio.h>
2#include <math.h>
3
4// API MPI
5#include <mpi.h>
6
7int main(int argc, char** argv) {
8
9  // Inicializa o MPI
10  MPI_Init(NULL, NULL);
11
12  // numero total de processos
13  int world_size;
14  MPI_Comm_size(MPI_COMM_WORLD, &world_size);
15
16  // ID (rank) do processo
17  int world_rank;
18  MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
19
20  double x;
21
22  if (world_rank == 0)
23    x = M_PI;
24
25  MPI_Bcast (&x, 1, MPI_DOUBLE,
26             0, MPI_COMM_WORLD);
27
28  printf ("Processo %d x = %f\n",
29          world_rank, x);
30
31  // Finaliza o MPI
32  MPI_Finalize();
33
34  return 0;
35}

Vejamos o seguinte teste de rodagem

$ mpic++ bcast.cc
$ mpirun -np 3 ./a.out
Processo 0 x = 3.141593
Processo 1 x = 3.141593
Processo 2 x = 3.141593

3.3.3 Distribuição coletiva de dados

Em revisão

A rotina MPI_Scatter permite que um processo faça a distribuição uniforme de pedaços sequenciais de um array de dados para todos os demais processos. Sua sintaxe é a seguinte

int MPI_Scatter(
  const void *sendbuf,
  int sendcount,
  MPI_Datatype sendtype,
  void *recvbuf,
  int recvcount,
  MPI_Datatype recvtype,
  int root,
  MPI_Comm comm)

O primeiro argumento sendbuf aponta para o endereço de memória do array de dados a ser distribuído. O argumento sendcount é o tamanho do pedaço e sendtype é o tipo de dado a ser transmitido. Os argumentos recvbuf, recvcount e recvtype se referem ao ponteiro para o local de memória onde o dado recebido será alocado, o tamanho do pedaço a ser recebido e o tipo de dado, respectivamente. Por fim, o argumento root identifica o processo de origem da distribuição dos dados e comm é o comunicador.

Exemplo 3.3.3.

No Código scatter.cc abaixo, o processo 0 aloca o vetor

v=(1,2,,10), (3.5)

distribui pedaços sequenciais do vetor para cada processo no comunicador MPI_COMM_WORLD e, então, cada processo computa a soma dos elementos recebidos.

Código: scatter.cc
1#include <stdio.h>
2#include <stdlib.h>
3#include <math.h>
4
5// API MPI
6#include <mpi.h>
7
8int main(int argc, char** argv) {
9
10  // Inicializa o MPI
11  MPI_Init(NULL, NULL);
12
13  // numero total de processos
14  int world_size;
15  MPI_Comm_size(MPI_COMM_WORLD, &world_size);
16
17  // ID (rank) do processo
18  int world_rank;
19  MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
20
21  const size_t n = 10;
22  double *v = NULL;
23
24  if (world_rank == 0) {
25    v = (double*) malloc (n * sizeof(double));
26
27    for (size_t i=0; i<n; i++)
28      v[i] = i+1;
29  }
30
31  size_t my_n = n/world_size;
32  double *my_v = (double*) malloc (my_n * sizeof(double));
33
34  MPI_Scatter (v, my_n, MPI_DOUBLE,
35               my_v, my_n, MPI_DOUBLE,
36               0, MPI_COMM_WORLD);
37
38  double soma = 0.0;
39  for (size_t i=0; i<my_n; i++) {
40    soma += my_v[i];
41  }
42
43  printf ("Processo %d soma = %f\n",
44          world_rank, soma);
45
46  free (v);
47  free (my_v);
48
49  // Finaliza o MPI
50  MPI_Finalize();
51
52  return 0;
53}

Vejamos o seguinte teste de rodagem

$ mpic++ scatter.cc -lgsl -lgslcblas
$ mpirun -np 2 ./a.out
Processo 0 soma = 15.000000
Processo 1 soma = 40.000000

Neste caso, o processo 0 recebe

my_v=(1,2,3,4,5), (3.6)

enquanto o processo 1 recebe

my_v=(6,7,8,9,10). (3.7)
Observação 3.3.2.

Note que o MPI_Scatter distribuí apenas pedaços de arrays de mesmo tamanho. Para a distribuição de pedaços de tamanhos diferentes entre os processos, pode-se usar a rotina MPI_Scatterv. Veja os exercícios resolvidos abaixo.

3.3.4 Recebimento coletivo de dados distribuídos

Em revisão

A rotina MPI_Gather, permite que um processo receba simultaneamente dados que estão distribuídos entre os demais processos. Sua sintaxe é a seguinte

int MPI_Gather(
  const void *sendbuf,
  int sendcount,
  MPI_Datatype sendtype,
  void *recvbuf,
  int recvcount,
  MPI_Datatype recvtype,
  int root,
  MPI_Comm comm)

Sua sintaxe é parecida com a da rotina MPI_Scatter. Veja lá! Aqui, root é o identificador rank do processo receptor.

Exemplo 3.3.4.

No Código gather.cc, cada processo i aloca um vetor

my_v=(5i+1,5i+2,,5i+5), (3.8)

então, o processo 0 recebe estes vetores alocando-os em um único vetor

𝚟=(1,2,,5np), (3.9)

onde np é o número de processos inicializados.

Código: gather.cc
1#include <stdio.h>
2#include <math.h>
3
4// API MPI
5#include <mpi.h>
6
7int main(int argc, char** argv) {
8
9  // Inicializa o MPI
10  MPI_Init(NULL, NULL);
11
12  // numero total de processos
13  int world_size;
14  MPI_Comm_size(MPI_COMM_WORLD, &world_size);
15
16  // ID (rank) do processo
17  int world_rank;
18  MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
19
20  const size_t my_n = 5;
21  double *my_v = (double*) malloc (my_n * sizeof(double));
22  for (size_t i=0; i<my_n; i++)
23    my_v[i] = 5*world_rank+i+1;
24
25  const size_t n = world_size*my_n;
26  double *v = NULL;
27  if (world_rank == 0) {
28    v = (double*) malloc (n * sizeof(double));
29  }
30
31  MPI_Gather (my_v, my_n, MPI_DOUBLE,
32              v, my_n, MPI_DOUBLE,
33              0, MPI_COMM_WORLD);
34
35  if (world_rank == 0) {
36    printf ("v = ");
37    for (size_t i=0; i<n; i++)
38      printf ("%f ", v[i]);
39    printf("\n");
40  }
41
42  // Finaliza o MPI
43  MPI_Finalize();
44
45  return 0;
46}

Vejamos o seguinte teste de rodagem

$ mpic++ gather.cc -lgsl -lgslcblas
$ mpirun -np 2 ./a.out
v = 1.000000 2.000000 3.000000 4.000000 5.000000
6.000000 7.000000 8.000000 9.000000 10.000000

Neste caso, o processo 0 aloca

my_v=(1,2,3,4,5) (3.10)

e o processo 1 aloca

my_v=(6,7,8,9,10). (3.11)

Então, o processo 0, recebe os dois pedaços de cada um, formando o vetor

𝚟=(1,2,,10). (3.12)

Verifique!

Observação 3.3.3.

Para recebimento de pedaços distribuídos e de tamanhos diferentes, pode-se usar a rotina MPI_Gatherv. Veja os exercícios resolvidos abaixo.

Observação 3.3.4.

Observamos que com a rotina MPI_Gather podemos juntar pedaços de dados distribuídos em um único processo. Analogamente, a rotina MPI_Allgather nos permite juntar os pedaços de dados distribuídos e ter uma cópia do todo em cada um dos processos. Sua sintaxe é a seguinte

int MPI_Allgather(
  const void *sendbuf,
  int  sendcount,
  MPI_Datatype sendtype,
  void *recvbuf,
  int recvcount,
  MPI_Datatype recvtype,
  MPI_Comm comm)

Note que esta rotina não contém o argumento root, pois neste caso todos os processos receberam os dados juntados na variável recvbuf!

Exercícios

Em revisão

E. 3.3.1.

Faça um código MPI para computar a média aritmética simples de n números randômicos em ponto flutuante.

E. 3.3.2.

Faça um código MPI para computar o produto interno de dois vetores de n elementos randômicos em ponto flutuante.

E. 3.3.3.

Faça um código MPI para computar a norma L2 de um vetor de n elementos randômicos em ponto flutuante.

E. 3.3.4.

Faça um código MPI para computar

erf(x)=2π0xet2𝑑t (3.13)

usando a regra composta do ponto médio.

E. 3.3.5.

Faça uma implementação MPI do método de Jacobi para computar a solução de um sistema Ax=b n×n. Inicialize A e b com números randômicos em ponto flutuante.


Envie seu comentário

As informações preenchidas são enviadas por e-mail para o desenvolvedor do site e tratadas de forma privada. Consulte a Política de Uso de Dados para mais informações. Aproveito para agradecer a todas/os que de forma assídua ou esporádica contribuem enviando correções, sugestões e críticas!