การออกแบบของล่าม

โมเดลข้อมูล

โปรแกรม StableHLO เป็นการคำนวณผ่าน Tensor (อาร์เรย์ 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 โดยใช้แผนที่ตารางสัญลักษณ์ M
  2. สำหรับแต่ละการดำเนินการภายใน func ตามลำดับ SSACFG
    • เรียกใช้ eval ในแอป สำหรับตัวถูกดำเนินการ SSA แต่ละรายการของ op ให้แยก ค่ารันไทม์จาก M สำหรับระบุเป็นอาร์กิวเมนต์ของการเรียกใช้ eval
    • ติดตามผลลัพธ์ SSA ของการดำเนินการและค่าที่ประเมินเป็น M

eval ระดับการดำเนินการที่กล่าวถึงใน (2) รับผิดชอบในการใช้ ความหมายของการดำเนินการของอภิปราย ต่อไปนี้เป็นตัวอย่างสำหรับ stablehlo::AddOp ในตัวอย่างนี้ เอลิเมนต์เดี่ยวของ tensor ของ 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 = 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 Op. แต่ละรายการ คุณดูการทดสอบได้ที่นี่

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

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

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

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

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

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

(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) เราควรรวมตัวอย่างที่ให้ไว้แล้วในข้อมูลจำเพาะหรือไม่ ได้ (เพื่อความครบถ้วนของการทดสอบ)