Python çoklu işlemeyi kullanarak utanç verici derecede paralel sorunları çözme


82

Utanç verici derecede paralel sorunların üstesinden gelmek için çoklu işlem nasıl kullanılır ?

Utanç verici derecede paralel sorunlar tipik olarak üç temel bölümden oluşur:

  1. Giriş verilerini okuyun (bir dosyadan, veritabanından, tcp bağlantısından vb.).
  2. Her hesaplamanın diğer hesaplamalardan bağımsız olduğu giriş verilerinde hesaplamalar çalıştırın .
  3. Hesaplamaların sonuçlarını yazın (bir dosyaya, veritabanına, tcp bağlantısına vb.).

Programı iki boyutta paralelleştirebiliriz:

  • Her hesaplama bağımsız olduğundan Bölüm 2 birden çok çekirdekte çalışabilir; işlem sırası önemli değil.
  • Her bölüm bağımsız olarak çalışabilir. Bölüm 1 verileri bir girdi kuyruğuna yerleştirebilir, bölüm 2 verileri girdi kuyruğundan çekebilir ve sonuçları bir çıktı kuyruğuna koyabilir ve bölüm 3 sonuçları çıktı kuyruğundan çekip yazabilir.

Bu, eşzamanlı programlamada en temel model gibi görünüyor, ancak yine de onu çözmeye çalışırken kayboldum, bu yüzden çoklu işlem kullanılarak bunun nasıl yapıldığını göstermek için kanonik bir örnek yazalım .

Örnek problem: Girdi olarak tamsayı satırları olan bir CSV dosyası verildiğinde , toplamlarını hesaplayın. Problemi hepsi paralel çalışabilecek üç kısma ayırın:

  1. Girdi dosyasını ham veriye işleyin (tamsayıların listeleri / yinelenenleri)
  2. Paralel olarak verilerin toplamını hesaplayın
  3. Toplamları çıkar

Aşağıda, bu üç görevi çözen geleneksel, tek işlemli bağlı Python programı verilmiştir:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# basicsums.py
"""A program that reads integer values from a CSV file and writes out their
sums to another CSV file.
"""

import csv
import optparse
import sys

def make_cli_parser():
    """Make the command line interface parser."""
    usage = "\n\n".join(["python %prog INPUT_CSV OUTPUT_CSV",
            __doc__,
            """
ARGUMENTS:
    INPUT_CSV: an input CSV file with rows of numbers
    OUTPUT_CSV: an output file that will contain the sums\
"""])
    cli_parser = optparse.OptionParser(usage)
    return cli_parser


def parse_input_csv(csvfile):
    """Parses the input CSV and yields tuples with the index of the row
    as the first element, and the integers of the row as the second
    element.

    The index is zero-index based.

    :Parameters:
    - `csvfile`: a `csv.reader` instance

    """
    for i, row in enumerate(csvfile):
        row = [int(entry) for entry in row]
        yield i, row


def sum_rows(rows):
    """Yields a tuple with the index of each input list of integers
    as the first element, and the sum of the list of integers as the
    second element.

    The index is zero-index based.

    :Parameters:
    - `rows`: an iterable of tuples, with the index of the original row
      as the first element, and a list of integers as the second element

    """
    for i, row in rows:
        yield i, sum(row)


def write_results(csvfile, results):
    """Writes a series of results to an outfile, where the first column
    is the index of the original row of data, and the second column is
    the result of the calculation.

    The index is zero-index based.

    :Parameters:
    - `csvfile`: a `csv.writer` instance to which to write results
    - `results`: an iterable of tuples, with the index (zero-based) of
      the original row as the first element, and the calculated result
      from that row as the second element

    """
    for result_row in results:
        csvfile.writerow(result_row)


def main(argv):
    cli_parser = make_cli_parser()
    opts, args = cli_parser.parse_args(argv)
    if len(args) != 2:
        cli_parser.error("Please provide an input file and output file.")
    infile = open(args[0])
    in_csvfile = csv.reader(infile)
    outfile = open(args[1], 'w')
    out_csvfile = csv.writer(outfile)
    # gets an iterable of rows that's not yet evaluated
    input_rows = parse_input_csv(in_csvfile)
    # sends the rows iterable to sum_rows() for results iterable, but
    # still not evaluated
    result_rows = sum_rows(input_rows)
    # finally evaluation takes place as a chain in write_results()
    write_results(out_csvfile, result_rows)
    infile.close()
    outfile.close()


if __name__ == '__main__':
    main(sys.argv[1:])

Bu programı alalım ve yukarıda özetlenen üç parçayı paralelleştirmek için çoklu işlemeyi kullanmak üzere yeniden yazalım. Aşağıda, yorumlardaki bölümleri ele almak için detaylandırılması gereken bu yeni, paralelleştirilmiş programın bir iskeleti verilmiştir:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# multiproc_sums.py
"""A program that reads integer values from a CSV file and writes out their
sums to another CSV file, using multiple processes if desired.
"""

import csv
import multiprocessing
import optparse
import sys

NUM_PROCS = multiprocessing.cpu_count()

def make_cli_parser():
    """Make the command line interface parser."""
    usage = "\n\n".join(["python %prog INPUT_CSV OUTPUT_CSV",
            __doc__,
            """
ARGUMENTS:
    INPUT_CSV: an input CSV file with rows of numbers
    OUTPUT_CSV: an output file that will contain the sums\
"""])
    cli_parser = optparse.OptionParser(usage)
    cli_parser.add_option('-n', '--numprocs', type='int',
            default=NUM_PROCS,
            help="Number of processes to launch [DEFAULT: %default]")
    return cli_parser


def main(argv):
    cli_parser = make_cli_parser()
    opts, args = cli_parser.parse_args(argv)
    if len(args) != 2:
        cli_parser.error("Please provide an input file and output file.")
    infile = open(args[0])
    in_csvfile = csv.reader(infile)
    outfile = open(args[1], 'w')
    out_csvfile = csv.writer(outfile)

    # Parse the input file and add the parsed data to a queue for
    # processing, possibly chunking to decrease communication between
    # processes.

    # Process the parsed data as soon as any (chunks) appear on the
    # queue, using as many processes as allotted by the user
    # (opts.numprocs); place results on a queue for output.
    #
    # Terminate processes when the parser stops putting data in the
    # input queue.

    # Write the results to disk as soon as they appear on the output
    # queue.

    # Ensure all child processes have terminated.

    # Clean up files.
    infile.close()
    outfile.close()


if __name__ == '__main__':
    main(sys.argv[1:])

Bu kod parçalarının yanı sıra test amacıyla örnek CSV dosyaları oluşturabilen başka bir kod parçası da github'da bulunabilir .

Eşzamanlılık uzmanlarının bu soruna nasıl yaklaşacağına dair buradaki herhangi bir içgörüyü takdir ediyorum.


İşte bu problem hakkında düşünürken aklıma gelen bazı sorular. Herhangi birini / tümünü ele almak için bonus puanlar:

  • Verileri okumak ve kuyruğa yerleştirmek için çocuk süreçlerim olmalı mı, yoksa ana süreç bunu tüm girdiler okunana kadar engellemeden yapabilir mi?
  • Aynı şekilde, işlenen kuyruktan sonuçları yazmak için bir alt süreç kullanmalı mıyım yoksa ana süreç tüm sonuçları beklemek zorunda kalmadan bunu yapabilir mi?
  • Toplam işlemler için bir işlem havuzu kullanmalı mıyım ?
  • Veri girerken girdi ve çıktı kuyruklarını sifonlamamıza gerek olmadığını, ancak tüm girdiler ayrıştırılana ve tüm sonuçlar hesaplanana kadar beklediğimizi varsayalım (örneğin, tüm girdi ve çıkışın sistem belleğine sığacağını biliyoruz). Algoritmayı herhangi bir şekilde değiştirmeli miyiz (örneğin, herhangi bir işlemi G / Ç ile aynı anda çalıştırmamalı mıyız)?

2
Haha, utanç verici derecede paralel terimini seviyorum. Bu terimi ilk defa duyduğuma şaşırdım, bu kavrama atıfta bulunmanın harika bir yolu.
Tom Neyland

Yanıtlar:


70

Çözümümde, çıktının sırasının giriş sırasıyla aynı olduğundan emin olmak için fazladan bir zil ve ıslık var. İşlemler arasında veri göndermek için multiprocessing.queue kullanıyorum, dur mesajları gönderiyorum, böylece her işlem kuyrukları kontrol etmeyi bırakacağını biliyor. Bence kaynaktaki yorumlar neler olup bittiğini açıklığa kavuşturmalı ama bana haber vermiyorlarsa.

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# multiproc_sums.py
"""A program that reads integer values from a CSV file and writes out their
sums to another CSV file, using multiple processes if desired.
"""

