Один тензор, много возможностей: инженерные пользовательские операции в Tensorflow

Один тензор, много возможностей: инженерные пользовательские операции в Tensorflow

25 июля 2025 г.

Обзор контента

  • Встройте расширенные функции в свой OP
  • Условные проверки и проверка
  • Регистрация OP
  • Поддержка графического процессора
  • Реализуйте градиент в Python
  • Функции формы в C ++
  • Создайте пакет PIP для пользовательского OP

Встройте расширенные функции в свой OP

Теперь, когда вы знаете, как построить базовую (и несколько ограниченную) OP и реализацию, мы рассмотрим некоторые из более сложных вещей, которые вам обычно понадобятся, чтобы встроить в свой OP. Это включает в себя:

  • Условные проверки и проверка
  • ОП регистрация
    • Атрис
    • АТРТ типы
    • Полиморфизм
    • Входы и выходы
    • Обратная совместимость
  • Поддержка графического процессора
    • Скомпилирование ядра для устройства GPU
  • Реализуйте градиент в Python
  • Функции формы в C ++

Условные проверки и проверка

Приведенный выше пример предполагал, что OP применяется к тензору любой формы. Что если это применимо только к векторам? Это означает добавление проверки в вышеуказанную реализацию Opkernel.

void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);

    OP_REQUIRES(context, TensorShapeUtils::IsVector(input_tensor.shape()),
                errors::InvalidArgument("ZeroOut expects a 1-D vector."));
    // ...
  }

Это утверждает, что ввод является вектором, и возвращает, установивInvalidArgumentСтатус, если это не так. АOP_REQUIRESМакро принимает три аргумента:

  • Аcontext, что может бытьOpKernelContextилиOpKernelConstructionУказатель (смtensorflow/core/framework/op_kernel.h), для егоSetStatus()метод
  • Состояние. Например, существуют функции для проверки формы тензора вtensorflow/core/framework/tensor_shape.h
  • Сама ошибка, которая представленаStatusобъект, смtensorflow/core/platform/status.hПолем АStatusимеет оба типа (частоInvalidArgument, но см. Список типов) и сообщение. Функции для построения ошибки могут быть найдены вtensorflow/core/platform/errors.hПолем

В качестве альтернативы, если вы хотите проверить, есть лиStatusОбъект, возвращаемый с какой -то функции, является ошибкой, и если это вернуть, используйтеOP_REQUIRES_OKПолем Оба эти макроса возвращаются из функции по ошибке.

ОП регистрация

Атрис

OPS может иметь ATRS, значения которых устанавливаются, когда OP добавляется на график. Они используются для настройки OP, и их значения могут быть доступны как в реализации ядра, так и в типах входов и выходов в регистрации OP. Предпочитаю использовать вход вместо ATTR, когда это возможно, поскольку входные данные более гибки. Это связано с тем, что ATRS являются постоянными и должны быть определены во время построения графика. Напротив, входные данные являются тензорами, значения которых могут быть динамическими; То есть входы могут изменять каждый шаг, быть установленным с помощью подачи и т. Д. Атрицы используются для вещей, которые не могут быть сделаны с помощью входов: любая конфигурация, которая влияет на подпись (число или тип входов или выходов) или не может измениться от шага к шагу.

Вы определяете атрис при регистрации OP, указав его имя и введите, используяAttrМетод, который ожидает спецификации формы:

<name>: <attr-type-expr>

где<name>начинается с буквы и может состоять из буквенно -цифровых символов и недостатков, и<attr-type-expr>тип выражения формыописано нижеПолем

Например, если вам нравитсяZeroOutOP, чтобы сохранить указанный пользователем индекс, а не только 0-й элемент, вы можете зарегистрировать OP, как SO:

REGISTER_OP("ZeroOut")
    .Attr("preserve_index: int")
    .Input("to_zero: int32")
    .Output("zeroed: int32");

(Обратите внимание, что наборТипы атрибутовотличается отtf.DTypeиспользуется для входов и выходов.)

Ваше ядро может затем получить доступ к этому ATTR в своем конструкторе черезcontextПараметр:

class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {
    // Get the index of the value to preserve
    OP_REQUIRES_OK(context,
                   context->GetAttr("preserve_index", &preserve_index_));
    // Check that preserve_index is positive
    OP_REQUIRES(context, preserve_index_ >= 0,
                errors::InvalidArgument("Need preserve_index >= 0, got ",
                                        preserve_index_));
  }
  void Compute(OpKernelContext* context) override {
    // ...
  }
 private:
  int preserve_index_;
};

который затем можно использовать вComputeМетод:

void Compute(OpKernelContext* context) override {
    // ...

    // We're using saved attr to validate potentially dynamic input
    // So we check that preserve_index is in range
    OP_REQUIRES(context, preserve_index_ < input.dimension(0),
                errors::InvalidArgument("preserve_index out of range"));

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the requested input value
    output_flat(preserve_index_) = input(preserve_index_);
  }

АТРТ типы

Следующие типы поддерживаются в ATTR:

  • string: Любая последовательность байтов (не требуется UTF8).
  • int: Подписанное целое число.
  • float: Номер плавающей запятой.
  • bool: True или ложь.
  • type: Одно из (не-реф-) значенийDataTypeПолем
  • shape: АTensorShapeProtoПолем
  • list(<type>): Список<type>, где<type>является одним из вышеперечисленных типов. Обратите внимание, чтоlist(list(<type>))недействителен.

Смотрите также:op_def_builder.cc:FinalizeAttrдля окончательного списка.

Значения и ограничения по умолчанию

ATTR могут иметь значения по умолчанию, и некоторые типы ATTR могут иметь ограничения. Чтобы определить ATTR с ограничениями, вы можете использовать следующее<attr-type-expr>S:

{'<string1>', '<string2>'}: Значение должно быть строкой, которая имеет либо значение<string1>или<string2>Полем Название типа,string, подразумевается, когда вы используете этот синтаксис. Это эмулирует перечисление:

REGISTER_OP("EnumExample")
    .Attr("e: {'apple', 'orange'}");

{<type1>, <type2>}: Значение типаtypeи должен быть одним из<type1>или<type2>, где<type1>и<type2>поддерживаютсяtf.DTypeПолем Вы не указываете, что тип атристаtypeПолем Это подразумевается, когда у вас есть список типов в{...}Полем Например, в этом случае ATTRtэто тип, который должен бытьint32, аfloat, илиbool:

REGISTER_OP("RestrictedTypeExample")
    .Attr("t: {int32, float, bool}");

Есть ярлыки для ограничений общего типа:

  • numbertype: Типtypeограничен численными (не строгими и небелыми) типами.
  • realnumbertype: Нравитьсяnumbertypeбез сложных типов.
  • quantizedtype: НравитьсяnumbertypeНо только квантованные типы числа.

Конкретные списки типов, разрешенные этимиNumberTypes()) вtensorflow/core/framework/types.hПолем В этом примере ATTRtДолжен быть один из числовых типов:

REGISTER_OP("NumberType")
    .Attr("t: numbertype");

Для этого OP:

tf.number_type(t=tf.int32)  # Valid
tf.number_type(t=tf.bool)   # Invalid

Списки могут быть объединены с другими списками и отдельными типами. Следующий OP позволяет ATTRtбыть любым из цифровых типов или типа Bool:

REGISTER_OP("NumberOrBooleanType")
    .Attr("t: {numbertype, bool}");

Для этого OP:

tf.number_or_boolean_type(t=tf.int32)  # Valid
tf.number_or_boolean_type(t=tf.bool)   # Valid
tf.number_or_boolean_type(t=tf.string) # Invalid

int >= <n>: Значение должно быть int, значение которого больше или равно<n>, где<n>это естественное число. Например, следующая регистрация OP указывает, что ATTRaДолжен иметь значение, по крайней мере,2:

REGISTER_OP("MinIntExample")
    .Attr("a: int >= 2");

list(<type>) >= <n>: Список типа<type>чья длина больше или равна<n>Полем Например, следующая регистрация OP указывает, что ATTRaэто список типов (либоint32илиfloat), и что их должно быть не менее 3:

REGISTER_OP("TypeListExample")
    .Attr("a: list({int32, float}) >= 3");

Чтобы установить значение по умолчанию для ATTR (делая его необязательным в сгенерированном коде), добавьте= <default>К концу, как в:

REGISTER_OP("AttrDefaultExample")
    .Attr("i: int = 0");

Кроме того, можно указать как ограничение, так и значение по умолчанию:

REGISTER_OP("AttrConstraintAndDefaultExample")
    .Attr("i: int >= 1 = 1");

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

