diff --git a/README.md b/README.md index 69b4a96..d9b26fe 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,7 @@ Wenn eine Änderung nach "master" gepushed wird, dann baut sich die Website auto ## FAQ: ### Wie installiere ich hugo? -Gerade die hugo version, welche durch `apt-get` installiert wird ist relativ alt (0.40.x) und nicht brauchbar. -Sieht auch: + * https://gohugo.io/getting-started/installing/#debian-and-ubuntu * [Quick Install: Binary (Cross-platform)](https://gohugo.io/getting-started/installing/#binary-cross-platform) diff --git a/content/projekt/bfloat16nn/M-Extension.png b/content/projekt/bfloat16nn/M-Extension.png new file mode 100644 index 0000000..231dbdf Binary files /dev/null and b/content/projekt/bfloat16nn/M-Extension.png differ diff --git a/content/projekt/bfloat16nn/R-Types_Base_I.png b/content/projekt/bfloat16nn/R-Types_Base_I.png new file mode 100644 index 0000000..e585404 Binary files /dev/null and b/content/projekt/bfloat16nn/R-Types_Base_I.png differ diff --git a/content/projekt/bfloat16nn/bfloat16_overview.png b/content/projekt/bfloat16nn/bfloat16_overview.png new file mode 100644 index 0000000..59efa84 Binary files /dev/null and b/content/projekt/bfloat16nn/bfloat16_overview.png differ diff --git a/content/projekt/bfloat16nn/index.md b/content/projekt/bfloat16nn/index.md new file mode 100644 index 0000000..443e410 --- /dev/null +++ b/content/projekt/bfloat16nn/index.md @@ -0,0 +1,301 @@ +--- +title: Wie ich lernte Half-Precision-Floats zu lieben (und den gcc!) +author: Ivo Snett +status: Aktiv +difficulty: Nö ... +time: ~1d +date: 2021-06-08 +image: bfloat16_Overview.png +keywords: FPGA, bfloat16, RISC-V, gcc, Litex, migen, colorlight-5a-75b +--- + +Für ein besseres Verständnis wird dringend geraten, zuerst +[diesen Artikel hier](https://www.hacknology.de/projekt/2020/neopixelar/) +durchzuarbeiten ... + +## Motivation + +Aus dem letzten [Projekt](https://www.hacknology.de/projekt/2021/risq5/) waren noch zwei Fragestellungen übergeblieben: +1. Könnte man nicht auch die kürzeren Fließkommaformate für neuronale Netze (typischerweise) nebst passender mathematischer +Operation selbst implementieren? +2. Wäre es dann möglich, entsprechende 'custom' Mnemonics (opcodes) im Compiler nachzurüsten? Und wenn ja: Wieviel Aufwand +wäre hierfür zu treiben? +Diesen Themen widmet sich dieses 'Projekt' ... + +## 1. Warum spielen die kurzen 16-Bit-Floats überhaupt so eine Rolle? + +Der Platz auf einem Chip (oder - wie hier - dem FPGA) ist normalerweise begrenzt, d.h. 'teuer'. Umgekehrt kommt es bei +Anwendungen im Graphikbereich (3d) oder auch neuronalen Netzen nicht so sehr auf die Genauigkeit an. Daher kann es sinnvoll +sein, einen Datentyp zu verwenden, der praktisch den halben Platz einspart (50%!) und dafür schon in der ersten (dezimalen) +Nachkommastelle nicht mehr so ganz exakt ist ... + +## 2. FP16, das(!) IEEE 754-2008 Format oder was? + +Da ich im letzten Projekt bereits mit IEEE 754 Fließkommazahlen operiert hatte, schien es naheliegend, das dort beschriebene +16-bit Fließkommaformat (1 Bit Vorzeichen, 5 Bit Exponent, 10 Bit Mantisse) zu unterstützen. +Nachdem ich mich bereits eine Woche damit beschäftigt hatte (und die Zerlegung bzw. +Zusammensetzung sowie die einfache Addition schon am Laufen hatte), stieß ich auf einen Artikel (Link leider nicht gemerkt - shit happenz 🤔!), +der die im Laufe der Zeit entstandenen verschiedenen Formate miteinander verglich und ihre jeweiligen Stärken herausarbeitete. +Es leuchtete mir unmittelbar ein, daß allein die Konversion von 32-Bit-Float zu 16-Bit-Float (und zurück) einen Gutteil der +möglichen Performance-Gewinne bei [FP16](https://en.wikipedia.org/wiki/Half-precision_floating-point_format)-Verwendung zunichte machen könnte. +Viel besser erschien da doch [bfloat16](https://en.wikipedia.org/wiki/bfloat16_floating-point_format) mit seinem einfachen Abschneiden der Mantisse, +d.h. im Wesentlichen wird halt nur das halbe (32-Bit-)Wort kopiert (bzw. umgekehrt der Rest mit Nullen aufgefüllt). +Also nochmal das Ganze von vorne mit 'bfloat16' ... +Einer der ganz großen Vorteile war natürlich, daß es sich hier um denselben Aufbau handelt wie beim bereits im letzten Projekt +implementierten IEEE 754 32-Bit Floattyp, lediglich die Mantisse wird von 23 auf 7 Bit reduziert (also: 1 Bit Vorzeichen, +8 Bit Exponent, 7 Bit Mantisse). +Dies ermöglicht die 1:1 Wiederverwendung des existierenden Rechenwerks. Streng genommen bräuchten nur die fehlenden Mantissenbits +mit Nullen aufgefüllt werden, ich habe aber gleich die wenigen Adaptionen vorgenommen um 'wirklich' mit den kürzeren Formaten +zu rechnen. + +## 3. Eine nützliche Operation + +Der Speicherverbrauch für die Daten (On-Chip wohlgemerkt!) ist jetzt reduziert, was also damit anfangen? +Vor ein paar Jahren habe ich mich ein wenig für die Technik hinter den neuronalen Netzen interessiert (wer nicht?), und zu diesem +Zweck 'toten Baum' studiert (Tariq Rashid, 'Neuronale Netze selbst programmieren', O'Reilly, ISBN 978-3-96009-043-4). +Das dortige größte (Python-)Beispielprogramm hatte ich für mich leicht abgewandelt und mit einer Qt5 Oberfläche versehen (es handelt +sich um die Auswertung für den 'Standard'-[MNIST Datensatz](http://yann.lecun.com/exdb/mnist) für die Handschrifterkennung von +Zahlen auf Überweisungsträgern). +Bei Durchsicht der 'alten' Quellen stieß ich auf folgende zentrale Bestimmungsfunktion für den laufenden Betrieb (das Modell +liegt hier bereits vor - es ist zeitunkritisch und kann vorab berechnet werden - und wird hier auch direkt von der Platte geladen ...): + +__Auszug:__ +```python +def query(self,inputs_list): + '''Query the neural network''' + # Convert inputs list to 2nd arrays + inputs = numpy.array(inputs_list,ndmin=2).T # Transpose ... + + # Calculate signals into hidden layer + hidden_inputs = numpy.dot(self.wih,inputs) # Dot/inner matrice product + # Calculate the signals emerging from hidden layer + hidden_outputs = self.activation_function(hidden_inputs) + + # Calculate signals into final output layer + final_inputs = numpy.dot(self.who,hidden_outputs) + # Calculate the signals emerging from final output layer + final_outputs = self.activation_function(final_inputs) + + return final_outputs +``` + +Zentral ist hier die Funktion numpy.dot(), diese berechnet das innere [('dot') Produkt](https://en.wikipedia.org/wiki/Matrix_multiplication#Dot_product,_bilinear_form_and_inner_product) von +Matrix & Vektor (bzw. Zeilenvektor & Spaltenvektor). Würde man diese Operation auf das FPGA verlagern, sollte sich die Performance +doch verbessern lassen? Wir ignorieren hier mal die Problematik des Python-zu-C-Marshalling etc. ... + +Es zeigt sich, das im vorliegenden Fall die Daten als 28x28 Punktmatrix vorliegen (der ursprünglichen 'Scan'-Auflösung), womit sich +eine Eintragslänge von 784 Werten ergibt. Im Programm wird effektiv jeweils mit einem 'hidden layer' von 200 Knoten gerechnet woraus +bei dem schlichten numpy.dot() bereits mit einer Matrix mit 200 Zeilen à 784 Spalten gerechnet wird. Oophs, das wird wohl kaum auf den +Chip passen! +Also eine Nummer kleiner: Jede Zeile einzeln berechnen - ergibt auch jeweils nur einen Ergebniswert (den 'Skalar'). +Wir brauchen also eine Operation, die einen Zeilenvektor mit 784 Werten mit einem Spaltenvektor von 784 Werten Länge multipliziert. + +So etwas kann für (z.Bsp.) eine RISC-V CPU in geradezu idealer Weise zerlegt werden in 'fused' multiply/add Befehle. +Außerdem können mehrere Rechenkerne das Problem sehr gut parallel ermitteln, d.h. die Zeiten lassen sich fast gemäß Kernzahl herunterdrücken, +lediglich am Ende müssen die Einzelsummen noch separat addiert werden (leider werden bei mir effektiv nur zwei 16-Bit-Rechenwerke auf's FPGA passen ...). + + +## 4. Implementierung + +Hier wird parallel zur mitgelieferten vexriscv-CPU ein Verarbeitungs(Master-)knoten mit zwei Rechenwerken installiert. + +{{< image alt="bfloat16" src="bfloat16_overview.png" >}} + +Das Projekt liegt auf unserem [Git-Server](https://git.hacknology.de/projekte/bfloat16nn). Es kann einzeln übersetzt werden, ist +jedoch eigentlich als Erweiterung für das Risq5-Projekt gedacht. Es enthält *nur* eine Operation (dot product) sollte jedoch als +Beispiel verstanden werden für die Implementierung weiterer nützlicher Funktionen. + + +## 5. Ergebnisse + +Im vorliegenden Vergleichsfall mit einem Testprogramm in C ergibt sich eine Performance-Verbesserung (bei 2 Kernen) um den Faktor 7-10. +Der langsamste Teil (und der Teil mit etwas Jitter) ist der DRAM-DMA-Transfer zum FPGA. Auf einem größeren FPGA mit mehr Kernen ließe +sich natürlich noch erheblich mehr herausholen ... + +## 6. Das 'Sahnehäubchen' - RISC-V Opcode-Support im gcc + +Ideal wäre eine Instruktion wie z.B.: matmul \,\,\ (ok, eigentlich wäre 'vecmul' oder 'dotprod' korrekter gewesen 🤔 ...) wobei + * \ das Register mit der Basisadresse der Matrixzeile (des Zeilenvektors) im RAM bezeichnet, + * \ die Basisadresse des Spaltenvektors im RAM ist und + * in \ wird der (skalare) Ergebniswert zurückgemeldet. + +Diese Instruktion könnte dann per Inline-Assembly in jedem C Programm Verwendung finden ... + +Eigentlich gäbe es wohl bereits passende Instruktionen in der RISC-V Vector-Extension, da diese aber niemals auf das hier verwendete +kleine Lattice-FPGA passen würden (& das *echt* viele Instruktionen sind!), habe ich einfach die M-Extension erweitert +(komplett Non-standard! Eigentlich Integer! Grauslig 💀 ...). + +Die ursprüngliche Annahme war 'LLVM ist modularer (moderner) aufgebaut als der gcc'. Nachdem ich mich fast zwei Wochen mit LLVM - ohne +so recht voranzukommen - herumgeschlagen hatte (ich wollte gleich eine neue *Architektur* +einführen um dann das Backend zu erweitern ...), habe ich es lieber mit dem gcc versucht. + +*Außerdem 💡:* __AUS__ *was würde eigentlich in diese Instruktion übersetzt, also welche IL-Instruktion(en) käme(n) da wohl in Frage? +Vermutlich gäbe es gar nichts Passendes 🗑 (es finden sich wohl bisher nur +[Proposals](https://llvm.org/devmtg/2019-04/slides/TechTalk-Kruppe-Espasa-RISC-V_Vectors_and_LLVM.pdf) +), d.h. es würde eh' nur Inline-Assembly zur Nutzung in Frage kommen.* + +Also gcc (für riscv): + + git clone https://github.com/riscv/riscv-gnu-toolchain + cd riscv-gnu-toolchain + git submodule update --init --recursive + mkdir build + cd build + ../configure --prefix=//riscv-gnu-toolchain/pub --enable-multilib + make report-linux SIM=qemu + +Was soll ich sagen? In zwei Dateien für die RISC-V Opcodes zusätzliche Makros für die neue Mnemonic eintragen (für Assembly & Disassembly), +Toolchain neu übersetzen und ... läuft 👍! + +Wie findet man die richtigen Dateien? Ich habe mir die 'mulhu' Instruktion herausgesucht zum *grep'en* da sie hinreichend 'exotisch' ist +(klingt auch irgendwie nach H.P.Lovecraft 🎃): + + grep -r "mulhu" . + +Im Ergebnis findet sich neben unwichtigen Binärdateien (und einigem 'Gemüse' im ignorierbaren 'testsuite' Unterordner bzw. bei __anderen__ +Prozessor-Architekturen & dem Simulator qemu) die Datei __riscv-opc.h__, allerdings in zwei Pfaden innerhalb +des 'riscv-gnu-toolchain' Projekts (beide Dateien sind gleich aufgebaut) - unter .riscv-gdb/include/opcode & ./riscv-binutils/include/opcode. Hier trägt man +an zwei Stellen die neuen Daten ein: + +```c +... +#define MATCH_FENCE_TSO 0x8330000f +#define MASK_FENCE_TSO 0xfff0707f + +// MATMUL instruction (MUL w/ one bit higher!) +#define MATCH_MATMUL 0x06000033 +#define MASK_MATMUL 0xfe00707f +// MATMUL end + +#define MATCH_MUL 0x2000033 +#define MASK_MUL 0xfe00707f +#define MATCH_MULH 0x2001033 +#define MASK_MULH 0xfe00707f +... + +... +DECLARE_INSN(fence_i, MATCH_FENCE_I, MASK_FENCE_I) + +// matmul instruction added +DECLARE_INSN(matmul, MATCH_MATMUL, MASK_MATMUL) +// matmul end + +DECLARE_INSN(mul, MATCH_MUL, MASK_MUL) +DECLARE_INSN(mulh, MATCH_MULH, MASK_MULH) +... +``` + +MATCH_\ ist die Basiscodierung, MASK_\ ist der Filter um die Basiscodierung zu extrahieren. + +*Hinweis: Ich hatte zunächst ./org-riscv-binutils/include/opcode/riscv-opc.h übersehen, sie scheint aber +im vorliegenden Fall nicht relevant zu sein ...* + +Woher weiss man, wie ein neue Opcode-Kodierung aussehen darf? +Die Bits 0 bis 6 enthalten die Instruktionstypkennung, 0x33 ist eine 'R-Type' Instruktion ('register (only)'). + +{{< image alt="R-Types I-base" src="R-Types_Base_I.png" >}} + +Die M-Extension ist hier in den Bits 26 bis 31 kodiert (als 0000001b, im Basissatz 'I' sind noch 0000000b +und 0100000 belegt). +Dazu hier mal die originale M-Extension (Integer Multiply/Divide) Kodierungstabelle: + +{{< image alt="M-Extension" src="M-Extension.png" >}} + +In den Bits 26 bis 31 könnte man also noch jede Menge neue Funktionen einführen. Ich habe im vorliegenden Fall lediglich +das Bit 26 zusätzlich zum Bit 25 gesetzt ... + +Nach erfolgreicher Neuübersetzung der Toolchain (make clean, dann make) kann man jetzt Folgendes +kompilieren (__test.c__): + +```c +#pragma GCC diagnostic ignored "-Wunused-function" +static void testmatmul(void) +{ + __asm__ __volatile__ ("\ + matmul x0,x0,x0 # 'Pure' opcode (all regs=0) \n\ + matmul t2,t0,t1 # t0=Adress of (matrice) row, t1=column vector, t2=(scalar)result \n\ + "); +} +``` + +__\/riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -S test.c__ + +für den Assembly-Output, ansonsten reicht + +__\/riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 test.c -o test__ + +Hier der Auszug aus __test.s__: +```asm +... + .text + .align 2 + .type testmatmul, @function +testmatmul: + addi sp,sp,-16 + sw s0,12(sp) + addi s0,sp,16 +#APP +# 6 "test.c" 1 + matmul x0,x0,x0 # 'Pure' opcode (all regs=0) + matmul t2,t0,t1 # t0=Adress of (matrice) row, t1=column vector, t2=(scalar)result + +# 0 "" 2 +#NO_APP + nop + lw s0,12(sp) + addi sp,sp,16 + jr ra +... +``` +Das Ergebnis-Executable sollte wohl besser *richtig* disassembliert werden (zur Prüfung der Hex-Codierung): + +__\/riscv64-unknown-elf-objdump -D -m riscv test >test.dis__ + +Hier der Auszug aus __test.dis__ (schon hilfreicher!): +```dis +... +00010144 : + 10144: ff010113 addi sp,sp,-16 + 10148: 00812623 sw s0,12(sp) + 1014c: 01010413 addi s0,sp,16 + 10150: 06000033 matmul zero,zero,zero + 10154: 066283b3 matmul t2,t0,t1 + 10158: 00000013 nop + 1015c: 00c12403 lw s0,12(sp) + 10160: 01010113 addi sp,sp,16 + 10164: 00008067 ret +... +``` +Hat geklappt 👍! + +## 7. Was noch zu tun bleibt ... + +Leider paßt die ganze Logik gar nicht mit dem eigenen Risq5-Kern auf das kleine Lattice-FPGA. +Das Ganze müsste also noch auf eine andere Zielplattform mit größerem FPGA verschoben werden. +Außerdem müssen die 16-Bit-Rechenwerke noch mit der Logik des Risq5-Kerns *verschmolzen* werden, +in etwa wie hier dargestellt: + +```python +aus libmodules/instruction_decode.py im Risq5-Projekt + +... +If(regs.op == 0x33, # R-Type + If(regs.f3 == 0x00, # add/sub/mul + If(regs.f7 == 0x00, # add rd, rs1, rs2 (all indexes prep'd already!) + NextValue(regs.rd_wrport.dat_w, regs.xs1s + regs.xs2s), + ).Elif(regs.f7 == 0x20, # sub rd, rs1, rs2 + NextValue(regs.rd_wrport.dat_w, regs.xs1s - regs.xs2s), + ).Elif(regs.f7 == 0x01, # mul rd, rs1, rs2 (M extension) + NextValue(regs.rd_wrport.dat_w, regs.rd_mul64s[0:32]), + ).Elif(regs.f7 == 0x03, # matmul rd, rs1, rs2 (test!) + ... hier Parameterübergabe etc. ... + ), +... +``` + +The remaining plumbing work is left as an exercise to the reader ... grrrh 💀 ☮ 🗣 (& sufficient FPGA resources assumed ...) +Jedenfalls solange, bis ich an ein größeres FPGA (mit LAN-Zugang wg. des Risq5-Debuggers) komme. + +Eine Verbesserung wäre auch das Ausfiltern, d.h. aus 32-Bit Float-Array direkt nur *halbe* Worte lokal im FPGA übernehmen ('striding'), +dann könnte ein Umkopieren komplett entfallen. +