Indexanalyse

In diesem Dokument wird die HLO-Indexierungsanalyse beschrieben, mit der Sie Indexierungskarten für HLO-Vorgänge symbolisch berechnen können. Die Indexierungszuordnung ist eine Funktion, die Indizes eines Tensors den Indizes eines anderen zuordnet, z.B. die Indizes einer HLO-Anweisungsausgabe den Indizes der HLO-Anweisungseingaben oder umgekehrt.

Beispiel

Für eine Übertragung von tensor<20xf32> bis tensor<10x20x30xf32>

p0 = f32[20] parameter(0)
bc0 = f32[10, 20, 30] broadcast(p0), dimensions={1}

Die Indexierungszuordnung von der Ausgabe in die Eingabe ist \((i, j, k) \mapsto (j)\) for $i \in [0, 10]\(, \)j \in [0, 20]\( and \)k \in [0, 30]$.

Ziel

XLA GPU verwendet mehrere maßgeschneiderte Lösungen zur Logik von Zusammenführung, Operandenauslastung und Tiling-Schemas (weitere Details unten). Das Ziel der Indexierungsanalyse besteht darin, eine wiederverwendbare Komponente für solche Anwendungsfälle bereitzustellen. Die Indexierungsanalyse basiert auf der Affine Map-Infrastruktur von MLIR und verfügt über eine HLO-Semantik.

Koaleszen

Die Logik der Speicherzusammenführung wird auch bei nicht trivialen Fällen machbar, wenn wir wissen, welche Elemente/Slices der Eingaben gelesen werden, um ein Element der Ausgabe zu berechnen.

Operand-Auslastung

Die Operandenauslastung in XLA gibt an, wie viel jede Eingabe der Anweisung verwendet wird, vorausgesetzt, ihre Ausgabe ist vollständig ausgelastet. Derzeit wird die Auslastung auch nicht für einen generischen Fall berechnet. Die Indexanalyse ermöglicht eine präzise Berechnung der Auslastung.

Kachelung:

Eine Kachel/ein Segment ist eine hyperrechteckige Teilmenge eines Tensors, die durch Versätze, Größen und Schritte parametriert ist. Die Kachelweitergabe ist eine Möglichkeit, die Kachelparameter des Erstellers/Nutzers des Vorgangs mithilfe der Tiling-Parameter des Vorgangs selbst zu berechnen. Es gibt bereits eine Bibliothek, die dies für Softmax und Punkt erledigt. Die Verbreitung von Kacheln kann allgemeiner und robuster gestaltet werden, wenn dies über Indexierungskarten erfolgt.

Funktion und Domain

Die Indexierungszuordnung ist eine Funktion, \(\boldsymbol{f}(\boldsymbol{d}, \boldsymbol{s})\)die einen Multiindex \(\boldsymbol{d}\) eines Tensors \(A\) den Elementen/Bereichen des Tensors \(B\)zuordnet. Der Parameter \(\boldsymbol{s}\) bezieht sich auf die Indexbereiche der Dimensionen, die in Tensor \(B\), aber nicht in Tensor \(A\)vorhanden sind.

Wenn wir beispielsweise eine Reduktion von tensor<2x4x8x16xf32> auf tensor<4x8xf32> haben, lautet die Indexierungszuordnung von der 2D-Ausgabe zur 4D-Eingabe\((d_0, d_1) \mapsto (s_0, d_0, d_1, s_1)\), wobei \(d_i\) die Dimensionsparameter sind, die den Indizes des Ausgabetensors entsprechen. Parameter \(s_j\)codieren mehrere Werte. Um beispielsweise ein \((d_0, d_1)\) Element der Ausgabe zu berechnen, benötigen wir \((s_0, d_0, d_1, s_1)\) Elemente der Eingabe, wobei \(s_0 \in [0, 2)\) und\(s_1 \in [0, 16)\).

Diese Zuordnung kann aus den Attributen von HLO-Anweisungen erstellt werden oder die Zuordnungen nicht zusammengeführter Anweisungen können für die Indexierung einer Fusion erstellt werden. Die Zuordnung hat auch eine Domain, die angibt, für welche Elemente des Tensors die Zuordnung existiert.

