إنشاء المرجع

إذا كنت ترغب في إنشاء عملية لا تغطيها مكتبة TensorFlow الحالية، فنوصيك بمحاولة كتابة العملية في Python كتركيبة من عمليات أو وظائف Python الموجودة. إذا لم يكن ذلك ممكنًا، فيمكنك إنشاء عملية C++ مخصصة. هناك عدة أسباب وراء رغبتك في إنشاء عملية C++ مخصصة:

  • ليس من السهل أو الممكن التعبير عن عمليتك كتركيبة من العمليات الموجودة.
  • ليس من الفعال التعبير عن عمليتك كتركيبة من البدائيات الموجودة.
  • تريد دمج تركيبة من البدائيات يدويًا والتي قد يجد المترجم المستقبلي صعوبة في دمجها.

على سبيل المثال، تخيل أنك تريد تنفيذ شيء مثل "التجميع المتوسط"، على غرار عامل التشغيل "MaxPool"، ولكنك تريد حساب المتوسطات عبر النوافذ المنزلقة بدلاً من القيم القصوى. قد يكون القيام بذلك باستخدام مجموعة من العمليات ممكنًا (على سبيل المثال، استخدام ExtractImagePatches وTopK)، ولكنه قد لا يكون بنفس كفاءة الأداء أو الذاكرة مثل العملية الأصلية حيث يمكنك القيام بشيء أكثر ذكاءً في عملية واحدة مدمجة. كما هو الحال دائمًا، من المفيد أولاً محاولة التعبير عما تريده باستخدام تكوين عامل التشغيل، واختيار إضافة عملية جديدة فقط إذا ثبت أن ذلك صعب أو غير فعال.

لدمج العملية المخصصة الخاصة بك، ستحتاج إلى:

  1. قم بتسجيل العملية الجديدة في ملف C++. يحدد تسجيل العملية واجهة (مواصفات) لوظيفة العملية، وهي مستقلة عن تنفيذ العملية. على سبيل المثال، يحدد تسجيل العملية اسم العملية ومدخلاتها ومخرجاتها. كما أنها تحدد دالة الشكل المستخدمة لاستدلال شكل الموتر.
  2. تنفيذ المرجع في C++. يُعرف تنفيذ العملية بالنواة، وهو التنفيذ الملموس للمواصفات التي قمت بتسجيلها في الخطوة 1. يمكن أن يكون هناك نواة متعددة لأنواع أو بنيات إدخال/إخراج مختلفة (على سبيل المثال، وحدات المعالجة المركزية (CPUs)، ووحدات معالجة الرسومات (GPU).
  3. قم بإنشاء غلاف بايثون (اختياري). هذا المجمع هو واجهة برمجة التطبيقات العامة المستخدمة لإنشاء العملية في بايثون. يتم إنشاء غلاف افتراضي من تسجيل العملية، والذي يمكن استخدامه مباشرة أو إضافته إلى.
  4. اكتب دالة لحساب التدرجات الخاصة بالعملية (اختياري).
  5. اختبر المرجع. عادةً ما نقوم بذلك في لغة Python من أجل الراحة، ولكن يمكنك أيضًا اختبار العملية في لغة C++. إذا قمت بتعريف التدرجات، فيمكنك التحقق منها باستخدام Python tf.test.compute_gradient_error . راجع relu_op_test.py كمثال يختبر الوظائف الأمامية لعوامل التشغيل المشابهة لـ Relu وتدرجاتها.

المتطلبات الأساسية

تحديد واجهة التشغيل

يمكنك تحديد واجهة العملية من خلال تسجيلها في نظام TensorFlow. في التسجيل، يمكنك تحديد اسم العملية الخاصة بك، ومدخلاتها (الأنواع والأسماء) والمخرجات (الأنواع والأسماء)، بالإضافة إلى سلاسل المستندات وأي سمات قد تتطلبها العملية.

لمعرفة كيفية عمل ذلك، لنفترض أنك ترغب في إنشاء عملية تأخذ موترًا من int32 s وتخرج نسخة من الموتر، مع تعيين كل العناصر باستثناء العنصر الأول على الصفر. للقيام بذلك، قم بإنشاء ملف باسم zero_out.cc . ثم قم بإضافة استدعاء إلى الماكرو REGISTER_OP الذي يحدد واجهة العملية الخاصة بك:

#include "tensorflow/core/framework/op.h"
#include "tensorflow/core/framework/shape_inference.h"

using namespace tensorflow;

REGISTER_OP("ZeroOut")
    .Input("to_zero: int32")
    .Output("zeroed: int32")
    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

تأخذ عملية ZeroOut هذه to_zero واحدًا من الأعداد الصحيحة 32 بت كمدخل، وتخرج موترًا zeroed من الأعداد الصحيحة 32 بت. يستخدم المرجع أيضًا وظيفة الشكل للتأكد من أن موتر الإخراج هو نفس شكل موتر الإدخال. على سبيل المثال، إذا كان الإدخال عبارة عن موتر للشكل [10، 20]، فإن دالة الشكل هذه تحدد أن شكل الإخراج هو أيضًا [10، 20].

تنفيذ النواة للمرجع

بعد تحديد الواجهة، قم بتوفير تطبيق واحد أو أكثر من المرجع. لإنشاء إحدى هذه النوى، قم بإنشاء فئة تعمل على توسيع OpKernel وتتجاوز أسلوب Compute . يوفر الأسلوب Compute وسيطة context واحدة من النوع OpKernelContext* ، والتي يمكنك من خلالها الوصول إلى أشياء مفيدة مثل موترات الإدخال والإخراج.

أضف النواة الخاصة بك إلى الملف الذي قمت بإنشائه أعلاه. قد تبدو النواة كما يلي:

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

using namespace tensorflow;

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

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

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

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

بعد تنفيذ النواة الخاصة بك، تقوم بتسجيلها في نظام TensorFlow. في التسجيل، تحدد القيود المختلفة التي سيتم بموجبها تشغيل هذه النواة. على سبيل المثال، قد يكون لديك نواة واحدة مخصصة لوحدات المعالجة المركزية (CPUs)، ونواة منفصلة لوحدات معالجة الرسومات (GPU).

للقيام بذلك في عملية ZeroOut ، قم بإضافة ما يلي إلى zero_out.cc :

REGISTER_KERNEL_BUILDER(Name("ZeroOut").Device(DEVICE_CPU), ZeroOutOp);

حبات وحدة المعالجة المركزية متعددة الخيوط

لكتابة نواة وحدة المعالجة المركزية متعددة الخيوط، يمكن استخدام وظيفة Shard في work_sharder.h . تقوم هذه الدالة بتقسيم وظيفة حسابية عبر الخيوط التي تم تكوينها لاستخدامها في الترابط الداخلي (راجع intra_op_parallelism_threads في config.proto ).

نواة GPU

يتم تنفيذ نواة GPU في جزأين: OpKernel ونواة CUDA ورمز الإطلاق الخاص بها.

في بعض الأحيان يكون تنفيذ OpKernel شائعًا بين وحدة المعالجة المركزية (CPU) ونواة وحدة معالجة الرسومات (GPU)، كما هو الحال فيما يتعلق بفحص المدخلات وتخصيص المخرجات. في هذه الحالة، التنفيذ المقترح هو:

  1. قم بتعريف OpKernel الموجود على الجهاز والنوع البدائي للموتر.
  2. لإجراء الحساب الفعلي للمخرجات، تستدعي الدالة Compute بنية عاملة مقولبة.
  3. يتم تعريف تخصص ذلك العامل لجهاز CPUDevice في نفس الملف، ولكن يتم تعريف تخصص GPUDevice في ملف .cu.cc، حيث سيتم تجميعه باستخدام مترجم CUDA.

هنا مثال على التنفيذ.

// kernel_example.h
#ifndef KERNEL_EXAMPLE_H_
#define KERNEL_EXAMPLE_H_

#include <unsupported/Eigen/CXX11/Tensor>

template <typename Device, typename T>
struct ExampleFunctor {
  void operator()(const Device& d, int size, const T* in, T* out);
};

#if GOOGLE_CUDA
// Partially specialize functor for GpuDevice.
template <typename T>
struct ExampleFunctor<Eigen::GpuDevice, T> {
  void operator()(const Eigen::GpuDevice& d, int size, const T* in, T* out);
};
#endif

#endif KERNEL_EXAMPLE_H_
// kernel_example.cc
#include "kernel_example.h"

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

using namespace tensorflow;

using CPUDevice = Eigen::ThreadPoolDevice;
using GPUDevice = Eigen::GpuDevice;

REGISTER_OP("Example")
    .Attr("T: numbertype")
    .Input("input: T")
    .Output("input_times_two: T")
    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

// CPU specialization of actual computation.
template <typename T>
struct ExampleFunctor<CPUDevice, T> {
  void operator()(const CPUDevice& d, int size, const T* in, T* out) {
    for (int i = 0; i < size; ++i) {
      out[i] = 2 * in[i];
    }
  }
};

// OpKernel definition.
// template parameter <T> is the datatype of the tensors.
template <typename Device, typename T>
class ExampleOp : public OpKernel {
 public:
  explicit ExampleOp(OpKernelConstruction* context) : OpKernel(context) {}

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

    // Create an output tensor
    Tensor* output_tensor = NULL;
    OP_REQUIRES_OK(context, context->allocate_output(0, input_tensor.shape(),
                                                     &output_tensor));

    // Do the computation.
    OP_REQUIRES(context, input_tensor.NumElements() <= tensorflow::kint32max,
                errors::InvalidArgument("Too many elements in tensor"));
    ExampleFunctor<Device, T>()(
        context->eigen_device<Device>(),
        static_cast<int>(input_tensor.NumElements()),
        input_tensor.flat<T>().data(),
        output_tensor->flat<T>().data());
  }
};

