\n\n\n\n Die Geschwindigkeit der Inferenz entfesseln: ein praktisches Tutorial zur GPU-Optimierung - AgntMax \n

Die Geschwindigkeit der Inferenz entfesseln: ein praktisches Tutorial zur GPU-Optimierung

📖 13 min read2,472 wordsUpdated Mar 29, 2026

Einführung: Die Suche nach schnellerer Inferenz

Im schnelllebigen Universum der künstlichen Intelligenz ist das Trainieren von Modellen nur die halbe Miete. Das wahre Maß für den Nutzen eines Modells liegt oft in seiner Fähigkeit, Inferenz — Vorhersagen zu treffen oder Ergebnisse zu generieren — schnell und effizient durchzuführen. Für viele Anwendungen in der realen Welt, von der Echtzeit-Objekterkennung bis hin zu den Antworten großer Sprachmodelle, ist die Inferenzgeschwindigkeit von entscheidender Bedeutung. Obwohl die CPU-basierte Inferenz ihre Bedeutung hat, macht die parallele Verarbeitungsleistung von Grafikprozessoren (GPU) sie zu den unbestrittenen Champions der hochdurchsatzfähigen und latenzarmen KI-Inferenz.

Dieses Tutorial wird Sie durch praktische Strategien und Techniken führen, um die Nutzung von GPUs während der Inferenz zu optimieren. Wir werden über theoretische Konzepte hinausgehen und konkrete Schritte erkunden, begleitet von Codebeispielen, um Ihnen zu helfen, die maximale Leistung aus Ihrer Hardware herauszuholen. Am Ende werden Sie ein solides Verständnis dafür haben, wie Sie Engpässe identifizieren und effektive Optimierungen für Ihre Deep-Learning-Inferenzlasten implementieren können.

Engpässe bei der GPU-Inferenz verstehen

Bevor Sie optimieren, ist es entscheidend zu verstehen, was Ihre Inferenz verlangsamen könnte. Die GPU-Inferenz wird nicht immer durch die Berechnung limitiert; oft wirken andere Faktoren als Engpässe. Zu den häufigsten Übeltätern gehören:

  • Datenübertragung (Host zu Gerät / Gerät zu Host): Die Bewegung von Daten zwischen dem CPU-Speicher (Host) und dem GPU-Speicher (Gerät) ist langsam. Minimieren Sie dies.
  • Kleine Batch-Größe: GPUs gedeihen durch Parallelität. Sehr kleine Batch-Größen könnten die Recheneinheiten der GPU nicht vollständig ausnutzen.
  • Kernel-Nutzlasten: Jedes Mal, wenn ein GPU-Kernel (ein kleines Programm, das auf der GPU ausgeführt wird) gestartet wird, gibt es eine kleine Überlastung. Viele kleine und sequenzielle Operationen können eine signifikante Überlastung ansammeln.
  • Speicherzugriffsmuster: Ineffizienter Speicherzugriff (z. B. nicht zusammenhängende Lesevorgänge) kann zu Cache-Fehlern und langsamerer Leistung führen.
  • Untergenutzte Recheneinheiten: Die Architektur des Modells oder die Inferenzstrategie könnten die Verarbeitungsleistung der GPU nicht vollständig ausnutzen.
  • Dynamische Formen/Kontrollfluss: Operationen, die die Kompilierung statischer Graphen verhindern (z. B. if-else-Zweige basierend auf Eingabedaten), können die Optimierung behindern.
  • Framework-Überlastung: Das Deep-Learning-Framework selbst könnte Überlastungen einführen.

Praktische Optimierungsstrategien

1. Modellquantifizierung: Ihren Fußabdruck reduzieren und die Geschwindigkeit erhöhen

Die Quantifizierung ist der Prozess der Reduzierung der Genauigkeit der Zahlen, die verwendet werden, um die Gewichte und Aktivierungen eines Modells darzustellen, normalerweise von 32-Bit-Gleitkommazahlen (FP32) auf Formate mit niedrigerer Genauigkeit wie 16-Bit-Gleitkommazahlen (FP16 oder BFloat16) oder 8-Bit-Ganzzahlen (INT8). Dies bietet mehrere Vorteile:

  • Reduzierung des Speicherfußabdrucks: Kleinere Modelle benötigen weniger Speicher, was größere Batch-Größen oder den Einsatz auf ressourcenbeschränkten Geräten ermöglicht.
  • Schnellere Berechnung: Operationen mit geringerer Genauigkeit sind in der Regel schneller und verbrauchen weniger Energie. Moderne GPUs verfügen oft über spezialisierte Hardware (z. B. Tensor Cores) für FP16- und INT8-Operationen.
  • Reduzierung des Datentransfers: Es müssen weniger Daten bewegt werden.

Beispiel: Quantifizierung mit PyTorch (FP16)

Die meisten modernen GPUs unterstützen FP16 (Halbgenauigkeit). PyTorch erleichtert die Konvertierung Ihres Modells.


import torch
import torch.nn as nn

# Angenommen, 'model' ist Ihr trainiertes PyTorch-Modell (z. B. ein ResNet)
model = nn.Sequential(
 nn.Linear(784, 128),
 nn.ReLU(),
 nn.Linear(128, 10)
)
model.eval() # Setzen Sie das Modell in den Evaluierungsmodus

# Modell auf die GPU verschieben
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)

# Option 1: Automatische gemischte Genauigkeit (AMP) für die Inferenz
# Dies wird normalerweise empfohlen, da es die Konvertierungen nur dort verwaltet, wo es vorteilhaft ist
from torch.cuda.amp import autocast

# Beispiel einer Inferenzschleife mit AMP
input_data = torch.randn(64, 784).to(device)

with autocast():
 output = model(input_data)
print(f"AMP-Inferenzausgabetyp: {output.dtype}")

# Option 2: Das gesamte Modell explizit in FP16 konvertieren (weniger verbreitet für die Inferenz)
# model_fp16 = model.half() # Konvertiert alle Parameter und Puffer in FP16
# input_data_fp16 = input_data.half()
# output_fp16 = model_fp16(input_data_fp16)
# print(f"Expliziter FP16-Inferenzausgabetyp: {output_fp16.dtype}")

# Für die INT8-Quantifizierung würden Sie normalerweise die nativen Quantifizierungstools von PyTorch verwenden
# oder in eine Laufzeitumgebung wie ONNX Runtime/TensorRT exportieren, die dies verwaltet.

2. Batch-Größe optimieren: Den richtigen Mittelweg finden

GPUs erreichen einen hohen Durchsatz, indem sie viele Datenpunkte parallel verarbeiten. Eine Erhöhung der Batch-Größe ermöglicht es der GPU, mehr Berechnungen gleichzeitig durchzuführen, was oft zu einer besseren Auslastung und einer insgesamt schnelleren Inferenzzeit führt, bis zu einem bestimmten Punkt. Eine zu große Batch-Größe kann jedoch zu Speicherfehlern oder abnehmenden Erträgen führen, wenn die Speicherbandbreite der GPU oder ihre Recheneinheiten gesättigt werden.

Strategie: Batch-Größe anpassen

Experimentieren Sie mit verschiedenen Batch-Größen. Beginnen Sie mit einer kleinen Batch-Größe (z. B. 1, 4, 8) und erhöhen Sie sie schrittweise, bis Sie abnehmende Erträge in der Inferenzgeschwindigkeit beobachten oder auf Speichergrenzen stoßen. Nutzen Sie Ihr Modell, um zu verstehen, wie die Batch-Größe die GPU-Auslastung beeinflusst.


import time

# ... (Modell- und Gerätekonfiguration von oben)

batch_sizes = [1, 16, 32, 64, 128, 256]
times = []

print("\nBewertung verschiedener Batch-Größen:")
for bs in batch_sizes:
 input_data = torch.randn(bs, 784).to(device)
 
 # Ausführungswärmung
 with autocast():
 _ = model(input_data)
 torch.cuda.synchronize() # Warten, bis die GPU fertig ist

 start_time = time.time()
 num_runs = 100
 for _ in range(num_runs):
 with autocast():
 _ = model(input_data)
 torch.cuda.synchronize()
 end_time = time.time()
 
 avg_time_per_batch = (end_time - start_time) / num_runs
 times.append(avg_time_per_batch)
 print(f"Batch-Größe: {bs}, Durchschnittszeit pro Batch: {avg_time_per_batch:.4f}s")

# Die Nachverfolgbarkeit oder Analyse der Liste 'times' würde die optimale Batch-Größe zeigen.

3. Graphkompilierung und JIT-Compiler (Just-In-Time)

Deep-Learning-Frameworks wie PyTorch und TensorFlow führen Modelle in der Regel interpretativ aus (Eager Mode). Obwohl dies flexibel ist, kann es Python-Überlastungen einführen und globale Optimierungen verhindern, die ein Compiler durchführen könnte. Die Graphkompilierung konvertiert Ihr Modell in einen statischen Berechnungsgraphen, der dann optimiert und in hochgradig effizienten Maschinencode kompiliert werden kann.

Beispiel: TorchScript mit PyTorch

TorchScript ist ein Weg, um serialisierbare und optimierbare Modelle aus PyTorch-Code zu erstellen. Es kann ein vorhandenes Modul nachverfolgen oder es über Scripting konvertieren.


# ... (Modell- und Gerätekonfiguration)

# Option 1: Nachverfolgung (für Modelle mit statischem Kontrollfluss)
# Fügen Sie eine Dummy-Eingabe hinzu, um die Operationen nachzuverfolgen
example_input = torch.randn(1, 784).to(device)
traced_model = torch.jit.trace(model, example_input)
print("\nTyp des nachverfolgten Modells:", type(traced_model))

# Inferenz mit dem nachverfolgten Modell
start_time = time.time()
num_runs = 100
for _ in range(num_runs):
 with autocast():
 _ = traced_model(example_input)
torch.cuda.synchronize()
end_time = time.time()
print(f"Inferenzzeit des nachverfolgten Modells (pro Ausführung): {(end_time - start_time)/num_runs:.6f}s")

# Option 2: Scripting (für Modelle mit dynamischem Kontrollfluss, erfordert jedoch spezifische Syntax)
# @torch.jit.script
# def my_scripted_function(x):
# if x.mean() > 0:
# return x * 2
# else:
# return x / 2
# scripted_output = my_scripted_function(torch.randn(10, 10).to(device))

Torch.compile (PyTorch 2.0+)

PyTorch 2.0 hat torch.compile eingeführt, einen leistungsstarken JIT-Compiler, der Technologien wie TorchInductor verwendet, um Modelle erheblich zu beschleunigen, ohne dass eine manuelle Konvertierung in TorchScript erforderlich ist. Dies ist oft die einfachste und effektivste Optimierung auf Graph-Ebene.


# ... (Modell- und Geräte-Konfiguration)

# Modell kompilieren
compiled_model = torch.compile(model)

# Inferenz mit dem kompilierten Modell
example_input = torch.randn(64, 784).to(device) # Größere Batch-Größe für besseren Effekt verwenden

# Aufwärm-Execution für die Kompilierung
with autocast():
 _ = compiled_model(example_input)
torch.cuda.synchronize()

start_time = time.time()
num_runs = 100
for _ in range(num_runs):
 with autocast():
 _ = compiled_model(example_input)
torch.cuda.synchronize()
end_time = time.time()
print(f"\nInference-Zeit mit Torch.compile (pro Ausführung): {(end_time - start_time)/num_runs:.6f}s")

4. Dedizierte Inferenz-Execution-Umgebungen: Über Frameworks hinaus

Für maximale Leistung und Flexibilität beim Deployment sollten Sie dedizierte Inferenz-Execution-Umgebungen in Betracht ziehen. Diese Umgebungen sind für Produktionsumgebungen optimiert und beinhalten oft fortgeschrittene Graph-Optimierungen, Kernel-Fusion und Unterstützung für verschiedene Hardware-Beschleuniger.

  • NVIDIA TensorRT: Ein Hochleistungs-Inferenz-Optimizer für Deep Learning und eine Execution-Umgebung von NVIDIA. Es nimmt ein trainiertes Netzwerk, optimiert es (z. B. Quantisierung, Layer-Fusion, automatische Kernel-Anpassung) und erzeugt einen optimierten Execution-Engine. Es ist speziell für NVIDIA-GPUs konzipiert.
  • ONNX Runtime: Unterstützt Modelle im Open Neural Network Exchange (ONNX) Format. Es bietet eine einheitliche Inferenz-Engine auf verschiedenen Hardware- und Betriebssystemen, mit Backends für CPU, GPU (CUDA, ROCm, DirectML) und spezialisierte KI-Beschleuniger.

Strategie: Exportieren nach ONNX und Inferenz mit ONNX Runtime

Das Exportieren Ihres PyTorch-Modells nach ONNX ist ein gängiger Schritt, um Umgebungen wie ONNX Runtime oder TensorRT zu nutzen.


import onnx
import onnxruntime as ort

# ... (Modell-Konfiguration)

