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など?
どんな助けでも大歓迎です。ありがとう!
あなたの計算の目的が何であるかは完全にはわかりませんが、次のように作業を行うことができます
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未満のワーキングメモリを維持し、ほとんどのコアを使用します。
だからここに、タスクでの過酷な試合後の私の観察があります:
(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
のドロップイン置換であると想定されています。ドロップイン置換により、コード内のあらゆる場所でnumpy
をcupy
に置き換えることができます(例外があり、この問題とは無関係です)。ただし、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秒に短縮されます。
まあ、私はこのコードを書くより良い方法があると確信しています、そして私はそれを楽しみにしています!