[ Автор: Илья Евсеев ]
[ Организация: ИВВиБД ]
[ Подразделение: ЦСТ ]
Демонстрация производительной мощности компьютера с параллельной архитектурой должна быть наглядной. Более высокая по сравнению с однопроцессорной машиной скорость вычислений должна быть, так сказать, видна невооруженным глазом. Такова постановка задачи. Далее в документе обобщается опыт, полученный мною при попытке решения этой задачи.
Было решено, что вычисления должны сводиться к построению мультипликации. Сравниваемые компьютеры генерируют эталонную серию картинок-кадров и в реальном времени отображают их на дисплее. Сравнительная скорость разных машин, таким образом, может быть немедленно оценена даже непрофессионалом "на глазок". Желательно (и в теории возможно), чтобы демонстрашка вызывала, помимо профессионального интереса, чисто праздное любопытство, то есть была бы сделана достаточно эффектно.
Наконец, визуализация - это ведь одна из основных областей применения скоростных машин; и поэтому демонстрация, основанная на ней, наиболее предпочтительна.
Это 8-процессорная массивно-параллельная машина, через SCSI-мост подключенная к рабочей станции SunSPARC. На вычислительных узлах работают копии однозадачной операционной системы Parsytec nanoKernel (далее nK). Параллельная программа строится на рабочей станции (для этого есть кросс-компиляторы Си/Фортрана и библиотеки к ним), и с нее (со станции) запускается загрузчиком на вычислительных узлах. nK вкупе с библиотеками предоставляет программисту API (applications programmer's interface), который более-менее соответствует стандарту Unix SVR4 (System V Release 4 фирмы ATT). При этом все (на самом деле не все) операции с файлами, выполняемые приложениями на узлах, nK перенаправляет на диск рабочей станции; собственных дисков у узлов нет. Для написания параллельных приложений программисту предлагаются API Parix ("родной" для Парситеков) и PowerMPI (MPI на базе Парикса).
Сетевой адрес нашего CC/nK: pink.csa.ru. Документацию на Парситек и MPI на сайте ЦСТ ищите здесь, здесь и здесь.
Изложенный далее материал применим не только к CC/nK, но так или иначе, ко всем массивно-параллельным системам. Кроме того, программистам-системщикам он (материал) будет (втайне надеюсь) крайне полезен для расширения общего кругозора.
В состав пакета MPICH (базовая, свободно распространяемая реализация MPI, на Парситек не перенесена) входит нечто под названием MPE (возможно, это расшифровывается как "MPI Extensions"). Вот выдержка из его README-файла:
This directory contains useful routines that are not part of MPI but that may be useful in writing and evaluating the performance of parallel programs. They include (so far) timing routines logging routines real-time graphics routines parallel I/O routinesMPE включает 3 примера параллельных вычислений с рисованием вычисленного: кривая Мандельброта, "жизнь" (моделирование биологической колонии), и нечто непонятное под названием Mastermind. Примеры сами по себе достаточно невзрачные (особенно по сравнению с рекламными MPEG-ами от Silicon Graphics;), но они, во всяком случае, скажем так, выдержаны в политически перспективном ключе. Это и есть та причина, по которой в этом документе появился данный раздел.
MPE предлагает программисту формальные правила для работы параллельного приложения с X-Window. Например, вызов функции XCreateWindow (создание окна) должен выполняться так, чтобы на дисплее появлялось одно окно, а не несколько (по числу ветвей в параллельном приложении). В то же время, идентификатор созданного окна должен быть доступен не только в ветви-создателе, но и в остальных ветвях, чтобы предоставить им возможность самостоятельно работать с окном (рисовать свой фрагмент общей картинки, и т.д.). В то же время, за ветвями должна сохраняться возможность создавать окна для "личных" нужд.
Для построения MPE требуются:
В состав Парикса не входит специальной версии библиотеки Xlib, с помощью которой Parix-приложения могли бы работать с X-Window. На Парситеках CC/AIX, где на вычислительных узлах установлен AIX (Юникс от IBM), каждая часть (ветвь) параллельного приложения, являясь в то же время полноправным процессом AIX'a, может пользоваться библиотеками AIX'a, в том числе и Xlib'ом. Почему Xlib трудно скомпилировать для CC/nK ?
Демонстрация должна состоять из двух частей: вычислительной и рисующей. Вычислительная часть, выполняющаяся на узлах, должна быть в явном виде распараллелена с использованием MPI. Рисующая часть, запускаемая на рабочей станции, должна получать подготовленные картинки от вычислительной одним из доступных способов, например, через файлы на диске, или через сокет, или через каналы именованные/неименованные.
В результате поисков в Интернете для дальнейшего рассмотрения были отобраны два пакета:
Известный пакет PovRay, вопреки слухам, не оказался параллельным (никаких следов MPI, PVM, многопоточности или чего-то в этом роде в его исходных текстах не обнаружено).
PGL отложен в сторону, так как:
Архив ray120998.tar.gz поврежден - некоторые подкаталоги в нем отсутствуют. Из-за потерянных подкаталогов половина примеров не запускается. Пакет содержит только вычислительную часть, которая производит файлы TGA-формата (студии Targa TrueVision когда-то были очень популярны...); рисующую часть я написал сам.
Главное внесенное в Ray изменение: переделана процедура записи построенного изображения в файл.
Добавления в файле ~ray/raysrc/tgafile.c:
void maketgafile( scenedef *scene ) /* EBCEEB */ { # define tgaHeadSize 18 static char tgaHeader[tgaHeadSize] = { 0, /* IdLength */ 0, /* ColorMapType */ 2, /* ImageTypeCode */ 0, /* ColorMapOrigin, low byte */ 0, /* ColorMapOrigin, high byte */ 0, /* ColorMapLength, low byte */ 0, /* ColorMapLength, high byte */ 0, /* ColorMapEntrySize */ 0, /* XOrigin, low byte */ 0, /* XOrigin, high byte */ 0, /* YOrigin, low byte */ 0, /* YOrigin, high byte */ 0, /* 12 <-- Width, low byte */ 0, /* 13 <-- Width, high byte */ 0, /* 14 <-- Height, low byte */ 0, /* 15 <-- Height, high byte */ 24, /* ImagePixelSize */ 0x20 /* ImageDescriptorByte: 0x20 == flip vertically */ }; int nwritten; FILE *f = NULL; unsigned char *filebuf; register unsigned char *src, *dest; register unsigned int pt; unsigned int total = scene->hres * scene->vres; /*\ Create file \*/ if( !scene->outfilename || (f=fopen(scene->outfilename,"w+b"))==NULL) { char msgtxt[2048]; sprintf( msgtxt, "Cannot create \"%s\" for output!", scene->outfilename ); rt_ui_message(MSG_ERR, msgtxt); rt_ui_message(MSG_ABORT, "Rendering Aborted."); exit(1); } setvbuf( f, NULL, _IONBF, 0 ); /*\ Allocate heap buffer \*/ if(( filebuf = malloc( tgaHeadSize + total * 3 )) == NULL ) { rt_ui_message(MSG_ERR,"writetgaregion: failed malloc()!\n"); return; } /*\ Prepare header \*/ memcpy( filebuf, tgaHeader, tgaHeadSize ); filebuf[12] = scene->hres & 0xff; /* Width, low byte */ filebuf[13] = (scene->hres >> 8) & 0xff; /* Width, high byte */ filebuf[14] = scene->vres & 0xff; /* Height, low byte */ filebuf[15] = (scene->vres >> 8) & 0xff; /* Height, high byte */ /*\ Convert points \*/ for( src = scene->img + total * 3, dest = filebuf + tgaHeadSize, pt = total; pt > 0; pt-- ) { *dest++ = *--src; *dest++ = *--src; *dest++ = *--src; } /*\ Write to disk \*/ if (( nwritten = fwrite( filebuf, tgaHeadSize + 3 * total, 1, f ) ) != tgaHeadSize + 3 * total ) { char msgtxt[256]; sprintf(msgtxt,"File write problem, %d bytes written.\n",nwritten); rt_ui_message(MSG_ERR, msgtxt); } /*\ All done \*/ free( filebuf ); fclose( f ); } /* maketgafile */Прототип maketgafile() помещен в tgafile.h
Изменения в файле ~ray/raysrc/render.c, функция renderio:
#if 1 /* EBCEEB */ maketgafile( scene ); #else /* this is old text */ createtgafile(scene->outfilename, (unsigned short) scene->hres, (unsigned short) scene->vres); outfile = opentgafile(scene->outfilename); writetgaregion(outfile, 1, 1, scene->hres, scene->vres, scene->img); closetgafile(outfile); #endifТеперь запись картинки производится не построчно, а за один вызов fwrite(). Как итог, скорость работы тестового примера на CC/nK увеличилась почти в 10 раз. Этому можно предложить 2 взаимодополняющих объяснения:
В стандартном Си существуют два способа работы с файлами: через файловые переменные (функции fopen, fwrite, fread, fputs, fprintf, fseek, fflush, fclose, еклмн) и через файловые номера (open, create, read, write, lseek, close и т.д.). Второй способ примитивнее и быстрее. Однако пользоваться на CC/nK (на CC/AIX не пробовал) им нельзя, так как write не перенаправляется на диск рабочей станции!!
Базовый Ray использует следующую схему распараллеливания: главная программа пишется как последовательная, без упоминаний об MPI. Таким образом, во всех ветвях сначала выполняется одно и то же. MPI используется только внутри функции построения кадра rt_renderscene(). Коллективом из N процессоров каждый кадр строится почти в N раз быстрее, чем на одиночном процессоре.
Однако с увеличением числа процессоров/ветвей увеличивается и количество приемопередач, а следовательно, и задержка, обусловленная низкой латентностью внутренней сети (см.выше). Из-за этого выигрыш от увеличения количества процессоров становится нелинейным, и, с какого-то момента - неоправданно малым.
Поэтому Ray строился в модифицированном виде (используемая для условной компиляции этого варианта переменная препроцессора названа мною MPI_DUMMY):
Здесь находится исправленный под MPI_DUMMY исходный текст примера ~ray/demosrc/mainanim.c (запускаемый файл называется ~ray/compile/???-dummy/animray). AnimRay изготавливает вот такие симпатичные картиночки:
Все-таки, последовательно-параллельная схема не может быть заменена параллельно-последовательной в двух наиболее частых ситуациях:
Может показаться странным, что для демонстрации идеи был выбран изначально параллельный Ray, перестроенный в последовательном режиме. Тому причин две:
Файл ~ray/unix/makearch, начало:
MPIDIR=/export/home/parix/mpi # EBCEEB UTILBIN=/export/home/il/epxbin # EBCEEBФайл ~ray/unix/makearch, после раздела "default":
parix-mpi-dummy: $(MAKE) all \ "ARCH = parix-mpi-dummy" \ "CC = $(MPIDIR)/bin/mpicc" \ "CFLAGS = -O2 -DParix -DMPI_DUMMY" \ "AR = $(UTILBIN)/px_ar" \ "ARFLAGS = r" \ "STRIP = $(UTILBIN)/px_strip" \ "RANLIB = $(UTILBIN)/px_ranlib" \ "LIBS = -L. -lmgf -lray -lm" parix-mpi: $(MAKE) all \ "ARCH = parix-mpi" \ "CC = $(MPIDIR)/bin/mpicc" \ "CFLAGS = -O2 -DParix -DMPI" \ "AR = $(UTILBIN)/px_ar" \ "ARFLAGS = r" \ "STRIP = $(UTILBIN)/px_strip" \ "RANLIB = $(UTILBIN)/px_ranlib" \ "LIBS = -L. -lmgf -lray -lm"
Поскольку все команды, задаваемые в Make-файле, должны быть однословными (вложенные одинарные кавычки не срабатывают, так как не раскрываются перед запуском их содержимого), а в Париксе они требуют префикса px, мне пришлось сочинить некоторое множество сценариев примерно следующего содержания:
# This is file /export/home/il/epxbin/px_strip px strip $*
-o PATH_PREFIX
Задает начало имени для создаваемых файлов-картинок.
По умолчанию: "/tmp/outfile". Это значит, что файлы будут иметь
имена /tmp/outfile.0000.tga, /tmp/outfile.0001.tga, и т.д.
-v
Включает вывод на консоль уведомляющего сообщения после генерации каждого кадра.
По умолчанию: сообщение выводится только после генерации всех кадров.
-t
В каждом уведомляющем сообщении будет выводиться время,
прошедшее с момента начала работы. Используйте этот ключ
вместо команды /bin/date, так как в последнем случае
ко времени счета будет добавлено время загрузки/выгрузки
приложения, в данном случае интереса не представляющее.
-n NN
Задает количество картинок. По умолчанию: NN = 100.
TGA Player - это графическое приложение, которое запускается на рабочей станции, отслеживает появление файлов-кадров в указываемом каталоге и рисует их. Проигрыватель работает одновременно с вычислительной частью, которая написана на Ray'e. Запускаться обе части могут, например, так:
#!/bin/csh setenv LD_LIBRARY_PATH /usr/openwin/lib # Important for Solaris!! if( ! $?DISPLAY ) setenv DISPLAY argon.csa.ru rm -f /tmp/outfile.????.tga tgaview -np 4 -n 100 -loop -prefix /tmp/outfile \ -caption "Real-time, 4 CPUs" & mpirun -np 4 ~il/ray/compile/mpi-dummy/animray
Проигрыватель, чтобы работать везде, не использует ни Gnome, ни KDE, ни что-либо еще. Даже Xtoolkit не используется, потому что у меня не было времени на его освоение (я до этого не писал под X-Window). Исходный текст находится здесь. К сожалению, он совершенно не откомментирован - ни времени, ни сил, ни желания для этого не было, нет и не предвидится.
Конструктивных особенностей у проигрывателя три:
Теоретически, должно без проблем осуществляться примерно следующим командным файлом:
#!/bin/sh # X11_PATH=/usr/openwin # for Solaris with OpenWindows X11_PATH=/usr/X11R6 # for Linux with XFree86 if [ ! -d ${X11_PATH} ]; then echo Edit $0 for right setting of X11_PATH !!! exit 1 fi # Path where Imlib is installed, not path to Imlib sources! IMLIB_PATH=/users/il/imlib-1.9.2 IMLIB_ADDONS= #IMLIB_ADDONS=-ljpeg -lpng -ltiff -lz -lgif cc tgaplay.c -o tgaplay \ -I/usr/local/include -I${IMLIB_PATH}/include \ -L${IMLIB_PATH}/lib \ -L${X11_PATH}/lib \ -lX11 -lXext -lImlib -lm ${IMLIB_ADDONS}До какого-то момента под Линуксом (RedHat 5.1) компоновщик при автоматическом вызове из gcc по так и невыясненным причинам не видел некоторых библиотек, но при ручном вызове все работало (список автоматически передаваемых аргументов сообщает "gcc -v"):
gcc -c tgaplay.c -I/usr/local/include && \ ld -m elf_i386 -dynamic-linker -o tgaplay \ -L/usr/local/lib \ -L/usr/X11R6/lib \ -L/usr/lib/gcc-lib/i386-redhat-linux/2.7.2.3 \ -L/usr/i386-redhat-linux/lib \ /lib/ld-linux.so.2 \ /usr/lib/crt1.o \ /usr/lib/crti.o \ /usr/lib/gcc-lib/i386-redhat-linux/2.7.2.3/crtbegin.o \ tgaplay.o -lX11 -lXext -ljpeg -lpng -ltiff \ -lz -lm -lgif -lImlib -lgcc -lc -lgcc \ /usr/lib/gcc-lib/i386-redhat-linux/2.7.2.3/crtend.o \ /usr/lib/crtn.oЭтот глюк исчез так же непонятно, как и появился. Может быть, в конец строки затесался DOS-овский символ "возврат каретки" ?
-prefix PATH_PREFIX
Это единственный обязательный аргумент.
Задает начальную часть имени всех файлов-картинок
Например, -prefix /tmp/outfile
означает, что картинки будут последоватьно грузиться из файлов
/tmp/outfile.0000.tga, /tmp/outfile.0001.tga и т.д.
-np NNODES
Количество параллельных ветвей в генерирующем картинки MPI-приложении.
Проигрывателю нужно знать его, чтобы не дергать изображение: пауза,
потом быстрый показ нескольких кадров (когда несколько ветвей одновременно
закончат запись своих файлов), потом новая пауза, новый рывок, ...
Чтобы этого избежать, проигрыватель на шаге k ожидает появления
не k+1-го файла, а файла с номером k*NNODES+NNODES.
После этого файлы k*NNODES+1 .. k*NNODES+NNODES с задержкой, равной
T/NNODES, где T - время генерации предыдущей порции файлов.
Примечание: даже если этот ключ не указан, проигрыватель
самостоятельно пытается сделать скорость показа наиболее равномерной.
-n NIMAGES
Общее количество кадров. По его достижении проигрыватель
либо останавливается, либо переходит к нулевому кадру.
(см.параметр "-loop"). По умолчанию: 32767.
-loop
Задает поведение при достижении кадра с максимальным номером:
остановка или циклический повтор. По умолчанию: остановка.
-period S
Задает интервал в миллисекундах между проверками появления новых файлов
на диске. По умолчанию: 50 миллисекунд. Не задавайте ни слишком большое,
ни слишком маленькое значение; оптимально - 10-20% от времени генерации
всех картинок, деленного на их количество.
-caption TEXT
Заголовок окна. Не забудьте кавычки, если заголовок содержит пробелы.
Пример: -caption "Real-time, 4 CPU\'s on CC/nK"
Imlib нужен единственно для того, чтобы преобразовывать 24-битное изображение для вывода на дисплей с меньшей размерностью палитры. Удивительно, но ни MS Windows, ни X-Window не содержат встроенных средств для такого преобразования.
В Imlib нет встроенной поддержки TGA-формата, поэтому я добавил ее в самом необходимом виде: только чтение, и только тот простейший вариант, которым пользуется Ray.
Ниже идут мои дополнения к файлу ~imlib/Imlib/load.c.
До функции Imlib_load_image:
int istga(FILE *f, char *fname) /* EBCEEB */ { return !strcmp( fname+strlen(fname)-4, ".tga" ); } unsigned char * _LoadTGA(ImlibData * id, FILE * f, int *w, int *h, int *transparent) { unsigned char tga[18]; # define tga_identsize tga[0] # define tga_imagetype tga[2] # define tga_colormaplength ( tga[5] + (((unsigned)tga[6]) << 8) ) # define tga_width ( tga[12] + (((unsigned)tga[13]) << 8) ) # define tga_height ( tga[14] + (((unsigned)tga[15]) << 8) ) # define tga_bits tga[16] # define tga_descriptor tga[17] char *buffer, c; unsigned bufsize, i; if( fread( &tga, 1, sizeof(tga), f ) != sizeof(tga) ) return NULL; if( tga_bits != 24 || tga_descriptor != 0x20 || tga_colormaplength || tga_imagetype != 2 || tga_identsize || tga_colormaplength ) { fprintf(stderr,"IMLIB ERROR: Invalid header\n"); return NULL; } *w = tga_width; *h = tga_height; *transparent = 0; bufsize = 3 * tga_width * tga_height; if(( buffer = malloc( bufsize )) == NULL ) { fprintf(stderr,"IMLIB ERROR: cannot malloc\n"); return NULL; } if( fread( buffer, 1, bufsize, f ) != bufsize ) { fprintf(stderr,"IMLIB ERROR: image too short\n"); free(buffer); return NULL; } for( i=0; i<bufsize; i+=3 ) { c=buffer[i]; buffer[i]=buffer[i+2]; buffer[i+2]=c; } fprintf(stderr,"DEBUG: file \"%s\", line %d\n", __FILE__, __LINE__ ); return buffer; }Функция Imlib_load_image, проверка типа файла:
... else if (istga(p,file)) /* EBCEEB */ { needs_conv = 0; fmt = 0xEBCEEB; }Функция Imlib_load_image, чтение файла:
switch (fmt) { case 0xEBCEEB: data = _LoadTGA(id, p, &w, &h, &trans); break; ...
В-принципе, благодаря Imlib, TgaPlay в состоянии проигрывать не только TGA, но и JPEG, PPM, TIFF, XPM, EIM, PNG, GIF и BMP, а так же все, для чего существуют конвертеры в один из этих форматов. Или, наоборот, можно в промежутке между конфигурированием и построением Imlib (иначе говоря, в каталоге с дистрибутивом Imlib, между "sh ./configure" и "make check") вписать в начало Imlib.h:
#undef HAVE_LIBJPEG #undef HAVE_LIBPNG #undef HAVE_LIBTIFF #undef HAVE_LIBGIFЭто сделает библиотеку менее универсальной, но более компактной. Построение упростится и ускорится, возможно, при этом исчезнут некоторые ошибки.
Imlib использует два настроечных файла: imrc ищется сначала в домашнем каталоге, затем в каталоге директория_установки_Imlib/etc; путь к файлу im_palette.pal прописан в imrc. Эти файлы (кажется) не содержат системно-зависимых данных и могут быть скопированы с машины на машину в одноименный каталог без редактирования.
В процессе инсталляции Imlib директория_установки_Imlib ...
Помните, что без настроечных файлов использующее библиотеку Imlib приложение не заработает! Диагноз: завершение с ошибкой "Imlib: required colormap not found" (цитируется по памяти) в момент вызова Imlib_init().
Первый вариант TGA Player'a был написан в среде Windows'95 с использованием двухтомника Петцольда, во-первых; и библиотеки Graphics Viewer версии 2.1 (см. описание), во-вторых. Автором библиотеки является некий Joe Oliphant. Написанная в эпоху засилья Windows 3.1, она и сейчас вполне работоспособна, если выскрести из нее всю 16-разрядность, а именно:
Факт, заслуживающий напоследок повторного упоминания: само по себе чтение TGA-файла не представляет проблемы. Единственная причина, по которой GV оказался необходим под MS Windows, а Imlib под X-Window: являющееся нетривиальным приведение TrueColor-цветов к текущей палитре дисплея не поддерживается ни той, ни другой графической системой, и в обоих случаях должно выполняться программой пользователя.
Кроме собственно вычислительных узлов, в демонстрации задействованы: внутренняя сеть, рабочая станция (ее диск, процессор, видеокарта), и, возможно, внешняя сеть (если TGA Player рисует на дисплее другой машины). Каждый из этих компонентов способен стать узким местом и сделать скорость работы фиксированной независимо от числа и скорости процессоров на CC-узлах. От системного интегратора требуется: а) выявление критически медленных аппаратных компонентов, и б) их замена.
Что известно сейчас о Sun-совместимой рабочей станции Tatung, подключенной к нашему CC/nK?
Если Tatung остается рабочей станцией для CC/nK, то его системному администратору настоятельно рекомендуется установить туда в конце концов компилятор Си для SPARC'a!
Трудоемкость нижеперечисленных пунктов сильно неодинакова и заранее неясна. Вот то, что приходит на ум в первую очередь:
31 мая 1999 | .... | первая версия документа |
7 июня 1999 | .... | поправки |
.