线程管理

目录

对于默认的 CPU 执行提供者,提供了默认设置以获得快速推理性能。您可以使用 API 中的以下旋钮自定义性能,以控制线程数和其他设置

Python (默认设置)

import onnxruntime as rt

sess_options = rt.SessionOptions()

sess_options.intra_op_num_threads = 0
sess_options.execution_mode = rt.ExecutionMode.ORT_SEQUENTIAL
sess_options.graph_optimization_level = rt.GraphOptimizationLevel.ORT_ENABLE_ALL
sess_options.add_session_config_entry("session.intra_op.allow_spinning", "1")
  • 算子内线程数

    • 控制用于运行模型的 INTRA 线程的总数
    • INTRA = 并行化每个算子内部的计算
    • 默认值:(未指定或 0)。sess_options.intra_op_num_threads = 0
      • INTRA 线程总数 = 物理 CPU 核数。保留默认值还会启用一些亲和性设置(如下所述)
      • 例如:6 核机器(带 12 个 HT 逻辑处理器)= 6 个 INTRA 线程总数
  • 顺序执行 vs 并行执行

    • 控制图中多个算子(节点)是顺序运行还是并行运行。
    • 默认值:sess_options.execution_mode = rt.ExecutionMode.ORT_SEQUENTIAL
    • 通常当模型有许多分支时,将此选项设置为 ORT_PARALLEL 会提供更好的性能。对于分支不多的某些模型,这可能会损害性能。
    • sess_options.execution_mode = rt.ExecutionMode.ORT_PARALLEL 时,您可以设置 sess_options.inter_op_num_threads 来控制用于并行化图执行(节点)的线程数。
  • 图优化级别

    • 默认值:sess_options.graph_optimization_level = rt.GraphOptimizationLevel.ORT_ENABLE_ALL 启用所有优化。
    • 有关所有优化级别的完整列表,请参阅 onnxruntime_c_api.h(枚举 GraphOptimizationLevel)。有关可用优化和用法的详细信息,请参阅图优化文档。
  • 线程池自旋行为

    • 控制额外的 INTRA 或 INTER 线程是否自旋等待工作。提供更快的推理速度,但消耗更多 CPU 周期、资源和电量
    • 默认值:1(启用)

设置算子内线程数

Onnxruntime 会话利用多线程来并行化每个算子内部的计算。

默认情况下,当 intra_op_num_threads=0 或未设置时,每个会话将从第一个核心上的主线程开始(不设置亲和性)。然后为每个额外的物理核心创建额外的线程,并将其亲和到该核心(1 或 2 个逻辑处理器)。

用户可以手动配置线程总数,例如

Python(如下)- C/C++ - .NET/C#

sess_opt = SessionOptions()
sess_opt.intra_op_num_threads = 3
sess = ort.InferenceSession('model.onnx', sess_opt)

通过上述 3 个线程的总配置,额外的 INTRA 线程池中将创建两个额外线程,因此连同主调用线程,总共有三个线程参与算子内计算。但是,如果用户像上面展示的那样显式设置线程数,则不会为任何创建的线程设置亲和性。

此外,Onnxruntime 还允许用户创建一个全局算子内线程池,以防止会话线程池之间的过度竞争,请此处查看其用法。

线程自旋行为

控制额外的 INTRA 或 INTER 线程是否自旋等待工作。提供更快的推理速度,但消耗更多 CPU 周期、资源和电量。

禁用自旋的示例,因此 WorkerLoop 不会消耗额外的活动周期来等待或尝试窃取工作

Python(如下)- C++ - .NET/C# -

sess_opt = SessionOptions()
sess_opt.AddConfigEntry(kOrtSessionOptionsConfigAllowIntraOpSpinning, "0")
sess_opt.AddConfigEntry(kOrtSessionOptionsConfigAllowInterOpSpinning, "0")

设置算子间线程数

算子间线程池用于算子之间的并行,并且只会在会话执行模式设置为并行时创建

默认情况下,算子间线程池也将为每个物理核心配备一个线程。

Python(如下)- C/C++ - .NET/C#

sess_opt = SessionOptions()
sess_opt.execution_mode  = ExecutionMode.ORT_PARALLEL
sess_opt.inter_op_num_threads = 3
sess = ort.InferenceSession('model.onnx', sess_opt)

设置算子内线程亲和性

