Dolmetscherdesign

Datenmodell

StableHLO-Programme sind Berechnungen über Tensoren (n-dimensionale Arrays), die im aktuellen Modell mithilfe des Klasse Tensor. Die zugrunde liegende Speicherklasse für ein Tensor-Objekt. detail::Buffer, speichert den mlir::ShapedType des Tensors zusammen mit einem mlir::HeapAsmResourceBlob-Objekt, das einen änderbaren Blob von Tensor darstellt Daten, die als zusammenhängendes Byte-Array in kleinere Bestellung. detail::Buffer-Objekte werden gezählt, um die Speicherverwaltung zu vereinfachen.

Einzelne Elemente eines Tensors werden mithilfe der Klasse Element dargestellt, die verwendet eine diskriminierte Gewerkschaft mit einem der folgenden Dokumente: APInt, APFloat oder pair<APFloat,APFloat> für Speicherplatz. Die letzte dient zum Speichern von Elementen. mit komplexen Typen.

Tensor hat die folgenden APIs, um mit den einzelnen Elementen zu interagieren:

  • Element Tensor::get(llvm::ArrayRef<int64_t> index): Zum Extrahieren eines Einzelnes Tensor-Element am mehrdimensionalen Index index als Element -Objekt enthält.
  • void Tensor::set(llvm::ArrayRef<int64_t> index, Element element);: Zum Aktualisieren eines Element-Objekts element in einen Tensor bei mehrdimensionalen Index index.

So funktioniert der Dolmetscher

Die Eingabefunktion für den Interpreter ist

SmallVector<Tensor> eval(func::FuncOp func, ArrayRef<Tensor> args);

Dabei wird Folgendes ausgeführt:

  1. Erfasst die SSA-Argumente von func und die zugehörige Laufzeit Tensor -Werte, die in args angegeben werden, unter Verwendung der Symboltabellenzuordnung M.
  2. Für jede Operation innerhalb von func in SSACFG-Reihenfolge: <ph type="x-smartling-placeholder">
      </ph>
    • Ruft eval im Vorgang auf. Extrahieren Sie für jeden SSA-Operanden des Vorgangs dessen Laufzeitwert aus M, der als Argument für den Aufruf eval angegeben werden soll.
    • Verfolgt die SSA-Ergebnisse des Vorgangs und den ausgewerteten Wert in M.

Die in (2) erwähnte eval auf Betriebsebene ist für die Implementierung der die Ausführungssemantik des Vorgangs. Hier ein Beispiel für stablehlo::AddOp: Im Beispiel sind die einzelnen Elemente der Tensoren lhs und rhs paarweise extrahiert als Element-Objekte, die dann hinzugefügt werden. Das Ergebnis der Addition Ein Element-Objekt, wird im endgültigen result-Tensor gespeichert.

Tensor eval(AddOp op, const Tensor &lhs, const Tensor &rhs) {
  Tensor result(op.getType());

  for (auto it = result.index_begin(); it != result.index_end(); ++it)
    result.set(*it, lhs.get(*it) + rhs.get(*it));

  return result;
}

Insgesamt ist das Design des Dolmetschers so optimiert, Implementierungen von eval-Funktionen für einzelne Vorgänge. als Referenzimplementierung für StableHLO. Anstatt beispielsweise Definition von eval als Vorlagenfunktion und parametrisieren mit Elementtypen erklären wir, wie die verschiedenen Elementtypen Element::operator+ usw., wodurch die Implementierung von eval vereinfacht wird.

Interpreter für konstantes Falten verwenden

Mit dem Interpreter-Mechanismus können wir Operationen mit einem konstanten Operanden falten Werte. Das folgende Code-Snippet zeigt eine Vorstellung für das Falten von stablehlo::AddOp mit Gleitkomma-Operanden:

OpFoldResult AddOp::fold(FoldAdaptor adaptor) {
  auto attrs = adaptor.getOperands();
  DenseElementsAttr lhsData = dyn_cast<DenseElementsAttr>(attrs[0]);
  DenseElementsAttr rhsData = dyn_cast<DenseElementsAttr>(attrs[1]);
  if (!lhsData || !rhsData) return {};

  auto lhs = Tensor(lhsData);
  auto rhs = Tensor(rhsData);
  auto result = eval(*this, lhs, rhs);

  SmallVector<APFloat> values;
  for (auto i = 0; i < result.getNumElements(); ++i) {
    Element element = result.get(i);
    values.push_back(cast<FloatAttr>(element.getValue()).getValue());
  }

  return DenseElementsAttr::get(result.getType(), values);
}

