LiteRT 編譯器外掛程式

何時該建立編譯器外掛程式?

如要將特定硬體加速器與編譯器依附元件整合至 LiteRT 架構,就必須使用 LiteRT 編譯器外掛程式

如有下列情況,請建立編譯器外掛程式:

  1. 您指定的新硬體後端不受支援。
  2. 您想將特定模型作業卸載至該硬體加速器,以提升效能或電源效率。
  3. 您需要支援 AOT 編譯 (在工作站上) 或裝置端編譯。

這個外掛程式可做為橋樑,擷取機器學習模型的部分內容,並透過呼叫後端編譯器,將這些內容轉換為目標硬體可執行的格式。LiteRT 會將外掛程式產生的自訂位元組碼,封裝到 .tflite 模型中,以便使用 LiteRT 執行階段執行。

編譯器外掛程式的運作方式

在模型載入或離線預先處理階段,LiteRT 架構會使用編譯器外掛程式,識別及準備要在目標硬體上執行的模型子圖。

這個程序包含兩個主要階段,架構會使用外掛程式匯出的函式來協調這兩個階段:

  1. 分割:外掛程式會檢查整個模型圖,找出支援的作業子集,並在目標硬體上有效加速。這些支援的子圖會「分割」(標示) 以供編譯和概述。
  2. 編譯:LiteRT 架構會將分割的子圖傳回外掛程式。外掛程式接著會使用內部邏輯和可能的外部工具鍊 (編譯器),產生一或多個實作分割區的硬體專屬位元碼模組。目標硬體的執行階段 (HAL/驅動程式) 最終會載入並執行這個位元碼。

這個架構會以自訂作業取代原始子圖,藉此叫用硬體驅動程式,並傳遞外掛程式建立的已編譯位元組碼。

LiteRT Dispatch 是編譯器外掛程式的執行階段類似項目。它們提供呼叫 HAL 的方式 (以編譯器輸出內容為準)。詳情請參閱調度說明文件

AOT 與裝置端

LiteRT 可以使用編譯器外掛程式,透過我們的工具支援 AOT 編譯,以及裝置端編譯。裝置端編譯更具彈性,完全內建於 LiteRT 執行階段 API 中,只需要管理單一模型。如果裝置資源不足,無法執行編譯作業,AOT 流程就能解除封鎖編譯作業,這可能是許多現代大型模型會遇到的情況。

備用

LiteRT 的建構方式支援異質圖形。外掛程式未選取的任何作業,都會交由 CPU 處理,或在其他後端上加速執行。

實作編譯器外掛程式

LiteRT 編譯器外掛程式會實作共用程式庫,並匯出 LiteRT C API 中定義的一組特定 C 函式。

基本介面函式

核心功能圍繞兩大編譯步驟:LiteRtCompilerPluginPartitionLiteRtCompilerPluginCompile

函式 目的
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 中的作業。對於目標硬體支援且可加速的每項作業,外掛程式會將該作業新增至 selected_ops 參數中提供的 LiteRtOpList$。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 函式會使用 out 參數 LiteRtCompiledResult 傳回輸出內容。

LiteRtCompiledResult 是外掛程式管理的結構體的不透明 (相對於 LiteRT) 控制代碼。這代表彙整結果,包含兩項主要資訊:

  1. 位元碼模組:一或多個原始記憶體緩衝區,內含硬體專屬的可執行位元碼 (即已編譯的指令)。
  2. 通話資訊:每個分割區的中繼資料。這會提供從第 i 個輸入子圖到結果位元組程式碼模組的對應,以及該模組的進入點 ID。

導入範例

下列程式碼片段說明基本外掛程式如何實作核心函式。這個範例取自 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)

這個範例顯示外掛程式只會在所有輸入和輸出都是 32 位元浮點數時,選取一組有限的作業 (mulsub 和特定複合作業)。通常判斷是否應選取作業時,會呼叫後端編譯器工具鍊中的驗證掛鉤。

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 提供各種工具,可將編譯器外掛程式套用至模型檔案、執行結果,以及驗證/基準化。請參閱加速器測試套件說明文件,以及基準測試和剖析說明文件