المكالمات المخصّصة في XLA

يوضِّح هذا المستند كيفية كتابة واستخدام طلبات XLA المخصّصة باستخدام مكتبة XLA FFI. الطلب المخصّص هو آلية لوصف "عملية" خارجية في وحدة HLO إلى برنامج XLA المترجِم (في وقت الترجمة)، وXLA FFI هي آلية لتسجيل تنفيذ هذه العمليات باستخدام XLA (في وقت التشغيل). يرمز FFI إلى "واجهة الدوال الخارجية" وهي مجموعة من واجهات برمجة التطبيقات C التي تحدّد واجهة ثنائية (ABI) لـ XLA لاستدعاء رمز خارجي مكتوب بلغات برمجة أخرى. توفّر XLA روابط تتضمّن العناوين فقط لـ XLA FFI مكتوبة بلغة C++، ما يخفي جميع التفاصيل المنخفضة المستوى لواجهات برمجة التطبيقات C الأساسية عن المستخدِم النهائي.

الطلبات المخصّصة في JAX + XLA

راجِع مستندات JAX للحصول على أمثلة شاملة عن دمج الطلبات المخصّصة وXLA FFI مع JAX.

ربط XLA FFI

ربط XLA FFI هو مواصفات وقت الترجمة لتوقيع الطلب المخصّص: وسيطات الطلب المخصّص وسماته وأنواعها، والمعلَمات الإضافية التي يتم تمريرها من خلال سياق التنفيذ (مثل تدفق وحدة معالجة الرسومات للواجهة الخلفية لوحدة معالجة الرسومات). يمكن ربط XLA FFI بأي دالة C++ قابلة للاستدعاء (مؤشر دالة أو دالة لامدا وما إلى ذلك) باستخدام توقيع operator() متوافق. يفكّ معالج الطلب الذي تم إنشاؤه إطار استدعاء XLA FFI (محدّد بواسطة واجهة برمجة التطبيقات C الثابتة)، ويتحقّق من نوع جميع المعلَمات، ويُعيد توجيه النتائج التي تم فك تشفيرها إلى معاودة الاتصال التي يحدّدها المستخدِم.

يعتمد ربط XLA FFI بشكل كبير على البرمجة الوصفية للنماذج لكي يتمكّن من تجميع معالج الطلب الذي تم إنشاؤه إلى رمز الآلة الأكثر كفاءة. تكون النفقات العامة لوقت التشغيل في حدود بضع نانوثوانٍ لكل معلَمة طلب مخصّص.

نقاط تخصيص XLA FFI يتم تنفيذها كعمليات تخصيص للنماذج، ويمكن للمستخدِمين تحديد كيفية فك تشفير أنواعهم المخصّصة، أي من الممكن تحديد فك تشفير مخصّص لأنواع enum class التي يحدّدها المستخدِم.

عرض الأخطاء من الطلبات المخصّصة

يجب أن تعرض عمليات تنفيذ الطلبات المخصّصة قيمة xla::ffi::Error للإشارة إلى النجاح أو الخطأ في وقت تشغيل XLA. وهي مشابهة لـ absl::Status، ولها المجموعة نفسها من رموز الخطأ. لا نستخدم absl::Status لأنّه ليس لديها واجهة التطبيق الثنائية (ABI) ثابتة، وسيكون من غير الآمن تمريرها بين مكتبة الطلبات المخصّصة التي يتم تحميلها ديناميكيًا وXLA نفسها.

// Handler that always returns an error.
auto always_error = Ffi::Bind().To(
    []() { return Error(ErrorCode::kInternal, "Oops!"); });

// Handler that always returns a success.
auto always_success = Ffi::Bind().To(
    []() { return Error::Success(); });

وسيطات المخزن المؤقت ونتائجه

تستخدم XLA نمط تمرير الوجهة للنتائج: لا تخصّص الطلبات المخصّصة (أو أي عمليات XLA أخرى) الذاكرة للنتائج، ولكنها تكتب بدلاً من ذلك في الوجهات التي يمرّرها وقت تشغيل XLA. تستخدم XLA عملية تخصيص المخزن المؤقت الثابتة، وتخصّص المخازن المؤقتة لجميع القيم استنادًا إلى نطاقات بقائها في وقت الترجمة.

النتائج التي يتم تمريرها إلى معالِجات FFI يتم تضمينها في نموذج Result<T>، الذي يتضمّن دلالات مشابهة للمؤشر: operator-> يتيح الوصول إلى المعلَمة الأساسية.

تتيح وسيطات AnyBuffer ونتائجها الوصول إلى معلَمات المخزن المؤقت للطلب المخصّص من أي نوع بيانات. يكون ذلك مفيدًا عندما يكون للطلب المخصّص عملية تنفيذ عامة تعمل مع أنواع بيانات متعدّدة، وتنفّذ عملية تنفيذ الطلب المخصّص عملية إرسال وقت التشغيل استنادًا إلى نوع البيانات. تتيح AnyBuffer الوصول إلى نوع بيانات المخزن المؤقت وأبعاده ومؤشر إلى المخزن المؤقت نفسه.

%0 = "stablehlo.custom_call"(%arg0) {
  call_target_name = "foo",
  api_version = 4 : i32
} : (tensor<2x2xf32>) -> tensor<2x2xf32>
// Buffers of any number of dimensions and data type.
auto handler = Ffi::Bind().Arg<AnyBuffer>().Ret<AnyBuffer>().To(
    [](AnyBuffer arg, Result<AnyBuffer> res) -> Error {
      void* arg_data = arg.untyped_data();
      void* res_data = res->untyped_data();
      return Error::Success();
    });

وسيطات المخزن المؤقت ونتائجه المقيّدة

تسمح Buffer بإضافة قيود على نوع بيانات المخزن المؤقت وعدد الأبعاد، وسيتحقّق المعالج منها تلقائيًا ويعرض خطأ في وقت تشغيل XLA، إذا لم تتطابق وسيطات وقت التشغيل مع توقيع معالج FFI.

// Buffers of any number of dimensions and F32 data type.
auto handler = Ffi::Bind().Arg<Buffer<F32>>().Ret<Buffer<F32>>().To(
    [](Buffer<F32> arg, Result<Buffer<F32>> res) -> Error {
      float* arg_data = arg.typed_data();
      float* res_data = res->typed_data();
      return Error::Success();
    });
// Buffers of number of dimensions 2 and F32 data type.
auto handler = Ffi::Bind().Arg<BufferR2<F32>>().Ret<BufferR2<F32>>().To(
    [](BufferR2<F32> arg, Result<BufferR2<F32>> res) -> Error {
      float* arg_data = arg.typed_data();
      float* res_data = res->typed_data();
      return Error::Success();
    });

مطابقة المخازن المؤقتة والتحقّق منها

يمكن أن تعبّر أنماط المخزن المؤقت عن قيود أكثر تحديدًا من أنواع الوسيطات والنتائج في ربط FFI. تكون هذه الأنماط مفيدة لتحسين AnyBuffer إلى نوع مخزن مؤقت ملموس، والتحقّق من أحجام الأبعاد، والتحقّق من العلاقات بين أشكال المخازن المؤقتة المتعدّدة.

الأنماط غير قابلة للتغيير ويمكنها تحديد نوع البيانات والترتيب إما مباشرةً أو باستخدام المعدِّلات المقابلة:

namespace m = ::xla::ffi::match;

m::Buffer<F32, 2>();
m::Buffer().WithDType<F32>().WithRank<2>();

تتحقّق الدالة Match من نمط وتعرض نوع المخزن المؤقت الذي تم تقييده إلى أقصى حد. لتحسين AnyBuffer، يجب أن يحدّد النمط نوع بيانات واحدًا وترتيبًا واحدًا فقط. تؤدي مطابقة مخزن مؤقت سبق تحديد نوعه إلى التحقّق من أي قيود إضافية والحفاظ على نوعه.

namespace m = ::xla::ffi::match;

