web-dev-qa-db-ja.com

Pool.map()を使用してマルチプロセッシング中にメモリの問題を解決するにはどうすればよいですか?

以下にプログラムを作成しました:

  • 巨大なテキストファイルを_pandas dataframe_として読み取ります
  • 次に、特定の列値を使用してデータを分割し、データフレームのリストとして保存するgroupby
  • 次に、データをmultiprocess Pool.map()にパイプして、各データフレームを並列処理します。

すべてが正常で、プログラムは私の小さなテストデータセットでうまく機能します。しかし、大きなデータ(約14 GB)をパイプすると、メモリ消費が指数関数的に増加し、コンピューターがフリーズするか、(HPCクラスターで)停止します。

データ/変数が役に立たなくなったらすぐにメモリをクリアするコードを追加しました。また、プールが完成したらすぐに閉鎖します。それでも14 GBの入力では、2 * 14 GBのメモリ負荷しか期待していませんでしたが、多くのことが進行しているようです。また、_chunkSize and maxTaskPerChild, etc_を使用して微調整を試みましたが、テストファイルと大きなファイルの両方で最適化に違いは見られません。

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()))が、コード全体を投稿しています。

テスト例:最大250 mbのテストファイル( "genome_matrix_final-chr1234-1mb.txt")を作成し、プログラムを実行しました。システムモニターを確認すると、メモリ消費が約6 GB増加していることがわかります。 250 MBのファイルといくつかの出力によって、なぜそんなに多くのメモリ空間が使用されるのか、私はそれほど明確ではありません。実際の問題を確認するのに役立つ場合は、ドロップボックスを介してそのファイルを共有しました。 https://www.dropbox.com/sh/coihujii38t5prd/AABDXv8ACGIYczeMtzKBo0eea?dl=

誰かが、どのように問題を取り除くことができますか?

My python script:

_#!/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()
_

バウンティハンターの更新:

Pool.map()を使用してマルチプロセッシングを達成しましたが、コードにより大きなメモリ負荷が発生しています(入力テストファイル〜300 mb、メモリ負荷は約6 GB)。最大で3 * 300 mbのメモリ負荷しか期待していませんでした。

  • 誰かが説明できますか、このような小さなファイルとそのような小さな長さの計算のためにそのような巨大なメモリ要件を引き起こしているのは何ですか?.
  • また、私は答えを取り、それを使用して私の大きなプログラムのマルチプロセスを改善しようとしています。したがって、メソッドの追加、計算部分(CPUバウンドプロセス)の構造をあまり変更しないモジュールは問題ありません。
  • コードで遊ぶために、テスト目的で2つのテストファイルを含めました。
  • 添付コードは完全なコードであるため、コピーアンドペーストした場合に意図したとおりに動作するはずです。変更は、マルチプロセッシングステップの最適化を改善するためにのみ使用してください。
19
everestial007

前提条件

  1. Python(以下ではPython 3.6.5の64ビットビルドを使用)では、すべてがオブジェクトです。これにはオーバーヘッドがあり、 getsizeof を使用すると、オブジェクトのサイズをバイト単位で正確に確認できます。

    >>> import sys
    >>> sys.getsizeof(42)
    28
    >>> sys.getsizeof('T')
    50
    
  2. Forkシステムコールを使用して(* nixのデフォルト、multiprocessing.get_start_method()を参照)子プロセスを作成すると、親の物理メモリはコピーされず、 copy-on-write テクニックが使用されます。
  3. Fork子プロセスは、親プロセスの完全なRSS(常駐セットサイズ)を引き続き報告します。このため、 [〜#〜] pss [〜#〜] (比例セットサイズ)は、フォークアプリケーションのメモリ使用量を推定するためのより適切なメトリックです。このページの例を次に示します。
  • プロセスAには50 KiBの非共有メモリがあります
  • プロセスBには300 KiBの非共有メモリがあります
  • プロセスAとプロセスBの両方に、同じ共有メモリ領域の100 KiBがあります

PSSはプロセスの非共有メモリと他のプロセスと共有されるメモリの割合の合計として定義されるため、これら2つのプロセスのPSSは次のようになります。

  • プロセスAのPSS = 50 KiB +(100 KiB/2)= 100 KiB
  • プロセスBのPSS = 300 KiB +(100 KiB/2)= 350 KiB

データフレーム

DataFrameだけを見てはいけません。 memory_profiler が役立ちます。

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()

プロファイラを使用してみましょう。

mprof run justpd.py
mprof plot

プロットを見ることができます:

memory_profile

および行ごとのトレース:

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')

データフレームは、構築中に〜3 GiBにピークがあり、最大2 GiBになることがわかります。さらに興味深いのは、 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

ただし、info(memory_usage='deep')(「深い」とは、objectdtypesを調べることによりデータを深くイントロスペクションすることです。以下を参照してください):

memory usage: 7.9 GB

え?プロセスの外側を見ると、memory_profilerの数字が正しいことを確認できます。 sys.getsizeofもフレームに同じ値を表示します(おそらくカスタム__sizeof__が原因です)。また、それを使用して割り当てられたgc.get_objects()を推定する他のツールも表示されます。 pympler

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

与える:

                                             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

では、これらの7.93 GiBはどこから来たのでしょうか?これを説明してみましょう。 4M行と34列があり、134Mの値が得られます。それらはint64またはobject(64ビットポインターです。詳細な説明については sing pandas with large data のいずれかです。したがって、データフレームの値に対してのみ134 * 10 ** 6 * 8 / 2 ** 20〜1022 MiBがあります。残りの〜6.93 GiBはどうですか?

ストリングインターン

動作を理解するには、Pythonが文字列インターンを行うことを知る必要があります。 Python 2.の文字列インターンについて、2つの良い記事( onetwo )があります。Python 3および PEP 39 in Python 3.3 C構造が変更されましたが、考え方は同じです。基本的に、識別子のように見える短い文字列はすべて内部辞書のPythonによってキャッシュされ、参照は同じPythonオブジェクトを指します。つまり、シングルトンのように振る舞うと言えます。上記の記事では、メモリプロファイルの重要性とパフォーマンスの向上について説明しています。 internedPyASCIIObject フィールドを使用して、文字列がインターンされているかどうかを確認できます。

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)
    ]

