โมเดลข้อมูล
โปรแกรม 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);
ซึ่งทำหน้าที่ดังต่อไปนี้
- ติดตามอาร์กิวเมนต์ SSA ของ
func
และค่าTensor
ของรันไทม์ที่เกี่ยวข้องซึ่งระบุในargs
โดยใช้แผนที่ตารางสัญลักษณ์ - สำหรับการดำเนินการแต่ละรายการภายใน
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) เราต้องทดสอบประเภทที่รองรับทั้งหมดสำหรับทุกการดำเนินการหรือไม่
เราสามารถใช้กฎต่อไปนี้ร่วมกันในการตัดสินใจ
ขณะใช้ op หากมีโค้ดอยู่ในฟังก์ชัน
eval
ที่เกี่ยวข้องเพื่อจัดการกับประเภทหนึ่งๆ ก็จำเป็นต้องมีการทดสอบเพื่อให้ครอบคลุมประเภทนั้น ตัวอย่างเช่น สำหรับตัวเลือกadd
จะมีโค้ดพิเศษในการจัดการประเภทจำนวนเต็ม บูลีน จุดลอยตัว และประเภทซับซ้อน เราจึงต้องมีการทดสอบ 1 ครั้งสำหรับแต่ละประเภทหากมีการจัดการชุดประเภทในแบบเดียวกันในฟังก์ชัน
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) มีอะไรเกี่ยวกับรูปแบบการเขียนโค้ดของการทดสอบไหม
- ตรวจสอบว่าคุณใช้ชื่อจริงของอินพุต/เอาต์พุตแทนที่จะใช้ค่าเริ่มต้น เป็นค่า SSA (เช่น %0, %1 เป็นต้น)
- ตรวจสอบว่าการทดสอบใช้รูปแบบที่มีการพิมพ์เหมือนจริง (หากมี)
(G8) เราควรใส่ตัวอย่างที่ให้ไว้ในข้อกำหนดแล้วไหม ใช่ (สำหรับการทดสอบโดยสมบูรณ์)