Этот пример использует «Игру жизни» Конвея, чтобы продемонстрировать, как операции трафарета могут выполняться с помощью графический процессор.
Многие операции над массивами могут быть выражены как «операция трафарета», где каждый элемент выхода массива зависит от небольшой области входа массива. Примеры включают конечные различия, свертки, медианную фильтрацию и конечноэлементные методы. Этот пример использует «Игру жизни» Конвея, чтобы продемонстрировать два способа запустить операцию трафарета на графическом процессоре, начиная с кода в электронной книге Клева Молера «Эксперименты в MATLAB».
«Игра жизни» следует нескольким простым правилам:
Камеры расположены в 2D сетке
На каждом шаге судьба каждой камеры определяется жизнеспособностью восьми ее ближайших соседей
Любая камера с точно тремя живыми соседями оживает на следующем шаге
Живая камера с ровно двумя живыми соседями остается живой на следующем шаге
Все другие камеры (включая клетки с более чем тремя соседями) умирают на следующем шаге или остаются пустыми
«Трафарет» в этом случае является областью 3x3 вокруг каждого элемента. Вот несколько примеров обновления камеры:
Этот пример является функцией, позволяющей использовать вложенные функции:
function paralleldemo_gpu_stencil()
На 2D сетке создается начальная генеральная совокупность камер с примерно 25% мест.
gridSize = 500; numGenerations = 100; initialGrid = (rand(gridSize,gridSize) > .75); gpu = gpuDevice(); % Draw the initial grid hold off imagesc(initialGrid); colormap([1 1 1;0 0.5 0]); title('Initial Grid');
Электронная книга Experiments in MATLAB обеспечивает начальную реализацию, которая может использоваться для сравнения. Эта версия полностью векторизирована, обновляя все камеры в сетке за один проход на генерацию.
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 grid = initialGrid; % Loop through each generation updating the grid and displaying it for generation = 1:numGenerations grid = updateGrid(grid, gridSize); imagesc(grid); title(num2str(generation)); drawnow; end
Теперь перезапустите игру и измерьте, сколько времени это занимает для каждой генерации.
grid = initialGrid; timer = tic(); for generation = 1:numGenerations grid = updateGrid(grid, gridSize); end cpuTime = toc(timer); fprintf('Average time on the CPU: %2.3fms per generation.\n', ... 1000*cpuTime/numGenerations);
Average time on the CPU: 11.323ms per generation.
Сохраните этот результат, чтобы проверить правильность каждой версии ниже.
expectedResult = grid;
Чтобы запустить Игру Жизни на графическом процессоре, начальная генеральная совокупность отправляется на графический процессор с помощью gpuArray
. Алгоритм остается неизменным. Обратите внимание, что wait (GPUDevice)
используется для проверки завершения вычисления графического процессора перед остановкой таймера. Это требуется только для точной синхронизации.
grid = gpuArray(initialGrid); timer = tic(); for generation = 1:numGenerations grid = updateGrid(grid, gridSize); end wait(gpu); % Only needed to ensure accurate timing gpuSimpleTime = toc(timer); % Print out the average computation time and check the result is unchanged. fprintf(['Average time on the GPU: %2.3fms per generation ', ... '(%1.1fx faster).\n'], ... 1000*gpuSimpleTime/numGenerations, cpuTime/gpuSimpleTime); assert(isequal(grid, expectedResult));
Average time on the GPU: 1.655ms per generation (6.8x faster).
Смотрите на расчеты в updateGrid
очевидно, что одни и те же операции применяются к каждому местоположению сетки независимо. Это говорит о том, что arrayfun
можно использовать для проведения оценки. Однако каждая камера должна знать о своих восьми соседях, нарушая поэлементную независимость. Каждый элемент должен иметь возможность доступа к полной сетке, одновременно работая независимо.
Решением является использование вложенной функции. Вложенные функции, даже те, что используются с arrayfun
, может получить доступ к переменным, объявленным в их родительской функции. Это означает, что каждая камера может считывать всю сетку с предыдущего временного шага и указывать в нее.
grid = gpuArray(initialGrid); function X = updateParentGrid(row, col, N) % Take account of boundary effects rowU = max(1,row-1); rowD = min(N,row+1); colL = max(1,col-1); colR = min(N,col+1); % Count neighbors neighbors ... = grid(rowU,colL) + grid(row,colL) + grid(rowD,colL) ... + grid(rowU,col) + grid(rowD,col) ... + grid(rowU,colR) + grid(row,colR) + grid(rowD,colR); % A live cell with two live neighbors, or any cell with % three live neighbors, is alive at the next step. X = (grid(row,col) & (neighbors == 2)) | (neighbors == 3); end timer = tic(); rows = gpuArray.colon(1, gridSize)'; cols = gpuArray.colon(1, gridSize); for generation = 1:numGenerations grid = arrayfun(@updateParentGrid, rows, cols, gridSize); end wait(gpu); % Only needed to ensure accurate timing gpuArrayfunTime = toc(timer); % Print out the average computation time and check the result is unchanged. fprintf(['Average time using GPU arrayfun: %2.3fms per generation ', ... '(%1.1fx faster).\n'], ... 1000*gpuArrayfunTime/numGenerations, cpuTime/gpuArrayfunTime); assert(isequal(grid, expectedResult));
Average time using GPU arrayfun: 0.795ms per generation (14.2x faster).
Обратите внимание, что мы также использовали другую новую возможность arrayfun
здесь: расширение размерности. Нам нужно было передать только векторы строка и столбец, и они были автоматически расширены в полную сетку. Эффект, как будто мы называли:
[cols,rows] = meshgrid(cols,rows);
как часть arrayfun
вызов. Это сохраняет нам и некоторые расчеты, и некоторую передачу данных между памятью центральный процессор и памятью графический процессор.
В этом примере на графическом процессоре была реализована простая операция трафарета, «Игра жизни» Конвея arrayfun
и переменные, объявленные в родительской функции. Этот метод может использоваться, чтобы реализовать область значений операций трафарета, включая конечноэлементные алгоритмы, свертки и фильтры. Он также может использоваться для доступа к элементам в интерполяционной таблице, определенной в родительской функции.
fprintf('CPU: %2.3fms per generation.\n', ... 1000*cpuTime/numGenerations); fprintf('Simple GPU: %2.3fms per generation (%1.1fx faster).\n', ... 1000*gpuSimpleTime/numGenerations, cpuTime/gpuSimpleTime); fprintf('Arrayfun GPU: %2.3fms per generation (%1.1fx faster).\n', ... 1000*gpuArrayfunTime/numGenerations, cpuTime/gpuArrayfunTime);
CPU: 11.323ms per generation. Simple GPU: 1.655ms per generation (6.8x faster). Arrayfun GPU: 0.795ms per generation (14.2x faster).
end