web-dev-qa-db-ja.com

カーネルでopenclプロジェクトをコンパイルする方法

私は完全にopenclの初心者です。インターネットを検索したところ、openclプロジェクトの「helloworld」デモが見つかりました。通常、このような最小限のプロジェクトでは、*。clファイルに何らかのopenclカーネルが含まれており、*。cファイルに主な機能が含まれています。次に、この種のプロジェクトをコマンドラインを使用してコンパイルするにはどうすればよいですか。 Linuxでは-lOpenCLフラグを使用し、Macでは-framework OpenCLを使用する必要があることはわかっています。しかし、*。clカーネルをメインのソースファイルにリンクすることは考えていません。コメントや役立つリンクをありがとう。

18
wallen

OpenCLでは、デバイスカーネルコードを含む.clファイルは通常、実行時にコンパイルおよびビルドされます。これは、ホストOpenCLプログラムのどこかで、デバイスプログラムを使用できるようにコンパイルおよびビルドする必要があることを意味します。この機能により、最大の移植性が実現します。

2冊の本から集めた例を考えてみましょう。以下は、2つのグローバル配列から2つの数値を追加し、それらを別のグローバル配列に保存する非常に単純な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];
}

以下は、OpenCL C++ APIを利用するC++で記述されたホストコードです。 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ファイルはどこで使用されていますか? Hostコードを見ると、以下に番号を付けて繰り返している4つの部分があります。

// 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");

最初のステップでは、デバイスコードを保持するファイルの内容を読み取り、sourceCodeという名前のstd::stringに入れます。次に、文字列とその長さのペアを作成し、タイプcl::Program::Sourcesを持つsourceに保存します。コードを準備したら、programcontextという名前のcl::programオブジェクトを作成し、ソースコードをプログラムオブジェクトにロードします。 3番目のステップは、OpenCLコードがdevice用にコンパイル(およびリンク)されるステップです。デバイスコードは3番目のステップで作成されるため、vecadd_kernelという名前のカーネルオブジェクトを作成し、その内部のvecaddというカーネルをcl::kernelオブジェクトに関連付けることができます。これは、プログラムで.clファイルをコンパイルするために必要な一連のステップでした。

私が示して説明したプログラムは、カーネルソースコードからデバイスプログラムを作成します。別のオプションは、代わりにバイナリを使用することです。バイナリプログラムを使用すると、アプリケーションの読み込み時間が長くなり、プログラムのバイナリ配布が可能になりますが、移植性が制限されます。1つのデバイスで正常に機能するバイナリは別のデバイスでは機能しない場合があるためです。ソースコードとバイナリを使用してプログラムを作成することは、それぞれオフラインコンパイルとオンラインコンパイルとも呼ばれます(詳細 ここ )。答えが長すぎるため、ここでは省略します。

27
Farzad

私の答えは4年遅れて来ます。それにもかかわらず、@ Farzadの答えを補足する次のようなものを追加します。

紛らわしいことに、OpenCLの慣例では、動詞(to to compile)は、2つの異なる互換性のないものを意味するために使用されます。

  • 1つの使用法では、コンパイルするは、あなたがすでにそれを意味すると考えていることを意味します。これは、ビルド時にリンクするための* .cソースから* .oオブジェクトを生成するように、ビルド時にビルドすることを意味します。
  • ただし、別の使用法では、そしてこの他の使用法はあなたにとってなじみのないかもしれません—コンパイルするは、*。clソースからのように実行時に解釈して、GPUマシンコードを生成することを意味します。

1つはビルド時に発生します。もう1つは実行時に発生します。

2つの異なる動詞が導入されていれば、混乱が少なくなったかもしれませんが、用語が進化したわけではありません。通常、動詞コンパイルするは両方に使用されます。

わからない場合は、この実験を試してください。*。clファイルの名前を変更して、他のソースファイルが見つけられないようにしてからビルドします。

見る?それはうまく構築しますね?

これは、ビルド時に* .clファイルが参照されないためです。その後、バイナリ実行可能ファイルを実行しようとすると、プログラムは失敗します。

役立つ場合は、*。clファイルをデータファイル、構成ファイル、またはスクリプトであるかのように考えることができます。文字通り、データファイル、構成ファイル、スクリプトではありません。おそらく、最終的には一種のマシンコードにコンパイルされますが、マシンコードはGPUコードであり、*。clプログラムテキストから作成されていません実行時まで。さらに、実行時には、Cコンパイラ自体は関与しません。むしろ、ビルドを行うのはOpenCLライブラリです。

これらの概念を頭の中で正直するのにかなり長い時間がかかりました。これは、主に-あなたと同じように-私はC/C++ビルドサイクルのステージに長い間慣れていたためです。したがって、私はコンパイルするのような単語の意味がわかっていると思っていました。あなたの心が言葉と概念をまっすぐに理解すると、さまざまなOpenCLドキュメントが意味を持ち始め、作業を開始できます。

3
thb