\[ \begin{eqnarray} \boldsymbol{f}(\boldsymbol{d}, \boldsymbol{s})\; &s.t.& \\ \boldsymbol{lb}_d &\leq& \boldsymbol{d} \leq \boldsymbol{ub}_d \\ \boldsymbol{lb}_s &\leq& \boldsymbol{s} \leq \boldsymbol{ub}_s \\ \boldsymbol{lb}_g &\leq& \boldsymbol{g}(\boldsymbol{d}, \boldsymbol{s}) \leq \boldsymbol{ub}_g \end{eqnarray} \]

Da wir die Neuberechnung minimieren möchten, benötigen wir eine Bibliothek für symbolische Berechnungen. Da XLA bereits von MLIR abhängig ist, verwenden wir mlir::AffineMap, anstatt eine symbolische arithmetische Bibliothek zu schreiben.

Ein typisches AffineMap-Objekt sieht so aus

(d0)[s0, s1] -> (s0 + 5, d0 * 2, s1 * 3 + 50)

Bei AffineMap gibt es zwei praktische Parametertypen: Dimensionen und Symbole, die wir für \(\boldsymbol d\) und \(\boldsymbol s\) verwenden können. AffineMap enthält keine Metadaten zu Dimensionsbereichen. Wir müssen diese Daten also selbst bereitstellen.

struct Range {
 int64_t lower_bound;
 int64_t upper_bound;
};

struct IndexingMap {
 mlir::AffineMap affine_map;
 std::vector<Range> dimension_ranges;
 std::vector<Range> symbol_ranges;
 llvm::DenseMap<mlir::AffineExpr, Range> expr_ranges;
};

dim_ranges codiert die inklusiven Boxeneinschränkungen für die Dimensionsparameter \(\boldsymbol{d}\) der Indexierungskarte, die normalerweise mit der Form des Ausgabetensors für Operationen wie Transponieren, Reduzieren, elementweise oder Punkt übereinstimmen. Es gibt jedoch einige Ausnahmen wie HloConcatenateInstruction.

symbol_ranges-codieren mögliche Werte, die \(\boldsymbol {s}\) Parameter annehmen können.

Sehen wir uns anhand eines Beispiels an, was das alles genau bedeutet.

Indexierung von Karten für nicht zusammengeführte Vorgänge

Elementweise

Bei elementweisen Vorgängen ist die Indexierungskarte eine Identität.

  p0 = f32[10, 20] parameter(0)
  p1 = f32[10, 20] parameter(1)
  add = f32[10, 20] add(p0, p1)

Die Ausgabe für die Eingabe entspricht:

  • Ausgabe -> Eingabe_0: \((d_0, d_1) \mapsto (d_0, d_1)\) für $\boldsymbol{d} \in [0,9] \times [0, 19]\(, i.e. \)\boldsymbol{d} \in {\rm Dom}(output)$
  • Ausgabe -> Eingabe_1: \((d_0, d_1) \mapsto (d_0, d_1)\) für $\boldsymbol{d} \in {\rm Dom} (Ausgabe)$

Die Eingabe für Ausgabezuordnungen

  • Eingabe_i -> Ausgabe: \((d_0, d_1) \mapsto (d_0, d_1)\) für $\boldsymbol{d} \in {\rm Dom}(output)$

Nachricht an alle

Beim Broadcasting werden einige Dimensionen entfernt, wenn die Ausgabe der Eingabe zugeordnet und bei der Zuordnung der Eingabe zur Ausgabe hinzugefügt wird.

p0 = f32[20] parameter(0)
bc0 = f32[10, 20, 30] broadcast(p0), dimensions={1}

Die Ausgabe für die Eingabezuordnung:

  • Ausgabe -> Eingabe: \((d_0, d_1, d_2) \mapsto (d_1)\) for $\boldsymbol{d} \in {\rm Dom}(output)$

Die Eingabe für die Ausgabezuordnung

  • Eingabe -> Ausgabe: \((d_0) \mapsto (s_0, d_1, s_1)\) für $\boldsymbol{d} \in {\rm Dom}(output)\( and \)\boldsymbol{s} \in [0, 9] \times [0, 29]$.

