VOICEVOX用にONNXRuntime（DirectML）を改造してGPUを選択できるようにしたい

# モチベ

VOICEVOX_COREの中で使用するGPUは0番目に固定されています
Multi-GPU環境とかだとVOICEVOXで使用するGPUが選べると便利だとおもいませんか？
VOICEVOX_COREのビルドは現状ヒホ氏しかできないので、ONNXRuntimeのほうを改造します
※いずれGPUを選択できるAPIが実装されるかもしれませんが暫定対応ということで

# 調査

onnxruntime-rsの session.rs を見る
↓
with_append_execution_provider_directml 関数定義を見る
↓
onnxruntime.dll の OrtSessionOptionsAppendExecutionProvider_DML コールされている
↓
なんか２番目 device_id がGPUを指定する番号っぽい！

```rust
//https://github.com/VOICEVOX/onnxruntime-rs/blob/master/onnxruntime/src/session.rsより抜粋

mod directml {
    use super::*;

    impl<'a> SessionBuilder<'a> {
        pub fn with_append_execution_provider_directml(
            self,
            device_id: usize,
        ) -> Result<SessionBuilder<'a>> {
            let status = unsafe {
                sys::OrtSessionOptionsAppendExecutionProvider_DML(
                    self.session_options_ptr,
                    device_id as ::std::os::raw::c_int,
                )
            };
            status_to_result(status).map_err(OrtError::SessionOptions)?;
            Ok(self)
        }
    }
}
```

ちなみにVOICEVOX同封の onnxruntime.dll はv1.13.1っぽい
念のため改造するときはこのバージョンを使う

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/388f7df7-393d-3947-1785-890cbd711402.png)


# とりあえずビルドしてみる

## ひつようなもの

 - Visual Studio 2019
※C++によるデスクトップ開発が入っていればOK
※ONNX Runtimeの比較的新しいバージョンを試したいって人はVisual Studio 2022のほうがいいかも
※極力不要なものはインストールしたくない人はBuild Toolsだけで行けるかも

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/42134fed-00b6-c2d5-c116-e1aaa56d723d.png)

 -  Git
 -  CMake
 
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/ab39fa02-d640-6166-9c92-2e4cb2bcedb9.png)

 -  Python3※python3.12は上手くいかないかもしれない

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/c7dd0375-e658-b38a-9862-d060a2e90792.png)

## ビルド

コマンドプロンプトで下記を実行
```bat
git clone --recursive https://github.com/microsoft/onnxruntime.git -b v1.13.1
cd onnxruntime
build.bat --cmake_extra_defines CMAKE_SYSTEM_NAME=Windows CMAKE_SYSTEM_PROCESSOR=x86_64 --config Release --parallel --update --build --build_shared_lib --use_dml
```
ビルドは通った！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/9cfbd9dd-42e7-966e-6b78-3783783e74ed.png)



# 動作確認（結果は失敗）

onnxruntime\build\Windows\Release\Release に
 - onnxruntime.dll
 - DirectML.dll
 
があるので、VOICEVOXのインストールパスにオリジナルのDLLを退避しつつ差し替える

差し替えた後VOICEVOXを起動してみたのだが上手くいかなかった

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/45153151-7891-40ee-f665-c83fdc736f5b.png)

エラーメッセージを見るために、ターミナルから起動してみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/f0d8aff3-bf7c-0fd0-f853-d13b53060e9d.png)

# 原因調査

Dependencies でオリジナルと今回ビルドした onnxruntime.dll を比較してみる

今回ビルドしたやつ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/b3a44aca-1349-da1f-a451-ed6993ca1d07.png)

同封されていたオリジナル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/283996e1-7ea2-3f3f-1bf4-75248727dd58.png)

比較のために並べる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/881bc5cc-4ced-2446-a5ae-a09c9ccc26a5.png)

OrtGetWinMLAdapter なる関数が足りてない・・・
~~RustからC++のDLLを読み込むメカニズムはよくわかってないけど、これが原因？~~
Rustとか関係なく序数による関数のexportによる方法があることを知りませんでした…
序数がずれたらそりゃ読み込めんですな

# OrtGetWinMLAdapterについて少し調べる

OrtGetWinMLAdapter だが onnxruntime\winml\adapter の winml_adapter_c_api.cpp に定義されてる
```c++
//https://github.com/microsoft/onnxruntime/blob/main/winml/adapter/winml_adapter_c_api.cppより抜粋
const WinmlAdapterApi* ORT_API_CALL OrtGetWinMLAdapter(_In_ uint32_t ort_version) NO_EXCEPTION {
  if (ort_version >= 2) {
    return &winml_adapter_api_1;
  }

  return nullptr;
}
```

どうやら、WinML（Windows Machine Learning ）関係の関数っぽい

~~build.bat のオプションに --use_winml があるのだが、これを指定するとどうやってもビルドが通らない~~
~~いろいろやってみたが・・・無理だったのであきらめた~~
~~そもそもVOICEVOX側でこの関数使ってるのか？~~
~~うん、たぶん使ってない！~~
~~よしダミー関数でもでっち上げよう！~~

2024/2/2
※後述しますがbuildオプションを追加したらいけました・・・

# 再ビルド（ボツ案）
2024/2/2
※この方法は邪道かつ力技なので、無視してください
※とはいっても、今後役に立つこともあるかもしれないので備忘として

いくつかファイルに修正をいれる

onnxruntime\onnxruntime\core\providers\cpu\symbols.txt を下記のように修正
```
OrtGetApiBase
OrtSessionOptionsAppendExecutionProvider_CPU
```
↓
```
OrtGetApiBase
OrtGetWinMLAdapter
OrtSessionOptionsAppendExecutionProvider_CPU
```

onnxruntime\core\session\provider_bridge_ort.cc の末尾に下記を追加

```C++
//nullptrを返すだけのOrtGetWinMLAdapter関数をでっち上げる
struct WinmlAdapterApi;
typedef struct WinmlAdapterApi WinmlAdapterApi;

const WinmlAdapterApi* ORT_API_CALL OrtGetWinMLAdapter(_In_ uint32_t ort_version) NO_EXCEPTION {
  return nullptr;
}
```

準備が整ったので再びビルドする

# 再ビルド（正攻法）※2024/2/2 追記

build.batのオプションに --enable_wcos --use_winml を追加するだけでよかった・・・
```bat
build.bat --cmake_extra_defines CMAKE_SYSTEM_NAME=Windows CMAKE_SYSTEM_PROCESSOR=x86_64 --config Release --parallel --update --build --build_shared_lib --use_dml --enable_wcos --use_winml
```


# 動作確認

まずは、Dependencies で確認してみる・・・大丈夫そう！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/e3902957-2e30-4681-6d0c-32e240a629b6.png)

VOICEVOXも・・・起動できた！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/805fec75-12e7-8b59-5378-15aa30d93632.png)

# GPUを選択できるよう修正して再ビルド

お手軽に環境変数でコントロールしようかと
OrtSessionOptionsAppendExecutionProvider_DML は下記の部分ですね
```c++
//https://github.com/microsoft/onnxruntime/blob/6d7ac9c93ae7cd1f979b35e7f6d7af207962cd99/onnxruntime/core/providers/dml/dml_provider_factory.ccより抜粋
ORT_API_STATUS_IMPL(OrtSessionOptionsAppendExecutionProvider_DML, _In_ OrtSessionOptions* options, int device_id) {
API_IMPL_BEGIN
  options->provider_factories.push_back(onnxruntime::DMLProviderFactoryCreator::Create(device_id));
API_IMPL_END
  return nullptr;
}
```

これをこんな感じに変えてみます
```c++
#include <charconv>
ORT_API_STATUS_IMPL(OrtSessionOptionsAppendExecutionProvider_DML, _In_ OrtSessionOptions* options, int device_id) {
API_IMPL_BEGIN
  std::size_t len;
  char buff[1024];

  if(!getenv_s(&len, buff, sizeof(buff), "ONNX_DML_DEVICE")){
    if(len > 0 && buff[len - 1] == '\0') len--;

    auto first = buff;
    auto last = buff + len;

    decltype(device_id) val;

    auto [ptr, ec] = std::from_chars(first, last, val);

    if(ec == std::errc{} && ptr == last){
      device_id = val;
    }
  }

  options->provider_factories.push_back(onnxruntime::DMLProviderFactoryCreator::Create(device_id));
API_IMPL_END
  return nullptr;
}
```

再ビルドして、DLLを差し替えます

# 動作確認（最終）
うちの環境では、０番目に内蔵GPU、１番目に外付けGPUという環境です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124801/00219322-3dc6-536c-ad37-d59935bcf970.png)

VOICEVOXのエディターのほうではよく確認できなかったので自作のDiscord TTS-Botで確認します。
https://qiita.com/kmatsumoto630823/items/6bc0e1f85be27e026849

まずVOICEVOXエンジンを普通に起動します
```bat
"C:\Program Files\VOICEVOX.\run.exe"  --use_gpu
```
内蔵GPUが貧弱すぎてTTS-Botはうんともすんとも言いません

環境変数をセットして起動します
```bat
set ONNX_DML_DEVICE=1
"C:\Program Files\VOICEVOX.\run.exe"  --use_gpu
```
TTS-Botから声が出るようになりました！

# 最後に

いずれはVOICEVOXにもGPUを選択できるようなAPIが実装されるかもしれませんが
それまではこの暫定対応で行こうかと思います！