65.9K
CodeProject 正在变化。 阅读更多。
Home

第一部分:使用 Java 和 OpenCL 编写显卡 (GPU) 程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (8投票s)

2010 年 6 月 9 日

LGPL3

10分钟阅读

viewsIcon

181118

downloadIcon

2847

学习如何安装和使用 OpenCL 与 Java 的基础知识,释放你的 GPU 性能。

引言

本文讨论使用 Java 和 OpenCL 编程你的显卡 (GPU)。

你的电脑很可能配备了 3D 加速显卡。如果你的电脑是台式机,尤其如此。即使在笔记本电脑中,加速视频卡也越来越普遍。你的显卡,或图形处理单元 (GPU),拥有相当大的处理能力。你可以为常规 Java 程序利用这种计算能力。本文是我将要撰写的关于 GPU 编程系列文章的第一篇。这是第一篇,只涉及在你的 GPU 上执行一个非常简单的应用程序。有许多细节需要你关注。所以第一步就是确保一切都设置妥当。这就是本文的目的。

可以使用多 GPU 实际做什么?下面的文章展示了一个大学研究团队如何使用多 GPU 来获得一个“桌面超级计算机”。

你也应该考虑哪些类型的应用程序可能受益于 GPU 加速。如果你的程序花费大量时间进行计算和通用数字处理,GPU 加速可能会非常有益。如果你的任务可以并行处理,GPU 加速尤其有用。GPU 通常由大量流处理器组成,它们本身速度不快。许多 GPU 卡都配有超过 100 个流处理器。显然,如果你想利用 GPU,你必须让你的任务并行化。如果你认为让应用程序在四核上获得性能已经足够困难,那么 GPU 加速可能不会有帮助。GPU 编程是未来的一瞥。不久的将来,我们将拥有 100+ 核的 CPU。

GPU 处理的另一个限制是你不能在 GPU 上执行 Java 代码。这些代码必须使用一种称为 OpenCL 的类 C 语言创建。OpenCL 代码将无法直接访问你的 Java 数据。你必须打包数据才能让 OpenCL 工作。在很多方面,使用 OpenCL 就像使用 SQL。你首先创建一个包含你的 OpenCL 例程的字符串。然后你将参数从你的 Java 应用程序绑定到 OpenCL。最后,你将结果从你的 OpenCL 例程整合回你的应用程序。整个过程与使用数据库和 SQL 非常相似。

我对 GPU 编程感兴趣是因为我是 Encog Java 神经网络项目的创始人兼主要程序员。Encog 项目是一个开源的 LGPL 神经网络框架,支持 Java 和 .NET。神经网络在数学上非常密集。如果实现得当,它们的处理可以并行进行。这使得神经网络成为 GPU 加速的理想选择。Encog 利用 GPU 加速神经网络的训练。我的文章旨在独立于神经网络编程,讨论 GPU 编程。然而,最后一篇文章将演示使用 GPU 训练神经网络。这些文章可以应用于任何数字处理任务。

使用 JOCL OpenCL 绑定

OpenCL 被打包成一个安装在你机器上的 DLL。我们将在下一节讨论安装这些驱动程序。现在,我们将看看如何安装绑定。因为 OpenCL 包含在 DLL 中,你将不得不使用 Java 本地接口 (JNI) 来与之通信。你不需要直接处理 JNI,因为这就是绑定所做的。我们将使用的绑定称为 JOCL。JOCL 可以从 这里 下载。

JOCL 非常依赖平台。你将有一个平台无关的 JAR 接口,但是你需要为你将要使用的平台下载正确的 DLL。32 位和 64 位之间也有区别。下载正确的绑定非常重要。我用来写这篇文章的电脑是一台 Windows 7 64 位机器。因此,我将演示相应的绑定。一旦我打开 Windows 64 位存档,除了许可证文件外,我还看到了两个文件。

  • JOCL-0.1.3a-beta.jar
  • JOCL-windows-x86_64.dll

你可以看到平台无关的 JAR 文件和平台相关的 DLL 文件。你必须将 JAR 文件包含在你的类路径中。这与任何其他 Java 应用程序没有什么不同。但是,JAR 文件将使用正确的平台相关的 DLL。这个 DLL 必须位于系统路径中,或者在你 Java 应用程序的当前目录中。

安装驱动程序

OpenCL 驱动程序可能已经安装在你的计算机系统中。你可以跳到下一节,看看是否会出现驱动程序错误。如果你出现驱动程序错误,在下一节中,你需要安装驱动程序。驱动程序错误通常表现为无法找到 OpenCL.dll

你要安装的具体驱动程序将取决于你的 GPU。我们只关心你的 GPU 的芯片组。截至撰写本文时,有三种 GPU 芯片组被广泛使用。

  • AMD/ATI
  • nVidia
  • Intel

有许多不同的 GPU 制造商;然而,它们通常会使用上述芯片组之一。如果你有不同的芯片组,请查看供应商的驱动程序页面。如果你有 Intel GPU,那么你现在就完成了!截至撰写本文时,Intel 尚未为其显卡提供 OpenCL 驱动程序。

