Ускорители продолжают расти в своем значении и использовании, что это означает для разработчиков?

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

Профессор Дэвид Паттерсон из Калифорнийского университета в Беркли (автор всех книг по компьютерной архитектуре, которые я читал в колледже) много говорил о предметно-ориентированных архитектурах и ускорителях. Сегодня, когда мы говорим об аппаратном ускорителе, мы часто говорим о графическом процессоре. Тем не менее, существует множество различных типов ускорителей, предназначенных для решения различных задач, включая глубокое обучение и ИИ, в которых используется оборудование, специально разработанное для выполнения крупномасштабных матричных операций, лежащих в основе рабочих нагрузок глубокого обучения.

Кроме того, в традиционные ЦП встроены технологии аппаратного ускорения, такие как Intel® Advanced Vector Extensions (AVX) и Intel® Advanced Matrix Extensions (AMX).

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

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

Обзор параллелизма

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

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

Параллелизм задач

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

Если мы сопоставим это с компьютерной программой, то каждый человек будет аналогичен некоторому вычислительному оборудованию (ЦП/ГП/ПЛИС), и сбор различных элементов – это задачи, которые необходимо выполнить.

Параллелизм данных

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

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

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

Доступные ресурсы, доступный параллелизм и конкуренция

Некоторые важные вещи, которые следует учитывать при параллелизме, — это доступные ресурсы и доступный параллелизм в заданном пространстве решений.

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

Еще одна отдельная проблема, которую следует рассмотреть, — это споры. Ресурсы, как правило, ограничены, и часто наши попытки распараллелить проблему наталкиваются на проблемы с доступом к ресурсам.

Давайте представим, что мы идем в магазин за мороженым, а в морозилке 100 контейнеров с мороженым, но перед морозильной камерой одновременно могут стоять только три человека. Это означает, что даже если 100 человек приходят за мороженым, получение мороженого по-прежнему имеет максимум три параллелизма из-за ограниченного доступа к морозильной камере.

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

Простой пример кода

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

Параллелизм с OpenMP®

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

Этот простой код состоит из четырех простых частей:

  1. Строка 18: создайте 65536 покупателей
  2. Строка 21: вызов функции Shop
  3. Строка 10–12: попросите каждого покупателя пойти за покупками.
  4. Строка 9: прагма, указывающая компилятору при использовании OpenMP выполнять итерации этого цикла параллельно.

Остальное — это просто таймеры, поэтому мы можем увидеть преимущества параллельного выполнения кода.

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

В этой статье я использую ноутбук HP Envy* с процессором Intel® Core™ i7–12700H, 32 ГБ памяти и встроенным графическим процессором Intel® Iris® Xe. В системе работает Ubuntu* 22.04, ядро ​​6.0 и компилятор Intel® oneAPI DPC++/C++.

Во-первых, я включил среду компилятора Intel с помощью этой команды:

> /opt/intel/oneapi/setvars.sh

Следующие команды компилируют код:

> icx -lstdc++ grocery-omp.cpp -o serial-test
> icx -lstdc++ -fiopenmp grocery-omp.cpp -o omp-test

Последняя команда включает флаг -fiopenmp, указывающий компилятору включить параллелизм с использованием OpenMP. Запустив исполняемые файлы, я получаю этот вывод в своей системе:

> ./serial-test
Elapsed time in milliseconds: 27361 ms
> ./omp-test
Elapsed time in milliseconds: 4002 ms

Запуск OpenMP использует несколько ядер ЦП для одновременного выполнения работы, что приводит к 6-7-кратному ускорению запуска OpenMP.

Параллелизм с oneAPI/SYCL

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

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

  1. Строка 31: создайте 65536 покупателей
  2. Строка 46: вызов функции Shop
  3. Строка 25: Попросите каждого покупателя пойти за покупками.

В отличие от примера OpenMP, где мы используем директивы, SYCL позволяет пользователям явно определять параллельное поведение своей программы с помощью кода и конструкций C++. Это обеспечивает гибкость во время выполнения, которая недоступна в OpenMP.

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

> SYCL_DEVICE_FILTER=host:host:0 ./sycl-test
Running on device: SYCL host device
Elapsed time in milliseconds: 27201 ms
> SYCL_DEVICE_FILTER=opencl:cpu:1 ./sycl-test
Running on device: 12th Gen Intel(R) Core(TM) i7-12700H
Elapsed time in milliseconds: 4197 ms
> SYCL_DEVICE_FILTER=opencl:gpu:3 ./sycl-test
Running on device: Intel(R) Graphics [0x46a6]
Elapsed time in milliseconds: 3988 ms

Значения для SYCL_DEVICE_FILTER получаются при выполнении команды sycl-ls, которая входит в состав Intel® oneAPI Base Toolkit.

Отличительной особенностью oneAPI C++ с реализацией SYCL является то, что двоичный файл может направлять работу на несколько устройств.

Заключение

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

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

Спасибо за прочтение!

Want to Connect?
If you want to see what random tech news I’m reading, you can follow me on Twitter.

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