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
onnxruntime问题请教 #8
Comments
ENV确实应该是全局的。如果你是在一个类中使用ENV,那么ENV需要是类内全局的,它的生命周期和这个类一致。也就说,ENV必须是这个类的一个属性,而不应该是类的某个方法中临时变量。因为Session绑定了ENV,如果ENV是方法的临时变量,则在方法退出时会被销毁,此时Session就会指向一个不存在的资源。按照这个逻辑,如果你想实现一个类,在这个类中使用多线程,每个线程有单独的Session,可尝试的选择就是,让ENV成为这个类的属性,在每个子线程的Session共享这个ENV。如果ENV是属于线程,而Session的生命周期却不限于子线程,那么就有可能引发堆栈溢出,而ENV是类全局(类属性)时,则可避免这个问题,此时无论Session何时调用,都是指向有效的ENV。简单来说就是,如果你能确保ENV的生命周期大于等于你所使用的Session的生命周期,应该是比较合理的。 #ifdef ORT_API_MANUAL_INIT
const OrtApi* Global<T>::api_{};
inline void InitApi() { Global<void>::api_ = OrtGetApiBase()->GetApi(ORT_API_VERSION); }
#else
const OrtApi* Global<T>::api_ = OrtGetApiBase()->GetApi(ORT_API_VERSION);
#endif
// This returns a reference to the OrtApi interface in use, in case someone wants to use the C API functions
inline const OrtApi& GetApi() { return *Global<void>::api_; }
struct Session : Base<OrtSession> {
explicit Session(std::nullptr_t) {}
Session(Env& env, const ORTCHAR_T* model_path, const SessionOptions& options);
Session(Env& env, const void* model_data, size_t model_data_length, const SessionOptions& options);
// ...
}
struct Env : Base<OrtEnv> {
Env(std::nullptr_t) {}
Env(OrtLoggingLevel logging_level = ORT_LOGGING_LEVEL_WARNING, _In_ const char* logid = "");
// ...
}
template <typename T>
struct Base {
using contained_type = T;
Base() = default;
Base(T* p) : p_{p} {
if (!p)
ORT_CXX_API_THROW("Allocation failure", ORT_FAIL);
}
~Base() { OrtRelease(p_); }
// ...
}
// 官方案例片段
g_ort->ReleaseSessionOptions(session_options);
g_ort->ReleaseSession(session);
g_ort->ReleaseEnv(env); 官方案例已经转移至onnxruntime-inference-examples (似乎是一个礼拜前的事情,以前不放这,issue#8441) |
非常感谢大佬的详细解答,ort_useful_api.zh.md 这个文档我也比较仔细的看过了,应该目前关于onnxRuntime最详细的中文材料了,对于上面的解答是c接口下的使用来申请与释放资源的,我在解读了<onnxruntime/core/session/onnxruntime_cxx_api.h> |
感谢支持,通过阅读接口代码,找到了正确的使用方式 Ort::OrtRelease(m_session.release()); |
可以从ort_env.cc以及ort_env.h中看到OrtEnv实际上是 单例模式 + 引用计数 的模式: // 在 ort_env.h中的定义
struct OrtEnv {
public:
struct LoggingManagerConstructionInfo {
LoggingManagerConstructionInfo(OrtLoggingFunction logging_function1,
void* logger_param1,
OrtLoggingLevel default_warning_level1,
const char* logid1)
: logging_function(logging_function1),
logger_param(logger_param1),
default_warning_level(default_warning_level1),
logid(logid1) {}
OrtLoggingFunction logging_function{};
void* logger_param{};
OrtLoggingLevel default_warning_level;
const char* logid{};
};
static OrtEnv* GetInstance(const LoggingManagerConstructionInfo& lm_info,
onnxruntime::common::Status& status,
const OrtThreadingOptions* tp_options = nullptr);
static void Release(OrtEnv* env_ptr);
const onnxruntime::Environment& GetEnvironment() const {
return *(value_.get());
}
onnxruntime::logging::LoggingManager* GetLoggingManager() const;
void SetLoggingManager(std::unique_ptr<onnxruntime::logging::LoggingManager> logging_manager);
/**
* Registers an allocator for sharing between multiple sessions.
* Returns an error if an allocator with the same OrtMemoryInfo is already registered.
*/
onnxruntime::common::Status RegisterAllocator(onnxruntime::AllocatorPtr allocator);
/**
* Creates and registers an allocator for sharing between multiple sessions.
* Return an error if an allocator with the same OrtMemoryInfo is already registered.
*/
onnxruntime::common::Status CreateAndRegisterAllocator(const OrtMemoryInfo& mem_info,
const OrtArenaCfg* arena_cfg = nullptr);
private:
static OrtEnv* p_instance_; // 静态属性 类共享
static onnxruntime::OrtMutex m_;
static int ref_count_;
std::unique_ptr<onnxruntime::Environment> value_;
OrtEnv(std::unique_ptr<onnxruntime::Environment> value1);
~OrtEnv();
ORT_DISALLOW_COPY_AND_ASSIGNMENT(OrtEnv);
};
// 在ort_env.cc中的实现 增加引用计数 和 减少引用计数
OrtEnv* OrtEnv::GetInstance(const OrtEnv::LoggingManagerConstructionInfo& lm_info,
onnxruntime::common::Status& status,
const OrtThreadingOptions* tp_options) {
std::lock_guard<onnxruntime::OrtMutex> lock(m_);
if (!p_instance_) {
std::unique_ptr<LoggingManager> lmgr;
std::string name = lm_info.logid;
if (lm_info.logging_function) {
std::unique_ptr<ISink> logger = onnxruntime::make_unique<LoggingWrapper>(lm_info.logging_function,
lm_info.logger_param);
lmgr.reset(new LoggingManager(std::move(logger),
static_cast<Severity>(lm_info.default_warning_level),
false,
LoggingManager::InstanceType::Default,
&name));
} else {
#ifdef __ANDROID__
ISink* sink = new AndroidLogSink();
#else
ISink* sink = new CLogSink();
#endif
lmgr.reset(new LoggingManager(std::unique_ptr<ISink>{sink},
static_cast<Severity>(lm_info.default_warning_level),
false,
LoggingManager::InstanceType::Default,
&name));
}
std::unique_ptr<onnxruntime::Environment> env;
if (!tp_options) {
status = onnxruntime::Environment::Create(std::move(lmgr), env);
} else {
status = onnxruntime::Environment::Create(std::move(lmgr), env, tp_options, true);
}
if (!status.IsOK()) {
return nullptr;
}
p_instance_ = new OrtEnv(std::move(env));
}
++ref_count_;
return p_instance_;
}
void OrtEnv::Release(OrtEnv* env_ptr) {
if (!env_ptr) {
return;
}
std::lock_guard<onnxruntime::OrtMutex> lock(m_);
ORT_ENFORCE(env_ptr == p_instance_); // sanity check
--ref_count_;
if (ref_count_ == 0) {
delete p_instance_;
p_instance_ = nullptr;
}
}
// 真正析构时卸载一些全局资源,并且按照c++的析构规则,在调用delete后,会析构类的成员等等
OrtEnv::~OrtEnv() {
// We don't support any shared providers in the minimal build yet
#if !defined(ORT_MINIMAL_BUILD)
UnloadSharedProviders();
#endif
} 然后我们再来看最外层的接口是怎么导向至上面的源码的 ,首先看在onnxruntime_cxx_api.h中,Env的其中一个构造函数为: inline Env::Env(OrtLoggingLevel logging_level, _In_ const char* logid) {
ThrowOnError(GetApi().CreateEnv(logging_level, logid, &p_));
if (strcmp(logid, "onnxruntime-node") == 0) {
ThrowOnError(GetApi().SetLanguageProjection(p_, OrtLanguageProjection::ORT_PROJECTION_NODEJS));
} else {
ThrowOnError(GetApi().SetLanguageProjection(p_, OrtLanguageProjection::ORT_PROJECTION_CPLUSPLUS));
}
} 里面调用了CreateEnv,而这个函数的实现可以在onnxruntime_c_api.cc中找到,它长这样: ORT_API_STATUS_IMPL(OrtApis::CreateEnv, OrtLoggingLevel logging_level,
_In_ const char* logid, _Outptr_ OrtEnv** out) {
API_IMPL_BEGIN
OrtEnv::LoggingManagerConstructionInfo lm_info{nullptr, nullptr, logging_level, logid};
Status status;
*out = OrtEnv::GetInstance(lm_info, status);
return ToOrtStatus(status);
API_IMPL_END
} 这里给out赋值了一个单例的指针并且会在GetInstance方法中增加引用计数,这个单例就是 OrtEnv的p_instance_。另外由于直接暴露给用户使用的是Env,我们看到: struct Env : Base<OrtEnv> {} ;
// Base的实现
template <typename T>
struct Base {
using contained_type = T;
Base() = default;
Base(T* p) : p_{p} {
if (!p)
ORT_CXX_API_THROW("Allocation failure", ORT_FAIL);
}
~Base() { OrtRelease(p_); }
operator T*() { return p_; }
operator const T*() const { return p_; }
T* release() {
T* p = p_;
p_ = nullptr;
return p;
}
protected:
Base(const Base&) = delete;
Base& operator=(const Base&) = delete;
Base(Base&& v) noexcept : p_{v.p_} { v.p_ = nullptr; }
void operator=(Base&& v) noexcept {
OrtRelease(p_);
p_ = v.p_;
v.p_ = nullptr;
}
T* p_{};
template <typename>
friend struct Unowned; // This friend line is needed to keep the centos C++ compiler from giving an error
}; Base是一个简单的Wrapper,Env 是 Base,这意味着 Env其实只是持有一个指向真正OrtEnv的指针,它不是OrtEnv本身。当你调用 Env的release方法是,会将Env持有的p_置为nullptr,并且返回指向真实OrtEnv的指针p; 所以在外部就可以像你这里用的那样: Ort::OrtRelease(m_env.release()); 不过其实你看Env的析构函数,干的正是这件事情。 ~Base() { OrtRelease(p_); } emmmmmm,所以我觉得,在C++里,如果你是在栈上用Env或Session,等它退出作用域就会自用调用OrtRelease,不需要手动释放。如果是new出来的,在delete时会也会进入析构函数,从而调用OrtRelease. Env持有的OrtEnv是全局的,无论你在父线程使用还是子线程使用,其实使用的都是同一个单例,它的引用计数会在加锁情况下进行增减,你Release时只是减少了引用计数,只有计数为0时才会释放真正的资源。这其实也解释了,如果你时在一个类的方法中生成一个临时的Env,会有问题,因为在生成时,引用计数为1,退出方法时,析构了一次,调用了OrtRelease一次(也就是调用了OrtApis::ReleaseEnv -> 内部再调用了 OrtEnv::Release),引用计数变为0,这个全局的资源被释放了,就会有问题。但Env若作为类的属性,那么在退出类的某个方法时,Env并没有被析构,也就不会有问题。需要注意的是,Session、OrtValue、OrtRunOptions等不是全局的,当你调用Release的时候是真的释放了资源: #define DEFINE_RELEASE_ORT_OBJECT_FUNCTION(INPUT_TYPE, REAL_TYPE) \
ORT_API(void, OrtApis::Release##INPUT_TYPE, _Frees_ptr_opt_ Ort##INPUT_TYPE* value) { \
delete reinterpret_cast<REAL_TYPE*>(value); \
}
DEFINE_RELEASE_ORT_OBJECT_FUNCTION(Value, OrtValue)
DEFINE_RELEASE_ORT_OBJECT_FUNCTION(RunOptions, OrtRunOptions)
DEFINE_RELEASE_ORT_OBJECT_FUNCTION(Session, ::onnxruntime::InferenceSession)
DEFINE_RELEASE_ORT_OBJECT_FUNCTION(ModelMetadata, ::onnxruntime::ModelMetadata) 可以看到,这里是使用了delete来销毁,按照c++的析构规则,在调用delete后,会析构类的成员等等。其实Sesssion的p指向了OrtSeesion,而OrtSeesion实际上指向了InferenceSession,我们去挖InferenceSession时的析构时,发现它只是记录了些log,并没有释放资源的行为: // 看起来释放了个寂寞
InferenceSession::~InferenceSession() {
if (session_options_.enable_profiling) {
ORT_TRY {
EndProfiling();
}
ORT_CATCH(const std::exception& e) {
// TODO: Currently we have no way to transport this error to the API user
// Maybe this should be refactored, so that profiling must be explicitly
// started and stopped via C-API functions.
// And not like now a session option and therefore profiling must be started
// and stopped implicitly.
ORT_HANDLE_EXCEPTION([&]() {
LOGS(*session_logger_, ERROR) << "Error during EndProfiling(): " << e.what();
});
}
ORT_CATCH(...) {
LOGS(*session_logger_, ERROR) << "Unknown error during EndProfiling()";
}
}
#ifdef ONNXRUNTIME_ENABLE_INSTRUMENT
if (session_activity_started_)
TraceLoggingWriteStop(session_activity, "OrtInferenceSessionActivity");
#endif
#if !defined(ORT_MINIMAL_BUILD) && defined(ORT_MEMORY_PROFILE)
MemoryInfo::GenerateMemoryProfile();
#endif
} 但是按照c++的析构规则,在调用delete后,会析构类的成员。所以其实Session中的持有资源的成员都是被释放了的,这个Session应该无法使用了. 但是这里面还有个问题,就是InferenceSession内持有一个SessionOptions,在析构InferenceSession时也会被析构。但其实这个SessionOptions和你外部生成的SessionOptions不是同一个,他只是你传入的SessionOptions的一个拷贝。 // use the constructed session options
finalized_session_options = constructed_session_options;
} else {
// use user provided session options instance
finalized_session_options = user_provided_session_options; // 不展开所有源码了。这里使用结构体默认的拷贝
... 所以当InferenceSession被析构后,它持有的SessionOptions被析构了,但你在外部生成的那个SessionOptions并没有被析构。所以你在外部还可以(还需要)释放这个SessionOptions。 然而你看: struct SessionOptions : Base<OrtSessionOptions> {};
~Base() { OrtRelease(p_); }
// 再加上一串宏定义和跳转最终调用了
ORT_API(void, OrtApis::ReleaseSessionOptions, _Frees_ptr_opt_ OrtSessionOptions* ptr) {
delete ptr;
} SessionOptions的默认析构行为正和你手动调用OrtRelease是一致的,所以,大部分情况下,应该按照正常的c++语法规则来使用就可以了。 |
感谢大佬详细的解读源码分析,但是我昨天通过试验发现一个问题,在调用onnruntime后显存无法全部释放,我的代码流程如下
深度学习模型squeezenet分类算法 显存变化情况如下:创建session成功:499M 完整代码如下所示:
|
哈哈,我也不是大佬,也就是业余玩一下~ 关于内存的问题的,我也没研究太深,你可以看下 onnxruntime内存增长问题探讨 资料。欢迎分享新知识,共同学习~🙃🙃🙃 |
请问一下 在include lite.h这一步出了问题 |
每个类已经添加LITE_EXPORTS来兼容不同的操作系统。我没有在windows上玩过,你可能需要检测一下动态库链接有没有正确。方便把报错的log放上来吗? |
我也想请教大佬一个类似的问题,在我的应用场景中,希望session在进程中只创建一次然后可以在进程内的不同个线程多次使用,但是在实现的时候发现session的声明和定义无法分开进行,只能Ort:: Session session(env,model_path,session_options)声明和定义一块完成,这是不是意味着,session的每一次Run都要加载一次模型,感觉很浪费资源,刚接触c++部署,很多地方都不熟悉,希望大佬能指点一二! |
试试使用指针的方式,声明的时候
定义的时候
|
也可以使用模型路径的方式定义和初始化模型:
|
多谢解答!这就试试 |
|
能否加下微信 |
Ort::Env m_env;
Ort::Session m_session;
请问这两个关系是怎么样的,之前看onnxruntime的文档介绍,Ort::Env是一个全局唯一的,如果要实现一个生产者消费者的推理模块来扩大推理引擎的并发性,是不是所有线程共用一个Ort::Env,每个消费者线程新建一个Ort::Session对象?麻烦不吝指教
The text was updated successfully, but these errors were encountered: