W tym dokumencie opisujemy specyfikację 8-bitowego schematu kwantyzacji LiteRT. Ma to pomóc deweloperom sprzętu w zapewnieniu obsługi sprzętowej wnioskowania za pomocą skwantyzowanych modeli LiteRT.
Podsumowanie specyfikacji
Podajemy specyfikację i możemy zagwarantować pewne zachowania tylko wtedy, gdy jest ona przestrzegana. Zdajemy sobie też sprawę, że różne urządzenia mogą mieć preferencje i ograniczenia, które mogą powodować niewielkie odchylenia podczas wdrażania specyfikacji, co skutkuje implementacjami, które nie są identyczne co do bitu. W większości przypadków może to być dopuszczalne (udostępnimy zestaw testów, które według naszej wiedzy obejmują tolerancje poszczególnych operacji zebrane z kilku modeli), ale charakter uczenia maszynowego (a w najczęstszym przypadku uczenia głębokiego) uniemożliwia udzielenie jakichkolwiek twardych gwarancji.
Kwantyzacja 8-bitowa przybliża wartości zmiennoprzecinkowe za pomocą tego wzoru:
\[real\_value = (int8\_value - zero\_point) \times scale\]
Wagi na osi (czyli na kanał w operacjach konwolucji) lub na tensor są reprezentowane przez int8 wartości w systemie uzupełnień do dwóch w zakresie [-127, 127], przy czym punkt zerowy jest równy 0. Aktywacje/dane wejściowe dla poszczególnych tensorów są reprezentowane przez wartości int8 w zakresie [-128, 127], z punktem zerowym w zakresie [-128, 127].
Istnieją inne wyjątki dotyczące konkretnych operacji, które zostały opisane poniżej.
Liczba całkowita ze znakiem a liczba całkowita bez znaku
Kwantyzacja LiteRT będzie przede wszystkim priorytetowo traktować narzędzia i jądra do int8kwantyzacji 8-bitowej. Ułatwia to reprezentowanie kwantyzacji symetrycznej za pomocą punktu zerowego równego 0. Wiele backendów ma też dodatkowe optymalizacje pod kątem akumulacji int8xint8.
Na osi a na tensorze
Kwantyzacja na poziomie tensora oznacza, że dla każdego tensora będzie istniał jeden współczynnik skali lub punkt zerowy. Kwantyzacja na osi oznacza, że dla każdego wycinka w quantized_dimension będzie istniała jedna skala lub zero_point. Skwantowany wymiar określa wymiar kształtu tensora, do którego odnoszą się skale i punkty zerowe. Na przykład tensor t z parametrami kwantyzacji dims=[4, 3, 2, 1]: scale=[1.0, 2.0, 3.0], zero_point=[1, 2, 3], quantization_dimension=1 zostanie skwantyzowany wzdłuż drugiego wymiaru tensora 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 output_channel wag konwolucji, ale w teorii może to być wymiar odpowiadający każdemu iloczynowi skalarnemu w implementacji jądra, co pozwala na większą szczegółowość kwantyzacji bez wpływu na wydajność. Znacznie zwiększa to dokładność.
TFLite obsługuje rosnącą liczbę operacji na osi. W momencie tworzenia tego dokumentu obsługiwane są warstwy Conv2d i DepthwiseConv2d.
Symetryczne i asymetryczne
Funkcje aktywacji są asymetryczne: ich punkt zerowy może znajdować się w dowolnym miejscu w zakresie int8[-128, 127]. Wiele aktywacji ma charakter asymetryczny, a punkt zerowy jest stosunkowo niedrogim sposobem na uzyskanie dodatkowego bitu precyzji. Aktywacje są mnożone tylko przez stałe wagi, więc stałą wartość punktu zerowego można w znacznym stopniu zoptymalizować.
Wagi są symetryczne: wymuszone, aby miały punkt zerowy równy 0. Wartości wag są mnożone przez wartości dynamicznych danych wejściowych i aktywacji. Oznacza to, że istnieje nieunikniony koszt czasu działania związany z pomnożeniem punktu zerowego wagi przez wartość aktywacji. Wymuszając, aby punkt zerowy wynosił 0, możemy uniknąć tego kosztu.
Wyjaśnienie matematyczne: jest to podobne do sekcji 2.3 w arXiv:1712.05877, z tą różnicą, że dopuszczamy wartości skali dla poszczególnych osi. Można to łatwo uogólnić w ten sposób:
$A$ to macierz $m \times n$ skwantyzowanych aktywacji.
$B$ to macierz $n \times p$ skwantowanych wag.
Rozważ pomnożenie j-tego wiersza macierzy A, $a_j$, przez k-tą kolumnę macierzy B, $b_k$, o długości n. Skwantowane wartości całkowite i wartości punktów zerowych to odpowiednio $q_a$, $z_a$ oraz $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\]
Termin \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) jest nieunikniony, ponieważ wykonuje iloczyn skalarny wartości wejściowej i wartości wagi.
Terminy \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) i \(\sum_{i=0}^{n} z_a z_b\) składają się ze stałych, które pozostają takie same w przypadku każdego wywołania wnioskowania, dlatego można je wstępnie obliczyć.
Wartość tego \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) wyrażenia musi być obliczana przy każdej inferencji, ponieważ aktywacja zmienia się przy każdej inferencji. Wymuszając symetrię wag, możemy usunąć koszt tego terminu.
specyfikacje operatorów skwantowanych int8,
Poniżej opisujemy wymagania dotyczące kwantyzacji w przypadku 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