カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

ブラウザで“サクサク”AI推論!!― Wasm × C++ による画像処理

こんにちは、Kaminashi StatHackカンパニーの渡邉健です。

初めてエンジニアブログを書いてみます。

以下のリンクに普段の取り組みを書いているので興味があれば見てみてください。

AI というテクノロジーを現場 SaaS でどう実現するか 〜AI チームのアプリケーションエンジニアに話を聞いてみた〜|カミナシnote編集部

1. StatHackのこれまでの画像処理の取り組み

StatHack カンパニーでは、現場作業を楽にする AI 画像処理 をいくつか開発してきました。

たとえばスマホで鉄筋束を撮影し、クラウド側で本数を自動カウントする Web アプリなどです。

JFE条鋼がAI個数検査システム『カミナシ CountAI』を導入し、鋼材の員数確認作業時間を約1/10に効率化

従来アーキテクチャ

私たちの今まで利用していたアプリは以下の手順で動いていました。

  1. 端末で写真を撮影
  2. 画像をサーバへアップロード
  3. GPU サーバが推論
  4. 結果を返却 → UI に表示

サーバにアップロードする方式

メリット

  • サーバに強力なGPU を載せられる
  • 新しいモデルをデプロイしやすい

デメリット

  • アップロード/レスポンス待ちで UX が悪化
  • GPU インスタンス維持費が高い
  • 工場によっては回線が遅く「そもそも使えない」

2. 推論を端末でやっちゃおう!

というわけで、色々デメリットがあります。

みんな薄々思っていたのですが、

「推論を全部端末で行っちゃえば神じゃね?」

通信コストも待ち時間もほぼゼロにできれば UX は劇的に改善します。

今回色々それができちゃいました。

しかし

JavaScript だけでは性能もライブラリも足りない

――ここが最初の壁でした。

今回の画像処理では、最終的に推論の速度があまりに早くなった(~200ms)ので、Webカメラで動画を撮りながら、推論をし続ける構成にしています!

3. WebAssemblyによる推論

最終的な構成にたどり着くまで紆余曲折をへたのですが、試行錯誤Logを抜粋します。

3-1. OpenCV.js とその挫折

最初は OpenCV.js を活用しましたが、

行列を大量に管理 → メモリリーク → 連続撮影でクラッシュ

という悪循環に陥りました。

具体的にはOpenCV.jsではガベージコレクションがないので、推論の際に定義するすべての行列の変数で、明示的にメモリを解放しないといけず、それを忘れるとどんどんメモリが圧迫していきます。

今回は連続撮影を行う都合上、一つメモリを解放し損ねると、メモリが「いつかオーバーフローする」みたいなことが発生します。

以下に図を示していて、何度も割り当て続けると、たとえ一つがちっさくても連続撮影を続けていると、いつかoverflowが起きてしまうわけです。

OpenCV.js

最終的にすべての行列を一元管理するアプローチを採用して解決はできたのですが、全体的にWebの世界からWasmの呼び出しが多すぎて実行速度が思ったより伸びませんでした。

3-2 推論のすべてC++でWasmにしちゃおう

今まではOpenCV.jsを活用してWasmの世界の処理を何度も呼び出していましたが、本来、推論の処理をすべて一発の呼び出しで終わらせて結果が返せれば最高ですよね。

それを達成するべく、画像処理やAIによる推論をすべてC++で書いてしまって、Wasmにコンパイルしました。

それによって、推論のたびにメモリを割り当てるのではなく、Wasmで最初に確保したメモリ領域メモリに上書きし続けることも可能になります。

これによって、解放し損ねたとしても、一回のメモリ割り当てですみ、重大な問題にはならなくなっています。

C++によるWasm

4. 開発体制の分離による問題から全体最適へ

当初は

  • Wasm 開発チーム (C++)
  • 呼び出し側チーム (TS / React)

で分業していたため、"とりあえず動く" 実装が乱立し、Wasmを不必要にダウンロードしてしまったり、useEffect が多段にネスト → デバッグ地獄になっていました。

ここでチームで議論して以下のアプローチを取りました。

解決アプローチ

  1. 推論における状態遷移図 (オートマトン) を全員で書き出し
  2. 同時実行できる処理と依存関係を整理
  3. 必要最小限の State管理に絞り込み
  4. Wasmを一回Loadingして使いまわせるように

いままで乱立していた”とりあえず動く”実装を撲滅することができ、結果として

  • 検査開始までの速度を 10 倍
  • 検査 1 回あたりの所要時間を 2 倍短縮

という大幅な高速化を達成しました。

最終的な状態遷移図は以下のようになっています。

副次的なメリットとして、状態遷移図を書いたことで、エラー処理への対応が非常にしやすくなりました。

Wasm利用の状態遷移図

5. Wasmの軽量化・高速化Tips

今回のWasmをブラウザがダウンロードする必要があるのですが,

1GByteのようにサイズが大きい場合、今回のWasmを入れても本末転倒です。

最終的にいろんな工夫を施して9MByteくらいにおさまったのですが、そこに至るまでのTipsをいくつか書いておきます。

軽量化

  • Wasmファイルをgzipで圧縮 (15MByte → 9Mbyte)くらいに

読み込みの高速化

  • HTTPのCache Control headerの付与による再ダウンロードの防止
  • Wasmを使う前にLazy Loading
    • Wasmを使って推論するまで、作業員の人はアプリを開いて幾つかの手順を踏むのですが、その裏でダウンロードが走るように

実行の高速化

  • SIMD の利用
  • コンパイラによる最適化(最適化フラグ、不要コードの自動除去とそれに伴うロード時間短縮)
  • メモリ利用の最適化(オブジェクトの不要なコピーを回避)

6. まとめ

  • JSの限界を Wasm × C++ で突破し、クライアント推論を実現
  • メモリリークや巨大バイナリ問題は 自前実装+キャッシュ戦略 で解決

実は、まだまだ早くなるTipsは残していて、自分たちの冒険はまだ続きそうです。

  • フロントを Web Worker / Service Worker 前提に組み直し、UX を大幅改善
  • マルチスレッド&WebGPU ― ブラウザで完結する AI をもっと速く!

最後に、宣伝ですが、カミナシでは絶賛採用募集中です!