Edocti
Training tehnic avansat pentru inginerul software de mâine

Deep learning at the edge - partea 1

Articol scris de Paul Ianas | Embedded AI & Automotive Expert la Edocti

Rularea modelelor de machine learning în sisteme embedded în timp real (real-time) vine adesea cu constrângerea de a reduce latența (latency), amprenta de memorie (memory footprint) și consumul de energie, obținând în același timp aceeași acuratețe (accuracy). DAR: în anumite aplicații (ex. sisteme critice de Functional-Safety) ai nevoie de garanții puternice că inferența rețelei tale neuronale este corectă (adică o limitare la 20 km/h nu este confundată cu una de 50 km/h!). S-ar putea să te gândești că, pentru a obține o acuratețe bună, ai nevoie de cât mai mulți biți fracționari posibili.

Această idee are sens, dar îți vom arăta că este posibil să atingi toate aceste obiective folosind algoritmii potriviți și o reprezentare de tip floating-point (FP) care utilizează un număr mai mic de biți. Astfel, amprenta de memorie scade dramatic, la fel și latența.

Acest newsletter tehnic este prima parte dintr-o serie și introduce programarea în virgulă fixă (fixed-point programming) într-un mod concis. În partea următoare vom vedea FXP (Fixed-Point) aplicat pe o problemă de segmentare a imaginilor, rulată pe NVIDIA Drive PX2 și pe Jetson TX2.

Programarea cu virgulă fixă (Fixed-Point) - motivație

Obții un număr mai mare de OPS, o amprentă de memorie mai mică, latență redusă și, uneori, chiar o precizie mai mare. Adesea, ești nevoit să folosești aceste tehnici în sisteme embedded cu cerințe de calcul ridicate, constrângeri de low-latency și low-power.

Dar acest lucru nu te ajută doar la sistemele embedded! A avea o „fermă” de instanțe scumpe AWS EC2 (ex. p2.8xlarge ) sau chiar echipamente DGX-1 pentru testare end-to-end (unde de obicei ai nevoie de sute de ore de inferență) nu este ieftin! Reducerea timpilor de calcul și a memoriei la jumătate îți va economisi mulți bani.

Performanța Fixed-Point (FXP) poate atinge o eficiență de până la 10 ori mai mare pe Watt (și o reducere a latenței de ~2-3 ori) comparativ cu o implementare Floating-Point (FP), în funcție de aplicație și de cerințele de precizie. În anumite aplicații, procesarea FXP va obține o acuratețe mai mare decât varianta sa FP. Vom explora acest aspect în ediția următoare.

Exemplu: când calculăm probabilități știm că:

  • Toate numerele sunt pozitive
  • Valoarea numerică maximă este 1

Să comparăm rezoluțiile maxime pentru a înțelege rapid cum am obținut aceste numere:

FXP 16-bit: 0.0000152587890625
FXP 16-bit normalized: 0.00006103515625
FXP 16-bit sub normal: 0.0000000596

Reprezentarea Fixed-Point

Semnificația unui cuvânt de N-biți depinde în întregime de interpretarea sa == setul de reprezentare și maparea pe care alegem să le folosim. Un registru pe 16-biți are doar opțiunea de a seta sau de a curăța unul dintre biții săi. Valoarea acestei stări depinde în totalitate de ceea ce decidem noi că înseamnă.

Formatul Qn.q

Un mod comun de a reprezenta numerele fixed-point se numește formatul Q (Q number format) sau Qn.q.

  • Q indică faptul că numărul este în notația formatului Q
  • n este numărul de biți ai registrului (numărul total de biți)
  • q este numărul de biți utilizați pentru a desemna partea fracționară a numărului == numărul de biți de la dreapta punctului binar („binary point”)
  • Q poate fi prefixat de U (Unsigned) sau S (Signed) pentru a desemna un număr fără semn sau cu semn.

Exemple

UQ16.0 - număr unsigned pe 16-biți cu 0 biți fracționari, adică un întreg (integer) [de la 0 la 65535]
UQ16.8 - număr unsigned pe 16-biți cu 8 biți fracționari, lăsând 8 biți pentru partea întreagă [de la 0 la 255]
SQ16.15 - număr signed pe 16-biți cu 15 biți fracționari, lăsând 0 biți pentru partea întreagă [-1 până la aproape 1)

Mai jos, cu verde am marcat biții părții întregi, cu auriu biții fracționari și cu roșu bitul de semn (sign bit). Între partea întreagă și partea fracționară se află un punct binar (binary point) imaginar:

1514131211109876543210 PointSignednessQValue
0111 0011 1001 1100 FixedUnsignedUQ16.029596
0111 0011 1001 1100 FixedUnsignedUQ16.8115.609375
0111 0011 1001 1100 FixedSignedSQ16.15~0.903198
0111 0011 1001 1100 Float--15638.86

Ce observăm?

Deoarece avem o dimensiune fixă a registrului, ne lovim de un trade-off (compromis) între intervalul dinamic (dynamic range) al numerelor pe care le putem reprezenta și precizia acestor numere:

  • reducerea intervalului dinamic produce o precizie mai mare
  • maximizarea intervalului dinamic produce o precizie mai mică

Dynamic Range (Intervalul Dinamic)

Dynamic range-ul este raportul dintre cel mai mare număr și cel mai mic număr pozitiv care poate fi reprezentat folosind formatul Qn.q dat. Poate fi exprimat în dB (decibeli) folosind următoarea formulă:

Dynamic Range (DR) = 20 * log10( |max_val| / |min_val| )

Notă: în notația FXP, intervalul dinamic depinde doar de dimensiunea registrului. Să înțelegem de ce!

Exemplu

Pentru  Q16 .0  avem (omitem modulul):

max_value = 2 N-1 - 1 = 65535
min_value = 1 (cel mai mic număr pozitiv)
max_value / min_value = 65535
DR = 20 * log10 65535 ~ 96dB

Pentru  Q16 .16  avem (omitem modulul):

max_value = 1 - 2 N-1
min_value = 2 -(N-1)
max_value / min_value = [1 / 2-(N-1)] - [2N-1 / 2-(N-1)] = 2N-1 - 20 = 2N-1 - 1 = 65535
Deci DR este același: 20 * log10 65535 ~ 96dB

Conversii de format (Format conversions)

Vom vedea că în interiorul unui CNN (Convolutional Neural Network) trebuie adesea să modificăm formatul (ghicești motivul?), deci este important să înțelegem cum funcționează aceste conversii.

Conversia de la mic la mare

Conversia unei valori FXP într-un format mai mare se poate face în două moduri:

  • creșterea părții întregi:
    • extensie folosind bitul de semn - sign extension (pentru formate signed)
    • extensie folosind zerouri - zero extension (pentru formate unsigned)
  • creșterea părții fracționare

Creșterea părții întregi

În acest exemplu considerăm un număr FXP cu semn (signed) => facem extensie folosind bitul de semn când convertim din SQ16.8 în SQ32.8:

Creșterea părții fracționare

Această operație este simplă: pur și simplu adăugăm zerouri la dreapta (padding).

Conversia de la mare la mic

Conversia unei valori FXP într-un format mai mic se poate face, de asemenea, în două moduri:

  • trunchiere (truncation)
  • rotunjire (rounding)

Cel mai bun mod de a explica este să vedem efectiv ce se întâmplă cu biții:

Exemplu de trunchiere

Iată cum un SQ32.24 poate fi trunchiat la un SQ16.1:

Observăm că mai întâi facem o deplasare (shift) la dreapta până când reprezentăm numărul cu precizia dorită. Apoi facem o extensie de semn (sign-extend) pentru biții rămași către stânga.

Exemplu de rotunjire

Să vedem același exemplu, dar de data aceasta convertit folosind metoda de rotunjire (rounding).

Matematica în virgulă fixă (Fixed point math)

Vom analiza mai întâi cele mai importante (și comune) operații cu formatul FXP.

Adunarea (Addition)

Regulă: Pentru a putea aduna două numere FXP, acestea trebuie să aibă același format.

Putem observa că operația este pur și simplu o adunare binară normală. Particularitatea constă în modul în care interpretăm numerele, lucru pe care l-am discutat deja.

Înmulțirea (Multiplication)

Înmulțirea nu are această limitare. Putem înmulți orice format FXP cu orice alt format FXP.

  • Regula 1: Numărul total de biți pentru rezultat = suma numărului de biți ai operanzilor.
  • Regula 2: Dimensiunea părții fracționare pentru rezultat = suma dimensiunilor fracționare ale operanzilor.
Exemplu

Să vedem un exemplu de înmulțire a unui SQ 16.8 cu un SQ 16.4. Dacă aplicăm cele două reguli, trebuie să obținem un număr cu semn, unde numărul de biți este următorul:

  • Numărul total de biți = total_nr_bits_N1 + total_nr_bits_N2 = 16 + 16 = 32
  • Numărul de biți fracționari = fractional_size_N1 + fractional_size_N2 = 8 + 4 = 12

Aceasta înseamnă că rezultatul nostru va avea formatul SQ32.12

Până în acest punct probabil ați realizat că operațiile aritmetice pentru numerele în virgulă fixă implică doar aritmetică de numere întregi (integer arithmetic) => Unitatea FP a procesorului nici măcar nu este folosită! Ai putea chiar să consideri că reprezentarea prin numere întregi este doar un caz special al reprezentării FXP :)

Ne oprim aici cu scurta noastră introducere. Sper că v-a plăcut acest material!

Ce urmează?

În partea următoare vom începe cu operația MAC (Multiply-Accumulate) și importanța ei în Rețelele Neuronale Convoluționale (CNN). Vom prezenta câștigul de performanță la rularea unei inferențe pe un AlexNet modificat, folosit pentru segmentarea imaginilor, folosind FP32 versus FXP (în diverse formate), rulat pe un NVIDIA Drive PX2 și pe Jetson TX2. Vom vedea, de asemenea, limitările care apar la realizarea acestui tip de transformare a rețelei neuronale.

Pregătim, de asemenea, un material despre sincronizarea ceasurilor cu înaltă precizie folosind (g)PTP peste Ethernet sau CAN.

Dar mai întâi, am o întrebare pentru tine: Ce crezi că funcționează cel mai bine?

  1. Antrenarea rețelei folosind FXP încă de la început și implementarea (deployment) în această formă?
  2. Sau antrenarea folosind FP32 normal și transformarea ei abia la momentul deployment-ului?

Ești gata să stăpânești Deep Learning at the Edge?

Dacă ți s-au părut utile aceste concepte de aritmetică fixed-point, fă următorul pas. Învață cum să proiectezi, optimizezi și să faci deployment pe modele AI moderne direct pe hardware embedded.

Explorează cursul de Deep Learning & Computer Vision

Despre acest newsletter

Acesta este primul dintr-o serie de newslettere tehnice create de Edocti . Vrem să păstrăm un ton tehnic și concis. Prezentăm provocări tehnice pe care le întâlnim direct în proiectele noastre de Autonomous Driving. Focusul nostru principal este pe Industrial Autonomous Driving, robotică și Industrial IoT. Ne concentrăm intens pe subiecte precum RTOS (QNX, Integrity, OSEK) și programare Real-Time, Linux (embedded, Yocto, RT-Linux, POSIX, ...), Deep Learning, computer vision și OpenVX.