WebAssembly(Wasm)とは
・WASMはウェブブラウザ(クライアントサイド)で動作する低水準プログラミング言語
・現在はウェブブラウザにとどまらず、サーバー、IoTデバイスなどで動作するようになってきている
・wasmの実行環境がsandbox化されて、メモリ空間が隔離されていたり、ファイルシステムなどのリソースへのアクセスを制限することができる。
・wasm moduleは、ポータビリティが高く、同一のbinaryが異なるCPU アーキテクチャ上で動作させることができる。
・機械学習の分野でもwasm を使うケースが増えてきている、クラウド上で動かしたモデルをwasmのポータビリティを活かして、同じmoduleをエッジ側にdeployするなどが可能になっている。
WebAssemblyが生まれた経緯
・元々ブラウザでは対応できない高度な処理を行うための言語としてJava Scriptが開発された
例: ブラウザ上でのリアルタイムグラフの作成、検索、ソート、ブラウザゲームの実装など
・ 時代と共にJSに求められる機能も増えていったが、JAVAと同様にインタプリタ言語であり、サイズが大きく、CやRustなどの
コンパイル言語と比較して速度が遅い
・ ブラウザがスマートフォンなどの携帯端末に実装されるようになり、低速なCPUでもより実行速度を求められるようになった
・ JSを高速化する方法としてasm.jsが開発されたが、コンパイル時間が長く、データ構造の概念が存在しないなど使用上の
問題が多く、JavaScriptの根本的な問題を解決する方法としてWebAssemblyが提唱される
Yolov3をwasmにビルドする
・Cで書かれたYolov3 を WASI-SDKを使ってwasm module にビルドしてみた。
やったこと①
・wasm module化するためのWASI-SDKのインストール。
https://github.com/WebAssembly/wasi-sdk
export WASI_VERSION=14
export WASI_VERSION_FULL=${WASI_VERSION}.0
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_VERSION}/wasi-sdk-${WASI_VERSION_FULL}-linux.tar.gz
tar xvf wasi-sdk-${WASI_VERSION_FULL}-linux.tar.gz
sudo mkdir /opt/
sudo cp -rf ./wasi-sdk-${WASI_VERSION_FULL} /opt/
やったこと②
・Makefileの修正
コンパイラをgccからwasi-sdkに含まれているclangに変更
-CC=gcc
-CPP=g++
+WASI_SDK_PATH = /opt/wasi-sdk-14.0
+CC=$(WASI_SDK_PATH)/bin/clang --sysroot=$(WASI_SDK_PATH)/share/wasi-sysroot
+CPP=$(WASI_SDK_PATH)/bin/clang++ --sysroot=$(WASI_SDK_PATH)/share/wasi-sysroot
やったこと③
・推論(detector)だけ動かしたかったので、不要な物をbuild対象から外す。
推論に必要なものは、yolo.o と detector.o と darknet.o のみ。
-EXECOBJA=captcha.o lsd.o super.o art.o tag.o cifar.o go.o rnn.o segmenter.o regressor.o classifier.o coco.o yolo.o detector.o nightmare.o instance-segmenter.o darknet.o
+EXECOBJA=yolo.o detector.o darknet.o
やったこと④
・実行時にout of bounds memory accessが起きるので、その修正。
Loading weights from ./yolov3-tiny.weights...Done!
Enter Image Path: ./data/dog.jpg
./data/dog.jpg: Predicted in 47.716545 seconds.
Exception: out of bounds memory access
最大の難所だったのが、使ったyolov3-tinyのweightファイル(https://pjreddie.com/darknet/yolo/) が64bit環境で動かす前提で作られていた。
wasmはまだ32bit 対応しかされてないので、weightファイルのパースに失敗していた。ここを強制的に修正。
--- a/src/parser.c
+++ b/src/parser.c
@@ -1234,7 +1234,8 @@ void load_weights_upto(network *net, char *filename, int start, int cutoff)
fread(&minor, sizeof(int), 1, fp);
fread(&revision, sizeof(int), 1, fp);
if ((major*10 + minor) >= 2 && major < 1000 && minor < 1000){
- fread(net->seen, sizeof(size_t), 1, fp);
+ fread(net->seen, 8, 1, fp);
} else {
int iseen = 0;
fread(&iseen, sizeof(int), 1, fp);
実際にビルドして動作させたコードはこちら
https://github.com/daiki0321/darknet_wasm
buildしたwasmをWeb Assembly Micro Runtime上で実行する
・今回buildしたyolo wasmの実行環境は、Web Assembly Micro Runtimeを使用。
このruntimeをx86_64環境と、ラズパイ(arm64) 用にビルドして、同一のyolo wasmを実行してみる。
Web Assembly Micro Runtimeのビルド方法
・こちらを参考にして、Runtimeのビルド
https://github.com/bytecodealliance/wasm-micro-runtime/blob/main/doc/build_wamr.md#linux
cd product-mini/platforms/linux/
mkdir build
cd build
cmake -DWAMR_BUILD_INTERP=1 -DWAMR_BUILD_FAST_INTERP=1 -DWAMR_BUILD_JIT=0 -DWAMR_BUILD_AOT=1 -DWAMR_BUILD_LIBC_WASI=1 -DWAMR_BUILD_SHARED_MEMORY=1 -DWAMR_BUILD_LIB_PTHREAD=1 ..
make
# iwasm is generated under current directory
ラズパイ用にビルドするときは、cmake時に-DWAMR_BUILD_TARGET=AARCH64を追加する。
Web Assembly Micro Runtime上で実行する。
・--dir を使って、wasm moduleが指定したディレクトリにアクセスすることを許可する。
darknet.wasm 以降のコマンドはもとのyolov3 のコマンドと同じ。
weightsファイルはyolov3の本家のサイトからDownlonad.
$ ./iwasm --dir=cfg --dir=data --dir=. ./darknet.wasm detect cfg/yolov3-tiny.cfg ./yolov3-tiny.weights ./data/dog.jpg
layer filters size input output
0 conv 16 3 x 3 / 1 416 x 416 x 3 -> 416 x 416 x 16 0.150 BFLOPs
1 max 2 x 2 / 2 416 x 416 x 16 -> 208 x 208 x 16
2 conv 32 3 x 3 / 1 208 x 208 x 16 -> 208 x 208 x 32 0.399 BFLOPs
3 max 2 x 2 / 2 208 x 208 x 32 -> 104 x 104 x 32
4 conv 64 3 x 3 / 1 104 x 104 x 32 -> 104 x 104 x 64 0.399 BFLOPs
5 max 2 x 2 / 2 104 x 104 x 64 -> 52 x 52 x 64
6 conv 128 3 x 3 / 1 52 x 52 x 64 -> 52 x 52 x 128 0.399 BFLOPs
7 max 2 x 2 / 2 52 x 52 x 128 -> 26 x 26 x 128
8 conv 256 3 x 3 / 1 26 x 26 x 128 -> 26 x 26 x 256 0.399 BFLOPs
9 max 2 x 2 / 2 26 x 26 x 256 -> 13 x 13 x 256
10 conv 512 3 x 3 / 1 13 x 13 x 256 -> 13 x 13 x 512 0.399 BFLOPs
11 max 2 x 2 / 1 13 x 13 x 512 -> 13 x 13 x 512
12 conv 1024 3 x 3 / 1 13 x 13 x 512 -> 13 x 13 x1024 1.595 BFLOPs
13 conv 256 1 x 1 / 1 13 x 13 x1024 -> 13 x 13 x 256 0.089 BFLOPs
14 conv 512 3 x 3 / 1 13 x 13 x 256 -> 13 x 13 x 512 0.399 BFLOPs
15 conv 255 1 x 1 / 1 13 x 13 x 512 -> 13 x 13 x 255 0.044 BFLOPs
16 yolo
17 route 13
18 conv 128 1 x 1 / 1 13 x 13 x 256 -> 13 x 13 x 128 0.011 BFLOPs
19 upsample 2x 13 x 13 x 128 -> 26 x 26 x 128
20 route 19 8
21 conv 256 3 x 3 / 1 26 x 26 x 384 -> 26 x 26 x 256 1.196 BFLOPs
22 conv 255 1 x 1 / 1 26 x 26 x 256 -> 26 x 26 x 255 0.088 BFLOPs
23 yolo
Loading weights from ./yolov3-tiny.weights...Done!
./data/dog.jpg: Predicted in 48.841929 seconds.
dog: 57%
car: 62%
car: 52%
truck: 56%
bicycle: 59%
・X86_64のubuntuでは、大体50sec かかった
ラズパイにubuntu(22.04) を入れて測ると、963 secもかかった。
さすがにwasmのままだと速度が出てなくて使い物にならない。
JITとAOTコンパイラの速度比較
・Wasmは、AOT (Ahead-of-time) コンパイルができ、事前にWeb Assembly のByte CodeからCPUに応じた機械語にコンパイルしておくことができる。
以下のコマンドでAoTコンパイルする。
https://github.com/bytecodealliance/wasm-micro-runtime/blob/main/doc/build_wasm_app.md#compile-wasm-to-aot-module
./wamr-compiler/wamrc --disable-simd --target=x86_64 -o ../darknet_wasm/darknet.wasm.aot ../darknet_wasm/darknet.wasm
・以下の4パターンで実行時間を計測した。
① JITコンパイラで実行した場合(darknet.wasm)
② AOTコンパイル後に実行した場合(darknet.wasm.aot)
③ ② + yoloの中で行列計算を行う部分のgemm_nnのみをnativeで実行した場合
④ wasmを使わずに、nativeで実行した場合(darknet)
・機械学習モデルをwasm化する場合は、AOTコンパイルは必須で必要になる。
AIモデルの推論は行列計算のような同じコードを何度も通るので、その部分のみをnativeにするか、GPUやアクセラレータにオフロードすれば、wasm化したことによる速度低下はほとんどなく、さらにwasmの良さであるsandboax化やPortabilityの恩恵を受けることができる。