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

initializeで全モデルを読み込まなくても良いようにした #124

Merged
merged 10 commits into from
May 9, 2022
6 changes: 3 additions & 3 deletions core/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
lib = cdll.LoadLibrary(str(core_dll_path))

# 関数型定義
lib.initialize.argtypes = (c_bool, c_int)
lib.initialize.argtypes = (c_bool, c_int, c_bool)
lib.initialize.restype = c_bool

lib.finalize.argtypes = ()
Expand All @@ -52,8 +52,8 @@


# ラッパー関数
def initialize(use_gpu: bool, cpu_num_threads=0):
success = lib.initialize(use_gpu, cpu_num_threads)
def initialize(use_gpu: bool, cpu_num_threads=0, load_all_models=True):
success = lib.initialize(use_gpu, cpu_num_threads, load_all_models)
if not success:
raise Exception(lib.last_error_message().decode())

Expand Down
149 changes: 101 additions & 48 deletions core/src/core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <array>
#include <exception>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>

Expand All @@ -19,9 +20,7 @@
#include "core.h"

#define NOT_INITIALIZED_ERR "Call initialize() first."
#define NOT_FOUND_ERR "No such file or directory: "
#define FAILED_TO_OPEN_MODEL_ERR "Unable to open model files."
#define FAILED_TO_OPEN_METAS_ERR "Unable to open metas.json."
#define NOT_LOADED_ERR "Model is not loaded."
Comment on lines -22 to +23
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

消したのは未使用のエラーメッセージ

#define ONNX_ERR "ONNX raise exception: "
#define JSON_ERR "JSON parser raise exception: "
#define GPU_NOT_SUPPORTED_ERR "This library is CPU version. GPU is not supported."
Expand All @@ -43,13 +42,19 @@ EMBED_DECL(YUKARIN_S);
EMBED_DECL(YUKARIN_SA);
EMBED_DECL(DECODE);

const struct {
/**
* 3種類のモデルを一纏めにしたもの
*/
struct VVMODEL {
embed::EMBED_RES (*YUKARIN_S)();
embed::EMBED_RES (*YUKARIN_SA)();
embed::EMBED_RES (*DECODE)();
} MODELS_LIST[] = {{YUKARIN_S, YUKARIN_SA, DECODE}};
};
const VVMODEL VVMODEL_LIST[] = {
{YUKARIN_S, YUKARIN_SA, DECODE},
};
} // namespace EMBED_DECL_NAMESPACE
using EMBED_DECL_NAMESPACE::MODELS_LIST;
using EMBED_DECL_NAMESPACE::VVMODEL_LIST;
Comment on lines +45 to +57
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

今まではMODELS={YUKARIN_S, YUKARIN_SA, DECODE}という命名規則でしたが、話者ごとのmodelsなのかyukarin_s/saなどのmodelsなのか分かりづらかったので、VVMODEL={YUKARIN_S, YUKARIN_SA, DECODE}という形にしました。


// 複数モデルある場合のspeaker_idマッピング
// {元のspeaker_id: {モデル番号, 新しいspeaker_id}}
Expand All @@ -76,8 +81,23 @@ SupportedDevices get_supported_devices() {
}

struct Status {
Status(bool use_gpu_)
: use_gpu(use_gpu_), memory_info(Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU)) {}
Status(int model_count, bool use_gpu, int cpu_num_threads)
: memory_info(Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU)) {
yukarin_s_list = std::vector<std::optional<Ort::Session>>(model_count);
yukarin_sa_list = std::vector<std::optional<Ort::Session>>(model_count);
decode_list = std::vector<std::optional<Ort::Session>>(model_count);

session_options.SetInterOpNumThreads(cpu_num_threads).SetIntraOpNumThreads(cpu_num_threads);
if (use_gpu) {
#ifdef DIRECTML
session_options.DisableMemPattern().SetExecutionMode(ExecutionMode::ORT_SEQUENTIAL);
Ort::ThrowOnError(OrtSessionOptionsAppendExecutionProvider_DML(session_options, 0));
#else
const OrtCUDAProviderOptions cuda_options;
session_options.AppendExecutionProvider_CUDA(cuda_options);
#endif
}
}
Copy link
Member Author

