Japan
サイト内の現在位置
NumPy互換数値演算ライブラリNLCPy オーバーヘッド削減のための遅延評価
no.0212022.9.291. NLCPy Lazy Evaluation
SX Aurora TSUBASAの演算処理部はCPU(ベクトルホスト)とベクトルエンジンの2点から構成されます。これら2つのプロセッサはPCI Expressによって接続され、相互にデータ転送が行われます。現在のモデルではPCI Express Gen3が使用されており、以下のように最大8GT/Sの転送速度でベクトルホストとベクトルエンジン間のデータ送受信が行われます。

Pythonスクリプトはベクトルホストにおいて処理されますが、スクリプト中にNLCPy関数が現れるとその関数内の処理はベクトルエンジンにオフロードされます。この時、ベクトルエンジンに転送されるデータサイズが大きく、また高い頻度であるとき、転送に要する時間がスクリプト処理の実行速度に影響を及ぼす恐れがあります。これを避けるために、SX-Aurora TSUBASAではLazy Evaluationと呼ばれる仕組みを用意し、データ転送に伴う処理速度への影響をできるだけ抑える工夫をしています。今回は、このLazy Evaluationを中心に説明を行います。
Figure-1に示した例は、NLCPy関数を使ってaとbを定義した後、c=a+bの処理を行うものです。こうしたNLCPy関数が非常に多数使用されるようなスクリプトでは、出現する度にベクトルホストとベクトルエンジンの間で生じるデータ転送が実行速度に悪影響を及ぼしかねません。これを避けるため、ベクトルエンジンへの計算のリクエストをリクエストスタックと呼ばれる場所へ一時的に保管し、何らかのトリガーを契機に一括してオフロード胥吏します。Figure-1の例ではprint(c)がトリガーとなり、リクエストスタックに保管された処理はフラッシュされて、ベクトルエンジンで処理が行われます。

