快捷方式

後端與委託

目標受眾:對將自己的編譯器和硬體整合到 ExecuTorch 中感興趣的供應商、後端委託開發者

後端委託是後端處理和執行 PyTorch 程式的入口點,以便利用專用後端和硬體的效能及效率優勢,同時仍為 PyTorch 使用者提供接近 PyTorch 執行時的體驗。

後端介面:概述

從高層來看,後端的入口點由兩個元件定義

  • 用於表示程式的 IR:Edge 方言(透過 to_edge API 生成)

  • 後端需要實現的幾個介面

    • 預先 (Ahead-of-Time, AOT)

      • 程式預處理(例如,預先編譯、轉換、最佳化等)。

    • 執行時

      • 程式初始化(例如,執行時編譯)。

      • 程式執行。

      • (可選)程式銷燬(例如,釋放後端擁有的資源)。

委託後端實現由以下部分組成:

  1. 預先預處理介面

  2. 執行時初始化和執行介面

圖示如下

drawing

圖 1. 後端介面的高層入口點,包括預先和執行時。

後端介面:預先預處理

後端主要需要實現兩個預先入口點:partitionpreprocess

partitioner 是由後端實現的演算法,用於標記要下層處理到後端的節點。to_backend API 將應用分割槽演算法,並將每個由相互連線的標記節點組成的子圖下層處理到目標後端。每個子圖將被髮送到後端提供的 preprocess 部分,編譯成二進位制塊(binary blob)。

在分割槽過程中,不允許 exported_program 修改程式,它應該為每個節點應用標籤。PartitionResult 包含帶有標記的匯出程式和分割槽標籤字典,供 to_backend 查詢標籤並連結到 backend_idcompile_spec

def partition(
    exported_program: ExportedProgram,
) -> PartitionResult:

在預處理過程中,後端會接收一個 Edge 方言程式和一份指定編譯所需值的編譯規範列表,並預期返回一個已編譯的二進位制塊(compiled blob),即包含期望在後端執行的程式的二進位制檔案。在序列化期間,該已編譯的二進位制塊將作為 .pte 檔案的一部分被序列化,並直接載入到裝置上。此過程的 API 是

def preprocess(
    edge_program: ExportedProgram,
    compile_specs: List[CompileSpec],
) -> PreprocessResult:

此處 實現了一個預處理函式演示。該演示迴圈遍歷 edge_program 圖模組中的節點,並將 addmulsin 指令序列化為字串,該字串稍後在執行時解析並執行。

圖示如下

drawing

圖 2. 圖經過分割槽,每個子圖將傳送到預處理部分。

後端介面:執行時初始化和執行

在執行時,來自 preprocess 函式的已編譯二進位制塊將被載入並直接傳遞給後端的自定義 init 函式。該函式負責進一步處理已編譯單元,以及執行任何後端初始化。然後將呼叫後端的自定義 execute 函式來執行由 init 生成的控制代碼。最後,如果某些後端需要銷燬,則後端可以實現一個 destroy 函式,該函式將在程式生命週期結束時被呼叫。

// Runtime check
ET_NODISCARD bool is_available();

// Runtime initialization
ET_NODISCARD virtual Result<DelegateHandle*> init(
    BackendInitContext& context,
    FreeableBuffer* processed,
    ArrayRef<CompileSpec> compile_specs);

// Runtime execution
ET_NODISCARD virtual Error execute(
    BackendExecutionContext& context,
    DelegateHandle* handle,
    EValue** args);

// [optional] Runtime destroy. Destroy the resource held by the backend
virtual void destroy(ET_UNUSED DelegateHandle* handle);

圖示如下

drawing

圖 3. 標準 ExecuTorch 執行時與後端入口點的關係。

為了使後端對 ExecuTorch 執行時可用,必須透過 register_backend API 進行註冊

ET_NODISCARD Error register_backend(const Backend& backend);

後端的靜態註冊,即在庫初始化或載入時,可以透過以下方式實現

namespace {
auto cls = BackendWithCompiler();
Backend backend{"BackendWithCompilerDemo", &cls};
static auto success_with_compiler = register_backend(backend);
} // namespace