Derzeit arbeiten wir nicht aktiv daran, den Dolmetscher in die da wir nicht vorhaben, Ordner für StableHLO zu implementieren. Wir planen jedoch, den Dolmetscher für konstante Dann verbessern wir die Ergonomie des Code-Snippets, (Wir könnten z.B. eine Hilfsfunktion haben, die konstante Operanden in Tensor-Objekte und entpackt Tensor-Ergebnisse in OpFoldResult).

StableHLO-Interpreter testen

Der Interpreter nimmt als Eingaben (A) ein StableHLO-Programm und (B) Datenwerte in das Programm eingespeist werden und Ausgabedatenwerte generieren, die mit dem mit den vom Nutzer bereitgestellten erwarteten Datenwerten vergleichen. Die Datenwerte (B) sind im Programm selbst mit stablehlo.constant-Operationen hartcodiert sind. Die der Interpreter das Eingabeprogramm bewertet. Die Ausgabe(en) des zu testenden Vorgangs wird mithilfe von Schecks (z.B. check.expect_eq, check.expect_almost_eq) überprüft, da wie unten dargestellt. check.expect_eq und check.expect_eq_const auf Bitweise prüfen Gleichheit für alle unterstützten Typen sowie check.expect_almost_eq und check.expect_almost_eq_const auf nahezu Gleichheit innerhalb einer Toleranz in der Testrichtlinie (G6) für Gleitkomma- und komplexe Typen erläutert.

// CHECK-LABEL: Evaluated results of function: add_op_test_ui4
func.func @add_op_test_ui4() {
  %0 = stablehlo.constant dense<[0, 2]> : tensor<2xui4>
  %1 = stablehlo.constant dense<[15, 3]> : tensor<2xui4>
  %2 = stablehlo.add %0, %1 : tensor<2xui4>
  check.expect_eq_const %2, [15, 5] : tensor<2xui4>
  func.return
}

Ein Testdienstprogramm stablehlo-translate --interpret (Code) ist verantwortlich für das Parsen des Programms, die Interpretation jeder Funktion, einschließlich der Operationen, aus denen die Funktion besteht. Wir haben eine spezielle Testsuite, die aus Tests mit unterschiedlichem Laufzeitverhalten für jeden StableHLO-Vorgang Die Tests finden Sie hier.

Testrichtlinien

(G1) Müssen wir für jede Operation alle unterstützten Typen testen?

Wir können die folgenden Regeln miteinander kombinieren, um zu entscheiden:

  1. Wenn bei der Implementierung eines Vorgangs Code im entsprechenden eval vorhanden ist um einen bestimmten Typ zu verarbeiten, sind Tests erforderlich, um diesen Typ abzudecken. Für den Vorgang add gibt es beispielsweise exklusiven Code. ganzzahlige, boolesche, Gleitkomma- und komplexe Typen zu verarbeiten. für jede Typkategorie einen Test.

  2. Wenn eine Reihe von Typen in der entsprechenden eval-Funktion einheitlich verarbeitet werden, dann sollte für alle diese Typen ein einziger Test ausreichen. Beispiel: beim Vorgang add alle Varianten der Ganzzahltypen (si4, u4, si8, u8) und so weiter) mithilfe von llvm::APInt-APIs gleich behandelt werden. Daher können wir diesen Schritt überspringen. Tests für jede dieser Varianten hinzufügen, statt einen einzelnen repräsentativen Tests. Um Unklarheiten bei der Auswahl des Vertreters zu vermeiden, sollten folgende Richtlinien beachtet werden:

    • Wenn alle Typen, die einheitlich behandelt werden, denselben primitiven Typ haben (d.h. wenn alle Ganzzahlen, Gleitkomma- oder komplexe Typen sind), gilt Folgendes: wählen Sie die Option mit maximaler Bitbreite aus.
    • Wenn alle Typen einheitlich behandelt und eine Mischung aus primitiven Typen haben, wählen Sie den mit dem folgenden primitiven Typ in absteigender Reihenfolge Präferenz: Ganzzahl, Gleitkommazahl, boolescher Wert, Komplex.

(G2) Wie entscheiden wir, wie viele Tests erforderlich sind, um die Anzahl der Operationen abzudecken Verhalten?