import csv
import multiprocessing
import optparse
import sys

NUM_PROCS = multiprocessing.cpu_count()

def make_cli_parser():
    """Make the command line interface parser."""
    usage = "\n\n".join(["python %prog INPUT_CSV OUTPUT_CSV",
            __doc__,
            """
ARGUMENTS:
    INPUT_CSV: an input CSV file with rows of numbers
    OUTPUT_CSV: an output file that will contain the sums\
"""])
    cli_parser = optparse.OptionParser(usage)
    cli_parser.add_option('-n', '--numprocs', type='int',
            default=NUM_PROCS,
            help="Number of processes to launch [DEFAULT: %default]")
    return cli_parser

class CSVWorker(object):
    def __init__(self, numprocs, infile, outfile):
        self.numprocs = numprocs
        self.infile = open(infile)
        self.outfile = outfile
        self.in_csvfile = csv.reader(self.infile)
        self.inq = multiprocessing.Queue()
        self.outq = multiprocessing.Queue()

        self.pin = multiprocessing.Process(target=self.parse_input_csv, args=())
        self.pout = multiprocessing.Process(target=self.write_output_csv, args=())
        self.ps = [ multiprocessing.Process(target=self.sum_row, args=())
                        for i in range(self.numprocs)]

        self.pin.start()
        self.pout.start()
        for p in self.ps:
            p.start()

        self.pin.join()
        i = 0
        for p in self.ps:
            p.join()
            print "Done", i
            i += 1

        self.pout.join()
        self.infile.close()

    def parse_input_csv(self):
            """Parses the input CSV and yields tuples with the index of the row
            as the first element, and the integers of the row as the second
            element.

            The index is zero-index based.

            The data is then sent over inqueue for the workers to do their
            thing.  At the end the input process sends a 'STOP' message for each
            worker.
            """
            for i, row in enumerate(self.in_csvfile):
                row = [ int(entry) for entry in row ]
                self.inq.put( (i, row) )

            for i in range(self.numprocs):
                self.inq.put("STOP")

    def sum_row(self):
        """
        Workers. Consume inq and produce answers on outq
        """
        tot = 0
        for i, row in iter(self.inq.get, "STOP"):
                self.outq.put( (i, sum(row)) )
        self.outq.put("STOP")

    def write_output_csv(self):
        """
        Open outgoing csv file then start reading outq for answers
        Since I chose to make sure output was synchronized to the input there
        is some extra goodies to do that.

        Obviously your input has the original row number so this is not
        required.
        """
        cur = 0
        stop = 0
        buffer = {}
        # For some reason csv.writer works badly across processes so open/close
        # and use it all in the same process or else you'll have the last
        # several rows missing
        outfile = open(self.outfile, "w")
        self.out_csvfile = csv.writer(outfile)

        #Keep running until we see numprocs STOP messages
        for works in range(self.numprocs):
            for i, val in iter(self.outq.get, "STOP"):
                # verify rows are in order, if not save in buffer
                if i != cur:
                    buffer[i] = val
                else:
                    #if yes are write it out and make sure no waiting rows exist
                    self.out_csvfile.writerow( [i, val] )
                    cur += 1
                    while cur in buffer:
                        self.out_csvfile.writerow([ cur, buffer[cur] ])
                        del buffer[cur]
                        cur += 1

        outfile.close()

def main(argv):
    cli_parser = make_cli_parser()
    opts, args = cli_parser.parse_args(argv)
    if len(args) != 2:
        cli_parser.error("Please provide an input file and output file.")

    c = CSVWorker(opts.numprocs, args[0], args[1])

if __name__ == '__main__':
    main(sys.argv[1:])

1
Bu sadece aslında kullanılan cevabı multiprocessing. Ödül size gidiyor, efendim.
gotgenes

1
joinGirdi ve numara hesaplama süreçlerini çağırmak gerçekten gerekli mi? Sadece çıktı sürecine katılmakla ve diğerlerini görmezden gelmekle kurtulamaz mısın? Öyleyse join, diğer tüm süreçleri çağırmak için hala iyi bir neden var mı?
Ryan C. Thompson

"böylece iş parçacıkları çıkmayı bilir" - "iş parçacıkları arasında veri gönder" - İş parçacıkları ve süreçler çok farklıdır. Bunun acemiler için kafa karıştırıcı olabileceğini görüyorum. Daha da önemlisi, bu kadar olumlu oy alan bir yanıtta doğru terminolojiyi kullanmaktır. Burada yeni süreçlere başlıyorsunuz. Şu anki süreçte sadece ipler üretmiyorsunuz.
Dr Jan-Philip Gehrcke

Yeterince adil. Metni düzelttim.
hbar

Harika cevap. Çok teşekkür ederim.
eggonlegs

7

Partiye geç geliyor ...

joblib , döngüler için paralel oluşturmaya yardımcı olmak için çoklu işlemenin üstünde bir katmana sahiptir. Size tembel bir iş dağıtımı ve çok basit sözdizimine ek olarak daha iyi hata raporlama gibi olanaklar sunar.

Feragatname olarak, joblib'in orijinal yazarıyım.


3
Öyleyse Joblib, G / Ç'yi paralel olarak yönetebilir mi yoksa bunu elle yapmanız mı gerekiyor? Joblib kullanarak bir kod örneği sağlayabilir misiniz? Teşekkürler!
Roko Mijic

5

Partiye biraz geç kaldığımı anlıyorum, ancak yakın zamanda GNU paralelini keşfettim ve bu tipik görevi onunla başarmanın ne kadar kolay olduğunu göstermek istiyorum.

cat input.csv | parallel ./sum.py --pipe > sums

Bunun gibi bir şey şunları yapacak sum.py:

#!/usr/bin/python

from sys import argv

if __name__ == '__main__':
    row = argv[-1]
    values = (int(value) for value in row.split(','))
    print row, ':', sum(values)

Paralel, girişteki sum.pyher satır için çalışır input.csv(tabii ki paralel olarak), ardından sonuçları sums. Açıkça daha iyi multiprocessinggüçlük


