C++ Boost Compute

Da Andreabont's Wiki.

La sottolibreria compute delle BOOST permette di utilizzare facilmente la GPU del proprio computer per effettuare calcoli, attraverso l'interfaccia OpenCL. Per funzionare il sistema deve aver correttamente configurato OpenCL.

Compilare

g++ --std=c++11 <file_cpp> -lOpenCL

Richiesta informazioni sui device utilizzabili

#include <iostream>
#include <iomanip>
#include <boost/compute/core.hpp>

std::string device_type_to_string(cl_device_type type) {
    
    switch(type) {

        case CL_DEVICE_TYPE_ACCELERATOR:
            return "ACCELERATOR";
        
        case CL_DEVICE_TYPE_CPU:
            return "CPU";
        
        case CL_DEVICE_TYPE_GPU:
            return "GPU";
        
        case CL_DEVICE_TYPE_ALL:
            return "ALL";
            
        case CL_DEVICE_TYPE_DEFAULT:
            return "DEFAULT";
            
        default:
            return "Unknow";
        
    }
    
}

int main() {

    boost::compute::device default_device = boost::compute::system::default_device();
    
    for(boost::compute::device device : boost::compute::system::devices()) {
        
        std::cout << "• OPENCL DEVICE:" << std::endl;
        
        std::cout << "Default:        " << std::boolalpha << (device == default_device) << std::endl;
        std::cout << "Id:             " << device.id() << std::endl;
        std::cout << "Type:           " << device_type_to_string(device.type()) << std::endl;
        std::cout << "Name:           " << device.name() << std::endl;
        std::cout << "Vendor:         " << device.vendor() << std::endl;
        std::cout << "Profile:        " << device.profile() << std::endl;
        std::cout << "Version:        " << device.version() << std::endl;
        std::cout << "Driver version: " << device.driver_version() << std::endl;
        std::cout << "Frequency:      " << device.clock_frequency() << " MHz" << std::endl;
        std::cout << "Compute units:  " << device.compute_units() << " cores" <<std::endl;
        std::cout << "Global memory:  " << device.global_memory_size() / 1024 / 1024 << " Mb" << std::endl;
        
        std::cout << std::endl;

        std::cout << "Extensions:" << std::endl;
        std::cout << std::left;
        
        int i = 1;
        for(std::string s : device.extensions()) {
            std::cout << std::setw(50) << s;
            if(i%2 == 0) std::cout << '\n';
            i++;
        }
        
        std::cout << std::endl;
        std::cout << std::endl;
        
    }

}

Computazione

Per appofondire è possibile visionare la Lista delle API.

Trasformazione

#include <vector>
#include <iostream>
#include <boost/compute/algorithm/copy.hpp>
#include <boost/compute/container/vector.hpp>
#include <boost/compute/algorithm/transform.hpp>

int main() {
    
    // Ottieni device di default e contesto.
    boost::compute::device device = boost::compute::system::default_device();
    boost::compute::context context(device);
    boost::compute::command_queue queue(context, device);

    // Preparo i dati da computare
    std::vector<float> host_vector = {1,2,3,4,5,6,7};
    
    // Creo vettore sulla GPU
    boost::compute::vector<float> device_vector(host_vector.size(), context);
    
    // Copio il vettore locale su quello della GPU
    boost::compute::copy(
        std::begin(host_vector),       // Inizio vettore sorgente
        std::end(host_vector),         // Fine vettore sorgente
        std::begin(device_vector),     // Inizio vettore destinazione
        queue
    );
    
    // Applico una funzione sul vettore della GPU
    boost::compute::transform(
        std::begin(device_vector),     // Inizio vettore sorgente
        std::end(device_vector),       // Fine vettore sorgente
        std::begin(device_vector),     // Inizio vettore destinazione
        boost::compute::sqrt<float>(), // Funzione da applicare
        queue
    );

    // Copio i dati dal vettore della GPU su quello locale
    boost::compute::copy(
        std::begin(device_vector),     // Inizio vettore sorgente
        std::end(device_vector),       // Fine vettore sorgente
        std::begin(host_vector),       // Inizio vettore destinazione
        queue
    );
    
    // Ora posso usare i dati computati
    for(float i : host_vector) {
        std::cout << i << ' ';
    }
    std::cout << std::endl;

}

Accumulo

#include <vector>
#include <iostream>
#include <boost/compute/algorithm/copy.hpp>
#include <boost/compute/container/vector.hpp>
#include <boost/compute/algorithm/accumulate.hpp>

