Передача параметров

Опубликовано марта 5, 2018 в Технология CUDA

Мы обещали рассказать о том, как передаются параметры ядру, и теперь пришло время исполнить обещание. Рассмотрим следующую модификацию программы «Здравствуй, мир!»:

Новых строк здесь много, но новых концепций всего две:

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

Передача параметров

Ничего особенного в передаче параметров ядру нет. Несмотря на угловые скобки, вызов ядра выглядит и работает как вызов любой другой написанной на с тандартном С функции. Исполняющая среда берет на себя заботу о передаче параметров от CPU устройству.

Более интересно выделение памяти с помощью функции cudaMalloc () Она очень похожа на стандартную функцию malloc (), но говорит исполняющей среде CUDA, что память должна быть выделена на устройстве. Первый аргумент — это указатель на указатель, в котором будет возвращен адрес выделенной области памяти, а второй — размер этой области. Если не считать того, что указатель на выделенную область не возвращается функцией в виде значения, то поведение ничем не отличается от malloc (), даже тип возвращаемого значения void* совпадает. Конструкция HANDIE_ERROR (), окружающая обращения, — это служебный макрос, определенный в коде, прилагаемом к книге. Когда функция возвращает ошибку, он печатает сообщение и завершает приложение с кодом EXIT_FAILURE. Хотя никто нс запрещает вам использовать этот макрос и в своих программах, скорее всего, такой стратегии обработки ошибок в промышленном коде будет недостаточно.

И тут возникает тонкий, но важный момент. Своей простотой и мощью язык CUDA С во многом обязан стиранию грани между кодом для CPU и для устройства. Однако программист не должен разыменовывать указатель, возвращаемый cudaMalloc (), в коде, исполняемом CPU. Этот указатель можно передавать другим функциям, выполнять с ним арифметические операции и даже приводить к другому типу. Но ни читать, ни писать в эту область памяти нельзя.

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

  1. Разрешается передавать указатели на память, выделенную cudaMalloc (), функциям, исполняемым устройством.
  2. Разрешается использовать указатели на память, выделенную cudaMalloc (), для чтения и записи в эту память в коде, который исполняется устройством.
  3. Разрешается передавать указатели на память, выделенную cudaMalloc (), функциям, исполняемым CPU.
  4. Не разрешается использовать указатели на память, выделенную cudaMalloc (), для чтения и записи в эту память в коде, который исполняется CPU.

Если вы читаете внимательно, то, наверное, предвидите и следующее правило: нельзя использовать стандартную функцию free () для освобождения памяти, выделенной функцией cudaMalloc (). Чтобы освободить память, выделенную cudaMalloc (), следует обращаться к функции cudaFree (), которая ведет себя точно также, как free ().

Мы видели, как выделять и освобождать память устройства, но при этом столкнулись с печальным фактом: модифицировать эту память в коде, исполняемом CPU, нельзя. В следующих двух строчках примера иллюстрируются два наиболее употребительных способа доступа к памяти устройства: использование возвращенных указателей в коде, исполняемом самим устройством, и обращение к функции cudaMemcpy ().

Внутри кода, исполняемого устройством, указатели используются точно так же, как в обычном коде на С, который исполняется CPU. Оператор *с = а + b не таит в себе никаких подвохов. Мы складываем параметры а и b и сохраняем результат в области памяти, на которую указывает с. Это настолько просто, что даже неинтересно.

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

Но к памяти устройства можно обратиться также с помощью вызова функции cudaMemcpy () из кода, исполняемого CPU. Она похожа на стандартную функцию memcpy (), но принимает дополнительный параметр, который говорит о том, какой из двух указателей адресует память устройства. В нашем примере последний параметр cudaMemcpy () равен cudaMemcpyDeviceToHost, то есть начальный указатель адресует память устройства, а конечный — память CPU.

Вряд ли вы удивитесь, узнав, что константа cudaMemcpyHostToDevice описывает противоположную ситуацию, когда исходные данные находятся в памяти CPU, а конечный адрес — в памяти устройства. Наконец, можно сказать, что оба указателя относятся к памяти устройства, для этого нужно передать константу cudaMemcpyDeviceToDevice. Если же и начальный, и конечный указатели адресуют память CPU, то копирование производится стандартной функцией memcpy ().