3
GNU paralel dokümanları, girdi dosyasındaki her satır için yeni bir Python yorumlayıcısı çağıracaktır. Yeni bir Python yorumlayıcısını başlatmanın ek yükü (katı hal sürücülü i7 MacBook Pro'mda Python 2.7 için yaklaşık 30 milisaniye ve Python 3.3 için 40 milisaniye), tek bir veri satırını işlemek için gereken süreden büyük ölçüde ağır basabilir ve çok fazla zaman israfı ve beklenenden daha kötü kazançlar. Örnek probleminiz söz konusu olduğunda, muhtemelen çoklu işlemeye uzanırdım .
gotgenes

4

Eski okul.

p1.py

import csv
import pickle
import sys

with open( "someFile", "rb" ) as source:
    rdr = csv.reader( source )
    for line in eumerate( rdr ):
        pickle.dump( line, sys.stdout )

p2.py

import pickle
import sys

while True:
    try:
        i, row = pickle.load( sys.stdin )
    except EOFError:
        break
    pickle.dump( i, sum(row) )

p3.py

import pickle
import sys
while True:
    try:
        i, row = pickle.load( sys.stdin )
    except EOFError:
        break
    print i, row

İşte çoklu işlemenin son yapısı.

python p1.py | python p2.py | python p3.py

Evet, kabuk bunları işletim sistemi düzeyinde birbirine ördü. Bana daha basit görünüyor ve çok güzel çalışıyor.

Evet, turşu (veya cPickle) kullanmanın biraz daha fazla yükü var. Ancak sadeleştirme çabaya değer görünüyor.

Dosya adının bir argüman olmasını istiyorsanız p1.py, bu kolay bir değişikliktir.

Daha da önemlisi, aşağıdaki gibi bir işlev çok kullanışlıdır.

def get_stdin():
    while True:
        try:
            yield pickle.load( sys.stdin )
        except EOFError:
            return

Bu, bunu yapmanıza izin verir:

for item in get_stdin():
     process item

Bu çok basittir, ancak birden fazla P2.py kopyasının çalıştırılmasına kolayca izin vermez.

İki probleminiz var: yayılma ve yayılma. P1.py bir şekilde birden fazla P2.py'ye yayılmalıdır. Ve P2.py'lerin sonuçlarını bir şekilde tek bir P3.py'de birleştirmesi gerekir.

Yayılmaya yönelik eski okul yaklaşımı, çok etkili bir "İtme" mimarisidir.

Teorik olarak, birden fazla P2.py'nin ortak bir kuyruktan çekilmesi, kaynakların en iyi şekilde tahsis edilmesidir. Bu genellikle idealdir, ancak aynı zamanda makul bir programlama miktarıdır. Programlama gerçekten gerekli mi? Yoksa sıralı işleme yeterince iyi olacak mı?

Pratik olarak, P1.py'nin birden fazla P2.py arasında basit bir "döngüsel robin" yapması gerektiğini göreceksiniz. P1.py, adlandırılmış yöneltmeler aracılığıyla P2.py'nin n kopyasını işleyecek şekilde yapılandırılırdı . P2.py'lerin her biri kendi uygun kanallarından okur.

Ya bir P2.py tüm "en kötü durum" verilerini alır ve çok geride kalırsa? Evet, round-robin mükemmel değil. Ancak bu, yalnızca bir P2.py'den daha iyidir ve bu önyargıyı basit rastgele seçimlerle ele alabilirsiniz.

Birden fazla P2.py'den bir P3.py'ye giriş yapmak biraz daha karmaşıktır. Bu noktada, eski usul yaklaşımı avantajlı olmaktan çıkıyor. P3.py'nin okumaları selectserpiştirmek için kitaplığı kullanarak birden çok adlandırılmış kanaldan okuması gerekir .


nP2.py örneklerini başlatmak , p1.py tarafından çıkarılan satır mparçalarını tüketmelerini ve rişlemelerini sağlamak ve p3.py'nin tüm p2.py örneklerinden mx rsonuçlarını nalmasını sağlamak istediğimde bu daha saçma olmaz mı?
gotgenes

1
Soruda bu şartı görmedim. (Belki de soru, bu gereksinimi öne çıkarmak için çok uzun ve karmaşıktı.) Önemli olan, birden çok p2'nin performans sorununuzu gerçekten çözmesini beklemek için gerçekten iyi bir nedeninizin olması gerektiğidir. Böyle bir durumun var olabileceğini varsayabilirken, * nix mimarisi buna hiç sahip olmadı ve kimse onu eklemeyi uygun görmedi. Birden fazla p2'ye sahip olmak faydalı olabilir. Ancak son 40 yıldır kimse onu kabuğun birinci sınıf bir parçası yapmaya yeterince ihtiyaç duymadı.
S.Lott

O zaman bu benim hatam. Bu noktayı düzenlememe ve netleştirmeme izin verin. Soruyu geliştirmeme yardımcı olmak için, kafa karışıklığı kullanımından sum()mı kaynaklanıyor ? Bu açıklama amaçlıdır. Onunla değiştirebilirdim do_something(), ancak somut, anlaşılması kolay bir örnek istedim (ilk cümleye bakın). Gerçekte, benim do_something()çok CPU yoğun, ancak her çağrı bağımsız olduğu için utanç verici şekilde paralelleştirilebilir. Bu nedenle, üzerinde çiğneme birden fazla çekirdek yardımcı olacaktır.
gotgenes

"karışıklık sum () kullanımından mı kaynaklanıyor?" Açıkça değil. Neden bahsettiğinden emin değilim. "P2.py'nin n adet örneğini başlatmak istediğimde bu daha da saçma olmaz mı" dediniz. Soruda bu şartı görmedim.
S.Lott

0

Birinci bölüme de biraz paralellik katmak muhtemelen mümkündür. Muhtemelen CSV kadar basit bir formatla ilgili bir sorun değildir, ancak giriş verilerinin işlenmesi, verilerin okunmasından belirgin ölçüde daha yavaşsa, daha büyük parçaları okuyabilir ve ardından bir "satır ayırıcı" bulana kadar okumaya devam edebilirsiniz ( CSV durumunda yeni satır, ancak yine bu, okunan biçime bağlıdır; biçim yeterince karmaşıksa çalışmaz).

Her biri muhtemelen birden fazla girdi içeren bu parçalar, daha sonra bir kuyruktaki işleri okuyan paralel süreçler kalabalığına toplanabilir, burada ayrıştırılır ve ayrılır, ardından 2. aşama için sıraya yerleştirilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.