Thread-Management
Inhalt
- Anzahl der Intra-Op-Threads festlegen
- Thread-Spinning-Verhalten
- Anzahl der Inter-Op-Threads festlegen
- Affinität von Intra-Op-Threads festlegen
- NUMA-Unterstützung und Leistungstuning
- Benutzerdefinierte Threading-Callbacks
- Verwendung in benutzerdefinierten Ops
Für den Standard-CPU-Ausführungsanbieter werden Standardeinstellungen bereitgestellt, um eine schnelle Inferenzleistung zu erzielen. Sie können die Leistung mit den folgenden Parametern in der API anpassen, um die Thread-Anzahl und andere Einstellungen zu steuern.
Python (Standards)
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-Thread-Anzahl
- Steuert die *Gesamtzahl* der INTRA-Threads, die zur Ausführung des Modells verwendet werden.
- INTRA = Parallelisierung der Berechnung *innerhalb* jedes Operators.
- Standard: (nicht angegeben oder 0).
sess_options.intra_op_num_threads = 0- INTRA-Threads insgesamt = Anzahl der physischen CPU-Kerne. Wenn der Standard beibehalten wird, wird auch eine gewisse Affinität aktiviert (siehe unten).
- Beispiel: 6-Kern-Maschine (mit 12 logischen HT-Prozessoren) = 6 INTRA-Threads insgesamt.
-
Sequentielle vs. parallele Ausführung
- Steuert, ob *mehrere* Operatoren im Graphen (*über* Knoten hinweg) sequenziell oder parallel ausgeführt werden.
- Standard:
sess_options.execution_mode = rt.ExecutionMode.ORT_SEQUENTIAL - Wenn ein Modell viele Verzweigungen aufweist, bietet die Einstellung dieser Option auf
ORT_PARALLELnormalerweise eine bessere Leistung. Dies kann bei einigen Modellen ohne viele Verzweigungen auch die Leistung beeinträchtigen. - Wenn
sess_options.execution_mode = rt.ExecutionMode.ORT_PARALLEL, können Siesess_options.inter_op_num_threadsfestlegen, um die Anzahl der Threads zu steuern, die zur Parallelisierung der Ausführung des Graphen (*über* Knoten hinweg) verwendet werden.
-
Graph-Optimierungsstufe
- Standard:
sess_options.graph_optimization_level = rt.GraphOptimizationLevel.ORT_ENABLE_ALLaktiviert alle Optimierungen. - Weitere Informationen finden Sie unter onnxruntime_c_api.h (Enum
GraphOptimizationLevel) für die vollständige Liste aller Optimierungsstufen. Details zu verfügbaren Optimierungen und deren Verwendung finden Sie in der Dokumentation zu den Graph-Optimierungen.
- Standard:
-
Thread-Pool-Spinning-Verhalten
- Steuert, ob zusätzliche INTRA- oder INTER-Threads darauf warten, dass Arbeit zugewiesen wird. Bietet schnellere Inferenz, verbraucht aber mehr CPU-Zyklen, Ressourcen und Energie.
- Standard: 1 (Aktiviert)
Anzahl der Intra-Op-Threads festlegen
Onnxruntime-Sitzungen nutzen Multithreading, um die Berechnung *innerhalb* jedes Operators zu parallelisieren.
Standardmäßig (mit intra_op_num_threads=0 oder nicht gesetzt) startet jede Sitzung mit dem Hauptthread auf dem ersten Kern (nicht affinitiert). Anschließend werden für jeden zusätzlichen physischen Kern zusätzliche Threads erstellt und diesen Kernen zugeordnet (1 oder 2 logische Prozessoren).
Kunden können die Gesamtzahl der Threads manuell konfigurieren, wie z. B.:
Python (unten) - C/C++ - .NET/C#
sess_opt = SessionOptions()
sess_opt.intra_op_num_threads = 3
sess = ort.InferenceSession('model.onnx', sess_opt)
Mit der oben genannten Konfiguration von 3 Threads insgesamt werden zwei zusätzliche Threads im zusätzlichen INTRA-Pool erstellt, sodass zusammen mit dem Hauptaufrufthread insgesamt drei Threads an der Intra-Op-Berechnung teilnehmen. Wenn der Kunde jedoch explizit die Anzahl der Threads festlegt, wie oben gezeigt, wird keine Affinität zu den erstellten Threads festgelegt.
Darüber hinaus ermöglicht Onnxruntime Kunden die Erstellung eines globalen Intra-Op-Thread-Pools, um übermäßige Konflikte zwischen Sitzungs-Thread-Pools zu vermeiden. Die Verwendung finden Sie hier.
Thread-Spinning-Verhalten
Steuert, ob zusätzliche INTRA- oder INTER-Threads darauf warten, dass Arbeit zugewiesen wird. Bietet schnellere Inferenz, verbraucht aber mehr CPU-Zyklen, Ressourcen und Energie.
Beispiel für das Deaktivieren des Spinnens, damit WorkerLoop keine zusätzlichen aktiven Zyklen mit Warten oder dem Versuch, Arbeit zu stehlen, verbraucht.
Python (unten) - C++ - .NET/C# - Schlüssel
sess_opt = SessionOptions()
sess_opt.AddConfigEntry(kOrtSessionOptionsConfigAllowIntraOpSpinning, "0")
sess_opt.AddConfigEntry(kOrtSessionOptionsConfigAllowInterOpSpinning, "0")
Anzahl der Inter-Op-Threads festlegen
Ein Inter-Op-Thread-Pool dient zur Parallelisierung *zwischen* Operatoren und wird nur erstellt, wenn der Sitzungsausführungsmodus auf parallel gesetzt ist.
Standardmäßig verfügt der Inter-Op-Thread-Pool ebenfalls über einen Thread pro physischem Kern.
Python (unten) - 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)
Affinität von Intra-Op-Threads festlegen
Normalerweise ist es am besten, die Thread-Affinität nicht festzulegen und das Betriebssystem die Thread-Zuweisung aus Leistungs- und Energiegründen übernehmen zu lassen. In bestimmten Szenarien kann es jedoch vorteilhaft sein, die Affinitäten von Intra-Op-Threads anzupassen, z. B.:
- Es werden mehrere Sitzungen parallel ausgeführt, und der Kunde möchte möglicherweise, dass seine Intra-Op-Thread-Pools auf separaten Kernen laufen, um Konflikte zu vermeiden.
- Der Kunde möchte einen Intra-Op-Thread-Pool auf nur einen der NUMA-Knoten beschränken, um den Aufwand für teure Cache-Fehltreffer zwischen den Knoten zu reduzieren.
Für den Intra-Op-Thread-Pool einer Sitzung lesen Sie bitte die Konfiguration und verwenden Sie sie wie folgt:
Python (unten) - C++ - .NET/C# - Schlüssel
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, ...)
Für den globalen Thread-Pool lesen Sie bitte die API und Verwendung.
NUMA-Unterstützung und Leistungstuning
Seit Version 1.14 kann der Onnxruntime-Thread-Pool alle physischen Kerne nutzen, die über NUMA-Knoten verfügbar sind. Der Intra-Op-Thread-Pool erstellt einen zusätzlichen Thread auf jedem physischen Kern (außer dem ersten Kern). Beispiel: Angenommen, es gibt ein System mit 2 NUMA-Knoten, von denen jeder 24 Kerne hat. Daher erstellt der Intra-Op-Thread-Pool 47 Threads und weist jedem Kern eine Affinität zu.
Für NUMA-Systeme wird empfohlen, einige Thread-Einstellungen zu testen, um die beste Leistung zu erzielen, da Threads, die auf verschiedene NUMA-Knoten verteilt sind, beim Zusammenwirken möglicherweise höhere Cache-Fehler-Overheads aufweisen. Wenn beispielsweise die Anzahl der Intra-Op-Threads 8 betragen muss, gibt es verschiedene Möglichkeiten, die Affinität festzulegen.
Python (unten) - 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, ...)
Tests haben gezeigt, dass die Festlegung der Affinität auf einen einzelnen NUMA-Knoten eine Leistungssteigerung von fast 20 Prozent gegenüber dem anderen Fall erzielt.
Benutzerdefinierte Threading-Callbacks
Gelegentlich möchten Benutzer ihre eigenen, fein abgestimmten Threads für das Multithreading verwenden. ORT bietet Thread-Erstellungs- und -Join-Callbacks in der C++ API an.
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);
...
}
Für den globalen Thread-Pool
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);
}
Beachten Sie, dass CreateThreadCustomized und JoinThreadCustomized, sobald sie festgelegt sind, einheitlich für sowohl die ORT-Intra-Op- als auch die Inter-Op-Thread-Pools gelten.
Verwendung in benutzerdefinierten Ops
Seit Version 1.17 können Entwickler benutzerdefinierter Ops ihren CPU-Code mit dem ORT-Intra-Op-Thread-Pool parallelisieren.
Verweisen Sie für die Verwendung auf die API und das Beispiel.