Рассмотрение различных приёмов программирования при фиксации ресурсов ПЛИСов на Colamo на примере решения задачи суммирования элементов массива


На примере программирования задачи суммирования элементов массива разберём некоторые приёмы программирования на языке Colamo.

Вводные замечания

Математика задачи и структуры графов вычислений её различных реализаций разбирается в другом месте. Мы же здесь сосредоточим внимание именно на том, на какие вещи нужно обращать внимание при программировании для ПЛИСов. Не надо забывать о том, что специфические для ПЛИС задачи, возникающие при адаптации программ к этой архитектуре, удобнее всего рассмотреть именно на примере таких простых программ. В данном случае важной проблемой будет распараллеливание суммирования массива в условиях фиксации ресурсов вычислительной системы, отводимой под конкретную часть вычислительной задачи. Рассмотрение будет начато несколько "издалека", с не очень удачных с точки зрения Коламо примеров, с постепенным подходом к решению поставленной задачи. Данный подход объясняется тем, что типичные ошибки в Коламо несколько другие, чем в обычных языках высокого уровня Попутно на этих примерах мы рассмотрим и некоторые другие особенности Коламо.

Постановка задачи

Рассмотрим решение задачи последовательного суммирования элементов массива NxM. Ресурс сумматоров, которые мы (по каким-то причинам)  можем отвести на эту задачу, фиксированный - 8 штук. Обращаем внимание читателя на ограниченность количества сумматоров. Как уже говорилось во вводных замечаниях к настоящему тексту, место сумматора, если мы будем рассматривать более сложные алгоритмы, может занять реализация более сложной операции (скалярной, матричной и т.п.) Поэтому читателя, знающего реальные ресурсы на ныне выпускаемых ПЛИСах, не должна удивлять кажущаяся малость цифры 8. Она может быть связана как с модельностью рассмотрения, так и с другими причинами вроде включения данной задачи в качестве составляющей более сложного алгоритма.
Когда мы говорим о количестве сумматоров, то следует иметь в виду, что никаких "сумматоров" как специализированных микросхем в ПЛИСах нет, их функцию выполняют заранее расписанные в библиотеке стандартных операций и соединённые между собой наборы элементарных процессорных элементов.

Структура, получающаяся при использовании одной накапливающей переменной

Рассмотрим вычислительную структуру1, в соответствии с которой реализовано последовательное суммирование, использующее одну накапливающую переменную, но задействующее весь выделенный ресурс - все восемь сумматоров:

Структура представляет собой группу из восьми сумматоров, соединенных последовательно, что само по себе уже показывает плохое параллельное программирование в фрагменте. Каждый сумматор структуры последовательно подсуммирует определенные элементы вектора. Видимо, что на эту структуру нельзя подавать элементы до тех пор, пока не будет выполнено суммирование на последнем, восьмом сумматоре, и не придет результат по обратной связи.
Можно, конечно, устроить конвейер на 8 потоков, но в программе это потребует не одну накапливающую переменную, а 8, и при этом в конце концов эти потоки нельзя, оставаясь в рамках описанной структуры, связать - получится суммирование 8 разных массивов или частей массивов. В результате этого структура будет большую часть времени простаивать. Суммирование с помощью такой структуры будет таким же "быстрым", как обычное суммирование на одном сумматоре, поскольку в каждый момент времени будет работать только один из них, но совершенно непонятно, зачем тогда занято столько оборудования, которое простаивает зря. Поэтому при создании структуры необходимо минимизировать длину обратной связи.

Правильная структура организации суммирования

Рассмотрим более правильную (связную по потокам и не простаивающую) вычислительную структуру1:

Данная структура во многом похожа на предыдущую. Здесь также используются 8 сумматоров, но последний сумматор имеет обратную связь, на которой стоит регистр для хранения промежуточных сумм. Этот сумматор выполняет подсуммирование результатов. Главное отличие данной структуры от предыдущей в том, что на эту структуру элементы массива могут подаваться по мере их поступления, и сумматоры работают все время, а не ждут когда придет результат по обратной связи. Результат задерживается только на одном, последнем сумматоре, где ему надо ждать вычисления предыдущей частной суммы, а суммирование на всех остальных сумматорах выполняется аналогично предыдущей структуре. Если бы задержка на сумматоре составляла 1 такт, то структура работала бы в потоке и давала бы возможность увеличить длину "конвейера" всех вычислений2. В дальнейшем будут рассмотрены способы, которые позволят обеспечить плотный поток для таких структур. В отличие от предыдущей, данная структура работает в 8 раз3 быстрее и эффективнее, поскольку не простаивает.

