Japan
サイト内の現在位置
NumPy互換数値演算ライブラリNLCPy NLCPy JITコンパイルの使い方
no.0222023.2.171. NLCPy JITコンパイルとは
今回はNLCPy JITコンパイルを使って何ができるのか具体的な例を使って紹介します。簡潔に言えば、JITコンパイルはPythonスクリプトを実行しながらPython以外のコンパイル型言語で書いたソースコードをコンパイルした後に共有ライブラリを作成して、その中に含まれる関数をPythonスクリプトから実行する枠組みを提供するものです。
NLCPyの関数やNLCで用意されていない数値計算処理をFortranやC/C++により関数として記載してコンパイル・共有ライブラリ化します。Pythonスクリプトの中からそれら関数をコールしてベクトルプロセッサへオフロードすることにより高速に処理する流れを自動的に実行する枠組みを提供します。
はじめに、簡単なサンプルコードを使ったJITコンパイル使用例を紹介します。次に、FortranやC/C++のプログラムを使用した例や、コンパイラオプションの指定方法を見ていきます。
2. NLCPy JITコンパイルの使い方
Pythonスクリプト内にCソースコードを併記してJITコンパイルを実施した例がFigure-1です。
入力[2]がCで記載した関数のソースコードです。次の入力[3]で、このソースコードをベクトルエンジンで実行可能にするためコンパイルして共有ライブラリを作成する定義を行います。コンパイルにはベクトルエンジン専用コンパイラnccが使われます。
作成された共有ライブラリの関数をPythonスクリプトで実行するために、入力[4]で関数の定義を行って関数への引数を指定します。この例では3つの64bit倍精度浮動小数点数型と1つの32bit整数型が関数への引数です。
最後に入力[5]で、NLCPyで定義した3つの配列'x','y','z'と配列長を関数've_add'に送り、ベクトルエンジンで処理します。
from nlcpy import ve_types
import nlcpy
# Cソースコード
c_src=r'''
int ve_add(double *px, double *py, double *pz, int n) {
#pragma omp parallel for
for (int i = 0; i < n; i++) pz[i] = px[i] + py[i];
return 0;
}
'''
# コンパイルと共有ライブラリ作成
ve_lib = nlcpy.jit.CustomVELibrary(code=c_src)
# 関数の定義
ve_add = ve_lib.get_function(
've_add',
args_type=(ve_types.uint64, ve_types.uint64, ve_types.uint64, ve_types.int32),
ret_type=ve_types.int32
)
x = nlcpy.arange(10., dtype='f8')
y = nlcpy.arange(10., dtype='f8')
z = nlcpy.empty(10, dtype='f8')
# 関数呼び出し
ret = ve_add(x.ve_adr, y.ve_adr, z.ve_adr, z.size, sync=True)
print(z)
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18.]
Figure-1
Pythonスクリプトの中にソースコードを直接記載するだけでなく、別に用意したソースコードをPythonスクリプトから読み込んだ後に共有ライブラリ化する使い方があります。
Figure-2の例では、ファイル名've_add.c'というソースコードをあらかじめ作成しておき、Pythonスクリプトでこれを読み込んでいます。入力[2]の1-3行でソースファイルの呼出し、5-10行でコンパイルを実施します。
この例ではコンパイルの際に、ftrace=True、compiler='/opt/nec/ve/bin/ncc'などFTRACE機能出力や使用するコンパイラの指定などのオプションを指定しています。コンパイラオプションはソースコードをPythonスクリプトに直接記載する場合も同様に指定することができます。
Figure-1、2どちらの例においても、ソースコードのコンパイル後に.so拡張子を持った共有ライブラリが指定ディレクトリに作成されます(Figure-3)。
from nlcpy import ve_types
import nlcpy
# 別のファイルで用意したソースコードの読み込み
src = ''
with open('./ve_add.c', 'r') as fs:
src += fs.read()
# コンパイルのためソースコードとコンパイラー指定。
ve_lib = nlcpy.jit.CustomVELibrary(
code=src,
ftrace=True,
compiler='/opt/nec/ve/bin/ncc',
dist_dir='./'
)
ve_add = ve_lib.get_function(
've_add',
args_type=(ve_types.uint64, ve_types.uint64, ve_types.uint64, ve_types.int32),
ret_type=ve_types.int32
)
x = nlcpy.arange(10., dtype='f8')
y = nlcpy.arange(10., dtype='f8')
z = nlcpy.empty(10, dtype='f8')
ret = ve_add(x.ve_adr, y.ve_adr, z.ve_adr, z.size, sync=True)
print(z)
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18.]

