Automatische gemischte Präzision mit PyTorch

Einführung

Größere Deep Learning Modelle erfordern mehr Rechenleistung und Speicherressourcen. Die schneller Training von Deep Neural Networks wurde durch die Entwicklung neuer Techniken erreicht. Anstatt von FP32 (voller Genauigkeit Fließkommazahlenformat), kann man FP16 (halbe Genauigkeit Fließkommazahlenformat) verwenden, und Forscher haben entdeckt, dass ihre Kombination besser ist.

Mischte Genauigkeit ermöglicht halbe Genauigkeit Training, während die meiste der Einzelgenauigkeit Netzwerkgenauigkeit erhalten wird. Der Begriff „mischte Genauigkeit Technik“ bezieht sich auf die Tatsache, dass diese Methode sowohl Einzel- als auch halbe Genauigkeit Darstellungen nutzt.

In diesem Überblick über das automatische Mischgenauigkeit (AMP) Training mit PyTorch, zeigen wir wie diese Technik funktioniert, indem wir Schritt für Schritt durch den Prozess des AMP-Einsatzes gehen und diskutieren wir weitere fortgeschrittene Anwendungen von AMP-Techniken mit Code-Schablonen, die Benutzern später zu integrieren in ihren eigenen Code erleichtern.

Voraussetzungen

Grundlagenwissen von PyTorch: Vertraut mit PyTorch, einschließlich seiner grundlegenden Konzepte wie Tensoren, Module und dem Trainingsschleifen.

Verständnis von Deep Learning Grundlagen: Konzepte wie neuronale Netze, Rückpropagation und Optimierung.

Wissen über gemischte Präzision Training: Erkenntnis über die Vorteile und Nachteile von gemischter Präzision, einschließlich verringerter Speichernutzung und schnellerer Berechnung.

Zugang zu kompatibler Hardware: Ein GPU, der gemischte Präzision unterstützt, wie z.B. NVIDIA-GPUs mit Tensor Cores (z.B. Architekturen von Volta, Turing, Ampere).

Python und CUDA-Einrichtung: Ein funktionierendes Python-Umfeld mit installiertem PyTorch und für GPU- Beschleunigung konfiguriertem CUDA.

Übersicht über gemischte Präzision

Wie die meisten Deep Learning-Frameworks arbeitet PyTorch normalerweise mit 32-bit Fließkommadaten (FP32). FP32 ist jedoch nicht immer notwendig, um zu erfolgreich arbeiten. Es ist möglich, einige Operationen mit 16-bit Fließkommadaten durchzuführen, bei denen FP32 mehr Zeit und Speicher in Anspruch nimmt.

Daher entwickelten die Ingenieure bei NVIDIA eine Technik, die gemischte Präzision Training zulässt, während ein kleiner Teil der Operationen in FP32 und die meisten des Netzwerks in FP16 durchgeführt wird.

  • Wandle das Modell so fort, dass es auf jeden Fall die float16-Datenart nutzen kann.
  • Halte die float32-Master-Gewichte, um den Gewichtsupdate jedes Durchlaufs zu kumulieren.
  • Verwende des Verlustskaliers, um kleine Gradientenwerte zu bewahren.

Mixed-Precision in PyTorch

Für die gemischte Genauigkeit trainiert PyTorch bereits eine Reihe von integrierten Features.
Wenn Sie die .half() Methode auf ein Modul aufrufen, werden die Parameter des Moduls in FP16 umgewandelt, und wenn Sie .half() auf einen Tensor aufrufen, wird die Daten des Tensors in FP16 umgewandelt. Schnelles FP16-Arithmetik wird verwendet, um jegliche Operationen auf diesen Modulen oder Tensoren durchzuführen. Die NVIDIA-Mathematikbibliotheken (cuBLAS und cuDNN) werden von PyTorch sehr gut unterstützt. Die Daten aus dem FP16-Pipeline werden mit Tensor Cores verarbeitet, um GEMMs und Konvolutionsoperationen durchzuführen. Um Tensor Cores in cuBLAS zu verwenden, müssen die Dimensionen eines GEMM ([M, K] x [K, N] -> [M, N]) Vielfache von 8 sein.