@Hiroshiba Hiroshiba May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use_gpuはSTATUSの初期化時に、cpu_num_threadsはモデルロード時に指定だったのを、どちらも初期化時に指定する形式に変更しました。

Comment on lines +84 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C++ではコンストラクタ内で例外が発生するとclass及びstructのfieldで確保されているリソースが開放されないためメモリリーク等の問題が起きます。そのためC++でのコンストラクタでは基本的にfieldの初期化のみ程度に留めるべきです。
Ort系の処理では例外が発生する可能性があるためこの処理はコンストラクタで行うべきではないでしょう。
そのためこのコンストラクタの処理をstaticメンバ関数にしたほうが望ましいです

Copy link
Member Author

@Hiroshiba Hiroshiba May 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほどです。
まあエラーが出たらそもそもこのライブラリが動かない(のでプロセスキルするはず)ので、そこまで気にしなくても良いのかなと思いました・・・!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コンストラク内の例外でメモリリークが発生してしまうのはデストラクタが呼ばれないからで、例外処理をきちんと書いていればメモリリークは起きません。
コンストラクタ外でメモリ確保をしても例外処理を書かなければメモリリークになるので、エラーハンドリングをしっかりしないなら何処に書いても変わらないと思われます。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コンストラク内の例外でメモリリークが発生してしまうのはデストラクタが呼ばれないからで、例外処理をきちんと書いていればメモリリークは起きません。

はい。正確にはデストラクタが呼ばれないですね。

コンストラクタ外でメモリ確保をしても例外処理を書かなければメモリリークになるので、エラーハンドリングをしっかりしないなら何処に書いても変わらないと思われます。

今回のケースだとリソースは各classのデストラクタで管理されているのでコンストラクタ外で処理を書けば例外が発生した場合各classのデストラクタが呼び出されるのでメモリリークは発生しないはずですね。
これが例えばStatusのフィールドで明示的に管理するリソースがあって、Status自身のデストラクタで開放しているのであればメモリリークする可能性はありますが。今回はStatus classとして明示的にデストラクタで開放してるフィールドは無いようなので
コンストラクタ外で処理を書けばメモリリークにはならないはずですが、Hihoさんがおっしゃるとおりライブラリが動かかないのでそもそもここを気にしてもあまり意味がないというのはあります。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

すみませんコンストラクタ周りの仕様についてちょっと勘違いしていたらしく、そもそもこの実装のままでも各フィールドのデストラクタは呼ばれそうです...?
https://clown.cube-soft.jp/entry/20110320/1300596620

コンストラクタで例外が発生した場合,そのクラス自体のデストラクタは実行されませんが,(初期化の終了した)メンバ変数のデストラクタは実行されます.したがって,例えば new でメモリを確保するようなメンバ変数の場合は,スマート・ポインタで保持しておく事によってその部分のリソース開放が自動的に行われ,リソースの開放忘れのかなりの部分を防ぐ事ができます.

このままにするとのことだったのであまり意味のない情報ですが

/**
* Loads the metas.json.
*
Expand All @@ -89,7 +109,7 @@ struct Status {
* version: string
* }]
*/
bool load(int cpu_num_threads) {
bool load_metas() {
embed::Resource metas_file = METAS();

metas = nlohmann::json::parse(metas_file.data, metas_file.data + metas_file.size);
Expand All @@ -100,36 +120,32 @@ struct Status {
supported_styles.insert(style["id"].get<int64_t>());
}
}
return true;
}

