前回作成したMyChain.nnpを環境構築したRaspberry pi ,Jetson Nanoへ転送させてPythonで推論させてみる。
オーディオ入出力はRaspberry Pi, Jetson Nano向けオーディオ入出力ボードの作成(動作コード例編)で完成したコーデックボードを使用する。
上記ページのボイラープレートコードへ、単純に推論部分を挿入してみると、すでに想定されていたように、推論の時間がかかりすぎ、リアルタイム処理に間に合わない。
リアルタイム処理に耐えられるように、試行錯誤で考えた処理モデルは下記の通り。
NNablaの学習モデルを各コアへ展開し、4並列で推論させて時間を稼ぐ。
出力は処理が終わった順にキューに積み、出力させる。
これを実現するポイントとなるコード部分が下記。
multiprocessingでタスクを振るためには、あらかじめpoolの初期化でNNablaの学習モデルを各コアにロードする重い処理を事前に行っておく必要がある。
そのために、
p = Pool(processes=4 , initializer=init_worker)
このようにして4プロセスをinit_workerで初期化する。
def init_worker():
import nnabla as nn
import nnabla.functions as F
import nnabla.parametric_functions as PF
from nnabla.utils.nnp_graph import NnpLoader
nnp = NnpLoader("MyChain.nnp")
net = nnp.get_network("MyChain", batch_size)
global xx
global yy
xx = net.inputs['xx']
yy = net.outputs['yy']
関数内でNNablaをインポートするトリッキーなやり方だが、上記で、各コアへモデルロードができた。
sounddeviceのコールバックでは
def callback(in_data, out_data, frames, time, status):
try:
res= p.apply_async(processing, args=(in_buffer, q_out ,)) # Create new process
res.get(timeout=0)
except TimeoutError as err:
pass
in_buffer = np.zeros(0, np.float32) # Empty the input buffer
上記でapply_asyncで並列にタスクを投げ、結果は関数に戻すのではなくキューに積むため、返り値確認処理はいらないので、TimeoutError をpassさせる。
各コアで行われる処理は、
def processing(indata, q ):
x = sliding_window(padded, input_timesteps, output_timesteps)
x = x[:, :, np.newaxis]
xx.d = x
yy.forward()
time.sleep(0.4)
q.put(yy.d[:, -output_timesteps:, :].reshape(-1)[:d_len])
スライディングウインドウを適用し、推論させ、キューに積む作業を行う。
メインでは、ストリームが安定して出せるまでのゼロフィルのキューを積んでおく。
これで、multiprocessingのCopy On Write処理でストリームが開始された後想定外の処理が走らないようにする意味もある。
def main():
for i in range(4):
prefill = np.zeros(block_size, np.float32)
p.apply(processing, args=(prefill, q_out ,))
try:
with sd.Stream(device=1,
samplerate=RATE, blocksize=CHUNK,
dtype=np.float32,
channels=1,
callback=callback,
prime_output_buffers_using_stream_callback=True):
print('#' * 80)
print('press Return to quit')
print('#' * 80)
print(" ")
input()
except KeyboardInterrupt:
parser.exit('')
p.close()
except Exception as e:
parser.exit(type(e).__name__ + ': ' + str(e))
余談だが、multiprocessingはLinuxでは標準はforkモード動作なので、Windows,Macの場合、動作モードを変更する必要があるらしい。
コンテキストと開始方式
プラットフォームにもよりますが、multiprocessing はプロセスを開始するために 3 つの方法をサポートしています。それら 開始方式 は以下のとおりです
spawn
親プロセスは新たに python インタープリタープロセスを開始します。子プロセスはプロセスオブジェクトの run() メソッドの実行に必要なリソースのみ継承します。特に、親プロセスからの不要なファイル記述子とハンドルは継承されません。この方式を使用したプロセスの開始は fork や forkserver に比べ遅くなります。
Unix と Windows で利用可能。Windows と macOS でのデフォルト。
fork
親プロセスは os.fork() を使用して Python インタープリターをフォークします。子プロセスはそれが開始されるとき、事実上親プロセスと同一になります。親プロセスのリソースはすべて子プロセスに継承されます。マルチスレッドプロセスのフォークは安全性に問題があることに注意してください。
Unix でのみ利用可能。Unix でのデフォルト。
forkserver
プログラムを開始するとき forkserver 方式を選択した場合、サーバープロセスが開始されます。それ以降、新しいプロセスが必要になったときはいつでも、親プロセスはサーバーに接続し、新しいプロセスのフォークを要求します。フォークサーバープロセスはシングルスレッドなので os.fork() の使用に関しても安全です。不要なリソースは継承されません。
Unix パイプを経由したファイル記述子の受け渡しをサポートする Unix で利用可能。
バージョン 3.8 で変更: macOS では、 spawn 開始方式がデフォルトになりました。 fork 開始方法は、サブプロセスのクラッシュを引き起こす可能性があるため、安全ではありません。 bpo-33725 を参照。
バージョン 3.4 で変更: すべての Unix プラットフォームで spawn が、一部のプラットフォームで forkserver が追加されました。Windows では親プロセスの継承可能な全ハンドルが子プロセスに継承されることがなくなりました。
この結果、Jetson Nanoでは、キュー余り3の余裕の動作を行い、リアルタイム推論が行えた。
その際のtopによるシステム負荷状況。
top - 22:55:28 up 25 min, 2 users, load average: 1.14, 1.16, 0.81
Tasks: 280 total, 2 running, 278 sleeping, 0 stopped, 0 zombie
%Cpu(s): 32.4 us, 3.3 sy, 0.0 ni, 63.7 id, 0.0 wa, 0.3 hi, 0.2 si, 0.0 st
KiB Mem : 4059472 total, 1346492 free, 1813648 used, 899332 buff/cache
KiB Swap: 2029728 total, 2029728 free, 0 used. 2069116 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8670 kmwebnet 20 0 329388 217208 26360 S 29.0 5.4 2:28.37 python3
8671 kmwebnet 20 0 329384 216988 26360 S 25.1 5.3 2:28.03 python3
8672 kmwebnet 20 0 329384 216984 26360 R 41.9 5.3 2:28.66 python3
8673 kmwebnet 20 0 329384 216972 26360 S 25.4 5.3 2:29.18 python3
各CPU負荷30%くらい、メモリは各コア217MBくらいの動作。
せっかくのGPUを使えていないが、動作としては安定している。
Raspberry pi 3B+では、スワップファイルへの書き出しが起こるとリアルタイム処理に影響が出るのでスワップファイルを無効にした。メモリも1GBでぎりぎりなので以下の処理を行った。
システムの状態をtopで確認。
top - 16:50:11 up 20 min, 2 users, load average: 3.00, 2.93, 2.04
Tasks: 110 total, 3 running, 107 sleeping, 0 stopped, 0 zombie
%Cpu(s): 73.6 us, 0.3 sy, 0.0 ni, 26.1 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 975.6 total, 223.3 free, 667.3 used, 85.0 buff/cache
MiB Swap: 2048.0 total, 2044.5 free, 3.5 used. 253.8 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
655 pi 20 0 219980 180608 23788 S 61.5 18.1 12:16.52 python3
658 pi 20 0 219724 180332 23764 R 76.4 18.1 12:17.68 python3
656 pi 20 0 219724 180012 23448 R 77.4 18.0 12:20.02 python3
657 pi 20 0 219468 179764 23460 S 75.7 18.0 12:20.11 python3
652 pi 20 0 85144 25956 13892 S 2.3 2.6 0:25.19 python3
662 pi 20 0 83804 18100 5660 S 1.3 1.8 0:12.00 python3
これでq_size 1で回った。
各CPU負荷77%くらい、メモリは各コア180MBくらいの動作。
CPUは結構熱くなる。
おそらくRaspberry pi4 2GB以上のメモリ環境が望ましいだろう。
別のJetson Nanoからテストサウンドを入力させ、また別のJetson Nanoで録音し、エフェクトのかかり具合を確認した。念のため、Cakewalk上で通常通りプラグインに通してエフェクトをかけたものをエクスポートし、比較用に用意した。
エフェクト前 strat-riff_115bpm_E_minor.wav
エフェクト後 strat-riff_115bpm_E_minor-effected.wav
Cakewalkでエフェクトをかけてエクスポート strat-riff_115bpm_E_minor-cakewalk-effected.wav
※サンプルの作り方が間違っていたので更新(3/17)
##考察
学習データの作成とRaspberry pi、Jetson Nanoへの持ち込みを行い、実装できた。
LSTM中間層セルの数、タイムステップ、バッチサイズ等チューニングの結果がnnpファイルの容量、推論環境へ要求するマシンスペックへどのくらい反映するかの基準を得ることもできた。
組込み向けにはC++や、Cでの実装が必要と想定していたが、Raspberry pi、Jetson NanoではPython 環境のほうが見通しがよく、アーキテクチャも構築しやすいと感じた。
応用として、音声信号の環境に合わせたノイズリダクション等が考えられそうだ。
全体のコードはgithubにて公開中。