通常最好不要设置线程亲和性,让操作系统出于性能和功耗原因处理线程分配。但是,在某些情况下,自定义算子内线程亲和性可能会有益,例如

  • 当有多个会话并行运行时,用户可能希望其算子内线程池在单独的核心上运行以避免竞争。
  • 用户希望将算子内线程池限制为仅在一个 NUMA 节点上运行,以减少节点之间昂贵的缓存未命中开销。

对于会话算子内线程池,请阅读配置并像这样使用它

Python(如下)- C++ - .NET/C# -

sess_opt = SessionOptions()
sess_opt.intra_op_num_threads = 3
sess_opt.add_session_config_entry('session.intra_op_thread_affinities', '1;2')
sess = ort.InferenceSession('model.onnx', sess_opt, ...)

对于全局线程池,请阅读API用法

NUMA 支持和性能调优

自 1.14 版本发布以来,Onnxruntime 线程池可以利用 NUMA 节点上所有可用的物理核心。算子内线程池将在每个物理核心上(除了第一个核心)创建一个额外的线程。例如,假设有一个包含 2 个 NUMA 节点的系统,每个节点有 24 个核心。因此,算子内线程池将创建 47 个线程,并为每个核心设置线程亲和性。

对于 NUMA 系统,建议测试几种线程设置以探索最佳性能,因为在 NUMA 节点之间分配的线程在相互协作时可能会有更高的缓存未命中开销。例如,当算子内线程数必须为 8 时,有不同的设置亲和性的方法

Python(如下)- C++ - .NET/C#

sess_opt = SessionOptions()
sess_opt.intra_op_num_threads = 8
sess_opt.add_session_config_entry('session.intra_op_thread_affinities', '3,4;5,6;7,8;9,10;11,12;13,14;15,16') # set affinities of all 7 threads to cores in the first NUMA node
# sess_opt.add_session_config_entry('session.intra_op_thread_affinities', '3,4;5,6;7,8;9,10;49,50;51,52;53,54') # set affinities for first 4 threads to the first NUMA node, and others to the second
sess = ort.InferenceSession('resnet50.onnx', sess_opt, ...)

测试表明,将亲和性设置为单个 NUMA 节点比其他情况有近 20% 的性能提升。

自定义线程回调

有时,用户可能更喜欢使用自己的精调线程进行多线程处理。ORT 在 C++ API 中提供了线程创建和加入回调。

std::vector<std::thread> threads;
void* custom_thread_creation_options = nullptr;
// initialize custom_thread_creation_options

// On thread pool creation, ORT calls CreateThreadCustomized to create a thread
OrtCustomThreadHandle CreateThreadCustomized(void* custom_thread_creation_options, OrtThreadWorkerFn work_loop, void* param) {
    threads.push_back(std::thread(work_loop, param));
    // configure the thread by custom_thread_creation_options
    return reinterpret_cast<OrtCustomThreadHandle>(threads.back().native_handle());
}

// On thread pool destruction, ORT calls JoinThreadCustomized for each created thread
void JoinThreadCustomized(OrtCustomThreadHandle handle) {
    for (auto& t : threads) {
    if (reinterpret_cast<OrtCustomThreadHandle>(t.native_handle()) == handle) {
        // recycling resources ... 
        t.join();
    }
    }
}

int main(...) {
    ...
    Ort::Env ort_env;
    Ort::SessionOptions session_options;
    session_options.SetCustomCreateThreadFn(CreateThreadCustomized);
    session_options.SetCustomThreadCreationOptions(&custom_thread_creation_options);
    session_options.SetCustomJoinThreadFn(JoinThreadCustomized);
    Ort::Session session(*ort_env, MODEL_URI, session_options);
    ...
}

对于全局线程池

int main() {
    const OrtApi* g_ort = OrtGetApiBase()->GetApi(ORT_API_VERSION);
    OrtThreadingOptions* tp_options = nullptr;
    g_ort->CreateThreadingOptions(&tp_options);
    g_ort->SetGlobalCustomCreateThreadFn(tp_options, CreateThreadCustomized);
    g_ort->SetGlobalCustomThreadCreationOptions(tp_options, &custom_thread_creation_options);
    g_ort->SetGlobalCustomJoinThreadFn(tp_options, JoinThreadCustomized);
    // disable per-session thread pool, create a session for inferencing
    g_ort->ReleaseThreadingOptions(tp_options);
}

请注意,CreateThreadCustomizedJoinThreadCustomized 一旦设置,将统一应用于 ORT 算子内和算子间线程池。

在自定义算子中的使用

自 1.17 版本起,自定义算子开发者有权使用 ORT 算子内线程池并行化其 CPU 代码。

有关用法,请参阅API示例