在我们开始编写一些背景知识之前。 有两个竞争的GPGPU SDK: OpenCL和CUDA 。 OpenCL是所有GPU供应商(即AMD,NVIDIA和Intel)都支持的开放标准,而CUDA是NVIDIA特定的,并且仅在NVIDIA卡上工作。 这两个SDK都支持C / C ++代码,这当然使我们Java开发人员感到不寒而栗。 到目前为止,还没有纯Java OpenCL或CUDA支持。 对于需要利用GPU大量并行潜力的Java程序员来说,这并没有太大帮助,除非她摆弄Java Native接口。 当然,有一些Java工具可以减轻GPGPU Java编程的麻烦。
最受欢迎的两个(IMHO)是jocl和jcuda 。 使用这些工具,您仍然必须编写C / C ++代码,但至少这仅适用于将在GPU中执行的代码,从而大大减少了工作量。
这次,我将看一看jcuda,看看如何编写一个简单的GPGPU程序。
让我们从设置CUDA GPGPU linux开发环境开始(尽管Windows和Mac环境也不难设置):
步骤1 :在计算机中安装启用了NVIDIA CUDA的GPU。 NVIDIA开发人员网站上有支持CUDA的GPU列表。 新的NVIDIA GPU几乎肯定可以启用CUDA,但以防万一,请检查卡的规格以确保…
步骤2 :安装NVIDIA驱动程序和CUDA SDK。 下载它们并从此处找到安装说明。
步骤3 :转到目录〜/ NVIDIA_GPU_Computing_SDK / C / src / deviceQuery并运行make 。
步骤4 :如果编译成功,请转到目录〜/ NVIDIA_GPU_Computing_SDK / C / bin / linux / release并运行文件deviceQuery 。 您将获得有关卡的许多技术信息。
这是我的GeForce GT 430卡所获得的:
注意2个具有48个CUDA核心的多处理器,每个核心共有96个核心,对于价值约40欧元的低端视频卡来说还不错!!!
第5步 :现在您有了CUDA环境,让我们用C语言编写和编译CUDA程序。编写以下代码并将其保存为multiple.cu
#include <iostream>
__global__ void multiply(float a, float b, float* c)
{
*c=a*b;
}
int main()
{
float a, b, c;
float *c_pointer;
a=1.35;
b=2.5;
cudaMalloc((void**)&c_pointer, sizeof(float));
multiply<<<1,1>>>(a, b, c_pointer);
cudaMemcpy(&c, c_pointer, sizeof(float),cudaMemcpyDeviceToHost);
/*** This is C!!! You manage your garbage on your own! ***/
cudaFree(c_pointer);
printf("Result = %f\n",c);
}
使用cuda编译器对其进行编译并运行:
$ nvcc multiply.cu -o multiply
$ ./multiply
Result = 3.375000
$
那么上面的代码做什么? 带有__global__限定符的乘法函数称为内核 ,是将在GPU中执行的实际代码。 尽管存在一些语义差异,但main函数中的代码在CPU中作为普通C代码执行:
- 用<<< 1,1 >>>括号调用乘法函数。 方括号内的两个数字告诉CUDA代码应执行多少次。 CUDA使我们能够创建所谓的一维,二维甚至三维线程块。 此示例中的数字表示一个维度上运行的单个线程块,因此我们的代码将执行1×1 = 1次。
- 使用cudaMalloc,cudaMemcpy和cudaFree函数以与处理C语言中的计算机普通内存类似的方式来处理GPU内存。cudaMemcpy函数非常重要,因为GPU具有自己的RAM,并且在处理内核中的任何数据之前我们需要将它们加载到GPU内存中。 当然,完成后,我们还需要将结果复制回普通内存。
现在我们掌握了如何在GPU中执行代码的基础知识,让我们看看如何从Java运行GPGPU代码。 请记住,内核代码仍将用C编写,但至少现在主要功能是在jcuda的帮助下的Java代码。
下载jcuda二进制文件,解压缩它们,并确保在JVM的java.library.path参数中指定了包含.so文件的目录(对于Windows,则为.dll),或者将其附加到LD_LIBRARY_PATH环境变量中(或Windows中的PATH变量)。 同样,在编译和执行Java程序期间,jcuda-xxxxxxx.jar文件必须位于类路径中。
现在,我们有了jcuda设置,让我们看一下与jcuda兼容的内核:
extern "C"
__global__ void multiply(float *a, float *b, float *c)
/*************** Kernel Code **************/
{
c[0]= a[0] * b[0];
}
您会注意到与以前的内核方法有以下区别:
- 我们使用extern“ C”限定符来告诉编译器不要混用乘法方法名称,因此可以使用其原始名称进行调用。
- 对于a,b和c,我们使用数组而不是基元。 jcuda要求这样做,因为jcuda不支持Java原语。 在jcuda中,数据作为浮点数,整数等东西的数组来回传递给GPU。
将此文件另存为multiple2.cu 。 这次我们不想将文件编译为可执行文件,而是编译为将在我们的Java程序中调用的CUDA库。 我们可以将内核编译为PTX文件或CUBIN文件。 PTX是人类可读的文件,其中包含将即时编译的程序集(如代码)。 CUBIN文件是已编译的CUda BINaries,无需进行即时编译即可直接调用。 除非您需要最佳的启动性能,否则PTX文件是可取的,因为它们与编译时使用的GPU的特定计算能力无关,而CUBIN文件将无法在计算能力较低的GPU上运行。
为了编译我们的内核,输入以下内容:
$ nvcc -ptx multiply2.cu -o multiply2.ptx
成功创建我们的PTX文件后,让我们看一下与我们的C示例中使用的main方法等效的java:
import static jcuda.driver.JCudaDriver.*;
import jcuda.*;
import jcuda.driver.*;
import jcuda.runtime.JCuda;
public class MultiplyJ {
public static void main(String[] args) {
float[] a = new float[] {(float)1.35};
float[] b = new float[] {(float)2.5};
float[] c = new float[1];
cuInit(0);
CUcontext pctx = new CUcontext();
CUdevice dev = new CUdevice();
cuDeviceGet(dev, 0);
cuCtxCreate(pctx, 0, dev);
CUmodule module = new CUmodule();
cuModuleLoad(module, "multiply2.ptx");
CUfunction function = new CUfunction();
cuModuleGetFunction(function, module, "multiply");
CUdeviceptr a_dev = new CUdeviceptr();
cuMemAlloc(a_dev, Sizeof.FLOAT);
cuMemcpyHtoD(a_dev, Pointer.to(a), Sizeof.FLOAT);
CUdeviceptr b_dev = new CUdeviceptr();
cuMemAlloc(b_dev, Sizeof.FLOAT);
cuMemcpyHtoD(b_dev, Pointer.to(b), Sizeof.FLOAT);
CUdeviceptr c_dev = new CUdeviceptr();
cuMemAlloc(c_dev, Sizeof.FLOAT);
Pointer kernelParameters = Pointer.to(
Pointer.to(a_dev),
Pointer.to(b_dev),
Pointer.to(c_dev)
);
cuLaunchKernel(function, 1, 1, 1, 1, 1, 1, 0, null, kernelParameters, null);
cuMemcpyDtoH(Pointer.to(c), c_dev, Sizeof.FLOAT);
JCuda.cudaFree(a_dev);
JCuda.cudaFree(b_dev);
JCuda.cudaFree(c_dev);
System.out.println("Result = "+c[0]);
}
}
好的,看起来很多代码只是将两个数字相乘,但是请记住,有关Java和C指针的限制。 因此,从第9至11行开始,我们将a,b和c参数转换为名为a,b和c的数组,每个数组仅包含一个浮点数。
在第13至17行中,我们告诉jcuda我们将在系统中使用第一个GPU(高端系统中可能有多个GPU。)
在第19至22行中,我们告诉jcuda我们的PTX文件是,以及我们要使用的内核方法的名称(在我们的例子中是乘法) 。
在第24行,事情变得有趣起来,在第24行,我们使用了特殊的jcuda类CUdeviceptr,它充当指针占位符。 在第25行中,我们使用刚创建的CUdeviceptr指针分配GPU内存。 请注意,如果我们的数组有多个项目,则需要将Sizeof.FLOAT常数乘以数组中元素的数量。 最后,在第26行中,我们将第一个数组的内容复制到GPU。 同样,我们创建指针并将内容复制到第二个数组(b)的GPU RAM中。 对于我们的输出数组(c),我们现在只需要分配GPU内存。
在第35行中,我们创建一个Pointer对象,该对象将保存我们要传递给乘法方法的所有参数。
我们在第41行执行内核代码,在此执行实用程序方法cuKernelLaunch,将函数和指针类作为参数传递。 在功能参数之后的前六个参数定义了网格的数量(一个网格是一组块),并且在我们的示例中块均为1,因为我们只执行一次内核。 接下来的两个参数是0和null ,用于标识我们可能定义的任何共享内存(可以在线程之间共享的内存),在本例中为无。 下一个参数包含我们创建的Pointer对象,其中包含a,b,c设备指针,最后一个参数用于其他选项。
内核返回后,我们只需将dev_c的内容复制到我们的c数组,释放我们在GPU中分配的所有内存,并打印存储在c [0]中的结果,这当然与我们的C示例相同。
这是我们编译和执行MultiplyJ.java程序的方式(假设multiple2.ptx在同一目录中):
$ javac -cp ~/GPGPU/jcuda/JCuda-All-0.4.0-beta1-bin-linux-x86_64/jcuda-0.4.0-beta1.jar MultiplyJ.java
$ java -cp ~/GPGPU/jcuda/JCuda-All-0.4.0-beta1-bin-linux-x86_64/jcuda-0.4.0-beta1.jar:. MultiplyJ
Result = 3.375
$
请注意,在此示例中,目录〜/ GPGPU / jcuda / JCuda-All-0.4.0-beta1-bin-linux-x86_64已经在我的LD_LIBRARY_PATH中,因此不需要在java.library.path参数上设置JVM。
希望到目前为止,jcuda的机制已经明确,尽管我们并未真正涉及到GPU真正的强大功能,即大规模并行性。 在以后的文章中,我将提供一个示例,说明如何使用java在CUDA中运行并行线程,并附带一个示例,说明哪些内容不能在GPU中运行。 GPU处理对于非常专业的任务很有意义,大多数任务最好由我们受信任的旧CPU处理。
参考: W4G合作伙伴 Spyros Sakellariou的 GPGPU Java编程 。
相关文章 :
翻译自: https://www.javacodegeeks.com/2011/09/gpgpu-java-programming.html