Introducing Apex

Apex’s gemischte Genauigkeit Werkzeuge sind dafür konzipiert, die Trainingsgeschwindigkeit zu erhöhen, während die Genauigkeit und Stabilität der Einzelgenauigkeit trainiert werden. Apex kann Operationen in FP16 oder FP32 durchführen, automatisch die Master-Parameter-Umwandlung verwalten und automatisch Verlustskalierungen durchführen.

Apex wurde entwickelt, um es Forschern zu erleichtern, gemischte Präzision trainierte Modelle in ihre Modelle aufzunehmen. Amp, das für Automatic Mixed-Precision steht, ist eine der Funktionen von Apex, einem leichten PyTorch-Erweiterungssatz. Es genügt ein paar weitere Zeilen in ihren Netzwerken, um die Nutzung der gemischten Präzision trainierten Modelle mit Amp zu ermöglichen. Apex wurde auf CVPR 2018 eingeführt, und es ist zu bemerken, dass die PyTorch-Community seit der Veröffentlichung stark auf Apex unterstützt hat.

Durch nur minimale Änderungen am laufenden Modell macht Amp es möglich, dass Sie sich nicht Sorgen machen müssen, weil es um gemischte Typen geht, während Sie Ihren Skripten erstellen oder ausführen. Die Annahmen von Amp könnten in Modellen, die PyTorch in unüblichen Weisen verwenden, nicht so gut passen, aber es gibt Schrauben, um diese Annahmen wie notwendig anzupassen.

Amp bietet all die Vorteile der gemischten Präzision trainierten Modelle ohne das Notwendigkeit, Verlustskalierung oder explizite Management von Typumwandlungen durchzuführen. Die Apex-GitHub-Webseite enthält Anleitungen für den Installationsprozess, und die offizielle API-Dokumentation kann hier gefunden werden.

Wie Amps funktionieren

Amp nutzt ein Weißliste/Schwarzliste-Paradigma auf der logischen Ebene. Tensoroperationen in PyTorch beinhalten neuronale Netzfunktionen wie torch.nn.functional.conv2d, einfache mathematische Funktionen wie torch.log und Tensormethoden wie torch.Tensor.add__. Es gibt drei Hauptkategorien von Funktionen in dieser Welt:

  • Weißliste: Funktionen, die von der Geschwindigkeitserhöhung durch FP16-Mathematik profitieren könnten. Typische Anwendungen schließen Matrixmultiplikation und Konvolution ein.
  • Schwarzliste: Eingaben sollten in FP32 sein, für Funktionen, bei denen 16 Bit Genauigkeit nicht ausreichend sein könnte.
  • Alles Andere (alle verbleibenden Funktionen): Funktionen, die in FP16 laufen können, aber der Aufwand für eine FP32 -> FP16-Konvertierung, um sie in FP16 auszuführen, ist nicht lohnenswert, da die bessere Geschwindigkeit nicht signifikant ist.

Die Aufgabe von Amp ist einfach, zumindest in der Theorie. Amp bestimmt, ob eine PyTorch-Funktion auf der Weißliste ist, auf der Schwarzliste oder gar nicht. Alle Argumente sollten auf FP16 konvertiert werden, wenn sie auf der Weißliste sind, oder auf FP32, wenn sie auf der Schwarzliste sind. Wenn keines der Fälle zutrifft, sollten nur alle Argumente gleichartig sein. Diese Politik ist in der Realität nicht so einfach zu implementieren, wie es scheint.

Amp in Verbindung mit einem PyTorch-Modell verwenden

Um Amp in ein aktuelles PyTorch-Skript zu integrieren, folgen Sie diesen Schritten:

  • Verwenden Sie die Apex-Bibliothek, um Amp zu importieren.
  • Initialisieren Sie Amp, sodass es die notwendigen Änderungen am Modell, dem Optimizer und den PyTorch-internen Funktionen vornehmen kann.
  • Beachten Sie, wo die backpropagation (.backward()) stattfindet, sodass Amp gleichzeitig den Verlust skalieren und den pro-Iterations-Zustand löschen kann.

Schritt 1

Der erste Schritt erfordert nur eine Zeile Codierung:

from apex import amp

Schritt 2

Für diesen Schritt müssen bereits die Neuronales Netzmodell und der Optimizer, die für die Trainings verwendet werden, definiert sein. Dieser Schritt besteht aus nur einer Zeile.

model, optimizer = amp.initialize(model, optimizer, opt_level="O1")

Zusätzliche Einstellungen ermöglichen Ihnen, Amp’s Tensor- und Operationstyp-Anpassungen fein abzustimmen. Die Funktion amp.initialize() akzeptiert viele Parameter, von denen wir hier nur drei angeben werden:

  • models (torch.nn.Module oder Liste von torch.nn.Modules) – Modelle zu ändern/casten.
  • Optimierer (optional, torch.optim.Optimizer oder Liste von torch.optim.Optimizers) – Optimierer zum Ändern/Umwandeln. ERFORDERLICH für die Training, optional für die Inferenz.
  • opt_level (str, optional, Standard=“O1”) – Rein oder gemischte Präzisionsebene der Optimierung. Zugelassene Werte sind “O0”, “O1”, “O2” und “O3”, die detailliert oben erklärt sind. Es gibt vier Optimierungsgrade:

O0 für FP32-Training: Dies ist ein no-op. Es besteht kein Bedarf, sich Sorgen zu machen, da Ihr eingehendes Modell bereits in FP32 sein sollte, und O0 kann helfen, eine Basis für die Genauigkeit zu etablieren.

O1 für gemischte Präzision (empfohlen für typische Anwendungen): Ändern Sie alle Tensor- und Torch-Methoden, um ein Weißliste-Schwarzliste-Eingangsabbildungsschema zu verwenden. In FP16 werden Weißlistenoperationen wie z.B. GEMMs und Konvolusionen mit Tensor Core-freundlichen Operationen durchgeführt. Softmax ist ein Schwarzlistenoperation, die in FP32-Genauigkeit erforderlich ist. Sofern nicht anders angegeben, verwendet O1 auch dynamische Verlustskalierung.

O2 für „fast FP16“ gemischte Präzision: O2 wandelt die Modellgewichte in FP16 um, patcht den Vorwärtsmethoden des Modells, um Eingabedaten in FP16 umzuschalten, behält die Batchnorms in FP32, hält die FP32-Mastergewichte erhalten, aktualisiert die Parametergruppen des Optimizers, sodass optimizer.step() direkt auf die FP32-Gewichte agiert und implementiert dynamische Verlustskalierung (außer wenn es überschrieben wird). Im Gegensatz zu O1 wandelt O2 keine Torch-Funktionen oder Tensormethoden um.

O3 für FP16-Training: O3 ist möglicherweise nicht so stabil wie O1 und O2 bezüglich der wahren gemischten Genauigkeit. Daher kann es vorteilhaft sein, eine Basisgeschwindigkeit für Ihren Modell festzulegen, gegen die die Effizienz von O1 und O2 Bewertet werden kann.
Die zusätzliche Eigenschaftsvorhersage keep_batchnorm_fp32=True in O3 könnte Ihnen helfen, die „Lichtgeschwindigkeit“ zu bestimmen, wenn Ihr Modell Batch-Normalisierung verwendet, was die cudnn-Batch-Normalisierung ermöglicht.

O0 und O3 sind keine echten gemischten Genauigkeiten, aber sie helfen jeweils bei der Festlegung von Genauigkeits- und Geschwindigkeitsgrundlinien. Eine gemischte Genauigkeit ergibt sich aus der Implementierung von O1 und O2.
Sie können beide versuchen und sehen, welche den Leistungs- und Genauigkeitsverlust am meisten für Ihr bestimmtes Modell verringert.