auto handler = Ffi::Bind().Arg<AnyBuffer>().Ret<AnyBuffer>().To(
    [](AnyBuffer input, Result<AnyBuffer> output) -> Error {
      int64_t rows;
      int64_t cols;

      ErrorOr<BufferR2<F32>> typed_input =
          Match("input", input,
                m::Buffer<F32>().WithDims(&rows, &cols));
      if (typed_input.has_error()) {
        return std::move(typed_input).error();
      }

      ErrorOr<Result<BufferR2<F32>>> typed_output =
          Match("output", output,
                m::Buffer<F32>().WithDims(rows, cols));
      if (typed_output.has_error()) {
        return std::move(typed_output).error();
      }

      float* input_data = typed_input->typed_data();
      float* output_data = (*typed_output)->typed_data();
      return Error::Success();
    });

في هذا المثال، تؤدي مطابقة input إلى تسجيل حجمَي أبعاده، وتؤدي مطابقة output إلى التحقّق من أنّ له الشكل نفسه. لا يتم تثبيت عمليات التسجيل إلا عندما يتطابق النمط الكامل بنجاح.

يمكن أن يكون نمط البُعد غير مقيّد أو ثابتًا أو مسجَّلاً. يتم تحويل القيم الثابتة ومؤشرات التسجيل ضمنًا إلى أنماط أبعاد:

m::Dim()       // Any dimension size.
m::Dim(16)     // A dimension whose size is 16.
m::Dim(&rows)  // Any dimension size, captured in `rows` on success.

m::Buffer<F32>().WithDims(&rows, 16);

تصف WithDims شكلاً كاملاً وتضبط ترتيبه من عدد الوسيطات. WithDim<I> أو WithDim(index, ...) تقيّد الأبعاد حسب الموضع بدون تثبيت الترتيب الكامل. يتطلّب القيد الموضعي وجود البُعد المُشار إليه، ولكن يترك كل بُعد آخر غير مقيّد:

ErrorOr<BufferR4<F32>> MatchActivations(AnyBuffer input) {
  return Match(
      "input", input,
      m::Buffer<F32, 4>().WithDim<0>(1).WithDim<3>(128));
}

Error VerifyDimension(AnyBuffer input, size_t index, int64_t size) {
  return Verify("input", input, m::Buffer().WithDim(index, size));
}

تتطلّب MatchActivations مخزنًا مؤقتًا من النوع F32 بترتيب 4، يكون بُعداه 0 و3 مساويَين لـ 1 و128 على التوالي، ولا تكون البُعدان 1 و2 مقيّدَين. بدون صريح WithRank، يقبل WithDim(index, ...) أي ترتيب أكبر من index.

يمكن أن تعبّر عمليات تسجيل الأبعاد أيضًا عن العلاقات بين المواضع:

auto matrix = m::Buffer<F32>().WithDims(&rows, 128);

int64_t n;
ErrorOr<BufferR2<F32>> square =
    Match("matrix", buffer,
          m::Buffer<F32>().WithDims(&n, &n));

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

لا تتطلّب العلاقات على مستوى المخزن المؤقت بأكمله تسجيل كل بُعد. تطابق WithShapeOf شكل وقت التشغيل الكامل لمخزن مؤقت آخر مع السماح بنوع بيانات مختلف. تتطلّب Like أيضًا نوع البيانات نفسه:

Error VerifySortBuffers(AnyBuffer keys, AnyBuffer values,
                        Result<AnyBuffer> keys_output) {
  Error error =
      Verify("values", values, m::Buffer().WithShapeOf(keys));
  if (error.failure()) {
    return error;
  }

  return Verify("keys output", *keys_output, m::Buffer().Like(keys));
}

ينسخ كلا المعدِّلَين بيانات المخزن المؤقت المرجعي الوصفية عند إنشاء النمط، ولا يحتفظ النمط بمرجع إلى المخزن المؤقت. بما أنّ هذه القيود هي قيود وقت التشغيل، فإنّها لا تحسّن AnyBuffer إلى نوع القيمة التي تم إرجاعها ملموس. تكون هذه القيود مفيدة بشكل أساسي مع Verify، أو مع Match عندما يكون للمخزن المؤقت للإدخال نوع ملموس.