# PyTorch-Modell nach ONNX exportieren
onnx_path = "model.onnx"
example_input = torch.randn(1, 784).to(device)

torch.onnx.export(
 model.cpu(), # Der ONNX-Export erfolgt in der Regel zuerst auf der CPU
 example_input.cpu(),
 onnx_path,
 input_names=["input"],
 output_names=["output"],
 dynamic_axes={
 "input": {0: "batch_size"}, # Dynamische Batch-Größe erlauben
 "output": {0: "batch_size"}
 },
 opset_version=14
)

print(f"Modell nach {onnx_path} exportiert")

# ONNX-Modell überprüfen
onnx_model = onnx.load(onnx_path)
onnx.checker.check_model(onnx_model)
print("ONNX-Modell erfolgreich überprüft.")

# Inferenz mit ONNX Runtime
# Erstellen einer Inferenz-Session
sess_options = ort.SessionOptions()
# Optional: Optimierungsgrad des Graphen für bessere Leistung festlegen
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL

# Verwenden Sie den CUDA-Anbieter für die GPU-Inferenz
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
ort_session = ort.InferenceSession(onnx_path, sess_options=sess_options, providers=providers)

# Eingabe für ONNX Runtime vorbereiten
input_name = ort_session.get_inputs()[0].name
output_name = ort_session.get_outputs()[0].name

# Beispiel für Inferenz mit einer Batch-Größe von 64
input_data_np = torch.randn(64, 784).cpu().numpy().astype(import numpy as np; np.float32)

start_time = time.time()
num_runs = 100
for _ in range(num_runs):
 ort_outputs = ort_session.run([output_name], {input_name: input_data_np})
end_time = time.time()

print(f"\nONNX Runtime Inferenz-Zeit (pro Ausführung): {(end_time - start_time)/num_runs:.6f}s")

5. Asynchrone Ausführung und Pipelining

GPU-Operationen sind asynchron. Die CPU startet einen Kernel und geht sofort zu etwas anderem über, während die GPU ihn im Hintergrund ausführt. Dies zu verstehen, ist entscheidend für ein effektives Pipelining.

Strategie: Datenübertragung und Berechnung überlagern

Anstatt zu warten, bis ein Batch vollständig abgeschlossen ist, bevor Sie das nächste verarbeiten, können Sie das Laden von Daten für das nächste Batch mit der Berechnung des aktuellen Batches überlagern. Der DataLoader von PyTorch mit num_workers > 0 und pin_memory=True hilft, Daten in den festen Speicher zu übertragen, der schneller für den GPU-Zugriff ist.


import torchvision.datasets as datasets
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

# Fiktives Dataset und DataLoader
transform = transforms.Compose([
 transforms.ToTensor(),
 transforms.Normalize((0.5,), (0.5,))
])
dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

# Wichtig: pin_memory=True für schnellere Host-zu-Gerät-Übertragungen
dataloader = DataLoader(dataset, batch_size=64, shuffle=False, num_workers=4, pin_memory=True)

# ... (Modell- und Geräte-Konfiguration, z. B. mit torch.compile oder traced_model)
compiled_model = torch.compile(model)

# Inferenzschleife mit asynchronem Datenladen
start_time = time.time()
for i, (images, labels) in enumerate(dataloader):
 images = images.view(images.shape[0], -1).to(device, non_blocking=True) # non_blocking=True ist entscheidend
 
 with autocast():
 outputs = compiled_model(images)
 
 # Wenn Sie die Ausgaben auf der CPU verwenden müssen, fügen Sie einen Synchronisationspunkt hinzu
 # Zum Beispiel, um Metriken nach einer bestimmten Anzahl von Batches zu berechnen
 # if (i+1) % 100 == 0: 
 # torch.cuda.synchronize()
 # # Verarbeiten Sie die Ausgaben hier

torch.cuda.synchronize() # Stellen Sie sicher, dass alle GPU-Operationen abgeschlossen sind, bevor das Timing endet
end_time = time.time()

print(f"\nAsynchrone Inferenzzeit für {len(dataloader.dataset)} Proben: {end_time - start_time:.4f}s")

6. Speicherverwaltung und -zuweisung

Eine effiziente Speichernutzung ist entscheidend. Fehler aufgrund unzureichenden Speichers unterbrechen die Inferenz, und häufige Neuzuweisungen können Überlastungen verursachen.

Strategie: Cache leeren und Kontext-Manager verwenden

