Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

synthesisで処理が終わった後にメモリを開放する #1506

Open
1 of 3 tasks
X-20A opened this issue Dec 18, 2024 · 12 comments
Open
1 of 3 tasks

synthesisで処理が終わった後にメモリを開放する #1506

X-20A opened this issue Dec 18, 2024 · 12 comments
Labels
機能向上 状態:必要性議論 必要性を議論している状態 状態:設計 設計をおこなっている状態 非アクティブ

Comments

@X-20A
Copy link
Contributor

X-20A commented Dec 18, 2024

内容

synthesisにクエリを送るとメモリが確保されますが、タスクマネージャを見ると処理が終わった後もメモリが占有され続けられているようです(最も大きいクエリの消費量が維持される?)。メモリ消費自体はクライアント側でクエリを小さく分割して送れば問題にならないはずなので(エディタもそうしてる?)、さほど優先順位は高くないかもしれませんが、処理が終わったら適宜解放するほうが他のソフトへの支障が避けられるかなと。synthesis以外のリクエストが同様であるかは未確認です。

Pros 良くなる点

メモリ占有率を抑えられる

Cons 悪くなる点

予めメモリが確保されているか否かでパフォーマンスに差がある、かも?無いと思いますが

実現方法

未検証

VOICEVOXのバージョン

VOICEVOX ENGINE 0.21.1

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

  • Windows
  • macOS
  • Linux
  • Windows 10 Home 64bit 22H2(19045.5247)
@qryxip
Copy link
Member

qryxip commented Dec 18, 2024

おそらくこのissueは #513 の重複になるのではないかと思っています。


私の理解では、/audio_query系および/synthesisは必ず次のステップを含みます。

  1. もしONNXモデルがロードされて (bool is_model_loaded(int64_t speaker_id))いないのなら、ロード (bool load_model(int64_t speaker_id))する
  2. ONNXモデルの実行

1.について補足すると、あるONNXモデルには複数の話者/スタイルが入っており、例えばあまあまずんだもんに対してロードすると春日部つむぎ等もロードされます。

https://github.com/VOICEVOX/voicevox_fat_resource/tree/177c5a3d96336ca74413b90609a4845091c19920/core/model#音声モデルvvmファイルと声キャラクタースタイル名とスタイル-id-の対応表
(このリンクの情報は少し古く、現在はVoidollまで含めると18種類)

1.でロードされたモデルはVOICEVOX ENGINEを終了するまでロードされたままです。そして私が見聞きした限りでは、「VOICEVOX ENGINEが使っているRAM/VRAM」とは1.を指すと思います。

ただし2.で使用メモリが膨らまないかというとそうでもないようで、CUDA版でメモリリークが起きるらしいことがわかっています。CUDAのメモリリークについてONNXモデルのアンロードが効くかどうかは未確認です。
[追記] あと、今のONNX RuntimeとCUDAで再現するかも不明

@X-20A
Copy link
Contributor Author

X-20A commented Dec 19, 2024

#513と同じ現象なのかちょっと疑問が残ります。
#513: リクエストを繰り返すと徐々にメモリを食っていき、解放されない
#1506: リクエストのうち、最大のクエリで使用されたメモリが以降解放されない

私の環境では#513は確認できませんでした。#1506は初め、ブラウザからリクエストしたときに確認しましたが、エディタからの場合でも同様に発生しているようです。
また、#1506で確保されるメモリの大きさはaudio_queryに渡す文字列の大きさと正の相関が見られます。
例:audio_queryとsynthesisをリクエストしたときのメモリサイズ
76文字のとき、375.9MB
653文字のとき、3877.3MB
なので、キャラクターモデルのロードとは別の処理に由来すると思います。

以下engineで調べたこと
synthesisにおいては、
tts_engine.py > synthesis_wave
core_adapter.py > safe_decode_forward → with self.mutex:
core_wrapper.py > decode_forward (ここから先はコアっぽい)
部で主にメモリが確保されている
waveなどはサイズを調べても、先の例(375.9MB)の内、1MBもなかったのでやはりコアの方でメモリリークが発生している?

