web-dev-qa-db-fra.com

multiprocessing in python - partage d'un grand objet (par exemple pandas dataframe) entre plusieurs processus)

J'utilise Python multiprocessing, plus précisément

from multiprocessing import Pool
p = Pool(15)

args = [(df, config1), (df, config2), ...] #list of args - df is the same object in each Tuple
res = p.map_async(func, args) #func is some arbitrary function
p.close()
p.join()

Cette approche a une énorme consommation de mémoire; manger à peu près tout mon RAM (à ce moment, il devient extrêmement lent, ce qui rend le multitraitement assez inutile). Je suppose que le problème est que df est un énorme objet ( un grand pandas dataframe) et il est copié pour chaque processus. J'ai essayé d'utiliser multiprocessing.Value pour partager la trame de données sans copier

shared_df = multiprocessing.Value(pandas.DataFrame, df)
args = [(shared_df, config1), (shared_df, config2), ...] 

(comme suggéré dans mémoire partagée multiprocessing Python ), mais cela me donne TypeError: this type has no size (identique à Partager un objet complexe entre Python? , auquel je ne comprends malheureusement pas la réponse).

J'utilise le multitraitement pour la première fois et peut-être que ma compréhension n'est pas (encore) assez bonne. Est multiprocessing.Value en fait même la bonne chose à utiliser dans ce cas? J'ai vu d'autres suggestions (par exemple, la file d'attente) mais je suis maintenant un peu confus. Quelles sont les options pour partager la mémoire et laquelle serait la meilleure dans ce cas?

41
Anne

Le premier argument de Value est typecode_or_type. Cela se définit comme:

typecode_or_type détermine le type de l'objet renvoyé: il s'agit soit d'un type ctypes, soit d'un type typed'un caractère du type utilisé par le module tableau. * args est transmis au constructeur pour le type.

Je souligne. Donc, vous ne pouvez tout simplement pas mettre un pandas dataframe dans un Value, il doit être n type ctypes .

Vous pouvez utiliser à la place un multiprocessing.Manager pour servir votre instance de trame de données singleton à tous vos processus. Il y a plusieurs façons de se retrouver au même endroit - probablement la plus simple est de simplement placer votre trame de données dans le Namespace du gestionnaire.

from multiprocessing import Manager

mgr = Manager()
ns = mgr.Namespace()
ns.df = my_dataframe

# now just give your processes access to ns, i.e. most simply
# p = Process(target=worker, args=(ns, work_unit))

Maintenant, votre instance de trame de données est accessible à tout processus qui obtient une référence au gestionnaire. Ou passez simplement une référence au Namespace, c'est plus propre.

Une chose que je n'ai pas/ne couvrirai pas est les événements et la signalisation - si vos processus doivent attendre que d'autres finissent de s'exécuter, vous devrez l'ajouter. Voici une page avec certains Event exemples qui couvrent également avec un peu plus de détails comment utiliser le Namespace du gestionnaire.

(notez que rien de tout cela ne permet de savoir si multiprocessing va entraîner des avantages tangibles en termes de performances, cela vous donne juste les outils pour explorer cette question)

35
roippi

Vous pouvez partager un pandas dataframe entre des processus sans surcharge de mémoire en créant un processus enfant data_handler. Ce processus reçoit des appels des autres enfants avec des demandes de données spécifiques (c.-à-d. Une ligne, une cellule spécifique, un tranche, etc.) à partir de votre très grand objet de trame de données. Seul le processus data_handler conserve votre trame de données en mémoire contrairement à un gestionnaire comme un espace de noms qui entraîne la copie de la trame de données dans tous les processus enfants. Voir ci-dessous pour un exemple de travail. Cela peut être converti en bassin.

Besoin d'une barre de progression pour cela? voir ma réponse ici: https://stackoverflow.com/a/55305714/11186769

import time
import Queue
import numpy as np
import pandas as pd
import multiprocessing
from random import randint

#==========================================================
# DATA HANDLER
#==========================================================

def data_handler( queue_c, queue_r, queue_d, n_processes ):

    # Create a big dataframe
    big_df = pd.DataFrame(np.random.randint(
        0,100,size=(100, 4)), columns=list('ABCD'))

    # Handle data requests
    finished = 0
    while finished < n_processes:

        try:
            # Get the index we sent in
            idx = queue_c.get(False)

        except Queue.Empty:
            continue
        else:
            if idx == 'finished':
                finished += 1
            else:
                try:
                    # Use the big_df here!
                    B_data = big_df.loc[ idx, 'B' ]

                    # Send back some data
                    queue_r.put(B_data)
                except:
                    pass    

# big_df may need to be deleted at the end. 
#import gc; del big_df; gc.collect()

#==========================================================
# PROCESS DATA
#==========================================================

def process_data( queue_c, queue_r, queue_d):

    data = []

    # Save computer memory with a generator
    generator = ( randint(0,x) for x in range(100) )

    for g in generator:

        """
        Lets make a request by sending
        in the index of the data we want. 
        Keep in mind you may receive another 
        child processes return call, which is
        fine if order isnt important.
        """

        #print(g)

        # Send an index value
        queue_c.put(g)

        # Handle the return call
        while True:
            try:
                return_call = queue_r.get(False)
            except Queue.Empty:
                continue
            else:
                data.append(return_call)
                break

    queue_c.put('finished')
    queue_d.put(data)   

#==========================================================
# START MULTIPROCESSING
#==========================================================

def multiprocess( n_processes ):

    combined  = []
    processes = []

    # Create queues
    queue_data = multiprocessing.Queue()
    queue_call = multiprocessing.Queue()
    queue_receive = multiprocessing.Queue()

    for process in range(n_processes): 

        if process == 0:

                # Load your data_handler once here
                p = multiprocessing.Process(target = data_handler,
                args=(queue_call, queue_receive, queue_data, n_processes))
                processes.append(p)
                p.start()

        p = multiprocessing.Process(target = process_data,
        args=(queue_call, queue_receive, queue_data))
        processes.append(p)
        p.start()

    for i in range(n_processes):
        data_list = queue_data.get()    
        combined += data_list

    for p in processes:
        p.join()    

    # Your B values
    print(combined)


if __name__ == "__main__":

    multiprocess( n_processes = 4 )
1
Mott The Tuple