ฉันควรสร้างปลั๊กอินคอมไพเลอร์เมื่อใด
ปลั๊กอินคอมไพเลอร์ LiteRT จำเป็นเมื่อคุณต้องการผสานรวมตัวเร่งฮาร์ดแวร์ที่เฉพาะเจาะจงกับทรัพยากร Dependency ของคอมไพเลอร์เข้ากับเฟรมเวิร์ก LiteRT
คุณควรสร้างปลั๊กอินคอมไพเลอร์ในกรณีต่อไปนี้
- คุณกําหนดเป้าหมายเป็นแบ็กเอนด์ฮาร์ดแวร์ใหม่ที่ไม่รองรับ
- คุณต้องการส่งต่อการดำเนินการโมเดลที่เฉพาะเจาะจงไปยังตัวเร่งฮาร์ดแวร์นั้นเพื่อประสิทธิภาพหรือการประหยัดพลังงาน
- คุณต้องรองรับการคอมไพล์ AOT (ในเวิร์กสเตชัน) หรือการคอมไพล์ในอุปกรณ์
ปลั๊กอินทำหน้าที่เป็นสะพาน โดยนำส่วนต่างๆ ของโมเดลแมชชีนเลิร์นนิงและ
แปลงเป็นรูปแบบที่ฮาร์ดแวร์เป้าหมายของคุณสามารถเรียกใช้ได้ โดยใช้
การเรียกไปยังคอมไพเลอร์ของแบ็กเอนด์ LiteRT จะรวมไบต์โค้ดที่กำหนดเองซึ่งสร้างโดย
ปลั๊กอินไว้ใน.tfliteโมเดล ทำให้สามารถเรียกใช้ได้โดยใช้รันไทม์ LiteRT
ปลั๊กอินคอมไพเลอร์ทำงานอย่างไร
เฟรมเวิร์ก LiteRT ใช้ปลั๊กอินคอมไพเลอร์ในระหว่างการโหลดโมเดลหรือ ขั้นตอนการประมวลผลล่วงหน้าแบบออฟไลน์เพื่อระบุและเตรียมกราฟย่อยของโมเดลสำหรับการ เรียกใช้ในฮาร์ดแวร์เป้าหมาย
กระบวนการนี้มี 2 ขั้นตอนหลักที่จัดระเบียบโดยเฟรมเวิร์กโดยใช้ฟังก์ชันที่ส่งออกจากปลั๊กอิน ดังนี้
- การแบ่งพาร์ติชัน: ปลั๊กอินจะตรวจสอบกราฟโมเดลทั้งหมดและระบุ ชุดย่อยของการดำเนินการที่รองรับและเร่งความเร็วได้อย่างมีประสิทธิภาพใน ฮาร์ดแวร์เป้าหมาย กราฟย่อยที่รองรับเหล่านี้จะได้รับการ "แบ่งพาร์ติชัน" (ทำเครื่องหมาย) เพื่อ การคอมไพล์และสรุป
- การคอมไพล์: เฟรมเวิร์ก LiteRT จะส่งกราฟย่อยที่แบ่งพาร์ติชันกลับ ไปยังปลั๊กอิน จากนั้นปลั๊กอินจะใช้ตรรกะภายในและอาจใช้ชุดเครื่องมือ (คอมไพเลอร์) ภายนอก เพื่อสร้างโมดูลโค้ดไบต์เฉพาะฮาร์ดแวร์อย่างน้อย 1 โมดูลที่ใช้การแบ่งพาร์ติชัน ไบต์โค้ดนี้คือสิ่งที่รันไทม์ (HAL/ไดรเวอร์) ของฮาร์ดแวร์เป้าหมาย จะโหลดและเรียกใช้ในที่สุด
เฟรมเวิร์กจะแทนที่กราฟย่อยเดิมด้วยการดำเนินการที่กำหนดเองซึ่งเรียกใช้ ไดรเวอร์ฮาร์ดแวร์ โดยส่งต่อไบต์โค้ดที่คอมไพล์แล้วซึ่งสร้างขึ้นโดยปลั๊กอิน
LiteRT Dispatch เป็นอะนาล็อกรันไทม์สำหรับปลั๊กอินคอมไพเลอร์ โดยจะให้ วิธีการเรียกใช้ HAL ตามเอาต์พุตของคอมไพเลอร์ ดูรายละเอียดเพิ่มเติมได้ที่เอกสารประกอบเกี่ยวกับการจัดส่ง
AOT เทียบกับในอุปกรณ์
LiteRT สามารถใช้ปลั๊กอินคอมไพเลอร์เพื่อรองรับการคอมไพล์ AOT ผ่านเครื่องมือของเรา รวมถึงการคอมไพล์ในอุปกรณ์ด้วย การคอมไพล์ในอุปกรณ์มีความยืดหยุ่นมากกว่า ภายใน API รันไทม์ของ LiteRT อย่างเต็มรูปแบบ และต้องมีการจัดการ โมเดลเดียวเท่านั้น โฟลว์ AOT สามารถยกเลิกการบล็อกการคอมไพล์เมื่อใช้ทรัพยากรมากเกินไป ที่จะเรียกใช้ในอุปกรณ์ ซึ่งอาจเป็นกรณีของโมเดลขนาดใหญ่ร่วมสมัยหลายๆ โมเดล
Fallback
LiteRT สร้างขึ้นโดยรองรับกราฟแบบไม่เหมือนกัน การดำเนินการใดๆ ที่ปลั๊กอินไม่ได้เลือกจะปล่อยให้ CPU ดำเนินการหรือพร้อมใช้งานสำหรับการเร่งความเร็วในแบ็กเอนด์อื่น
การติดตั้งใช้งานปลั๊กอินคอมไพเลอร์
ปลั๊กอินคอมไพเลอร์ LiteRT จะได้รับการติดตั้งใช้งานเป็นไลบรารีที่ใช้ร่วมกันซึ่งส่งออกชุดฟังก์ชัน C ที่เฉพาะเจาะจงซึ่งกำหนดไว้ใน LiteRT C API
ฟังก์ชันอินเทอร์เฟซที่สำคัญ
ฟังก์ชันหลักจะเกี่ยวข้องกับขั้นตอนการคอมไพล์ที่สำคัญ 2 ขั้นตอน ได้แก่
LiteRtCompilerPluginPartition และ LiteRtCompilerPluginCompile
| ฟังก์ชัน | วัตถุประสงค์ |
|---|---|
| LiteRtCompilerPluginPartition | เลือกและทําเครื่องหมายการดำเนินการที่รองรับทั้งหมดภายในกราฟย่อยของโมเดลที่กําหนด (ขั้นตอนพาร์ติชัน) |
| LiteRtCompilerPluginCompile$ | สร้างไบต์โค้ดเฉพาะฮาร์ดแวร์สำหรับพาร์ติชันที่เลือกไว้ล่วงหน้า (ขั้นตอนคอมไพล์) |
ข้อมูลโค้ด C API
// Name associated with the manufacturer this plugin relates to.
LITERT_CAPI_EXPORT const char* LiteRtGetCompilerPluginSocManufacturer();
// Create and initialize the plugin instance.
LITERT_CAPI_EXPORT LiteRtStatus
LiteRtCreateCompilerPlugin(LiteRtCompilerPlugin* compiler_plugin,
LiteRtEnvironmentOptions env, LiteRtOptions options);
// Choose ops for compilation.
// This is the PARTITION step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtSubgraph subgraph, LiteRtOpList selected_ops);
// Prepare result to pass to the runtime for given model containing partitioned
// subgraphs. This is the COMPILE step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtModel partitions, LiteRtCompiledResult* compiled_result);
1. ฟังก์ชันพาร์ติชัน
ลายเซ็นของฟังก์ชันคือ
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtSubgraph subgraph, LiteRtOpList selected_ops);
สิ่งที่ฟังก์ชัน partition ทำ: นี่คือเฟสการเลือก ปลั๊กอินจะวนซ้ำการดำเนินการในอินพุต LiteRtSubgraph สำหรับการดำเนินการทุกอย่างที่ฮาร์ดแวร์เป้าหมายรองรับและเร่งความเร็วได้ ปลั๊กอินจะเพิ่มการดำเนินการนั้นลงใน LiteRtOpList$ ที่ระบุไว้ในพารามิเตอร์ selected_ops
เฟรมเวิร์ก LiteRt ใช้รายการนี้เพื่อกำหนดขอบเขตของ
พาร์ติชันที่จะส่งไปยังขั้นตอนการคอมไพล์ขั้นสุดท้าย
โดยค่าเริ่มต้น LiteRT จะจัดกลุ่มการดำเนินการที่เลือกทั้งหมดเป็น DAG ย่อยที่ใหญ่ที่สุดเท่าที่จะเป็นไปได้ หากต้องการแบ่งพาร์ติชันที่ละเอียดยิ่งขึ้น คุณสามารถเชื่อมโยงดัชนีเมื่อ เลือกการดำเนินการซึ่งจะช่วยแบ่งกราฟย่อยเหล่านี้ออกเป็นส่วนๆ เพิ่มเติม
2. ฟังก์ชันคอมไพล์
ลายเซ็นของฟังก์ชันคือ
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtModel partitions, LiteRtCompiledResult* compiled_result);
ฟังก์ชัน compile ทำอะไร: นี่คือระยะการสร้าง
อินพุต partitions แสดงถึงโมเดลที่แยกกราฟย่อยที่เลือกทั้งหมด
แล้ว ปลั๊กอินจะประมวลผลพาร์ติชันเหล่านี้โดยเรียกใช้
เครื่องมือเฉพาะเพื่อสร้างไบต์โค้ดสำหรับฮาร์ดแวร์เป้าหมาย คาดว่าเอาต์พุตของปลั๊กอินจะให้จุดแรกเข้าสำหรับแต่ละกราฟย่อยที่ส่งผ่านสำหรับการคอมไพล์ ในกรณีส่วนใหญ่จะเป็นโมดูลโค้ดไบต์แต่ละโมดูลสำหรับแต่ละกราฟย่อยอินพุต หรือโมดูลโค้ดไบต์เดียวที่มีจุดแรกเข้าหลายจุด
ประเภทของข้อมูลที่ compile แสดงผล: ฟังก์ชัน LiteRtCompilerPluginCompile
จะแสดงผลลัพธ์โดยใช้พารามิเตอร์เอาต์ LiteRtCompiledResult
LiteRtCompiledResult เป็นแฮนเดิลที่ไม่โปร่งใส (เมื่อเทียบกับ LiteRT) ของโครงสร้างที่ปลั๊กอินจัดการ ซึ่งแสดงถึงเอาต์พุตของการคอมไพล์
และมีข้อมูลหลัก 2 ส่วน ได้แก่
- โมดูลรหัสไบต์: บัฟเฟอร์หน่วยความจำดิบอย่างน้อย 1 รายการที่มีรหัสไบต์ที่เรียกใช้งานได้เฉพาะฮาร์ดแวร์ (เช่น คำสั่งที่คอมไพล์แล้ว)
- ข้อมูลการโทร: ข้อมูลเมตาสำหรับแต่ละพาร์ติชัน ซึ่งจะให้การแมป
จากกราฟย่อยอินพุตที่
iไปยังโมดูลรหัสไบต์ผลลัพธ์และตัวระบุ จุดแรกเข้าในโมดูลนั้น
ตัวอย่างการใช้งาน
ข้อมูลโค้ดต่อไปนี้แสดงให้เห็นว่าปลั๊กอินพื้นฐานอาจใช้ฟังก์ชันหลักอย่างไร
ตัวอย่างนี้มาจากตัวอย่างที่ทำงานได้อย่างสมบูรณ์ใน
litert/vendors/examples/
การระบุและการตั้งค่าปลั๊กอิน
ฟังก์ชันเหล่านี้จะให้ข้อมูลพื้นฐานเกี่ยวกับปลั๊กอิน และฮาร์ดแวร์แก่เฟรมเวิร์ก
// Define the plugin's internal state structure
struct LiteRtCompilerPluginT {};
// Identify the manufacturer
const char* LiteRtGetCompilerPluginSocManufacturer() {
return "AcmeCorp"; // Example manufacturer name
}
// Specify the supported hardware (in this example, it supports kLiteRtHwAcceleratorNpu)
LiteRtStatus LiteRtGetCompilerPluginSupportedHardware(
LiteRtCompilerPlugin compiler_plugin,
LiteRtHwAccelerators* supported_hardware) {
// ... argument checking ...
*supported_hardware = kLiteRtHwAcceleratorNpu;
return kLiteRtStatusOk;
}
ตรรกะการแบ่งพาร์ติชัน (LiteRtCompilerPluginPartition)
ตัวอย่างนี้แสดงปลั๊กอินที่เลือกการดำเนินการแบบจำกัด (mul,
sub และการดำเนินการแบบคอมโพสิตที่เฉพาะเจาะจง) เฉพาะในกรณีที่อินพุตและเอาต์พุตทั้งหมดเป็นโฟลตแบบ 32 บิต
โดยปกติแล้ว การพิจารณาว่าจะเลือกการดำเนินการหรือไม่จะ
รวมถึงการเรียกใช้ Hook การตรวจสอบใน Toolchain คอมไพเลอร์ของแบ็กเอนด์
LiteRtStatus LiteRtCompilerPluginPartition(LiteRtCompilerPlugin compiler_plugin,
const char* soc_model,
LiteRtSubgraph subgraph,
LiteRtOpList selected_ops) {
// Iterate over ops and check criteria for selection
// (using a C++ wrapper namespace '::litert' for convenience).
// `subgraph` is a single subgraph from the original model, as such
// this function will be called for each subgraph in the original model.
::litert::Subgraph main_subgraph(subgraph);
for (const auto& op : main_subgraph.Ops()) {
// 1. Check a constraint: require all tensors to be Float32
bool only_f32 = true;
// ... logic to check input/output types ...
if (!only_f32) {
continue;
}
// 2. Check op codes and push to selected_ops list
if (op.Code() == kLiteRtOpCodeTflMul) {
LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
} else if (op.Code() == kLiteRtOpCodeTflSub) {
LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
} else if (op.Code() == kLiteRtOpCodeShloComposite) {
// Example of checking composite op options
// ... logic to check for "odml.rms_norm" name ...
LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
}
}
return kLiteRtStatusOk;
}
ก่อนที่จะเรียกการคอมไพล์ LiteRT จะตรวจสอบและ "ร่าง" การดำเนินการที่เลือกทั้งหมดลงในกราฟย่อยใหม่ในโมเดลกลางใหม่ โมเดลระดับกลางนี้คือสิ่งที่ส่งไปยังการคอมไพล์
ตรรกะการรวบรวม (LiteRtCompilerPluginCompile)
ฟังก์ชันนี้จะใช้กราฟย่อยที่แบ่งพาร์ติชันและสร้างLiteRtCompiledResultที่กำหนดเอง
ตัวอย่างนี้สร้างโมดูลไบต์โค้ดแบบสแตนด์อโลนสำหรับ
แต่ละพาร์ติชันที่จะคอมไพล์ ในกรณีจริง การดำเนินการนี้มักเกี่ยวข้องกับการแปลง
การดำเนินการ LiteRT เป็นประเภทต่างๆ ในไลบรารีคอมไพเลอร์แบ็กเอนด์ ตัวอย่างฟังก์ชัน
ปลั๊กอิน "การคอมไพล์" จะสร้างสตริงที่มนุษย์อ่านได้ซึ่งเข้ารหัสกราฟ
// Internal structure defining the compiled output
struct LiteRtCompiledResultT {
std::vector<std::string> byte_code; // The hardware bytecode buffers
std::vector<std::string> per_op_data; // Per-call metadata (CallInfo)
};
LiteRtStatus LiteRtCompilerPluginCompile(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtModel partitions, LiteRtCompiledResult* compiled_result) {
// 1. Create the internal result structure
auto model = litert::Model::CreateFromNonOwnedHandle(partitions);
const auto num_partitions = model.NumSubgraphs();
auto result = std::make_unique<LiteRtCompiledResultT>();
result->byte_code.resize(num_partitions);
result->per_op_data.resize(num_partitions);
// 2. Iterate and compile each partition
for (auto i = 0; i < num_partitions; ++i) {
// CompileSinglePartition is an internal helper that converts the subgraph
// into the target hardware's format and stores it in result->byte_code.
// In the case of the example this is just a stringification of the graph.
// ... internal call to CompileSinglePartition ...
// Example: result.byte_code[i] = generated_hw_code;
// Example: result.per_op_data[i] = absl::StrFormat("Partition_%d", i);
// The "per_op_data" is a unique identifier associated to the `ith` partition.
// This is analogous to the name of a function in a library.
// This is only meaningful when the plugin is preparing single modules with multiple entry points.
}
// 3. Pass ownership of the result back to the framework
*compiled_result = result.release();
return kLiteRtStatusOk;
}
// Functions to expose the compiled result data to the framework
LiteRtStatus LiteRtGetCompiledResultByteCode(
LiteRtCompiledResult compiled_result, LiteRtParamIndex byte_code_idx,
const void** byte_code, size_t* byte_code_size) {
// ... implementation reads from compiled_result->byte_code ...
}
// ... other LiteRtGetCompiledResult* functions ...
การใช้งานและการตรวจสอบ
LiteRT มีเครื่องมือต่างๆ สำหรับใช้ปลั๊กอินคอมไพเลอร์กับไฟล์โมเดล เรียกใช้ผลลัพธ์ และตรวจสอบ/เปรียบเทียบประสิทธิภาพ โปรดดูเอกสารประกอบชุดทดสอบ ตัวเร่งและเอกสารประกอบการเปรียบเทียบและการสร้างโปรไฟล์