f717d4a723bae29d2cf7cad33a7feae9.ppt
- Количество слайдов: 18
С. С. Андреев, С. А. Дбар, А. О. Лацис, Е. А. Плоткина Некоторые проблемы реализации вычислений на FPGA- ускорителях. Научный сервис в сети Интернет Абрау-Дюрсо, 2016
О чем этот доклад. • В суперкомпьютерной отрасли идет интенсивный поиск новых вычислительных архитектур. • Мы занимаемся одной из них - гибридно-параллельными вычислительными системами с ускорителями на FPGA. • Отличие от массивно-параллельных систем, построенных исключительно из FPGA: - большая часть исходного текста остается программой на традиционном языке, но требует глубокой структурной переработки под гибридную архитектуру; - разработка схем для FPGA ограничивается изготовлением небольших по объему логики, компактных вычислительных ядер, обрабатывающих объемы данных порядка первых мегабайт. • Вопрос структурной переработки программной части приложения, с целью эффективной декомпозиции данных и выделения вычислительных ядер, вообще очень не прост, а для ускорителей на FPGA — в особенности, но в настоящем докладе не рассматривается. • Ограничимся рассмотрением языка описания самих вычислительных ядер.
Что должен уметь такой язык? • Очевидно, он должен адекватно выражать что-то такое, что и позволяет FPGA-сопроцессорам работать быстрее процессоров, несмотря даже на более низкую частоту. • Обогнать современный процессор можно только за счет гораздо более рациональной организации коммуникаций на микро-уровне. • Для этого лучше всего построить систему коммуникаций, специализированную под конкретный алгоритм. • Способ ее построения давно известен — это техника синхронных конвейеров, или SDF (Synchronous Data Flow). • Это частный случай техники Data Flow, отличающийся тем, что взаимная синхронизация потоков данных заранее рассчитана в глобальном дискретном времени, и в ходе работы схемы происходит автоматически, без дополнительных затрат. • Значит, нам нужен язык, позволяющий эффективно строить синхронные конвейеры на аппаратном уровне.
Откуда берутся такие языки? • Языки, используемые схемотехниками (VHDL, Verilog), позволяют эффективно делать на аппаратном уровне что угодно, в том числе — синхронные конвейеры. Эти языки безобразно спроектированы, и потому непригодны для постижения схемотехнической картины мира путем их изучения. Они могут быть с успехом использованы схемотехниками, которые уже имеют в голове схемотехническую картину мира, построенную другим способом, но нам — не годятся. Нам нужен язык, использование которого не требует предварительной приборостроительной подготовки. • Известны три пути решения этой проблемы: - изучив VHDL, понять скрытую в нем модель программирования, и реализовать ее по-человечески, в языке собственной разработки (Автокод Stream, ИПМ РАН); - использовать функциональный язык, существующий или спроектированный специально (Mitrion C, Mitrionics); - использовать аннотированный специальным набором директив алгоритмический язык, например, C++ (Vivado HLS, Xilinx). • Mitrion C мы не пробовали, Vivado HLS — пробовали, Автокод Stream — наша разработка, о ней и расскажем.
Автокод HDL и Автокод Stream. • Искомый язык, таким образом, един в двух лицах: он должен быть языком схемотехнической модели программирования, а в ее рамках — быть заточен под построение длинных синхронных конвейеров, выполняющих вычисления с плавающей точкой. • Язык с самого начала сознательно строился как двухуровневый, наподобие «Астры» : - базовый язык схемотехнической модели программирования — Автокод HDL, «VHDL с человеческим лицом» ; - набор языковых расширений, автоматизирующий ввод/вывод и построение синхронных вычислительных конвейеров произвольной ширины — Автокод Stream. • Автокод Stream компилируется в Автокод HDL. Все, чего нет в Автокоде Stream, и что требуется разработчику схемы, может быть написано на Автокоде HDL непосредственно в том же исходном тексте.
Упрощенная схемотехническая модель программирования (Автокод HDL). • Аналог переменной называется сигналом. Логика — трехзначная. • Текст синтаксически делится на комбинационную и триггерную (тактированную) части. • В комбинационной части присваивания однократные, времени нет. • В тактированной части время дискретно, присваивания похожи на фоннеймановские, только отложены на такт. • На протяжении такта можно выполнить сколько угодно присваиваний (все вместе занимают 1 такт), и любое число проверок любой вложенности (времени не занимают). • Действует правило единственности источника значения: сигнал может получить значение либо один раз в комбинационной части, либо один раз за такт — в тактированной. • Есть целочисленная арифметика, логика. Массив адресуемой синхронной расслоенной памяти — специальный объект.
Средства построения конвейеров (Автокод Stream). • Конвейер строится автоматически, по записи формулы в традиционном виде, включающей арифметику и условные выражения. Присваивания однократные. Ширина конвейера задается явно отдельным параметром. • Входные данные подкрашиваются сигналом разрешения записи, на выходе автоматически формируются сигналы готовности. • Формирование сигналов разрешения и обработка сигналов готовности происходят вручную, средствами базового языка, как и формирование сигналов управления доступом к блокам адресуемой памяти (блоков много!).
Модельная программа для сравнения технологий построения схем Наивная запись: for ( k = 0; k < NITER; k++ ) { for ( i = 1; i < (M-1); i++ ) { for ( j = 1; j < (N-1); j++ ) { newf[i][j] = ((f[i-1][j]+f[i+1][j])*bx 2+ (f[i][j-1]+f[i][j+1])*by 2 -rhs[i][j])*beta; } } tmpf = newf; newf = f; f = tmpf; }
Схема на Автокоде Stream (1) define(LN, 4) define(W, 51200) define(ROW 1, 256) define(SIZE, 64) mainprogram 64 Memory: : (inout f, in rhs) Block. Register: : (c_BX 2, c_BY 2, c_BETA, c_len, c_last) Register: : (niter) endprogram declare index 24 rd_f, rd_rhs Shiftstream: : (dim 2, ROW 1) SIZE buf(LN) { REAL_ROW=c_len; SHIFT_POINTS=(left=>(0, -1), right=>(0, 1), up=>(-1, 0), down=>(1, 0)); } ram SIZE f(rams, LN, W, rd_f), rhs(rams, LN, W, rd_rhs) reg SIZE c_BX 2, c_BY 2, c_BETA reg 24 c_len, c_last, niter, wr_f
Схема на Автокоде Stream (2) AUstream: : () SIZE crest(LN) { float buf(f), rhs; float const c_BX 2, c_BY 2, c_BETA; crest=((buf. up+buf. down)*c_BX 2+(buf. left+buf. right)*c_BY 2 -rhs)*c_BETA; } enddeclare // запись результатов в массив f f. addra = wr_f f. dina = crest. out do @1=0, decr(LN) f. wea[@1] = crest. rdy && crest. blockrdy[@1] enddo Background: { // в квадратных скобках - начальный сброс [ crest. we = 0 {rd_f, niter}=0 ]
Схема на Автокоде Stream (3) // синхронизация адресов чтения массива rhs if(buf. rdy == 0) rd_rhs = c_len else rd_rhs++ endif // формирование адресов для записи результатов в массив f if(crest. rdy==0) wr_f=0 else wr_f++ endif }
cycle_read: Схема на Автокоде Stream (4) { if(niter > 0) next cycle_read if(rd_f < c_last) rd_f++ crest. we=1 else rd_f=0 crest. we=0 niter-endif } wait_finish: { if(crest. rdy == 1) next wait_finish else return endif }
#include "dimension. h" Схема на Vivado HLS (1) #define MAXMY 5008 #define LOCALMEM 120000 #define FACTOR 16 void itstep_accel( long mx, long my, double pf[LOCALMEM/FACTOR][FACTOR], double pr[LOCALMEM/FACTOR][FACTOR], double rdx 2, double rdy 2, double beta, long niter ) { #pragma HLS array_partition variable=pf cyclic factor=16 dim=2 #pragma HLS resource variable=pf core=RAM_2 P #pragma HLS array_partition variable=pr cyclic factor=16 dim=2 #pragma HLS resource variable=pr core=RAM_2 P static double lower[MAXMY/FACTOR][FACTOR]; #pragma HLS array_partition variable=lower cyclic factor=16 dim=2 #pragma HLS resource variable=lower core=RAM_2 P static double middle 1[MAXMY/FACTOR][FACTOR]; #pragma HLS array_partition variable=middle 1 cyclic factor=16 dim=2 #pragma HLS resource variable=middle 1 core=RAM_2 P
Схема на Vivado HLS (2) static double middle 2[MAXMY/FACTOR][FACTOR]; #pragma HLS array_partition variable=middle 2 cyclic factor=16 dim=2 #pragma HLS resource variable=middle 2 core=RAM_2 P static double upper[MAXMY/FACTOR][FACTOR]; #pragma HLS array_partition variable=upper cyclic factor=16 dim=2 #pragma HLS resource variable=upper core=RAM_2 P int i, j, k, m, k 0, k 1, mx 1, my 1; /***/ mx 1 = mx - 1; my 1 = my - 1; for ( k = 0; k < niter; k++ ) { for ( j = 0; j < my/FACTOR; j++ ) { #pragma HLS pipeline for ( m = 0; m < FACTOR; m++ ) { #pragma HLS unroll factor=16 middle 1[j][m] = pf[j][m]; middle 2[j][m] = pf[j][m]; upper[j][m] = pf[my/FACTOR+j][m]; }
} Схема на Vivado HLS (3) k 0 = 0; k 1 = my/FACTOR; for ( i = 1; i < mx 1; i++ ) { k 0 += my/FACTOR; k 1 += my/FACTOR; for ( j = 0; j < my/FACTOR; j++ ) { #pragma HLS pipeline for ( m = 0; m < FACTOR; m++ ) { #pragma HLS unroll factor=16 lower[j][m] = middle 1[j][m]; middle 1[j][m] = upper[j][m]; middle 2[j][m] = upper[j][m]; upper[j][m] = pf[k 1+j][m]; } }
Схема на Vivado HLS (4) for ( j = 0; j < my/FACTOR; j++ ) { #pragma HLS pipeline if ( j > 0 ) pf[k 0+j][0] = ((lower[j][0]+upper[j][0])*rdx 2 +(middle 1[j-1][FACTOR-1] +middle 2[j][1])*rdy 2 -pr[k 0+j][0])*beta; for ( m = 1; m < FACTOR-1; m++ ) { #pragma HLS unroll factor=16 -2 pf[k 0+j][m] = ((lower[j][m]+upper[j][m])*rdx 2 +(middle 1[j][m-1]+middle 2[j][m+1])*rdy 2 -pr[k 0+j][m])*beta; } if ( j < my/FACTOR-1 ) pf[k 0+j][FACTOR-1] = ((lower[j][FACTOR-1]+upper[j][FACTOR-1])*rdx 2 +(middle 1[j][FACTOR-2] +middle 2[j+1][0])*rdy 2 -pr[k 0+j][FACTOR-1])*beta; } }
Выводы. Пока не показано, что аннотированный язык фоннеймановского типа допускает эффективную схемную реализацию, и способен снизить барьер вхождения программиста в вычислительную схемотехнику. Работы по совершенствованию подхода на основе проблемно-ориентированного языка схемотехнической модели программирования следует продолжать.
Спасибо за внимание. Вопросы?
f717d4a723bae29d2cf7cad33a7feae9.ppt