LiteRT Next را در اندروید با C++ اجرا کنید

API های LiteRT Next در C++ موجود هستند و می توانند به توسعه دهندگان اندروید کنترل بیشتری بر تخصیص حافظه و توسعه سطح پایین نسبت به API های Kotlin ارائه دهند.

برای مثالی از برنامه LiteRT Next در C++، بخش‌بندی ناهمگام با نسخه نمایشی C++ را ببینید.

شروع کنید

از مراحل زیر برای افزودن LiteRT Next به برنامه اندروید خود استفاده کنید.

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

ساختن یک برنامه 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 ).
    • Dispatch Libraries: کتابخانه های اشتراکی خاص فروشنده که LiteRT برای برقراری ارتباط با درایورهای سخت افزاری استفاده می کند (به عنوان مثال، //litert/vendors/qualcomm/dispatch:dispatch_api_so ).
    • کتابخانه های پشتیبان GPU: کتابخانه های مشترک برای شتاب پردازنده گرافیکی (به عنوان مثال، "@litert_gpu//:jni/arm64-v8a/libLiteRtGpuAccelerator.so ).
    • کتابخانه های پشتیبان NPU: کتابخانه های مشترک خاص برای شتاب NPU، مانند کتابخانه های QNN HTP Qualcomm (به عنوان مثال، @qairt//:lib/aarch64-android/libQnnHtp.so ، @qairt//:lib/hexagon-v79/unsigned/libQnnHtpV79Skel.so ).
    • فایل‌های مدل و دارایی‌ها: فایل‌های مدل آموزش‌دیده، تصاویر آزمایشی، سایه‌زن‌ها یا هر داده دیگری که در زمان اجرا مورد نیاز است (به عنوان مثال، :model_files ، :shader_files ).
  • ویژگی deps (وابستگی‌های زمان کامپایل): این فهرست کتابخانه‌هایی را که کد شما باید در آنها کامپایل کند، فهرست می‌کند.
    • API ها و ابزارهای 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 است. هنگام ایجاد CompiledModel و TensorBuffer Environment مورد نیاز است. کد زیر یک Environment برای اجرای CPU و GPU بدون هیچ گزینه ای ایجاد می کند:

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

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

با استفاده از CompiledModel API، زمان اجرا را با شی 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 Next به بخش های زیر مراجعه کنید.

رسیدگی به خطا

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)

Compiled Model API ( 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));

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

// 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 API، به کد منبع litert_compiled_model.h مراجعه کنید.

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

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

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

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

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

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

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

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

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

قطعه کد ساده شده زیر نحوه ایجاد بافرهای تنسور مدیریت شده را نشان می دهد، جایی که TensorBuffer API بافرهای مربوطه را تخصیص می دهد:

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 Next در C++ مراجعه کنید.

استنتاج پایه (CPU)

نسخه زیر یک نسخه فشرده از قطعه کد از بخش Get Started است. این ساده ترین پیاده سازی استنتاج با LiteRT Next است.

// 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 Next Compiled Model API اصطکاک خطوط لوله استنتاج را کاهش می‌دهد، به‌ویژه زمانی که با پشتوانه‌های سخت‌افزاری متعدد و جریان‌های کپی صفر سروکار داریم. قطعه کد زیر هنگام ایجاد بافر ورودی از روش 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);