본문 바로가기
프로그래밍/병렬프로그래밍

[MPI] MPI Derived Datatypes (파생 데이터타입)

by 별준 2021. 11. 13.

References

  • An Introduction to Parallel Programming

Contents

  • MPI Derived Datatypes
  • MPI_Type_create_struct, MPI_Type_commit, MPI_Type_free

분산 메모리 시스템에서의 통신은 로컬 연산보다 훨씬 cost가 큽니다. 예를 들어, 한 노드에서 다른 노드로 double형을 전송하는 것은 노드의 로컬 메모리에 저장된 두 개의 double형을 덧셈하는 것보다 훨씬 더 시간이 많이 걸립니다. 게다가, 다중 메세지에서 고정된 양의 데이터를 전송하는 cost는 보통 하나의 메세지에서 동일한 양의 데이터를 전송하는 것보다 훨씬 큽니다.

따라서, 하나의 send/recieve 쌍보다 다음의 for 루프가 훨씬 느릴 것으로 예상됩니다.

double x[1000];
...
if (my_rank == 0)
	for (int i = 0; i < 1000; i++)
    	MPI_Send(&x[i], 1, MPI_DOUBLE, 1, 0, comm);
else /* my_rank == 1 */
	for (int i = 0; i < 1000; i++)
    	MPI_Recv(&x[i], 1, MPI_DOUBLE, 0, 0, comm, &status);

if (my_rank == 0)
	MPI_Send(x, 1000, MPI_DOUBLE, 1, 0, comm);
else /* my_rank == 1 */
	MPI_Recv(x, 1000, MPI_DOUBLE, 0, 0, comm, &status);

어떤 시스템에서는 for루프에서의 Send/Receive가 50배 더 느린 경우도 있고, 다른 시스템에서는 100배 더 느린 경우가 있을 수도 있습니다. 따라서, 전송하는 메세지의 총 개수를 줄인다면, 프로그램의 성능을 더욱 향상시킬 수 있습니다.

MPI에서는 다중메세지가 필요할 수 있는 데이터의 통합에 다음의 3가지 접근 방법을 제공합니다. 

  1. count 인수를 사용하는 다양한 통신 함수
  2. derived datatypes
  3. MPI_Pack/Unpck

이 3가지 방법 중에서 이번에는 derived datatype, 파생된 데이터타입을 구축하는 방법에 대해서 알아보도록 하겠습니다.


Derived Datatypes

MPI에서 derived datatype은 메모리에 존재하는 데이터의 타입과 상대적인 위치를 저장하여 데이터를 표현하는데 사용됩니다. 이 아이디어는 데이터를 전송하는 함수가 이 데이터가 존재하는 메모리의 상대적인 위치와 타입을 알고 있다면, 그 데이터들이 전송되기 전에 메모리로부터 데이터들을 분산시킬 수 있다는 것입니다. 비슷하게, 데이터를 받는 함수도 마찬가지입니다. 

2021.11.09 - [프로그래밍/병렬프로그래밍] - [MPI] 사다리꼴 공식 (trapezoidal rule) 병렬화 - 1

2021.11.10 - [프로그래밍/병렬프로그래밍] - [MPI] 사다리꼴 공식 (trapezoidal rule) 병렬화 - 2

이전에 살펴본 사다리꼴 공식을 병렬화하는 코드를 통해 파생 데이터타입을 어떻게 사용하는지 살펴보겠습니다.

 

https://github.com/junstar92/parallel_programming_study/blob/master/mpi/05_mpi_trap3.c

 

GitHub - junstar92/parallel_programming_study

Contribute to junstar92/parallel_programming_study development by creating an account on GitHub.

github.com

위 예제코드에서 a와 b, n을 입력받는 Get_input 함수에서는 숫자를 입력받고, 이 데이터들을 각 프로세스에 전달하기 위해서 MPI_Bcast 함수를 3번 호출해야합니다.

