Юлия: CUSPARSE параллельных вычислений на нескольких GPU

Я n отдельные графические процессоры, каждый из которых хранит свои собственные данные. Я хотел бы, чтобы каждый из них выполняет расчеты одновременно. В CUDArt документации здесь описывает использование потоков для асинхронного вызова пользовательских ядер C для достижения распараллеливания (см. Также этот другой пример здесь). С помощью пользовательских ядер это можно сделать с помощью stream аргумент в реализации CUDArt

2 ответов


хорошо, я думаю, что наконец-то наткнулся на что-то, что работает по крайней мере относительно хорошо. Я все еще был бы абсолютно рад предложить награду, хотя всем, у кого есть дальнейшие улучшения. В частности, улучшения, основанные на дизайне, который я попытался (но не смог) реализовать, как описано в этой так что вопрос был бы здорово. Но, любые улучшения или предложения по этому поводу, и я был бы рад дать щедрость.

ключ прорыв, который я обнаружил для способа получить такие вещи, как CUSPARSE и CUBLAS для параллелизации по нескольким графическим процессорам, заключается в том, что вам нужно создать отдельный дескриптор для каждого GPU. Е. Г. от документация на API CUBLAS:

приложение должно инициализировать дескриптор контекста библиотеки cuBLAS, вызвав функцию cublasCreate (). Затем он явно передается каждому последующему вызову библиотечной функции. Как только приложение завершит использование библиотека, она должна вызвать функцию cublasDestory (), чтобы освободить ресурсы, связанные с контекстом библиотеки cuBLAS.

этот подход позволяет пользователю контролировать установку библиотеки при использовании нескольких потоков хоста и нескольких GPU. Например, приложение может использовать cudaSetDevice() для связывания различных устройств с различными потоками узлов, и в каждом из этих потоков узлов оно может инициализировать уникальный дескриптор контекста библиотеки cuBLAS, который будет использовать конкретное устройство, связанное с этим потоком хоста. Затем:вызовы функций библиотеки cuBLAS, выполненные с разным дескриптором, автоматически отправят вычисления на разные устройства.

(курсив)

посмотреть здесь и здесь для некоторых дополнительных полезных документов.

теперь, чтобы действительно продвинуться вперед в этом, мне пришлось сделать кучу довольно грязного взлома. В будущее, я надеюсь связаться с людьми, которые разработали пакеты CUSPARSE и CUBLAS, чтобы увидеть о включении этого в свои пакеты. На данный момент, однако, это то, что я сделал:

во-первых, пакеты CUSPARSE и CUBLAS поставляются с функциями для создания дескрипторов. Но мне пришлось немного изменить пакеты, чтобы экспортировать эти функции (наряду с необходимыми другими функциями и типами объектов), чтобы я мог получить к ним доступ.

В Частности, Я добавил CUSPARSE.jl следующее:

export libcusparse, SparseChar

to libcusparse_types.jl следующее:

export cusparseHandle_t, cusparseOperation_t, cusparseMatDescr_t, cusparseStatus_t

to libcusparse.jl следующее:

export cusparseCreate

и sparse.jl следующее:

export getDescr, cusparseop

через все это я смог получить функциональный доступ к cusparseCreate() функция, которая может быть использована для создания новых дескрипторов (я не мог просто использовать CUSPARSE.cusparseCreate() потому что эта функция зависела от множества других функций и типов данных). Оттуда я определил новый версия операции умножения матрицы, которую я хотел, которая взяла дополнительный аргумент, дескриптор, для подачи в ccall() водителю CUDA. Ниже приведен полный код:

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

одним небольшим улучшением было бы обернуть ccall выражение в функции проверки, чтобы вы получили вывод в случае, если вызов CUDA возвращает ошибки.