Алгоритм оптимизации # потоков, используемых в расчетах

Я выполняю операцию, назовем ее CalculateSomeData. CalculateSomeData работает в последовательных «поколениях», пронумерованных 1..x. Количество поколений во всем прогоне фиксируется входными параметрами CalculateSomeData и известно априори. Одно поколение занимает от 30 минут до 2 часов. Часть этой изменчивости связана с входными параметрами и не может контролироваться. Однако часть этой изменчивости связана с такими вещами, как аппаратные мощности, загрузка ЦП другими процессами, загрузка полосы пропускания сети и т. д. Одним из параметров, которым можно управлять для каждого поколения, является количество потоков, используемых CalculateSomeData. Сейчас это исправлено и, вероятно, неоптимально. Я хотел бы отслеживать время, которое занимает каждое поколение, а затем иметь некоторый алгоритм, с помощью которого я настраиваю количество потоков, чтобы каждое последующее поколение улучшало время расчета предыдущего поколения (минимизируя время). Какой подход следует использовать? Насколько применимы генетические алгоритмы? Интуиция подсказывает мне, что диапазон будет довольно узким — может быть, от 1 до 16 потоков на машине с двумя четырехъядерными процессорами.

любые указатели, псевдокод и т. д. очень ценятся.


comment
Я предполагаю, что вы не используете С# или Microsoft C++, есть TPL (msdn.microsoft .com/en-us/library/dd460717.aspx), который может координировать ядра потоков.   -  person Iain    schedule 21.09.2010
comment
На самом деле я использую С#/TPL. CalculateSomeData не полностью используется в .net. Я делаю некоторое взаимодействие .net с Excel. Я нахожу TPL удобным способом добавления параллельной обработки, но для моих конкретных расчетов я обнаружил, что он ужасно справляется с выбором правильного количества потоков. Обычно он пытается слишком много. Я предпочитаю указывать количество потоков и иметь алгоритм, который корректирует число до тех пор, пока оно не будет оптимизировано.   -  person SFun28    schedule 22.09.2010
comment
TPL имеет встроенную настройку, но она нацелена на действия, связанные с ЦП. Я предполагаю, что встроенная настройка не работает очень хорошо, если у вас есть действия, связанные с сетью, или действия, которые со временем меняют характеристики своей рабочей нагрузки.   -  person Albin Sunnanbo    schedule 22.09.2010
comment
вопрос для меня не слишком ясен: вы просто пытаетесь заставить каждое поколение работать быстрее, чем предыдущее? Насколько я понимаю, ваши поколения не имеют ничего общего с эволюционным программированием, тот факт, что вы используете один и тот же термин, может вызвать путаницу, поскольку это действительно термин, используемый на жаргоне GA.   -  person JohnIdol    schedule 22.09.2010
comment
@Johnldol - я пытаюсь минимизировать время, необходимое для каждого поколения, оптимизируя параметр #of threads. Так что да, каждое поколение в идеале должно работать быстрее, чем предыдущее, до момента, когда я нашел оптимальную настройку. Я отправил сообщение в GA, потому что подумал, что алгоритм GA может помочь. Вы правы, может возникнуть путаница в том, что я имею в виду с точки зрения генерации и определения GA. В самом широком смысле, не является ли поколение просто набором вычислений, сгруппированных вместе?   -  person SFun28    schedule 23.09.2010


Ответы (3)


Как насчет эволюционного алгоритма.

Начните с предположения. 1 поток на ядро ​​ЦП кажется хорошим, но зависит от поставленной задачи.

Измерьте среднее время выполнения каждой задачи в поколении. Сравните это со временем, затраченным предыдущим поколением. (Предположим, что фактически бесконечное время и 0 потоков для поколения 0).

Если задачи последнего поколения в среднем показали лучшее время, чем предыдущее, продолжайте изменять количество потоков в том же направлении, что и на предыдущем шаге (поэтому, если в последнем поколении было больше потоков, чем в предыдущем, добавьте поток для нового поколения, но если их было меньше, то используйте на один меньше (очевидно, с нижним пределом в 1 поток).

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

Пока оптимальное количество потоков не слишком близко к 1, вы, вероятно, в конечном итоге будете колебаться между 3 значениями, которые все достаточно близки к оптимальному. Вы можете захотеть явно обнаружить этот случай и заблокировать себя в центральном значении, если вам нужно иметь дело с большим количеством поколений.

person Bill Michell    schedule 22.09.2010
comment
Митчелл - это то, что я искал! Поскольку мы минимизируем время, я думаю, что мы предполагаем бесконечное время для поколения 0. 0 потоков для поколения 0 не являются строго необходимыми, потому что поколение 1 всегда будет лучше, чем поколение 0 (‹ бесконечность). Конечно, поскольку вы изначально установили # потоков = # ядер, система должна случайным образом решить, увеличить или уменьшить на 1 для поколения 2 (поскольку нет понятия того же направления, что и на предыдущем шаге, пока у вас не будет завершено 2 поколения) . Это звучит правильно? - person SFun28; 23.09.2010
comment
Да, ты прав. Бесконечное время для G0 работает лучше. Как я это сформулировал, намерение состояло в том, что мы всегда должны увеличивать количество потоков на 1 для G2. - person Bill Michell; 23.09.2010
comment
Алгоритм, который вы описываете, больше похож на жадное восхождение на холм, чем на эволюционный. В этом случае, вероятно, это хорошее решение, но я не вижу, насколько этот подход эволюционирует. - person JohnIdol; 26.09.2010
comment
@JohnIdol Я предполагаю, что на каждое поколение приходится только одно испытание, так что, возможно, «эволюция» - не лучший термин. - person Bill Michell; 27.09.2010

Если вычисления полностью привязаны к процессору, количество потоков должно быть равно количеству ядер на машине. Таким образом вы минимизируете количество переключений контекста.

Если ваши вычисления связаны с вводом-выводом, сетью, синхронизацией или чем-то еще, что блокирует выполнение, вы должны найти ограничивающий ресурс и измерить его использование. Вам нужно следить за использованием и медленно добавлять больше потоков, пока использование не приблизится к 100%. У вас должно быть как можно меньше потоков, чтобы насытить ваш ограниченный ресурс.

person Albin Sunnanbo    schedule 21.09.2010
comment
Привет, Альбин. Это кажется довольно сложным по сравнению с простым измерением времени, необходимого для генерации, и минимизацией этого с помощью алгоритма оптимизации. Расчеты привязаны к процессору, диску, сети, и мне сложно сказать, какова пропорция и как она меняется со временем. Но одно я знаю точно - чем быстрее работает поколение, тем лучше... так не должно ли решение просто быть привязано ко времени поколения? - person SFun28; 22.09.2010
comment
@ SFun28 - Что произойдет, если вы запустите 2 потока для каждого ядра, вы получите ~ 100% процессора? - person Albin Sunnanbo; 22.09.2010
comment
@Donal Fellows - я бы предпочел избегать ручной настройки. Я мог сидеть там часами или днями, настраивая его. Конечно, простой алгоритм будет работать более эффективно, чем я, при выборе правильных # потоков. - person SFun28; 23.09.2010
comment
@ SFun28: Автонастройка сложнее, чем кажется, и обычно вы можете хорошо поработать над ручной настройкой, используя предварительные вычисления. Затем вы измеряете, смотрите, достаточно ли это хорошо, и если да, перестаньте с этим возиться. Оптимальность сложна, но адекватность обычно достижима с одной-двух попыток (особенно с небольшим опытом). - person Donal Fellows; 23.09.2010

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

Вам нужно гораздо больше задач, чем ядер, чтобы быть уверенным, что в конце генерации не останется только одна задача, а все остальные потоки будут бездействовать. Это то, что может произойти, если вы установите #tasks = #threads = #cores, как предлагает Альбин (если вы не можете гарантировать, что все задачи занимают одинаковое количество времени).

Вы также, вероятно, не хотите больше потоков, чем ядер. Переключение контекста не очень дорого, но больший объем кэш-памяти, связанный с одновременным выполнением более чем #cores задач, может навредить вам (если только ваши задачи не используют очень мало памяти).

person Keith Randall    schedule 21.09.2010
comment
Привет, Кит - см. комментарий к моему вопросу и ответ Альбину. Создавать по одному потоку на ядро ​​просто (на самом деле я делаю это прямо сейчас), но я думаю, что это неоптимально. - person SFun28; 22.09.2010
comment
Возможно, вы могли бы использовать еще несколько потоков, если части ваших вычислений связаны с вводом-выводом. Но вам не нужно много больше. Что-то вроде #cores + #disk head было бы неплохо. - person Keith Randall; 22.09.2010