TensorFlow Lite-8-Bit-Quantisierungsspezifikation

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

Verweise

arXiv:1712.05877