Le document suivant décrit la spécification du schéma de quantification 8 bits de LiteRT. Il vise à aider les développeurs de matériel à fournir une assistance matérielle pour l'inférence avec des modèles LiteRT quantifiés.
Résumé des spécifications
Nous fournissons une spécification et ne pouvons fournir certaines garanties sur le comportement que si la spécification est suivie. Nous comprenons également que différents matériels peuvent avoir des préférences et des restrictions qui peuvent entraîner de légères différences lors de l'implémentation de la spécification, ce qui entraîne des implémentations qui ne sont pas bit à bit. 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) rend impossible toute garantie ferme.
La quantification 8 bits permet d'approximer les 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 Conv) ou par Tensor sont représentées par des valeurs de complément à deux int8 dans la plage [-127, 127] avec un point zéro égal à 0. Les activations/entrées par Tensor sont représentées par des valeurs de complément à deux 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 spécifiques, qui sont décrites ci-dessous.
Entier signé et entier non signé
La quantification LiteRT donnera la priorité aux outils et aux kernels pour la quantification int8 en 8 bits. Cela permet de représenter facilement la quantification symétrique avec un point zéro égal à 0. De plus, de nombreux backends disposent d'optimisations supplémentaires pour l'accumulation int8xint8.
Par axe ou par Tensor
La quantification par tenseur signifie qu'il y aura une échelle et/ou un point zéro par tenseur entier. La quantification par axe signifie qu'il y aura une échelle et/ou un zero_point par tranche dans le 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 tenseur t, avec dims=[4, 3, 2, 1] et des paramètres de quantification : scale=[1.0, 2.0, 3.0], zero_point=[1, 2, 3], quantization_dimension=1, sera quantifié sur 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 la 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 granularité de quantification plus élevée sans impact sur les performances. Cela permet d'améliorer considérablement la précision.
TFLite est compatible avec les axes 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 : leur point zéro peut se trouver n'importe où dans la plage [-128, 127] signée [-128, 127].int8 De nombreuses activations sont de nature asymétrique, et un point zéro est un moyen relativement peu coûteux d'obtenir 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 constante du point zéro peut être fortement optimisée.
Les pondérations sont symétriques : le point zéro est défini sur 0. Les valeurs de pondération sont multipliées par les valeurs d'entrée et d'activation dynamiques. Cela signifie qu'il existe un coût d'exécution inévitable pour multiplier le point zéro du poids par la valeur d'activation. En appliquant un point zéro de 0, nous pouvons éviter ce coût.
Explication du calcul : cela ressemble à la section 2.3 de arXiv:1712.05877, à la différence près que nous autorisons les valeurs de mise à l'échelle par axe. Cette généralisation est facile à effectuer :
$A$ est une matrice $m \times n$ d'activations quantifiées.
$B$ est une matrice de poids quantifiés de taille $n \times p$.
Considérons la multiplication de la $j$-ième ligne de $A$, $a_j$, par la $k$-ième colonne de $B$, $b_k$, toutes deux de longueur $n$. Les valeurs entières quantifiées et les valeurs de point zéro 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és.
Le terme \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) doit être calculé à chaque inférence, car l'activation change à chaque inférence. En imposant la symétrie des poids, nous pouvons supprimer le coût de ce terme.
Spécifications des opérateurs quantifiés int8
Vous trouverez 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