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 Indexindex
alsElement
-Objekt enthält.void Tensor::set(llvm::ArrayRef<int64_t> index, Element element);
: Zum Aktualisieren einesElement
-Objektselement
in einen Tensor bei mehrdimensionalen Indexindex
.
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:
- Erfasst die SSA-Argumente von
func
und die zugehörige LaufzeitTensor
-Werte, die inargs
angegeben werden, unter Verwendung der Symboltabellenzuordnung M. - 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 Aufrufeval
angegeben werden soll. - Verfolgt die SSA-Ergebnisse des Vorgangs und den ausgewerteten Wert in M.
- Ruft
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:
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 Vorgangadd
gibt es beispielsweise exklusiven Code. ganzzahlige, boolesche, Gleitkomma- und komplexe Typen zu verarbeiten. für jede Typkategorie einen Test.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 Vorgangadd
alle Varianten der Ganzzahltypen (si4
,u4
,si8
,u8
) und so weiter) mithilfe vonllvm::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?
- 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.)
- 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).