web-dev-qa-db-ja.com

これを並列化する方法Python fornum使用時のループ

私はNumbaとともにPythonのAnacondaディストリビューションを使用しており、疎行列を乗算する次のPython関数を作成しましたA(密なベクトルによるCSR形式)x

_@jit
def csrMult( x, Adata, Aindices, Aindptr, Ashape ):

    numRowsA = Ashape[0]
    Ax       = numpy.zeros( numRowsA )

    for i in range( numRowsA ):
        Ax_i = 0.0
        for dataIdx in range( Aindptr[i], Aindptr[i+1] ):

            j     = Aindices[dataIdx]
            Ax_i +=    Adata[dataIdx] * x[j]

        Ax[i] = Ax_i

    return Ax 
_

ここではAは大きなscipy疎行列です。

_>>> A.shape
( 56469, 39279 )
#                  having ~ 142,258,302 nonzero entries (so about 6.4% )
>>> type( A[0,0] )
dtype( 'float32' )
_

およびxnumpy配列です。上記の関数を呼び出すコードスニペットを次に示します。

_x       = numpy.random.randn( A.shape[1] )
Ax      = A.dot( x )   
AxCheck = csrMult( x, A.data, A.indices, A.indptr, A.shape )
_

NumbaにcsrMult()関数のジャストインタイムコンパイルを実行するように指示する_@jit_-decoratorに注意してください。

私の実験では、関数csrMult()は約2倍の速さscipy.dot()メソッドと同じです。これはNumbaにとってかなり印象的な結果です。

ただし、MATLABはこの行列とベクトルの乗算をcsrMult()よりも6倍高速で実行します。これは、MATLABがスパース行列-ベクトル乗算を実行するときにマルチスレッドを使用するためだと思います。


質問:

Numbaを使用している場合、外側のforループを並列化するにはどうすればよいですか?

Numbaにはprange()関数がありましたが、これにより恥ずかしいほどに並列化するのが簡単になりましたfor-loops。残念ながら、Numbaにはprange()がありません[実際には、これは誤りです。以下の編集を参照してください]。 それでは、このfor- loopを並列化する正しい方法は何ですか、Numbaのprange()関数はなくなっていますか?

prange()がNumbaから削除されたとき、Numbaの開発者はどのような代替案を考えていましたか?


編集1:
Numbaの最新バージョンである.35に更新しました。prange()が復活しました!使用していたバージョン.33には含まれていませんでした。
それは朗報ですが、prange()を使用してforループを並列化しようとすると、残念ながらエラーメッセージが表示されます。以下は、Numbaのドキュメントの並列forループ example です(セクション1.9.2「明示的な並列ループ」を参照)。以下が新しいコードです。

_from numba import njit, prange
@njit( parallel=True )
def csrMult_numba( x, Adata, Aindices, Aindptr, Ashape):

    numRowsA = Ashape[0]    
    Ax       = np.zeros( numRowsA )

    for i in prange( numRowsA ):
        Ax_i = 0.0        
        for dataIdx in range( Aindptr[i],Aindptr[i+1] ):

            j     = Aindices[dataIdx]
            Ax_i +=    Adata[dataIdx] * x[j]

        Ax[i] = Ax_i            

    return Ax 
_

上記のコードスニペットを使用してこの関数を呼び出すと、次のエラーが表示されます。

AttributeError:nopythonで失敗(parforsに変換) 'SetItem'オブジェクトに属性 'get_targets'がありません


与えられた
上記のprangeを使用しようとするとクラッシュします。私の質問は次のとおりです。

正しい方法は何ですかprangeまたは代替方法を使用)これを並列化するにはPython for- loop? =

以下に示すように、C++で同様のforループを並列化し、2-omp-threadsで実行された8xスピードアップを取得することは簡単でした。 forループが非常に並列であるため(また、スパース行列とベクトルの乗算は科学計算の基本的な演算であるため)、Numbaを使用してそれを行う方法が必要です。


編集2:
これが私のC++バージョンのcsrMult()です。 C++バージョンでfor()ループを並列化すると、テストでコードが約8倍速くなります。これは、Numbaを使用する場合、Pythonバージョンでも同様の高速化が可能であることを示唆しています。

_void csrMult(VectorXd& Ax, VectorXd& x, vector<double>& Adata, vector<int>& Aindices, vector<int>& Aindptr)
{
    // This code assumes that the size of Ax is numRowsA.
    #pragma omp parallel num_threads(20)
    {       
        #pragma omp for schedule(dynamic,590) 
        for (int i = 0; i < Ax.size(); i++)
        {
            double Ax_i = 0.0;
            for (int dataIdx = Aindptr[i]; dataIdx < Aindptr[i + 1]; dataIdx++)
            {
                Ax_i += Adata[dataIdx] * x[Aindices[dataIdx]];
            }

            Ax[i] = Ax_i;
        }
    }
}
_
15
littleO

ダニエル、あなたの量的な更新をありがとう。
次の行は飲み込むのが難しいかもしれませんが、親切に私を信じてください、考慮すべきことがまだたくさんあります。私は、スケールのマトリックスがあるHPC /並列計算の問題に取り組んできました〜N [TB]; N > 10と、それらのまばらな随伴なので、いくつかの経験が今後のビューに役立つかもしれません。

警告:夕食が無料で提供されることを期待しないでください

コードの一部を並列化したいという願望は、ますます頻繁に現代的な再連結されたマナのように聞こえます。 問題はコードではなく、そのような移動のコスト

経済が一番の問題です。アムダールの法則は、元々はジーン・アムダールによって策定されたものであるので、[PAR]- processes-setups + [PAR]- processes-finalizations&terminatesのコストを考慮に入れていません。これらは実際にすべての現実の実装で支払う必要があります。

The overhead-strict Amdahl's Lawは、これらの避けられない悪影響のスケールを示し、並列化を導入する前に評価する必要があるいくつかの新しい側面を理解するのに役立ちます (そうすることの許容できるコストは、実際には、1つ以上の金額を支払うことは非常に簡単であり、処理パフォーマンスの低下による素朴な失望はストーリーの一部です)。

オーバーヘッドが厳しいアムダールの法則の再定式化に関する投稿をもっと読んでください。このトピックをよりよく理解し、[事前計算]実際"最小 "-subProblem-"size "、そのためsum-of -[PAR]- overheadsは少なくとも正当化されますN_trully_[PAR]_processesにsubProblemの並列分割を導入するための実際のツールから(「 "" [CONCURRENT]」ではなく、true -[PARALLEL]-これらは等しくない)。


Pythonは、パフォーマンスを向上させるためにステロイドを投与することがあります。

Pythonは優れたプロトタイピングエコシステムですが、numbanumpyなどのコンパイルされた拡張機能は、ネイティブのGILステップpython(co-)-processingが通常提供します。

ここでは、numba.jit()を強制して、ジョブをほぼ-for-freeで、自動化されたjit()- time lexical-analyser(あなたがあなたのコードを投げる)、それはあなたのグローバルなゴールを「理解する」(What to do)、そしてまたいくつかのベクトル化のトリック(最良の方法 CPU命令のヒープをアセンブルして、このようなコード実行の効率を最大化します)。

これは簡単に聞こえますが、簡単ではありません。

Travis Oliphantのチームはnumbaツールで莫大な進歩を作成しましたが、.jit()- lexer +コード内に自動化されたウィザードが実装されることを期待しないように現実的で公平にしましょう。コードを変換し、機械命令のより効率的なフローをアセンブルして高レベルのタスクの目標を実装しようとするときの分析。

@guvectorize?ここに?マジ?

[PSPACE]のサイズ設定により、GPUエンジンにデータを効率的に「詰め込む」ようにnumbaに要求することをすぐに忘れてしまう可能性があります。そのような数学的に「小さな」処理のためのGPUカーネルサイズは、単に[PAR]で乗算され、後で[SEQ]で合計されます)。

(再)-データを含むGPUの読み込みには、かなりの時間がかかります。これを支払った場合、GPU内メモリのレイテンシは「小さな」GPUカーネルエコノミーにとってあまり友好的ではありません-GPU-SMXコード実行は数値をフェッチするためだけに〜350-700 [ns]を支払う必要があります(次のステップで、SMキャッシュに適した最適な再利用のために自動的に再調整されない可能性が高く、決して繰り返さないでください。単一のマトリックスセルをまったく再利用しないでください。 、したがって、キャッシュ自体は、マトリックスセルごとの350~700 [ns]の下では何も配信しません)、一方、スマートな純粋なnumpy- vectorisedコードは、最大の1 [ns]- footprintsでもセルあたり[PSPACE]未満でマトリックス-ベクトル積を処理できます

それは比較する尺度です。

(プロファイリングは、ここでハードファクトをよりよく示すでしょうが、原理を事前によく知っており、少数のTBをGPUファブリックに移動して、これを自分で実現する方法をテストする必要はありません。)


悪いニュースの最悪:

マトリックスAのメモリスケールを考慮すると、予想されるより悪い影響は、マトリックス表現のストレージのスパース組織が、すべてではないにしても、ほとんどの場合、達成可能なパフォーマンスの向上のほとんどを破壊することです。密な行列表現でのnumba- vectorisedトリックによる。効率的なメモリフェッチキャッシュラインの再利用の可能性はほぼゼロであり、スパース性はベクトル化された操作のコンパクトなマッピングを実現する簡単な方法を壊し、これらはほとんど機能しません。高度なCPUハードウェアベクトル処理リソースに簡単に変換できます。


解決可能な問題の一覧:

  • ベクトルAx = np.zeros_like( A[:,0] )を常により適切に事前割り当てし、それを別のパラメーターとしてnumba.jit()でコンパイルされたコードの一部に渡すことにより、追加の[PTIME,PSPACE]-コストを繰り返し支払うことを回避します(再度)新しいメモリ割り当て(ベクターが外部で調整された反復最適化プロセス内で使用されている疑いがある場合はさらに多くなります)
  • 常により適切に指定する(結果のコードパフォーマンスのために、普遍性を狭めるため)
    少なくともnumba.jit( "f8[:]( f4[:], f4[:,:], ... )" )- callingインターフェイスディレクティブ
  • 利用可能なすべてのnumba.jit()- optionsとそれぞれのデフォルト値を常に確認する (バージョンをバージョンに変更する場合があります) 特定の状況の場合(GILを無効にし、目標をnumba +ハードウェア機能に合わせると、コードの数値が集中する部分で常に役立ちます)

@jit(   signature = [    numba.float32( numba.float32, numba.int32 ),                                   #          # [_v41] @decorator with a list of calling-signatures
                         numba.float64( numba.float64, numba.int64 )                                    #
                         ],    #__________________ a list of signatures for prepared alternative code-paths, to avoid a deferred lazy-compilation if undefined
        nopython = False,      #__________________ forces the function to be compiled in nopython mode. If not possible, compilation will raise an error.
        nogil    = False,      #__________________ tries to release the global interpreter lock inside the compiled function. The GIL will only be released if Numba can compile the function in nopython mode, otherwise a compilation warning will be printed.
        cache    = False,      #__________________ enables a file-based cache to shorten compilation times when the function was already compiled in a previous invocation. The cache is maintained in the __pycache__ subdirectory of the directory containing the source file.
        forceobj = False,      #__________________ forces the function to be compiled in object mode. Since object mode is slower than nopython mode, this is mostly useful for testing purposes.
        locals   = {}          #__________________ a mapping of local variable names to Numba Types.
        ) #____________________# [_v41] ZERO <____ TEST *ALL* CALLED sub-func()-s to @.jit() too >>>>>>>>>>>>>>>>>>>>> [DONE]
 def r...(...):
      ...
5
user3666197

Numbaが更新され、prange()が動作するようになりました! (私は自分の質問に答えています。)

Numbaの並列計算機能の改善については、2017年12月12日付けのこの ブログ投稿 で説明されています。ブログからの関連スニペットは次のとおりです。

ずっと前(20リリース以上!)に、Numbaはprange()と呼ばれる並列forループを書くためのイディオムをサポートしていた。 2014年にコードベースの大幅なリファクタリングを行った後、この機能は削除する必要がありましたが、それ以降、最も頻繁に要求されるNumba機能の1つになりました。インテルの開発者が配列式を並列化した後、prangeを戻すのはかなり簡単であることに気付きました

Numbaバージョン0.36.1を使用すると、次の簡単なコードを使用して、恥ずかしいほど並列のfor- loopを並列化できます。

_@numba.jit(nopython=True, parallel=True)
def csrMult_parallel(x,Adata,Aindices,Aindptr,Ashape): 

    numRowsA = Ashape[0]    
    Ax = np.zeros(numRowsA)

    for i in numba.prange(numRowsA):
        Ax_i = 0.0        
        for dataIdx in range(Aindptr[i],Aindptr[i+1]):

            j = Aindices[dataIdx]
            Ax_i += Adata[dataIdx]*x[j]

        Ax[i] = Ax_i            

    return Ax
_

私の実験では、for- loopを並列化すると、質問の最初に投稿したバージョンで、すでにNumbaを使用していたが並列化されていなかったバージョンよりも約8倍速く関数が実行されました。さらに、私の実験では、並列化されたバージョンは、scipyのスパース行列ベクトル乗算関数を使用するコマンドAx = A.dot(x)よりも約5倍高速です。 Numbaはscipyを押しつぶし、最終的にpython疎行列-ベクトル乗算ルーチンMATLABと同じ速さ。

8
littleO