Versiones del operador de TensorFlow Lite

Este documento describe el esquema de control de versiones de operaciones 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 debería manejar un archivo de modelo antiguo.
  • Compatibilidad futura: la implementación anterior de TensorFlow Lite debería manejar un nuevo archivo de modelo producido por la nueva versión del convertidor, siempre y cuando no se utilicen nuevas funciones.
  • Detección de incompatibilidad directa: 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, debería informar el error.

Ejemplo: agregar dilatación a una convolución profunda

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

No se requieren conocimientos de dilatación para comprender este documento. Tenga en cuenta que:

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

Cambiar el esquema 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 profunda tiene este aspecto:

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 y desde 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 una 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 donde faltan factores de dilatación, utilizará 1 como valor predeterminado y el nuevo kernel funcionará de manera consistente con el kernel anterior.

Cambiar el registro del kernel

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 kernel operativo que puede manejar DepthwiseConv2D versión 1 y 2, 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 necesaria para ejecutar la operación. En este ejemplo, significa:

  • Complete la versión = 1 cuando los factores de dilatación sean todos 1.
  • De lo contrario, complete la versión = 2.

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 versión del operador

El último paso es agregar la información de la nueva versión al mapa de versiones del operador. Este paso es necesario porque necesitamos generar la versión de tiempo de ejecución mínima requerida del modelo en función de este mapa de versiones.

Para hacer esto, necesita 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 actual del tiempo de ejecución definida en tensorflow/core/public/version.h .

Implementación de la 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, de modo que la delegación pueda detectar incompatibilidad al obtener una versión superior.