web-dev-qa-db-ja.com

Pythonオブジェクトを破壊してメモリを解放する方法

私は100,000を超える画像を反復処理し、いくつかの画像機能をキャプチャして、結果のdataFrameをピクルスファイルとしてディスクに保存しようとしています。

残念ながらRAM=制約のため、画像を20,000のチャンクに分割し、結果をディスクに保存する前にそれらの操作を実行する必要があります。

以下のコードは、次の20,000画像を処理するループを開始する前に、20,000画像の結果のデータフレームを保存することになっています。

ただし、メモリがRAM=最初のforループの終わりに解放されないため、これは私の問題を解決していないようです

したがって、50,000番目のレコードの処理中に、メモリ不足エラーが原因でプログラムがクラッシュします。

オブジェクトをディスクに保存してガベージコレクターを起動した後、オブジェクトを削除しようとしましたが、RAMの使用率が低下していないようです。

何が欠けていますか?

#file_list_1 contains 100,000 images
file_list_chunks = list(divide_chunks(file_list_1,20000))
for count,f in enumerate(file_list_chunks):
    # make the Pool of workers
    pool = ThreadPool(64) 
    results = pool.map(get_image_features,f)
    # close the pool and wait for the work to finish 
    list_a, list_b = Zip(*results)
    df = pd.DataFrame({'filename':list_a,'image_features':list_b})
    df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")
    del list_a
    del list_b
    del df
    gc.collect()
    pool.close() 
    pool.join()
    print("pool closed")
14
Thalish Sajeed

さて、5万分の1が非常に大きく、それがOOMの原因となっている可能性があるため、これをテストするために最初に試してみます。

file_list_chunks = list(divide_chunks(file_list_1,20000))[30000:]

10,000で失敗した場合、これは20kがチャンクサイズが大きすぎるかどうかを確認します。または、50,000で再び失敗した場合は、コードに問題があります...


さて、コードに...

まず、明示的なlistコンストラクターは必要ありません。リスト全体をメモリに生成するよりも、pythonで反復する方がはるかに優れています。

file_list_chunks = list(divide_chunks(file_list_1,20000))
# becomes
file_list_chunks = divide_chunks(file_list_1,20000)

ここでThreadPoolを誤用している可能性があります。

これ以上のタスクがプールに送信されないようにします。すべてのタスクが完了すると、ワーカープロセスが終了します。

これはcloseのように読みますが、いくつかの考えがまだ実行されている可能性がありますが、これは安全だと思いますが、Pythonに少し違和感があるようです。ThreadPoolのコンテキストマネージャを使用する方が良いでしょう。

with ThreadPool(64) as pool: 
    results = pool.map(get_image_features,f)
    # etc.

python 実際にメモリを解放することは保証されていません の明示的なdels。

あなたは結合/後を収集する必要があります:

with ThreadPool(..):
    ...
    pool.join()
gc.collect()

また、これを小さい部分に分割してみることもできます。 10,000以下!


ハンマー1

pandas DataFramesと大きなリストを使用する代わりに、ここで行うことを検討します。SQLデータベースを使用することです。これは sqlite でローカルに実行できます。

import sqlite3
conn = sqlite3.connect(':memory:', check_same_thread=False)  # or, use a file e.g. 'image-features.db'

そしてコンテキストマネージャを使用します:

with conn:
    conn.execute('''CREATE TABLE images
                    (filename text, features text)''')

with conn:
    # Insert a row of data
    conn.execute("INSERT INTO images VALUES ('my-image.png','feature1,feature2')")

そうすれば、大きなリストオブジェクトやDataFrameを処理する必要がなくなります。

各スレッドに接続を渡すことができます...次のような少し奇妙なことをする必要があるかもしれません:

results = pool.map(get_image_features, Zip(itertools.repeat(conn), f))

次に、計算が完了したら、データベースからすべてを選択して、好きな形式に変換できます。例えば。 read_sql を使用します。


ハンマー2

別のpython "Shell out"の同じインスタンスで実行するのではなく、ここでサブプロセスを使用します。

Startとendをsys.argsとしてpythonに渡すことができるため、これらをスライスできます。

# main.py
# a for loop to iterate over this
subprocess.check_call(["python", "chunk.py", "0", "20000"])

# chunk.py a b
for count,f in enumerate(file_list_chunks):
    if count < int(sys.argv[1]) or count > int(sys.argv[2]):
         pass
    # do stuff

このようにして、サブプロセスは適切にクリーンアップしますpython(プロセスが終了するため、メモリリークが発生することはありません)。


私の賭けは、ハンマー1が進むべき道であり、大量のデータを結合し、pythonリストに不必要に読み込み、sqlite3(または他のデータベース)を使用しているように感じる)それを完全に回避します。

6
Andy Hayden

注:これは答えではなく、質問と提案のクイックリストです

  • ThreadPool() _from multiprocessing.pool_を使用していますか? (_python3_に)十分に文書化されていないため、 ThreadPoolExecutor を使用したい( here も参照)
  • 各ループの最後でメモリに保持されているオブジェクトをデバッグしてみてください。 this solution を使用すると、sys.getsizeof()に依存して、宣言されたすべてのglobals()のリストと、それらのメモリフットプリントが返されます。
  • また、_del results_を呼び出します(これはそれほど大きくないはずですが)
1
Asmus

