线程管理

目录

对于默认的 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 线程的数。
    • INTRA = 并行化每个运算符内部的计算
    • 默认值:(未指定或 0)。sess_options.intra_op_num_threads = 0
      • INTRA 线程总数 = 物理 CPU 核心数。保留为默认值也会启用一些亲和性(如下所述)
      • 例如,6 核机器(具有 12 个 HT 逻辑处理器)= 6 个 INTRA 线程总数
  • 顺序执行与并行执行

    • 控制图中多个运算符(节点)是顺序运行还是并行运行。
    • 默认值: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 还允许客户创建全局操作内线程池,以防止会话线程池之间过热的争用,请在此处找到其用法 here

线程自旋行为

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

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

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

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# - Keys

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示例