【问题标题】:remove CUDA dependency?删除 CUDA 依赖项?
【发布时间】:2016-01-03 04:43:07
【问题描述】:

我感兴趣的开源 C++/Qt 应用依赖于 CUDA。我的 macbook pro(2014 年中)有现货 Intel Iris Pro,没有 NVidia 显卡。自然,预构建的应用程序不会运行。

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

我有源代码 - 我可以用 c++ 等效项替换 CUDA 依赖项,代价是处理速度变慢吗?我希望有类似的东西

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

但恐怕没那么简单。我想在开始之前进行健全性检查。

【问题讨论】:

  • 没那么简单。或者理智。
  • 我怀疑它会简单。这个问题本质上与“我如何将此代码移植到 CUDA?”相反。您必须对 CUDA 有一定的了解才能执行此操作 - 比您在此问题中显示的更多知识。 .cu.cpp 文件中都可以包含 CUDA 引用,您必须对其进行重构。大概.cu 文件包含 CUDA 设备代码。这些必须重写为 C/C++ 函数。简单的 CUDA 内核可以通过围绕内核代码的一组循环来重写,但没有通用的路线图。
  • 谢谢@RobertCrovella——我很害怕。如果您重新发布您的评论作为答案,我会接受。
  • 我不知道您对自己的时间投入有多少重视,但我认为花 200 美元左右购买 Jetson TK1 比尝试免费破解 CUDA 更具成本效益一些第三方代码的端口。

标签: c++ macos cuda


【解决方案1】:

在一般情况下,我认为没有“de-CUDA-fy”应用程序的具体路线图。正如我不认为有一个特定的“机械”路线图来“CUDA-fy”一个应用程序,我也没有找到针对一般编程问题的特定路线图。

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

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

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

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

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

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

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

例如,让我们以一个非常简单的向量添加代码为例。用于此的 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 并且每个步骤的开始都在 cmets 中标记。所以我们的“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. 用类似的普通 c++ 函数替换 cuda 内核代码
  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 知识,并且还需要一些实际的代码重构;这不能简单地通过更改文件扩展名和修改一些函数调用来完成。

其他几个cmets:

  1. 许多笔记本电脑都配备了支持 CUDA(即 NVIDIA)的 GPU。如果您有其中之一(我知道 没有,但我将其包括给可能阅读此内容的其他人),您可能可以在其上运行 CUDA 代码。

  2. 如果您有一台可用的台式 PC,您很可能只需不到 100 美元就可以为其添加支持 CUDA 的 GPU。

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

  4. 我相信在一般情况下,将 CUDA 代码转换为相应的 OpenCL 代码也并非易事。 (这里的动机是 CUDA 和 OpenCL 之间有很多相似之处,并且 OpenCL 代码可能可以在您的笔记本电脑上运行,因为 OpenCL 代码通常可以在各种目标上运行,包括 CPU 和 GPU)。这两种技术之间存在足够的差异,因此需要付出一些努力,这带来了额外的负担,需要对 OpenCL 和 CUDA 有一定程度的熟悉程度,而您的问题的主旨似乎是想要避免那些学习曲线。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-11-13
    • 2015-06-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多