5

Я изучаю пакет Multiprocessing Python для смущающих параллельных проблем, поэтому я написал серийные и параллельные версии для определения числа простых чисел, меньших или равных натуральному числу n , Исходя из того, что я читал из blog post и Stack Overflow question, я придумал следующий код:Ожидаемое ускорение от неловкой параллельной задачи с использованием многопроцессорной обработки Python

Последовательная

import math 
import time 

def is_prime(start, end): 
    """determine how many primes within given range""" 
    numPrime = 0 
    for n in range(start, end+1): 
     isPrime = True 
     for i in range(2, math.floor(math.sqrt(n))+1): 
      if n % i == 0: 
       isPrime = False 
       break 
     if isPrime: 
      numPrime += 1 
    if start == 1: 
     numPrime -= 1 # since 1 is not prime 
    return numPrime 

if __name__ == "__main__": 
    natNum = 0 
    while natNum < 2: 
     natNum = int(input('Enter a natural number greater than 1: ')) 
    startTime = time.time() 
    finalResult = is_prime(1, natNum) 
    print('Elapsed time:', time.time()-startTime, 'seconds') 
    print('The number of primes <=', natNum, 'is', finalResult) 

Parallel

import math 
import multiprocessing as mp 
import numpy 
import time 


def is_prime(vec, output): 
    """determine how many primes in vector""" 
    numPrime = 0 
    for n in vec: 
     isPrime = True 
     for i in range(2, math.floor(math.sqrt(n))+1): 
      if n % i == 0: 
       isPrime = False 
       break 
     if isPrime: 
      numPrime += 1 
    if vec[0] == 1: 
     numPrime -= 1 # since 1 is not prime 
    output.put(numPrime) 


def chunks(vec, n): 
    """evenly divide list into n chunks""" 
    for i in range(0, len(vec), n): 
     yield vec[i:i+n] 

if __name__ == "__main__": 
    natNum = 0 
    while natNum < 2: 
     natNum = int(input('Enter a natural number greater than 1: ')) 
    numProc = 0 
    while numProc < 1: 
     numProc = int(input('Enter the number of desired parallel processes: ')) 
    startTime = time.time() 
    numSplits = math.ceil(natNum/numProc) 
    splitList = list(chunks(tuple(range(1, natNum+1)), numSplits)) 
    output = mp.Queue() 
    processes = [mp.Process(target=is_prime, args=(splitList[jobID], output)) 
       for jobID in range(numProc)] 
    for p in processes: 
     p.start() 
    for p in processes: 
     p.join() 
    print('Elapsed time:', time.time()-startTime, 'seconds') 
    procResults = [output.get() for p in processes] 
    finalResult = numpy.sum(numpy.array(procResults)) 
    print('Results from each process:\n', procResults) 
    print('The number of primes <=', natNum, 'is', finalResult) 

Вот что я получаю за n = 10000000 (для параллельного запроса 8 процессов):

$ python serial_prime_test.py 
Enter a natural number greater than 1: 10000000 
Elapsed time: 162.1960825920105 seconds 
The number of primes <= 10000000 is 664579 
$ python parallel_prime_test.py 
Enter a natural number greater than 1: 10000000 
Enter the number of desired parallel processes: 8 
Elapsed time: 49.41204643249512 seconds 
Results from each process: 
[96469, 86603, 83645, 80303, 81796, 79445, 78589, 77729] 
The number of primes <= 10000000 is 664579 

Так что, похоже, я могу получить чуть более 3-х ступеней ускорения. Вот мои вопросы :

  1. Очевидно, что это не линейное ускорение, так как гораздо лучше, я могу сделать (или что убыстрение я должен реально рассчитывать)?
  2. Похоже, закон Amdahl's отвечает на это, но я не знаю, как определить, какая часть моей программы строго последовательна.

Любая помощь приветствуется.

Редактировать: Есть 4 физических ядра, способных к гиперповерхности.

+1

Сколько ядер у вас есть? –

+1

Ваш код, вероятно, займет больше времени для больших чисел, так что задача с наибольшим диапазоном займет больше времени. Вы проверяли, простаивают ли процессоры в конце запуска программы? –

+0

