맞춤 위임 구현

TensorFlow Lite 대리자를 사용하면 다른 실행자에서 모델의 일부 또는 전체를 실행할 수 있습니다. 이 메커니즘은 추론에 GPU나 Edge TPU (Tensor Processing Unit)와 같은 다양한 기기 내 가속기를 활용할 수 있습니다. 이를 통해 개발자는 기본 TFLite에서 유연하고 분리된 메서드를 통해 추론 속도를 높일 수 있습니다.

아래 다이어그램에는 대리인이 요약되어 있습니다. 자세한 내용은 아래 섹션을 참고하세요.

TFLite 위임

맞춤 위임은 언제 만들어야 하나요?

TensorFlow Lite에는 GPU, DSP, EdgeTPU와 같은 대상 가속기를 위한 다양한 위임이 있습니다.

다음과 같은 상황에서 자체 위임을 만들면 유용합니다.

  • 기존 대리자가 지원하지 않는 새 ML 추론 엔진을 통합하려고 합니다.
  • 알려진 시나리오의 런타임을 개선하는 커스텀 하드웨어 가속기가 있습니다.
  • 특정 모델의 속도를 높일 수 있는 CPU 최적화 (예: 연산자 융합)를 개발하고 있습니다.

대리인 작동 방식

다음과 같은 간단한 모델 그래프와 Conv2D 및 평균 작업을 더 빠르게 구현하는 대리자 'MyDelegate'를 생각해 보세요.

원본 그래프

이 'MyDelegate'를 적용하면 원래 TensorFlow Lite 그래프가 다음과 같이 업데이트됩니다.

대리자를 사용한 그래프

위 그래프는 TensorFlow Lite가 두 규칙에 따라 원래 그래프를 분할할 때 얻은 것입니다.

  • 대리자가 처리할 수 있는 특정 작업은 파티션에 배치되는 동시에 작업 간의 원래 컴퓨팅 워크플로 종속 항목을 충족합니다.
  • 위임할 각 파티션에는 대리자가 처리하지 않는 입력 노드와 출력 노드만 있습니다.

대리자에 의해 처리되는 각 파티션은 호출 호출 시 파티션을 평가하는 원래 그래프에서 대리자 노드로 대체됩니다 (대리자 커널이라고도 함).

모델에 따라 마지막 그래프는 하나 이상의 노드로 끝날 수 있습니다. 후자는 일부 작업이 대리자에서 지원되지 않음을 의미합니다. 일반적으로 대리자에서 기본 그래프로 전환할 때마다 메모리 사본으로 인해 발생하는 (예: GPU에서 CPU로) 위임 하위 그래프의 결과를 기본 그래프로 전달하는 데 오버헤드가 발생하기 때문에 대리자가 여러 개의 파티션을 처리하는 것은 바람직하지 않습니다. 이러한 오버헤드는 특히 메모리 사본이 많은 경우 성능 향상을 상쇄할 수 있습니다.

자체 맞춤 위임 구현

대리자를 추가하는 데 선호되는 방법은 SimpleDelegate API를 사용하는 것입니다.

새 대리자를 만들려면 인터페이스 2개를 구현하고 인터페이스 메서드의 자체 구현을 제공해야 합니다.

1~SimpleDelegateInterface

이 클래스는 대리자의 기능, 지원되는 작업 및 위임 그래프를 캡슐화하는 커널을 만들기 위한 팩토리 클래스를 나타냅니다. 자세한 내용은 이 C++ 헤더 파일에 정의된 인터페이스를 참고하세요. 코드의 주석은 각 API를 자세히 설명합니다.

2~SimpleDelegateKernelInterface

이 클래스는 위임된 파티션을 초기화 / 준비 / 실행하는 로직을 캡슐화합니다.

다음이 포함됩니다(정의 참고).

  • Init(...): 일회성 초기화를 수행하기 위해 한 번 호출됩니다.
  • 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 모델을 사용하여 무작위 입력을 생성한 다음 지정된 횟수의 실행 동안 모델을 반복적으로 실행합니다. 마지막에 집계된 지연 시간 통계가 출력됩니다.
  • 추론 차이 도구: 이 도구는 특정 모델에서 무작위 가우시안 데이터를 생성하여 서로 다른 두 개의 TFLite 인터프리터를 통해 전달합니다. 하나는 단일 스레드 CPU 커널을 실행하고 다른 하나는 사용자 정의 사양을 사용합니다. 이는 요소별로 각 인터프리터의 출력 텐서 간의 절대차를 측정합니다. 이 도구는 정확도 문제를 디버깅하는 데도 도움이 될 수 있습니다.
  • 이미지 분류 및 객체 감지를 위한 태스크별 평가 도구도 있습니다. 이러한 도구는 여기에서 찾을 수 있습니다.

또한 TFLite에는 더 많은 적용 범위로 새 대리자를 테스트하고 일반 TFLite 실행 경로가 중단되지 않도록 하는 데 재사용될 수 있는 대규모 커널 및 작업 단위 테스트 세트가 있습니다.

새 대리자에 TFLite 테스트와 도구를 재사용하려면 다음 두 가지 옵션 중 하나를 사용하면 됩니다.

최적의 접근 방식 선택하기

두 방법 모두 아래에 설명된 대로 몇 가지를 변경해야 합니다. 그러나 첫 번째 방법은 대리자를 정적으로 연결하므로 테스트, 벤치마킹, 평가 도구를 다시 빌드해야 합니다. 반대로 두 번째는 대리자를 공유 라이브러리로 만들고 공유 라이브러리에서 생성/삭제 메서드를 노출하도록 요구합니다.

따라서 외부 위임 메커니즘은 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.
)

이제 이러한 두 래퍼 규칙을 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
arm
aarch64
Android arm
aarch64