استخدِم Verify عندما يجب أن يحتفظ المخزن المؤقت بنوعه الحالي، أو عندما يقبل النمط أكثر من نوع بيانات أو ترتيب. على سبيل المثال، يمكن أن يقبل مخزن مؤقت للفهرس S32 أو S64، ويتم الإرسال استنادًا إلى نوع البيانات بعد التحقّق:

namespace m = ::xla::ffi::match;

Error VerifyIndices(AnyBuffer indices) {
  auto pattern = m::Buffer().WithDType<S32, S64>().WithRank<1, 2>();

  Error error = Verify("indices", indices, pattern);
  if (error.failure()) {
    return error;
  }

  // Dispatch an implementation based on `indices.element_type()`.
  return Error::Success();
}

بما أنّ هذا النمط يسمح بأنواع مخازن مؤقتة ملموسة متعدّدة، لا يمكنه تحسين AnyBuffer باستخدام Match: ليس هناك نوع القيمة التي تم إرجاعها فريد. لا يزال من الممكن تمريره إلى Match باستخدام مخزن مؤقت سبق تحديد نوعه، والذي يكون نوع القيمة التي تم إرجاعها الخاص به معروفًا. يمكن أيضًا التحقّق من مخزن مؤقت سبق تحديد نوعه، ويكون ذلك مفيدًا عادةً للتحقّق من العلاقات بين الأشكال:

Error VerifySameShape(BufferR2<F32> lhs, BufferR2<F32> rhs) {
  int64_t rows;
  int64_t cols;

  Error error = Verify(
      "lhs", lhs,
      m::Buffer().WithDims(&rows, &cols));
  if (error.failure()) {
    return error;
  }

  return Verify("rhs", rhs,
                m::Buffer().WithDims(rows, cols));
}

الاسم الذي يتم تمريره كأول وسيطة إلى Match وVerify مطلوب ويتم تضمينه في الأخطاء، بالإضافة إلى وصف للقيد الذي تعذّر تنفيذه.

تعرض واجهة برمجة تطبيقات FFI الخارجية في xla/ffi/api/ffi.h قيمة Error من Verify و ErrorOr<T> من Match، كما في الأمثلة أعلاه. توفر واجهة برمجة تطبيقات FFI الداخلية في xla/ffi/ffi.h واجهة المطابقة نفسها باستخدام absl::Status و absl::StatusOr<T>:

ABSL_ASSIGN_OR_RETURN(BufferR2<F32> input,
                 Match("input", buffer, m::Buffer<F32, 2>()));
ABSL_RETURN_IF_ERROR(Verify(
    "input", input,
    m::Buffer().WithDims(expected_rows, m::Dim())));

الوسيطات والنتائج المتغيّرة

إذا كان عدد الوسيطات والنتائج مختلفًا في حالات مختلفة من الطلب المخصّص، يمكن فك تشفيرها في وقت التشغيل باستخدام RemainingArgs وRemainingRets.

auto handler = Ffi::Bind().RemainingArgs().RemainingRets().To(
    [](RemainingArgs args, RemainingRets results) -> Error {
      ErrorOr<AnyBuffer> arg = args.get<AnyBuffer>(0);
      ErrorOr<Result<AnyBuffer>> res = results.get<AnyBuffer>(0);

      if (!arg.has_value()) {
        return Error(ErrorCode::kInternal, arg.error());
      }

      if (!res.has_value()) {
        return Error(ErrorCode::kInternal, res.error());
      }

      return Error::Success();
    });

يمكن الإعلان عن الوسيطات والنتائج المتغيّرة بعد الوسيطات والنتائج العادية، ولكن ربط الوسيطات والنتائج العادية بعد الوسيطات والنتائج المتغيّرة غير مسموح به.

auto handler =
    Ffi::Bind()
        .Arg<AnyBuffer>()
        .RemainingArgs()
        .Ret<AnyBuffer>()
        .RemainingRets()
        .To([](AnyBuffer arg, RemainingArgs args, AnyBuffer ret,
               RemainingRets results) -> Error { return Error::Success(); });

