![]() |
||||||||||||||||||||||||||||||||||
![]() |
||||||||||||||||||||||||||||||||||
![]() |
Инженерная методика адаптации приложения к гибридному кластеру с ускорителями на ПЛИСЧасть 7. Оформление текста программы унифицированным образом.С. С. Андреев, С. А. Дбар, А. О. Лацис, Е. А. Плоткина
Структурная переработка программы, необходимая для ее гибридно - параллельной реализации, в целом завершена. Осталось написать текст схемы сопроцессора и функций доступа к нему в форме, пригодной для конкретной системы программирования. Например, вместо условного, произвольным образом записанного действия "скопировать в сопроцессор фрагмент массива f", в программе должно появиться обращение к конкретной, реально существующей в некоторой реальной библиотеке, функции копирования, а вместо текста программной модели сопроцессора - текст, пригодный для синтеза, при помощи реальной схемотехнической САПР, битовой последовательности, пригодной для "прожига" реальной ПЛИС. Любопытно отметить, что, в рассуждениях многих потенциальных пользователей гибридно - параллельных суперкомпьютеров, разработка гибридно - параллельного приложения начинается именно с этой точки. Необходимость всей предшествующей работы, которую мы продемонстрировали выше, от самого начала настоящего документа до этих строк, то ли игнорируется, то ли считается чем-то мелким и несущественным. Если рассматривать изолированно ту часть работы, которая нам осталась, то самая трудная ее часть - это, конечно же, написание текста схемы. Соответственно, самой сокровенной мечтой разработчика, полагающего, что это и есть вся работа, является мечта о том, чтобы неведомый специальный язык, используемый для записи схем, был "как можно менее специальным". В нашем случае мечте суждено сбыться. Для написания текста схемы мы используем язык C++, аннотированный специальными прагмами для лучшего выражения в тексте схемотехнических сущностей. Первая, очень медленная, но формально работоспособная, версия сопроцессора может быть получена из текста на "голом" C, безо всяких прагм. Вопрос о том, удастся ли, действуя методом последовательных приближений, добавляя прагмы и, возможно, меняя стиль программирования, получить эффективную версию сопроцессора, написанную на C++, остается открытым в каждом конкретном случае. Чтобы на него ответить, придется много экспериментировать, меняя текст описания схемы, находя и исправляя ошибки, появление которых неизбежно при такого рода изменениях. Это ставит перед нами, как перед разработчиками технологии создания гибридно - параллельных приложений, важную технологическую задачу. Хотелось бы иметь единый текст гибридно - параллельного приложения на языке C, который, в зависимости от используемых трансляторов, мог бы с равным успехом транслироваться и в программно - аппаратное гибридное приложение, и в чисто программную модель вроде той, которую мы получили в предыдущем разделе этого документа. К концу этого раздела мы должны будем решить эту задачу. Как и на прошлом этапе преобразования программы, текст функции main() останется неизменным. Текст функции itstep() структурно не изменится, но некоторые вызываемые функции станут называться иначе. Вот этот текст: #include <stdio.h> #include "dimension.h" #include "comm.h" #define PORTION 15 extern int itstep_accel_test( int my_number, int n_of_nodes, int mx, int my, int stw, int portion ); int itstep_test( int my_number, int n_of_nodes, int mx, int my, int stw ) { if ( n_of_nodes > 8 ) { fprintf( stderr, "there are only 8 accels in the machine, but %d are requested\n", n_of_nodes ); return 1; } init_coprocessor( my_number%8, my_number%8 ); if ( stw > (PORTION/3) ) // See comments below { fprintf( stderr, "stencil width(%d) greater than 1/3 of the portion size (%d), this is not allowed\n", stw, PORTION ); return 1; } else #ifdef FOR_CPU return itstep_accel_test( my_number, n_of_nodes, mx, my, stw, PORTION ); #else return 0; #endif } // extern void itstep_accel( int mx, int my, double rdx2, double rdy2, // double beta, int niter ); void itstep( int mx, int my, void *pf, void *pr, double rdx2, double rdy2, double beta, int niter ) { DIM2( double, f, my ) = (typeof(f))pf; DIM2( double, r, my ) = (typeof(r))pr; int k, total, is, notlast, left, retv; unsigned long params[6]; /***/ // Fill in accel parameters that never change: params[1] = my; params[2] = *((unsigned long*)(&rdx2)); params[3] = *((unsigned long*)(&rdy2)); params[4] = *((unsigned long*)(&beta)); params[5] = niter; total = mx; // "is" is the number of rows at the beginning not to save back (i. e. from accel to CPU): is = 0; while ( total > 0 ) { // "k" is the number of rows to save back: k = total; // "notlast" is the number of rows at the end not to save back: notlast = 0; if ( k > PORTION ) { k = PORTION; left = total - k; // In the line below, we silently assume that PORTION is greater than niter: if ( left < niter ) k -= (niter-left); notlast = niter; } // Copy all the portion of "r" to accel: logic_put( 1, 0, r-is, (k+is+notlast)*my ); // Copy portion of "f" without the starting "is" rows to accel: logic_put( 0, is*my, f, (k+notlast)*my ); // For the very first portion, "is" is surely zero, so processing // immediately after the copy is OK: // itstep_accel( k+is+notlast, my, rdx2, rdy2, beta, niter ); params[0] = k+is+notlast; logic_put_block_reg( params, 6 ); logic_init( 1 ); logic_wait( 0, &retv ); // In case the portion is not last, we need to copy "is" starting rows // of the next portion to accel, to let them be "old", not "new": if ( notlast ) { // But, in case the portion is the very first, we cannot copy "is" starting rows // of the next portion, because the place for it // in accel is occupied by useful data not saved back to CPU yet, so // handle the case of "is == 0" separately: if ( is ) { // Do it the usual way: logic_put( 0, 0, f+(k-is), is*my ); logic_get( 0, is*my, f, k*my ); } else { // Perform a trick: logic_get( 0, 0, f, niter*my ); logic_put( 0, 0, f+(k-niter), niter*my ); logic_get( 0, niter*my, f+niter, (k-niter)*my ); // The trick above is OK iff niter is NOT GREATER THAN a half of the PORTION!!! } } else { // In case the portion is last, just save back: logic_get( 0, is*my, f, k*my ); } // Save "k" processed rows back to CPU: is = niter; f += k; r += k; total -= k; } } Посмотрим внимательно на текст функции itstep_test(). В данном, очень конкретном, случае, приложение предполагается запускать (командой mpirun) на единственном сервере, оснащенном восемью ПЛИС - сопроцессорами. Следуя упоминавшейся выше дисциплине "один MPI - процесс - один сопроцессор", мы обязаны проверить, что число запущенных MPI - процессов не превышает восьми. Далее следует обращение к функции init_coprocessor(). Это - функция общей инициализации системы доступа управляющей программы к сопроцессору. Подробное описание этой системы доступа можно найти в Руководстве программиста на языке Автокод. Аргументы функции init_coprocessor() - это диапазон (включительно) порядковых номеров сопроцессоров, которые данный процесс управляющей программы "берет на себя". Взяв в качестве аргументов при вызове этой функции не my_number, а my_number%8, мы учли то, что, возможно, когда-то в составе нашей установке появится более одного вычислительного узла. В этом случае надо было бы проконтролировать, что, при запуске приложения командой mpirun, было обеспечено размещение MPI - процессов в количестве именно восьми штук на узел, но, к сожалению, MPI не позволяет нам это сделать из программы. Далее следует проверка корректности соотношения величин stw и PORTION, требования к которому мы подробно рассмотрели в предыдущем разделе. Функция itstep_accel_test() теперь приобрела еще один аргумент, необходимость в котором мы также обсуждали в предыдущем разделе. Обращение к этой функции компилируется условно, по ключу FOR_CPU. Этот ключ должен быть задан, если рассматриваемый нами текст компилируется в составе программной модели, а не реального гибридно - параллельного приложения. Дело в том, что сама функция itstep_accel_test() является, как мы увидим ниже, частью этой самой программной модели, и в сборке гибридно - параллельного варианта просто отсутствует. Перейдем к тексту функции itstep(). Чтобы лучше его понять, опишем очень кратко тот набор функций доступа управляющей программы к сопроцессору, который в ней используется. Будем стараться при этом отталкиваться от того набора возможностей, который мы придумали из общих соображений, а затем реализовали, в предыдущем варианте программы. В предыдущем варианте программы мы выделили функцию itstep_accel(), и объявили ее "реализованной в сопроцессоре". Это ничуть не помешало нам записывать ее вызов в вызывающей программе, как обычное обращение к функции, с указанием имени функции, и передаваемых аргументов в скобках. При этом мы передавали этой функции обычным образом только входные и скалярные аргументы. Аргументы - массивы, среди которых были как входные, так и входные - выходные, мы передавали специальным образом, введя для этого понятие "копирования массива в память сопроцессора". В реально существующем сопроцессоре, реализованная в нем функция не вызывается по имени, поскольку она всегда одна. Вместо этого существует библиотечная функция доступа "запустить сопроцессор", обращение к которой, фактически, выполняет ту же роль, что и вызов реализованной в сопроцессоре функции. Функция "запустить сопроцессор" называется logic_init(), а функция "подождать, пока запущенный сопроцессор отработает", называется logic_wait(). Аргумент logic_init() в данном случае не используется, и его значение может быть задано любым. Первый аргумент logic_wait() задает время принудительного ожидания в секундах, по истечении которого считается, что сопроцессор отработал. Значение, равное нулю, означает "ждать, пока сопроцессор действительно отработает". Для реальной работы используется именно оно: ненулевые значения предназначены только для отладочных целей. Второй аргумент этой функции - возвращаемый, если первый аргумент задан равным нулю, то второй после возврата из функции всегда будет равен единице. Подробнее о функциях logic_init() и logic_wait() можно прочитать в Руководстве программиста на языке Автокод. Функции, реализованной в сопроцессоре, можно передать входные скалярные аргументы и аргументы - массивы. Начнем с порядка передачи входных скалярных аргументов. Все такие аргументы должны иметь одинаковый размер (в смысле sizeof()), причем вариантов такого размера три: 32, 64 или 128 разрядов. О выборе одного из этих трех вариантов будет сказано ниже, пока же примем на веру, что выбран 64 - разрядный вариант. При таком выборе, целое число должно быть представлено как [unsigned ]long, а вещественное - как double. Все скалярные аргументы следует сложить по порядку в служебный массив, состоящий из слов выбранной разрядности, и затем передать этот массив в сопроцессор обращением к функции logic_put_block_reg(). Первый аргумент этой функции - массив со значениями передаваемых параметров, второй - его длина в словах выбранной разрядности. Передача параметров действует однократно, только на один предстоящий запуск сопроцессора: перед каждым запуском сопроцессора обращением к logic_init() ее (передачу параметров) надо повторять. Передать можно только сразу все параметры, ожидаемые сопроцессором: нельзя, например, передать только первые два параметра из шести, надеясь, что остальные четыре не изменились с прошлого раза. В приведенном выше тексте программы, служебный массив для передачи входных скалярных параметров называется params, имеет тип unsigned long. Значения всех передаваемых параметров, кроме первого (задающего число строк в блоке), по ходу работы программы не меняются,и записываются в служебный массив один раз, в начале работы программы: // Fill in accel parameters that never change: params[1] = my; params[2] = *((unsigned long*)(&rdx2)); params[3] = *((unsigned long*)(&rdy2)); params[4] = *((unsigned long*)(&beta)); params[5] = niter; Сам массив, тем не менее, всякий раз передается перед запуском сопроцессора с указанием полной длины - 6 параметров: params[0] = k+is+notlast; logic_put_block_reg( params, 6 ); logic_init( 1 ); logic_wait( 0, &retv ); Передача в сопроцессор аргументов - массивов реализована практически так же, как мы это делали в предыдущем варианте программы. Отличие только в том, что массивы - параметры внутри сопроцессора нумеруются подряд от нуля, и при копировании данных между программой и сопроцессором указывается сопроцессорный номер массива, данные которого копируются. В нашем случае массив f имеет номер 0, массив r - номер 1. Для копирования массива из памяти программы в сопроцессор используется функция logic_put(), для копирования в обратном направлении - функция logic_get(). Аргументы у этих функций одинаковы: - сопроцессорный номер массива; - смещение от начала массива в памяти сопроцессора; - указатель на массив в памяти программы; - длина копируемых данных. Смещение и длина измеряются в словах выбранной разрядности. Подробнее о функциях logic_put() и logic_get() можно прочитать в Руководстве программиста на языке Автокод. Мы завершили обзор программной части нашего гибридно - параллельного приложения. Остальная его часть предназначена для аппаратной реализации. При этом, по счастливому стечению обстоятельств, имеется возможность закодировать аппаратную часть на том же языке C, что и программную, но транслироваться она будет совсем другим транслятором, и превратится в итоге не в машинный код для процессора общего назначения, а прямо в схему сопроцессора. Понимая все это в общих чертах, мы не можем, тем не менее, избавиться от некоторого чувства недосказанности, которое с неизбежностью должно было появиться у всякого внимательного читателя еще при рассказе о том, как устроена передача параметров в сопроцессор при помощи стандартных коммуникационных функций. В самом деле, для того, чтобы это действительно работало так, как рассказано выше, должна существовать какая-то "механика" установления соответствия между номерами и видами параметров, передаваемых программой через функции "logic_", и параметрами в заголовке функции, текст которой написан на языке C, но транслируется в схему сопроцессора. Такая "механика", действительно, существует. Чтобы понять ее с наименьшей затратой сил, построим сначала обещанную выше, в начале настоящего раздела, программную модель сопроцессора, то есть такие варианты функций "logic_", которые ни к каким ПЛИС не обращаются, а лишь имитируют их работу программными средствами. Соответствующий текст приводится ниже: #include <string.h> #define MAXPARAMS 100 #include "dimension.h" #define MAXMY 10000 #define LOCALMEM 50000 #include <stdio.h> // The arrays below mimics the accelerator's local memory. static double localmem[2][LOCALMEM]; static unsigned long localparams[MAXPARAMS]; void init_coprocessor( int from, int to ) {} void set_coprocessor( int ncopr ) {} void logic_put_block_reg( unsigned long *params, int count ) { int i, n; // n = count; if ( n > MAXPARAMS ) n = MAXPARAMS; for ( i = 0; i < n; i++ ) localparams[i] = params[i]; } extern void itstep_accel( long mx, long my, double *pf, double *pr, double rdx2, double rdy2, double beta, long niter ); void logic_init( int n ) { itstep_accel( (localparams[0]), (localparams[1]), localmem[0], localmem[1], *((double*)(localparams+2)), *((double*)(localparams+3)), *((double*)(localparams+4)), (localparams[5]) ); } void logic_wait( int tim, int *val ) { if ( tim ) sleep( tim ); *val = 1; } void logic_put( int narr, int internaloffset, void *array, int length ) { memcpy( localmem[narr]+internaloffset, array, length*sizeof(localmem[0][0]) ); } void logic_get( int narr, int internaloffset, void *array, int length ) { memcpy( array, localmem[narr]+internaloffset, length*sizeof(localmem[0][0]) ); } int itstep_accel_test( int my_number, int n_of_nodes, int mx, int my, int stw, int portion ) { if ( ((portion+2*stw)*my) > LOCALMEM ) { fprintf( stderr, " ((portion(%d)+2*stw(%d))*my(%d))>localmem(%d), no memory for arrays in accel\n", portion, stw, my, LOCALMEM ); return 1; } if ( my > MAXMY ) { fprintf( stderr, " my (%d) greater than maxmy (%d)\n", my, MAXMY ); return 1; } else return 0; } Текст этот исключительно прост, и может служить прекрасным дополнением к описанию того подмножества (и тех режимов использования) функций "logic_", которые предусмотрены в настоящем документе. Единственное, на что хотелось бы обратить внимание - так это на порядок установления соответствия между видами и номерами параметров, передаваемых через функции "logic_", с одной стороны, и аргументами, заданными при обращении к уже прекрасно известной нам функции itstep_accel() - с другой. Соответствие это задано автором приведенного только что текста, причем задано "волевым" образом. Например, в этом тексте написано, что в качестве аргумента - массива "f" в функцию itstep_accel() передается localmem[0], а в качестве массива "r" - localmem[1]. Именно поэтому мы в программе, запускающей сопроцессор, обязаны были полагать, что массив "f" при обращениях к logic_put()/logic_get() имеет номер 0, а массив "r" - номер 1, а не наоборот. Аналогично обстоит дело и со скалярными входными параметрами. Рассмотренный программный текст является "действующей моделью" интерфейсной логики "настоящего", реализованного в ПЛИС, сопроцессора. При замене "действующей модели" настоящим сопроцессором, этот текст просто выбрасывается за ненадобностью, поскольку функции "logic_" берутся из реальной библиотеки доступа к реальному "железу" ПЛИС. Но библиотечные варианты функций "logic_" не могут ничего "знать" о том, что массив "f" имеет номер 0, а массив "r" - номер 1, как и вообще об именах, видах и количестве параметров, передаваемых программой в сопроцессор. Стало быть, в описании настоящего сопроцессора тоже должно быть что-то, устанавливающее соответствие между видами и номерами параметров, передаваемых через функции "logic_", с одной стороны, и аргументами, заданными при объявлении функции itstep_accel() - с другой. Но описание "настоящего" сопроцессора в нашем случае - это просто текст функции itstep_accel(), который сам по себе никаких соответствий ни с чем не устанавливает! Значит, это загадочное "что-то" может быть только дополнением к тексту на C, имеющим вид или прагмы, или комментария специального вида. Посмотрев на текст функции itstep_accel(), убедимся в том, что так оно и есть: /*--vector_proc_64 void itstep_accel( double pf, : ram0,inout, wblock=1,size=50000 double pr, : ram1,in, wblock=1,size=50000 long mx, : reg0 long my, : reg1 double rdx2, : reg2 double rdy2, : reg3 double beta, : reg4 long niter : reg5 )--*/ void itstep_accel( long mx, long my, double pf[LOCALMEM], double pr[LOCALMEM], double rdx2, double rdy2, double beta, long niter ) { // DIM2( double, f, my ) = (typeof(f))pf; // DIM2( double, r, my ) = (typeof(r))pr; static double lower[MAXMY]; static double middle[MAXMY]; int i, j, k, mx1, my1, n; // mx1 = mx - 1; my1 = my - 1; for ( k = 0; k < niter; k++ ) { n = 0; for ( i = 0; i < mx1; i++ ) { for ( j = 0; j < my; j++ ) { lower[j] = middle[j]; middle[j] = pf[n+j]; } if ( i ) { for ( j = 1; j < my1; j++ ) { pf[n+j] = ((lower[j]+pf[n+j+my])*rdx2+(middle[j-1]+middle[j+1])*rdy2 - pr[n+j])*beta; } } n += my; } } } Комментарий, с которого начинается этот текст, имеет специальный вид, и является описанием интерфейсной части схемы сопроцессора. Он обрабатывается специальной программой - препроцессором, которая транслирует его в текст на VHDL. Затем работает транслятор основного текста функции на VHDL, который изготавливает внутреннюю, содержательную часть схемы сопроцессора. В момент работы этого, основного, транслятора, описание интерфейсной части схемы воспринимается, как комментарий, то есть игнорируется. Затем схемотехническая САПР объединяет все полученные тексты на VHDL вместе, и синтезирует схему сопроцессора в виде битовой последовательности, пригодной для прожига ПЛИС. Теперь разберем приведенный текст описания схемы сопроцессора в деталях. Начнем с описания интерфейсной части схемы. /*--vector_proc_64 void itstep_accel( double pf, : ram0,inout, wblock=1,size=50000 double pr, : ram1,in, wblock=1,size=50000 long mx, : reg0 long my, : reg1 double rdx2, : reg2 double rdy2, : reg3 double beta, : reg4 long niter : reg5 )--*/ Слово vector_proc_64 является ключевым. Задает разрядность параметров, передаваемых из программы в сопроцессор. Допустимые варианты: vector_proc_32, vector_proc_64 или vector_proc_128. Далее следует текст, похожий на объявление заголовка функции, где про каждый параметр говорится, какой именно вид и номер (в терминах функций "logic_" этот параметр имеет. Например, параметр mx является входным скалярным, типа long, и передается через logic_put_block_reg() под номером 0. Параметр pr является входным (in) массивом (ram1) типа double, при копировании посредством logic_put()/logic_get() имеет номер 1, размер его (size) 50000 элементов, а векторность массива (wblock) равна 1. О том, что такое векторность сопроцессорного массива, можно прочитать в Руководстве программиста на языке Автокод, пока же будем считать этот параметр "заклинанием", значение которого - всегда 1. Остальные атрибуты массивов - параметров в комментариях не нуждаются. Обратим внимание на то, что размер массива задавать обязательно. В сопроцессор массив нельзя "передать по указателю", его (массив) можно только синтезировать в составе схемы, а для этого необходимо знать его размер. Теперь посмотрим на текст функции itstep_accel(), описывающей внутреннюю логику сопроцессора. Обратим внимание на то, что аргументы - массивы в заголовке функции заданы с указанием длины, причем длина задана такая же, как в описании интерфейсной части схемы. Это всегда должно быть именно так. В остальном текст функции отличается от прошлого своего варианта тем, что массивы f и r теперь называются pf и pr, и адресуются как одномерные. Это сделано из опасения, что транслятор языка C в схему может не понимать объявления массивов с размерами, задаваемыми динамически, а также в надежде на синтез более эффективной схемы. Опасение, что транслятор не понимает объявления массивов с динамически задаваемыми размерами, впоследствии было проверено, и не подтведилось, так что использовать многомерные массивы, заданные описанным выше способом, в схеме можно. Обратим внимание, что при последующих изменениях в тексте этой функции, предпринимаемых с целью улучшить получаемую при ее трансляции схему, мы будем в каждый момент времени иметь легко проверяемую программную модель будущего сопроцессора. Задача, поставленная в начале настоящего раздела, тем самым, выполнена, а демонстрация инженерной методики превращения последовательной программы в формально работоспособное гибридно - параллельное приложение завершена. ◄ Часть 6 Часть 8 ► |
![]() |
||||||||||||||||||||||||||||||||
Тел. +7(499)220-79-72; E-mail: inform@kiam.ru |