Для серьезных ускорений вы должны сначала рассмотреть вопрос о том, является ли Python правильным выбором. Программа из скомпилированного языка, скорее всего, будет в 10 раз быстрее, что эквивалентно наличию 10 ядер идеально параллельным. –

ответ

5

Я думаю, вы хотите разделить работу по-разному.

Хотя ваша программа делит диапазона кандидатов целых равномерно по всем сердечникам, то работы в каждом диапазоне может быть даже не так. Это означает, что некоторые ядра заканчиваются раньше, и им нечего делать, в то время как другие все еще работают. Это теряет параллельную эффективность, fast.

Просто, чтобы понять, что у вас 1000 ядер. Первое ядро ​​видит очень маленькие номера кандидатов и не занимает много времени, чтобы их фактор, а затем простаивает. Последнее (тысячное) ядро ​​видит только очень большие числа кандидатов и занимает гораздо больше времени, чтобы разделить их. Таким образом, он запускается, в то время как первое ядро ​​сидит без дела. Отработанные циклы. Аналогично для 4 ядер.

Что вы хотите сделать, когда количество работы, передаваемой на ядро, неизвестно, это рука всех ядер большого количества кусков скромного размера, гораздо больше, чем ядер. Затем ядра могут заканчиваться неравномерно, и каждое ядро ​​возвращается, чтобы найти немного больше работы. Это, по сути, алгоритм рабочего списка. Вы в конечном итоге с неравномерностью в самом конце, но это только на небольших кусках, поэтому не так много потрачено впустую.

Я не программист на Python, поэтому вместо этого я закодировал решение в Parlanse.

(includeunique `Console.par') 
(includeunique `Timer.par') 

(define upper_limit 10000000) 

(define candidates_per_segment 10) 
(define candidates_per_segment2 (constant (* candidates_per_segment 2))) 

(= [prime_count natural] 0) 
[prime_finding_team team] 

(define primes_in_segment 
(action (procedure [lower natural] [upper natural]) 
    (;; 
     (do [candidate natural] lower upper 2 
     (block test_primality 
     (local (= [divisor natural] 3) 
      (;; 
       (while (< (* divisor divisor) candidate) 
        (ifthenelse (== (modulo candidate divisor) 0) 
        (exitblock test_primality) 
        (+= divisor 2) 
       )ifthenelse 
      )while 
       (ifthen (~= (* divisor divisor) candidate) 
       (consume (atomic+= prime_count)) 
      )ifthen 
      );; 
     )local 
    )block 
    )do 
);; 
)action 
)define 

(define main 
(action (procedure void) 
    (local [timer Timer:Timer] 
    (;; 
    (Console:Put (. `Number of primes found: ')) 
    (Timer:Reset (. timer)) 
    (do [i natural] 1 upper_limit candidates_per_segment2 
     (consume (draft prime_finding_team primes_in_segment 
        `lower':i 
        `upper':(minimum upper_limit (- (+ i candidates_per_segment2) 2)))) 
    )do 
    (consume (wait (@ (event prime_finding_team)))) 
    (Timer:Stop (. timer)) 
    (Console:PutNatural prime_count) 
    (Console:PutNewline) 
    (Timer:PrintElapsedTime (. timer) (. `Parallel computed in ')) 
    (Console:PutNewline) 
    );; 
)local 
)action 
)define 

Parlanse выглядит как LISP, но работает и собирает больше как C.

Работник primes_in_segment; он принимает диапазон значений кандидатов, определяемых его параметрами ниже и верхняя. Он пробует каждого кандидата в этом диапазоне и увеличивает (атомарно) итог prime_count, если этот кандидат является простым.

Полный диапазон разделен на небольшие пакеты диапазонов (последовательности нечетных чисел) на петлю в main. Параллелизм происходит в команде draft, которая создает параллельное выполнение вычислений (а не поток Windows) и добавляет его в prime_finding_team, который представляет собой совокупный набор работ, представляющий весь простой факторинг. (Цель команды - разрешить всю эту работу управлять как единицу, например, уничтожить, если необходимо, не требуется в этой программе). Аргументы для draft - это функция, которая должна выполняться разветвленным зерном и его параметрами. Работа выполняется с помощью набора потоков, управляемых Parlanse (Windows), с использованием алгоритма обработки работы. Если есть слишком много работы, Parlanse дросселирует производящие зерно и тратит свою энергию на зерно, которое является чистым вычислением.

Каждому зерну может быть передано только одно значение кандидата, но тогда у кандидата больше накладных расходов на вилку, а общая продолжительность выполнения соответственно ухудшается. Мы выбрали эмпирически, чтобы убедиться, что накладные расходы на вилку для каждого диапазона кандидатов малы; установка кандидатов на один сегмент до 1000 не покупает много дополнительного ускорения.

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

Мы провели это на шестигранном процессоре AMD AMD Phenom II X6 1090T 3,2 ГГц. Прогон выполнения ниже; первый 1 CPU:

>run -p1 -v ..\teamprimes 
PARLANSE RTS: Version 19.1.53 
# Processors = 1 
Number of primes found: 664579 
Parallel computed in 13.443294 seconds 
---- PARLANSE RTS: Performance Statistics 
    Duration = 13.527557 seconds. 
    CPU Time Statistics: 
    Kernel CPU Time: 0.031s 
    User CPU Time: 13.432s 
    Memory Statistics: 
Peak Memory Usage : 4.02 MBytes 
    Steals: 0 Attempts: 0 Success rate: 0.0% Work Rediscovered: 0 
Exiting with final status 0. 

Тогда для 6 процессоров (весы красиво):

>run -p6 -v ..\teamprimes 
PARLANSE RTS: Version 19.1.53 
# Processors = 6 
Number of primes found: 664579 
Parallel computed in 2.443123 seconds 
---- PARLANSE RTS: Performance Statistics 
    Duration = 2.538972 seconds. 
    CPU Time Statistics: 
Kernel CPU Time: 0.000s 
User CPU Time: 14.102s 
Total CPU Time: 14.102s 
    Memory Statistics: 
Peak Memory Usage : 4.28 MBytes 
    Steals: 459817 Attempts: 487334 Success rate: 94.4% Work Rediscovered: 153 

Вы обратите внимание, общее время процессора для параллельной версии примерно такой же, как и для серийной версии; это потому, что они выполняют ту же работу.

Учитывая действия «fork» и «join» Python, я уверен, что есть эквивалент Python, который вы можете легко кодировать.Это может закончиться пространством или потоками из-за возможности слишком большого количества вилок одновременно. (С candidates_per_segment в 10, до 1 миллиона живых зерен, работающих под Parlanse). Это - то, где автоматическое регулирование генерации работы - хорошая вещь, чтобы иметь. В качестве замены вы можете установить candidates_per_segment на гораздо больший номер, например, 10000, что означает, что вы получите только 1000 наихудших случаев. (Я думаю, вы по-прежнему будете платить высокую цену из-за интерпретационной природы Python). Когда вы устанавливаете кандидатов на один сегмент ближе и ближе к 1e7/4, вы подходите к точному поведению, которое у вас есть с вашим нынешним кодом Python.

1

Вы не получите больше параллелизма, чем количество ядер/потоков в вашем CPU. Если вы получаете 3-кратное ускорение на 4-ядерном компьютере, это очень хорошо. У вас только немного накладные расходы. Я бы предположил, что на 4-ядерном компьютере вы установите «количество параллельных процессов» на 4, чтобы уменьшить накладные расходы. Теперь, если вы используете это на 16-ядерном компьютере, скорость только в 3 раза кажется низкой. Я бы посмотрел на библиотеку многопроцессорности Python, в частности, на то, как она работает с потоками (процессы?).

Какие результаты были с numProc == 4?

Закон Amdahl применяется здесь, но только очень небольшая часть вашей параллельной программы является последовательной (в основном основной частью), поскольку работа довольно равномерно распараллелена в целые диапазоны.

+0

Спасибо @rspencer. Я должен был добавить это раньше, но у меня есть 4 физических ядра, способных к гиперповерхности. При использовании numProc == 4 время составляет 49,6 секунды. –

+0

Это то, чего я ожидал бы. Разница между numProc == 4 и numProc == 8 пренебрежимо мала (возможно, в пределах погрешности погрешности). –