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

94
9

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


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


ПЕРЕСМОТРЕННОЕ Обновление Bounty


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

спросил(а) 2016-07-10T18:39:00+03:00 3 года, 11 месяцев назад
1
Решение
86

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


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


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

Этот подход позволяет пользователю явно управлять настройкой библиотеки при использовании нескольких потоков хоста и нескольких графических процессоров. Например, приложение может использовать 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(), потому что эта функция зависела от множества других функций и типы данных). Оттуда я определил новую версию операции умножения матриц, которую я хотел, и потребовал дополнительный аргумент Handle для подачи в 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

ответил(а) 2016-07-17T22:55:00+03:00 3 года, 11 месяцев назад
56

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

ответил(а) 2016-07-19T19:58:00+03:00 3 года, 11 месяцев назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема