並列化

Cython は cython.parallel モジュールでネイティブの並列化をサ ポートしています。この並列化を使うには、 GIL をリリースせねばなりませ ん (Releasing the GIL を参照してください)。 現状、 cython.parallel は OpenMP をサポートしています。他の バックエンドも順次サポートする予定です。

Note

OpenMP の制約により、このモジュールの機能は、メインスレッド や並列実行領域 (parallel region) でのみ使えます。

cython.parallel.prange([start,] stop[, step][, nogil=False][, schedule=None[, chunksize=None]][, num_threads=None])

この関数は並列化ループに使えます。 OpenMP は自動的にスレッドプール を開始して、スケジュールに従って処理を分散します。 step を 0 にしてはなりません。この関数は GIL をリリースした状況でのみ使えま す。 nogilTrue にすると、ループ全体を nogil のセク ションで囲います。

変数のスレッドローカル性やリダクション変数は、自動的に推論されます。

prange ブロックの中で変数に代入すると、その変数は lastprivate にな ります。つまり、変数には、直前のイテレーションでの値が入ります。 インプレース計算を行う演算子を lastprivate な変数に適用すると、値 のリダクションが起こります。すなわち、変数のスレッドローカルなコピー の値が演算子によってリダクション (reduction, 還元) され、ループ後 にもとの変数に代入されます。インデクス変数は常に lastprivate です。 並列演算中にブロック内で使われる変数は private であり、ブロックか ら抜けると使えません。ループ中の変数を並べて最後の値を採るという概 念がないからです。

schedule は OpenMP に渡され、下記のいずれかの値を取ります:

static:

chunksize を指定すると、全スレッドに対し、イテレーションを指定 チャンクサイズのブロック単位で順に割り当ててゆきます。 chunksuze を指定しない場合、イテレーション空間はほぼイテレーショ ンサイズと同じ数のチャンクに分割され、予め各スレッドに最大1個 のチャンクを割り当てます。

この方法は、スケジューリングのオーバヘッドが問題であって、かつ、 イテレーションが常にほぼ同じ時間で実行される、均等分割したチャ ンクにすることで問題を解決できる場合の最適なスケジューリングで す。

dynamic:

イテレーションは可能な限り多くのスレッドに分割されます。チャン クサイズのデフォルト値は 1 です。

この方法は、各チャンクの実行時間に差があり、どれだけ掛かるか予 め予測できないため、スレッドを遊ばせないために小さなチャンクを 沢山使いたい場合に適しています。

guided:

dynamic スケジューリングと同様に、イテレーションを可能な限り多 くのスレッドに割り当てますが、チャンクサイズを徐々に減らしてい きます。各チャンクのサイズは未割り当てのイテレーションの数を処 理に参加するスレッドで割った値に比例し、最終的に 1 (または、 chunksize を指定した場合はその値) まで減少します。

この方法は、純粋な動的スケジューリングに比べて、最後のチャンク 処理に予測よりも大きな実行時間が掛かる、などの理由でスケジュー リングがうまく行かず、ほとんどのスレッドが遊び始めている一方で、 ごく小数のスレッドが最後のチャンクを処理するような状況で、 dynamic より優れているスケジューリングです。

runtime:
スケジュールとチャンクサイズを実行時のスケジュール変数に基いて 決めます。スケジュール変数は openmp.omp_set_schedule() 関数 呼び出しや環境変数 OMP_SCHEDULE でセットできます。この方法は、 本質的に、スケジューリングコード自体をコンパイルする際に行われ た静的な最適化を無効にするので、たとえコンパイル時に静的に設定 されたのと同じスケジューリングポリシーを使ったとしても、パフォー マンスをやや悪化させるので注意してください。
cython.parallel.parallel(num_threads=None)

このディレクティブを with 文の一部として使うと、ブロック内のコー ドを並列化させられます。このディレクティブは、 prange 内でスレッド ローカルなバッファをセットアップするときに便利です。ブロック内の prange は並列でないワークシェアリングループ (worksharing loop) と なるので、 parrallel セクション内で代入した変数は prange からも private になります。 parallel ブロック内で private な変数は、ブロッ クを抜けた後は使えません。

スレッドローカルバッファの例を示します:

from cython.parallel import parallel, prange
from libc.stdlib cimport abort, malloc, free

cdef Py_ssize_t idx, i, n = 100
cdef int * local_buf
cdef size_t size = 10

with nogil, parallel():
    local_buf = <int *> malloc(sizeof(int) * size)
    if local_buf == NULL:
        abort()

    # スレッドローカルなバッファを逐次実行ループで初期化する
    for i in xrange(size):
        local_buf[i] = i * 2

    # スレッドローカルバッファでワークシェアリングする
    for i in prange(n, schedule='guided'):
        func(local_buf)

    free(local_buf)

セクションの後ろの部分は parallel ブロックの中にあるので、 複数スレッドに分散してコードを実行し得ます。

cython.parallel.threadid()

スレッドの id を返します。 n 個スレッドがあれば、 id の値は 0 から n-1 になり得ます。

コンパイル

実際に OpenMP サポートを使うには、 C や C++ コンパイラに OpenMP を有効 にするよう指示せねばなりません。 gcc の場合には、 setup.py に以下のよ うに書きます:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_module = Extension(
    "hello",
    ["hello.pyx"],
    extra_compile_args=['-fopenmp'],
    extra_link_args=['-fopenmp'],
)

setup(
    name = 'Hello world app',
    cmdclass = {'build_ext': build_ext},
    ext_modules = [ext_module],
)

ループからブレークアウトする

prange で並列化したブロックは、 nogil モードでの break, continue, return をサポートしています。さらに、ブロックの中で with gil ブロックを使い、ブロック中で送出された例外を伝播させられます。 ただし、 prange ブロックは OpenMP を使うので、break や return, 例外を 放置しません。そのため、ループの処理はベストエフォートで終了します。 prange() の場合、いずれかのスレッドの、いずれかのイテレーションで、最 初に break や return, 例外の送出が起きた時点で、それ以後のループ本体の 実行をスキップします。 イテレーションの順番は決まっていないので、複数の異なる値を return しう るような状況では、どの値を返すか決定的ではありません:

from cython.parallel import prange

cdef int func(Py_ssize_t n):
    cdef Py_ssize_t i

    for i in prange(n, nogil=True):
        if i == 8:
            with gil:
                raise Exception()
        elif i == 4:
            break
        elif i == 2:
            return i

上の例では、例外が送出されるか、はたまた break するか、2 を返すかは決 定できないのです。

OpenMP の関数を使う

OpenMP の関数は openmp を cimport すると使えます:

from cython.parallel cimport parallel
cimport openmp

cdef int num_threads

openmp.omp_set_dynamic(1)
with nogil, parallel():
    num_threads = openmp.omp_get_num_threads()
    ...

参考文献

[1]http://www.openmp.org/mp-documents/spec30.pdf

Table Of Contents

Previous topic

型付きメモリビュー (Typed Memoryview)

Next topic

Cython プログラムのデバッグ

This Page