Dado que la biblioteca de operadores integrados de LiteRT solo admite una cantidad limitada de operadores de TensorFlow, no todos los modelos se pueden convertir. Para obtener más detalles, consulta la compatibilidad con operadores.
Para permitir la conversión, los usuarios pueden proporcionar su propia implementación personalizada de un operador de TensorFlow no admitido en LiteRT, conocido como operador personalizado. Si, en cambio, deseas combinar una serie de operadores de TensorFlow no compatibles (o compatibles) en un solo operador personalizado optimizado y fusionado, consulta la fusión de operadores.
El uso de operadores personalizados consta de cuatro pasos.
Crea un modelo de TensorFlow. Asegúrate de que el SavedModel (o GraphDef) haga referencia al operador LiteRT con el nombre correcto.
Convierte el modelo a LiteRT. Asegúrate de configurar el atributo del conversor de LiteRT correcto para convertir el modelo de forma exitosa.
Crea y registra el operador. Esto es para que el tiempo de ejecución de LiteRT sepa cómo asignar tu operador y los parámetros de tu gráfico al código C/C++ ejecutable.
Prueba y crea un perfil de tu operador. Si solo deseas probar tu operador personalizado, lo mejor es crear un modelo solo con tu operador personalizado y usar el programa benchmark_model.
Veamos un ejemplo de extremo a extremo de la ejecución de un modelo con un operador personalizado tf.atan
(llamado Atan
, consulta Crea un modelo de TensorFlow) que se admite en TensorFlow, pero no en LiteRT.
El operador de TensorFlow Text es un ejemplo de operador personalizado. Consulta el instructivo Cómo convertir TF Text a LiteRT para ver un ejemplo de código.
Ejemplo: Operador Atan
personalizado
Veamos un ejemplo de cómo admitir un operador de TensorFlow que LiteRT no tiene. Supongamos que usamos el operador Atan
y que estamos creando un modelo muy simple para una función y = atan(x + offset)
, en la que offset
es entrenable.
Crea un modelo de TensorFlow
En el siguiente fragmento de código, se entrena un modelo simple de TensorFlow. Este modelo solo contiene un operador personalizado llamado Atan
, que es una función y = atan(x +
offset)
, donde offset
es entrenable.
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
En este punto, si intentas generar un modelo de LiteRT con las marcas del convertidor predeterminado, recibirás el siguiente mensaje de error:
Error:
error: 'tf.Atan' op is neither a custom op nor a flex op.
Convierte el modelo en un modelo de LiteRT
Crea un modelo de LiteRT con operadores personalizados. Para ello, configura el atributo del convertidor allow_custom_ops
como se muestra a continuación:
converter = tf.lite.TFLiteConverter.from_concrete_functions([atan.get_concrete_function()], atan) converter.allow_custom_ops = True tflite_model = converter.convert()
En este punto, si lo ejecutas con el intérprete predeterminado usando comandos como los siguientes:
interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()
Seguirás recibiendo el error:
Encountered unresolved custom op: Atan.
Crea y registra el operador.
#include "third_party/tensorflow/lite/c/c_api.h"
#include "third_party/tensorflow/lite/c/c_api_opaque.h"
Los operadores personalizados de LiteRT se definen con una API simple de C puro que consta de un tipo opaco (TfLiteOperator
) y funciones relacionadas.
TfLiteOperator
es un tipo opaco:
typedef struct TfLiteOperator TfLiteOperator;
TfLiteOperator
almacena la identidad y la implementación del operador.
(Ten en cuenta que el operador es distinto de sus operandos, que se almacenan en los nodos del gráfico de LiteRT para los nodos que llaman al operador).
Las instancias de este tipo se construyen con llamadas a TfLiteOperatorCreate
y se pueden destruir con llamadas a TfLiteOperatorDelete
.
La identidad del operador se establece a través de los parámetros de la función del constructor 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.
);
La implementación del operador puede definir "métodos" con las siguientes firmas.
Todos estos métodos son opcionales, pero para que un operador se evalúe correctamente, la implementación del operador debe definir y establecer (con las funciones de configuración) al menos los métodos Prepare
y Invoke
.
// 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);
Los nombres de las funciones (o los prefijos de espacio de nombres, para C++) en la implementación de tu op no tienen que coincidir con los nombres de las funciones en el fragmento de código anterior, ya que la API de ops personalizadas de TF Lite solo usará sus direcciones. De hecho, te recomendamos que las declares en un espacio de nombres anónimo o como funciones estáticas.
Sin embargo, es una buena idea incluir el nombre del operador como un espacio de nombres o un prefijo en estos nombres de funciones:
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.
Dado que esta es una API de C, estos "métodos" se implementan como punteros de función de C en el tipo TfLiteOperator
, que se establecen pasando las direcciones de tus funciones de implementación a las funciones setter correspondientes TfLiteOperatorSet
MethodName:
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));
Consulta common.h
para obtener detalles sobre TfLiteContext
y TfLiteNode
. TfLiteContext
proporciona funciones de informes de errores y acceso a objetos globales, incluidos todos los tensores.
TfLiteNode
permite que las implementaciones de operadores accedan a sus entradas y salidas.
Cuando el intérprete carga un modelo, llama al método Init()
una vez para cada nodo del grafo. Se llamará a un Init()
determinado más de una vez si la operación se usa varias veces en el gráfico. Para las operaciones personalizadas, se proporcionará un búfer de configuración que contiene un flexbuffer que asigna nombres de parámetros a sus valores. El búfer está vacío para las operaciones integradas porque el intérprete ya analizó los parámetros de la operación. Las implementaciones del kernel que requieren estado deben inicializarlo aquí y transferir la propiedad al llamador. Para cada llamada a Init()
, habrá una llamada correspondiente a Free()
, lo que permitirá que las implementaciones descarten el búfer que podrían haber asignado en Init()
.
Cada vez que se cambie el tamaño de los tensores de entrada, el intérprete recorrerá el gráfico y notificará a las implementaciones sobre el cambio. Esto les da la oportunidad de cambiar el tamaño de su búfer interno, verificar la validez de las formas y los tipos de entrada, y volver a calcular las formas de salida. Todo esto se hace a través del método Prepare()
, y las implementaciones pueden acceder a su estado con TfLiteOpaqueNodeGetUserData(node)
.
Por último, cada vez que se ejecuta la inferencia, el intérprete recorre el gráfico llamando al método Invoke()
, y aquí también el estado está disponible como TfLiteOpaqueNodeGetUserData(node)
.
Las operaciones personalizadas se pueden implementar definiendo esas funciones de "método" y, luego, definiendo una función que muestre una instancia de TfLiteOperator
construida llamando a TfLiteOperatorCreate
y, luego, a los métodos setter pertinentes:
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; }
Ten en cuenta que el registro no es automático y se debe realizar una llamada explícita a tu función MyCustomOperator
(consulta los detalles a continuación). Si bien el BuiltinOpResolver
estándar (disponible desde el destino :builtin_ops
) se encarga del registro de los elementos integrados, las operaciones personalizadas deberán recopilarse en bibliotecas personalizadas independientes.
Cómo definir el kernel en el entorno de ejecución de LiteRT
Todo lo que necesitamos hacer para usar la operación en LiteRT es definir dos funciones (Prepare
y Eval
) y una tercera para construir un 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; }
Cuando inicialices OpResolver
, agrega la operación personalizada al solucionador (consulta un ejemplo más abajo). Esto registrará el operador en LiteRT para que pueda usar la nueva implementación.
Registra el operador con la biblioteca del kernel
Ahora debemos registrar el operador en la biblioteca del kernel. Esto se hace con un OpResolver
. En segundo plano, el intérprete cargará una biblioteca de kernels que se asignarán para ejecutar cada uno de los operadores del modelo.
Si bien la biblioteca predeterminada solo contiene kernels integrados, es posible reemplazarla o aumentarla con operadores de biblioteca personalizados.
La clase OpResolver
, que traduce los códigos y nombres de los operadores en código real, se define de la siguiente manera:
class OpResolver {
public:
virtual TfLiteRegistration* FindOp(tflite::BuiltinOperator op) const = 0;
virtual TfLiteRegistration* FindOp(const char* op) const = 0;
...
};
Ten en cuenta que, para la retrocompatibilidad, esta clase usa el tipo concreto anterior TfLiteRegistration
en lugar del tipo opaco TfLiteOperator
, pero la estructura TfLiteRegistration
contiene un campo registration_external
del tipo TfLiteOperator*
.
Las clases MutableOpResolver
y BuiltinOpResolver
se derivan de 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.
};
El uso normal (sin operaciones personalizadas) requiere que uses BuiltinOpResolver
y escribas lo siguiente:
tflite::ops::builtin::BuiltinOpResolver resolver;
Para agregar la operación personalizada creada anteriormente, puedes usar un MutableOpResolver
y llamar a tflite::AddOp
(antes de pasar el solucionador a InterpreterBuilder
):
tflite::ops::builtin::MutableOpResolver resolver;
resolver.AddAll(tflite::ops::builtin::BuiltinOpResolver());
tflite::AddOp(&resolver, AtanOpRegistration());
Si se considera que el conjunto de operaciones integradas es demasiado grande, se podría generar código para un nuevo OpResolver
en función de un subconjunto determinado de operaciones, posiblemente solo las que se incluyen en un modelo determinado. Esto equivale al registro selectivo de TensorFlow (y una versión simple de este está disponible en el directorio tools
).
Si deseas definir tus operadores personalizados en Java, actualmente deberás compilar tu propia capa de JNI personalizada y compilar tu propio AAR en este código de JNI. Del mismo modo, si deseas definir estos operadores disponibles en Python, puedes colocar tus registros en el código de wrapper de Python.
Ten en cuenta que se puede seguir un proceso similar al anterior para admitir un conjunto de operaciones en lugar de un solo operador. Solo agrega tantos operadores AddCustom
como necesites. Además, MutableOpResolver
también te permite anular las implementaciones de elementos integrados con AddBuiltin
.
Cómo probar y generar perfiles de tu operador
Para generar un perfil de tu operación con la herramienta de comparativas de LiteRT, puedes usar la herramienta de modelos de comparativas para LiteRT. Para realizar pruebas, puedes hacer que tu compilación local de LiteRT conozca tu op personalizada agregando la llamada AddCustom
adecuada (como se muestra arriba) a register.cc.
Prácticas recomendadas
Optimiza las asignaciones y desasignaciones de memoria con precaución. Asignar memoria en
Prepare
es más eficiente que enInvoke
, y asignar memoria antes de un bucle es mejor que en cada iteración. Usa datos de tensores temporales en lugar de asignar memoria tú mismo (consulta el elemento 2). Usa punteros o referencias en lugar de copiar tanto como sea posible.Si una estructura de datos persistirá durante toda la operación, te recomendamos que preasignes la memoria con tensores temporales. Es posible que debas usar una estructura OpData para hacer referencia a los índices de tensores en otras funciones. Consulta el ejemplo en el kernel para la convolución. A continuación, se incluye un fragmento de código de muestra.
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; }
Si no se desperdicia demasiada memoria, es mejor usar un array estático de tamaño fijo (o un
std::vector
preasignado enResize
) que unstd::vector
asignado de forma dinámica en cada iteración de ejecución.Evita crear instancias de plantillas de contenedores de bibliotecas estándar que aún no existan, ya que afectan el tamaño del archivo binario. Por ejemplo, si necesitas un
std::map
en tu operación que no existe en otros kernels, usar unstd::vector
con una asignación de indexación directa podría funcionar y mantener el tamaño del archivo binario pequeño. Consulta qué otros kernels usan para obtener información (o pregunta).Verifica el puntero a la memoria que muestra
malloc
. Si este puntero esnullptr
, no se debe realizar ninguna operación con él. Si usasmalloc
en una función y tienes una salida de error, libera la memoria antes de salir.Usa
TF_LITE_OPAQUE_ENSURE(context, condition)
para verificar una condición específica. Tu código no debe dejar memoria colgando cuando se usaTF_LITE_OPAQUE_ENSURE
, es decir, estas macros se deben usar antes de que se asignen recursos que generen fugas.