web-dev-qa-db-fra.com

Comment résoudre les problèmes de mémoire lors du multitraitement à l'aide de Pool.map ()?

J'ai écrit le programme (ci-dessous) pour:

  • lire un énorme fichier texte en tant que pandas dataframe
  • puis groupby en utilisant une valeur de colonne spécifique pour diviser les données et les stocker sous forme de liste de trames de données.
  • dirigez ensuite les données vers multiprocess Pool.map() pour traiter chaque trame de données en parallèle.

Tout va bien, le programme fonctionne bien sur mon petit jeu de données de test. Mais, lorsque je canalise mes grandes données (environ 14 Go), la consommation de mémoire augmente de façon exponentielle, puis fige l'ordinateur ou se fait tuer (dans le cluster HPC).

J'ai ajouté des codes pour effacer la mémoire dès que les données/variables ne sont pas utiles. Je ferme également la piscine dès qu'elle est terminée. Toujours avec une entrée de 14 Go, je ne m'attendais qu'à une charge de mémoire de 2 * 14 Go, mais il semble que beaucoup de choses se passent. J'ai également essayé de modifier en utilisant chunkSize and maxTaskPerChild, etc Mais je ne vois aucune différence d'optimisation dans les deux tests par rapport au gros fichier.

Je pense que des améliorations de ce code sont/sont nécessaires à cette position de code, lorsque je démarre multiprocessing.

p = Pool(3) # number of pool to run at once; default at 1 result = p.map(matrix_to_vcf, list(gen_matrix_df_list.values())) mais, je publie tout le code.

Exemple de test: J'ai créé un fichier de test ("genome_matrix_final-chr1234-1mb.txt") jusqu'à 250 Mo et j'ai exécuté le programme. Lorsque je vérifie le moniteur système, je constate que la consommation de mémoire a augmenté d'environ 6 Go. Je ne sais pas trop pourquoi tant d'espace mémoire est pris par un fichier de 250 Mo plus quelques sorties. J'ai partagé ce fichier via une boîte de dépôt si cela aide à voir le vrai problème. https://www.dropbox.com/sh/coihujii38t5prd/AABDXv8ACGIYczeMtzKBo0eea?dl=

Quelqu'un peut-il suggérer: Comment puis-je me débarrasser du problème?

Mon python:

#!/home/bin/python3

import pandas as pd
import collections
from multiprocessing import Pool
import io
import time
import resource

print()
print('Checking required modules')
print()


''' change this input file name and/or path as need be '''
genome_matrix_file = "genome_matrix_final-chr1n2-2mb.txt"   # test file 01
genome_matrix_file = "genome_matrix_final-chr1234-1mb.txt"  # test file 02
#genome_matrix_file = "genome_matrix_final.txt"    # large file 

def main():
    with open("genome_matrix_header.txt") as header:
        header = header.read().rstrip('\n').split('\t')
        print()

    time01 = time.time()
    print('starting time: ', time01)

    '''load the genome matrix file onto pandas as dataframe.
    This makes is more easy for multiprocessing'''
    gen_matrix_df = pd.read_csv(genome_matrix_file, sep='\t', names=header)

    # now, group the dataframe by chromosome/contig - so it can be multiprocessed
    gen_matrix_df = gen_matrix_df.groupby('CHROM')

    # store the splitted dataframes as list of key, values(pandas dataframe) pairs
    # this list of dataframe will be used while multiprocessing
    gen_matrix_df_list = collections.OrderedDict()
    for chr_, data in gen_matrix_df:
        gen_matrix_df_list[chr_] = data

    # clear memory
    del gen_matrix_df

    '''Now, pipe each dataframe from the list using map.Pool() '''
    p = Pool(3)  # number of pool to run at once; default at 1
    result = p.map(matrix_to_vcf, list(gen_matrix_df_list.values()))

    del gen_matrix_df_list  # clear memory

    p.close()
    p.join()


    # concat the results from pool.map() and write it to a file
    result_merged = pd.concat(result)
    del result  # clear memory

    pd.DataFrame.to_csv(result_merged, "matrix_to_haplotype-chr1n2.txt", sep='\t', header=True, index=False)

    print()
    print('completed all process in "%s" sec. ' % (time.time() - time01))
    print('Global maximum memory usage: %.2f (mb)' % current_mem_usage())
    print()


