اجرای API مدل کامپایل شده LiteRT در اندروید با C++

رابط‌های برنامه‌نویسی کاربردی مدل کامپایل‌شده‌ی LiteRT به زبان ++C در دسترس هستند و به توسعه‌دهندگان اندروید کنترل دقیقی بر تخصیص حافظه و توسعه‌ی سطح پایین می‌دهند.

برای مثالی از یک برنامه LiteRT در C++، به نسخه آزمایشی تقسیم‌بندی ناهمزمان با C++ مراجعه کنید.

شروع کنید

برای افزودن LiteRT Compiled Model API به برنامه اندروید خود، مراحل زیر را دنبال کنید.

پیکربندی ساخت را به‌روزرسانی کنید

ساخت یک برنامه C++ با LiteRT برای شتاب‌دهی GPU، NPU و CPU با استفاده از Bazel شامل تعریف یک قانون cc_binary است تا از کامپایل، لینک و بسته‌بندی شدن تمام اجزای لازم اطمینان حاصل شود. تنظیمات مثال زیر به برنامه شما اجازه می‌دهد تا به صورت پویا شتاب‌دهنده‌های GPU، NPU و CPU را انتخاب یا استفاده کند.

اجزای کلیدی در پیکربندی ساخت Bazel شما عبارتند از:

  • قانون cc_binary : این قانون اساسی Bazel است که برای تعریف فایل اجرایی ++C شما استفاده می‌شود (مثلاً name = "your_application_name" ).
  • ویژگی srcs : فایل‌های منبع C++ برنامه شما (مثلاً main.cc و سایر فایل‌های .cc یا .h ) را فهرست می‌کند.
  • ویژگی data (وابستگی‌های زمان اجرا): این ویژگی برای بسته‌بندی کتابخانه‌های مشترک و دارایی‌هایی که برنامه شما در زمان اجرا بارگذاری می‌کند، بسیار مهم است.
    • LiteRT Core Runtime: کتابخانه مشترک LiteRT C API (به عنوان مثال، //litert/c:litert_runtime_c_api_shared_lib ).
    • کتابخانه‌های توزیع: کتابخانه‌های اشتراکی مخصوص فروشنده که LiteRT از آنها برای ارتباط با درایورهای سخت‌افزاری استفاده می‌کند (مثلاً //litert/vendors/qualcomm/dispatch:dispatch_api_so ).
    • کتابخانه‌های پشتیبان GPU: کتابخانه‌های مشترک برای شتاب‌دهی GPU (مثلاً "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so ).
    • کتابخانه‌های پشتیبان NPU: کتابخانه‌های اشتراکی خاص برای شتاب‌دهی NPU، مانند کتابخانه‌های QNN HTP کوالکام (مثلاً @qairt//:lib/aarch64-android/libQnnHtp.so ، @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so ).
    • فایل‌ها و دارایی‌های مدل: فایل‌های مدل آموزش‌دیده، تصاویر آزمایشی، سایه‌زن‌ها یا هر داده دیگری که در زمان اجرا مورد نیاز است (مثلاً :model_files ، :shader_files ).
  • ویژگی deps (وابستگی‌های زمان کامپایل): این فهرست، کتابخانه‌هایی را که کد شما برای کامپایل شدن نیاز دارد، فهرست می‌کند.
    • رابط‌های برنامه‌نویسی کاربردی و ابزارهای LiteRT: هدرها و کتابخانه‌های استاتیک برای اجزای LiteRT مانند بافرهای تنسور (مثلاً //litert/cc:litert_tensor_buffer ).
    • کتابخانه‌های گرافیکی (برای GPU): وابستگی‌های مربوط به APIهای گرافیکی در صورتی که شتاب‌دهنده GPU از آنها استفاده کند (مثلاً gles_deps() ).
  • ویژگی linkopts : گزینه‌های ارسالی به لینکر را مشخص می‌کند، که می‌تواند شامل پیوند دادن به کتابخانه‌های سیستم (مثلاً -landroid برای ساخت‌های اندروید یا کتابخانه‌های GLES با gles_linkopts() ) باشد.

مثال زیر یک قانون cc_binary است:

cc_binary(
    name = "your_application",
    srcs = [
        "main.cc",
    ],
    data = [
        ...
        # litert c api shared library
        "//litert/c:litert_runtime_c_api_shared_lib",
        # GPU accelerator shared library
        "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so",
        # NPU accelerator shared library
        "//litert/vendors/qualcomm/dispatch:dispatch_api_so",
    ],
    linkopts = select({
        "@org_tensorflow//tensorflow:android": ["-landroid"],
        "//conditions:default": [],
    }) + gles_linkopts(), # gles link options
    deps = [
        ...
        "//litert/cc:litert_tensor_buffer", # litert cc library
        ...
    ] + gles_deps(), # gles dependencies
)

مدل را بارگذاری کنید

پس از دریافت مدل LiteRT یا تبدیل مدل به فرمت .tflite ، با ایجاد یک شیء Model ، مدل را بارگذاری کنید.

LITERT_ASSIGN_OR_RETURN(auto model, Model::CreateFromFile("mymodel.tflite"));

محیط را ایجاد کنید

شیء Environment یک محیط زمان اجرا فراهم می‌کند که شامل اجزایی مانند مسیر افزونه کامپایلر و زمینه‌های GPU است. Environment هنگام ایجاد CompiledModel و TensorBuffer مورد نیاز است. کد زیر یک Environment برای اجرای CPU و GPU بدون هیچ گزینه‌ای ایجاد می‌کند:

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

ایجاد مدل کامپایل شده

با استفاده از رابط برنامه‌نویسی کاربردی CompiledModel ، زمان اجرا را با شیء Model تازه ایجاد شده مقداردهی اولیه کنید. در این مرحله می‌توانید شتاب سخت‌افزاری ( 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

مفاهیم و اجزای کلیدی

برای کسب اطلاعات در مورد مفاهیم کلیدی و اجزای APIهای مدل کامپایل‌شده LiteRT به بخش‌های زیر مراجعه کنید.

مدیریت خطا

LiteRT از litert::Expected برای برگرداندن مقادیر یا انتشار خطاها به روشی مشابه absl::StatusOr یا std::expected استفاده می‌کند. می‌توانید خودتان به صورت دستی خطا را بررسی کنید.

برای راحتی، LiteRT ماکروهای زیر را ارائه می‌دهد:

  • LITERT_ASSIGN_OR_RETURN(lhs, expr) در صورتی که خطایی تولید نکند، نتیجه expr را به lhs اختصاص می‌دهد و در غیر این صورت، خطا را برمی‌گرداند.

    به چیزی شبیه به قطعه کد زیر گسترش خواهد یافت.

    auto maybe_model = Model::CreateFromFile("mymodel.tflite");
    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 مراجعه کنید.

مدل کامپایل شده (CompiledModel)

رابط برنامه‌نویسی کاربردی مدل کامپایل‌شده ( CompiledModel ) مسئول بارگذاری یک مدل، اعمال شتاب سخت‌افزاری، نمونه‌سازی زمان اجرا، ایجاد بافرهای ورودی و خروجی و اجرای استنتاج است.

قطعه کد ساده‌شده‌ی زیر نشان می‌دهد که چگونه API مدل کامپایل‌شده، یک مدل LiteRT ( .tflite ) و شتاب‌دهنده‌ی سخت‌افزاری هدف (GPU) را دریافت می‌کند و یک مدل کامپایل‌شده ایجاد می‌کند که آماده‌ی اجرای استنتاج است.

// Load model and initialize runtime
LITERT_ASSIGN_OR_RETURN(auto model, Model::CreateFromFile("mymodel.tflite"));
LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));
LITERT_ASSIGN_OR_RETURN(auto compiled_model,
  CompiledModel::Create(env, model, 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 */ };
LITERT_RETURN_IF_ERROR(
  input_buffers[0].Write<float>(absl::MakeConstSpan(input_values, /*size*/)));

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

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

برای مشاهده‌ی کامل‌تر نحوه‌ی پیاده‌سازی رابط برنامه‌نویسی کاربردی CompiledModel ، به کد منبع litert_compiled_model.h مراجعه کنید.

بافر تنسور (TensorBuffer)

LiteRT با استفاده از API Tensor Buffer ( TensorBuffer ) برای مدیریت جریان داده‌ها به داخل و خارج از مدل کامپایل شده، پشتیبانی داخلی برای قابلیت همکاری بافر I/O ارائه می‌دهد. API Tensor Buffer امکان نوشتن ( Write<T>() ) و خواندن ( Read<T>() ) و قفل کردن حافظه CPU را فراهم می‌کند.

برای مشاهده کامل‌تر نحوه پیاده‌سازی API TensorBuffer ، به کد منبع litert_tensor_buffer.h مراجعه کنید.

الزامات ورودی/خروجی مدل پرس و جو

الزامات تخصیص یک بافر تنسور ( TensorBuffer ) معمولاً توسط شتاب‌دهنده سخت‌افزاری مشخص می‌شود. بافرهای ورودی و خروجی می‌توانند الزاماتی در مورد ترازبندی، گام‌های بافر و نوع حافظه داشته باشند. می‌توانید از توابع کمکی مانند CreateInputBuffers برای مدیریت خودکار این الزامات استفاده کنید.

قطعه کد ساده‌شده‌ی زیر نشان می‌دهد که چگونه می‌توانید الزامات بافر برای داده‌های ورودی را بازیابی کنید:

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

برای مشاهده کامل‌تر نحوه پیاده‌سازی API مربوط به TensorBufferRequirements ، به کد منبع litert_tensor_buffer_requirements.h مراجعه کنید.

ایجاد بافرهای تنسور مدیریت‌شده (TensorBuffers)

قطعه کد ساده‌شده‌ی زیر نحوه‌ی ایجاد بافرهای مدیریت‌شده‌ی تنسور را نشان می‌دهد، که در آن 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));

ایجاد بافرهای تانسور با قابلیت کپی صفر

برای قرار دادن یک بافر موجود به عنوان یک بافر تنسور (نسخه صفر)، از قطعه کد زیر استفاده کنید:

// 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 model, Model::CreateFromFile("mymodel.tflite"));
LITERT_ASSIGN_OR_RETURN(auto env, Environment::Create({}));
LITERT_ASSIGN_OR_RETURN(auto compiled_model, CompiledModel::Create(env, model,
  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));

کپی صفر با حافظه میزبان

رابط برنامه‌نویسی کاربردی مدل کامپایل‌شده‌ی LiteRT، اصطکاک خطوط لوله‌ی استنتاج را کاهش می‌دهد، به‌خصوص هنگام مواجهه با چندین پایانه‌ی سخت‌افزاری و جریان‌های بدون کپی. قطعه کد زیر هنگام ایجاد بافر ورودی از متد 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 model1, Model::CreateFromFile("model1.tflite"));
LITERT_ASSIGN_OR_RETURN(auto compiled_model1, CompiledModel::Create(env, model1, 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 model2, Model::CreateFromFile("model2.tflite"));
LITERT_ASSIGN_OR_RETURN(auto compiled_model2, CompiledModel::Create(env, model2, 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);