cum se compilează proiectul opencl cu nuclee (Programare, Compilare, Kernel, Opencl)

wallen a intrebat.

Sunt total începător pe opencl, am căutat pe internet și am găsit câteva demo-uri „helloworld” pentru proiectul opencl. De obicei, într-un astfel de proiect minimalist, există un fișier *.cl care conține un fel de nuclee opencl și un fișier *.c care conține funcția principală. Apoi, întrebarea este cum pot compila acest tip de proiect folosind o linie de comandă. Știu că ar trebui să folosesc un fel de flag -lOpenCL pe linux și -framework OpenCL pe mac. Dar nu am nicio idee pentru a lega nucleul *.cl de fișierul meu sursă principal. Vă mulțumesc pentru orice comentarii sau link-uri utile.

2 răspunsuri
Farzad

În OpenCL, în OpenCL, se utilizează .cl fișierele care conțin codurile kernel-ului de dispozitiv sunt de obicei compilate și construite în timpul execuției. Înseamnă că undeva în programul OpenCL gazdă, va trebui să compilați și să construiți programul dispozitivului pentru a-l putea utiliza. Această caracteristică permite o portabilitate maximă.

Să luăm în considerare un exemplu pe care l-am colectat din două cărți. Mai jos este un kernel OpenCL foarte simplu care adaugă două numere din două array-uri globale și le salvează într-un alt array global. Am salvat acest cod într-un fișier numit 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];
}

Mai jos este codul gazdă scris în C++ care exploatează API OpenCL C++. Îl salvez într-un fișier numit ocl_vector_addition.cpp alături de cel în care am salvat fișierul .cl fișierul.

#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!
";
        else
            std::cout<< "Failed!
";

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

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

Compilez acest cod pe o mașină cu Ubuntu 12.04 în felul următor:

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

Se produce un fișier ocl_vector_addition.o, , care atunci când îl execut, arată o ieșire de succes. Dacă vă uitați la comanda de compilare, veți vedea că nu am trecut nimic despre .cl fișier. Am folosit doar -lOpenCL pentru a activa biblioteca OpenCL pentru programul nostru. De asemenea, nu vă lăsați distras de -std=c++11 comanda. Pentru că am folosit std::unique_ptr în codul gazdă, a trebuit să folosesc acest flag pentru o compilare reușită.

Așadar, unde se află acest .cl fișier este folosit? Dacă vă uitați la codul gazdă, veți găsi patru părți pe care le repet mai jos numerotate:

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

În primul pas, citim conținutul fișierului care conține codul dispozitivului nostru și îl punem într-un fișier std::string numit sourceCode. Apoi facem o pereche de șir și lungimea acestuia și o salvăm în source care are tipul cl::Program::Sources. După ce am pregătit codul, realizăm un cl::program obiect numit program pentru context și încărcăm codul sursă în obiectul program. Cea de-a treia etapă este cea în care codul OpenCL este compilat (și legat) pentru device. Deoarece codul dispozitivului este construit în a 3-a etapă, putem crea un obiect kernel numit vecadd_kernel și să asociem nucleul numit vecadd în interiorul acestuia cu nucleul nostru cl::kernel obiect. Acesta a fost destul de mult setul de pași implicați în compilarea unui .cl fișier într-un program.

Programul despre care am arătat și explicat creează programul de dispozitiv din codul sursă al kernelului. O altă opțiune este de a folosi în schimb binariile. Utilizarea programului binar îmbunătățește timpul de încărcare a aplicației și permite distribuirea binară a programului, dar limitează portabilitatea, deoarece binarele care funcționează bine pe un dispozitiv pot să nu funcționeze pe un alt dispozitiv. Crearea programului utilizând codul sursă și binar se mai numesc și compilare offline și, respectiv, online (mai multe informații aici). Trec peste asta aici, deoarece răspunsul este deja prea lung.

thb

Răspunsul meu vine cu 4 ani întârziere. Cu toate acestea, am ceva de adăugat care completează răspunsul lui @Farzad, după cum urmează.

În mod confuz, în practica OpenCL, verbul a compila este folosit pentru a însemna două lucruri diferite și incompatibile:

  • Într-o utilizare, a compila înseamnă ceea ce credeți deja că înseamnă. Înseamnă să construiești în momentul construirii, ca și cum ai porni de la surse *.c pentru a produce obiecte *.o pentru a le lega în momentul construirii.
  • Cu toate acestea, într-o altă utilizare – și este posibil ca această altă utilizare să vă fie necunoscută –a compila înseamnă să interpretezi în timp de execuție, ca și în cazul surselor *.cl, producând cod mașină GPU.

Una dintre acestea se întâmplă în momentul compilării. Cealaltă se întâmplă în timpul execuției.

Ar fi fost mai puțin confuz dacă ar fi fost introduse două verbe diferite, dar nu așa a evoluat terminologia. În mod convențional, verbul a compila este utilizat pentru ambele.

Dacă nu sunteți sigur, încercați acest experiment: redenumiți fișierul *.cl astfel încât celelalte fișiere sursă să nu-l poată găsi, apoi compilați.

Vedeți? Se compilează bine, nu-i așa?

Acest lucru se datorează faptului că fișierul *.cl nu este consultat în momentul compilării. Abia mai târziu, când încercați să executați executabilul binar, programul eșuează.

Dacă vă ajută, vă puteți gândi la fișierul *.cl ca și cum ar fi un fișier de date sau un fișier de configurare sau chiar un script. Poate că nu este literalmente un fișier de date, un fișier de configurare sau un script, pentru că, în cele din urmă, este compilat într-un fel de cod mașină, dar codul mașină este un cod GPU și nu este creat din textul programului *.cl până la momentul execuției. În plus, la momentul execuției, compilatorul C ca atare nu este implicat. Mai degrabă, biblioteca OpenCL este cea care se ocupă de compilare.

Mi-a luat destul de mult timp pentru a îndrepta aceste concepte în mintea mea, mai ales pentru că, la fel ca și dumneavoastră, eram de mult timp familiarizat cu etapele ciclului de construcție C/C++; și, prin urmare, am crezut că știu ce înseamnă cuvinte precum a compila însemna. Odată ce mintea dvs. are cuvintele și conceptele clare, diferitele documentații OpenCL încep să aibă sens și puteți începe să lucrați.

Comentarii

  • În completarea răspunsurilor de aici, m-am confruntat cu o problemă similară, gândindu-mă naiv la „compilare” în termeni normali, adică la modul în care o interpretăm în C/C++. Răspunsul de mai sus al lui @thb are mai mult sens pentru mine acum. Vedeți întrebarea mea aici stackoverflow.com/q/64381665/8731673 –  > Por Amine.
  • Nu este corect să spunem că este interpretat la execuție. Pentru ca codul să funcționeze pe mai multe hardware GPU, nu știm „compilatorul” care poate fi utilizat. Fiecare producător de GPU are un cod de asamblare diferit. Așadar, codul este „compilat” pentru un GPU în momentul execuției binarului (CPU care „găzduiește” GPU-ul). Este același lucru ca și în cazul codului „shader” din OpenGL. Utilizând reprezentări intermediare, puteți face optimizări independente de GPU (optimizator front-end) și lăsați back-end-ul să se întâmple la momentul execuției. A se vedea: SPIR. –  > Por zgomot fără artă.