מפרט קוונטיזציה של LiteRT ב-8 ביט

במסמך הבא מפורט המפרט של גרסת 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

קובצי עזר

arXiv:1712.05877