В этой теме объясняется, как создать исполняемое ядро из файлов CU или PTX (параллельное выполнение потока) и запустить это ядро на графическом процессоре из MATLAB®. Ядро представлено в MATLAB a CUDAKernel
объект, который может работать с переменными MATLAB массива или gpuArray.
Следующие шаги описывают общий рабочий процесс CUDAKernel:
Используйте скомпилированный код PTX для создания объекта CUDAKernel, который содержит исполняемый код GPU.
Установите свойства объекта CUDAKernel, чтобы контролировать его выполнение на графическом процессоре.
Функции 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);
В следующих разделах приведены подробные сведения об этих командах и шагах рабочего процесса.
Если у вас есть файл CU, который вы хотите выполнить на графическом процессоре, необходимо сначала скомпилировать его, чтобы создать файл PTX. Один из способов сделать это с nvcc
компилятор в NVIDIA® CUDA® Набор инструментальных средств. Например, если ваш файл CU вызывается myfun.cu
, можно создать скомпилированный файл PTX с помощью команды интерпретатора:
nvcc -ptx myfun.cu
Это генерирует файл с именем myfun.ptx
.
С .cu
файл и .ptx
файл, который можно создать CUDAKernel
объект в MATLAB, который можно затем использовать, чтобы оценить ядро:
k = parallel.gpu.CUDAKernel('myfun.ptx','myfun.cu');
Примечание
Вы не можете save
или load
Объекты CUDAKernel.
Если у вас нет файла 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 + + перечислены в следующей таблице.
Типы с плавающей точкой | Целые типы | Логические и символьные типы |
---|---|---|
|
|
|
Кроме того, следующие целые типы поддерживаются при включении tmwtypes.h
заголовочный файл в вашей программе.
Целые типы |
---|
|
Заголовочный файл поставляется следующим
. Вы включаете файл в свою программу со линией: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 не приводятся автоматически, поэтому их тип и сложность должны точно совпадать с ожидаемыми.
Эти правила имеют определенные последствия. Наиболее примечательно, что каждый выход из ядра должен обязательно также быть входом в ядро, поскольку вход позволяет пользователю определять размер выхода (что следует из неспособности выделить память на графическом процессоре).
Когда вы создаете объект ядра без оконечной точки с запятой или когда вы вводите переменную объекта в командной строке, 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.
Используйте 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
объекты как вход при запуске ядра:
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.
Код CU для этого следующий.
__global__ void add1( double * pi, double c ) { *pi += c; }
Директива __global__
указывает, что это точка входа в ядро. Код использует указатель, чтобы отправить результат в pi
, который является и входом, и выходом. Поместите этот код в файл с именем test.cu
в текущей директории.
Скомпилируйте код CU в командной строке интерпретатора, чтобы сгенерировать файл PTX под названием test.ptx
.
nvcc -ptx test.cu
Создайте ядро в MATLAB. В настоящее время этот файл PTX имеет только одну запись, поэтому вам не нужно указывать его. Если бы вы положили больше ядер, вы бы указали add1
как запись.
k = parallel.gpu.CUDAKernel('test.ptx','test.cu');
Запустите ядро с двумя числовыми входами. По умолчанию ядро работает в одном потоке.
result = feval(k,2,3)
result = 5
Этот пример расширяет предыдущий, чтобы добавить два вектора вместе. Для простоты примите, что существует точно такое же количество потоков, как и элементов в векторах, и что существует только один блок потоков.
Код CU немного отличается от последнего примера. Оба входа являются указателями, и один является постоянным, потому что вы не изменяете его. Каждый поток будет просто добавлять элементы по индексу потока. Индекс потока должен решить, какой элемент должен добавить этот поток. (Получение этих значений для потока и блока является очень распространенным шаблоном в программировании CUDA.)
__global__ void add2( double * v1, const double * v2 ) { int idx = threadIdx.x; v1[idx] += v2[idx]; }
Сохраните этот код в файле test.cu
.
Скомпилируйте как раньше, используя nvcc
.
nvcc -ptx test.cu
Если этот код был помещен в тот же файл CU вместе с кодом первого примера, необходимо задать имя точки входа на этот раз, чтобы отличить его.
k = parallel.gpu.CUDAKernel('test.ptx','test.cu','add2');
Прежде чем вы запустите ядро, правильно установите количество потоков для векторов, которые вы хотите добавить.
N = 128; k.ThreadBlockSize = N; in1 = ones(N,1,'gpuArray'); in2 = ones(N,1,'gpuArray'); result = feval(k,in1,in2);
Для примера, который показывает, как работать с CUDA, и предоставляет файлы CU и PTX для экспериментов, смотрите Иллюстрирование трех подходов к вычислениям графический процессор: Набор Мандельброта.