Соглашения об именах

Глава 2 спецификации MPI описывать ключевые понятия и соглашения стандарта. Рассмотрим некторые из них.

Названия C функций и подпрограмм Fortran в MPI имеют следующую структуру:

  • MPI_Class_action_subset,

  • MPI_Class_action,

  • MPI_Action_subset,

где Class — название группы функций, action — действие, выполняемое функцией (create, get, set, delete, is), subset — подмножество функций в рамках класса.

Например:

  • MPI_Type_create_struct(),

  • MPI_Type_create_hvector(),

  • MPI_Comm_get_attr(),

  • MPI_Win_create_keyval().

Названия функции стандартизованные MPI-1 могут отличаться от действующего на данный момент соглашения. Например: MPI_Send, MPI_Recv, MPI_Type_contiguous, MPI_Type_vector.

Спецификация прототипов функций

Аргументы функций C и Fortran отмечены семантическими спецификаторами, не зависящими от языка:

  • IN — входной аргумент функции, функция может использовать значение, но не модифицировать;

  • OUT — выходной аргумент, функция может использовать его для записи результата;

  • INOUT — функция может использовать значение аргумента, как для чтения так и для записи результата.

Важное замечание — аргумент типа OUT или типа INOUT не может быть псевдонимом (alias) любого другого аргумента функции. Если это явно не оговорено в стандарте. Например, в следующем вызове функции copyIntBuf два аргумента являются псевдонимами — указывают на пересекающуюся область памяти (argument aliasing):

int a [10];
copyIntBuffer (a , a + 3 , 7);

void copyIntBuffer (int *pin, int *pout , int len ) {
  for (int i =0; i < len ; ++ i ) *pout++ = *pin++;
}

В коллективных операциях алиасинг аргументов также запрещен — входной и выходной буферы не должны использовать перекрывающиеся области памяти.

Например, если есть желание обойтись одним буфером для передачи и приема, то решение — использовать константу MPI_IN_PLACE:

MPI_Allreduce (MPI_IN_PLACE, rbuf, count, MPI_INT, MPI_SUM, MPI_COMM_WORLD).

Использование одного буфера для приема и передачи в Gather

Рассмотрим крайний случай — попробуем передавать в Gather адрес одного и того же буфера для отправки и приема сообщений, и избежать пересечения областей памяти.

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

MPI_Gather(buf, 1, MPI_INT, buf, 1, recvtype, root, MPI_COMM_WORLD);

С первого взгляда кажется, что здесь явное нарушение спецификации — в качестве буфера отправки и приема указан адрес одного итого же массива buf (aliasing). Окончательный ответ может быть получен только после анализа структуры производного типа recvtype.

Производные типы данных MPI (Derived Datatypes) позволяют описывать размещение элементов буферов в памяти (typemap). В частности, можно описать новый тип recvtype, который будет пропускать первый элемент MPI_INT буфера buf, что позволит избежать пересечения областей памяти на стороне корневого процесса. Ниже приведен пример.

#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>

int main(int argc, char **argv)
{
    int rank, commsize, root = 0;
    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &commsize);

    int bufsize = commsize + 1;
    int *buf = malloc(sizeof(*buf) * bufsize);
    buf[0] = rank;

    // Один и тот-же буфер buf используется для отправки и приема сообщений:
    //   * каждый процесс запсывает отправляемое сообщение в buf[0]
    //   * корень принимает сообщения в buf[1..commsize]

    printf("Proc %d/%d: buf %d\n", rank, commsize, buf[0]);

    if (rank != root) {
        MPI_Gather(buf, 1, MPI_INT, NULL, 0, MPI_DATATYPE_NULL, root, MPI_COMM_WORLD);
    } else {
        // Новый тип данных: 1 элемент типа MPI_INT, со смещением в 4 байта от начала буфера
        MPI_Datatype recvtype;
        int blocklens[1] = {1};
        MPI_Aint displs[1] = {sizeof(int)};
        MPI_Datatype types[1] = {MPI_INT};
        MPI_Type_create_struct(1, blocklens, displs, types, &recvtype);
        MPI_Type_commit(&recvtype);
        // Выводим отладочную информацию о типе данных
        int typesize;                        // размер типа в байтах (только данные)
        MPI_Type_size(recvtype, &typesize);
        MPI_Aint lb, extent;                 // lb -- смещение (lower bound),
                                             // extent -- протяженность типа
        MPI_Type_get_extent(recvtype, &lb, &extent);
        printf("recvtype: size %d, lb %lu, extent %lu\n", typesize, lb, extent);

        /*
           Gather:
           Recv from i=0: (buf + i * count * extent, 1, recvtype)
                          => запись 1 MPI_INT в позицию buf[1]
           Recv from i=1: (buf + 4,                  1, recvtype)
                          => запись 1 MPI_INT в позицию buf[2]
           Recv from i=2: (buf + 8,                  1, recvtype)
                          => запись 1 MPI_INT в позицию buf[3]
           Recv from i=3: (buf + 12,                 1, recvtype)
                          => запись 1 MPI_INT в позицию buf[4]
         */

        MPI_Gather(buf, 1, MPI_INT, buf, 1, recvtype, root, MPI_COMM_WORLD);

        for (int i = 0; i < bufsize; i++)
            printf("%d ", buf[i]);
        printf("\n");
        MPI_Type_free(&recvtype);
    }

    free(buf);
    MPI_Finalize();
    return 0;
}

Пример запуска на 4 процессах:

$ mpicc -Wall -o prog ./prog.c
$ mpiexec -n 4 ./prog

Proc 2/4: buf 2
Proc 3/4: buf 3
Proc 1/4: buf 1
Proc 0/4: buf 0
recvtype: size 4, lb 4, extent 4
0 0 1 2 3

Здесь значение lb 4 для типа recvtype позволяет принимать элементы со смещением относительно начального адреса буфера приема.