for (const auto MODELS : MODELS_LIST) {
embed::Resource yukarin_s_model = MODELS.YUKARIN_S();
embed::Resource yukarin_sa_model = MODELS.YUKARIN_SA();
embed::Resource decode_model = MODELS.DECODE();

Ort::SessionOptions session_options;
session_options.SetInterOpNumThreads(cpu_num_threads).SetIntraOpNumThreads(cpu_num_threads);
yukarin_s_list.push_back(Ort::Session(env, yukarin_s_model.data, yukarin_s_model.size, session_options));
yukarin_sa_list.push_back(Ort::Session(env, yukarin_sa_model.data, yukarin_sa_model.size, session_options));
if (use_gpu) {
#ifdef DIRECTML
session_options.DisableMemPattern().SetExecutionMode(ExecutionMode::ORT_SEQUENTIAL);
Ort::ThrowOnError(OrtSessionOptionsAppendExecutionProvider_DML(session_options, 0));
#else
const OrtCUDAProviderOptions cuda_options;
session_options.AppendExecutionProvider_CUDA(cuda_options);
#endif
}
decode_list.push_back(Ort::Session(env, decode_model.data, decode_model.size, session_options));
}
/**
* モデルを読み込む
*/
bool load_model(int model_index) {
const auto VVMODEL = VVMODEL_LIST[model_index];
embed::Resource yukarin_s_model = VVMODEL.YUKARIN_S();
embed::Resource yukarin_sa_model = VVMODEL.YUKARIN_SA();
embed::Resource decode_model = VVMODEL.DECODE();

yukarin_s_list[model_index] =
std::move(Ort::Session(env, yukarin_s_model.data, yukarin_s_model.size, session_options));
yukarin_sa_list[model_index] =
std::move(Ort::Session(env, yukarin_sa_model.data, yukarin_sa_model.size, session_options));
decode_list[model_index] = std::move(Ort::Session(env, decode_model.data, decode_model.size, session_options));
return true;
}

std::string root_dir_path;
bool use_gpu;
Ort::SessionOptions session_options;
Ort::MemoryInfo memory_info;

Ort::Env env{ORT_LOGGING_LEVEL_ERROR};
std::vector<Ort::Session> yukarin_s_list, yukarin_sa_list, decode_list;
std::vector<std::optional<Ort::Session>> yukarin_s_list, yukarin_sa_list, decode_list;

nlohmann::json metas;
std::string metas_str;
Expand Down Expand Up @@ -166,7 +182,7 @@ std::pair<int64_t, int64_t> get_model_index_and_speaker_id(int64_t speaker_id) {
return found->second;
}

