Нижегородский государственный университет им. Н.И.Лобачевского
Опубликован: 30.05.2014 | Доступ: свободный | Студентов: 292 / 32 | Длительность: 11:26:00

Самостоятельная работа 1: Компиляция и запуск приложений на Intel Xeon Phi

< Лекция 5 || Самостоятельная работа 1: 123456 || Самостоятельная работа 2 >
Скалярное произведение векторов. MPI версия

Теперь рассмотрим задачу вычисления скалярного произведения массивов векторов. Пусть имеется массив векторов a и массив векторов b. Количество векторов в массивах одинаково. Необходимо вычислить скалярное произведение всех векторов a[i] на b[i] для всех i. В итоге получим скалярный массив произведений с, где c[i]=dot(a[i], b[i]) .

Очевидно, что данная задача является усложнением предыдущей и может использовать параллельную функцию вычисления скалярного произведения двух векторов, реализованную выше. При этом каждое скалярное произведение может быть вычислено независимо от остальных, а значит, здесь есть еще один ресурс для распараллеливания. Воспользуемся им для одновременного использования нескольких сопроцессоров в рамках MPI программы.

Писать программу начнем с подключения необходимых библиотек:

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

Добавим функцию вычисления скалярного произведения из предыдущего примера:

float dot(float* a, float* b, int n)
{
    float res = 0;

    #pragma omp parallel for reduction(+: res)
    for (int i = 0; i < n; ++i)
    {
        res += a[i]*b[i];
    }

    return res;
}

Функция main будет содержать весь остальной код. Сначала инициализируем библиотеку MPI, введем необходимые переменные, а так же укажем размерность задачи – размер вектора и количество таких векторов:

int main(int argc, char** argv)
{
    MPI_Init(&argc, &argv);

    float *a, *b, *c;
    float *send_a = NULL, *send_b = NULL, *recv_c = NULL;
    int vector_size = 500;
    int vector_num = 20;
    int portion_size, remainder;

Далее запросим информацию о количестве процессов и ранге текущего процесса:

    int mpi_rank, mpi_size;
    MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank);
    MPI_Comm_size(MPI_COMM_WORLD, &mpi_size);

Для понимания того, на каких узлах будет запущена наша программа, выведем их список:

    if (mpi_rank == 0)
    {
        printf("List of nodes:\n");
    }

    MPI_Barrier(MPI_COMM_WORLD);
    
    char node_name[MPI_MAX_PROCESSOR_NAME];
    int node_name_length;
    MPI_Get_processor_name(node_name, &node_name_length);
    printf("%s\n", node_name);

Приведенный ниже код описывает алгоритм вычисления размеров порции данных для обсчета каждым из доступных процессов. Вводятся два типа порций – send для массивов a и b, recv для массива c:

portion_size = vector_num/mpi_size;
    remainder = vector_num - portion_size*mpi_size;

    int* send_portion_size = new int[mpi_size];
    int* send_portion_index = new int[mpi_size];
    int send_index = 0;    
    for (int i = 0; i < remainder; ++i)
    {
        send_portion_size[i] = (portion_size + 1)
            *vector_size;
        send_portion_index[i] = send_index;
        send_index += send_portion_size[i];
    }
    for (int i = remainder; i < mpi_size; ++i)
    {
        send_portion_size[i] = portion_size*vector_size;
        send_portion_index[i] = send_index;
        send_index += send_portion_size[i];
    }

    int* recv_portion_size = new int[mpi_size];
    int* recv_portion_index = new int[mpi_size];
    int recv_index = 0;    
    for (int i = 0; i < remainder; ++i)
    {
        recv_portion_size[i] = portion_size + 1;
        recv_portion_index[i] = recv_index;
        recv_index += recv_portion_size[i];
    }
    for (int i = remainder; i < mpi_size; ++i)
    {
        recv_portion_size[i] = portion_size;
        recv_portion_index[i] = recv_index;
        recv_index += recv_portion_size[i];
    }

