2016-07-10 98 views
5

Ho GPU distinte n, ognuna delle quali memorizza i propri dati. Mi piacerebbe che ognuno di loro eseguisse contemporaneamente una serie di calcoli. La documentazione CUDArt here descrive l'uso degli stream per chiamare in modo asincrono i kernel C personalizzati per ottenere la parallelizzazione (vedere anche questo altro esempio here). Con i kernel personalizzati, questo può essere ottenuto mediante l'uso dell'argomento stream nell'implementazione di CUDArt della funzione launch(). Per quanto posso dire, tuttavia, le funzioni CUSPARSE (o CUBLAS) non hanno un'opzione simile per le specifiche del flusso.Julia: calcoli paralleli CUSPARSE su più GPU

Ciò è possibile con CUSPARSE o devo semplicemente scendere alla C se voglio utilizzare più GPU?

REVISIONE Bounty Aggiornamento

Ok, così, ora ho una soluzione di lavoro relativamente decente, finalmente. Ma sono sicuro che potrebbe essere migliorato in un milione di modi: al momento è abbastanza hacky. In particolare, mi piacerebbe ricevere suggerimenti per soluzioni sulla falsariga di ciò che ho provato e scritto nella domanda SO this (che non ho mai avuto modo di lavorare correttamente). Pertanto, sarei lieto di assegnare la generosità a chiunque abbia ulteriori idee qui.

risposta

4

Ok, quindi, penso di aver finalmente trovato qualcosa che funziona almeno relativamente. Sarei comunque assolutamente felice di offrire il Bounty a chiunque abbia ulteriori miglioramenti. In particolare, i miglioramenti basati sul progetto che ho tentato (ma non riuscito) di implementare come descritto nella domanda this SO sarebbero grandiosi. Ma, eventuali miglioramenti o suggerimenti su questo e sarei lieto di dare la taglia.

L'importante scoperta che ho scoperto per far sì che CUSPARSE e CUBLAS possano parallelizzare su più GPU è che è necessario creare un handle separato per ogni GPU. Per esempio. dal documentation su CUBLAS API:

La domanda deve inizializzare il manico alla cuBLAS contesto biblioteca chiamando la funzione cublasCreate(). Quindi, viene passato esplicitamente a ogni chiamata di funzione della libreria successiva. Una volta che l'applicazione ha terminato di utilizzare la libreria, deve chiamare la funzione cublasDestory() per rilasciare le risorse associate al contesto della libreria cuBLAS.

Questo approccio consente all'utente di controllare esplicitamente l'installazione della libreria quando si utilizzano più thread host e più GPU. Ad esempio, l'applicazione può utilizzare cudaSetDevice() per associare dispositivi diversi a thread host diversi e in ciascuno di questi thread host può inizializzare un handle univoco per il contesto della libreria cuBLAS, che utilizzerà il particolare dispositivo associato a quel thread host. Quindi, , le chiamate della funzione di libreria cuBLAS effettuate con handle diversi invieranno automaticamente il calcolo a dispositivi diversi.

(enfasi aggiunta)

Vedi here e here per alcuni documenti utile aggiuntivi.

Ora, per andare avanti effettivamente su questo, ho dovuto fare un po 'di hacking piuttosto complicato. In futuro, spero di entrare in contatto con le persone che hanno sviluppato i pacchetti CUSPARSE e CUBLAS per vedere come incorporare questo nei loro pacchetti.Per il momento però, questo è quello che ho fatto:

Per prima cosa, i pacchetti CUSPARSE e CUBLAS sono dotati di funzioni per creare maniglie. Ma, ho dovuto modificare un po 'i pacchetti per esportare quelle funzioni (insieme alle altre funzioni e tipi di oggetto necessari) in modo che potessi effettivamente accedervi da solo.

Specificamente, ho aggiunto al CUSPARSE.jl seguente:

export libcusparse, SparseChar 

al libcusparse_types.jl seguente:

export cusparseHandle_t, cusparseOperation_t, cusparseMatDescr_t, cusparseStatus_t 

al libcusparse.jl seguente:

export cusparseCreate 

e sparse.jl il seguente:

export getDescr, cusparseop 

