การออกแบบโหมดล่าม

โมเดลข้อมูล

โปรแกรม StableHLO คือการคำนวณผ่าน tensors (อาร์เรย์ n-dimensional) ซึ่งในรูปแบบปัจจุบันจะใช้คลาส Tensor คลาสพื้นที่เก็บข้อมูลที่สำคัญของออบเจ็กต์ Tensor ชื่อ detail::Buffer จะจัดเก็บ mlir::ShapedType ของ Tensor พร้อมกับออบเจ็กต์ mlir::HeapAsmResourceBlob ที่แสดงถึง BLOB ที่เปลี่ยนแปลงได้ของ tensor ในรูปแบบไบต์อาร์เรย์ต่อเนื่องในลำดับหลักถึงรอง ระบบจะนับออบเจ็กต์ detail::Buffer รายการเป็นการอ้างอิงเพื่อลดความซับซ้อนในการจัดการหน่วยความจำ

องค์ประกอบแต่ละรายการของ tensor แสดงโดยใช้คลาส Element ซึ่งใช้สหภาพที่เลือกปฏิบัติที่มี APInt, APFloat หรือ pair<APFloat,APFloat> สำหรับพื้นที่เก็บข้อมูล ประเภทสุดท้ายใช้สำหรับจัดเก็บองค์ประกอบ ที่มีประเภทที่ซับซ้อน

Tensor มี API ต่อไปนี้เพื่อโต้ตอบกับองค์ประกอบแต่ละรายการ

  • Element Tensor::get(llvm::ArrayRef<int64_t> index): หากต้องการแยกองค์ประกอบ Tensor แต่ละรายการที่ดัชนีหลายมิติ index เป็นออบเจ็กต์ Element
  • void Tensor::set(llvm::ArrayRef<int64_t> index, Element element);: หากต้องการอัปเดตออบเจ็กต์ Element element เป็น Tensor ที่ดัชนีหลายมิติ index

วิธีการทำงานของล่าม

ฟังก์ชันป้อนข้อมูลไปยังอินเตอร์พรีเตอร์คือ

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

ซึ่งทำหน้าที่ดังต่อไปนี้

  1. ติดตามอาร์กิวเมนต์ SSA ของ func และค่า Tensor ของรันไทม์ที่เกี่ยวข้องซึ่งระบุใน args โดยใช้แผนที่ตารางสัญลักษณ์
  2. สำหรับการดำเนินการแต่ละรายการภายใน func ในลำดับ SSACFG
    • เรียกใช้ eval ในคำสั่ง สำหรับตัวถูกดำเนินการ SSA แต่ละรายการของการดำเนินการ ให้ดึงค่ารันไทม์ของการดำเนินการจาก M เพื่อเป็นอาร์กิวเมนต์ของการเรียกใช้ eval
    • ติดตามผลลัพธ์ SSA ของการทดสอบและค่าที่ประเมินแล้วในหน่วย M

