Spécification de la quantification 8 bits TensorFlow Lite

Le document suivant présente la spécification du schéma de quantification 8 bits de TensorFlow Lite. L'objectif est d'aider les développeurs de matériel à assurer la compatibilité matérielle avec l'inférence avec des modèles TensorFlow Lite quantifiés.

Récapitulatif des spécifications

Nous fournissons une spécification et nous ne pouvons fournir certaines garanties sur le comportement que si elle est respectée. Nous sommes également conscients que différents matériels peuvent avoir des préférences et des restrictions qui peuvent entraîner de légères variations lors de la mise en œuvre des spécifications, entraînant des implémentations qui ne sont pas exactes. Bien que cela puisse être acceptable dans la plupart des cas (et nous fournirons une suite de tests qui, à notre connaissance, incluent des tolérances par opération que nous avons recueillies à partir de plusieurs modèles), la nature du machine learning (et du deep learning, dans le cas le plus courant) fait qu'il est impossible de fournir une garantie stricte.

La quantification 8 bits fournit une approximation des valeurs à virgule flottante à l'aide de la formule suivante.

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

Les pondérations par axe (ou par canal dans les opérations de conv.) ou par Tensor sont représentées par les valeurs de complément à deux de int8 dans la plage [-127, 127] dont le point zéro est égal à 0. Les activations/entrées par Tensor sont représentées par les valeurs de complément à deux de int8 dans la plage [-128, 127], avec un point zéro dans la plage [-128, 127].

Il existe d'autres exceptions pour des opérations particulières décrites ci-dessous.

Entier signé et entier non signé

La quantification TensorFlow Lite donne principalement la priorité aux outils et aux noyaux pour la quantification int8 pour 8 bits. Pour des raisons de commodité, la quantification symétrique est représentée par un point zéro égal à 0. En outre, de nombreux backends bénéficient d'optimisations supplémentaires pour l'accumulation de int8xint8.

Par axe ou par Tensor

La quantification par Tensor signifie qu'il y aura une échelle et/ou un point zéro par Tensor entier. La quantification par axe signifie qu'il y aura une échelle et/ou une zero_point par tranche dans la quantized_dimension. La dimension quantifiée spécifie la dimension de la forme du Tensor à laquelle correspondent les échelles et les points zéro. Par exemple, un Tensor t, avec dims=[4, 3, 2, 1] avec les paramètres de quantification scale=[1.0, 2.0, 3.0], zero_point=[1, 2, 3], quantization_dimension=1, sera quantifié dans la deuxième dimension de 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

Souvent, quantized_dimension est le output_channel des pondérations des convolutions, mais en théorie, il peut s'agir de la dimension qui correspond à chaque produit scalaire dans l'implémentation du noyau, ce qui permet une plus grande précision de la quantification sans incidence sur les performances. Cela a permis d'améliorer considérablement la précision.

TFLite est compatible avec chaque axe pour un nombre croissant d'opérations. Au moment de la rédaction de ce document, Conv2d et DepthwiseConv2d sont compatibles.

Symétrique ou asymétrique

Les activations sont asymétriques: elles peuvent avoir leur point zéro n'importe où dans la plage int8 signée [-128, 127]. De nombreuses activations sont de nature asymétrique. Un point zéro est un moyen relativement peu coûteux d'atteindre efficacement un bit de précision binaire supplémentaire. Étant donné que les activations ne sont multipliées que par des pondérations constantes, la valeur de point zéro constante peut être fortement optimisée.

Les pondérations sont symétriques: le point zéro doit être égal à 0. Les valeurs de pondération sont multipliées par des valeurs d'entrée dynamique et d'activation. Cela signifie que le coût d'exécution est inévitable en multipliant le point zéro de la pondération par la valeur d'activation. En appliquant que le point zéro est égal à 0, nous pouvons éviter ce coût.

Explication du calcul: cette méthode est semblable à la section 2.3 de arXiv:1712.05877, à la différence que nous autorisons les valeurs d'échelle par axe. Elle se prête facilement à la généralisation, comme suit:

$A$ est une matrice d'activations quantifiées $m \times n$.
$B$ est une matrice $n \times p$ de pondérations quantifiées.
Vous pouvez multiplier la $j$e ligne de $A$, $a_j$ par la $k$e colonne de $B$, $b_k$, toutes deux de longueur $n$. Les valeurs entières quantifiées et les valeurs à zéro point sont respectivement $q_a$, $z_a$ et $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\]

Le terme \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) est inévitable, car il effectue le produit scalaire de la valeur d'entrée et de la valeur de pondération.

Les termes \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) et \(\sum_{i=0}^{n} z_a z_b\) sont constitués de constantes qui restent les mêmes pour chaque appel d'inférence et peuvent donc être précalculées.

Le \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) term doit être calculé à chaque inférence, car l'activation modifie chaque inférence. En appliquant les pondérations à la symétrie, nous pouvons éliminer le coût de ce terme.

Spécifications d'opérateurs quantifiés int8

Nous décrivons ci-dessous les exigences de quantification pour nos noyaux 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

Références

arXiv:1712.05877