Specyfikacja 8-bitowej kwantyzacji TensorFlow Lite

Poniższy dokument przedstawia specyfikację 8-bitowego schematu kwantyzacji TensorFlow Lite. Ma to na celu pomóc twórcom sprzętu w zapewnieniu sprzętowej obsługi wnioskowania ze skwantowanymi modelami TensorFlow Lite.

Podsumowanie specyfikacji

Dostarczamy specyfikację i możemy zapewnić pewne gwarancje dotyczące zachowania tylko wtedy, gdy specyfikacja będzie przestrzegana. Rozumiemy również, że inny sprzęt może mieć preferencje i ograniczenia, które mogą powodować niewielkie odchylenia podczas wdrażania specyfikacji, co skutkuje implementacjami, które nie są dokładne bitowo. Chociaż może to być akceptowalne w większości przypadków (i zapewnimy zestaw testów, które zgodnie z naszą najlepszą wiedzą obejmują tolerancje operacji, które zebraliśmy z kilku modeli), charakter uczenia maszynowego (i głębokiego uczenia się w najpowszechniejszych przypadku) uniemożliwia udzielenie jakichkolwiek twardych gwarancji.

Kwantyzacja 8-bitowa przybliża wartości zmiennoprzecinkowe za pomocą następującego wzoru.

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

Wagi na oś (inaczej na kanał w Conv ops) lub wagi na tensor są reprezentowane przez wartości uzupełnienia do dwóch int8 w zakresie [-127, 127] z punktem zerowym równym 0. Aktywacje/wejścia na tensor są reprezentowane przez int8 wartości uzupełnienia do dwóch w zakresie [-128, 127] , z punktem zerowym w zakresie [-128, 127] .

Istnieją inne wyjątki dla określonych operacji, które opisano poniżej.

Liczba całkowita ze znakiem vs liczba całkowita bez znaku

Kwantyzacja TensorFlow Lite będzie przede wszystkim priorytetem dla narzędzi i jąder do kwantyzacji int8 dla 8-bitów. Ma to na celu wygodę symetrycznej kwantyzacji reprezentowanej przez punkt zerowy równy 0. Dodatkowo wiele backendów ma dodatkowe optymalizacje dla akumulacji int8xint8 .

Na oś vs na tensor

Kwantyzacja na tensor oznacza, że ​​na cały tensor będzie przypadać jedna skala i/lub punkt zerowy. Kwantyzacja na osi oznacza, że ​​będzie jedna skala i/lub zero_point na wycinek w quantized_dimension . Skwantowany wymiar określa wymiar kształtu tensora, któremu odpowiadają skale i punkty zerowe. Na przykład tensor t z dims=[4, 3, 2, 1] z parametrami kwantyzacji: scale=[1.0, 2.0, 3.0] , zero_point=[1, 2, 3] , quantization_dimension=1 będzie kwantowany w poprzek drugi wymiar 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 kanałem output_channel wag splotów, ale teoretycznie może to być wymiar odpowiadający każdemu iloczynowi skalarnemu w implementacji jądra, umożliwiając większą szczegółowość kwantyzacji bez wpływu na wydajność. Zapewnia to znaczną poprawę dokładności.

TFLite obsługuje każdą oś dla rosnącej liczby operacji. W momencie tworzenia tego dokumentu dostępna była obsługa Conv2d i DepthwiseConv2d.

Symetryczny kontra asymetryczny

Aktywacje są asymetryczne: mogą mieć punkt zerowy w dowolnym miejscu w zakresie int8 ze znakiem [-128, 127] . Wiele aktywacji ma charakter asymetryczny, a punkt zerowy jest stosunkowo niedrogim sposobem na skuteczne uzyskanie dodatkowej binarnej precyzji. Ponieważ aktywacje są mnożone tylko przez stałe wagi, stałą wartość punktu zerowego można dość mocno zoptymalizować.

Wagi są symetryczne: punkt zerowy musi być równy 0. Wartości wag są mnożone przez dynamiczne wartości wejściowe i aktywacyjne. Oznacza to, że istnieje nieunikniony koszt w czasie działania związany z pomnożeniem punktu zerowego ciężaru przez wartość aktywacji. Wymuszając, że punkt zerowy wynosi 0, możemy uniknąć tego kosztu.

Wyjaśnienie matematyki: jest to podobne do sekcji 2.3 w arXiv:1712.05877 , z tą różnicą, że pozwalamy, aby wartości skali były przypisane do osi. Można to łatwo uogólnić w następujący sposób:

\(A\) jest macierzą \(m \times n\) skwantowanych aktywacji.
\(B\) to macierz \(n \times p\) skwantowanych wag.
Rozważ pomnożenie \(j\)-tego wiersza \(A\), \(a_j\) przez \(k\)-tą kolumnę\(B\), \(b_k\), oba o długości \(n\). Skwantowane wartości całkowite i wartości punktów zerowych to odpowiednio \(q_a\), \(z_a\) i \(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 przy każdym wywołaniu wnioskowania, dzięki czemu można je wstępnie obliczyć.

Termin \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) należy obliczyć przy każdym wnioskowaniu, ponieważ aktywacja zmienia każde wnioskowanie. Wymuszając symetryczność wag, możemy usunąć koszt tego terminu.

specyfikacje operatora kwantyzowanego int8

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

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

Bibliografia

arXiv:1712.05877