Figure-1
リクエストスタックへのデータ退避とフラッシュというLazy Evaluationの管理はNLCPyとVEOSが共同して自動的に行います。また、トリガーの発生はベクトルエンジンのメモリにあるデータをベクトルホストが必要になるタイミングです。その他にも、
1. NLCPyの配列データをNumPyの配列データに転送するnlcpy.ndarray.get()の使用
2. if/elifなど条件分岐の中でNLCPyの配列データに対する操作が行われる
3. リクエストスタックに退避された命令が100を超える
4. nlcpy.fft.fft()やnlcpy.linalg.solve()等、Lazy Evaluation対象外の関数の使用
がトリガーとなります。
トリガーに伴うフラッシュの他に、nlcpy.request.flush()を使ってリクエストスタックに溜められた処理を意図的にフラッシュすることも可能です。また、初めからLazy Evaluationにおけるスタックへの退避を行わずに、NLCPyの関数が現れる都度、ベクトルエンジンで処理を行うモードに切り替えるnlcpy.request.set_offload_timing_onthefly()と呼ばれる関数が存在します。次の項でその使用例と目的を説明します。
2. NLCPy on-the-flyモード
逐次ベクトルエンジンに処理実行させるOn-the-flyの説明をFigure-2のサンプルを使って行います。このコードには意図的に異常値データに対する数値演算を実行させる箇所が含まれています。この処理をLazy Evaluationとnlcpy.request.set_offload_timing_onthefly()を使ったon-the-flyの2種類の処理モードで実行して比較します。
Lazy Evaluationではprint(x)が実行されたタイミングで2回のRuntimeWarningが表示されます。これらWarningの原因は先のループ内部で行われた演算のいずれかにあらますが、具体的にどの演算にその原因があるか特定するヒントは示されません。一方、on-the-flyで実行した例では、演算実行直後にループ内部のline4と12で異常値データに対する演算が行われたことが示されます。
import nlcpy as vp
import time, math
a = vp.array([[-1.0, 3.0, 2.0, 5.0],[2.0,-6.0,4.0,7.0],[3.0,4.0,5.0,6.0],[4.0,3.0,2.0,1.0]])
b = vp.array([9.0,-7.0,-6.0,-8.0])
x = vp.zeros(4)
print(a)
[[-1. 3. 2. 5.] [ 2. -6. 4. 7.] [ 3. 4. 5. 6.] [ 4. 3. 2. 1.]]
offload timing 'lazy'による実行¶
vp.request.set_offload_timing_lazy()
for k in range(4-1):
for i in range(k+1, 4):
r = a[i, k] / a[k, k]
b[i] -= r * b[k]
for j in range(k+1, 4):
a[i, j] -= r * a[k, j]
for i in reversed(range(0, 4-1)):
s = 0
for j in range(i+1, 4):
s += a[i, j] * x[j]
x[i] = (b[i] - s) / a[i, i]
print(x)
[nan nan nan 0.]
/usr/local/lib/python3.6/site-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in nlcpy.core.core """Entry point for launching an IPython kernel. /usr/local/lib/python3.6/site-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in nlcpy.core.core """Entry point for launching an IPython kernel.
offload timing 'on-the-fly'による実行¶
vp.request.set_offload_timing_onthefly()
for k in range(4-1):
for i in range(k+1, 4):
r = a[i, k] / a[k, k]
b[i] -= r * b[k]
for j in range(k+1, 4):
a[i, j] -= r * a[k, j]
for i in reversed(range(0, 4-1)):
s = 0
for j in range(i+1, 4):
s += a[i, j] * x[j]
x[i] = (b[i] - s) / a[i, i]
/usr/local/lib/python3.6/site-packages/ipykernel_launcher.py:4: RuntimeWarning: invalid value encountered in nlcpy.core.core after removing the cwd from sys.path. /usr/local/lib/python3.6/site-packages/ipykernel_launcher.py:12: RuntimeWarning: invalid value encountered in nlcpy.core.core if sys.path[0] == '':
print(x)
[nan nan nan 0.]
Figure-2
このように、on-the-flyモードを使用することによって、バグが発生する個所の特定が容易になります。On-the-flyモードを使うほかに、Lazy Evaluationで実行している状態でnlcpy.request.flush()により強制的にベクトルエンジンへのオフロード処理を実行させて、Warning発生個所を明示することも可能です。このやり方にはしかし、命令の追加や、どの演算が疑わしいかあらかじめ予測するといった手間が伴います。 on-the-flyは各処理において逐次データ転送を行うことで数値処理に伴うエラー発生個所の特定を容易にしますが、その代償は処理速度が低下することです。このため、動作検証確認後のスクリプト実行はLazy Evaluation状態で行うようにすることを強く推奨します。両モードにおける速度差は処理内容、処理量に依存しますが、Figure-3は実行時間差の一例です。この場合、'on-the-fly'の0.2秒に対して'lazy'は0.07秒の処理時間となります。
import nlcpy as vp
import time, math
2つのoffload timingを定義¶
timings = [
vp.request.set_offload_timing_onthefly,
vp.request.set_offload_timing_lazy,
]
N = 1000
a = vp.random.rand(1000000).reshape((1000,1000))
b = vp.random.rand(1000)
for t in timings:
t() # set offload timing
print(vp.request.get_offload_timing())
begin = time.time()
for i in range(N):
x= vp.dot(a, b)
end = time.time()
print(x[0])
print("elapsed time =", end - begin, "\n")
current offload timing is 'on-the-fly' 252.64211310711258 elapsed time = 0.20160913467407227 current offload timing is 'lazy' 252.64211310711258 elapsed time = 0.07206296920776367
Figure-3
なおコラムに記載した機能に関しては、将来変更になる可能性があります。変更の際にはコラムにおいて変更点を記載します。
関連リンク
最新資料/カタログはこちらからダウンロード
ご紹介資料、各種カタログをダウンロードいただけます。
My NEC登録が必要です
My NEC登録が必要です