السمات

تتيح XLA FFI فك التشفير التلقائي لـ mlir::DictionaryAttr التي يتم تمريرها كـ backend_config لـ custom_call إلى وسيطات معالج FFI.

%0 = "stablehlo.custom_call"(%arg0) {
  call_target_name = "foo",
  backend_config= {
    i32 = 42 : i32,
    str = "string"
  },
  api_version = 4 : i32
} : (tensor<f32>) -> tensor<f32>

في هذا المثال، يتضمّن الطلب المخصّص وسيطة مخزن مؤقت واحدة وسمتَين، ويمكن لـ XLA FFI فك تشفيرهما تلقائيًا وتمريرهما إلى الدالة القابلة للاستدعاء التي يحدّدها المستخدِم.

auto handler = Ffi::Bind()
  .Arg<BufferR0<F32>>()
  .Attr<int32_t>("i32")
  .Attr<std::string_view>("str")
  .To([](BufferR0<F32> buffer, int32_t i32, std::string_view str) {
    return Error::Success();
  });

سمات التعداد التي يحدّدها المستخدِم

يمكن لـ XLA FFI فك تشفير سمات MLIR المتكاملة تلقائيًا إلى تعدادات يحدّدها المستخدِم. يجب أن يكون لنوع التعداد النوع المتكامل الأساسي نفسه، ويجب تسجيل فك التشفير بشكل صريح باستخدام XLA FFI.

%0 = "stablehlo.custom_call"(%arg0) {
  call_target_name = "foo",
  backend_config= {
    command = 0 : i32
  },
  api_version = 4 : i32
} : (tensor<f32>) -> tensor<f32>
enum class Command : int32_t {
  kAdd = 0,
  kMul = 1,
};

XLA_FFI_REGISTER_ENUM_ATTR_DECODING(Command);

auto handler = Ffi::Bind().Attr<Command>("command").To(
    [](Command command) -> Error { return Error::Success(); });

ربط جميع سمات الطلب المخصّص

من الممكن الوصول إلى جميع سمات الطلب المخصّص كقاموس وفك تشفير السمات المطلوبة فقط في وقت التشغيل.

auto handler = Ffi::Bind().Attrs().To([](Dictionary attrs) -> Error {
  ErrorOr<int32_t> i32 = attrs.get<int32_t>("i32");
  return Error::Success();
});

سمات البنية التي يحدّدها المستخدِم

يمكن لـ XLA FFI فك تشفير سمات القاموس إلى بُنى يحدّدها المستخدِم.

%0 = "stablehlo.custom_call"(%arg0) {
  call_target_name = "foo",
  backend_config= {
    range = { lo = 0 : i64, hi = 42 : i64 }
  },
  api_version = 4 : i32
} : (tensor<f32>) -> tensor<f32>

في المثال أعلاه، range هي سمة mlir::DictionaryAttr، وبدلاً من الوصول إلى حقول القاموس بالاسم، يمكن فك تشفيرها تلقائيًا كبنية C++. يجب تسجيل فك التشفير بشكل صريح باستخدام وحدة الماكرو XLA_FFI_REGISTER_STRUCT_ATTR_DECODING (في الخلفية، تحدّد هذه الوحدة عملية تخصيص نموذج في مساحة الاسم ::xla::ffi، لذا يجب إضافة وحدة الماكرو إلى مساحة الاسم العامة).

struct Range {
  int64_t lo;
  int64_t hi;
};

XLA_FFI_REGISTER_STRUCT_ATTR_DECODING(Range, StructMember<int64_t>("lo"),
                                             StructMember<int64_t>("hi"));

auto handler = Ffi::Bind().Attr<Range>("range").To([](Range range) -> Error{
  return Error::Success();
});

يمكن تحميل السمات المخصّصة من قاموس، تمامًا مثل أي سمة أخرى. في المثال أدناه، يتم فك تشفير جميع سمات الطلب المخصّص كـ Dictionary، ويمكن الوصول إلى range بالاسم.

