Deep Learning at the Edge - Teil 1
Das Deployment von Machine Learning in Real-Time Embedded Systems geht oft mit der strikten Anforderung einher, die Latency, den Memory Footprint und die Power Consumption zu reduzieren – und das bei gleichbleibender Accuracy. ABER: In bestimmten Anwendungsfällen (z. B. kritisch für die Functional-Safety) benötigen Sie starke Garantien, dass das, was Ihr Neural Network ableitet (Inference), absolut korrekt ist (eine Begrenzung auf 20 km/h ist eben keine Begrenzung auf 50 km/h!). Man könnte meinen, dass man für eine gute Accuracy so viele Fractional Bits wie möglich benötigt.
Dieser Gedanke erscheint logisch, aber wir werden zeigen, dass es durchaus möglich ist, alle Ziele zu erreichen, indem man die richtigen Algorithmen und eine Floating-Point-Darstellung mit einer geringeren Anzahl von Bits verwendet. Sowohl der Memory Footprint als auch die Latency sinken dadurch drastisch.
Dieser technische Newsletter ist der erste Teil und führt präzise in das Fixed-Point Programming ein. Im nächsten Teil werden wir sehen, wie FXP (Fixed-Point) auf ein Image-Segmentation-Problem angewendet wird, das auf einer NVIDIA Drive PX2 und auf einem Jetson TX2 ausgeführt wird.
Fixed-Point Programming - Motivation
Höhere OPS (Operations per Second), kleinerer Memory Footprint, niedrigere Latenz und manchmal sogar höhere Precision. Diese Techniken müssen häufig in Embedded Systems mit hohen Berechnungsanforderungen sowie strengen Low-Latency- und Low-Power-Constraints eingesetzt werden.
Aber das hilft nicht nur bei Embedded Systemen! Eine Farm von teuren AWS EC2-Instanzen (z. B. p2.8xlarge ) oder gar DGX-1 Maschinen für End-to-End-Testing zu betreiben (was meist Hunderte von Stunden an Inference erfordert), ist nicht billig! Eine Halbierung der Berechnungszeiten und des Speicherbedarfs spart bares Geld.
Die Fixed-Point (FXP)-Performance kann – abhängig von der Anwendung und den Precision-Anforderungen – eine bis zu 10-mal höhere Leistung pro Watt (und eine Reduzierung der Latency um etwa das 2-3-fache) im Vergleich zu einer Floating-Point (FP)-Implementierung erreichen. In manchen Anwendungen erzielt die FXP-Verarbeitung sogar eine höhere Accuracy als die FP-Version. Das werden wir in der nächsten Ausgabe genauer betrachten.
Beispiel: Bei der Berechnung von Wahrscheinlichkeiten wissen wir, dass:
- Alle Zahlen positiv sind
- Der maximale numerische Wert 1 beträgt
Vergleichen wir die maximalen Auflösungen (Maximal Resolutions). Wir werden gleich verstehen, wie diese Zahlen zustande kommen:
| FXP | 16-bit: | 0.0000152587890625 |
| FXP | 16-bit normalized: | 0.00006103515625 |
| FXP | 16-bit sub normal: | 0.0000000596 |
Fixed-Point Representation
Die Bedeutung eines N-Bit-Wortes hängt vollständig von seiner Interpretation ab == das verwendete Repräsentationsset und das Mapping. Ein 16-Bit-Register bietet nur die Möglichkeit, Bits zu setzen (1) oder zu löschen (0). Welchen Wert dieser Zustand repräsentiert, hängt einzig davon ab, welche Bedeutung wir ihm zuweisen.
Qn.q Format
Eine gängige Methode, Fixed-Point-Zahlen darzustellen, ist das Q Number Format oder Qn.q.
- Q gibt an, dass die Zahl in der Q-Format-Notation vorliegt.
- n ist die Anzahl der Bits des Registers (Gesamtanzahl der Bits).
- q ist die Anzahl der Bits, die den fraktionalen Teil der Zahl bestimmen == die Anzahl der Bits rechts vom "Binary Point" (Komma).
- Dem Q kann ein U (Unsigned) oder S (Signed) vorangestellt werden.
Beispiele
UQ16.0 - Unsigned Number von 16 Bits mit 0 Fraction Bits, bedeutet ein reiner Integer [0 bis 65535]
UQ16.8 - Unsigned Number von 16 Bits mit 8 Fraction Bits, es bleiben 8 Integer Bits [0 bis 255]
SQ16.15 - Signed Number von 16 Bits mit 15 Fraction Bits, es bleiben 0 Integer Bits [-1 bis fast 1)
Unten kennzeichnen wir die Integer-Bits in Grün, die Fraction-Bits in Gold und das Sign-Bit (Vorzeichen) in Rot. Zwischen dem Integer-Teil und dem fraktionalen Teil befindet sich ein "imaginärer" Binary Point:
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | Point | Signedness | Q | Value |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | Fixed | Unsigned | UQ16.0 | 29596 |
| 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | Fixed | Unsigned | UQ16.8 | 115.609375 |
| 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | Fixed | Signed | SQ16.15 | ~0.903198 |
| 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | Float | - | - | 15638.86 |
Was beobachten wir?
Da wir eine feste Registergröße haben, entsteht ein Trade-off zwischen dem Dynamic Range (Dynamikumfang) der Zahlen, die wir darstellen können, und der Precision (Genauigkeit) dieser Zahlen:
- Die Minimierung des Dynamic Range führt zu einer höheren Precision.
- Die Maximierung des Dynamic Range führt zu einer geringeren Precision.
Dynamic Range
Der Dynamic Range ist das Verhältnis zwischen der größten Zahl und der kleinsten positiven Zahl, die mit dem gegebenen Qn.q-Format dargestellt werden kann. Er kann in dB (Dezibel) mit folgender Formel ausgedrückt werden:
Dynamic Range (DR) = 20 * log10( |max_val| / |min_val| )Hinweis: In der FXP-Notation hängt der Dynamic Range nur von der Registergröße ab. Schauen wir uns an, warum das so ist!
Beispiel
Für Q16 .0 gilt (wir lassen den Betrag weg):
max_value = 2 N-1 - 1 = 65535min_value = 1 (die kleinste positive Zahl)
max_value / min_value = 65535
DR = 20 * log10 65535 ~ 96dB
Für Q16 .16 gilt (wir lassen den Betrag weg):
max_value = 1 - 2 N-1min_value = 2 -(N-1)
max_value / min_value = [1 / 2-(N-1)] - [2N-1 / 2-(N-1)] = 2N-1 - 20 = 2N-1 - 1 = 65535
Der DR ist also gleich: 20 * log10 65535 ~ 96dB
Format-Conversions
Wir werden sehen, dass wir innerhalb eines CNN (Convolutional Neural Network) häufig das Format ändern müssen (können Sie sich vorstellen, warum?). Deshalb ist es wichtig zu verstehen, wie diese Konvertierungen funktionieren.
Konvertierung von klein nach groß (Low to Large)
Die Konvertierung eines FXP-Wertes in ein größeres Format kann auf zwei Arten erfolgen:
- Vergrößerung des Integer-Teils:
- Sign-Extension für Signed Formats (mit dem Sign-Bit auffüllen)
- Zero-Extension für Unsigned Formats (mit Nullen auffüllen)
- Vergrößerung des fraktionalen Teils
Vergrößerung des Integer-Teils
In diesem Beispiel betrachten wir eine Signed FXP-Zahl => wir führen eine Sign-Extension durch, wenn wir von SQ16.8 nach SQ32.8 konvertieren:
Vergrößerung des fraktionalen Teils
Das ist simpel: Wir füllen einfach rechts mit Nullen auf (Zero-Padding).
Konvertierung von groß nach klein (Large to Low)
Auch die Konvertierung eines FXP-Wertes in ein kleineres Format kann auf zwei Arten erfolgen:
- Truncation (Abschneiden)
- Rounding (Runden)
Der beste Weg, dies zu erklären, ist, sich genau anzusehen, was mit den Bits passiert:
Beispiel für Truncation
So kann ein SQ32.24 auf ein SQ16.1 trunkiert werden:
Wir beobachten: Zuerst führen wir einen Right-Shift aus, bis die Zahl in der gewünschten Precision vorliegt. Danach füllen wir die verbleibenden Bits links mittels Sign-Extension auf.
Beispiel für Rounding
Betrachten wir dasselbe Beispiel, aber diesmal mit der Rounding-Methode (Rundung).
Fixed-Point Math (Arithmetik)
Wir analysieren zunächst die wichtigsten (und häufigsten) Operationen mit dem FXP-Format.
Addition
Regel: Um zwei FXP-Zahlen addieren zu können, müssen sie das gleiche Format haben.
Wir stellen fest, dass die obige Operation eine einfache binäre Addition ist. Die Besonderheit liegt allein darin, wie wir die Zahlen interpretieren – das haben wir bereits besprochen.
Multiplication
Die Multiplikation unterliegt nicht dieser Einschränkung. Wir können jedes beliebige FXP-Format mit einem anderen FXP-Format multiplizieren.
- Regel 1: Die Gesamtanzahl der Bits für das Ergebnis = die Summe der Bits der Operanden.
- Regel 2: Die Größe des fraktionalen Teils für das Ergebnis = die Summe der fraktionalen Größen der Operanden.
Beispiel
Nehmen wir als Beispiel die Multiplikation eines SQ 16.8 mit einem SQ 16.4. Wenden wir die beiden Regeln an, erhalten wir eine Signed Number mit folgender Bit-Struktur:
- Total number of bits = total_nr_bits_N1 + total_nr_bits_N2 = 16 + 16 = 32
- Number of fractional bits = fractional_size_N1 + fractional_size_N2 = 8 + 4 = 12
Das bedeutet, unser Ergebnis hat das Format SQ32.12
An diesem Punkt werden Sie feststellen, dass arithmetische Operationen für Fixed-Point-Zahlen lediglich reine Integer-Arithmetik erfordern => Die FP-Unit (FPU) des Prozessors wird nicht einmal verwendet! Man kann sogar sagen, dass die Integer-Darstellung nur ein Spezialfall der FXP-Darstellung ist :)
Hier beenden wir unsere kurze Einführung. Ich hoffe, Ihnen hat dieses Material gefallen!
Wie geht es weiter?
Im nächsten Teil beginnen wir mit der MAC (Multiply-Accumulate)-Operation und ihrer Bedeutung in Convolutional Neural Networks (CNNs). Wir werden den Performance-Gewinn demonstrieren, wenn eine Inference auf einem modifizierten AlexNet für die Image Segmentation ausgeführt wird – und zwar FP32 versus FXP (in verschiedenen Formaten), deployed auf einer NVIDIA Drive PX2 und einem Jetson TX2. Wir werden auch die Einschränkungen betrachten, die bei dieser Art von neuronalen Netzwerk-Transformationen auftreten.
Außerdem bereiten wir ein Thema zur hochpräzisen Synchronisation von Clocks mittels (g)PTP über Ethernet oder CAN vor.
Aber vorher habe ich eine Frage an Sie: Was denken Sie, funktioniert am besten?
- Das Netzwerk von Beginn an mit FXP zu trainieren und es genau so zu deployen?
- Oder es mit regulärem FP32 zu trainieren und erst beim Deployment zu transformieren?
Bereit, Deep Learning at the Edge zu meistern?
Wenn diese Konzepte der Fixed-Point-Arithmetik für Sie nützlich waren, gehen Sie den nächsten Schritt. Lernen Sie, wie man moderne AI-Modelle direkt auf Embedded-Hardware entwirft, optimiert und implementiert.
Entdecken Sie den Deep Learning & Computer Vision KursÜber diesen Newsletter
Dies ist der erste aus einer Serie von technischen Newslettern von Edocti . Wir wollen es technisch und präzise halten. Wir präsentieren Themen, denen wir während unserer Autonomous Driving Projekte begegnen. Unser Hauptfokus liegt auf Industrial Autonomous Driving, Robotics und Industrial IoT. Wir beschäftigen uns intensiv mit Themen wie RTOS (QNX, Integrity, OSEK) und RT-Programmierung, Linux (Embedded, Yocto, RT-Linux, POSIX, ...), Deep Learning, Computer Vision und OpenVX.