Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Вычислительная модель GPU
Рассмотрим вычислительную модель GPU более подробно: · является сопроцессором к CPU (называемому host); · обладает собственной памятью (DRAM); · обладает возможностью параллельного выполнения огромного количества отдельных нитей (threads).
При этом между нитями на CPU и нитями на GPU есть принципиальные различия - нити на GPU обладают крайне " небольшой стоимостью" - их создание и управление требует минимальных ресурсов (в отличии от CPU)для эффективной утилизации возможностей GPU нужно использовать многие тысячи отдельных нитей (для CPU обычно нужно не более 10-20 нитей). Сами программы пишутся на " расширенном" С, при этом их параллельная часть (ядра) выполняется на GPU, а обычная часть - на CPU. CUDA автоматически осуществляет разделением частей и управлением их запуском. CUDA использует большое число отдельных нитей для вычислений, часто каждому вычисляемому элементами соответствует одна нить. Все нити группируются в иерархию - grid/block/thread, как показано на (рис. 3). Рис. 3 Иерархия потоков CUDA. Верхний уровень - grid - соответствует ядру и объединяет все нити выполняющие данное ядро. grid представляет собой одномерный или двухмерный массив блоков (block). Каждый блок (block) представляет из себя одно/двух/трехмерный массив нитей (threads). При этом каждый блок представляет собой полностью независимый набор взаимодействующих между собой нитей, нити из разных блоков не могут между собой взаимодействовать (см. [1]). Фактически блок соответствует независимо решаемой подзадаче, так например если нужно найти произведение двух матриц, то матрицу-результат можно разбить на отдельные подматрицы одинакового размера. Нахождение каждой такой подматрицы может происходить абсолютно независимо от нахождения остальных подматриц. Нахождение такой подматрицы - задача отдельного блока, внутри блока каждому элементу подматрицы соответствует отдельная нить. При этом нити внутри блока могут взаимодействовать между собой (т.е. совместно решать подзадачу) через общую память (shared memory)функцию синхронизации всех нитей блока (__synchronize. Подобная иерархия довольно естественна - с одной стороны хочется иметь возможность взаимодействия между отдельными нитями, а с другой - чем больше таких нитей, тем выше оказывается цена подобного взаимодействия. Поэтому исходная задача (применение ядра к входным данным) разбивается на ряд подзадач, каждая из которых решается абсолютно независимо (т.е. никакого взаимодействия между подзадачами нет) и в произвольном порядке. Сама же подзадача решается при помощи набора взаимодействующих между собой нитей. С аппаратной точки зрения все нити разбиваются на так называемые warp'ы - блоки подряд идущих нитей, которые одновременно (физически) выполняются и могут взаимодействовать друг с другом. Каждый блок нитей разбивается на несколько warp'ов, размер warp'а для всех существующих сейчас GPU равен 32 (см. [2]). Важным моментом является то, что нити фактически выполняют одну и ту же команды, но каждая со своими данными. Поэтому если внутри warp'а происходит ветвление (например в результате выполнения оператора if), то все нити warp'а выполняют все возникающие при этом ветви. Поэтому крайне желательно уменьшить ветвление в пределах каждого отдельного warp'а. Также используется понятие half-warp'а - это первая или вторая половина warp'а. Подобное разбиение warp'а на половины связано с тем, что обычно обращение к памяти делаются отдельно для каждого half-warp'а. Кроме иерархии нитей существует также несколько различных типов памяти. Быстродействие приложения очень сильно зависит от скорости работы с памятью. Именно поэтому в традиционных CPU большую часть кристалла занимают различные кэши, предназначенные для ускорения работы с памятью (в то время как для GPU основную часть кристалла занимают ALU).
2.4 Модель памяти CUDA Модель памяти в CUDA отличается возможностью побайтной адресации. Доступно довольно большое количество регистров на каждый потоковый процессор, до 1024 штук. Доступ к ним очень быстрый, хранить в них можно 32-битные целые или числа с плавающей точкой (см.[9]). Каждый поток имеет доступ к пяти типам памяти, все они изображены на (рис. 4).
Рис. 4 Типы памяти CUDA Глобальная память — самый большой объём памяти, доступный для всех мультипроцессоров на видеочипе, размер составляет от 256 мегабайт до 1.5 гигабайт на текущих решениях (и до 4 Гбайт на Tesla). Обладает высокой пропускной способностью, более 100 гигабайт/с для лучших решений Nvidia, но очень большими задержками в несколько сот тактов. Не кэшируется, поддерживает обобщённые инструкции load и store, и обычные указатели на память. Локальная память — это небольшой объём памяти, к которому имеет доступ только один потоковый процессор. Она относительно медленная — такая же, как и глобальная. Разделяемая память — это 16-килобайтный (в видеочипах нынешней архитектуры) блок памяти с общим доступом для всех потоковых процессоров в мультипроцессоре. Эта память весьма быстрая, такая же, как регистры. Она обеспечивает взаимодействие потоков, управляется разработчиком напрямую и имеет низкие задержки. Преимущества разделяемой памяти: использование в виде управляемого программистом кэша первого уровня, снижение задержек при доступе исполнительных блоков (ALU) к данным, сокращение количества обращений к глобальной памяти. Память констант — область памяти объемом 64 килобайта (то же — для нынешних GPU), доступная только для чтения всеми мультипроцессорами. Она кэшируется по 8 килобайт на каждый мультипроцессор. Довольно медленная — задержка в несколько сот тактов при отсутствии нужных данных в кэше. Текстурная память — блок памяти, доступный для чтения всеми мультипроцессорами. Выборка данных осуществляется при помощи текстурных блоков видеочипа, поэтому предоставляются возможности линейной интерполяции данных без дополнительных затрат. Кэшируется по 8 килобайт на каждый мультипроцессор. Медленная, как глобальная — сотни тактов задержки при отсутствии данных в кэше (см.[10]). Естественно, что глобальная, локальная, текстурная и память констант — это физически одна и та же память, известная как локальная видеопамять видеокарты. Их отличия в различных алгоритмах кэширования и моделях доступа. Центральный процессор может обновлять и запрашивать только внешнюю память: глобальную, константную и текстурную. Как изображено на (рис. 5). Рис. 5 Обращение CPU к памяти GPU Из написанного выше понятно, что CUDA предполагает специальный подход к разработке, не совсем такой, как принят в программах для CPU. Нужно помнить о разных типах памяти, о том, что локальная и глобальная память не кэшируется и задержки при доступе к ней гораздо выше, чем у регистровой памяти, так как она физически находится в отдельных микросхемах. Типичный, но не обязательный шаблон решения задач: · задача разбивается на подзадачи; · входные данные делятся на блоки, которые вмещаются в разделяемую память; · каждый блок обрабатывается блоком потоков; · подблок подгружается в разделяемую память из глобальной; · над данными в разделяемой памяти проводятся соответствующие вычисления; · результаты копируются из разделяемой памяти обратно в глобальную.
Программирование CUDA При использовании традиционных API программист вне зависимости от сложности алгоритма всегда обязан конфигурировать все части графического конвейера. Этот факт существенно затрудняет использование GPU для решения задач общего назначения, так как даже простое сложение двух матриц требует выполнение ряда команд по подготовке и отрисовке изображений во внеэкранном буфере. В итоге на несколько строк шейдерной программы приходятся сотни строк дополнительного кода. При решении задач с небольшой размерностью эти дополнительные затраты способны свести на нет весь выигрыш от использования GPU (см.[8]). Модель программирования, используемая в CUDA отличается от традиционных API тем, что полностью скрывает графический конвейер от программиста, позволяя ему тем писать программы в более привычных для него «терминах» на расширенной вариации языка C. Кроме того, CUDA предоставляет программисту более удобную модель работы с памятью. Больше нет необходимости хранить данные в 128-битных текстурах, так как CUDA позволяет читать данные напрямую из памяти видеокарты. В состав NVIDIACUDA входят два API: высокого уровня (CUDA Runtime API) и низкого (CUDA Driver API) смотри (рис. 6).
Рис. 6 Схема CUDA API При необходимости задействовать низкоуровневые функции графического процессора программист всегда может отказаться от Runtime API в пользу Driver API (см.[12]). Первым шагом при переносе существующего алгоритма на CUDA непременно является его анализ, цель которого состоит в поиске «узкого места», нуждающегося в распараллеливании. Как правило, в алгоритме для CPU такие участки заключены в цикл или рекурсию. Полный перенос алгоритма на GPU не является возможным, так как графический процессор не имеет доступа ни к памяти компьютера, ни к устройствам ввода/вывода (за исключением буфера кадра, который может быть отображен в виде картинки на экране компьютера). При исполнении программы CPU по-прежнему отвечает за подготовку и постобработку данных, сама же трудоемкая работа ложится на GPU. Набор инструкций, исполняемый на графическом процессоре, называется ядром (kernel). Ядро, по сути, является развитием концепции шейдеров. За формирование и компиляцию ядер отвечает CPU. Видеочип просто принимает уже скомпилированное ядро и создает его копии для каждого элемента данных. Каждое из ядер исполняется в своем собственном потоке. Потоки в GPU могут исполняться лишь группами по 32 экземпляра (wrap). При этом общее число потоков необходимое для решения задачи может превосходить максимально допустимое для текущей видеокарты. Поэтому каждый такт аппаратное обеспечение выбирает, какой из wrap будет исполнен. Но если бы в CPU подобное переключение заняло бы сотни тактов, то GPU делает это почти мгновенно. В отличие от шейдеров, где все данные представлены в виде четырех компонентных векторов, данные в ядре скалярных. Такое представление более естественно для большинства неграфических задач. Модель программирования CUDA предполагает группировку потоков блоки–одно-, двух-или трехмерные матрицы. Взаимодействие между ними осуществляется при помощи разделяемой памяти. Также существуют точки синхронизации, позволяющие привести данные во всех потоках в актуальное состояние. Каждое из ядер исполняется над сеткой (grid) блоков. В каждый момент времени на GPU может исполняться лишь одна сетка. Подобная группировка позволяет достичь максимальной масштабируемости. Если у GPU недостаточно ресурсов для запуска всех блоков – они будут выполняться последовательно, друг за другом. Это позволяет разработчику не задумываться о мощности устройства, на котором будет запущено приложение. В каждом приложении, написанном на NVIDIACUDA вне зависимости от его назначения можно выделить ряд общих шагов: Подготовка памяти. ПосколькуGPU не имеет доступа к оперативной памяти программисту необходимо заранее позаботиться о том, что все ресурсы, необходимые для исполнения ядра приложения находятся в памяти видеокарты. Для этих целей используются три основных функции из CUDASDK: cudaMalloc, cudaMemcpy и cudaFree. Эти функции имеют то же назначение, что и стандартные malloc, memcpy и free, но, разумеется, все операции проводятся над видеопамятью. Так же стоит отметить, что функция cudaMemcpy имеет дополнительный параметр, обозначающий направление копирования информации (из CPU в GPU или наоборот). Конфигурация сетки ( grid ) и блоков ( blocks ). Процесс конфигурации крайне прост и заключается в задании размеров сетки и блоков. Основной задачей программиста на данном шаге является нахождение оптимального баланса между размером и количеством блоков. Увеличением количества потоков в блоке можно снизить количество обращений к глобальной памяти за счет увеличения интенсивности обмена данными между потоками через быструю разделяемую память. С другой стороны, количество регистров выделяемых на блок фиксировано и если количество потоков окажется сильно большим, то GPU начнет размещать данные в медленной локальной памяти, что существенно увеличит время исполнения ядра. NVIDIA рекомендует программистам использовать блоки по 128 или 256 потоков. В большинстве задач такое количество потоков в блоке позволяет достичь оптимальных задержек и количества регистров. Запуск ядра. Ядро вызывается как обычная функция в языке C. Единственное существенное отличие заключается в том, что при вызове ядра необходимо передать ранее определенные размерности сетки и блока. Получение результатов и освобождение памяти. После исполнения ядра необходимо скопировать результаты выполнения программы назад, в оперативную память при помощи функции cudaMemcpy с указанием обратного направления копирования (из GPU в CPU). Точно также, как и в любой C-программе для предотвращения утечек памяти необходимо освободить все выделенные ресурсы.
|
Последнее изменение этой страницы: 2019-03-20; Просмотров: 465; Нарушение авторского права страницы