Clay Breshears

Dr. Dobb's Bloggers

Defining an MPI Derived Type for Game of Life

August 30, 2013

What if you need to send data that is composed of two or more primitive types?

When using MPI to send data between distributed processes, you must specify the type of data being sent so that the MPI library can bundle up the information and ship it out to all receiving processes and a receiving process can know how to place the data into local memory. The library comes with predefined primitive data types — character, integer, floating-point. I can send an array of a primitive data type by setting the parameter in send and receive functions specifying the number of objects being communicated.

For many instances of data exchanges these primitive types are sufficient. But what if you need to send data that is composed of two or more primitive types? For example, think of a simple personnel record that contains a name, address, contact phone number, salary, number of dependents, and an indicator of the retirement plan that the employee is enrolled in. (I'm a bit troubled that I keep defaulting to employee records when I want to give an example of heterogeneous record types. It must stem from learning COBOL as a first programming language.) This record (structure) contains character strings, integers, and a floating-point value.

If two processes are required to share such a record, you could send and receive each different data type with a separate message. This would quickly clutter up your code and be a drag on performance since each message would have the overhead associated with sending a message. Alternatively, especially if the data were gathered into a logical record within memory, you can define a derived data type describing the structure of the data and send/receive a single message to communicate the record. The process for setting up a derived data type in an MPI code is the subject of this post. The example I will be using is a derived type that corresponds to the List structure I defined for my list-based version of Game of Life for a distributed-memory environment.

Every MPI derived type is ultimately made up of MPI primitive types. Besides specifying what primitive types make up a user-defined type, I need to describe how those are primitive types are composed together. This is done through a typemap, which is simply the relative displacement of each primitive type element from the base address of the derived type in memory. Before I describe how to define the myList MPI derived type, here is the type declaration that creates a List data structure in my Game of Life code.

For the Game of Life simulation, a list holds pairs of coordinate indices of cells that are of interest. The ListEntry type has two integers, row and col, for this pair. The List type itself is an array of ListEntry elements and a count of the number of pairs currently on the list. The constant MAXLIST is the maximum number of coordinate pairs that a list can hold. The fixed size of the List data structure has some obvious drawbacks, but it is what allows me to define a derived type that can be sent and received between MPI processes.

If you remember my previous post that used myList, there was a call to MakeList() to create the derived type. Before I describe that function, I want to go over the MakeListEntry() function, called from MakeList(), to define the derived type for an individual grid cell coordinate pair element. Mimicking the definition of the List type above, the function calls I employ are modeled on the structure of the struct declarations.

The purpose of the MakeListEntry() function is to define an MPI derived type for a single grid coordinate pair structure and return that new derived type through the parameter. As I mentioned above, I need to define a derived type by noting the primitive types that make up the new type, the number of items, and the displacement of each different primitive type from the start address of the structure. In this case, I have integers (row and col) that are both MPI_INT types, thus I only need one displacement for the pair. The relative address of the first of these two elements is zero bytes from the base address. This information is encoded in the three declarations of the function.

The entryType array (of one element) holds a collection of MPI_Datatype primitive types in the order that they are defined within the derived type structure. The count array (one corresponding element for each element in the entryType array) holds the number of copies for each primitive type item within the derived type structure. The MPI_Aint type is used to hold a valid C type address. In my case, it is used as the declared type of the array of displacements for each type listed in the entryType array. With only one pair of MPI_INT elements that make up my derived type, this displacement is zero bytes, which is held in the (one element) array addr.

(Note: I'm making the assumption that two integers declared as they were in ListEntry will be right next to each other, address-wise. I don't expect consecutively declared int types will be separately aligned on some 8-byte boundary. When I create the derived type that holds an array of the myListEntry types, I will be sure to compute displacements that may be offset by "blank" bytes to satisfy memory boundaries between the count integer and array of list entries.)

Once all the information is assembled, I call MPI_Create_type_struct() to create the typemap for my new derived type in the myListEntry. Before the new type can actually be used in communication functions, it must be committed. This is done with a call to MPI_Type_commit(). Plus, after a derived type has been committed, it can be used as a primitive type in the definition of other derived types as I show now.

With the single grid coordinate pair as a derived type, I can create a type holding an array of coordinate pairs that will be the data section of the myList type. Along with the array of coordinates, the list also contains a count of the number of items on the list. Below is the function I used to construct the myList derived type. The first thing that MakeList() does is to call MakeListEntry() to define and commit the type for a single coordinate pair.

After defining the coordinate pair type, I declare and assign some two element arrays to hold the displacement, the types and the counts for the two parts of the List data structure; one item in the newly declared arrays for the count and one for the coordinate array. Next, a List is declared in order to determine the offsets of the two parts of the structure. I call MPI_Get_address() to find these displacements.

When I was defining the grid point pair type, I assumed that the two integers would be placed in memory one immediately after the other. Since the List structure has a single integer and an array of listEntry, I'm not completely sure what word or byte boundaries the compiler or the hardware will enforce when the structure is declared and allocated in memory. Thus, I create a List and use the MPI function to determine the exact offsets of the allocated data structure. Of course, since I want the offset from a relative base of zero, I subtract the address of the array from the address of the count integer and set the count displacement to zero.

Because I preloaded the data into the arrays holding the component MPI data types (type) and the count of each type (blockCount), I now simply call MPI_Type_struct() to define the myList derived type composed of the two primitive types and then MPI_Type_commit() to complete the definition.

After breaking it down, it all seems rather easy and straightforward. As I said before, the only way that this works is due to the fact that the List data structure is going to be a fixed size. If I were to use a dynamic list structure that changes size based on the actual number of entries in the list, I could not create a derived type. For that implementation of a distributed Game of Life simulation, I would recommend using the ghost cell idea presented in my previous post, which will ensure that a known quantity of data will be exchanged between neighboring processes.

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task.
However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!