Schritt 3

Stellen Sie sicher, dass Sie erkennen können, wo sich die Rückwärtspassage in Ihrem Code befindet.
Einige Zeilen Code, die wie folgt aussehen:

loss = criterion(…)
loss.backward()
optimizer.step()

Schritt 4

Mit dem Amp-Kontextmanager können Sie die Verlustskalierung einfach durch Abdecken der Rückwärtspassage aktivieren:

loss = criterion(…)
with amp.scale_loss(loss, optimizer) as scaled_loss:
    scaled_loss.backward()
optimizer.step()

Das ist alles. Nun können Sie Ihre Skript mit aktivierter gemischter Genauigkeit wiederholen.

Funktionsaufrufe aufnehmen

PyTorch verfügt nicht über einen statischen Modellobjekt oder Graph, auf den gegriffen und die oben genannten Casts eingefügt werden könnten, da es so flexibel und dynamisch ist. Durch die „Monkey Patching„-Methode kann Amp die erforderlichen Funktionen aufspüren und Parameter dynamisch umwandeln.

Als Beispiel kannst du den folgenden Code verwenden, um sicherzustellen, dass die Argumente der Methode torch.nn.functional.linear immer in fp16 umgewandelt werden:

orig_linear = torch.nn.functional.linear
def wrapped_linear(*args):
 casted_args = []
  for arg in args:
    if torch.is_tensor(arg) and torch.is_floating_point(arg):
      casted_args.append(torch.cast(arg, torch.float16))
    else:
      casted_args.append(arg)
  return orig_linear(*casted_args)
torch.nn.functional.linear = wrapped_linear

Obwohl Amp Verbesserungen hinzufügen kann, um den Code robuster zu machen, führt das Aufrufen von Amp.init() tatsächlich zu Monkey Patches in alle relevanten PyTorch-Funktionen, sodass Argumente zur Laufzeit korrekt umgewandelt werden.

Casts Minimieren

Jeder Gewichtszusammenhang wird nur einmal pro Durchlauf von FP32 nach FP16 gewandelt, da Amp einen internen Zwischenspeicher aller Parameterumwandlungen aufbaut und diese wie notwendig wiederverwendet. Bei jeder Iteration zeigt der Kontextmanager für den Rückwärtsgang an, dass Amp den Zwischenspeicher löschen soll.

Autocasting und Gradienten Skalierung mit PyTorch

„Automatisiertes gemischte Präzisionstraining“ bezeichnet die Kombination von torch.cuda.amp.autocast und torch.cuda.amp.GradScaler. Mit torch.cuda.amp.autocast kann man Autocasting nur für bestimmte Bereiche einrichten. Autocasting wählt automatisch die Präzision für GPU-Operationen aus, um Effizienz zu optimieren und gleichzeitig Genauigkeit zu wahren.

Die Instanzen von torch.cuda.amp.GradScaler erleichtern die Durchführung der Gradienten skalierenden Schritte. Die Gradienten skalierung reduziert den Gradienten unterlauf und hilft Netzwerken mit float16 Gradienten zu besserer Konvergenz.

Hier ist ein Codebeispiel, das zeigt, wie man mit autocast() in PyTorch automatisiertes gemischtes Präzisionstraining erreicht:

# Erstellt das Modell und den Optimizer in der Standardpräzision
model = Net().cuda()
optimizer = optim.SGD(model.parameters(), ...)

# Erstellt ein GradScaler einmal am Anfang des Trainings.
scaler = GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()

        # Führt die Vorwärtspass-Schritte mit Autocasting durch.
        with autocast(device_type='cuda', dtype=torch.float16):
            output = model(input)
            loss = loss_fn(output, target)

        # Die Rückwärtsoperationen laufen in der gleichen Datentyp-Präzision, die autocast für die entsprechenden Vorwärtsoperationen ausgewählt hat.
        scaler.scale(loss).backward()

        # scaler.step() entpackt zunächst die Gradienten der vom Optimierer zugewiesenen Parameter.
   
        scaler.step(optimizer)

        # Aktualisiert die Skalierung für die nächste Iteration.
        scaler.update()

