VOICEVOX / voicevox_engine

無料で使える中品質なテキスト読み上げソフトウェア、VOICEVOXの音声合成エンジン
https://voicevox.hiroshiba.jp/
Other
1.28k stars 194 forks source link

`/speaker_info`と`/singer_info`の高速化 #1129

Closed sabonerune closed 2 months ago

sabonerune commented 5 months ago

内容

VOICEVOXエディタのデータ準備中の待機時間が話者追加に伴って増加傾向にあります。 この原因の一つに/speaker_info/singer_infoの取得に時間がかかっている点があると思います。

ref: https://github.com/VOICEVOX/voicevox_engine/pull/1073#issuecomment-1963998561 このコメントのBlockingのURL、Load9118からLoad9952までが/speaker_info/singer_infoの取得処理だと思います。

Pros 良くなる点

エディタの起動速度改善

Cons 悪くなる点

コードの複雑化やそれに伴うバグ

実現方法

あとそもそもの処理速度が上がれば他の方法でもいいのですが…

その他

Load9118からLoad9952Waiting for socket threadが伸びているのはHTTP1.1の同時接続制限によるものだと思います。 しかしFirefoxの設定から強引に同時接続数を増やしても速度がほぼ改善しなかったことから接続数の方は大きなボトルネックではないように思います。(そもそもこの制限を回避することは非常に困難だと思います)

tarepan commented 5 months ago

VOICEVOXエディタのデータ準備中の待機時間が話者追加に伴って増加傾向

👍
明確な UX 上の問題点だと認識しました。


このコメントのBlockingのURL、Load9118からLoad9952までが/speaker_info/singer_infoの取得処理

プロファイルにリクエストURLがあれば確定できるんですが、このプロファイルには無さそうですね。
エディタの実動作を知らないのですが、Load9118 ~ Load9947 で30リクエストが同時発行されています。現在の speaker 数は 30 キャラなので、同じ数です。
なのでここのリクエスト実体は:

と解釈するのが妥当そうに見えます(本当はエディタコードおよびプロファイル中の URL を確認すべき)。


HTTP1.1の同時接続制限

👍
正しい解釈に見えます。
30 の同時リクエストのうち、Load9118 ~ Load9123 の 6 リクエストが同時に実通信を開始しています。HTTP1.1 の同時接続上限は 6 であり、同じ数です。


Firefoxの設定から強引に同時接続数を増やしても速度がほぼ改善しなかったことから接続数の方は大きなボトルネックではないように思います。

この解釈はあり得る一方、早計な気もします。

GET /speaker_info の内部処理は話者フィルタリング+ファイルロードであり、話者依存性が小さいです。
Load9118 は 0.371 秒でリクエスト完了しているため、他話者であっても 1 秒はかからないと想定されます。
なので 30 の同時リクエストが parallel に処理されれば、30リクエスト全てが 1 秒以内に処理されうるはずです。

しかし同時接続設定をいじってもこうならなかったわけで、その解釈は以下の2通りに絞られそうです:

前者の可能性は https://github.com/VOICEVOX/voicevox_engine/pull/1073#issuecomment-1963998561 と同様のプロファイルを取れば排除できるはずです。

@sabonerune
このプロファイルを取るのはかなり手間でしょうか?
私はフロントエンドのプロファイリング実務がわからないので、手間が評価できず…🥺

sabonerune commented 5 months ago

とりあえずFirefoxのnetwork.http.max-persistent-connections-per-serverの値を6にしたものと1024にしたものを共有します。

同時接続数1024のものはWaiting for socket threadが非常に短くなりますがHTTP request and waiting for responseがその分伸びています。 結果、パフォーマンスはほぼ改善していないという感じです。


そもそもこの制限を回避するのはElectronでlocalhostのHTTPサーバーにリクエストを送るというVOICEVOXの構成上以下の点で非常に難しいと思います。

tarepan commented 5 months ago

検証ありがとうございます!


この制限を回避するのは ... 非常に難しい

👍
同意です。素の HTTP 1.1 を維持するのは必須要件と考えます。
あくまで原因切り分けのために必要、という整理ですね。


同時接続数1024のものはWaiting for socket threadが非常に短くなります

👍
「Firefox 設定が上手く効いていない」は明確に No ですね。


HTTP request and waiting for responseがその分伸びています。
結果、パフォーマンスはほぼ改善していない

👍
これは ENGINE 内部の並列性に問題有りですね。

キャッシュは有効な対症療法になると思います。
ただキャッシュは設計上の考慮事項をドンドン増やしていくので、根本原因の解決ができるならそっちを優先したいです。

ENGINE が同時リクエストを受けるフローで並列性/並行性が問題になりそうな箇所だと:

あたりが怪しそうです。
@sabonerune さんは他に怪しそうな要素思い当たりますか?

sabonerune commented 5 months ago

他にあるとしたらシリアライズ周りでしょうか? PythonはブロッキングIOはマルチスレッドの恩恵を受ける一方、CPUバウンドの処理はGILによって恩恵を受けることができません。

レスポンスのサイズと速度になんとなく相関関係があるような感じがあるので…

tarepan commented 5 months ago

シリアライズ ... レスポンスのサイズと速度になんとなく相関関係があるような感じ

単純に処理が重いということですね。全然有り得そうです。
その場合の対応策はキャッシュ系かマルチプロセス化くらいですかねぇ。

現時点での文献調査結果

FastAPI server モデル

デフォルトだとシングルプロセス・マルチスレッド式(スレッドプール)。
高度な設定としてマルチプロセスが可能(docs)。

ENGINE mutex

1回の GET /speaker_info で 1 回の CoreAdapter.speakers アクセス。
.speaker は mutex を利用していない
よって影響はない。

話者情報ファイル I/O 性能上限

FastAPI はスレッドプールで並行処理を提供し、GIL は I/O アクセス時に開放される。
よって

となるため、OS には複数スレッドから I/O syscall が発行されうる。
ゆえに最悪ケースだと 6 ファイルの I/O が OS に要求されるため、性能上限に引っかかる可能性がある。
すなわち ENGINE として I/O バウンドになりうる。

並行処理の同期 I/O 待ち

Python GIL は I/O アクセス時に開放される。ゆえにスレッド間で I/O ブロッキングは起きない。

tarepan commented 5 months ago

現段階だと有り得そうな仮説は次のいずれかになりそうです:

後者は speaker_info() 関数の Python プロファイルを測定し、b64encode_str() あたりが時間食っているかで判断できそうです。
前者はファイル読み込み数をいじれば検証可能?

議論に抜け漏れありそうでしょうか?

tarepan commented 5 months ago

意見修正しました。

Hiroshiba commented 5 months ago

ただキャッシュは設計上の考慮事項をドンドン増やしていくので、根本原因の解決ができるならそっちを優先したいです。

この発想が足りてませんでした、仰るとおりだと思います。

議論に抜け漏れありそうでしょうか?

そもそもFastAPIがこれだけのクエリ数(と容量)をさばくのにちょっと時間かかる、とかも無きにしもあらずかもです。

前者はファイル読み込み数をいじれば検証可能?

他の方法として、ちょっと、いやかなり面倒かもですが、ファイルを読みこんでバイナリを返す部分を関数化してfunctools.cache辺りで包んだ上で、起動時に全てのファイルを読み込んでキャッシュを作ったあと、エンジンにリクエストを投げまくって時間測定すれば一応切り分けはできるのかなと思いました。


仮にファイルI/Oが重い場合、そもそも画像や音声をbase64で返すのではなく、ファイルAPIを作ってそのURLを返して読み込みを遅延させる方法とかも有り得るのですが、これはなるべく考慮から省きたいと思っています。 どう考えても設計上こっちのが良いのですが、APIのmodelの破壊的変更になってしまうので 、まあなるべく避けたいかなと・・・・・ 😇

sabonerune commented 5 months ago

軽くline_profilerを使用して_speaker_infoを動かしてみました。その結果、

同時接続数1の場合

同時接続数を増やした場合

まだline_profilerがどのような基準で時間を計測しているか分からないため同時接続数を増やした場合の信頼度が分かりません。 また、fastapi周りの影響については確認できていません。

tarepan commented 5 months ago

プロファイリングありがとうございます!
私の勘とは違う結果で、やはり想像より計測ですね👍️

どうも CPU バウンドでは無さそうな雰囲気ですね。
現実装の I/O はブロッキング I/O なので、同時接続数を上げた際の read_ 系も改善の余地がありそうです。

感想としては、根本原因の排除で速度改善ができそうな予感がします(キャッシュ実装は時期尚早?)。

Hiroshiba commented 5 months ago

良いですね!!

実際の実行時間はどうなっていましたか? 割合(相対量)だけじゃなく実際の実行時間(絶対時間)も大事だと思ったためです。 1個だけ処理するのにかかる時間と、複数並列で処理するのにかかった時間÷処理数を比べて、どこが並列かの恩恵を受けててどこが受けてないのかを分析する必要がある気がします。 (なんかもう十分に並列ができている可能性もある気がちょっとしてます)

あととりあえず原因候補と解決策を列挙してみました。


個人的にはもうキャッシュの方向に倒しちゃっても良い気がしてきました。 というのも、エンジンのバージョンをキャッシュのタグに正しく含めていればキャッシュの問題は発生しえないことに気付いたためです。 (マルチエンジンのこと考えると念のためエンジン名も含めた方が良いかもですが。)

あとたぶん銀の弾丸はなさそうなことと、どうしても高速ができない部分がいくつかあるのもあって、割とキャッシュが妥当なのかなと思い始めました! エディタにとって起動速度の向上はかなり嬉しいので早めに実装されると嬉しい、という思いもあっての意見です。

でも未知のトラブルもありそうなのでキャッシュは避けられるなら避けたいし、エンジンのアップデートの度に起動時間がまた遅くなるのもできれば避けたいですが、まあトラブルが思いつかなければキャッシュ実装もありに思いました。 とりあえずキャッシュ実装してから高速化を頑張っていく、的なニュアンスかなと。

tarepan commented 5 months ago

ENGINE が本質的に遅いという暫定見解が得られたのでエディタで対処する、もアリな気がします。
エディタは素人で自信無いのですが、エディタの遅延リソースロード(placeholder 画像で起動したうえでバックグラウンド API call & 画像差し替え)で本質的に解決できるという話を見た覚えがあります。
手間は掛かると思いますが、初回ロードも高速化できるし話者数に依存しなくなるため、ENGINE にワークアラウンド的キャッシュを実装するより筋が良いかもです。

Hiroshiba commented 4 months ago

@tarepan 現状のエンジンの API 構成だとエディター側の改善はかなり難しいです・・・!

というのも結構単純な話で、どのキャラクターがエンジンにいるかわからないと、例えば一番最初に表示するテキスト欄のキャラクターを誰にすればいいのかがわからなかったりするので、UIが表示できなかったりします。 もちろんその起動シーケンスを変える方法もありますが、どちらかというとエンジン側を何とかした方が圧倒的に早そうに感じます。 あと、課題があるのはどちらかというとエディターではなく API 側だと思っていて、例えば他のサードパーティーから使う場合もかなりハンドリングが難しかったりするんじゃないかなと。

キャッシュが難しいのであれば、ちょっと頑張って互換性のない API v2 を作っていくのはありかもと思いました。 とりあえず/speaker_info系内にあるバイナリを全て URL に置き換えた物を v2にするのを考えてます。

まあでも個人的にはとりあえずキャッシュにして起動が遅い問題を何とかして、後で時間をかけて頑張っていくのが良いのかもとか思いました。 エンジン ID+話者 IDで一意・・・なはず。

(v2 API の設定をしていくならなんか専用で考えられる場所があるといいかもですね!)

tarepan commented 4 months ago

どのキャラクターがエンジンにいるかわからない

👍️
スタートアップに必須な話者情報テキストが単独で取り出せない(画像とセットになっている)ため ENGINE 側の問題、ということですね。理解しました。

とりあえずキャッシュにして起動が遅い問題を何とかして、後で時間をかけて頑張っていく

👍️
キャッシュ先行実装に同意です。


互換性のない API v2

/speaker_info にテキスト情報のみを高速返却するオプションを付ける手がありそうです。
GET /speaker_info?minimum=true をすると画像バイト列を placeholder とした話者情報を返すイメージです。
こうすれば v1 として後方互換性を維持 & 最小の ENGINE 実装変更で段階的情報取得ができそうです。

この辺はご指摘の通り、別の issue で更に議論できればと思います。

sabonerune commented 4 months ago

@Hiroshiba

どのキャラクターがエンジンにいるかわからないと

確か/Speakers/Singersにスタイル情報(スタイル名、スタイルID、スタイルのタイプ)が含まれているので一応現在のAPIでも遅延ロードは可能だと思います。

ただ、現在のエディタ側は初期化時にSpeakersSingersSpeakerInfoSingerInfoを単一のツリーに組み替えて保持しています。 これに遅延ロードの機能を追加するのはかなり面倒な作業だと思います。 (少なくとも自分にはできそうもなさそうでした)

Hiroshiba commented 4 months ago

@sabonerune たしかにテキストデータは別でも得られますね! 改修がとんでもない労力な気がするのも同感です。 正直どこから手を付けて良いのかパッと思いつかないくらいには複雑なんですよね・・・。


@tarepan

/speaker_info にテキスト情報のみを高速返却するオプションを付ける手がありそうです。 GET /speaker_info?minimum=true をすると画像バイト列を placeholder とした話者情報を返すイメージです。 こうすれば v1 として後方互換性を維持 & 最小の ENGINE 実装変更で段階的情報取得ができそうです。

なるほどです。 いつどうやってそのplaceholderのデータを取得するか、表示UI側のコードからどうやったらそのデータ取得関数を叩けるか、placeholder中の表示をどうするか、とかも考えると結構しんどいんですよね・・・。

引数パラメータ指定で他の値を返すのは面白いアイデアかもと思いました。 その仕組みで画像や音声はURLを返せば良いのかもしれません。 まあ、マルチエンジンとかのことも考えないといけないのと、複雑化してしまうのと、v2を作ったときに仕様がダブるのととかいろいろ考えないといけないことはありそうですが。


とりあえずissue化しようと思います!

Hiroshiba commented 4 months ago

URLにする方法をいろいろ検討してisuse化してみました!

書いてて思ったのですが、エディタ側は「srcに指定しているからURLにするとコード変更量が少なくかなり直感的なコードになる」というのが自分が推してる理由として大きいかもと感じました。

tarepan commented 3 months ago

1240 を用い、本番環境ベンチマークを取りました。

追記: #1240 更新に伴いベンチマーク結果も更新

手法

本番環境にて、GET /speaker_info直列で全話者に対してリクエストしました。すなわち最悪ケースの模倣です。
また、擬似的なプロファイルを ablation study により取りました。条件は「リソースファイル読み込み変換 and/or deepcopy をコメントアウト」です。

結果

以下が生データになります(detail)。

intel Core i3 13100 CPU, WSL2 ubuntu, OSS版 VOICEVOX ENGINE latest + 製品版 VOICEVOX 0.19.1 ```bash # フルスペック vscode ➜ /workspaces/voicevox_engine (add/benchmark_speed) $ python -m test.benchmark.speed.speaker Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores. Info: Loading core 0.15.3. `GET /speakers` fakeserve: 0.0217 sec `GET /speakers` localhost: 0.0200 sec Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores. Info: Loading core 0.15.3. 全話者 `GET /speaker_info` fakeserve: 0.570 sec 全話者 `GET /speaker_info` localhost: 0.633 sec Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores. Info: Loading core 0.15.3. 全話者 `GET /` fakeserve: 0.069 sec 全話者 `GET /` localhost: 0.064 sec # リソースファイル読み込み&変換をスキップ vscode ➜ /workspaces/voicevox_engine (add/benchmark_speed) $ python -m test.benchmark.speed.speaker Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores. Info: Loading core 0.15.3. `GET /speakers` fakeserve: 0.0224 sec `GET /speakers` localhost: 0.0200 sec Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores. Info: Loading core 0.15.3. 全話者 `GET /speaker_info` fakeserve: 0.252 sec 全話者 `GET /speaker_info` localhost: 0.261 sec Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores. Info: Loading core 0.15.3. 全話者 `GET /` fakeserve: 0.072 sec 全話者 `GET /` localhost: 0.068 sec # リソースファイル読み込み&変換をスキップ + deepcopy スキップ vscode ➜ /workspaces/voicevox_engine (add/benchmark_speed) $ python -m test.benchmark.speed.speaker Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores. Info: Loading core 0.15.3. `GET /speakers` fakeserve: 0.0202 sec `GET /speakers` localhost: 0.0186 sec Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores. Info: Loading core 0.15.3. 全話者 `GET /speaker_info` fakeserve: 0.157 sec 全話者 `GET /speaker_info` localhost: 0.159 sec Warning: cpu_num_threads is set to 0. Setting it to half of the logical cores. Info: Loading core 0.15.3. 全話者 `GET /` fakeserve: 0.070 sec 全話者 `GET /` localhost: 0.063 sec ```

上記を集計した結果、直列リクエスト時の所要時間が以下のようになることがわかりました。

|-------------| 0.65
|-|             0.05 request
 |--------|     0.40 file load & convert
         |--|   0.10 deepcopy
           |--| 0.10 other processings

ファイルI/O+変換は 0.4 秒程度かかっています。

考察

ボトルネック候補になっていたファイルI/O+変換が 0.4 秒程度しかかかっていません。
本実験は直列リクエストであるため、これ以上遅くなることはありません。またこの結果は Core i3 13100 CPU で得られたため、他 CPU はより高速と考えられます。

この結果に基づき、製品版 VOICEVOX 立ち上げが徐々に遅くなっている原因は以下の 2 つのいずれかと考えます:

(追記) 現状ではエンジンの GET /speaker_info は充分な速度を出せている、と考えます。換言すれば、エディタ側(内部処理かリクエスト発出周りか)にボトルネック有りと示唆される、と考えます。

ゆえにエンジン側に複雑な高速化システムを入れるのはデメリットが勝ると考えます。

後者の原因を重視するのであれば GET /speaker_info/all API を新しく生やすのが良いと考えます。(追記: 直列で問題無しなのでこれは無意味)

@Hiroshiba @sabonerune @y-chan ご意見伺えれば幸いです。

sabonerune commented 3 months ago

https://github.com/VOICEVOX/voicevox_engine/issues/1129#issuecomment-2008672645 のプロファイリングでネットワークのグラフが大半を占めていることからエディタのコード自体は現状ボトルネックにはなっていないと思います。 もしボトルネックになっているとしたら通信をしていない区間が多く占めるはずです。 (エンジン側のレスポンスが高速化した場合次はエディタ側のBase64デコードがボトルネックになる可能性はあります)

となると遅いのはここで計測していない通信周りでしょうか?(FastAPI~ElectronのHTTPデコードまで辺り?)

そもそもVOICEVOX 0.19.0時点でspeaker_infoディレクトリ全体のファイルサイズは280MBもあります。 これを起動時に全部読み込みを行うという設計に限界があると思います。 となるとURL化 #1208 かエディタ側で遅延読み込みhttps://github.com/VOICEVOX/voicevox_engine/issues/1129#issuecomment-2084813663 をするのがいいような気がします。

tarepan commented 3 months ago

@sevenc-nanashi
(メンションし損ねてました)ご意見伺えると嬉しいです。エディタ起動高速化の一助になれば幸いです。

Hiroshiba commented 3 months ago

@tarepan 検証ありがとうございます!

他の実験結果からの推察が外れたことになるのでとても驚きです!!! 0.5秒くらいだというのは意外と直感には合う気がします。ファイルロードとbase64化しかしてないはずなので。。

こちらでも試してみたいのと、なにかしらのミスがありえなくはなとで、もしよければ検証コード見てみたいです・・・!!

tarepan commented 3 months ago

0.5秒くらいだというのは意外と直感には合う気がします

私の経験上も、この程度の負荷であれば 0.5 秒は妥当な処理時間に感じます。

検証コード

https://github.com/VOICEVOX/voicevox_engine/issues/1129#issuecomment-2107319939 にある通りです。#1240 を利用した上で 詳細 タブにあるコマンドを打っています。別プロセスでエンジンを建てておく必要があり、それは #1240 で追加されているspeed/speaker.pyif __name__ == "__main__": 以下に手順が記載してあります。

Hiroshiba commented 3 months ago

おお、なるほどです!!! ありがとうございます!!

Hiroshiba commented 3 months ago

手元のmacOSでも試してみました! 結論は @tarepan さんの仰るとおりで、エンジン側は十分(ではないけど少なくとも思っていたよりはずっと)速いと感じました。

結果こんな感じでした。(fakeの方はRosettaをうまく動かせなかったので計測できませんでした。。)

`GET /speakers` localhost: 0.0328 sec
全話者 `GET /speaker_info` localhost: 0.910 sec
全話者 `GET /` localhost: 0.058 sec

エディタ側は/singer_infoも実行するので、合わせると倍ほどかかるとは思います。 それでもエディタの起動は、エンジンが起動してからデータ準備する部分だけでも10秒ほどかかっているので、でかいボトルネックはエンジンじゃないと感じました!

Hiroshiba commented 3 months ago

こちらのissueに関して、もしかしたら実装できることは何もないのかもとちょっと思いました。 というのももう十分に実装がもう早いのではないかなと・・・。

例えばbase64エンコードするのは多少時間かかってそうなので、それをファイルキャッシュする手はありえるかもしれません。 ただ本当にそれが必要かの議論がまだなので、もしかしたら実装者募集は早いかも・・・?

個人的には↓の議論を進めたい思いがちょっとあります!!

tarepan commented 2 months ago

URL による取得が実装され、また従来式もベンチマークと高速化が完了しました。

@Hiroshiba
本 issue は resolve につき close 可能そうです。

Hiroshiba commented 2 months ago

そうですね!!

エディタが11秒くらいかかってた処理が、最終的に3秒くらいに縮まりました🎉 エンジンの速度はだいたい0.5秒〜1秒弱だった記憶で、締める時間の割合は上がったので将来キャラが増えてきたらまた見直しが必要になるかも。 (リソースマネージャーも入ってだいぶ早くなってると思うので、後々のためにresource_typeをurlにした状態でどれくらいの速度出るのか残しといても良いかも? https://github.com/VOICEVOX/voicevox_engine/issues/1129#issuecomment-2107319939

非常に良い機能が実装できたと思います! 一連の調査・議論・実装お疲れ様でした!!