Здравствуйие! Я хочу пройти курс Введение в принципы функционирования и применения современных мультиядерных архитектур (на примере 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.