'''function to convert the dataframe from genome matrix to desired output '''
def matrix_to_vcf(matrix_df):

    print()
    time02 = time.time()

    # index position of the samples in genome matrix file
    sample_idx = [{'10a': 33, '10b': 18}, {'13a': 3, '13b': 19},
                    {'14a': 20, '14b': 4}, {'16a': 5, '16b': 21},
                    {'17a': 6, '17b': 22}, {'23a': 7, '23b': 23},
                    {'24a': 8, '24b': 24}, {'25a': 25, '25b': 9},
                    {'26a': 10, '26b': 26}, {'34a': 11, '34b': 27},
                    {'35a': 12, '35b': 28}, {'37a': 13, '37b': 29},
                    {'38a': 14, '38b': 30}, {'3a': 31, '3b': 15},
                    {'8a': 32, '8b': 17}]

    # sample index stored as ordered dictionary
    sample_idx_ord_list = []
    for ids in sample_idx:
        ids = collections.OrderedDict(sorted(ids.items()))
        sample_idx_ord_list.append(ids)


    # for haplotype file
    header = ['contig', 'pos', 'ref', 'alt']

    # adding some suffixes "PI" to available sample names
    for item in sample_idx_ord_list:
        ks_update = ''
        for ks in item.keys():
            ks_update += ks
        header.append(ks_update+'_PI')
        header.append(ks_update+'_PG_al')


    #final variable store the haplotype data
    # write the header lines first
    haplotype_output = '\t'.join(header) + '\n'


    # to store the value of parsed the line and update the "PI", "PG" value for each sample
    updated_line = ''

    # read the piped in data back to text like file
    matrix_df = pd.DataFrame.to_csv(matrix_df, sep='\t', index=False)

    matrix_df = matrix_df.rstrip('\n').split('\n')
    for line in matrix_df:
        if line.startswith('CHROM'):
            continue

        line_split = line.split('\t')
        chr_ = line_split[0]
        ref = line_split[2]
        alt = list(set(line_split[3:]))

        # remove the alleles "N" missing and "ref" from the alt-alleles
        alt_up = list(filter(lambda x: x!='N' and x!=ref, alt))

        # if no alt alleles are found, just continue
        # - i.e : don't write that line in output file
        if len(alt_up) == 0:
            continue

        #print('\nMining data for chromosome/contig "%s" ' %(chr_ ))
        #so, we have data for CHR, POS, REF, ALT so far
        # now, we mine phased genotype for each sample pair (as "PG_al", and also add "PI" tag)
        sample_data_for_vcf = []
        for ids in sample_idx_ord_list:
            sample_data = []
            for key, val in ids.items():
                sample_value = line_split[val]
                sample_data.append(sample_value)

            # now, update the phased state for each sample
            # also replacing the missing allele i.e "N" and "-" with ref-allele
            sample_data = ('|'.join(sample_data)).replace('N', ref).replace('-', ref)
            sample_data_for_vcf.append(str(chr_))
            sample_data_for_vcf.append(sample_data)

        # add data for all the samples in that line, append it with former columns (chrom, pos ..) ..
        # and .. write it to final haplotype file
        sample_data_for_vcf = '\t'.join(sample_data_for_vcf)
        updated_line = '\t'.join(line_split[0:3]) + '\t' + ','.join(alt_up) + \
            '\t' + sample_data_for_vcf + '\n'
        haplotype_output += updated_line

    del matrix_df  # clear memory
    print('completed haplotype preparation for chromosome/contig "%s" '
          'in "%s" sec. ' %(chr_, time.time()-time02))
    print('\tWorker maximum memory usage: %.2f (mb)' %(current_mem_usage()))

    # return the data back to the pool
    return pd.read_csv(io.StringIO(haplotype_output), sep='\t')


