Запуск кода CUDA или PTX на графическом процессоре

Обзор

В этой теме объясняется, как создать исполняемое ядро из файлов CU или PTX (параллельное выполнение потока) и запустить это ядро на графическом процессоре из MATLAB®. Ядро представлено в MATLAB a CUDAKernel объект, который может работать с переменными MATLAB массива или gpuArray.

Следующие шаги описывают общий рабочий процесс CUDAKernel:

  1. Используйте скомпилированный код PTX для создания объекта CUDAKernel, который содержит исполняемый код GPU.

  2. Установите свойства объекта CUDAKernel, чтобы контролировать его выполнение на графическом процессоре.

  3. Функции feval на CUDAKernel с необходимыми входами, чтобы запустить ядро на графическом процессоре.

Код MATLAB, который следует этим шагам, может выглядеть примерно так:

% 1. Create CUDAKernel object.
k = parallel.gpu.CUDAKernel('myfun.ptx','myfun.cu','entryPt1');

% 2. Set object properties.
k.GridSize = [8 1];
k.ThreadBlockSize = [16 1];

% 3. Call feval with defined inputs.
g1 = gpuArray(in1); % Input gpuArray.
g2 = gpuArray(in2); % Input gpuArray.

result = feval(k,g1,g2);

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

Создайте объект CUDAKernel

Скомпилируйте файл PTX из файла CU

Если у вас есть файл CU, который вы хотите выполнить на графическом процессоре, необходимо сначала скомпилировать его, чтобы создать файл PTX. Один из способов сделать это с nvcc компилятор в NVIDIA® CUDA® Набор инструментальных средств. Например, если ваш файл CU вызывается myfun.cu, можно создать скомпилированный файл PTX с помощью команды интерпретатора:

nvcc -ptx myfun.cu

Это генерирует файл с именем myfun.ptx.

Создайте объект CUDAKernel с входом файла CU

С .cu файл и .ptx файл, который можно создать CUDAKernel объект в MATLAB, который можно затем использовать, чтобы оценить ядро:

k = parallel.gpu.CUDAKernel('myfun.ptx','myfun.cu');

Примечание

Вы не можете save или load Объекты CUDAKernel.

Создайте объект CUDAKernel с входным параметром прототипа C

Если у вас нет файла CU, соответствующего вашему файлу PTX, можно задать прототип C для ядра C вместо файла CU. Для примера:

k = parallel.gpu.CUDAKernel('myfun.ptx','float *, const float *, float');

Другое использование для входа прототипа C - когда в исходном коде используется неопознанное переименование поддерживаемого типа данных. (См. поддерживаемые типы ниже.) Предположим, что ваше ядро содержит следующий код.

typedef float ArgType;
__global__ void add3( ArgType * v1, const ArgType * v2 )
{
    int idx = threadIdx.x;
    v1[idx] += v2[idx];
}

ArgType сам по себе не распознается как поддерживаемый тип данных, поэтому включенный в него файл CU не может непосредственно использоваться в качестве входов при создании объекта CUDAKernel в MATLAB. Однако поддерживаемые типы входа к add3 ядро может быть задано как вход прототипа C в конструктор CUDAKernel. Для примера:

k = parallel.gpu.CUDAKernel('test.ptx','float *, const float *','add3');

Поддерживаемые типы данных

Поддерживаемые стандартные типы данных C/C + + перечислены в следующей таблице.

Типы с плавающей точкойЦелые типыЛогические и символьные типы

double, double2

float, float2

short, unsigned short, short2, ushort2

int, unsigned int, int2, uint2

long, unsigned long, long2, ulong2

long long, unsigned long long, longlong2, ulonglong2

ptrdiff_t, size_t

bool

char, unsigned char, char2, uchar2

Кроме того, следующие целые типы поддерживаются при включении tmwtypes.h заголовочный файл в вашей программе.

Целые типы

int8_T, int16_T, int32_T, int64_T

uint8_T, uint16_T, uint32_T, uint64_T

Заголовочный файл поставляется следующим matlabroot/ extern/include/tmwtypes.h. Вы включаете файл в свою программу со линией:

#include "tmwtypes.h"

Ограничения аргументов

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

Объявление C ядра всегда имеет форму:

__global__ void aKernel(inputs ...)
  • Ядро не должно возвращать ничего, и работать только со своими входными параметрами (скалярами или указателями).

  • Ядро не может выделить какую-либо форму памяти, поэтому все выходы должны быть предварительно выделены до выполнения ядра. Поэтому размеры всех выходов должны быть известны, прежде чем вы запустите ядро.

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