開發者工具整合:可除錯性

提供一致的除錯體驗,無論是執行時故障還是效能分析,都非常重要。ExecuTorch 為此目的採用了原生的開發者工具,該工具透過除錯控制代碼(debug handles)實現程式指令與原始 PyTorch 程式碼的關聯。您可以在此處閱讀更多資訊。

委託程式或子圖對於 ExecuTorch 執行時來說是不可見的,它們表現為一個特殊的 call_delegate 指令,該指令要求相應的後端處理子圖或程式的執行。由於後端委託的不透明性,原生的開發者工具無法檢視委託的程式。因此,與非委託執行相比,委託執行的除錯(功能性或效能)體驗會顯著受損。

為了為使用者提供一致的除錯體驗,無論模型是否使用委託,開發者工具都提供了一個介面來關聯委託(子)圖與原始(子)圖。開發者工具透過除錯控制代碼對映(debug handles map)來實現這一點,該對映允許委託生成可以與委託消費的原始(子)圖相關聯的內部控制代碼。然後在執行時,後端開發者可以使用內部控制代碼報告錯誤或效能分析資訊,這些資訊將透過除錯控制代碼對映映射回原始(子)圖。更多資訊請參閱委託除錯

透過利用除錯識別符號(debug identifier),後端開發者可以將除錯資訊嵌入到委託的二進位制塊(delegated blob)中

drawing

這樣,在執行階段,藉助除錯識別符號,後端開發者可以將委託內部的失敗指令關聯回原始 Python 程式碼的確切行。

drawing

常見問題

1. 如何在 backend.preprocess 中獲取資料?

正在預處理的圖模組是一個提升後的圖(lifted graph),這意味著靜態資料(如權重和偏差)作為輸入提供給圖。但是,我們可以透過匯出程式(exported program)預先訪問這些權重和偏差。要從給定節點訪問這些引數,可以使用 torch/_export/utils.py 中提供的函式 get_params

2. 如何將資料(如權重/偏差)嵌入到後端?

後端通常有一些方法來最佳化常量資料。在這種情況下,我們需要在分割槽器中標記作為狀態的佔位符節點,並且在 backend.preprocess 期間,我們可以按照第一個問題中的描述獲取權重。

3. 如何使用特定後端在 Python 中執行下層處理後的模組?

我們尚未新增此功能,但這在計劃中!

4. 我們是否應該期望在 edge 方言程式中看到 get_attr 節點?

get_attr 節點僅用於控制流或委託的子模組。它不包含任何資料。

5. 我們可以委託給多個後端嗎?

可以!有兩種方法可以實現這一點

選項 1: 對不同的後端多次執行 to_backend

如果我們有兩個後端:backend_1 和 backend_2,並且它們有各自的分割槽器:backend_1_parititioner 和 backend_2_partitioner,我們可以這樣執行:

# Will first lower nodes to backend_1 depending on the backend_1_parititioner depending on partitioner algorithm
exported_program_backend_1 = to_backend(exported_program, backend_1_parititioner())
# For the rest of nodes, they will be lowered to backend_2 depending on backend_2_parititioner
exported_program_backend_1_and_2 = to_backend(exported_program_backend_1, backend_2_parititioner())

更具體的示例可以在此處找到。在此示例中,qnnpack 是一個後端,xnnpack 是另一個後端。我們尚未開源這兩個後端委託,因此此示例無法直接執行。它可以作為參考,瞭解如何實現。

此選項易於嘗試,因為通常所有後端都會實現自己的分割槽器。然而,如果我們改變 to_backend 呼叫的順序,此選項可能會得到不同的結果。如果我們想更好地控制節點(例如它們應該去哪個後端),選項 2 更好。

選項 2: 擁有一個為不同後端分割槽的分割槽器

另一個選項是建立一個自定義分割槽器,例如分割槽器 backend_1_2_partitioner,並在分割槽器邏輯內部,

