CUDA実装のONNXRuntimeカスタムオペレータを実装してみた

前回の続き。

前回の記事 ↓ maminus.hatenadiary.org

今回のソースコードgithub.com

ありがたいことにGitHubのIssueでCUDA版のカスタムオペレータ実装方法について問い合わせをいただいたので時間ができた時に実装してみた。

※ONNXRuntimeのカスタムオペレータについては前回の記事を参照

ONNXRuntimeのExecution Providerについて

CUDA版のカスタムオペレータを実装する場合、一応以下の2パターン方法が考えられる。

  1. カスタムオペレータだけCUDAで実行し、他のオペレータはCPUで実行する
  2. すべてCUDAで実行する

1のパターンはあまりうれしくないと思うので2のカスタムオペレータも他のオペレータもCUDAで動かすことを考える。

この時デフォルトだとONNXRuntimeはCPUで動くのでCUDA版のExecution Providerというのを指定して動かす必要がある。

Execution ProviderというのはONNXRuntime内のフレームワークらしくて、推論処理などのハードウェアアクセラレーションが受けられそうな箇所を切り替え可能な機能ブロックとして分離しているらしい。

詳細は以下の公式ドキュメントを参照のこと。 onnxruntime.ai

そして、CUDAExecutionProviderを使うと推論処理をCUDA版で実行できるらしい。

CUDA版のカスタムオペレータを実装する時にはCUDAExecutionProvider版のカスタムオペレータとして実装すれば全体がCUDAで実行できることになる。

CUDAExecutionProviderでカスタムオペレータを実装する際のポイント

少しだけ前回の復習をしておくとC++でカスタムオペレータを実装する時には主に以下の要素を実装した。

実装すべきもの 主な用途
kernelクラス 計算処理本体
オペレータクラス カスタムオペレータの仕様(引数の個数など)に関する情報を返すクラス
Register関数など ONNXRuntimeへの登録

CUDA版を実装する際には大雑把には以下のような実装内容の違いがある。

実装すべきもの CPU版 CUDA版
kernelクラス 計算処理本体を実装する CUDAホスト関数を呼び出す
オペレータクラス CPUExecutionProviderとして実装する CUDAExecutionProviderとして実装する
Register関数など CPU/CUDAどちらも同じ CPU/CUDAどちらも同じ
CUDAソースコード 実装不要 計算処理本体を実装する

具体的な変更点は以下の通り。

  • kernelクラスのCompute()メソッド
    • GetTensorData()はCUDAのデバイスポインタが返ってくるのでそのままCUDA関数に渡してOK
    • CUDAのStream IDはKernelContext_GetGPUComputeStream()で取得できる(該当コード
  • オペレータクラスのGetExecutionProviderType()メソッド
    • CPU版ではオーバーライド不要
    • 文字列"CUDAExecutionProvider"を返すようにオーバーライドすることでCUDAExecutionProviderになる(該当コード

Pythonの呼び出しコード

Pythonの推論処理コードはInferenceSessionのコンストラクタ引数にExecutionProviderのリストを追加で指定するようにすればOK。

import onnxruntime as ort


model = ...

# カスタムオペレータを実装したDLLをロードする
option = ort.SessionOptions()
option.register_custom_ops_library('./libmy_custom_multi_with_cuda.so')

# 使いたいExecutioinProviderを指定する
providers = ['CUDAExecutionProvider']

# カスタムオペレータDLLとExecutionProviderを指定してセッションを生成する
sess = ort.InferenceSession(model.SerializeToString(), option, providers)

# sess.run()で推論できる
...

お試しコードか上で紹介した公式ドキュメントも参考に。