exponenta event banner

Запуск кода CUDA или PTX на GPU

Обзор

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

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

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

  2. Задайте свойства объекта CUDAKernel для управления его выполнением на GPU.

  3. Звонить feval на CUDAKernel с необходимыми входами для запуска ядра на GPU.

Код 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-файла, который требуется выполнить на GPU, необходимо сначала скомпилировать его для создания PTX-файла. Один из способов сделать это с nvcc компилятор в NVIDIA ® CUDA ® Toolkit. Например, если вызывается файл CU myfun.cu, можно создать скомпилированный файл PTX с помощью команды shell:

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 входы указателей в С (const double *и т.д.) могут быть скалярами или матрицами в MATLAB. Они приводятся к правильному типу, копируются на устройство, и указатель на первый элемент передается ядру. В ядро не передается информация об исходном размере. Как будто ядро непосредственно получило результат mxGetData на mxArray.

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

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

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

Свойства объекта 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 для версии float, и 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 на GPU. В следующих примерах показано, как выполнить ядро с использованием переменных рабочей области MATLAB и переменных gpuArray.

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

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

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 и 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.

Завершить рабочий процесс ядра

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

В этом примере в GPU добавляются два двойника. Необходимо установить набор инструментов 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 для экспериментов см. в разделе Иллюстрация трех подходов к вычислениям GPU: набор Мандельброта.

См. также

|

Связанные темы