Wenn die Forward-Pass-Schritte für eine bestimmte Operation mit float16-Eingaben erfolgen, dann erzeugt der Backward-Pass für diese Operation float16-Gradienten, und float16 kann Gradienten mit kleinen Magnitudin nicht korrekt abbilden.

Wenn diese Werte auf Null gespeichert werden („Unterlauf“), kann die Aktualisierung der verknüpften Parameter verloren gehen.

Gradienten-Skalierung ist eine Technik, die ein Skalierungsfaktor verwendet, um die Verluste des Netzwerks zu multiplizieren und anschließend auf dem skalierten Verlust den Backward-Pass durchzuführen, um einen Unterlauf zu vermeiden. Es ist auch notwendig, dass die rückwärtsfließenden Gradienten durch das gleiche Faktor skaliert werden. Dadurch erhalten die Gradientenwerte eine größere Magnitude, was sie nicht auf Null abspeichert.

Vor der Aktualisierung der Parameter sollte jedem Parametergradienten (.grad Attribut) der Skalierungsfaktor entfernt werden, damit dieser nicht die Lernrate stört. Sowohl autocast als auch GradScaler können unabhängig voneinander verwendet werden, da sie modulär sind.

Arbeiten mit unskalierten Gradienten

Gradientenklippung

Wir können alle Gradienten skalieren, indem wir die Methode Scaler.scale(Loss).backward() verwenden. Die Eigenschaften .grad der Parameter zwischen backward() und scaler.step(optimizer) müssen vor dem Ändern oder Untersuchen des Werts rückgängig gemacht werden, wenn Sie die globale Norm (siehe torch.nn.utils.clip_grad_norm_()) oder die maximale Größe (siehe torch.nn.utils.clip_grad_value_()) Ihrer Gradientenmenge auf weniger als oder gleich einem bestimmten Wert begrenzen möchten (ein von Ihnen festgelegter Schwellenwert). Dazu kann eine Technik namens „Gradientenklippung“ verwendet werden.

Ohne die Rückgängigmachen der Skalierung würde die Norm/Maximalgröße der Gradienten skaliert werden, was die von Ihnen angeforderte Schwellenwerteinheit ungültig macht (die als Schwellenwert für nicht skalierte Gradienten vorgesehen war). Die Gradienten, die die Parameter des Optimizers umfassen, werden durch scaler.unscale (optimizer) rückgängig gemacht.
Sie können die Gradienten anderer Parameter rückgängig machen, die zuvor einem anderen Optimizer (wie z.B. optimizer1) gegeben wurden, indem Sie scaler.unscale (optimizer1) verwenden. Dies kann durch die Hinzufügung zweier Codezeilen verdeutlicht werden:

# Rückgängig macht die Gradienten der von optimizer zugewiesenen Parameter
        scaler.unscale_(optimizer)
# Da die Gradienten der von optimizer zugewiesenen Parameter rückgängig gemacht wurden, klippt man wie gewohnt: 
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)

Arbeiten mit skalierten Gradienten

Gradientenakkumulation

Gradientenakkumulation basiert auf einem absurd einfachen Konzept. Statt die Modelparameter zu aktualisieren, wartet es und addiert die Gradienten über mehrere aufeinanderfolgende Batchs zu Loss und Gradientenberechnung.

Nach einer bestimmten Anzahl von Batchs werden die Parameter entsprechend der kumulierten Gradienten aktualisiert. Hier ist ein Codebeispiel, wie man Gradientenakkumulation verwenden mit pytorch:

scaler = GradScaler()

