詳解Paddle Lite底層在backend上的Kernel選擇策略

Paddle Lite是飛槳的輕量化推理引擎,爲手機、IoT端提供高效推理能力,且廣泛整合跨平臺硬件,滿足端側部署及應用落地的需求。本文將描述Paddle Lite在模型轉換過程(模型轉換opt工具)中,靜態Kernel選擇的策略以及一些思考。

:華爲NPU、XPU、APU等硬件設備的Kernel選擇,有其整體的"subgraph"的OP和Kernel,一般只有一個Kernel可選,與本文所述的方法存在不同。

圖1 Paddle Lite架構圖

Paddle Lite底層Kernel選擇上會考慮候選Place,Place由設備(Target)、精度(Precision)、數據排布(DataLayout)等構成。

/* Place specifies the execution context of a Kernel or input/output for a
 * kernel. It is used to make the analysis of the MIR more clear and accurate.
 */
struct LITE_API Place {
  TargetType target{TARGET(kUnk)};
  PrecisionType precision{PRECISION(kUnk)};
  DataLayoutType layout{DATALAYOUT(kUnk)};
  int16_t device{0};  // device ID

  Place() = default;
  Place(TargetType target,
        PrecisionType precision = PRECISION(kFloat),
        DataLayoutType layout = DATALAYOUT(kNCHW),
        int16_t device = 0)
      : target(target), precision(precision), layout(layout), device(device) {}
}


01

Kernel註冊的Place:同一個op根據Place的不同可註冊實現多種Kernel

同一個op如conv2d,可能會有不同設備的實現如ARM CPU、OpenCL、x86、CUDA等。在Kernel註冊時,需要指定Kernel的Place信息。

Place用於Kernel註冊,以區分唯一性。如實現一個基於ARM CPU以NCHW數據排布且以FP32計算的conv2d Kernel,那麼其註冊時候就會以conv2d、kARM、kFloat,kNCHW,def,用來區分這個Kernel的唯一性。下面是conv2d的多種不衝突的Kernel註冊形式:

conv2d, kARM, kInt8, kNCHW, def
conv2d, kARM, kFloat, kNCHW, def
conv2d, kARM, kFloat, kNHWC, def

conv2d, kOpenCL, kFloat, kNCHW, def
conv2d, kOpenCL, kFloat, kImageDefault, def

PS:def默認爲用來區分Kernel註冊時唯一性起名的一部分,作爲補充。

02

用於Kernel選擇的候選valid_places

模型推理時,遇到conv2d是選擇OpenCL還是ARM CPU來執行呢?如上面5個conv2d,模型執行時候選哪個?

這個涉及到同一個op算子,在對應不同Kernel註冊的Place(上面5個conv2d Kernel)和候選的執行valid_places的比較打分排序。其中,valid_place是預設好的,例如下面是以ARM CPU跑Float kernel時的預設valid_places:

std::vector<Place> valid_places({
      Place{TARGET(kARM), PRECISION(kFloat)},
});

再如,下面是OpenCL以FP16的精度ImageDefault的數據排布跑模型時的預設 valid_places:

  std::vector<Place> valid_places({
      Place{TARGET(kOpenCL), PRECISION(kFP16), DATALAYOUT(kImageDefault)},
      Place{TARGET(kOpenCL), PRECISION(kFloat), DATALAYOUT(kNCHW)},
      Place{TARGET(kOpenCL), PRECISION(kAny), DATALAYOUT(kImageDefault)},
      Place{TARGET(kOpenCL), PRECISION(kAny), DATALAYOUT(kNCHW)},
      TARGET(kARM),  // enable kARM CPU Kernel when no opencl Kernel
  });

此外,valid_places中place順序越靠前,越傾向選擇該place對應的Kernel,即權重係數越大(見後文KernelGrade方法中的 weight計算)。


03

Kernel選擇策略:候選Kernel的place與用戶valid_places的笛卡爾積

