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

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

קובצי עזר

arXiv:1712.05877