移除CUDA依赖

remove CUDA dependency?

本文关键字:依赖 CUDA 移除      更新时间:2023-10-16

我感兴趣的开源c++/Qt应用程序依赖于CUDA。我的macbook pro(2014年年中)配备的是英特尔Iris pro,没有NVidia显卡。当然,预构建的应用程序不会运行。

我找到了这个模拟器:https://github.com/gtcasl/gpuocelot -但它只针对Linux进行了测试,并且有几个关于它不能在Mac上编译的开放问题。

我有源代码-我可以用c++等价物代替CUDA依赖,以较慢的处理为代价吗?我希望是这样的

  1. 将文件扩展名:.cu重命名为。cpp
  2. 从make文件中删除CUDA引用
  3. 用等价的c++ std库头替换CUDA头
  4. 调整makefile,根据需要添加缺失的库引用
  5. 修复剩余的缺失函数调用(希望只有一个或两个)与c++代码(可能从Ocelot剽窃)

但恐怕事情没那么简单。在我开始之前,我想要一个完整的检查。

在一般情况下,我不认为有一个特定的路线图来"去cuda -fy"一个应用程序。正如我不认为有一个特定的"机械"路线图来"CUDA-fy"一个应用程序,我也没有找到一个特定的路线图来解决一般的编程问题。

此外,我认为提议的路线图有缺陷。仅举一个例子,.cu文件通常会有cuda特定的引用,这是用于编译.cpp代码的普通c++编译器所不能容忍的。这些引用中的一些可能是依赖于CUDA运行时API的项目,例如cudaMalloccudaMemcpy,尽管这些可以通过普通的c++编译器(它们只是库调用)传递,但对于删除CUDA字符的应用程序来说,将这些保留在适当的位置是不明智的。此外,一些参考可能是CUDA特定的语言功能,如通过__global____device__声明设备代码或启动设备"内核"函数,其相应的语法<<<...>>>。这些不能让通过普通的c++编译器,必须特别处理。此外,简单地删除那些CUDA关键字和语法将不太可能产生有用的结果。

简而言之,代码必须被重构;没有合理的简明路线图来解释这样做的机械过程。我建议重构过程的复杂性与将代码的非CUDA版本转换为CUDA版本的原始过程(如果有的话)的复杂性大致相同。为了理解CUDA结构,至少需要一些CUDA编程的非机械知识。

对于非常简单的CUDA代码,可能会设置一个有点机械的过程来消除CUDA-fy代码。回顾一下,基本的CUDA处理顺序如下:

  1. 为设备上的数据分配空间(可能使用cudaMalloc)并将数据复制到设备(可能使用cudaMemcpy)
  2. 启动一个在设备上运行的函数(__global__或"内核"函数)来处理数据并创建结果
  3. 从设备复制结果返回(也许,再次使用cudaMemcpy)

因此,一个简单的方法是:

  1. 消除cudaMalloc/cudaMemcpy操作,从而将感兴趣的数据以原始形式留在主机
  2. 上。
  3. 将cuda处理函数(内核)转换为对主机数据执行相同操作的普通c++函数

由于CUDA是一个并行处理架构,将固有的并行CUDA"内核"代码转换为普通c++代码(上面的第2步)的一种方法是使用一个循环或一组循环。但除此之外,路线图往往会变得相当分歧,这取决于代码实际在做什么。此外,线程间通信、非转换算法(如缩减)以及CUDA内在特性或其他语言特定功能的使用将使第2步变得相当复杂。

例如,让我们拿一个非常简单的向量ADD代码。CUDA内核代码将通过一些特征来区分,这些特征将使转换到CUDA实现或从CUDA实现转换变得容易:
  1. 没有线程间通信。问题是"令人尴尬的平行"。每个线程所做的工作独立于所有其他线程。这只描述了CUDA代码的一个有限子集。

  2. 不需要或使用任何CUDA特定的语言特性或内在特性(除了全局唯一的线程索引变量),因此内核代码几乎可以识别为完全有效的c++代码。同样,这个特征可能只描述了CUDA代码的有限子集。

所以CUDA版本的矢量添加代码可能看起来像这样(为了表示目的而大大简化):