Pass是Paddle Lite用於遍歷計算圖,並對計算圖進行修改的系列操作,如某些op融合、op刪除。針對op選擇合適的Kernel,也是通過Pass實現的。Kernel選擇有兩種方法,一種是對同一個op多種Kernel動態測試運行時間,選擇時間最短的的Kernel組合;另外一種根據預設的規則來選擇,規則中已做了較爲綜合的考慮。那Paddle Lite選擇策略是怎樣的呢?

Paddle Lite有一個基於已有註冊Kernel與valid_places匹配度的打分策略,即第二種,根據預設規則來做選擇。其實現的代碼對應下面兩個文件:

1. ./lite/core/mir/static_kernel_pick_pass.cc:全圖遍歷,爲每個op選擇Kernel。該過程會計算圖中的每個計算節點對應的多種Kernel, 這些Kernel的Place與用戶傳入的 valid_places中的每個Place,兩兩打分(笛卡爾積),選擇分數最高的Kernel;

2. ./lite/core/mir/static_kernel_pick_pass.h:KernelGrade會計算特定Kernel與valid_place 中每個Place的匹配分數。匹配分數是基於Place中包含的設備(Target)、精度(Precision)、數據排布(DataLayout)等信息計算得到,即打分策略。

下面描述一下這兩個步驟:


3.1 全圖遍歷選擇Kernel

上面第一個步驟代碼化簡如下,步驟解釋見下面註釋:

  // lite/core/mir/static_kernel_pick_pass.cc
  // 1. 依次遍歷模型graph節點
  for (auto& node : graph->mutable_nodes()) {
    if (!node.IsStmt()) continue; // 跳過非計算節點
    auto& instruct = node.AsStmt();

    // 獲取所有該節點的輸入和輸出的tensor精度,實現略
    std::unordered_map<std::string, PrecisionType> in_precision_types;
    std::unordered_map<std::string, PrecisionType> out_precision_types;

    // 獲取該層op的不同kernel候選實現:instruct.kernels()
    //     比方該層是conv2d,那麼instruct.kernels()方法,
    //     就可獲取到所有編譯進去的conv2d的不同實現。
    // 2. 依次(for)對不同Kernel實現打分(KernelGrade),
    //      KernelGrade用來找出該Kernel實現的最佳Place,
    //      及最佳Place下的Kernel得分。
    std::vector<std::pair<float, std::unique_ptr<KernelBase>>> scored;
    for (auto&& kernel : instruct.kernels()) {
      float score = KernelGrade(instruct,
                                *kernel,
                                graph->valid_places(),
                                in_precision_types,
                                out_precision_types,
                                instruct.op_info()->input_names(),
                                instruct.op_info()->output_names());
      // 3. 記錄每種Kernel實現在最佳Place下的最高分值
      scored.emplace_back(score, std::move(kernel));
    }

    // 4. 對打分結果scored排序,clear清空候選Kernel列表
    //       重置候選Kernel列表爲分數最高的那一個Kernel,
    //       即最終選中要執行的Kernel
    std::sort(scored.begin(), scored.end(), KernelScoreCmp);
    instruct.kernels().clear();
    instruct.kernels().emplace_back(std::move(scored.front().second));
  }


3.2 KernelGrade:對Kernel的不同Place打分

在全圖遍歷選擇Kernel的過程中,KernelGrade方法起了至關重要的作用:該方法找出當前Kernel下的最佳Place(方法內會對用戶傳入的 valid_places遍歷計算打分: final_score=score*weight),及最佳Place下的該Kernel得分。

公式中 weight就是 valid_places中的次序,越靠前的Place, weight越大。例如希望模型以CPU的NCHW的layout來跑,其中的 valid_places第一個必須是Place{kARM, kFloat, kNCHW},假設第二個是 Place{kARM,kFloat,kNHWC},除了layout其他都和第一個Place一樣,那麼,在兩個Place都有對應Kernel註冊且實現過的前提下(候選Kernel裏二者都有),因NCHW是第一位,則NCHW對應的Place的weight就更大,包含NCHW的Place最終被選中爲winner_place概率會大,包含NCHW的Place的Kernel被選中的概率也會更大。

Kernel對Place打分的過程是前文所述的第二個步驟,該步驟有5個階段,代碼簡化如下:

  // lite/core/mir/static_kernel_pick_pass.h
  size_t KernelGrade(
      const mir::Node::Stmt& instruct,
      const KernelBase& kernel,
      const vector<Place>& valid_places,
      const unordered_map<std::string, PrecisionType>& in_node_precisons,
      const unordered_map<std::string, PrecisionType>& out_node_precisons) {

    float final_score_for_winner_place{-1.};
    const int kMax = numeric_limits<int>::max();
    size_t place_size = valid_places.size();

    for (size_t pidx = 0; pidx < place_size; ++pidx) {
      const auto& place = valid_places[pidx];
      float weight = static_cast<float>(place_size - pidx) / place_size;
      size_t place_score{0};

      if (place.target == kernel.target())
        place_score += kMax / KernelPickFactor::Factor::TargetFirst;
      if (place.precision == kernel.precision())
        place_score += kMax / KernelPickFactor::Factor::PrecisionFirst;
      if (place.layout == kernel.layout())
        place_score += kMax / KernelPickFactor::Factor::DataLayoutFirst;
      if ((in_node_precisons == kernel_registered_in_tensor_precisions) &&
            out_node_precisons == kernel_registered_out_tensor_precisions))
        place_score *= 2;

      if (weight * place_score > final_score_for_winner_place) {
        final_score_for_winner_place = weight * place_score;
        winner_place = place;
      }
    }

    return final_score_for_winner_place;
  }

這5個階段,對應當前Place信息所包含的的設備、精度、數據排布、輸入輸出精度檢查、當前place信息在預設的valid_place中的排位係數,前3個在計算時有對應係數,下面來看看代碼中的設定以及思考:

// /lite/core/types.h
// 係數在實際計算中轉爲分母
class KernelPickFactor {
 public:
  using value_type = unsigned char;
  enum class Factor : int {
    // The following factors are sorted by priority.
    TargetFirst = 1,
    PrecisionFirst = 1 << 1,
    DataLayoutFirst = 1 << 2,
    DeviceFirst = 1 << 3,
  };
  1. 設備target(係數爲1):相比Place中的其他兩個數據,設備係數排在首位,因爲數據在不同設備上的傳輸開銷極大。若模型中conv都是GPU計算,中間有些層的實現是CPU的,且無zero copy前提下,來回的數據拷貝帶來的性能下降就很明顯。

  2. 精度precision(係數爲1/4):其實精度還有數據排布哪個排在第二位更好,還需實踐檢驗,以OpenCL來說,數據排布layout爲cl::image(kImageDefault)可利用L1 cache,一般性能比cl::buffer(NCHW)要好,精度FP16比FP32性能也要好不少,就從OpenCL來說可能二者打分的係數可以一樣。當前Paddle Lite的實現是精度的重要性係數(比layout)更大。

  3. 數據排布datalayout(係數爲1/8):同上。訪存的優化也是必要的,CPU爲了更極致的計算性能,而定義了NHWC的數據排布,也是打分的一項考量。

  4. Kernel註冊的輸入輸出的tensor精度,與該graph中當前op的輸入輸出精度是否匹配。全部匹配就分數翻倍。該打分會檢查當前graph中的節點精度和Kernel註冊時tensor的精度是否一致。其實不僅是精度,layout和target也可以做這個判斷。

  5. 分數乘以當前place在valid_places中的排位係數。這個前面已經說過,排在越靠前的place,對應Kernel被選中的 概率就越大。

以上,便是Kernel靜態選擇的整個過程。

04

思考

其實可以看到:

