Versions de l'opérateur TensorFlow Lite

Ce document décrit le schéma de gestion des versions des opérations de TensorFlow Lite. La gestion des versions des opérations permet aux développeurs d'ajouter de nouvelles fonctionnalités et paramètres aux opérations existantes. De plus, il garantit ce qui suit :

  • Rétrocompatibilité : la nouvelle implémentation de TensorFlow Lite doit gérer un ancien fichier de modèle.
  • Compatibilité ascendante : l'ancienne implémentation de TensorFlow Lite doit 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é en aval : si une ancienne implémentation de TensorFlow Lite lit un nouveau modèle contenant une nouvelle version d'une opération qui n'est pas prise en charge, elle doit signaler l'erreur.

Exemple : ajouter une dilatation à une convolution en profondeur

Le reste de ce document explique la gestion des versions op dans TFLite en montrant comment ajouter des paramètres de dilatation à l'opération de convolution en profondeur.

La connaissance de la dilatation n'est pas nécessaire pour comprendre ce document. Notez que:

  • 2 nouveaux paramètres entiers seront ajoutés : dilation_width_factor et dilation_height_factor .
  • Les anciens noyaux de convolution en profondeur qui ne prennent pas en charge la dilatation équivalent à définir les facteurs de dilatation sur 1.

Modifier le schéma FlatBuffer

Pour ajouter de nouveaux paramètres dans une opération, modifiez la table des options dans lite/schema/schema.fbs .

Par exemple, le tableau des options de convolution en profondeur ressemble à ceci :

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

Lors de l'ajout de nouveaux paramètres :

  • Ajoutez des commentaires indiquant quels paramètres sont pris en charge par quelle version.
  • Lorsque la nouvelle implémentation obtient les valeurs par défaut pour les paramètres nouvellement ajoutés, elle devrait fonctionner exactement de la même manière que l'ancienne implémentation.

Le tableau ressemblera à ceci après l'ajout des nouveaux paramètres :

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 regénéré pour le nouveau schéma.

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

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

Le paramètre original de convolution en profondeur 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 quels paramètres sont pris en charge à partir de quelle version. Le résultat est visible 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 pour lire FlatBuffer et produire la structure C est 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 op ici. Lorsque la nouvelle implémentation lit un ancien fichier de modèle où les facteurs de dilatation sont manquants, elle utilisera 1 comme valeur par défaut et le nouveau noyau fonctionnera de manière cohérente avec l'ancien noyau.

Modifier l'enregistrement du noyau

Le MutableOpResolver (défini dans lite/mutable_op_resolver.h ) fournit quelques fonctions pour enregistrer les noyaux op. La version minimale et maximale sont 1 par défaut :

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

à:

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

Changer la version d'exploitation de TFLite

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

  • Remplir version=1 lorsque les facteurs de dilatation sont tous égaux à 1.
  • Remplir version=2 sinon.

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 la carte de version de l'opérateur

La dernière étape consiste à ajouter les nouvelles informations de version dans la carte de version 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 de version.

Pour ce faire, vous devez ajouter une nouvelle 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 d'exécution actuelle définie dans tensorflow/core/public/version.h .

Mise en place de la délégation

TensorFlow Lite fournit une API de délégation qui permet de déléguer des opérations à des backends matériels. Dans la fonction de Prepare du délégué, vérifiez si la version est prise en charge pour chaque nœud dans le 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.
}

Ceci est requis 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 une incompatibilité lors de l'obtention d'une opération de version supérieure.