#513で提案されている、エンジンからコアをリフレッシュする方法は良さそうですが、Rustが分からないので私は手が出ないかもしれません。

@qryxip
Copy link
Member

qryxip commented Dec 19, 2024

ロードされたモデルという表現はよくなかったかもしれません。protobufで記述されたONNXファイルが、ONNX RuntimeOrtSessionという形式になることを指して"モデルをロードする"という表現をしています。OrtSessionは計算グラフを実行可能な形で持っており、状況によってGB単位に膨れるのは不自然ではないように感じました。

実際VOICEVOX COREのPython API"、".join(itertools.repeat("ア" * 9, 50)) + "ア"(計500文字)の音声合成をCPUで試したところ、確かに数GBのRAMが確保されたのですが、unload_voice_modelを通じたReleaseSession(OrtSession*)によりその数GBが解放されるらしいことを確認しました。

このことから、メモリリークとして分類するのは微妙かもしれません。というのも解放しようとしたらできるので。というより今調べたら、ちゃんとドキュメント化されていた挙動のようです。

  • Memory arena shrinkage:
    • Description: By default, memory arenas do not shrink (return unused memory back to the system). This feature allows users to “shrink” the arena at some cadence. Currently, the only supported cadence is at the end of every Run() (i.e.) if this feature is used, the arena memory is scanned to potentially free up unused memory at the end of every Run(). This is achieved through a RunOption.

Get started with ORT for C

どうするかですが、 #513 の流れからすると/unload_voice_model?ids={comma-separated-vvm-uuids}のようなAPIを誕生させるのが一案としてあると思います。ENGINE利用者が自分の判断によって一部もしくは全部のモデルのアンロードを指示する形で。ただ個人的には、上記リンクで紹介されているようなONNX Runtimeのメモリ周りの機能を検討してからでも遅くはないかなと思っています。 VOICEVOX/voicevox_core#545 が終わったら実験してみようかなと。
[追記] CUDAやROCmも含めて


@Hiroshiba このissueについてはどうしましょう?個人的にはせっかくなのでsub-issue機能を使ってもいいかと思ったですが、上手い整理のしかたが思い付きませんでした。親子の関係というよりは兄弟っぽいし...
[追記] バグレポートとしては兄弟、enhancementとしては重複なのかなと思っています。

@Hiroshiba
Copy link
Member

Hiroshiba commented Dec 20, 2024

issue作成ありがとうございます!
端的にまとめると、話者を指定して使ってるメモリを解放するためのAPI(/initialize_speakerの逆、/creanup_speakerとか?)を生やし、その実装方法を話したり試したりするのが良さそうに思いました!


調査もありがとうございます!
コアの中でメモリが使われてる感じですよね。おそらくonnxruntimeで音声合成モデルを動かしてる周りだと思われます!
毎回メモリ解放すればいいじゃんという発想はありませんでした。onnxruntime側にもそれを可能にできそうなオプションがありますね!
一旦この辺りの感想を書くと・・・

音声合成するたびに毎回メモリ解放しても速度的に問題がないかはちょっとわからないのが本音です。
少なくともCPUではメモリ確保にそこまで時間かからないだろうし、毎回解放して良さそうな気がします。
GPUではそこそこメモリ確保に時間がかかるかもです。WindowsでのGPUのデフォルトとして使ってるDirectMLはわからないですが、少なくともCUDAは少しかかるはず。

@qryxip さんが貼ってるページを見るに、onnxruntimeはどれくらいメモリを確保し続けるか指定できそうなので、それで制御可能にするのもありかも。

とはいえコア側変えないとなのでかなり大変だと思います。
なので提案としては @qryxip さんと同じ感じで、話者を指定してコアを解放する/creanup_speakerを新たに作るというのはどうでしょうか!

実装としてはおそらくCoreManagerにunregisterを用意してAPIから叩けば良い・・・気がするのですが、それでメモリが解放されるかはやってみないとわからなそうです。
@X-20A もしご興味あればこの辺り試してみていただけませんか 🙇

@Hiroshiba Hiroshiba added 状態:必要性議論 必要性を議論している状態 状態:設計 設計をおこなっている状態 labels Dec 20, 2024
@qryxip
Copy link
Member

qryxip commented Dec 20, 2024

「話者を指定して」は少々無理があるように思えます。現状のENGINEだと、libvoicevox_core単位で丸ごとアンロードする他無いのではないかと。

VVMが導入された後だとしても、引数をStyleIdにするのは懸念があると思います。

COREの方でVVMについての議論をしていたとき、「例えばあまあまずんだもんをアンロードしようとしたら春日部つむぎ等もアンロードされる。これはユーザーにとって理解するのが難しくないか」という流れから、VVMを陽に指定してアンロードする形式に落ち着いたという認識です。

async with await VoiceModelFile.open("./path/to/0.vvm") as model:
    await synth.load_voice_model(model)

# …

synth.unload_voice_model(model.id)

ENGINEでもVVMのUUIDを指定する形式はそんなに無理が出ないのではと思ってます。例えば/speakers?format=by_vvmlist[Speaker]ではなくdict[UUID, list[Speaker]]を見られるようにしておけば、VVMによる区切りをユーザーに教えられるはず。

@Hiroshiba
Copy link
Member

Hiroshiba commented Dec 20, 2024

@qryxip ちょっとコアの話に寄りすぎ気味かもです。
エンジンAPIのシグネチャと、今の処理をどうするか、将来詰まないかで整理したいです。

話者となってるけどStyleIdでしたね。。
エンジンは互換性の観点から指定する引数はAPI間で統一した方が良いだろうなと強めに思います。
なので少なくともinitializeとシグネチャを揃えた方が良く、かつ今initializeのシグネチャを変えるほどではないと思うので、styleId指定が良いかなと。
(今稼働してるエンジンメンテナの余力でシグネチャを抜本的に見直す時間とその恩恵より、今のシグネチャに合わせて作って早くリリースした方が恩恵が大きい)

実装は、とりあえず今のエンジンは指定したスタイルが含まれるコアをunloadする感じで良いと思います。
将来的にはvvmのunloadとかになるかもですが、web APIのシグネチャは変えずにいけるかなと!


兎にも角にも、そもそもエンジンマネージャーからコアインスタンスを弾いた時にメモリ解放できるのかが重要なので、そこの検証結果がほしい…!

@qryxip
Copy link
Member

qryxip commented Dec 21, 2024

指定したスタイルが含まれるコアをunload

よくわかっていないのですが、ENGINEにとっての声とは(style_id, core_version)の組で区別されるものである認識です。であるなら今後の互換性の観点からはspeaker_idを引数に取る必要は無く、reload_core?core_version={core_version?}/uninitialize_allといったAPIが十分な役割を果たすのではないかと思いました。

initializeとシグネチャを揃えた方が良く

将来的にはvvmのunloadとかになるかもですが、web APIのシグネチャは変えずにいけるかなと!

ちょっと個人的にはこれに懐疑的です。モデルをロードしたい理由はほぼほぼ限られると思いますが、反面モデルをアンロードしたい理由は多岐に渡ると思います。このissueのように巨大な入力を与えてしまってメモリが数GBに膨れてしまったのをどうにかしたい場合もあれば、一定時間使っていない声に対してメモリを節約したいという場合もあるのではないかと。なので/initialize_speakerStyleId指定でuninitializeは別の指定方法というのは別に不自然にはならないんじゃないかと思っています。

そもそもエンジンマネージャーからコアインスタンスを弾いた時にメモリ解放できるのか

finalizeは多分叩いておいた方がよさそうですね。というかもしかしたらfinalize()initialize()とするだけでもよさそう。

@Hiroshiba
Copy link
Member

