カスタムデリゲートの実装

TensorFlow Lite デリゲートとは何ですか?

TensorFlow Liteデリゲートを使用すると、モデル (部分または全体) を別のエグゼキューターで実行できます。このメカニズムでは、GPU や Edge TPU (Tensor Processing Unit) などのさまざまなオンデバイス アクセラレータを推論に利用できます。これにより、開発者は、デフォルトの TFLite から柔軟で分離された方法を利用して、推論を高速化できます。

以下の図は参加者をまとめたもので、詳細は以下のセクションで説明します。

TFLite Delegates

カスタムデリゲートはいつ作成すればよいですか?

TensorFlow Lite には、GPU、DSP、EdgeTPU などのターゲット アクセラレータや Android NNAPI などのフレームワーク用のさまざまなデリゲートがあります。

独自のデリゲートを作成すると、次のシナリオで役立ちます。

  • 既存のデリゲートでサポートされていない新しい ML 推論エンジンを統合したいと考えています。
  • 既知のシナリオの実行時間を改善するカスタム ハードウェア アクセラレータがあります。
  • 特定のモデルを高速化できる CPU 最適化 (演算子の融合など) を開発しています。

代表者はどのように機能しますか?

次のような単純なモデル グラフと、Conv2D および Mean 操作をより高速に実装したデリゲート「MyDelegate」を考えてみましょう。

Original graph

この「MyDelegate」を適用すると、元の TensorFlow Lite グラフは次のように更新されます。

Graph with delegate

上のグラフは、TensorFlow Lite が 2 つのルールに従って元のグラフを分割することで得られます。

  • デリゲートによって処理される特定の操作は、操作間の元のコンピューティング ワークフローの依存関係を満たしながらパーティションに配置されます。
  • 委任される各パーティションには、委任によって処理されない入力ノードと出力ノードのみがあります。

デリゲートによって処理される各パーティションは、呼び出し呼び出しでパーティションを評価する元のグラフ内のデリゲート ノード (デリゲート カーネルとも呼ばれる) に置き換えられます。

モデルによっては、最終的なグラフが 1 つ以上のノードになる場合があります。後者は、一部の操作がデリゲートによってサポートされていないことを意味します。一般に、デリゲートによって複数のパーティションを処理させることは望ましくありません。デリゲートからメイン グラフに切り替えるたびに、デリゲートされたサブグラフからメイン グラフに結果を渡すためのオーバーヘッドがメモリによって発生するためです。コピー (たとえば、GPU から CPU)。このようなオーバーヘッドは、特に大量のメモリ コピーがある場合にパフォーマンスの向上を相殺する可能性があります。

独自のカスタムデリゲートの実装

デリゲートを追加するための推奨される方法は、 SimpleDelegate APIを使用することです。

新しいデリゲートを作成するには、2 つのインターフェイスを実装し、インターフェイス メソッドに独自の実装を提供する必要があります。

1 - SimpleDelegateInterface

このクラスは、デリゲートの機能、サポートされる操作、およびデリゲートされたグラフをカプセル化するカーネルを作成するためのファクトリ クラスを表します。詳細については、このC++ ヘッダー ファイルで定義されているインターフェイスを参照してください。コード内のコメントでは、各 API について詳しく説明しています。

2 - SimpleDelegateKernelInterface

このクラスは、委任されたパーティションを初期化/準備/実行するためのロジックをカプセル化します。

それは次のとおりです: (定義を参照)

  • Init(...): 1 回限りの初期化を行うために 1 回呼び出されます。
  • Prepare(...): このノードの異なるインスタンスごとに呼び出されます。これは、複数の委任されたパーティションがある場合に発生します。これはテンソルのサイズが変更されるたびに呼び出されるため、通常はここでメモリ割り当てを行います。
  • Invoke(...): 推論のために呼び出されます。

この例では、float32 テンソルのみを使用して 2 種類の演算 (ADD) と (SUB) のみをサポートできる非常に単純なデリゲートを作成します。

// MyDelegate implements the interface of SimpleDelegateInterface.
// This holds the Delegate capabilities.
class MyDelegate : public SimpleDelegateInterface {
 public:
  bool IsNodeSupportedByDelegate(const TfLiteRegistration* registration,
                                 const TfLiteNode* node,
                                 TfLiteContext* context) const override {
    // Only supports Add and Sub ops.
    if (kTfLiteBuiltinAdd != registration->builtin_code &&
        kTfLiteBuiltinSub != registration->builtin_code)
      return false;
    // This delegate only supports float32 types.
    for (int i = 0; i < node->inputs->size; ++i) {
      auto& tensor = context->tensors[node->inputs->data[i]];
      if (tensor.type != kTfLiteFloat32) return false;
    }
    return true;
  }

  TfLiteStatus Initialize(TfLiteContext* context) override { return kTfLiteOk; }

