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