Будущее Программирования Аппаратных Ускорителей

Многие из новейших суперкомпьютеров основаны на аппаратных ускорителях.

включая две самые быстрые системы по версии TOP500 от 11/2013. Ускорители распространяются и на обычные ПК и даже появляются в портативных устройствах, что еще больше способствует росту интереса к программированию ускорителей.

Такое широкое распространение ускорителей обусловлено их высокой производительностью, энергоэффективностью и низкой стоимостью.

Например, если мы сравним Xeon E5-2687W и GTX 680, выпущенные в марте 2012 года, мы увидим, что GTX 680 в четыре раза дешевле, имеет в 8 раз большую производительность одинарной точности и в 4 раза большую пропускную способность памяти, а также обеспечивает более 30 в 6 раз выше производительности на доллар и в 6 раз выше производительности на ватт. Исходя из таких сравнительных результатов, ускорители следует использовать везде и всегда.

Почему этого не происходит? Есть две основные трудности.

Во-первых, ускорители могут эффективно выполнять только определенные классы программ, в частности программы с достаточным параллелизмом, повторным использованием данных, непрерывностью потока управления и структурами доступа к памяти.

Во-вторых, писать эффективные программы для ускорителей сложнее, чем для обычных ЦП, из-за архитектурных различий, таких как очень высокий параллелизм, открытая (без аппаратных кэшей) иерархия памяти, жесткие процедуры выполнения и объединение операций доступа к памяти.

Поэтому было предложено несколько языков программирования и расширений, чтобы в разной степени скрыть эти аспекты и тем самым упростить программирование ускорителей.

Первоначальные попытки использовать графические процессоры, которые сейчас являются самой известной формой ускорителей, для ускорения неграфических приложений были громоздкими и требовали представления вычислений в шейдерном коде, который поддерживал ограниченный поток управления и не поддерживал целочисленные операции.

Постепенно эти ограничения были сняты, что способствует более широкому внедрению вычислений на графических чипах и позволяет специалистам неграфических областей их программировать.

Важный шаг в этом направлении был сделан с выпуском языка программирования CUDA. Он расширяет C/C++ дополнительными спецификаторами и ключевыми словами, а также библиотеками функций и механизмом выполнения частей кода, называемых ядрами и графическими процессорами.

Быстрое внедрение CUDA в сочетании с тем фактом, что это запатентованный продукт, и сложностью написания качественного кода на CUDA приводит к созданию других подходов к программированию-ускорителю, включая OpenCL, C++ AMP и OpenACC. OpenCL является непатентованным аналогом CUDA и поддерживается многими крупными компаниями.

Он не ограничивается только чипами NVidia, но также поддерживает графические процессоры AMD, многоядерные процессоры, микросхемы (Intel Xeon Phi), DSP и FPGA, что делает его портативным.

Однако, как и CUDA, он очень низкоуровневый.

Требует от программиста непосредственного управления перемещением данных, требует непосредственного определения мест хранения переменных в иерархии памяти и ручной реализации параллелизма в коде.

C++ Accelerated Massive Parallelism (C++ AMP) работает на промежуточном уровне.

Он позволяет описывать параллельные алгоритмы на самом C++ и скрывает от программиста весь низкоуровневый код. Оператор foreach инкапсулирует параллельный код. C++ AMP привязан к Windows, пока не поддерживает ЦП и требует больших затрат при запуске, что делает непрактичным ускорение короткоисполняемого кода.

OpenACC — это уже очень высокоуровневый подход к программированию ускорителей, который позволяет программистам снабжать код директивами, тем самым сообщая компилятору, какие части кода необходимо ускорить, например, отправив их в графический процессор.

Идея аналогична тому, как OpenMP используется для распараллеливания программ ЦП.

Фактически предпринимаются усилия по объединению этих двух подходов.

OpenACC находится на стадии зрелости и в настоящее время поддерживается лишь несколькими компиляторами.

Чтобы понять, как в будущем будет развиваться область программирования аппаратных ускорителей, стоит изучить, как аналогичный процесс происходил в прошлом с другими аппаратными ускорителями.

Например, ранние продвинутые ПК имели дополнительный процессор — сопроцессор, выполнявший вычисления с плавающей запятой.

Позже он был объединен на кристалле с центральным процессором — CPU — и теперь является его неотъемлемой частью.

У них только разные регистры и арифметико-логические устройства (АЛУ).

Более поздние расширения процессора SIMD (MMX, SSE, AltiVec и AVX) не выпускались как отдельные чипы, а теперь также полностью интегрированы в ядро процессора.

Как и операции с плавающей запятой, инструкции SIMD вычисляются на отдельных ALU и с использованием собственных регистров.

Удивительно, но эти два типа инструкций существенно отличаются с точки зрения программиста.

Реальные типы и операции с ними давно стандартизированы (IEEE 754) и сегодня используются повсеместно.

Они доступны в языках программирования высокого уровня посредством обычных арифметических операций и встроенных вещественных типов данных: 32 бита для вещественных чисел одинарной точности и 64 бита для вещественных чисел двойной точности.

Напротив, для инструкций SIMD не существует стандартов, и само их существование во многом скрыто от программиста.

Использование этих инструкций для векторизации вычислений делегируется компилятору.

Разработчики, желающие явно использовать эти инструкции, должны связаться с компилятором, используя специальные некроссплатформенные макросы.

Поскольку производительность ускорителей GPU и MIC определяется их SIMD-природой, мы полагаем, что их развитие пойдет по пути предыдущих SIMD-ускорителей.

Еще одно сходство с SIMD и ключевая особенность CUDA, сделавшая его успешным, заключается в том, что CUDA скрывает SIMD-природу графических процессоров и позволяет программисту мыслить с точки зрения потоков, работающих со скалярными данными, а не с точки зрения деформаций, работающих с векторами.

Поэтому, несомненно, ускорители тоже будут перенесены на чип вместе с процессором, но мы считаем, что их программный код не будет в достаточной степени интегрирован в обычный код ЦП, равно как и аппаратные типы данных графического процессора не будут напрямую доступны программистам.

Некоторые ускорители уже объединены на кристалле с традиционными процессорами, это AMD APU (используется в Xbox One), процессоры Intel со встроенной HD-графикой и Tegra SoC от NVIDIA. Однако ускорители, вероятно, останутся отдельным ядром, поскольку их сложно объединить с традиционным процессорным ядром в той же степени, как это было сделано с математическим сопроцессором и SIMD-расширениями, то есть свести к набору регистров и отдельному АЛУ, как часть центрального процессора.

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

Следовательно, сложность запуска кода на ускорителях сохранится.

Поскольку даже процессорные ядра, реализованные на одном кристалле, обычно имеют общие лишь нижние уровни иерархии памяти, скорость обмена данными между ЦП и ускорителями, вероятно, увеличится, но все равно останется узким местом.

Необходимость явного управления обменом данными между устройствами является существенным источником ошибок и ложится тяжелым бременем на программистов.

Часто для небольших алгоритмов бывает, что для организации обмена данными приходится писать больше кода, чем для самих вычислений.

Устранение этого бремени — одно из основных преимуществ подходов к программированию высокого уровня, таких как C++ AMP и OpenACC. Даже низкоуровневые реализации решают эту проблему.

Например, хорошо настроенный и унифицированный доступ к памяти — одно из основных улучшений, которые реализованы в последних версиях аппаратных решений CUDA, OpenCL и NVIDIA GPU. Тем не менее, достижение хорошей производительности обычно требует помощи программиста, даже в решениях очень высокого уровня, таких как OpenACC. В частности, выделение памяти в необходимых местах и перенос данных часто приходится выполнять вручную.

К сожалению, все упрощения, предлагаемые такими подходами, могут быть лишь частичным решением.

Учитывая, что будущие процессоры будут близки к сегодняшним (маленьким) суперкомпьютерам, вполне вероятно, что у них будет больше ядер, чем сможет обработать их общая память.

Вместо этого мы думаем, что каждый чип будет иметь кластеры ядер, и каждый кластер будет иметь собственную память, возможно, расположенную над этими ядрами в трехмерном пространстве.

Кластеры будут связаны друг с другом сетью, работающей на одном чипе, с использованием протокола типа MPI. И это не так уж далеко от истины, потому что Intel только что объявила, что в будущих чипах Xeon появятся сетевые возможности, и это шаг в этом направлении.

Поэтому вполне вероятно, что чипы в будущем станут все более гетерогенными, объединяя ядра, оптимизированные по задержке и пропускной способности; сетевые адаптеры, центры сжатия и кодирования, ПЛИС и т. д. Это поднимает важнейший вопрос о том, как программировать такие устройства.

Мы считаем, что ответ на этот вопрос удивительно похож на то, как он решается сегодня для многоядерных процессоров, SIMD-расширений и существующих на данный момент аппаратных ускорителей.

Это происходит на трех уровнях, которые мы называем библиотеками, инструментами автоматизации и «сделай сам».

Библиотеки — это самый простой подход, основанный на простом вызове функций из библиотеки, которую кто-то уже оптимизировал для ускорителя.

Многие современные математические библиотеки относятся к этому классу.

Если большая часть вычислений программы выполняется в этих библиотечных функциях, то использование такого подхода будет вполне оправдано.

Это позволяет нескольким людям написать одну хорошую библиотеку, чтобы ускорить работу многих приложений, которые будут использовать эту библиотеку.

C++ AMP и OpenACC используют другой подход — инструменты автоматизации.

При таком подходе вся тяжелая работа передается компилятору.

Его успех зависит от качества и сложности существующих программных средств и, как уже было сказано, часто требует вмешательства программиста.

Однако большинство программистов могут довольно быстро добиться хороших результатов, используя этот подход, который не ограничивается простым использованием предопределенных функций из библиотек.

Это похоже на то, как несколько команд специалистов реализуют «внутренности» SQL, что позволяет обычным разработчикам впоследствии использовать готовый оптимизированный код. Наконец, подход «сделай сам» используется в CUDA и OpenCL. Он дает программисту полный контроль над доступом практически ко всем ресурсам ускорителя.

При правильной реализации полученный код превосходит по производительности любой из двух предыдущих.

Но это требует значительных усилий по изучению этого подхода, написанию большого количества дополнительного кода и больше места для возможных ошибок.

Всевозможные улучшения сред разработки и отладки могут смягчить все эти трудности, но лишь до определенной степени.

Это означает, что данный подход полезен в первую очередь для экспертов.

Те, кто именно развивает методы, упомянутые в двух предыдущих подходах.

Благодаря простоте использования библиотек программисты могут использовать их везде, где это возможно.

Но это возможно только при наличии соответствующих библиотечных функций.

В популярных районах такие библиотеки обычно существуют. Например, операции с матрицами (BLAS).

Но в смежных областях или там, где вычисления не структурированы, библиотеки-ускорители реализовать сложно.

При отсутствии соответствующих библиотек программисты выбирают средства автоматизации, если конечно они достаточно развиты.

Вычисления, которые недоступны в виде библиотек, не требуют большой производительности и поддерживаются компилятором, скорее всего, реализуются с использованием средств автоматизации.

В остальных случаях применяется метод своими руками.

Поскольку OpenCL сочетает в себе успешные решения, представленные в CUDA, не является проприетарным и поддерживает множество аппаратных решений, мы думаем, что он или его производные станут доминирующими в этой области, подобно тому, как MPI стал фактическим стандартом программирования систем с распределенной памятью.

.

Принимая во внимание аппаратные особенности и описанный выше процесс эволюции, можно сказать, что будущие процессорные чипы будут содержать множество кластеров с собственной памятью.

Каждый кластер будет состоять из множества ядер, и не обязательно все ядра будут функционально идентичны.

Каждое многопоточное ядро будет состоять из множества вычислительных блоков (то есть функциональных блоков или ALU), и каждый вычислительный блок будет выполнять инструкции SIMD. Даже если будущие чипы не будут включать в себя все это одновременно, все они будут иметь одно ключевое сходство: иерархию уровней параллелизма.

Чтобы создать эффективные и переносимые программы для таких систем, мы предлагаем то, что мы называем методом «обильного параллелизма».

Это обобщение того, как программы MPI адаптируются программистом к разному количеству вычислительных узлов или как код OpenMP неявно адаптируется к разному количеству ядер или потоков.

Основная идея расширенного параллелизма и причина его названия заключается в предоставлении обширных параметризуемых возможностей параллелизма на каждом уровне.

Параметризация позволит снизить степень параллелизма программы на любом уровне, чтобы привести ее в соответствие со степенью аппаратного параллелизма на этом уровне.

Например, в системе с разделяемой памятью не требуется высший уровень параллелизма, ее необходимо установить в один «кластер».

Аналогично, в ядре, где вычислительные блоки не могут выполнять инструкции SIMD, параметр, определяющий ширину SIMD, должен быть установлен равным единице.

Используя этот метод, вы можете реализовать возможности многоядерных процессоров, графических процессоров, микросхем и других устройств, а также возможных будущих аппаратных архитектур.

Несомненно, так писать программы сложнее, но обширный параллелизм позволяет извлекать высокую производительность из широкого класса устройств, используя единую кодовую базу.

Мы проверено Этот подход основан на задаче прямого моделирования n тел.

Мы написали одну высокопараллельную реализацию алгоритма с использованием OpenCL и провели измерения на четырех совершенно разных аппаратных архитектурах: графическом процессоре NVIDIA GeForce Titan, графическом процессоре AMD Radeon 7970, процессоре Intel Xeon E5-2690 и Intel Xeon Phi 5110P MIC. Учитывая, что 54% всех операций с плавающей запятой были не-FMA-операциями (FMA — операции умножения и накопления), обширный параллелизм позволил достичь производительности 75% от теоретического пика у NVIDIA Titan, 95% у Radeon, 80,5% у CPU и 80%.

% для ВПК.

Это лишь единичный пример, но результаты очень обнадеживают. Фактически, мы считаем, что в течение некоторого времени расширенный параллелизм будет единственным подходом к написанию переносимых высокопроизводительных программ для нынешних и будущих систем аппаратного ускорителя.

[ Источник ] 9 января 2014 г.

Камил Роки и Мартин Бурчер Теги: #cuda #opencl #GPGPU #Высокая производительность #GPGPU #Параллельное программирование

Вместе с данным постом часто просматривают: