Toán tử tuỳ chỉnh

Vì thư viện toán tử tích hợp LiteRT chỉ hỗ trợ một số toán tử TensorFlow, nên không phải mô hình nào cũng có thể chuyển đổi. Để biết thông tin chi tiết, hãy tham khảo phần khả năng tương thích của toán tử.

Để cho phép chuyển đổi, người dùng có thể cung cấp chế độ triển khai tuỳ chỉnh của riêng họ cho một toán tử TensorFlow không được hỗ trợ trong LiteRT, được gọi là toán tử tuỳ chỉnh. Nếu bạn muốn kết hợp một loạt các toán tử TensorFlow không được hỗ trợ (hoặc được hỗ trợ) thành một toán tử tuỳ chỉnh được tối ưu hoá hợp nhất duy nhất, hãy tham khảo hợp nhất toán tử.

Việc sử dụng toán tử tuỳ chỉnh bao gồm 4 bước.

Hãy xem một ví dụ toàn diện về cách chạy mô hình bằng một toán tử tuỳ chỉnh tf.atan (được đặt tên là Atan, hãy tham khảo phần Tạo mô hình TensorFlow) được hỗ trợ trong TensorFlow nhưng không được hỗ trợ trong LiteRT.

Toán tử TensorFlow Text là một ví dụ về toán tử tuỳ chỉnh. Hãy xem hướng dẫn Chuyển đổi văn bản TF sang LiteRT để biết ví dụ về mã.

Ví dụ: Toán tử Atan tuỳ chỉnh

Hãy xem một ví dụ về việc hỗ trợ một toán tử TensorFlow mà LiteRT không có. Giả sử chúng ta đang sử dụng toán tử Atan và đang xây dựng một mô hình rất đơn giản cho hàm y = atan(x + offset), trong đó offset có thể huấn luyện.

Tạo mô hình TensorFlow

Đoạn mã sau đây huấn luyện một mô hình TensorFlow đơn giản. Mô hình này chỉ chứa một toán tử tuỳ chỉnh có tên là Atan, là một hàm y = atan(x + offset), trong đó offset có thể huấn luyện.

import tensorflow as tf

# Define training dataset and variables
x = [-8, 0.5, 2, 2.2, 201]
y = [-1.4288993, 0.98279375, 1.2490457, 1.2679114, 1.5658458]
offset = tf.Variable(0.0)

# Define a simple model which just contains a custom operator named `Atan`
@tf.function(input_signature=[tf.TensorSpec.from_tensor(tf.constant(x))])
def atan(x):
  return tf.atan(x + offset, name="Atan")

# Train model
optimizer = tf.optimizers.Adam(0.01)
def train(x, y):
    with tf.GradientTape() as t:
      predicted_y = atan(x)
      loss = tf.reduce_sum(tf.square(predicted_y - y))
    grads = t.gradient(loss, [offset])
    optimizer.apply_gradients(zip(grads, [offset]))

for i in range(1000):
    train(x, y)

print("The actual offset is: 1.0")
print("The predicted offset is:", offset.numpy())
The actual offset is: 1.0
The predicted offset is: 0.99999905

Tại thời điểm này, nếu bạn cố gắng tạo một mô hình LiteRT bằng các cờ trình chuyển đổi mặc định, bạn sẽ nhận được thông báo lỗi sau:

Error:
error: 'tf.Atan' op is neither a custom op nor a flex op.

Chuyển đổi sang mô hình LiteRT

Tạo một mô hình LiteRT bằng các toán tử tuỳ chỉnh, bằng cách đặt thuộc tính trình chuyển đổi allow_custom_ops như minh hoạ bên dưới:

converter = tf.lite.TFLiteConverter.from_concrete_functions([atan.get_concrete_function()], atan)
converter.allow_custom_ops = True
tflite_model = converter.convert()

Tại thời điểm này, nếu bạn chạy ứng dụng bằng trình thông dịch mặc định bằng các lệnh như sau:

interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()

Bạn vẫn sẽ gặp lỗi:

Encountered unresolved custom op: Atan.

Tạo và đăng ký toán tử.

#include "third_party/tensorflow/lite/c/c_api.h"
#include "third_party/tensorflow/lite/c/c_api_opaque.h"

Các toán tử tuỳ chỉnh LiteRT được xác định bằng cách sử dụng một API C thuần tuý đơn giản bao gồm một loại không rõ ràng (TfLiteOperator) và các hàm liên quan.

TfLiteOperator là một loại mờ:

typedef struct TfLiteOperator TfLiteOperator;

TfLiteOperator lưu trữ danh tính và quá trình triển khai của toán tử. (Xin lưu ý rằng toán tử khác biệt với các toán hạng của toán tử. Các toán hạng này được lưu trữ trong các nút đồ thị LiteRT cho những nút gọi toán tử.)

Các thực thể thuộc loại này được tạo bằng các lệnh gọi đến TfLiteOperatorCreate và có thể bị huỷ bằng cách gọi TfLiteOperatorDelete.

Danh tính của toán tử được thiết lập thông qua các tham số cho hàm khởi tạo TfLiteOperatorCreate:

TfLiteOperator*
TfLiteOperatorCreate(
    TfLiteBuiltinOperator builtin_code,  // Normally `TfLiteBuiltinCustom`.
    const char* custom_name,  // The name of the custom op.
    int version  // Normally `1` for the first version of a custom op.
);

Quá trình triển khai toán tử có thể xác định "phương thức" bằng các chữ ký sau. Tất cả các phương thức này đều không bắt buộc, nhưng để một toán tử được đánh giá thành công, việc triển khai toán tử cần xác định và đặt (bằng cách sử dụng các hàm setter) ít nhất các phương thức PrepareInvoke.

// Initializes the op from serialized data.
void* Init(TfLiteOpaqueContext* context, const char* buffer, size_t length);

// Deallocates the op.
// The pointer `buffer` is the data previously returned by an Init invocation.
void Free(TfLiteOpaqueContext* context, void* buffer);

// Called when the inputs that this node depends on have been resized.
TfLiteStatus Prepare(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node);

// Called when the node is executed. (Should read node inputs and write to
// node outputs).
TfLiteStatus Invoke(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node);

// Retrieves the async kernel.
TfLiteAsyncKernel AsyncKernel(TfLiteOpaqueContext* context,
                              TfLiteOpaqueNode* node);

Các tên hàm (hoặc tiền tố không gian tên, đối với C++) trong quá trình triển khai op không cần phải khớp với tên hàm trong đoạn mã ở trên, vì API op tuỳ chỉnh TF Lite sẽ chỉ sử dụng địa chỉ của các tên hàm đó. Thật vậy, bạn nên khai báo chúng trong một không gian tên ẩn danh hoặc dưới dạng các hàm tĩnh.

Tuy nhiên, bạn nên thêm tên nhà mạng làm không gian tên hoặc tiền tố cho những tên hàm này:

C++

namespace my_namespace::my_custom_op {
  void* Init(TfLiteOpaqueContext* context,
             const char* buffer, size_t length) { ... }
  // ... plus definitions of Free, Prepare, and Invoke ...
}
      

C

void* MyCustomOpInit(TfLiteOpaqueContext* context,
                     const char* buffer, size_t length) { ... }
// ... plus definitions of MyCustomOpFree, MyCustomOpPrepare, and
// MyCustomOpInvoke.
      

Vì đây là một API C, nên các "phương thức" này được triển khai dưới dạng con trỏ hàm C trong loại TfLiteOperator. Các con trỏ này được đặt bằng cách truyền địa chỉ của các hàm triển khai đến các hàm setter tương ứng TfLiteOperatorSetMethodName:

void TfLiteOperatorSetInit(
    TfLiteOperator* operator,
    void* (*init)(TfLiteOpaqueContext* context, const char* buffer,
                  size_t length));
void TfLiteOperatorSetFree(
    TfLiteOperator* operator,
    void (*free)(TfLiteOpaqueContext* context, void* data));
void TfLiteOperatorSetPrepare(
    TfLiteOperator* operator,
    TfLiteStatus (*prepare)(TfLiteOpaqueContext* context,
                            TfLiteOpaqueNode* node));
void TfLiteOperatorSetInvoke(
    TfLiteOperator* operator,
    TfLiteStatus (*invoke)(TfLiteOpaqueContext* context,
                           TfLiteOpaqueNode* node));
void TfLiteOperatorSetAsyncKernel(
    TfLiteOperator* operator,
    struct TfLiteAsyncKernel* (*async_kernel)(TfLiteOpaqueContext* context,
                                              TfLiteOpaqueNode* node));

Tham khảo common.h để biết thông tin chi tiết về TfLiteContextTfLiteNode. TfLiteContext cung cấp các cơ sở báo cáo lỗi và quyền truy cập vào các đối tượng chung, bao gồm cả tất cả các tensor. TfLiteNode cho phép các phương thức triển khai của toán tử truy cập vào dữ liệu đầu vào và đầu ra của chúng.

Khi trình thông dịch tải một mô hình, trình thông dịch sẽ gọi phương thức Init() một lần cho mỗi nút trong biểu đồ. Một Init() nhất định sẽ được gọi nhiều lần nếu thao tác được dùng nhiều lần trong biểu đồ. Đối với các thao tác tuỳ chỉnh, một vùng đệm cấu hình sẽ được cung cấp, chứa một flexbuffer ánh xạ tên tham số với các giá trị của chúng. Vùng đệm trống đối với các thao tác tích hợp vì trình thông dịch đã phân tích cú pháp các tham số thao tác. Các phương thức triển khai kernel yêu cầu trạng thái phải khởi tạo trạng thái đó tại đây và chuyển quyền sở hữu cho phương thức gọi. Đối với mỗi lệnh gọi Init(), sẽ có một lệnh gọi tương ứng đến Free(), cho phép các hoạt động triển khai loại bỏ vùng đệm mà chúng có thể đã phân bổ trong Init().

Bất cứ khi nào các tensor đầu vào được đổi kích thước, trình thông dịch sẽ đi qua biểu đồ thông báo cho các triển khai về thay đổi. Điều này cho phép các thao tác này có cơ hội đổi kích thước vùng đệm nội bộ, kiểm tra tính hợp lệ của các loại và hình dạng đầu vào, đồng thời tính toán lại các hình dạng đầu ra. Tất cả những việc này đều được thực hiện thông qua phương thức Prepare() và các hoạt động triển khai có thể truy cập vào trạng thái của chúng bằng cách sử dụng TfLiteOpaqueNodeGetUserData(node).

Cuối cùng, mỗi khi quá trình suy luận chạy, trình thông dịch sẽ duyệt qua biểu đồ gọi phương thức Invoke() và trạng thái cũng có sẵn dưới dạng TfLiteOpaqueNodeGetUserData(node).

Bạn có thể triển khai các thao tác tuỳ chỉnh bằng cách xác định những hàm "phương thức" đó, rồi xác định một hàm trả về một thực thể của TfLiteOperator được tạo bằng cách gọi TfLiteOperatorCreate rồi gọi các phương thức setter có liên quan:

C++

namespace my_namespace::my_custom_op {
  namespace {
    void* Init(TfLiteOpaqueContext* context,
               const char* buffer, size_t length) { ... }
    void Free(TfLiteOpaqueContext* context, void* buffer) { ... }
    TfLiteStatus Prepare(TfLiteOpaqueContext* context,
                         TfLiteOpaqueNode* node) { ... }
    TfLiteStatus Invoke(TfLiteOpaqueContext* context,
                        TfLiteOpaqueNode* node) {... }
  };

  const TfLiteOperator* MyCustomOperator() {
    // Singleton instance, intentionally never destroyed.
    static const TfLiteOperator* my_custom_op = ()[] {
        TfLiteOperator* r =
            TfLiteOperatorCreate(
                kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1);
        TfLiteOperatorSetInit(r, Init);
        TfLiteOperatorSetFree(r, Free);
        TfLiteOperatorSetPrepare(r, Prepare);
        TfLiteOperatorSetInvoke(r, Eval);
        return r;
      };
    return my_custom_op;
  }
}  // namespace my_namespace
      

C

static void* MyCustomOpInit(TfLiteOpaqueContext* context, const char* buffer,
                     size_t length) { ... }
static void MyCustomOpFree(TfLiteOpaqueContext* context, void* buffer) { ... }
static TfLiteStatus MyCustomOpPrepare(TfLiteOpaqueContext* context,
                                      TfLiteOpaqueNode* node) { ... }
static TfLiteStatus MyCustomOpInvoke(TfLiteOpaqueContext* context,
                                     TfLiteOpaqueNode* node) {... }

static TfLiteOperator* MyCustomOpCreate() {
  const TfLiteOperator* r =
      TfLiteOperatorCreate(
          kTfLiteBuiltinCustom, "MyCustomOp", /*version=*/ 1);
  TfLiteOperatorSetInit(r, MyCustomOpInit);
  TfLiteOperatorSetFree(r, MyCustomOpFree);
  TfLiteOperatorSetPrepare(r, MyCustomOpPrepare);
  TfLiteOperatorSetInvoke(r, MyCustomOpEval);
  return r;
}

const TfLiteOperator* MyCustomOperator() {
  // Singleton instance, intentionally never destroyed.
  static const TfLiteOperator* my_custom_op = MyCustomOpCreate();
  return my_custom_op;
}
      

Xin lưu ý rằng quá trình đăng ký không tự động và bạn phải thực hiện một lệnh gọi rõ ràng đến hàm MyCustomOperator (xem thông tin chi tiết bên dưới). Mặc dù BuiltinOpResolver tiêu chuẩn (có sẵn từ mục tiêu :builtin_ops) sẽ đảm nhận việc đăng ký các thành phần tích hợp, nhưng các thao tác tuỳ chỉnh sẽ phải được thu thập trong các thư viện tuỳ chỉnh riêng biệt.

Xác định nhân trong thời gian chạy LiteRT

Tất cả những gì chúng ta cần làm để sử dụng thao tác này trong LiteRT là xác định 2 hàm (PrepareEval), và hàm thứ ba để tạo TfLiteOperator:

C++

namespace atan_op {
  namespace {
    TfLiteStatus AtanPrepare(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
      TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumInputs(node), 1);
      TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumOutputs(node), 1);

      const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
      TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

      int num_dims = TfLiteOpaqueTensorNumDimensions(input);

      TfLiteIntArray* output_size = TfLiteIntArrayCreate(num_dims);
      for (int i=0; i < num_dims; ++i) {
        output_size->data[i] = input->dims->data[i];
      }

      return TfLiteOpaqueContextResizeTensor(context, output, output_size);
    }

    TfLiteStatus AtanEval(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
      const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
      TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

      float* input_data = static_cast<float*>(TfLiteOpaqueTensorData(input));
      float* output_data = static_cast<float*>(TfLiteOpaqueTensorData(output));

      size_t count = 1;
      int num_dims = TfLiteOpaqueTensorNumDimensions(input);
      for (int i = 0; i < num_dims; ++i) {
        count *= input->dims->data[i];
      }

      for (size_t i = 0; i < count; ++i) {
        output_data[i] = atan(input_data[i]);
      }
      return kTfLiteOk;
    }
  }  // anonymous namespace

  const TfLiteOperator* AtanOperator() {
    // Singleton instance, intentionally never destroyed.
    static const TfLiteOperator* atan_op = ()[] {
        auto* r = TfLiteOperatorCreate(
            kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1);
        TfLiteOperatorSetPrepare(r, Prepare);
        TfLiteOperatorSetInvoke(r, Eval);
        return r;
      };
    return atan_op;
  }
}  // namespace atan_op
      