class Backend_1_2_Partitioner(Partitioner):
    """
    Partitions all add/mul nodes regardless of order for Backend2
    """

    def __init__(self) -> None:
        self.delegation_spec_1 = DelegationSpec("Backend1", [])
        self.delegation_spec_2 = DelegationSpec("Backend2", [])
        self.partition_tags = {}

    def partition(
        self, exported_program: ExportedProgram
    ) -> ExportedProgram:

        # Tag all nodes in the first partiton to backend 1
        node_to_backend_1 = ... # some logic to select the nodes from the graph
        delegation_tag = f"backend2_tag{partitioner_1.id}"
        node.meta["delegation_tag"] = delegation_tag
        self.partition_tags[delegation_tag] = self.delegation_spec_1

        # Tag all nodes in the first partiton to backend 2
        node_to_backend_2 = ... # some logic to select the nodes from the graph
        delegation_tag = f"backend2_tag{partitioner_2.id}"
        node.meta["delegation_tag"] = delegation_tag
        self.partition_tags[delegation_tag] = self.delegation_spec_2
        return exported_program

6. 有沒有簡單的方法來編寫分割槽器?

我們此處提供了一些輔助分割槽器,以便輕鬆地從分解的運算元中查詢節點。

7. 如何將節點連結回原始碼? 我們提供了一個輔助函式

from executorch.exir.print_program import inspect_node

print(inspect_node(graph, node))

它將突出顯示圖中的節點並指向原始碼,示例輸出如下所示

_param_constant1 error_msg:  Here is the node in the graph module:
graph():
    %arg0_1 : [num_users=1] = placeholder[target=arg0_1]
    %_param_constant0 : [num_users=1] = get_attr[target=_param_constant0]
--> %_param_constant1 : [num_users=1] = get_attr[target=_param_constant1]
    %aten_convolution_default : [num_users=2] = call_function[target=executorch.exir.dialects.edge._ops.aten.convolution.default](args = (%arg0_1, %_param_constant0, %_param_constant1, [1, 1], [0, 0], [1, 1], False, [0, 0], 1), kwargs = {})
    %_param_constant2 : [num_users=1] = get_attr[target=_param_constant2]
    %_param_constant3 : [num_users=1] = get_attr[target=_param_constant3]
    %aten_convolution_default_1 : [num_users=1] = call_function[target=executorch.exir.dialects.edge._ops.aten.convolution.default](args = (%aten_convolution_default, %_param_constant2, %_param_constant3, [1, 1], [0, 0], [1, 1], False, [0, 0], 1), kwargs = {})
    %aten_add_tensor : [num_users=1] = call_function[target=executorch.exir.dialects.edge._ops.aten.add.Tensor](args = (%aten_convolution_default, %aten_convolution_default_1), kwargs = {})
    %_param_constant4 : [num_users=1] = get_attr[target=_param_constant4]
    %_param_constant5 : [num_users=1] = get_attr[target=_param_constant5]
    %aten_convolution_default_2 : [num_users=1] = call_function[target=executorch.exir.dialects.edge._ops.aten.convolution.default](args = (%aten_add_tensor, %_param_constant4, %_param_constant5, [1, 1], [0, 0], [1, 1], False, [0, 0], 1), kwargs = {})
    %aten_gelu_default : [num_users=1] = call_function[target=executorch.exir.dialects.edge._ops.aten.gelu.default](args = (%aten_convolution_default_2,), kwargs = {})
    return [aten_gelu_default]
This node _param_constant1 has metadata of:
The node stacktrace:
Traceback (most recent call last):
    File "/tmp/ipykernel_1204253/3382880687.py", line 7, in forward
return self.test_model(x)
    File "/mnt/xarfuse/uid-25337/7b86ad0c-seed-nspid4026532987_cgpid2707357-ns-4026532984/torch/nn/modules/module.py", line 1528, in _call_impl
return forward_call(*args, **kwargs)
    File "/tmp/ipykernel_1204253/712280972.py", line 10, in forward
a = self.conv1(x)

文件

查閱 PyTorch 全面的開發者文件

檢視文件

教程

獲取適用於初學者和高階開發者的深度教程

檢視教程

資源

查詢開發資源並獲取問題解答

檢視資源