Подтвердить что ты не робот

Статический класс openCL, который не был правильно выпущен в модуле python, используя boost.python

EDIT: Хорошо, все изменения сделали макет вопроса немного запутанным, поэтому я попытаюсь переписать вопрос (не меняя контент, а улучшая его структуру).

Короче говоря

У меня есть openCL-программа, которая отлично работает, если я скомпилирую ее как исполняемый файл. Теперь я пытаюсь сделать его вызываемым из Python с помощью boost.python. Однако, как только я выхожу из Python (после импорта моего модуля), сбой python.

Причина, похоже, связана с

статически сохраняя только GPU CommandQueues и механизм их выпуска, когда программа завершает

MWE и настройка

Настройка

  • Используемая среда IDE: Visual Studio 2015

  • Используемая ОС: Windows 7 64bit

  • Версия Python: 3.5

  • Заголовки AMD OpenCL APP 3.0

  • cl2.hpp прямо из Khronos, как предлагается здесь: пустая программа openCL выдает предупреждение об отказе

  • Также у меня есть процессор Intel со встроенным графическим оборудованием и никакой другой выделенной графической картой.

  • Я использую версию 1.60 библиотеки boost, скомпилированную как 64-разрядные версии

  • Я использую dll boost: boost_python-vc140-mt-1_60.dll

  • Программа openCL без python отлично работает

  • Модуль python без openCL отлично работает

MWE

#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(). Поэтому я нажимаю на debug, и 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)

Вопрос

Итак, какова может быть причина этого и какие возможные решения? Я подозреваю, что это связано с тем, что мой тестовый класс является статическим, но поскольку он работает с исполняемым файлом, я не понимаю, что его вызывает.

4b9b3361

Ответ 1

В прошлом я столкнулся с подобной проблемой.

clRetain* поддерживаются функции OpenCL1.2. При получении устройств для первой платформы графического процессора (platforms[0].getDevices(...) для CL_DEVICE_TYPE_GPU) в вашем случае это должно быть платформа pre OpenCL1.2, поэтому вы получаете сбой. При получении устройств любого типа (GPU/CPU/...) ваша первая платформа меняется как OpenCL1.2 +, и все в порядке.

Чтобы устранить проблему:

#define CL_HPP_MINIMUM_OPENCL_VERSION 110

Это гарантирует, что вызовы clRetain* не будут созданы для неподдерживаемых платформ (pre OpenCL 1.2)


Обновление. Я думаю, что в cl2.hpp есть ошибка, которая, несмотря на то, что при установке минимальной версии OpenCL версии 1.1 она все еще пытается использовать clRetain* на pre OpenCL1.2 устройствах при создании очереди команд. Настройка минимальной версии 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;
}

Update2:

Итак, какова может быть причина этого и какие возможные решения? Я подозреваю, что это имеет какое-то отношение к тому, что мой тестовый класс static, но поскольку он работает с исполняемым файлом, я в недоумении, что вызывая его.

Статичность TestClass, по-видимому, является причиной. Похоже, что освобождение памяти происходит в неправильном порядке при запуске с python. Чтобы исправить это, вы можете добавить метод, который должен быть явно вызван для освобождения объектов opencl до того, как python начнет освобождать память.

static TestClass& getInstance() // <- const removed
{
    static TestClass instance;
    return instance;
}

void release()
{
    queues.clear();
}

BOOST_PYTHON_MODULE(FrameWork)
{
    TestClass::getInstance();
    TestClass::getInstance().release();
}

Ответ 2

"Я был бы признателен за ответ, который объясняет мне, в чем проблема, и есть ли способы ее исправить".

Во-первых, позвольте мне сказать, что doqtor уже ответил, как исправить эту проблему - гарантируя четко определенное время уничтожения всех используемых ресурсов OpenCL. ИМО, это не "взлом", а правильная вещь. Пытаясь полагаться на статическую магию init/cleanup, чтобы делать правильные вещи - и смотреть, как это не получается, - это настоящий хак!

Во-вторых, некоторые соображения по поводу проблемы: актуальная проблема еще сложнее, чем общие истории фиаско, связанные с порядком статического инициализации. Он включает в себя порядок загрузки/выгрузки DLL, как в связи с загрузкой python вашей пользовательской dll во время выполнения, так и (что более важно) с помощью модели OpenCL для устанавливаемого клиента (ICD).

Какие DLL задействованы при запуске приложения /dll, использующего OpenCL? Для приложения единственной релевантной DLL является opencl.dll, с которой вы ссылаетесь. Он загружается в память процесса во время запуска приложения (или когда ваша пользовательская DLL, которая нуждается в opencl, динамически загружается в python). Затем в то время, когда вы сначала вызываете clGetPlatformInfo() или аналогичный код в коде, логика ICD срабатывает: opencl.dll будет искать установленные драйверы (в окнах, которые упоминаются где-то в реестре) и динамически загружать соответствующие DLL (используя sth, как системный вызов LoadLibrary()). Это может быть, например, nvopencl.dll для nvidia или какой-либо другой dll для установленного драйвера intel. Теперь, в отличие от относительно простой opencl.dll, эта библиотека ICD может и будет иметь множество зависимостей самостоятельно - возможно, используя Intel IPP, или TBB, или что-то еще. Таким образом, к настоящему времени все стало действительно грязным.

Теперь, во время выключения, загрузчик окон должен решить, какие DLL выгрузить в каком порядке. Когда вы компилируете свой пример в одном исполняемом файле, количество и порядок загрузки/выгрузки dll будут, разумеется, разными, чем в сценарии "python загружает вашу пользовательскую DLL во время выполнения". И это вполне может быть причиной, по которой вы сталкиваетесь с проблемой только в последнем случае, и только в том случае, если у вас все еще есть opencl-context + commandqueue во время выключения вашей пользовательской dll. Уничтожение вашей очереди (вызванное с помощью clRelease... при статическом уничтожении вашего экземпляра testclass) делегируется в intel-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, которая создает экземпляр COM-объекта в какой-то момент его жизни. Если эта DLL хранит ссылку на объект COM в глобальной переменной и не освобождает объект COM до DLL_PROCESS_DETACH, то DLL, реализующая COM-объект, будет храниться в памяти в течение жизни COM-объекта. Эффективно DLL, реализующая объект COM, становится зависимой от DLL, которая содержит ссылку на COM-объект. Но у загрузчика нет способа зная об этой зависимости. Все, что он знает, это то, что DLL загружаются в память."


Теперь я написал много слов, не прибегая к окончательному доказательству того, что на самом деле происходит не так. Главный урок, который я узнал из таких ошибок: не входите в эту змеиную яму и делайте очистку ресурсов в определенном месте, как предлагал doqtor. Спокойной ночи.