// Register the CPU kernels.
#define REGISTER_CPU(T)                                          \
  REGISTER_KERNEL_BUILDER(                                       \
      Name("Example").Device(DEVICE_CPU).TypeConstraint<T>("T"), \
      ExampleOp<CPUDevice, T>);
REGISTER_CPU(float);
REGISTER_CPU(int32);

// Register the GPU kernels.
#ifdef GOOGLE_CUDA
#define REGISTER_GPU(T)                                          \
  /* Declare explicit instantiations in kernel_example.cu.cc. */ \
  extern template class ExampleFunctor<GPUDevice, T>;            \
  REGISTER_KERNEL_BUILDER(                                       \
      Name("Example").Device(DEVICE_GPU).TypeConstraint<T>("T"), \
      ExampleOp<GPUDevice, T>);
REGISTER_GPU(float);
REGISTER_GPU(int32);
#endif  // GOOGLE_CUDA
// kernel_example.cu.cc
#ifdef GOOGLE_CUDA
#define EIGEN_USE_GPU
#include "kernel_example.h"
#include "tensorflow/core/util/gpu_kernel_helper.h"

using namespace tensorflow;

using GPUDevice = Eigen::GpuDevice;

// Define the CUDA kernel.
template <typename T>
__global__ void ExampleCudaKernel(const int size, const T* in, T* out) {
  for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < size;
       i += blockDim.x * gridDim.x) {
    out[i] = 2 * __ldg(in + i);
  }
}

// Define the GPU implementation that launches the CUDA kernel.
template <typename T>
void ExampleFunctor<GPUDevice, T>::operator()(
    const GPUDevice& d, int size, const T* in, T* out) {
  // Launch the cuda kernel.
  //
  // See core/util/gpu_kernel_helper.h for example of computing
  // block count and thread_per_block count.
  int block_count = 1024;
  int thread_per_block = 20;
  ExampleCudaKernel<T>
      <<<block_count, thread_per_block, 0, d.stream()>>>(size, in, out);
}

// Explicitly instantiate functors for the types of OpKernels registered.
template struct ExampleFunctor<GPUDevice, float>;
template struct ExampleFunctor<GPUDevice, int32>;

#endif  // GOOGLE_CUDA

بناء مكتبة العمليات

قم بتجميع المرجع باستخدام برنامج التحويل البرمجي للنظام الخاص بك (تثبيت TensorFlow الثنائي)

يجب أن تكون قادرًا على ترجمة zero_out.cc باستخدام مترجم C++ مثل g++ أو clang المتوفر على نظامك. تقوم حزمة PIP الثنائية بتثبيت ملفات الرأس والمكتبة التي تحتاجها لتجميع عملية التشغيل الخاصة بك في المواقع الخاصة بالنظام. ومع ذلك، توفر مكتبة TensorFlow python وظيفة get_include للحصول على دليل الرأس، ويحتوي دليل get_lib على كائن مشترك للارتباط به. فيما يلي مخرجات هذه الوظائف على جهاز Ubuntu.

$ python
>>> import tensorflow as tf
>>> tf.sysconfig.get_include()
'/usr/local/lib/python3.6/site-packages/tensorflow/include'
>>> tf.sysconfig.get_lib()
'/usr/local/lib/python3.6/site-packages/tensorflow'

بافتراض أنك قمت بتثبيت g++ ، فإليك تسلسل الأوامر التي يمكنك استخدامها لتجميع العملية الخاصة بك في مكتبة ديناميكية.

TF_CFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_compile_flags()))') )
TF_LFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_link_flags()))') )
g++ -std=c++14 -shared zero_out.cc -o zero_out.so -fPIC ${TF_CFLAGS[@]} ${TF_LFLAGS[@]} -O2

في نظام التشغيل macOS، تكون العلامة الإضافية "-unifieddynamic_lookup" مطلوبة عند إنشاء ملف .so .

ملاحظة حول إصدار gcc >=5 : يستخدم gcc واجهة برمجة تطبيقات C++ الجديدة منذ الإصدار 5 . تم إنشاء TensorFlow 2.8 والإصدارات الأقدم باستخدام gcc4 الذي يستخدم ABI الأقدم. إذا كنت تستخدم هذه الإصدارات من TensorFlow وتحاول تجميع مكتبة العمليات الخاصة بك باستخدام gcc>=5 ، أضف -D_GLIBCXX_USE_CXX11_ABI=0 إلى سطر الأوامر لجعل المكتبة متوافقة مع ABI الأقدم. تتوافق حزم TensorFlow 2.9+ مع واجهة برمجة التطبيقات الأحدث بشكل افتراضي.

ترجمة المرجع باستخدام bazel (تثبيت مصدر TensorFlow)

إذا كان لديك مصادر TensorFlow مثبتة، فيمكنك الاستفادة من نظام بناء TensorFlow لتجميع العملية الخاصة بك. ضع ملف BUILD مع قاعدة بناء Bazel التالية في دليل tensorflow/core/user_ops .

load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")

tf_custom_op_library(
    name = "zero_out.so",
    srcs = ["zero_out.cc"],
)

قم بتشغيل الأمر التالي لإنشاء zero_out.so .

$ bazel build --config opt //tensorflow/core/user_ops:zero_out.so

لتجميع عملية Example ، باستخدام CUDA Kernel، تحتاج إلى استخدام المعلمة gpu_srcs الخاصة بـ tf_custom_op_library . ضع ملف BUILD بقاعدة بناء Bazel التالية في مجلد جديد داخل دليل tensorflow/core/user_ops (على سبيل المثال "example_gpu").

load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")

tf_custom_op_library(
    # kernel_example.cc  kernel_example.cu.cc  kernel_example.h
    name = "kernel_example.so",
    srcs = ["kernel_example.h", "kernel_example.cc"],
    gpu_srcs = ["kernel_example.cu.cc", "kernel_example.h"],
)

قم بتشغيل الأمر التالي لإنشاء kernel_example.so .

$ bazel build --config opt //tensorflow/core/user_ops/example_gpu:kernel_example.so

استخدم المرجع في بايثون

توفر TensorFlow Python API وظيفة tf.load_op_library لتحميل المكتبة الديناميكية وتسجيل العملية باستخدام إطار عمل TensorFlow. تقوم load_op_library بإرجاع وحدة Python التي تحتوي على أغلفة Python الخاصة بالمرجع والنواة. وبالتالي، بمجرد إنشاء المرجع، يمكنك القيام بما يلي لتشغيله من بايثون:

import tensorflow as tf
zero_out_module = tf.load_op_library('./zero_out.so')
print(zero_out_module.zero_out([[1, 2], [3, 4]]).numpy())

# Prints
array([[1, 0], [0, 0]], dtype=int32)

ضع في اعتبارك أن الوظيفة التي تم إنشاؤها سيتم منحها اسم Snake_case (ليتوافق مع PEP8 ). لذلك، إذا كان اسم العملية الخاصة بك هو ZeroOut في ملفات C++، فسيتم تسمية وظيفة python باسم zero_out .

لإتاحة العملية كوظيفة عادية قابلة import من وحدة Python، قد يكون من المفيد إجراء استدعاء load_op_library في ملف مصدر Python كما يلي:

import tensorflow as tf

zero_out_module = tf.load_op_library('./zero_out.so')
zero_out = zero_out_module.zero_out

تحقق من أن المرجع يعمل

إحدى الطرق الجيدة للتحقق من أنك قمت بتنفيذ العملية الخاصة بك بنجاح هي كتابة اختبار لها. قم بإنشاء الملف zero_out_op_test.py بالمحتويات:

import tensorflow as tf

class ZeroOutTest(tf.test.TestCase):
  def testZeroOut(self):
    zero_out_module = tf.load_op_library('./zero_out.so')
    with self.test_session():
      result = zero_out_module.zero_out([5, 4, 3, 2, 1])
      self.assertAllEqual(result.eval(), [5, 0, 0, 0, 0])

if __name__ == "__main__":
  tf.test.main()

ثم قم بإجراء الاختبار (على افتراض أن لديك Tensorflow مثبتًا):

$ python zero_out_op_test.py

بناء ميزات متقدمة في المرجع الخاص بك

الآن بعد أن عرفت كيفية إنشاء عملية وتنفيذ أساسية (ومقيدة إلى حد ما)، سنلقي نظرة على بعض الأشياء الأكثر تعقيدًا التي ستحتاج عادةً إلى بنائها في عملية التشغيل الخاصة بك. وهذا يشمل:

الشيكات المشروطة والتحقق من الصحة

يفترض المثال أعلاه أن المرجع ينطبق على موتر من أي شكل. ماذا لو تم تطبيقه فقط على المتجهات؟ وهذا يعني إضافة فحص لتطبيق 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 ثلاث وسيطات:

بدلاً من ذلك، إذا كنت تريد اختبار ما إذا كان كائن Status الذي تم إرجاعه من بعض الوظائف يعد خطأً، وإذا كان الأمر كذلك، قم بإعادته، استخدم OP_REQUIRES_OK . ترجع كل من وحدات الماكرو هذه من الوظيفة عند حدوث خطأ.

تسجيل المرجع

Attrs

يمكن أن تحتوي العمليات على attrs، والتي يتم تعيين قيمها عند إضافة op إلى الرسم البياني. يتم استخدامها لتكوين العملية، ويمكن الوصول إلى قيمها داخل تطبيق kernel وفي أنواع المدخلات والمخرجات في تسجيل العملية. يُفضل استخدام مُدخل بدلاً من attr عندما يكون ذلك ممكنًا، نظرًا لأن المدخلات أكثر مرونة. وذلك لأن attrs ثوابت ويجب تعريفها في وقت إنشاء الرسم البياني. في المقابل، المدخلات هي موترات يمكن أن تكون قيمها ديناميكية؛ أي أن المدخلات يمكن أن تتغير في كل خطوة، ويمكن ضبطها باستخدام موجز، وما إلى ذلك. يتم استخدام Attrs للأشياء التي لا يمكن القيام بها باستخدام المدخلات: أي تكوين يؤثر على التوقيع (عدد أو نوع المدخلات أو المخرجات) أو الذي يمكن القيام به. لا تتغير من خطوة إلى خطوة.

يمكنك تحديد attr عند تسجيل العملية، وذلك عن طريق تحديد اسمها ونوعها باستخدام طريقة Attr ، التي تتوقع مواصفات النموذج:

<name>: <attr-type-expr>

حيث يبدأ <name> بحرف ويمكن أن يتكون من أحرف أبجدية رقمية وشرطات سفلية، و <attr-type-expr> هو تعبير نوعي للنموذج الموضح أدناه .

على سبيل المثال، إذا كنت تريد أن تحافظ عملية ZeroOut على فهرس محدد من قبل المستخدم، بدلاً من العنصر 0 فقط، فيمكنك تسجيل العملية كما يلي:

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 : صحيح أو خطأ.
  • type : إحدى القيم (غير المرجعية) لـ DataType .
  • shape : TensorShapeProto .
  • list(<type>) : قائمة <type> ، حيث <type> هو أحد الأنواع المذكورة أعلاه. لاحظ أن list(list(<type>)) غير صالحة.

راجع أيضًا: op_def_builder.cc:FinalizeAttr للحصول على قائمة نهائية.

القيم والقيود الافتراضية

قد تحتوي Attrs على قيم افتراضية، وقد تحتوي بعض أنواع Attrs على قيود. لتحديد 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 . لم تحدد أن نوع attr هو type . يتم تضمين هذا عندما يكون لديك قائمة بالأنواع في {...} . على سبيل المثال، في هذه الحالة، attr t هو نوع يجب أن يكون int32 أو float أو bool :

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

توجد اختصارات لقيود النوع الشائعة:

  • numbertype : type الكتابة يقتصر على الأنواع الرقمية (غير السلسلة وغير المنطقية).
  • realnumbertype : مثل numbertype بدون أنواع معقدة.
  • quantizedtype : مثل numbertype ولكن أنواع الأرقام الكمية فقط.

يتم تحديد قوائم الأنواع المسموح بها من خلال الوظائف (مثل NumberTypes() ) في tensorflow/core/framework/types.h . في هذا المثال، يجب أن يكون attr t أحد الأنواع الرقمية:

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

لهذا المرجع:

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

يمكن دمج القوائم مع قوائم أخرى وأنواع فردية. تسمح العملية التالية بأن يكون attr t أيًا من الأنواع الرقمية، أو النوع المنطقي:

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

لهذا المرجع:

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> هو رقم طبيعي. على سبيل المثال، يحدد تسجيل العملية التالي أن attr a يجب أن تكون له قيمة 2 على الأقل:

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

list(<type>) >= <n> : قائمة من النوع <type> طولها أكبر من أو يساوي <n> . على سبيل المثال، يحدد تسجيل العملية التالي أن attr a عبارة عن قائمة من الأنواع (إما 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");

بناء الجملة المدعوم للقيمة الافتراضية هو ما يمكن استخدامه في التمثيل الأولي لتعريف GraphDef الناتج.

فيما يلي أمثلة لكيفية تحديد الإعداد الافتراضي لجميع الأنواع:

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 .

تعدد الأشكال

نوع تعدد الأشكال

بالنسبة للعمليات التي يمكن أن تأخذ أنواعًا مختلفة كمدخلات أو تنتج أنواعًا مختلفة من المخرجات، يمكنك تحديد attr في نوع الإدخال أو الإخراج في تسجيل العملية. عادةً ما تقوم بعد ذلك بتسجيل OpKernel لكل نوع مدعوم.

على سبيل المثال، إذا كنت تريد أن تعمل عملية ZeroOut على float s بالإضافة إلى int32 s، فقد يبدو تسجيل العملية الخاص بك كما يلي:

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

يحدد تسجيل العملية الخاص بك الآن أن نوع الإدخال يجب أن يكون float أو int32 ، وأن مخرجاته ستكون من نفس النوع، حيث أن كلاهما من النوع T

تسمية

بشكل عام، يجب إعطاء المدخلات والمخرجات والميزات أسماء على شكل ثعبان. الاستثناء الوحيد هو attrs التي يتم استخدامها كنوع الإدخال أو نوع الإخراج. يمكن استنتاج تلك السمات عند إضافة المرجع إلى الرسم البياني وبالتالي لا تظهر في وظيفة المرجع. على سبيل المثال، هذا التعريف الأخير لـ 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 ). يتم إعطاء تلك السمات المستنتجة أسماء بأحرف كبيرة أو CamelCase.

قارن هذا مع عملية تحتوي على النوع 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);

للحفاظ على التوافق مع الإصدارات السابقة ، يجب عليك تحديد قيمة افتراضية عند إضافة attr إلى عملية موجودة:

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
قائمة المدخلات والمخرجات

بالإضافة إلى القدرة على قبول أو إنتاج أنواع مختلفة، يمكن أن تستهلك العمليات أو تنتج عددًا متغيرًا من الموترات.

في المثال التالي، يحتوي attr 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 attr N لتحديد طول القائمة.

ويمكن جعل هذا النوع متعدد الأشكال أيضًا. في المثال التالي، الإدخال عبارة عن قائمة من الموترات (بطول "N" ) من نفس النوع (لكن غير محدد) ( "T" )، والإخراج عبارة عن موتر واحد من النوع المطابق:

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

افتراضيًا، يبلغ الحد الأدنى لطول قوائم الموتر 1. يمكنك تغيير هذا الافتراضي باستخدام قيد ">=" على attr المقابل . في المثال التالي، الإدخال عبارة عن قائمة تضم على الأقل 2 موتر int32 :

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

يعمل نفس بناء الجملة مع attrs "list(type)" :

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

المدخلات والمخرجات

لتلخيص ما سبق، يمكن أن يكون للتسجيل التشغيلي مدخلات ومخرجات متعددة:

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> هو اسم Attr بنوع type أو list(type) (مع تقييد نوع محتمل). يسمح بناء الجملة هذا بعمليات متعددة الأشكال .

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

    تتيح لك الإشارة إلى attr من 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> هو اسم Attr من النوع int . يمكن أن يكون <type> إما tf.DType أو اسم attr مع النوع type . كمثال على الأول، تقبل هذه العملية قائمة من موترات int32 :

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

    حيث أن هذه العملية تقبل قائمة الموترات من أي نوع، طالما أنها كلها متماثلة:

    REGISTER_OP("SameTypeSequenceExample")
        .Attr("NumTensors: int")
        .Attr("T: type")
        .Input("in: NumTensors * T")
    
  • للإشارة إلى الموتر: Ref(<type>) ، حيث <type> هو أحد الأنواع السابقة.

سيتم استنتاج أي attr مستخدم في نوع الإدخال. وفقًا للاتفاقية، تستخدم تلك السمات المستنتجة أسماء كبيرة (مثل T أو N ). بخلاف ذلك، يكون للمدخلات والمخرجات وattrs أسماء مثل معلمات الوظيفة (على سبيل المثال num_outputs ). لمزيد من التفاصيل، راجع القسم السابق حول التسمية .

لمزيد من التفاصيل، راجع tensorflow/core/framework/op_def_builder.h .

التوافق مع الإصدارات السابقة

لنفترض أنك كتبت عملية مخصصة لطيفة وشاركتها مع الآخرين، بحيث يكون لديك عملاء سعداء باستخدام عمليتك. ومع ذلك، قد ترغب في إجراء تغييرات على العملية بطريقة ما.

بشكل عام، يجب أن تكون التغييرات التي يتم إجراؤها على المواصفات الحالية والمسجلة متوافقة مع الإصدارات السابقة: يجب ألا يؤدي تغيير مواصفات العملية إلى كسر المخازن المؤقتة لبروتوكول GraphDef المتسلسلة السابقة والتي تم إنشاؤها من المواصفات القديمة. تفاصيل توافق GraphDef موضحة هنا .

