it-swarm.com.de

so kompilieren Sie das opencl-Projekt mit Kerneln

Ich bin ein absoluter Anfänger bei opencl, ich habe im Internet gesucht und einige "helloworld" -Demos für das opencl-Projekt gefunden. In einer solchen Art von Minimalprojekt gibt es normalerweise eine * .cl-Datei, die eine Art Opencl-Kernel enthält, und eine * .c-Datei enthält die Hauptfunktion. Die Frage ist dann, wie ich diese Art von Projekt mithilfe einer Befehlszeile kompiliere. Ich weiß, ich sollte eine Art -lOpenCL-Flag für Linux und -Framework OpenCL für Mac verwenden. Ich habe jedoch keine Idee, den * .cl-Kernel mit meiner Hauptquelldatei zu verknüpfen. Vielen Dank für alle Kommentare oder nützliche Links. 

16
wallen

In OpenCL werden die .cl-Dateien, die Gerätekerncodes enthalten, normalerweise zur Laufzeit kompiliert und erstellt. Es bedeutet, dass Sie irgendwo in Ihrem Host OpenCL-Programm Ihr Geräteprogramm kompilieren und erstellen müssen, um es verwenden zu können. Diese Funktion ermöglicht maximale Portabilität.

Betrachten wir ein Beispiel, das ich aus zwei Büchern gesammelt habe. Unten ist ein sehr einfacher OpenCL-Kernel, der zwei Zahlen aus zwei globalen Arrays hinzufügt und sie in einem anderen globalen Array speichert. Ich speichere diesen Code in einer Datei namens 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];
}

Nachfolgend finden Sie den in C++ geschriebenen Host-Code, der die OpenCL C++ - API nutzt. Ich speichere es in einer Datei namens ocl_vector_addition.cpp neben der Stelle, an der ich meine .cl-Datei gespeichert habe.

#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 );
}

Ich kompiliere diesen Code auf einem Computer mit Ubuntu 12.04 wie folgt:

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

Es erzeugt einen ocl_vector_addition.o, der beim Ausführen eine erfolgreiche Ausgabe anzeigt. Wenn Sie sich den Kompilierungsbefehl anschauen, sehen Sie, dass wir nichts über unsere .cl-Datei übergeben haben. Wir haben nur -lOpenCL-Flag verwendet, um die OpenCL-Bibliothek für unser Programm zu aktivieren. Lassen Sie sich auch nicht vom -std=c++11-Befehl ablenken. Da ich std::unique_ptr im Hostcode verwendet habe, musste ich dieses Flag für ein erfolgreiches Kompilieren verwenden.

Wo wird diese .cl-Datei verwendet? Wenn Sie sich den Host-Code ansehen, finden Sie vier Teile, die ich in der folgenden Nummer wiederhole:

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

Im ersten Schritt lesen wir den Inhalt der Datei, die unseren Gerätecode enthält, und legen ihn in einen std::string namens sourceCode ab. Dann machen wir ein Paar aus dem String und seiner Länge und speichern es in source, das den Typ cl::Program::Sources hat. Nachdem wir den Code vorbereitet haben, erstellen wir ein cl::program-Objekt mit dem Namen program für die context und laden den Quellcode in das Programmobjekt. Der dritte Schritt ist der, in dem der OpenCL-Code für die device kompiliert (und verknüpft) wird. Da der Gerätecode im dritten Schritt erstellt wird, können wir ein Kernelobjekt mit dem Namen vecadd_kernel erstellen und den Kernel mit dem Namen vecadd unserem cl::kernel-Objekt zuordnen. Dies war so ziemlich die Menge an Schritten, die zum Kompilieren einer .cl-Datei in einem Programm erforderlich waren.

Das Programm, das ich gezeigt und erklärt habe, erstellt das Geräteprogramm aus dem Kernel-Quellcode. Eine andere Option ist die Verwendung von Binärdateien. Die Verwendung eines binären Programms verlängert die Ladezeit der Anwendung und ermöglicht die binäre Verteilung des Programms, begrenzt jedoch die Portabilität, da Binärdateien, die auf einem Gerät einwandfrei funktionieren, auf einem anderen Gerät nicht funktionieren. Das Erstellen von Programmen mit Quellcode und Binärdateien wird auch als Offline- und Online-Kompilierung bezeichnet (weitere Informationen hier ). Ich überspringe es hier, da die Antwort schon zu lang ist. 

25
Farzad

Meine Antwort kommt vier Jahre zu spät. Trotzdem habe ich etwas hinzuzufügen, das die Antwort von @ Farzad wie folgt ergänzt.

Verwirrenderweise wird in der OpenCL-Übung das Verb zum Kompilieren verwendet, um zwei verschiedene, inkompatible Dinge zu bedeuten:

  • In einer Verwendung bedeutet kompilieren , was Sie bereits denken, dass es bedeutet. Es bedeutet zu Build-Time bauen, wie aus * .c-Quellen, um * .o-Objekte für Build-Time-Links zu erzeugen.
  • In einer anderen Verwendung jedoch - und diese andere Verwendung ist Ihnen möglicherweise nicht vertraut - zum Kompilieren bedeutet zur Laufzeit interpretieren, wie aus * .cl-Quellen, wobei GPU-Maschinencode erzeugt wird.

Eines geschieht zur Bauzeit. Das andere geschieht zur Laufzeit.

Es wäre vielleicht weniger verwirrend gewesen, wenn zwei verschiedene Verben eingeführt worden wären, aber so hat sich die Terminologie nicht entwickelt. Üblicherweise wird das Verb zum Kompilieren für beide verwendet.

Wenn Sie sich nicht sicher sind, probieren Sie dieses Experiment aus: Benennen Sie Ihre * .cl-Datei um, sodass Ihre anderen Quelldateien sie nicht finden können, und erstellen Sie dann.

Sehen? Es baut gut, nicht wahr?

Dies liegt daran, dass die * .cl-Datei nicht zur Erstellungszeit abgefragt wird. Wenn Sie versuchen, die binäre ausführbare Datei auszuführen, schlägt das Programm erst später fehl.

Wenn es hilft, können Sie sich die * .cl-Datei wie eine Datendatei, eine Konfigurationsdatei oder sogar ein Skript vorstellen. Es ist nicht buchstäblich eine Datendatei, eine Konfigurationsdatei oder ein Skript, vielleicht wird es schließlich zu einer Art Maschinencode kompiliert, aber der Maschinencode ist GPU-Code und wird nicht aus dem Programmcode * .cl erstellt bis zur Laufzeit. Zur Laufzeit ist Ihr C-Compiler als solcher nicht beteiligt. Es ist vielmehr Ihre OpenCL-Bibliothek, die das Gebäude übernimmt.

Ich habe ziemlich lange gebraucht, um diese Konzepte in meinem Kopf zu begründen, meistens, weil ich - wie Sie - schon lange mit den Stadien des C/C++ - Buildzyklus vertraut war. und deshalb hatte ich geglaubt, ich wüsste, welche Wörter wie kompilieren gemeint sind. Sobald Ihr Verstand die Worte und Konzepte klar hat, macht die verschiedene OpenCL-Dokumentation Sinn und Sie können mit der Arbeit beginnen.

2
thb