8-Bit-Quantisierungsspezifikation für LiteRT

In diesem Dokument wird die Spezifikation für das 8-Bit-Quantisierungsschema von LiteRT beschrieben. Dies soll Hardwareentwicklern helfen, Hardwareunterstützung für die Inferenz mit quantisierten LiteRT-Modellen bereitzustellen.

Zusammenfassung der Spezifikationen

Wir stellen eine Spezifikation zur Verfügung und können nur dann Garantien für das Verhalten übernehmen, wenn die Spezifikation eingehalten wird. Wir wissen auch, dass verschiedene Hardware möglicherweise Vorlieben und Einschränkungen hat, die bei der Implementierung der Spezifikation zu leichten Abweichungen führen können, sodass die Implementierungen nicht bitgenau sind. Das ist in den meisten Fällen akzeptabel. Wir stellen eine Reihe von Tests zur Verfügung, die nach unserem besten Wissen Toleranzen pro Vorgang enthalten, die wir aus verschiedenen Modellen gesammelt haben. Aufgrund der Natur des maschinellen Lernens (und des Deep Learnings im häufigsten Fall) ist es jedoch unmöglich, harte Garantien zu geben.

Bei der 8‑Bit-Quantisierung werden Gleitkommawerte mit der folgenden Formel angenähert.

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

Gewichte pro Achse (auch als „pro Channel“ in Conv-Operationen bezeichnet) oder pro Tensor werden durch int8-Zweierkomplementwerte im Bereich [-127, 127] mit einem Nullpunkt von 0 dargestellt. Aktivierungen/Eingaben pro Tensor werden durch int8-Zweierkomplementwerte im Bereich [-128, 127] mit einem Nullpunkt im Bereich [-128, 127] dargestellt.

Es gibt weitere Ausnahmen für bestimmte Vorgänge, die unten dokumentiert sind.

Ganzzahl mit Vorzeichen im Vergleich zu vorzeichenloser Ganzzahl

Bei der LiteRT-Quantisierung werden in erster Linie Tools und Kernels für die int8-Quantisierung für 8 Bit priorisiert. Dies dient der Einfachheit halber, da die symmetrische Quantisierung durch einen Nullpunkt von 0 dargestellt wird. Viele Back-Ends bieten außerdem zusätzliche Optimierungen für die int8xint8-Akkumulierung.

Pro Achse oder pro Tensor

Bei der Quantisierung pro Tensor gibt es einen Skalierungsfaktor und/oder Nullpunkt für den gesamten Tensor. Bei der Quantisierung pro Achse gibt es eine Skalierung und/oder zero_point pro Slice im quantized_dimension. Die quantisierte Dimension gibt die Dimension der Tensorform an, der die Skalierungen und Nullpunkte entsprechen. 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 wird beispielsweise ü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 quantized_dimension die output_channel der Gewichte von Faltungen. Theoretisch kann es jedoch die Dimension sein, die jedem Skalarprodukt in der Kernel-Implementierung entspricht. So lässt sich eine höhere Quantisierungsgranularität ohne Leistungseinbußen erzielen. Dadurch wird die Genauigkeit erheblich verbessert.

TFLite unterstützt für eine wachsende Anzahl von Vorgängen die Achsenweise-Ausführung. Zum Zeitpunkt der Erstellung dieses Dokuments werden Conv2d und DepthwiseConv2d unterstützt.

Symmetrisch und asymmetrisch

Aktivierungen sind asymmetrisch: Ihr Nullpunkt kann an einer beliebigen Stelle im signierten int8-Bereich [-128, 127] liegen. Viele Aktivierungen sind asymmetrisch. Ein Nullpunkt ist eine relativ kostengünstige Möglichkeit, um effektiv bis zu einem zusätzlichen binären Bit an Präzision zu erhalten. Da Aktivierungen nur mit konstanten Gewichten multipliziert werden, kann der konstante Nullpunktwert stark optimiert werden.

Gewichtungen sind symmetrisch: Der Nullpunkt muss 0 sein. Gewichtswerte werden mit dynamischen Eingabe- und Aktivierungswerten multipliziert. Das bedeutet, dass es unvermeidliche Laufzeitkosten für die Multiplikation des Nullpunkts des Gewichts mit dem Aktivierungswert gibt. Wenn wir erzwingen, dass der Nullpunkt 0 ist, können wir diese Kosten vermeiden.

Mathematische Erklärung: Dies ähnelt Abschnitt 2.3 in arXiv:1712.05877, mit dem Unterschied, dass die Skalenwerte achsenweise sein können. Das lässt sich leicht verallgemeinern:

$A$ ist eine $m \times n$-Matrix mit quantisierten Aktivierungen.
$B$ ist eine $n \times p$-Matrix mit quantisierten Gewichten.
Betrachten Sie die Multiplikation der $j$-ten Zeile von $A$, $a_j$, mit der $k$-ten Spalte von $B$, $b_k$, beide mit der Länge $n$. Die quantisierten Ganzzahlwerte und Nullpunktwerte 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 \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) Begriff ist unvermeidlich, da er das Skalarprodukt des Eingabewerts und des Gewichtungswerts berechnet.

Die Terme \(\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 \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) Begriff muss bei jeder Inferenz berechnet werden, da sich die Aktivierung bei jeder Inferenz ändert. Wenn wir die Gewichte symmetrisch festlegen, können wir die Kosten dieses Terms eliminieren.

Spezifikationen für INT8-quantisierte Operatoren

Im Folgenden werden die Quantisierungsanforderungen für unsere int8-TFLite-Kernels 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