В данном разделе мы рассмотрим, каким образом по исходному коду Фортран программы строится аналитическая модель, каким образом она отображается на ускоритель, а также оценку эффективности данного отображения.
Прежде всего, уточним, какая информация извлекается из базы данных САПФОР после работы анализатора и DVMH-эксперта.
4.1 Входные данные. Система САПФОР.
Предиктор извлекает из базы данных и сохраняет для дальнейшего анализа информацию о следующих свойствах:
-
список всех процедур программы, с информацией о параметрах и операторах вызова;
-
для каждой процедуры извлекаются тела циклов, с учётом вложенности;
-
для каждого цикла извлекается доступная информация об операторах цикла (ветвления, вызовы процедур, обращения к переменным, операции в теле цикла – арифметические, логические, приводящие к преобразованиям типов, трансцендентные);
-
для каждого цикла извлекается информация о наличии межвитковых зависимостей, коллективных операций, создании приватных переменных на каждом витке;
-
извлекается список всех переменных программы (их тип, размер, размерность – в случае, если идентификатор обозначает массив);
-
для обращения к массивам извлекается информация о выражениях, использованных при индексации;
-
извлекается информация о том, для каких циклов DVMH-эксперт указал возможность распараллеливания в данной схеме;
-
извлекается информация о об участках программы, подлежащих исполнению на ускорителе (размечены как DVMH-регионы), указания по обновлению данных на выходе/входе в регион.
В базе данных САПФОР по умолчанию не отражены операции в теле цикла, поэтому информация о них была добавлена отдельно. Процесс был автоматизирован: в процессе работы был написан лексический анализатор Фортран, который в исходный код последовательной программы вставляет комментарии особого вида. Они содержатся в базе данных после работы DVMH-эксперта.
4.2 Обзор модели программы для исполнения на графическом ускорителе.
После извлечения информации, описанного выше, становится возможным понять, какие циклы не содержат препятствий для распараллеливания их витков на кластер или мультипроцессор. В частности, такие циклы должны иметь только один вход или выход, не должны содержать вызовов подпрограмм с другими параллельными циклами, должны иметь регулярный шаг и зависимости по данным, индексные выражения в обращениях к массивам должны быть линейными. Согласно [10] и [11] выполнения этих условий уже достаточно для построения некоторого отображения параллельных циклов в функции, предназначенные для запуска на ускорителе (далее будем называть их функциями-ядрами). Уточним условия:
-
в функцию-ядро отображается каждый параллельный цикл, для которого DVMH-эксперт указал возможность распараллеливания;
-
для выполнения витков цикла строится конфигурация решётки блоков и нитей ускорителя, каждая нить отвечает за обработку одного или более витков;
-
число машинных инструкций и операций с памятью в каждой нити рассчитывается на основании анализа исходных данных;
-
число инструкций, выполняемых нитью, число обращений в память, число регистров, необходимых во время исполнения витка цикла, рассчитывается на основании анализа исходных данных;
-
при наличии зависимостей между витками организуется конвейер исполнения блоков, при этом параллелизм внутри блока сохраняется.
4.3 Алгоритмы
4.3.3 Алгоритм базового подсчёта числа инструкций.
Зная количество итераций цикла, конфигурацию решётки блоков, построенной по умолчанию или заданной пользователем, можно подсчитать число итераций, которое обрабатывает одна нить:
num_iter_per_thread = cycle_iters / (grid_size * block_size) (1)
Такой расчёт производится для каждого измерения параллельного цикла. Построение вычислительной решётки и анализ зависимостей описывается более подробно в следующих разделах.
Чтобы получить общее количество инструкций и обращений к памяти, обрабатываемых нитью, следует вычислить данные характеристики на одной итерации и умножить на количество итераций, которое эта нить обрабатывает (формула (1)). Количество инструкций и операций во время выполнения одного витка цикла рассчитывается согласно данным, предоставляемым БД САПФОР:
-
первое обращение к переменной, не являющейся массивом, трактуется как обращение в глобальную память, последующие – обращения к локальному регистру;
-
в случае, когда пользователь указал не использовать кэш при построении прогноза, каждое обращение к массиву считается обращением в глобальную память с анализом выравнивания доступа;
-
в случаем, когда влияние кэша эмулируется, первое обращение к массиву считается обращением в глобальную память с анализом выравнивания доступа, иначе значение берётся из кэша L1, задержка – как для арифметической операции;
-
из БД извлекается информация об арифметических, логических операциях, вызовах стандартных процедур, ветвлениях, содержащихся в теле цикла;
-
производится соответствующий подсчёт числа регистров и разделяемой памяти, необходимой каждому блоку.
Таким образом, основные характеристики:
-
num_arithm– число простых арифметических операций;
-
num_reciproc– число операций, содержащих обращения значений (задержка больше, чем для простых инструкций);
-
num_branch – число ветвлений в коде итерации;
-
num_sync – число инструкций барьерной синхронизации в коде
-
coal_mem_access – число выровненных доступов в глобальную память;
-
uncoal_mem_access – число невыровненных доступов в глобальную память;
-
uncoal_per_access – среднее число транзакций, генерируемое для одного невыровненного доступа в память;
-
coal_per_access - – среднее число транзакций, генерируемое для одного выровненного доступа в память;
-
load_bytes_per_warp – средняя длина каждой транзакции;
Глобальная память обладает высокой латентностью, поэтому для достижения эффективной работы приложений доступ к ней необходимо оптимизировать.
При чтении и записи значений глобальной памяти на низком уровне используются 32-, 64-, 128-битные слова. Если при кратном выравниванию размере элементов массива, базовый адрес массива по какой-либо причине оказался невыровненным, то чтение каждого элемента потребует двух обращений, вместо одного.
Существует возможность объединения запросов всех нитей варпа в одно обращение к непрерывному блоку глобальной памяти (coalescing – выровненный доступ), что может на порядок ускорить работу приложения. Существуют определённые условия (для каждой версии compute capability ускорителя), при которых происходит объединение запросов в одну транзакцию. Эти условия проверяются для каждого варпа отдельно, но на практике обычно они выполняются либо для всех варпов сразу, либо не выполняются ни для одного.
Для того, чтобы GPU с compute capability 1.0 или 1.1 произвёл объединение запросов нитей полуварпа в одну транзакцию, необходимо выполнение следующих условий:
-
нити должны обращаться к 32-битным словам (или 64-битным словам), в результате получая 64-байтовый блок (или 128-байтовый соответственно);
-
полученный блок должен быть выровнен по размеру (адрес 64-байтного блока кратен 64, адрес 128-байтного – 128);
-
все 16 слов, к которым обращаются нити, находятся внутри блока;
-
нити обращаются к словам последовательно, т.е. нить с номером n обращается к n-ому слову. При этом допускается, что некоторые нити пропустят обращение к соответствующим словам.
Условия для compute capability 1.2 и 1.3 были ослаблены. При этом порядок обращения нитей словам (при условии попадания запросов в соответствующий сегмент) роли не играет. Также существенно, что доступ в случае отсутствия выровненности для всех нитей в варпе разбивается на наименьшее возможное число транзакций (в предыдущих версиях compute capability в случае невыровненности транзакция генерировалась для каждой нити).
Таким образом, пусть базовый тип переменных массива занимает var_size байт. Число нитей в варпе – threads_per_warp = 32.
Тогда размер сегментов (он же размер каждой из транзакций), к которым обращаются нити:
-
segment_size = 32, var_size = 1
-
segment_size = 64, var_size = 2
-
segment_size = 128, var_size >= 4 (2)
Размер смещения между обращениями соседних нитей:
stride = fastest_changing_index * var_size (3)
Число транзакций для данной архитектуры – число обращений в различные сегменты размера segment_size. Отсюда, чем меньше смещение, тем больше адресов попадает в один сегмент. Количество транзакций:
uncoal_per_access = ceil (threads_per_warp / ceil (segment_size / stride)) (4)
где ceil() –функция округления вверх. Если uncoal_per_access = 1, то все запросы удалось объединить в одну транзакцию, тогда coal_per_access = 1 (доступ выровненный). Есть особенность – если все запрашиваемые адреса попадают в один сегмент, и при этом кэширование отключено, то такой выровненный доступ обслуживается несколькими транзакциями, размером по 32 байта.
coal_per_access = segment_size / 32 (5)
Стоит отметить, что в архитектуре Fermi (compute capability 2.0) нарушение выравнивания может не привести к дополнительной транзакции, если остаток (последний сегмент) успешно был найден в кэше L1. Поскольку алгоритмы работы кэша не являются общедоступными, а само моделирование происходит без анализа сгенерированного кода, такая ситуация не может быть точно предсказана в данном исследовании. Как бы то ни было, полезно помнить о такой возможности, во время сравнения результатов предсказания и запуска реальных программ на ускорителе.
4.3.5 Алгоритм подсчёта ресурсов, необходимых блоку.
Число ресурсов, необходимых активному блоку определяет количество блоков, которые могут обслуживаться одновременно на ускорителе. К сожалению, алгоритмы выделения регистров на потоковом процессоре ускорителя не являются общедоступными, поэтому были сделаны некоторые эвристические предположения.
Алгоритм подсчёта числа регистров, требуемых каждой нити
-
для локальной переменной, не являющаяся массивом, отводится 1, 2 или 4 регистра в зависимости от своего типа (INT, REAL, DOUBLE);
-
отводится регистр для хранения счётчиков итераций нити, в случае выполнения нитями блока нескольких витков;
-
отводится регистр для хранения индекса нити по каждому измерению решётки нитей;
-
регистр для хранения указателя инструкции;
-
вычисляется количество регистров, требуемых для хранения промежуточных результатов итераций;
-
регистр для хранения промежуточных результатов на каждые 10 строк цикла.
Общее число регистров, требуемых каждому блоку может быть подсчитано по формуле:
required_regs = ceil (regs_per_thread * threads_per_warp, 128) * threads_in_block / 32 (6)
при этом regs_per_thread подсчитано выше, threads_per_warp – введено выше, _in_block – общее число нитей в блоке, ceil(x,y) – округляет x до ближайшего целого, кратного y.
Другим важным ресурсом является разделяемая память. В системе DVM разделяемая память используется для проведения коллективных операций, для прочих не используется. Тем не менее, компилятор может выделить некоторое количество разделяемой памяти для нужд системы. В модели данное число оценивается, как required_shared_mem = block_dimensions * 32
4.3.6 Алгоритм анализа выполнения коллективных операций
В DVM-системе редукция осуществляется отдельным ядром, выполняющегося отдельным блоком размерности [threads_x 1 1], где threads_x – количество нитей по x-измерению блока; по y- и z- измерениям число нитей равно 1.
threads_x = )/ 2, (7)
где iterationsi – количество итераций ядровой функции по соответствующему измерению, а theadsi – количество нитей по измерению блоков, которые использовались для начальной обработки ядровой функции (i = 1..3). На каждом шаге активный варп формирует одно значение (из 32-х нитей). Если число варпов в блоке превышает 32 (размерность варпа), производится повторная редукция, иначе значения отсылаются хосту, где происходит финальный этап редукции.
Выделение разделяемой памяти для использования блока определяется формулой:
required_shared_mem += num_threads_in_block * reduction_var_size, (8)
где num_threads_per_block есть общее число нитей в блоке, а reduction_var_size – размер переменной в байтах, по которой производится коллективная редукция.
Задержки на синхронизацию при этом определяются так:
num_sync += log2 (num_threads_in_block * num_iterations_per_thread) (9)
Аналогично определяется число арифметических операций, во время обработки данных:
num_arithm += log2 (num_threads_in_block * num_iter_per_thread) (10)
4.3.7 Алгоритм использования кэш-памяти в модели
По умолчанию в модели предполагается использование 48 килобайтного кэша уровня L1 архитектуры Fermi (максимальное значение для L1), минимальный объём кэша составляет 16 килобайт. У пользователя имеется возможность запустить моделирование с учётом влияния кэш-памяти или указать предиктору не использовать кэш. Также имеется возможность задать размер кэша (значение в килобайтах, не меньше минимального и не больше максимального).
Допущения, принятые при моделировании:
-
для каждой переменной, адресующей массив, в кэш записывается сегмент памяти, начиная с адреса этой переменной. Длина сегмента зависит от базового типа переменной – соответствие определяется формулой (2);
-
переменная остаётся в кэше максимально долго, пока не будет замещена;
-
в первую очередь замещению подлежат наименее использовавшиеся переменные;
-
при добавлении в кэш нового значения производится вычисление свободного места. Если свободного места недостаточно, то происходит замещение одной или нескольких переменных (в зависимости от размера добавляемого сегмента), по принципу, описанному выше.
Предлагаемый алгоритм является эмпирическим. Как показывает данное исследование, прогноз с учётом таких предположений предполагает максимальное использование кэша и прогнозирует время в среднем меньшее, чем при реальном запуске. Прогноз времени работы с отключенным кэшем считает каждое обращение к массиву обращением в глобальную память, из-за чего время получается большее, чем при запуске, т.к. полностью использование кэша при запуске задачи из системы DVM отключить не получается, можно только ограничить использование. Для учёта эффекта, производимого кэшем при запуске на ускорителе, на простых задачах были вычислен поправочные коэффициенты.
4.3.7 Алгоритм построения вычислительной сетки нитей и блоков.
Построение вычислительной сетки нитей и блоков в модели осуществляется точно так, как это реализовано в системе DVM. Это было сделано для того, чтобы обеспечить сопоставимые условия для прогноза и запуска – соответствие размеров вычислительных блоков, их количества, порядка заполнения решётки. Соответствие вычислительных блоков, порядка их построения, - необходимые условия для адекватности анализа выровненности обращений нитей к памяти. Рассмотрим установки DVM, касающиеся отображения пространства витков цикла на блоки графического ускорителя.
Итак:
-
сетка блоков всегда является одномерной – т.е. [bloks_num 1 1] (где blocks_num – число блоков, необходимых для запуска функции-ядра на ускорителе);
-
размерность блока по числу нитей зависит от глубины вложенности параллельного цикла (функции-ядра в модели) отображаемого на ускоритель:
-
для одномерного случая устанавливается сетка [448 1 1] (здесь и далее – сначала обозначается количество нитей по измерению X, затем по Y, затем по Z);
-
для двумерного случая – [32 14 1];
-
для глубины не менее 3 сетка нитей – [32 2 7];
Оптимальность данных установок не является предметом настоящего исследования. Чтобы иметь возможность адекватно сравнивать результаты запуска реальных программ с использованием системы DVM и прогноза предиктора, именно они были приняты по умолчанию. В DVM системе у пользователя имеется возможность вручную задать размерность блока по числу нитей для каждого цикла, такая же возможность реализована в предикторе.
В системе DVM пространство витков цикла заполняется по следующему алгоритму:
-
CUDA-блок переводится в пространство размерности цикла, при этом:
-
размерностью цикла считается количество вложенных подциклов плюс 1 (сам цикл);
-
младшим измерением цикла считается наиболее глубоко вложенное, старшим - внешнее измерении цикла, т.е. для гнезда циклов
do i = 1,3
do j = 1,6
...
enddo
enddo
старшим является измерение i, младшим – j;
-
если необходимо, при размерности цикла >= 3-м, по старшим измерениям блок дополняется единицами;
-
по самому младшему измерению будет взят размер блока по оси X, затем по оси Y, затем по Z;
-
таким образом получается необходимое для отображения замощение.
4.3.8 Алгоритм построения вычислительной решётки
Схема 1. Построение вычислительной решётки
Заполнение пространства витков блоками в предикторе происходит аналогично описанному выше. Мы имеем некоторый цикл, замощение по самому младшему измерению осуществляется с шагом X, следующему по старшинству с шагом Y, следующему – Z, где X, Y, Z – соответствующие размерности блока. По всем остальным измерениям шаг замощения равен единице. При этом могут получаться остатки от деления итераций цикла на соответствующую размерность блока, тогда образуются соответствующие блоки. На схеме остатки обозначены как x, y, z. Иллюстрация приведена для трёхмерного случая.
После чего одномерная решётка блоков формируется последовательным обходом по измерениям, начиная с самого младшего, т.е. того, которому соответствует наиболее быстроменяющийся индекс параллельного цикла.
Достарыңызбен бөлісу: |