次に:

>>> 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

2つの文字列を使用して、ID比較を行うこともできます(CPythonの場合のメモリ比較で対処します)。

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

そのため、objectdtypeに関して、データフレームは最大20個の文字列(アミノ酸ごとに1つ)を割り当てます。ただし、Pandasは列挙に categorical types を推奨していることに注意してください。

パンダの記憶

したがって、次のように7.93 GiBの単純な推定値を説明できます。

>>> 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

str_sizeは58バイトであり、1文字のリテラルで上で見たように50バイトではないことに注意してください。 PEP 393がコンパクト文字列と非コンパクト文字列を定義しているためです。 sys.getsizeof(gen_matrix_df.REF[0])で確認できます。

gen_matrix_df.info()によって報告されるように、実際のメモリ消費量は〜1 GiBである必要があります。これは2倍です。 PandasまたはNumPyによって行われたメモリ(事前)割り当てと関係があると想定できます。次の実験は、理由がないわけではないことを示しています(複数の実行で保存画像が表示されます)。

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())])

このセクションは、Pandasの原作者による 設計の問題と将来のPandas2についての最新記事 からの引用で終了します。

パンダの経験則:データセットのサイズの5〜10倍のRAM

プロセスツリー

最後にプールに行き、コピーオンライトを利用できるかどうかを確認しましょう。 smemstat (Ubuntuリポジトリから入手可能)を使用してプロセスグループのメモリ共有を推定し、 glances を使用してシステム全体の空きメモリを書き留めます。 。どちらもJSONを記述できます。

Pool(2)を使用して元のスクリプトを実行します。 3つのターミナルウィンドウが必要です。

  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

次に、mprof plotは以下を生成します。

3 processes

合計チャート(mprof run --nopython --include-children ./script.py)は次のようになります。

enter image description here

上記の2つのグラフはRSSを示していることに注意してください。仮説は、コピーオンライトのため、実際のメモリ使用量を反映していないということです。これで、smemstatglancesからの2つのJSONファイルができました。 JSONファイルを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]]()

まず、freeメモリを見てみましょう。

enter image description here

最初と最小の違いは〜4.15 GiBです。そして、PSSの数値は次のようになります。

enter image description here

そして合計:

enter image description here

したがって、コピーオンライトのため、実際のメモリ消費量は約4.15 GiBであることがわかります。ただし、Pool.map経由でワーカープロセスに送信するために、データをシリアル化しています。ここでもコピーオンライトを活用できますか?

共有データ

コピーオンライトを使用するには、list(gen_matrix_df_list.values())をグローバルにアクセスできるようにする必要があります。これにより、fork後のワーカーはそれを読み取ることができます。

  1. maindel gen_matrix_dfの後のコードを次のように変更しましょう。

    ...
    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. 後で行くdel gen_matrix_df_listを削除します。
  3. そして、matrix_to_vcfの最初の行を次のように変更します。

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

次に、再実行してみましょう。空きメモリ:

free

プロセスツリー:

process tree

そしてその合計:

sum

したがって、実際のメモリ使用量は最大で約2.9 GiBであり(データフレームの構築中にメインプロセスがピークに達します)、コピーオンライトが役立ちました!

補足説明として、Pythonのリファレンスサイクルガベージコレクターの動作であるコピーオンリードと呼ばれる Instagram Engineeringで説明されていますissue31558gc.freezeに至りました)。しかし、gc.disable()はこの特定の場合には影響を与えません。

更新

コピーオンライトコピーレスデータ共有の代わりに、 numpy.memmap を使用して最初からカーネルに委任することができます。Pythonの高性能データ処理talkの 実装例 があります。 トリッキーな部分 は、Pandasを作成して、mmaped Numpy配列を使用します。

19
saaj

