Versiones del operador TensorFlow Lite

Este documento describe el esquema de control de versiones operativo de TensorFlow Lite. El control de versiones de operaciones permite a los desarrolladores agregar nuevas funcionalidades y parámetros a las operaciones existentes. Además, garantiza lo siguiente:

  • Compatibilidad con versiones anteriores: la nueva implementación de TensorFlow Lite debe manejar un archivo de modelo antiguo.
  • Compatibilidad con versiones anteriores: la implementación anterior de TensorFlow Lite debe manejar un nuevo archivo de modelo producido por una nueva versión del convertidor, siempre que no se usen funciones nuevas.
  • Detección de incompatibilidad hacia adelante: si una implementación antigua de TensorFlow Lite lee un nuevo modelo que contiene una nueva versión de una operación que no es compatible, debe informar el error.

Ejemplo: Adición de dilatación a la convolución en profundidad

El resto de este documento explica el control de versiones en TFLite al mostrar cómo agregar parámetros de dilatación a la operación de convolución en profundidad.

No se requiere conocimiento de la dilatación para entender este documento. Tenga en cuenta que:

  • Se agregarán 2 nuevos parámetros enteros: dilation_width_factor y dilation_height_factor .
  • Los antiguos núcleos de convolución en profundidad que no admiten la dilatación son equivalentes a establecer los factores de dilatación en 1.

Cambiar el esquema de FlatBuffer

Para agregar nuevos parámetros a una operación, cambie la tabla de opciones en lite/schema/schema.fbs .

Por ejemplo, la tabla de opciones de convolución en profundidad se ve así:

table DepthwiseConv2DOptions {
  padding:Padding;
  stride_w:int;
  stride_h:int;
  depth_multiplier:int;
  fused_activation_function:ActivationFunctionType;
}

Al agregar nuevos parámetros:

  • Agregue comentarios que indiquen qué parámetros son compatibles con qué versión.
  • Cuando la nueva implementación obtenga los valores predeterminados para los parámetros recién agregados, debería funcionar exactamente igual que la implementación anterior.

La tabla quedará así después de agregar los nuevos parámetros:

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 debe volver a generarse para el nuevo esquema.

Cambiar las estructuras de C y la implementación del kernel

En TensorFlow Lite, la implementación del kernel está desacoplada de la definición de FlatBuffer. Los núcleos leen el parámetro de las estructuras C definidas en lite/c/builtin_op_data.h .

El parámetro de convolución en 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 FlatBuffer, agregue comentarios que indiquen qué parámetros son compatibles a partir de qué versión. El resultado se ve 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;

Cambie también la implementación del kernel para leer los parámetros recién agregados de las estructuras C. Los detalles son omitidos aquí.

Cambiar el código de lectura de FlatBuffer

La lógica para leer FlatBuffer y producir la estructura C está en lite/core/api/flatbuffer_conversions.cc .

Actualice el archivo para manejar 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 operativa aquí. Cuando la nueva implementación lee un archivo de modelo antiguo en el que faltan factores de dilatación, utilizará 1 como valor predeterminado y el kernel nuevo funcionará de forma coherente con el kernel antiguo.

Cambiar el registro del kernel

El MutableOpResolver (definido en lite/mutable_op_resolver.h ) proporciona algunas funciones para registrar núcleos operativos. La versión mínima y máxima es 1 por defecto:

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 están registradas en lite/kernels/register.cc . En este ejemplo, implementamos un nuevo núcleo operativo que puede manejar las versiones 1 y 2 de DepthwiseConv2D , por lo que debemos cambiar esta línea:

AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D());

a:

AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D(),
             /* min_version = */ 1,
             /* max_version = */ 2);

Cambiar la versión operativa 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:

  • Complete version=1 cuando los factores de dilatación sean todos 1.
  • Complete version=2 de lo contrario.

Modifique la función GetBuiltinOperatorVersion para el operador en lite/tools/versioning/op_version.cc agregando la nueva versión 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;

Actualizar el mapa de versiones del operador

El último paso es agregar la información de la nueva versión en el 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 en función de este mapa de versión.

Para hacer esto, debe agregar una nueva entrada de mapa en lite/tools/versioning/runtime_version.cc .

En este ejemplo, debe agregar la siguiente entrada en op_version_map :

{ {BuiltinOperator_DEPTHWISE_CONV_2D, 2}, %CURRENT_RUNTIME_VERSION%}

donde %CURRENT_RUNTIME_VERSION% corresponde a la versión de tiempo de ejecución actual definida en tensorflow/core/public/version.h .

Implementación de delegación

TensorFlow Lite proporciona una API de delegación que permite delegar operaciones a backends de hardware. En la función Prepare del delegado, verifique si la versión es compatible con cada nodo en el código de Delegación.

const int kMaxVersion = 1;
TfLiteNode* node;
TfLiteRegistration* registration = nullptr;
TF_LITE_ENSURE_STATUS(context->GetNodeAndRegistration(context, node_index, &node, &registration));

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, por lo que la delegación puede detectar la incompatibilidad al obtener una versión superior de la operación.