3. 既存する共有ライブラリの呼出しや複数言語での処理
ベクトルエンジンで実行可能な共有ライブラリをPythonスクリプトから直接指定して関数の呼び出す例がFigure-4です。ここではFigure-1,2の処理を行う関数をあらかじめ共有ライブラリ化して'fig4lib.so'と名前を付けています。これをPythonスクリプトから読出し、関数の処理をベクトルエンジンで実行します。
あらかじめ個別に作成した共有ライブラリをPythonから実行することで、コンパイラオプションを多用や、makeを使用した複数ソースファイルの依存管理などが容易にします。
from nlcpy import ve_types
import nlcpy
# 別途用意した共有ライブラリを呼び出し
ve_lib = nlcpy.jit.CustomVELibrary(path='./fig4lib.so')
ve_add = ve_lib.get_function(
've_add',
args_type=(ve_types.uint64, ve_types.uint64, ve_types.uint64, ve_types.int32),
ret_type=ve_types.int32
)
x = nlcpy.arange(10., dtype='f8')
y = nlcpy.arange(10., dtype='f8')
z = nlcpy.empty(10, dtype='f8')
ret = ve_add(x.ve_adr, y.ve_adr, z.ve_adr, z.size, sync=True)
print(z)
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18.]
Figure-4
複数の関数を定義して、これらをスクリプトから呼び出す例がFigure-5です。入力[2]で've-add'と've-mul'の2つの関数を定義し、コンパイルの後に入力[4]でそれぞれの関数に対して個別に定義を行っています。
from nlcpy import ve_types
import nlcpy
# 2つの関数've_add'、've_mul'
c_src=r'''
int ve_add(double *px, double *py, double *pz, int n) {
#pragma omp parallel for
for (int i = 0; i < n; i++) pz[i] = px[i] + py[i];
return 0;
}
int ve_mul(double *px, double *py, double *pz, int n) {
#pragma omp parallel for
for (int i = 0; i < n; i++) pz[i] = px[i] * py[i];
return 0;
}
'''
ve_lib = nlcpy.jit.CustomVELibrary(code=c_src)
# 2つの関数の定義
ve_add = ve_lib.get_function(
've_add',
args_type=(ve_types.uint64, ve_types.uint64, ve_types.uint64, ve_types.int32),
ret_type=ve_types.int32
)
ve_mul = ve_lib.get_function(
've_mul',
args_type=(ve_types.uint64, ve_types.uint64, ve_types.uint64, ve_types.int32),
ret_type=ve_types.int32
)
x = nlcpy.arange(10., dtype='f8')
y = nlcpy.arange(10., dtype='f8')
z = nlcpy.empty(10, dtype='f8')
ret = ve_add(x.ve_adr, y.ve_adr, z.ve_adr, z.size, sync=True)
print(z)
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18.]
ret = ve_mul(x.ve_adr, y.ve_adr, z.ve_adr, z.size, sync=True)
print(z)
[ 0. 1. 4. 9. 16. 25. 36. 49. 64. 81.]
Pythonスクリプトから複数の言語で記載したソースコードをコンパイルして使用する例をFigure-6に示しました。ここではCとFortranで記載したソースコードをコンパイルして共有ライブラリ化した後、関数've_add'、've-sub'を呼び出します。Fortranの関数定義の際は入力[7]2行目にあるように関数名を've_sub_'とします。スクリプトから関数を呼び出す際は末尾の'_'を取り除きます。
from nlcpy import ve_types
import nlcpy
# Cソースコード
c_src=r'''
int ve_add(double *px, double *py, double *pz, int n) {
#pragma omp parallel for
for (int i = 0; i < n; i++) pz[i] = px[i] + py[i];
return 0;
}
'''
# Fortranソースコード
f_src = r"""
integer(kind=4) function ve_sub(px, py, pz, n)
integer(kind=4), value :: n
double precision :: px(n), py(n), pz(n)
!$omp parallel do
do i=1, n
pz(i) = px(i) - py(i)
end do
ve_add = 0
end
"""
# Cソースコードのコンパイルと共有ライブラリ化
ve_lib = nlcpy.jit.CustomVELibrary(code=c_src, compiler='/opt/nec/ve/bin/ncc')
# 関数定義
ve_add = ve_lib.get_function(
've_add',
args_type=(ve_types.uint64, ve_types.uint64, ve_types.uint64, ve_types.int32),
ret_type=ve_types.int32
)
# Fortranソースコードのコンパイルと共有ライブラリ化
ve_lib = nlcpy.jit.CustomVELibrary(code=f_src, compiler='/opt/nec/ve/bin/nfort')
# 関数定義
ve_sub = ve_lib.get_function(
've_sub_',
args_type=(ve_types.uint64, ve_types.uint64, ve_types.uint64, ve_types.int32),
ret_type=ve_types.int32
)
x = nlcpy.arange(10., dtype='f8')
y = nlcpy.arange(10., dtype='f8')
z = nlcpy.empty(10, dtype='f8')
ret = ve_add(x.ve_adr, y.ve_adr, z.ve_adr, z.size, sync=True)
print(z)
[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18.]
ret = ve_sub(x.ve_adr, y.ve_adr, z.ve_adr, z.size, sync=True)
print(z)
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
Figure-6
以上、JITコンパイルの使い方を簡単な例を示しながら紹介しました。大規模データやシミュレーションなどの数値演算処理をベクトルプロセッサにオフロードし、その他の部分はPythonを使ってCPUで処理するという複合的で柔軟な実行環境をぜひお試しください。
次回はJITコンパイル使用時における配列データ属性への参照方法やコールバックについて、さらにk-meansクラスタリング処理の一部をベクトルプロセッサで実行するPythonのサンプルコードを紹介します。
関連リンク
最新資料/カタログはこちらからダウンロード
ご紹介資料、各種カタログをダウンロードいただけます。
My NEC登録が必要です
My NEC登録が必要です
