Versiones de operador de LiteRT

En este documento, se describe el esquema de control de versiones de operaciones de LiteRT. Control de versiones 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 debería controlar un archivo de modelo anterior.
  • Compatibilidad con versiones futuras: La implementación antigua de LiteRT debería controlar un archivo de modelo nuevo producido por la nueva versión del conversor, siempre que no haya usar esos atributos.
  • Reenvía la detección de incompatibilidad si se trata de una implementación antigua de LiteRT. lee un nuevo modelo que contiene una nueva versión de una op que no está compatible, debería informar el error.

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

En el resto de este documento, se explica el control de versiones de operaciones en TFLite mostrando cómo para agregar parámetros de dilatación a la operación de convolución en profundidad.

No se requiere tener conocimientos sobre dilatación para comprender este documento. Ten en cuenta lo siguiente:

  • Se agregarán 2 nuevos parámetros de número entero: dilation_width_factor y dilation_height_factor
  • Son equivalentes los antiguos kernels de convolución en profundidad que no admiten dilatación. a establecer los factores de dilatación en 1.

Cambia el esquema FlatBuffer

Para agregar parámetros nuevos en una op, cambia la tabla de opciones en lite/schema/schema.fbs

Por ejemplo, la tabla de opciones de convolución 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 son compatibles con cada versión.
  • Cuando la implementación nueva obtenga los valores predeterminados para los cambios recién agregados por parámetros, 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 debe volver a generarse para el nuevo .

Cómo cambiar las estructuras de C y la implementación de kernel

En LiteRT, la implementación del kernel se desacopla de FlatBuffer. definición. Los kernels leen el parámetro de las estructuras C definidas en lite/c/builtin_op_data.h

El parámetro de convolución original en profundidad 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, agrega comentarios que indiquen qué parámetros se compatible 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;

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

Cambia 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

Actualiza el archivo para procesar los parámetros nuevos, 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 lee un archivo de modelo antiguo donde faltan factores de dilatación, usará 1 como valor predeterminado, y el kernel nuevo funcionará de forma coherente con el anterior.

Cómo cambiar el registro del kernel

MutableOpResolver (definido en lite/mutable_op_resolver.h) proporciona algunos para registrar kernels de op. La versión mínima y máxima son 1 por Predeterminado:

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 operación que puede controlar la versión 1 de DepthwiseConv2D 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);

Cambia la versión de la operación de TFLite

El siguiente paso es hacer que TFLite propague la versión mínima necesaria para ejecutar la op. En este ejemplo, significa lo siguiente:

  • Propaga version=1 cuando los factores de dilatación sean todos 1.
  • De lo contrario, propaga version=2.

Modifica la función GetBuiltinOperatorVersion del operador en lite/tools/versioning/op_version.cc agregando la versión nueva a la sentencia case 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 nueva información de la versión al mapa de la versión del operador. Esta el paso es obligatorio porque necesitamos generar las características del entorno de ejecución en función de este mapa de versiones.

Para hacerlo, necesitas 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%}

donde %CURRENT_RUNTIME_VERSION% corresponde a la versión actual del entorno de ejecución se define en tensorflow/core/public/version.h.

Implementación de delegación

LiteRT proporciona una API de delegación que permite delegar operaciones a en los backends de hardware. En la función Prepare del delegado, comprueba si la versión se es compatible con todos los nodos 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, &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 tanto, la delegación puede detectar incompatibilidad cuando obtiene una op de versión superior.