Hiroshiba commented Dec 21, 2024

たしかに必ずinitializeと対応させるは必要はないかもです。
全体としてAPIがより使い勝手が良ければOKだと思います。

であるなら今後の互換性の観点からはspeaker_idを引数に取る必要は無く

あっすみませんここ誤解で、エンジンは内容的にstyleIdを受け取るものでも互換性のために引数名をspeakerIdにしてて、僕はstyleIdのことを意図してspeakerIdと書いてしまいました!
今までissueに書かれたspeakerIdは全てstyleIdのことを意味してます!

uninitialize_all

全コアのインスタンスを破棄するAPIはその需要での使い勝手は良さそうですが、ひとつずつ破棄したい需要をカバーできないかな?

コアバージョンの指定はたしかに必要だと思います。
これはinitializeでもそうだったかな。

まとめると、引数はstyleId(引数名はspeakerIdかもしれない)、core_versionとするのが使い勝手良さそうですね!

@qryxip
Copy link
Member

qryxip commented Dec 21, 2024

今までissueに書かれたspeakerIdは全てstyleIdのことを意味してます!

これは承知しておりまして、Web APIのパラメータの意味で発言しました。というより、VOICEVOXのコードの読み書きをしている者同士で誤解が生じうるとは考えてもいませんでした。「話者」を指したかったらspeaker_uuidがあるわけですし。まあよくよく考えたらややこしいやつですね…

引数はstyleId(引数名はspeakerIdかもしれない)

「かもしれない」というのが引っ掛かったのですが、なるほど。新規追加のAPIであればstyle_idを引数名にはできますね。ただ今更style_idとしてもという気もしますし、speaker_idでよい...?

@Hiroshiba
Copy link
Member

Hiroshiba commented Dec 21, 2024

たしかソングAPIでめちゃくちゃ迷って(意味的にはstyleIdだし、そもそもspeaker=話者ですらない)、迷った末にspeakerにした記憶があります。
まだ考えきれてないですが、まあ今回もいろいろ考えた末にspeaker_idとする気がします。

とりあえずAPIのシグネチャはまとまりそうですね!
API名を何にするかは迷いますが、そのスタイルのいろいろを消しかつすでに消えてても問題ない名前が良さそうで、cleanup辺りを仮置きし、/initialize_speakerと対象になる書き方にして・・・改良の余地はあるかもですが↓がベースかな。

/cleanup_speaker (speaker_id: StyleId, core_version=None)
効果は「指定したスタイルを後片付けする」。
やることは最初は「コアインスタンスを解放する」だけ。

@Hiroshiba
Copy link
Member

すみません、勘違いしていました!!
話者やスタイルを指定したメモリ解放は現状のコア0.15ではできないですね!!!
コアインスタンスの破棄は、もしメモリが解放されるとしても、全スタイルのメモリが解放されるはず。
だから/creanup_speakerAPIを作ることは現状できないですね・・・!

コアインスタンスの破棄あるいはfinalize()を前提に、その機能を持つAPIの名称候補は/creanup_all_speakersですかね・・・。
他候補だと

  • /creanup_all_speakers
    • initialize_speakerと対比できるので分かりやすい
    • まあちょっと名前が不格好だけど、一番候補な気がする
  • /cleanup
    • 何がクリーンアップされるかわからない。エンジン全部かもしれない。
    • 「エンジン全部のクリーンナップをするAPI」を作るのを目標にするのもありかもしれない。
  • /creanup_speaker
    • 現状この API に合うような機能を作れない

実装は @qryxip さんがちょこっとおっしゃってるように、core_wrapper.finalize()を叩くだけで良いかもしれないです!!

Copy link

本 Issue は直近 30 日間で活動がありません。今後の方針について VOICEVOX チームによる再検討がおこなわれる予定です。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
機能向上 状態:必要性議論 必要性を議論している状態 状態:設計 設計をおこなっている状態 非アクティブ
Projects
None yet
Development

No branches or pull requests

3 participants