CUDA 及其 golang 調(diào)用 - 從入門到放棄 - 1. 初見

環(huán)境:

  • 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 中實(shí)現(xiàn)一個(gè) GPU 計(jì)算的浮點(diǎn)數(shù)向量內(nèi)積函數(shù),以及一個(gè) CPU 的入口函數(shù)進(jìn)行數(shù)據(jù)傳遞和調(diào)用:

__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 自己的一些擴(kuò)展。其中,__global__ 表示該函數(shù)可運(yùn)行于 GPU,稱為核函數(shù)。由 cudaMalloc 申請的顯存只能在 GPU 中訪問,顯存和內(nèi)存之間的數(shù)據(jù)傳輸使用 cudaMemcpy。核函數(shù)調(diào)用處后面的 <<<1, 1>>> 是 CUDA 擴(kuò)展的語法,所以只能用 CUDA 專用的編譯器前端 nvcc 進(jìn)行編譯,其意義以后再表。核函數(shù)的執(zhí)行對 CPU 是異步的,需要調(diào)用 cudaDeviceSynchronize 來同步。

使用以下命令將代碼編譯為一個(gè)動(dòng)態(tài)庫(需要將 VC 編譯器所在目錄加入 PATH):

nvcc lib.cu -o cuda.dll --shared

將 dll 文件復(fù)制到 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 中使用動(dòng)態(tài)加載,比較計(jì)算結(jié)果和運(yùn)行時(shí)間。下面使用 nvprof 來觀察運(yùn)行結(jié)果,在其中一次運(yùn)行中,CPU 版計(jì)算 100 次耗時(shí)約 120ms,而 GPU 版約 4187ms,其中:

  • cudaMalloc 約 361ms
  • cudaMemcpy 約 292ms
  • cudaDeviceSynchronize 約 3360ms,其中:
    • devDot 約 3321ms

這種哈嘍級(jí)別的 CUDA 嘗試終究慘敗被虐出一個(gè)數(shù)量級(jí),這就是所謂的從入門到放棄……嗎?

Licensed under CC BY-SA 4.0

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容