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