bool initialize(bool use_gpu, int cpu_num_threads) {
bool initialize(bool use_gpu, int cpu_num_threads, bool load_all_models) {
initialized = false;

#ifdef DIRECTML
Expand All @@ -178,18 +194,29 @@ bool initialize(bool use_gpu, int cpu_num_threads) {
return false;
}
try {
status = std::make_unique<Status>(use_gpu);
if (!status->load(cpu_num_threads)) {
const int model_count = std::size(VVMODEL_LIST);
status = std::make_unique<Status>(model_count, use_gpu, cpu_num_threads);
if (!status->load_metas()) {
return false;
}
if (use_gpu) {
// 一回走らせて十分なGPUメモリを確保させる
int length = 500;
int phoneme_size = 45;
std::vector<float> phoneme(length * phoneme_size), f0(length);
int64_t speaker_id = 0;
std::vector<float> output(length * 256);
decode_forward(length, phoneme_size, f0.data(), phoneme.data(), &speaker_id, output.data());

if (load_all_models) {
for (int model_index = 0; model_index < model_count; model_index++) {
if (!status->load_model(model_index)) {
return false;
}
}

if (use_gpu) {
// 一回走らせて十分なGPUメモリを確保させる
// TODO: 全MODELに対して行う
int length = 500;
int phoneme_size = 45;
std::vector<float> phoneme(length * phoneme_size), f0(length);
int64_t speaker_id = 0;
std::vector<float> output(length * 256);
decode_forward(length, phoneme_size, f0.data(), phoneme.data(), &speaker_id, output.data());
}
}
} catch (const Ort::Exception &e) {
error_message = ONNX_ERR;
Expand All @@ -208,6 +235,17 @@ bool initialize(bool use_gpu, int cpu_num_threads) {
return true;
}

bool load_model(int64_t speaker_id) {
auto [model_index, _] = get_model_index_and_speaker_id(speaker_id);
return status->load_model(model_index);
}

bool is_model_loaded(int64_t speaker_id) {
auto [model_index, _] = get_model_index_and_speaker_id(speaker_id);
return (status->yukarin_s_list[model_index].has_value() && status->yukarin_sa_list[model_index].has_value() &&
status->decode_list[model_index].has_value());
}

void finalize() {
initialized = false;
status.reset();
Expand All @@ -231,6 +269,11 @@ bool yukarin_s_forward(int64_t length, int64_t *phoneme_list, int64_t *speaker_i
return false;
}
auto [model_index, model_speaker_id] = get_model_index_and_speaker_id(*speaker_id);
auto &model = status->yukarin_s_list[model_index];
if (!model) {
error_message = NOT_LOADED_ERR;
return false;
}
try {
const char *inputs[] = {"phoneme_list", "speaker_id"};
const char *outputs[] = {"phoneme_length"};
Expand All @@ -240,8 +283,8 @@ bool yukarin_s_forward(int64_t length, int64_t *phoneme_list, int64_t *speaker_i
to_tensor(&model_speaker_id, speaker_shape)};
Ort::Value output_tensor = to_tensor(output, phoneme_shape);

status->yukarin_s_list[model_index].Run(Ort::RunOptions{nullptr}, inputs, input_tensors.data(),
input_tensors.size(), outputs, &output_tensor, 1);
model.value().Run(Ort::RunOptions{nullptr}, inputs, input_tensors.data(), input_tensors.size(), outputs,
&output_tensor, 1);

for (int64_t i = 0; i < length; i++) {
if (output[i] < PHONEME_LENGTH_MINIMAL) output[i] = PHONEME_LENGTH_MINIMAL;
Expand All @@ -266,6 +309,11 @@ bool yukarin_sa_forward(int64_t length, int64_t *vowel_phoneme_list, int64_t *co
return false;
}
auto [model_index, model_speaker_id] = get_model_index_and_speaker_id(*speaker_id);
auto &model = status->yukarin_sa_list[model_index];
if (!model) {
error_message = NOT_LOADED_ERR;
return false;
}
try {
const char *inputs[] = {
"length", "vowel_phoneme_list", "consonant_phoneme_list", "start_accent_list",
Expand All @@ -283,8 +331,8 @@ bool yukarin_sa_forward(int64_t length, int64_t *vowel_phoneme_list, int64_t *co
to_tensor(&model_speaker_id, speaker_shape)};
Ort::Value output_tensor = to_tensor(output, phoneme_shape);

status->yukarin_sa_list[model_index].Run(Ort::RunOptions{nullptr}, inputs, input_tensors.data(),
input_tensors.size(), outputs, &output_tensor, 1);
model.value().Run(Ort::RunOptions{nullptr}, inputs, input_tensors.data(), input_tensors.size(), outputs,
&output_tensor, 1);
} catch (const Ort::Exception &e) {
error_message = ONNX_ERR;
error_message += e.what();
Expand Down Expand Up @@ -346,6 +394,11 @@ bool decode_forward(int64_t length, int64_t phoneme_size, float *f0, float *phon
return false;
}
auto [model_index, model_speaker_id] = get_model_index_and_speaker_id(*speaker_id);
auto &model = status->decode_list[model_index];
if (!model) {
error_message = NOT_LOADED_ERR;
return false;
}
try {
// 音が途切れてしまうのを避けるworkaround処理が入っている
// TODO: 改善したらここのpadding処理を取り除く
Expand Down Expand Up @@ -381,8 +434,8 @@ bool decode_forward(int64_t length, int64_t phoneme_size, float *f0, float *phon
const char *inputs[] = {"f0", "phoneme", "speaker_id"};
const char *outputs[] = {"wave"};

status->decode_list[model_index].Run(Ort::RunOptions{nullptr}, inputs, input_tensor.data(), input_tensor.size(),
outputs, &output_tensor, 1);
model.value().Run(Ort::RunOptions{nullptr}, inputs, input_tensor.data(), input_tensor.size(), outputs,
&output_tensor, 1);

// TODO: 改善したらここのcopy処理を取り除く
copy_output_with_padding_to_output(output_with_padding, output, padding_f0_size);
Expand Down
20 changes: 19 additions & 1 deletion core/src/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,30 @@ typedef enum {
* @brief 音声合成するための初期化を行う。他の関数を正しく実行するには先に初期化が必要
* @param use_gpu trueならGPU用、falseならCPU用の初期化を行う
* @param cpu_num_threads 推論に用いるスレッド数を設定する。0の場合論理コア数の半分か、物理コア数が設定される
* @param load_all_models trueなら全てのモデルをロードする
* @return 成功したらtrue、失敗したらfalse
* @detail
* 何度も実行可能。use_gpuを変更して実行しなおすことも可能。
* 最後に実行したuse_gpuに従って他の関数が実行される。
*/
VOICEVOX_CORE_API bool initialize(bool use_gpu, int cpu_num_threads = 0);
VOICEVOX_CORE_API bool initialize(bool use_gpu, int cpu_num_threads = 0, bool load_all_models = true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#122 の通りCではデフォルト引数が使えないので使用は避けたほうが良いです。
次にデフォルト引数をつかっても関数のシグネチャは bool initialize(bool,int,bool) になってしまうためABI互換性が破壊されると思います。互換性を保つことを意図してるのであればinitiaize関数の宣言はこのままにし、新しい名前のinitialize関数を定義したほうが良いでしょう
互換性保たない方針にするのであればこれについてもprefix voicevox をつけたほうが良いと思います。

Copy link
Member Author

@Hiroshiba Hiroshiba May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

名前の違う似た動作をするinitialize関数が2つあると混乱しそうなので避けたいです。
引数が1つ、2つ、3つの関数をそれぞれ定義するのが良いのかなと感じました。
別PRでも良さそうですが、どうでしょう…?👀

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

実はCにはoverloadがないんですよ。initializeは extern "C" 内に宣言されてるのでそれをやろうとするとコンパイルエラーになるはずです

Copy link
Member Author

@Hiroshiba Hiroshiba May 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なんと・・・。
じゃあ・・・互換性を捨てますか・・・。

今回はvoicevox_のprefixを付けることで別関数を定義できますが、今後引数が増えるたびに考える必要があって開発が大変そうです。
コアを利用するニーズを加味して、引数を増やしたときの互換性は考えない(再コンパイルが必要な)ようにする方針で進めようかと思いました。

(以前もABI互換性などで議論があった記憶がありますが、もしそのときの判断と異なっていそうであればコメントいただけると助かります。)

voicevox_prefixを付けるのは別のPRで行いたいと思います!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ABI互換性は捨ててよいとは思いますが、コードをなるべく書き換えないように努めたほうが良さそうですね。リファクタリングの範疇になりますが。
リファクタリングといえば、initialize等古い関数は戻り値をboolではなくVoicevoxResultCodeに変えたい気持ちはありますね。これをすることによって last_error_message関数への依存を減らせますし

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VoicevoxResultCode統一、良さそうです!


/**
* モデルをロードする
* @param speaker_id 話者番号
* @return 成功したらtrue、失敗したらfalse
* @detail
* 必ずしも話者とモデルが1:1対応しているわけではない。
*/
VOICEVOX_CORE_API bool load_model(int64_t speaker_id);

/**
* @fn
* モデルがロード済みかどうか
* @param speaker_id 話者番号
* @return ロード済みならtrue、そうでないならfalse
*/
VOICEVOX_CORE_API bool is_model_loaded(int64_t speaker_id);
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved

/**
* @fn
Expand Down