Вот примеры того, как указать дефолт для всех типов:

REGISTER_OP("AttrDefaultExampleForAllTypes")
   .Attr("s: string = 'foo'")
   .Attr("i: int = 0")
   .Attr("f: float = 1.0")
   .Attr("b: bool = true")
   .Attr("ty: type = DT_INT32")
   .Attr("sh: shape = { dim { size: 1 } dim { size: 2 } }")
   .Attr("te: tensor = { dtype: DT_INT32 int_val: 5 }")
   .Attr("l_empty: list(int) = []")
   .Attr("l_int: list(int) = [2, 3, 5, 7]");

Обратите внимание, в частности, что значения типаtypeиспользоватьtf.DTypeПолем

Полиморфизм

Тип полиморфизма

Для OPS, который может принимать разные типы в качестве ввода или создавать разные типы выводов, вы можете указатьаттвВход или выходной типв регистрации OP. Обычно вы затем зарегистрировалиOpKernelдля каждого поддерживаемого типа.

Например, если вы хотитеZeroOutOP для работы надfloatS в дополнение кint32S, ваша регистрация OP может выглядеть как:

REGISTER_OP("ZeroOut")
    .Attr("T: {float, int32}")
    .Input("to_zero: T")
    .Output("zeroed: T");

Ваша регистрация OP теперь указывает, что тип ввода должен бытьfloat, илиint32и что его выход будет одинаковым типом, так как оба имеют типTПолем

Именование

Входные данные, выходы и атрис, как правило, должны давать имена Snake_case. Единственным исключением является атрис, которые используются в качестве типа входа или в типе выхода. Эти атристы могут быть выведены при добавлении OP к графику, поэтому не появляются в функции OP. Например, это последнее определение Zeroout генерирует функцию Python, которая выглядит как:

def zero_out(to_zero, name=None):
  """...
  Args:
    to_zero: A `Tensor`. Must be one of the following types:
        `float32`, `int32`.
    name: A name for the operation (optional).

  Returns:
    A `Tensor`. Has the same type as `to_zero`.
  """

Еслиto_zeroпроходитint32Тенсор, тогдаTавтоматически устанавливается наint32(Ну, на самом делеDT_INT32) Эти предполагаемые ATRS получают капитализированные или названия Camelcase.

Сравните это с OP, который имеет тип ATTR, который определяет тип выхода:

REGISTER_OP("StringToNumber")
    .Input("string_tensor: string")
    .Output("output: out_type")
    .Attr("out_type: {float, int32} = DT_FLOAT");
    .Doc(R"doc(
Converts each string in the input Tensor to the specified numeric type.
)doc");

В этом случае пользователь должен указать тип вывода, как в сгенерированном Python:

def string_to_number(string_tensor, out_type=None, name=None):
  """Converts each string in the input Tensor to the specified numeric type.

  Args:
    string_tensor: A `Tensor` of type `string`.
    out_type: An optional `tf.DType` from: `tf.float32, tf.int32`.
      Defaults to `tf.float32`.
    name: A name for the operation (optional).

  Returns:
    A `Tensor` of type `out_type`.
  """

Пример типа полиморфизма

#include "tensorflow/core/framework/op_kernel.h"

class ZeroOutInt32Op : public OpKernel {
  // as before
};

class ZeroOutFloatOp : public OpKernel {
 public:
  explicit ZeroOutFloatOp(OpKernelConstruction* context)
      : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);
    auto input = input_tensor.flat<float>();

    // Create an output tensor
    Tensor* output = NULL;
    OP_REQUIRES_OK(context,
                   context->allocate_output(0, input_tensor.shape(), &output));
    auto output_flat = output->template flat<float>();

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value
    if (N > 0) output_flat(0) = input(0);
  }
};

// Note that TypeConstraint<int32>("T") means that attr "T" (defined
// in the op registration above) must be "int32" to use this template
// instantiation.
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<int32>("T"),
    ZeroOutInt32Op);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<float>("T"),
    ZeroOutFloatOp);

Чтобы сохранитьобратная совместимость, вам следует указатьзначение по умолчаниюПри добавлении атриста в существующий OP:

REGISTER_OP("ZeroOut")
  .Attr("T: {float, int32} = DT_INT32")
  .Input("to_zero: T")
  .Output("zeroed: T")

