web-dev-qa-db-ja.com

Python multiprocessing throws Killed:9

multiprocessingを使用して、2000個の形状の配列(76、76)を3D配列に並べてスケーリング係数を適用する関数を高速化しようとしています。

タイルの数が約200未満の場合は問題なく機能しますが、それよりも多い場合は_Killed: 9_が返され、1000タイルのオーダーで処理できるようにする必要があります。

コードの簡略版は次のとおりです。

_from functools import partial
from multiprocessing.pool import ThreadPool
from multiprocessing import cpu_count
import numpy as np

def func_A(data, scale, N):
    """Tile the data N times and scale it"""
    arr = np.tile(data, (N, 1, 1))
    arr *= scale
    return arr

def func_B(N=4):
    """Create scaled arrays"""
    # Make data
    data = np.random.normal(size=(2000, 76, 76))

    # Make scales
    scales = np.arange(2000)

    # Multiprocess into tiled arrays
    pool = ThreadPool(cpu_count())
    func = partial(func_A, N=N)
    inpt = list(Zip(data, scales))
    results = np.asarray(pool.starmap(func, inpt), dtype=np.float64)
    pool.close()
    pool.join()

    return results.swapaxes(0, 1)
_

したがって、func_B(4)の場合は問題ありませんが、func_B(500)の場合は無効になります。

私はそのような大きな配列でPythonのメモリに負担をかけていることを理解していますが、_func_B_を大きなN...で動作させるための最善の方法は何ですか? multiprocessingを間違って使用していますか?他のものを一緒に使用する必要がありますか? Dask、Numba、Cythonなど?

どんな助けでも大歓迎です。ありがとう!

3
Joe Flip

あなたの計算の目的が何であるかは完全にはわかりませんが、次のように作業を行うことができます

import dask.array as da
import numpy as np

# Make data
data = da.random.normal(size=(2000, 76, 76), chunks=(2000, 76, 76))

# Make scales
scales = np.arange(2000)
N = 500
out = da.repeat(data, N, axis=0).reshape((N, 2000, 76, 76)) * scales.reshape((1, 2000, 1, 1))
out = out.sum(axis=0).compute()

5GB未満のワーキングメモリを維持し、ほとんどのコアを使用します。

2
mdurant

だからここに、タスクでの過酷な試合後の私の観察があります:

  • 出力として取得することになっている最後の配列は4次元で、_(2000, 2000, 76, 76)_という形をしており、_float64_型の値で構成されています。大雑把な計算では、この配列のサイズは2000 * 2000 * 76 * 76 * 8バイト=〜170 GBs...であることを示しているので、すべてを確実に保持することはできません。すぐに記憶。
  • multiprocessingの使用法は複雑で(マルチプロセッシングを徹底的に研究していない人にとっては常にそうでした)、計算時間はそれほど良くありません。たとえば、Google Colab、(Tesla T4 GPUバックエンド、12GB RAM)の場合、_N = 50_の実行には約4.5秒(最小)かかります。モジュールでのより良い実装が可能かもしれませんが、私はそれのためのものではありません。

私の行動方針:

2番目の問題に取り組むために、私はcupyを使用します。これは、Pythonのnumpyドロップイン置換であると想定されています。ドロップイン置換により、コード内のあらゆる場所でnumpycupyに置き換えることができます(例外があり、この問題とは無関係です)。ただし、cupyはNvidia GPUでCUDAを使用するため、cupyインストールを実行する前にCUDAをインストールする必要があります。 ( このガイドを確認してください。 )または、可能であれば、Google Colabのようにオンラインコンピューティングリソースを使用することもできます。
また、作品をパーツに分割します。関数fnh(a, scale, N)を使用して、任意のNのスケーリングされたタイル配列を計算します。
目的の出力配列を複数の部分にスライスし、これらのスライスに対してfnh(...)を繰り返し実行します。スライシングはより良い最適化のために調整できますが、私は粗雑な推測に基づいたものを使用しました。

これがコードです:

_import cupy as cp


def fnh(a, scale, N):
    arr = cp.einsum('i,ijk->ijk', scale, a)
    result = cp.tile(arr, (N, 1, 1, 1))

    del arr
    return result


def slicer(arr, scales, N = 400):
    mempool = cp.get_default_memory_pool()
    pinned_mempool = cp.get_default_pinned_memory_pool()
    # result = np.empty((N, 2000, 76, 76))    # to large to be allocated

    section = 500                             # Choices subject
    parts = 80                                # to optimization
    step = N // parts

    for i in range(parts):                    # Slice N into equal parts
        begin = i*step
        end = begin + step

        stacked = cp.empty((step, 2000, 76, 76))

        for j in range(2000 // section):      # Section the 2000 arrays into equal parts
            begin = j*section
            end = begin + section

            s = scales[begin:end]
            a = arr[begin:end]
            res = fnh(a, s, step)
            stacked[:, begin:end] = res       # Accumulate values

            del a, res

        # result[begin:end] = stacked         # This is where we were supposed to 
                                              # accumulate values in result
        del stacked
        mempool.free_all_blocks()
        pinned_mempool.free_all_blocks()
_

まず、_cupy.einsum_を使用して、配列のスケーリングvectoriallyを計算します。

次に、スペースを回復するために可能な限りアレイを削除します。具体的には、mempool.free_all_blocks()およびpinned_mempool.free_all_blocks()を使用して、GPUメモリプール内のcupyによって割り当てられたスペースの割り当てを解除し、使用可能なGPUメモリを回復することが不可欠です。それについて読んでください ここ 。ただし、cupyは割り当てられたメモリをキャッシュするため、速度を上げるためにこのキャッシュを制限された方法で使用すると役立つ場合があります。 (これは直感であり、私は特にそれについて知らされていません。)したがって、セクション化されたタイルに同じメモリを使用し、Nスライスの完了後にそれをクリアします。

3番目に、_# result[begin:end] = stacked_がある場所では、配列をオフロードする必要があります。前述したように、アレイ全体をメモリに格納する余裕はありません。 yourアプリケーションに適合する場所と方法を特定のビンにオフロードすることは、おそらくメモリの問題を回避するための良い方法です。

第4に、このコードは不完全です。これは、前述のように、形成されたアレイが適切な処理を必要とするためです。しかし、それは主要な重労働を行います。

最後に、timeitを使用してこのコードの時間を計測するには、Google Colabで:
比較のために、_N = 50_の実行には最大50ミリ秒(最小)、_N = 2000_の実行には最大7.4秒(最小)かかります。

更新:_parts = 40_および_section = 250_に変更すると、最小時間が〜6.1秒に短縮されます。

まあ、私はこのコードを書くより良い方法があると確信しています、そして私はそれを楽しみにしています!

1
amzon-ex