  const char* Name() const override {
    static constexpr char kName[] = "MyDelegate";
    return kName;
  }

  std::unique_ptr<SimpleDelegateKernelInterface> CreateDelegateKernelInterface()
      override {
    return std::make_unique<MyDelegateKernel>();
  }
};

次に、 SimpleDelegateKernelInterfaceから継承して独自のデリゲート カーネルを作成します。

// My delegate kernel.
class MyDelegateKernel : public SimpleDelegateKernelInterface {
 public:
  TfLiteStatus Init(TfLiteContext* context,
                    const TfLiteDelegateParams* params) override {
    // Save index to all nodes which are part of this delegate.
    inputs_.resize(params->nodes_to_replace->size);
    outputs_.resize(params->nodes_to_replace->size);
    builtin_code_.resize(params->nodes_to_replace->size);
    for (int i = 0; i < params->nodes_to_replace->size; ++i) {
      const int node_index = params->nodes_to_replace->data[i];
      // Get this node information.
      TfLiteNode* delegated_node = nullptr;
      TfLiteRegistration* delegated_node_registration = nullptr;
      TF_LITE_ENSURE_EQ(
          context,
          context->GetNodeAndRegistration(context, node_index, &delegated_node,
                                          &delegated_node_registration),
          kTfLiteOk);
      inputs_[i].push_back(delegated_node->inputs->data[0]);
      inputs_[i].push_back(delegated_node->inputs->data[1]);
      outputs_[i].push_back(delegated_node->outputs->data[0]);
      builtin_code_[i] = delegated_node_registration->builtin_code;
    }
    return kTfLiteOk;
  }

  TfLiteStatus Prepare(TfLiteContext* context, TfLiteNode* node) override {
    return kTfLiteOk;
  }

  TfLiteStatus Eval(TfLiteContext* context, TfLiteNode* node) override {
    // Evaluate the delegated graph.
    // Here we loop over all the delegated nodes.
    // We know that all the nodes are either ADD or SUB operations and the
    // number of nodes equals ''inputs_.size()'' and inputs[i] is a list of
    // tensor indices for inputs to node ''i'', while outputs_[i] is the list of
    // outputs for node
    // ''i''. Note, that it is intentional we have simple implementation as this
    // is for demonstration.

    for (int i = 0; i < inputs_.size(); ++i) {
      // Get the node input tensors.
      // Add/Sub operation accepts 2 inputs.
      auto& input_tensor_1 = context->tensors[inputs_[i][0]];
      auto& input_tensor_2 = context->tensors[inputs_[i][1]];
      auto& output_tensor = context->tensors[outputs_[i][0]];
      TF_LITE_ENSURE_EQ(
          context,
          ComputeResult(context, builtin_code_[i], &input_tensor_1,
                        &input_tensor_2, &output_tensor),
          kTfLiteOk);
    }
    return kTfLiteOk;
  }

 private:
  // Computes the result of addition of 'input_tensor_1' and 'input_tensor_2'
  // and store the result in 'output_tensor'.
  TfLiteStatus ComputeResult(TfLiteContext* context, int builtin_code,
                             const TfLiteTensor* input_tensor_1,
                             const TfLiteTensor* input_tensor_2,
                             TfLiteTensor* output_tensor) {
    if (NumElements(input_tensor_1) != NumElements(input_tensor_2) ||
        NumElements(input_tensor_1) != NumElements(output_tensor)) {
      return kTfLiteDelegateError;
    }
    // This code assumes no activation, and no broadcasting needed (both inputs
    // have the same size).
    auto* input_1 = GetTensorData<float>(input_tensor_1);
    auto* input_2 = GetTensorData<float>(input_tensor_2);
    auto* output = GetTensorData<float>(output_tensor);
    for (int i = 0; i < NumElements(input_tensor_1); ++i) {
      if (builtin_code == kTfLiteBuiltinAdd)
        output[i] = input_1[i] + input_2[i];
      else
        output[i] = input_1[i] - input_2[i];
    }
    return kTfLiteOk;
  }

  // Holds the indices of the input/output tensors.
  // inputs_[i] is list of all input tensors to node at index 'i'.
  // outputs_[i] is list of all output tensors to node at index 'i'.
  std::vector<std::vector<int>> inputs_, outputs_;
  // Holds the builtin code of the ops.
  // builtin_code_[i] is the type of node at index 'i'
  std::vector<int> builtin_code_;
};


新しいデリゲートのベンチマークと評価