Ziel ist es, die Logik des Interpreters für die Operation umfassend zu untersuchen. (d.h. alle Sonderfälle der Implementierung) mit einer minimalen Anzahl von Tests. Für die Verwaltbarkeit ist es wichtig, die Anzahl der Tests zu minimieren. Je weniger Tests desto einfacher ist es, sie zu überprüfen und sicherzustellen, umfassend behandelt. Daher erwarten wir, dass die meisten der einfacheren wird es nur einen Test geben. Wenn Sie aus einem guten Grund nicht praktikabel ist, können Sie bei >= 90 % anhalten. Dies wird entschieden bei der Überprüfung der Pull-Anfrage von Fall zu Fall entscheiden.

(G3) Wie wäre es mit dem Hinzufügen von Tests für die Dolmetscherinfrastruktur?

Die Infrastruktur für Dolmetscher ist übersichtlich und kann zu unserer Vertrauensbasis. Der einzige nicht triviale Teil besteht darin, wie die verschiedenen Typen und aus dem zugrunde liegenden Dolmetscherspeicher entpackt werden. Wie in G1 besprochen, werden nur die unterschiedlich gehandhabten Operationen getestet. Mit dass der Verpackungs-/Entpackungscode, der den unterschiedlichen Varianten von Ganzzahl-/Gleitkommazahlentypen, werden während Tests durchführen. Um eine vollständige Abdeckung zu gewährleisten, können wir eine Operation wie constant auswählen, die unterstützt alle StableHLO-Elementtypen und schreibt umfassende Tests.

(G4) Wenn die Implementierung eines Vorgangs von anderen Vorgängen abhängt, sollten wir dann Tests für Letzteres?

Nein. Die Implementierung von batch_norm_grad kann beispielsweise auf divide, subtract, multiply und weitere. Wir sollten die zweite Option vermeiden Operationen beim Testen des alten Systems ausgeführt.

(G5) Sollten wir Tests schreiben, um die implementierungsdefinierten / nicht definierten Verhaltensweisen?

Wir sollten keine Tests schreiben, die die von der Implementierung definierten nicht definiertes Verhalten des Vorgangs. Tests zum Ausführen von implementierungsdefinierten Verhaltensweisen ein lokales Verhalten des Dolmetschers zeigen, was nicht verallgemeinert. Tests mit nicht definiertem Verhalten tragen nicht zu das Verhalten des Vorgangs zu verstehen.

(G6) Mit welcher Genauigkeit werden beim Schreiben von Tests für Gleitkommatypen erwartetes Ergebnis bei der Prüfung angegeben werden muss?

Für Elementaroperationen (Addition, Subtraktion, Multiplikation, Division und Square), wird bei einer Implementierung gemäß der IEEE-Spezifikation ein gerundetes Ergebnis innerhalb von 0,5 ULP des mathematisch exakten Ergebnisses. Dennoch sich das erwartete Ergebnis dieser Operationen Abstand zwischen maximal 1 ULP. Bei transzendentalen Funktionen funktioniert dies möglicherweise nicht. (sine, cosine usw.), für die die Genauigkeitsgarantien gelten Implementierung definiert (Begründung).

In der aktuellen Implementierung wird Toleranzwert von 0,0001. Das folgende Beispiel zeigt die obige Toleranz in Aktion.

func.func @check_tolerance() {
  %0 = stablehlo.constant dense<0.2> : tensor<f32>

  // The following check succeeds as %0 is almost equal to the provided
  // constant modulo the tolerance, mentioned above.
  check.expect_almost_eq_const %0, dense<0.19999> : tensor<f32>

  // The following check fails as %0 is not bitwise equal to the provided
  // constant.
  check.expect_eq_const %0, dense<0.19999> : tensor<f32>

  func.return
}

Dies ist nur der erste Schritt zum Testen der numerischen Genauigkeit von StableHLO-Operationen. Im Moment ist dies ein zu wenig angegebener Bereich der StableHLO-Spezifikation. fortlaufende Maßnahmen zur Ermittlung des Problems #1156 auf Grundlage unserer Erfahrungen mit StableHLO in der Praxis und des Feedbacks Stakeholdern. Wir aktualisieren die Infrastruktur. entsprechend anpassen.

(G7) Gibt es etwas zum Programmierstil der Tests?

  1. Achten Sie darauf, den tatsächlichen Namen der Ein-/Ausgaben zu verwenden, anstatt die Standardnamen zu verwenden in SSA-Werte (z.B. %0, %1 usw.)
  2. Stellen Sie sicher, dass die Tests ein gut gedrucktes Format verwenden, falls vorhanden.

(G8) Sollten wir das Beispiel aus der Spezifikation aufnehmen? Ja (um die Vollständigkeit der Tests zu gewährleisten).