Юлия: Параллельные вычисления 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 возвращает ошибки.