هناك عدة طرق للحفاظ على التوافق مع الإصدارات السابقة.

  1. أي attrs جديدة تضاف إلى العملية يجب أن يكون لها قيم افتراضية محددة، ومع هذه القيمة الافتراضية يجب أن يكون للعملية السلوك الأصلي. لتغيير عملية من غير متعددة الأشكال إلى متعددة الأشكال، يجب عليك إعطاء قيمة افتراضية للنوع الجديد attr للحفاظ على التوقيع الأصلي افتراضيًا. على سبيل المثال، إذا كانت عمليتك:

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

    يمكنك جعله متعدد الأشكال بطريقة متوافقة مع الإصدارات السابقة باستخدام:

    REGISTER_OP("MyGeneralUnaryOp")
        .Input("in: T")
        .Output("out: T")
        .Attr("T: numerictype = DT_FLOAT");
    
  2. يمكنك بأمان جعل القيد على attr أقل تقييدًا. على سبيل المثال، يمكنك التغيير من {int32, int64} إلى {int32, int64, float} أو type . أو يمكنك التغيير من {"apple", "orange"} إلى {"apple", "banana", "orange"} أو string .

  3. يمكنك تغيير المدخلات/المخرجات الفردية إلى مدخلات/مخرجات القائمة، طالما أن الإعداد الافتراضي لنوع القائمة يطابق التوقيع القديم.

  4. يمكنك إضافة قائمة إدخال/إخراج جديدة، إذا كانت الإعدادات الافتراضية فارغة.

  5. مساحة الاسم: أي عمليات جديدة تقوم بإنشائها، عن طريق بادئة أسماء العمليات بشيء فريد لمشروعك. يؤدي هذا إلى تجنب تصادم عمليتك مع أي عمليات قد يتم تضمينها في الإصدارات المستقبلية من TensorFlow.

  6. خطط للمستقبل! حاول توقع الاستخدامات المستقبلية للمرجع. لا يمكن إجراء بعض تغييرات التوقيع بطريقة متوافقة (على سبيل المثال، إنشاء قائمة من نفس النوع في قائمة من أنواع مختلفة).

يمكن العثور على القائمة الكاملة للتغييرات الآمنة وغير الآمنة في tensorflow/core/framework/op_compatibility_test.cc . إذا لم تتمكن من جعل تغييرك إلى عملية متوافقًا مع الإصدارات السابقة، فقم بإنشاء عملية جديدة باسم جديد بالدلالات الجديدة.

لاحظ أيضًا أنه على الرغم من أن هذه التغييرات يمكن أن تحافظ على توافق GraphDef ، إلا أن كود Python الذي تم إنشاؤه قد يتغير بطريقة غير متوافقة مع المتصلين القدامى. قد تظل واجهة برمجة تطبيقات Python متوافقة من خلال إجراء تغييرات دقيقة في غلاف Python المكتوب بخط اليد، وذلك عن طريق الاحتفاظ بالتوقيع القديم باستثناء إمكانية إضافة وسائط اختيارية جديدة إلى النهاية. بشكل عام، لا يمكن إجراء تغييرات غير متوافقة إلا عندما يقوم TensorFlow بتغيير الإصدارات الرئيسية، ويجب أن يتوافق مع دلالات إصدار GraphDef .

دعم GPU

يمكنك تنفيذ OpKernels مختلفة وتسجيل واحدة لوحدة المعالجة المركزية (CPU) وأخرى لوحدة معالجة الرسومات (GPU)، تمامًا كما يمكنك تسجيل النوى لأنواع مختلفة . هناك عدة أمثلة للنوى التي تدعم GPU في tensorflow/core/kernels/ . لاحظ أن بعض النواة تحتوي على إصدار وحدة المعالجة المركزية (CPU) في ملف .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 kernel من 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 لتنفيذ عملية. يقبل tf_custom_op_library وسيطة gpu_srcs التي يمكن من خلالها تحديد قائمة الملفات المصدر التي تحتوي على نواة CUDA (ملفات *.cu.cc ). للاستخدام مع التثبيت الثنائي لـ TensorFlow، يجب تجميع نواة CUDA باستخدام برنامج التحويل البرمجي nvcc الخاص بـ NVIDIA. فيما يلي تسلسل الأوامر التي يمكنك استخدامها لتجميع 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 .

تنفيذ التدرج في بايثون

بالنظر إلى رسم بياني للعمليات، يستخدم TensorFlow التمايز التلقائي (الانتشار العكسي) لإضافة عمليات جديدة تمثل التدرجات فيما يتعلق بالعمليات الحالية. لجعل التمايز التلقائي يعمل للعمليات الجديدة، يجب عليك تسجيل دالة التدرج التي تحسب التدرجات فيما يتعلق بمدخلات العمليات المعطاة للتدرجات فيما يتعلق بمخرجات العمليات.