C

static TfLiteStatus AtanPrepare(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
  TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumInputs(node), 1);
  TF_LITE_OPAQUE_ENSURE_EQ(context, TfLiteOpaqueNodeNumOutputs(node), 1);

  const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
  TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

  int num_dims = TfLiteOpaqueTensorNumDimensions(input);

  TfLiteIntArray* output_size = TfLiteIntArrayCreate(num_dims);
  for (int i = 0; i < num_dims; ++i) {
    output_size->data[i] = input->dims->data[i];
  }

  return TfLiteOpaqueContextResizeTensor(context, output, output_size);
}

static TfLiteStatus AtanEval(TfLiteOpaqueContext* context, TfLiteOpaqueNode* node) {
  const TfLiteOpaqueTensor* input = TfLiteOpaqueNodeGetInput(context, node, 0);
  TfLiteOpaqueTensor* output = TfLiteOpaqueNodeGetOutput(context, node, 0);

  float* input_data = static_cast<float*>(TfLiteOpaqueTensorData(input));
  float* output_data = static_cast<float*>(TfLiteOpaqueTensorData(output));

  size_t count = 1;
  int num_dims = TfLiteOpaqueTensorNumDimensions(input);
  for (int i = 0; i < num_dims; ++i) {
    count *= input->dims->data[i];
  }

  for (size_t i = 0; i < count; ++i) {
    output_data[i] = atan(input_data[i]);
  }
  return kTfLiteOk;
}

static const TfLiteOperator* AtanOpCreate() {
  TfLiteOperator* r = TfLiteOperatorCreate(
          kTfLiteBuiltinCustom, "ATAN", /*version=*/ 1);
  TfLiteOperatorSetPrepare(r, Prepare);
  TfLiteOperatorSetInvoke(r, Eval);
  return r;
}

const TfLiteOperator* AtanOperator() {
  // Singleton instance, intentionally never destroyed.
  static const TfLiteOperator* atan_op = AtanOpCreate();
  return atan_op;
}
      

Khi khởi tạo OpResolver, hãy thêm thao tác tuỳ chỉnh vào trình phân giải (xem ví dụ bên dưới). Thao tác này sẽ đăng ký toán tử với LiteRT để LiteRT có thể sử dụng phương thức triển khai mới.

Đăng ký toán tử với thư viện nhân

Bây giờ, chúng ta cần đăng ký toán tử với thư viện nhân. Việc này được thực hiện bằng một OpResolver. Ở chế độ nền, trình thông dịch sẽ tải một thư viện các nhân được chỉ định để thực thi từng toán tử trong mô hình. Mặc dù thư viện mặc định chỉ chứa các hạt nhân tích hợp, nhưng bạn có thể thay thế/tăng cường thư viện này bằng các toán tử tuỳ chỉnh của thư viện op.

Lớp OpResolver (dịch mã và tên toán tử thành mã thực tế) được xác định như sau:

class OpResolver {
 public:
  virtual TfLiteRegistration* FindOp(tflite::BuiltinOperator op) const = 0;
  virtual TfLiteRegistration* FindOp(const char* op) const = 0;
  ...
};

Xin lưu ý rằng để đảm bảo khả năng tương thích ngược, lớp này sử dụng loại cụ thể cũ hơn TfLiteRegistration thay vì loại mờ TfLiteOperator, nhưng cấu trúc TfLiteRegistration chứa một trường registration_external thuộc loại TfLiteOperator*.

Các lớp MutableOpResolverBuiltinOpResolver được lấy từ OpResolver:

class MutableOpResolver : public OpResolver {
 public:
  MutableOpResolver();  // Constructs an initially empty op resolver.
  void AddAll(const MutableOpResolver& other);
  ...
};

class BuiltinOpResolver : public MutableOpResolver {
 public:
  BuiltinOpResolver();  // Constructs an op resolver with all the builtin ops.
};

Việc sử dụng thông thường (không có các thao tác tuỳ chỉnh) yêu cầu bạn sử dụng BuiltinOpResolver và viết:

tflite::ops::builtin::BuiltinOpResolver resolver;

Để thêm thao tác tuỳ chỉnh đã tạo ở trên, bạn có thể dùng MutableOpResolver và gọi tflite::AddOp (trước khi truyền trình phân giải đến InterpreterBuilder):

tflite::ops::builtin::MutableOpResolver resolver;
resolver.AddAll(tflite::ops::builtin::BuiltinOpResolver());
tflite::AddOp(&resolver, AtanOpRegistration());

Nếu tập hợp các thao tác tích hợp được coi là quá lớn, thì OpResolver mới có thể được tạo mã dựa trên một tập hợp con nhất định của các thao tác, có thể chỉ là những thao tác có trong một mô hình nhất định. Đây là phiên bản tương đương của tính năng đăng ký có chọn lọc của TensorFlow (và phiên bản đơn giản của tính năng này có trong thư mục tools).

Nếu muốn xác định các toán tử tuỳ chỉnh trong Java, hiện tại, bạn cần phải tạo lớp JNI tuỳ chỉnh của riêng mình và biên dịch AAR tuỳ chỉnh của riêng mình trong mã jni này. Tương tự, nếu muốn xác định những toán tử này có trong Python, bạn có thể đặt các giá trị đăng ký trong mã trình bao bọc Python.

Xin lưu ý rằng bạn có thể làm theo quy trình tương tự như trên để hỗ trợ một nhóm thao tác thay vì một toán tử duy nhất. Bạn chỉ cần thêm bao nhiêu toán tử AddCustom tuỳ ý. Ngoài ra, MutableOpResolver cũng cho phép bạn ghi đè các cách triển khai của các thành phần tích hợp bằng cách sử dụng AddBuiltin.

Kiểm thử và lập hồ sơ cho toán tử

Để lập hồ sơ cho hoạt động của bạn bằng công cụ đo điểm chuẩn LiteRT, bạn có thể sử dụng công cụ mô hình đo điểm chuẩn cho LiteRT. Để kiểm thử, bạn có thể tạo bản dựng cục bộ của LiteRT nhận biết thao tác tuỳ chỉnh bằng cách thêm lệnh gọi AddCustom thích hợp (như minh hoạ ở trên) vào register.cc

