讓 Windows 的 R 用上 CUDA

2021-03-02 R語言中文社區
特別感謝 @諸葛不亮 的點撥。

R 是一個統計學經常用到的軟體,提供了非常多的統計學函數。 但是它是一個單線程解釋語言,面對大數據量的時候,往往性能跟不上,可以利用 Rcpp 編寫 C++ 包提供給 R 使用,可以大大提高性能。 而對於大規模數據的處理,使用 CUDA 則是一個非常好的解決方案。 在 Linux 和 macOS 下, CUDA 程序和 C++ 程序都使用 gcc 編譯器, 但是在 Windows 下,Rcpp 的包必須用 MinGW 編譯器, CUDA 的包必須用 MSVC 編譯器,需要一定的技巧才能讓 R 用上 CUDA。

本文介紹 MSVC 包和 MinGW 包的混合編譯,不僅適用於 R 語言,也不僅適用於 CUDA 程序, 也適用於其他需要通過 MinGW 的程序調用 MSVC 程序的情況。

MinGW 調用 MSVC 庫函數的條件

MinGW 其實一直都是可以直接調用 MSVC 庫函數的,只是這樣的函數需要滿足幾個條件:

MinGW 可以調用 MSVC 編譯的動態庫,但是不能調用 MSVC 編譯的靜態庫, 因為 MinGW 和 MSVC 中會引用同樣的符號,但 MSVC 有的符號在 MinGW 中沒有,調用 MSVC 靜態庫時需要加載 MSVC 的符號,導致衝突。

MSVC 編譯的 DLL 必須導出 C 接口,否則 MinGW 中找不到符號,這是因為 C++ 會給函數名做修改以支持函數重載,但 MSVC 和 MinGW 對函數修改的方式不一樣。 也有人說時 __cdecl__ 和 __stdcall__ 的問題,但我試了一下不太行,還是找不到符號。 既然時 C 接口,那麼參數和返回值不能是 class ,只能用指向 class 類型的指針類型。

不能將 MinGW 中創建的指針傳遞到 MSVC 中進行操作,否則在 MSVC 中就相當於野指針。反之也不可以。 因為 MinGW 和 MSVC 編譯的庫,內存地址是兩套,指針不互通。

在 R 中,一定會大量用到矩陣,一有矩陣那必然涉及到指針,而且一定是在 MinGW 函數中創建的指針。那麼該怎麼將矩陣傳到 MSVC 的函數中呢? 這其實非常類似於 CUDA 編程中的內存問題,我們只需要在兩邊分別開闢內存,然後將內存中的數據複製一下,相當於寫一個 cudaMalloc 和 cudaMemcpy 。 這樣相當於在全局創建了很多的變量,如果使用一些方法將這些全局變量統一管理會更好。 可以將這些全局變量保存在一個結構體中,工廠函數返回一個指向這個結構體的指針,並為這個結構體成員創建初始值。

也可以利用 C++ 類的封裝我們要在 R 中調用的函數,將這個類繼承自一個抽象類(所有函數都是純虛函數),將接口類導出, 同時導出一個 C 的工廠函數,返回指向這個接口類的指針, 利用 MSVC 和 MinGW 虛表結構一致的特點,就可以在 MinGW 的 C++ 代碼中使用這個接口類中的函數了(類沒有虛析構函數)。 將矩陣數據作為類的成員變量,利用 C++ 成員函數進行創建、賦值、銷毀。

非類的寫法

首先演示一種純 C 非類的寫法。首先需要建立一個 VS 的 CUDA 工程,並設置該項目配置類型為「動態連結庫」。 項目目錄如下:

AddCUDA

add.cpp

add.h

kernel.cu

kernel.h

AddCUDA.vcxproj

文件 kernel.cu 使用的 CUDA 函數是 VS 中 CUDA 工程自帶的模板,做了一些修改, 主要去掉了 goto 語句,並設置了核函數啟動配置。

// kernel.cu#include "cuda_runtime.h"#include "device_launch_parameters.h"
#include <stdio.h>
#include "kernel.h"
__global__ void addKernel(int *c, const int *a, const int *b){int i = threadIdx.x; c[i] = a[i] + b[i];}
// Helper function for using CUDA to add vectors in parallel.bool addWithCuda(int *c, const int *a, const int *b, unsigned int size){int *dev_a = 0;int *dev_b = 0;int *dev_c = 0; cudaError_t cudaStatus; // Choose which GPU to run on, change this on a multi-GPU system. cudaStatus = cudaSetDevice(0);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaSetDevice failed! Do you have a CUDA-capable GPU installed?"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b);return false; }
// Allocate GPU buffers for three vectors (two input, one output) . cudaStatus = cudaMalloc((void**)&dev_c, size * sizeof(int));if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMalloc failed!"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b);return false; }cudaStatus = cudaMalloc((void**)&dev_a, size * sizeof(int));if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMalloc failed!"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b);return false; }
cudaStatus = cudaMalloc((void**)&dev_b, size * sizeof(int));if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMalloc failed!"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b);return false; }// Copy input vectors from host memory to GPU buffers. cudaStatus = cudaMemcpy(dev_a, a, size * sizeof(int), cudaMemcpyHostToDevice);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMemcpy failed!"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b);return false; }
cudaStatus = cudaMemcpy(dev_b, b, size * sizeof(int), cudaMemcpyHostToDevice);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMemcpy failed!"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b);return false; }
// Launch a kernel on the GPU with one thread for each element.dim3 blockSize(256), gridSize((size + blockSize.x - 1) / blockSize.x); addKernel<<<gridSize, blockSize >>>(dev_c, dev_a, dev_b);// Check for any errors launching the kernel cudaStatus = cudaGetLastError();if (cudaStatus != cudaSuccess) {fprintf(stderr, "addKernel launch failed: %s\n", cudaGetErrorString(cudaStatus)); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b);return false; }
// cudaDeviceSynchronize waits for the kernel to finish, and returns// any errors encountered during the launch. cudaStatus = cudaDeviceSynchronize();if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaDeviceSynchronize returned error code %d after launching addKernel!\n", cudaStatus); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b);return false; }  // Copy output vector from GPU buffer to host memory. cudaStatus = cudaMemcpy(c, dev_c, size * sizeof(int), cudaMemcpyDeviceToHost);if (cudaStatus != cudaSuccess) {fprintf(stderr, "cudaMemcpy failed!"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b);return false; }
return true;}

將 addWithCuda 函數的聲明移動到 kernel.h 文件中。

// kernel.h#pragma once#include <cuda_runtime.h>bool addWithCuda(int *c, const int *a, const int *b, unsigned int size);

然後需要編寫 MSVC 的接口函數了。在 add.h 文件中做如下聲明:

// add.h#pragma once
#ifdef DLL_EXPORT#define ADDCUDA_API extern "C" __declspec(dllexport)#else#define ADDCUDA_API extern "C" __declspec(dllimport)#endif // DLL_EXPORT
ADDCUDA_API int* createVector(int n);ADDCUDA_API void setVector(int* ptr, int i, int value);ADDCUDA_API int getVector(int* ptr, int i);ADDCUDA_API void deleteVector(int* ptr);ADDCUDA_API bool addVector(int* a, int* b, int n, int *c);

當然, __declspec(dllimport) 可要可不要,要也可以,這樣這個 Dll 也可以給 Windows 程序使用。

在 add.cpp 中進行定義:

// add.cpp#include "add.h"#include "kernel.h"
int* createVector(int n){return new int[n];}

void deleteVector(int* ptr){delete[] ptr;}
void setVector(int* ptr, int i, int value){ ptr[i] = value;}
int getVector(int* ptr, int i){return ptr[i];}
bool addVector(int* a, int* b, int n, int *c){bool cudaStatus = addWithCuda(c, a, b, n);return cudaStatus;}