あなたの問題は、マルチプロセッシングを使用する必要があるスレッドを使用していることです(CPUバインドvs IOバインド)。

私はあなたのコードを次のように少しリファクタリングします:

from multiprocessing import Pool

if __name__ == '__main__':
    cpus = multiprocessing.cpu_count()        
    with Pool(cpus-1) as p:
        p.map(get_image_features, file_list_1)

そして、私は関数を変更しますget_image_features(これらのような2つの行を末尾に追加するなど)。これらの画像をどのように処理しているのか正確にはわかりませんが、各プロセス内ですべての画像を処理し、すぐにディスクに保存するという考え方です。

df = pd.DataFrame({'filename':list_a,'image_features':list_b})
df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")

したがって、データフレームは、終了した後ではなく、各プロセス内でピクル化されて保存されます。プロセスは終了するとすぐにメモリから消去されるため、これはメモリのフットプリントを低く保つために機能するはずです。

1
delica

一部のLinuxビルドではpd.DataFrame(...)がリークする可能性があるため(github issue and "workaround" を参照)、del dfでも役に立たない場合があります。

あなたの場合、githubからのソリューションはpd.DataFrame.__del__のモンキーパッチなしで使用できます:

from ctypes import cdll, CDLL
try:
    cdll.LoadLibrary("libc.so.6")
    libc = CDLL("libc.so.6")
    libc.malloc_trim(0)
except (OSError, AttributeError):
    libc = None


if no libc:
    print("Sorry, but pandas.DataFrame may leak over time even if it's instances are deleted...")


CHUNK_SIZE = 20000


#file_list_1 contains 100,000 images
with ThreadPool(64) as pool:
    for count,f in enumerate(divide_chunks(file_list_1, CHUNK_SIZE)):
        # make the Pool of workers
        results = pool.map(get_image_features,f)
        # close the pool and wait for the work to finish 
        list_a, list_b = Zip(*results)
        df = pd.DataFrame({'filename':list_a,'image_features':list_b})
        df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")

        del df

        # 2 new lines of code:
        if libc:  # Fix leaking of pd.DataFrame(...)
            libc.malloc_trim(0)

print("pool closed")

追伸このソリューションは、単一のデータフレームが大きすぎる場合には役立ちません。これはCHUNK_SIZEを減らすことによってのみ助けることができます

0
imposeren

つまり、Pythonインタプリタでメモリを解放することはできません。各プロセスが独自にメモリを処理できるため、マルチプロセッシングを使用するのが最善の策です。

ガベージコレクターはメモリを「解放」しますが、期待するコンテキストではありません。ページとプールの処理は、CPythonソースで調べることができます。高レベルの記事もここにあります: https://realpython.com/python-memory-management/

0
user397836

List()を呼び出さないでください。これは、divide_chunks()から返されるもののメモリ内リストを作成します。あなたの記憶の問題がおそらく起こっているところです。

Memeoryにあるすべてのデータを一度に必要とするわけではありません。一度に1つずつファイル名を反復処理するだけで、すべてのデータが一度にメモリに存在するわけではありません。

スタックトレースを投稿して、詳細情報を入手してください

0
Reddy Kilowatt

celery でそれが可能になると思います。celeryのおかげで、Pythonで並行処理と並列処理を簡単に使用できます。

画像の処理はべき等でアトミックなので、 セロリタスク になる可能性があります。

タスクを処理する 数人のワーカー を実行できます-イメージを操作します。

さらに、メモリリークのための 構成 があります。

0
MartinP

この種の問題に対する私の解決策は、並列処理ツールを使用することです。ローカルで作成された関数(「実装の詳細」であり、モジュール内でグローバルにしないことをお勧めします)でも並列化できるので、私は joblib を好みます。私の他のアドバイス:pythonではスレッド(およびスレッドプール)を使用せず、代わりにプロセス(およびプロセスプール)を使用してください。 joblibに少なくとも2つのプロセスのプールを作成してください。そうしないと、元のpythonプロセスですべてが実行されるため、最終的にRAMは解放されません。 joblibワーカープロセスが自動的に閉じられると、それらが割り当てたRAMはOSによって完全に解放されます。私のお気に入りの武器は joblib.Parallel です。ワーカーに大きなデータ(2GBより大きい)を転送する必要がある場合は、 joblib.dump (pythonオブジェクトをメインプロセスのファイルに書き込む)を使用し、- joblib.load (ワーカープロセスで読み取るため)。

_del object_について:Pythonでは、コマンドは実際にはオブジェクトを削除しません。参照カウンターを減らすだけです。 import gc; gc.collect()を実行すると、ガベージコレクターは解放するメモリと割り当てたままにするメモリを自分で決定します。可能な限りすべてのメモリを強制的に解放する方法は知りません。さらに悪いことに、一部のメモリが実際にpythonではなく、たとえば、一部の外部C/C++/Cython/etcコードで割り当てられ、コードがpythonを関連付けなかった場合メモリでカウンターを参照すると、上で書いたものを除いて、Pythonからそれを解放するためにできることはまったくありません。つまり、RAMを割り当てたpythonプロセスを終了することで、その場合はOSによって解放されることが保証されています。そのためPythonで一部のメモリを解放する唯一の100%信頼できる方法は、並列プロセスでそれを割り当てるコードを実行して、プロセスを終了することです

0
S.V