TFLite には、TFLite モデルに対して迅速にテストできるツールのセットが用意されています。

  • モデル ベンチマーク ツール: このツールは TFLite モデルを取得し、ランダムな入力を生成し、指定された実行回数だけモデルを繰り返し実行します。最後に、集約された遅延統計を出力します。
  • Inference Diff Tool : 特定のモデルに対して、このツールはランダムなガウス データを生成し、それを 2 つの異なる TFLite インタープリターに渡します。1 つはシングル スレッドの CPU カーネルを実行し、もう 1 つはユーザー定義の仕様を使用します。各インタプリタからの出力テンソル間の絶対差を要素ごとに測定します。このツールは、精度の問題のデバッグにも役立ちます。
  • 画像分類や物体検出用のタスク固有の評価ツールもあります。これらのツールはここで見つけることができます

さらに、TFLite には、より多くの範囲で新しいデリゲートをテストし、通常の TFLite 実行パスが壊れていないことを確認するために再利用できる、カーネルお​​よび演算の単体テストの大規模なセットがあります。

新しいデリゲート用に TFLite テストとツールを再利用するには、次の 2 つのオプションのいずれかを使用できます。

最適なアプローチの選択

どちらのアプローチでも、以下で詳しく説明するように、いくつかの変更が必要です。ただし、最初のアプローチではデリゲートを静的にリンクするため、テスト、ベンチマーク、評価ツールを再構築する必要があります。対照的に、2 番目の方法ではデリゲートを共有ライブラリとして作成し、共有ライブラリから作成/削除メソッドを公開する必要があります。

その結果、外部デリゲート メカニズムは、TFLite の事前構築された Tensorflow Lite ツール バイナリで動作します。ただし、これはあまり明示的ではなく、自動統合テストでの設定がより複雑になる可能性があります。より明確にするために、代理レジストラのアプローチを使用します。

オプション 1: 代理レジストラを活用する

デリゲート レジストラーはデリゲート プロバイダーのリストを保持しており、各プロバイダーはコマンド ライン フラグに基づいて TFLite デリゲートを簡単に作成する方法を提供するため、ツールに便利です。新しいデリゲートを上記のすべての Tensorflow Lite ツールにプラグインするには、まずこのような新しいデリゲート プロバイダーを作成し、次に BUILD ルールにいくつかの変更を加えるだけです。この統合プロセスの完全な例を以下に示します (コードはここにあります)。

以下に示すように、SimpleDelegate API を実装するデリゲートと、この「ダミー」デリゲートを作成/削除する外部「C」API があると仮定します。

// Returns default options for DummyDelegate.
DummyDelegateOptions TfLiteDummyDelegateOptionsDefault();

// Creates a new delegate instance that need to be destroyed with
// `TfLiteDummyDelegateDelete` when delegate is no longer used by TFLite.
// When `options` is set to `nullptr`, the above default values are used:
TfLiteDelegate* TfLiteDummyDelegateCreate(const DummyDelegateOptions* options);

// Destroys a delegate created with `TfLiteDummyDelegateCreate` call.
void TfLiteDummyDelegateDelete(TfLiteDelegate* delegate);

「DummyDelegate」をベンチマークツールや推論ツールと統合するには、以下のようにDelegateProviderを定義します。

class DummyDelegateProvider : public DelegateProvider {
 public:
  DummyDelegateProvider() {
    default_params_.AddParam("use_dummy_delegate",
                             ToolParam::Create<bool>(false));
  }

  std::vector<Flag> CreateFlags(ToolParams* params) const final;

  void LogParams(const ToolParams& params) const final;

  TfLiteDelegatePtr CreateTfLiteDelegate(const ToolParams& params) const final;

  std::string GetName() const final { return "DummyDelegate"; }
};
REGISTER_DELEGATE_PROVIDER(DummyDelegateProvider);

std::vector<Flag> DummyDelegateProvider::CreateFlags(ToolParams* params) const {
  std::vector<Flag> flags = {CreateFlag<bool>("use_dummy_delegate", params,
                                              "use the dummy delegate.")};
  return flags;
}

void DummyDelegateProvider::LogParams(const ToolParams& params) const {
  TFLITE_LOG(INFO) << "Use dummy test delegate : ["
                   << params.Get<bool>("use_dummy_delegate") << "]";
}

TfLiteDelegatePtr DummyDelegateProvider::CreateTfLiteDelegate(
    const ToolParams& params) const {
  if (params.Get<bool>("use_dummy_delegate")) {
    auto default_options = TfLiteDummyDelegateOptionsDefault();
    return TfLiteDummyDelegateCreateUnique(&default_options);
  }
  return TfLiteDelegatePtr(nullptr, [](TfLiteDelegate*) {});
}

BUILD ルールの定義は、ライブラリが常にリンクされ、オプティマイザーによって削除されないことを確認する必要があるため重要です。

#### The following are for using the dummy test delegate in TFLite tooling ####
cc_library(
    name = "dummy_delegate_provider",
    srcs = ["dummy_delegate_provider.cc"],
    copts = tflite_copts(),
    deps = [
        ":dummy_delegate",
        "//tensorflow/lite/tools/delegates:delegate_provider_hdr",
    ],
    alwayslink = 1, # This is required so the optimizer doesn't optimize the library away.
)

