Specyfikacja 8-bitowej kwantyzacji TensorFlow Lite

W tym dokumencie znajdziesz specyfikację 8-bitowego schematu kwantyzacji TensorFlow Lite. Ta funkcja ma pomagać deweloperom sprzętu w oferowaniu sprzętowej obsługi wnioskowania za pomocą skwantyzowanych modeli TensorFlow Lite.

Podsumowanie specyfikacji

Udostępniamy specyfikację i możemy dać tylko pewne gwarancje dotyczące jej działania w przypadku jej zgodności z tą specyfikacją. Zdajemy sobie też sprawę, że w przypadku różnych urządzeń mogą występować preferencje i ograniczenia, które mogą powodować niewielkie odchylenia przy implementacji specyfikacji, która skutkuje niedokładnymi implementacjami. W większości przypadków takie rozwiązanie może być akceptowalne (a my dostarczymy zestaw testów, które zgodnie z naszą najlepszą wiedzą obejmują tolerancje dla poszczególnych operacji zebrane na podstawie kilku modeli), jednak charakter systemów uczących się (i w najczęstszym przypadku deep learning) nie pozwala na uzyskanie żadnych twardych gwarancji.

Kwantyzacja 8-bitowa zapewnia przybliżone wartości zmiennoprzecinkowe za pomocą poniższego wzoru.

\[real\_value = (int8\_value - zero\_point) \times scale\]

Wagi na oś (w operacjach konw.) lub wagi na intensywność parametru są reprezentowane przez int8 wartości uzupełniające w zakresie [-127, 127] z punktem zerowym równym 0. Aktywacje/wejściowe dane na podstawie liczby agentów są reprezentowane przez wartości uzupełniające (int8) z zakresu [-128, 127] o punkcie zerowym w zakresie [-128, 127].

Występują też inne wyjątki dotyczące konkretnych operacji, które zostały opisane poniżej.

Podpisana liczba całkowita a nieoznaczona liczba całkowita

Kwantyzacja TensorFlow Lite traktuje priorytetowo narzędzia i jądra przy kwantyzacji int8 w przypadku wersji 8-bitowej. Ułatwia to kwantyzację symetryczną reprezentowaną przez punkt zerowy równy 0. Poza tym wiele backendów ma dodatkowe optymalizacje pod kątem akumulacji int8xint8.

Na oś lub tenisor

Kwantyzacja na podstawie intensywności oznacza, że na cały tensor przypada jedna skala lub punkt 0 punktu. Kwantyzacja na osi oznacza, że występuje 1 skala lub zero_point na wycinek w funkcji quantized_dimension. Poddany kwantyzacji wymiar określa wymiar kształtu Tensora, któremu odpowiadają skale i punkty zerowe. Na przykład tensor t z parametrem dims=[4, 3, 2, 1] z parametrami kwantyzacji scale=[1.0, 2.0, 3.0], zero_point=[1, 2, 3] i quantization_dimension=1 zostanie poddany kwantyzacji w drugim wymiarze obiektu t:

t[:, 0, :, :] will have scale[0]=1.0, zero_point[0]=1
t[:, 1, :, :] will have scale[1]=2.0, zero_point[1]=2
t[:, 2, :, :] will have scale[2]=3.0, zero_point[2]=3

Często quantized_dimension jest wartością output_channel wagi splotów, ale teoretycznie może też odpowiadać każdemu produktowi punktowemu w implementacji jądra, co zapewnia większą szczegółowość kwantyzacji bez wpływu na wydajność. Pozwoli to znacznie zwiększyć dokładność.

TFLite obsługuje liczby operacji na jednej osi. W chwili obecnej tego dokumentu obsługiwane są programy Conv2d i DepthwiseConv2d.

Symetryczne czy asymetryczne

Aktywacje są asymetryczne: mogą mieć punkt zerowy w dowolnym miejscu w zakresie [-128, 127] zawierającym znak int8. Wiele aktywacji ma asymetryczny charakter, a punkt zerowy to stosunkowo tani sposób na uzyskanie nawet bardzo binarnej precyzji. Aktywacje są mnożone tylko przez stałą wagę, więc stałą wartość zerową można znacznie optymalizować.

Wagi są symetryczne: punkt zerowy ma wartość 0. Wartości wag są mnożone przez dynamiczne wartości wejściowe i aktywacyjne. Oznacza to, że mnożenie punktu 0 wagi przez wartość aktywacji jest nieuniknionym kosztem działania w czasie działania. Dzięki wyegzekwowaniu, że punkt zerowy ma wartość 0, możemy tego uniknąć.

Wyjaśnienie matematyki: działa podobnie do sekcji 2.3 w artykule arXiv:1712.05877, z tą różnicą, że dozwolone jest przypisywanie wartości skali na osi. Można to łatwo uogólnić w ten sposób:

$A$ to macierz $m \times n$ aktywacji kwantyzowanych.
$B$ to macierz $n \times p$ kwantyzowanych wag.
Rozważ pomnożenie $j$th wiersza $A$, $a_j$ przez kolumnę $k$th w kolumnie $B$ i $b_k$. Obie wartości w postaci liczb całkowitych $n$ to odpowiednio: $q_a$, $z_a$, $q_b$, $z_b$.

\[a_j \cdot b_k = \sum_{i=0}^{n} a_{j}^{(i)} b_{k}^{(i)} = \sum_{i=0}^{n} (q_{a}^{(i)} - z_a) (q_{b}^{(i)} - z_b) = \sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)} - \sum_{i=0}^{n} q_{a}^{(i)} z_b - \sum_{i=0}^{n} q_{b}^{(i)} z_a + \sum_{i=0}^{n} z_a z_b\]

Nie można uniknąć \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) terminu, ponieważ zapewnia iloczyn skalarny wartości wejściowej i wagi.

Warunki \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) i \(\sum_{i=0}^{n} z_a z_b\) są składane ze stałych, które pozostają takie same zgodnie z wywołaniem wnioskowania, więc można je wstępnie obliczyć.

Termin \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) musi być obliczany przy każdym wnioskowaniu, ponieważ aktywacja zmienia się w każdym wnioskowaniu. Dzięki wyegzekwowaniu symetrycznego wag możemy zmniejszyć koszt tego terminu.

specyfikacja operatora skwantyzowanego int8

Poniżej opisujemy wymagania dotyczące kwantyzacji dla naszych jąder tflite int8:

ADD
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

AVERAGE_POOL_2D
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

CONCATENATION
  Input ...:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

CONV_2D
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1 (Weight):
    data_type  : int8
    range      : [-127, 127]
    granularity: per-axis (dim = 0)
    restriction: zero_point = 0
  Input 2 (Bias):
    data_type  : int32
    range      : [int32_min, int32_max]
    granularity: per-axis
    restriction: (scale, zero_point) = (input0_scale * input1_scale[...], 0)
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

DEPTHWISE_CONV_2D
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1 (Weight):
    data_type  : int8
    range      : [-127, 127]
    granularity: per-axis (dim = 3)
    restriction: zero_point = 0
  Input 2 (Bias):
    data_type  : int32
    range      : [int32_min, int32_max]
    granularity: per-axis
    restriction: (scale, zero_point) = (input0_scale * input1_scale[...], 0)
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

FULLY_CONNECTED
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1 (Weight):
    data_type  : int8
    range      : [-127, 127]
    granularity: per-axis (dim = 0)
    restriction: zero_point = 0
  Input 2 (Bias):
    data_type  : int32
    range      : [int32_min, int32_max]
    granularity: per-tensor
    restriction: (scale, zero_point) = (input0_scale * input1_scale[...], 0)
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

L2_NORMALIZATION
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
    restriction: (scale, zero_point) = (1.0 / 128.0, 0)

LOGISTIC
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
    restriction: (scale, zero_point) = (1.0 / 256.0, -128)

MAX_POOL_2D
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

MUL
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

RESHAPE
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

RESIZE_BILINEAR
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

SOFTMAX
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
    restriction: (scale, zero_point) = (1.0 / 256.0, -128)

SPACE_TO_DEPTH
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

TANH
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
    restriction: (scale, zero_point) = (1.0 / 128.0, 0)

PAD
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

GATHER
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

BATCH_TO_SPACE_ND
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

SPACE_TO_BATCH_ND
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

TRANSPOSE
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

MEAN
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

SUB
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

SUM
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

SQUEEZE
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

LOG_SOFTMAX
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
    restriction: (scale, zero_point) = (16.0 / 256.0, 127)

MAXIMUM
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

ARG_MAX
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

MINIMUM
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

LESS
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

PADV2
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

GREATER
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

GREATER_EQUAL
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

LESS_EQUAL
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

SLICE
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  restriction: Input and outputs must all have same scale/zero_point

EQUAL
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

NOT_EQUAL
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Input 1:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

SHAPE
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

QUANTIZE (Requantization)
  Input 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor
  Output 0:
    data_type  : int8
    range      : [-128, 127]
    granularity: per-tensor

Źródła

arXiv:1712.05877