Заметим также, что порядок суммирования в ветви, состоящей из 7 сумматоров и подающей результат на последний сумматор, не так важен - лишь бы не было петель в графе потоков данных. Поэтому в качестве подающих ветвей могут быть рассмотрены разные структуры1, запрограммировапнные на основе только что разобранной. К ним мы сейчас и обратимся, а записаны они будут на Colamo. Попутно мы увидим некоторые приёмы программирования на этом языке.

1Под структурой здесь везде понимается набор вычислительных ресурсов (в данном случае сумматоров) ПЛИС, со связями между ними. Она не имеет ничего общего со структурами данных в обычных языках (типа Си, Паскаля и т.п.)
2 Вообще говоря, сумматоры в ПЛИСах сами по себе являются конвейерами. Например, количество ступеней в 32-битной реализации суммирования с плавающей запятой на ПЛИС типа Virtex4 равно 18.
3Эта оценка, как и утверждение "не простаивает", не учитывает использования конвейерности самих сумматоров (см. предыдущее примечание). Ясно, что при полной синхронизации потоков данных увеличение производительности может быть гораздо большим. (8*18=144). Однако для этого, даже если б была проведена виртуозная синхронизация потоков с работой сумматоров,  необходимо сведение разнесённых на 1 такт потоков результатов в одну сумму, что требует дополнительной вычислительной структуры, которой мы на рисунке не видим.

Последовательное суммирование всего массива

Для начала, однако, прежде чем использовать правильную структуру, использующую 8 сумматоров, рассмотрим пример программы, реализующей последовательное суммирование элементов по всему массиву. Она будет использовать только 1 сумматор, что видно на рисунке.

Program SimpleSumArray;

include MylibNewElement;
// описание переменных
Var N, M : Integer Mem;
Var I,J, N1, M1 : Number;
Var ResSum : Real Mem;
Var Sum : Real reg;
//инициализация переменных и констант
Define N = 10; // размерность векторной составляющей массива
Define M = 51; // размерность потоковой составляющей массива
Define N1 = N-1; // последний индекс векторной составляющей массива
Define M1 = M-1; // последний индекс потоковой составляющей массива
Define Sum = 0;
// описание суммируемого массива
Var Arr : Array Real [10: Vector, 51: Stream] Mem;
CADR SimpleSum;
For J := 0 to M1 do
For I := 0 to N1 do
begin
Sum := Sum +Arr[I,J];
end;
ResSum := Sum;
ENDCADR;

End_Program.

Здесь N и M - размерности массива, N1 и M1 - индексы для организации циклов. ResSum - мемориальная переменная для хранения результата суммирования. Sum - регистровая переменная для организации подсуммирования. Организованы два вложенных цикла: первый - по элементам потоков, второй - по элементам вектора. Структура вычислений в этом примере реализуется следующим образом: стоит один регистр, один сумматор, затем - мультиплексор, который выбирает нужные элементы массива, поступление которых на вход мультиплексора регулируется с помощью задержек. Подчеркнём, что это регулирование выполняется в кадре транслятором автоматически, с учётом типов размерности массива Arr. Само собой, что при такой простоте программы реализация самого медленного способа суммирования останется таковой и при реализации на ПЛИСах: параллельная (по векторной размерности массива) подача потоков не даст нам никакого выигрыша, они будут простаивать очень долго, пока не истекут необходимые времена задержек..


Далее мы не будем возвращаться к последовательному суммированию по всему массиву, а будем рассматривать только две схемы иерархического суммирования: последовательное и пирамидальное (по разным частям массива).

Пирамида сумматоров (сдваивание)

Теперь рассмотрим суммирование элементов массива с помощью пирамиды сумматоров.

CADR Test_1;

for j := 0 to N/8-1 do
begin
for i := 0 to M-1 do
begin
for k := 0 to 8-1 do
begin
in_com[k]:=x[j*8+k,i];
end;
Sum_pir (in_com, out_com);
sum := sum + out_com;
end;
end;

ENDCADR;

Здесь используется подкадр Sum_pir, в котором организована пирамида из семи сумматоров, а в качестве входного параметра подается массив чисел:

SubCadr Sum_pir (In : in_com; Out : out_com);

Var L : Integer Reg;
var com0, com1, com2, com3, com4, com5: real com;
com0 := in_com[0] + in_com[1];
com1 := in_com[2] + in_com[3];
com2 := in_com[4] + in_com[5];
com3 := in_com[6] + in_com[7];
com4 := com0 + com1;
com5 := com2 + com3;
out_com := com4 + com5;

EndSubCadr;

Результат суммирования out_com, получаемый после выполнения подкадра на каждой итерации цикла, подсуммируется со значением, которое содержится в регистровой переменной sum.

Знакомые с приёмом сдваивания, наверное, сразу заметили, что хоть сумматоров в подающей пирамиде и 7, но используются они в рабочем подкадре не одновременно, а (при прохождении по пирамиде сдваивания) сначала 4, потом 2, потом последний в пирамиде. Поэтому суммирование пирамидой само по себе в этом варианте не даст полной эффективности и должно использоваться с чем-то ещё.  Это "что-то ещё" - поток данных, связанный со второй размерностью, превращающий описанную структуру кадра в конвейер.  Мы видим, что, однако, этот конвейер не сможет использовать конвейерность самих операций суммирования, поскольку задержка между суммированиями на последнем (восьмом) сумматоре явно будет не меньше, чем время выполнения полного суммирования, и это не даст запустить  потоки данных на пирамиде 7 сумматоров с минимальной задержкой.

В данной программе как бы подразумевается, что у нас векторная составляющая потока данных имеет размерность 8. Поэтому далее нам надо рассмотреть приёмы, связанные с обработкой потоков большей "ширины".

Последовательное суммирование элементов массива

Рассмотрим следующий пример программы, реализующей последовательное суммирование элементов массива.

Program Sum_posledovatelno;
include MyLibNewElement;
VAR x : array real [10 : Vector, 20 : Stream] mem;
VAR summa : real mem;
VAR i, j, chan, chan_m, chan_p, n : number;
VAR cm1 : real com;
// переменные Let'а
VAR xl : array real [10 : Vector] com;
VAR res : real reg;
VAR s : real com;
Define chan=10;
Define chan_p=2; // 10/8+1
Define chan_m=8;
Define n=20;
Define summa=0;
Let A(in: xl, s; out: res);

VAR yl : array real [8 : Vector] com;
VAR rg : real reg;
rg:=s;
for j:=0 to chan_p-1 do
begin
yl[0]:=xl[j*chan_m];
for i:=1 to chan_m-1 do
begin
if i+j*chan_m <= chan then
yl[i]:=xl[i+j*chan_m]+yl[i-1];
else
yl[i]:=yl[i-1];
end;
rg:=rg+yl[chan_m-1];
end;
res:=rg;

endlet;
CADR Sum1;

cm1:=0;
for i:=0 to n-1 do
begin
A(in:x[*,i], cm1; out:cm1);
end;
summa:=cm1;

ENDCADR;

В кадре Sum1 организуется цикл по элементам потока. В теле цикла вызывается структура A, которая задана с помощью конструкции Let. В качестве входных параметров структура1 A принимает элементы входного массива x - вектор за вектором, а так же переменную cm1. В качестве результата структура A возвращает модифицированную переменную cm1, которая сохраняется в мемориальной переменной summa.

Рассмотрим структуру A. Сначала выполняется инициализация регистровой переменной rg, которая необходима для подсуммирования результатов. В нее записывается входной параметр s, который в начальный момент времени равен нулю. Далее организуется цикл по переменной j до переменной chan_p, которая определяет количество порций каналов по 8. В данном случае таких порций две: в одной восемь каналов, в другой - два. В данном цикле организован цикл по переменной i до переменной chan_m, которая задана константой и равна 8. Это количество каналов в j-й порции. Если номер канала меньше 10, поскольку это общее количество каналов, то выполняется подсуммирование с предыдущим результатом.

При такой реализации каналы, номера которых превышают 10, не заполняются нулями и вместо значения канала выдается значение y1 на предыдущем шаге. 

Использование пирамиды сумматоров

Рассмотрим теперь снова пример программы, реализующей суммирование элементов массива (но уже с векторным потоком не размерности 8, а большей) с использованием пирамиды сумматоров.

Let A(in: xl, s; out: res);

VAR yl : array real [4 : Vector] com;
VAR rg : real reg;
VAR c1, c2, c3 : real com;
rg:=s;
for j:=0 to chan_p-1 do
begin
for i:=0 to chan_m/2-1 do
begin
if 2*i+j*chan_m <= chan then
yl[i]:=xl[2*i+j*chan_m]+xl[2*i+1+j*chan_m];
else
yl[i]:=0;
if 2*i+1+j*chan_m <= chan then
yl[i]:=xl[2*i+j*chan_m]+xl[2*i+1+j*chan_m];
else
yl[i]:=xl[2*i+j*chan_m];
end;
c1:=yl[0]+yl[1];
c2:=yl[2]+yl[3];
c3:=c1+c2;
rg:=rg+c3;
end;
res:=rg;

endlet;

В данной программе основной кадр Sum1 полностью аналогичен тому, который использовался в предыдущем примере, поэтому мы не будем его приводить. Рассмотрим структуру A, которая реализует суммирование с помощью пирамиды сумматоров. Структура A во многом аналогична соответствующей структуре предыдущего примера. В то же время, цикл по переменной i в данном случае выполняется до значения chan_m/2. В теле цикла используются два оператора if, которые выполняют суммирование элементов в зависимости от их номеров в массиве. Далее организуется пирамида сумматоров и выполняется подсуммирование результата, полученного с пирамиды сумматоров, с предыдущим результатом. Результат накапливается в регистровой переменной rg.

Недостаток программы - в низком классе программирования. Это касается фрагмента кода, который реализует пирамиду сумматоров в структуре A. При использовании такого подхода (ручное суммирование оператор за оператором) программа реализации пирамиды, состоящей из нескольких сотен сумматоров, будет очень громоздкой.

И снова - последовательное суммирование элементов массива

Рассмотрим пример программы, реализующей последовательное суммирование элементов массива.

program Test_5;//Сумма последовательно

//const Num = 800;//8 * 100
var A : array integer [8:vector, 100:stream] mem;
var Sum : integer reg;
var C:array integer [7:vector] com;
var I, j, Num : number;
define Sum = 0;
define Num = 800;
cadr cadr1
for I := 0 To Num / 8 - 1 do begin
c[0] := A[0, i] + A[1, i];
for j := 1 to 6 do
c[j] := c[j - 1] + A[j + 1, i];
Sum := Sum + c[6];
end;
endcadr;

end_program.

Для данного примера массив разделен на восемь. Сначала вычисляется результат для самого первого сумматора последовательности c[0], а дальше выполняется последовательное суммирование. Для накопления результата используется регистровая переменная sum. Стоит отметить, что в данной программе не используется оператор if. Программа компактная и легко читается, и полностью соответствует описанной выше структуре. Вместе с тем в ней остались проблемы, связанные с фиксированным заданием первой размерности суммируемого массива.

Возвращаемся к пирамиде сумматоров

Рассмотрим пример программы, реализующей суммирование элементов массива с использованием пирамиды сумматоров.

program Test_4;//Сумма пирамидой

//const Num = 800;//8 * 100
var A : array integer [8:vector, 100:stream] mem;
var Sum : integer reg;
var C:array integer [7:vector] com;
var I, num : number;
define Sum = 0;
define Num = 800;
cadr cadr1
for I := 0 To Num / 8 - 1 do begin
c[0] := A[0, i] + A[1, i];
c[1] := A[2, i] + A[3, i];
c[2] := A[4, i] + A[5, i];
c[3] := A[6, i] + A[7, i];
c[4] := c[0] + c[1];
c[5] := c[2] + c[3];
c[6] := c[4] + c[5];
Sum := Sum + c[6];
end;
endcadr;

end_program.

Недостаток программы - в низком классе программирования. По сути, эта программа унаследовала недостатки двух предыдущих, вместе взятые (мы их обозначили красным цветом). Такой подход (ручное, по сути, сдваивание) не годится для случая, когда пирамида состоит из множества сумматоров (порядка нескольких сотен).

Комбинация пирамиды и последовательности

Теперь, после рассмотрения разных по стилю и не лишённых недостатков программ, посмотрим всё же, как реально ограничить использование ресурса (сумматоров), не фиксируя при этом размерности массива и при этом используя ресурс с максимальной эффективностью.

4 сумматора

Рассмотрим пример программы, реализующей последовательное или пирамидальное сложение с использованием 4-х сумматоров:

Program SimpleSumArray;

include MylibNewElement;
// описание переменных
Var N, M : Integer Mem;
Var I,J, N1, M1 : Number;
Var ComSum : Real Com;
Var ResSum : Real Mem;
Var Sum : Real reg;
//инициализация переменных и констант
Define N = 10; // размерность векторной составляющей массива
Define M = 51; // размерность потоковой составляющей массива
Define N1 = N-1; // последний индекс векторной составляющей массива
Define M1 = M-1; // последний индекс потоковой составляющей массива
Define Sum = 0;
Define ComSum = 0;
// описание суммируемого массива
Var Arr : Array Real [10: Vector, 51: Stream] Mem;
Var Arg1, Arg2, Arg3, Arg4 : Real Com;
SubCadr Sum4Vect(in :FirstEl,SecondEl,ThirdEl,FourthEl);
Var Sm12, Sm34 : Real Com;
// естественный порядок следования
//Result := FirstEl+SecondEl+ThirdEl+FourthEl;
// пирамида сумматоров
Sm12 := FirstEl+SecondEl;
Sm34 := ThirdEl+FourthEl;
Result := Sm12 + Sm34;
endSubCadr;

Здесь использован подкадр, в котором реализовано либо последовательное суммирование (закомментировано в программе), либо пирамида сумматоров. Оба варианта используют 3 сумматора. Однако возникает вопрос насчет естественного порядка при последовательном суммировании. Где гарантия, что будет соблюден естественный порядок? В данном случае лучше использовать скобки, которые жестко зададут естественный порядок суммирования элементов (Result := ((FirstEl+SecondEl)+ThirdEl)+FourthEl;).

Далее приведен кадр для подачи данных в подкадр Sum4Vect и подсуммированием на четвёртом сумматоре, причем с возможностью масштабирования для любой размерности массива:

CADR SimpleSum;

For I := 0 to N1 Step 4 do
begin
For J := 0 to M1 do
begin
if I+1 > N1 then
begin
Arg1 := Arr[I,J];
Arg2 := 0;
Arg3 := 0;
Arg4 := 0;
end
else
if I+2 > N1 then
begin
Arg1 := Arr[I,J];
Arg2 := Arr[I+1,J];
Arg3 := 0;
Arg4 := 0;
end
else
if I+3 > N1 then
begin
Arg1 := Arr[I,J];
Arg2 := Arr[I+1,J];
Arg3 := Arr[I+2,J];
Arg4 := 0;
end
else
begin
Arg1 := Arr[I,J];
Arg2 := Arr[I+1,J];
Arg3 := Arr[I+2,J];
Arg4 := Arr[I+3,J];
end;
Sum := Sum +Sum4Vect(Arg1, Arg2, Arg3,Arg4);
end;
end;
ResSum := Sum;
ENDCADR;

End_Program.

Такой пример был рассмотрен на семинаре, где задачу суммирования рассматривали специалисты-схемотехники. Мы же здесь, отметив, что выбор альтернативы в кадре происходит внутри двойного цикла, наверное, могли бы с позиции опыта программирования на традиционных языках высокого уровня посчитать, что более экономным (в отношении не сумматоров, а других элементов ПЛИС) будет другое написание кадра:

CADR SimpleSum;

For I := 0 to N1 Step 4 do
begin
if I+1 > N1 then
For J := 0 to M1 do
begin
Arg1 := Arr[I,J];
Arg2 := 0;
Arg3 := 0;
Arg4 := 0;
Sum := Sum +Sum4Vect(Arg1, Arg2, Arg3,Arg4);
end
else
if I+2 > N1 then
For J := 0 to M1 do
begin
Arg1 := Arr[I,J];
Arg2 := Arr[I+1,J];
Arg3 := 0;
Arg4 := 0;
Sum := Sum +Sum4Vect(Arg1, Arg2, Arg3,Arg4);
end
else
if I+3 > N1 then
For J := 0 to M1 do
begin
Arg1 := Arr[I,J];
Arg2 := Arr[I+1,J];
Arg3 := Arr[I+2,J];
Arg4 := 0;
Sum := Sum +Sum4Vect(Arg1, Arg2, Arg3,Arg4);
end
else
For J := 0 to M1 do
begin
Arg1 := Arr[I,J];
Arg2 := Arr[I+1,J];
Arg3 := Arr[I+2,J];
Arg4 := Arr[I+3,J];
Sum := Sum +Sum4Vect(Arg1, Arg2, Arg3,Arg4);
end
end;
ResSum := Sum;
ENDCADR;

а для других архитектур могли бы ещё наэкономить, разбив внешний цикл на основную часть (делящуюся на 4) и остаток, в котором и сосредоточились бы все проверки условий. Однако будет ли подобная оптимизация действительно экономной для ПЛИС?

Оказывается, нет.
В приведенном первым фрагменте написано более правильно. Это связано с особенностью структурной реализации условного оператора: при структурной реализации оператора условного выбора if - then - else структурно реализуются обе ветки условия (и для then, и для else), а на выход выдается результат с мультиплексора, объединяющего результат вычислений, на управляющий вход которого подаётся результат вычисления условия if (как и в примере из описания языка Коламо). Дополнительно к этому, все вычисления в теле кадра выполняются с максимально возможной степенью параллелизма, если между ними нет информационной зависимости. Поэтому для приведенного примера, где I - векторная компонента, а J - потоковая, произойдет распараллеливание внутреннего цикла, содержащего условие, по числу векторных компонент внешнего цикла (т.е. при N1 = 9 будет 3 внутренних цикла, каждый из которых содержит условие из 4 веток. Итого - циклов 3, условий 3, подкадр 1). В предлагаемом в качестве более экономной альтернативы втором варианте произойдет распараллеливание оператора условия, содержащего 4 цикла и вызовы структурной составляющей подкадра Sum4Vect, что приведет к существенному увеличению занимаемого аппаратного ресурса (при N1 = 9 будет 3 условия, каждое из которых содержит 4 цикла и 4 подкадра. Итого - циклов 12, условий 3, подкадров 12). Скорость выполнения обоих рассматриваемых вариантов одинакова, но второй вариант занимает вычислительный ресурс в 4 раза больше, чем первый и поэтому обладает меньшей эффективностью. При программировании на Colamo нельзя пользоваться общепринятыми для фон-неймановских архитектур критериями эффективности программы, поскольку в расчет кроме компактности записи и скорости реализации необходимо также принимать критерий используемого ресурса оборудования. Поэтому важно вовремя остановиться на скользком пути оптимизации в стиле последовательного программирования, когда имеешь дело с программированием для ПЛИСов.

8 сумматоров

Ниже приведен пример программы, реализующей последовательное и пирамидальное сложение с использованием 8-ми сумматоров:

Program SumArray_8Sum;

include MylibNewElement;
// описание переменных
// размерность массива
Var N, M : Integer Mem;
// I,J,K - переменные цикла, N1,M1 - граничные индексы суммируемого массива
Var I,J, K, N1, M1 : Number;
// ячейка памяти для результата
Var ResSum : Real Mem;
// регистровая переменная для накопления суммы
Var Sum : Real reg;
//инициализация переменных и констант
Define N = 10; // размерность векторной составляющей массива
Define M = 51; // размерность потоковой составляющей массива
Define N1 = N-1; // последний индекс векторной составляющей массива
Define M1 = M-1; // последний индекс потоковой составляющей массива
Define Sum = 0;
// описание суммируемого массива
Var Arr : Array Real [10: Vector, 51: Stream] Mem;
// описание вспомогательного массива для суммирования строго по 8 элементов
Var Stream18VectorArr : Array Real [1:Stream, 8:Vector] Com;
// подкадр-функция, выполняющая суммирование 8 элементов массива на 7 сумматорах
SubCadr Sum8Vect(in :Stream1Vector8);
Var Sm01, Sm23, Sm45, Sm67, Sm0123, Sm4567 : Real Com;
Var Stream1Vector8 : Array Real [1:Stream, 8:Vector] Com;
// естественный порядок следования
//Result := Stream1Vector8[0,0]+Stream1Vector8[0,1]+Stream1Vector8[0,2]+Stream1Vector8[0,3]+
//Stream1Vector8[0,4]+Stream1Vector8[0,5]+Stream1Vector8[0,6]+Stream1Vector8[0,7];
// пирамида сумматоров
Sm01 := Stream1Vector8[0,0]+Stream1Vector8[0,1];
Sm23 := Stream1Vector8[0,2]+Stream1Vector8[0,3];
Sm45 := Stream1Vector8[0,4]+Stream1Vector8[0,5];
Sm67:= Stream1Vector8[0,6]+Stream1Vector8[0,7];
Sm0123 := Sm01 +Sm23;
Sm4567 := Sm45 +Sm67;
Result := Sm0123 + Sm4567;
endSubCadr;
// основной кадр суммирования массива
CADR Sum8;
// цикл по векторной составляющей массива с шагом 8 для суммирования по 8 элементов
For I := 0 to N1 Step 8 do
begin
// цикл по потоковой составляющей массива
For J := 0 to M1 do
begin
// цикл по заполнению данных для подкадра
For K := 0 to 7 do
if I+K > N1 then
Stream18VectorArr[0,K] := 0
else
Stream18VectorArr[0,K] := Arr[I,J];
// накопление суммы в регистре
Sum := Sum +Sum8Vect(Stream18VectorArr);
end;
end;
// запись итоговой суммы в ячейку памяти
ResSum := Sum;
ENDCADR;

End_Program.

Как и в случае с пирамидой суммирования из 4 сумматоров, в данной программе на первый взгляд может показаться не вполне экономной организация заполнения данных для основного подкадра, использующего 7 сумматоров из 8, но, как и в пирамиде для 4 сумматоров, это только кажется привыкшим к традиционному стилю программирования. Вместе с тем отметим, что эта проблема не настолько важна, как проблема универсальной настраиваемой масштабируемости по ресурсам, которые у нас будут в распоряжении. В этом плане, конечно, данная программа выглядит не так коряво, как уже рассмотренная выше, но по сути в ней осталась заложена немасштабируемость ресурсов. Хотя главной задачей для нас здесь при рассмотрении примеров было понять, как вообще ограничиваются используемые в ПЛИС ресурсы при программировании, отметим, что проблема масштабируемости не так сложна, как кажется. Скажем, при программировании пирамиды на 16 сумматоров можно использовать приведённую ниже программу (причём мы видим, что в ней число 16 вовсе не существенно - вместо него может быть параметр, равный как 1024, так и 220 и даже более):

Program Piramida_Summatorov;
include MyLibNewElement;

VAR a : array real [16:vector, 100:stream] mem;
VAR res : real mem;
VAR i, j, k, l, m, n, s, kk, vect, vectd2 : number;
VAR cm1, cm2, cm3, cm4, cm5, cm6 : real com;
VAR c : array real [8:vector, 8:vector] com;
VAR rg : real reg;

Define vect=16;
Define vectd2=7;//vect/2-1
Define n=100;
Define m=3;
Define k=0;
Define kk=0;
Define l=8;//vect/2

Cadr ev;
for s:=0 to n-1 do
begin
    for i:=0 to vectd2 do
    begin
        c[0,i]:=a[k,s]+a[k+1,s];
        k:=k+2;
    end;
    for j:=1 to m do
    begin
        kk:=0;
        l:=l/2;
        for i:=0 to l-1 do
        begin
            c[j,i]:=c[j-1,kk]+c[j-1,kk+1];
            kk:=kk+2;
        end;
    end;
    rg:=rg+c[m,vectd2];
end;
res:=rg;
EndCadr;
End_Program.

Как и в примерах, приведённых ранее, в этой программе при программировании последовательно-пирамидального суммирования один из сумматоров задействуется на подсуммировании результатов пирамид, а сами пирамиды задействуют остальные ресурсы, но, в отличие от них, эта программа легко масштабируется на любые размерности массива.

Как ограничиваются ресурсы при программировании на Colamo

Рассмотрим задачу суммирования элементов массива, для решения которой имеется 4 сумматора и множество каналов распределенной памяти, например, 24, и при этом нет потока.

DCL A [ 24 : vector ]

Необходимо сложить элементы массива последовательно-параллельно при условии, что есть некоторый подкадр, который суммирует эти элементы по четыре. Предположим, что существует некоторая функция Add, которая выполняет суммирование:

Add(R, R', a)
dcl R, R' : com;
dcl a : [4 : vector] com;
R':=R+a1+a2+a3+a4

Как нужно обратиться к подкадру Add, при условии, что входной параметр - вектор, а также чтобы не произошло размножение элементов add? Если написать фрагмент:

R[0]:=0
For i:=1 to 5 do
Add (R[i-1], R[i], a[i*4-3]) ,

то здесь R - информационно-независимые коммутационные переменные, A - вектор. При организации цикла элементы распараллелятся, в результате чего потребуется 24 сумматора. Это произойдет, если приведенный выше фрагмент кода будет находиться в теле кадра:

R[0]:=0
cadr
for i:=1 to 5 do
Add (R[i-1], R[i], a[i*4-3])
endcadr

Но если оператор цикла будет вынесен за пределы кадра, то тогда произойдет последовательное обращение:

R[0]:=0
for i:=1 to 5 do
begin
cadr
Add (R[i-1], R[i], a[i*4-3])
endcadr
end;

Поэтому введение в программу, реализующую последовательное и пирамидальное сложение с использованием 8-ми сумматоров, переменной Stream18VectorArr, которая предотвратит распараллеливание, абсолютно оправдано.

Следует отметить, что не всегда можно вынести операторы цикла за пределы кадра, поскольку это приведет к тому, что смысл программы изменится. Поэтому единственный выход - делать переприсваивания элементов массива с использованием специально описанной для этого переменной. Необходимо помнить, что операция переприсваивания одних коммутационных переменных в другие коммутационные переменные не занимает ни ресурсы, ни время. Операция переприсваивания выполняется только транслятором и является своего рода указанием, как сформировать информационный граф.

Большой половник дёгтя в бочку и так не высшего качества мёда

Выше уже отмечалось, что все эти схемы и программы при выстраивании конвейеров из сумматоров не используют конвейерности самих сумматоров. Поэтому все рассмотренные выше программы будут, и то в лучшем случае, использовать всего 1/18 часть выделенных ресурсов ПЛИС. Остальные 17 ступеней конвейеров будут простаивать во всех сумматорах. Это - общая проблема всех рекурсивных алгоритмов. Она может быть обойдена по-разному, в случае суммирования нам помогает (но может и сбить с толку) ассоциативность операции сложения.

Заключение

При подготовке данной страницы в качестве базы были использованы материалы семинара НИИ МВС ТРТУ по языку Colamo. Редактор страницы убрал лишние, с точки зрения задачи фиксирования ресурсов, "отходы в сторону" вроде того, где будут использованы эти суммирования, поскольку считает переход к реально сложным программам на этом этапе преждевременным, а также добавил в ряде мест комментарии, понятные программистам на других языках. В этом плане пока что структурные рисунки (в начале текста) столь обширно прокомментировать нельзя, ибо без привыкания к схемотехническому, до определённой степени, мышлению, эффективное программирование на Colamo не представляется возможным в принципе.

Обращаю внимание читателя, что здесь в текстах программ уже появляются некоторые расширения начального стандарта языка, в частности, например, include, в Коламо, в отличие от Си, подключающий структурные библиотеки элементарных операций и задание структуры. При этом надо чётко помнить, что в Colamo, в отличие от языков типа Си, задают не структуру данных, а структуру вычислений.

Обзор структур вычислений, использованных в данном описании, приводит к выводу, что во всех их не используется конвейерность самих "сумматоров", и потому для полной загрузки ресурсов представляется необходимым их параллельно-конвейерное выполнение на потоке задач. Вместе с тем, вышеизложенный разбор полезен тем, что показывает особенности языка Коламо в отношении:

Именно в осмыслении этих особенностей, а также в учёте проблем рекурсивных алгоритмов, и состоит основной смысл этого разбора.



© Лаборатория Параллельных информационных технологий НИВЦ МГУ
Rambler's Top100