O seguinte documento descreve a especificação para a interface de usuário esquema de quantização. Destina-se a ajudar os desenvolvedores de hardware a fornecer suporte de hardware para inferência com modelos LiteRT quantizados.
Resumo da especificação
Estamos fornecendo uma especificação e só podemos dar algumas garantias sobre se as especificações forem seguidas. Também entendemos que hardwares diferentes podem têm preferências e restrições que podem causar pequenos desvios implementar a especificação que resultam em implementações que não são exatas. Isso pode ser aceitável na maioria dos casos (e vamos fornecer um pacote de testa que, segundo nosso conhecimento, incluem tolerâncias por operação que coletadas de vários modelos), a natureza do machine learning (e do aprendizado profundo) no caso mais comum) impossibilita a oferta de garantias rígidas.
A quantização de 8 bits se aproxima dos valores de ponto flutuante usando o seguinte fórmula.
\[real\_value = (int8\_value - zero\_point) \times scale\]
As ponderações por eixo (também conhecidas como por canal em operações de conversão) ou pesos por tensor são representadas por
Valores complementares de int8
no intervalo [-127, 127]
com ponto zero igual
como zero. As ativações/entradas por tensor são representadas pelo complemento de dois int8
valores no intervalo [-128, 127]
, com um ponto zero no intervalo [-128, 127]
.
Há outras exceções para operações específicas que estão documentadas abaixo.
Número inteiro assinado x número inteiro não assinado
A quantização LiteRT vai priorizar principalmente ferramentas e kernels para
Quantização int8
para 8 bits. Isso é para a conveniência dos dados
sendo representada por um ponto zero igual a 0. Além disso, muitos
os back-ends têm outras otimizações para o acúmulo de int8xint8
.
Por eixo x por tensor
A quantização por tensor significa que haverá uma escala e/ou ponto zero por
todo o tensor. A quantização por eixo significa que haverá uma escala e/ou
zero_point
por fração em quantized_dimension
. A dimensão quantizada
especifica a dimensão do formato do tensor que as escalas e os pontos zero
correspondem. Por exemplo, um tensor t
, com dims=[4, 3, 2, 1]
com
parâmetros de quantização: scale=[1.0, 2.0, 3.0]
, zero_point=[1, 2, 3]
,
quantization_dimension=1
será quantizado na segunda dimensão 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
Muitas vezes, o quantized_dimension
é o output_channel
dos pesos de
convoluções, mas, em teoria, pode ser a dimensão que corresponde a cada
dot-product na implementação do kernel, permitindo mais granularidade de quantização
sem implicações no desempenho. Isso traz grandes melhorias na precisão.
O TFLite oferece suporte por eixo para um número crescente de operações. No momento de este documento, há suporte para Conv2d e DepthwiseConv2d.
Simétrica x assimétrica
As ativações são assimétricas: elas podem ter o ponto zero em qualquer lugar da
assinado int8
intervalo [-128, 127]
. Muitas ativações são de natureza assimétrica e
o ponto zero é uma maneira relativamente barata de conseguir um valor
um bit binário de precisão. Como as ativações só são multiplicadas pela constante,
o ponto zero constante pode ser bastante otimizado.
Os pesos são simétricos: forçados a ter um ponto zero igual a 0. Os valores de peso são multiplicados por valores de entrada dinâmica e de ativação. Isso significa que há um inevitável em tempo de execução de multiplicar o ponto zero do peso pelo valor de ativação. Ao aplicar o ponto zero como 0, podemos evitar esse custo.
Explicação dos cálculos: este é semelhante à seção 2.3 no arXiv:1712.05877, exceto pela diferença que os valores de escala sejam definidos por eixo. Isso generaliza prontamente, pois da seguinte forma:
$A$ é uma matriz de $m n$ de ativações quantizadas.
$B$ é uma matriz de $n p$ de pesos quantizados.
Considere multiplicar a linha $j$th linha de $A$, $a_j$ pela coluna $k$th de
$B$, $b_k$, ambos de comprimento $n$. Os valores inteiros quantizados e
os valores de ponto zero são $q_a$, $z_a$ e $q_b$, $z_b$, respectivamente.
\[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\]
O termo \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) é inevitável, já que é realizando o produto escalar entre os valores de entrada e peso.
Os \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) e \(\sum_{i=0}^{n} z_a z_b\) termos são compostas por constantes que permanecem as mesmas por invocação de inferência e, assim, podem ser pré-calculados.
O termo \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) precisa ser calculado a cada inferência já que a ativação muda a cada inferência. Ao impor os pesos a serem simétrica, podemos remover o custo desse termo.
especificações do operador quantizado int8
Veja abaixo os requisitos de quantização para nossos kernels do 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
Referências
arXiv:1712.05877 (link em inglês)