eval ระดับการดำเนินการที่กล่าวถึงใน (2) มีหน้าที่รับผิดชอบในการใช้อรรถศาสตร์การดำเนินการของ op ข้อมูลต่อไปนี้เป็นตัวอย่างของ stablehlo::AddOp ในตัวอย่างนี้ องค์ประกอบแต่ละรายการของ tensors lhs และ rhs จะแยกออกแบบจับคู่เป็นออบเจ็กต์ Element แล้วเพิ่มเข้าไป ผลของการเพิ่มออบเจ็กต์ Element จะจัดเก็บไว้ใน tensor 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 = attrs[0].dyn_cast<DenseElementsAttr>();
  DenseElementsAttr rhsData = attrs[1].dyn_cast<DenseElementsAttr>();
  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(element.getValue().cast<FloatAttr>().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 (code) มีหน้าที่ในการแยกวิเคราะห์โปรแกรม ตีความแต่ละฟังก์ชัน รวมถึงการดำเนินการที่ประกอบเป็นฟังก์ชัน เรามีชุดการทดสอบเฉพาะซึ่งประกอบด้วยการทดสอบหลายรายการที่ใช้ลักษณะการทำงานของรันไทม์ที่หลากหลายสำหรับ StableHLO Op แต่ละรายการ ดูการทดสอบได้ที่นี่ (เช่น translated_*.mlir)

หลักเกณฑ์การตรวจหาเชื้อไวรัส

(G1) เราต้องทดสอบประเภทที่รองรับทั้งหมดสำหรับทุกการดำเนินการหรือไม่

เราสามารถใช้กฎต่อไปนี้ร่วมกันในการตัดสินใจ

  1. ขณะใช้ op หากมีโค้ดอยู่ในฟังก์ชัน eval ที่เกี่ยวข้องเพื่อจัดการกับประเภทหนึ่งๆ ก็จำเป็นต้องมีการทดสอบเพื่อให้ครอบคลุมประเภทนั้น ตัวอย่างเช่น สำหรับตัวเลือก add จะมีโค้ดพิเศษในการจัดการประเภทจำนวนเต็ม บูลีน จุดลอยตัว และประเภทซับซ้อน เราจึงต้องมีการทดสอบ 1 ครั้งสำหรับแต่ละประเภท

  2. หากมีการจัดการชุดประเภทในแบบเดียวกันในฟังก์ชัน eval ที่เกี่ยวข้อง การทดสอบ 1 รายการสำหรับประเภทเหล่านั้นทั้งหมดก็น่าจะเพียงพอแล้ว ตัวอย่างเช่น สำหรับการดำเนินการ add ระบบจะจัดการตัวแปรทั้งหมดของประเภทจำนวนเต็ม (si4, u4, si8, u8 และอื่นๆ) โดยใช้ API ของ llvm::APInt เราจึงข้ามการเพิ่มการทดสอบสำหรับตัวแปรเหล่านั้นแต่ละรายการได้ และเพิ่มการทดสอบแบบเป็นตัวแทนเพียงรายการเดียวแทน เราควรใช้หลักเกณฑ์ต่อไปนี้เพื่อหลีกเลี่ยงความคลุมเครือในการเลือกตัวแทน

    • หากประเภททั้งหมดที่จัดการในแบบเดียวกันมีประเภทพื้นฐานเหมือนกัน (เช่น หากทั้งหมดเป็นประเภทจำนวนเต็ม จุดลอย หรือเชิงซ้อน) ให้เลือกประเภทที่มีความกว้างบิตสูงสุด
    • หากประเภททั้งหมดซึ่งมีการจัดการในแบบเดียวกันมีประเภทดั้งเดิมผสมกัน ให้เลือกประเภทที่มีประเภทพื้นฐานต่อไปนี้ โดยเรียงลำดับจากมากไปหาน้อยคือ จำนวนเต็ม จุดลอย บูลีน ซับซ้อน

(G2) เราเลือกจำนวนการทดสอบที่จำเป็นเพื่อให้ครอบคลุมพฤติกรรมของการดำเนินการได้อย่างไร

เป้าหมายคือเพื่อให้ครอบคลุมตรรกะของล่ามสำหรับการดำเนินการทุกประเภท (เช่น ทุกกรณีของการนำไปใช้งาน) โดยใช้การทดสอบเพียงเล็กน้อย การลดจํานวนการทดสอบให้เหลือน้อยที่สุดเป็นสิ่งสำคัญต่อการบำรุงรักษา ยิ่งเรามีการทดสอบน้อย การทดสอบเหล่านั้นจะง่ายขึ้นและทำให้แน่ใจว่าการทดสอบนั้นครอบคลุมการทดสอบได้อย่างครอบคลุม ด้วยเหตุนี้ เราจึงคาดหวังว่าการดำเนินการที่ง่ายขึ้นส่วนใหญ่จะมีการทดสอบเพียงรายการเดียว หากมีเหตุผลที่ดีที่ความครอบคลุม ครอบคลุมไม่ในทางปฏิบัติ คุณก็สามารถหยุดที่ >= 90% ได้ โดยจะมีการพิจารณาเป็นกรณีๆ ไปในระหว่างการตรวจสอบการดึงคำขอ

(G3) ลองเพิ่มการทดสอบสำหรับโครงสร้างพื้นฐานของล่ามไหม

โครงสร้างพื้นฐานของล่ามส่วนใหญ่ตรงไปตรงมาและสามารถเพิ่มในฐานความน่าเชื่อถือของเราได้ สิ่งเดียวที่ไม่สำคัญคือวิธีการบรรจุประเภทต่างๆ เข้าและคลายแพ็กจากพื้นที่เก็บข้อมูลอินเตอร์พรีเตอร์ ตามที่กล่าวไว้ใน (G1) เราจะทดสอบ เฉพาะการดำเนินการประเภทซึ่งมีการจัดการแตกต่างกัน เนื่องจากอาจเป็นไปได้ว่ารหัสการบรรจุ/ยกเลิกการบรรจุหีบห่อซึ่งสอดคล้องกับตัวแปรของจำนวนเต็ม/จุดลอยตัวที่แตกต่างกันอาจไม่ครอบคลุมทั้งหมดระหว่างการทดสอบ เพื่อให้ครอบคลุมทั้งหมด เราเลือกการดำเนินการอย่าง constant ที่รองรับองค์ประกอบ StableHLO ทุกประเภทและเขียนการทดสอบอย่างละเอียดได้

(G4) หากการดำเนินการของการดำเนินการทำงานขึ้นอยู่กับการดำเนินการอื่นๆ เราควรเขียนการทดสอบสำหรับตัวเลือกหลังไหม

ไม่ได้ ตัวอย่างเช่น การใช้ batch_norm_grad อาจอิงตาม divide, subtract, multiply และอื่นๆ เราควรหลีกเลี่ยงการทดสอบตัวเลือกหลัง ขณะทดสอบเวอร์ชันก่อน

(G5) เราควรเขียนการทดสอบสำหรับพฤติกรรมที่กำหนดตามการใช้งาน / ที่ไม่ได้กำหนดไหม

เราไม่ควรเขียนการทดสอบที่มีพฤติกรรมที่กำหนดหรือไม่มีการกำหนดของการใช้งานของการทดสอบ การทดสอบที่ใช้พฤติกรรมที่กำหนดโดยการนำไปใช้จะแสดงให้เห็นพฤติกรรมในท้องถิ่นของล่ามที่ไม่ควรทำให้เป็นแบบทั่วไป การทดสอบที่ใช้ลักษณะการทำงานที่ไม่ได้กำหนดจะไม่ส่งผลต่อความเข้าใจในลักษณะการทำงานของการดำเนินการดังกล่าว

(G6) ขณะเขียนการทดสอบสำหรับประเภทจุดลอยตัว จะต้องระบุผลลัพธ์ที่คาดหวังในการตรวจสอบด้วยความแม่นยำในระดับใด

สำหรับการดำเนินการขั้นทุติยภูมิ (การบวก การลบ การคูณ การหาร และสี่เหลี่ยมจัตุรัส) การใช้งานที่เป็นไปตามข้อกำหนดของ IEEE ควรให้ผลการปัดเศษภายใน 0.5 ULP ของผลลัพธ์ที่ได้ทางคณิตศาสตร์ อย่างไรก็ตาม เรานึกภาพผลลัพธ์ที่คาดหวังจากการดําเนินการเหล่านี้โดยอยู่ห่างกันไม่เกิน 1 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) เราควรใส่ตัวอย่างที่ให้ไว้ในข้อกำหนดแล้วไหม ใช่ (สำหรับการทดสอบโดยสมบูรณ์)