Parallele Matrizenmultiplikation
# Parallele Matrizenmultiplikation

Die Multiplikation von Matrizen ist Bestandteil vieler Algorithmen zur Lösung komplexerer Problemstellungen. So wird diese zur Berechnung der Pfadlänge in Graphen genutzt oder zur Bestimmung erreichbarer Knoten verwendet. Hierbei wird die Multiplikation häufig mehrmals ausgeführt, sodass es ein Bestreben ist, die Laufzeit für die Multiplikation zu verringern. Zusätzlich werden Datensätze immer größer, beispielsweise Abhängigkeitsgraphen zwischen Nutzern sozialer Plattformen, wodurch die Größe der Matrizen wächst und effizientere Algorithmen benötigt werden. Neben der Möglichkeit, Algorithmen mit besserer Komplexität zu entwickeln, kann die Multiplikation parallel, also auf mehreren Prozessoren gleichzeitig, ausgeführt werden. Ein Beispiel hierfür ist die Matrizenmultiplikation nach Cannon, entwickelt von Lynn Elliot Cannon.

## Algorithmen

### Fox-Algorithmus

Der Fox-Algorithmus, benannt nach Geoffrey C. Fox, ist ein Algorithmus zur parallel, auf p Prozessoren gleichzeitig ausgeführten Matrizenmultiplikation. In der Entwicklung des Algorithmus wurde eine Topologie genutzt, in der die Prozessoren in einem Hyperwürfel angeordnet sind. Als Speichermodell wird das Modell des verteilten Speicher genutzt. Hierbei hat jeder Prozessor seinen eigenen privaten Adressraum und die Kommunikation zwischen Prozessoren ist nachrichtenbasiert. Der Anspruch bei der Entwicklung war es einen effizienten und einfach einzusetzenden Algorithmus, zur Nutzung in wissenschaftlichen Berechnungen, zu erstellen. Des Weiteren war es ein Bestreben einen Algorithmus zu entwickeln, der auch für zukünftige Maschinen mit vielen Prozessoren skaliert. Essentiell für den Algorithmus sind Systolische Arrays. Dieses bezeichnet ein Pipe-Netzwerk, durch welches Datenströme hindurchgetaktet werden.

**Beschreibung**

Es werden zwei ${n\times n}$-Matrizen A und B multipliziert. Hierbei sind A und B vollbesetzte Matrizen. Die Elemente der Matrizen werden mit Hilfe der i- und j-Koordinaten in der Matrix auf die Prozessoren, die ebenfalls eine i- und j-Koordinate innerhalb des Hyperwürfel besitzen, verteilt. Somit hat zu Begin der Berechnung jeder Prozessor ein Element der Matrix A und ein Element der Matrix B. Für Matrizen, bei denen gilt ${n=p}$ erhält jeder Prozessor einen Eintrag der Matrix A und einen Eintrag der Matrix B. Für Matrizen, bei denen gilt ${n>p}$ erhält jeder Prozessor eine Teilmatrix der Größe $({\frac {n}{\sqrt {p}}})\times ({\frac {n}{\sqrt {p}}})$ von A und B. Im Fall der Teilmatrizen führt jeder Prozessor eine eigene Matrizenmultiplikation der Teilmatrizen durch.

Während des Algorithmus werden die Elemente der Matrix A mittels Broadcast in den Prozessorreihen, also allen Prozessoren mit gleicher j-Koordinate, verteilt. Die Elemente der Matrix B werden an den darüber liegenden Prozessor weitergegeben. Bei Prozessoren am oberen Rand werden die Elemente an den Prozessor am unteren Ende weitergegeben.

Bei initialer Startkonfiguration hält jeder Prozessor das Element der Matrix A und B, das die gleichen Werte für die Koordinaten i und j besitzt, wie der Prozessor selbst im Hyperwürfel.

Der Algorithmus besteht aus folgenden Schritten:

* Broadcast der Elemente auf der Diagonalen der Matrix A an die Prozessoren mit gleicher j-Koordinate. Anschließend halten alle Prozessoren in der ersten Reihe das Element $A_{00}$, alle Prozessoren in der zweiten Reihe das Element $A_{11}$. Dieses Muster setzt sich für alle Prozessoren fort.
* Multiplikation des empfangenen Elements der Matrix A mit dem derzeit im Prozessor vorhandenen Element der Matrix B.
* Alle Prozessoren reichen ihr derzeitiges Element der Matrix B an den Prozessor in der gleichen Spalte, eine Stelle über sich, weiter. Die Prozessoren am Rand übergeben ihr Element an die Prozessoren am unteren Rand.
* Broadcast der Elemente auf der 'Diagonalen + 1' der Matrix A an die Prozessoren mit gleicher j-Koordinate. Anschließend halten alle Prozessoren in der ersten Reihe das Element $A_{01}$, alle Prozessoren in der zweiten Reihe das Element $A_{12}$. Dieses Muster setzt sich für alle Prozessoren fort.
* Multiplikation des empfangenen Elements der Matrix A mit dem derzeit im Prozessor vorhandenen Element der Matrix B und anschließende Addition des Ergebnisses mit dem Ergebnis aus der vorherigen Multiplikation.

Dieses Muster setzt sich fort bis die Elemente von B wieder in der Ausgangsstellung sind und alle Nebendiagonalen von A verwendet wurden.

**Pseudocode**

In Pseudocode kann der Algorithmus wie folgt implementiert werden:

**Analyse**

Sollen zwei Matrizen der Größe $n\times n$ mittels ${\sqrt {q}}\times {\sqrt {q}}$ Prozessoren multipliziert werden. So erhält jeder Prozessor zwei Teilmatrizen der Größe $({\frac {n}{\sqrt {p}}})\times ({\frac {n}{\sqrt {p}}})$.

