במסמך הבא מוצג המפרט של סכמת הקוונטיזציה ב-8 ביט של TensorFlow Lite. היא נועדה לעזור למפתחי חומרה לספק תמיכה בחומרה בהֶקֵּשׁ באמצעות מודלים של TensorFlow Lite שמכמים אותם.
סיכום המפרט
אנחנו מספקים מפרט טכני, ואנחנו יכולים להבטיח דיוק מסוים לגבי ההתנהגות רק אם עומדים במפרט. אנחנו גם מבינים שלחומרה אחרת עשויות להיות העדפות והגבלות שעלולות לגרום לסטיות קלות במהלך הטמעת המפרט, וכתוצאה מכך יש הטמעות שאינן מדויקות במקצת. זה יהיה מקובל ברוב המקרים (ואנחנו נספק חבילת בדיקות שלמיטב ידיעתנו כוללת סבילות שאספנו לכל פעולה מכמה מודלים), האופי של למידת המכונה (ולמידה עמוקה במקרה הנפוץ ביותר) לא מאפשר לספק אחריות קשה.
קוונטיזציה ב-8 ביט מבצעת הערכה של ערכי הנקודות הצפה באמצעות הנוסחה הבאה.
\[real\_value = (int8\_value - zero\_point) \times scale\]
משקולות לכל ציר (כלומר, לכל ערוץ ב-Convsor) או לכל טנזור, מיוצגים על ידי הערכים המשלימים של int8
בטווח [-127, 127]
עם נקודת אפס ששווה ל-0. הפעלות/קלט של Tensor מיוצגים על ידי הערכים המשלימים של int8
בטווח [-128, 127]
, עם נקודת אפס בטווח [-128, 127]
.
קיימים חריגים נוספים לפעולות מסוימות המתועדות בהמשך.
מספר שלם חתום לעומת מספר שלם לא חתום
שירותי הקוונטיזציה ב-TensorFlow Lite ייתנו עדיפות לכלים ולליבות של int8
לקוונטיזציה של 8 ביט. זאת, מטעמי נוחות, באמצעות קוונטיזציה סימטרית מיוצגת על ידי נקודת אפס ששווה ל-0. בנוסף, לקצוות עורפיים רבים יש אופטימיזציות נוספות לצבירת int8xint8
.
לפי ציר לעומת כל טנזור
המשמעות של קוונטיזציה לפי טנסור היא שתהיה סקאלה אחת ו/או נקודת אפס לכל טנזור שלם. משמעות הקונטיזציה לפי ציר היא שיהיה סולם אחד ו/או zero_point
לכל פרוסה ב-quantized_dimension
. המאפיין הכמותי מציין את ממד הצורה של Tensor שהקנה המידה ונקודות האפס מתאימות לו. לדוגמה, img_tensor t
, עם dims=[4, 3, 2, 1]
עם פרמטרים של קוונטיזציה: scale=[1.0, 2.0, 3.0]
, zero_point=[1, 2, 3]
, quantization_dimension=1
, יחושב ככמת בכל המאפיין השני של 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.
סימטרי לעומת אסימטרי
ההפעלות הן אסימטריות: הן יכולות לקבל את נקודת האפס בכל מקום בטווח החתום של int8
, [-128, 127]
. הפעלות רבות הן אסימטריות מטבען, ונקודת אפס היא דרך זולה יחסית להשגת דיוק בינארי נוסף. מכיוון שההפעלות מוכפלות במשקלים קבועים, אפשר לבצע אופטימיזציה די גבוהה לערך הקבוע של אפס נקודות.
המשקולות סימטריות: נקודת האפס צריכה להיות שווה ל-0. ערכי המשקל מכפילים בערכי הקלט וההפעלה הדינמי. כלומר, יש עלות זמן ריצה בלתי נמנעת של הכפלת נקודת האפס של המשקל בערך ההפעלה. אם מגדירים שנקודת האפס היא 0, אפשר להימנע מהעלות הזו.
הסבר על החשבון: הדבר דומה לסעיף 2.3 ב-arXiv:1712.05877, למעט ההבדל שבו אנחנו מאפשרים לערכי קנה המידה להיות לפי ציר. היא יוצרת כלל כללי במהירות, באופן הבא:
$A$ הוא מטריצה של $m\times n$ של הפעלות לפי כמות.
$B$ היא מטריצה של $n \times p$ של משקלים כמותיים.
אפשר להכפיל את השורה $j$של $A$ ו-$a_j$ בעמודה $k$th שגודלה הוא
$B$, $b_k$, שניהם באורך $n$. ערכי המספרים השלמים המכמים
וערכי אפס הנקודות הם $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