En este documento, se describe el esquema de versionado de operaciones de LiteRT. El versionamiento de operaciones permite a los desarrolladores agregar nuevas funcionalidades y parámetros a las operaciones existentes. Además, garantiza lo siguiente:
- Retrocompatibilidad: La nueva implementación de LiteRT debe controlar un archivo de modelo anterior.
- Compatibilidad con versiones posteriores: La implementación anterior de LiteRT debe controlar un archivo de modelo nuevo producido por la versión nueva del convertidor, siempre y cuando no se usen funciones nuevas.
- Detección de incompatibilidad hacia adelante: Si una implementación anterior de LiteRT lee un modelo nuevo que contiene una versión nueva de una operación que no es compatible, debe informar el error.
Ejemplo: Cómo agregar dilatación a la convolución separable en profundidad
En el resto de este documento, se explica el control de versiones de las operaciones en TFLite. Para ello, se muestra cómo agregar parámetros de dilatación a la operación de convolución separable en profundidad.
No es necesario tener conocimientos sobre la dilatación para comprender este documento. Ten en cuenta lo siguiente:
- Se agregarán 2 parámetros de números enteros nuevos:
dilation_width_factorydilation_height_factor. - Los kernels de convolución separables antiguos que no admiten la dilatación son equivalentes a establecer los factores de dilatación en 1.
Cómo cambiar el esquema de FlatBuffer
Para agregar parámetros nuevos a una operación, cambia la tabla de opciones en lite/schema/schema.fbs.
Por ejemplo, la tabla de opciones de la convolución separable en profundidad se ve de la siguiente manera:
table DepthwiseConv2DOptions {
padding:Padding;
stride_w:int;
stride_h:int;
depth_multiplier:int;
fused_activation_function:ActivationFunctionType;
}
Cuando agregues parámetros nuevos, ten en cuenta lo siguiente:
- Agrega comentarios que indiquen qué parámetros admite cada versión.
- Cuando la nueva implementación obtiene los valores predeterminados para los parámetros agregados recientemente, debería funcionar exactamente igual que la implementación anterior.
La tabla se verá de la siguiente manera después de que se agreguen los parámetros nuevos:
table DepthwiseConv2DOptions {
// Parameters for DepthwiseConv version 1 or above.
padding:Padding;
stride_w:int;
stride_h:int;
depth_multiplier:int;
fused_activation_function:ActivationFunctionType;
// Parameters for DepthwiseConv version 2 or above.
dilation_w_factor:int = 1;
dilation_h_factor:int = 1;
}
El archivo lite/schema/schema_generated.h se debe volver a generar para el nuevo esquema.
Cambia las estructuras C y la implementación del kernel
En LiteRT, la implementación del kernel está desacoplada de la definición de FlatBuffer.
Los kernels leen el parámetro de las estructuras C definidas en lite/c/builtin_op_data.h.
El parámetro de convolución separable por profundidad original es el siguiente:
typedef struct {
TfLitePadding padding;
int stride_width;
int stride_height;
int depth_multiplier;
TfLiteFusedActivation activation;
} TfLiteDepthwiseConvParams;
Al igual que con el esquema de FlatBuffer, agrega comentarios que indiquen qué parámetros se admiten a partir de qué versión. El resultado se muestra a continuación:
typedef struct {
// Parameters for DepthwiseConv version 1 or above.
TfLitePadding padding;
int stride_width;
int stride_height;
int depth_multiplier;
TfLiteFusedActivation activation;
// Parameters for DepthwiseConv version 2 or above.
int dilation_width_factor;
int dilation_height_factor;
} TfLiteDepthwiseConvParams;
También cambia la implementación del kernel para leer los parámetros recién agregados de las estructuras de C. Aquí se omiten los detalles.
Cambia el código de lectura de FlatBuffer
La lógica para leer FlatBuffer y producir la estructura en C se encuentra en lite/core/api/flatbuffer_conversions.cc.
Actualiza el archivo para controlar los nuevos parámetros, como se muestra a continuación:
TfLiteStatus ParseDepthwiseConv2D(const Operator* op,
ErrorReporter* error_reporter,
BuiltinDataAllocator* allocator,
void** builtin_data) {
CheckParsePointerParams(op, error_reporter, allocator, builtin_data);
SafeBuiltinDataAllocator safe_allocator(allocator);
std::unique_ptr<TfLiteDepthwiseConvParams,
SafeBuiltinDataAllocator::BuiltinDataDeleter>
params = safe_allocator.Allocate<TfLiteDepthwiseConvParams>();
TF_LITE_ENSURE(error_reporter, params != nullptr);
const DepthwiseConv2DOptions* schema_params =
op->builtin_options_as_DepthwiseConv2DOptions();
if (schema_params != nullptr) {
params->padding = ConvertPadding(schema_params->padding());
params->stride_width = schema_params->stride_w();
params->stride_height = schema_params->stride_h();
params->depth_multiplier = schema_params->depth_multiplier();
params->activation =
ConvertActivation(schema_params->fused_activation_function());
params->dilation_width_factor = schema_params->dilation_w_factor();
params->dilation_height_factor = schema_params->dilation_h_factor();
}
*builtin_data = params.release();
return kTfLiteOk;
}
No es necesario verificar la versión de la operación aquí. Cuando la nueva implementación lea un archivo de modelo anterior en el que falten factores de dilatación, usará 1 como valor predeterminado, y el nuevo kernel funcionará de manera coherente con el kernel anterior.
Cambiar el registro del kernel
El MutableOpResolver (definido en lite/mutable_op_resolver.h) proporciona algunas funciones para registrar kernels de operaciones. Las versiones mínima y máxima son 1 de forma predeterminada:
void AddBuiltin(tflite::BuiltinOperator op, TfLiteRegistration* registration,
int min_version = 1, int max_version = 1);
void AddCustom(const char* name, TfLiteRegistration* registration,
int min_version = 1, int max_version = 1);
Las operaciones integradas se registran en lite/kernels/register.cc. En este ejemplo, implementamos un nuevo kernel de op que puede controlar las versiones 1 y 2 de DepthwiseConv2D, por lo que debemos cambiar esta línea:
AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D());
para:
AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D(),
/* min_version = */ 1,
/* max_version = */ 2);
Cómo cambiar la versión de la operación de TFLite
El siguiente paso es hacer que TFLite complete la versión mínima que se requiere para ejecutar la operación. En este ejemplo, significa lo siguiente:
- Se propaga con version=1 cuando todos los factores de dilatación son 1.
- De lo contrario, completa version=2.
Modifica la función GetBuiltinOperatorVersion para el operador en lite/tools/versioning/op_version.cc agregando la versión nueva al caso de DepthwiseConv2D:
case BuiltinOperator_DEPTHWISE_CONV_2D:
auto depthwise_conv_params =
reinterpret_cast<TfLiteDepthwiseConvParams*>(op_sig.builtin_data);
TFLITE_DCHECK(depthwise_conv_params != nullptr);
if (depthwise_conv_params->dilation_width_factor != 1 ||
depthwise_conv_params->dilation_height_factor != 1) {
return 2;
}
return 1;
Actualiza el mapa de versiones del operador
El último paso es agregar la información de la versión nueva al mapa de versiones del operador. Este paso es obligatorio porque necesitamos generar la versión de tiempo de ejecución mínima requerida del modelo según este mapa de versiones.
Para ello, debes agregar una nueva entrada de mapa en lite/tools/versioning/runtime_version.cc.
En este ejemplo, debes agregar la siguiente entrada a op_version_map:
{ {BuiltinOperator_DEPTHWISE_CONV_2D, 2}, %CURRENT_RUNTIME_VERSION%}
Aquí, %CURRENT_RUNTIME_VERSION% corresponde a la versión actual del entorno de ejecución definida en release_version.h.
Implementación de la delegación
LiteRT proporciona una API de delegación que permite delegar operaciones en backends de hardware. En la función Prepare del delegado, verifica si se admite la versión para cada nodo del código de delegación.
const int kMaxVersion = 1;
TfLiteNode* node;
TfLiteRegistration* registration = nullptr;
TF_LITE_ENSURE_STATUS(context->GetNodeAndRegistration(context, node_index, &node, ®istration));
if (registration->version > kMaxVersion) {
// Reject the node if the version isn't supported.
}
Esto es necesario incluso si la delegación solo admite operaciones de la versión 1, de modo que la delegación pueda detectar incompatibilidad cuando se obtiene una operación de una versión superior.