线程管理
目录
对于默认的 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 个逻辑处理器)。
用户可以手动配置线程总数,例如
sess_opt = SessionOptions()
sess_opt.intra_op_num_threads = 3
sess = ort.InferenceSession('model.onnx', sess_opt)
通过上述 3 个线程的总配置,额外的 INTRA 线程池中将创建两个额外线程,因此连同主调用线程,总共有三个线程参与算子内计算。但是,如果用户像上面展示的那样显式设置线程数,则不会为任何创建的线程设置亲和性。
此外,Onnxruntime 还允许用户创建一个全局算子内线程池,以防止会话线程池之间的过度竞争,请此处查看其用法。
线程自旋行为
控制额外的 INTRA 或 INTER 线程是否自旋等待工作。提供更快的推理速度,但消耗更多 CPU 周期、资源和电量。
禁用自旋的示例,因此 WorkerLoop 不会消耗额外的活动周期来等待或尝试窃取工作
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 节点上运行,以减少节点之间昂贵的缓存未命中开销。
对于会话算子内线程池,请阅读配置并像这样使用它
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 代码。