![]() |
| ||
|
|
Новости |
Поиск |
FAQ |
Статьи |
Книги |
Новости VCL
| | Форумы ЧАТ |
Орешник |
|
| |||
| |||
« предыдущая ветвь | форум | следующая ветвь »
Организация многопоточности
Vitaliy_____ © (2009-07-03 08:24:00)Доброе время суток!
Озадачился вопросом распараллеливания алгоритма для повышения вычислительной производительности.
Есть модуль, реализующий искуственную нейросеть. В нем есть процедура DoTeach(), выполняющая обучение ИНС по модификации алгоритма обратного распространения ошибки.
В ней есть часто используемая процедура GetNetOut(), которая вычисляет значение выходов многослойной ИНС при заданных входах. Учитывая то, что коррекция весов происходит по завершении эпохи обучения, думаю будет достаточно распараллелить только GetNetOut, так как основная вычислительная нагрузка на ней.
Теперь ближе к вопросу.
Сама процедура сейчас выглядит так:
procedure TTeachNet.GetNetOut();
var
i,j,k,LN_1,LN_2: integer;
begin
LN_1:=LayersNum-1; LN_2:=LayersNum-2;
// Первый слой (входной), c активационной функции.
for i:=0 to LayerSize[0]-1 do
OutLayers[0,i]:=LayerActivateF(InpLayers[0,i],1);
// Рассчитываем все остальные слои кроме последнего.
for k:=1 to LN_2 do
for i:=0 to LayerSize[k]-1 do begin
InpLayers[k,i]:=0;
for j:=0 to LayerSize[k-1]-1 do
InpLayers[k,i]:=InpLayers[k,i]+OutLayers[k-1,j]*LayerWeights[k-1,i,j];
// Учет сдвига
InpLayers[k,i]:=InpLayers[k,i]+LayerWeights[k-1,i,LayerSize[k-1]]; // *1
OutLayers[k,i]:=LayerActivateF(InpLayers[k,i],1); // 1 - сигмоид
end;
// Рассчитываем последний слой c акт. ф. на выходе
for i:=0 to LayerSize[LN_1]-1 do begin
InpLayers[LN_1,i]:=0;
for j:=0 to LayerSize[LN_2]-1 do
InpLayers[LN_1,i]:=InpLayers[LN_1,i]+OutLayers[LN_2,j]*LayerWeights[LN_2,i,j];
// Учет сдвига
InpLayers[LN_1,i]:=InpLayers[LN_1,i]+LayerWeights[LN_2,i,LayerSize[LN_2]]; // *1
//OutLayers[LN_1,i]:=InpLayers[LN_1,i];
OutLayers[LN_1,i]:=LayerActivateF(InpLayers[LN_1,i],1);
end;
end;
Я вижу своей целью распараллелить циклы: каждый поток будет рассчитывать свой кусок цикла. По идее не сложно, но есть пара технических вопросов:
1) Нужно чтобы потоки не создавались при каждом вызове GetNetOut, а были созданы 1 раз при вызове DoTeach и по ее команде выполняли вычисление с новыми данными.
2) Массивы InpLayers, OutLayers, LayerWeights, LayerSize - это поля класса TTeachNet, и они необходимы для вычислений выхода ИНС. Как их лучше передавать, чтобы они были доступны, как глобальные переменные всем потокам? В принципе, есть экземпляр класса TTeachNet, и можно сделать эти переменные public (т.к. потоки планируется создавать и уничтожать при гарантированной жизни TTeachNet, его же методом, то ошибки не должно быть). Но, возможно, есть более хорошие идеи?
3) Также потребуется барьерная синхронизация между вычислениями различных слоев ИНС. Не силен я в этом, буклет или пример нужен...
4) Взаимодействия с VCL в этой части нет, с синхронизацией вроде проблем не будет по этому вопросу. Только распараллеливание вычислений, это должно облегчить задачу.
------------
Я понимаю, что мой вопрос в каком-то смысле звучит как "код давай", но что бы мне действительно хотелось:
- Ваше мнение, верны ли мои мысли на идейном уровне?
- пару примеров по техническим вопросам "а как это сделать", т.к. мне делфийских примеров на потоки показалось маловато, хотя бы барьерной синхронизации там нет, а она потребуется.
- подводные камни: тут нужен опыт, мало ли о чем не подумал, в многопоточности их много :)
------------
Прочитать если кто что напишет смогу не раньше вторника (7-го числа).
Leonid Troyanovsky (2009-08-17 13:12:00) [21]
> Leonid Troyanovsky © (17.08.09 13:01) [37]
> здесь, IMHO, вовсе не нужен):
Гуи не нужен, консольный поток менее ресурсоемок.
--
Regards, LVT.
Leonid Troyanovsky (2009-08-17 13:01:00) [22]
> Vitaliy_____ (17.08.09 11:06) [33]
> Что-то ну в упор не понимаю, при чем здесь критические секции
> - поясните, исходная функция, которую надо распараллелить,
> дана в [0] посте.
Вот простенький пример для гуи и вторичного потока (хотя он
здесь, IMHO, вовсе не нужен):
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
critsect: TRTLCriticalSection;
s: Longint;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
type
TGlobalData = record
ID: Longint;
Data: Longint;
end;
TCalcThread = class(TThread)
public
cs : TRTLCriticalSection;
calcid: Longint;
global: TGlobalData;
procedure Execute; override;
end;
procedure TCalcThread.Execute;
var
i: Longint;
begin
while not Terminated do
begin
EnterCriticalSection(cs);
if calcid <> global.ID then
begin
for i := 0 to 100000 do
global.Data := global.Data + 500 - Random(1001); { здесь считаем половину цикла}
global.ID := calcid;
end;
LeaveCriticalSection(cs);
end;
end;
var
ct: TCalcThread;
procedure TForm1.Button1Click(Sender: TObject);
var
i: Longint;
begin
ct.Resume;
s := 0;
while not Application.Terminated do
begin
EnterCriticalSection(CritSect);
ct.Global.Data := ct.Global.Data + s;
Caption := Format('%d %d', [ct.Global.Id, ct.Global.Data ]);
ct.calcid := ct.calcid + 1;
LeaveCriticalSection(CritSect);
Application.ProcessMessages;
for i := 0 to 100000 do
s := s + 500 - Random(1001); { здесь считаем другую половину }
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
InitializeCriticalSection(CritSect);
ct := TCalcThread.Create(True);
ct.cs := CritSect;
ct.calcid := 0;
ct.global.ID := 0;
ct.global.Data := 0;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
DeleteCriticalSection(critsect);
end;
Вполне возможно, что делить цикл надо не точно пополам,
а с учетом накладных расходов первичного потока.
И несмотря на простоту и изученность объекта синхронизации
здесь еще таится куча тонкостей.
> КРИТЕРИЯ верю только в засечение времени, за которое совершается
> одинаковая (достаточно большая) работа.
Для анализа работы потребуется не только общее время, но и все его
составляющие, включая количество переключений контекстов,
переключений между режимами, частота сброса кеша(ей), реакция на
появление высокоприоритетных (в т.ч. системных) потоков и др.
--
Regards, LVT.
Vitaliy_____ (2009-08-17 12:38:00) [23]Конечно, так и сделано, на форме спинедит: количество процессоров, дефолтом в нем число реальных...
Единственное отличие в моем алгоритме - так это если 1 процессор и несколько: для одного лишние потоки не создаются, синхронизаций нет, для нескольких (без разницы сколько) - 1 управляющий поток (на нем труднораспараллеливаемая часть и создание/запуск расчетных) и n (=числу реальных ядер) расчетных.
Основное время работают расчетные, и немного основной (когда этап расчета завершен) - он проверяет логическую часть, дает обновиться гую и запускает следующий этап.
Это все как раз работает - вопрос который остался я попытался сформулировать в [31] (2)
Anatoly Podgoretsky (2009-08-17 11:45:00) [24]> Vitaliy_____ (17.08.2009 11:06:33) [33]
> Сегодня 2, завтра 4 - зачем писать код, привязанный к железу - чтобы его переписывать потом?
Не обязательно переписывать код, можно написать код, который при старте будет узнавать количество логических процессоров, мы же все же делаем software, а не hardwarе
Дмитрий Белькевич (2009-08-17 11:30:00) [25]
> Не понимаю я подхода "исходя из числа ядер" - слишком уж
> часто меняются технологии в наше время.
Для разного количества ядер оптимальны разные алгоритмы. Так что если хочется оптимально под несколько разных вариантов, то, по хорошему, нужно делать несколько разных алгоритмов.
Vitaliy_____ (2009-08-17 11:06:00) [26]
> Если вычисления распараллеливаются между двумя вторичными
> потоками, то всего в процессе 3 потока, и, значит, за удовольствие
> смотреть на мелькающие цифирки/картинки придется заплатить
> увеличением числа переключений контекстов на где-то на треть.
>
А вот это сейчас и проверяю. Тест подтверждает увеличение производительности на 1.9 - что на мой взгляд опровергает ваше утверждение. Накладные расходы на барьеры, + (что намного важнее) _частичное_ распараллеливание задачи (только та часть, где мало логики и много вычислений), и, в конце концов, другие процессы в системе - результат на мой взгляд чуть ли не идеален.
Что-то ну в упор не понимаю, при чем здесь критические секции - поясните, исходная функция, которую надо распараллелить, дана в [0] посте.
> Для двух ядер...
Сегодня 2, завтра 4 - зачем писать код, привязанный к железу - чтобы его переписывать потом? Если уж делать программу параллельной, так уж не ограничивать по возможности тем, что имеешь в данный момент! Не понимаю я подхода "исходя из числа ядер" - слишком уж часто меняются технологии в наше время.
> И, во-ще, для данной формулировки важно не "работает/не
> работает",
> а эффективность работы - загрузка процессоров полезной
> (вычислительной) работой, а не переключением контекстов
> и мод.
>
Категорически не согласен. В качестве понимания "почему" работает медленно - все это знать нужно, но в качестве КРИТЕРИЯ верю только в засечение времени, за которое совершается одинаковая (достаточно большая) работа. У нас много оптимизаций, связанных с памятью и т.д. - "работа" может быть и полезной, с точки зрения процессора, но "лишней", "не запланированной" с точки зрения алгоритма.
Leonid Troyanovsky (2009-08-17 09:15:00) [27]
> Vitaliy_____ (17.08.09 07:36) [31]
> В качестве синхронизации сейчас используется n семафоров,
> где n - количество барьеров.
Для двух ядер достаточно 1 критической секции.
КС даже предпочтительней объектов ядра, т.к., переключает поток
в kernel mode лишь по необходимости.
> Существуют итерационные вычисления, между частями итераций
> нужно синхронизировать потоки (т.е. барьер, нам на след.
> части нужны данные с предыдущей, значит она должна быть
> полностью досчитана).
С этим вполне справляется даже 1 КС.
Можно найти и более "легкое" решение.
> Я в магию не верю, значит в коде есть ошибка.
[26]
> 4) Гуй. Если "о нем можно забыть" - это не значит что его
> нет :)
Если шашечки важней, чем ехать, то, конечно, можно и с гуем.
Если вычисления распараллеливаются между двумя вторичными
потоками, то всего в процессе 3 потока, и, значит, за удовольствие
смотреть на мелькающие цифирки/картинки придется заплатить
увеличением числа переключений контекстов на где-то на треть.
И, во-ще, для данной формулировки важно не "работает/не работает",
а эффективность работы - загрузка процессоров полезной
(вычислительной) работой, а не переключением контекстов и мод.
--
Regards, LVT.
Vitaliy_____ (2009-08-17 07:36:00) [28]
> Leonid Troyanovsky ©
Много букв, но осилил.
1) В данный момент задача решена, находится в стадии тестирования. Остался вопрос, касаемый "барьера с автоустановкой" - подробнее ниже.
В качестве синхронизации сейчас используется n семафоров, где n - количество барьеров. И вроде все работает. Конечно, слово "вроде" нехорошее, но пока проблем не выявлено, а вот в [24] они есть, хотя лично мне не понятно где.
2) Если уж продолжать тему, то вопрос сейчас можно сформулировать так:
Существуют итерационные вычисления, между частями итераций нужно синхронизировать потоки (т.е. барьер, нам на след. части нужны данные с предыдущей, значит она должна быть полностью досчитана).
Для того чтобы "кто-то" (читать, управляющий всем этим барахлом поток) не брал на себя тяжкий труд по организации необходимых барьеров (ему достаточно того, что надо синхронизировать сами итерации и выполнять действия, необходимые между ними) - требуется " барьер с автоустановкой".
3) Идея [24] заключается в следующем: имеется 2 семафора, как только последний поток достиг барьера, он СНАЧАЛА выставляет барьер для следующей итерации (четная/нечетная определяется переменной state), затем сбрасывает текущий барьер. Вроде логически корректно, пока мы не выставим следующий барьер, все будут ждать на текущем, но на практике получается что потоки "магическим" образом оказываются на разных барьеров и, как следствие, впадают в бесконечное ожидание (т.к. ни на один не придут ВСЕ потоки и, значит, барьер не "рухнет").
Я в магию не верю, значит в коде есть ошибка.
4) Гуй. Если "о нем можно забыть" - это не значит что его нет :)
Он есть, написан на паре timer + Application.ProcessMessages и переписывать смысла не вижу. Единственное, что важно, так это то что в потоках у нас никаких Application.ProcessMessages быть не должно, т.к. это VCL.
Потоки никакого гуя не знают и знать не должны - они просто должны досчитать итерацию (внутри которой еще и барьеры нужны) а затем последний досчитавший ставит событие EndCalcEvent. После этого все ждут команды на след. итерацию или умирание (вот тут как раз WaitForMultipleObjects для 2-х объектов - семафора старта и события умирания)
Leonid Troyanovsky (2009-08-14 14:53:00) [29]
> Leonid Troyanovsky © (14.08.09 14:50) [29]
> Для эффективных вычислений требуется (консольное) приложение
> с двумя, четырьмя и т.д. (по кол-ву процессоров)
> дополнительными потоками.
Слово "дополнительными" здесь лишнее, sorry.
--
Regards, LVT.
Leonid Troyanovsky (2009-08-14 14:50:00) [30]
> Vitaliy_____ (03.07.09 08:24)
> т.к. мне делфийских примеров на потоки показалось маловато,
> хотя бы барьерной синхронизации там нет, а она потребуется.
Не понял, зачем здесь потребуется барьерная синхронизация.
Для эффективных вычислений требуется (консольное) приложение
с двумя, четырьмя и т.д. (по кол-ву процессоров)
дополнительными потоками.
Т.е., про гуи приложение на двух ядрах лучше забыть.
А для управления одним потоком хватит одного-пары Event
и одной-пары критических секций.
Ну, и, конечно, основная рабочая лошадь - WaitForMultipleObjects[Ex]
--
Regards, LVT.
Leonid Troyanovsky (2009-08-14 14:23:00) [31]
> Vitaliy_____ (08.07.09 07:12) [13]
> простаивает и запуская 2 варианта моей однопоточной программы
> мы получаем почти двукратную прибавку к скорости. На мой
И почему бы не распараллелить задачу столь естественным образом?
--
Regards, LVT.
Leonid Troyanovsky (2009-08-14 14:16:00) [32]
> Vitaliy_____ (11.08.09 06:10) [14]
> То есть при такой синхронизации мы можем ждать не больше
> 64 потоков.
Для двух ядер - 64 потока - как до звезды.
Ну, а если очень нужно сделай 2 потока, каждый из которых
будет ждать по 64. Но, все это не для двухпроцессорной машины.
--
Regards, LVT.
Leonid Troyanovsky (2009-08-14 14:11:00) [33]
> Vitaliy_____ (14.08.09 07:58) [24]
Много буков, не осилил.
А чего требуется, в чем сакральный смысл проектируемого?
Могу заметить, чтобы написать и отладить не очень сложный составной объект синхронизации нужна масса времени и терпения.
Какие объекты (системные) синхронизации, кроме семафоров,
изучены и почему они неприменимы для данной задачи?
И как, собс-но, формулируется сама задача?
--
Regards, LVT.
Vitaliy_____ (2009-08-14 12:07:00) [34]Похоже, все же где-то в этом модуле есть ошибка. При создании n семафоров (где n - число барьеров) - все работает, а при таком "автосбросе" где-то пролетаем на красный свет.
> Вариант Похоже эта тема особо интересна нам двоим :) Если будет желание, подумай где может быть косяк: у меня явный дедлок - проблема есть, а с какого боку ее решать не знаю :)
Vitaliy_____ (2009-08-14 07:58:00) [35]Честно, не очень понимаю в чем дело - вроде идейно все правильно, но потоки каким-то магическим образом проскакивают барьер. В итоге попадаем на бесконечное ожидание разных семафоров. (число потоков выставлено правильно во всех тредах). Если кто укажет ошибку - здорово, хотя слабо в это верю. Попробую пока сделать для каждого участка свой барьер из обычного одного семафора, интересно что там будет.
unit BarrierSinchronization;
interface
uses Windows, Utils, SysUtils, SyncObjs;
type TMyBarrierSinchronization=class(TObject)
private
NP:integer; // Число потоков для синхронизации
MBSSemaphore1,MBSSemaphore2:THandle; // 2 семафора для реализации барьера
SemName1,SemName2:String; // Имена семафоров
state:boolean; // Ключ, с каким семафором работаем
public
procedure Initialize(NumProcesses:integer);
procedure WaitBarrier(ThreadState:string);
constructor Create;
destructor destroy; override;
end;
var MyBarrierSinchronization:TMyBarrierSinchronization;
resourcestring
WinAPIErrMsg1='Can not create CriticalSection for barrier synchronization!';
WinAPIErrMsg2='Can not create semaphore 1 for synchronization!';
WinAPIErrMsg3='Can not create semaphore 2 for synchronization!';
implementation
constructor TMyBarrierSinchronization.Create;
begin
SemName1:=MyUtils.GetUnicalName+'MBSMySemaphore';
SemName2:=SemName1+'2';
SemName1:=SemName1+'1';
np:=0;
// Создаем "пустые" семафоры, чтобы корректно работала Initialize
MBSSemaphore1:=0;
MBSSemaphore2:=0;
end;
destructor TMyBarrierSinchronization.destroy;
begin
if MBSSemaphore1<>0 then
CloseHandle(MBSSemaphore1);
if MBSSemaphore2<>0 then
CloseHandle(MBSSemaphore2);
np:=0;
inherited;
end;
procedure TMyBarrierSinchronization.Initialize(NumProcesses:integer);
begin
NP:=NumProcesses;
if MBSSemaphore1<>0 then
CloseHandle(MBSSemaphore1);
MBSSemaphore1:=CreateSemaphore(nil,0,np,PChar(SemName1)); // Занят
if GetLastError<>0 then raise Exception.Create(WinAPIErrMsg2);
if MBSSemaphore2<>0 then
CloseHandle(MBSSemaphore2);
MBSSemaphore2:=CreateSemaphore(nil,0,np,PChar(SemName2)); // Занят
if GetLastError<>0 then raise Exception.Create(WinAPIErrMsg3);
state:=true; // работаем с первым семафором, ждем второй
end;
procedure TMyBarrierSinchronization.WaitBarrier(ThreadState:string);
var MSemPrevCount:longint;
key:cardinal;
procedure ReSetSemaphore(Sem:THandle);
var sig:cardinal;
begin
repeat
sig:=WaitForSingleObject(Sem,0);
until sig <> WAIT_OBJECT_0;
end;
begin
if state then begin // На нечетных барьерах ждем второй семафор, работаем с первым
ReleaseSemaphore(MBSSemaphore1,1,@MSemPrevCount);
if MSemPrevCount=np-1 then begin // Это последний процесс, можно стартовать след. цикл
ReSetSemaphore(MBSSemaphore1);
State:=false;
ReleaseSemaphore(MBSSemaphore2,np-1,nil); // Освободили семафор ожидания - все начали работу (np-1 - т.к. этот {последний} поток не будет ждать)
end else begin
repeat
key:=WaitForSingleObject(MBSSemaphore2,3000);
if key=WAIT_TIMEOUT then begin
MessageBox(0,PChar('WaitTimeOut on semaphore 2'+#13+ThreadState),PChar(IntToStr(GetCurrentThreadID)),0);
DebugBreak;
end;
until key=WAIT_OBJECT_0;
end;
end else begin // На четных барьерах наоборот
ReleaseSemaphore(MBSSemaphore2,1,@MSemPrevCount);
if MSemPrevCount=np-1 then begin // Это последний процесс, можно стартовать след. цикл
ReSetSemaphore(MBSSemaphore2);
if GetLastError<>0 then raise Exception.Create(WinAPIErrMsg3);
State:=true;
ReleaseSemaphore(MBSSemaphore1,np-1,nil); // Освободили семафор ожидания - все начали работу (np-1 - т.к. этот {последний} поток не будет ждать)
end else begin
repeat
key:=WaitForSingleObject(MBSSemaphore1,3000);
if key=WAIT_TIMEOUT then begin
MessageBox(0,PChar('WaitTimeOut on semaphore 1'+#13+ThreadState),PChar(IntToStr(GetCurrentThreadID)),0);
DebugBreak;
end;
until key=WAIT_OBJECT_0;
end;
end;
end;
initialization
MyBarrierSinchronization:=TMyBarrierSinchronization.Create;
end.
Vitaliy_____ (2009-08-11 14:38:00) [36]Ну не знаю, мне по большому счету пока без разницы, все равно планируется 2 потока с большой загрузкой между барьерами. Еще бы время найти доделать :)
Хотя на будущее - спасибо за совет: MSDN для меня достаточный аргумент.
Вариант (2009-08-11 14:28:00) [37]
> Vitaliy_____ (11.08.09 13:55) [21]
Например
while(true)
do
sig:=WaitForSingleObject(Sem,0);
if sig <> WAIT_OBJECT_0 then
break;///объект в не сигнальном состоянии, то есть дошли до нуля или была ошибка
end;
насчет проще - не скажу. MSDN советует WaitFor в цикле.
Но не знаю, если процессов более 1000 или еще больше например, что больше займет времени - сброс на ноль или уничтожение/создание... Не знаю.
Vitaliy_____ (2009-08-11 13:55:00) [38]Нет, а как вы себе представляете, в цикле вызывать ожидание до числа процессов? Может проще уничтожить?
Вариант (2009-08-11 13:26:00) [39]А зачем уничтожать? Сбросить можно без пересоздания. Уменьшить счетчик можно WaitForSingleObject функцией.
Vitaliy_____ (2009-08-11 13:16:00) [40]Уточнил у рихтера, при создании или открытии у нас НЕ будут переписаны параметры объекта ядра, если он существует, так что уничтожать его надо. Кстати, в инициализации, думаю, можно будет перед созданием семафоров тоже CloseHandle добавить. Согласно рихтеру
BOOL CloseHandle(HANDLE hobj);
Эта функция сначала проверяет таблицу описателей, принадлежащую вызываю
щему процессу, чтобы убедиться, идентифицирует ли переданный ей индекс (описатель) объект, к которому этот процесс действительно имеет доступ. Если переданный индекс правилен, система получает адрес структуры данных объекта и уменьшает в этой структуре счетчик числа пользователей; как только счетчик обнулится, ядро удалит объект из памяти.
Если же описатель неверен, происходит одно из двух. В нормальном режиме вы
полнения процесса CloseHandle возвращает FALSE, а GetLastError код ERROR_INVALID_HANDLE. Но при выполнении процесса в режиме отладки система просто уведомляет отладчик об ошибке.
Конечно, можно еще и в Create Прописать создание семафоров и тогда в Initialize все будет честно.