במסמך הבא מפורט המפרט של גרסת 8 הביטים של LiteRT סכמת קוונטיזציה. היא נועדה לעזור למפתחי חומרה לספק תמיכה בחומרה לצורך הסקת מסקנות במודלים של LiteRT באמצעות כמות נתונים.
סיכום המפרט
אנחנו מספקים מפרט, ואנחנו יכולים לספק רק חלק מהאחריות התנהגות מסוימת אם פועלים לפי המפרט. אנחנו גם מבינים שחומרה שונה יש העדפות והגבלות שעלולות לגרום לסטיות קלות כאשר להטמיע את המפרט שמובילים להטמעות לא מדויקות ברמת הביט. הוא עשוי להיות מקובל ברוב המקרים (ואנחנו נספק למיטב ידיעתנו, בדיקות שכוללות סבילות לכל פעולה שנאספו מכמה מודלים), האופי של למידת המכונה (והלמידה העמוקה) במקרה הנפוץ ביותר) בלתי אפשרי לספק אחריות קשיחה.
קוונטיזציה של 8 ביט מבצעת הערכה של ערכי נקודה צפה (floating-point) באמצעות בנוסחה.
\[real\_value = (int8\_value - zero\_point) \times scale\]
לפי ציר (נקרא גם 'לכל ערוץ בפעולות המרה') או משקולות לפי טנזור מיוצגים באמצעות
int8 הערכים המשלימים של שני הערכים בטווח [-127, 127] עם ערך שווה של אפס נקודות
ל-0. הפעלות/קלט של כל-כלי מיוצגות על ידי השילוב של int8
בטווח [-128, 127], עם אפס נקודות בטווח [-128, 127].
יש מקרים חריגים נוספים לגבי פעולות מסוימות, שמפורטות בהמשך.
מספר שלם חתום לעומת מספר שלם לא חתום
כימות LiteRT ייתן עדיפות בעיקר לכלים ולליבה עבור
קוונטיזציה של int8 ל-8 ביט. זאת מטעמי נוחות
קוונטיזציה מיוצגת על ידי נקודת אפס ששווה ל-0. בנוסף, רבות
בקצוות העורפיים יש אופטימיזציות נוספות לצבירת int8xint8.
לפי ציר לעומת טנזור
קוונטיזציה לפי טנזור פירושה שיהיה סולם אחד ו/או אפס נקודה
את כל tensor. כימות לפי ציר פירושו שיהיה סולם אחד ו/או
zero_point לכל פרוסה ב-quantized_dimension. המאפיין הכמותי
מציין את הממדים של צורת ה-Tensor שהגודל ואפס הנקודות
תואמים. לדוגמה, t e n s o r 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 של המשקולות של
אבל בתיאוריה זה יכול להיות המימד שמתאים
נקודה-product בהטמעת הליבה, וכך לאפשר רמת פירוט גבוהה יותר של קונטיינרים
ללא השלכות על הביצועים. בתהליך הזה יש שיפורים משמעותיים ברמת הדיוק.
ל-TFLite יש תמיכה בכל ציר זמן במספר הולך וגדל של פעולות. בזמן במסמך זה, קיימת תמיכה עבור Conv2d ו-DepthwiseConv2d.
סימטרי לעומת אסימטרי
הפעלות הן אסימטריות: הן יכולות להופיע בכל מקום
חתמת על הטווח int8 [-128, 127]. הפעלות רבות הן אסימטריות מטבען
נקודת הצירוף היא דרך לא יקרה יחסית
של דיוק בינארי. מכיוון שההפעלות מוכפלות רק בקבוע
את הערך הקבוע של אפס נקודות ניתן לבצע אופטימיזציה די גדולה.
המשקולות הן סימטריות: התוצאה של אפס נקודות שווה ל-0. ערכי המשקל הם כפול ערכים של קלט והפעלה דינמיים. המשמעות היא שיש עלות זמן ריצה בלתי נמנעת של הכפלת נקודת האפס של המשקל ערך הפעלה. אם לא קובעים שנקודת האפס היא 0, אנחנו יכולים להימנע מהעלות הזו.
הסבר לחשבון: דומה לסעיף 2.3 ב arXiv:1712.05877, מלבד ההפרש שאנחנו מאפשרים לערכים של קנה המידה להיות לכל ציר. זה הופך את זה לכללי באופן כללי, ככה:
$A$ היא מטריצה של $m \times n$ של הפעלות כמותיות. 
$B$ היא מטריצה של $n\times p$ של משקולות כמותיות. 
כדאי להכפיל את השורה $j$th של $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
בהמשך נתאר את דרישות הקוונטיזציה לליבות ה-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