如何用内核编译opencl项目

时间:2014-10-22 21:06:18

标签: compilation kernel opencl

我是opencl的初学者,我在互联网上搜索了一些" helloworld" opencl项目的演示。通常在这种最小项目中,有一个* .cl文件包含某种opencl内核,* .c文件包含main函数。那么问题是如何使用命令行编译这种项目。我知道我应该在linux上使用某种-lOpenCL标志,在mac上使用-framework OpenCL。但我不知道将* .cl内核链接到我的主源文件。感谢您提供任何意见或有用的链接。

2 个答案:

答案 0 :(得分:25)

在OpenCL中,包含设备内核代码的.cl文件通常在运行时进行编译和构建。它意味着您的主机OpenCL程序中的某个位置,您必须编译和构建您的设备程序才能使用它。此功能可实现最大的可移植性。

让我们考虑一下我从两本书中收集的一个例子。下面是一个非常简单的OpenCL内核,它从两个全局数组中添加两个数字并将它们保存在另一个全局数组中。我将此代码保存在名为vector_add_kernel.cl的文件中。

kernel void vecadd( global int* A, global int* B, global int* C ) {
    const int idx = get_global_id(0);
    C[idx] = A[idx] + B[idx];
}

下面是用C ++编写的利用OpenCL C ++ API的主机代码。我将其保存在我保存ocl_vector_addition.cpp文件旁边的.cl文件中。

#include <iostream>
#include <fstream>
#include <string>
#include <memory>
#include <stdlib.h>

#define __CL_ENABLE_EXCEPTIONS
#if defined(__APPLE__) || defined(__MACOSX)
#include <OpenCL/cl.cpp>
#else
#include <CL/cl.hpp>
#endif

int main( int argc, char** argv ) {

    const int N_ELEMENTS=1024*1024;
    unsigned int platform_id=0, device_id=0;

    try{
        std::unique_ptr<int[]> A(new int[N_ELEMENTS]); // Or you can use simple dynamic arrays like: int* A = new int[N_ELEMENTS];
        std::unique_ptr<int[]> B(new int[N_ELEMENTS]);
        std::unique_ptr<int[]> C(new int[N_ELEMENTS]);

        for( int i = 0; i < N_ELEMENTS; ++i ) {
            A[i] = i;
            B[i] = i;
        }

        // Query for platforms
        std::vector<cl::Platform> platforms;
        cl::Platform::get(&platforms);

        // Get a list of devices on this platform
        std::vector<cl::Device> devices;
        platforms[platform_id].getDevices(CL_DEVICE_TYPE_GPU|CL_DEVICE_TYPE_CPU, &devices); // Select the platform.

        // Create a context
        cl::Context context(devices);

        // Create a command queue
        cl::CommandQueue queue = cl::CommandQueue( context, devices[device_id] );   // Select the device.

        // Create the memory buffers
        cl::Buffer bufferA=cl::Buffer(context, CL_MEM_READ_ONLY, N_ELEMENTS * sizeof(int));
        cl::Buffer bufferB=cl::Buffer(context, CL_MEM_READ_ONLY, N_ELEMENTS * sizeof(int));
        cl::Buffer bufferC=cl::Buffer(context, CL_MEM_WRITE_ONLY, N_ELEMENTS * sizeof(int));

        // Copy the input data to the input buffers using the command queue.
        queue.enqueueWriteBuffer( bufferA, CL_FALSE, 0, N_ELEMENTS * sizeof(int), A.get() );
        queue.enqueueWriteBuffer( bufferB, CL_FALSE, 0, N_ELEMENTS * sizeof(int), B.get() );

        // Read the program source
        std::ifstream sourceFile("vector_add_kernel.cl");
        std::string sourceCode( std::istreambuf_iterator<char>(sourceFile), (std::istreambuf_iterator<char>()));
        cl::Program::Sources source(1, std::make_pair(sourceCode.c_str(), sourceCode.length()));

        // Make program from the source code
        cl::Program program=cl::Program(context, source);

        // Build the program for the devices
        program.build(devices);

        // Make kernel
        cl::Kernel vecadd_kernel(program, "vecadd");

        // Set the kernel arguments
        vecadd_kernel.setArg( 0, bufferA );
        vecadd_kernel.setArg( 1, bufferB );
        vecadd_kernel.setArg( 2, bufferC );

        // Execute the kernel
        cl::NDRange global( N_ELEMENTS );
        cl::NDRange local( 256 );
        queue.enqueueNDRangeKernel( vecadd_kernel, cl::NullRange, global, local );

        // Copy the output data back to the host
        queue.enqueueReadBuffer( bufferC, CL_TRUE, 0, N_ELEMENTS * sizeof(int), C.get() );

        // Verify the result
        bool result=true;
        for (int i=0; i<N_ELEMENTS; i ++)
            if (C[i] !=A[i]+B[i]) {
                result=false;
                break;
            }
        if (result)
            std::cout<< "Success!\n";
        else
            std::cout<< "Failed!\n";

    }
    catch(cl::Error err) {
        std::cout << "Error: " << err.what() << "(" << err.err() << ")" << std::endl;
        return( EXIT_FAILURE );
    }

    std::cout << "Done.\n";
    return( EXIT_SUCCESS );
}

我在使用Ubuntu 12.04的机器上编译此代码,如下所示:

g++ ocl_vector_addition.cpp -lOpenCL -std=c++11 -o ocl_vector_addition.o

它产生一个ocl_vector_addition.o,当我运行时,显示成功的输出。如果查看编译命令,您会发现我们没有传递任何有关.cl文件的信息。我们只使用-lOpenCL标志为我们的程序启用了OpenCL库。另外,不要被-std=c++11命令分散注意力。因为我在主机代码中使用了std::unique_ptr,所以我不得不使用这个标志来成功编译。

那么这个.cl文件在哪里使用?如果你看一下主机代码,你会发现我在下面重复编写的四个部分:

// 1. Read the program source
std::ifstream sourceFile("vector_add_kernel.cl");
std::string sourceCode( std::istreambuf_iterator<char>(sourceFile), (std::istreambuf_iterator<char>()));
cl::Program::Sources source(1, std::make_pair(sourceCode.c_str(), sourceCode.length()));

// 2. Make program from the source code
cl::Program program=cl::Program(context, source);

// 3. Build the program for the devices
program.build(devices);

// 4. Make kernel
cl::Kernel vecadd_kernel(program, "vecadd");

在第1步中,我们阅读了保存设备代码的文件的内容,并将其放入名为std::string的{​​{1}}中。然后我们创建一对字符串及其长度,并将其保存到sourceCode,其类型为source。在我们准备好代码之后,我们为cl::Program::Sources创建一个名为cl::program的{​​{1}}对象,并将源代码加载到程序对象中。第三步是为program编译(和链接)OpenCL代码的步骤。由于设备代码是在第3步中构建的,因此我们可以创建一个名为context的内核对象,并将其中名为device的内核与我们的vecadd_kernel对象相关联。这几乎是编译程序中vecadd文件所涉及的一系列步骤。

我展示和解释的程序从内核源代码创建了设备程序。另一种选择是使用二进制文件。使用二进制程序增强了应用程序加载时间并允许程序的二进制分发,但限制了可移植性,因为在一个设备上正常工作的二进制文件可能无法在另一个设使用源代码和二进制文件创建程序也分别称为脱机和在线编译(更多信息here)。我在这里跳过它,因为答案已经太久了。

答案 1 :(得分:2)

我的答案来晚了四年。尽管如此,我还是要补充一些补充@Farzad的答案,如下所示。

令人困惑的是,在OpenCL实践中,动词要编译用来表示两个不同的不兼容的事物:

  • 在一种用法中,要编译表示您已经认为的含义。这意味着在构建时进行构建,例如从* .c源生成* .o对象以进行构建时链接。
  • 但是,在另一种用法(而且您可能不熟悉这种用法)中,编译意味着在运行时进行解释,例如* .cl来源,生成GPU机器代码。

在构建时发生一次。另一个发生在运行时。

如果引入了两个不同的动词,可能会减少混乱,但这并不是术语演变的方式。通常,动词要编译都用于这两种情况。

如果不确定,请尝试以下实验:重命名* .cl文件,以便其他源文件找不到它,然后进行构建。

看到了吗?它可以构建,不是吗?

这是因为在构建时未参考* .cl文件。仅在稍后,当您尝试执行二进制可执行文件时,程序才会失败。

如果有帮助,您可以将* .cl文件视为数据文件,配置文件甚至脚本。它可能不是字面上的数据文件,配置文件或脚本,因为它最终最终会被编译为某种机器代码,但是机器代码是GPU代码,并且不是由* .cl程序文本构成的直到运行时。而且,在运行时,您的C编译器本身不会参与。而是由您的OpenCL库进行构建。

我花了很长时间弄清楚这些概念,主要是因为-和您一样-我很早就熟悉C / C ++构建周期的各个阶段。因此,我以为我知道要编译之类的词的含义。一旦您有了直截了当的词汇和概念,各种OpenCL文档就会变得有意义,您就可以开始工作了。