''' to monitor memory '''
def current_mem_usage():
    return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.


if __name__ == '__main__':
    main()

Mise à jour pour les chasseurs de primes:

J'ai réalisé le multitraitement en utilisant Pool.map() mais le code cause une charge mémoire importante (fichier de test d'entrée ~ 300 Mo, mais la charge mémoire est d'environ 6 Go). Je m'attendais seulement à une charge de mémoire de 3 * 300 Mo au maximum.

  • Quelqu'un peut-il expliquer, Qu'est-ce qui cause une telle exigence de mémoire énorme pour un si petit fichier et pour un si petit calcul de longueur.
  • Aussi, j'essaie de prendre la réponse et de l'utiliser pour améliorer le multiprocessus dans mon grand programme. Donc, l'ajout de toute méthode, module qui ne change pas trop la structure de la partie de calcul (processus lié au CPU) devrait être bien.
  • J'ai inclus deux fichiers de test à des fins de test pour jouer avec le code.
  • Le code attaché est du code complet, il devrait donc fonctionner comme prévu comme il est copié-collé. Toutes les modifications doivent être utilisées uniquement pour améliorer l'optimisation dans les étapes de multitraitement.
19
everestial007

Prérequis

  1. Dans Python (dans ce qui suit, j'utilise une version 64 bits de Python 3.6.5) tout est un objet. Cela a sa surcharge et avec getsizeof nous pouvons voir exactement la taille d'un objet en octets:

    >>> import sys
    >>> sys.getsizeof(42)
    28
    >>> sys.getsizeof('T')
    50
    
  2. Lorsque l'appel système fork est utilisé (par défaut sur * nix, voir multiprocessing.get_start_method()) pour créer un processus enfant, la mémoire physique du parent n'est pas copiée et la technique copie sur écriture est utilisée.
  3. Le processus enfant Fork rapportera toujours le RSS complet (taille de l'ensemble résident) du processus parent. De ce fait, PSS (taille de jeu proportionnelle) est une mesure plus appropriée pour estimer l'utilisation de la mémoire de l'application de forking. Voici un exemple de la page:
  • Le processus A dispose de 50 Ko de mémoire non partagée
  • Le processus B dispose de 300 Ko de mémoire non partagée
  • Le processus A et le processus B ont tous les deux 100 Ko de la même région de mémoire partagée

Étant donné que le PSS est défini comme la somme de la mémoire non partagée d'un processus et de la proportion de mémoire partagée avec d'autres processus, le PSS pour ces deux processus est le suivant:

  • PSS du processus A = 50 KiB + (100 KiB/2) = 100 KiB
  • PSS du processus B = 300 KiB + (100 KiB/2) = 350 KiB

Le bloc de données

Ne regardons pas votre DataFrame seul. memory_profiler nous aidera.

justpd.py

#!/usr/bin/env python3

import pandas as pd
from memory_profiler import profile

@profile
def main():
    with open('genome_matrix_header.txt') as header:
        header = header.read().rstrip('\n').split('\t')

    gen_matrix_df = pd.read_csv(
        'genome_matrix_final-chr1234-1mb.txt', sep='\t', names=header)

    gen_matrix_df.info()
    gen_matrix_df.info(memory_usage='deep')

if __name__ == '__main__':
    main()

Utilisons maintenant le profileur:

mprof run justpd.py
mprof plot

Nous pouvons voir l'intrigue:

memory_profile

et trace ligne par ligne:

Line #    Mem usage    Increment   Line Contents
================================================
     6     54.3 MiB     54.3 MiB   @profile
     7                             def main():
     8     54.3 MiB      0.0 MiB       with open('genome_matrix_header.txt') as header:
     9     54.3 MiB      0.0 MiB           header = header.read().rstrip('\n').split('\t')
    10                             
    11   2072.0 MiB   2017.7 MiB       gen_matrix_df = pd.read_csv('genome_matrix_final-chr1234-1mb.txt', sep='\t', names=header)
    12                                 
    13   2072.0 MiB      0.0 MiB       gen_matrix_df.info()
    14   2072.0 MiB      0.0 MiB       gen_matrix_df.info(memory_usage='deep')

Nous pouvons voir que la trame de données prend ~ 2 GiB avec un pic à ~ 3 GiB pendant sa construction. Ce qui est plus intéressant, c'est la sortie de info .

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4000000 entries, 0 to 3999999
Data columns (total 34 columns):
...
dtypes: int64(2), object(32)
memory usage: 1.0+ GB

Mais info(memory_usage='deep') ("deep" signifie une introspection approfondie des données en interrogeant objectdtypes, voir ci-dessous) donne:

memory usage: 7.9 GB

Hein?! En regardant en dehors du processus, nous pouvons nous assurer que les chiffres de memory_profiler Sont corrects. sys.getsizeof Affiche également la même valeur pour le cadre (probablement à cause de __sizeof__ Personnalisé) et il en sera de même pour les autres outils qui l'utilisent pour estimer la gc.get_objects() allouée, par exemple pympler .

# added after read_csv
from pympler import tracker
tr = tracker.SummaryTracker()
tr.print_diff()   

Donne:

                                             types |   # objects |   total size
================================================== | =========== | ============
                 <class 'pandas.core.series.Series |          34 |      7.93 GB
                                      <class 'list |        7839 |    732.38 KB
                                       <class 'str |        7741 |    550.10 KB
                                       <class 'int |        1810 |     49.66 KB
                                      <class 'dict |          38 |      7.43 KB
  <class 'pandas.core.internals.SingleBlockManager |          34 |      3.98 KB
                             <class 'numpy.ndarray |          34 |      3.19 KB

Alors d'où viennent ces 7,93 GiB? Essayons d'expliquer cela. Nous avons 4M lignes et 34 colonnes, ce qui nous donne 134M valeurs. Ce sont soit int64 Ou object (qui est un pointeur 64 bits; voir en utilisant pandas avec de grandes données pour une explication détaillée). Ainsi, nous avons 134 * 10 ** 6 * 8 / 2 ** 20 ~ 1022 Mio uniquement pour les valeurs de la trame de données. Qu'en est-il des ~ 6,93 Gio restants?

Interning de chaîne

Pour comprendre le comportement, il est nécessaire de savoir que Python fait l'internement de chaînes. Il y a deux bons articles ( n , deux ) sur l'internement de chaînes in Python 2. Outre le changement Unicode dans Python 3 et PEP 39 in Python 3.3 les structures C ont changé, mais l'idée est la même. Fondamentalement, chaque chaîne courte qui ressemble à un identifiant sera mise en cache par Python dans un dictionnaire interne et les références pointeront vers le même Python objets. Dans un autre Word, nous pouvons dire qu'il se comporte comme un singleton. Les articles que j'ai mentionnés ci-dessus expliquent le profil de mémoire significatif et les améliorations de performances qu'il donne. Nous pouvons vérifier si une chaîne est internée en utilisant - interned champ de PyASCIIObject:

import ctypes

class PyASCIIObject(ctypes.Structure):
     _fields_ = [
         ('ob_refcnt', ctypes.c_size_t),
         ('ob_type', ctypes.py_object),
         ('length', ctypes.c_ssize_t),
         ('hash', ctypes.c_int64),
         ('state', ctypes.c_int32),
         ('wstr', ctypes.c_wchar_p)
    ]

Ensuite:

>>> a = 'name'
>>> b = '!@#$'
>>> a_struct = PyASCIIObject.from_address(id(a))
>>> a_struct.state & 0b11
1
>>> b_struct = PyASCIIObject.from_address(id(b))
>>> b_struct.state & 0b11
0

Avec deux chaînes, nous pouvons également faire une comparaison d'identité (abordée dans la comparaison de mémoire dans le cas de CPython).

>>> a = 'foo'
>>> b = 'foo'
>>> a is b
True
>> gen_matrix_df.REF[0] is gen_matrix_df.REF[6]
True

De ce fait, en ce qui concerne objectdtype, la trame de données alloue au plus 20 chaînes (une par acides aminés). Cependant, il convient de noter que Pandas recommande types catégoriels pour les énumérations.

Mémoire des pandas

Ainsi, nous pouvons expliquer l'estimation naïve de 7,93 GiB comme:

>>> rows = 4 * 10 ** 6
>>> int_cols = 2
>>> str_cols = 32
>>> int_size = 8
>>> str_size = 58  
>>> ptr_size = 8
>>> (int_cols * int_size + str_cols * (str_size + ptr_size)) * rows / 2 ** 30
7.927417755126953

Notez que str_size Est de 58 octets, pas 50 comme nous l'avons vu ci-dessus pour un littéral à 1 caractère. C'est parce que PEP 393 définit des chaînes compactes et non compactes. Vous pouvez le vérifier avec sys.getsizeof(gen_matrix_df.REF[0]).

La consommation réelle de mémoire doit être ~ 1 GiB comme indiqué par gen_matrix_df.info(), c'est deux fois plus. Nous pouvons supposer que cela a quelque chose à voir avec l'allocation (pré) de mémoire effectuée par Pandas ou NumPy. L'expérience suivante montre que ce n'est pas sans raison (plusieurs exécutions montrent l'image de sauvegarde):

Line #    Mem usage    Increment   Line Contents
================================================
     8     53.1 MiB     53.1 MiB   @profile
     9                             def main():
    10     53.1 MiB      0.0 MiB       with open("genome_matrix_header.txt") as header:
    11     53.1 MiB      0.0 MiB           header = header.read().rstrip('\n').split('\t')
    12                             
    13   2070.9 MiB   2017.8 MiB       gen_matrix_df = pd.read_csv('genome_matrix_final-chr1234-1mb.txt', sep='\t', names=header)
    14   2071.2 MiB      0.4 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[gen_matrix_df.keys()[0]])
    15   2071.2 MiB      0.0 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[gen_matrix_df.keys()[0]])
    16   2040.7 MiB    -30.5 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    ...
    23   1827.1 MiB    -30.5 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    24   1094.7 MiB   -732.4 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    25   1765.9 MiB    671.3 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    26   1094.7 MiB   -671.3 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    27   1704.8 MiB    610.2 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    28   1094.7 MiB   -610.2 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    29   1643.9 MiB    549.2 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    30   1094.7 MiB   -549.2 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    31   1582.8 MiB    488.1 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    32   1094.7 MiB   -488.1 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])    
    33   1521.9 MiB    427.2 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])    
    34   1094.7 MiB   -427.2 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    35   1460.8 MiB    366.1 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    36   1094.7 MiB   -366.1 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    37   1094.7 MiB      0.0 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
    ...
    47   1094.7 MiB      0.0 MiB       gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])

