تصميم أداة الترجمة الفورية

نموذج البيانات

برامج SttableHLO هي عمليات حسابية على موتّرات. (الصفائف متعددة الأبعاد)، والتي يتم تنفيذها في النموذج الحالي باستخدام الفئة Tensor. فئة التخزين الأساسية للعنصر Tensor، تُخزن الدالة detail::Buffer mlir::ShapedType لمتسق متوتر مع كائن mlir::HeapAsmResourceBlob يمثّل كائنًا ثنائي الأبعاد متقلّبًا البيانات محددة كصفيفة بايت متجاورة في ترتيب كبير إلى صغار. ويتم احتساب detail::Buffer عناصر كمرجع لتبسيط إدارة الذاكرة.

يتم تمثيل العناصر الفردية للمتزر باستخدام الفئة Element، التي اتحادًا تمييزيًا يضم أحد APInt أو APFloat أو pair<APFloat,APFloat> للتخزين. يُستخدم العنصر الأخير لتخزين العناصر ذات الأنواع المعقدة.

يتضمن Tensor واجهات برمجة التطبيقات التالية للتفاعل مع العناصر الفردية:

  • Element Tensor::get(llvm::ArrayRef<int64_t> index): لاستخراج عنصر متوتر فردي في الفهرس متعدد الأبعاد index على شكل Element الخاص بك.
  • void Tensor::set(llvm::ArrayRef<int64_t> index, Element element);: لتعديل كائن Element element إلى متسلسل متعدد الأبعاد الفهرس index.

آلية عمل ميزة "الترجمة الفورية"

تُعد وظيفة الإدخال للمترجم الفوري

SmallVector<Tensor> eval(func::FuncOp func, ArrayRef<Tensor> args);

وهو ما يلي:

  1. يتم تتبُّع وسيطات SSA في func وبيئة التشغيل المرتبطة بها Tensor. المقدمة في args، باستخدام خريطة جدول الرموز، M.
  2. لكل عملية ضمن func، بترتيب SSACFG:
    • لاستدعاء "eval" في العملية. لكل معامل SSA في العملية، استخرج قيمة بيئة التشغيل من M سيتم تقديمها كوسيطة لاستدعاء eval.
    • تتبع نتائج SSA الخاصة بالعملية والقيمة المُقيّمة في M.

إنّ مستوى العمليات eval المذكور في (2) مسؤول عن تنفيذ دلالات التنفيذ في العملية إليك مثال على السمة stablehlo::AddOp. في المثال، تكون العناصر الفردية لتوتر lhs وrhs زوجيًا. المستخرجة على أنّها عناصر Element تتم إضافتها بعد ذلك. نتيجة الإضافة، يُخزَّن كائن Element في متسلسل result الأخير.

Tensor eval(AddOp op, const Tensor &lhs, const Tensor &rhs) {
  Tensor result(op.getType());

  for (auto it = result.index_begin(); it != result.index_end(); ++it)
    result.set(*it, lhs.get(*it) + rhs.get(*it));

  return result;
}

بشكل عام، تم تحسين تصميم المترجم الفوري لإتاحة قراءة تنفيذ دوال eval للعمليات الفردية لأنها تهدف إلى تعمل كتنفيذ مرجعي لـ StableHLO. على سبيل المثال، بدلاً من وتعريف eval كدالة نموذج وترميزها بأنواع العناصر، نقوم بتغليف تفاصيل حول كيفية التعامل مع أنواع العناصر المختلفة في Element::operator+ وما إلى ذلك، يتم تبسيط تنفيذ eval.

استخدام ميزة الترجمة الفورية لطيّ الفيديوهات بشكل ثابت

يمكننا استخدام آلية الترجمة الفورية لطي العمليات بمعامل ثابت القيم. يوضح مقتطف الرمز التالي فكرة عن التنفيذ لطي stablehlo::AddOp باستخدام معاملات مكتوبة بنقاط عائمة:

OpFoldResult AddOp::fold(FoldAdaptor adaptor) {
  auto attrs = adaptor.getOperands();
  DenseElementsAttr lhsData = dyn_cast<DenseElementsAttr>(attrs[0]);
  DenseElementsAttr rhsData = dyn_cast<DenseElementsAttr>(attrs[1]);
  if (!lhsData || !rhsData) return {};

  auto lhs = Tensor(lhsData);
  auto rhs = Tensor(rhsData);
  auto result = eval(*this, lhs, rhs);

  SmallVector<APFloat> values;
  for (auto i = 0; i < result.getNumElements(); ++i) {
    Element element = result.get(i);
    values.push_back(cast<FloatAttr>(element.getValue()).getValue());
  }

  return DenseElementsAttr::get(result.getType(), values);
}

في الوقت الحالي، لا نعمل بنشاط على دمج الترجمة الفورية في الطي المستمر لأننا لا نخطط لتنفيذ المجلد في StableHLO. ولكننا نخطط في المستقبل للاستفادة من هذه الترجمة في MHLO، وعندها سنعمل على تحسين بيئة عمل مقتطف الرمز أعلاه (على سبيل المثال، يمكن أن يكون لدينا دالة مساعدة تجمع المعاملات الثابتة في Tensor عناصر وفكّ حزمة Tensor نتيجة إلى OpFoldResult).

اختبار المترجم الفوري StableHLO

يأخذ المُترجم الفوري قيم البيانات كمدخلات (A) برنامج StableHLO و (B) إلى البرنامج، وتنشئ قيم بيانات المخرجات التي تتم مطابقتها مقارنةً بقيم البيانات المتوقّعة المقدَّمة من المستخدِم قيم البيانات (B) هي غير قابل للتغيير في البرنامج نفسه باستخدام عمليات stablehlo.constant. تشير رسالة الأشكال البيانية يقيّم المترجم الفوري برنامج الإدخال. مخرجات العملية قيد الاختبار يتم التحقّق منه من خلال عمليات التحقّق (مثل check.expect_eq وcheck.expect_almost_eq)، باعتبارها كما هو موضح أدناه. يتحقق check.expect_eq وcheck.expect_eq_const من استخدام طريقة البتّ. المساواة لأي نوع متوافق وcheck.expect_almost_eq check.expect_almost_eq_const تتحقق من وجود مساواة قريبة ضمن مبدأ التفاوت وشرحها في إرشادات الاختبار (G6)، للنقاط العائمة والأنواع المعقّدة.

// CHECK-LABEL: Evaluated results of function: add_op_test_ui4
func.func @add_op_test_ui4() {
  %0 = stablehlo.constant dense<[0, 2]> : tensor<2xui4>
  %1 = stablehlo.constant dense<[15, 3]> : tensor<2xui4>
  %2 = stablehlo.add %0, %1 : tensor<2xui4>
  check.expect_eq_const %2, [15, 5] : tensor<2xui4>
  func.return
}

أداة اختبار stablehlo-translate --interpret (الرمز) ويكون مسئولاً عن تحليل البرنامج وتفسير كل دالة بما في ذلك العمليات التي تشكل الوظيفة. لدينا مجموعة اختبارية مخصصة تتكون من من عدة اختبارات تمارس سلوكيات مختلفة في بيئة التشغيل لكل عملية StableHLO يمكن العثور على الاختبارات هنا.

إرشادات بشأن إجراء الفحوصات

(G1) هل نحتاج إلى اختبار جميع الأنواع المتوافقة في كل عملية؟

