![]() |
| ||
|
|
Новости |
Поиск |
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-го числа).
Vitaliy_____ (2009-08-11 13:00:00) [41]
> Вариант (11.08.09 10:29) [17]
>
> Посмотрел внимательней свой код, есть ошибки,автоинициализация
> не пойдет - какой-то ожидающий поток может не сразу получить
> управление по установке события, а другой уже вошел в следующий
> цикл ожидания и сбросил событие... это нехорошо. Поспешил
> однако.
Только хотел это написать, но ты меня опередил :)
Именно поэтому семафоров 2 а не 1 (для четной и нечетной итерации). в принципе, у меня был вариант с событиями. Но их все равно надо не меньше 2-х. А вот по поводу пересоздания семафора - тут у меня вопрос, может его совсем не обязательно "убивать". Надо еще раз заглянуть в рихтера, мне вообще надо его "сбросить". может CloseHandle не обязателен, проконсультируйте кто знает: может достаточно вызвать Create или Open и оно перепишет как надо?
У меня тоже есть ошибки, точнее неточности: если 2-ды вызвать TMyBarrierSinchronization.Initialize, то семафоры будут созданы второй раз. Правда имя тоже. Знатоки, подскажите, что при этом произойдет? Будут просто переписаны параметры объекта ядра или еще и увеличен счетчик пользователей этого объекта? (в последнем случае мы его тогда не освободим, как я понимаю).
Вариант (2009-08-11 10:29:00) [42]Посмотрел внимательней свой код, есть ошибки,автоинициализация не пойдет - какой-то ожидающий поток может не сразу получить управление по установке события, а другой уже вошел в следующий цикл ожидания и сбросил событие... это нехорошо. Поспешил однако.
Вариант (2009-08-11 10:15:00) [43]Не учел, что мой вариант можно использовать только в одном процессе. Для разных процессов надо дать имя Event, а вместо переменной-счетчика надо таки будет использовать то же именованный семафор. Но суть алгоритма остается та же.
Вариант (2009-08-11 10:10:00) [44]Мне показалось сложновато несколько. Два семафора для того, что бы можно было барьер постоянно в цикле использовать без переинициализации (если правильно понял идею), постоянное пересоздание семафоров
Предлагаю еще вариант(естественно упрощенный, без проверок, без деструктора и т.п.):-)
Пусть класс содержит счетчик ожидаемых процессов (переменная типа integer), переменная для хранения начального значения счетчика и объект синхронизации типа Event(THandle)
procedure TMyBarrierSinchronization.Create(NP:integer);
begin
TP:=NP;//хранилище стартового значения счетчика
NumProc:=NP-1;// счетчик = число процессов для ожидания-1
WaitEvent:=CreateEvent(nil,true,false,nil);//Объект синхронизации
end;
procedure TMyBarrierSinchronization.Initialize(NumProcesses:integer);
begin
NumProc:=NumProcesses-1;
end;
procedure TMyBarrierSinchronization.WaitBarrier();
begin
ResetEvent(WaitEvent);
if InterlockedDecrement(NumProc) < 0 then
begin
Initialize(TP);
SetEvent(WaitEvent);
end;
WaitForSingleObject(WaitEvent,INFINITE);
end;
PS: Не проверял работает код или нет
Vitaliy_____ (2009-08-11 06:10:00) [45]Не прошло и месяца как отпуск закончился :)
Решил поднять старую ветку: как ни наберешь в поиске "барьерная синхронизация на семафорах", никак не получаешь примера программы. Вроде все просто, но примера нет.
И так, пара комментариев по своему же вопросу:
Функция
DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE* lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
);
имеет один существенный на мой взгляд недостаток: есть константа, ограничивающая число объектов, которое она может ожидать.
function WaitForMultipleObjects(nCount: DWORD; lpHandles: PWOHandleArray;
bWaitAll: BOOL; dwMilliseconds: DWORD): DWORD; stdcall;
{$EXTERNALSYM WaitForMultipleObjects}
type
TWOHandleArray = array[0..MAXIMUM_WAIT_OBJECTS - 1] of THandle;
PWOHandleArray = ^TWOHandleArray;
MAXIMUM_WAIT_OBJECTS = 64;
{$EXTERNALSYM MAXIMUM_WAIT_OBJECTS}
То есть при такой синхронизации мы можем ждать не больше 64 потоков. Это достаточно много, но "недальновидно", учитывая что уже сейчас есть видеокарты с большим числов процессоров (кто сказал, что обязательно проводить вычисления на ЦП, в дальнейшем возможно будут еще идеи по усовершенствованию, но это уже не для этой ветки).
Для облегчения работы с барьерной синхронизацией написал модуль, помогающий ее организовать. Хотелось бы услышать критику по нему:
unit BarrierSinchronization;
interface
type TMyBarrierSinchronization=class(TObject)
private
NP:integer; // Число потоков для синхронизации
MBSSemaphore1,MBSSemaphore2:THandle; // 2 семафора для реализации барьера
SemName1,SemName2:String; // Имена семафоров
state:boolean; // Ключ, с каким семафором работаем
public
procedure WaitBarrier();
procedure Initialize(NumProcesses:integer);
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
uses Windows, SysUtils, StrUtils;
constructor TMyBarrierSinchronization.Create;
begin
SemName1:=DateTimeToStr(Now());
SemName1:=AnsiReplaceStr(SemName1,':','_');
SemName1:=AnsiReplaceStr(SemName1,'\','_');
SemName1:=AnsiReplaceStr(SemName1,'.','_');
SemName1:=AnsiReplaceStr(SemName1,' ','_');
SemName1:=SemName1+'MBSMySemaphore';
SemName2:=SemName1+'2';
SemName1:=SemName1+'1';
np:=0;
if GetLastError<>0 then Exception.Create(WinAPIErrMsg1);
end;
destructor TMyBarrierSinchronization.destroy;
begin
CloseHandle(MBSSemaphore1);
CloseHandle(MBSSemaphore2);
np:=0;
inherited;
end;
procedure TMyBarrierSinchronization.Initialize(NumProcesses:integer);
begin
NP:=NumProcesses;
MBSSemaphore1:=CreateSemaphore(nil,0,np,PChar(SemName1)); // Занят
if GetLastError<>0 then Exception.Create(WinAPIErrMsg2);
MBSSemaphore2:=CreateSemaphore(nil,0,np,PChar(SemName2)); // Занят
if GetLastError<>0 then Exception.Create(WinAPIErrMsg3);
state:=true; // работаем с первым семафором, ждем второй
end;
procedure TMyBarrierSinchronization.WaitBarrier();
var MSemPrevCount:longint;
begin
if state then begin // На нечетных барьерах ждем второй семафор, работаем с первым
ReleaseSemaphore(MBSSemaphore1,1,@MSemPrevCount);
if MSemPrevCount=np-1 then begin // Это последний процесс, можно стартовать след. цикл
CloseHandle(MBSSemaphore1);
MBSSemaphore1:=CreateSemaphore(nil,0,np,PChar(SemName1)); // Занят
if GetLastError<>0 then Exception.Create(WinAPIErrMsg2);
State:=(State=false); // Обратили состояние
ReleaseSemaphore(MBSSemaphore2,np-1,nil); // Освободили семафор ожидания - все начали работу (np-1 - т.к. этот {последний} поток не будет ждать)
end else
WaitForSingleObject(MBSSemaphore2,INFINITE);
end else begin // На четных барьерах наоборот
ReleaseSemaphore(MBSSemaphore2,1,@MSemPrevCount);
if MSemPrevCount=np-1 then begin // Это последний процесс, можно стартовать след. цикл
CloseHandle(MBSSemaphore2);
MBSSemaphore2:=CreateSemaphore(nil,0,np,PChar(SemName2)); // Занят
if GetLastError<>0 then Exception.Create(WinAPIErrMsg3);
State:=(State=false); // Обратили состояние
ReleaseSemaphore(MBSSemaphore1,np-1,nil); // Освободили семафор ожидания - все начали работу (np-1 - т.к. этот {последний} поток не будет ждать)
end else
WaitForSingleObject(MBSSemaphore1,INFINITE);
end;
end;
initialization
MyBarrierSinchronization:=TMyBarrierSinchronization.Create;
end.
Vitaliy_____ (2009-07-08 07:12:00) [46]
> Leonid Troyanovsky ©
...1. На однопроцессорных системах нет смысла использовать многопоточные приложения. Это не приводит к повышению производительности, а наоборот, усложняет систему из-за необходимости выполнять синхронизацию потоков.
- ежу понятно, потому и написал, что более чем одно ядро. 2 или 4 в данном случае не важно.
У нас 2 чисто расчетные задачи, примерно равные по объему. Статья интересная, прочитал с удовольствием, но пока не вижу причин для беспокойства. В нашем случае должно работать.
Про пул: мы заранее знаем, а точнее планируем число потоков, есть ли смысл так усложнять? По-моему для серверных решений это действительно важно (хотя сталкивался с этим лишь на лабораторных в универе и там обходились отдельным потоком на запрос пользователя - идейно это все достаточно понятно и в статье подробно разжевано).
Сергей М. ©
> Кроме потоков твоего процесса в системе одновременно существует еще туева хуча процессов
Трудно поспорить, но большинство их (по крайней мере у меня) простаивает и запуская 2 варианта моей однопоточной программы мы получаем почти двукратную прибавку к скорости. На мой взгляд достаточный аргумент, чтобы задуматься о потоках.
Loginov Dmitry ©
> Слои-то разной размерности, верно, но насколько помню, выходы
> слоя расчитываются как вектор входов умноженный на матрицу
> весов слоя.
Да, все верно. На выходе получим входной вектор для след. слоя.
> в составе замечательной библиотеки Lapack есть замечательная
> библиотека Blas. Все в них реализовано на фортране, это
> язык с математическим уклоном, и компилятор фортрана донильзя
> оптимизирует операции по обработке матриц
Слово "замечательная" напоминает рекламу БАДов. Есть тесты, которые это подтверждают? В магию не верю. Кстати, спасибо что напомнили выпускнику прикладной математики о существовании фортрана, а то я уже совсем его забыл. Помнится сравнивали решение одной и той же задачи, реализованной на фортране и на билдере 5-м тогда еще, так вот... Магии нет :) А неудобство писать пользовательский интерфейс на фортране есть.
Про оптимизацию: дык это пересобирать каждый раз надо под архитектуру системы, чтобы получить выигрыш. Нет, мы будем ручками делать. Хотя бы потому, что давно пора пересматривать наши последовательные алгоритмы.
Loginov Dmitry (2009-07-07 22:54:00) [47]
> По сути да, но сложность в том что слои разной размерности.
>
> У тебя на сайте есть раздел "нейронные сети" (правда пустой),
> думаю ты в курсе что делается при "прямом проходе". Но
> есть тонкости. Учет случайного сдвига, разный алгоритм для
> входного, выходного и скрытых слоев.
> И вообще, как-то слабо верится, что умножение чисел возможно
> ускорить. На чем экономия? На вычислении адресов при умножении
> матриц? Так тут размерности у слоев разные, не выйдет...
> Откуда 50 раз к скорости?
> По сути да, но сложность в том что слои разной размерности.
Слои-то разной размерности, верно, но насколько помню, выходы слоя расчитываются как вектор входов умноженный на матрицу весов слоя.
> И вообще, как-то слабо верится, что умножение чисел возможно
> ускорить. На чем экономия? На вычислении адресов при умножении
> матриц? Так тут размерности у слоев разные, не выйдет...
> Откуда 50 раз к скорости?
в составе замечательной библиотеки Lapack есть замечательная библиотека Blas. Все в них реализовано на фортране, это язык с математическим уклоном, и компилятор фортрана донильзя оптимизирует операции по обработке матриц, причем может учитывать особенности того или иного процессора, оттуда и ускорение в десятки раз. Благодаря этому и Lapack и Blas используются в большинстве известных математических пакетах. Магия.
Сергей М. (2009-07-07 15:14:00) [48]
> У нас 1 ядро простаивает
С чего бы вдруг оно простаивает ?
Кроме потоков твоего процесса в системе одновременно существует еще туева хуча процессов, каждый из которых породил минимум 1 поток.
Они, эти потоки, не святым духом же исполняются, им тоже нужны кванты проц.времени .. а процессоров-то всего два .. а потоков - хуча .. да ты еще пару-тройку добавил)..
Leonid Troyanovsky (2009-07-07 14:52:00) [49]
> Vitaliy_____ (07.07.09 14:34) [9]
http://dtf.ru/articles/read.php?id=39888
http://rsdn.ru/article/baseserv/threadpool.xml
--
Regards, LVT.
Vitaliy_____ (2009-07-07 14:34:00) [50]Сергей М.
> Ты неверно трактуешь функциональность пула.
Гм, да, согласен. По сути будет что-то подобное.
> Достичь повышения сквозной выч.производительности алгоритма
> увеличением кол-ва кодовых потоков при неизменном (фиксированном)
> числе процессоров невозможно.
Не понял фразы. Если у нас в системе 2 ядра и мы организуем 2 вычислительных потока, то производительность возрастет по сравнению с 1 потоком (не в 2 раза, конечно, но возрастет). Учитывая, что непрерывные вычисления на 1 поток значительны, синхронизация отъест не так много (сколько - уже покажет практика).
Что значит "...при неизменном (фиксированном) числе процессоров невозможно"? У нас 1 ядро простаивает в принципе при 1 вычислительном потоке и 2 процессорах.
Сергей М. (2009-07-07 12:31:00) [51]
> Сколько физических процессоров в целевой системе?
> А это важно?
Куда как важней !
Достичь повышения сквозной выч.производительности алгоритма увеличением кол-ва кодовых потоков при неизменном (фиксированном) числе процессоров невозможно.
Сергей М. (2009-07-07 12:19:00) [52]
> Vitaliy_____ (07.07.09 11:39) [5]
> зачем создавать поток каждый раз, пусть даже пулом?
Ты неверно трактуешь функциональность пула.
http://en.wikipedia.org/wiki/Thread_pool
> если есть четкое разграничение по данным
Из твоего объяснения это совершенно не очевидно.
Vitaliy_____ (2009-07-07 11:41:00) [53]
> Вариант
WaitForMultipleObjects - интересно возможно подойдет, посмотрю Рихтера, слышал про эту книгу... Будем разбираться дальше :)
Vitaliy_____ (2009-07-07 11:39:00) [54]Сергей М.
> Нужно организовать тред-пул.
Не согласен, я вижу реализацию в создании тредов 1 раз и убивании их по завершении вычислений. Эта функция будет вызываться 300 000 раз за 1 эпоху вычислений, зачем создавать поток каждый раз, пусть даже пулом?
> Доступ по записи к этим массивам следует обязательно синхронизировать.
И тут не согласен: если есть четкое разграничение по данным (а его можно сделать: передаем потоку перед началом число потоков и его номер, по этим параметрам определяем фрагмент массива для обработки), то синхронизация нужна только между слоями (т.к. нам потребуется полностью вычисленный предыдущий слой для рассчета следующего).
Loginov Dmitry
> Этот расчет - обычное матричное умножение, верно?
По сути да, но сложность в том что слои разной размерности.
У тебя на сайте есть раздел "нейронные сети" (правда пустой), думаю ты в курсе что делается при "прямом проходе". Но есть тонкости. Учет случайного сдвига, разный алгоритм для входного, выходного и скрытых слоев.
И вообще, как-то слабо верится, что умножение чисел возможно ускорить. На чем экономия? На вычислении адресов при умножении матриц? Так тут размерности у слоев разные, не выйдет... Откуда 50 раз к скорости?
Leonid Troyanovsky
> Сколько физических процессоров в целевой системе?
Более 1. В данный момент 2, возможно будет 4. А это важно? Размерности слоев достаточно большие, чтобы загрузить и 100 процессоров :)
Leonid Troyanovsky (2009-07-04 13:55:00) [55]
> Vitaliy_____ (03.07.09 08:24)
> Озадачился вопросом распараллеливания алгоритма для повышения
> вычислительной производительности.
Сколько физических процессоров в целевой системе?
--
Regards, LVT.
Вариант (2009-07-03 14:16:00) [56]
> Vitaliy_____ (03.07.09 08:24)
> - пару примеров по техническим вопросам "а как это сделать",
> т.к. мне делфийских примеров на потоки показалось маловато,
> хотя бы барьерной синхронизации там нет, а она потребуется.
>
В принципе сделать барьер можно множеством вариантов, сходу пришло в голову два из тех, что мне кажутся попроще...
Можно сделать на
DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE* lpHandles,
BOOL bWaitAll ,
DWORD dwMilliseconds
);
Можно сделать на семафоре, правда код будет посложнее.
Синхронизация, многопоточность хорошо описана у Рихтера в
"Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии Windows".
Пул потоков организую обычно aeyrwbtq QueueUserWorkItem, но это дело вкуса, можно создать и свой. Кстати примеры тоже есть у Рихтера в той же книге.
Loginov Dmitry (2009-07-03 12:50:00) [57]
> В ней есть часто используемая процедура GetNetOut(), которая
> вычисляет значение выходов многослойной ИНС при заданных
> входах. Учитывая то, что коррекция весов происходит по завершении
> эпохи обучения, думаю будет достаточно распараллелить только
> GetNetOut, так как основная вычислительная нагрузка на ней.
>
Этот расчет - обычное матричное умножение, верно?
Тогда используй Blass.dll, будет раз в 50 быстрее.
Примеры можешь найти на моем сайте.
Сергей М. (2009-07-03 09:58:00) [58]
> 1)
Нужно организовать тред-пул.
Как пример для изучения см. класс TIdSchedulerOfThreadPool в составе Indy10
> 2)
Доступ по записи к этим массивам следует обязательно синхронизировать.
Для этого можно использовать, например, TCriticalSection, TMultiReadExclusiveWriteSynchronizer.
Синхронизацию доступа к cписочныv ресурсам можно организовать, например, на базе TThreadList
> 3)
см. 2)