גרסאות אופרטור LiteRT

במסמך הזה מתוארת הסכימה של ניהול הגרסאות של פעולות ב-LiteRT. ניהול גרסאות של פעולות מאפשר למפתחים להוסיף פונקציות ופרמטרים חדשים לפעולות קיימות. בנוסף, היא מבטיחה את הדברים הבאים:

  • תאימות לאחור: ההטמעה החדשה של LiteRT אמורה לטפל בקובץ מודל ישן.
  • תאימות לעתיד: ההטמעה הישנה של LiteRT אמורה לטפל בקובץ מודל חדש שנוצר על ידי גרסה חדשה של הממיר, כל עוד לא נעשה שימוש בתכונות חדשות.
  • זיהוי קדימה של תאימות: אם הטמעה ישנה של LiteRT קוראת מודל חדש שמכיל גרסה חדשה של פעולה שאינה נתמכת, היא צריכה לדווח על השגיאה.

דוגמה: הוספת התכונה 'הרחבה' ל-depthwise convolution

בהמשך המסמך נסביר איך להוסיף פרמטרים של התרחבות לפעולה של convolveion לעומק (depthwise) כדי להסביר את ניהול הגרסאות של פעולות ב-TFLite.

אין צורך להכיר את הנושא של התרחבות כדי להבין את המסמך הזה. חשוב לזכור:

  • יתווספו 2 פרמטרים חדשים של מספרים שלמים: dilation_width_factor ו-dilation_height_factor.
  • ליבות קוונטיות ישנות של עיבוד עומק (depthwise) שלא תומכות בהרחבה (dilation) שקולות להגדרת גורמי ההרחבה לערך 1.

שינוי הסכימה של FlatBuffer

כדי להוסיף פרמטרים חדשים לפעולה, משנים את טבלת האפשרויות בקובץ lite/schema/schema.fbs.

לדוגמה, טבלת האפשרויות של עיבוד עומק (depthwise) נראית כך:

table DepthwiseConv2DOptions {
  padding:Padding;
  stride_w:int;
  stride_h:int;
  depth_multiplier:int;
  fused_activation_function:ActivationFunctionType;
}

כשמוסיפים פרמטרים חדשים:

  • מוסיפים הערות שמציינות אילו פרמטרים נתמכים בכל גרסה.
  • כשהטמעה חדשה מקבלת את ערכי ברירת המחדל של פרמטרים שנוספו לאחרונה, היא אמורה לפעול בדיוק כמו הטמעה ישנה.

הטבלה תיראה כך אחרי הוספת הפרמטרים החדשים:

table DepthwiseConv2DOptions {
  // Parameters for DepthwiseConv version 1 or above.
  padding:Padding;
  stride_w:int;
  stride_h:int;
  depth_multiplier:int;
  fused_activation_function:ActivationFunctionType;
  // Parameters for DepthwiseConv version 2 or above.
  dilation_w_factor:int = 1;
  dilation_h_factor:int = 1;
}

צריך ליצור מחדש את הקובץ lite/schema/schema_generated.h עבור הסכימה החדשה.

שינוי המבנים ב-C והטמעת הליבה

ב-LiteRT, ההטמעה של הליבה מופרדת מההגדרה של FlatBuffer. הליבות קוראות את הפרמטר ממבנים של C שמוגדרים ב-lite/c/builtin_op_data.h.

הפרמטר המקורי של עיבוד קוונטי עומק הוא:

typedef struct {
  TfLitePadding padding;
  int stride_width;
  int stride_height;
  int depth_multiplier;
  TfLiteFusedActivation activation;
} TfLiteDepthwiseConvParams;

כמו בסכמה של FlatBuffer, מוסיפים הערות שמציינות אילו פרמטרים נתמכים ומאיזו גרסה. התוצאה מופיעה בהמשך:

typedef struct {
  // Parameters for DepthwiseConv version 1 or above.
  TfLitePadding padding;
  int stride_width;
  int stride_height;
  int depth_multiplier;
  TfLiteFusedActivation activation;
  // Parameters for DepthwiseConv version 2 or above.
  int dilation_width_factor;
  int dilation_height_factor;
} TfLiteDepthwiseConvParams;

צריך גם לשנות את ההטמעה בליבה כדי לקרוא את הפרמטרים החדשים שנוספו מהמבנים של C. הפרטים לא מופיעים כאן.

שינוי קוד הקריאה של FlatBuffer

הלוגיקה לקריאת FlatBuffer וליצור מבנה C נמצאת ב-lite/core/api/flatbuffer_conversions.cc.

מעדכנים את הקובץ כדי לטפל בפרמטרים החדשים, כפי שמתואר בהמשך:

TfLiteStatus ParseDepthwiseConv2D(const Operator* op,
                                  ErrorReporter* error_reporter,
                                  BuiltinDataAllocator* allocator,
                                  void** builtin_data) {
  CheckParsePointerParams(op, error_reporter, allocator, builtin_data);

  SafeBuiltinDataAllocator safe_allocator(allocator);

  std::unique_ptr<TfLiteDepthwiseConvParams,
                  SafeBuiltinDataAllocator::BuiltinDataDeleter>
      params = safe_allocator.Allocate<TfLiteDepthwiseConvParams>();
  TF_LITE_ENSURE(error_reporter, params != nullptr);

  const DepthwiseConv2DOptions* schema_params =
      op->builtin_options_as_DepthwiseConv2DOptions();

  if (schema_params != nullptr) {
    params->padding = ConvertPadding(schema_params->padding());
    params->stride_width = schema_params->stride_w();
    params->stride_height = schema_params->stride_h();
    params->depth_multiplier = schema_params->depth_multiplier();
    params->activation =
        ConvertActivation(schema_params->fused_activation_function());

    params->dilation_width_factor = schema_params->dilation_w_factor();
    params->dilation_height_factor = schema_params->dilation_h_factor();
  }

  *builtin_data = params.release();
  return kTfLiteOk;
}

אין צורך לבדוק כאן את גרסת ה-op. כשהטמעה החדשה קוראת קובץ מודל ישן שבו חסרים גורמי הרחבה, היא משתמשת ב-1 בתור ערך ברירת המחדל, והליבה החדשה תפעל באופן עקבי עם הליבה הישנה.

שינוי הרישום של הליבה

MutableOpResolver (המוגדרת ב-lite/mutable_op_resolver.h) מספקת כמה פונקציות לרישום ליבות של פעולות. הגרסה המינימלית והמקסימלית הן 1 כברירת מחדל:

void AddBuiltin(tflite::BuiltinOperator op, TfLiteRegistration* registration,
                int min_version = 1, int max_version = 1);
void AddCustom(const char* name, TfLiteRegistration* registration,
               int min_version = 1, int max_version = 1);

הפעולות המובנות רשומות ב-lite/kernels/register.cc. בדוגמה הזו הטמענו ליבה חדשה של אופרטורים שיכולה לטפל ב-DepthwiseConv2D בגרסאות 1 ו-2, לכן צריך לשנות את השורה הזו:

AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D());

אל:

AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D(),
             /* min_version = */ 1,
             /* max_version = */ 2);

שינוי גרסת הפעולה של TFLite

השלב הבא הוא לגרום ל-TFLite לאכלס את הגרסה המינימלית שנדרשת כדי להריץ את הפעולה. בדוגמה הזו, המשמעות היא:

  • מאכלסים את version=1 כשכל גורמי ההרחבה הם 1.
  • אחרת, מאכלסים את version=2.

משנים את הפונקציה GetBuiltinOperatorVersion של המפעיל ב-lite/tools/versioning/op_version.cc על ידי הוספת הגרסה החדשה למקרה של DepthwiseConv2D:

case BuiltinOperator_DEPTHWISE_CONV_2D:
  auto depthwise_conv_params =
      reinterpret_cast<TfLiteDepthwiseConvParams*>(op_sig.builtin_data);
  TFLITE_DCHECK(depthwise_conv_params != nullptr);
  if (depthwise_conv_params->dilation_width_factor != 1 ||
       depthwise_conv_params->dilation_height_factor != 1) {
    return 2;
  }
  return 1;

עדכון המפה של גרסאות המפעילים

השלב האחרון הוא להוסיף את פרטי הגרסה החדשה למפת הגרסאות של המפעיל. השלב הזה נדרש כי אנחנו צריכים ליצור את גרסת סביבת זמן הריצה המינימלית הנדרשת של המודל על סמך מפת הגרסאות הזו.

לשם כך, צריך להוסיף רשומה חדשה למפה בקובץ lite/tools/versioning/runtime_version.cc.

בדוגמה הזו, צריך להוסיף את הרשומה הבאה לקובץ op_version_map:

{ {BuiltinOperator_DEPTHWISE_CONV_2D, 2}, %CURRENT_RUNTIME_VERSION%}

כאשר %CURRENT_RUNTIME_VERSION% תואם לגרסה הנוכחית בסביבת זמן הריצה שמוגדרת בקובץ release_version.h.

הטמעת הענקת גישה

‏LiteRT מספק ממשק API להענקת גישה (delegation) שמאפשר להעניק גישה לפעולות לקצוות עורפיים של חומרה. בפונקציה Prepare של הנציג, בודקים אם הגרסה נתמכת בכל הצמתים בקוד הענקת הגישה.

const int kMaxVersion = 1;
TfLiteNode* node;
TfLiteRegistration* registration = nullptr;
TF_LITE_ENSURE_STATUS(context->GetNodeAndRegistration(context, node_index, &node, &registration));

if (registration->version > kMaxVersion) {
  // Reject the node if the version isn't supported.
}

הדרישה הזו נדרשת גם אם הענקת הגישה תומכת רק בפעולות בגרסה 1, כדי שהיא תוכל לזהות חוסר תאימות כשמקבלים פעולה בגרסה גבוהה יותר.