執行緒管理

目錄

對於預設的 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示例