使用Python Multiprocessing从令人尴尬的并行任务中获得预期的加速

时间:2014-10-24 20:26:11

标签: python parallel-processing python-multiprocessing embarrassingly-parallel parallelism-amdahl

我正在学习使用Python的多处理软件包来解决令人尴尬的并行问题,所以我编写了串行和并行版本来确定小于或等于自然数 n的素数的数量。根据我从blog postStack Overflow question中读到的内容,我想出了以下代码:

串行

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)

并行

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的法律回答了这个问题,但我不知道如何确定我的程序的哪一部分是严格连续的。
  3. 感谢任何帮助。

    编辑:有4个物理核心,能够超线程化。

2 个答案:

答案 0 :(得分:6)

我认为你想以不同的方式划分工作。

虽然您的程序在核心之间均匀划分候选整数的范围,但每个范围内的工作可能不均匀。这意味着一些核心提前完成,无所事事,而其他核心仍在运行。这会失去并行效率,快速

为了说明问题,想象一下你有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 ;它需要一系列由其参数 lower upper 定义的候选值。它尝试该范围内的每个候选者,并且如果该候选者是素数,则递增(原子地)总 prime_count

整个范围被do分成小范围的范围(奇数序列) 在 main 中循环。并行性发生在 draft 命令上,该命令创建并行执行计算粒度(不是Windows线程)并将其添加到 prime_finding_team ,这是一组聚合工作代表所有主要因素。 (团队的目的是允许所有这些工作作为一个单元进行管理,例如,在必要时销毁,在此程序中不需要)。 draft 的参数是由分叉粒度及其参数运行的函数。这项工作是由Parlanse管理的一组(Windows)线程使用工作窃取算法完成的。如果有太多的工作,Parlanse会限制产生工作的粒子,并将其能量用于处理纯粹计算的粒子。

每个粒子只能传递一个候选值,但是每个候选者的叉开销会更多,总运行时间会相应变差。我们根据经验选择了 10 ,以确保每个候选范围的分支开销很小;将每个段的候选人设置为1000并不会增加额外的加速。

do 循环只是尽可能快地制作工作。当有足够的并行性有用时,Parlanse会限制 draft 步骤。团队活动中的等待会导致主程序等待所有团队成员完成。

我们在HP六核AMD Phenom II X6 1090T 3.2 GHz上运行。 执行运行如下;首先是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个CPU(很好地扩展):

>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

您注意到并行版本的总CPU时间与串行版本大致相同;这是因为他们正在做同样的工作。

鉴于Python的“fork”和“join”操作,我确信有一个可以轻松编写的Python等价物。它可能会耗尽空间或线程,因为同时可能有太多的叉子。 (当 candidates_per_segment 为10时,Parlanse下有多达100万粒活粒运行。这就是自动限制工作生成是一件好事。作为替代,您可以将candidates_per_segment设置为更大的数字,例如10000,这意味着您只能获得1000个最坏情况的线程。 (我认为由于Python的解释性质,你仍然会付出高昂的代价)。当您将每个段的候选项设置得越来越接近1e7 / 4时,您将接近使用当前Python代码的确切行为。

答案 1 :(得分:1)

您不会获得比CPU中的内核/线程数更多的并行性。如果您在4核计算机上获得3倍的加速,那就相当不错了。你只有轻微的开销。我建议你在4核机器上设置&#34;并行进程的数量&#34;到4以减少开销。现在,如果你在16核机器上运行它,速度只有3倍似乎很低。我会看一下Python Multiprocessing库,特别是它如何运行它的线程(进程?)。

numProc == 4的结果是什么?

Amdahl定律适用于此,但只有很小一部分并行程序是顺序的(基本上是主要部分),因为工作非常均匀地并行化为整数范围。