如何设计一个仅在其中一个部分使用 CUDA 的库,以便其他部分在没有安装 CUDA 的情况下也可以工作?

Posted

技术标签:

【中文标题】如何设计一个仅在其中一个部分使用 CUDA 的库,以便其他部分在没有安装 CUDA 的情况下也可以工作?【英文标题】:How to design a library which uses CUDA only in its one part so that other parts also work without CUDA installed? 【发布时间】:2021-12-02 20:21:02 【问题描述】:

假设我们正在开发一个 C++ 库,其中包含多个函数,用于对某些数据执行多个操作,例如。 SumArraySquareElementsAddVectors。它被编译成一个 C++ 库,可以在其他程序中正常使用。

然后我们添加一个函数MatrixMultiply。因为这是 GPU 加速的完美目标,所以我们还添加了一个函数 MatrixMultiplyCuda,它在内部调用了一些 CUDA 内核。

所以现在整个库都需要 CUDA,即使库的用户从未使用过MatrixMultiplyCuda 函数。

那么,问题是:有没有办法让更新的库即使在没有 CUDA 的系统上也能正常工作?有没有处理类似问题的库? 显然,如果没有 CUDA,MatrixMultiplyCuda 函数将无法工作,这很好。

我目前的解决方案是有一个宏MYLIB_USE_CUDA来保护所有CUDA特定的代码和函数,这样只有在定义了宏MYLIB_USE_CUDA时才使用它们,如果没有定义宏,则排除代码.我使用 CMake 编译库,如果将标志 -DMYLIB_USE_CUDA 传递给 CMake,则在编译过程中定义宏并链接 CUDA 库。

但是,我不太喜欢这种解决方案,因为如果要在其他代码中使用该库,如果要使用 CUDA 特定的函数,则仍然必须定义宏 MYLIB_USE_CUDA(因为头文件)使用,使库的使用复杂化。

这不仅必须是CUDA,任何其他库的问题都是一样的,但是当库很小时,这无关紧要。人们不想因为我的库而安装数 GB 的 CUDA,如果他们甚至不打算使用 CUDA 功能的话。

【问题讨论】:

"如果在其他代码中使用该库,如果要使用 CUDA 特定的函数,则仍然必须定义宏 MYLIB_USE_CUDA(因为头文件),这使得使用图书馆。” - 如果其他代码使用您的库,调用find_package(your_library_package REQUIRED) 并链接到相应的IMPORTED 目标就足够了。导入的目标本身关心设置(或不设置)MYLIB_USE_CUDA 标志。 是的,我还不太擅长 CMake,我需要进一步研究 find_package 的工作原理以及它可以做什么。但这个建议乍一看似乎不错 【参考方案1】:

您不需要宏。使用 CMake(或您可能使用的任何其他工具)您可以简单地做

find_package(CUDA QUIET)  // detect is CUDA is installed in the system
if(CUDA_FOUND)
  // add MatrixMultiplyCuda.cpp to your list of sources
else()
  // add MatrixMultiplNormal.cpp to your list of sources
endif()

并且在您的代码中使用一个公共标头(比如MatrixMultiply.hpp),其矩阵函数签名独立于 CUDA(例如Matrix multiply(const Matrix& lhs, const Matrix& rhs);)。根据正在编译的源文件,您的库将使用一种实现或另一种实现。

【讨论】:

是的,这很聪明,但我希望正常的 CPU 实现始终存在,并且除此之外还添加 CUDA 作为奖励 Oki,然后始终将MatrixMultiplNormal.cpp 包含到您的源中,并在标题MatrixMultiply.hpp 中还包含仅限CUDA 的函数签名,但如果系统中未检测到CUDA,请添加MatrixMultiplNoimpl.cpp到消息来源。它应该包含 CUDA-only 函数的空实现,你可以在里面例如throw std::runtime_error("CUDA functions not supported on your system"); 这看起来很不错。但是如果函数签名依赖于 CUDA 呢?例如。 cudaError_t MatrixMultiplyCuda(Matrix &m1, Matrix &m2, Matrix &m, cudaStream_t stream);?从设计的角度忽略它是否有意义。 好吧,那你有问题,不能使用这个解决方案。但这是一个糟糕的设计。 “无 cuda”有两种模式,您的软件必须在正确的层处理这两种模式。编译时和运行时。运行时:在使用 CUDA 构建的完整库中,您应该能够使用 CUDA 实现或 CPU 实现运行,具体取决于 GPU 可用性和用户覆盖的运行时检测。这意味着库的客户端无论如何都无法更改其 API,因此 CUDA 类型不应成为 API 的一部分。编译时:禁用任何引用 CUDA 的部分的构建,并对非 CUDA 运行时路径进行硬编码。【参考方案2】:

您可以通过将所有动态链接和符号查找工作从链接时转移到运行时来实现。现在,这完全依赖于平台,但在 POSIX 系统上,以下工作:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <cuda_runtime.h>

