TensorFlow 작업 퓨전

개요

이 페이지에서는 복합 작업을 변환하는 데 필요한 설계와 단계를 설명합니다. LiteRT의 융합된 오퍼레이션으로 변환했습니다. 이러한 인프라는 범용이며 TensorFlow의 모든 복합 연산 변환을 지원합니다. LiteRT에서 해당하는 통합 작업으로 전송됩니다.

이 인프라의 사용 예로는 LiteRT(여기에 자세히 알아보기)

통합 작업이란 무엇인가요?

그림

TensorFlow 작업은 tf.add 또는 다른 원시 연산(예: tf.einsum을 사용합니다. 프리미티브 연산은 TensorFlow 그래프에서 단일 노드로 표시되는 반면 TensorFlow 그래프의 노드 모음입니다. 실행하기 복합 작업은 각 요소를 구성하는 프리미티브 작업을 수행할 수 있습니다

융합된 작업은 단일 작업에 상응하며 각 프리미티브 연산에서 수행된 계산을 할 수 있습니다

통합 작업의 이점

융합된 작업은 기본 커널의 성능을 극대화하기 위해 존재합니다. 메모리 사용을 줄이고 전체 연산을 최적화하고 가능성이 큽니다. 이는 특히 지연 시간이 짧은 추론 워크로드에 유용합니다. 리소스가 제한된 모바일 플랫폼입니다.

또한 통합 작업은 복잡한 작업을 정의하는 상위 수준 인터페이스를 이러한 변환은 실행 불가능하거나 보다 세부적인 수준에서는 하기가 어렵습니다.

LiteRT에는 다음과 같은 이유로 통합 작업 인스턴스가 많습니다. 설명하겠습니다. 이러한 통합 작업은 일반적으로 작업을 실행할 수 있습니다. 다음에서의 복합 작업의 예: LiteRT에서 단일 통합 작업으로 구현되는 TensorFlow 단방향 및 양방향 시퀀스와 같은 다양한 RNN 작업을 포함합니다. LSTM, 컨볼루션 (conv2d, bias add, relu), 완전 연결 (matmul, bias add, relu) 등이 있습니다. LiteRT에서 LSTM 양자화는 현재 Fused LSTM 작업에 구현되어 있습니다.

융합된 운영 문제

복합 작업을 TensorFlow에서 LiteRT는 매우 어려운 문제입니다. 이유는 다음과 같습니다.

  1. 복합 작업은 TensorFlow 그래프에서 원시 연산을 수행할 때 매우 효율적입니다 그것은 매우 하위 그래프를 식별하기 어려운 경우 (예: 패턴 일치를 통해) 해당하는 리소스입니다.

  2. 융합된 LiteRT 연산입니다. 예를 들어 LSTM 구현은 TensorFlow (Keras, Babelfish/lingvo 등)에서 실행되며, 이들은 각각 다른 원시 연산을 사용하지만 여전히 모든 원시 연산을 동일한 융합 LSTM 작업을 수행했습니다.

따라서 융합된 작업의 변환은 매우 어려운 것으로 입증되었습니다.

tf.function에서 복합 작업 래핑

많은 경우 모델의 일부는 TFLite. 이렇게 하면 최적화된 구현을 작성할 때 성능을 향상할 수 있습니다. 사용할 수 있습니다 TFLite에서 통합 작업을 생성하려면 그래프에서 융합된 연산을 나타내는 부분을 식별하고 이를 tf.function 'experimental_implementations' 속성을 tf.function로 설정합니다. 값이 truetfl_fusable_op 값 커스텀 작업이 속성을 사용하여 동일한 'experimental_implementations'의 일부로 전달합니다.

예:

def get_implements_signature():
  implements_signature = [
    # 'name' will be used as a name for the operation.
    'name: "my_custom_fused_op"',
    # attr "tfl_fusable_op" is required to be set with true value.
    'attr {key: "tfl_fusable_op" value { b: true } }',
    # Example attribute "example_option" that the op accepts.
    'attr {key: "example_option" value { i: %d } }' % 10
  ]
  return ' '.join(implements_signature)

@tf.function(experimental_implements=get_implements_signature())
def my_custom_fused_op(input_1, input_2):
  # An empty function that represents pre/post processing example that
  # is not represented as part of the Tensorflow graph.
  output_1 = tf.constant(0.0, dtype=tf.float32, name='first_output')
  output_2 = tf.constant(0.0, dtype=tf.float32, name='second_output')
  return output_1, output_2

class TestModel(tf.Module):
  def __init__(self):
    super(TestModel, self).__init__()
    self.conv_1 = tf.keras.layers.Conv2D(filters=1, kernel_size=(3, 3))
    self.conv_2 = tf.keras.layers.Conv2D(filters=1, kernel_size=(3, 3))

  @tf.function(input_signature=[
      tf.TensorSpec(shape=[1, 28, 28, 3], dtype=tf.float32),
      tf.TensorSpec(shape=[1, 28, 28, 3], dtype=tf.float32),
  ])
  def simple_eval(self, input_a, input_b):
    return my_custom_fused_op(self.conv_1(input_a), self.conv_2(input_b))

변환기에서 allow_custom_ops를 다음과 같이 설정할 필요는 없습니다. tfl_fusable_op 속성은 이미 이를 암시합니다.

맞춤 작업 구현 및 TFLite 인터프리터로 등록

통합 작업을 TFLite 맞춤 작업으로 구현합니다. 안내를 따르세요.

작업을 등록할 이름은 다음 이름과 비슷해야 합니다. 구현 서명의 name 속성에 지정됩니다.

이 예시에서 연산의 예는 다음과 같습니다.

  TfLiteRegistration reg = {};
  // This name must match the name specified in the implements signature.
  static constexpr char kOpName[] = "my_custom_fused_op";
  reg.custom_name = kOpName;
  reg.prepare = [](TfLiteContext* context, TfLiteNode* node) -> TfLiteStatus {
    // Add your code.
    return kTfLiteOk;
  };
  reg.invoke = [](TfLiteContext* context, TfLiteNode* node) -> TfLiteStatus {
    // Add your code.
    return kTfLiteOk;
  };
  reg.builtin_code = kTfLiteCustom;
  resolver->AddCustom(kOpName, &reg);

복합 작업에서 통합 작업으로 변환 (Advanced)

TensorFlow 복합 작업을 LiteRT 통합 작업은 다음과 같습니다.

그림

tf.function에서 복합 작업 래핑

TensorFlow 모델 소스 코드에서 복합 모델을 식별하고 추상화합니다. 작업을 tf.function experimental_implements 함수 주석이 필요합니다. 임베딩 조회의 예를 참조하세요. 이 함수는 인터페이스를 정의하며 그 인수는 변환 로직을 사용할 수 있습니다.

전환 코드 작성

변환 코드는 implements 주석 임베딩을 위한 융합 예시 참조 조회를 참조하세요. 개념적으로 전환 코드가 복합 방문 코드를 대체합니다. 이 인터페이스를 융합된 인터페이스와 함께 구현해 보겠습니다.

준비-복합-함수 전달에서 전환 코드를 찾습니다.

고급 사용법에서는 융합 연산의 피연산자를 얻기 위한 복합 연산의 피연산자 연산으로 해석됩니다. 자세한 내용은 Keras를 LSTM. 전환 코드를 예로 들어 보겠습니다.

LiteRT로 변환

사용 TFLiteConverter.from_saved_model API를 사용하여 LiteRT로 변환할 수 있습니다

자세히 들여다보기

이제 융합된 형식으로 변환하는 전체 설계에 대해 개략적으로 설명합니다. 사용할 수 있습니다

TensorFlow에서 연산 작성

tf.function 사용 다음 코드로 교체합니다. experimental_implements 함수 속성을 사용하면 사용자가 명시적으로 새 연산을 작성할 수 있습니다. TensorFlow 기본 작업을 실행하고 결과 사용됩니다. 이는 다음과 같은 점에서 매우 유용합니다.

  1. 기본 애플리케이션의 복합 작업에 대해 잘 정의된 경계 TensorFlow 그래프
  2. 이 작업에서 구현하는 인터페이스를 명시적으로 지정합니다. 이 인수 tf.function 이 인터페이스의 인수에 해당합니다.

한 가지 예로, 모델 아키텍처의 구현을 위해 정의된 복합 작업을 임베딩 조회가 포함됩니다. 이는 LiteRT의 통합 작업에 매핑됩니다.

  @tf.function(
        experimental_implements="embedding_lookup")
    def EmbFprop(embs, ids_vec):
      """Embedding forward prop.

      Effectively, it computes:
        num = size of ids_vec
        rets = zeros([num, embedding dim])
        for i in range(num):
          rets[i, :] = embs[ids_vec[i], :]
        return rets

      Args:
        embs: The embedding matrix.
        ids_vec: A vector of int32 embedding ids.

      Returns:
        The result of embedding lookups. A matrix of shape
        [num ids in ids_vec, embedding dims].
      """
      num = tf.shape(ids_vec)[0]
      rets = inplace_ops.empty([num] + emb_shape_suf, py_utils.FPropDtype(p))

      def EmbFpropLoop(i, embs, ids_vec, rets):
        # row_id = ids_vec[i]
        row_id = tf.gather(ids_vec, i)
        # row = embs[row_id]
        row = tf.reshape(tf.gather(embs, row_id), [1] + emb_shape_suf)
        # rets[i] = row
        rets = inplace_ops.alias_inplace_update(rets, [i], row)
        return embs, ids_vec, rets

      _, _, rets = functional_ops.For(
          start=0,
          limit=num,
          delta=1,
          inputs=[embs, ids_vec, rets],
          body=EmbFpropLoop,
          rewrite_with_while=compiled)
      if len(weight_shape) > 2:
        rets = tf.reshape(rets, [num, symbolic.ToStatic(p.embedding_dim)])
      return rets

모델이 복합 작업을 사용하도록 함 tf.function 일반적인 인프라를 구축하여 이러한 작업을 식별하고 통합된 LiteRT 작업으로 변환합니다.

LiteRT 변환기 확장

올해 초 출시된 LiteRT 변환기는 모든 변수가 그래프로 대체된 그래프로 TensorFlow 모델을 가져옵니다. 나타냅니다. 작업 융합에는 작동하지 않습니다. 이러한 그래프에는 모든 함수가 인라인 처리되어 변수가 상수입니다.

이러한 tf.function 변환 과정에서 experimental_implements 특성을 가져오는 경우 보존해야 합니다.

이에 따라 TensorFlow를 가져오고 변환하는 새로운 워크플로를 구현했습니다. 복합 연산 퓨전 사용 사례를 지원하도록 변환기의 구체적으로, 새로 추가된 기능은 다음과 같습니다.

  1. TensorFlow로 저장된 모델을 MLIR로 가져오기
  2. 복합 작업 융합
  3. 변수 변경 가능성 분석
  4. 읽기 전용 변수 모두 고정

이를 통해 복합 작업을 수행하는 방법을 학습했습니다.

작업 융합 구현

연산 퓨전 패스를 더 자세히 살펴보겠습니다. 이 패스는 있습니다.

  1. MLIR 모듈의 모든 함수를 반복합니다.
  2. 함수에 tf._implementations 속성이 있는 경우 값이면 적절한 작업 fusion 유틸리티를 호출합니다.
  3. 연산 퓨전 유틸리티는 함수의 피연산자에 대해 작동하며 속성 (전환 인터페이스 역할을 함)을 사용하고 다음과 같은 동일한 함수 본문이 있는 함수의 본문입니다. 일으킬 수 있습니다.
  4. 대부분의 경우 교체된 본문에는 일으킬 수 있습니다. 이는 함수의 피연산자를 대체합니다. 이러한 계산은 모두 일정한 방식으로 접을 수 있으므로 내보낸 플랫 버퍼에 존재하며 여기서 융합된 작업만 존재해야 합니다.

다음은 기본 워크플로를 보여주는 패스의 코드 스니펫입니다.

void PrepareCompositeFunctionsPass::ConvertTFImplements(FuncOp func,
                                                        StringAttr attr) {
  if (attr.getValue() == "embedding_lookup") {
    func.eraseBody();
    func.addEntryBlock();
    // Convert the composite embedding_lookup function body to a
    // TFLite fused embedding_lookup op.
    ConvertEmbeddedLookupFunc convert_embedded_lookup(func);
    if (failed(convert_embedded_lookup.VerifySignature())) {
      return signalPassFailure();
    }
    convert_embedded_lookup.RewriteFunc();
  } else if (attr.getValue() == mlir::TFL::kKerasLstm) {
     func.eraseBody();
     func.addEntryBlock();
     OpBuilder builder(func.getBody());
     if (failed(ConvertKerasLSTMLayer(func, &builder))) {
       return signalPassFailure();
     }
  } else if (.....) /* Other fusions can plug in here */
}

다음은 이 복합 작업을 융합된 변환 인터페이스로 사용할 수 있습니다.

void RewriteFunc() {
    Value lookup = func_.getArgument(1);
    Value value = func_.getArgument(0);
    auto output_type = func_.getType().getResult(0);

    OpBuilder builder(func_.getBody());
    auto op = builder.create<mlir::TFL::EmbeddingLookupOp>(
        func_.getLoc(), output_type, lookup, value);

    builder.create<mlir::ReturnOp>(func_.getLoc(), op.getResult());
  }