在一般情况下,我认为没有“de-CUDA-fy”应用程序的具体路线图。正如我不认为有一个特定的“机械”路线图来“CUDA-fy”一个应用程序,我也没有找到针对一般编程问题的特定路线图。
此外,我认为提议的路线图存在缺陷。仅举一个例子,.cu 文件通常具有特定于 CUDA 的引用,用于编译.cpp 代码的普通 c++ 编译器不会容忍这些引用。其中一些引用可能是依赖于 CUDA 运行时 API 的项目,例如 cudaMalloc 和 cudaMemcpy,虽然这些可以通过普通的 c++ 编译器进行传递(它们只是库调用),但这并不明智为删除了 CUDA 字符的应用程序保留这些内容。此外,一些参考可能是 CUDA 特定的语言功能,例如通过__global__ 或__device__ 声明设备代码或使用相应语法<<<...>>> 启动设备“内核”函数。这些不能通过普通的c++编译器,必须专门处理。此外,简单地删除那些 CUDA 关键字和语法不太可能产生有用的结果。
简而言之,代码必须重构;没有合理简洁的路线图来解释这样做的或多或少的机械过程。我建议重构过程的复杂性与将代码的非 CUDA 版本转换为 CUDA 版本的原始过程(如果有的话)大致相同。为了理解 CUDA 结构,至少需要一些 CUDA 编程的非机械知识。
对于非常简单的 CUDA 代码,可能会布置一个有点机械的过程来 de-CUDA-fy 代码。回顾一下,基本的CUDA处理顺序如下:
- 为设备上的数据分配空间(可能使用
cudaMalloc)并将数据复制到设备(可能使用cudaMemcpy)
- 启动在设备上运行的函数(
__global__ 或“内核”函数)以处理数据并创建结果
- 将结果从设备中复制回来(可能再次使用
cudaMemcpy)
因此,一个简单的方法是:
- 消除
cudaMalloc/cudaMemcpy 操作,从而将感兴趣的数据以原始形式保留在主机上
- 将 cuda 处理函数(内核)转换为普通的 c++ 函数,对主机数据执行相同的操作
由于 CUDA 是一种并行处理架构,将固有的并行 CUDA“内核”代码转换为普通 c++ 代码(上面的步骤 2)的一种方法是使用一个循环或一组循环。但除此之外,路线图往往会变得非常不同,具体取决于代码实际在做什么。此外,线程间通信、非转换算法(如归约)以及使用 CUDA 内在函数或其他语言特定功能将使第 2 步变得相当复杂。
例如,让我们以一个非常简单的向量添加代码为例。用于此的 CUDA 内核代码将通过许多特征来区分,这些特征可以很容易地转换为 CUDA 实现或从 CUDA 实现转换:
没有线程间通信。问题是“令人尴尬的并行”。每个线程完成的工作独立于所有其他线程。这仅描述了有限的 CUDA 代码子集。
不需要或使用任何 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 并且每个步骤的开始都在 cmets 中标记。所以我们的“de-CUDA-fy”路线图再次:
- 消除
cudaMalloc/cudaMemcpy 操作,从而将感兴趣的数据以其原始形式保留在主机上
- 将 cuda 处理函数(内核)转换为普通的 c++ 函数,对主机数据执行相同的操作
对于第 1 步,我们将直接删除 cudaMalloc 和 cudaMemcpy 行,而是计划直接对主机代码中的 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];
}
结合以上步骤,我们需要(在这个简单的例子中):
- 删除
cudaMalloc 和cudaMemcpy 操作
- 用类似的普通 c++ 函数替换 cuda 内核代码
- 将
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 知识,并且还需要一些实际的代码重构;这不能简单地通过更改文件扩展名和修改一些函数调用来完成。
其他几个cmets:
许多笔记本电脑都配备了支持 CUDA(即 NVIDIA)的 GPU。如果您有其中之一(我知道 您 没有,但我将其包括给可能阅读此内容的其他人),您可能可以在其上运行 CUDA 代码。
如果您有一台可用的台式 PC,您很可能只需不到 100 美元就可以为其添加支持 CUDA 的 GPU。
尝试利用仿真技术 IMO 不是可行的方法,除非您可以以交钥匙方式使用它。在我看来,将模拟器中的点点滴滴拼凑到你自己的应用程序中是一项非常重要的练习。
我相信在一般情况下,将 CUDA 代码转换为相应的 OpenCL 代码也并非易事。 (这里的动机是 CUDA 和 OpenCL 之间有很多相似之处,并且 OpenCL 代码可能可以在您的笔记本电脑上运行,因为 OpenCL 代码通常可以在各种目标上运行,包括 CPU 和 GPU)。这两种技术之间存在足够的差异,因此需要付出一些努力,这带来了额外的负担,需要对 OpenCL 和 CUDA 有一定程度的熟悉程度,而您的问题的主旨似乎是想要避免那些学习曲线。