Leeren Sie regelmäßig den GPU-Speicher-Cache, insbesondere wenn Sie Modelle laden/entladen oder sehr unterschiedliche Eingangsgrößen verarbeiten.


import gc

# ... einige Inferenzaufgaben ...

del model # Modell löschen, wenn es nicht mehr benötigt wird
gc.collect()
torch.cuda.empty_cache() # Leert den GPU-Speicher-Cache von PyTorch
print("GPU-Cache geleert.")

Strategie: Tensoren vorab zuweisen (für feste Eingangsgrößen)

Wenn die Größe Ihres Eingangs-Tensors fest ist, weisen Sie die Eingangs- und Ausgangstensoren im Voraus auf der GPU zu, um wiederholte Zuweisungen zu vermeiden.


# ... (Modell- und Geräte-Konfiguration)

# Eingangs- und Ausgangstensoren vorab zuweisen
fixed_batch_size = 64
fixed_input_shape = (fixed_batch_size, 784)

pre_allocated_input = torch.empty(fixed_input_shape, dtype=torch.float32, device=device)
# Fiktive Ausführung, um die Ausgangsform zu erhalten
with autocast():
 dummy_output = model(pre_allocated_input)
pre_allocated_output = torch.empty(dummy_output.shape, dtype=dummy_output.dtype, device=device)

# Jetzt, in Ihrer Inferenzschleife, kopieren Sie die Daten in pre_allocated_input
# und verwenden Sie pre_allocated_output, um die Ergebnisse zu speichern
# Beispiel: (vorausgesetzt, Sie haben ein numpy-Array 'new_batch_data')
# pre_allocated_input.copy_(torch.from_numpy(new_batch_data))
# with autocast():
# model(pre_allocated_input, out=pre_allocated_output) # Einige Modelle/Operationen unterstützen das Argument 'out'

Profiling und Leistungs-Debugging

Optimierung ist ein iterativer Prozess. Sie benötigen Werkzeuge, um zu identifizieren, wo Ihre Zeit ausgegeben wird.

  • PyTorch Profiler: Verwenden Sie torch.profiler, um detaillierte Berichte über CPU- und GPU-Operationen, Kernel-Startzeiten, Speichernutzung und Datenübertragungen zu erhalten.
  • NVIDIA Nsight Systems / Nsight Compute: Leistungsstarke, eigenständige Werkzeuge für tiefgehendes GPU-Profiling, die Kernel-Ausführungszeitpläne, Speicherbandbreite und Rechenutzung anzeigen.
  • Python time-Modul: Einfach, aber effektiv für das Timing auf hoher Ebene von Codeblöcken.

Beispiel: PyTorch Profiler


from torch.profiler import profile, schedule, tensorboard_trace_handler, ProfilerActivity

# ... (Modell- und Geräte-Konfiguration)

with profile(
 schedule=schedule(wait=1, warmup=1, active=3, repeat=1),
 on_trace_ready=tensorboard_trace_handler("./log/profiler_inference"),
 activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
 record_shapes=True,
 with_stack=True
) as prof:
 for step in range(1 + 1 + 3 + 1): # wait, warmup, active, repeat_delay
 input_data = torch.randn(64, 784).to(device)
 with autocast():
 _ = model(input_data)
 prof.step()

print("\nProfilergebnisse in ./log/profiler_inference gespeichert. Mit 'tensorboard --logdir=./log' ansehen.")

Fazit

Die Optimierung der GPU-Inferenz ist eine vielschichtige Herausforderung, aber durch die systematische Anwendung der in diesem Tutorial beschriebenen Strategien können Sie signifikante Geschwindigkeitsgewinne erzielen. Beginnen Sie mit der Quantifizierung, experimentieren Sie mit Batch-Größen, verwenden Sie Graph-Compiler wie torch.compile, und ziehen Sie dedizierte Laufzeiten wie ONNX Runtime oder TensorRT für Produktionsbereitstellungen in Betracht. Vergessen Sie nie, Ihren Code zu profilieren, um die tatsächlichen Engpässe zu identifizieren, denn eine vorzeitige Optimierung kann kontraproduktiv sein. Mit diesen Werkzeugen und Techniken sind Sie gut gerüstet, um das volle Potenzial Ihrer GPUs für eine ultraschnelle KI-Inferenz auszuschöpfen.

🕒 Published:

✍️
Written by Jake Chen

AI technology writer and researcher.

Learn more →
Browse Topics: benchmarks | gpu | inference | optimization | performance
Scroll to Top