int main() 
    typedef cudaError_t (*cudaMalloc_t) ( void** devPtr, size_t size );

    const char* cuda_rt_dll_filename = find_the_cuda_so_somehow();
        // e.g. on my system it's 
        // "/usr/local/cuda-11.4.1/targets/x86_64-linux/lib/libcudart.so" 
    void *cuda_rt_dll = dlopen(cuda_rt_dll_filename , RTLD_NOW);
    if (cuda_rt_dll == NULL) 
        fprintf(stderr, "Failed opening %s\n", cuda_rt_dll_filename);
        exit(EXIT_FAILURE);
    
    cudaMalloc_t cudaMalloc_ = dlsym(cuda_rt_dll, "cudaMalloc");
    void* device_buffer;
    cudaError_t ret = cudaMalloc_(&device_buffer, 1000);

    // error-checking of ret here

    // Do stuff with the device buffer,
    // e.g. with more dynamically-loaded functions 

    dlclose(cuda_rt_dll);

显然,您可以在另一个函数中而不是在main() 中执行此操作,然后您的程序的其余部分可以完全忽略 CUDA;而不是退出程序,您可以返回一个指示失败的错误代码。

您可以做的其他事情是为您使用的每个 CUDA 函数编写存根,仅在执行动态加载的对象之外可见,并且与原始 CUDA 函数名称具有相同的名称。 CUDA API 函数f 的存根将执行以下操作

检查程序是否已经动态加载了f(到一些f_函数指针中)。 如果没有,请尝试加载它。 如果加载失败,放弃并返回一些 CUDA 失败代码。 如果加载成功,使用提供给f 的参数调用f_ 并返回结果。

PS:

关于共享对象/DLL 的运行时加载,另请参见:Get DLL path at runtime 请记住,CUDA 的运行时 API 也有一些 C++ 函数。要加载这些,您需要考虑名称修改(请参阅here 或on Wikipedia)。

【讨论】:

这似乎太复杂了。如果没有安装 CUDA,编译器不会抱怨找不到 cuda_runtime.h 标头吗? @JakubHomola:CUDA 会在您编译时安装 - 但您的用户不需要安装 CUDA。如果您想在编译时支持缺少 CUDA,那么您可以使用 #if defined HAVE_CUDA 之类的东西,并构建系统检查等。另外,是的,这有点复杂,但它只复杂一次,即对于 API 中的所有内容,它都是相同的“技巧”。【参考方案3】:

所以,经过与同事的思考和讨论,我是这样解决的(发一个简化版,实际代码要复杂得多)。

mymath.h:

#pragma once
#include "MatrixMultiply.h"
// ...

#ifdef MYMATH_USE_CUDA
#include "MatrixMultiplyCUDA.h"
#endif

mymath_cuda.h:

#pragma once
#ifndef MYMATH_USE_CUDA
#define MYMATH_USE_CUDA
#endif

#include "mymath.h"

MatrixMultiplyCUDA.h 标头中声明了使用 CUDA 进行操作的函数,MatrixMultiplyCUDA.cu 中是实现。对于纯 CPU 版本,MatrixMultiply.h 与此类似。

如果有人想使用库中经典的纯 CPU 部分,他们#include "mymath.h"。如果有人想使用使用 CUDA 的 附加 GPU 加速功能,他们#include "mymath_cuda.h"

然后,CMakeLists.txt:

cmake_minimum_required(VERSION 3.18)
project(mymath)

set(MYMATH_USE_CUDA OFF)
find_package(CUDA QUIET)
if(CUDA_FOUND)
    set(MYMATH_USE_CUDA ON)
    enable_language(CUDA)
endif()

set(mymath_SOURCES src/MatrixMultiply.cpp src/otherstuff.cpp)
set(mymath_cuda_SOURCES src/MatrixMultiplyCUDA.cu)

add_library(mymath STATIC $mymath_SOURCES)
target_include_directories(mymath PUBLIC $CMAKE_CURRENT_SOURCE_DIR/include)

if(MYMATH_USE_CUDA)
    add_library(mymath_cuda STATIC $mymath_cuda_SOURCES)
    target_include_directories(mymath_cuda PUBLIC $CMAKE_CURRENT_SOURCE_DIR/include)
    target_include_directories(mymath_cuda SYSTEM PUBLIC $CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES)
    target_link_libraries(mymath_cuda PUBLIC mymath $CUDA_LIBRARIES)
    target_compile_definitions(mymath_cuda PUBLIC MYMATH_USE_CUDA)
endif()

如果找到 CUDA,则将 mymath_cuda 库及其所有依赖项添加到编译中,包括 mymath

如果用户只使用mymath库,只链接就足够了,但如果使用CUDA加速功能,则需要链接mymathmymath_cuda这两个库。

基本上它是扩展第一个库的功能的另一个库,但头文件相互连接在一起。它们不必是,mymath_cuda.h 可以包含所有 cuda 特定的标头本身,而不依赖于 mymath.h#ifdefs,但这是我们做出的设计选择。

同样,这不是实际代码,因此它可能包含拼写错误或部分不完整/不正确,但基本原理是希望可见的。

【讨论】:

以上是关于如何设计一个仅在其中一个部分使用 CUDA 的库,以便其他部分在没有安装 CUDA 的情况下也可以工作?的主要内容,如果未能解决你的问题,请参考以下文章

cuda并行程序设计 gpu编程指南

设计模式 - 如何仅在某些情况下强制执行对象属性(构建器模式,依赖注入)

mtensor一个tensor计算库,支持cuda延迟计算

vs2017+opencv+qt+cuda,使用cmake编译opencv的库

NextJS 是不是包含仅在包中的 getServerSideProps 中引用的库?

OnClick on div 元素仅在其中一部分起作用