for epoch in epochs:
    for i, (input, target) in enumerate(data):
        with autocast():
            output = model(input)
            loss = loss_fn(output, target)
            # den Verlust normalisieren 
            loss = loss / iters_to_accumulate

        # Addiert skalierte Gradienten.
        scaler.scale(loss).backward()
          # Parameteraktualisierung
        if (i + 1) % iters_to_accumulate == 0:
            # könnte hier unscale_ verwenden, wenn gewünscht 
            scaler.step(optimizer)
            scaler.update()
            optimizer.zero_grad()
  • Gradientenakkumulation fügt Gradienten über eine angemessene Batchgröße von batch_per_iter * iters_to_accumulate hinzu.
    Die Skalierung sollte für den effektiven Batch kalibriert werden; dies bedeutet, dass man auf Inf/NaN-Gradienten aufmerksam ist, bei deren Detektion einen Schritt auslassen und die Skalierung auf die Genauigkeit des effektiven Batchs aktualisieren sollte.
    Es ist auch wichtig, Gradienten in einer skalierten und konsistenten Skalierungsfaktor zu behalten, wenn Gradienten für einen bestimmten effektiven Batch zusammengefasst werden.

Wenn die Gradienten vor Abschluss der Akkumulation nicht skaliert sind (oder der Skalierungsfaktor sich ändert), addiert der nächste Rückwärtspass skalierte Gradienten zu nicht skalierten Gradienten (oder Gradienten, die mit einem anderen Faktor skaliert sind), und es ist danach unmöglich, die skalierte Gradientenakkumulation des vorhergehenden Schritts wiederherzustellen. Der Schritt muss angewendet werden.

  • Du kannst die Gradienten skalierungsfrei machen, indem du unscale kurz vor dem Schritt verwendest, nachdem alle skalierten Gradienten für den bevorstehenden Schritt akkumuliert wurden.
    Um eine vollständige und effektive Batchgröße zu gewährleisten, rufe nur update am Ende jeder Iteration auf, an der du zuvor step aufgerufen hast.
  • enumerate(data)-Funktion lässt uns die Batch-Nummer beibehalten, während wir durch die Daten iterieren.
  • Teile die laufende Verlustsumme durch iters_to_accumulate(loss / iters_to_accumulate). Dies reduziert die Beiträge jedes Mini-Batches, die wir verarbeiten, indem der Verlust normalisiert wird. Wenn du den Verlust innerhalb jedes Batchs durchschnittlich berechnest, ist die Division bereits richtig und es ist keine weitere Normalisierung erforderlich. Dieser Schritt ist abhängig von deiner Verlustberechnung möglicherweise nicht notwendig.
  • Wenn wir scaler.scale(loss).backward() verwenden, sammelt PyTorch skalierte Gradienten und speichert sie bis wir optimizer.zero_grad() aufrufen.

Gradientenstrafe

Beim Implementieren einer Gradientenstrafe verwendet man torch.autograd.grad(), um Gradienten zu bilden, die kombiniert werden, um die Strafe zu bilden, und dann zur Verlustfunktion hinzugefügt werden. Ein L2-Strafe ohne Skalierung oder Autocasting ist in dem folgenden Beispiel zu sehen.

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)

        # Erzeugt Gradienten
        grad_prams = torch.autograd.grad(outputs=loss,
                                          inputs=model.parameters(),
                                          create_graph=True)

        # Berechnet die Strafe und fügt sie der Verlustfunktion hinzu
        grad_norm = 0
        for grad in grad_prams:
            grad_norm += grad.pow(2).sum()
        grad_norm = grad_norm.sqrt()
        loss = loss + grad_norm

        loss.backward()

        # Hier kann man Gradienten klippeln

        optimizer.step()

Tensoren, die an torch.autograd.grad() übergeben werden, sollten skaliert werden, um eine Gradientenstrafe zu implementieren. Es ist notwendig, die Gradienten vor der Kombination zur Erlangung der Strafe aufzuwecken. Da die Strafe Berechnung Teil des Forward-Durchlaufs ist, sollte dies innerhalb eines Autocast-Kontextes passieren.
Für dieselbe L2-Strafe sieht es wie folgt aus:

scaler = GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()
        with autocast():
            output = model(input)
            loss = loss_fn(output, target)

        # Führe lossbasierte Skalierung für die Backward-Pass-Phase von autograd.grad durch, #ergebnis #scaled_grad_prams
        scaled_grad_prams = torch.autograd.grad(outputs=scaler.scale(loss),
                                                 inputs=model.parameters(),
                                                 create_graph=True)

        # Erstelle grad_prams vor der Berechnung der Strafe (grad_prams muss #unscaled sein). 
        # Da kein Optimizer scaled_grad_prams besitzt, wird eine herkömmliche Division #stattdessen als scaler.unscale_ verwendet:
        inv_scaled = 1./scaler.get_scale()
        grad_prams = [p * inv_scaled for p in scaled_grad_prams]

        # Der Strafteil wird berechnet und zum Verlust hinzugefügt. 
        with autocast():
            grad_norm = 0
            for grad in grad_prams:
                grad_norm += grad.pow(2).sum()
            grad_norm = grad_norm.sqrt()
            loss = loss + grad_norm

        # Setzt Skalierung für die Rückwärtskallbe für Schritte ein.
        # Rechtfertigt skalierten Wurzelgradienten.
        scaler.scale(loss).backward()

        # Du kannst hier unscale_ verwenden 

        # step() und update() verlaufen wie üblich.
        scaler.step(optimizer)
        scaler.update()

Arbeiten mit mehreren Modellen, Verlusten und Optimizierern

Scaler.scale muss auf jedem Verlust in Ihren Netzwerken aufgerufen werden, wenn Sie viele davon haben.
Wenn Sie viele Optimierer in Ihrem Netzwerk haben, können Sie scaler.unscale auf jedem davon ausführen und Sie müssen auf jedem davon scaler.step aufrufen. Allerdings sollte scaler.update nur einmal verwendet werden, nach dem Schritt aller Optimierer, die in dieser Iteration verwendet wurden:

scaler = torch.cuda.amp.GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer1.zero_grad()
        optimizer2.zero_grad()
        with autocast():
            output1 = model1(input)
            output2 = model2(input)
            loss1 = loss_fn(2 * output1 + 3 * output2, target)
            loss2 = loss_fn(3 * output1 - 5 * output2, target)

       # Obwohl retain graph nicht mit AMP in Verbindung steht, ist er in diesem Beispiel enthalten, da beide backward()-Aufrufe einen bestimmten Bereich des Graphen teilen. 
        scaler.scale(loss1).backward(retain_graph=True)
        scaler.scale(loss2).backward()

        # Wenn Sie die Gradienten der Parameter anschauen oder anpassen möchten, die sie besitzen, können Sie angeben, welche Optimierer explizite Entskalierung erhalten. .
        scaler.unscale_(optimizer1)

        scaler.step(optimizer1)
        scaler.step(optimizer2)

        scaler.update()

Jeder Optimierer prüft seine Gradienten auf Infinity/NaNs und trifft einen individuellen Schritt, ob er den Schritt überspringen soll oder nicht. Einige Optimierer könnten den Schritt überspringen, während andere das nicht tun. Der Schrittüberspringen tritt nur einmal auf jeden mehreren hundert Iterationen; daher sollte es die Konvergenz nicht beeinflussen. Für Modelle mit mehreren Optimierern können Sie das Problem melden, wenn Sie eine schlechte Konvergenz nach der Hinzufügung der Gradienten skaliert haben.

Arbeiten mit mehreren GPUs

Eines der größten Probleme mit Deep Learning-Modellen ist, dass sie zu groß werden, um mit einer einzigen GPU trainiert zu werden. Es kann zu lange dauern, ein Modell mit einer einzigen GPU zu trainieren, und daher ist die Multi-GPU-Trainings notwendig, um die Modelle so schnell wie möglich fertigzustellen. Ein bekannter Forscher konnte die ImageNet-Trainingzeit von zwei Wochen auf 18 Minuten verkürzen oder den umfassendsten und fortschrittlichsten Transformer-XL in zwei Wochen anstatt von vier Jahren trainieren.

DataParallel und DistributedDataParallel