Beachten Sie, dass sich jetzt \(\boldsymbol s\) auf der rechten Seite für die Eingabe/Ausgabe-Zuordnung befindet. Dies sind die Symbole, die Wertebereiche darstellen. In diesem speziellen Fall wird beispielsweise jedes Eingabeelement mit dem Index \(d_0\) einem Slice der Ausgabe von 10 × 1 × 30 zugeordnet.

Konstante und Iota

Praktischerweise haben sie keine Eingabeparameter, sodass für die Indexierung nichts berechnet werden muss.

Transponieren

Die Indexierungskarte für das Transponieren ist eine Permutation von Eingabe-/Ausgabedimensionen.

p0 = f32[3, 12288, 6, 128] parameter(0)
transpose = f32[3, 6, 128, 12288] transpose(p0), dimensions={0, 2, 3, 1}

Die Ausgabe für die Eingabezuordnung:

  • Ausgabe -> Eingabe: \((d_0, d_1, d_2, d_3) \mapsto (d_0, d_3, d_1, d_2)\) für\(\boldsymbol{d} \in {\rm Dom}(output)\)

Die Eingabe für die Ausgabezuordnung:

  • Eingabe -> Ausgabe: \((d_0, d_1, d_2, d_3) \mapsto (d_0, d_2, d_3, d_1)\) für\(\boldsymbol{d} \in {\rm Dom}(input)\)

Reverse

Die Indexierungskarte für umgekehrt ändert die zurückgesetzten Dimensionen in $upper_bound(d_i)–d_i$:

p0 = f32[1, 17, 9, 9] parameter(0)
reverse = f32[1, 17, 9, 9] reverse(p0), dimensions={1, 2}

Die Ausgabe für die Eingabezuordnung:

  • Ausgabe -> Eingabe: $(d_0, d_1, d_2, d_3) \mapsto (d_0, -d_1 + 16, -d_2 + 8, d_3)\( for \)\boldsymbol{d} \in {\rm Dom}(output)$

Die Eingabe für die Ausgabezuordnung:

  • Eingabe -> Ausgabe: $(d_0, d_1, d_2, d_3) \mapsto (d_0, -d_1 + 16, -d_2 + 8, d_3)\( for \)\boldsymbol{d} \in {\rm Dom}(input)$

(Variadic)Einschränken

Die variative Reduktion hat mehrere Eingaben und mehrere Initialisierungen. Bei der Zuordnung von der Ausgabe zur Eingabe werden die reduzierten Dimensionen hinzugefügt. Sie verhält sich also in gewisser Weise wie eine Umkehrung zu einem Broadcast.

p0 = f32[256,10] parameter(0)
p0_init = f32[] constant(-inf)
p1 = s32[256,10] parameter(1)
p1_init = s32[] constant(0)
reduce = (f32[10], s32[10]) reduce(p0, p1, p0_init, p1_init),
  dimensions={0}, to_apply=min

Die Ausgabe für die Eingabe entspricht:

  • Ausgabe -> input_j: \((d_0) \mapsto (s_0, d_0)\) for $\boldsymbol{d} \in {\rm Dom}(output)\( and \)\boldsymbol{s} \in [0, 9]$
  • Ausgabe -> init_j: \((d_0) \mapsto ()\) für $\boldsymbol{d} \in {\rm Dom}(output)$

Die Eingabe für die Ausgabe ordnet Folgendes zu:

  • Eingabe_i -> Ausgabe_j: \((d_0, d_1) \mapsto (d_1)\) für $\boldsymbol{d} \in {\rm Dom}(input)$
  • init_i ->output_j: \(() \mapsto (s_0)\) für \(\boldsymbol{s} \in [0, 9]\)

für \(i, j = 0, \ldots, INPUT\\_COUNT\).

Slice

Die Indexierung von Ausgabe zu Eingabe für Slice führt zu einer gestaffelten Indexierungskarte, die für jedes Element der Ausgabe gültig ist. Die Zuordnung von Eingabe zu Ausgabe ist auf einen abgegrenzten Bereich der Elemente in der Eingabe beschränkt.

p0 = f32[10, 20, 50] parameter(0)
slice = f32[5, 3, 25] slice(f32[10, 20, 50] p0),
  slice={[5:10:1], [3:20:7], [0:50:2]}

Die Ausgabe für die Eingabezuordnung:

  • Ausgabe -> Eingabe: \((d_0, d_1, d_2) \mapsto (d_0 + 5, 7d_1 + 3, 2d_2)\) für\(\boldsymbol{d} \in {\rm Dom}(output)\)

Die Eingabe für die Ausgabezuordnung:

  • Eingabe -> Ausgabe: \((d_0, d_1, d_2) \mapsto (d_0, d_1 / 7, d_2 / 2)\) für\(\boldsymbol{d} \in [5, 9] \times [3, 19] \times [0, 49]\) mit den Schritten $[1, 7, 2]$.

Noch offen: Indexierung von Eingabe/Ausgabe

Form ändern

Reshapes gibt es in verschiedenen Geschmacksrichtungen.

Form minimieren

Dies ist eine „linearisierte“ Umformung von N–D nach 1D.

p0 = f32[4,8] parameter(0)
reshape = f32[32] reshape(p0)

Die Ausgabe für die Eingabezuordnung:

  • Ausgabe -> Eingabe: \((d_0) \mapsto (d_0 / 8, d_0 \mod 8)\) for $\boldsymbol{d} \in {\rm Dom}(output)$

Die Eingabe für die Ausgabezuordnung:

  • Eingabe -> Ausgabe: \((d_0, d_1) \mapsto (8 d_0 + d_1)\) für $\boldsymbol{d} \in {\rm Dom}(input)$.

Form maximieren

Dies ist eine inverse „Minimierungsform“-Operation, die eine 1D-Eingabe in eine N-D-Ausgabe umformt.

p0 = f32[32] parameter(0)
reshape = f32[4, 8] reshape(p0)

Die Ausgabe für die Eingabezuordnung:

  • Ausgabe -> Eingabe: \((d_0, d_1) \mapsto (8 d_0 + d_1)\) for $\boldsymbol{d} \in {\rm Dom}(output)$

Die Eingabe für die Ausgabezuordnung:

  • Eingabe -> Ausgabe: \((d_0) \mapsto (d_0 / 8, d_0 \mod 8)\) für $\boldsymbol{d} \in {\rm Dom}(input)$.

Allgemeine Formulierung

Dies sind die Vorgänge zur Umformung, die nicht als einzelne Maximierungs- oder Minimierungsform dargestellt werden können. Sie können nur als Zusammensetzung aus zwei oder mehr Erweiterungs- oder Minimierungsformen dargestellt werden.

Beispiel 1: Linearisierung/Delinearisierung.
p0 = f32[4,8] parameter(0)
reshape = f32[2, 4, 4] reshape(p0)

Diese Umformung kann als Zusammensetzung der Minimierungsform von tensor<4x8xf32> zu tensor<32xf32> und dann als Erweiterung der Form auf tensor<2x4x4xf32> dargestellt werden.

Die Ausgabe für die Eingabezuordnung:

  • Ausgabe -> Eingabe: $(d_0, d_1, d_2) \mapsto (2d_0 + (4d_1 + d_2) / 8, 4d_1 + d_2) \mod 8)$

für \(\boldsymbol{d} \in {\rm Dom}(output)\)

Die Eingabe für die Ausgabezuordnung:

  • Eingabe -> Ausgabe: $(d_0, d_1) \mapsto ((8d_0 + d_1) / 16, ((8d_0 + d_1) \mod 16) / 4, d_1 \mod 4)$

für \(\boldsymbol{d} \in {\rm Dom}(input)\).

Beispiel 2: Maximierte und minimierte Unterformen
p0 = f32[4, 8, 12] parameter(0)
reshape = f32[32, 3, 4] reshape(p0)

Diese Umformung kann als Kombination aus zwei Umformungen dargestellt werden. Die erste minimiert die äußersten Dimensionen tensor<4x8x12xf32> auf tensor<32x12xf32> und die zweite erweitert die innerste Dimension tensor<32x12xf32> auf tensor<32x3x4xf32>.

Die Ausgabe für die Eingabezuordnung:

  • Ausgabe -> Eingabe: \((d_0, d_1, d_2) \mapsto (d_0 / 8, d_0 \mod 8, 4d_1 + d_2)\) für \(\boldsymbol{d} \in {\rm Dom}(output)\)