AMD 和 nVidia 都有来自以下页面的驱动程序。

AMD 目前的驱动程序比 nVidia 好。AMD 实际上可以使用你的 CPU 作为 OpenCL 设备,创建一个真正的异构计算环境。

创建示例应用程序

本文提供的示例应用程序是基于 JOCL 提供的示例。它是一个相对简短的应用程序,但它展示了如何使用 OpenCL 的所有基础知识。示例下载包含一个已设置好的 Eclipse 项目。但是,你可以使用以下指令从命令行轻松编译它。

javac  OpenCLPart1.java  -classpath .;./JOCL-0.1.3a-beta.jar

要执行该应用程序,请使用以下指令:.

java  -classpath .;./JOCL-0.1.3a-beta.jar OpenCLPart1

如果程序成功执行,您将看到以下输出:

Obtaining platform...
Test PASSED
Result: [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0]

如果你没有,那么就停止。不要继续前进。直接去本文末尾的故障排除部分。

从 Java 使用 OpenCL

现在我们将深入了解这个非常简单的应用程序,看看它是如何工作的。OpenCL 从根本上就是为了大规模并行而设计的。这是内置于你执行的内核(或一小段 OpenCL 代码)中的。我们正在执行的内核如下所示。

    private static String programSource =
        "__kernel void "+
        "sampleKernel(__global const float *a,"+
        "             __global const float *b,"+
        "             __global float *c)"+
        "{"+
        "    int gid = get_global_id(0);"+
        "    c[gid] = a[gid] + b[gid];"+
        "}";

这是内核,嵌入在 Java 字符串中。实际的内核,不含 Java,如下所示:

__kernel void sampleKernel(__global const float *a,
__global const float *b,
__global float *c)
{
int gid = get_global_id(0);
c[gid] = a[gid] + b[gid];
}

这就是 OpenCL。它基于 C99,因此看起来非常像 C。注意内核接受三个参数。这三个都是指针。指针有点像 Java 引用,但实际上比它多得多。指针也可以用作数组。这就是它们在这里的用途。如果我将上面的内核翻译成纯 Java,它看起来会像这样:

void sampleKernel(int index, float[] a,float[] b, float[] c)
{
	c[index] = a[index] + b[index];
}

基本上,我们正在对“a”和“b”数组的内容求和,并将结果保存在“c”数组中。注意“index”变量。这是从 OpenCL get_global_id(0) 函数获得的。这返回当前正在执行的线程。即使是这个简单的例子也是多线程的。数组中的每个元素都可能由你 GPU 上的不同流处理器添加。我们一开始就是多线程的。创建一个单线程的 OpenCL 应用程序确实没有意义。单个流处理器的速度太慢。如果你只使用一个流处理器,那么将数据发送到流处理器进行处理的开销很可能是不值得的。

既然我们已经检查了此应用程序的 OpenCL,我们将查看执行它所需的 Java 代码。我需要警告你,JOCL 是 OpenCL 之上的一个薄层。这不会是世界上最漂亮的 Java 代码。我通常做的第一件事就是将 OpenCL 的细节隐藏在一些支持类中。Encog 就是这样做的。

我们首先获取一个平台。这是 OpenCL 层次结构的一部分。起初很容易被忽略,但这个层次结构非常重要。平台位于顶层。如果你只安装了一个供应商的驱动程序,你将只有一个平台。多平台很有用的一个例子是如果你有一个 nVidia 卡。nVidia 尚未发布 CPU 驱动程序。所以你实际上可以为你的 GPU 运行 nVidia OpenCL 驱动程序,然后为 CPU 运行 AMD 驱动程序。AMD 驱动程序可以在 Intel CPU 上运行。以下代码获取第一个平台。

System.out.println("Obtaining platform...");
cl_platform_id platforms[] = new cl_platform_id[1];
clGetPlatformIDs(platforms.length, platforms, null);
cl_context_properties contextProperties = new cl_context_properties();
contextProperties.addProperty(CL_CONTEXT_PLATFORM, platforms[0]);

这段代码只会访问一个平台。它只会获取它找到的第一个平台。如果你想支持多个平台,你需要修改上面的代码来获取一个更大的数组。

接下来我们创建一个上下文。每个平台必须有自己的上下文。一个上下文可以有多个设备。例如,如果你有两个 ATI GPU,你将有一个平台,但有两个设备。上下文的创建如下。在这里,我们只请求 GPU。

cl_context context = clCreateContextFromType(
    contextProperties, CL_DEVICE_TYPE_GPU, null, null, null);

我们获取第一个 GPU 设备

int numDevices = (int) numBytes[0] / Sizeof.cl_device_id;
cl_device_id devices[] = new cl_device_id[numDevices];
clGetContextInfo(context, CL_CONTEXT_DEVICES, numBytes[0],  
    Pointer.to(devices), null);

命令将通过命令队列传达给 OpenCL 设备。如果你使用双显卡,你将需要双命令队列。OpenCL 会自动将任务发送到显卡上的流处理器。但你需要将任务分解到多个命令队列上,这在使用双 GPU 时是必要的。