Ohne Qualitätsverlust bietet PyTorch eine optimale Kombination von Einfachheit der Nutzung und Steuerung. nn.DataParallel und nn.parallel.DistributedDataParallel sind zwei Features von PyTorch für die Verteilung der Trainingsprozesse auf mehrere GPUs. Sie können diese einfach zu bedienenden Wrapper verwenden und Änderungen anwenden, um das Netz auf mehreren GPUs zu trainieren.

DataParallel in einem Prozess

Mit DataParallel kann die Trainingsprozess auf einem einzigen Rechner auf viele GPUs ausgedehnt werden.
Lassen Sie uns näher anschauen, wie DataParallel tatsächlich in der Praxis funktioniert.
Wenn Sie DataParallel verwenden, um ein neuronales Netz zu trainieren, gehen die folgenden Schritte ein:

Quelle:

  • Der Mini-Batch wird auf GPU:0 aufgeteilt.
  • Der Mini-Batch wird aufgeteilt und an alle verfügbaren GPUs verteilt.
  • Das Modell wird auf den GPUs kopiert.
  • Der Forward-Durchlauf findet auf allen GPUs statt.
  • Der Verlust in Bezug auf die Netzwerkausgaben wird auf GPU:0 berechnet und die Verluste werden zu allen GPUs zurückgegeben. Die Gradienten sollten auf jeder GPU berechnet werden.
  • Summierung der Gradienten auf GPU:0 und Anwendung des Optimizers, um das Modell zu aktualisieren.

Es ist darauf hinzuweisen, dass die hier diskutierten Anmerkungen sich ausschließlich auf autocast beziehen. Der Verhalten von GradScaler bleibt unverändert. Es ist unerheblich, ob torch.nn.DataParallel Threads für jedes Gerät erstellt, um die Forward-Pass-Phase durchzuführen. Der autocast-Zustand wird in jedem einzelnen kommuniziert und das Folgende funktioniert:

model = Model_m()
p_model = nn.DataParallel(model)

# Setzt autocast im Hauptschritt
with autocast():
    # Es wird autocast in p_model. 
    output = p_model(input)
    # loss_fn wird ebenfalls autocast
    loss = loss_fn(output)

Verteiltes Datenparallelisierung, ein GPU pro Prozess

Die Dokumentation für torch.nn.parallel.DistributedDataParallel empfiehlt die Verwendung eines GPUs pro Prozess für die beste Leistung. In dieser Situation startet DistributedDataParallel keine Threads intern; daher ist der Einsatz von Autocast und GradScaler nicht beeinträchtigt.

DistributedDataParallel, mehrere GPUs pro Prozess

Hier kann torch.nn.parallel.DistributedDataParallel einen Nebenthread starten, um den Forward-Durchlauf auf jedem Gerät auszuführen, wie bei torch.nn.DataParallel. Die Lösung ist dieselbe: Aktivieren Sie Autocast als Teil Ihrer Modells-Forward-Methode, um sicherzustellen, dass er in Nebenthreads aktiviert ist.

Schlussfolgerung

In diesem Artikel haben wir:

  • Apex eingeführt.
  • Gesehen, wie AMPs funktionieren.
  • Gesehen, wie man Gradienten skaliert, Gradienten klippt, Gradientenaccumulation und Gradientenstrafen durchführt.
  • Gesehen, wie wir mit mehreren Modelle, Verlusten und Optimierern arbeiten können.
  • Gesehen, wie wir DataParallel in einem einzigen Prozess durchführen können, wenn wir mit mehreren GPUs arbeiten.

Referenzen

https://developer.nvidia.com/blog/apex-pytorch-easy-mixed-precision-training/
https://nvidia.github.io/apex/amp.html
https://discuss.pytorch.org/t/accumulating-gradients/30020
https://towardsdatascience.com/how-to-scale-training-on-multiple-gpus-dae1041f49d2

Source:
https://www.digitalocean.com/community/tutorials/automatic-mixed-precision-using-pytorch