Il seguente documento descrive la specifica dello schema di quantizzazione a 8 bit di LiteRT. Lo scopo è aiutare gli sviluppatori hardware a fornire supporto hardware per l'inferenza con modelli LiteRT quantizzati.
Riepilogo delle specifiche
Forniamo una specifica e possiamo fornire alcune garanzie sul comportamento solo se la specifica viene seguita. Siamo inoltre consapevoli che hardware diversi potrebbero avere preferenze e limitazioni che potrebbero causare lievi deviazioni durante l'implementazione della specifica, con conseguenti implementazioni non bit-exact. Sebbene ciò possa essere accettabile nella maggior parte dei casi (e forniremo una serie di test che, a nostra conoscenza, includono tolleranze per operazione che abbiamo raccolto da diversi modelli), la natura del machine learning (e del deep learning nel caso più comune) rende impossibile fornire garanzie certe.
La quantizzazione a 8 bit approssima i valori in virgola mobile utilizzando la seguente formula.
\[real\_value = (int8\_value - zero\_point) \times scale\]
I pesi per asse (ovvero per canale nelle operazioni di convoluzione) o per tensore sono rappresentati da
int8 valori in complemento a due nell'intervallo [-127, 127] con punto zero uguale
a 0. Le attivazioni/gli input per tensore sono rappresentati da valori int8 in complemento a due
nell'intervallo [-128, 127], con un punto zero nell'intervallo [-128, 127].
Esistono altre eccezioni per operazioni particolari, documentate di seguito.
Numero intero con segno e numero intero senza segno
La quantizzazione LiteRT darà la priorità principalmente agli strumenti e ai kernel per la quantizzazione a 8 bit.int8 Questo per comodità della quantizzazione simmetrica
rappresentata da un punto zero uguale a 0. Inoltre, molti backend hanno
ottimizzazioni aggiuntive per l'accumulo di int8xint8.
Per asse e per tensore
La quantizzazione per tensore prevede una scala e/o un punto zero per
l'intero tensore. La quantizzazione per asse significa che ci sarà una scala e/o
zero_point per slice in quantized_dimension. La dimensione quantizzata
specifica la dimensione della forma del tensore a cui corrispondono le scale e i punti zero. Ad esempio, un tensore t, con dims=[4, 3, 2, 1] con
parametri di quantizzazione: scale=[1.0, 2.0, 3.0], zero_point=[1, 2, 3],
quantization_dimension=1 verrà quantizzato nella seconda dimensione di 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
Spesso, quantized_dimension è output_channel dei pesi delle convoluzioni, ma in teoria può essere la dimensione che corrisponde a ogni prodotto scalare nell'implementazione del kernel, consentendo una maggiore granularità della quantizzazione senza implicazioni sulle prestazioni. Ciò comporta notevoli miglioramenti all'accuratezza.
TFLite supporta le operazioni per asse per un numero crescente di operazioni. Al momento di questo documento, è disponibile il supporto per Conv2d e DepthwiseConv2d.
Simmetrico e asimmetrico
Le attivazioni sono asimmetriche: il punto zero può trovarsi in qualsiasi punto dell'intervallo int8con segno [-128, 127]. Molte attivazioni sono asimmetriche per natura e
un punto zero è un modo relativamente economico per ottenere in modo efficace fino a un bit binario
di precisione in più. Poiché le attivazioni vengono moltiplicate solo per pesi costanti, il valore costante del punto zero può essere ottimizzato in modo piuttosto significativo.
Le ponderazioni sono simmetriche: il punto zero è forzato a essere uguale a 0. I valori di ponderazione vengono moltiplicati per i valori di input dinamico e di attivazione. Ciò significa che esiste un costo di runtime inevitabile per la moltiplicazione del punto zero del peso con il valore di attivazione. Se imponiamo che il punto zero sia 0, possiamo evitare questo costo.
Spiegazione del calcolo: è simile alla sezione 2.3 di arXiv:1712.05877, tranne per la differenza che consente di impostare i valori di scala per asse. Questa generalizzazione è facile, come segue:
$A$ è una matrice $m \times n$ di attivazioni quantizzate.
$B$ è una matrice $n \times p$ di pesi quantizzati.
Considera di moltiplicare la riga $j$di $A$, $a_j$, per la colonna $k$di
$B$, $b_k$, entrambe di lunghezza $n$. I valori interi quantizzati e
i valori dei punti zero sono $q_a$, $z_a$ e $q_b$, $z_b$ rispettivamente.
\[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\]
Il termine \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) è inevitabile perché esegue il prodotto scalare del valore di input e del valore del peso.
I termini \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) e \(\sum_{i=0}^{n} z_a z_b\) sono costituiti da costanti che rimangono invariate per ogni chiamata di inferenza e possono quindi essere precalcolati.
Il \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) termine deve essere calcolato a ogni inferenza, poiché l'attivazione cambia a ogni inferenza. Applicando pesi simmetrici, possiamo rimuovere il costo di questo termine.
Specifiche dell'operatore quantizzato int8
Di seguito sono descritti i requisiti di quantizzazione per i nostri kernel 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