Это история о том, как мы c mildly_parallel замедляли ускоряли расчеты на самом мощном суперкомпьютере в мире.

В апреле этого года наша команда приняла участие в финале Asia supercomputer challenge 2017, одним из заданий которого было ускорение программы для моделирования океанских волн Masnum-Wave на китайском суперкомпьютере Sunway TaihuLight.
Все началось с отборочного тура в феврале: мы получили доступ к суперкомпьютеру и познакомились с нашим новым другом на ближайшие пару месяцев.
Вычислительные узлы выставлены в форме двух овалов, между которыми стоит сетевое железо. В узлах используются процессоры Shenwei 26010. Каждый из них состоит из 4 гетерогенных процессорных групп, которые включают одно управляющее и 64 вычислительных ядра с тактовой частотой 1,45 ГГц и размером локального кэша 64 Кб.

Охлаждается все это с помощью водяной системы, расположенной в отдельном здании.
Из ПО в нашем распоряжении были компиляторы фортрана и си с “поддержкой” OpenACC 2 и athreads (аналог POSIX Threads) и планировщик задач, совмещающий в себе одновременно возможности обычного планировщика и mpirun.
Доступ к кластеру осуществлялся через особый VPN, плагины для работы с которым были доступны только для Windows и Mac OS. Все это добавляло особенного шарма при работе.
Заданием было ускорение Masnum-Wave на этом суперкомпьютере. Нам предоставили исходный код Masnum-Wave, несколько крохотных readme файлов с описанием основ работы на кластере и данные для замера ускорения.
Masnum-Wave – это программа для моделирования движения волн по всему земному шару. Она написана на Фортране с использованием MPI. В двух словах, она итеративно выполняет следующее: читает данные функций внешнего воздействия, синхронизирует граничные области между MPI-процессами, вычисляет продвижение волн и сохраняет результаты. Нам дали workload на 8 модельных месяцев с шагом по 7.5 минуты.
В первый же день мы нашли в интернете статью: “The Sunway TaihuLight supercomputer:
system and applications, Haohuan Fu” с описанием ускорения Masnum-Wave на архитектуре Sunway TaihuLight с помощью конвейерной обработки. Авторы статьи использовали всю мощь кластера (10649600 ядер), в нашем же распоряжении было 64 вычислительных группы (4160 ядер).
Masnum-Wave состоит из нескольких модулей:

Мы впервые столкнулись с таким большим количеством кода, порожденного наукой. Так как код представляет собой смесь двух версий фортрана 90 и 77, это порой ломало работу их собственного компилятора. В большом количестве там встречаются чудесные конструкции goto, куски закомментированного кода и, конечно же, комментарии на китайском.
Сокращенный пример кода для наглядности:
do 1201 j=1,jl js=j j11=jp1(mr,j) j12=jp2(mr,j) j21=jm1(mr,j) j22=jm2(mr,j) !**************************************************************** ! do 1202 ia=ix1,ix2 ! do 1202 ic=iy1,iy2 eij=e(ks,js,ia,ic) !if (eij.lt.1.e-20) goto 1201 if (eij.lt.1.e-20) cycle ea1=e(kp ,j11,ia,ic) ea2=e(kp ,j12,ia,ic) ! ... eij2=eij**2 zua=2.*eij/al31 ead1=sap/al11+sam/al21 ead2=-2.*sap*sam/al31 ! fcen=fcnss(k,ia,ic)*enh(ia,ic) fcen=fconst0(k,ia,ic)*enh(ia,ic) ad=cwks17*(eij2*ead1+ead2*eij)*fcen adp=ad/al13 adm=ad/al23 delad =cwks17*(eij*2.*ead1+ead2) *fcen deladp=cwks17*(eij2/al11-zua*sam)*fcen/al13 deladm=cwks17*(eij2/al21-zua*sap)*fcen/al23 !* nonlinear transfer se(ks ,js )= se(ks ,js )-2.0*ad se(kp2,j11)= se(kp2,j11)+adp*wp11 se(kp2,j12)= se(kp2,j12)+adp*wp12 se(kp3,j11)= se(kp3,j11)+adp*wp21 se(kp3,j12)= se(kp3,j12)+adp*wp22 se(im ,j21)= se(im ,j21)+adm*wm11 se(im ,j22)= se(im ,j22)+adm*wm12 se(im1,j21)= se(im1,j21)+adm*wm21 se(im1,j22)= se(im1,j22)+adm*wm22 !... ! 1202 continue ! 1212 continue 1201 continue
Прежде всего мы с помощью вывода времени выполнения каждой функции определили узкие места в коде и кандидатов для оптимизации. Наибольший интерес у нас вызвал модуль warcor, отвечающий за численное решение модельного уравнения и функции записи контрольных точек.
Изучив скудную документацию к китайским компиляторам, мы решили использовать OpenACC, так как это стандарт от Nvidia с примерами и спецификацией. К тому же, код из readme к athreads казался нам неоправданно сложным и просто не компилировался. Как же мы ошибались.
Одна из первых идей, которая приходит в голову при оптимизации кода на ускорителях – это использование локальной памяти. В OpenACC это можно сделать несколькими директивами, но результат всегда должны быть один: данные перед началом вычислений должны быть скопированы на локальную память.
Для проверки и выбора нужной директивы мы написали на фортране несколько тестовых программ, на которых убедились, что они работают и что можно таким образом получить ускорение. Далее мы расставили эти директивы в Masnum-Wave, указав им имена наиболее используемых переменных. После компиляции они стали появляться в логах, сопутствующие надписи на китайском не были подсвечены красным, и мы посчитали, что все скопировалось и работает.

Но оказалось, что не все так просто. Компилятор OpenACC не копировал массивы в Masnum-Wave, но работал исправно с тестовыми программами. Проведя пару дней с Google Translate мы поняли, что он не копирует объекты, которые определены в файлах, подключаемых через директивы препроцессора (include)!
Всю следующею неделю мы переносили код Masnum-Wave из подключаемых файлов (а их больше 30) в файлы с исходным кодом, при этом нужно было убедиться, что все определятся и линкуется в правильном порядке. Но, так как ни у кого из нас не было опыта работы с Фортраном, и все сводилось к “давайте жахнем и посмотрим, что получится”, то без замены некоторых основных функций на костыльные версии тут тоже не обошлось.
И вот, когда все модули были перелопачены, и мы, в надежде получить свое мизерное ускорение, запустили свежескомилированный код, то получили очередную порцию разочарования! Директивы, написанные по всем канонам стандарта OpenACC 2.0, выдают ошибки в runtime. В этот момент в наши головы начала закрадываться идея, что этот чудесный суперкомпьютер поддерживает какой-то свой особый стандарт.
Тогда мы попросили у организаторов соревнований документацию, и с третьей попытки они нам ее предоставили.

Пару часов с Google Translate подтвердили наши опасения: стандарт, который они поддерживают, называется OpenACC 0.5, и он кардинально отличается от OpenACC 2.0, поставляемого с pgi компилятором.
Например, основной нашей задумкой было переиспользование данных на ускорителе. Для выполнения этого в стандарте 2.0 необходимо обернуть параллельный код в блок data. Вот как это делается в примерах от Nvidia:
!$acc data copy(A, Anew) do while ( error .gt. tol .and. iter .lt. iter_max ) error=0.0_fp_kind !$omp parallel do shared(m, n, Anew, A) reduction( max:error ) !$acc kernels do j=1,m-2 do i=1,n-2 Anew(i,j) = 0.25_fp_kind * ( A(i+1,j ) + A(i-1,j ) + & A(i ,j-1) + A(i ,j+1) ) error = max( error, abs(Anew(i,j)-A(i,j)) ) end do end do !$acc end kernels !$omp end parallel do if(mod(iter,100).eq.0 ) write(*,'(i5,f10.6)'), iter, error iter = iter +1 !$omp parallel do shared(m, n, Anew, A) !$acc kernels do j=1,m-2 do i=1,n-2 A(i,j) = Anew(i,j) end do end do !$acc end kernels !$omp end parallel do end do !$acc end data
Но на кластере этот код не скомпилируется, потому что в их стандарте эта операция делается через указание индекса для каждого блока данных:
#include <stdio.h> #include <stdlib.h> #define NN 128 int A[NN],B[NN],C[NN]; int main() { int i; for (i=0;i<NN;i++) { A[i]=1; B[i]=2; } #pragma acc data index(1) { #pragma acc parallel loop packin(A,B, at data 1) copyout(C) for (i=0;i<NN;i++) { C[i]=A[i]+B[i]; } } for(i=0;i<NN;i++) { if(C[i]!=3) { printf("Test Error! C[%d] = %d\n", i, C[i]); exit(-1); } } printf("Test OL!\n"); return 0; }
Коря себя за выбор OpenACC, мы все же продолжили работу, так как времени оставалось всего пару дней. В последний день отборочного тура нам наконец-то удалось запустить наш “ускоренный” код. Мы получили замедление в 3.5 раза. Нам ничего не оставалось, кроме как написать в отчете к заданию все, что мы думаем о их реализации OpenACC в цензурной форме. Не смотря на это, мы получили множество позитивных эмоций. Когда еще придется удаленно отлаживать код на самом мощном компьютере в мире?
P.S.: В результате мы все же прошли в финальную часть конкурса и съездили в Китай.
Последним заданием финала была презентация с описанием решений. Лучшего результата добилась местная команда, которая написала свою библиотеку на Си c использованием athread т.к. OpenACC, по их словам, не работает.
