Das folgende Dokument enthält die Spezifikation für das 8-Bit-Quantisierungsschema von TensorFlow Lite. Dies soll Hardwareentwicklern bei der Bereitstellung von Hardwaresupport für Inferenzen mit quantisierten TensorFlow Lite-Modellen helfen.
Zusammenfassung der Spezifikation
Wir stellen hier eine Spezifikation bereit und können nur dann einige Garantien für das Verhalten geben, wenn die Spezifikation eingehalten wird. Uns ist auch bewusst, dass unterschiedliche Hardware Voreinstellungen und Einschränkungen haben kann, die zu geringfügigen Abweichungen bei der Implementierung der Spezifikation führen können. Dies führt zu nichtbitgenauen Implementierungen. Während dies in den meisten Fällen akzeptabel ist (und wir eine Reihe von Tests zur Verfügung stellen, die nach bestem Wissen die Toleranzen pro Vorgang umfassen, die wir aus mehreren Modellen ermittelt haben), macht es die Art des maschinellen Lernens (und im häufigsten Fall von Deep Learning) unmöglich, feste Garantien zu geben.
Bei der 8-Bit-Quantisierung werden Gleitkommawerte mithilfe der folgenden Formel angenähert.
\[real\_value = (int8\_value - zero\_point) \times scale\]
Gewichtungen pro Achse (auch bei Conv.-Vorgängen pro Kanal genannt) oder Gewichtungen pro Tensor werden durch die Komplementwerte von int8
im Bereich [-127, 127]
mit einem Nullpunkt gleich 0 dargestellt. Aktivierungen/Eingaben pro Tensor werden durch die Komplementwerte von int8
im Bereich [-128, 127]
mit einem Nullpunkt im Bereich [-128, 127]
dargestellt.
Es gibt andere Ausnahmen für bestimmte Vorgänge, die unten dokumentiert sind.
Vorzeichenbehaftete Ganzzahl im Vergleich zu vorzeichenloser Ganzzahl
Bei der TensorFlow Lite-Quantisierung werden für die int8
-Quantisierung für 8-Bit hauptsächlich Tools und Kernel priorisiert. Dies soll der symmetrischen Quantisierung dienen, wenn sie durch einen Nullpunkt gleich 0 dargestellt wird. Darüber hinaus haben viele Back-Ends zusätzliche Optimierungen für die int8xint8
-Akkumulierung.
Pro Achse vs. Per Tensor
Die Quantisierung pro Tensor bedeutet, dass es eine Skala und/oder einen Nullpunkt pro gesamtem Tensor gibt. Eine Quantisierung pro Achse bedeutet, dass es eine Skala und/oder zero_point
pro Slice im quantized_dimension
gibt. Die quantisierte Dimension gibt die Dimension der Form des Tensors an, der die Skalierungen und Nullpunkte entsprechen. Beispielsweise wird ein Tensor t
mit dims=[4, 3, 2, 1]
mit Quantisierungsparametern: scale=[1.0, 2.0, 3.0]
, zero_point=[1, 2, 3]
, quantization_dimension=1
über die zweite Dimension von t
quantisiert:
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
Häufig ist die quantized_dimension
der output_channel
der Gewichtung von Faltungen, doch theoretisch kann es die Dimension sein, die jedem Punktprodukt in der Kernelimplementierung entspricht, was einen höheren Detaillierungsgrad der Quantisierung ohne Auswirkungen auf die Leistung ermöglicht. Dadurch wird die Genauigkeit erheblich verbessert.
TFLite unterstützt pro Achse eine wachsende Anzahl von Vorgängen. Zum Zeitpunkt der Veröffentlichung dieses Dokuments werden Conv2d und DepthwiseConv2d unterstützt.
Symmetrische vs. asymmetrische
Aktivierungen sind asymmetrisch: Sie können ihren Nullpunkt überall innerhalb des signierten int8
-Bereichs [-128, 127]
haben. Viele Aktivierungen sind asymmetrischer Natur und ein Nullpunkt ist eine relativ kostengünstige Möglichkeit, effektiv eine zusätzliche binäre Genauigkeit zu erreichen. Da Aktivierungen nur mit konstanten Gewichtungen multipliziert werden, kann der konstante Nullpunktwert ziemlich stark optimiert werden.
Die Gewichtungen sind symmetrisch, das heißt, es wird erzwungen, dass ein Nullpunkt gleich 0 ist. Gewichtungswerte werden mit dynamischen Eingabe- und Aktivierungswerten multipliziert. Das bedeutet, dass unvermeidbare Laufzeitkosten entstehen, wenn der Nullpunkt der Gewichtung mit dem Aktivierungswert multipliziert wird. Durch Erzwingen, dass der Nullpunkt 0 ist, können wir diese Kosten vermeiden.
Erklärung der mathematischen Berechnung: Dies ähnelt Abschnitt 2.3 in arXiv:1712.05877, mit der Ausnahme, dass die Skalierungswerte pro Achse zulässig sind. Dies verallgemeinert leicht, wie folgt:
$A$ ist eine $m \times n$-Matrix quantisierter Aktivierungen.
$B$ ist eine $n \times-p$-Matrix mit quantisierten Gewichtungen.
Du könntest die $j$. Zeile von $A$, $a_j$ mit der $k$. Spalte von $B$, $b_k$, beide mit der Länge $n$, multiplizieren. Die quantisierten Ganzzahlwerte und Nullpunkte sind $q_a$, $z_a$ bzw. $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\]
Der Term \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) ist unvermeidlich, da er das Skalarprodukt des Eingabewerts und des Gewichtungswerts berechnet.
Die Begriffe \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) und \(\sum_{i=0}^{n} z_a z_b\) bestehen aus Konstanten, die pro Inferenzaufruf gleich bleiben und daher vorab berechnet werden können.
Der Term \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) muss jede Inferenz berechnet werden, da die Aktivierung jede Inferenz ändert. Durch Erzwingen symmetrischer Gewichtungen können wir die Kosten dieses Begriffs eliminieren.
Spezifikationen für quantisierte int8-Operatoren
Im Folgenden werden die Quantisierungsanforderungen für unsere int8-TFlite-Kernel beschrieben:
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