Je veux terminer cette section par une citation de nouvel article sur les problèmes de conception et les futurs Pandas2 par l'auteur original de Pandas.

règle de base des pandas: avoir 5 à 10 fois plus RAM que la taille de votre ensemble de données

Arbre de processus

Venons-en enfin au pool et voyons si nous pouvons utiliser la copie sur écriture. Nous utiliserons smemstat (disponible depuis un référentiel Ubuntu) pour estimer le partage de mémoire du groupe de processus et glances pour écrire à l'échelle du système mémoire libre. Les deux peuvent écrire en JSON.

Nous exécuterons le script original avec Pool(2). Nous aurons besoin de 3 fenêtres de terminal.

  1. smemstat -l -m -p "python3.6 script.py" -o smemstat.json 1
  2. glances -t 1 --export-json glances.json
  3. mprof run -M script.py

Alors mprof plot Produit:

3 processes

Le graphique de somme (mprof run --nopython --include-children ./script.py) Ressemble à:

enter image description here

Notez que deux graphiques ci-dessus montrent RSS. L'hypothèse est qu'en raison de la copie sur écriture, elle ne reflète pas l'utilisation réelle de la mémoire. Nous avons maintenant deux fichiers JSON de smemstat et glances. Je vais le script suivant pour convertir les fichiers JSON en CSV.