Die Eingabe für die Ausgabezuordnung:

  • Eingabe -> Ausgabe: \((d_0, d_1, d_2) \mapsto (8d_0 + d_1, d_2 / 4, d_2 \mod 4)\)für \(\boldsymbol{d} \in {\rm Dom}(input)\).

Bitcast

Ein Bitcast-Vorgang kann als Sequenz von Transpose-Reshape-Transpose dargestellt werden. Daher sind die Indexierungskarten nur eine Zusammensetzung von Indexierungskarten für diese Sequenz.

Verketten

Die Ausgabe-zu-Eingabe-Zuordnung für Concat ist für alle Eingaben definiert, aber mit nicht überlappenden Domains, d.h., es wird immer nur eine der Eingaben verwendet.

p0 = f32[3,50] parameter(0)
p1 = f32[3,30] parameter(1)
concat = f32[3,80] concatenate(f32[3,50] p0, f32[3,30] p1),
  dimensions={1}

Die Ausgabe für die Eingabezuordnung:

  • Ausgabe -> Eingabe 1:

\((d_0, d_1) \mapsto (d_0, d_1)\) für \(\boldsymbol{d} \in [0, 2] \times [0, 49]\)

  • Ausgabe -> Eingabe 2:

\((d_0, d_1) \mapsto (d_0, d_1 - 50)\) für $\boldsymbol{d} \in [0, 2] \times [50, 79]$

Die Eingaben für die Ausgabe:

  • Eingabe 1 -> Ausgabe: \((d_0, d_1) \mapsto (d_0, d_1)\) für $\boldsymbol{d} \in {\rm Dom}(input_1)$.
  • Eingabe 2 -> Ausgabe: \((d_0, d_1) \mapsto (d_0, d_1 + 50)\) für $\boldsymbol{d} \in {\rm Dom}(input_2)$.

Punkt (Ausgabe-zu-Eingabe implementiert)

Die Indexierungskarten für Punkte sind denen von Reduce ähnlich.

p0 = f32[4, 128, 256] parameter(0)
p1 = f32[4, 256, 64] parameter(1)
dot = f32[4, 128, 64] dot(p0, p1),
  lhs_batch_dims={0}, rhs_batch_dims={0},
  lhs_contracting_dims={2}, rhs_contracting_dims={1}

Die Ausgabe für Eingaben entspricht:

  • Ausgabe -> Eingabe_1: \((d_0, d_1, d_2) \mapsto (d_0, d_1, s_0)\) für\(\boldsymbol{d} \in {\rm Dom}(output)\) und \(\boldsymbol{s} \in [0, 255]\)
  • Ausgabe -> Eingabe_2: \((d_0, d_1, d_2) \mapsto (d_0, s_0, d_2)\) für\(\boldsymbol{d} \in {\rm Dom}(output)\) und \(\boldsymbol{s} \in [0, 255]\)

Die Eingaben für die Ausgabe entsprechen:

  • Eingabe_1 -> Ausgabe: \((d_0, d_1, d_2) \mapsto (d_0, d_1, s_0)\) für\(\boldsymbol{d} \in {\rm Dom}(input_1)\) und \(\boldsymbol{s} \in [0, 63]\)
  • Eingabe_2 -> Ausgabe: \((d_0, d_1, d_2) \mapsto (d_0, s_0, d_1)\) für\(\boldsymbol{d} \in {\rm Dom}(input_2)\) und \(\boldsymbol{s} \in [0, 127]\)

Fenster verkleinern (TBD)

Auflage (TBD)

Maps for Fusion indexieren

Die Indexierungskarte für Fusionsvorgänge ist eine Zusammenstellung von Indexierungskarten für jeden Vorgang im Cluster. Es kann vorkommen, dass einige Eingaben mit unterschiedlichen Zugriffsmustern mehrmals gelesen werden.

Eine Eingabe, mehrere Indexierungskarten

Hier ist ein Beispiel für: \(p_0 + p_0^T\)

f {
  p0 = f32[1000, 1000] parameter(0)
  transpose_p0 = f32[1000, 1000]{0, 1} transpose(p0), dimensions={1, 0}
  ROOT a0 = f32[1000, 1000] add(p0, transpose_p0)
}

Die Indexierungszuordnungen von Ausgabe zu Eingabe für p0 lauten $(d_0, d_1) \mapsto (d_0, d_1)\( and \)(d_0, d_1) \mapsto (d_1, d_0)$. Dies bedeutet, dass wir zum Berechnen eines Elements der Ausgabe möglicherweise den Eingabeparameter zweimal lesen müssen.

Eine Eingabe, deduplizierte Indexierungskarte

img

Es gibt Fälle, in denen die Indexierungskarten tatsächlich identisch sind, obwohl dies nicht sofort offensichtlich ist.

f {
  p0 = f32[20, 10, 50] parameter(0)
  lhs_transpose_1 = f32[10, 20, 50] transpose(p0), dimensions={1, 0, 2}
  lhs_e = f32[10, 20, 50] exponential(lhs_transpose_1)
  lhs_transpose_2 = f32[10, 50, 20] transpose(lhs_e), dimensions={0, 2, 1}
  rhs_transpose_1 = f32[50, 10, 20] transpose(p0), dimensions={2, 1, 0}
  rhs_log = f32[50, 10, 20] exponential(rhs_transpose_1)
  rhs_transpose_2 = f32[10, 50, 20] transpose(rhs_log), dimensions={1, 0, 2}
  ROOT add = f32[10, 50, 20] add(lhs_transpose_2, rhs_transpose_2)
}

Die Indexierungszuordnung von Ausgabe zu Eingabe für p0 ist in diesem Fall nur $(d_0, d_1, d_2) \mapsto (d_2, d_0, d_1)$.

SoftMax

img

Die Indexierung von Ausgabe zu Eingabe ist für parameter 0 für Softmax zugeordnet:

  • \((d_0, d_1, d_2) \mapsto (d_0, d_1, d_2)\)
  • \((d_0, d_1, d_2)[s_0] \mapsto (d_0, d_1, s_0)\)

for \(\boldsymbol{d} \in {\rm Dom}(output)\) and \(\boldsymbol{s} \in [0, 124]\)bezieht sich auf die innerste Dimension der Eingabe.

Kartenvereinfacher indexieren

Mit der Standardvereinfachung für mlir::AffineMap-Upstream können keine Annahmen über die Dimensions-/Symbolbereiche getroffen werden. Daher können Ausdrücke mit mod und div nicht effizient vereinfacht werden.

Wir können das Wissen über die Unter- und Obergrenze der Unterausdrücke in den affinen Maps nutzen, um sie noch weiter zu vereinfachen.

Die Vereinfachungsfunktion kann die folgenden Ausdrücke umschreiben.

  1. \((d_0, d_1) \mapsto (d_0 + d1 / 16, d1 \mod 16)\) für $\boldsymbol{d} \in [0, 6] \times [0, 14]\( becomes \)(d_0, d_1) \mapsto (d_0, d_1)$
  2. $(d_0, d_1, d_2) \mapsto ((100d_0 + 10d_1 + d_2) /100, ((100d_0 + 10d_1 + d_2) \mod 100) / 10, d_0, \, d_0, \, d_1, d_9, d_1, d_1\( for \)\( becomes \)
  3. $(d_0, d_1, d_2) \mapsto ((16d_0 + 4d_1 + d_2) /8, (16d_0 + 4d_1 + d_2) \mod 8)\( for \)d_i \in [0, 9]\( becomes \)(d_0, d_1, +to d_2) \
  4. \((d_0, d_1) \mapsto (-(-11d_0 - d_1 + 109) / 11 + 9)\) für $\boldsymbol{d} \in [0, 9] \times [0, 10]\( becomes \)(d_0, d_1) \mapsto (d_0)$.

Durch die Indexierung der Kartenvereinfachung können wir nachvollziehen, dass sich einige der verketteten Umformen in HLO gegenseitig aufheben.

p0 = f32[10, 10, 10] parameter(0)
reshape1 = f32[50, 20] reshape(p0)
reshape2 = f32[10, 10, 10] reshape(reshape1)

Nach der Zusammensetzung der Indexierungskarten und deren Vereinfachung erhalten wir

\((d_0, d_1, d_2) \mapsto (d_0, d_1, d_2)\).