#include <stdio.h>
#define N 512
// perform c = a + b vector add
__global__ void vector_add(const float *a, const float *b, float *c){
int idx = threadIdx.x;
c[idx]=a[idx]+b[idx];
}
int main(){
float a[N] = {1};
float b[N] = {2};
float c[N] = {0};
float *d_a, *d_b, *d_c;
int dsize = N*sizeof(float);
cudaMalloc(&d_a, dsize); // step 1 of CUDA processing sequence
cudaMalloc(&d_b, dsize);
cudaMalloc(&d_c, dsize);
cudaMemcpy(d_a, a, dsize, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, b, dsize, cudaMemcpyHostToDevice);
vector_add<<<1,N>>>(d_a, d_b, d_c); // step 2
cudaMemcpy(c, d_c, dsize, cudaMemcpyDeviceToHost); // step 3
for (int i = 0; i < N; i++) if (c[i] != a[i]+b[i]) {printf("Fail!n"); return 1;}
printf("Success!n");
return 0;
}

我们看到上面的代码遵循典型的CUDA处理顺序1-2-3,并且在注释中标记了每个步骤的开始。所以我们的"de-CUDA-fy"路线图是:

  1. 消除cudaMalloc/cudaMemcpy操作,从而将感兴趣的数据保留在主机
  2. 将cuda处理函数(内核)转换为对主机数据执行相同操作的普通c++函数

对于步骤1,我们将直接删除cudaMalloccudaMemcpy行,而我们将计划直接操作主机代码中的a[],b[]c[]变量。然后,剩下的步骤是将vector_addCUDA"内核"函数转换为普通的c++函数。同样,一些CUDA基础知识对于理解并行执行的操作的程度是必要的。但是内核代码本身(除了使用threadIdx.x内置CUDA变量)是完全有效的c++代码,并且没有线程间通信或其他复杂因素。因此,一个普通的c++实现可以只是内核代码,放入一个合适的for循环中迭代并行范围(在本例中为N),并放入一个可比较的c++函数中:

void vector_add(const float *a, const float *b, float *c){
for (int idx=0; idx < N; idx++)
c[idx]=a[idx]+b[idx];
}
结合以上步骤,我们需要(在这个简单的例子中):
  1. 删除cudaMalloccudaMemcpy操作
  2. 将cuda内核代码替换为类似的普通c++函数
  3. main中的内核调用修正为普通的c++函数调用

我们得到:

#include <stdio.h>
#define N 512
// perform c = a + b vector add
void vector_add(const float *a, const float *b, float *c){
for (int idx = 0; idx < N; idx++)
c[idx]=a[idx]+b[idx];
}
int main(){
float a[N] = {1};
float b[N] = {2};
float c[N] = {0};
vector_add(a, b, c);
for (int i = 0; i < N; i++) if (c[i] != a[i]+b[i]) {printf("Fail!n"); return 1;}
printf("Success!n");
return 0;
}

通过这个示例的重点并不是建议这个过程通常会如此简单。但希望它是明显的,这个过程不是一个纯粹的机械的,而是依赖于一些CUDA的知识,也需要一些实际的代码重构;这不是简单地通过更改文件扩展名和修改几个函数调用来完成的。

其他注释:

  1. 许多笔记本电脑都有cuda功能(即NVIDIA) gpu。如果你有其中一个(我意识到你没有,但我把它包括在其他人可能读到这篇文章),你可能可以运行CUDA代码。

  2. 如果你有一台可用的台式电脑,很可能不到100美元,你就可以添加一个支持cuda的GPU。

  3. 尝试利用仿真技术IMO不是这里的方式,除非您可以在交钥匙方式中使用它。在我看来,将模拟器中的点点滴滴拼凑到您自己的应用程序中是一项非常重要的工作。

  4. 我相信在一般情况下,将CUDA代码转换为相应的OpenCL代码也不是微不足道的。(这里的动机是CUDA和OpenCL之间有很多相似之处,OpenCL代码可能会在您的笔记本电脑上运行,因为OpenCL代码通常可以在各种目标上运行,包括cpu和gpu)。这两种技术之间有足够的差异,需要一些努力,这带来了额外的负担,需要对OpenCL和CUDA有一定程度的熟悉,你的问题的重点似乎是想要避免这些学习曲线。