Các phương pháp hay nhất

  1. Thận trọng tối ưu hoá việc phân bổ và huỷ phân bổ bộ nhớ. Việc phân bổ bộ nhớ trong Prepare hiệu quả hơn so với trong Invoke và việc phân bổ bộ nhớ trước một vòng lặp sẽ tốt hơn so với trong mỗi lần lặp. Sử dụng dữ liệu tensor tạm thời thay vì tự phân bổ (xem mục 2). Hãy sử dụng con trỏ/tham chiếu thay vì sao chép nhiều nhất có thể.

  2. Nếu một cấu trúc dữ liệu sẽ duy trì trong toàn bộ hoạt động, bạn nên phân bổ trước bộ nhớ bằng cách sử dụng các tensor tạm thời. Bạn có thể cần sử dụng một cấu trúc OpData để tham chiếu các chỉ mục tensor trong các hàm khác. Hãy xem ví dụ trong nhân cho phép tích chập. Dưới đây là một đoạn mã mẫu.

    struct MyOpData {
      int temp_tensor_index;
      ...
    };
    
    void* Init(TfLiteOpaqueContext* context,
        const char* buffer, size_t length) {
      auto* op_data = new MyOpData{};
      ...
      return op_data;
    }
    void Free(TfLiteOpaqueContext* context, void* buffer) {
      ...
      delete reinterpret_cast<MyOpData*>(buffer);
    }
    TfLiteStatus Prepare(TfLiteOpaqueContext* context,
                         TfLiteOpaqueNode* node) {
      ...
      auto* op_data =
          reinterpret_cast<MyOpData*>(TfLiteOpaqueNodeGetUserData(node));
      const int num_temporaries = 1;
      int temporary_tensor_indices[num_temporaries];
      TfLiteOpaqueTensorBuilder* builder = TfLiteOpaqueTensorBuilderCreate();
      TfLiteOpaqueTensorBuilderSetType(builder, kTfLiteFloat32);
      TfLiteOpaqueTensorBuilderSetAllocationType(builder, kTfLiteArenaRw);
      TfLiteOpaqueContextAddTensor(context, builder,
          &temporary_tensor_indices[0]);
      TfLiteOpaqueTensorBuilderDelete(builder);
      TfLiteOpaqueNodeSetTemporaries(node, temporary_tensor_indices,
          num_temporaries);
      op_data->temp_tensor_index = temporary_tensor_indices[0];
      ...
      return kTfLiteOk;
    }
    TfLiteStatus Invoke(TfLiteOpaqueContext* context,
                        TfLiteOpaqueNode* node) {
      ...
      auto* op_data = reinterpret_cast<MyOpData*>(
          TfLiteOpaqueNodeGetUserData(node));
      TfLiteOpaqueTensor* temp_tensor =
          TfLiteOpaqueContextGetOpaqueTensor(context,
              op_data->temp_tensor_index);
      TF_LITE_OPAQUE_ENSURE(context,
          TfLiteTensorType(temp_tensor) == kTfLiteFloat32);
      TF_LITE_OPAQUE_ENSURE(context,
          TfLiteTensorGetAllocationType(temp_Tensor) == kTfLiteArenaRw);
      void *temp_data = TfLiteTensorData(temp_tensor);
      TF_LITE_OPAQUE_ENSURE(context, temp_data != nullptr);
      ...
      return kTfLiteOk;
    }
    
  3. Nếu không tốn quá nhiều bộ nhớ lãng phí, hãy ưu tiên sử dụng một mảng có kích thước cố định tĩnh (hoặc std::vector được phân bổ trước trong Resize) thay vì sử dụng std::vector được phân bổ động trong mỗi lần lặp lại quá trình thực thi.

  4. Tránh khởi tạo các mẫu vùng chứa thư viện chuẩn chưa tồn tại, vì chúng ảnh hưởng đến kích thước nhị phân. Ví dụ: nếu bạn cần một std::map trong thao tác không có trong các nhân khác, thì việc sử dụng std::vector với ánh xạ lập chỉ mục trực tiếp có thể hoạt động trong khi vẫn giữ kích thước nhị phân nhỏ. Xem các nhân khác sử dụng những gì để thu thập thông tin chi tiết (hoặc hỏi).

  5. Kiểm tra con trỏ đến bộ nhớ do malloc trả về. Nếu con trỏ này là nullptr, bạn không nên thực hiện bất kỳ thao tác nào bằng con trỏ đó. Nếu bạn malloc trong một hàm và có lỗi thoát, hãy giải phóng bộ nhớ trước khi thoát.

  6. Dùng TF_LITE_OPAQUE_ENSURE(context, condition) để kiểm tra một điều kiện cụ thể. Mã của bạn không được để lại bộ nhớ treo khi TF_LITE_OPAQUE_ENSURE được dùng, tức là bạn nên dùng các macro này trước khi phân bổ bất kỳ tài nguyên nào sẽ bị rò rỉ.