Das Muster Broadcast-Multiplikation-Rotation wiederholt sich ${\sqrt {p}}$-mal, bis das Ergebnis feststeht.

Insgesamt hat der Algorithmus eine Berechnungszeit von $T={\sqrt {q}}*(2m^{3}t_{flop}+2m^{2}t_{comm}+({\sqrt {p}}1)t_{start})$

Diese setzt sich zusammen aus den einzelnen Teilschritten:

Hierbei ist $t_{comm}$ die Zeit, um eine Gleichkommazahl zwischen Prozessoren zu übertragen, $t_{start}$ die Startzeit um die Pipeline zu befüllen und $t_{flop}$ die Zeit für eine Multiplikation oder Addition von Gleichkommazahlen.

Die Analyse gilt für alle Maschinen, die der flynnsche Klassifikation MIMD zugeordnet werden können und das Speichermodell des verteilten Speichers haben.

Ein Nachteil des Algorithmus ist die Skalierbarkeit für die Multiplikation von zwei $n\times n$-Matrizen. Es können höchstens $n^{2}$ Prozessoren benutzt werden, womit die Laufzeit in $\Omega (n)$ liegt, da es $\Theta (n^{3})$ Operationen im seriellen Algorithmus sind.

### DNS-Algorithmus

Der DNS-Algorithmus, benannt nach Elizier Dekel, David Nassimi und Sartaj Sahni, wurde 1981 von diesen veröffentlicht. Der Algorithmus wurde entwickelt für die parallele Multiplikation von Matrizen auf general purpose Prozessoren. Der Algorithmus richtet sich an parallele Computer die der flynnsche Klassifikation SIMD zugeordnet werden können. Diese Computer haben gemeinsam, dass sie aus p verarbeitenden Elementen (processing elements) PE bestehen. Jedes dieser verarbeitenden Elemente kann die Standard arithmetischen und logischen Operationen durchführen. Insgesamt können bis zu $n^{3}$ Prozessoren benutzt werden. Diese sind hierbei in einem dreidimensionalen Hyperwürfel angeordnet. Bei $n^{3}$ skalaren Multiplikationen folgt daraus, dass jeder Prozessor eine skalare Multiplikation durchführen muss.

Der Algorithmus wurde vor allem entwickelt, um Probleme der Graphentheorie zu lösen, beispielsweise das Finden des kürzesten Weges zwischen allen All-Pairs Shortest Path oder das bestimmen von Radius, Durchmesser und Mittelpunkt eines Graphen.

**Beschreibung**

Es werden zwei ${n\times n}$-Matrizen, A und B multipliziert. Diese sind vollbesetzte Matrizen. Konkret werden bei diesem Algorithmus $m:={\sqrt[{3}]{p}}$ und damit $m^{3}$ Prozessoren genutzt, die in einem Würfel angeordnet sind. Die Elemente der Matrizen werden mittels der i- und j-Koordinate den Prozessoren mit passenden Koordinaten innerhalb des dreidimensionalen Hyperwürfels zugeordnet. Die Multiplikation soll dann auf $m\times m$-Matrizen durchgeführt werden. Die Einträge der Matrizen sind wiederum Matrizen mit der Größe ${\frac {m}{n}}\times {\frac {m}{n}}$. Hierbei soll $m$ ein Teiler von $n$ sein.

Um Prozessoren zu adressieren, nutzen wir die Schreibweise $PE(i,j,k)$. Der initiale Zustand kann daher ausgedrückt werden durch $A(i,k,0),B(0,k,j)$. Es befinden sich die Elemente der Matrix A auf den Prozessoren auf der Vorderseite des Hyperwürfels und die Elemente der Matrix B auf der linken Seitenfläche des Hyperwürfels. Die Prozessoren der linken vorderen Kante haben somit bereits ein Element der Matrix A und ein Element der Matrix B. Dies ist die initiale Startkonfiguration.

Der Algorithmus besteht aus folgenden Schritten:

* Broadcast der Matrizen A und B über die $n^{3}$ Prozessoren. Hierbei werden die Elemente der Matrix A (befindet sich auf der Vorderseite des Würfels) entlang der j-Dimension nach hinten gesendet. Die Elemente der Matrix B (befindet sich auf der linken Außenfläche des Würfels) werden entlang der k-Dimension nach rechts gesendet. Nach Abschluss des Broadcast hält jeder Prozessor 1 Element der Matrix A und ein Element der Matrix B.
* Nun führt jeder Prozessor die Multiplikation der vorhandenen Elemente durch.
* In der letzten Phase wird die Summe der Einzelergebnisse gebildet. Dies geschieht, indem von oben nach unten die Teilergebnisse gesendet und auf dem Weg addiert werden. Nach Abschluss dieser Operation befindet sich die Ergebnismatrix auf der Bodenfläche des Hyperwürfels.

**Pseudocode**

Eine Implementierung in Pseudocode könnte wie folgt aussehen.

**Analyse**

Angenommen es sollen zwei Matrizen der Größe ${n\times n}$ mittels $m:={\sqrt[{3}]{p}}$ und damit $m^{3}$ Prozessoren multipliziert werden. Es gilt die Anzahl Prozessoren $p=q^{3}$.

Insgesamt hat der Algorithmus hat eine Berechnungszeit von:

Die Berechnungszeit setzt sich zusammen aus:

Hieraus ergibt sich:

Durch einsetzen der obigen Bedingung $p=q^{3}$ erhält man die obige Berechnungszeit:

Der Algorithmus ist für $n^{3}=\Theta (p\log p)$ oder $p=\Omega ({\frac {n^{3}}{\log n}})$ kostenoptimal.

