5

Ich lerne Python Multiprocessing-Paket für peinlich parallele Probleme zu verwenden, so schrieb ich serielle und parallele Versionen für die Bestimmung der Anzahl der Primzahlen kleiner oder gleich einer natürlichen Zahl n . Nach dem, was ich von einem blog post lesen und Stack Overflow question, kam ich mit dem folgenden Code auf:Erwartete Beschleunigung von peinlich parallelen Aufgabe mit Python Multiprocessing

Serien

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) 

Hier ist, was ich für n = 10000000 (für parallele Ich Anfrage 8 Prozesse):

$ 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 

So sieht es aus wie ich etwas über 3x beschleunigen kann. Hier sind meine Fragen:

  1. Klar ist das nicht lineare Beschleunigung, so wie viel besser kann ich (oder was Speedup soll ich erwarten, realistisch)?
  2. Es sieht so aus, als ob Amdahls Gesetz dies beantwortet, aber ich weiß nicht, wie ich feststellen soll, welcher Teil meines Programms streng seriell ist.

Jede Hilfe wird geschätzt.

Edit: Es gibt 4 physikalische Kerne, Hyperthreading fähig.

+1

Wie viele Kerne haben Sie? –

+1

Ihr Code wird bei größeren Zahlen wahrscheinlich länger dauern, so dass die Aufgabe mit der größten Reichweite am längsten dauert. Haben Sie überprüft, ob die Prozessoren gegen Ende des Programmlaufs inaktiv sind? –

+0

Für ernsthafte Beschleunigungen sollten Sie zuerst überlegen, ob Python die richtige Wahl ist. Ein Programm aus einer kompilierten Sprache ist wahrscheinlich 10x so schnell, das Äquivalent davon, dass 10 Kerne perfekt parallel sind. –

Antwort

5

Ich denke, dass Sie die Arbeit anders aufteilen möchten.

Obwohl Ihr Programm teilt den Bereich von Kandidaten ganzen Zahlen gleichmäßig auf Kerne, die Arbeit in jedem Bereich ist nicht wahrscheinlich sogar sein. Das bedeutet, dass einige Kerne früher fertig sind und nichts zu tun haben, während andere noch laufen. Das verliert parallel Effizienz, schnell.

Um den Punkt zu verdeutlichen, stellen Sie sich 1000 Kerne vor. Der erste Kern sieht sehr kleine Kandidatenzahlen und braucht nicht lange, um sie zu faktorisieren, dann geht er in den Leerlauf. Der letzte (tausendste) Kern sieht nur sehr große Kandidatenzahlen und benötigt viel länger, um sie zu berücksichtigen. So läuft es, während der erste Kern im Leerlauf ist. Vergeudete Zyklen. Ähnlich für 4 Kerne.

Was Sie tun möchten, wenn die Menge der Arbeit, die an einen Kern übergeben wird, unbekannt ist, ist Hand alle Kerne eine Menge bescheidener Größe Brocken, viel mehr als es Kerne gibt. Dann können Kerne mit ungleichmäßigen Raten fertig werden, und jeder Kern geht zurück, um etwas mehr Arbeit zu finden. Dies ist im Wesentlichen ein Arbeitslistenalgorithmus. Am Ende kommt es zu Ungleichmäßigkeit, aber es ist nur auf kleinen Brocken, also wird nicht viel verschwendet.

Ich bin kein Python-Programmierer, also habe ich stattdessen eine Lösung in Parlanse programmiert.

