다음 문서에서는 LiteRT의 8비트 양자화 스키마 사양을 간략하게 설명합니다. 이는 하드웨어 개발자가 양자화된 LiteRT 모델을 사용한 추론을 위한 하드웨어 지원을 제공하는 데 도움을 주기 위한 것입니다.
사양 요약
사양을 제공하며 사양을 따르는 경우에만 동작에 관한 보증을 제공할 수 있습니다. 또한 다양한 하드웨어에는 사양을 구현할 때 약간의 편차를 일으켜 비트 정확하지 않은 구현이 발생할 수 있는 환경설정과 제한사항이 있을 수 있습니다. 대부분의 경우 허용될 수 있지만 (Google에서는 여러 모델에서 수집한 작업별 허용 오차를 포함하는 테스트 모음을 제공할 예정임) 머신러닝 (가장 일반적인 경우 딥러닝)의 특성상 확실한 보장을 제공할 수는 없습니다.
8비트 양자화는 다음 공식을 사용하여 부동 소수점 값을 근사합니다.
\[real\_value = (int8\_value - zero\_point) \times scale\]
축별 (컨볼루션 작업의 채널별) 또는 텐서별 가중치는 0과 동일한 영점과 함께 [-127, 127] 범위의 int8 2의 보수 값으로 표현됩니다. 텐서별 활성화/입력은 [-128, 127] 범위의 2의 보수 값으로 표현되며, 0점은 [-128, 127] 범위에 있습니다.int8
아래에 설명된 특정 작업에 대한 다른 예외도 있습니다.
부호 있는 정수와 부호 없는 정수 비교
LiteRT 양자화는 주로 8비트의 int8 양자화를 위한 도구와 커널에 우선순위를 둡니다. 이는 0과 같은 0점으로 표현되는 대칭 양자화의 편의를 위한 것입니다. 또한 많은 백엔드에는 int8xint8 누적을 위한 추가 최적화가 있습니다.
축별 vs 텐서별
텐서별 양자화는 전체 텐서당 하나의 스케일 또는 제로 포인트가 있음을 의미합니다. 축당 양자화는 quantized_dimension의 슬라이스당 하나의 스케일 또는 zero_point가 있음을 의미합니다. 양자화된 차원은 스케일과 0점이 해당하는 텐서 모양의 차원을 지정합니다. 예를 들어 양자화 매개변수 scale=[1.0, 2.0, 3.0], zero_point=[1, 2, 3], quantization_dimension=1이 있는 dims=[4, 3, 2, 1]이 있는 텐서 t는 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
일반적으로 quantized_dimension는 컨볼루션 가중치의 output_channel이지만 이론적으로는 커널 구현의 각 점곱에 해당하는 차원일 수 있으므로 성능에 영향을 주지 않고 더 많은 양자화 세부사항을 허용합니다. 이렇게 하면 정확도가 크게 향상됩니다.
TFLite는 증가하는 수의 작업에 대해 축별 지원을 제공합니다. 이 문서를 작성하는 시점에는 Conv2d와 DepthwiseConv2d가 지원됩니다.
대칭과 비대칭
활성화는 비대칭입니다. 활성화의 0점은 부호가 있는 int8 범위 [-128, 127] 내에 있을 수 있습니다. 많은 활성화는 본질적으로 비대칭이며 0점은 최대 1비트의 정밀도를 효과적으로 얻을 수 있는 비교적 저렴한 방법입니다. 활성화는 상수 가중치로만 곱해지므로 상수 0점 값을 상당히 많이 최적화할 수 있습니다.
가중치는 대칭입니다. 0과 동일한 0점이 강제됩니다. 가중치 값은 동적 입력 및 활성화 값과 곱해집니다. 이는 가중치의 0점을 활성화 값과 곱하는 런타임 비용이 불가피하다는 의미입니다. 영점을 0으로 강제하면 이 비용을 방지할 수 있습니다.
수학적 설명: 이는 arXiv:1712.05877의 섹션 2.3과 유사하지만, 스케일 값이 축별로 허용된다는 점이 다릅니다. 다음과 같이 쉽게 일반화할 수 있습니다.
$A$ 는 양자화된 활성화의 $m \times n$ 행렬입니다.
$B$ 는 양자화된 가중치의 $n \times p$ 행렬입니다.
길이가 $n$인 $A$의 $j$ 번째 행 $a_j$에 $B$의 $k$번째 열 $b_k$를 곱한다고 가정해 보겠습니다. 양자화된 정수 값과 0점 값은 각각 $q_a$, $z_a$ 와 $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\]
\(\sum_{i=0}^{n} q_{a}^{(i)} q_{b}^{(i)}\) 항은 입력 값과 가중치 값의 점곱을 수행하므로 피할 수 없습니다.
\(\sum_{i=0}^{n} q_{b}^{(i)} z_a\) 및 \(\sum_{i=0}^{n} z_a z_b\) 항은 추론 호출마다 동일하게 유지되는 상수로 구성되므로 미리 계산할 수 있습니다.
활성화는 추론마다 변경되므로 \(\sum_{i=0}^{n} q_{a}^{(i)} z_b\) 항목은 추론마다 계산해야 합니다. 가중치가 대칭이 되도록 강제하면 이 항의 비용을 삭제할 수 있습니다.
int8 양자화 연산자 사양
아래에서는 int8 tflite 커널의 양자화 요구사항을 설명합니다.
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