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