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

Обзор

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

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

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

  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® Toolkit. Например, если ваш файл 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++). Однако, когда сгенерировано nvcc имя PTX всегда содержит исходное имя функции из файла CU. Например, если файл CU задает функцию ядра как

__global__ void simplestKernelEver( float * x, float val )

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

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

Примечание

Функция 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 хранят значения первых и вторых входных параметров указателя неconst к функции 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 и 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 выполнится.

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

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

Этот пример добавляет два, удваивается вместе в графическом процессоре. Вы должны иметь Инструментарий CUDA NVIDIA, установленный, и иметь 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 для вас, чтобы экспериментировать с, смотрите Иллюстрирование Трех Подходов к Вычислению графического процессора: Множество Мандельброта.