我有n
个独立的GPU,每个GPU都存储自己的数据。我想让他们每个人同时进行一组计算。 CUDArt文档here描述了使用流来异步调用自定义C内核以实现并行化(另请参见另一个示例here)。使用自定义内核,可以通过在CUDArt的stream
函数实现中使用launch()
参数来实现。但据我所知,CUSPARSE(或CUBLAS)函数没有类似的流规范选项。
这可能与CUSPARSE一起使用,或者如果我想使用多个GPU,我是否只需要深入到C?
修订赏金更新
好的,所以,我现在有一个相对不错的解决方案,最后。但是,我确信它可以通过百万种方式得到改善 - 现在它非常黑客。特别是,我喜欢在this SO问题(我从来没有正常工作)中尝试和写过的解决方案的建议。因此,我很高兴将赏金奖励给任何有进一步想法的人。
答案 0 :(得分:4)
好的,所以,我认为我终于找到了至少相对的东西。我仍然非常高兴地向所有有进一步改进的人提供赏金。特别是,基于我尝试(但未能)实现的设计的改进,如this SO问题所述,这将是很好的。但是,对此有任何改进或建议,我很高兴给予赏金。
我发现的一个关键突破,就是让CUSPARSE和CUBLAS等多个GPU并行化的方法是你需要为每个GPU创建一个单独的句柄。例如。来自CUBLAS API上的documentation:
应用程序必须通过调用cublasCreate()函数初始化cuBLAS库上下文的句柄。然后,显式传递给每个后续的库函数调用。一旦应用程序完成使用库,它必须调用函数cublasDestory()来释放与cuBLAS库上下文相关联的资源。
此方法允许用户在使用多个主机线程和多个GPU时显式控制库设置。例如,应用程序可以使用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()
,因为该函数依赖于一堆其他功能和数据类型)。从那里,我定义了一个新版本的矩阵乘法运算,我希望它采用另一个参数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
答案 1 :(得分:1)
一个小的改进是将ccall
表达式包装在一个检查函数中,以便在对CUDA的调用返回错误时获得输出。