Допустим, вы хотите добавить больше типов, скажем,double:

REGISTER_OP("ZeroOut")
    .Attr("T: {float, double, int32}")
    .Input("to_zero: T")
    .Output("zeroed: T");

Вместо того, чтобы писать другойOpKernelС резервированным кодом, как указано выше, часто вы сможете использовать шаблон C ++. У вас все еще будет одна регистрация ядра (REGISTER_KERNEL_BUILDERзвонить) за перегрузку.

template <typename T>
class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);
    auto input = input_tensor.flat<T>();

    // Create an output tensor
    Tensor* output = NULL;
    OP_REQUIRES_OK(context,
                   context->allocate_output(0, input_tensor.shape(), &output));
    auto output_flat = output->template flat<T>();

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value
    if (N > 0) output_flat(0) = input(0);
  }
};

// Note that TypeConstraint<int32>("T") means that attr "T" (defined
// in the op registration above) must be "int32" to use this template
// instantiation.
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<int32>("T"),
    ZeroOutOp<int32>);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<float>("T"),
    ZeroOutOp<float>);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<double>("T"),
    ZeroOutOp<double>);

Если у вас более пары перегрузки, вы можете поместить регистрацию на макрос.

#include "tensorflow/core/framework/op_kernel.h"

#define REGISTER_KERNEL(type)                                       \
  REGISTER_KERNEL_BUILDER(                                          \
      Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
      ZeroOutOp<type>)

REGISTER_KERNEL(int32);
REGISTER_KERNEL(float);
REGISTER_KERNEL(double);

#undef REGISTER_KERNEL

В зависимости от списка типов, для которых вы регистрируете ядро, вы можете использовать макрос, предоставленныйtensorflow/core/framework/register_types.h:

#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/framework/register_types.h"

REGISTER_OP("ZeroOut")
    .Attr("T: realnumbertype")
    .Input("to_zero: T")
    .Output("zeroed: T");

template <typename T>
class ZeroOutOp : public OpKernel { ... };

#define REGISTER_KERNEL(type)                                       \
  REGISTER_KERNEL_BUILDER(                                          \
      Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
      ZeroOutOp<type>)

TF_CALL_REAL_NUMBER_TYPES(REGISTER_KERNEL);

#undef REGISTER_KERNEL

Список входов и выходов

В дополнение к возможности принимать или производить разные типы, OPS может потреблять или производить переменное количество тензоров.

В следующем примере аттроTдержитсписоктипов и используется как тип обоих вводовinи выводoutПолем Ввод и вывод - это списки тензоров такого типа (а число и типы тензоров на выходе совпадают с входом, поскольку оба имеют типT)

REGISTER_OP("PolymorphicListExample")
    .Attr("T: list(type)")
    .Input("in: T")
    .Output("out: T");

Вы также можете наложить ограничения на то, какие типы могут быть указаны в списке. В этом следующем случае ввод - списокfloatиdoubleтензоры. ОП принимает, например, типы вводов(float, double, float)и в этом случае выход вывода также будет(float, double, float)Полем

REGISTER_OP("ListTypeRestrictionExample")
    .Attr("T: list({float, double})")
    .Input("in: T")
    .Output("out: T");

Если вы хотите, чтобы все тензоры в списке были одного типа, вы можете сделать что -то вроде:

REGISTER_OP("IntListInputExample")
    .Attr("N: int")
    .Input("in: N * int32")
    .Output("out: int32");

Это принимает списокint32тензоры и используютintатрисNЧтобы указать длину списка.

Это можно сделатьтип полиморфнойтакже. В следующем примере вход представляет собой список тензоров (с длиной"N") того же (но неуточненного) типа ("T"), и выход - единственный тензор типа соответствующего типа:

REGISTER_OP("SameListInputExample")
    .Attr("N: int")
    .Attr("T: type")
    .Input("in: N * T")
    .Output("out: T");

По умолчанию списки тензоров имеют минимальную длину 1. Вы можете изменить это по умолчанию, используяа">="Ограничение на соответствующем атри. В следующем примере вход является списком не менее 2int32Тенсоры:

REGISTER_OP("MinLengthIntListExample")
    .Attr("N: int >= 2")
    .Input("in: N * int32")
    .Output("out: int32");

Тот же синтаксис работает с"list(type)"ATTRS:

REGISTER_OP("MinimumLengthPolymorphicListExample")
    .Attr("T: list(type) >= 3")
    .Input("in: T")
    .Output("out: T");

Входы и выходы

Подводя итог выше, регистрация OP может иметь несколько входов и выходов:

REGISTER_OP("MultipleInsAndOuts")
    .Input("y: int32")
    .Input("z: float")
    .Output("a: string")
    .Output("b: int32");

Каждая входная или выходная спецификация формы:

<name>: <io-type-expr>

где<name>начинается с буквы и может быть составлен из буквенно -цифровых символов и подчеркиваний.<io-type-expr>является одним из следующих выражений типа:

  • <type>, где<type>является поддерживаемым типом ввода (например,floatВint32Вstring) Это указывает один тензор данного типа.

    Видетьtf.DTypeПолем

REGISTER_OP("BuiltInTypesExample")
    .Input("integers: int32")
    .Input("complex_numbers: complex64");
  • <attr-type>, где<attr-type>это имяАтрисс типомtypeилиlist(type)(с возможным ограничением типа). Этот синтаксис позволяетПолиморфный ОпсПолем

REGISTER_OP("PolymorphicSingleInput")
    .Attr("T: type")
    .Input("in: T");

REGISTER_OP("RestrictedPolymorphicSingleInput")
    .Attr("T: {int32, int64}")
    .Input("in: T");

Ссылка на атрис типаlist(type)Позволяет принять последовательность тензоров.

REGISTER_OP("ArbitraryTensorSequenceExample")
    .Attr("T: list(type)")
    .Input("in: T")
    .Output("out: T");

REGISTER_OP("RestrictedTensorSequenceExample")
    .Attr("T: list({int32, int64})")
    .Input("in: T")
    .Output("out: T");

Обратите внимание, что число и типы тензоров в выходеoutтакой же, как и на входеin, поскольку оба типаTПолем

  • Для последовательности тензоров с тем же типом:<number> * <type>, где<number>это имяАтрисс типомintПолем А<type>может быть либоtf.DType, или название аттроса с типомtypeПолем В качестве примера первого, этот OP принимает списокint32Тенсоры:

REGISTER_OP("Int32SequenceExample")
    .Attr("NumTensors: int")
    .Input("in: NumTensors * int32")

Принимая во внимание, что этот OP принимает список тензоров любого типа, если они все одинаковы:

REGISTER_OP("SameTypeSequenceExample")
    .Attr("NumTensors: int")
    .Attr("T: type")
    .Input("in: NumTensors * T")
  • Для ссылки на тензор:Ref(<type>), где<type>является одним из предыдущих типов.

Любой ATTR, используемый в типе ввода, будет выведен. По соглашению, эти предполагаемые атрис используют имена капитала (например,TилиN) В противном случае входные данные, выходы и атрис имеют имена, такие как параметры функции (например,num_outputs) Для получения более подробной информации см.более ранний раздел об названииПолем

Для получения более подробной информации см.tensorflow/core/framework/op_def_builder.hПолем

Обратная совместимость

Давайте предположим, что вы написали хороший, пользовательский OP и поделились им с другими, поэтому у вас есть счастливые клиенты, которые используют вашу работу. Тем не менее, вы хотели бы каким -то образом внести изменения в OP.

В целом, изменения в существующих, проверенных спецификациях должны быть связаны обратно: изменение спецификации OP не должно нарушать предварительные сериализованныеGraphDefБуферы протокола, построенные из более старых спецификаций. ДеталиGraphDefсовместимость естьописано здесьПолем

Есть несколько способов сохранения обратной совместимости.

  1. Любые новые ATRS, добавленные в операцию, должны иметь определенные значения по умолчанию, и с этим значением по умолчанию OP должен иметь исходное поведение. Чтобы изменить операцию с не полиморфной на полиморфную, выдолженДайте значение по умолчанию новому типу ATTR, чтобы по умолчанию сохранить исходную подпись. Например, если ваша операция была:

REGISTER_OP("MyGeneralUnaryOp")
    .Input("in: float")
    .Output("out: float");

Вы можете сделать его полиморфным на обратном порядке, используя:

REGISTER_OP("MyGeneralUnaryOp")
    .Input("in: T")
    .Output("out: T")
    .Attr("T: numerictype = DT_FLOAT");
  1. Вы можете безопасно сделать ограничение на ATTR менее ограничительным. Например, вы можете измениться с{int32, int64}к{int32, int64, float}илиtypeПолем Или вы можете измениться с{"apple", "orange"}к{"apple", "banana", "orange"}илиstringПолем
  2. Вы можете изменить отдельные входы / выходы на входы / выходы списка, если по умолчанию по умолчанию тип списка соответствует старой подписи.
  3. Вы можете добавить новый ввод списка / вывод, если по умолчанию по умолчанию.
  4. Пространство имен любые новые OPS, которые вы создаете, путем префикса имен OP с чем -то уникальным для вашего проекта. Это избегает столкновения с OP с любым OPS, который может быть включен в будущие версии TensorFlow.
  5. Планируйте заранее! Попробуйте предвидеть будущее использование для OP. Некоторые изменения подписи не могут быть сделаны совместимыми способом (например, составление списка одного и того же типа в список различных типов).

Полный список безопасных и небезопасных изменений можно найти вtensorflow/core/framework/op_compatibility_test.ccПолем Если вы не можете внести свои изменения в операцию обратно совместимой, создайте новую операцию с новым именем с новой семантикой.

Также обратите внимание, что хотя эти изменения могут поддерживатьGraphDefСовместимость, сгенерированный код Python может измениться таким образом, чтобы не совместимо со старыми абонентами. Python API может быть совместимы с помощью тщательных изменений в рукописной обертке Python, сохраняя старую подпись, за исключением того, что, возможно, добавляя новые дополнительные аргументы к концу. Как правило, несовместимые изменения могут быть внесены только тогда, когда тензоры меняют основные версии и должны соответствоватьGraphDefВерсия семантика.

Поддержка графического процессора

Вы можете реализовать разные Opkernels и зарегистрировать один для процессора, а другой - для графического процессора, как вы можетеЗарегистрируйте ядра для разных типовПолем Есть несколько примеров ядра с поддержкой графического процессора вtensorflow/core/kernels/Полем Обратите внимание, что у некоторых ядер есть версия процессора в.ccфайл, версия GPU в файле, заканчивающемся в_gpu.cu.ccи какой -то код, который общий общий в.hфайл.

Например,tf.padесть все, кроме ядра графического процессора вtensorflow/core/kernels/pad_op.ccПолем Ядро графического процессора находится вtensorflow/core/kernels/pad_op_gpu.cu.ccи общий код - это шаблонный класс, определенный вtensorflow/core/kernels/pad_op.hПолем Мы организуем код таким образом по двум причинам: он позволяет вам обмениваться общим кодом между реализациями процессора и графического процессора, и он помещает реализацию GPU в отдельный файл, чтобы его можно было собрать только компилятором GPU.

Одна вещь, которую нужно отметить, даже когда версия ядра графического процессораpadиспользуется, он все еще нуждается в его"paddings"Ввод в память процессора. Чтобы отметить, что входы или выходы хранятся на процессоре, добавьтеHostMemory()Призовите регистрацию ядра, например:

#define REGISTER_GPU_KERNEL(T)                         \
  REGISTER_KERNEL_BUILDER(Name("Pad")                  \
                              .Device(DEVICE_GPU)      \
                              .TypeConstraint<T>("T")  \
                              .HostMemory("paddings"), \
                          PadOp<GPUDevice, T>)

Скомпилирование ядра для устройства GPU

Посмотрите наcuda_op_kernel.cu.ccДля примера, который использует ядро CUDA для реализации OP. Аtf_custom_op_libraryпринимаетgpu_srcsаргумент, в котором список исходных файлов, содержащих ядра CUDA (*.cu.ccфайлы) могут быть указаны. Для использования с бинарной установкой TensorFlow, ядра CUDA должны быть составлены с NVIDIAnvccкомпилятор. Вот последовательность команд, которые вы можете использовать для составленияcuda_op_kernel.cu.ccиcuda_op_kernel.ccв единую динамически загружаемую библиотеку:

nvcc -std=c++14 -c -o cuda_op_kernel.cu.o cuda_op_kernel.cu.cc \
  ${TF_CFLAGS[@]} -D GOOGLE_CUDA=1 -x cu -Xcompiler -fPIC

g++ -std=c++14 -shared -o cuda_op_kernel.so cuda_op_kernel.cc \
  cuda_op_kernel.cu.o ${TF_CFLAGS[@]} -fPIC -lcudart ${TF_LFLAGS[@]}

cuda_op_kernel.soпроизводится выше, можно загружать как обычно в Python, используяtf.load_op_libraryфункция

Обратите внимание, что если ваши библиотеки CUDA не установлены в/usr/local/lib64, вам нужно явно указать путь во второй (G ++) команде выше. Например, добавить-L /usr/local/cuda-8.0/lib64/Если ваша CUDA установлена в/usr/local/cuda-8.0Полем

Примечание:В некоторых настройках Linux дополнительные параметры наnvccТребуется шаг компиляции. Добавлять-D_MWAITXINTRIN_H_INCLUDEDвnvccкомандная строка, чтобы избежать ошибок отmwaitxintrin.hПолем

Реализуйте градиент в Python

Учитывая график OPS, TensorFlow использует автоматическую дифференциацию (обратное распространение) для добавления новых OP, представляющих градиенты по отношению к существующим OPS. Чтобы сделать автоматическую дифференциацию для новых OPS, вы должны зарегистрировать градиентную функцию, которая вычисляет градиенты по отношению к данным градиентам OPS в отношении выходов OPS.

Математически, если OP вычисляет y = f (x), зарегистрированный градиент OP преобразует градиенты ∂l/∂y потерь L по отношению к y в градиенты ∂l/∂x относительно x через правило цепи:

∂l∂x = ∂l∂y∂y∂x = ∂l∂y∂f∂x.

В случаеZeroOut, только одна запись на входе влияет на выход, поэтому градиент по входу представляет собой разреженный «один горячий» тензор. Это выражено следующим образом:

from tensorflow.python.framework import ops
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import sparse_ops

@ops.RegisterGradient("ZeroOut")
def _zero_out_grad(op, grad):
  """The gradients for `zero_out`.

  Args:
    op: The `zero_out` `Operation` that we are differentiating, which we can use
      to find the inputs and outputs of the original op.
    grad: Gradient with respect to the output of the `zero_out` op.

  Returns:
    Gradients with respect to the input of `zero_out`.
  """
  to_zero = op.inputs[0]
  shape = array_ops.shape(to_zero)
  index = array_ops.zeros_like(shape)
  first_grad = array_ops.reshape(grad, [-1])[0]
  to_zero_grad = sparse_ops.sparse_to_dense([index], shape, first_grad, 0)
  return [to_zero_grad]  # List of one Tensor, since we have one input

Подробная информация о регистрации градиентных функций с помощьюtf.RegisterGradient:

  • Для OP с одним выходом функция градиента приметtf.OperationВopи аtf.Tensorgradи построить новые операции из тензоровop.inputs[i]Вop.outputs[i], иgradПолем Информацию о любых ATRS можно найти черезtf.Operation.get_attrПолем
  • Если у OP есть несколько выходов, функция градиента займетopиgrads, гдеgradsэто список градиентов по отношению к каждому выходу. Результатом градиентной функции должен быть списокTensorОбъекты, представляющие градиенты по отношению к каждому входу.
  • Если для некоторого входа нет четко определенного градиента, например, для целочисленных входов, используемых в качестве индексов, соответствующий возвращаемый градиент должен бытьNoneПолем Например, для OP, принимающего тензор с плавающей запятойxи целочисленный индексi, градиентная функция будетreturn [x_grad, None]Полем
  • Если для OP нет значимого градиента, вам часто не приходится регистрировать какой -либо градиент, и до тех пор, пока градиент OP никогда не нужен, у вас все будет в порядке. В некоторых случаях OP не имеет четко определенного градиента, но может участвовать в вычислении градиента. Здесь вы можете использоватьops.NotDifferentiableавтоматически распространять нули назад.

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

Добавьте подсказки типа при регистрации пользовательского градиента для типа OP, чтобы сделать код более читаемым, отладчиком, проще в обслуживании и более надежного посредством проверки данных. Например, при принятииopВ качестве параметра в функции укажите, что функция градиента будет приниматьtf.Operationкак тип параметров.

Функции формы в C ++

API TensorFlow имеет функцию, называемую «вывод формы», которая предоставляет информацию о формах тензоров без необходимости выполнять график. Вывод формы поддерживается «Функциями формы», которые зарегистрированы для каждого типа OP в C ++REGISTER_OPОбъявление и выполните две роли: утверждая, что формы входов совместимы во время построения графика, и определение форм для выходов.

