Доступ к усовершенствованным функциям CUDA Используя MEX

Этот пример демонстрирует, как к расширенным функциям графического процессора можно получить доступ с помощью файлов MEX. Это основывается на Операциях Шаблона в качестве примера на графическом процессоре. Предыдущий пример использует "Игру Конуэя Жизни", чтобы продемонстрировать, как операции шаблона могут быть выполнены с помощью кода MATLAB®, который работает на графическом процессоре. Существующий пример демонстрирует, как можно далее улучшать производительность операций шаблона, использующих две расширенных функции графического процессора: общая память и память структуры. Вы делаете это путем записи собственного кода CUDA в файле MEX и вызова файла MEX из MATLAB. Можно найти введение в использование графического процессора в файлах MEX в Запущенных MEX-функциях, Содержащих Код 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

Чтобы получить базовую линию производительности, мы начинаем с начального внедрения, описанного в Экспериментах в 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 операции шаблона мы должны разделить входные данные в блоки, с которыми может работать каждый блок потока. Каждый поток в блоке будет считывать данные, которые также необходимы другим потокам в блоке. Один способ минимизировать количество операций чтения состоит в том, чтобы скопировать необходимые входные данные в общую память перед обработкой. Эта копия должна включать некоторые соседние элементы, чтобы позволить правильное вычисление ребер блока. Для Игры Жизни, где наш шаблон только 3x3 квадрат элементов, нам нужен один контур элемента. Например, для 9x9 сетка обработала использование 3x3 блоки, пятый блок будет работать с подсвеченной областью, где желтыми элементами является "ореол", это должно также читать.

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

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

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

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

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

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

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

  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, которые могут использоваться, чтобы считать память структуры. Мы принимаем решение использовать структуру CUDA ссылочный API, который поддерживается на всех устройствах CUDA. Максимальное количество элементов, которые могут быть в массиве, связанном со структурой$2^{27}$, таким образом, подход структуры не будет работать, если вход будет иметь больше элементов.

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

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

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

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

В этом файле функция устройства CUDA более проста, чем прежде. Это только должно выполнить Игру Жизненных вычислений. Нет никакой потребности скопировать в общую память или синхронизировать потоки.

Как в версии общей памяти, код хоста вызывает функцию устройства 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