web-dev-qa-db-fra.com

comment compiler un projet opencl avec des noyaux

Je suis totalement débutant sur opencl, j'ai cherché sur Internet et trouvé des démos "helloworld" pour le projet opencl. Généralement, dans ce type de projet minimal, il existe un fichier * .cl contenant une sorte de noyau opencl et un fichier * .c contenant la fonction principale. Ensuite, la question est de savoir comment compiler ce type de projet en utilisant une ligne de commande. Je sais que je devrais utiliser une sorte de drapeau -lOpenCL sur linux et -framework OpenCL sur mac. Mais je n'ai aucune idée de lier le noyau * .cl à mon fichier source principal. Merci pour vos commentaires ou liens utiles. 

16
wallen

Dans OpenCL, les fichiers .cl contenant les codes du noyau de périphérique sont généralement compilés et générés au moment de l’exécution. Cela signifie que quelque part dans votre programme Host OpenCL, vous devrez compiler et construire votre programme de périphérique pour pouvoir l'utiliser. Cette fonctionnalité permet une portabilité maximale.

Prenons un exemple que j'ai recueilli de deux livres. Vous trouverez ci-dessous un noyau OpenCL très simple, qui ajoute deux nombres de deux tableaux globaux et les enregistre dans un autre tableau global. Je sauvegarde ce code dans un fichier nommé 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];
}

Vous trouverez ci-dessous le code d'hôte écrit en C++ qui exploite l'API OpenCL C++. Je l’enregistre dans un fichier nommé ocl_vector_addition.cpp à côté de celui où j’ai enregistré mon fichier .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 );
}

Je compile ce code sur une machine avec Ubuntu 12.04 comme ceci:

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

Il produit un ocl_vector_addition.o qui, lorsque je suis en train d’exécuter, indique une sortie réussie. Si vous regardez la commande de compilation, vous verrez que nous n’avons rien transmis à notre fichier .cl. Nous avons uniquement utilisé l'indicateur -lOpenCL pour activer la bibliothèque OpenCL pour notre programme. En outre, ne vous laissez pas distraire par la commande -std=c++11. Comme j'ai utilisé std::unique_ptr dans le code de l'hôte, j'ai dû utiliser cet indicateur pour une compilation réussie.

Alors, où ce fichier .cl est-il utilisé? Si vous regardez le code hôte, vous trouverez quatre parties que je répète ci-dessous et numérotées:

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

Dans la 1ère étape, nous lisons le contenu du fichier qui contient notre code d’appareil et le plaçons dans un std::string nommé sourceCode. Ensuite, nous faisons une paire de la chaîne et de sa longueur et l'enregistrons dans source qui a le type cl::Program::Sources. Après avoir préparé le code, nous créons un objet cl::program nommé program pour le context et chargeons le code source dans l'objet programme. La 3ème étape est celle dans laquelle le code OpenCL est compilé (et lié) pour device. Comme le code de périphérique est généré à la 3ème étape, nous pouvons créer un objet noyau nommé vecadd_kernel et associer le noyau nommé vecadd à l'intérieur de celui-ci à notre objet cl::kernel. C’est à peu près l’ensemble des étapes nécessaires à la compilation d’un fichier .cl dans un programme.

Le programme que j'ai montré et expliqué crée le programme de périphérique à partir du code source du noyau. Une autre option consiste à utiliser des fichiers binaires à la place. L'utilisation d'un programme binaire améliore le temps de chargement des applications et permet la distribution binaire du programme, mais limite la portabilité, car les fichiers binaires fonctionnant correctement sur un périphérique risquent de ne pas fonctionner sur un autre périphérique. La création de programme en utilisant le code source et le binaire est également appelée compilation en ligne et hors ligne (plus d'informations ici ). Je le saute ici car la réponse est déjà trop longue. 

25
Farzad

Ma réponse a quatre ans de retard. Néanmoins, j'ai quelque chose à ajouter qui complète la réponse de @ Farzad, comme suit.

De façon confuse, dans la pratique OpenCL, le verbe compiler signifie deux choses différentes et incompatibles:

  • Dans un usage, compiler signifie ce que vous pensez déjà que cela signifie. Cela signifie construire au moment de la construction, à partir des sources * .c, pour produire des objets * .o pour la liaison au moment de la construction.
  • Cependant, dans une autre utilisation (et cette autre utilisation peut ne pas vous être familière) - compiler signifie interpréter au moment de l'exécution, à partir de sources * .cl, produisant du code machine GPU.

L'une se produit au moment de la construction. L'autre arrive au moment de l'exécution.

Cela aurait peut-être été moins déroutant si deux verbes différents avaient été introduits, mais ce n'est pas ainsi que la terminologie a évolué. Classiquement, le verbe pour compiler est utilisé pour les deux.

En cas de doute, essayez cette expérience: renommez votre fichier * .cl afin que vos autres fichiers source ne puissent pas le trouver, puis générez.

Voir? Ça se construit bien, n'est-ce pas?

En effet, le fichier * .cl n’est pas consulté au moment de la construction. Le programme échoue uniquement lorsque vous essayez d'exécuter l'exécutable binaire.

Si cela vous aide, vous pouvez considérer le fichier * .cl comme un fichier de données, un fichier de configuration ou même un script. Ce n'est pas littéralement un fichier de données, un fichier de configuration ou un script, peut-être, car il finit par être compilé en une sorte de code machine, mais le code machine est un code GPU et il n'est pas créé à partir du texte du programme * .cl jusqu'au moment de l'exécution. De plus, au moment de l'exécution, votre compilateur C en tant que tel n'est pas impliqué. C'est plutôt votre bibliothèque OpenCL qui fait le bâtiment.

Il m'a fallu assez de temps pour redéfinir ces concepts dans mon esprit, principalement parce que - comme vous - je connaissais depuis longtemps les étapes du cycle de construction de C/C++; et, par conséquent, j'avais pensé savoir quels mots comme compiler voulait dire. Une fois que votre esprit a bien compris les mots et les concepts, les différentes documentations OpenCL commencent à avoir un sens et vous pouvez commencer à travailler.

2
thb