Quantization

Quantization

Um das ONNX-Modell auf einer CPU um bis zu 4x zu beschleunigen, ist eine statische oder dynamische Quantisierung in das INT8-Format erforderlich. Anstatt rechenintensive 32-Bit-Gleitkommazahlen (FP32) zu verwenden, führt der Prozessor Berechnungen mit effizienten 8-Bit-Ganzzahlen (INT8) durch. Moderne Prozessoren (sowohl Intel als auch AMD) nutzen integrierte Befehle (AVX-512, VNNI), um solche Operationen hardwareseitig mit enormer Geschwindigkeit auszuführen. Wir verwenden das offizielle ONNX Runtime Quantization Tool.

1. Quantisierungsskript. Wir verwenden das zuvor erhaltene Modell. (quantize_onnx.py)

Um eine hohe Präzision des fraktalen Routers zu gewährleisten, verwenden wir dynamische Quantisierung. Dabei werden die Layer-Gewichte im Voraus in INT8 konvertiert und die Aktivierungen dynamisch quantisiert, was ideal für CPUs ist.


import os
from onnxruntime.quantization import quantize_dynamic, QuantType

def quantize_model(model_fp32_path="onnx_models/fractal_net.onnx", model_int8_path="onnx_models/fractal_net_int8.onnx"):
    """
    Wendet die dynamische INT8-Quantisierung auf das FP32 ONNX-Modell an.
    Optimiert für mehrere CPU-Beschleunigungen.
    """
    if not os.path.exists(model_fp32_path):
        print(f"Fehler: Quellmodell '{model_fp32_path}' nicht gefunden. Führen Sie zuerst export_onnx.py aus.")
        return

    print("Starte den Prozess der Quantisierung des Modells auf INT8...")

    # Starten des ONNX Runtime Optimizers
    quantize_dynamic(
        model_input=model_fp32_path,        # Pfad zum ursprünglichen FP32-Modell
        model_output=model_int8_path,       # Pfad zum Speichern des INT8-Modells
        weight_type=QuantType.QUInt8        # Gewichte in 8-Bit-Ganzzahlen ohne Vorzeichen umwandeln
    )

    # Vergleichen Sie die Dateigrößen auf der Festplatte
    size_fp32 = os.path.getsize(model_fp32_path) / (1024 * 1024)
    size_int8 = os.path.getsize(model_int8_path) / (1024 * 1024)

    print("\n--- Quantisierung erfolgreich abgeschlossen! ---")
    print(f"Originale Modellgröße (FP32): {size_fp32:.2f} MB")
    print(f"Komprimierte Modellgröße (INT8): {size_int8:.2f} MB")
    print(f"Das Modell hat sich um etwa {size_fp32/size_int8:.1f} Mal verkleinert.")

if __name__ == "__main__":
    quantize_model()
              

2. Geschwindigkeitsmessung und Modellverifizierung (benchmark_onnx.py)

Um die Geschwindigkeitssteigerung um bis zu das Vierfache zu visualisieren, schreiben wir ein Skript, das 500 Inferenziterationen sowohl auf FP32- als auch auf INT8-Modellen durchführt, die Prozessorausführungszeit mit reinem NumPy misst und die endgültige Differenz anzeigt.


import time
import numpy as np
import onnxruntime as ort

def benchmark_models(fp32_path="onnx_models/fractal_net.onnx", int8_path="onnx_models/fractal_net_int8.onnx"):
    # 1. Vorbereitung einer Testcharge (Größe 1, 3 Kanäle, 128x128)
    fake_image = np.random.randn(1, 3, 128, 128).astype(np.float32)

    # Konfigurieren eines reinen CPU-Starts mit Thread-Optimierung
    opts = ort.SessionOptions()
    opts.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
    opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL

    # Wir erstellen Sitzungen für beide Modelle
    session_fp32 = ort.InferenceSession(fp32_path, sess_options=opts, providers=['CPUExecutionProvider'])
    session_int8 = ort.InferenceSession(int8_path, sess_options=opts, providers=['CPUExecutionProvider'])

    input_name = session_fp32.get_inputs()[0].name
    inputs = {input_name: fake_image}

    # Anzahl der Testzyklen
    num_runs = 500

    print(f"Benchmark wird auf der CPU ausgeführt ({num_runs} Läufe)...")

    # Test 1: Basismodell FP32
    start_time = time.time()
    for _ in range(num_runs):
        _ = session_fp32.run(None, inputs)
    fp32_time = time.time() - start_time

    # Test 2: Beschleunigtes INT8-Modell
    start_time = time.time()
    for _ in range(num_runs):
        _ = session_int8.run(None, inputs)
    int8_time = time.time() - start_time

    # Berechnung der endgültigen Leistungskennzahlen
    fps_fp32 = num_runs / fp32_time
    fps_int8 = num_runs / int8_time
    speedup = fps_int8 / fps_fp32

    print("\n--- Ergebnisse des CPU-Leistungstests ---")
    print(f"Normales Modell (FP32): Gesamtzeit: {fp32_time: .3f} Sek. | Geschwindigkeit: {fps_fp32: .1f} FPS")
    print(f"Beschleunigtes Modell (INT8): Gesamtzeit: {int8_time: .3f} Sek. | Geschwindigkeit: {fps_int8: .1f} FPS")
    print(f"Finale Hardwarebeschleunigung auf der CPU: {speedup:.2f} mal!")

if __name__ == "__main__":
    benchmark_models()
              

3. Überprüfung auf Speicherlecks

Memray ist ein leistungsstarker, moderner Speicherprofiler. Er kann Speicherzuweisungen nicht nur im Python-Code nachverfolgen, sondern auch innerhalb nativer C/C++ Erweiterungsmodule (wie NumPy, PyTorch oder Treibern) – Bereiche, in die Standard-Python-Tools keinen Einblick gewähren.


pip install memray
                          

memray run benchmark_onnx.py
                          

Um Allokationen zu erfassen, die innerhalb der C++-Binärdateien von onnxruntime stattfinden, müssen Sie beim Ausführen von Memray das Flag --native verwenden.


memray run --native benchmark_onnx.py
                          

memray flamegraph memray_benchmark_onnx.bin.*
                          

Ergebnisse prüfen (memray-flamegraph-benchmark_onnx.html):

  • Beachten Sie die C++ Symbole: Da Sie die Option --native angegeben haben, sehen Sie neben Python-Funktionsframes wie `run_inference_loop` auch tief verschachtelte Aufrufstapel, die bis zu Funktionen in `onnxruntime.capsule` zurückreichen, die von `libonnxruntime.so` stammen.
  • Speicherlecks finden: Achten Sie auf breite Bereiche im oberen Bereich des Flammendiagramms, die mit der Zeit nicht verschwinden. Wenn ein onnxruntime-Speicherleck durch eine nicht beendete Sitzung verursacht wird, sehen Sie, wie Speicherbelegungen, die mit C++-Klassen wie onnxruntime::InferenceSession oder Speicherverwaltungspools (onnxruntime::AllocatorPtr) verbunden sind, einen großen Anteil des Gesamtspeichers belegen.

Verwendung in der Production

Einsatz in der Produktion. Sehen wir uns an, wie wir eine Produktionsumgebung einrichten können. Nächster Beitrag