Здравствуйте хабровчане, в этой статье я хочу рассказать о взаимодействии двух технологий MPI(mpich2) и NVIDIA CUDA. Упор я хочу сделать именно на саму структуру программы и настройку вышеописанных технологий для работы в одной программе. И так поехали…


Для удобства, я написал небольшой план по которому мы будем двигаться:
1) Подготовка системы для работы.
2) Установка библиотеки mpich2.
3) Установка NVIDIA CUDA.
4) Написание программного кода (структура программы)
5) Настройка компилятора.
6) Компиляция и запуск исполняемого файла.

Пункт 1
Возможно стоило назвать его как-то по-другому, но тем не менее. Я использую операционную систему Ubuntu 12.04 и теоретически для всей настройки достаточно пакетного менеджера — synaptic
sudo apt-get install synaptic
Или же при желании вы можете поставить все прямо из терминала.

Пункт 2
И так приступим. Что же такое MPI? Немного перефразирую википедию — это некий API который позволяет обмениваться данными между процессами, выполняющими одну задачу. Проще г��воря, это одна из нескольких технологий параллельного программирования. Более подробно можно почитать на википедии. Мы будем использовать MPICH2 — библиотека в которой реализован стандарт MPI, т. к. данная библиотека является самой распространенной. Для ее установки необходимо в терминале прописать:
sudo apt-get install mpi-default-dev
sudo apt-get install mpich2
sudo apt-get install libmpich2-dev

Либо Alt+f2:
gksu synaptic


и в поиске прописать:
mpi-default-dev
mpich2
libmpich2-dev



Выбрать найденный пакет и установить его. Так же при желании вы можете установить и другие с вашей точки зрения полезные пакеты связанные с mpi или mpich2. Желательно установить документацию. Далее проверим, что у нас получилось, создадим файл test.cpp и добавим в него следующий код:
#include <mpi.h>
#include <iostream> 

int main (int argc, char* argv[])
{
    int rank, size;
    MPI_Init (&argc, &argv);
    MPI_Comm_rank (MPI_COMM_WORLD, &rank);
    MPI_Comm_size (MPI_COMM_WORLD, &size);	
	std::cout<<"\nHello Habrahabr!!"<<std::endl;
    MPI_Finalize();
    return 0;
}

Скомпилируем его:
mpic++ test.cpp -o test

Запустим:
mpirun.mpich2 -l -n 8 ./test


В итоге должно получиться что-то вроде этого:


Пункт 3
Данный процесс я описывал в своем прошлом посте.

Пункт 4
Предположим, что у нас есть папка habr, создадим в ней следующие файлы:
main.cu
head.h // здесь будут содержаться хедерные файлы.
GPU.cu // код предназначенный для гпу.
CPU.cpp // код предназначенный для процессора.

main.cu — здесь напишем простейший mpi код, который послужит для запуска программы на нескольких ядрах. В функциях gpu и cpu происходит обычное умножение, с той лишь разницей, что в функции gpu умножение происходит на видео карте.
#include "head.h"

int main(int argc, char* argv[]){
	
	int rank, size;
	int x = 9999;
	int y = 9999;
	
	MPI_Init (&argc, &argv);
	MPI_Comm_rank (MPI_COMM_WORLD, &rank);//номер текущего процесса
    MPI_Comm_size (MPI_COMM_WORLD, &size);//число процессов
	
	int res_gpu = gpu(x, y);	
	int res_cpu = cpu(x, y);
	
	std::cout<<"res_gpu = "<<res_gpu<<std::endl;
	std::cout<<"res_cpu = "<<res_cpu<<std::endl;
	
	MPI_Finalize();
	return 0;
}


head.h — здесь опишем необходимые инклюды.
#include <iostream>
#include <mpi.h>
#include <cuda.h>

#include "CPU.cpp"
#include "GPU.cu"


GPU.cu — непосредственно код который умножает два числа на видео карточке.
#include <cuda.h>
#include <iostream>
#include <stdio.h>

__global__ void mult(int x, int y, int *res) {
	
	*res = x * y;
	
	}


int gpu(int x, int y){
	int *dev_res;	
	int res = 0;	
	cudaMalloc((void**)&dev_res, sizeof(int));	
	mult<<<1,1>>>(x, y, dev_res);	
	cudaMemcpy(&res, dev_res, sizeof(int), cudaMemcpyDeviceToHost);	
	cudaFree(dev_res);
	
	return res;
}



CPU.cpp — данный код, скорее для проверки умножения происходящего на гпу и в принципе больше полезности ни какой не несет.
int cpu(int x, int y){
        int res;
	res = x * y;
	
	return res;
}

Созданные файлы положить в папку ../src. В итоге должно получиться как-то так:


Пункт 5
Тут самое интересное, необходимо настроить компилятор nvcc для компиляции не только CUDA кода, но и MPI кода, для этого напишем небольшой make файл:
CXX        = nvcc
LD         = $(CXX)

LIBS_PATH = -L/usr/lib 
LIBS =  -lmpi -lopa -lmpl -lrt -lcr -lpthread
INCLUDE_PATH = -I/usr/lib/mpich2/include/
FLAGS = -g
TARGET = "/home/relaps/habr/src/main.cu"
OBIN = "/home/relaps/habr/bin/cuda&mpi"

all: $(TARGET)

$(TARGET):
	$(LD) $(INCLUDE_PATH) $(FLAGS) $(TARGET) -o $(OBIN) $(LIBS_PATH) $(LIBS)

И так теперь необходимо всего лишь перейти в папку с проектом и собрать его.

Пункт 6
Здесь я думаю комментарии излишни на скриншоте все видно.
Собираем:


Запускаем, получается что-то вроде этого:


Ну собственно на этом работа закончена, в итоге у нас получилась программа в которой задействованы и видео карточка, и несколько ядер процессора. Конечно пример с умножением двух чисел представленный в данном контексте, совершенно не актуален для данных технологий, но повторюсь — я ставил перед собой задачу показать, что mpi и cuda могут вполне сосуществовать в одной программе(считаю, что код элементарный за исключением директив cuda и mpi, поэтому особо его не пояснял). Естественно, если взять более сложную программу, то возникает сразу много нюансов, начиная от структуры кластера и т.п., продолжать тут можно долго, каждая такая программа требует индивидуального рассмотрения. Но в итоге оно того стоит.

p.s. В следующем посте, вероятнее всего, попытаюсь рассказать про основы nvidia cuda.