  1. Paddle Lite的Kernel選擇前先做graph層級op粒度的融合操作,與硬件無關;

  2. 在之後,是與硬件信息相關的靜態Kernel選擇。選擇基於Place{target, precision, layout}信息,從而確定要執行的Kernel,其中沒有參考如卷積核的大小,輸入的大小等信息。換句話說,該過程與模型輸入、op具體信息無關,選擇的依據粒度仍然較大;

  3. static_pick_kernel_pass是模型轉換爲Paddle Lite格式的過程中一個pass,在之後的pass裏應該還有更大的操作空間。比方結合試跑,結合模型更細粒度的信息做一些更細粒度的Kernel選擇和定製化修改。


05

補充

1. 細粒度的Kernel選擇如CPU conv3x3s2p1或者OpenCL的cl Kernel是什麼階段選擇的呢?

答:細粒度如conv3x3s2p1要執行的Kernel,會在運行期lite kernel第一次執行的時候基於op具體信息做選擇。此外,如果是動態shape,即當前層本次推理的輸入與下次不同,也會觸發ReinitWhenNeeded方法,進而重新選擇。

以OpenCL爲例,選擇cl Kernel的階段位於執行的Kernel裏,該階段也會定義lws等與硬件相關的信息。若想做針對OpenCL做模型自動化調優,需要在Lite Kernel這個粒度來做。而且也僅限當前Kernel這個Place,前面我們說過Place包含三個信息target/precision/layout,對於Opencl有兩種layout:kNCHW和kImageDefault,對應cl::Buffer和cl::Image2D。但前面說了Lite Kernel的layout已經在靜態選Kernel時確定了,即一次只能調優一種Layout下的實現。

2. 基於模型試跑的最佳Kernel搜索,是否易於實現呢?

答:目前Paddle Lite還不支持基於試跑的最佳Kernel搜索。一般的策略是讓每個Kernel持有一個計算最快的方法,在跑第一遍網絡時,根據每層跑多種實現的耗時,記錄最快方法,以供後續使用。

如果要更大範圍,考慮更多backend做最佳性能的Kernel搜索的話。可能有2種方式:

  1. 靜態選擇和具體選擇,對應的兩個階段需要打通。即StaticPickKernel過程,與具體的Kernel選擇綁定,這時可以全盤考慮。這個過程也需要拿到conv的kernel size,input shape等信息。但這樣雖然兩個階段的Kernel選擇打通,但是二階段的具體Kernel判斷需要再寫一遍,維護上有一定成本;

  2. 兩階段分開做Kernel選擇,即每個階段相對於局部的最優,從而達到相對全局的(次)最優。其實我們的目的是找一個模型在所有不同target、precision、layout的Kernel實現上排列組合這個模型下的最佳性能。但靜態選擇的策略,在本質上已經考慮了backend不同帶來的差異。端側對性能的極致要求,可能不同backend下的Kernel組合出的一個模型,也會帶來性能不穩定,在端側會非常不友好,而且還有拷貝帶來的性能損耗。

如在使用過程中有問題,可加入飛槳官方QQ羣進行交流:703252161,飛槳推理部署交流羣官方QQ羣:696965088。

如果您想詳細瞭解更多飛槳的相關內容,請參閱以下文檔。

官網地址:

https://www.paddlepaddle.org.cn

飛槳輕量化推理引擎Paddle Lite項目地址:

GitHub: 

https://github.com/PaddlePaddle/Paddle-Lite

Gitee: 

https://gitee.com/paddlepaddle/paddle-lite

飛槳開源框架項目地址:

GitHub:

https://github.com/PaddlePaddle/Paddle

Gitee: 

https://gitee.com/paddlepaddle/Paddle

想了解Paddle Lite如何在端側部署的小夥伴可以掃碼或點擊“閱讀原文”報名下面課程,就在下週二飛槳B站直播間哦~

END

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章