cl_command_queue commandQueue = 
    clCreateCommandQueue(context, devices[0], 0, null);

接下来,我们必须为三个数组分配内存。前两个数组用于将数据发送到内核。第三个用于将数据从内核写回主应用程序。

cl_mem memObjects[] = new cl_mem[3];
memObjects[0] = clCreateBuffer(context, 
    CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
    Sizeof.cl_float * n, srcA, null);
memObjects[1] = clCreateBuffer(context, 
    CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
    Sizeof.cl_float * n, srcB, null);
memObjects[2] = clCreateBuffer(context, 
    CL_MEM_READ_WRITE, 
    Sizeof.cl_float * n, null, null);

现在我们创建一个程序来保存我们之前创建的内核。

cl_program program = clCreateProgramWithSource(context,
            1, new String[]{ programSource }, null, null);

我们必须编译 OpenCL 内核。

clBuildProgram(program, 0, null, null, null, null);

现在我们将内核绑定到前面指定的名称。

cl_kernel kernel = clCreateKernel(program, "sampleKernel", null);

现在缓冲区已经创建,我们必须分配三个参数。

clSetKernelArg(kernel, 0, 
    Sizeof.cl_mem, Pointer.to(memObjects[0]));
clSetKernelArg(kernel, 1, 
    Sizeof.cl_mem, Pointer.to(memObjects[1]));
clSetKernelArg(kernel, 2, 
    Sizeof.cl_mem, Pointer.to(memObjects[2]));

我们指定任务要分解成多少个线程。这是数组的总元素数量。可以进一步细分工作量。我们将工作量大小简单地指定为 1。这意味着每个项目都是一个独立的工作组。有时将多个工作组合并在一起很有用,因为它们可以共享内存。

long global_work_size[] = new long[]{n};
long local_work_size[] = new long[]{1};

我们终于准备好执行内核了。它将并行执行每个数组元素。

clEnqueueNDRangeKernel(commandQueue, kernel, 1, null,
    global_work_size, local_work_size, 0, null, null);

最后,我们从“c”参数中读取结果。

clEnqueueReadBuffer(commandQueue, memObjects[2], CL_TRUE, 0,
    n * Sizeof.cl_float, dst, 0, null, null);

现在我们有了内核的结果。

这是一个非常简单的应用程序,但是它展示了所有基础知识。你看到了如何构造要发送到内核的数据。你看到了如何从内核读取数据。在下一部分中,我们将学习如何扩展内核以进行更有用的处理。

如果你对使用 GPU 进行神经网络感兴趣,你应该看看 Encog Java 神经网络项目

故障排除

使用适用于你系统的正确 DLL 非常重要。即使你的计算机支持 64 位,你可能也没有以 64 位模式运行 Java。下面的错误显示了这种情况的一个例子。请注意,它将架构位大小指定为 32。我的计算机正在以 64 位模式运行。但是,Java 没有以该模式运行。调整 Eclipse IDE 的 JRE 设置可以解决此问题。

Error while loading native library "JOCL-windows-x86" with base name "JOCL"
Operating system name: Windows 7
Architecture         : x86
Architecture bit size: 32

你可能也没有加载 OpenCL.DLL。这可能导致以下错误。

Error while loading native library "JOCL-windows-x86" with base name "JOCL"
Operating system name: Windows 2003
Architecture         : x86
Architecture bit size: 32
Stack trace:
java.lang.UnsatisfiedLinkError: no JOCL-windows-x86 in java.library.path
	at java.lang.ClassLoader.loadLibrary(Unknown Source)
	at java.lang.Runtime.loadLibrary0(Unknown Source)
	at java.lang.System.loadLibrary(Unknown Source)
	at org.jocl.LibUtils.loadLibrary(LibUtils.java:67)
	at org.jocl.CL.assertInit(CL.java:1694)
	at org.jocl.CL.clGetPlatformIDs(CL.java:1744)
	at org.encog.util.cl.EncogCL.<init>(EncogCL.java:53)
	at org.encog.Encog.initCL(Encog.java:150)
	at org.encog.examples.neural.opencl.SimpleCLTest.main(SimpleCLTest.java:10)
Exception in thread "main" org.encog.EncogError: java.lang.UnsatisfiedLinkError: 
		Could not load the native library
	at org.encog.Encog.initCL(Encog.java:155)
	at org.encog.examples.neural.opencl.SimpleCLTest.main(SimpleCLTest.java:10)
Caused by: java.lang.UnsatisfiedLinkError: Could not load the native library
	at org.jocl.LibUtils.loadLibrary(LibUtils.java:78)
	at org.jocl.CL.assertInit(CL.java:1694)
	at org.jocl.CL.clGetPlatformIDs(CL.java:1744)
	at org.encog.util.cl.EncogCL.<init>(EncogCL.java:53)
	at org.encog.Encog.initCL(Encog.java:150)
	... 1 more

历史

  • 2010 年 6 月 9 日:初始帖子
© . All rights reserved.