LINUX.ORG.RU

Многопоточный for внутри QThread

 , , ,


0

3

День добрый. Есть рассчётное ядро софтины для моделирования (QFrost). Нужно внутри std::vector выполнить некоторый метод каждого его элемента. Порядок выполнения не важен, элементы друг с другом никак не связаны. Так что хочу распараллелить. Потом распараллелю это с помощью CUDA или OpenCL, но пока хочется задействовать хотя бы все ядра процессора.

Пробовал OpenMP с его #pragma omp parallel for, но так как расчёты запускаются гуём и производятся внутри QThread, OpenMP не хочет окуппировать более одного потока. Если делать это в основном потоке, всё ок. Гугл знает об этой проблеме с некоторыми версиями GCC, но решения мной найдено не было.

Как лучше решить эту проблему, если во главе угла — производительность и кроссплатформенность? Пока попробую TBB (parallel_for оттуда). Средства Qt, желательно, не предлагать, т.к. не хочу использовать их в расчётном ядре. С радостью бы воспользовался std::async из C++11, но тогда появится геморрой со сборой в винде. Напоминаю, основная проблема — то, что этот for будет запускаться внутри QThread.

★★★★★

OpenCL вроде как умеет распараллеливать все ядра процессоров...

Думаю openmp вам тут не поможет, наиболее простой вариант - создавать наверное руками сколько нужно штук QThread, и распределять всё между ними (ну то есть использовать qt, да).

BattleCoder ★★★★★
()
Ответ на: комментарий от BattleCoder

openmp-то почему не поможет? #pragma omp parallel for отлично работает внутри QThread, но лишь в некоторых версиях GCC.

Касательно OpenCL, я пока не хочу переписывать расчётное ядро. Да и склоняюсь я к CUDA.

Obey-Kun ★★★★★
() автор топика
Последнее исправление: Obey-Kun (всего исправлений: 1)

С радостью бы воспользовался std::async из C++11, но тогда появится геморрой со сборой в винде.

В бусте async() вроде нет, но что-то подобное соорудить возможно.

Pavval ★★★★★
()
Ответ на: комментарий от Pavval

Точно, про boost я и не подумал, хотя уже использую Boost::Geometry. Посмотрю. Спасибо.

Obey-Kun ★★★★★
() автор топика

std::vector
Qt

/0

Потом распараллелю это с помощью CUDA или OpenCL

Мда, ты таки видать не совсем понимаешь что это такое или вообще не понимаешь...

Гугл знает об этой проблеме с некоторыми версиями GCC, но решения мной найдено не было.

Выбрать другую версию gcc, не? Сделать QThreadPool или ещё какой велосипед на QThread?

erfea ★★★★★
()
Ответ на: комментарий от erfea

std::vector
Qt

/0

Я же сказал, расчётное ядро — отдельно, гуй — отдельно. В ядре Qt не используется, потому и контейнеры — из STL.

Мда, ты таки видать не совсем понимаешь что это такое или вообще не понимаешь...

Расчёты будут выноситься на видеокарту, для чего часть расчётного ядра будет переписана с использованием OpenCL или CUDA. Чего я не понимаю?

Выбрать другую версию gcc, не? Сделать QThreadPool или ещё какой велосипед на QThread?

А если кому-то другому нужно будет собрать софтину, его тоже заставлять использовать конкретную версию gcc? А велосипеды с костылями нафиг, хочу нормальное решение.

Obey-Kun ★★★★★
() автор топика
Ответ на: комментарий от Obey-Kun

Я же сказал, расчётное ядро — отдельно, гуй — отдельно

QThread У тебя таки где? Ну и каша таки всё-равно.

Расчёты будут выноситься на видеокарту, для чего часть расчётного ядра будет переписана с использованием OpenCL или CUDA. Чего я не понимаю?

Использовать OpenCL или CUDA это уже не распаралелить, а на другом инструментарии реализовать. Я вот о чем, называй вещи своими именами.

А если кому-то другому нужно будет собрать софтину, его тоже заставлять использовать конкретную версию gcc?

Ну хз, у меня гента и много софта с openmp. Много версий уже gcc сменилось. Всё работает... Может всё не так уж плохо.

erfea ★★★★★
()
Ответ на: комментарий от erfea

openmp бывает не хочет распараллеливать в не совсем стандартных ситуациях типа сабжа. А в остальных - всегда пожалуйста.

Pavval ★★★★★
()
Ответ на: комментарий от erfea

qfgui::ComputationThread — создаётся гуём, он создаёт объект типа qfcore::Domain и делает это:

    for (QDate date = mInitialDate; date != mFinalDate; date = date.addDays(1)) {
        if (date.day() == 1) {
            // Каждый месяц пробуем выслать сцене новые данные...
            if (mNeedBlocksRedrawing) {
                if (mSceneWantsNewData) {
                    if (mSceneIsReadyForRedraw) {
                        mSceneIsReadyForRedraw = false;
                        mSceneWantsNewData = false;
                        mComputationData = ComputationData(mDomain,
                                                           date,
                                                           dateOfPreviousData);
                        emit signalNewDataIsReady(mComputationData);
                        dateOfPreviousData = date;
                    }
                }
            }
            // ... и переводим граничные условия в рассчётной области на новый месяц
            mDomain.setMonth(date.month());
        }
        emit dateChanged(date);

        // только теперь делаем шаги, что переведёт нас на завтрашний день
        mDomain.doSteps(mStepsInDay);

        if (mMustStop) {
            break;
        }
    }

Почему каша? Если будет желание сделать расчёты без гуя (скажем, если захочется провести сложные расчёты на суперкомпьютере), то есть расчётное ядро, которому Qt не нужно.

Obey-Kun ★★★★★
() автор топика
Ответ на: комментарий от erfea

Использовать OpenCL или CUDA это уже не распаралелить, а на другом инструментарии реализовать. Я вот о чем, называй вещи своими именами.

Потом распараллелю это с помощью CUDA или OpenCL

Wikipedia

CUDA (англ. Compute Unified Device Architecture) — программно-аппаратная архитектура параллельных вычислений, которая позволяет существенно увеличить вычислительную производительность благодаря использованию графических процессоров фирмы NVIDIA.

Нету тут невязки, не придирайся к словам.

Obey-Kun ★★★★★
() автор топика

TBB заработал, оба ядра задействованы, отлично. Но попробую и другие решения, сравню производительность.

Obey-Kun ★★★★★
() автор топика
Последнее исправление: Obey-Kun (всего исправлений: 1)
Ответ на: комментарий от Obey-Kun

Странно, на двух ядрах tbb::parallel_for_each делается примерно вчетверо медленее, чем такой же std::for_each. Блокировки на чтение? Вряд ли. Но надо проверить. Попробую только сперва другие варианты.

Obey-Kun ★★★★★
() автор топика
Ответ на: комментарий от Obey-Kun

Даже если если создан tbb::task_scheduler_init init(1) (то есть если расчёты TBB ведутся одним ядром), он считает гораздо медленнее, чем std::for_each. Странно.

Obey-Kun ★★★★★
() автор топика
Последнее исправление: Obey-Kun (всего исправлений: 1)

Есть такая STL-подобная библиотека Thrust. Совместима с STL (в частности, с тамошними векторами). Умеет кучу параллельных примитивов, из них «parallel forall» (transform в терминах Thrust) - самое простое. Есть параллельные редукции, сортировки, фильтрация (copy_if/remove_if) и куча всего другого.

Самая мякотка в том, что библиотека работает как с GPU, так и с CPU. Можно просто переключать параллелящие backend'ы (CUDA, std::thread, OpenMP, TBB), при этом исходный код остаётся одним и тем же.

anonymous
()

Что, если попробовать QtConcurrent::run()?

Y ★★
()

Если for запускается внутри QThread, и каждый рассчёт реализован в другой функции, то QtConcurrent::run(). Тот самый QThread, в котором работает for, ещё не считается рассчётным ядром?

Y ★★
()
Ответ на: комментарий от Pavval

Пока что (т.к. это двухмерная задача) от нескольких секунд до 10-15 минут. В трёхмерной без использования видеокарты считать будет несколько суток, если не больше. Код чего именно? Вообще, софт (двухмерная его версия), будет под GPLv3 через пару недель (вместе с релизом).

Конкретно расчёты выглядят так:

void Domain::doSteps(unsigned int inNumSteps)
{
    for (unsigned int i = 0; i < inNumSteps; ++i) {
        moveInTime();
    }
}

static void moveBlockInTime(SoilBlock &b)
{
    b.moveInTime();
}

static void moveHeatSurfaceInTime(HeatSurface &s)
{
    s.moveInTime();
}

void Domain::moveInTime()
{
    // Вот распаралелливаемая часть
    std::for_each(mHeatSurfaces.begin(),
                  mHeatSurfaces.end(),
                  moveHeatSurfaceInTime);
    std::for_each(mSoilBlocks.begin(),
                  mSoilBlocks.end(),
                  moveBlockInTime);
}

Obey-Kun ★★★★★
() автор топика
Последнее исправление: Obey-Kun (всего исправлений: 1)
Ответ на: комментарий от Obey-Kun

См. выше на тему Thrust. Код будет выглядеть так:

thrust::device_vector<SoilBlock> dSoilBlocks(mSoilBlocks);
thrust::device_vector<HeatSurface> dHeatSurfaces(mHeatSurfaces);

struct moveBlockInTime
{
    __host__ __device__ void operator()(SoilBlock &b) {
        b.moveInTime();
    }
}

struct moveHeatSurfaceInTime
{
    __host__ __device__ void operator()(HeatSurface &s) {
        s.moveInTime();
    }
}

void Domain::moveInTime()
{
    // Вот распаралелливаемая часть
    thrust::for_each(dHeatSurfaces.begin(),
                  dHeatSurfaces.end(),
                  moveHeatSurfaceInTime());
    thrust::for_each(dSoilBlocks.begin(),
                  dSoilBlocks.end(),
                  moveBlockInTime());
}

При компиляции можно выбрать любой из параллелящих backend'ов: CUDA, STL, OpenMP, TBB. Исходный код для переключения между backend'ами менять не требуется. Если интересно, могу пояснить, что это за device_vector, зачем понадобилось конвертировать функции в функторы и т.п.

anonymous
()
Ответ на: комментарий от anonymous

Здорово. Интересно, как оно вообще умудрится перевести SoilBlock::moveInTime и HeatSurface::MoveInTime в, скажем, CUDA? Для этого и нужны thrust::device_vector?

И, вероятно, логичнее сделать соответствующие __host__ __device__ void operator() внутри HeatSurface и SoilBlock, дабы не делать отдельные структуры и уменьшить кол-во вызовов функций?

Если интересно, могу пояснить, что это за device_vector, зачем понадобилось конвертировать функции в функторы и т.п.

Поясни, пожалуйста.

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

Obey-Kun ★★★★★
() автор топика
Последнее исправление: Obey-Kun (всего исправлений: 2)
Ответ на: комментарий от Obey-Kun

Кстати, в моем случае все HeatSurfaces используют указатели на один или два SoilBlock (они берут оттуда их текущие температуры и теплопроводности) и (иногда) 1 экземпляр граничного условия (BoundaryCondition). А каждый SoilBlock использует указатели на 1 или более HeatSurface (и берут оттуда входящее/исходящее тепло). Так что (в случае Thrust) придётся каким-то образом обновлять эти указатели таким образом, чтобы они указывали на те экземпляры этих классов, которые хранятся на считающем девайсе. Как это можно будет осуществить?

Obey-Kun ★★★★★
() автор топика
Ответ на: комментарий от Obey-Kun

То бишь примерно вот так это сейчас:

void HeatSurface::moveInTime()
{
    // Рассчитываем плотность теплопотока.
    if (mHasBoundaryCondition) {
        switch (mBoundaryCondition->type()) {
        case BoundaryCondition::FirstType:
            mH = (mBoundaryCondition->temperature() - mSoilBlock1->temperature())
                / (mR * mSoilBlock1->invertedEffectiveConductivity());
            break;
        case BoundaryCondition::SecondType:
            mH = mBoundaryCondition->heatFlowDensity();
            break;
        case BoundaryCondition::ThirdType:
            mH = (mBoundaryCondition->temperature() - mSoilBlock1->temperature())
                / (mR * mSoilBlock1->invertedEffectiveConductivity()
                   + mBoundaryCondition->resistivity());
            break;
        default:
            // не должны сюда попасть
            assert(false);
            mH = 0.0; // а это чтоб компилятор не ругался
        }
    } else {
        mH = (mSoilBlock2->temperature() - mSoilBlock1->temperature())
            / (mR1 * mSoilBlock1->invertedEffectiveConductivity()
               + mR2 * mSoilBlock2->invertedEffectiveConductivity());
    }

    // Переводим плотность теплопотока в теплопоток
    mH *= mSquare;
}

void SoilBlock::moveInTime()
{
    for (std::vector<HeatSurfaceContact>::const_iterator it = mHeatSurfaces.begin(); it != mHeatSurfaces.end(); ++it) {
        mEnthalpy += it->h() * mTimeStepPerVolume;
    }
    mEnthalpy += mInternalHeatSourcePowerDensity * mTimeStepPerVolume;
    calcCondition();
    calcIEConductivity();
}

При этом HeatSurfaceContact содержит указатель на HeatSurface (и знак, с которым теплопоток должен добавляться к блоку).

Соответственно, HeatSurface::mSoilBlock1, HeatSurface::mSoilBlock2 и HeatSurface::mBoundaryCondition, а также всё содержимое SoilBlock::mHeatSurfaces должно будет указывать на нечто новое, как это осуществить?

Obey-Kun ★★★★★
() автор топика
Последнее исправление: Obey-Kun (всего исправлений: 1)
Ответ на: комментарий от erfea

std::vector
Qt

/0

Ок, мистер умник, покажите контейнер из Qt, заменяющий std::vector, но без COW, чтобы производительность на неконстантных операциях не просиралась.

annulen ★★★★★
()
Ответ на: комментарий от Obey-Kun

Наверное, решили не дублировать STL, так как она теперь обязательна для сборки Qt.

annulen ★★★★★
()
Ответ на: комментарий от Obey-Kun

Если у тебя так модуль расчета абстрагирован, может сделать отдельное приложение? Отдельный процесс так понимаю не будет иметь тех проблем, что в QThread.

erfea ★★★★★
()
Ответ на: комментарий от annulen

Зато появятся проблемы с IPC

Расскажите, доктор вам поможет.

erfea ★★★★★
()

Архитектуропроблемы же. Правильнее делать вычислительное ядро отдельным бинарником без окон и дверей, который читает описание задачи из файла, считает, пишет дамп в файл, и отдельно опциональное GUI, которое запускает ядро и анализирует этот дамп.

imtw
()
Ответ на: комментарий от Obey-Kun

Файловые операции замедлят счёт

Делай дамп не каждый шаг, а каждые n~100. Всё равно они мало отличаются. У меня, более того, (о, ужас!) всё в plain text пишется. При грамотном дизайне особого замедления нет.

Преимуществ описанного мной подхода масса:

Если рано или поздно придётся использовать MPI, весь твой GUI точно придётся выкинуть.

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

Такую схему можно автоматизировать. У нас легко создавать цепочки вроде «скрипт создаёт описание системы» -> трудоёмкие вычисления -> несколько скриптов для автоматического анализа данных -> ...

Посмотри на любой серьёзный софт (GAMESS, LAMMPS, GROMACS, ...), он устроен именно так.

imtw
()
Ответ на: комментарий от imtw

Если рано или поздно придётся использовать MPI, весь твой GUI точно придётся выкинуть.

Не придётся, видеокарты хватит за глаза.

Такую схему можно автоматизировать. У нас легко создавать цепочки вроде «скрипт создаёт описание системы» -> трудоёмкие вычисления -> несколько скриптов для автоматического анализа данных -> ...

Посмотри на любой серьёзный софт (GAMESS, LAMMPS, GROMACS, ...), он устроен именно так.

Мой софт заточен на использование недалёкими юзерами.

Obey-Kun ★★★★★
() автор топика
Ответ на: комментарий от Obey-Kun

Моё дело предупредить. Для проблемы из ОП-поста можно ещё попробовать потыкать cilk+, в gcc есть.

imtw
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.