ปลั๊กอินคอมไพเลอร์ LiteRT

ฉันควรสร้างปลั๊กอินคอมไพเลอร์เมื่อใด

ปลั๊กอินคอมไพเลอร์ LiteRT จำเป็นเมื่อคุณต้องการผสานรวมตัวเร่งฮาร์ดแวร์ที่เฉพาะเจาะจงกับทรัพยากร Dependency ของคอมไพเลอร์เข้ากับเฟรมเวิร์ก LiteRT

คุณควรสร้างปลั๊กอินคอมไพเลอร์ในกรณีต่อไปนี้

  1. คุณกําหนดเป้าหมายเป็นแบ็กเอนด์ฮาร์ดแวร์ใหม่ที่ไม่รองรับ
  2. คุณต้องการส่งต่อการดำเนินการโมเดลที่เฉพาะเจาะจงไปยังตัวเร่งฮาร์ดแวร์นั้นเพื่อประสิทธิภาพหรือการประหยัดพลังงาน
  3. คุณต้องรองรับการคอมไพล์ AOT (ในเวิร์กสเตชัน) หรือการคอมไพล์ในอุปกรณ์

ปลั๊กอินทำหน้าที่เป็นสะพาน โดยนำส่วนต่างๆ ของโมเดลแมชชีนเลิร์นนิงและ แปลงเป็นรูปแบบที่ฮาร์ดแวร์เป้าหมายของคุณสามารถเรียกใช้ได้ โดยใช้ การเรียกไปยังคอมไพเลอร์ของแบ็กเอนด์ LiteRT จะรวมไบต์โค้ดที่กำหนดเองซึ่งสร้างโดย ปลั๊กอินไว้ใน.tfliteโมเดล ทำให้สามารถเรียกใช้ได้โดยใช้รันไทม์ LiteRT

ปลั๊กอินคอมไพเลอร์ทำงานอย่างไร

เฟรมเวิร์ก LiteRT ใช้ปลั๊กอินคอมไพเลอร์ในระหว่างการโหลดโมเดลหรือ ขั้นตอนการประมวลผลล่วงหน้าแบบออฟไลน์เพื่อระบุและเตรียมกราฟย่อยของโมเดลสำหรับการ เรียกใช้ในฮาร์ดแวร์เป้าหมาย

กระบวนการนี้มี 2 ขั้นตอนหลักที่จัดระเบียบโดยเฟรมเวิร์กโดยใช้ฟังก์ชันที่ส่งออกจากปลั๊กอิน ดังนี้

  1. การแบ่งพาร์ติชัน: ปลั๊กอินจะตรวจสอบกราฟโมเดลทั้งหมดและระบุ ชุดย่อยของการดำเนินการที่รองรับและเร่งความเร็วได้อย่างมีประสิทธิภาพใน ฮาร์ดแวร์เป้าหมาย กราฟย่อยที่รองรับเหล่านี้จะได้รับการ "แบ่งพาร์ติชัน" (ทำเครื่องหมาย) เพื่อ การคอมไพล์และสรุป
  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. โมดูลรหัสไบต์: บัฟเฟอร์หน่วยความจำดิบอย่างน้อย 1 รายการที่มีรหัสไบต์ที่เรียกใช้งานได้เฉพาะฮาร์ดแวร์ (เช่น คำสั่งที่คอมไพล์แล้ว)
  2. ข้อมูลการโทร: ข้อมูลเมตาสำหรับแต่ละพาร์ติชัน ซึ่งจะให้การแมป จากกราฟย่อยอินพุตที่ 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 มีเครื่องมือต่างๆ สำหรับใช้ปลั๊กอินคอมไพเลอร์กับไฟล์โมเดล เรียกใช้ผลลัพธ์ และตรวจสอบ/เปรียบเทียบประสิทธิภาพ โปรดดูเอกสารประกอบชุดทดสอบ ตัวเร่งและเอกสารประกอบการเปรียบเทียบและการสร้างโปรไฟล์