במסמך הבא מפורט המפרט של סכמת הכימות של LiteRT ב-8 ביט. המידע הזה מיועד לעזור למפתחי חומרה לספק תמיכה בחומרה להסקת מסקנות באמצעות מודלים של LiteRT שעברו קוונטיזציה.
סיכום המפרט
אנחנו מספקים מפרט, ויכולים לספק הבטחות מסוימות לגבי התנהגות המערכת רק אם פועלים לפי המפרט. אנחנו גם מבינים שלחומרה שונה עשויות להיות העדפות והגבלות שגורמות לסטייה קלה כשמיישמים את המפרט, וכתוצאה מכך היישומים לא זהים ברמת הביט. יכול להיות שברוב המקרים זה יהיה מקובל (ואנחנו נספק חבילה של בדיקות שלפי הידע שלנו כוללות סטיות מותרות לכל פעולה שאספנו מכמה מודלים), אבל אופי למידת המכונה (ולמידה עמוקה במקרה הנפוץ ביותר) לא מאפשר לנו לספק ערבויות חד-משמעיות.
קוונטיזציה של 8 ביט מחשבת ערכים של נקודה צפה באמצעות הנוסחה הבאה.
\[real\_value = (int8\_value - zero\_point) \times scale\]
משקלים לכל ציר (כלומר לכל ערוץ בפעולות המרה) או לכל טנסור מיוצגים על ידי
int8 ערכי משלים ל-2 בטווח [-127, 127] עם נקודת אפס ששווה ל-0. הפעלה/קלט לכל טנסור מיוצגים על ידי ערכים בשיטת המשלים ל-2 בטווח int8, עם נקודת אפס בטווח [-128, 127].[-128, 127]
יש חריגים נוספים לפעולות מסוימות שמפורטים בהמשך.
מספר שלם עם סימן לעומת מספר שלם ללא סימן
בכימות LiteRT, העדיפות העיקרית היא לכלים וליבות לint8
כימות ל-8 ביט. הסיבה לכך היא הנוחות של כימות סימטרי שמיוצג על ידי נקודת אפס ששווה ל-0. בנוסף, יש הרבה קצה עורפיים עם אופטימיזציות נוספות לצבירה של int8xint8.
לכל ציר לעומת לכל טנסור
קוונטיזציה לכל טנסור פירושה שיהיה קנה מידה אחד או נקודת אפס אחת לכל הטנסור. קוונטיזציה לכל ציר פירושה שיהיה קנה מידה אחד או zero_point לכל פרוסה ב-zero_point.quantized_dimension המאפיין quantized_dimension מציין את המימד של צורת הטנזור שאליו מתייחסים גורמי קנה המידה ונקודות האפס. לדוגמה, טנסור 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$של $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