静态 openCL 类未在使用 boost.python 的 python 模块中正确发布

static openCL class not properly released in python module using boost.python

本文关键字:python 模块 boost openCL 静态      更新时间:2023-10-16

编辑:好的,所有的编辑都使问题的布局有点混乱,所以我将尝试重写问题(不改变内容,而是改进其结构)。

简而言之,问题

我有一个 openCL 程序,如果我将其编译为可执行文件,则可以正常工作。现在我尝试使用 boost.python 从 Python 调用它。但是,一旦我退出 Python(导入我的模块后),python 就会崩溃。

原因似乎与

程序终止时仅静态存储 GPU 命令队列及其释放机制

MWE和设置

设置

  • 使用的IDE:Visual Studio 2015

  • 使用的操作系统:视窗 7 64 位

  • 蟒蛇版本:3.5

  • AMD OpenCL APP 3.0 标头

  • 按照此处的建议直接从 Khronos cl2.hpp:空的 openCL 程序抛出弃用警告

  • 此外,我有一个带有集成图形硬件的英特尔CPU,没有其他专用显卡

  • 我使用编译为 64 位版本的 1.60 版提升库

  • 我使用的提升 dll 称为:boost_python-vc140-mt-1_60.dll

  • 没有python的openCL程序工作正常

  • 没有 openCL 的 python 模块工作正常

兆威

#include <vector>
#define CL_HPP_ENABLE_EXCEPTIONS
#define CL_HPP_TARGET_OPENCL_VERSION 200
#define CL_HPP_MINIMUM_OPENCL_VERSION 200 // I have the same issue for 100 and 110
#include "cl2.hpp"
#include <boost/python.hpp>
using namespace std;
class TestClass
{
private:
    std::vector<cl::CommandQueue> queues;
    TestClass();
public:
    static const TestClass& getInstance()
    {
        static TestClass instance;
        return instance;
    }
};
TestClass::TestClass()
{
    std::vector<cl::Device> devices;
    vector<cl::Platform> platforms;
    cl::Platform::get(&platforms);
    //remove non 2.0 platforms (as suggested by doqtor)
    platforms.erase(
        std::remove_if(platforms.begin(), platforms.end(),
            [](const cl::Platform& platform)
    {
        int v = cl::detail::getPlatformVersion(platform());
        short version_major = v >> 16;
        return !(version_major >= 2);
    }),
        platforms.end());
    //Get all available GPUs
    for (const cl::Platform& pl : platforms)
    {
        vector<cl::Device> plDevices;
        try {
            pl.getDevices(CL_DEVICE_TYPE_GPU, &plDevices);
        }
        catch (cl::Error&)
        {
            // Doesn't matter. No GPU is available on the current machine for 
            // this platform. Just check afterwards, that you have at least one
            // device
            continue;
        }       
        devices.insert(end(devices), begin(plDevices), end(plDevices));
    }
    cl::Context context(devices[0]);
    cl::CommandQueue queue(context, devices[0]);
    queues.push_back(queue);
}
int main()
{
    TestClass::getInstance();
    return 0;
}
BOOST_PYTHON_MODULE(FrameWork)
{
    TestClass::getInstance();
}

呼叫程序

因此,在将程序编译为dll后,我启动python并运行以下程序

import FrameWork
exit()

虽然导入工作没有问题,但 python 在 exit() 上崩溃。所以我点击调试,Visual Studio告诉我以下代码部分(cl2.hpp)中有一个异常:

template <>
struct ReferenceHandler<cl_command_queue>
{
    static cl_int retain(cl_command_queue queue)
    { return ::clRetainCommandQueue(queue); }
    static cl_int release(cl_command_queue queue)  //  --  HERE  --
    { return ::clReleaseCommandQueue(queue); }
};

如果您将上述代码编译为简单的可执行文件,它可以正常工作。如果满足以下条件之一,则代码也有效:

  • CL_DEVICE_TYPE_GPU替换为CL_DEVICE_TYPE_ALL

  • 删除queues.push_back(queue)

问题

那么可能是什么

原因,可能的解决方案是什么?我怀疑这与我的测试类是静态的事实有关,但由于它适用于可执行文件,我不知是什么导致了它。

我过去遇到过类似的问题。

OpenCL1.2 支持clRetain*功能。在您的情况下,当为第一个GPU平台获取设备(platforms[0].getDevices(...)用于CL_DEVICE_TYPE_GPU)时,它必须恰好是OpenCL1.2前的平台,因此您会遇到崩溃。当获得任何类型的设备(GPU/CPU/...)时,您的第一个平台更改为OpenCL1.2+,一切都很好。

要修复问题集,请执行以下操作:

#define CL_HPP_MINIMUM_OPENCL_VERSION 110

这将确保不会针对不受支持的平台(OpenCL 1.2 之前)调用clRetain*


更新:我认为cl2.hpp中存在一个错误,尽管将最低 OpenCL 版本设置为 1.1,但在创建命令队列时,它仍然尝试在预OpenCL1.2设备上使用clRetain*。将最低 OpenCL 版本设置为 110 和版本过滤对我来说效果很好。

完整的工作示例:

#include "stdafx.h"
#include <vector>
#define CL_HPP_ENABLE_EXCEPTIONS
#define CL_HPP_TARGET_OPENCL_VERSION 200
#define CL_HPP_MINIMUM_OPENCL_VERSION 110
#include <CL/cl2.hpp>
using namespace std;
class TestClass
{
private:
    std::vector<cl::CommandQueue> queues;
    TestClass();
public:
    static const TestClass& getInstance()
    {
        static TestClass instance;
        return instance;
    }
};
TestClass::TestClass()
{
    std::vector<cl::Device> devices;
    vector<cl::Platform> platforms;
    cl::Platform::get(&platforms);
    size_t x = 0;
    for (; x < platforms.size(); ++x)
    {
        cl::Platform &p = platforms[x];
        int v = cl::detail::getPlatformVersion(p());
        short version_major = v >> 16;
        if (version_major >= 2) // OpenCL 2.x
            break;
    }
    if (x == platforms.size())
        return; // no OpenCL 2.0 platform available
    platforms[x].getDevices(CL_DEVICE_TYPE_GPU, &devices); 
    cl::Context context(devices);
    cl::CommandQueue queue(context, devices[0]);
    queues.push_back(queue); 
}
int main()
{
    TestClass::getInstance();
    return 0;
}

更新2

那么可能是什么

原因,可能的解决方案是什么? 我怀疑这与我的测试类是 静态的,但由于它适用于可执行文件,我不知所措 导致它。

TestClass 静态似乎是一个原因。看起来从 python 运行时释放内存的顺序错误。为了解决这个问题,您可能需要添加一个方法,在python开始释放内存之前,必须显式调用该方法以释放opencl对象。

static TestClass& getInstance() // <- const removed
{
    static TestClass instance;
    return instance;
}
void release()
{
    queues.clear();
}
BOOST_PYTHON_MODULE(FrameWork)
{
    TestClass::getInstance();
    TestClass::getInstance().release();
}

"我希望得到一个答案,向我解释问题到底是什么,以及是否有办法解决它。

首先,我要说的是,doqtor 已经回答了如何解决这个问题——通过确保所有已用 OpenCL 资源的销毁时间定义明确。IMO,这不是"黑客",而是正确的做法。试图依靠静态初始化/清理魔法来做正确的事情 - 并且看着它失败 - 是真正的黑客!

第二,关于这个问题的一些想法:实际问题甚至比常见的静态初始化顺序惨败故事还要复杂。它涉及DLL加载/卸载顺序,既与python在运行时加载自定义dll有关,也与OpenCL的可安装客户端驱动程序(ICD)模型有关。

运行使用 OpenCL 的应用程序/DLL 时涉及哪些 DLL?对于应用程序,唯一相关的 DLL 是链接所针对的opencl.dll。它在应用程序启动期间(或者当需要 opencl 的自定义 DLL 在 python 中动态加载时)加载到进程内存中。然后,当你第一次在代码中调用clGetPlatformInfo()或类似内容时,ICD逻辑就会启动:opencl.dll将查找已安装的驱动程序(在Windows中,这些驱动程序在注册表中的某处提到)并动态加载它们各自的dll(使用sth,如LoadLibrary()系统调用)。例如,这可能是 nvopencl.dll适用于 NVIDIA,或适用于您已安装的英特尔驱动程序的其他一些 DLL。现在,与相对简单的opencl.dll相比,这个ICD dll可以而且将具有许多依赖关系 - 可能使用Intel IPP,TBB或其他任何东西。所以到现在为止,事情已经变得非常混乱了。

现在,在关闭期间,Windows 加载程序必须决定以哪种顺序卸载哪些 dll。在单个可执行文件中编译示例时,加载/卸载的 dll 的数量和顺序肯定与"python 在运行时加载自定义 dll"方案中不同。这很可能是为什么您仅在后一种情况下遇到问题的原因,并且只有在自定义 dll 关闭期间仍然有一个 opencl-context+命令队列处于活动状态时。队列的销毁(通过 clRelease 触发...在 TestClass 实例的静态销毁期间)委托给英特尔-ICD-DLL,因此此时此 DLL 必须仍能完全正常运行。如果由于某种原因不是这种情况(可能是因为加载程序选择卸载它或它需要的 dll 之一),您会崩溃。

这个思路让我想起了这篇文章:

https://blogs.msdn.microsoft.com/larryosterman/2004/06/10/dll_process_detach-is-the-last-thing-my-dlls-going-to-see-right/

有一段话谈到了"COM对象",它可能同样适用于"OpenCL资源":

"因此,请考虑以下情况:您有一个DLL,该DLL在其生存期中的某个时刻实例化COM对象。 如果该 DLL 在全局变量中保留对 COM 对象的引用,并且在DLL_PROCESS_DETACH之前不释放 COM 对象,则实现 COM 对象的 DLL 将在 COM 对象的生存期内保留在内存中。 实际上,实现 COM 对象的 DLL 已依赖于保存对 COM 对象的引用的 DLL。 但是加载程序无法知道这种依赖关系。 它只知道DLL被加载到内存中。


现在,我写了很多字,但没有明确的证据证明到底出了什么问题。我从这样的错误中学到的主要教训是:不要进入那个蛇坑,像 doqtor 建议的那样在一个定义明确的地方进行资源清理。晚安。