void Get_input(int my_rank, int comm_sz, double* p_a, double* p_b, int* p_n)
{
    if (my_rank == 0) {
        printf("Enter a, b, and n\n");
        scanf("%lf %lf %d", p_a, p_b, p_n);
    }

    MPI_Bcast(p_a, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
    MPI_Bcast(p_b, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
    MPI_Bcast(p_n, 1, MPI_INT, 0, MPI_COMM_WORLD);
}

프로세스 0에서 a,b, 그리고 n의 값을 읽고, 다른 프로세스에게 전달하는 것이죠.

 

3번이나 호출하는 것이 비효율적으로 느껴집니다. 이때, 파생 데이터타입을 사용하면 2개의 double형과 하나의 int형으로 구성된 하나의 파생 데이터타입을 만들 수 있고, 이렇게되면 MPI_Bcast를 한 번만 호출하면 필요한 정보를 모든 프로세스에 전달할 수 있게 됩니다.

 

일반적으로 파생 데이터타입은 각 datatype을 기본 MPI datatype으로 변환한 시퀀스로 구성됩니다. 위 예제 코드에서 프로세스 0의 변수 a, b, n이 다음 주소의 메모리 위치에 저장되어 있다고 가정해봅시다.

그럼 파생 데이터타입은 다음과 같이 각 데이터들을 나타냅니다.

각 쌍의 첫 번째 항목은 데이터의 타입에 해당되고, 두 번째 항목은 이 데이터타입(파생된)의 첫 번째 요소로부터의 상대적인 위치를 나타냅니다. 이 타입은 변수 a부터 시작하므로, a에 해당되는 상대 위치는 0이 됩니다. b는 a의 위치로부터 16바이트 떨어져있고, n은 a로부터 24바이트 떨어져있습니다. 

위의 정보들을 각 배열로 저장하여, MPI_Type_create_struct 함수를 사용하여 서로 다른 기본 타입을 갖는 데이터들로 구성된 파생 데이터타입을 생성할 수 있습니다.

int MPI_Type_create_struct(
	int            count                    /* in  */,
    int            array_of_blocklengths[]  /* in  */,
    MPI_Aint       array_of_displacements[] /* in  */,
    MPI_Datatype   array_of_types[]         /* in  */,
    MPI_Datatype*  new_type_p               /* out */);

count 인수는 datatype에 존재하는 데이터의 수입니다. 따라서 위 예제에서는 3이 됩니다. 그 다음 배열인 인수들은 그 배열의 크기가 3이어야하는데, 첫 번째 배열인 array_of_blocklengths는 datatype에 존재하는 데이터가 subarray가 될 수 있는 가능성을 제공합니다. 예를 들어, datatype의 첫 번째 데이터가 크기가 5인 배열이라면 다음의 값을 갖습니다.

array_of_blocklengths[0] = 5;

위 예제에서는 단순히 double, double, int형의 데이터이기 때문에 배열이 존재하지 않습니다. 따라서 다음과 같이 간단하게 정의할 수 있습니다. (각 데이터들이 1개의 값을 가지고 있다는 의미)

int array_of_blocklengths[3] = {1, 1, 1};

두 번째 배열인 array_displacements는 첫 번째 데이터로부터의 상대 위치를 설정합니다. 따라서 다음과 같습니다.

array_of_displacements[] = {0, 16, 24};

각 변수의 주소가 24, 40, 48이라고 가정했기 때문에 쉽게 입력할 수 있지만, 실제 프로그램에서는 각 변수의 메모리 주소를 알아야 이 값들을 채울 수 있습니다. 이 값을 구하기 위해서 MPI_Get_address 함수를 사용하면 됩니다.

int MPI_Get_address(
	void*        location_p /* in  */,
    MPI_Aint*    address_p  /* out */);

이 함수는 location_p에 참조된 메모리의 주소를 리턴하고, 그 리턴되는 주소는 address_p에 저장됩니다. 여기서 사용되는 MPI_Aint는 시스템의 주소를 저장할 수 있을만큼 큰 정수형 타입입니다. 따라서, array_of_displacements의 값을 구하기 위해서는 다음의 코드를 사용합니다.

MPI_Aint a_addr, b_addr, n_addr;

MPI_Get_address(&a, &a_addr);
array_of_displacements[0] = 0;
MPI_Get_address(&b, &b_addr);
array_of_displacements[1] = b_addr - a_addr;
MPI_Get_address(&n, &n_addr);
array_of_displacements[2] = n_addr - a_addr;

마지막 배열 array_of_types는 MPI_Datatype형인 데이터 타입으로 구성됩니다.

MPI_Datatype array_of_types[3] = {MPI_DOUBLE, MPI_DOUBLE, MPI_INT};

 

그리고 함수를 호출하여 새로운 데이터타입을 생성할 수 있습니다.

MPI_Datatype input_mpi_t;
...
MPI_Type_create_struct(3, array_of_blocklengths,
		array_of_displacements, array_of_types,
        &input_mpi_t);

그리고, 통신 함수에서 생성한 input_mpi_t를 사용하기 전에 먼저 다음과 같이 MPI_Type_commit 함수를 사용하여 새로운 input_mpi_t를 커밋(commit)해야 합니다.

int MPI_Type_commit(MPI_Datatype* new_mpi_t_p /* in/out */);

위의 함수 호출은 통신 함수에서 이 데이터 타입을 사용하기 위해 내부 표현(internal representation)을 최적화하도록 해줍니다. 

 

이제 new_mpi_t를 사용하기 위해서 각 프로세스에서 MPI_Bcast를 호출하면 됩니다. 그러면 하나의 기본 MPI 데이터타입을 사용하는 것처럼 input_mpi_t를 사용할 수 있습니다.

 

새로운 타입을 생성하는 것은 MPI 구현 부분에서 내부적으로 추가적인 저장 공간을 할당합니다. 따라서 새로운 타입 사용이 끝나면 MPI_Type_free 함수를 호출하여 할당된 추가 저장공간을 해제할 수 있습니다.

int MPI_Type_free(MPI_Datatype* old_mpi_t_p /* in/out */);

 

이를 사용하여 a, b, n을 읽는 Get_input 함수를 새로운 build_mpi_type 함수를 사용하여 다음과 같이 구현할 수 있습니다.

void Get_input(int my_rank, int comm_sz, double* p_a, double* p_b, int* p_n)
{
    MPI_Datatype input_mpi_t;

    Build_mpi_type(p_a, p_b, p_n, &input_mpi_t);

    if (my_rank == 0) {
        printf("Enter a, b, and n\n");
        scanf("%lf %lf %d", p_a, p_b, p_n);
    }

    MPI_Bcast(p_a, 1, input_mpi_t, 0, MPI_COMM_WORLD);

    MPI_Type_free(&input_mpi_t);
}

void Build_mpi_type(double* p_a, double* p_b, int* p_n, MPI_Datatype* p_input_mpi_t)
{
    int array_of_blocklengths[3] = {1, 1, 1};
    MPI_Datatype array_of_types[3] = {MPI_DOUBLE, MPI_DOUBLE, MPI_INT};
    MPI_Aint a_addr, b_addr, n_addr;
    MPI_Aint array_of_displacements[3] = {0};

    MPI_Get_address(p_a, &a_addr);
    MPI_Get_address(p_b, &b_addr);
    MPI_Get_address(p_n, &n_addr);

    array_of_displacements[1] = b_addr - a_addr;
    array_of_displacements[2] = n_addr - a_addr;
    
    MPI_Type_create_struct(3, array_of_blocklengths, array_of_displacements,
                            array_of_types, p_input_mpi_t);
    MPI_Type_commit(p_input_mpi_t);
}

 

이렇게 함으로써, Get_input에서 3번의 MPI_Bcast 호출은 1번으로 감소시켜 프로그램 성능을 향상시킬 수 있습니다.

 

마찬가지로 전체 코드는 아래 링크 참조바랍니다 !

https://github.com/junstar92/parallel_programming_study/blob/master/mpi/06_mpi_trap4.c

 

GitHub - junstar92/parallel_programming_study

Contribute to junstar92/parallel_programming_study development by creating an account on GitHub.

github.com

 

댓글