auto handler = Ffi::Bind().Attrs().To([](Dictionary attrs) -> Error {
  ErrorOr<Range> range = attrs.get<Range>("range");
  return Error::Success();
});

إنشاء طلب مخصّص على وحدة المعالجة المركزية

يمكنك إنشاء تعليمات HLO تمثّل طلبًا مخصّصًا من خلال واجهة برمجة تطبيقات العميل في XLA. على سبيل المثال، يستخدم الرمز البرمجي التالي طلبًا مخصّصًا لحساب A[i] = B[i % 128]+ C[i] على وحدة المعالجة المركزية. (بالطبع يمكنك – ويجب عليك! – إجراء ذلك باستخدام HLO العادي).

#include "xla/client/xla_builder.h"
#include "xla/service/custom_call_target_registry.h"

void do_it() {
  xla::XlaBuilder b("do_it");
  xla::XlaOp param0 =
      xla::Parameter(&b, 0, xla::ShapeUtil::MakeShape(xla::F32, {128}), "p0");
  xla::XlaOp param1 =
      xla::Parameter(&b, 1, xla::ShapeUtil::MakeShape(xla::F32, {2048}), "p1");
  xla::XlaOp custom_call =
      xla::CustomCall(&b, "do_custom_call", /*operands=*/{param0, param1},
        /*shape=*/xla::ShapeUtil::MakeShape(xla::F32, {2048}),
        /*opaque=*/"", /*has_side_effect=*/false,
        /*output_operand_aliasing=*/{}, /*literal=*/nullptr,
        /*schedule=*/CustomCallSchedule::SCHEDULE_NONE,
        /*api_version=*/CustomCallApiVersion::API_VERSION_TYPED_FFI);
}

// Constrain custom call arguments to 1-dimensional buffers of F32 data type.
using BufferF32 = xla::ffi::BufferR1<xla::ffi::DataType::F32>;

// Implement a custom call as a C++ function. Note that we can use `Buffer` type
// defined by XLA FFI that gives us access to buffer data type and shape.
xla::ffi::Error do_custom_call(BufferF32 in0, BufferF32 in1,
                               xla::ffi::Result<BufferF32> out) {
  size_t d0 = in0.dimensions[0];
  size_t d1 = in1.dimensions[0];

  // Check that dimensions are compatible.
  assert(out->dimensions[0] == d1 && "unexpected dimensions");

  for (size_t i = 0; i < d1; ++i) {
    out->data[i] = in0.data[i % d0] + in1.data[i];
  }
}

// Explicitly define an XLA FFI handler signature and bind it to the
// `do_custom_call` implementation. XLA FFI handler can automatically infer
// type signature from the custom call function, but it relies on magical
// template metaprogramming an explicit binding provides and extra level of
// type checking and clearly states custom call author intentions.
XLA_FFI_DEFINE_HANDLER(handler, do_custom_call,
                       ffi::Ffi::Bind()
                           .Arg<Buffer>()
                           .Arg<Buffer>()
                           .Ret<Buffer>());

// Registers `handler` with and XLA FFI on a "Host" platform.
XLA_FFI_REGISTER_HANDLER(xla::ffi::GetXlaFfiApi(), "do_custom_call",
                         "Host", handler);

إنشاء طلب مخصّص على وحدة معالجة الرسومات

يكون تسجيل الطلب المخصّص لوحدة معالجة الرسومات باستخدام XLA FFI متطابقًا تقريبًا، والفرق الوحيد هو أنّه بالنسبة إلى وحدة معالجة الرسومات، عليك طلب تدفق أساسي للمنصة (تدفق CUDA أو ROCM) لتتمكّن من تشغيل النواة على الجهاز. في ما يلي مثال على CUDA يُجري عملية الحساب نفسها (A[i] = B[i % 128] + C[i]) مثل رمز وحدة المعالجة المركزية أعلاه.

void do_it() { /* same implementation as above */ }

__global__ custom_call_kernel(const float* in0, const float* in1, float* out) {
  size_t idx = blockIdx.x * blockDim.x + threadIdx.x;
  out[idx] = in0[idx % 128] + in1[idx];
}