Функции формы определяются как операции наshape_inference::InferenceContextсорт. Например, в функции формы для Zeroout:

.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

c->set_output(0, c->input(0));заявляет, что форма первого вывода должна быть установлена на форму первого входа. Если выход выбран его индексом, как в приведенном выше примере, второй параметрset_outputдолжен бытьShapeHandleобъект. Вы можете создать пустойShapeHandleобъект по его конструктору по умолчанию. АShapeHandleобъект для ввода с индексомidxможет быть полученc->input(idx)Полем

Есть ряд общих функций формы, которые применяются ко многим операциям, напримерshape_inference::UnchangedShapeкоторый можно найти вcommon_shape_fns.hи используется следующим образом:

REGISTER_OP("ZeroOut")
    .Input("to_zero: int32")
    .Output("zeroed: int32")
    .SetShapeFn(::tensorflow::shape_inference::UnchangedShape);

Функция формы также может ограничить форму входа. Для версииZeroOutПри ограничении формы вектора функция формы была бы следующей:

.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      ::tensorflow::shape_inference::ShapeHandle input;
      TF_RETURN_IF_ERROR(c->WithRank(c->input(0), 1, &input));
      c->set_output(0, input);
      return Status::OK();
    });

АWithRankВызов подтверждает, что форма вводаc->input(0)имеет форму с ровным размером (или если входная форма неизвестна, выходная форма будет вектором с одним неизвестным измерением).

Если ваш OPПолиморфный с несколькими входами, вы можете использовать членовInferenceContextЧтобы определить количество форм для проверки, иMergeЧтобы подтвердить, что все формы совместимы (альтернативно, атрибуты доступа, которые указывают на длины, сInferenceContext::GetAttr, который обеспечивает доступ к атрибутам ОП).

.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      ::tensorflow::shape_inference::ShapeHandle input;
      ::tensorflow::shape_inference::ShapeHandle output;
      for (size_t i = 0; i < c->num_inputs(); ++i) {
        TF_RETURN_IF_ERROR(c->WithRank(c->input(i), 2, &input));
        TF_RETURN_IF_ERROR(c->Merge(output, input, &output));
      }
      c->set_output(0, output);
      return Status::OK();
    });

Поскольку вывод формы является дополнительной особенностью, и формы тензоров могут динамически различаться, функции формы должны быть надежными до неполной информации о форме для любого из входов. АMergeМетод вInferenceContextПозволяет вызывающему утверждать, что две формы одинаковы, даже если у кого -либо или оба из них нет полной информации. Функции формы определяются для всех основных Tensorflow Ops и предоставляют много разных примеров использования.

АInferenceContextКласс имеет ряд функций, которые можно использовать для определения манипуляций с функцией формы. Например, вы можете подтвердить, что конкретное измерение имеет очень специфическое значение, используяInferenceContext::DimиInferenceContext::WithValue; Вы можете указать, что выходной размер - это сумма / продукт двух входных измерений, используяInferenceContext::AddиInferenceContext::MultiplyПолем УвидетьInferenceContextКласс для всех различных манипуляций с формой, которые вы можете указать. Следующий пример устанавливает форму первого вывода в (n, 3), где первый вход имеет форму (n, ...)

.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
    c->set_output(0, c->Matrix(c->Dim(c->input(0), 0), 3));
    return Status::OK();
});

Если у вас есть сложная функция формы, вам следует рассмотреть вопрос о добавлении теста для проверки того, что различные комбинации ввода формируют ожидаемые комбинации форм вывода. Вы можете увидеть примеры того, как написать эти тесты в некоторых нашихCore Ops тестыПолем (СинтаксисINFER_OKиINFER_ERRORнемного загадочно, но старайтесь быть компактными в представлении спецификаций ввода и вывода в тестах. На данный момент см. В этих тестах, чтобы получить представление о спецификации строки формы).

Создайте пакет PIP для пользовательского OP

Чтобы построить аpipпакет для вашего OP, см.Tensorflow/Custom-Opпример. В этом руководстве показано, как создать пользовательские операции из пакета Tensorflow PIP вместо того, чтобы построить TensorFlow из Source.

Первоначально опубликовано наTensorflowВеб -сайт, эта статья появляется здесь под новым заголовком и имеет лицензию в CC на 4.0. Образцы кода, разделенные по лицензии Apache 2.0.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE