VOICEVOX / voicevox_core

無料で使える中品質なテキスト読み上げソフトウェア、VOICEVOXのコア
https://voicevox.hiroshiba.jp/
MIT License
872 stars 116 forks source link

Windows向けONNX RuntimeのビルドでDirectMLとCUDAが一緒くたになっている #804

Open qryxip opened 4 months ago

qryxip commented 4 months ago

不具合の内容

https://github.com/VOICEVOX/voicevox_core/pull/802#issuecomment-2202402961

現象・ログ

割愛

再現手順

割愛

期待動作

VOICEVOXのバージョン

N/A

OSの種類/ディストリ/バージョン

その他

DirectML版とCUDA版を分離する場合、段取りとしてはonnxruntime-builder (ビルドしなおし) → VOICEVOX/ort (リストの更新) → voicevox_core (voicevox-ortの更新)となるはず。

Hiroshiba commented 4 months ago

まあダウンロードサイズは50MBくらいなので、一緒でもいいかもですね・・・!! VOICEVOX/ort的に別れていた方が都合良さそうであれば(本家からの変更が少なくて済みそうであれば)、onnxruntime-builder側を変えてあげる方が綺麗かも?

qryxip commented 4 months ago

300MBは解凍後ですね。

drwx------    - ryo ryo 2024-07-02 17:29 ./voicevox_core-windows-x64-directml-999.999.999
.rw-r--r--  17M ryo ryo 2024-07-02 17:29 ├── onnxruntime.dll
.rw-r--r-- 324M ryo ryo 2024-07-02 17:29 ├── onnxruntime_providers_cuda.dll
.rw-r--r--  11k ryo ryo 2024-07-02 17:29 ├── onnxruntime_providers_shared.dll
.rw-r--r--  11k ryo ryo 2024-07-02 17:29 ├── README.txt
.rw-r--r--   12 ryo ryo 2024-07-02 17:29 ├── VERSION
.rw-r--r-- 3.6M ryo ryo 2024-07-02 17:29 ├── voicevox_core.dll
.rw-r--r--  54k ryo ryo 2024-07-02 17:29 ├── voicevox_core.h
.rw-r--r--  18k ryo ryo 2024-07-02 17:29 └── voicevox_core.lib

microsoft/onnxruntimeだとCPU版とDirectML版とCUDA版を分けてて、pykeio/ortだとCPU&DirectML版とCUDA版という分け方にしてますね。

正直onnxruntime_providers_cuda.dllみたいなのが入ってくるのは見通しが悪くなるし混乱もしそうなので、microsoft/onnxruntime式か、あるいはpykeio/ort式でもいいんじゃないか?と思っています。

あとsupported_devices{ "cuda": true, "dml": true }を返すというのもありますが、まあこれは #802 でOnnxruntimeのメソッドになるということもあり「ONNX Runtime (≠ VOICEVOX CORE)が対応しているデバイス情報」というドキュメント/仕様にしてしまってもよいかなと思ってます。 (SupportedDevices::THISstd::ops::BitAnd実装を追加して「VOICEVOX COREとONNX RuntimeとでGPU対応がそれぞれ異なる」ということを表明する、といった工夫もできそうです)

Hiroshiba commented 4 months ago

正直onnxruntime_providers_cuda.dllみたいなのが入ってくるのは見通しが悪くなるし混乱もしそうなので、microsoft/onnxruntime式か、あるいはpykeio/ort式でもいいんじゃないか?と思っています。

良いと思います!どっちの仕様にしようか迷いますねぇ。 まあonnxruntime式(全部バラバラ)のが便利そうではありますが・・・。


supported_devicesの仕様は・・・あれ、今ってcudaとdml別々に返してないんでしたっけ。 だったら{ "cuda": true, "dml": true }的なのを返したほうが便利だと感じますね!!

qryxip commented 4 months ago

気付いたのですがpykeio/ort式だと静的リンクなのに対し、DirectML版の動的リンクだとDirectML.dllの管理を別途しなければならないですね。なのでCPU版とDirectML版を一緒にしてしまうと、CPU版も余計な手間が一つ増えることに…


(SupportedDevices::THISstd::ops::BitAnd実装を追加して「VOICEVOX COREとONNX RuntimeとでGPU対応がそれぞれ異なる」ということを表明する、といった工夫もできそうです)

これの補足ですが、こんな感じに二つのSupportedDevicesを提供するのはどうかなと考えています。

// 「このVOICEVOX COREのビルドがサポートしているもの」
SupportedDevices::THIS == SupportedDevices { cuda: true, dml: true }
// 「ONNX Runtimeがサポートしているもの」
ort.supported_devices() == Ok(SupportedDevices { cuda: false, dml: true })
// 「このVOICEVOC COREのビルドとONNX Runtimeの両方がサポートしているもの」
SupportedDevices::THIS & ort.supported_devices().unwrap() == SupportedDevices { cuda: false, dml: true}
Hiroshiba commented 4 months ago

気付いたのですがpykeio/ort式だと静的リンクなのに対し、DirectML版の動的リンクだとDirectML.dllの管理を別途しなければならないですね。なのでCPU版とDirectML版を一緒にしてしまうと、CPU版も余計な手間が一つ増えることに…

なーーるほどです!たぁしかに。

これの補足ですが、こんな感じに二つのSupportedDevicesを提供するのはどうかなと考えています。

andでandが取れるのなるほどです。 まーーでも大体の場合で不一致なら環境構築ミスってると思うので、and取りたいユースケースは無さそう…?

…あれ、そもそもボイボコアはGPU版とCPU版で全く同じビルドになる気がしてきました!! onnxruntime.dll入りzipファイルとかは各デバイスそれぞれに必要そうですが。 …いやこっちもダウンローダーがしっかりすれば各アーキテクチャごとに1種でよくなる…? あれ、別に動的読み込みになったから変わったという話ではない気がする。元からビルドは一つで良かった…?

qryxip commented 4 months ago

…あれ、そもそもボイボコアはGPU版とCPU版で全く同じビルドになる気がしてきました!! onnxruntime.dll入りzipファイルとかは各デバイスそれぞれに必要そうですが。 …いやこっちもダウンローダーがしっかりすれば各アーキテクチャごとに1種でよくなる…? あれ、別に動的読み込みになったから変わったという話ではない気がする。元からビルドは一つで良かった…?

確かONNX RuntimeにはCUDA版とかDirectML版でしか生えないAPIがあったはずで、それらを使うためにRust側のコンパイルをconditionalにする必要がある… のですが、 #802 をやった今ならその必要はもう無いですね。

pykeio/ortとしても、実装を見る限りdlopen/LoadLibraryExWモードになった時点でどうやらfeatureに関わらずすべてのexecution providerが利用可能になるっぽい…? (ドキュメントにはそんなことは書かれてませんが)

いずれにせよ少なくともバイナリとしてリリースするWindows版とLinux版はビルドが一種類ずつになりますし、libvoicevox_onnxruntimeを同梱しないのならリリースを分ける必要も無くなりますね。macOS + Swift package + Core MLをやりたいとなったときにまたRust側で分けないといけなくなるくらい?

追記: 今のCargo featureをこうすればよさそう。

まあまずはCORE本体のリリースにONNX Runtimeを含めないようにして、ダウンローダーからダウンロードするようにするところから?

Hiroshiba commented 4 months ago

なるほど!!専用の関数があったりなかったりするんですね。

ビルドするものは少ない方がシンプルで嬉しそうですね! なのてloadで良いものはload版だけビルドでも良さそう。 でも何か予期せぬ問題も起こるかもなので、link-onnxruntime-directml等がすぐ作れるようにはしておきたいかも。 あ。あとエンジンのためにもしばらくは作ってもらえると助かりそうかも…?

qryxip commented 4 months ago

あ。あとエンジンのためにもしばらくは作ってもらえると助かりそうかも…?

ちょっと若干意図が掴めなかったのですが、私の理解ではDLLパス無指定での"load"は"link"と同じ感じで動くはずです。 (なのでcompatible_engineのAPIは変わらないし、環境変数とかを注入する必要も無い)

あとエンジンに組み込むときはlibonnxruntimeもlibvoicevox_coreになる(多分)上に、ONNX Runtime自体のバージョンとかCUDAのバージョンとかも上がり、model/*.binはVVMになっているので、どの道現0.15からはセットアップのしかたが変わると思います。

Hiroshiba commented 4 months ago

あ、たしかにです!!

簡単に設定できること、あとそもそも他の変更要因が大規模なことから、このlink/loadの件は今のところエンジンについて意識から外して良さそうに思いました!

動作確認と需要満たしができたることを確認するまでしばらくどっちも作るのは方針としてありだと思いますが、まあ一気に置き換えでも良いかも。 コードメンテがそんなに大変じゃないならどっちもが良い、大変そうなら片方提供が良さそう、って感じかなと思いました!

qryxip commented 4 months ago

でも何か予期せぬ問題も起こるかもなので、link-onnxruntime-directml等がすぐ作れるようにはしておきたいかも

こっちについては一応、特になんか用意しておく必要はないんじゃないかと思います。

コメントを残すかどうかですが、Rustの経験およびプログラミングの勘と調査能力がある人であれば何も無くても数分で次のような結論に辿り着けるはず…多分。

  1. pykeio/ortはEPに対応するCargo featureがあるらしい
  2. load-onnxruntimeでは↑を利用していない ⇒ "link"の方では必要?
  3. pykeioの実装を見に行ったら#[cfg(any(feature = "load-dynamic", feature = "directml"))]みたいな指定がされている
  4. link-onnxruntime-directml = ["voicevox-ort/directml"]のようにすればよい
Hiroshiba commented 4 months ago

でも何か予期せぬ問題も起こるかもなので、link-onnxruntime-directml等がすぐ作れるようにはしておきたいかも

こっちについては一応、特になんか用意しておく必要はないんじゃないかと思います。

たしかにしっかり確認する工程があれば大丈夫そう! 怖いのはエンジンもビルドしてエディタもビルドした後に、初見殺しで特定の環境でのみなぜか動かなかった場合くらいかなと。あるのかわかりませんが。 設定変えてサクッと変えて数日以内くらいにlinkもビルドできるなら問題なさそう感!

コメントを残すかどうかですが、

どこの話かわからないですが、未来の僕たちのためになりそうだったら残しても良いかも。 と思ったけどdocs辺りにメモ書きがあるんでしたっけ。ならそれで良さそう感。

qryxip commented 4 months ago

featureをload-onnxruntime | link-onnxruntime-cpuにするやつを今やっているのですが、一つ困ったことがありました。今現在ユーザーから指定されるのが以下なので、もし「CUDAもDirectMLも使えるlib(voicevox_)onnxruntime」を提供するとしたらこれに対してAutoとかGpuとかを指定されてもどちらを使えばよいかわかりません。あと #783 をやろうとするとis_gpu_modeもちょっと苦しくなります。

https://github.com/VOICEVOX/voicevox_core/blob/d66a8b0c1422ae7b8b07aca7da18cce988d4b8d6/crates/voicevox_core/src/synthesizer.rs#L47-L57

というのも、EPが実際に使えるかどうかは Sessionを一つ作ってみなくてはわからないんですよね

思い浮かぶ方針は次の四つで、どれにしようか迷っています。

  1. 最初のdecode用Sessionを作る際にCUDAもDirectMLもとりあえず試行してみる。どっちがOKだったのかを記憶しておいて、二度目以降はそちらを選ぶようにする

  2. 何もしないモデルを用意してそれのSessionを作ることで、Synthesizerのコンストラクタの段階でCUDA/DirectMLの利用可能性を判定する

    これなら上記のis_gpu_modeも、Autoを指定したけどCPUモードにフォールバックされるであろう状況に対してfalseを返せます。

    [追記1] 本当に空のモデルはONNX Runtimeに拒否されるのですが、これ(↓)ならいけそう。とりあえずLinuxのCUDAは判別できそうでした。50バイトなので文字列リテラルで埋め込んだ上でprotobuf表現に対する注釈も付けれそう。

    import onnx
    from onnx import TensorProto
    
    i = onnx.helper.make_tensor_value_info("I", TensorProto.BOOL, [])
    o = onnx.helper.make_tensor_value_info("O", TensorProto.BOOL, [])
    node = onnx.helper.make_node("Not", ["I"], ["O"])
    graph = onnx.helper.make_graph([node], "_", [i], [o])
    model = onnx.helper.make_model(graph)

    [追記2] EPの"register"はOrtSessionじゃなくOrtSessionOptionsに対して行うので、ダミーのモデルなんか作る必要は無かった!!!

  3. AccelerationModeを拡張し、具体的なGPUの種類を指定できるようにする

    いっそのことこういう感じでdeivce_idも指定できるようにするのもよいと思います。

    // `AccelerationSpec`とかあるいは単に`Acceleration`という名前にしてもよいかも
    pub enum AccelerationMode {
        Auto, // 上記1.の挙動
        Cpu,
        Dml { device_id: i32 },
        Cuda { device_id: i32 },
    }
    class AccelerationMode:
        AUTO: "AccelerationMode"
        CPU: "AccelerationMode"
    
        def dml(*, device_id: int = 0) -> "AccelerationMode": ..
    
        def cuda(*, device_id: int = 0) -> "AccelerationMode": ..
    #[derive(Clone, Copy)]
    #[repr(C)]
    pub struct VoicevoxAccelerationMode {
        kind: VoicevoxAccelerationModeKind,
        options: VoicevoxAccelerationModeOptions,
    }
    
    #[allow(non_camel_case_types)]
    #[derive(Clone, Copy)]
    #[repr(usize)]
    pub enum VoicevoxAccelerationModeKind {
        VOICEVOX_ACCELERATION_MODE_KIND_AUTO,
        VOICEVOX_ACCELERATION_MODE_KIND_CPU,
        VOICEVOX_ACCELERATION_MODE_KIND_DML,
        VOICEVOX_ACCELERATION_MODE_KIND_CUDA,
    }
    
    #[derive(Clone, Copy)]
    #[repr(C)]
    pub union VoicevoxAccelerationModeOptions {
        none: [u8; 1],
        dml: VoicevoxAccelerationModeDmlOptions,
        cuda: VoicevoxAccelerationModeCudaOptions,
    }
    
    #[derive(Clone, Copy)]
    #[repr(C)]
    pub struct VoicevoxAccelerationModeDmlOptions {
        device_id: i32,
    }
    
    #[derive(Clone, Copy)]
    #[repr(C)]
    pub struct VoicevoxAccelerationModeCudaOptions {
        device_id: i32,
    }
  4. lib(voicevox_)onnxruntimeは一つのバイナリにつき高々一つのEPしか有効化しない。CUDAとDirectMLは分ける。VOICEVOC CORE側もEPが一つであるという前提を持ち、もし複数のEPが有効化されたlibonnxruntimeが読み込まれたらwarningを出しつつEPを一つ選ぶ (e.g. CUDAとDirectMLが両方あるならCUDAにしておく)

    これが現状の挙動に近いのかなと思います。ただDirectMLとCUDAの同時有効化も便利ではあると思うので、個人的には1., 2., 3.のどれかにできればなと思っています。

qryxip commented 4 months ago

↑ ちょっと実装の目算が立ったので、案2. + 案3.で手を付けてみようと思います。

Hiroshiba commented 4 months ago

ちょっと考え切れてないのですが、先々のことを考えると案3が良さそうに思いました! (案2はちょっとよくわかってないというのが本音です)

ユーザーもサードパーティーアプリ開発者もやりたいのは「GPUを使う」なのでdevice.GPUを選べる形が良い気はするのですが、コアは基幹寄りな実装をしていても良いと思うのと、将来なんか細分化されていきそうな雰囲気を感じるためです。 もしかしたらNPUが増えてきて、色々選べるようにしたくなるかもしれない、くらい。 なので案1より案3のが良い・・・・かも・・・?くらいの気持ちです!

あとload-onnxruntimeはEPと思想が同じなので、せっかくだから1種のvoicevox_onnxruntime.dllでいろんなEPを挿せるようにしたいですね! なので案4はできれば後回しにしたい。

qryxip commented 4 months ago

案2.についてですが例として、CUDAが使えないLinuxのPCでCUDA版のコア&エンジンの利用を試みた場合を考えます。

エンジンを--use_gpuで起動すると多分次のようになると思います。

  1. --use_gpuでエンジンが起動
  2. Open JTalkの辞書とONNX Runtime本体が読めさえすればinitialize(use_gpu=True, …)は成功する
  3. モデルをloadしようとするとコアはここで初めてエラーを返し、エンジンの/initialize_speakerは500系を返す (ことになってたような?自信無し)

コアのC APIでも、現状では同じような挙動をします。

  1. Open JTalkの辞書とONNX Runtime本体が読めさえすれば、acceleration_mode=GpuでもSynthesizerは作れる
  2. VVMを読み込もうとすると、ここで初めてエラーが出る

ちなみにacceleration_mode=Autoはどうなのかというと、現状ではvoicevox_coreとlibonnxruntimeをセットで扱う限りacceleration_mode=Gpuと全く変わりません。

これを次のようにする案です。

  1. acceleration_mode=GpuSynthesizerを作ろうとすると、CUDAもDirectMLも使えないという旨のエラーで失敗する。acceleration_mode=Autoの場合でも、同様にちゃんと検査してCPU版にフォールバックする (#783)

利点としては、 #783 でCPU版にフォールバックしたときにis_gpu_modefalseを返せるようになります。


案3. (ユーザーからGPUを選択可能)ですが、エンジン(のようなソフトウェア)から使うことを考えるとAccelerationModeはもう一種類必要そうですね (compatible_engine専用の内部APIでもいいとは思いますが)。

pub enum AccelerationMode {
    Auto,
    AnyGpu, // `Auto`のようにGPUを試行するが、CPU版にフォールバックせずにエラー
    Cpu,
    Dml { device_id: i32 },
    Cuda { device_id: i32 },
}
Hiroshiba commented 4 months ago

@qryxip なーーーーーーるほどです!!! ちょっとまだ一部わかってないかもなのですが、案2は良さそうだと感じました!! よほどしんどいことにならない限り、実装するのが良いかなと思います!

案3のAnyGpuもなるほどです!こちらもあると嬉しそうです! Enumの名前はGpuでも良いかも。やるならAutoGpuかも? (AnyGpuにするとAuto→Anyにしたくなってしまいそう。・・・・・・・ちょっと自信ないですが・・・!!!)