Сказ о том, как «цифирь» не сошлась

от автора

Некоторое время назад я писал про то, как получать воспроизводимые результаты и какие сложности с этим связаны. Также подробно рассказал про модели, позволяющие контролировать работу с числами с плавающей точкой в компиляторе и отдельно уточнил, что если мы используем какие-либо библиотеки или стандарты, то должны позаботится, чтобы нужные флаги были указаны и для них. И вот совсем недавно я натолкнулся на интересную проблемку, связанную именно с воспроизводимостью результатов при работе с OpenMP.
Что такое воспроизводимость? Да всё просто – мы хотим получать одну и ту же «хорошую цифирь» от запуска к запуску, потому что для нас это важно. Это критично во многих областях, где сейчас активно используются параллельные вычисления.
Итак, как вы помните, для машинных вычислений существенную роль играет порядок суммирования, и если у нас имеются циклы, распараллеленные с помощью любой технологии, то неизбежно возникнет проблема воспроизводимости результатов, потому что никто не знает в каком порядке будет проводиться суммирование, и на сколько «кусков» будет разбит наш исходный цикл. В частности это проявляется при использовании OpenMP в редукциях.

Рассмотрим простой пример.

!$OMP PARALLEL DO schedule(static) do i=1,n   a=a+b(i) enddo 

В этом случае при использовании статического планировщика мы просто разобьем всё пространство итераций на равное количество частей, и дадим каждому потоку выполнить эти итерации. Скажем, если у нас 1000 итераций, то при работе 4 потоков мы получим по 250 итераций «на брата». Наш массив является примером общих данных для разных потоков и поэтому нам нужно позаботится о безопасности кода. Вполне рабочий вариант использовать редукцию и вычислять в каждом потоке своё значение, а затем складывать полученные «промежуточные» результаты:

!$OMP PARALLEL DO REDUCTION(+:a) schedule(static) do i=1,n   a=a+b(i) enddo 

Так вот, даже на таком простом примере получить разброс в значениях можно достаточно просто.
Я поменял число потоков с помощью OMP_SET_NUM_THREADS и получил, что при 2 потоках a= 204.5992, а при 4 уже 204.6005. Способ инициализации массива b(i) и a я опустил.
Интересно то, что говорить о воспроизводимых результатах можно только при соблюдении целого ряда условий. Так вот, архитекутра, ОС, версия компилятора, которым собиралось приложение и число потоков должно быть всегда постоянным от запуска к запуску. Если мы изменили число потоков, то результаты будут отличаться, и это абсолютно нормально. Тем не менее, даже при соблюдении всех этих условий результат всё равно может отличаться, и здесь нам должна помочь переменная окружения KMP_DETERMINISTIC_REDUCTION и статический планировщик. Оговорюсь, что её использование не даст нам гарантию совпадения результатов параллельной и последовательной версий приложения, равно как и с другим запуском, при котором использовалось отличное количество потоков. Это важно понимать.

Речь о достаточно узком случае, когда мы действительно ничего не меняли, а результаты не сошлись. И вот самый главный сюрприз заключается в том, что в некоторых случаях и KMP_DETERMINISTIC_REDUCTION не работает, хотя мы и «играли по правилам».
Такой код, который незначительно сложнее первого примера, даёт различные результаты:

!$OMP PARALLEL DO REDUCTION(+:ue) schedule(static)     do is=1,ns         do y=1,ny             do x=1,nx                 ue(x,y)=ue(x,y) + ua(x,y,is)             enddo         enddo     enddo !$OMP END PARALLEL DO 

Даже после выставленной переменной KMP_DETERMINISTIC_REDUCTION, ничего не изменилось. Почему? Оказывается, в некоторых случаях компилятор из соображений производительности создаёт свою собственную реализацию цикла с использованием локов, и не заботится о результатах при этом. Эти случаи легко отследить по ассемблеру. В «хороших» вариантах обязательно должен быть вызов функции __kmp_reduce_nowait. А вот для моего примера подобного сделано не было, что несколько подрывает доверие к KMP_DETERMINISTIC_REDUCTION.

Итак, если вы написали код и результаты ваших вычислений скачут от запуска к запуску, не спешите посыпать голову пеплом. Отключите оптимизации и запустите ваше приложение. Проверьте, выравнены ли ваши данные и задайте «строгую» модель работы с числами с плавающей точкой. Для теста могут быть полезны следующие опции компилятора:

ifort -O0 -openmp -fp-model strict -align array32byte 

Если и при таком наборе опций результаты вас удивляют, проверьте циклы, распараллеленные с помощью OpenMP и редукций и включите KMP_DETERMINISTIC_REDUCTION. Это может сработать и решить проблему. Если нет, то посмотрите на ассемблер и проверьте наличие вызова __kmp_reduce_nowait. В случае наличия этого вызова – проблема, вероятно, не с OpenMP и не с компилятором, а в код закралась ошибка. Кстати, проблему с KMP_DETERMINISTIC_REDUCTION мы должны решить в скором времени. Но учитывайте эту особенность уже сейчас.

ссылка на оригинал статьи http://habrahabr.ru/post/261681/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *