‫LiteRT CompiledModel C++ API

‫LiteRT CompiledModel API זמין ב-C++‎, ומאפשר למפתחים שליטה מדויקת בהקצאת זיכרון ופיתוח ברמה נמוכה. דוגמה מופיעה במאמר Image segmentation C++ App.

במדריך הבא מוצגת מסקנת ה-CPU הבסיסית של Kotlin API‏ CompiledModel. למידע על תכונות מתקדמות של האצה, אפשר לעיין במדריך בנושא האצת GPU והאצת NPU.

הוספת יחסי תלות ב-build

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

  • שימוש בספרייה מוכנה מראש (פלטפורמות שונות): אפשר להשתמש בספרייה מוכנה מראש של LiteRT כדי להגדיר את המעקב באופן מיידי. אפשר לעיין בהוראות לשימוש בספריית C++‎ שנבנתה מראש מחבילת LiteRT Maven ב-Android, או להוריד ולשלב את הקובץ הבינארי של C++‎ שנבנה מראש ב-Android,‏ iOS,‏ macOS,‏ Linux ו-Windows.

  • קומפילציה ממקור (פלטפורמות שונות): קומפילציה ממקור באמצעות CMake לשליטה מלאה ותמיכה בפלטפורמות שונות (Android, ‏ iOS, ‏ macOS, ‏ Linux, ‏ Windows). פרטים נוספים מופיעים במדריך הזה.

הסקה בסיסית

בקטע הזה מוסבר איך מתבצעת ההסקה הבסיסית.

יצירת הסביבה

אובייקט Environment מספק סביבת זמן ריצה שכוללת רכיבים כמו הנתיב של תוסף הקומפיילר והקשרים של ה-GPU. העמודה Environment נדרשת כשיוצרים CompiledModel ו-TensorBuffer. הקוד הבא יוצר Environment להרצה ב-CPU וב-GPU ללא אפשרויות:

LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));

יצירת CompiledModel

אחרי שמקבלים מודל LiteRT או ממירים מודל לפורמט .tflite, מאתחלים את זמן הריצה באמצעות קובץ המודל באמצעות CompiledModel API. בשלב הזה אפשר לציין את שיפור המהירות באמצעות חומרה (kLiteRtHwAcceleratorCpu או kLiteRtHwAcceleratorGpu):

LITERT_ASSIGN_OR_RETURN(auto compiled_model,
  CompiledModel::Create(env, model, kLiteRtHwAcceleratorCpu));

יצירת מאגרי קלט ופלט

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

LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

אם משתמשים בזיכרון של ה-CPU, ממלאים את נתוני הקלט על ידי כתיבת הנתונים ישירות לתוך מאגר הקלט הראשון.

input_buffers[0].Write<float>(absl::MakeConstSpan(input_data, input_size));

הפעלת המודל

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

compiled_model.Run(input_buffers, output_buffers);

אחזור פלטים

אחזור פלטים על ידי קריאה ישירה של פלט המודל מהזיכרון.

std::vector<float> data(output_data_size);
output_buffers[0].Read<float>(absl::MakeSpan(data));
// ... process output data

מושגים ורכיבים מרכזיים

בקטעים הבאים מוסבר על מושגים ורכיבים מרכזיים של LiteRT CompiledModel API.

טיפול בשגיאות

הפונקציה litert::Expected ב-LiteRT מחזירה ערכים או מעבירה שגיאות בדומה לפונקציות absl::StatusOr או std::expected. אתם יכולים לבדוק את השגיאה באופן ידני.

לנוחותכם, LiteRT מספק את פקודות המאקרו הבאות:

  • הפונקציה LITERT_ASSIGN_OR_RETURN(lhs, expr) מקצה את התוצאה של expr ל-lhs אם היא לא יוצרת שגיאה, ואחרת מחזירה את השגיאה.

    הוא יורחב למשהו כמו הקטע הבא.

    auto maybe_model = CompiledModel::Create(env, "mymodel.tflite", HwAccelerators::kCpu);
    if (!maybe_model) {
      return maybe_model.Error();
    }
    auto model = std::move(maybe_model.Value());
    
  • LITERT_ASSIGN_OR_ABORT(lhs, expr) עושה את אותו הדבר כמו LITERT_ASSIGN_OR_RETURN אבל מבטל את התוכנית במקרה של שגיאה.

  • הפונקציה LITERT_RETURN_IF_ERROR(expr) מחזירה expr אם ההערכה שלה יוצרת שגיאה.

  • LITERT_ABORT_IF_ERROR(expr) עושה את אותו הדבר כמו LITERT_RETURN_IF_ERROR אבל מפסיק את התוכנית במקרה של שגיאה.

מידע נוסף על פקודות מאקרו של LiteRT זמין במאמר litert_macros.h.

Tensor Buffer (TensorBuffer)

‫LiteRT מספק תמיכה מובנית בהפעלה הדדית של מאגרים של קלט/פלט, באמצעות Tensor Buffer API ‏ (TensorBuffer) כדי לטפל בזרימת הנתונים אל המודל המהודר וממנו. Tensor Buffer API מאפשר לכתוב (Write<T>()) ולקרוא (Read<T>()) ולנעול את זיכרון המעבד.

כדי לקבל תמונה מלאה יותר של אופן ההטמעה של TensorBuffer API, אפשר לעיין בקוד המקור של litert_tensor_buffer.h.

דרישות הקלט והפלט של מודל השאילתה

הדרישות להקצאת Tensor Buffer‏ (TensorBuffer) מצוינות בדרך כלל על ידי מאיץ החומרה. יכול להיות שיהיו דרישות לגבי יישור, צעדי מאגר וסוג הזיכרון של מאגרים לקלט ולפלט. אפשר להשתמש בפונקציות עזר כמו CreateInputBuffers כדי לטפל בדרישות האלה באופן אוטומטי.

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

LITERT_ASSIGN_OR_RETURN(auto reqs, compiled_model.GetInputBufferRequirements(signature_index, input_index));

כדי לקבל תמונה מלאה יותר של האופן שבו ה-API של TensorBufferRequirements מיושם, אפשר לעיין בקוד המקור של litert_tensor_buffer_requirements.h.

יצירה של מאגרי Tensor מנוהלים (TensorBuffers)

בקטע הקוד הפשוט הבא מוצג איך ליצור Managed Tensor Buffers, שבהם ה-API‏ TensorBuffer מקצה את המאגרים המתאימים:

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_cpu,
TensorBuffer::CreateManaged(env, /*buffer_type=*/kLiteRtTensorBufferTypeHostMemory,
  ranked_tensor_type, buffer_size));

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_gl, TensorBuffer::CreateManaged(env,
  /*buffer_type=*/kLiteRtTensorBufferTypeGlBuffer, ranked_tensor_type, buffer_size));

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_ahwb, TensorBuffer::CreateManaged(env,
  /*buffer_type=*/kLiteRtTensorBufferTypeAhwb, ranked_tensor_type, buffer_size));

יצירת Tensor Buffers ללא העתקה

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

// Create a TensorBuffer from host memory
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_host,
  TensorBuffer::CreateFromHostMemory(env, ranked_tensor_type,
  ptr_to_host_memory, buffer_size));

// Create a TensorBuffer from GlBuffer
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_gl,
  TensorBuffer::CreateFromGlBuffer(env, ranked_tensor_type, gl_target, gl_id,
  size_bytes, offset));

// Create a TensorBuffer from AHardware Buffer
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_ahwb,
  TensorBuffer::CreateFromAhwb(env, ranked_tensor_type, ahardware_buffer, offset));

קריאה וכתיבה מ-Tensor Buffer

קטע הקוד הבא מדגים איך אפשר לקרוא ממאגר קלט ולכתוב למאגר פלט:

// Example of reading to input buffer:
std::vector<float> input_tensor_data = {1,2};
LITERT_ASSIGN_OR_RETURN(auto write_success,
  input_tensor_buffer.Write<float>(absl::MakeConstSpan(input_tensor_data)));
if(write_success){
  /* Continue after successful write... */
}

// Example of writing to output buffer:
std::vector<float> data(total_elements);
LITERT_ASSIGN_OR_RETURN(auto read_success,
  output_tensor_buffer.Read<float>(absl::MakeSpan(data)));
if(read_success){
  /* Continue after successful read */
}

מתקדם: פעולה הדדית של מאגרים ללא העתקה לסוגים מיוחדים של מאגרי חומרה

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

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_ahwb,
  TensorBuffer::CreateManaged(env, kLiteRtTensorBufferTypeAhwb,
  ranked_tensor_type, buffer_size));
// Buffer interop: Get OpenGL buffer from AHWB,
// internally creating an OpenGL buffer backed by AHWB memory.
LITERT_ASSIGN_OR_RETURN(auto gl_buffer, tensor_buffer_ahwb.GetGlBuffer());

אפשר גם ליצור מאגרי OpenCL מ-AHardwareBuffer:

LITERT_ASSIGN_OR_RETURN(auto cl_buffer, tensor_buffer_ahwb.GetOpenClMemory());

במכשירים ניידים שתומכים בפעולה הדדית בין OpenCL לבין OpenGL, אפשר ליצור מאגרי CL ממאגרי GL:

LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_gl,
  TensorBuffer::CreateFromGlBuffer(env, ranked_tensor_type, gl_target, gl_id,
  size_bytes, offset));

// Creates an OpenCL buffer from the OpenGL buffer, zero-copy.
LITERT_ASSIGN_OR_RETURN(auto cl_buffer, tensor_buffer_from_gl.GetOpenClMemory());

הטמעות לדוגמה

בהמשך מפורטות הטמעות של LiteRT ב-C++.

הסקת מסקנות בסיסית (CPU)

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

// Load model and initialize runtime
LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));
LITERT_ASSIGN_OR_RETURN(auto compiled_model, CompiledModel::Create(env, "mymodel.tflite",
  kLiteRtHwAcceleratorCpu));

// Preallocate input/output buffers
LITERT_ASSIGN_OR_RETURN(auto input_buffers, compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model.CreateOutputBuffers());

// Fill the first input
float input_values[] = { /* your data */ };
input_buffers[0].Write<float>(absl::MakeConstSpan(input_values, /*size*/));

// Invoke
compiled_model.Run(input_buffers, output_buffers);

// Read the output
std::vector<float> data(output_data_size);
output_buffers[0].Read<float>(absl::MakeSpan(data));

העתקה אפסית עם זיכרון המארח

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

// Define an LiteRT environment to use existing EGL display and context.
const std::vector<Environment::Option> environment_options = {
   {OptionTag::EglDisplay, user_egl_display},
   {OptionTag::EglContext, user_egl_context}};
LITERT_ASSIGN_OR_RETURN(auto env,
   Environment::Create(absl::MakeConstSpan(environment_options)));

// Load model1 and initialize runtime.
LITERT_ASSIGN_OR_RETURN(auto compiled_model1, CompiledModel::Create(env, "model1.tflite", kLiteRtHwAcceleratorGpu));

// Prepare I/O buffers. opengl_buffer is given outside from the producer.
LITERT_ASSIGN_OR_RETURN(auto tensor_type, model.GetInputTensorType("input_name0"));
// Create an input TensorBuffer based on tensor_type that wraps the given OpenGL Buffer.
LITERT_ASSIGN_OR_RETURN(auto tensor_buffer_from_opengl,
    litert::TensorBuffer::CreateFromGlBuffer(env, tensor_type, opengl_buffer));

// Create an input event and attach it to the input buffer. Internally, it creates
// and inserts a fence sync object into the current EGL command queue.
LITERT_ASSIGN_OR_RETURN(auto input_event, Event::CreateManaged(env, LiteRtEventTypeEglSyncFence));
tensor_buffer_from_opengl.SetEvent(std::move(input_event));

std::vector<TensorBuffer> input_buffers;
input_buffers.push_back(std::move(tensor_buffer_from_opengl));

// Create an output TensorBuffer of the model1. It's also used as an input of the model2.
LITERT_ASSIGN_OR_RETURN(auto intermedidate_buffers,  compiled_model1.CreateOutputBuffers());

// Load model2 and initialize runtime.
LITERT_ASSIGN_OR_RETURN(auto compiled_model2, CompiledModel::Create(env, "model2.tflite", kLiteRtHwAcceleratorGpu));
LITERT_ASSIGN_OR_RETURN(auto output_buffers, compiled_model2.CreateOutputBuffers());

compiled_model1.RunAsync(input_buffers, intermedidate_buffers);
compiled_model2.RunAsync(intermedidate_buffers, output_buffers);