رياضيا، إذا كان المرجع يحسب \(y = f(x)\) يقوم التدرج المسجل بتحويل التدرجات \(\partial L/ \partial y\) من الخسارة \(L\) بالنسبة إلى\(y\) إلى التدرجات \(\partial L/ \partial x\) بالنسبة إلى \(x\) عبر قاعدة السلسلة:

\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial f}{\partial 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 :

  • بالنسبة إلى عملية بمخرج واحد، ستأخذ دالة التدرج tf.Operation و op و tf.Tensor grad وتبني عمليات جديدة من الموترات op.inputs[i] و op.outputs[i] و grad . يمكن العثور على معلومات حول أي attrs عبر tf.Operation.get_attr .

  • إذا كانت العملية تحتوي على مخرجات متعددة، فإن دالة التدرج ستأخذ op و grads ، حيث grads عبارة عن قائمة من التدرجات فيما يتعلق بكل مخرجات. يجب أن تكون نتيجة دالة التدرج قائمة بكائنات Tensor التي تمثل التدرجات فيما يتعلق بكل إدخال.

  • إذا لم يكن هناك تدرج محدد جيدًا لبعض المدخلات، مثل المدخلات الصحيحة المستخدمة كمؤشرات، فيجب أن يكون التدرج المقابل الذي تم إرجاعه None . على سبيل المثال، بالنسبة لعملية تأخذ موتر النقطة العائمة x وفهرس عدد صحيح i ، فإن دالة التدرج return [x_grad, None] .

  • إذا لم يكن هناك تدرج ذو معنى للعملية على الإطلاق، فلن تضطر غالبًا إلى تسجيل أي تدرج، وطالما لم تكن هناك حاجة إلى التدرج اللوني للعملية مطلقًا، فستكون على ما يرام. في بعض الحالات، لا تحتوي العملية على تدرج محدد جيدًا ولكن يمكن أن تشارك في حساب التدرج. هنا يمكنك استخدام ops.NotDifferentiable لنشر الأصفار تلقائيًا إلى الخلف.

لاحظ أنه في وقت استدعاء دالة التدرج، يتوفر فقط الرسم البياني لتدفق البيانات الخاص بالعمليات، وليس بيانات الموتر نفسه. وبالتالي، يجب إجراء جميع العمليات الحسابية باستخدام عمليات Tensorflow الأخرى، ليتم تشغيلها في وقت تنفيذ الرسم البياني.

أضف تلميحات الكتابة عند تسجيل التدرج المخصص لنوع العملية لجعل التعليمات البرمجية أكثر قابلية للقراءة، وتصحيح الأخطاء، وأسهل في الصيانة، وأكثر قوة من خلال التحقق من صحة البيانات. على سبيل المثال، عند استخدام op كمعلمة في دالة، حدد أن وظيفة التدرج ستأخذ tf.Operation كنوع المعلمة الخاصة بها.

وظائف الشكل في C++

تحتوي واجهة برمجة تطبيقات TensorFlow على ميزة تسمى "استدلال الشكل" والتي توفر معلومات حول أشكال الموترات دون الحاجة إلى تنفيذ الرسم البياني. يتم دعم استدلال الشكل من خلال "وظائف الشكل" المسجلة لكل نوع عملية في إعلان 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) له شكل ذو بعد واحد بالضبط (أو إذا كان شكل الإدخال غير معروف، فسيكون شكل الإخراج متجهًا ذو بعد واحد غير معروف).

إذا كانت العملية الخاصة بك متعددة الأشكال مع مدخلات متعددة ، فيمكنك استخدام أعضاء 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 الأساسية وتوفر العديد من أمثلة الاستخدام المختلفة.

تحتوي فئة 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();
});

إذا كانت لديك وظيفة شكل معقدة، فيجب أن تفكر في إضافة اختبار للتحقق من أن مجموعات أشكال الإدخال المختلفة تنتج مجموعات أشكال الإخراج المتوقعة. يمكنك رؤية أمثلة لكيفية كتابة هذه الاختبارات في بعض اختبارات العمليات الأساسية لدينا. (إن بناء جملة INFER_OK و INFER_ERROR غامض بعض الشيء، لكن حاول أن تكون مضغوطًا في تمثيل مواصفات شكل الإدخال والإخراج في الاختبارات. في الوقت الحالي، راجع التعليقات المحيطة في تلك الاختبارات للتعرف على مواصفات سلسلة الشكل).

أنشئ حزمة نقاط للعملية المخصصة الخاصة بك

لإنشاء حزمة pip للعملية الخاصة بك، راجع مثال Tensorflow/custom-op . يوضح هذا الدليل كيفية إنشاء عمليات مخصصة من حزمة النقاط TensorFlow بدلاً من إنشاء TensorFlow من المصدر.