#!/usr/bin/env python3

import csv
import sys
import json

def smemstat():
  with open('smemstat.json') as f:
    smem = json.load(f)

  rows = []
  fieldnames = set()    
  for s in smem['smemstat']['periodic-samples']:
    row = {}
    for ps in s['smem-per-process']:
      if 'script.py' in ps['command']:
        for k in ('uss', 'pss', 'rss'):
          row['{}-{}'.format(ps['pid'], k)] = ps[k] // 2 ** 20

    # smemstat produces empty samples, backfill from previous
    if rows:            
      for k, v in rows[-1].items():
        row.setdefault(k, v)

    rows.append(row)
    fieldnames.update(row.keys())

  with open('smemstat.csv', 'w') as out:
    dw = csv.DictWriter(out, fieldnames=sorted(fieldnames))
    dw.writeheader()
    list(map(dw.writerow, rows))

def glances():
  rows = []
  fieldnames = ['available', 'used', 'cached', 'mem_careful', 'percent',
    'free', 'mem_critical', 'inactive', 'shared', 'history_size',
    'mem_warning', 'total', 'active', 'buffers']
  with open('glances.csv', 'w') as out:
    dw = csv.DictWriter(out, fieldnames=fieldnames)
    dw.writeheader()
    with open('glances.json') as f:
      for l in f:
        d = json.loads(l)
        dw.writerow(d['mem'])

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

