Юлия: Параллельные вычисления CUSPARSE на нескольких графических процессорах

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

Возможно ли это с CUSPARSE или мне просто нужно погрузиться в C, если я хочу использовать несколько графических процессоров?

ПЕРЕСМОТРЕННОЕ Обновление Баунти

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

2 ответа

Решение

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

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

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

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

(выделение добавлено)

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

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

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

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

export libcusparse, SparseChar

в libcusparse_types.jl следующие:

export cusparseHandle_t, cusparseOperation_t, cusparseMatDescr_t, cusparseStatus_t

в 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 возвращает ошибки.

Другие вопросы по тегам