מפרט קוונטיזציה של TensorFlow Lite בגרסת 8 ביט

במסמך הבא מוצג המפרט של סכמת הקוונטיזציה ב-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

קובצי עזר

arXiv:1712.05877