雖然這幾個函數很短小,但是也不能寫在頭文件裡。否則 MinGW 中會報符號二義性錯誤。

然後在 MinGW 的主函數中進行調用

// mingw.cpp#include "add.h"#include <stdio.h>
int main(int argc, char const *argv[]){int n = 100000;int *a = createVector(n);int *b = createVector(n);int *c = createVector(n);for (size_t i = 0; i < n; i++) { setVector(a, i, 10); setVector(b, i, 100); setVector(c, i, 0); } addVector(a, b, n, c);int *result = new int[n];for (size_t i = 0; i < n; i++) { result[i] = getVector(c, i); }printf("result:\n");for (size_t i = 0; i < 10; i++) {printf("%5d", result[i]); }printf("\n");
deleteVector(a); deleteVector(b); deleteVector(c);
return 0;}

MinGW 在連結時,需要手動指定 MSVC 生成的 lib 文件,而且要放到 -o 參數的後面,方法如下:

g++ -I"./AddCUDA" -L"./x64/Release" mingw.o -o cudaMinGWC -lAddCUDA

這樣就生成了 cudaMinGWC.exe 文件。運行一下可以得到結果:

result:  110  110  110  110  110  110  110  110  110  110

抽象類的寫法

抽象類的寫法主要用到了多態的特性。首先需要創建一個抽象基類 IAdd 和派生類 CAdd 。

// IAdd.h#pragma once
#ifdef DLL_EXPORT#define ADDCUDA_API __declspec(dllexport)#else#define ADDCUDA_API __declspec(dllimport)#endif // DLL_EXPORT

class ADDCUDA_API IAdd{public:virtual void SetA(int i, int value) = 0;virtual void SetB(int i, int value) = 0;virtual int GetC(int i) = 0;virtual bool Add() = 0;};
extern "C" ADDCUDA_API IAdd* Add_new(int n);extern "C" ADDCUDA_API void Add_del(IAdd* ptr);
// IAdd.cpp#include "IAdd.h"#include "CAdd.h"
IAdd* Add_new(int n){return new CAdd(n);}
void Add_del(IAdd* ptr){delete ptr;}

在抽象類中完全不實現類的任何接口,都標記為純虛函數。 同時,在抽象類的外面,定義一套工廠函數,用於創建和銷毀這個抽象類派生類的對象。

派生類的定義如下:

// CAdd.h#pragma once
#include "IAdd.h"
class CAdd : public IAdd{private:int n;int* a;int* b;int* c;public: CAdd(int n); ~CAdd();
virtual void SetA(int i, int value);virtual void SetB(int i, int value);virtual int GetC(int i);virtual bool Add();};// CAdd.cpp#include "CAdd.h"#include "kernel.h"#include <memory.h>

CAdd::CAdd(int n){this->n = n; a = new int[n]; b = new int[n]; c = new int[n];memset(a, 0, sizeof(int) * n);memset(b, 0, sizeof(int) * n);memset(c, 0, sizeof(int) * n);}CAdd::~CAdd(){delete[] a;delete[] b;delete[] c;}
void CAdd::SetA(int i, int value){if (i < n) a[i] = value;}
void CAdd::SetB(int i, int value){if (i < n) b[i] = value;}
int CAdd::GetC(int i){return (i < n) ? c[i] : 0;}
bool CAdd::Add(){return addWithCuda(c, a, b, n);}

在 MinGW 中調用如下:

#include <stdio.h>#include "IAdd.h"int main(int argc, char const *argv[]){int n = 100000;    IAdd* ptr = Add_new(n);for (size_t i = 0; i < n; i++)    {        ptr->SetA(i, 10);        ptr->SetB(i, 100);    }    ptr->Add();printf("result:\n");for (size_t i = 0; i < 10; i++)    {printf("%5d", ptr->GetC(i));    }printf("\n");
Add_del(ptr);
return 0;}

程序運行結果:

result:110  110  110  110  110  110  110  110  110  110

可見已經可以運行了。

總結

總體而言,這種方式調用方式的開銷還是比較大的。 不僅在內存中複製了一份數據,在傳遞數據的過程中是一個一個傳遞的,比直接內存拷貝開銷大很多。 另外如果採用抽象類的方式,還有虛函數調用的開銷。 因此,如果不是必須在 Windows 上用 MinGW 調用 CUDA 程序,儘量還是使用 MSVC 編譯器。

對於 Rcpp 而言,恰恰是必須在 Windows 使用 MinGW ,此使想調用 CUDA 程序,則需要通過這種方式。

相關焦點

  • cuda 安裝 小記.
    linux 桌面安裝:https://aws.amazon.com/cn/premiumsupport/knowledge-center/connect-to-linux-desktop-from-windows
  • cuda入門:如何進行矩陣乘法優化
    特別注意到它用 double 來儲存暫時的計算結果,以提高精確度。不過,因為我們的矩陣乘法函式可以指定 pitch(即 lda、ldb、和 ldc),所以如果用一般的 cudaMemcpy 函式來複製內存的話,會需要每個 row 都分開複製,那會需要呼叫很多次 cudaMemcpy 函式,會使效率變得很差。因此,在這裡我們用了一個新的 cudaMemcpy2D 函式,它是用來複製二維數組,可以指定數組的 pitch。這樣就可以透過一次函數調用就可以了。
  • 解讀CUDA C Programming Guide 第三章第2節-Device Memory
    linear memory通常是使用cudaMalloc()來分配,cudaFree()來釋放,host和device間的數據傳輸使用cudaMemcpy(),用cudaMemset()賦值。例如一個向量相加的例子,vectors向量需要從host傳到device,然後進行計算。
  • 在windows上怎麼用vim
    windows &nbsp;中怎麼安裝vim ,以及使用 vim 時會遇到的一些麻煩、 失靈現象。先baidu搜vim org進vim網站 下載gvim7.exe,並安裝agree,next,yes 直到完成。
  • CUDA之CUDA編程模型概述(一)
    CUDA平臺也提供了著一些列的工具供我們使用,我們這一章主要就是講解這些工具怎麼用,如何編寫調試CUDA程序。以及編寫兩個矩陣運算有關的CUDA應用,以供大家把玩。下面我們會說兩個我們GPU架構下特有幾個功能:通過組織層次結構在GPU上組織線程的方法通過組織層次結構在GPU上組織內存的方法也就是對內存和線程的控制將伴隨我們寫完前十幾篇。
  • 寫CUDA到底難在哪?
    比如要使用一個多GPU的機器,你要把數據分成每個GPU的大組,每個GPU要分成每個Thread的小組,然後為了使用張量(Tensor)核心還要分成張量處理器能用的固定數量的小組,這個過程簡直是難上艱難,燒腦子掉頭髮。
  • Numba:用CUDA加速的高性能Python編譯器
    ,我們只需將目標更改為「CPU」,它將在編譯水平上帶來性能,在CPU上向量化C代碼。這種靈活性可以幫助你生成更可重用的代碼,並允許你在沒有GPU的機器上開發。請注意,mandel_kernel函數使用Numba提供的cuda.threadIdx,cuda.blockIdx,cuda.blockDim和cuda.gridDim架構來計算當前線程的全局X和Y像素索引。
  • windows安裝R語言及Rstudio(R系列教程1)
    windows安裝R語言及RstudioR語言R是用於統計分析、繪圖的語言和操作環境。
  • CUDA系列學習(二)(轉)
    一個host thread同一時刻只能用一個device context,每個host thread都有一個保存當前contexts的stack。那麼如果warp中的部分threads走上了數據相關的條件分支,warp就連續在各個branch上執行,暫停沒進入branch的threads。直到所有branch上的threads都執行完再合併了一起向下走。所以實現性能提升要注意儘量使warp內線程不要出現divergence。另外,注意這個branch divergence 之發生在warp內部;不同warp之間是獨立執行的。
  • 用cmake搭建環境來編譯一個CUDA程序
    【IT168 文檔】一、簡介  在Windows平臺下,用cmake來搭建環境環境,在VS2005下運行CUDA程序。其實,在Windows下,在CUDA2.3的SDK裡,有一個Cuda.Rules的文件。通過這個文件,在VS2005裡可以很方便的設置各個編譯參數。
  • CUDA編程學習系列1
    修改為GPU上運行函數      修改函數聲明,在CUDA中,把在device(區別於CPU)運行的函數稱為kernal function,注意下面代碼,在聲明處增加__global__,表明它是device code.
  • CUDA在MFC中的聯調方法實例
    ./ -Ihttp://www.cnblogs.com/common/inc -o $(ConfigurationName)\deviceQuery.obj deviceQuery.cu  注意紅色下劃線部分,帶便往上退兩個文件夾,然後進入commom文件夾中的inc文件夾。再比較之前的兩個路徑,這正好是deciceQuery.sin文件到cutil.h的一個訪問的過程。
  • CUDA8.0+VS2015+Win10開發環境搭建教程
    視覺IMAX的第38篇原創文章前言本文參考了網絡上的其他文檔,在此不一一致謝了,前輩們的經驗都是強大的指明燈。根據實踐中的具體情況,進行了勘誤和修正,僅供新手借鑑,至於高手們可以略過了。三 檢測cuda是否安裝成功在上述流程進行後,接下來我們需要檢測cuda是否安裝成功,方法如下:進入cmd(我用的是管理員),輸入nvcc –V,此處需要注意的是:中間是有空格的
  • EC2上的深度學習:CUDA 7/cuDNN/caffe/DIGITS實戰教程
    /repos/ubuntu1404/x86_64/cuda-repo-ubuntu1404_7.0-28_amd64.deb # installing CUDAsudo dpkg -i cuda-repo-ubuntu1404_7.0-28_amd64.deb sudo apt-get updatesudo apt-get install cuda # setting the environment
  • 文本或代碼中 \n 和 \r 的區別
    這就是"換行"和"回車"的來歷,從它們的英語名字上也可以看出一二。二、\n 和 \r差異後來,計算機發明了,這兩個概念也就被搬到了計算機上。那時,存儲器很貴,一些科學家認為在每行結尾加兩個字符太浪費了,加一個就可以。於是,就出現了分歧。
  • 系統性學習CUDA編程的推薦資料
    https://developer.nvidia.com/blog/even-easier-introduction-cuda/針對初學者,對統一內存的說明。https://developer.nvidia.com/blog/unified-memory-cuda-beginners/如果在1基礎上對CUDA產生濃厚興趣了,那麼下一步動手實踐。
  • Numba:基於CUDA加速的高性能Python
    因此,關心效率的Python程式設計師通常會用C重寫最內層的循環,並從Python調用編譯好的C函數。有許多旨在使此優化更容易的項目(如Cython),但它們通常需要學習新的語法。理想情況下,Python程式設計師希望使現有的Python代碼更快,而不需要使用另一種程式語言,而且自然,很多人希望使用加速器從而使代碼具有更高的性能。
  • (Windows、Mac、Ubuntu全講解)
    cuda歷史版本下載:https://developer.nvidia.com/cuda-toolkit-archive要看NVIDIA的組件,自己的CUDA支持哪個版本,我是1060顯卡,所以我下的10.0版本的cuda
  • windows許可證即將過期怎麼辦
    打開APP windows許可證即將過期怎麼辦 網絡整理 發表於 2020-12-23 14:53:18   windows許可證即將過期怎麼辦   最近很多朋友諮詢關於怎麼解決windows許可證即將過期的問題,今天的這篇經驗就來聊一聊這個話題,希望可以幫助到有需要的朋友。