int main() {
    
    // Ottieni device di default e contesto.
    boost::compute::device device = boost::compute::system::default_device();
    boost::compute::context context(device);
    boost::compute::command_queue queue(context, device);

    // Preparo i dati da computare
    std::vector<int> host_vector = {1,2,3,4,5,6,7};
    
    // Creo vettore sulla GPU
    boost::compute::vector<int> device_vector(host_vector.size(), context);
    
    // Copio il vettore locale su quello della GPU
    boost::compute::copy(
        std::begin(host_vector),       // Inizio vettore sorgente
        std::end(host_vector),         // Fine vettore sorgente
        std::begin(device_vector),     // Inizio vettore destinazione
        queue
    );
    
    // Applico l'accumulo
    int result = boost::compute::accumulate(
        std::begin(device_vector),     // Inizio vettore sorgente
        std::end(device_vector),       // Fine vettore sorgente
        0,                             // Valore iniziale
        boost::compute::plus<int>(),   // Funzione da applicare
        queue  
    );
    
    // Risultato
    for(int i : host_vector) {
        std::cout << i;
        if(i != host_vector.back()) std::cout << " + ";
    }
    
    std::cout << " = " << result << std::endl;

}

Trasformazione + Accumulo

#include <vector>
#include <iostream>
#include <boost/compute/algorithm/copy.hpp>
#include <boost/compute/container/vector.hpp>
#include <boost/compute/functional/bind.hpp>
#include <boost/compute/algorithm/transform_reduce.hpp>
#include <boost/compute/algorithm/transform.hpp>


int main() {
    
    // Ottieni device di default e contesto.
    boost::compute::device device = boost::compute::system::default_device();
    boost::compute::context context(device);
    boost::compute::command_queue queue(context, device);

    // Preparo i dati da computare
    std::vector<float> host_vector = {1,2,3,4,5,6,7};
    
    // Creo vettore sulla GPU
    boost::compute::vector<float> device_vector(host_vector.size(), context);
    
    // Copio il vettore locale su quello della GPU
    boost::compute::copy(
        std::begin(host_vector),       // Inizio vettore sorgente
        std::end(host_vector),         // Fine vettore sorgente
        std::begin(device_vector),     // Inizio vettore destinazione
        queue
    );
    
    float result = 0;
    
    boost::compute::transform_reduce(
        std::begin(device_vector),     // Inizio vettore sorgente
        std::end(device_vector),       // Fine vettore sorgente
        &result,                       // Risultato
        boost::compute::bind(boost::compute::pow<float>(), 2.0f, boost::compute::placeholders::_1),// Funzione transform da applicare
        boost::compute::plus<float>(), // Funzione reduce da applicare
        queue  
    );
    
    std::cout << "Somma dei quadrati: " << result << std::endl;

    std::cout << std::endl;
    
}

Definire funzioni personalizzate

E' possibile definire delle funzioni personalizzate, da notare che per la trasformazione è richiesta una funzione unaria, mentre per il reduce serve una funzione binaria.

boost::compute::function<int (int)> double_int = boost::compute::make_function_from_source<int (int)> (
    "double_int",
    "int double_int(int x) { return x * 2; }"
);

Oppure sfruttando una macro:

BOOST_COMPUTE_FUNCTION(int, double_int, (int x), {
    return x * 2;
});

Copia asincrona + profilazione

#include <vector>
#include <iostream>
#include <boost/compute/core.hpp>
#include <boost/compute/container/vector.hpp>

int main()
{
    // Ottieni device di default e contesto.
    boost::compute::device device = boost::compute::system::default_device();
    boost::compute::context context(device);

    // Ottengo una coda comandi con abilitato il profiling
    boost::compute::command_queue queue(context, device, boost::compute::command_queue::enable_profiling);

    // Preparo i dati da computare
    std::vector<int> host_vector(16000000,50);
    
    // Creo vettore sulla GPU
    boost::compute::vector<int> device_vector(host_vector.size(), context);

    // Copio il vettore locale su quello della GPU in modalità asincrona
    boost::compute::future<void> future = boost::compute::copy_async(
        std::begin(host_vector),       // Inizio vettore sorgente
        std::end(host_vector),         // Fine vettore sorgente
        std::begin(device_vector),     // Inizio vettore destinazione
        queue
    );
    
    std::cout << "Trasferimento dati..." << std::endl;
    
    // Attendo che la copia sia completata
    future.wait();

    // Ottengo tempo di esecuzione dulla GPU
    boost::chrono::milliseconds duration = future.get_event().duration<boost::chrono::milliseconds>();

    std::cout << "Copia completeta in " << duration.count() << " ms" << std::endl;
    
}