void do_custom_call(CUstream stream, BufferF32 in0, BufferF32 in1,
                    xla::ffi::Result<BufferF32> out) {
  size_t d0 = in0.dimensions[0];
  size_t d1 = in1.dimensions[0];
  size_t d2 = out->dimensions[0];

  assert(d0 == 128 && d1 == 2048 && d2 == 2048 && "unexpected dimensions");

  const int64_t block_dim = 64;
  const int64_t grid_dim = 2048 / block_dim;
  custom_call_kernel<<<grid_dim, block_dim, 0, stream>>>(
    in0.data, in1.data, out->data);
}

XLA_FFI_DEFINE_HANDLER(handler, do_custom_call,
                       ffi::Ffi::Bind()
                           .Ctx<xla::ffi::PlatformStream<CUstream>>()
                           .Arg<BufferF32>()
                           .Arg<BufferF32>()
                           .Ret<BufferF32>());

XLA_FFI_REGISTER_HANDLER(xla::ffi::GetXlaFfiApi(), "do_custom_call",
                         "CUDA", handler);

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

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

تمرير الصفوف إلى الطلبات المخصّصة

ضَع في اعتبارك الطلب المخصّص التالي.

using xla::ShapeUtil;
using xla::F32;
Shape p0_shape = ShapeUtil::MakeTuple({
    ShapeUtil::MakeShape(F32, {32}),
    ShapeUtil::MakeTuple({
        ShapeUtil::MakeShape(F32, {64}),
        ShapeUtil::MakeShape(F32, {128}),
    }),
    ShapeUtil::MakeShape(F32, {256}),
});
xla::XlaOp p0 = xla::Parameter(0, p0_shape, "p0");

Shape out_shape = ShapeUtil::MakeTuple({
  ShapeUtil::MakeShape(F32, {512}),
  ShapeUtil::MakeShape(F32, {1024}),
});
xla::CustomCall(&b, "do_custom_call", /*operands=*/{p0}, out_shape, ...);

على كلٍّ من وحدة المعالجة المركزية ووحدة معالجة الرسومات، يتم تمثيل الصف في الذاكرة كمصفوفة من المؤشرات. عندما تستدعي XLA الطلبات المخصّصة باستخدام وسيطات أو نتائج الصف، فإنّها تُسطّحها وتمرّرها كوسيطات أو نتائج مخزن مؤقت عادية.

نتائج الصفوف كمخازن مؤقتة مؤقتة

تُعدّ مدخلات الصفوف للطلبات المخصّصة ميزة مفيدة، ولكنّها ليست ضرورية تمامًا. إذا لم تكن مدخلات الصفوف للطلبات المخصّصة متاحة، يمكنك دائمًا فك حزم الصفوف باستخدام get-tuple-element قبل تمريرها إلى الطلب المخصّص.

من ناحية أخرى، تتيح لك مخرجات الصفوف إجراء أشياء لا يمكنك إجراؤها بخلاف ذلك.

السبب الواضح لتوفّر مخرجات الصفوف هو أنّها الطريقة التي يعرض بها الطلب المخصّص (أو أي عملية XLA أخرى) مصفوفات متعدّدة ومستقلة.

ولكن بشكل أقل وضوحًا، فإنّ مخرجات الصفوف هي أيضًا طريقة لمنح الطلب المخصّص ذاكرة مؤقتة. نعم، يمكن أن تمثّل المخرجات مخزنًا مؤقتًا مؤقتًا. ضَع في اعتبارك أنّ المخزن المؤقت للإخراج يتضمّن الخاصية التي تسمح للعملية بالكتابة إليه، ويمكنها القراءة منه بعد الكتابة إليه. هذا هو بالضبط ما تريده من مخزن مؤقت مؤقت.

في المثال أعلاه، لنفترض أنّنا أردنا استخدام F32[1024] كمخزن مؤقت مؤقت. بعد ذلك، سنكتب HLO تمامًا كما هو موضح أعلاه، ولن نقرأ ببساطة فهرس الصف 1 من مخرجات الطلب المخصّص.