(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 sieht aus wie Lisp, aber funktioniert und stellt mehr wie C.

Der Arbeiter primes_in_segment ist; es nimmt einen Bereich von Kandidatenwerten, die durch seine Parameter definiert sind unteren und oberen. Es versucht jeden Kandidaten in diesem Bereich und erhöht (atomar) insgesamt prime_count, wenn dieser Kandidat eine Primzahl ist.

Der gesamte Bereich ist in kleine Pakete von Bereichen (Folgen von ungeraden Zahlen) durch die Do Schleife in Haupt aufgeteilt. Die Parallelität geschieht auf dem Befehl Entwurf, die eine parallele Ausführung Korn der Berechnung erstellt (nicht ein Windows-Thread) und fügt sie prime_finding_team, die eine aggregierte Menge von Arbeit, die alle Prim Factoring darstellt. (Der Zweck eines Teams besteht darin, all diese Arbeit als Einheit verwalten zu lassen, z. B. bei Bedarf zerstört, in diesem Programm nicht benötigt). Die Argumente zu Entwurf sind die Funktion, die von dem gabelförmigen Korn und seinen Parametern ausgeführt werden soll. Die Arbeit wird von einem Parlanse-verwalteten Satz von (Windows-) Threads mithilfe eines Work-Stealing-Algorithmus ausgeführt. Wenn es zu viel Arbeit gibt, drosselt Parlanse die Arbeit erzeugenden Körner, und gibt seine Energie aus, Körner ausführend, die reine Berechnung sind.

Man könnte zu jedem Korn nur einen Kandidatenwert übergeben, aber dann gibt es mehr Gabel-Overhead pro Kandidat und die Gesamtlaufzeit wird entsprechend schlechter. Wir haben empirisch ausgewählt, um sicherzustellen, dass der Gabel-Overhead pro Kandidatenbereich klein ist; das Setzen der Kandidaten pro Segment auf 1000 kostet nicht viel zusätzliche Beschleunigung.

Die tun Schleife einfach produziert Arbeit so schnell wie es kann. Parlanse drosselt den Schritt Entwurf, wenn genügend Parallelität vorhanden ist, um nützlich zu sein. Das Warten auf auf dem Teamereignis bewirkt, dass das Hauptprogramm auf alle Teammitglieder wartet, um abzuschließen.

Wir haben dies auf einem HP Hex-Kern AMD Phenom II X6 1090T 3,2 GHz ausgeführt. Ausführungsläufe sind unten; zuerst für 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. 

Dann gilt für 6 CPUs (skaliert gut):

>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 

Sie beachten Sie die gesamte CPU-Zeit für die parallele Version ist in etwa die gleiche wie bei der Serienversion; Das liegt daran, dass sie die gleiche Arbeit machen.

Angesichts Python "Gabel" und "Join" -Operationen, ich bin sicher, es gibt eine Python-Entsprechung, die Sie einfach programmieren können.Es könnte Platz oder Threads wegen der Möglichkeit von zu vielen Gabeln zur selben Zeit ausgehen. (Mit candidates_per_segment um 10 gibt es bis zu 1 Million lebende Körner unter Parlanse). Hier ist die automatische Drosselung der Arbeitserzeugung eine gute Sache. Als Ersatz können Sie candidates_per_segment auf eine viel größere Zahl, z. B. 10000, festlegen, was bedeutet, dass Sie im schlimmsten Fall nur 1000 Threads erhalten. (Ich denke, Sie werden immer noch einen hohen Preis bezahlen, weil Python interpretativ ist). Wenn Sie die Kandidaten pro Segment näher an 1e7/4 setzen, nähern Sie sich dem genauen Verhalten, das Sie mit Ihrem aktuellen Python-Code haben.

1

Sie erhalten keine Parallelität mehr als die Anzahl der Kerne/Threads in Ihrer CPU. Wenn Sie eine 4-Kern-Maschine 3x beschleunigen, ist das ziemlich gut. Du hast nur einen kleinen Overhead. Ich würde vorschlagen, dass Sie auf einer 4-Kern-Maschine "Anzahl der parallelen Prozesse" auf 4 setzen, um den Overhead zu reduzieren. Nun, wenn Sie das auf einem 16-Core-Rechner ausführen, scheint eine Beschleunigung von nur 3x niedrig. Ich würde mir die Python Multiprocessing-Bibliothek dafür anschauen, insbesondere wie sie ihre Threads (Prozesse?) Ausführt.

Was waren Ihre Ergebnisse mit numProc == 4?

Amdahls Gesetz gilt hier, aber nur ein sehr kleiner Teil Ihres parallelen Programms ist sequentiell (im Wesentlichen der Hauptteil), da die Arbeit ziemlich gleichmäßig in den ganzen Zahlenbereichen parallelisiert ist.

+0

Danke @rspencer. Ich hätte das früher hinzufügen sollen, aber ich habe 4 physische Kerne, die zum Hyperthreading fähig sind. Mit numProc == 4 beträgt die Zeit 49,6 Sekunden. –

+0

Das ist ungefähr das, was ich erwarten würde. Der Unterschied zwischen numProc == 4 und numProc == 8 ist vernachlässigbar (wahrscheinlich innerhalb der Fehlergrenze). –

Verwandte Themen