Или: возвращаясь к основам

Это третья часть серии статей о программировании на CUDA, вы можете ознакомиться с предыдущими публикациями:

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

Суммируя

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

Что такое модель памяти?

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

Модель памяти CUDA

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

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

К счастью для нас, добрые люди из NVIDIA, которые разрабатывают CUDA, дали нам отличное решение, которое решает обе проблемы - унифицированную память.

Как ясно видно из изображения и названия, унифицированная модель памяти преследует одну простую цель - она ​​дает программистам одну область памяти для работы. Простая по идее, но на практике эта модель памяти экономит нам, разработчикам, время и силы. Предоставляя нам одну кучу для работы, мы теперь можем выделить память, доступную как для CPU, так и для GPU. Более того, этот метод позволяет нам предварительно выбрать память перед использованием, что означает, что она будет более доступна для графического процессора во время выполнения.

Время кодирования!

Давайте посмотрим на код в действии.

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

Этот код будет основан на базовом коде, который вы впервые увидели в предыдущем посте.

В этом коде стоит отметить несколько моментов.

Во-первых, для каждого указателя существует только один вызов выделения. Указатель теперь содержит адрес, который может использоваться как CPU, так и GPU. Вам может быть интересно, как это возможно. Короткий ответ заключается в том, что за кулисами CUDA перемещает память в память устройства по запросу графического процессора (для более подробного ответа вы можете прочитать этот пост команды разработчиков NVIDIA, в котором более подробно рассказывается о миграции объединенной памяти. система). Если вы думаете, что это неэффективно, вы правы. Мы увидим, как улучшить производительность через секунду.

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

Вот вторая версия того же кода, на этот раз с предварительной выборкой. Единственные изменения коснулись функции main в строках 13–17.

Мы используем функцию cudaMemPrefetchAsync, чтобы сделать память доступной для графического процессора до того, как он фактически запросит ее. Обратите внимание, где мы вызываем функцию. Мы делаем это непосредственно перед вызовом ядра, но после всех вызовов функции ЦП. Вызов функции предварительной выборки перед запуском ядра приводит к тому, что память начинает копироваться в фоновом режиме в память устройства. Для выполнения этой операции требуются как ЦП, так и ГП, поэтому мы выполняем ее только после всех остальных операций ЦП. Обратите внимание, что мы также должны указать функции предварительной выборки устройство, на которое мы хотим скопировать память. Получение идентификатора устройства (строки 14–15) обычно выполняется автоматически библиотекой CUDA, но также может быть отменено путем присвоения функции другого номера (например, если вы хотите написать код для нескольких графических процессоров). Для заинтересованных, полную документацию по функции предварительной выборки можно найти здесь.

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

Резюме

Итак, что мы узнали?

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

В следующий раз - проверка ошибок CUDA. Тогда увидимся!

Бонусные баллы: Напишите функцию, инициализирующую массивы как ядро, где нам теперь нужно предварительно выбрать память?