Especificación de cuantización de LiteRT de 8 bits

En el siguiente documento, se describen las especificaciones para la arquitectura de esquema de cuantización. Su objetivo es ayudar a los desarrolladores de hardware a brindar compatibilidad de hardware para inferencia con modelos LiteRT cuantizados.

Resumen de especificaciones

Le proporcionamos una especificación y solo podemos dar algunas garantías sobre su comportamiento si se sigue la especificación. También entendemos que los distintos tipos de hardware pueden tienen preferencias y restricciones que pueden causar pequeñas desviaciones cuando las especificaciones que dan como resultado implementaciones que no son exactas. Sin embargo, esto puede ser aceptable en la mayoría de los casos (y proporcionaremos un conjunto de que, a nuestro leal saber y entender, incluyen tolerancias por operación que recopiladas de varios modelos), la naturaleza del aprendizaje automático (y el aprendizaje profundo) en el caso más común) hace que sea imposible proporcionar garantías estrictas.

La cuantización de 8 bits se aproxima a los valores de punto flotante mediante los siguientes fórmula.

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

Los pesos por eje (también conocidos por canal en operaciones de conv.) o por tensor se representan Valores del complemento de dos de int8 en el rango [-127, 127] con cero punto igual en 0. Las activaciones/entradas por tensor se representan con el complemento de dos de int8 valores en el rango [-128, 127], con un punto cero en el rango [-128, 127].

Hay otras excepciones para operaciones particulares que se documentan a continuación.

Número entero con firma frente a número sin firma

La cuantización de LiteRT priorizará principalmente las herramientas y los kernels para Cuantización int8 para 8 bits. Esto es para la conveniencia de la cuantización representada por un punto cero igual a 0. Además, muchos backends tienen optimizaciones adicionales para la acumulación de int8xint8.

Por eje frente a por tensor

La cuantización por tensor significa que habrá una escala o punto cero por tensor completo. La cuantización por eje significa que habrá una escala o zero_point por porción en quantized_dimension. La dimensión cuantizada especifica la dimensión de la forma del tensor, que la escala y los puntos cero a los que corresponden. Por ejemplo, un tensor t, con dims=[4, 3, 2, 1] con parámetros de cuantización: scale=[1.0, 2.0, 3.0], zero_point=[1, 2, 3], quantization_dimension=1 se cuantizará en la segunda dimensión 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

A menudo, quantized_dimension es la output_channel de las ponderaciones de pero, en teoría, puede ser la dimensión que corresponda a cada punto-product en la implementación del kernel, lo que permite un mayor nivel de detalle de la cuantización sin afectar el rendimiento. Esto tiene grandes mejoras en la exactitud.

TFLite tiene compatibilidad por eje para una cantidad creciente de operaciones. En el momento del este documento, hay compatibilidad con Conv2d y DepthwiseConv2d.

Simétrico versus asimétrico

Las activaciones son asimétricas: pueden tener su punto cero en cualquier lugar del rango de int8 firmado [-128, 127]. Muchas activaciones son asimétricas por naturaleza y un punto cero es una forma relativamente económica de obtener de manera efectiva hasta un bit binario de precisión. Dado que las activaciones solo se multiplican por la constante el valor constante de punto cero se puede optimizar bastante bien.

Los pesos son simétricos: forzados a tener un punto cero igual a 0. Los valores de las ponderaciones son multiplicados por la entrada dinámica y los valores de activación. Esto significa que hay un el costo de tiempo de ejecución inevitable de multiplicar el punto cero de la ponderación por de activación. Si aplicamos que el punto cero es 0, podemos evitar este costo.

Explicación de la matemática: esto es similar a la sección 2.3 en arXiv:1712.05877, excepto por la diferencia que permitimos que los valores de la escala sean por eje. Esto se generaliza con facilidad, ya que sigue:

$A$ es una matriz $m \times n$ de activaciones cuantificadas.
$B$ es una matriz $n \times p$ de pesos cuantificados.
Considera multiplicar la fila $j$th de $A$, $a_j$ por la columna $k$th de $B$ y $b_k$ con duración de $n$ Los valores de números enteros cuantificados y los valores de punto cero son $q_a$, $z_a$ y $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\]

El término \(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) es inevitable ya que es con el producto escalar del valor de entrada y el valor de ponderación.

Los términos \(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) y \(\sum_{i=0}^{n} z_a z_b\) están compuesta de constantes que permanecen iguales por invocación de inferencia y, por lo tanto, pueden calcularse previamente.

El término \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) debe calcularse en cada inferencia ya que la activación cambia cada inferencia. Al hacer que las ponderaciones sean simétrica, podemos quitar el costo de este término.

Especificaciones del operador cuantizado int8

A continuación, describimos los requisitos de cuantización para nuestros kernels de 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

Referencias

arXiv:1712.05877