GEMM Micro Kernel für AVX
Für spezielle Werte von GEMM_MR und DGEMM_NR soll ein in Assembler geschriebener GEMM-Micro-Kernel verwendet werden.
Register bei der Intel64-Architektur
-
Die Archtiketur verfügt über 16 Integer-Register, die jeweils 64-Bit breit sind. Die Namen der Register lauten: rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8, r9, r10, r11, r12, r13, r14 und r15.
-
Für Fließkommazahlen stehen 16 AVX-Register zur Verfügung, die 256-Bit breit sind. Es können in einem Register also 4 Werte vom Type double gespeichert werden. Hier sind die Namen etwas leichter zu merken: ymm0, ymm1, ..., ymm15.
Über die Namen xmm0, xmm1, ..., xmm15 werden die unteren 128-Bit angesprochen.
-
Das Register rip enthält die Adresse des gerade ausgeführten Befehles.
-
Das Register rsp wird per Konvention als Stack-Pointer verwendet.
Die Assembler-Syntax ist relativ ähnlich zur ULM-Syntax (was kein Zufall ist). Dazu später mehr.
Parameterübergabe bei Funktionsaufrufen
Uns interessiert insbesondere wie Integer-Werte und Fließkommazahlen bei einem Funktionsaufruf übergeben werden:
-
Die ersten 6 Integer-Werte werden in den Registern rdi, rsi, rdx, rcx, r8 und r9. Weitere Werte können über den Stack übergeben werden. Was in unserem Fall aber nicht notwenig sein wird. Zum Glück benötigt der GEMM-Micro-kernel
void dgemm_micro(int k, double alpha, const double *A, const double *B, double beta, double *C, int incRowC, int incColC);
Genau sechs Integer-Werte: k, A, B, C, incRowC, incColC. Beachtet, dass eine Variable vom Type double *` eine Adresse also auch ein Integer-Wert ist.
-
Die ersten 8 Fließkommazahlen werden in den Registern xmm0 bis xmm7 übergeben. Bei einer Fließkommazahl vom Typ float werden diese in die unteren 32-Bit, bei double in die unteren 64-Bit gelegt.
Im Fall des GEMM-Micro-Kernel liegen die Argumente somit in folgenden Registern:
Parameter |
Register |
k |
rdi |
alpha |
xmm0 |
A |
rsi |
B |
rdx |
beta |
xmm1 |
C |
rcx |
incRowC |
r8 |
incColC |
r9 |
Konventionen zur Registerverwendung
Register gehören teilweise dem Caller (Aufrufer einer Funktion) oder Callee (aufgerufene Funktion). Soll in der Funktion ein Caller-Register verwendet werden muss der Inhalt zunächst gesichert werden und vor dem Rücksprung wiederhergestellt werden. Beispielsweise indem der Inhalt auf dem Stack gesichert wird.
Für uns relevant sind folgende Register:
rax |
Callee |
rbx |
Caller |
rcx |
Callee |
rdx |
Callee |
rsp |
Caller |
rbp |
Caller |
rsi |
Callee |
rdi |
Callee |
r8-r11 |
Callee |
r12-r15 |
Caller |
xmm0-xmm15 |
Callee |
Rückkehr vom Funktionsaufruf
Die Rücksprungadresse wird über den Stack übergeben (und liegt auf der Stack-Spitze). Mit dem Befehl retq wird der Stack abgebaut und zur Rücksprungsadresse zurückgekehrt.
Rumpf für den GEMM-Micro-Kernel
# void # gemm_micro(long k, # double alpha, # double *A, # double *B, # double beta, # double *C, long incRowC, long incColC); .globl ugemm_4_8 ugemm_4_8: # Arguments: # %rdi long k # %xmm0 double alpha # %rsi double *A # %rdx double *B # %xmm1 double beta # %rcx double *C # %r8 long incRowC # %r9 long incColC # # ... code for function ... # retq
Befehle für Integer-Register
Im Gegensatz zur ULM haben Befehle für Integer-Register auf der Intel64-Architektur meist nur zwei Argumente oder sogar nur ein Argument.
Laden und Speichern von Daten
movq (%rsi), %r10 |
64-Bit von Adresse %rsi in Register r10 laden |
movq 8(%rsi), %r10 |
64-Bit von Adresse %rsi+8 in Register r10 laden |
movq (%rcx,%r8), %r10 |
64-Bit von Adresse %rcx+%r8 in Register r10 laden |
movq %r10, (%rsi) |
64-Bit Inhalt von Register r10 an die Adresse %rsi laden. |
movq %r10, 8(%rsi) |
64-Bit Inhalt von Register r10 an die Adresse %rsi+8 laden. |
movq %r10, (%rsi,%r8) |
64-Bit Inhalt von Register r10 an die Adresse %rsi+%r8 laden. |
Bei der Angabe einer Adresse wird allgemein das Konstrukt d(a, b, c) interpretiert als der Speicherinhalt bei Adresse \(\text{%}a + c\cdot\text{%}b + d\). Dabei sind für \(c\) allerdings nur die Wert \(1\), \(2\) oder \(4\) erlaubt. Mit dem Befehl leaq (Load effective Adresse) kann dies beispielsweise ausgenutzt werden, um eine Integer-Zahl mit 2 oder 4 zu multiplizieren:
leaq (,%r8,2) %r10
Hier wird (,%r8,2) als Adresse \(2\cdot \text{%r8}\) interpretiert und dieser Wert in das Register r10 kopiert.
Integer-Arithmetik
addq %rsi, %rdi |
\(\text{%rsi} + \text{%rdi} \rightarrow \text{%rdi}\) |
addq $10, %rdi |
\(10 + \text{%rdi} \rightarrow \text{%rdi}\) |
subq %rsi, %rdi |
\(\text{%rdi} - \text{%rsi} \rightarrow \text{%rdi}\) |
subq $10, %rdi |
\(\text{%rdi} - 10 \rightarrow \text{%rdi}\) |
incq %rdi |
\(\text{%rdi} + 1 \rightarrow \text{%rdi}\) |
decq %rdi |
\(\text{%rdi} - 1 \rightarrow \text{%rdi}\) |
Mit testq wird eine bitweise Und-Verknüpfung ausgeführt, die nur die Statusregister setzt aber kein Register-Argumnet verändert:
testq %rsi, %rdi |
\(\text{%rdi} \land \text{%rsi}\) |
testq $10, %rdi |
\(\text{%rdi} \land 10\) |
Insbesondere kann mit testq \text{%}rdi, \text{%}rdi überprüft werden, ob ein Register den Wert Null enthält oder nicht.
Sprungbefehle
Wie beim ULM-Assembler können Befehle mit einem Label markiert werden und bei Sprungbefehlen als Ziel angegeben werden.
jmp foo |
Unbedingter Sprung zum Label foo |
jl foo |
Bedingter jump less Sprung zum Label foo |
Die Bezeichnung aller weiteren Varianten (je, jg, ...) von bedingten Sprüngen entsprechen denen des ULM-Assembler.
Befehle für AVX-Register
Beachtet, dass zum Beispiel mit %xmm0 und %ymm0 dasselber Register bezeichnet wird. Mit %xmm0 werden davon aber nur die unteren 128-Bit angesprochen. Enthält ymm0 etwa die vier double-Werte \(a\), \(b\), \(c\) und \(d\). Entspricht das dem Bit-Muster \((d,c,b,a)_{64}\). Der Inhalt von %xmm0 ist dann \((b,a)_{64}\). Wobei \(a\) und \(b\) in beiden Fällen identisch sind.
Laden von Daten
vmovapd (%rsi), %ymm0 |
move aligned packed double: Von Adresse %rsi 256-Bit in Register ymm0 laden. Die Adresse %rsi muss durch 32 teilbar sein. |
vbroadcastsd (%rsi), %ymm0 |
broadcast single double: Von Adresse %rsi 64-Bit in die vier Slots des Registers ymm0 laden. Ist \(a\) der Wert bei Adresse %rsi, dann enthält %ymm0danach die Werte \((a,a,a,a)_{64}\). |
vmovlpd (%rsi), %ymm0, %ymm0 |
move low packed double: Von Adresse %rsi 64-Bit in Register in die untersten 64-Bit von ymm0 laden. Ist \(a\) der Wert bei Adresse %rsi, dann enthält %ymm0danach die Wert \((*,*,*,a)_{64}\). Mit Stern markierte Werte werden nicht verändert. |
vmovhpd (%rsi), %ymm0, %ymm0 |
move high packed double: Von Adresse %rsi 64-Bit in Register in die nächst höheren 64-Bit von ymm0 laden. Ist \(a\) der Wert bei Adresse %rsi, dann enthält %ymm0danach die Wert \((*,*,a,*)_{64}\). |
Speichern von Daten
vmovapd %ymm0, (%rsi) |
move aligned packed double: Nach Adresse %rsi 256-Bit aus Register ymm0 laden. Die Adresse %rsi muss durch 32 teilbar sein. |
vmovlpd %ymm0, (%rsi) |
move low packed double: Nach Adresse %rsi die untersten 64-Bit aus Register laden. |
vmovhpd %ymm0, (%rsi) |
move high packed double: Nach Adresse %rsi das zweit-unterste 64-Bitmuster von Register %ymm0 laden. |
Sowohl beim Laden als auch Speichern sind die üblichen Varianten zur Angabe einer Adresse möglich:
-
Adresse ist Inhalt eines Registers.
-
Adresse ist Summe von zwei Registerinhalten.
-
Adresse ist Inhalt eines Registers plus ein immediate value (Konstante vor der Klammer).
Muliplikation und Addition
vmulpd %ymm0, %ymm1, %ymm2 |
multiplicate packed double: Komponentenweise Multiplikation \((a_3,a_2,a_1,a_0)\otimes (b_3,b_2,b_1,b_0)\rightarrow(a_3\cdot b_3,a_2\cdot b_2,a_1\cdot b_1,a_0\cdot b_0)\) |
vaddpd %ymm0, %ymm1, %ymm2 |
multiplicate packed double: Komponentenweise Addition \((a_3,a_2,a_1,a_0) \oplus (b_3,b_2,b_1,b_0) \rightarrow (a_3+b_3,a_2+b_2,a_1+b_1,a_0+b_0)\) |
Bit-Operationen
Wir benötigen nur eine XOR-Operation, um ein Register auf Null zusetzen. Allgemein lautet der Befehl
vxorpd %ymm0, %ymm1, %ymm2 |
xor packed double: Komponentenweise XOR-Verknüpfung \((a_3,a_2,a_1,a_0)\veebar (b_3,b_2,b_1,b_0)\rightarrow(a_3\veebar b_3,a_2\veebar b_2,a_1\veebar b_1,a_0 \veebar b_0)\) |
AVX-Algorithmus für GEMM-Micro Kernel
Als Paramater für die Computer im E44 wählen wir \(M_r = 4\) und \(N_r = 8\). Damit ergibt sich die spezielle Operation
\[C\leftarrow\beta\;\underbrace{\left(\begin{array}{cccccccc} c_{0,0} & c_{0,1} & c_{0,2} & c_{0,3} & c_{0,4} & c_{0,5} & c_{0,6} & c_{0,7} \\ c_{1,0} & c_{1,1} & c_{1,2} & c_{1,3} & c_{1,4} & c_{1,5} & c_{1,6} & c_{1,7} \\ c_{2,0} & c_{2,1} & c_{2,2} & c_{2,3} & c_{2,4} & c_{2,5} & c_{2,6} & c_{2,7} \\ c_{3,0} & c_{3,1} & c_{3,2} & c_{3,3} & c_{3,4} & c_{3,5} & c_{3,6} & c_{3,7} \\ \end{array}\right)}_{=C}+\alpha\;\left(\begin{array}{cccc} a_{0,0} & a_{0,1} & \dots & a_{0,k-1} \\ a_{1,0} & a_{1,1} & \dots & a_{1,k-1} \\ a_{2,0} & a_{2,1} & \dots & a_{2,k-1} \\ a_{3,0} & a_{3,1} & \dots & a_{3,k-1} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0,0} & b_{0,1} & b_{0,2} & b_{0,3} & b_{0,4} & b_{0,5} & b_{0,6} & b_{0,7} \\ b_{1,0} & b_{1,1} & b_{1,2} & b_{1,3} & b_{1,4} & b_{1,5} & b_{1,6} & b_{1,7} \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ b_{k-1,0} & b_{k-1,1} & b_{k-1,2} & b_{k-1,3} & b_{k-1,4} & b_{k-1,5} & b_{k-1,6} & b_{k-1,7} \\ \end{array}\right)\]Die Elemente \(a_{i,\ell}\) und \(b_{\ell,j}\) mit \(0\leq i < 4\), \(0\leq j < 8\) und \(0\leq \ell < k\) nummerieren wir entsprechend der Reihenfolge in der diese im Speicher liegen:
\[C\leftarrow\beta\;\left(\begin{array}{cccccccc} c_{0,0} & c_{0,1} & c_{0,2} & c_{0,3} & c_{0,4} & c_{0,5} & c_{0,6} & c_{0,7} \\ c_{1,0} & c_{1,1} & c_{1,2} & c_{1,3} & c_{1,4} & c_{1,5} & c_{1,6} & c_{1,7} \\ c_{2,0} & c_{2,1} & c_{2,2} & c_{2,3} & c_{2,4} & c_{2,5} & c_{2,6} & c_{2,7} \\ c_{3,0} & c_{3,1} & c_{3,2} & c_{3,3} & c_{3,4} & c_{3,5} & c_{3,6} & c_{3,7} \\ \end{array}\right)+\alpha\;\left(\begin{array}{cccc} a_{0} & a_{4} & \dots & a_{4\cdot k-4} \\ a_{1} & a_{5} & \dots & a_{4\cdot k-3} \\ a_{2} & a_{6} & \dots & a_{4\cdot k-2} \\ a_{3} & a_{7} & \dots & a_{4\cdot k-1} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0} & b_{1} & b_{2} & b_{3} & b_{4} & b_{5} & b_{6} & b_{7} \\ b_{8} & b_{9} & b_{10} & b_{11} & b_{12} & b_{13} & b_{14} & b_{15} \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ b_{8\cdot k-8} & b_{8\cdot k-7} & b_{8\cdot k-6} & b_{8\cdot k-5} & b_{8\cdot k-4} & b_{8\cdot k-3} & b_{8\cdot k-2} & b_{8\cdot k-1} \\ \end{array}\right)\]Das Produkt der Paneelen betrachten wir als Summe dyadischer Produkte:
\[C\leftarrow\beta\;\left(\begin{array}{cccccccc} c_{0,0} & c_{0,1} & c_{0,2} & c_{0,3} & c_{0,4} & c_{0,5} & c_{0,6} & c_{0,7} \\ c_{1,0} & c_{1,1} & c_{1,2} & c_{1,3} & c_{1,4} & c_{1,5} & c_{1,6} & c_{1,7} \\ c_{2,0} & c_{2,1} & c_{2,2} & c_{2,3} & c_{2,4} & c_{2,5} & c_{2,6} & c_{2,7} \\ c_{3,0} & c_{3,1} & c_{3,2} & c_{3,3} & c_{3,4} & c_{3,5} & c_{3,6} & c_{3,7} \\ \end{array}\right)+\alpha\;\left(\left(\begin{array}{c} a_{0} \\ a_{1} \\ a_{2} \\ a_{3} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0} & b_{1} & b_{2} & b_{3} & b_{4} & b_{5} & b_{6} & b_{7} \\ \end{array}\right)+\left(\begin{array}{c} a_{4} \\ a_{5} \\ a_{6} \\ a_{7} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{8} & b_{9} & b_{10} & b_{11} & b_{12} & b_{13} & b_{14} & b_{15} \\ \end{array}\right)+\dots+\left(\begin{array}{c} a_{4\cdot k-4} \\ a_{4\cdot k-3} \\ a_{4\cdot k-2} \\ a_{4\cdot k-1} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{8\cdot k-8} & b_{8\cdot k-7} & b_{8\cdot k-6} & b_{8\cdot k-5} & b_{8\cdot k-4} & b_{8\cdot k-3} & b_{8\cdot k-2} & b_{8\cdot k-1} \\ \end{array}\right)\right)\]Zuordnung von Registern
Die Werte von \(\alpha\) und \(\beta\) in den Registern ymm6 und ymm7 werden zunächst nicht benötigt. Um die Register anderweitig benutzen zu können retten wir deren Inhalt auf den Stack:
vmovlpd %xmm0, -8(%rsp) vmovlpd %xmm1, -16(%rsp)
Um ebenfalls die Register rax und rbx verwenden zu können retten wir auch diese:
movq %rax, -24(%rsp) movq %rbx, -32(%rsp)
Die Register ymm8 bis ymm15 benutzen wir, um dis Summe der dyadischen Produkte zu puffern:
\[\begin{array}{lcllcl}\text{%ymm8 } & \leadsto & \left(c_{0,0},\,c_{0,1},\,c_{0,2},\,c_{0,3}\right), & \qquad\text{%ymm9 } & \leadsto & \left(c_{0,4},\,c_{0,5},\,c_{0,6},\,c_{0,7}\right), \\\text{%ymm10} & \leadsto & \left(c_{1,0},\,c_{1,1},\,c_{1,2},\,c_{1,3}\right), & \qquad\text{%ymm11} & \leadsto & \left(c_{1,4},\,c_{1,5},\,c_{1,6},\,c_{1,7}\right), \\\text{%ymm12} & \leadsto & \left(c_{2,0},\,c_{2,1},\,c_{2,2},\,c_{2,3}\right), & \qquad\text{%ymm13} & \leadsto & \left(c_{2,4},\,c_{2,5},\,c_{2,6},\,c_{2,7}\right), \\\text{%ymm14} & \leadsto & \left(c_{3,0},\,c_{3,1},\,c_{3,2},\,c_{3,3}\right), & \qquad\text{%ymm15} & \leadsto & \left(c_{3,4},\,c_{3,5},\,c_{3,6},\,c_{3,7}\right). \\\end{array}\]Diese Register werden zunächst mit Null initialisiert.
Die Register ymm0 bis ymm3 verwenden wir um mit einem Broadcast einen Spaltenvektor der Paneele \(A\) zu laden. Im ersten Schritt (\(\ell=0\)) ergibt sich:
\[\begin{array}{lcl}\text{%ymm0 } & \leftarrow & \left(a_{0},\,a_{0},\,a_{0},\,a_{0}\right) \\\text{%ymm1 } & \leftarrow & \left(a_{1},\,a_{1},\,a_{1},\,a_{1}\right) \\\text{%ymm2 } & \leftarrow & \left(a_{2},\,a_{2},\,a_{2},\,a_{2}\right) \\\text{%ymm3 } & \leftarrow & \left(a_{3},\,a_{3},\,a_{3},\,a_{3}\right) \\\end{array}\]In ymm4 und ymm5 laden wir eine Zeile aus der Paneele von \(B\). Im Fall \(\ell=0\) also
\[\begin{array}{lcllcl}\text{%ymm4 } & \leftarrow & \left(b_{0},\,b_{1},\,b_{2},\,b_{3}\right), & \qquad\text{%ymm5 } & \leadsto & \left(b_{4},\,b_{5},\,b_{6},\,b_{7}\right), \\\end{array}\]Durch eine komponentenweise Multiplikation kann somit das dyadische Produkt berechnet werden:
\[\begin{array}{lcllcl}\text{%ymm0 }\otimes\text{%ymm4 } & = & \left(a_{0}b_{0},\,a_{0}b_{1},\,a_{0}b_{2},\,a_{0}b_{3}\right) & \text{%ymm0 }\otimes\text{%ymm5 } & = & \left(a_{0}b_{4},\,a_{0}b_{1},\,a_{0}b_{2},\,a_{0}b_{3}\right) \\\text{%ymm1 }\otimes\text{%ymm4 } & = & \left(a_{1}b_{0},\,a_{1}b_{1},\,a_{1}b_{2},\,a_{1}b_{3}\right) & \text{%ymm1 }\otimes\text{%ymm5 } & = & \left(a_{1}b_{4},\,a_{1}b_{1},\,a_{1}b_{2},\,a_{1}b_{3}\right) \\\text{%ymm2 }\otimes\text{%ymm4 } & = & \left(a_{2}b_{0},\,a_{2}b_{1},\,a_{2}b_{2},\,a_{2}b_{3}\right) & \text{%ymm2 }\otimes\text{%ymm5 } & = & \left(a_{2}b_{4},\,a_{2}b_{1},\,a_{2}b_{2},\,a_{2}b_{3}\right) \\\text{%ymm3 }\otimes\text{%ymm4 } & = & \left(a_{3}b_{0},\,a_{3}b_{1},\,a_{3}b_{2},\,a_{3}b_{3}\right) & \text{%ymm3 }\otimes\text{%ymm5 } & = & \left(a_{3}b_{4},\,a_{3}b_{1},\,a_{3}b_{2},\,a_{3}b_{3}\right) \\\end{array}\]Die noch freien Register ymm6 und ymm7 werden für Zwischenergebnisse verwendet. Bezeichne \(\overline{AB}\) den Puffer für die Summe der dyadischen Produkte. Die Berechnung von
\[\overline{AB}\leftarrow\left(\begin{array}{c} a_{0} \\ a_{1} \\ a_{2} \\ a_{3} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0} & b_{1} & b_{2} & b_{3} & b_{4} & b_{5} & b_{6} & b_{7} \\ \end{array}\right)+\left(\begin{array}{c} a_{4} \\ a_{5} \\ a_{6} \\ a_{7} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{8} & b_{9} & b_{10} & b_{11} & b_{12} & b_{13} & b_{14} & b_{15} \\ \end{array}\right)+\dots\]Realisieren wir iterativ durch
\[\overline{AB}\leftarrow\mathbf{0},\quad\overline{AB}\leftarrow\overline{AB}+\left(\begin{array}{c} a_{0} \\ a_{1} \\ a_{2} \\ a_{3} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0} & b_{1} & b_{2} & b_{3} & b_{4} & b_{5} & b_{6} & b_{7} \\ \end{array}\right),\quad\overline{AB}\leftarrow\overline{AB}+\left(\begin{array}{c} a_{4} \\ a_{5} \\ a_{6} \\ a_{7} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{8} & b_{9} & b_{10} & b_{11} & b_{12} & b_{13} & b_{14} & b_{15} \\ \end{array}\right),\;\dots\]Als Pseudo-Code für die AVX-Architektur erhalten wir:
-
\(\text{%ymm8} \leftarrow \mathbf{0}\)
-
\(\dots\)
-
\(\text{%ymm15} \leftarrow \mathbf{0}\)
-
\(\text{for}\;\ell=0,\dots,k-1\)
-
\(\text{%ymm0} \leftarrow \left( a_{\ell }, a_{\ell }, a_{\ell }, a_{\ell },\right)\)
-
\(\text{%ymm1} \leftarrow \left( a_{\ell+1}, a_{\ell+1}, a_{\ell+1}, a_{\ell+1},\right)\)
-
\(\text{%ymm2} \leftarrow \left( a_{\ell+2}, a_{\ell+2}, a_{\ell+2}, a_{\ell+2},\right)\)
-
\(\text{%ymm3} \leftarrow \left( a_{\ell+3}, a_{\ell+3}, a_{\ell+3}, a_{\ell+3},\right)\)
-
\(\text{%ymm4} \leftarrow \left( b_{\ell }, b_{\ell+1}, b_{\ell+2}, b_{\ell+3},\right)\)
-
\(\text{%ymm5} \leftarrow \left( b_{\ell+4}, b_{\ell+5}, b_{\ell+6}, b_{\ell+7},\right)\)
-
\(\text{%ymm0 }\otimes\text{%ymm4 } \rightarrow \text{%ymm6}\)
-
\(\text{%ymm0 }\otimes\text{%ymm5 } \rightarrow \text{%ymm7}\)
-
\(\text{%ymm6} \oplus \text{%ymm8} \rightarrow \text{%ymm8}\)
-
\(\text{%ymm7} \oplus \text{%ymm9} \rightarrow \text{%ymm9}\)
-
\(\text{%ymm1 }\otimes\text{%ymm4 } \rightarrow \text{%ymm6}\)
-
\(\text{%ymm1 }\otimes\text{%ymm5 } \rightarrow \text{%ymm7}\)
-
\(\text{%ymm6} \oplus \text{%ymm10} \rightarrow \text{%ymm10}\)
-
\(\text{%ymm7} \oplus \text{%ymm11} \rightarrow \text{%ymm11}\)
-
\(\text{%ymm2 }\otimes\text{%ymm4 } \rightarrow \text{%ymm6}\)
-
\(\text{%ymm2 }\otimes\text{%ymm5 } \rightarrow \text{%ymm7}\)
-
\(\text{%ymm6} \oplus \text{%ymm12} \rightarrow \text{%ymm12}\)
-
\(\text{%ymm7} \oplus \text{%ymm13} \rightarrow \text{%ymm13}\)
-
\(\text{%ymm3 }\otimes\text{%ymm4 } \rightarrow \text{%ymm6}\)
-
\(\text{%ymm3 }\otimes\text{%ymm5 } \rightarrow \text{%ymm7}\)
-
\(\text{%ymm6} \oplus \text{%ymm14} \rightarrow \text{%ymm14}\)
-
\(\text{%ymm7} \oplus \text{%ymm15} \rightarrow \text{%ymm15}\)
-
Skalierung und Aufdatierung
Anschließend wird der Puffer skaliert
\[\overline{AB} \leftarrow \alpha \overline{AB}\]und die Matrix \(C\) aufdatiert
\[C \leftarrow \beta C + \overline{AB}\]Skalierung
Die Skalierung des Puffers erfolgt durch
-
\(\text{%ymm5} \leftarrow \left(\alpha, \alpha, \alpha, \alpha\right)\)
-
\(\text{%ymm5} \otimes \text{%ymm8 } \rightarrow \text{%ymm8 }\)
-
\(\dots\)
-
\(\text{%ymm5} \otimes \text{%ymm15} \rightarrow \text{%ymm15}\)
Der Wert für \(\alpha\) muss dazu zunächst vom Stack geladen werd:
vbroadcastsd -8(%rsp), %ymm6
Aufdatierung
Die Aufdatierung von \(C\) erfolgt zeilenweise. Zu beachten ist, dass wir keine zeilen- oder spaltenweise Speicherung von \(C\) voraussetzen. Jedes Matrixelement muss also einzeln geladen und zurückgeschrieben werden. In C erfolgt der Zugriff auf die erts Zeile mit C[0*incColC], C[1*incColC], ..., C[7*incColC].
In unserem Fall ist der Wert incColC im Register r9. Zu beachten ist, dass in C bei der Adressierung implizit der Faktor sizeof(C) auf einen Index angewandt wird. Im Assembler-Code muss dies explizit erfolgen. Wir multiplizieren deshlab zunächst incRowC und incColC mit 8:
leaq (,%r8,8), %r8 leaq (,%r9,8), %r9 # 1*incCol
In die Register r10, r11 und rdx schreiben wir zusätzlich Werte, um einfacher auf weiteren Spalten zugreifen zu können:
leaq (,%r9,2), %r10 # 2*incCol leaq (%r10,%r9), %r11 # 3*incCol leaq (%rcx,%r10,2), %rdx # 4*incCol
In das Register ymm7 laden wir den Wert für \(\beta\) wieder vom Stack:
vbroadcastsd -16(%rsp), %ymm7
Mit dem Befehl
vextractf128 $1, %ymm8, %xmm4
werden die oberen 128-Bit von Register ymm8 in die unteren 128-Bit von Register ymm4 geladen. Die unteren 128-Bit von ymm4 sind identisch mit xmm4.
Die erste Zeile von \(C\) kann dann wie folgt aufdatiert werden:
# # Update C(0,:) # vmovlpd (%rcx), %xmm0, %xmm0 vmovhpd (%rcx,%r9), %xmm0, %xmm0 vmovlpd (%rcx,%r10), %xmm1, %xmm1 vmovhpd (%rcx,%r11), %xmm1, %xmm1 vmovlpd (%rdx), %xmm2, %xmm2 vmovhpd (%rdx,%r9), %xmm2, %xmm2 vmovlpd (%rdx,%r10), %xmm3, %xmm3 vmovhpd (%rdx,%r11), %xmm3, %xmm3 vmulpd %xmm7, %xmm0, %xmm0 vmulpd %xmm7, %xmm1, %xmm1 vmulpd %xmm7, %xmm2, %xmm2 vmulpd %xmm7, %xmm3, %xmm3 vextractf128 $1, %ymm8, %xmm4 vextractf128 $1, %ymm9, %xmm5 vaddpd %xmm0, %xmm8, %xmm0 vaddpd %xmm1, %xmm4, %xmm1 vaddpd %xmm2, %xmm9, %xmm2 vaddpd %xmm3, %xmm5, %xmm3 vmovlpd %xmm0, (%rcx) vmovhpd %xmm0, (%rcx,%r9) vmovlpd %xmm1, (%rcx,%r10) vmovhpd %xmm1, (%rcx,%r11) vmovlpd %xmm2, (%rdx) vmovhpd %xmm2, (%rdx,%r9) vmovlpd %xmm3, (%rdx,%r10) vmovhpd %xmm3, (%rdx,%r11)
Vorlage
Schreibt in ugemm_4_8.s die obige Vorlage für die Funktion Funktion ugemm_4_8. Mit as -o ugemm_4_8.o ugemm_4_8.s kann daraus ein Object-File erzeugt werden.
Zum Testen kann folgendes C-Programm bench_dgemm_4_8.c benutzt werden. Mit gcc -Wall -O3 ugemm_4_8.o bench_dgemm_4_8.c kann ein ausführbaren Programm erzeugt werden.
#include <stdio.h> #include <math.h> #include <stdlib.h> #include <sys/times.h> #include <unistd.h> #include <stdint.h> //-- setup and print matrices -------------------------------------------------- double walltime() { struct tms ts; static double ClockTick=0.0; if (ClockTick==0.0) { ClockTick = 1.0 / ((double) sysconf(_SC_CLK_TCK)); } return ((double) times(&ts)) * ClockTick; } void initGeMatrix(int m, int n, double *A, int incRowA, int incColA) { int i, j; for (i=0; i<m; ++i) { for (j=0; j<n; ++j) { A[i*incRowA+j*incColA] = i*n + j + 1; } } } void randGeMatrix(int m, int n, double *A, int incRowA, int incColA) { int i, j; for (j=0; j<n; ++j) { for (i=0; i<m; ++i) { A[i*incRowA+j*incColA] = ((double)rand()-RAND_MAX/2)*200/RAND_MAX; } } } void printGeMatrix(int m, int n, const double *A, int incRowA, int incColA) { int i, j; for (i=0; i<m; ++i) { for (j=0; j<n; ++j) { printf("%10.4lf ", A[i*incRowA+j*incColA]); } printf("\n"); } printf("\n\n"); } void dgemm_ref(int m, int n, int k, double alpha, const double *A, int incRowA, int incColA, const double *B, int incRowB, int incColB, double beta, double *C, int incRowC, int incColC) { int i, j, l; if (beta!=1) { for (i=0; i<m; ++i) { for (j=0; j<n; ++j) { if (beta!=0) { C[i*incRowC+j*incColC] *= beta; } else { C[i*incRowC+j*incColC] = 0; } } } } if (alpha!=0) { for (i=0; i<m; ++i) { for (j=0; j<n; ++j) { for (l=0; l<k; ++l) { C[i*incRowC+j*incColC] += alpha*A[i*incRowA+l*incColA] *B[l*incRowB+j*incColB]; } } } } } double dgenrm1(int m, int n, const double *A, int incRowA, int incColA) { int i, j; double result = 0; for (j=0; j<n; ++j) { double sum = 0; for (i=0; i<m; ++i) { sum += fabs(A[i*incRowA+j*incColA]); } if (sum>result) { result = sum; } } return result; } void dgecopy(int m, int n, const double *X, int incRowX, int incColX, double *Y, int incRowY, int incColY) { int i, j; for (i=0; i<m; ++i) { for (j=0; j<n; ++j) { Y[i*incRowY+j*incColY] = X[i*incRowX+j*incColX]; } } } void dgescal(int m, int n, double alpha, double *X, int incRowX, int incColX) { int i, j; for (j=0; j<n; ++j) { for (i=0; i<m; ++i) { X[i*incRowX+j*incColX] *= alpha; } } } void dgeaxpy(int m, int n, double alpha, const double *X, int incRowX, int incColX, double *Y, int incRowY, int incColY) { int i, j; if (alpha==0 || m==0 || n==0) { return; } for (j=0; j<n; ++j) { for (i=0; i<m; ++i) { Y[i*incRowY+j*incColY] += alpha*X[i*incRowX+j*incColX]; } } } #define MIN(X,Y) ((X)<(Y) ? (X) : (Y)) #define MAX(X,Y) ((X)>(Y) ? (X) : (Y)) double err_dgemm(int m, int n, int k, double alpha, const double *A, int incRowA, int incColA, const double *B, int incRowB, int incColB, double beta, const double *C0, int incRowC0, int incColC0, double *C, int incRowC, int incColC) { double normA = dgenrm1(m, k, A, incRowA, incColA); double normB = dgenrm1(k, n, B, incRowB, incColB); double normC = dgenrm1(m, n, C, incRowC0, incColC0); double normD; int mn = (m>n) ? m : n; int mnk = (mn>k) ? mn : k; normA = MAX(normA, fabs(alpha)*normA); normC = MAX(normC, fabs(beta)*normC); dgeaxpy(m, n, -1.0, C0, incRowC0, incColC0, C, incRowC, incColC); normD = dgenrm1(m, n, C, incRowC, incColC); return normD/(mnk*normA*normB*normC); } //------------------------------------------------------------------------------ //-- new with alignment -------------------------------------------------------- void * malloc_(size_t alignment, size_t size) { size += alignment; void *ptr = malloc(size); void *ptr2 = (void *)(((size_t)ptr + alignment) & ~(alignment-1)); void **vp = (void**) ptr2 - 1; *vp = ptr; return ptr2; } void free_(void *ptr) { free(*((void**)ptr-1)); } //------------------------------------------------------------------------------ #define DGEMM_MC 256 #define DGEMM_NC 512 #define DGEMM_KC 256 #define DGEMM_MR 4 #define DGEMM_NR 8 //------------------------------------------------------------------------------ void dpack_A(int m, int k, const double *A, int incRowA, int incColA, double *p) { int i, i0, j, l, nu; int mp = (m+DGEMM_MR-1) / DGEMM_MR; for (j=0; j<k; ++j) { for (l=0; l<mp; ++l) { for (i0=0; i0<DGEMM_MR; ++i0) { i = l*DGEMM_MR + i0; nu = l*DGEMM_MR*k + j*DGEMM_MR + i0; p[nu] = (i<m) ? A[i*incRowA+j*incColA] : 0; } } } } void dpack_B(int k, int n, const double *B, int incRowB, int incColB, double *p) { int i, j, j0, l, nu; int np = (n+DGEMM_NR-1) / DGEMM_NR; for (l=0; l<np; ++l) { for (j0=0; j0<DGEMM_NR; ++j0) { j = l*DGEMM_NR + j0; for (i=0; i<k; ++i) { nu = l*DGEMM_NR*k + i*DGEMM_NR + j0; p[nu] = (j<n) ? B[i*incRowB+j*incColB] : 0; } } } } void ugemm_4_8(int64_t k, double alpha, const double *A, const double *B, double beta, double *C, int64_t incRowC, int64_t incColC); void dgemm_macro(int m, int n, int k, double alpha, const double *A, const double *B, double beta, double *C, int incRowC, int incColC) { double C_[DGEMM_MR*DGEMM_NR]; int i, j; const int MR = DGEMM_MR; const int NR = DGEMM_NR; for (j=0; j<n; j+=NR) { int nr = (j+NR<n) ? NR : n - j; for (i=0; i<m; i+=MR) { int mr = (i+MR<m) ? MR : m - i; if (mr==MR && nr==NR) { ugemm_4_8(k, alpha, &A[i*k], &B[j*k], beta, &C[i*incRowC+j*incColC], incRowC, incColC); } else { ugemm_4_8(k, alpha, &A[i*k], &B[j*k], 0., C_, 1, MR); dgescal(mr, nr, beta, &C[i*incRowC+j*incColC], incRowC, incColC); dgeaxpy(mr, nr, 1., C_, 1, MR, &C[i*incRowC+j*incColC], incRowC, incColC); } } } } void dgemm(int m, int n, int k, double alpha, const double *A, int incRowA, int incColA, const double *B, int incRowB, int incColB, double beta, double *C, int incRowC, int incColC) { int i, j, l; const int MC = DGEMM_MC; const int NC = DGEMM_NC; const int KC = DGEMM_KC; if (alpha==0.0 || k==0) { dgescal(m, n, beta, C, incRowC, incColC); } else { double *A_ = (double *)malloc_(32, sizeof(double)*DGEMM_MC*DGEMM_KC); double *B_ = (double *)malloc_(32, sizeof(double)*DGEMM_KC*DGEMM_NC); for (j=0; j<n; j+=NC) { int nc = (j+NC<=n) ? NC : n - j; for (l=0; l<k; l+=KC) { int kc = (l+KC<=k) ? KC : k - l; double beta_ = (l==0) ? beta : 1; dpack_B(kc, nc, &B[l*incRowB+j*incColB], incRowB, incColB, B_); for (i=0; i<m; i+=MC) { int mc = (i+MC<=m) ? MC : m - i; dpack_A(mc, kc, &A[i*incRowA+l*incColA], incRowA, incColA, A_); dgemm_macro(mc, nc, kc, alpha, A_, B_, beta_, &C[i*incRowC+j*incColC], incRowC, incColC); } } } free_(A_); free_(B_); } } //------------------------------------------------------------------------------ #ifndef ALPHA #define ALPHA 2 #endif #ifndef BETA #define BETA 2 #endif #ifndef MIN_N #define MIN_N 100 #endif #ifndef MAX_N #define MAX_N 1500 #endif #ifndef INC_N #define INC_N 100 #endif #ifndef MIN_M #define MIN_M 100 #endif #ifndef MAX_M #define MAX_M 1500 #endif #ifndef INC_M #define INC_M 100 #endif #ifndef MIN_K #define MIN_K 100 #endif #ifndef MAX_K #define MAX_K 1500 #endif #ifndef INC_K #define INC_K 100 #endif #ifndef ROWMAJOR_A #define ROWMAJOR_A 0 #endif #ifndef ROWMAJOR_B #define ROWMAJOR_B 0 #endif #ifndef ROWMAJOR_C #define ROWMAJOR_C 0 #endif #if (ROWMAJOR_A==1) # define INCROW_A MAX_K # define INCCOL_A 1 #else # define INCROW_A 1 # define INCCOL_A MAX_M #endif #if (ROWMAJOR_B==1) # define INCROW_B MAX_N # define INCCOL_B 1 #else # define INCROW_B 1 # define INCCOL_B MAX_K #endif #if (ROWMAJOR_C==1) # define INCROW_C MAX_N # define INCCOL_C 1 #else # define INCROW_C 1 # define INCCOL_C MAX_M #endif double A_[MAX_M*MAX_K*MIN(INCROW_A,INCCOL_A)]; double B_[MAX_K*MAX_N*MIN(INCROW_B,INCCOL_B)]; double C_[MAX_M*MAX_N]; double C_0[MAX_M*MAX_N*MIN(INCROW_A,INCCOL_A)]; double C_1[MAX_M*MAX_N*MIN(INCROW_A,INCCOL_A)]; int main() { int m, n, k; randGeMatrix(MAX_M, MAX_K, A_, INCROW_A, INCCOL_A); randGeMatrix(MAX_K, MAX_N, B_, INCROW_B, INCCOL_B); randGeMatrix(MAX_M, MAX_N, C_, 1, MAX_M); printf("#%9s %9s %9s", "m", "n", "k"); printf(" %12s %12s %12s", "t", "MFLOPS", "err"); printf(" %12s %12s %12s", "t", "MFLOPS", "err"); printf(" %12s %12s %12s", "t", "MFLOPS", "err"); printf("\n"); for (m=MIN_M, n=MIN_N, k=MIN_K; n<=MAX_N && m<=MAX_M && k<=MAX_K; m+=INC_M, n+=INC_N, k+=INC_K) { double t, dt, err; int runs = 1; double ops = 2.0*m/1000*n/1000*k; printf(" %9d %9d %9d", m, n, k); // compute reference solution dgecopy(m, n, C_, 1, MAX_M, C_0, INCROW_C, INCCOL_C); dgemm_ref(m, n, k, ALPHA, A_, INCROW_A, INCCOL_A, B_, INCROW_B, INCCOL_B, BETA, C_0, INCROW_C, INCCOL_C); // bench dgemm t = 0; runs = 0; do { dgecopy(m, n, C_, 1, MAX_M, C_1, INCROW_C, INCCOL_C); dt = walltime(); dgemm(m, n, k, ALPHA, A_, INCROW_A, INCCOL_A, B_, INCROW_B, INCCOL_B, BETA, C_1, INCROW_C, INCCOL_C); dt = walltime() - dt; t += dt; ++runs; } while (t<0.3); t /= runs; err = err_dgemm(m, n, k, ALPHA, A_, INCROW_A, INCCOL_A, B_, INCROW_B, INCCOL_B, BETA, C_0, INCROW_C, INCCOL_C, C_1, INCROW_C, INCCOL_C); printf(" %12.2e %12.2lf %12.2e", t, ops/t, err); printf("\n"); } return 0; }