Versions d'opérateur LiteRT

Ce document décrit le schéma de gestion des versions des opérations de LiteRT. Le versionnage des opérations permet aux développeurs d'ajouter de nouvelles fonctionnalités et de nouveaux paramètres aux opérations existantes. De plus, il garantit les points suivants :

  • Rétrocompatibilité : la nouvelle implémentation LiteRT doit pouvoir gérer un ancien fichier de modèle.
  • Compatibilité ascendante : l'ancienne implémentation LiteRT doit pouvoir gérer un nouveau fichier de modèle produit par la nouvelle version du convertisseur, tant qu'aucune nouvelle fonctionnalité n'est utilisée.
  • Détection d'incompatibilité future : si une ancienne implémentation LiteRT lit un nouveau modèle qui contient une nouvelle version d'une opération non compatible, elle doit signaler l'erreur.

Exemple : Ajout de la dilatation à la convolution depthwise

Le reste de ce document explique le versioning des opérations dans TFLite en montrant comment ajouter des paramètres de dilatation à l'opération de convolution depthwise.

Il n'est pas nécessaire de connaître la dilatation pour comprendre ce document. Remarques :

  • Deux nouveaux paramètres entiers seront ajoutés : dilation_width_factor et dilation_height_factor.
  • Les anciens noyaux de convolution depthwise qui ne prennent pas en charge la dilatation sont équivalents à la définition des facteurs de dilatation sur 1.

Modifier le schéma FlatBuffer

Pour ajouter des paramètres à une opération, modifiez le tableau des options dans lite/schema/schema.fbs.

Par exemple, le tableau des options de convolution depthwise se présente comme suit :

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

Lorsque vous ajoutez des paramètres :

  • Ajoutez des commentaires indiquant les paramètres compatibles avec chaque version.
  • Lorsque la nouvelle implémentation obtient les valeurs par défaut pour les paramètres nouvellement ajoutés, elle doit fonctionner exactement comme l'ancienne implémentation.

Voici à quoi ressemblera le tableau une fois les nouveaux paramètres ajoutés :

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;
}

Le fichier lite/schema/schema_generated.h doit être régénéré pour le nouveau schéma.

Modifier les structures C et l'implémentation du noyau

Dans LiteRT, l'implémentation du noyau est dissociée de la définition FlatBuffer. Les noyaux lisent le paramètre à partir des structures C définies dans lite/c/builtin_op_data.h.

Le paramètre de convolution depthwise d'origine est le suivant :

typedef struct {
  TfLitePadding padding;
  int stride_width;
  int stride_height;
  int depth_multiplier;
  TfLiteFusedActivation activation;
} TfLiteDepthwiseConvParams;

Comme pour le schéma FlatBuffer, ajoutez des commentaires indiquant les paramètres compatibles à partir de quelle version. Le résultat est indiqué ci-dessous :

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;

Veuillez également modifier l'implémentation du noyau pour lire les paramètres nouvellement ajoutés à partir des structures C. Les détails sont omis ici.

Modifier le code de lecture FlatBuffer

La logique de lecture de FlatBuffer et de production de la structure C se trouve dans lite/core/api/flatbuffer_conversions.cc.

Mettez à jour le fichier pour gérer les nouveaux paramètres, comme indiqué ci-dessous :

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;
}

Il n'est pas nécessaire de vérifier la version de l'OS ici. Lorsque la nouvelle implémentation lit un ancien fichier de modèle où les facteurs de dilatation sont manquants, elle utilise 1 comme valeur par défaut, et le nouveau noyau fonctionne de manière cohérente avec l'ancien noyau.

Modifier l'enregistrement du noyau

MutableOpResolver (défini dans lite/mutable_op_resolver.h) fournit quelques fonctions pour enregistrer les noyaux d'opérations. Par défaut, les versions minimale et maximale sont définies sur 1 :

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);

Les opérations intégrées sont enregistrées dans lite/kernels/register.cc. Dans cet exemple, nous avons implémenté un nouveau noyau d'op qui peut gérer les versions 1 et 2 de DepthwiseConv2D. Nous devons donc modifier cette ligne :

AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D());

par :

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

Modifier la version de l'opération TFLite

L'étape suivante consiste à faire en sorte que TFLite renseigne la version minimale requise pour exécuter l'opération. Dans cet exemple, cela signifie :

  • Renseignez "version=1" lorsque tous les facteurs de dilatation sont égaux à 1.
  • Renseignez version=2 dans les autres cas.

Modifiez la fonction GetBuiltinOperatorVersion pour l'opérateur dans lite/tools/versioning/op_version.cc en ajoutant la nouvelle version au cas 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;

Mettre à jour le mappage des versions d'opérateur

La dernière étape consiste à ajouter les informations sur la nouvelle version dans le mappage des versions de l'opérateur. Cette étape est nécessaire, car nous devons générer la version d'exécution minimale requise du modèle en fonction de cette carte des versions.

Pour ce faire, vous devez ajouter une entrée de carte dans lite/tools/versioning/runtime_version.cc.

Dans cet exemple, vous devez ajouter l'entrée suivante dans op_version_map :

{ {BuiltinOperator_DEPTHWISE_CONV_2D, 2}, %CURRENT_RUNTIME_VERSION%}

%CURRENT_RUNTIME_VERSION% correspond à la version actuelle du runtime définie dans release_version.h.

Implémentation de la délégation

LiteRT fournit une API de délégation qui permet de déléguer des opérations à des backends matériels. Dans la fonction Prepare du délégué, vérifiez si la version est compatible avec chaque nœud du code de délégation.

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.
}

Cela est nécessaire même si la délégation ne prend en charge que les opérations de version 1, afin que la délégation puisse détecter l'incompatibilité lors de l'obtention d'une opération de version supérieure.