同じ問題がありました。数百万行のDataFramesのナレッジベースをメモリにロードしながら、巨大なテキストコーパスを処理する必要がありました。この問題は一般的なものだと思うので、一般的な目的のために回答の方向性を維持します。

設定のcombinationが問題を解決しました(1&3&5のみがあなたのためにそれを行うかもしれません):

  1. _Pool.imap_の代わりに_imap_unordered_(または_Pool.map_)を使用します。これは、処理を開始する前にすべてのデータをメモリにロードするよりも遅延してデータを繰り返し処理します。

  2. chunksizeパラメーターに値を設定します。これにより、imapも高速になります。

  3. maxtasksperchildパラメーターに値を設定します。

  4. メモリよりもディスクに出力を追加します。瞬時に、または一定のサイズに達するたびに。

  5. 異なるバッチでコードを実行します。イテレータがある場合は、 itertools.islice を使用できます。アイデアはlist(gen_matrix_df_list.values())を3つ以上のリストに分割し、最初の3分の1をmapまたはimapにのみ、次に2番目の3番目を別の実行などに渡すことです。 。リストがあるので、同じコード行に単純にスライスできます。

4

_multiprocessing.Pool_を使用すると、fork()システムコールを使用して多数の子プロセスが作成されます。これらの各プロセスは、その時点での親プロセスのメモリの正確なコピーから始まります。サイズ3のPoolを作成する前にcsvをロードしているため、プール内のこれら3つのプロセスのそれぞれには、データフレームのコピーが不必要にあります。 (_gen_matrix_df_と_gen_matrix_df_list_は、現在のプロセスと3つの子プロセスのそれぞれに存在するため、これらの各構造の4つのコピーがメモリ内にあります)

ファイルをロードする前に(実際には最初に)Poolを作成してみてください。これにより、メモリ使用量が削減されます。

それでも高すぎる場合は、次のことができます。

  1. Gen_matrix_df_listをファイルにダンプします(1行に1アイテム、例:

    _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. このファイルにダンプした行に対して、イテレーターでPool.imap()を使用します。例:

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

    (上記の例では、値だけでなく_matrix_to_vcf_が_(key, value)_タプルを取ることに注意してください)

それがお役に立てば幸いです。

NB:上記のコードはテストしていません。これは、アイデアを示すことのみを目的としています。

4
tomas

マルチプロセッシングによるメモリに関する一般的な回答

あなたは尋ねました:「これほど多くのメモリが割り当てられる原因は何ですか」。答えは2つの部分に依存しています。

最初、すでにお気づきのように、multiprocessingワーカーは独自のデータのコピーを取得します(引用符付き- ここから )、したがって、大きな引数をチャンクする必要があります。または、大きなファイルの場合は、可能であれば一度に少しずつ読んでください。

デフォルトでは、プールのワーカーはn_jobs!= 1のときにPython標準ライブラリのマルチプロセッシングモジュールを使用して分岐された実際のPythonプロセスです。Parallel呼び出しへの入力として渡される引数は各ワーカープロセスのメモリ。

これは、ワーカーによってn_jobs回再割り当てされるため、大きな引数では問題になる可能性があります。

Second、メモリを再生しようとする場合、pythonは他の言語とは異なる動作をすること、およびに依存していることを理解する必要があります- delが解放されないときにメモリを解放する。最善かどうかはわかりませんが、自分のコードでは、変数をNoneまたは空のオブジェクトに再割り当てすることでこれを克服しました。

特定の例-最小コード編集

大きなデータをメモリに収まる限りtwiceとすれば、1行変更するだけで目的の処理を実行できると思います。私は非常に似たコードを書いており、変数を再割り当てしたときに機能しました(副呼び出しdelまたはあらゆる種類のガベージコレクト)。これが機能しない場合は、上記の提案に従ってディスクI/Oを使用する必要がある場合があります。

    #### 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

特定の例-最適なメモリ使用量

メモリに大きなデータを収めることができる限りonceで、ファイルの大きさについてある程度知っている場合は、Pandas read_csv 部分的なファイルの読み取り、読み取る 一度にnrowsのみ 読み取っているデータの量を実際にマイクロ管理する場合、または[チャンクサイズ]を使用して、一度に一定量のメモリを割り当て、イテレータを返します 5 。つまり、nrowsパラメーターは1回の読み取りにすぎません。これを使用して、ファイルを覗くだけでよいし、何らかの理由で各パーツにまったく同じ行数を持たせたい場合もあります(たとえば、データのいずれかが可変長の文字列である場合、各行は同じ量のメモリを消費しません。しかし、マルチプロセッシング用にファイルを準備する目的では、チャンクを使用する方がはるかに簡単になると思います。チャンクはメモリに直接関係しているため、懸念事項です。行数よりも特定のサイズのチャンクに基づいてメモリに収まるように試行錯誤を使用する方が簡単です。行のデータ量に応じてメモリ使用量が変化します。他の唯一の難しい部分は、アプリケーション固有の理由により、いくつかの行をグループ化しているため、少し複雑になることです。コードを例として使用する:

   '''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