Объединение операций TensorFlow

Обзор

На этой странице описывается структура и шаги, необходимые для преобразования составных операций TensorFlow в объединённые операции LiteRT. Эта инфраструктура является универсальной и поддерживает преобразование любой составной операции TensorFlow в соответствующую объединённую операцию LiteRT.

Примером использования этой инфраструктуры является слияние операций TensorFlow RNN с LiteRT, как подробно описано здесь .

Что такое объединенные операции?

рисунок

Операции TensorFlow могут быть примитивными операциями, например, tf.add , или составлены из других примитивных операций, например, tf.einsum . Примитивная операция отображается как отдельный узел в графе TensorFlow, а составная операция — как набор узлов в графе TensorFlow. Выполнение составной операции эквивалентно выполнению каждой из входящих в неё примитивных операций.

Объединенная операция соответствует одной операции, которая включает все вычисления, выполняемые каждой примитивной операцией, в соответствующую составную операцию.

Преимущества объединенных операций

Объединенные операции предназначены для максимального повышения производительности базовых реализаций ядра за счет оптимизации общих вычислений и сокращения потребления памяти. Это очень ценно, особенно для задач логического вывода с низкой задержкой и мобильных платформ с ограниченными ресурсами.

Объединенные операции также предоставляют интерфейс более высокого уровня для определения сложных преобразований, таких как квантование, которые в противном случае было бы невозможно или очень сложно реализовать на более детальном уровне.

В LiteRT существует множество примеров объединённых операций по причинам, изложенным выше. Эти объединённые операции обычно соответствуют составным операциям в исходной программе TensorFlow. Примеры составных операций в TensorFlow, реализованных в LiteRT как одна объединённая операция, включают различные операции RNN, такие как однонаправленные и двунаправленные последовательности LSTM, свёртки (conv2d, bias add, relu), полносвязные (matmul, bias add, relu) и другие. В LiteRT квантование LSTM в настоящее время реализовано только в объединённых операциях LSTM.

Проблемы с объединенными операциями

Преобразование составных операций из TensorFlow в объединённые операции в LiteRT — сложная задача. Это связано со следующими причинами:

  1. Составные операции представлены в графе TensorFlow как набор примитивных операций без чётко определённых границ. Определить (например, с помощью сопоставления с образцом) подграф, соответствующий такой составной операции, может быть очень сложно.

  2. Может существовать несколько реализаций TensorFlow, ориентированных на объединённую операцию LiteRT. Например, в TensorFlow существует множество реализаций LSTM (Keras, Babelfish/lingvo и т. д.), и каждая из них состоит из различных примитивных операций, но все они могут быть преобразованы в одну и ту же объединённую операцию LSTM в LiteRT.

Таким образом, преобразование объединенных операций оказалось весьма сложной задачей.

Оберните составную операцию в tf.function

Во многих случаях часть модели можно сопоставить с одной операцией в TFLite. Это может повысить производительность при написании оптимизированной реализации для конкретных операций. Чтобы создать объединённую операцию в TFLite, определите часть графа, представляющую объединённую операцию, и оберните её в функцию tf.function с атрибутом "experimental_implements" в функцию tf.function , имеющую значение атрибута tfl_fusable_op со значением true . Если пользовательская операция принимает атрибуты, передайте их как часть того же "experimental_implements".

Пример,

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 Interpreter

Реализуйте объединенную операцию как пользовательскую операцию TFLite — см. инструкции .

Обратите внимание, что имя, под которым регистрируется операция, должно быть похоже на имя, указанное в атрибуте name в сигнатуре implements.

Пример для операции в примере:

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

Переход от композитной к слитной операции (расширенная)

Общая архитектура преобразования составных операций TensorFlow в объединенные операции LiteRT представлена ​​ниже:

рисунок

Оберните составную операцию в tf.function

В исходном коде модели TensorFlow определите и абстрагируйте составную операцию в функцию tf.function с помощью аннотации experimental_implements . См. пример встраивания lookup . Функция определяет интерфейс, а её аргументы должны использоваться для реализации логики преобразования.

Написать код преобразования

Код преобразования написан на основе интерфейса функции с аннотацией implements . См. пример слияния для поиска встраивания . Концептуально, код преобразования заменяет составную реализацию этого интерфейса на слитую.

В проходе prepare-composite-functions подключите код преобразования .

В более сложных случаях можно реализовать сложные преобразования операндов составной операции для получения операндов объединённой операции. См. пример кода преобразования Keras LSTM .

Конвертировать в LiteRT

Используйте API TFLiteConverter.from_saved_model для конвертации в LiteRT.

Под капотом

Теперь мы опишем общие детали общей конструкции при переходе к объединенным операциям в 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._implements, на основе значения атрибута вызывает соответствующую утилиту слияния операций.
  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 */
}

Ниже представлен фрагмент кода, демонстрирующий сопоставление этой составной операции с объединенной операцией в LiteRT, использующей функцию как интерфейс преобразования.

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