¡El Día de la Comunidad de ML es el 9 de noviembre! Únase a nosotros para recibir actualizaciones de TensorFlow, JAX, y más Más información

Implementación de un delegado personalizado

¿Qué es un delegado de TensorFlow Lite?

Un delegado de TensorFlow Lite te permite ejecutar tus modelos (en parte o en su totalidad) en otro ejecutor. Este mecanismo puede aprovechar una variedad de aceleradores en el dispositivo, como la GPU o Edge TPU (Unidad de procesamiento de tensor) para la inferencia. Esto proporciona a los desarrolladores un método flexible y desacoplado del TFLite predeterminado para acelerar la inferencia.

El siguiente diagrama resume a los delegados, más detalles en las secciones siguientes.

TFLite Delegates

¿Cuándo debo crear un delegado personalizado?

TensorFlow Lite tiene una amplia variedad de delegados para aceleradores de destino como GPU, DSP, EdgeTPU y marcos como Android NNAPI.

Crear su propio delegado es útil en los siguientes escenarios:

  • Desea integrar un nuevo motor de inferencia ML que no sea compatible con ningún delegado existente.
  • Tiene un acelerador de hardware personalizado que mejora el tiempo de ejecución para escenarios conocidos.
  • Está desarrollando optimizaciones de CPU (como la fusión de operadores) que pueden acelerar ciertos modelos.

¿Cómo trabajan los delegados?

Considere 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 Conv2D y Mean.

Original graph

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

Graph with delegate

El gráfico anterior se obtiene cuando TensorFlow Lite divide el gráfico original siguiendo dos reglas:

  • Las operaciones específicas que podría manejar el delegado se colocan en una partición sin dejar de satisfacer las dependencias del flujo de trabajo informático original entre las operaciones.
  • Cada partición que se va a delegar solo tiene nodos de entrada y salida que no son manejados por el delegado.

Cada partición que es manejada por un delegado es reemplazada por un nodo delegado (también se puede llamar como un kernel delegado) en el gráfico original que evalúa la partición en su llamada de invocación.

Dependiendo del modelo, el gráfico final puede terminar con uno o más nodos, esto último significa que el delegado no admite algunas operaciones. En general, no desea que el delegado maneje múltiples particiones, porque cada vez que cambia de delegado al gráfico principal, hay una sobrecarga para pasar los resultados del subgráfico delegado al gráfico principal que resulta debido a la memoria. copias (por ejemplo, GPU a CPU). Dicha sobrecarga podría contrarrestar las ganancias de rendimiento, especialmente cuando hay una gran cantidad de copias de memoria.

Implementación de su propio delegado personalizado

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

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

1 - SimpleDelegateInterface

Esta clase representa las capacidades del delegado, qué operaciones son compatibles, y una clase de fábrica para crear un núcleo que encapsula el gráfico delegado. Para obtener más detalles, consulte la interfaz definida en este archivo de encabezado de 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: (Ver definición )

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

Ejemplo

En este ejemplo, creará un delegado muy simple que puede admitir solo 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, cree su propio kernel delegado heredando de 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_;
};


Comparar y evaluar al nuevo delegado

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

  • Herramienta de referencia de modelo : la herramienta toma un modelo TFLite, genera entradas aleatorias y luego ejecuta repetidamente el modelo para un número específico de ejecuciones. Imprime estadísticas de latencia agregadas al final.
  • Herramienta de diferencia de inferencia : para un modelo dado, la herramienta genera datos gaussianos aleatorios y los pasa a través de dos intérpretes TFLite diferentes, uno que ejecuta un núcleo de CPU de un solo subproceso y el otro utiliza 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 de tareas específicas, para la clasificación de imágenes y la detección de objetos. Estas herramientas se pueden encontrar aquí.

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

Para lograr reutilizar las pruebas y las herramientas de TFLite para el nuevo delegado, puede usar cualquiera de las dos opciones siguientes:

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 reconstruir las herramientas de prueba, evaluación comparativa y evaluación. Por el contrario, el segundo hace que el delegado sea una biblioteca compartida y requiere que exponga los métodos de creación / eliminación de la biblioteca compartida.

Como resultado, el mecanismo de delegado externo funcionará con los binarios de herramientas preconstruidos de Tensorflow Lite de TFLite . Pero es menos explícito y podría ser más complicado de configurar en pruebas de integración automatizadas. Utilice 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 TFLite basados ​​en indicadores de línea de comandos y, por lo tanto, son convenientes para las herramientas. Para conectar el nuevo delegado a todas las herramientas Tensorflow Lite se mencionó anteriormente, primero debe crear un nuevo proveedor de delegado como esta uno , y después se hace sólo unos pocos cambios en las reglas de generación. A continuación se muestra un ejemplo completo de este proceso de integración (y el código se puede encontrar aquí ).

Suponiendo que tiene un delegado que implementa las API SimpleDelegate y las API "C" externas para crear / eliminar 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 el "DummyDelegate" con la herramienta de referencia y la herramienta de inferencia, defina 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, ya que debe asegurarse de que la biblioteca esté siempre vinculada y no descartada por el optimizador.

#### 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 agregue estas dos reglas contenedoras en su archivo BUILD para crear una versión de la herramienta de referencia y la herramienta de inferencia, y otras herramientas de evaluación, que podrían ejecutarse con su 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 puede conectar este proveedor delegado a las pruebas del kernel de TFLite como se describe aquí .

Opción 2: Aprovechar al delegado externo

En esta alternativa, primero crea un adaptador de delegado externo el external_delegate_adaptor.cc como se muestra a continuación. Tenga en cuenta que este enfoque es un poco menos preferido en comparación con la Opción 1, como se ha mencionado 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 cree el destino BUILD correspondiente para construir 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",
    ],
)

Una vez creado este archivo .so de delegado externo, puede crear binarios o utilizar los preconstruidos para ejecutar con el nuevo delegado siempre que el binario esté vinculado con la biblioteca external_delegate_provider que admite marcas de línea de comandos como se describe aquí . Nota: este proveedor delegado externo ya se ha vinculado a los binarios de herramientas y pruebas existentes.

Consulte las descripciones aquí para obtener una ilustración de cómo comparar al delegado ficticio a través de este enfoque de delegado externo. Puede usar comandos similares para las herramientas de prueba y evaluación mencionadas anteriormente.

Vale la pena señalar que el delegado externo es la implementación de C ++ correspondiente del delegado en el enlace de Python de Tensorflow Lite como se muestra aquí . Por lo tanto, la biblioteca de adaptadores de delegados externos dinámicos creada aquí podría usarse directamente con las API de Python de Tensorflow Lite.

Recursos

SO ARCO BINARY_NAME
Linux x86_64
brazo
aarch64
Androide brazo
aarch64