يمكننا استخدام مجموعة من القواعد التالية لاتخاذ قرار:

  1. أثناء تنفيذ العملية، في حال توفُّر رمز في eval المقابل للتعامل مع نوع معين، فلا بد من إجراء اختبارات لتغطية هذا النوع. على سبيل المثال، بالنسبة إلى العملية add، يوجد رمز حصري لمعالجة الأعداد الصحيحة والمنطقية والنقاط العائمة والأنواع المعقدة، وبالتالي إلى اختبار واحد لكل فئة من الأنواع.

  2. إذا تمت معالجة مجموعة من الأنواع بشكل موحّد في دالة eval المقابلة، فمن المفترض أن يكون اختبار واحد لجميع هذه الأنواع كافيًا. على سبيل المثال، في العملية add، كل صيغ أنواع الأعداد الصحيحة (si4، u4، si8، u8 وما إلى ذلك) على حد سواء باستخدام واجهات برمجة تطبيقات llvm::APInt، وبالتالي يمكننا تخطي وإضافة اختبارات لكل صيغة، وإضافة اختبار تمثيلي. لتجنب الغموض في اختيار الممثل، اتّباع الإرشادات التالية:

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

(G2) كيف نتخذ قرارًا بشأن عدد الاختبارات اللازمة لتغطية حالة العمليات السلوك؟

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

(G3) ماذا عن إضافة اختبارات للبنية الأساسية لخدمة الترجمة الفورية؟

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

(G4) إذا كان تنفيذ العملية يعتمد على عمليات أخرى، هل يجب أن نكتب الخاصة بالخيار الثاني؟

لا، على سبيل المثال، يمكن أن يعتمد تنفيذ batch_norm_grad على "divide" و"subtract" و"multiply" وآخرون وينبغي أن نتجنب اختبار علامة التبويب الأخيرة والعمليات أثناء اختبار الأولى.

(G5) هل علينا كتابة اختبارات لإجراء عملية التنفيذ المحدّدة أو غير المحدّدة؟ وسلوكياتهم؟

وعلينا ألا نكتب الاختبارات التي تمارس السلوكيات غير المحددة للعملية الاختبارات التي تمارس على السلوكيات المحدّدة لعملية التنفيذ إظهار السلوك المحلي للمترجم الذي لا ينبغي معممة. لا تساهم الاختبارات التي تمارس سلوكًا غير محدد في فهم سلوك العملية.

(G6) أثناء كتابة اختبارات لأنواع النقاط العائمة، إلى أي مدى دقة هل يلزم تحديد النتيجة المتوقعة في عمليات التحقق؟

بالنسبة للعمليات الأولية (الجمع والطرح والضرب والقسمة في هذا الإطار)، فمن المتوقع أن يوفر أي تنفيذ يتبع مواصفات معهد الهندسة الكهربائية والإلكترونية (IEEE) إلى نتيجة تقريبية ضمن 0.5 ULP للنتيجة الدقيقة من الناحية الرياضية. ومع ذلك، أن تخيل بأمان النتيجة المتوقعة القادمة من هذه العمليات لتكون في على أكثر من ULP واحد. ومع ذلك، قد لا يصلح هذا الخيار مع الدوال التجاوزية. (sine، وcosine، وما إلى ذلك) التي تكون ضمانات الدقة لها محددة التنفيذ (الأسباب).

تعتمد عملية التنفيذ الحالية استراتيجية "واحد يناسب الجميع" قيمة التفاوت 0.0001. يوضح المثال التالي مبدأ التسامح أعلاه.

func.func @check_tolerance() {
  %0 = stablehlo.constant dense<0.2> : tensor<f32>

  // The following check succeeds as %0 is almost equal to the provided
  // constant modulo the tolerance, mentioned above.
  check.expect_almost_eq_const %0, dense<0.19999> : tensor<f32>

  // The following check fails as %0 is not bitwise equal to the provided
  // constant.
  check.expect_eq_const %0, dense<0.19999> : tensor<f32>

  func.return
}

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

(G7) هل هناك أي معلومات عن أسلوب الترميز في الاختبارات؟

  1. تأكَّد من استخدام الاسم الفعلي للمدخلات/المُخرجات بدلاً من الاسم التلقائي. إلى قيم SSA (مثل %0 و%1 وما إلى ذلك)
  2. التأكد من أن الاختبارات تستخدم تنسيقًا مطبوعًا بالكامل، إن وجد.

(G8) هل علينا تضمين المثال المقدَّم في المواصفات؟ نعم (لاكتمال الاختبار).