线程管理
目录
对于默认的 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 个逻辑处理器)。
客户可以手动配置线程总数,例如
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")
设置操作间线程数
操作间线程池用于运算符之间的并行性,并且仅当会话执行模式设置为并行时才会创建
默认情况下,操作间线程池也将为每个物理核心提供一个线程。
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, ...)
Numa 支持和性能调优
自 1.14 版本起,Onnxruntime 线程池可以利用 NUMA 节点上所有可用的物理核心。操作内线程池将在每个物理核心(第一个核心除外)上创建一个额外的线程。例如,假设有一个由 2 个 NUMA 节点组成的系统,每个节点有 24 个核心。因此,操作内线程池将创建 47 个线程,并将线程关联性设置为每个核心。
对于 NUMA 系统,建议测试一些线程设置以探索最佳性能,因为在 NUMA 节点之间分配的线程在彼此协作时可能会有更高的缓存未命中开销。例如,当操作内线程数必须为 8 时,有不同的方法来设置关联性
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);
}
请注意,CreateThreadCustomized
和 JoinThreadCustomized
一旦设置,将统一应用于 ORT 操作内和操作间线程池。
在自定义操作中的用法
自 1.17 起,自定义操作开发人员有权使用 ort 操作内线程池并行化其 cpu 代码。