exponenta event banner

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

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

Как определено в предыдущем примере, в «операции трафарета» каждый элемент выходной матрицы зависит от небольшой области входной матрицы. Примеры включают конечные различия, свертку, медианную фильтрацию и методы конечных элементов. Если мы предполагаем, что трафаретная операция является ключевой частью нашего рабочего процесса, мы можем потратить время, чтобы преобразовать ее в написанное вручную ядро CUDA в надежде получить максимальную выгоду от GPU. В этом примере в качестве шаблона используется «Игра жизни» Конвея и выполняется перемещение вычислений в файл 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. Эта версия может работать на GPU, просто убедившись, что начальное заполнение находится на GPU с помощью 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, создает рабочее пространство на GPU и возвращает выходные данные. Функцию шлюза 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 с использованием текстурной памяти

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

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