Cómo implementar un delegado personalizado

LiteRT Delegar te permite hacer lo siguiente: ejecutar tus modelos (parte o todo) en otro ejecutor. Este mecanismo puede aprovechar una variedad de aceleradores integrados en el dispositivo, como GPU o Edge TPU (Tensor (Tensor) unidad de procesamiento) para la inferencia. Esto les brinda a los desarrolladores una experiencia separado del TFLite predeterminado para acelerar la inferencia.

En el siguiente diagrama, se resumen los delegados. Obtén más detalles en las siguientes secciones.

Delegados de TFLite

¿Cuándo debo crear un delegado personalizado?

LiteRT tiene una amplia variedad de delegados para aceleradores de destino, como GPU, DSP y EdgeTPU.

Crear tu propio delegado es útil en las siguientes situaciones:

  • Quieres integrar un nuevo motor de inferencias de AA no compatible con ninguna delegado existente.
  • Tiene un acelerador de hardware personalizado que mejora el entorno de ejecución para los reales.
  • Estás desarrollando optimizaciones de CPU (como fusión de operadores) que pueden acelerar ciertos modelos.

¿Cómo trabajan los delegados?

Considera un grafo de modelo simple como el siguiente, y un delegado “MyDelegate” con una implementación más rápida para las operaciones Conv2D y Media.

Gráfico original

Después de aplicar este “MyDelegate”, el gráfico LiteRT original será se actualicen de la siguiente manera:

Gráfico con delegado

El gráfico de arriba se obtiene a medida que LiteRT divide el gráfico original. siguiendo dos reglas:

  • Las operaciones específicas que puede manejar el delegado se colocan en un a la vez que satisface el flujo de trabajo original de procesamiento las dependencias entre las operaciones.
  • Cada partición que se delegará solo tiene nodos de entrada y salida que no estén que maneja el delegado.

Cada partición controlada por un delegado se reemplaza por un nodo delegado (puede también llamado kernel delegado) en el grafo original que evalúa la partición en su llamada de invocación.

Según el modelo, el grafo final puede terminar con uno o más nodos, el Esto significa que el delegado no admite algunas operaciones. En general, no quieres tener varias particiones controladas por el delegado, ya que cada cuando cambias de delegado al gráfico principal, se produce una sobrecarga y pasar los resultados del subgrafo delegado al grafo principal debido a las copias en la memoria (por ejemplo, de GPU a CPU). Esa sobrecarga podría compensar en el rendimiento, especialmente cuando hay una gran cantidad de copias de memoria.

Cómo implementar tu propio delegado personalizado

El método preferido para agregar a un delegado es usar API de SimpleDelegate.

Para crear un nuevo delegado, debes implementar 2 interfaces y proporcionar tu tu propia implementación para los métodos de la interfaz.

De 1 a SimpleDelegateInterface

Esta clase representa las capacidades del delegado, qué operaciones y una clase de fábrica para crear un kernel que encapsule la grafo delegado. Para obtener más información, consulta la interfaz definida en esta Archivo de encabezado C++. Los comentarios en el código explican cada API en detalle.

2 - SimpleDelegateKernelInterface

Esta clase encapsula la lógica para inicializar, preparar y ejecutar el partición delegada.

Contiene lo siguiente: (Consulta definición).

  • Init(...): se llamará una vez para realizar cualquier inicialización única.
  • Prepare(...): se llama para cada instancia diferente de este nodo; esto sucede si tienes múltiples particiones delegadas. Por lo general, quieres hacer memoria de asignación de tamaño, ya que se llamará cada vez que se cambie el tamaño de los tensores.
  • Invoke(...): que se llamará para inferencia.

Ejemplo

En este ejemplo, crearás un delegado muy simple que solo admita 2 tipos de operaciones (ADD) y (SUB) solo con tensores float32.

// MyDelegate implements the interface of SimpleDelegateInterface.
// This holds the Delegate capabilities.
class MyDelegate : public SimpleDelegateInterface {
 public:
  bool IsNodeSupportedByDelegate(const TfLiteRegistration* registration,
                                 const TfLiteNode* node,
                                 TfLiteContext* context) const override {
    // Only supports Add and Sub ops.
    if (kTfLiteBuiltinAdd != registration->builtin_code &&
        kTfLiteBuiltinSub != registration->builtin_code)
      return false;
    // This delegate only supports float32 types.
    for (int i = 0; i < node->inputs->size; ++i) {
      auto& tensor = context->tensors[node->inputs->data[i]];
      if (tensor.type != kTfLiteFloat32) return false;
    }
    return true;
  }

  TfLiteStatus Initialize(TfLiteContext* context) override { return kTfLiteOk; }

  const char* Name() const override {
    static constexpr char kName[] = "MyDelegate";
    return kName;
  }

  std::unique_ptr<SimpleDelegateKernelInterface> CreateDelegateKernelInterface()
      override {
    return std::make_unique<MyDelegateKernel>();
  }
};

A continuación, crea tu propio kernel delegado heredando SimpleDelegateKernelInterface

// My delegate kernel.
class MyDelegateKernel : public SimpleDelegateKernelInterface {
 public:
  TfLiteStatus Init(TfLiteContext* context,
                    const TfLiteDelegateParams* params) override {
    // Save index to all nodes which are part of this delegate.
    inputs_.resize(params->nodes_to_replace->size);
    outputs_.resize(params->nodes_to_replace->size);
    builtin_code_.resize(params->nodes_to_replace->size);
    for (int i = 0; i < params->nodes_to_replace->size; ++i) {
      const int node_index = params->nodes_to_replace->data[i];
      // Get this node information.
      TfLiteNode* delegated_node = nullptr;
      TfLiteRegistration* delegated_node_registration = nullptr;
      TF_LITE_ENSURE_EQ(
          context,
          context->GetNodeAndRegistration(context, node_index, &delegated_node,
                                          &delegated_node_registration),
          kTfLiteOk);
      inputs_[i].push_back(delegated_node->inputs->data[0]);
      inputs_[i].push_back(delegated_node->inputs->data[1]);
      outputs_[i].push_back(delegated_node->outputs->data[0]);
      builtin_code_[i] = delegated_node_registration->builtin_code;
    }
    return kTfLiteOk;
  }

  TfLiteStatus Prepare(TfLiteContext* context, TfLiteNode* node) override {
    return kTfLiteOk;
  }

  TfLiteStatus Eval(TfLiteContext* context, TfLiteNode* node) override {
    // Evaluate the delegated graph.
    // Here we loop over all the delegated nodes.
    // We know that all the nodes are either ADD or SUB operations and the
    // number of nodes equals ''inputs_.size()'' and inputs[i] is a list of
    // tensor indices for inputs to node ''i'', while outputs_[i] is the list of
    // outputs for node
    // ''i''. Note, that it is intentional we have simple implementation as this
    // is for demonstration.

    for (int i = 0; i < inputs_.size(); ++i) {
      // Get the node input tensors.
      // Add/Sub operation accepts 2 inputs.
      auto& input_tensor_1 = context->tensors[inputs_[i][0]];
      auto& input_tensor_2 = context->tensors[inputs_[i][1]];
      auto& output_tensor = context->tensors[outputs_[i][0]];
      TF_LITE_ENSURE_EQ(
          context,
          ComputeResult(context, builtin_code_[i], &input_tensor_1,
                        &input_tensor_2, &output_tensor),
          kTfLiteOk);
    }
    return kTfLiteOk;
  }

 private:
  // Computes the result of addition of 'input_tensor_1' and 'input_tensor_2'
  // and store the result in 'output_tensor'.
  TfLiteStatus ComputeResult(TfLiteContext* context, int builtin_code,
                             const TfLiteTensor* input_tensor_1,
                             const TfLiteTensor* input_tensor_2,
                             TfLiteTensor* output_tensor) {
    if (NumElements(input_tensor_1) != NumElements(input_tensor_2) ||
        NumElements(input_tensor_1) != NumElements(output_tensor)) {
      return kTfLiteDelegateError;
    }
    // This code assumes no activation, and no broadcasting needed (both inputs
    // have the same size).
    auto* input_1 = GetTensorData<float>(input_tensor_1);
    auto* input_2 = GetTensorData<float>(input_tensor_2);
    auto* output = GetTensorData<float>(output_tensor);
    for (int i = 0; i < NumElements(input_tensor_1); ++i) {
      if (builtin_code == kTfLiteBuiltinAdd)
        output[i] = input_1[i] + input_2[i];
      else
        output[i] = input_1[i] - input_2[i];
    }
    return kTfLiteOk;
  }

  // Holds the indices of the input/output tensors.
  // inputs_[i] is list of all input tensors to node at index 'i'.
  // outputs_[i] is list of all output tensors to node at index 'i'.
  std::vector<std::vector<int>> inputs_, outputs_;
  // Holds the builtin code of the ops.
  // builtin_code_[i] is the type of node at index 'i'
  std::vector<int> builtin_code_;
};

Compara y evalúa el nuevo delegado

TFLite tiene un conjunto de herramientas que puedes probar rápidamente con un modelo de TFLite.

  • Herramienta de comparativas de modelos: La herramienta toma un modelo de TFLite, genera entradas aleatorias y, luego, de forma repetida ejecuta el modelo durante una cantidad específica de ejecuciones. Imprime la latencia agregada estadísticas al final.
  • Herramienta de diferencias de inferencia: Para un modelo determinado, la herramienta genera datos gaussianos aleatorios y los pasa a través de dos intérpretes de TFLite diferentes, uno que ejecuta la CPU kernel y el otro con una especificación definida por el usuario. Mide el valor absoluto diferencia entre los tensores de salida de cada intérprete, en un base por elemento. Esta herramienta también puede ser útil para mejorar la precisión de la depuración. problemas.
  • También hay herramientas de evaluación específicas para tareas, para clasificación de imágenes la detección de objetos. Puedes encontrar estas herramientas aquí

Además, TFLite tiene un gran conjunto de pruebas de unidades de kernel y op se reutilizan para probar el nuevo delegado con más cobertura La ruta de ejecución de TFLite no está dañada.

Si quieres reutilizar las pruebas y herramientas de TFLite para el nuevo delegado, puedes usar cualquiera de las dos opciones siguientes:

Cómo elegir el mejor enfoque

Ambos enfoques requieren algunos cambios, como se detalla a continuación. Sin embargo, el primer vincula al delegado de forma estática y requiere volver a compilar las pruebas, y herramientas de evaluación. Por el contrario, la segunda como una biblioteca compartida y requiere que expongas el directorio create/delete de la Biblioteca compartida.

Como resultado, el mecanismo de delegado externo funcionará con Objetos binarios de herramientas LiteRT compilados previamente. Sin embargo, es menos explícito y puede ser más complicado configurarlo pruebas de integración. Usa el enfoque de registrador delegado para mayor claridad.

Opción 1: Aprovecha el registrador delegado

El registrador delegado mantiene una lista de proveedores delegados, cada uno de los cuales brinda una forma fácil de crear Delegados de TFLite basados en marcas de línea de comandos y, por lo tanto, son convenientes para herramientas. Agregar el nuevo delegado a todas las herramientas de LiteRT mencionadas arriba, primero debes crear un nuevo proveedor delegado, y, luego, solo se aplicarán algunos cambios a las reglas de COMPILACIÓN. Un ejemplo completo de esto del proceso de integración se muestra a continuación (y puedes encontrar el código aquí).

Supongamos que tienes un delegado que implementa las APIs de SimpleDelegate y extern “C” APIs de creación y eliminación de este "fijo" delegado como se muestra a continuación:

// Returns default options for DummyDelegate.
DummyDelegateOptions TfLiteDummyDelegateOptionsDefault();

// Creates a new delegate instance that need to be destroyed with
// `TfLiteDummyDelegateDelete` when delegate is no longer used by TFLite.
// When `options` is set to `nullptr`, the above default values are used:
TfLiteDelegate* TfLiteDummyDelegateCreate(const DummyDelegateOptions* options);

// Destroys a delegate created with `TfLiteDummyDelegateCreate` call.
void TfLiteDummyDelegateDelete(TfLiteDelegate* delegate);

Para integrar “DummyDelegate” con la herramienta de comparativas y la herramienta de inferencia, define un DelegateProvider como se muestra a continuación:

class DummyDelegateProvider : public DelegateProvider {
 public:
  DummyDelegateProvider() {
    default_params_.AddParam("use_dummy_delegate",
                             ToolParam::Create<bool>(false));
  }

  std::vector<Flag> CreateFlags(ToolParams* params) const final;

  void LogParams(const ToolParams& params) const final;

  TfLiteDelegatePtr CreateTfLiteDelegate(const ToolParams& params) const final;

  std::string GetName() const final { return "DummyDelegate"; }
};
REGISTER_DELEGATE_PROVIDER(DummyDelegateProvider);

std::vector<Flag> DummyDelegateProvider::CreateFlags(ToolParams* params) const {
  std::vector<Flag> flags = {CreateFlag<bool>("use_dummy_delegate", params,
                                              "use the dummy delegate.")};
  return flags;
}

void DummyDelegateProvider::LogParams(const ToolParams& params) const {
  TFLITE_LOG(INFO) << "Use dummy test delegate : ["
                   << params.Get<bool>("use_dummy_delegate") << "]";
}

TfLiteDelegatePtr DummyDelegateProvider::CreateTfLiteDelegate(
    const ToolParams& params) const {
  if (params.Get<bool>("use_dummy_delegate")) {
    auto default_options = TfLiteDummyDelegateOptionsDefault();
    return TfLiteDummyDelegateCreateUnique(&default_options);
  }
  return TfLiteDelegatePtr(nullptr, [](TfLiteDelegate*) {});
}

Las definiciones de la regla BUILD son importantes porque necesitas asegurarte de que el La biblioteca siempre está vinculada y el optimizador no la descarta.

#### The following are for using the dummy test delegate in TFLite tooling ####
cc_library(
    name = "dummy_delegate_provider",
    srcs = ["dummy_delegate_provider.cc"],
    copts = tflite_copts(),
    deps = [
        ":dummy_delegate",
        "//tensorflow/lite/tools/delegates:delegate_provider_hdr",
    ],
    alwayslink = 1, # This is required so the optimizer doesn't optimize the library away.
)

Ahora, agrega estas dos reglas de wrapper en tu archivo COMPILACIÓN para crear una versión de la Herramienta de comparativas y de inferencia, y otras herramientas de evaluación que se podrían ejecutar con tu propio delegado.

cc_binary(
    name = "benchmark_model_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/benchmark:benchmark_model_main",
    ],
)

cc_binary(
    name = "inference_diff_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/inference_diff:run_eval_lib",
    ],
)

cc_binary(
    name = "imagenet_classification_eval_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/imagenet_image_classification:run_eval_lib",
    ],
)

cc_binary(
    name = "coco_object_detection_eval_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/coco_object_detection:run_eval_lib",
    ],
)

También puedes incorporar este proveedor delegado a las pruebas de kernel de TFLite como se describe aquí.

Opción 2: Aprovecha el delegado externo

En esta alternativa, primero crea un adaptador de delegado externo external_delegate_adaptor.cc como se muestra a continuación. Ten en cuenta que este enfoque es un poco menos preferido en comparación con Opción 1, como se mencionó anteriormente.

TfLiteDelegate* CreateDummyDelegateFromOptions(char** options_keys,
                                               char** options_values,
                                               size_t num_options) {
  DummyDelegateOptions options = TfLiteDummyDelegateOptionsDefault();

  // Parse key-values options to DummyDelegateOptions.
  // You can achieve this by mimicking them as command-line flags.
  std::unique_ptr<const char*> argv =
      std::unique_ptr<const char*>(new const char*[num_options + 1]);
  constexpr char kDummyDelegateParsing[] = "dummy_delegate_parsing";
  argv.get()[0] = kDummyDelegateParsing;

  std::vector<std::string> option_args;
  option_args.reserve(num_options);
  for (int i = 0; i < num_options; ++i) {
    option_args.emplace_back("--");
    option_args.rbegin()->append(options_keys[i]);
    option_args.rbegin()->push_back('=');
    option_args.rbegin()->append(options_values[i]);
    argv.get()[i + 1] = option_args.rbegin()->c_str();
  }

  // Define command-line flags.
  // ...
  std::vector<tflite::Flag> flag_list = {
      tflite::Flag::CreateFlag(...),
      ...,
      tflite::Flag::CreateFlag(...),
  };

  int argc = num_options + 1;
  if (!tflite::Flags::Parse(&argc, argv.get(), flag_list)) {
    return nullptr;
  }

  return TfLiteDummyDelegateCreate(&options);
}

#ifdef __cplusplus
extern "C" {
#endif  // __cplusplus

// Defines two symbols that need to be exported to use the TFLite external
// delegate. See tensorflow/lite/delegates/external for details.
TFL_CAPI_EXPORT TfLiteDelegate* tflite_plugin_create_delegate(
    char** options_keys, char** options_values, size_t num_options,
    void (*report_error)(const char*)) {
  return tflite::tools::CreateDummyDelegateFromOptions(
      options_keys, options_values, num_options);
}

TFL_CAPI_EXPORT void tflite_plugin_destroy_delegate(TfLiteDelegate* delegate) {
  TfLiteDummyDelegateDelete(delegate);
}

#ifdef __cplusplus
}
#endif  // __cplusplus

Ahora, crea el destino de COMPILACIÓN correspondiente para compilar una biblioteca dinámica, como se muestra a continuación a continuación:

cc_binary(
    name = "dummy_external_delegate.so",
    srcs = [
        "external_delegate_adaptor.cc",
    ],
    linkshared = 1,
    linkstatic = 1,
    deps = [
        ":dummy_delegate",
        "//tensorflow/lite/c:common",
        "//tensorflow/lite/tools:command_line_flags",
        "//tensorflow/lite/tools:logging",
    ],
)

Después de crear el archivo .so del delegado externo, puedes compilar objetos binarios o usar precompiladas para que se ejecuten con el delegado nuevo, siempre y cuando el objeto binario esté vinculado con el external_delegate_provider que admite marcas de línea de comandos como se describe aquí. Nota: Este proveedor delegado externo ya se vinculó a una cuenta de prueba y herramientas.

Consulta las descripciones aquí para ver una ilustración de cómo comparar el delegado ficticio con este enfoque de delegado externo. Puedes usar comandos similares para las pruebas de las herramientas de evaluación que mencionamos antes.

Vale la pena señalar que el delegado externo es el C++ correspondiente implementación del delegate en la vinculación LiteRT de Python como se muestra aquí. Por lo tanto, la biblioteca de adaptador de delegado externo dinámico creada aquí podría que se usa directamente con las APIs LiteRT de Python.

Recursos

SO ARCH BINARY_NAME
Linux x86_64
arm
aarch64
Android arm
aarch64