在 GPU 上运行 CUDA 或 PTX 代码
CUDAKernel
工作流概述
本页介绍如何从 CUDA® C++ 源文件(CU)创建可执行内核,并在 MATLAB® 中的 GPU 上运行该内核。内核在 MATLAB 中由 CUDAKernel
对象表示,该对象可以对存储在主机内存中的数组或 GPU 数组进行操作。
以下步骤描述了 CUDAKernel
的一般工作流:
使用
mexcuda
从 CU 文件编译并行线程执行 (PTX) 文件。您不需要 CUDA 工具包来使用mexcuda
编译 PTX 文件。在 R2023a 之前: 使用 NVIDIA® CUDA 工具包中的
nvcc
编译器来编译 PTX 文件,而不是mexcuda
函数。使用
parallel.gpu.CUDAKernel
函数从 CU 文件和 PTX 文件创建CUDAKernel
对象。CUDAKernel
包含 GPU 可执行代码。设置
CUDAKernel
的属性来控制其在 GPU 上的执行。使用所需的输入在
CUDAKernel
上调用feval
,以在 GPU 上运行内核。
遵循以下步骤的 MATLAB 代码可能看起来像这样:
% 1. Compile a PTX file. mexcuda -ptx myfun.cu % 2. Create CUDAKernel object. k = parallel.gpu.CUDAKernel("myfun.ptx","myfun.cu"); % 3. Set object properties. k.GridSize = [8 1]; k.ThreadBlockSize = [16 1]; % 4. Call feval with defined inputs. g1 = gpuArray(in1); % Input gpuArray. g2 = gpuArray(in2); % Input gpuArray. result = feval(k,g1,g2);
以下部分提供了这些命令和工作流步骤的详细信息。
创建一个 CUDAKernel
对象
如果您有一个想要在 GPU 上执行的 CU 文件,您必须首先编译它以创建 PTX 文件。要编译 PTX 文件,请将 CU 文件传递给带有 -ptx
标志的 mexcuda
。
mexcuda -ptx myfun.cu
这将生成一个 PTX 文件 myfun.ptx
。
使用 CU 和 PTX 文件创建一个 CUDAKernel
对象。
k = parallel.gpu.CUDAKernel("myfun.ptx","myfun.cu");
注意
您不能 save
或 load
CUDAKernel
对象。
CUDAKernel
对象属性
当您创建一个没有终止分号的 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
对象的属性控制它的一些执行行为。使用点符号来改变可以更改的属性。有关对象属性的描述,请参阅 CUDAKernel
。修改可设置属性的典型原因是指定线程数,如下所述。
指定入口点
单个 PTX 文件可以包含不同内核的多个入口点。每个入口点都有一个唯一的名称。每个入口点的名称都被重组(如同 C++ 重组),但始终包含来自 CU 文件的原始函数名称。例如,如果 CU 文件将核函数定义为
__global__ void simplestKernelEver( float * x, float val )
那么 PTX 代码包含一个可能被称为 _Z18simplestKernelEverPff
的条目。
当您有多个入口点时,在调用 parallel.gpu.CUDAKernel
生成内核时指定特定内核的入口名称。
k = parallel.gpu.CUDAKernel("myfun.ptx","myfun.cu","myKernel1");
注意
parallel.gpu.CUDAKernel
函数在 PTX 文件中搜索您的条目名称,并匹配任何子字符串出现。因此,您不应将任何入口点命名为任何其他入口点的子字符串。
您可能无法控制原始条目名称,在这种情况下,您必须了解每个条目派生的唯一混乱。例如,考虑以下函数模板。
template <typename T> __global__ void add4( T * v1, const T * v2 ) { int idx = threadIdx.x; v1[idx] += v2[idx]; }
当模板针对 float 和 double 进行扩展时,会产生两个入口点,都包含子字符串 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 个元素的向量并行运行逐元素操作。设置 ThreadBlockSize
使得其元素的乘积为 500。
k = parallel.gpu.CUDAKernel("myfun.ptx","myfun.cu"); k.ThreadBlockSize = [500,1,1];
通常,您可以根据输入的大小来设置网格和线程块的大小。有关线程层次结构以及多维网格和块的信息,请参阅 NVIDIA CUDA C 编程指南。
使用 C 原型输入构造 CUDAKernel
对象. 如果您没有与 PTX 文件对应的 CU 文件,您可以为您的 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
本身不被识别为受支持的数据类型,因此在 MATLAB 中创建 CUDAKernel
对象时,包含它的 CU 文件不能直接用作输入。但是,add3
内核支持的输入类型可以指定为 CUDAKernel
构造函数的 C 原型输入。例如:
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
变量。C 中的所有常量指针输入(
const double *
等)都可以是 MATLAB 中的标量或矩阵。它们被转换为正确的类型,复制到设备上,并且指向第一个元素的指针被传递给内核。没有有关原始大小的信息传递给内核。就好像内核直接在mxArray
上接收了mxGetData
的结果一样。C 中的所有非常量指针输入都被精确地作为非常量指针传输到内核。但是,由于非常量指针可能会被内核改变,因此这将被视为内核的输出。
来自 MATLAB 工作区标量和数组的输入被转换为请求的类型,然后传递给内核。但是,
gpuArray
输入不会自动转换,因此它们的类型和复杂性必须与预期完全匹配。
这些规则有一些含义。最值得注意的是,内核的每个输出也必须是内核的输入,因为输入允许用户定义输出的大小(这是由于无法在 GPU 上分配内存而导致的)。
运行 CUDAKernel
使用 feval
函数在 GPU 上评估 CUDAKernel
。
假设您已经编写了一些内核并希望在 MATLAB 中使用它们在 GPU 上执行。您有一个对两个向量进行卷积的内核;用两个随机输入向量加载并运行它。
k = parallel.gpu.CUDAKernel("conv.ptx","conv.cu"); result = feval(k,rand(100,1),rand(100,1));
即使输入不是,输出也是 gpuArray
。但是,在运行内核时使用 gpuArray
对象作为输入可能会更有效。
k = parallel.gpu.CUDAKernel("conv.ptx","conv.cu"); i1 = rand(100,1,"single","gpuArray"); i2 = rand(100,1,"single","gpuArray"); result1 = feval(k,i1,i2);
因为输出是 gpuArray
,所以您现在可以使用此输入或输出数据执行其他操作,而无需在 GPU 内存和主机内存之间进行进一步传输。
确定输入和输出对应关系
调用 [out1, out2] = feval(kernel,in1,in2,in3)
时,输入 in1
、in2
和 in3
对应于 CU 文件内函数的每个输入参量。输出 out1
和 out2
将内核执行后的第一个和第二个非常量指针输入参量的值存储到函数中。
例如,如果 CU 文件中的内核具有以下签名:
void reallySimple( float * pInOut, float c )
MATLAB 中对应的内核对象(k
)具有以下属性:
MaxNumLHSArguments: 1 NumRHSArguments: 2 ArgumentTypes: {'inout single vector' 'in single scalar'}
因此,要将此代码中的内核对象与 feval
一起使用,您需要为 feval
提供两个输入参量(除了内核对象),并且可以使用一个输出参量。
y = feval(k,x1,x2)
输入值 x1
和 x2
对应于函数原型中的 pInOut
和 c
。输出参量 y
对应于内核执行后函数原型中 pInOut
的值。
下面是一个稍微复杂一点的例子,展示了常量指针和非常量指针的组合:
void moreComplicated( const float * pIn, float * pInOut1, float * pInOut2 )
MATLAB 中对应的内核对象具有以下属性:
MaxNumLHSArguments: 2 NumRHSArguments: 3 ArgumentTypes: {'in single vector' 'inout single vector' 'inout single vector'}
您可以使用三个输入参量和两个输出参量在此代码的内核(k
)上使用 feval
。
[y1,y2] = feval(k,x1,x2,x3)
三个输入参量 x1
、x2
、x3
,对应传递给函数的三个参量。输出参量 y1
和 y2
对应内核执行后的 pInOut1
和 pInOut2
的值。
完整的内核工作流
两个数字相加
此示例在 GPU 中将两个双精度数相加。
执行此操作的 CU 代码如下。
__global__ void add1( double * a, double b ) { *a += b; }
指令
__global__
表示这是内核的入口点。代码使用指针将a
中的结果发送出去,它既是输入又是输出。将此代码保存在当前目录中名为test.cu
的文件中。使用
mexcuda
编译 CU 代码以生成名为test.ptx
的 PTX 文件。mexcuda -ptx test.cu
在 MATLAB 中创建内核。目前此 PTX 文件只有一个条目,因此您不需要指定它。如果 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
中。使用
mexcuda
从 CU 文件编译一个名为test.ptx
的 PTX 文件。mexcuda -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);
CU 和 PTX 文件示例
有关如何使用 CUDA 的示例,以及提供 CU 和 PTX 文件供您试验,请参阅 说明 GPU 计算的三种方法:曼德布洛特集合。
另请参阅
mexcuda
| CUDAKernel
| feval