Examinons d'abord la mémoire free.

enter image description here

La différence entre le premier et le minimum est de ~ 4,15 Gio. Et voici à quoi ressemblent les chiffres PSS:

enter image description here

Et la somme:

enter image description here

Ainsi, nous pouvons voir qu'en raison de la copie sur écriture, la consommation de mémoire réelle est d'environ 4,15 Gio. Mais nous sérialisons toujours les données pour les envoyer aux processus de travail via Pool.map. Pouvons-nous également tirer parti de la copie sur écriture?

Données partagées

Pour utiliser la copie sur écriture, nous devons avoir la list(gen_matrix_df_list.values()) accessible globalement afin que le travailleur après fork puisse toujours la lire.

  1. Modifions le code après del gen_matrix_df Dans main comme suit:

    ...
    global global_gen_matrix_df_values
    global_gen_matrix_df_values = list(gen_matrix_df_list.values())
    del gen_matrix_df_list
    
    p = Pool(2)
    result = p.map(matrix_to_vcf, range(len(global_gen_matrix_df_values)))
    ...
    
  2. Supprimez del gen_matrix_df_list Qui va plus tard.
  3. Et modifiez les premières lignes de matrix_to_vcf Comme:

    def matrix_to_vcf(i):
        matrix_df = global_gen_matrix_df_values[i]
    

Maintenant, réexécutons-le. Mémoire libre:

free

Arbre de processus:

process tree

Et sa somme:

sum

Ainsi, nous sommes au maximum de ~ 2,9 GiB d'utilisation réelle de la mémoire (le processus principal de pointe a lors de la construction du bloc de données) et la copie sur écriture a aidé!

En guise de remarque, il y a ce qu'on appelle la copie en lecture, le comportement du garbage collector du cycle de référence de Python, décrit dans Instagram Engineering (ce qui a conduit à gc.freeze Dans issue31558 ). Mais gc.disable() n'a pas d'impact dans ce cas particulier.

Mise à jour

Une alternative au partage de données sans copie avec copie sur écriture peut être de le déléguer au noyau depuis le début en utilisant numpy.memmap . Voici n exemple d'implémentation de Traitement de données hautes performances en Python parler. La partie délicate consiste alors à faire Pandas pour utiliser le tableau Numpy mmapé.

19
saaj

J'ai eu le même problème. J'avais besoin de traiter un énorme corpus de texte tout en conservant une base de connaissances de quelques DataFrames de millions de lignes chargées en mémoire. Je pense que ce problème est courant, je vais donc garder ma réponse orientée à des fins générales.

Une combinaison de paramètres a résolu le problème pour moi (1 & 3 & 5 seulement pourrait le faire pour vous):

  1. Utilisez Pool.imap (Ou imap_unordered) Au lieu de Pool.map. Cela parcourra les données paresseusement plutôt que de les charger toutes en mémoire avant de commencer le traitement.

  2. Définissez une valeur sur le paramètre chunksize. Cela rendra également imap plus rapide.

  3. Définissez une valeur sur le paramètre maxtasksperchild.

  4. Ajoutez la sortie au disque plutôt qu'en mémoire. Instantanément ou à tout moment lorsqu'il atteint une certaine taille.

  5. Exécutez le code en différents lots. Vous pouvez utiliser itertools.islice si vous avez un itérateur. L'idée est de diviser votre list(gen_matrix_df_list.values()) en trois listes ou plus, puis vous ne passez que le premier tiers à map ou imap, puis le deuxième tiers dans une autre exécution, etc. Puisque vous avez une liste, vous pouvez simplement la découper dans la même ligne de code.

4
Abdulrahman Bres

Lorsque vous utilisez multiprocessing.Pool, Un certain nombre de processus enfants seront créés à l'aide de l'appel système fork(). Chacun de ces processus démarre avec une copie exacte de la mémoire du processus parent à ce moment-là. Comme vous chargez le fichier csv avant de créer le Pool de taille 3, chacun de ces 3 processus dans le pool aura inutilement une copie du bloc de données. (gen_matrix_df Ainsi que gen_matrix_df_list Existeront dans le processus actuel ainsi que dans chacun des 3 processus enfants, donc 4 copies de chacune de ces structures seront en mémoire)

Essayez de créer le Pool avant de charger le fichier (au tout début en fait) Cela devrait réduire l'utilisation de la mémoire.