attraverso tutti questi, sono stato in grado di ottenere l'accesso funzionale alla funzione cusparseCreate() che può essere usato per creare nuove maniglie (non ho potuto semplicemente usare CUSPARSE.cusparseCreate() perché quella funzione dipendeva da una serie di altre funzioni e tipi di dati). Da lì, ho definito una nuova versione dell'operazione di moltiplicazione della matrice che volevo che avesse un argomento aggiuntivo, il Gestore, per alimentare lo ccall() al driver CUDA. Di seguito è riportato il codice completo:

using CUDArt, CUSPARSE ## note: modified version of CUSPARSE, as indicated above. 

N = 10^3; 
M = 10^6; 
p = 0.1; 

devlist = devices(dev->true); 
nGPU = length(devlist) 

dev_X = Array(CudaSparseMatrixCSR, nGPU) 
dev_b = Array(CudaArray, nGPU) 
dev_c = Array(CudaArray, nGPU) 
Handles = Array(Array{Ptr{Void},1}, nGPU) 


for (idx, dev) in enumerate(devlist) 
    println("sending data to device $dev") 
    device(dev) ## switch to given device 
    dev_X[idx] = CudaSparseMatrixCSR(sprand(N,M,p)) 
    dev_b[idx] = CudaArray(rand(M)) 
    dev_c[idx] = CudaArray(zeros(N)) 
    Handles[idx] = cusparseHandle_t[0] 
    cusparseCreate(Handles[idx]) 
end 


function Pmv!(
    Handle::Array{Ptr{Void},1}, 
    transa::SparseChar, 
    alpha::Float64, 
    A::CudaSparseMatrixCSR{Float64}, 
    X::CudaVector{Float64}, 
    beta::Float64, 
    Y::CudaVector{Float64}, 
    index::SparseChar) 
    Mat  = A 
    cutransa = cusparseop(transa) 
    m,n = Mat.dims 
    cudesc = getDescr(A,index) 
    device(device(A)) ## necessary to switch to the device associated with the handle and data for the ccall 
    ccall(
     ((:cusparseDcsrmv),libcusparse), 

     cusparseStatus_t, 

     (cusparseHandle_t, cusparseOperation_t, Cint, 
     Cint, Cint, Ptr{Float64}, Ptr{cusparseMatDescr_t}, 
     Ptr{Float64}, Ptr{Cint}, Ptr{Cint}, Ptr{Float64}, 
     Ptr{Float64}, Ptr{Float64}), 

     Handle[1], 
     cutransa, m, n, Mat.nnz, [alpha], &cudesc, Mat.nzVal, 
     Mat.rowPtr, Mat.colVal, X, [beta], Y 
    ) 
end 

function test(Handles, dev_X, dev_b, dev_c, idx) 
    Pmv!(Handles[idx], 'N', 1.0, dev_X[idx], dev_b[idx], 0.0, dev_c[idx], 'O') 
    device(idx-1) 
    return to_host(dev_c[idx]) 
end 


function test2(Handles, dev_X, dev_b, dev_c) 

    @sync begin 
     for (idx, dev) in enumerate(devlist) 
      @async begin 
       Pmv!(Handles[idx], 'N', 1.0, dev_X[idx], dev_b[idx], 0.0, dev_c[idx], 'O') 
      end 
     end 
    end 
    Results = Array(Array{Float64}, nGPU) 
    for (idx, dev) in enumerate(devlist) 
     device(dev) 
     Results[idx] = to_host(dev_c[idx]) ## to_host doesn't require setting correct device first. But, it is quicker if you do this. 
    end 

    return Results 
end 

## Function times given after initial run for compilation 
@time a = test(Handles, dev_X, dev_b, dev_c, 1); ## 0.010849 seconds (12 allocations: 8.297 KB) 
@time b = test2(Handles, dev_X, dev_b, dev_c); ## 0.011503 seconds (68 allocations: 19.641 KB) 

# julia> a == b[1] 
# true 
1

Un piccolo miglioramento potrebbe essere quello di avvolgere l'espressione ccall in una funzione di controllo in modo da ottenere in uscita nel caso in cui la chiamata a CUDA restituisce gli errori.