При переводе определения ядра на C в MATLAB:

  • Все скалярные входы в C (double, float, int, и т.д.) должны быть скалярами в MATLAB, или скалярными (то есть одноэлементными) переменными gpuArray.

  • Все const входы указателей на C (const double *, и т.д.) могут быть скалярами или матрицами в MATLAB. Они приводятся к правильному типу, копируются на устройство, а указатель на первый элемент передается в ядро. Никакая информация об исходном размере не передается в ядро. Это как будто ядро непосредственно получило результат mxGetData на mxArray.

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

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

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

Свойства объекта CUDAKernel

Когда вы создаете объект ядра без оконечной точки с запятой или когда вы вводите переменную объекта в командной строке, MATLAB отображает свойства объекта ядра. Для примера:

k = parallel.gpu.CUDAKernel('conv.ptx','conv.cu')
k = 
  parallel.gpu.CUDAKernel handle
  Package: parallel.gpu

  Properties:
     ThreadBlockSize: [1 1 1]
  MaxThreadsPerBlock: 512
            GridSize: [1 1 1]
    SharedMemorySize: 0
          EntryPoint: '_Z8theEntryPf'
  MaxNumLHSArguments: 1
     NumRHSArguments: 2
       ArgumentTypes: {'in single vector'  'inout single vector'}

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

Описание свойств объекта смотрите в CUDAKernel страница с описанием объекта. Типичная причина изменения настраиваемых свойств состоит в том, чтобы задать количество потоков, как описано ниже.

Задайте точки входа

Если ваш файл PTX содержит несколько точек входа, можно идентифицировать конкретное ядро в myfun.ptx что вы хотите, чтобы объект ядра k см.:

k = parallel.gpu.CUDAKernel('myfun.ptx','myfun.cu','myKernel1');

Один файл PTX может содержать несколько точек входа в различные ядра. Каждая из этих точек входа имеет уникальное имя. Эти имена обычно искажаются (как в C++ mangling). Однако, когда сгенерирован nvcc имя PTX всегда содержит исходное имя функции из файла CU. Например, если файл CU определяет функцию ядра как

__global__ void simplestKernelEver( float * x, float val )

затем код PTX содержит запись, которая может вызываться _Z18simplestKernelEverPff.

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

Примечание

The CUDAKernel функция ищет ваше имя записи в файле PTX и совпадает с любыми вхождениями подстроки. Поэтому вы не должны называть какие-либо записи как подстроки других.

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

template <typename T>
__global__ void add4( T * v1, const T * v2 )
{
    int idx = threadIdx.x;
    v1[idx] += v2[idx];
}

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

template __global__ void add4<float>(float *, const float *);
template __global__ void add4<double>(double *, const double *);

PTX имеет соответствующие записи:

_Z4add4IfEvPT_PKS0_
_Z4add4IdEvPT_PKS0_

Используйте add4If точки входа для версии с плавающей точкой и add4Id для двойной версии.

k = parallel.gpu.CUDAKernel('test.ptx','double *, const double *','add4Id');

Задайте количество потоков

Вы задаете количество вычислительных потоков для CUDAKernel путем установки двух его свойств объекта:

  • GridSize - вектор из трех элементов, продукт которого определяет количество блоков.

  • ThreadBlockSize - вектор из трех элементов, продукт которого определяет количество потоков на блок. (Обратите внимание, что продукт не может превысить значение свойства MaxThreadsPerBlock.)

Значение по умолчанию для обоих этих свойств [1 1 1], но предположим, что вы хотите использовать 500 потоков для выполнения поэлементных операций с векторами 500 элементов параллельно. Простой способ добиться этого - создать свой CUDAKernel и соответственно задать его свойства:

k = parallel.gpu.CUDAKernel('myfun.ptx','myfun.cu');
k.ThreadBlockSize = [500,1,1];

Обычно размеры блоков сетки и резьбы задаются на основе размеров входных входов. Для получения информации об иерархии потоков и многомерных сетках и блоках см. Руководство по программированию NVIDIA CUDA C.

Запуск CUDAKernel

Используйте feval функция для оценки CUDAKernel на графическом процессоре. Следующие примеры показывают, как выполнить ядро с помощью переменных рабочего пространства MATLAB и переменных gpuArray.

Использование переменных рабочей области

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

k = parallel.gpu.CUDAKernel('conv.ptx','conv.cu');

result = feval(k,rand(100,1),rand(100,1));

Даже если входами являются константы или переменные для данных рабочему пространству MATLAB, выход gpuArray.

Используйте переменные gpuArray

Может быть более эффективным использование gpuArray объекты как вход при запуске ядра:

k = parallel.gpu.CUDAKernel('conv.ptx','conv.cu');

i1 = gpuArray(rand(100,1,'single'));
i2 = gpuArray(rand(100,1,'single'));

result1 = feval(k,i1,i2);

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

result2 = feval(k,i1,i2);

r1 = gather(result1);
r2 = gather(result2);

Определите входное и выходное соответствие

При вызове [out1, out2] = feval(kernel, in1, in2, in3), входы in1, in2, и in3 соответствуют каждому из входных параметров функции C в файле CU. Выходные выходы out1 и out2 сохраните значения входных параметров первого и второго неконстатных указателей в функцию C после выполнения ядра C.

Для примера, если ядро C в файле CU имеет следующую сигнатуру:

void reallySimple( float * pInOut, float c )

соответствующий объект ядра (k) в MATLAB имеет следующие свойства:

MaxNumLHSArguments: 1
   NumRHSArguments: 2
     ArgumentTypes: {'inout single vector'  'in single scalar'}

Поэтому использовать объект ядра из этого кода с feval, вы должны предоставить feval два входных параметров (в дополнение к объекту ядра), и можно использовать один выходной аргумент:

y = feval(k,x1,x2)

Значения входа x1 и x2 соответствуют pInOut и c в прототипе функции C. Выходной аргумент y соответствует значению pInOut в прототипе функции C после выполнения ядра C.

Следующий пример является несколько более сложным, который показывает комбинацию const и non-const указателей:

void moreComplicated( const float * pIn, float * pInOut1, float * pInOut2 )

Соответствующий объект ядра в MATLAB тогда имеет свойства:

MaxNumLHSArguments: 2
   NumRHSArguments: 3
     ArgumentTypes: {'in single vector'  'inout single vector'  'inout single vector'}

Можно использовать feval на ядре этого кода (k) с синтаксисом:

[y1,y2] = feval(k,x1,x2,x3)

Три входных параметров x1, x2, и x3, соответствуют трем аргументам, которые передаются в функцию C. Выходные аргументы y1 и y2, соответствуют значениям pInOut1 и pInOut2 после выполнения ядра C.

Полный рабочий процесс ядра

Добавьте два числа

Этот пример добавляет два двойных значения в графический процессор. Необходимо установить набор инструментов NVIDIA CUDA Toolkit и установить драйверы для устройства с поддержкой CUDA.

  1. Код CU для этого следующий.

    __global__ void add1( double * pi, double c ) 
    {
        *pi += c;
    }

    Директива __global__ указывает, что это точка входа в ядро. Код использует указатель, чтобы отправить результат в pi, который является и входом, и выходом. Поместите этот код в файл с именем test.cu в текущей директории.

  2. Скомпилируйте код CU в командной строке интерпретатора, чтобы сгенерировать файл PTX под названием test.ptx.

    nvcc -ptx test.cu
  3. Создайте ядро в MATLAB. В настоящее время этот файл PTX имеет только одну запись, поэтому вам не нужно указывать его. Если бы вы положили больше ядер, вы бы указали add1 как запись.

    k = parallel.gpu.CUDAKernel('test.ptx','test.cu');
  4. Запустите ядро с двумя числовыми входами. По умолчанию ядро работает в одном потоке.

    result = feval(k,2,3)
    result = 
        5
    

Добавить два вектора

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

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

    __global__ void add2( double * v1, const double * v2 ) 
    {
        int idx = threadIdx.x;
        v1[idx] += v2[idx];
    }

    Сохраните этот код в файле test.cu.

  2. Скомпилируйте как раньше, используя nvcc.

    nvcc -ptx test.cu
  3. Если этот код был помещен в тот же файл CU вместе с кодом первого примера, необходимо задать имя точки входа на этот раз, чтобы отличить его.

    k = parallel.gpu.CUDAKernel('test.ptx','test.cu','add2');
    
  4. Прежде чем вы запустите ядро, правильно установите количество потоков для векторов, которые вы хотите добавить.

    N = 128;
    k.ThreadBlockSize = N;
    in1 = ones(N,1,'gpuArray');
    in2 = ones(N,1,'gpuArray');
    result = feval(k,in1,in2);
    

Пример с файлами CU и PTX

Для примера, который показывает, как работать с CUDA, и предоставляет файлы CU и PTX для экспериментов, смотрите Иллюстрирование трех подходов к вычислениям графический процессор: Набор Мандельброта.

См. также

|

Похожие темы