Cómo implementar un delegado personalizado

Un Delegado de TensorFlow Lite te permite ejecutar tus modelos (de forma parcial o total) en otro ejecutor. Este mecanismo puede aprovechar una variedad de aceleradores en el dispositivo, como la GPU o Edge TPU (unidad de procesamiento tensorial) para la inferencia. Esto proporciona a los desarrolladores un método flexible y separado del TFLite predeterminado para acelerar la inferencia.

En el siguiente diagrama, se resumen los delegados y se ofrecen más detalles en las secciones a continuación.

Delegados de TFLite

¿Cuándo debo crear un delegado personalizado?

TensorFlow Lite 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:

  • Deseas integrar un nuevo motor de inferencia de AA que no sea compatible con ningún delegado existente.
  • Tienes un acelerador de hardware personalizado que mejora el entorno de ejecución para situaciones conocidas.
  • Estás desarrollando optimizaciones de CPU (como la fusión de operadores) que pueden acelerar ciertos modelos.

¿Cómo funcionan los delegados?

Considera un gráfico de modelo simple como el siguiente y un delegado “MyDelegate” que tiene una implementación más rápida para las operaciones de Conv2D y media.

Gráfico original

Después de aplicar “MyDelegate”, el gráfico original de TensorFlow Lite se actualizará de la siguiente manera:

Gráfico con delegado

El gráfico de arriba se obtiene cuando TensorFlow Lite divide el gráfico original según dos reglas:

  • Las operaciones específicas que el delegado puede controlar se colocan en una partición y, al mismo tiempo, satisfacen las dependencias del flujo de trabajo de procesamiento original entre las operaciones.
  • Cada partición que se delegará solo tiene nodos de entrada y salida que el delegado no controla.

Cada partición que controla un delegado se reemplaza por un nodo delegado (también se puede llamar como 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. Este último significa que el delegado no admite algunas operaciones. En general, no es recomendable que el delegado controle varias particiones, ya que cada vez que cambias del delegado al gráfico principal, hay una sobrecarga para pasar los resultados del subgrafo delegado al gráfico principal que se genera debido a las copias de memoria (por ejemplo, de GPU a CPU). Esa sobrecarga puede compensar las mejoras de rendimiento, en especial cuando hay una gran cantidad de copias de memoria.

Cómo implementar tu propio delegado personalizado

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

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

1 a SimpleDelegateInterface

Esta clase representa las capacidades del delegado, las operaciones que se admiten, y una clase de fábrica para crear un kernel que encapsula el gráfico delegado. Para obtener más detalles, consulta la interfaz definida en este 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 la partición delegada.

Tiene lo siguiente: (consulta la definición).

  • Init(...): Se llamará una vez para realizar cualquier inicialización única.
  • Preparar(...): Se llama para cada instancia diferente de este nodo; esto sucede si tienes varias particiones delegadas. Por lo general, querrás realizar asignaciones de memoria aquí, ya que se llamará cada vez que se cambie el tamaño de los tensores.
  • Invocar(...): Se llamará para la inferencia.

Ejemplo

En este ejemplo, crearás un delegado muy simple que solo pueda admitir 2 tipos de operaciones (ADD) y (SUB) 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, hereda elementos de SimpleDelegateKernelInterface para crear tu propio kernel delegado

// 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 en un modelo de TFLite.

  • Herramienta de comparativas de modelos: La herramienta toma un modelo de TFLite, genera entradas aleatorias y, luego, ejecuta el modelo de forma repetida para una cantidad específica de ejecuciones. Imprime las estadísticas de latencia agregadas al final.
  • Herramienta de diferencia 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 el kernel de CPU de un solo subproceso y el otro con una especificación definida por el usuario. Mide la diferencia absoluta entre los tensores de salida de cada intérprete por elemento. Esta herramienta también puede ser útil para depurar problemas de precisión.
  • También hay herramientas de evaluación específicas de tareas para la clasificación de imágenes y la detección de objetos. Puedes encontrar estas herramientas aquí.

Además, TFLite tiene un gran conjunto de pruebas de unidades operativas y de kernel que podrían reutilizarse para probar el delegado nuevo con más cobertura y garantizar que no se rompa la ruta de ejecución normal de TFLite.

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

Cómo elegir el mejor enfoque

Ambos enfoques requieren algunos cambios, como se detalla a continuación. Sin embargo, el primer enfoque vincula al delegado de forma estática y requiere volver a compilar las herramientas de prueba, comparativas y evaluación. En cambio, la segunda convierte al delegado como una biblioteca compartida y requiere que expongas los métodos create/delete de la biblioteca compartida.

Como resultado, el mecanismo de delegado externo funcionará con los objetos binarios de herramientas precompilados de TensorFlow Lite de TFLite. Sin embargo, es menos explícito y puede ser más complicado configurarlo en pruebas de integración automatizadas. Usa el enfoque de registrador delegado para mayor claridad.

Opción 1: Aprovechar el registrador delegado

El registrador delegado mantiene una lista de proveedores delegados, cada uno de los cuales proporciona una manera fácil de crear delegados de TFLite según marcas de línea de comandos y, por lo tanto, son convenientes para las herramientas. Para conectar el delegado nuevo a todas las herramientas de TensorFlow Lite mencionadas anteriormente, primero debes crear un proveedor delegado y, luego, realizar solo algunos cambios en las reglas de COMPILACIÓN. A continuación, se muestra un ejemplo completo de este proceso de integración (puedes encontrar el código aquí).

Supón que tienes un delegado que implementa las APIs de SimpleDelegate y las APIs "C" externas de crear o borrar este delegado "ficticio", 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 Benchmark 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 de COMPILACIÓN son importantes, ya que debes asegurarte de que la biblioteca esté siempre vinculada y que el optimizador no la descarte.

#### 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 a tu archivo BUILD para crear una versión de la Herramienta de comparativas y de la Herramienta de inferencia, así como otras herramientas de evaluación, que podrían ejecutarse 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 conectar este proveedor delegado a las pruebas del kernel de TFLite como se describe aquí.

Opción 2: Aprovecha el delegado externo

Como alternativa, primero debes crear un adaptador de delegado externo para 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 la opción 1 como se mencionó.

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 BUILD correspondiente para compilar una biblioteca dinámica, como se muestra 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 este archivo .so delegado externo, puedes compilar objetos binarios o usar unos compilados previamente para ejecutarlos con el delegado nuevo, siempre y cuando el objeto binario esté vinculado con la biblioteca external_delegate_provider que admita marcas de línea de comandos, como se describe aquí. Nota: Este proveedor delegado externo ya se vinculó a los objetos binarios de herramientas y pruebas existentes.

Consulta las descripciones aquí para ver una ilustración de cómo comparar el delegado ficticio a través de este enfoque de delegado externo. Puedes usar comandos similares para las herramientas de prueba y evaluación que se mencionaron antes.

Vale la pena señalar que el delegado externo es la implementación correspondiente de C++ del delegado en la vinculación de Python para TensorFlow Lite, como se muestra aquí. Por lo tanto, la biblioteca de adaptador de delegado externo dinámico que se creó aquí se puede usar directamente con las APIs de Python de TensorFlow Lite.

Recursos

SO ARCHIVO BINARY_NAME
Linux x86_64
arm
aarch64
Android arm
aarch64