Далее на главном (нулевом) процессе выделяем память под вектора и инициализируем их случайными числами:

    if (mpi_rank == 0)
    {
        send_a = new float[vector_num*vector_size];
        send_b = new float[vector_num*vector_size];
        recv_c = new float[vector_num];

        for (int i = 0; i < vector_num*vector_size; ++i)
        {
            send_a[i] = (float)rand()/RAND_MAX;
            send_b[i] = (float)rand()/RAND_MAX;
        }
    }

Следует заметить, что для удобства передачи данных по сети вектора каждого из массивов лежат в памяти последовательно друг за другом.

Следующий шаг – выделение памяти под порции данных и их отправка остальным процессам:

a = new float[send_portion_size[mpi_rank]];
    b = new float[send_portion_size[mpi_rank]];
    c = new float[recv_portion_size[mpi_rank]];

    MPI_Scatterv(send_a, send_portion_size,
        send_portion_index, MPI_FLOAT, a,
        send_portion_size[mpi_rank], MPI_FLOAT,
        0, MPI_COMM_WORLD);
    MPI_Scatterv(send_b, send_portion_size,
        send_portion_index, MPI_FLOAT,
        b, send_portion_size[mpi_rank], MPI_FLOAT,
        0, MPI_COMM_WORLD);

Далее выполняем необходимые расчеты:

    for (int i = 0; i < recv_portion_size[mpi_rank]; ++i)
    {
        c[i] = dot(a + i*vector_size,
            b + i*vector_size, vector_size);
    }

Принимаем результаты от других процессов:

MPI_Gatherv(c, recv_portion_size[mpi_rank], MPI_FLOAT, 
        recv_c, recv_portion_size, recv_portion_index,
        MPI_FLOAT, 0, MPI_COMM_WORLD); 

Выводим результаты на консоль:

if (mpi_rank == 0)
    {        
        printf("Results:\n");
        for (int i = 0; i < vector_num; ++i)
        {
            printf("%f\n", recv_c[i]);
        }

        delete[] send_a;
        delete[] send_b;
        delete[] recv_c;
    }

И освобождаем оставшуюся память:

    delete[] a;
    delete[] b;
    delete[] c;

    delete[] send_portion_size;
    delete[] send_portion_index;
    delete[] recv_portion_size;
    delete[] recv_portion_index;

    MPI_Finalize();

    return 0;
}

Компиляция приведенного выше кода осуществляется командой:

mpicc –O2 –openmp –mmic main.cpp –o ./lab1_dot_native_mpi.mic

Отличие от предыдущего примера заключает в использовании утилиты mpicc для компиляции кода с подключением заголовочных файлов и библиотеки MPI. Напомним, что для компиляции должна использоваться связка компилятора Intel и библиотеки Intel MPI.

Для запуска на четырех сопроцессорах Intel Xeon Phi с использованием MPI Hydra нужно выполнить команду:

mpiexec.hydra –hosts 4 mic0 mic1 mic2 mic3 –n 4 –perhost 1 ./lab1_dot_native_mpi.mic

При работе на кластере с системой управления SLURM можно использовать следующую команду:

sbatch –N 2 –-gres=mic:2 native_run.sh ./lab1_dot_native_mpi

Запуск будет выполнен на двух узлах, на каждом будет запущено по 2 процесса (в силу наличия на каждом узле по 2 Intel Xeon Phi).

Как и в предыдущем примере, для работы скрипта native_run.sh требуется подгрузить модуль launcher/mic, если это не было сделано ранее:

module load launcher/mic

Посмотреть статистику по количеству свободных и используемых узлов кластера можно командой:

sinfo

Результаты работы программы приведены на рис. 6.6. Как и в предыдущем примере, результаты запишутся в файл slurm-<номер задачи>.out.

Результаты работы программы lab1_dot_native_mpi

Рис. 6.6. Результаты работы программы lab1_dot_native_mpi
< Лекция 5 || Самостоятельная работа 1: 123456 || Самостоятельная работа 2 >
Svetlana Svetlana
Svetlana Svetlana

Здравствуйие! Я хочу пройти курс Введение в принципы функционирования и применения современных мультиядерных архитектур (на примере Intel Xeon Phi), в презентации самостоятельной работы №1 указаны логин и пароль для доступ на кластер и выполнения самостоятельных работ, но войти по такой паре логин-пароль не получается. Как предполагается выполнение самосоятельных работ в этом курсе?