次に、これら 2 つのラッパー ルールを BUILD ファイルに追加して、独自のデリゲートで実行できるベンチマーク ツール、推論ツール、およびその他の評価ツールのバージョンを作成します。

cc_binary(
    name = "benchmark_model_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/benchmark:benchmark_model_main",
    ],
)

cc_binary(
    name = "inference_diff_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/inference_diff:run_eval_lib",
    ],
)

cc_binary(
    name = "imagenet_classification_eval_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/imagenet_image_classification:run_eval_lib",
    ],
)

cc_binary(
    name = "coco_object_detection_eval_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/coco_object_detection:run_eval_lib",
    ],
)

ここで説明されているように、このデリゲート プロバイダーを TFLite カーネル テストにプラグインすることもできます。

オプション 2: 外部デリゲートを活用する

この代替方法では、次に示すように、まず外部デリゲート アダプターexternal_delegate_adaptor.ccを作成します。前述しように、このアプローチはオプション 1 に比べて推奨度がわずかに低いことに注意してください。

TfLiteDelegate* CreateDummyDelegateFromOptions(char** options_keys,
                                               char** options_values,
                                               size_t num_options) {
  DummyDelegateOptions options = TfLiteDummyDelegateOptionsDefault();

  // Parse key-values options to DummyDelegateOptions.
  // You can achieve this by mimicking them as command-line flags.
  std::unique_ptr<const char*> argv =
      std::unique_ptr<const char*>(new const char*[num_options + 1]);
  constexpr char kDummyDelegateParsing[] = "dummy_delegate_parsing";
  argv.get()[0] = kDummyDelegateParsing;

  std::vector<std::string> option_args;
  option_args.reserve(num_options);
  for (int i = 0; i < num_options; ++i) {
    option_args.emplace_back("--");
    option_args.rbegin()->append(options_keys[i]);
    option_args.rbegin()->push_back('=');
    option_args.rbegin()->append(options_values[i]);
    argv.get()[i + 1] = option_args.rbegin()->c_str();
  }

  // Define command-line flags.
  // ...
  std::vector<tflite::Flag> flag_list = {
      tflite::Flag::CreateFlag(...),
      ...,
      tflite::Flag::CreateFlag(...),
  };

  int argc = num_options + 1;
  if (!tflite::Flags::Parse(&argc, argv.get(), flag_list)) {
    return nullptr;
  }

  return TfLiteDummyDelegateCreate(&options);
}

#ifdef __cplusplus
extern "C" {
#endif  // __cplusplus

// Defines two symbols that need to be exported to use the TFLite external
// delegate. See tensorflow/lite/delegates/external for details.
TFL_CAPI_EXPORT TfLiteDelegate* tflite_plugin_create_delegate(
    char** options_keys, char** options_values, size_t num_options,
    void (*report_error)(const char*)) {
  return tflite::tools::CreateDummyDelegateFromOptions(
      options_keys, options_values, num_options);
}

TFL_CAPI_EXPORT void tflite_plugin_destroy_delegate(TfLiteDelegate* delegate) {
  TfLiteDummyDelegateDelete(delegate);
}

#ifdef __cplusplus
}
#endif  // __cplusplus

次に、以下に示すように、動的ライブラリをビルドするための対応する BUILD ターゲットを作成します。

cc_binary(
    name = "dummy_external_delegate.so",
    srcs = [
        "external_delegate_adaptor.cc",
    ],
    linkshared = 1,
    linkstatic = 1,
    deps = [
        ":dummy_delegate",
        "//tensorflow/lite/c:common",
        "//tensorflow/lite/tools:command_line_flags",
        "//tensorflow/lite/tools:logging",
    ],
)

この外部デリゲート .so ファイルが作成された後、バイナリがここで説明されているコマンド ライン フラグをサポートするexternal_delegate_providerライブラリにリンクされている限り、バイナリをビルドするか、事前にビルドされたバイナリを使用して新しいデリゲートで実行できます。注: この外部デリゲート プロバイダーは、既存のテストおよびツール バイナリにすでにリンクされています。

この外部デリゲートのアプローチを介してダミー デリゲートのベンチマークを行う方法の図については、ここの説明を参照してください。前述のテストおよび評価ツールにも同様のコマンドを使用できます。

ここで示すように、外部デリゲートは、Tensorflow Lite Python バインディングにおけるデリゲートの対応する C++ 実装であることに注意してください。したがって、ここで作成した動的外部デリゲート アダプター ライブラリは、Tensorflow Lite Python API で直接使用できます。

リソース

OSアーチBINARY_NAME
Linux x86_64
aarch64
アンドロイド
aarch64