环境:
- NVIDIA GeForce GTX 1050
- cuda 10.2.89 windows
- visual studio 2017
- windows SDK 10.0.14393.0
- go 1.13.4 windows/amd64
我们在文件 lib.cu
中实现一个 GPU 计算的浮点数向量内积函数,以及一个 CPU 的入口函数进行数据传递和调用:
__global__ void devDot(float *x, float *y, size_t n, float *r) {
float s = 0.0;
for (size_t i = 0; i < n; i++) s += x[i] * y[i];
*r = s;
}
extern "C" __declspec(dllexport) void dot(float *x, float *y, size_t n, float *r) {
float *xd, *yd, *rd;
size_t sz = sizeof(float) * n;
cudaMalloc(&xd, sz);
cudaMalloc(&yd, sz);
cudaMalloc(&rd, sizeof(float));
cudaMemcpy(xd, x, sz, cudaMemcpyHostToDevice);
cudaMemcpy(yd, y, sz, cudaMemcpyHostToDevice);
devDot<<<1, 1>>>(xd, yd, n, rd);
cudaDeviceSynchronize();
cudaMemcpy(r, rd, sizeof(float), cudaMemcpyDeviceToHost);
cudaFree(xd);
cudaFree(yd);
cudaFree(rd);
}
文件后缀 cu
表示 C/C++ 的语法加上 CUDA 自己的一些扩展。其中,__global__
表示该函数可运行于 GPU,称为核函数。由 cudaMalloc
申请的显存只能在 GPU 中访问,显存和内存之间的数据传输使用 cudaMemcpy
。核函数调用处后面的 <<<1, 1>>>
是 CUDA 扩展的语法,所以只能用 CUDA 专用的编译器前端 nvcc 进行编译,其意义以后再表。核函数的执行对 CPU 是异步的,需要调用 cudaDeviceSynchronize
来同步。
使用以下命令将代码编译为一个动态库(需要将 VC 编译器所在目录加入 PATH
):
nvcc lib.cu -o cuda.dll --shared
将 dll 文件复制到 main.go
同目录下,main.go
如下:
package main
import (
"math/rand"
"syscall"
"time"
"unsafe"
)
const N = 1 << 20
type Lib struct {
dll *syscall.DLL
dotProc *syscall.Proc
}
func LoadLib() (*Lib, error) {
l := &Lib{}
var err error
defer func() {
if nil != err {
l.Release()
}
}()
if l.dll, err = syscall.LoadDLL("cuda.dll"); nil != err {
return nil, err
}
if l.dotProc, err = l.dll.FindProc("dot"); nil != err {
return nil, err
}
return l, nil
}
func (l *Lib) Release() {
if nil != l.dll {
l.dll.Release()
}
}
func (l *Lib) Dot(x, y []float32) float32 {
var r float32
l.dotProc.Call(
uintptr(unsafe.Pointer(&x[0])),
uintptr(unsafe.Pointer(&y[0])),
uintptr(len(x)),
uintptr(unsafe.Pointer(&r)),
)
return r
}
func main() {
lib, err := LoadLib()
if nil != err {
println(err.Error())
return
}
defer lib.Release()
rand.Seed(time.Now().Unix())
x, y := make([]float32, N), make([]float32, N)
for i := 0; i < N; i++ {
x[i], y[i] = rand.Float32(), rand.Float32()
}
t := time.Now()
var r float32
for i := 0; i < 100; i++ {
r = 0
for i := 0; i < N; i++ {
r += x[i] * y[i]
}
}
println(time.Now().Sub(t).Microseconds())
println(r)
t = time.Now()
for i := 0; i < 100; i++ {
r = lib.Dot(x, y)
}
println(time.Now().Sub(t).Microseconds())
println(r)
}
在 golang 中使用动态加载,比较计算结果和运行时间。下面使用 nvprof 来观察运行结果,在其中一次运行中,CPU 版计算 100 次耗时约 120ms,而 GPU 版约 4187ms,其中:
-
cudaMalloc
约 361ms -
cudaMemcpy
约 292ms -
cudaDeviceSynchronize
约 3360ms,其中:-
devDot
约 3321ms
-
这种哈喽级别的 CUDA 尝试终究惨败被虐出一个数量级,这就是所谓的从入门到放弃……吗?
Licensed under CC BY-SA 4.0