S'il est encore trop élevé, vous pouvez:

  1. Dump gen_matrix_df_list dans un fichier, 1 élément par ligne, par exemple:

    import os
    import cPickle
    
    with open('tempfile.txt', 'w') as f:
        for item in gen_matrix_df_list.items():
            cPickle.dump(item, f)
            f.write(os.linesep)
    
  2. Utilisez Pool.imap() sur un itérateur sur les lignes que vous avez sauvegardées dans ce fichier, par exemple:

    with open('tempfile.txt', 'r') as f:
        p.imap(matrix_to_vcf, (cPickle.loads(line) for line in f))
    

    (Notez que matrix_to_vcf Prend un tuple (key, value) Dans l'exemple ci-dessus, pas seulement une valeur)

J'espère que ça aide.

NB: je n'ai pas testé le code ci-dessus. Il ne s'agit que de démontrer l'idée.

4
tomas

RÉPONSE GÉNÉRALE SUR LA MÉMOIRE AVEC LE MULTI-TRAITEMENT

Vous avez demandé: "Qu'est-ce qui fait que tant de mémoire est allouée". La réponse repose sur deux parties.

D'abord, comme vous l'avez déjà remarqué, chaque multiprocessing travailleur obtient sa propre copie des données ( entre guillemets d'ici ), vous devez donc découper de gros arguments. Ou pour les fichiers volumineux, lisez-les petit à petit, si possible.

Par défaut, les travailleurs du pool sont de vrais processus Python bifurqués à l'aide du module multiprocessing de la bibliothèque standard Python lorsque n_jobs! = 1. Les arguments passés en entrée à l'appel parallèle sont sérialisés et réalloués dans la mémoire de chaque processus de travail.

Cela peut être problématique pour les gros arguments car ils seront réaffectés n_jobs fois par les travailleurs.

Second, si vous essayez de récupérer de la mémoire, vous devez comprendre que python fonctionne différemment des autres langages, et vous comptez sur del pour libérer la mémoire quand ce n'est pas le cas. Je ne sais pas si c'est mieux, mais dans mon propre code, j'ai surmonter cela en réaffectant la variable à un objet None ou vide.

POUR VOTRE EXEMPLE SPÉCIFIQUE - ÉDITION MINIMALE DE CODE

Tant que vous pouvez stocker vos grandes données en mémoire - deux fois, je pense que vous pouvez faire ce que vous essayez de faire en changeant simplement une seule ligne. J'ai écrit un code très similaire et cela a fonctionné pour moi lorsque j'ai réaffecté la variable (vice call del ou tout type de ramasse-miettes). Si cela ne fonctionne pas, vous devrez peut-être suivre les suggestions ci-dessus et utiliser les E/S disque:

    #### earlier code all the same
    # clear memory by reassignment (not del or gc)
    gen_matrix_df = {}

    '''Now, pipe each dataframe from the list using map.Pool() '''
    p = Pool(3)  # number of pool to run at once; default at 1
    result = p.map(matrix_to_vcf, list(gen_matrix_df_list.values()))

    #del gen_matrix_df_list  # I suspect you don't even need this, memory will free when the pool is closed

    p.close()
    p.join()
    #### later code all the same

POUR VOTRE EXEMPLE SPÉCIFIQUE - UTILISATION OPTIMALE DE LA MÉMOIRE

Tant que vous pouvez stocker vos grandes données en mémoire - une fois, et que vous avez une idée de la taille de votre fichier, vous pouvez utiliser Pandas read_csv lecture partielle de fichier , à lire seulement nrows à la fois si vous voulez vraiment micro-gérer la quantité de données lues, ou une [quantité fixe de mémoire à la fois en utilisant chunksize], qui renvoie un itérateur 5 . J'entends par là que le paramètre nrows est juste une lecture unique: vous pouvez l'utiliser pour obtenir un aperçu d'un fichier, ou si pour une raison quelconque vous vouliez que chaque partie ait exactement le même nombre de lignes (parce que, par exemple, si l'une de vos données est une chaîne de longueur variable, chaque ligne ne prendra pas la même quantité de mémoire). Mais je pense que dans le but de préparer un fichier pour le multitraitement, il sera beaucoup plus facile d'utiliser des morceaux, car cela concerne directement la mémoire, ce qui est votre préoccupation. Il sera plus facile d'utiliser les essais et les erreurs pour tenir dans la mémoire en fonction de blocs de taille spécifique que le nombre de lignes, ce qui changera la quantité d'utilisation de la mémoire en fonction de la quantité de données dans les lignes. La seule autre partie difficile est que, pour une raison spécifique à l'application, vous regroupez des lignes, ce qui rend les choses un peu plus compliquées. En utilisant votre code comme exemple:

   '''load the genome matrix file onto pandas as dataframe.
    This makes is more easy for multiprocessing'''

    # store the splitted dataframes as list of key, values(pandas dataframe) pairs
    # this list of dataframe will be used while multiprocessing
    #not sure why you need the ordered dict here, might add memory overhead
    #gen_matrix_df_list = collections.OrderedDict()  
    #a defaultdict won't throw an exception when we try to append to it the first time. if you don't want a default dict for some reason, you have to initialize each entry you care about.
    gen_matrix_df_list = collections.defaultdict(list)   
    chunksize = 10 ** 6

    for chunk in pd.read_csv(genome_matrix_file, sep='\t', names=header, chunksize=chunksize)
        # now, group the dataframe by chromosome/contig - so it can be multiprocessed
        gen_matrix_df = chunk.groupby('CHROM')
        for chr_, data in gen_matrix_df:
            gen_matrix_df_list[chr_].append(data)

    '''Having sorted chunks on read to a list of df, now create single data frames for each chr_'''
    #The dict contains a list of small df objects, so now concatenate them
    #by reassigning to the same dict, the memory footprint is not increasing 
    for chr_ in gen_matrix_df_list.keys():
        gen_matrix_df_list[chr_]=pd.concat(gen_matrix_df_list[chr_])

    '''Now, pipe each dataframe from the list using map.Pool() '''
    p = Pool(3)  # number of pool to run at once; default at 1
    result = p.map(matrix_to_vcf, list(gen_matrix_df_list.values()))
    p.close()
    p.join()
2
Jeff Ellen