В этом примере показано, как использовать pagefun для повышения производительности применения большого количества независимых поворотов и перемещений к объектам в среде 3-D. Это типично для ряда проблем, которые включают большую партию вычислений на небольших массивах.
GPU наиболее эффективны при выполнении вычислений на очень больших матрицах. В MATLAB ® это обычно достигается векторизацией кода для максимизации работы, выполняемой в каждой инструкции. При наличии большого набора данных, но при разделении вычислений на множество небольших матричных операций, может оказаться сложным добиться максимальной производительности за счет одновременной работы на многих сотнях ядер графического процессора.
arrayfun и bsxfun функции позволяют параллельно выполнять скалярные операции на GPU. pagefun функция добавляет возможность выполнения матричных операций в пакете аналогичным образом. pagefun доступна в Parallel Computing Toolbox™ для использования с gpuArrays.
В этом примере робот осуществляет навигацию по известной карте, содержащей большое количество признаков, которые робот может идентифицировать с помощью своих датчиков. Робот находит себя на карте, измеряя относительное положение и ориентацию этих объектов и сравнивая их с местоположениями карты. Предполагая, что робот не полностью потерян, он может использовать любую разницу между ними для коррекции своего положения, например, используя фильтр Калмана. Мы сосредоточимся на первой части алгоритма.
Этот пример является функцией, так что вспомогательные функции могут быть вложены в нее.
function paralleldemo_gpu_pagefun
Давайте создадим карту объектов с рандомизированными позициями и ориентациями в большой комнате.
numObjects = 1000;
roomDimensions = [50 50 5]; % Length * breadth * height in meters
Мы представляем положения и ориентации с помощью векторов 3 на 1 T и матрицы вращения 3 на 3 R. Когда у нас есть N из этих преобразований, мы упаковываем трансляции в 3-by-N матрицу, а повороты в 3-by-3-by-N массив. Следующая функция инициализирует N преобразований случайными значениями, предоставляя структуру в качестве выходного сигнала:
function Tform = randomTransforms(N) Tform.T = zeros(3, N); Tform.R = zeros(3, 3, N); for i = 1:N Tform.T(:,i) = rand(3, 1) .* roomDimensions'; % To get a random orientation, we can extract an orthonormal % basis for a random 3-by-3 matrix. Tform.R(:,:,i) = orth(rand(3, 3)); end end
Теперь используйте его для настройки карты преобразований объектов и начального местоположения робота.
Map = randomTransforms(numObjects); Robot = randomTransforms(1);
Чтобы правильно идентифицировать элементы карты, роботу необходимо преобразовать карту, чтобы поместить ее датчики в начало координат. Затем он может найти объекты карты, сравнивая то, что он видит, с тем, что он ожидает увидеть.
Для объекта карты
можно найти его положение относительно робота
и ориентацию путем
преобразования его глобального местоположения карты:

где
и
являются положением и ориентацией робота, а
также
представляют данные карты. Эквивалентный код MATLAB выглядит следующим образом:
Rrel(:,:,i) = Rbot' * Rmap(:,:,i) Trel(:,i) = Rbot' * (Tmap(:,i) - Tbot)
for петляНам нужно преобразовать каждый объект карты в его местоположение относительно робота. Мы можем сделать это последовательно, закольцовывая все преобразования по очереди. Обратите внимание на синтаксис 'like' для zeros что позволит использовать тот же код на GPU в следующем разделе.
function Rel = loopingTransform(Robot, Map) Rel.R = zeros(size(Map.R), 'like', Map.R); % Initialize memory Rel.T = zeros(size(Map.T), 'like', Map.T); % Initialize memory for i = 1:numObjects Rel.R(:,:,i) = Robot.R' * Map.R(:,:,i); Rel.T(:,i) = Robot.R' * (Map.T(:,i) - Robot.T); end end
Во время расчета мы используем timeit функция, которая вызовет loopingTransform несколько раз, чтобы получить среднее время. Поскольку для этого требуется функция без аргументов, мы используем @() для создания анонимной функции правильной формы.
cpuTime = timeit(@()loopingTransform(Robot, Map)); fprintf('It takes %3.4f seconds on the CPU to execute %d transforms.\n', ... cpuTime, numObjects);
It takes 0.0104 seconds on the CPU to execute 1000 transforms.
Выполнение этого кода на GPU является просто вопросом копирования данных в gpuArray. Когда MATLAB обнаруживает данные, хранящиеся на GPU, он запускает любой код, использующий его на GPU, пока он поддерживается.
gMap.R = gpuArray(Map.R); gMap.T = gpuArray(Map.T); gRobot.R = gpuArray(Robot.R); gRobot.T = gpuArray(Robot.T);
Теперь звоним gputimeit, что является эквивалентом timeit для кода, который включает в себя вычисления GPU. Перед записью времени убедитесь, что все операции графического процессора завершены.
fprintf('Computing...\n'); gpuTime = gputimeit(@()loopingTransform(gRobot, gMap)); fprintf('It takes %3.4f seconds on the GPU to execute %d transforms.\n', ... gpuTime, numObjects); fprintf(['Unvectorized GPU code is %3.2f times slower ',... 'than the CPU version.\n'], gpuTime/cpuTime);
Computing... It takes 0.5588 seconds on the GPU to execute 1000 transforms. Unvectorized GPU code is 53.90 times slower than the CPU version.
pagefunВерсия GPU выше была очень медленной, потому что, хотя все вычисления были независимыми, они выполнялись последовательно. Используя pagefun мы можем выполнять все вычисления параллельно. Мы также нанимаем bsxfun для вычисления переводов, поскольку это операции по элементам.
function Rel = pagefunTransform(Robot, Map) Rel.R = pagefun(@mtimes, Robot.R', Map.R); Rel.T = Robot.R' * bsxfun(@minus, Map.T, Robot.T); end gpuPagefunTime = gputimeit(@()pagefunTransform(gRobot, gMap)); fprintf(['It takes %3.4f seconds on the GPU using pagefun ',... 'to execute %d transforms.\n'], gpuPagefunTime, numObjects); fprintf(['Vectorized GPU code is %3.2f times faster ',... 'than the CPU version.\n'], cpuTime/gpuPagefunTime); fprintf(['Vectorized GPU code is %3.2f times faster ',... 'than the unvectorized GPU version.\n'], gpuTime/gpuPagefunTime);
It takes 0.0008 seconds on the GPU using pagefun to execute 1000 transforms. Vectorized GPU code is 13.55 times faster than the CPU version. Vectorized GPU code is 730.18 times faster than the unvectorized GPU version.
Первым вычислением был расчет вращений. Это включало матричное умножение, которое переводится в функцию mtimes (*). Мы передаем это pagefun вместе с двумя наборами вращений, которые должны быть умножены:
Rel.R = pagefun(@mtimes, Robot.R', Map.R);
Robot.R' является матрицей 3 на 3, и Map.R является 3-by-3-by-N массивом. pagefun функция сопоставляет каждую независимую матрицу с карты с одним и тем же поворотом робота и дает нам требуемые выходные данные 3-by-3-by-N.
Вычисление преобразования также включает в себя умножение матрицы, но обычные правила умножения матрицы позволяют это выйти за пределы цикла без каких-либо изменений. Однако это также включает вычитание Robot.T от Map.T, которые имеют разные размеры. Поскольку это поэлементная операция, мы можем использовать bsxfun для подбора размеров таким же образом, как pagefun сделал для вращений:
Rel.T = Robot.R' * bsxfun(@minus, Map.T, Robot.T);
На этот раз нам нужно было использовать оператор элемента, который сопоставляется с функцией minus (-).
Если бы наш робот был в неизвестной части карты, он мог бы использовать алгоритм глобального поиска, чтобы найти себя. Алгоритм будет тестировать ряд возможных местоположений путем выполнения вышеописанных вычислений и поиска хорошего соответствия между объектами, наблюдаемыми датчиками робота, и тем, что он ожидал бы видеть в этой позиции.
Теперь у нас есть несколько роботов, а также несколько объектов. N объектов и M роботов должны дать нам N * M преобразования. Для отличия «пространства робота» от «пространства объекта» используется 4-е измерение для вращений и 3-е - для перемещений. Это означает, что наши вращения роботов будут 3-by-3-by-1-by-M, и переводы будут 3-by-1-by-M.
Мы инициализируем наш поиск со случайными местоположениями роботов. Хороший алгоритм поиска будет использовать топологические или другие подсказки, чтобы получить более интеллектуальный поиск.
numRobots = 10; Robot = randomTransforms(numRobots); Robot.R = reshape(Robot.R, 3, 3, 1, []); % Spread along the 4th dimension Robot.T = reshape(Robot.T, 3, 1, []); % Spread along the 3rd dimension gRobot.R = gpuArray(Robot.R); gRobot.T = gpuArray(Robot.T);
Наша новая функция преобразования с закольцовыванием требует двух вложенных циклов, чтобы закольцовывать как роботов, так и объекты.
function Rel = loopingTransform2(Robot, Map) Rel.R = zeros(3, 3, numObjects, numRobots, 'like', Map.R); Rel.T = zeros(3, numObjects, numRobots, 'like', Map.T); for i = 1:numObjects for j = 1:numRobots Rel.R(:,:,i,j) = Robot.R(:,:,1,j)' * Map.R(:,:,i); Rel.T(:,i,j) = ... Robot.R(:,:,1,j)' * (Map.T(:,i) - Robot.T(:,1,j)); end end end cpuTime = timeit(@()loopingTransform2(Robot, Map)); fprintf('It takes %3.4f seconds on the CPU to execute %d transforms.\n', ... cpuTime, numObjects*numRobots);
It takes 0.1493 seconds on the CPU to execute 10000 transforms.
Для наших таймингов GPU мы используем tic и toc на этот раз, потому что в противном случае расчет займет слишком много времени. Это будет достаточно точно для наших целей. Чтобы обеспечить включение любых затрат, связанных с созданием выходных данных, мы вызываем loopingTransform2 с одной выходной переменной, так же, как timeit и gputimeit по умолчанию.
fprintf('Computing...\n'); tic; gRel = loopingTransform2(gRobot, gMap); %#ok<NASGU> Suppress unused variable warning gpuTime = toc; fprintf('It takes %3.4f seconds on the GPU to execute %d transforms.\n', ... gpuTime, numObjects*numRobots); fprintf(['Unvectorized GPU code is %3.2f times slower ',... 'than the CPU version.\n'], gpuTime/cpuTime);
Computing... It takes 7.0564 seconds on the GPU to execute 10000 transforms. Unvectorized GPU code is 47.26 times slower than the CPU version.
Как и ранее, циклическая версия работает намного медленнее на GPU, поскольку не выполняет вычисления параллельно.
Новое pagefun версия должна включать transpose оператор, а также mtimes в вызов pagefun. Нам также нужно squeeze транспонированные ориентации робота, чтобы поместить разброс по роботам в 3-е измерение, чтобы соответствовать трансляциям. Несмотря на это, полученный код значительно компактнее.
function Rel = pagefunTransform2(Robot, Map) Rt = pagefun(@transpose, Robot.R); Rel.R = pagefun(@mtimes, Rt, Map.R); Rel.T = pagefun(@mtimes, squeeze(Rt), ... bsxfun(@minus, Map.T, Robot.T)); end
Еще раз, pagefun и bsxfun соответствующим образом расширить размеры. Итак, где мы умножаем 3-by-3-by-1-by-M матрицу Rt с матрицей 3-by-3-by-N-by-1 Map.R, мы получаем матрицу 3-by-3-by-N-by-M.
gpuPagefunTime = gputimeit(@()pagefunTransform2(gRobot, gMap)); fprintf(['It takes %3.4f seconds on the GPU using pagefun ',... 'to execute %d transforms.\n'], gpuPagefunTime, numObjects*numRobots); fprintf(['Vectorized GPU code is %3.2f times faster ',... 'than the CPU version.\n'], cpuTime/gpuPagefunTime); fprintf(['Vectorized GPU code is %3.2f times faster ',... 'than the unvectorized GPU version.\n'], gpuTime/gpuPagefunTime);
It takes 0.0025 seconds on the GPU using pagefun to execute 10000 transforms. Vectorized GPU code is 59.97 times faster than the CPU version. Vectorized GPU code is 2834.45 times faster than the unvectorized GPU version.
pagefun функция поддерживает ряд 2-D операций, а также большинство скалярных операций, поддерживаемых arrayfun и bsxfun. Вместе эти функции позволяют векторизировать ряд вычислений, включающих матричную алгебру и манипулирование массивом, устраняя необходимость в петлях и делая огромный прирост производительности.
Везде, где выполняются небольшие вычисления данных графического процессора в цикле, следует рассмотреть возможность преобразования в пакетную реализацию таким образом. Это также может дать возможность использовать графический процессор для повышения производительности в тех случаях, когда ранее он не давал преимуществ производительности.
end