Thread-Management

Inhalt

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_PARALLEL normalerweise 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 Sie sess_options.inter_op_num_threads festlegen, 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_ALL aktiviert 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.
  • 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.