Доступ к расширенным функциям CUDA с помощью MEX

Этот пример демонстрирует, как расширенные функции GPU могут быть доступны с помощью файлов MEX. Он основан на примере операций Stencil на графическом процессоре. Предыдущий пример использует «Игру жизни» Конвея, чтобы продемонстрировать, как операции трафарета могут выполняться с помощью кода MATLAB ®, который запускается на графическом процессоре. Настоящий пример демонстрирует, как можно дополнительно улучшить эффективность операций трафарета с помощью двух расширенных функций GPU: общей памяти и текстурной памяти. Вы делаете это, записывая свой собственный код CUDA в файл MEX и вызывая файл MEX из MATLAB. Вы можете найти введение в использование графический процессор в файлах MEX в Run MEX-Functions, содержащих код CUDA.

Как определено в предыдущем примере, в «операции трафарета» каждый элемент выходной матрицы зависит от небольшой области входной матрицы. Примеры включают конечные различия, свертки, медианную фильтрацию и конечноэлементные методы. Если мы предположим, что операция трафарета является ключевой частью нашего рабочего процесса, мы могли бы взять время, чтобы преобразовать его в написанное вручную ядро CUDA в надежде получить максимальную выгоду от графический процессор. Этот пример использует «Игру жизни» Конвея в качестве нашей операции трафарета и перемещает вычисление в файл MEX.

«Игра жизни» следует нескольким простым правилам:

  • Камеры расположены в 2D сетке

  • На каждом шаге судьба каждой камеры определяется жизнеспособностью восьми ее ближайших соседей

  • Любая камера с точно тремя живыми соседями оживает на следующем шаге

  • Живая камера с ровно двумя живыми соседями остается живой на следующем шаге

  • Все другие камеры (включая клетки с более чем тремя соседями) умирают на следующем шаге или остаются пустыми

«Трафарет» в этом случае является областью 3x3 вокруг каждого элемента. Для получения дополнительной информации смотрите код для paralleldemo_gpu_stencil.

Этот пример является функцией, которая позволяет нам использовать подфункции:

function paralleldemo_gpu_mexstencil()

Сгенерируйте случайную начальную генеральную совокупность

На 2D сетке создается начальная генеральная совокупность камер с приблизительно 25% мест нахождения живых.

    gridSize = 500;
    numGenerations = 100;
    initialGrid = (rand(gridSize,gridSize) > .75);

    hold off
    imagesc(initialGrid);
    colormap([1 1 1;0 0.5 0]);
    title('Initial Grid');

Создайте базовую версию графический процессор в MATLAB

Чтобы получить базовый уровень эффективности, мы начинаем с начальной реализации, описанной в Experiments in MATLAB. Эта версия может выполняться на графическом процессоре, просто убедившись, что начальная генеральная совокупность на графическом процессоре используется gpuArray.

function X = updateGrid(X, N)
    p = [1 1:N-1];
    q = [2:N N];
    % Count how many of the eight neighbors are alive.
    neighbors = X(:,p) + X(:,q) + X(p,:) + X(q,:) + ...
        X(p,p) + X(q,q) + X(p,q) + X(q,p);
    % A live cell with two live neighbors, or any cell with
    % three live neighbors, is alive at the next step.
    X = (X & (neighbors == 2)) | (neighbors == 3);
end

currentGrid = gpuArray(initialGrid);
% Loop through each generation updating the grid and displaying it
for generation = 1:numGenerations
    currentGrid = updateGrid(currentGrid, gridSize);

    imagesc(currentGrid);
    title(num2str(generation));
    drawnow;
end

Теперь перезапустите игру и измерьте, сколько времени это занимает для каждой генерации.

% This function defines the outer loop that calls each generation, without
% doing the display.
function grid=callUpdateGrid(grid, gridSize, N)
    for gen = 1:N
        grid = updateGrid(grid, gridSize);
    end
end

gpuInitialGrid = gpuArray(initialGrid);

% Retain this result to verify the correctness of each version below.
expectedResult = callUpdateGrid(gpuInitialGrid, gridSize, numGenerations);

gpuBuiltinsTime = gputimeit(@() callUpdateGrid(gpuInitialGrid, ...
                                               gridSize, numGenerations));

fprintf('Average time on the GPU: %2.3fms per generation \n', ...
        1000*gpuBuiltinsTime/numGenerations);
Average time on the GPU: 1.528ms per generation 

Создайте версию MEX, которая использует общую память

При записи Ядра CUDA версии операции трафарета мы должны разделить входные данные на блоки, на которых может работать каждый блок потоков. Каждый поток в блоке будет считывать данные, которые также необходимы другим потокам в блоке. Один из способов минимизировать количество операций чтения - скопировать необходимые входные данные в общую память перед обработкой. Эта копия должна включать некоторые соседние элементы, чтобы разрешить правильное вычисление ребер блоков. Для Игры Жизни, где наш трафарет - всего лишь 3х3 квадрат элементов, нам нужен один контур элемента. Для примера для сетки 9x9, обработанной с использованием блоков 3x3, пятый блок будет работать с подсвеченной областью, где желтые элементы являются «ореолом», который он также должен считать.

Мы поместили код CUDA, который иллюстрирует этот подход, в файл pctdemo_life_cuda_shmem.cu. Функция устройства CUDA в этом файле работает следующим образом:

  1. Все потоки копируют соответствующую часть сетки входа в общую память, включая ореол.

  2. Потоки синхронизируются друг с другом, чтобы убедиться, что общая память готова.

  3. Резьбы, которые помещаются в сетке выхода, выполняют расчет Игры Жизни.

Хост- код в этом файле вызывает функцию устройства CUDA один раз для каждой генерации, используя CUDA runtime API. Он использует два различных буфера с возможностью записи для входа и выхода. При каждой итерации файл MEX меняет входной и выходной указатели так, чтобы копирование не требовалось.

В порядок вызвать функцию из MATLAB, нам нужен шлюз MEX, который разворачивает вход массивы из MATLAB, создает рабочую область на графическом процессоре и возвращает выход. Функцию шлюза MEX можно найти в файле pctdemo_life_mex_shmem.cpp.

Прежде чем мы сможем вызвать файл MEX, мы должны скомпилировать его с помощью mexcuda, что требует установки nvcc компилятор. Можно скомпилировать эти два файла в одну MEX-функцию с помощью команды like

  mexcuda -output pctdemo_life_mex_shmem ...
         pctdemo_life_cuda_shmem.cu pctdemo_life_mex_shmem.cpp

который будет создавать файл MEX с именем pctdemo_life_mex_shmem.

% Calculate the output value using the MEX file with shared memory. The
% initial input value is copied to the GPU inside the MEX file.
grid = pctdemo_life_mex_shmem(initialGrid, numGenerations);
gpuMexTime = gputimeit(@()pctdemo_life_mex_shmem(initialGrid, ...
                                                 numGenerations));
% Print out the average computation time and check the result is unchanged.
fprintf('Average time of %2.3fms per generation (%1.1fx faster).\n', ...
        1000*gpuMexTime/numGenerations, gpuBuiltinsTime/gpuMexTime);
assert(isequal(grid, expectedResult));
Average time of 0.055ms per generation (27.7x faster).

Создайте версию MEX, которая использует память текстур

Второй способ решения проблемы повторных операций чтения - использовать память текстуры графический процессор. Доступ к текстуре кэшируется таким образом, чтобы обеспечить хорошую эффективность, когда несколько потоков получают доступ к перекрывающимся 2D данным. Это именно тот шаблон доступа, который у нас есть в операции трафарета.

Существует два API CUDA, которые могут использоваться для чтения текстурной памяти. Мы принимаем решение использовать ссылка API, который поддерживается на всех устройствах CUDA. Максимальное количество элементов, которые могут быть в массиве, связанном с текстурой, является, $2^{27}$поэтому подход текстуры не будет работать, если вход имеет больше элементов.

Код CUDA, который иллюстрирует этот подход, находится в pctdemo_life_cuda_texture.cu. Как и в предыдущей версии, этот файл содержит как код хоста, так и код устройства. Три функции этого файла позволяют использовать память текстур в функции устройства.

  1. Переменная текстуры объявлена в верхней части файла MEX.

  2. Функция устройства CUDA извлекает вход из ссылки на текстуру.

  3. Этот Файл MEX связывает ссылку на текстуру с буфером входа.

В этом файле функция устройства CUDA проще, чем раньше. Ему нужно только выполнить расчеты Game of Life. Нет необходимости копировать в общую память или синхронизировать потоки.

Как и в версии с общей памятью, хост- код вызывает функцию устройства CUDA один раз для каждой генерации, используя API среды выполнения CUDA. Он снова использует два буфера с возможностью записи для входа и выхода и заменяет их указатели на каждой итерации. Перед каждым вызовом функции устройства он связывает ссылку на текстуру с соответствующим буфером. После выполнения функции устройства она разблокирует ссылку текстуры.

Существует файл шлюза MEX для этой версии, pctdemo_life_mex_texture.cpp который заботится о входе и выходе массивах и о выделении рабочей области. Эти файлы могут быть встроены в один файл MEX с помощью следующей команды.

  mexcuda -output pctdemo_life_mex_texture ...
         pctdemo_life_cuda_texture.cu pctdemo_life_mex_texture.cpp
% Calculate the output value using the MEX file with textures.
grid = pctdemo_life_mex_texture(initialGrid, numGenerations);
gpuTexMexTime = gputimeit(@()pctdemo_life_mex_texture(initialGrid, ...
                                                  numGenerations));
% Print out the average computation time and check the result is unchanged.
fprintf('Average time of %2.3fms per generation (%1.1fx faster).\n', ...
        1000*gpuTexMexTime/numGenerations, gpuBuiltinsTime/gpuTexMexTime);
assert(isequal(grid, expectedResult));
Average time of 0.025ms per generation (61.5x faster).

Заключения

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

fprintf('First version using gpuArray:  %2.3fms per generation.\n', ...
        1000*gpuBuiltinsTime/numGenerations);
fprintf(['MEX with shared memory: %2.3fms per generation ',...
         '(%1.1fx faster).\n'], 1000*gpuMexTime/numGenerations, ...
        gpuBuiltinsTime/gpuMexTime);
fprintf(['MEX with texture memory: %2.3fms per generation '...
         '(%1.1fx faster).\n']', 1000*gpuTexMexTime/numGenerations, ...
        gpuBuiltinsTime/gpuTexMexTime);
First version using gpuArray:  1.528ms per generation.
MEX with shared memory: 0.055ms per generation (27.7x faster).
MEX with texture memory: 0.025ms per generation (61.5x faster).
end