それで、私は_multiprocessing.Pool
_とNumpy
で遊んでいますが、いくつかの重要な点を見逃したようです。 pool
バージョンがはるかに遅いのはなぜですか? htop
を調べたところ、いくつかのプロセスが作成されていることがわかりましたが、それらはすべてCPUの1つを共有しており、合計で最大100%になります。
_$ cat test_multi.py
import numpy as np
from timeit import timeit
from multiprocessing import Pool
def mmul(matrix):
for i in range(100):
matrix = matrix * matrix
return matrix
if __name__ == '__main__':
matrices = []
for i in range(4):
matrices.append(np.random.random_integers(100, size=(1000, 1000)))
pool = Pool(8)
print timeit(lambda: map(mmul, matrices), number=20)
print timeit(lambda: pool.map(mmul, matrices), number=20)
$ python test_multi.py
16.0265390873
19.097837925
_
[更新]
timeit
に変更されましたまだ変化はありません。 pool
バージョンはまだ遅く、htop
で、1つのコアのみが使用され、複数のプロセスが生成されていることがわかります。
[update2]
現時点では、multiprocessing.Process()
とQueue
を使用するという@ Jan-PhilipGehrckeの提案について読んでいます。しかし、その間に私は知りたいです:
Numpy
を使用しているので、私のコードは悪い例ですか?他の人が私の最終目標を知っているとき、私はしばしばより良い答えが得られることを学びました:私はたくさんのファイルを持っています、それらはatmでロードされてシリアルに処理されます。処理はCPUを集中的に使用するため、並列化によって多くのことが得られると思います。私の目的は、ファイルを並行して分析するpython関数を呼び出すことです。さらに、この関数は、違いを生むCコードへの単なるインターフェイスであると思います。
1 Ubuntu 12.04、Python 2.7.3、i7 860 @ 2.80-詳細が必要な場合は、コメントを残してください。
[update3]
Stefanoのサンプルコードの結果は次のとおりです。どういうわけかスピードアップはありません。 :/
_testing with 16 matrices
base 4.27
1 5.07
2 4.76
4 4.71
8 4.78
16 4.79
testing with 32 matrices
base 8.82
1 10.39
2 10.58
4 10.73
8 9.46
16 9.54
testing with 64 matrices
base 17.38
1 19.34
2 19.62
4 19.59
8 19.39
16 19.34
_
[更新4] Jan-Philip Gehrckeのコメントへの回答
はっきりさせていないのでごめんなさい。 Update 2で書いたように、私の主な目標は、サードパーティの多くのシリアル呼び出しを並列化することですPythonライブラリ関数。この関数は、いくつかのCコードへのインターフェイスです。Pool
を使用することをお勧めしました。しかし、これは機能しなかったので、上記の例のnumpy
を使用して、もっと簡単なものを試しました。しかし、「並列化可能」を探しても、パフォーマンスを向上させることはできませんでした。したがって、何かを見逃したに違いないと思います。重要です。この情報は、私がこの質問と報奨金で探しているものです。
[更新5]
多大なご意見をありがとうございました。しかし、あなたの答えを読むことは、私にとってより多くの質問を生み出すだけです。そのため、 基本 について読み、わからないことをより明確に理解したときに、新しいSO質問を作成します。
すべてのプロセスが同じCPUで実行されているという事実に関して、 ここで私の答えを参照してください 。
インポート中に、numpy
は親プロセスのCPUアフィニティを変更します。これにより、後でPool
を使用すると、生成されるすべてのワーカープロセスが、同じコアではなく、同じコアを争うことになります。マシンで利用可能なすべてのコアを使用します。
taskset
をインポートした後にnumpy
を呼び出して、CPUアフィニティをリセットし、すべてのコアが使用されるようにすることができます。
import numpy as np
import os
from timeit import timeit
from multiprocessing import Pool
def mmul(matrix):
for i in range(100):
matrix = matrix * matrix
return matrix
if __name__ == '__main__':
matrices = []
for i in range(4):
matrices.append(np.random.random_integers(100, size=(1000, 1000)))
print timeit(lambda: map(mmul, matrices), number=20)
# after importing numpy, reset the CPU affinity of the parent process so
# that it will use all cores
os.system("taskset -p 0xff %d" % os.getpid())
pool = Pool(8)
print timeit(lambda: pool.map(mmul, matrices), number=20)
出力:
$ python tmp.py
12.4765810966
pid 29150's current affinity mask: 1
pid 29150's new affinity mask: ff
13.4136221409
このスクリプトの実行中にtop
を使用してCPUの使用状況を監視すると、「並列」部分を実行するときにすべてのコアを使用していることがわかります。他の人が指摘しているように、元の例では、データのピクルス化、プロセスの作成などに伴うオーバーヘッドが、並列化によるメリットを上回っている可能性があります。
編集:単一のプロセスが一貫して高速であるように見える理由の一部は、numpy
が高速化のためのいくつかのトリックを持っている可能性があるためだと思いますジョブが複数のコアに分散している場合は使用できない要素ごとの行列乗算を増やします。
たとえば、通常のPythonリストを使用してフィボナッチ数列を計算すると、並列化によって大幅に高速化できます。同様に、要素ごとの乗算を利点のない方法で実行すると、ベクトル化の場合、並列バージョンでも同様のスピードアップが得られます。
import numpy as np
import os
from timeit import timeit
from multiprocessing import Pool
def fib(dummy):
n = [1,1]
for ii in xrange(100000):
n.append(n[-1]+n[-2])
def silly_mult(matrix):
for row in matrix:
for val in row:
val * val
if __name__ == '__main__':
dt = timeit(lambda: map(fib, xrange(10)), number=10)
print "Fibonacci, non-parallel: %.3f" %dt
matrices = [np.random.randn(1000,1000) for ii in xrange(10)]
dt = timeit(lambda: map(silly_mult, matrices), number=10)
print "Silly matrix multiplication, non-parallel: %.3f" %dt
# after importing numpy, reset the CPU affinity of the parent process so
# that it will use all CPUS
os.system("taskset -p 0xff %d" % os.getpid())
pool = Pool(8)
dt = timeit(lambda: pool.map(fib,xrange(10)), number=10)
print "Fibonacci, parallel: %.3f" %dt
dt = timeit(lambda: pool.map(silly_mult, matrices), number=10)
print "Silly matrix multiplication, parallel: %.3f" %dt
出力:
$ python tmp.py
Fibonacci, non-parallel: 32.449
Silly matrix multiplication, non-parallel: 40.084
pid 29528's current affinity mask: 1
pid 29528's new affinity mask: ff
Fibonacci, parallel: 9.462
Silly matrix multiplication, parallel: 12.163
ここでは、通信オーバーヘッドと計算速度の向上の間の予測できない競争が間違いなく問題になっています。あなたが観察していることは完全に素晴らしいです。正味のスピードアップが得られるかどうかは多くの要因に依存し、(あなたがしたように)適切に定量化する必要があるものです。
あなたの場合、なぜmultiprocessing
は「予想外に遅い」のですか?multiprocessing
のmap
および_map_async
_関数は実際にPythonオブジェクトを接続するパイプを介して前後にピクルスします)子プロセスを持つ親。これにはかなりの時間がかかる場合があります。その間、子プロセスはほとんど何もしません。これはhtop
に表示されます。異なるシステム間では、パイプトランスポートのパフォーマンスにかなりの違いがある可能性があります。これは、一部の人にとってはプールコードが単一のCPUコードよりも速い理由でもありますが、そうではありません(他の要因がここで関係する可能性がありますが、これは効果を説明するための単なる例です)。
高速化するために何ができますか?
POSIX準拠のシステムでは入力をピクルスにしないでください。
Unixを使用している場合は、POSIXのプロセスフォークの動作(書き込み時にメモリをコピーする)を利用することで、親子通信のオーバーヘッドを回避できます。
グローバルにアクセス可能な変数の親プロセスで作業するジョブ入力(大きなマトリックスのリストなど)を作成します。次に、multiprocessing.Process()
を自分で呼び出してワーカープロセスを作成します。子では、グローバル変数からジョブ入力を取得します。簡単に言えば、これにより、子は通信オーバーヘッドなしで親のメモリにアクセスできます(*、以下の説明)。たとえば、結果を親に送り返します。 _multiprocessing.Queue
_。これにより、特に出力が入力に比べて小さい場合に、通信のオーバーヘッドを大幅に節約できます。この方法は、たとえばWindows、multiprocessing.Process()
があるため、親の状態を継承しないまったく新しいPythonプロセスが作成されます。
numpyマルチスレッドを使用します。実際の計算タスクによっては、multiprocessing
を使用してもまったく役に立たない場合があります。 numpyを自分でコンパイルし、OpenMPディレクティブを有効にすると、ラージマトリックスでの操作は、それ自体で非常に効率的にマルチスレッド化される可能性があります(そして、多くのCPUコアに分散されます。GILはここでは制限要因ではありません)。基本的に、これはnumpy/scipyのコンテキストで取得できる複数のCPUコアの最も効率的な使用法です。
*一般的に、子供は親の記憶に直接アクセスすることはできません。ただし、fork()
の後、親と子は同等の状態になります。親のメモリ全体をRAM内の別の場所にコピーするのはばかげています。そのため、コピーオンライトの原則が採用されています。子がメモリ状態を変更しない限り、実際には親のメモリにアクセスします。変更があった場合にのみ、対応するビットとピースが子のメモリ空間にコピーされます。
主な編集:
複数のワーカープロセスで大量の入力データを処理し、「1。POSIX準拠のシステムで入力をピクルスにしないでください」というアドバイスに従うコードを追加しましょう。さらに、ワーカーマネージャー(親プロセス)に転送される情報の量は非常に少ないです。この例の重い計算部分は、単一値分解です。 OpenMPを多用することができます。この例を複数回実行しました。
OMP_NUM_THREADS=1
_を使用すると、各ワーカープロセスは100%の最大負荷を作成します。そこでは、前述の労働者数-計算時間のスケーリング動作はほぼ線形であり、正味のスピードアップ係数は関与する労働者の数に対応します。OMP_NUM_THREADS=4
_を使用すると、各プロセスが最大400%の負荷を作成します(4つのOpenMPスレッドを生成することにより)。私のマシンには16個の実際のコアがあるため、それぞれ最大400%の負荷を持つ4つのプロセスは、ほぼマシンから最大のパフォーマンスを引き出します。スケーリングは完全に線形ではなくなり、スピードアップ係数は関係するワーカーの数ではありませんが、絶対計算時間は_OMP_NUM_THREADS=1
_と比較して大幅に短縮され、ワーカープロセスの数とともに時間は大幅に短縮されます。OMP_NUM_THREADS=4
_を使用した場合。その結果、平均システム負荷は1253%になります。OMP_NUM_THREADS=5
_。その結果、平均システム負荷は1598%になります。これは、16コアのマシンからすべてを取得したことを示しています。ただし、実際の計算の実時間は後者の場合に比べて改善されません。コード:
_import os
import time
import math
import numpy as np
from numpy.linalg import svd as svd
import multiprocessing
# If numpy is compiled for OpenMP, then make sure to control
# the number of OpenMP threads via the OMP_NUM_THREADS environment
# variable before running this benchmark.
MATRIX_SIZE = 1000
MATRIX_COUNT = 16
def rnd_matrix():
offset = np.random.randint(1,10)
stretch = 2*np.random.Rand()+0.1
return offset + stretch * np.random.Rand(MATRIX_SIZE, MATRIX_SIZE)
print "Creating input matrices in parent process."
# Create input in memory. Children access this input.
INPUT = [rnd_matrix() for _ in xrange(MATRIX_COUNT)]
def worker_function(result_queue, worker_index, chunk_boundary):
"""Work on a certain chunk of the globally defined `INPUT` list.
"""
result_chunk = []
for m in INPUT[chunk_boundary[0]:chunk_boundary[1]]:
# Perform single value decomposition (CPU intense).
u, s, v = svd(m)
# Build single numeric value as output.
output = int(np.sum(s))
result_chunk.append(output)
result_queue.put((worker_index, result_chunk))
def work(n_workers=1):
def calc_chunksize(l, n):
"""Rudimentary function to calculate the size of chunks for equal
distribution of a list `l` among `n` workers.
"""
return int(math.ceil(len(l)/float(n)))
# Build boundaries (indices for slicing) for chunks of `INPUT` list.
chunk_size = calc_chunksize(INPUT, n_workers)
chunk_boundaries = [
(i, i+chunk_size) for i in xrange(0, len(INPUT), chunk_size)]
# When n_workers and input list size are of same order of magnitude,
# the above method might have created less chunks than workers available.
if n_workers != len(chunk_boundaries):
return None
result_queue = multiprocessing.Queue()
# Prepare child processes.
children = []
for worker_index in xrange(n_workers):
children.append(
multiprocessing.Process(
target=worker_function,
args=(
result_queue,
worker_index,
chunk_boundaries[worker_index],
)
)
)
# Run child processes.
for c in children:
c.start()
# Create result list of length of `INPUT`. Assign results upon arrival.
results = [None] * len(INPUT)
# Wait for all results to arrive.
for _ in xrange(n_workers):
worker_index, result_chunk = result_queue.get(block=True)
chunk_boundary = chunk_boundaries[worker_index]
# Store the chunk of results just received to the overall result list.
results[chunk_boundary[0]:chunk_boundary[1]] = result_chunk
# Join child processes (clean up zombies).
for c in children:
c.join()
return results
def main():
durations = []
n_children = [1, 2, 4]
for n in n_children:
print "Crunching input with %s child(ren)." % n
t0 = time.time()
result = work(n)
if result is None:
continue
duration = time.time() - t0
print "Result computed by %s child process(es): %s" % (n, result)
print "Duration: %.2f s" % duration
durations.append(duration)
normalized_durations = [durations[0]/d for d in durations]
for n, normdur in Zip(n_children, normalized_durations):
print "%s-children speedup: %.2f" % (n, normdur)
if __name__ == '__main__':
main()
_
出力:
_$ export OMP_NUM_THREADS=1
$ /usr/bin/time python test2.py
Creating input matrices in parent process.
Crunching input with 1 child(ren).
Result computed by 1 child process(es): [5587, 8576, 11566, 12315, 7453, 23245, 6136, 12387, 20634, 10661, 15091, 14090, 11997, 20597, 21991, 7972]
Duration: 16.66 s
Crunching input with 2 child(ren).
Result computed by 2 child process(es): [5587, 8576, 11566, 12315, 7453, 23245, 6136, 12387, 20634, 10661, 15091, 14090, 11997, 20597, 21991, 7972]
Duration: 8.27 s
Crunching input with 4 child(ren).
Result computed by 4 child process(es): [5587, 8576, 11566, 12315, 7453, 23245, 6136, 12387, 20634, 10661, 15091, 14090, 11997, 20597, 21991, 7972]
Duration: 4.37 s
1-children speedup: 1.00
2-children speedup: 2.02
4-children speedup: 3.81
48.75user 1.75system 0:30.00elapsed 168%CPU (0avgtext+0avgdata 1007936maxresident)k
0inputs+8outputs (1major+809308minor)pagefaults 0swaps
$ export OMP_NUM_THREADS=4
$ /usr/bin/time python test2.py
Creating input matrices in parent process.
Crunching input with 1 child(ren).
Result computed by 1 child process(es): [22735, 5932, 15692, 14129, 6953, 12383, 17178, 14896, 16270, 5591, 4174, 5843, 11740, 17430, 15861, 12137]
Duration: 8.62 s
Crunching input with 2 child(ren).
Result computed by 2 child process(es): [22735, 5932, 15692, 14129, 6953, 12383, 17178, 14896, 16270, 5591, 4174, 5843, 11740, 17430, 15861, 12137]
Duration: 4.92 s
Crunching input with 4 child(ren).
Result computed by 4 child process(es): [22735, 5932, 15692, 14129, 6953, 12383, 17178, 14896, 16270, 5591, 4174, 5843, 11740, 17430, 15861, 12137]
Duration: 2.95 s
1-children speedup: 1.00
2-children speedup: 1.75
4-children speedup: 2.92
106.72user 3.07system 0:17.19elapsed 638%CPU (0avgtext+0avgdata 1022240maxresident)k
0inputs+8outputs (1major+841915minor)pagefaults 0swaps
$ /usr/bin/time python test2.py
Creating input matrices in parent process.
Crunching input with 4 child(ren).
Result computed by 4 child process(es): [21762, 26806, 10148, 22947, 20900, 8161, 20168, 17439, 23497, 26360, 6789, 11216, 12769, 23022, 26221, 20480, 19140, 13757, 23692, 19541, 24644, 21251, 21000, 21687, 32187, 5639, 23314, 14678, 18289, 12493, 29766, 14987, 12580, 17988, 20853, 4572, 16538, 13284, 18612, 28617, 19017, 23145, 11183, 21018, 10922, 11709, 27895, 8981]
Duration: 12.69 s
4-children speedup: 1.00
174.03user 4.40system 0:14.23elapsed 1253%CPU (0avgtext+0avgdata 2887456maxresident)k
0inputs+8outputs (1major+1211632minor)pagefaults 0swaps
$ export OMP_NUM_THREADS=5
$ /usr/bin/time python test2.py
Creating input matrices in parent process.
Crunching input with 4 child(ren).
Result computed by 4 child process(es): [19528, 17575, 21792, 24303, 6352, 22422, 25338, 18183, 15895, 19644, 20161, 22556, 24657, 30571, 13940, 18891, 10866, 21363, 20585, 15289, 6732, 10851, 11492, 29146, 12611, 15022, 18967, 25171, 10759, 27283, 30413, 14519, 25456, 18934, 28445, 12768, 28152, 24055, 9285, 26834, 27731, 33398, 10172, 22364, 12117, 14967, 18498, 8111]
Duration: 13.08 s
4-children speedup: 1.00
230.16user 5.98system 0:14.77elapsed 1598%CPU (0avgtext+0avgdata 2898640maxresident)k
0inputs+8outputs (1major+1219611minor)pagefaults 0swaps
_
あなたのコードは正しいです。私はそれを私のシステム(2コア、ハイパースレッディング)で実行し、次の結果を得ました:
$ python test_multi.py
30.8623809814
19.3914041519
私はプロセスを調べましたが、予想どおり、いくつかのプロセスがほぼ100%で動作していることを示す並列部分を確認しました。これは、システム内の何か、またはpythonインストールである必要があります。
デフォルトでは、Pool
はn個のプロセスのみを使用します。ここで、nはマシン上のCPUの数です。 Pool(5)
のように、使用するプロセスの数を指定する必要があります。
ファイルがたくさんあるとおっしゃっていたので、次の解決策をお勧めします。
Pool.map()
を使用して、ファイルのリストに関数を適用します。すべてのインスタンスが独自のファイルをロードするようになったため、渡されるデータはファイル名のみであり、(潜在的に大きな)numpy配列ではありません。
また、Pool.map()関数内でnumpy行列の乗算を実行すると、特定のマシンでの実行速度が大幅に低下することにも気づきました。私の目標は、Pool.map()を使用して作業を並列化し、マシンの各コアでプロセスを実行することでした。物事が高速で実行されていたとき、numpy行列の乗算は、並行して実行された全体的な作業のごく一部にすぎませんでした。プロセスのCPU使用率を見ると、各プロセスで使用できることがわかりました。低速で実行されたマシンでは400 +%CPUですが、高速で実行されたマシンでは常に<= 100%です。私にとっての解決策は マルチスレッドからnumpyを停止する でした。 Pool.map()の実行速度が遅いマシンで、numpyがマルチスレッドに設定されていることがわかりました。明らかに、Pool.map()を使用して既に並列化している場合、numpyも並列化すると、干渉が発生するだけです。 export MKL_NUM_THREADS=1
私のPythonコードを実行する前に、どこでも高速に動作しました。
次の環境変数を設定しますbefore任意の計算(以前のバージョンのnumpyではimport numpy
を実行する前に設定する必要がある場合があります):
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
os.environ["OPENBLAS_NUM_THREADS"] = "1"
os.environ["VECLIB_MAXIMUM_THREADS"] = "1"
os.environ["NUMEXPR_NUM_THREADS"] = "1"
Numpyの実装はすでに OpenMP、MKL、OpenBLASなどの最適化ライブラリでマルチスレッドを使用しています。そのため、マルチプロセッシングを自分で実装してもあまり改善は見られません。さらに悪いことに、スレッドが多すぎます。たとえば、私のマシンに8つのCPUコアがある場合、single-処理コードを記述すると、numpyは計算に8つのスレッドを使用する可能性があります。次に、マルチプロセッシングを使用して8つのプロセスを開始し、64のスレッドを取得します。これは有益ではなく、スレッドと他のオーバーヘッドの間のコンテキスト切り替えにはより多くの時間がかかる可能性があります。上記の環境変数を設定することにより、プロセスあたりのスレッド数を1に制限し、合計スレッド数が最も効率的になります。
from timeit import timeit
from multiprocessing import Pool
import sys
import os
import numpy as np
def matmul(_):
matrix = np.ones(shape=(1000, 1000))
_ = np.matmul(matrix, matrix)
def mixed(_):
matrix = np.ones(shape=(1000, 1000))
_ = np.matmul(matrix, matrix)
s = 0
for i in range(1000000):
s += i
if __name__ == '__main__':
if sys.argv[1] == "--set-num-threads":
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
os.environ["OPENBLAS_NUM_THREADS"] = "1"
os.environ["VECLIB_MAXIMUM_THREADS"] = "1"
os.environ["NUMEXPR_NUM_THREADS"] = "1"
if sys.argv[2] == "matmul":
f = matmul
Elif sys.argv[2] == "mixed":
f = mixed
print("Serial:")
print(timeit(lambda: list(map(f, [0] * 8)), number=20))
with Pool(8) as pool:
print("Multiprocessing:")
print(timeit(lambda: pool.map(f, [0] * 8), number=20))
8つのvCPU(必ずしも8つのコアを意味するわけではありません)を持つAWSp3.2xlargeインスタンスでコードをテストしました。
$ python test_multi.py --no-set-num-threads matmul
Serial:
3.3447616740000115
Multiprocessing:
3.5941055110000093
$ python test_multi.py --set-num-threads matmul
Serial:
9.464500446000102
Multiprocessing:
2.570238267999912
これらの環境変数を設定する前は、シリアルバージョンとマルチプロセッシングバージョンで大きな違いはありませんでした。すべて約3秒で、OPで示されているように、マルチプロセッシングバージョンの方が遅いことがよくありました。スレッド数を設定した後、シリアルバージョンは9.46秒かかり、非常に遅くなっていることがわかります。これは、単一のプロセスが使用されている場合でも、numpyがマルチスレッドを利用していることの証拠です。マルチプロセッシングバージョンは2.57秒かかり、少し改善されました。これは、クロススレッドデータ転送時間が私の実装で節約されたためである可能性があります。
Numpyはすでに並列化を使用しているため、この例ではマルチプロセッシングの能力はあまり示されていません。マルチプロセッシングは、通常のPython集中的なCPU計算がnumpy操作と混合されている場合に最も有益です。たとえば、
$ python test_multi.py --no-set-num-threads mixed
Serial:
12.380275611000116
Multiprocessing:
8.190792100999943
$ python test_multi.py --set-num-threads mixed
Serial:
18.512066430999994
Multiprocessing:
4.8058130150000125
ここでは、スレッド数を1に設定したマルチプロセッシングが最速です。
備考:これは、PyTorchなどの他のCPU計算ライブラリでも機能します。