【问题标题】:cuda call fails in destructorcuda 调用在析构函数中失败
【发布时间】:2016-05-29 03:32:41
【问题描述】:

我是 CUDA 新手,在使用 CUDA 编写单例/全局变量时遇到了问题。单例分配一些 cuda 内存并尝试在析构函数中释放它。但是,析构函数崩溃,cudaError 为 29“驱动程序关闭”。

通过一些搜索,我注意到原因可能是在程序退出后调用了单例析构函数,此时 CUDA 已经关闭。

https://github.com/NVlabs/SASSI/issues/4 当在静态成员的析构函数中调用 cuda 函数时,此链接报告了类似的问题。

https://devtalk.nvidia.com/default/topic/457922/cudafree-crash-in-destructor-when-exit-is-called/ 此链接报告相同的问题和不明确的解决方案。

老实说,我没有太多 CUDA 知识,所以我想针对这个问题寻求一些详细的解释和正式的解决方案。

编辑:

感谢@Robert Crovella 的提醒,我做了一些测试来重现该问题。好的,我发现这个问题发生在 std::unordered_map 或 std::map 的单例变量和全局变量中,它们在其值对象的析构函数中调用 cuda。

工作代码,不使用 std::map:

#include <iostream>
#include <map>

#define CUDA_CHECK(x) std::cerr << (x) << std::endl;

class cuda_user
{   
    char* data;
public:
    cuda_user() {
        std::cerr << "constr" << std::endl;
        CUDA_CHECK(cudaMalloc((void**)&data, 1024));
    }
    void foo() {
        std::cerr << "foo" << std::endl;
    };
    ~cuda_user() {
        std::cerr << "destr"  << std::endl;
        CUDA_CHECK(cudaFree(data));
    }
};

cuda_user cu;
int main()
{   
    cu.foo();
}

输出:

constr
0
foo
destr
0

崩溃的代码,具有相同的 cuda_user 类,但使用了 std::map:

#include <iostream>
#include <map>

#define CUDA_CHECK(x) std::cerr << (x) << std::endl;

class cuda_user
{   
    char* data;
public:
    cuda_user() {
        std::cerr << "constr" << std::endl;
        CUDA_CHECK(cudaMalloc((void**)&data, 1024));
    }
    void foo() {
        std::cerr << "foo" << std::endl;
    };
    ~cuda_user() {
        std::cerr << "destr"  << std::endl;
        CUDA_CHECK(cudaFree(data));
    }
};

std::map<int, cuda_user> map;
int main()
{   
    map[1].foo();
}

输出:

constr
0
foo
destr
29 << Error!

更新:

我在 CentOS 6.3 上使用 gcc48 和 nvcc75

【问题讨论】:

  • 在你离开 main 之前清理你的单例:int main() { { SingletonController singcont; /* .... */ } return 0; } 如果你有线程,你还应该确保它们在你离开 main 的内部范围块之前结束。然后,SingletonController 的析构函数将不会与其他线程竞争,并且您将完全关闭。当然,大多数做单例的人,不会那样做......
  • 对 CUDA 一无所知,但是在操作系统消除您的进程并释放所有内存之前,您需要释放任何内存的特殊原因吗?我在这里感觉到对“内存泄漏”的毫无根据的恐惧,但也许使用 CUDA 情况会有所不同。
  • 单身人士是出了名的棘手,至少有two canonical implementations that have significant differences。你能举一个简单的例子来说明你正在使用什么吗?我怀疑您的确切实施可能会影响@BitTickler 建议的方法的可行性。如果你不打算使用“可清理”的单例设计模式,那么 Christian Hackl 建议的方法可能是最简单的:从析构函数中消除 cuda 调用。
  • 将 CUDA 调用放置在 main 范围之外的全局对象中将导致有问题的行为。见here。尽管该描述主要集中在此类/对象中的内核调用上,但正如您所发现的,危险适用于任何 CUDA 调用。我建议您的问题可以说是该问题的重复,至少就您要求描述正在发生的事情而言,并且现在您的示例没有任何单例字符。
  • @BoLi:是的,很抱歉术语错误。当您使用 CUDA 运行时 API 时,CUDA 前端会默默地发出许多样板支持代码,这些代码与您的翻译单元范围映射/单例/对象具有相同的范围。您的代码是否可以工作取决于对象实例化和销毁的特定序列是否意味着当您的类调用包含运行时 API 调用的构造函数/析构函数时仍然存在活动的 CUDA 上下文。语言中没有定义对象实例化和销毁的顺序——这实际上依赖于 UB。

标签: c++ stl cuda singleton


【解决方案1】:

[将 cmets 扩展为摘要答案]

您的代码在不知不觉中依赖于未定义的行为(翻译单元对象的销毁顺序),除了显式控制在其析构函数中包含 CUDA 运行时 API 调用的对象和对象的生命周期之外,没有真正的解决方法,或者只是避免使用那些完全在析构函数中调用 API。

详细说明:

nvcc 调用的 CUDA 前端默默地添加了许多样板代码和翻译单元范围对象,这些对象执行 CUDA 上下文设置和拆卸。该代码必须在任何依赖于 CUDA 上下文的 API 调用可以执行之前运行。如果在其析构函数中包含 CUDA 运行时 API 调用的对象在上下文被拆除后调用 API,则您的代码可能会因运行时错误而失败。当对象超出范围时,C++ 没有定义销毁顺序。您的单例或对象需要在 CUDA 上下文被拆除之前被销毁,但不能保证会发生。这实际上是未定义的行为。

您可以在answer 中看到更完整的示例(在内核启动的上下文中)。

【讨论】:

    猜你喜欢
    • 2011-01-21
    • 1970-01-01
    • 2019-10-31
    • 1970-01-01
    • 2013-10-09
    • 2011-12-06
    • 2023-02-06
    • 2021-06-12
    相关资源
    最近更新 更多