国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

【TVM 教程】向 TVM 中添加 Codegen 原創

發布于 2025-8-20 10:24
瀏覽
0收藏

Apache TVM 是一個深度的深度學習編譯框架,適用于 CPU、GPU 和各種機器學習加速芯片。更多 TVM 中文文檔可訪問 →https://tvm.hyper.ai/

隨著深度學習工作負載所針對的硬件設備數量不斷增加,用戶在各種設備上實現高性能所需的知識也在不斷增加。為了讓數據科學家在開發新模型時不必擔心性能問題,硬件廠商或是基于一些常見的深度學習算子,提供 MKLDNN 或 cuDNN 等庫,或是提供 TensorRT 等框架,讓用戶按照某種方式描述模型,從而提高模型性能。

然而,用戶在嘗試使用新的庫或設備時,必須學習新的編程接口。因此,一個統一的編程接口變得越來越重要:1)讓所有用戶及硬件廠商信息同步,2)提供一個可行的解決方案,讓特定硬件或庫只支持具有極高性能的、廣泛使用的算子,不受支持的算子則回退到 CPU/GPU 等通用設備。

本開發手冊演示了硬件廠商如何輕松實現自己的 Codegen,并將其注冊為 Relay 后端編譯器,從而支持自己的硬件設備/庫。本手冊涵蓋了兩種基于不同計算圖的 codegen:

1. 希望生成 C 代碼。

如果你的硬件已經具備了一個高度優化的 C/C++ 庫,如對于 CPU 而言的 Intel CBLAS/MKL 庫,或針對 GPU 而言的 NVIDIA CUBLAS 庫,那么本節內容非常適合你。幸運的是,C 源代碼模塊與 TVM runtime 模塊完全兼容,這意味著生成的代碼可以由任何具有適當編譯標志的 C/C++ 編譯器編譯,因此用戶只需實現一個能為子圖生成 C 代碼的 codegen,并將 C 源代碼模塊集成到 TVM runtime 模塊中。下一節內容講詳細演示如何為硬件實現 C codegen。

2. 希望生成任意計算圖。

有時候,硬件可能需要其他形式的計算圖如 JSON。這種情況下,用戶不僅要實現一個 codegen,還要實現一個自定義 TVM runtime 模塊,從而使得 TVM runtime 知道如何執行這個計算圖。如果你的硬件已經擁有完整的計算圖執行引擎(graph execution engine),如適用于 GPU 的 TensorRT,那么該解決方案對你而言非常具有參考價值。

完成 codegen 和 runtime 后,可以讓客戶借助你的自定義標簽,對模型進行注釋并加以利用。終端用戶如何注釋和啟動特定 codegen 的教程,將在后續進行補充。

實現 C Codegen

在這一部分中,我們將演示如何借助預實現的算子函數,生成 C 代碼的 codegen。簡單起見,本示例 codegen 不依賴于第三方庫。相反,我們在 C 中手動實現了兩個宏:

#define CSOURCE_BINARY_OP_1D(p_ID_, p_OP_, p_DIM1_)         \
    extern "C" void p_ID_(float* a, float* b, float* out) { \
        for (int64_t i = 0; i < p_DIM1_; ++i) {             \
            out[i] = a[i] p_OP_ b[i];                       \
        }                                                   \
    }

#define CSOURCE_BINARY_OP_2D(p_ID_, p_OP_, p_DIM1_, p_DIM2_)  \
    extern "C" void p_ID_(float* a, float* b, float* out) {   \
        for (int64_t i = 0; i < p_DIM1_; ++i) {               \
            for (int64_t j = 0; j < p_DIM2_; ++j) {           \
                int64_t k = i * p_DIM2_ + j;                  \
                out[k] = a[k] p_OP_ b[k];                     \
            }                                                 \
        }                                                     \
    }

使用這兩個宏,可以為一維和二維張量生成二元算子(binary operator)。例如,給定如下所示的子圖,假設所有輸入都是 shape 為(10, 10)的二維張量:

c_compiler_input0
       |
      add <-- c_compiler_input1
       |
    subtract <-- c_compiler_input2
       |
    multiply <-- c_compiler_input3
       |
      out

我們的目標是生成以下可編譯代碼來執行子圖:

#include <tvm/runtime/c_runtime_api.h>
#include <tvm/runtime/packed_func.h>
#include <dlpack/dlpack.h>
#include <cstdint>
#include <cstring>
#include <iostream>

#define GCC_BINARY_OP_1D(p_ID_, p_OP_, p_DIM1_)           \
  extern "C" void p_ID_(float* a, float* b, float* out) { \
    for (int64_t i = 0; i < p_DIM1_; ++i) {               \
      out[i] = a[i] p_OP_ b[i];                           \
    }                                                     \
  }

#define GCC_BINARY_OP_2D(p_ID_, p_OP_, p_DIM1_, p_DIM2_)  \
  extern "C" void p_ID_(float* a, float* b, float* out) { \
    for (int64_t i = 0; i < p_DIM1_; ++i) {               \
      for (int64_t j = 0; j < p_DIM2_; ++j) {             \
        int64_t k = i * p_DIM2_ + j;                      \
        out[k] = a[k] p_OP_ b[k];                         \
      }                                                   \
    }                                                     \
  }

// 注 1
GCC_BINARY_OP_2D(gcc_0_0, *, 10, 10);
GCC_BINARY_OP_2D(gcc_0_1, -, 10, 10);
GCC_BINARY_OP_2D(gcc_0_2, +, 10, 10);

// 注 2
extern "C" void gcc_0_(float* gcc_input0, float* gcc_input1,
                       float* gcc_input2, float* gcc_input3, float* out) {
  float* buf_0 = (float*)malloc(4 * 100);
  float* buf_1 = (float*)malloc(4 * 100);
  gcc_0_2(gcc_input0, gcc_input1, buf_0);
  gcc_0_1(buf_0, gcc_input2, buf_1);
  gcc_0_0(buf_1, gcc_input3, out);
  free(buf_0);
  free(buf_1);
}

// 注 3
extern "C" int gcc_0_wrapper(DLTensor* arg0, DLTensor* arg1, DLTensor* arg2,
                             DLTensor* arg3, DLTensor* out) {
  gcc_0_(static_cast<float*>(arg0->data), static_cast<float*>(arg1->data),
         static_cast<float*>(arg2->data), static_cast<float*>(arg3->data),
         static_cast<float*>(out->data));
  return 0;
}
TVM_DLL_EXPORT_TYPED_FUNC(gcc_0, gcc_0_wrapper);

這里詳細介紹一下上面代碼里的注釋:

  • 注1:子圖中三個節點的函數實現。
  • 注2:通過分配中間數組(intermediate buffer)并調用相應函數來執行子圖的函數。
  • 注3:TVM runtime 兼容的包裝函數。它接收一個輸入張量列表和一個輸出張量(最后一個參數),并將其轉換為正確的數據類型,調用注2 中描述的子圖函數。此外,TVM_DLL_EXPORT_TYPED_FUNC?是一個 TVM 宏,它通過將所有張量打包到?TVMArgs?來生成另一個函數?gcc_0,該函數具有統一的函數參數。因此,TVM runtime 可以直接調用?gcc_0?來執行子圖,無需其他操作。生成上述代碼后,TVM 能夠將其與計算圖的其余部分一起編譯并導出單個庫以進行部署。

在本節的其余部分,我們將逐步創建一個 codegen,來實現上述代碼。你的 codegen 必須位于?src/relay/backend/contrib/<your-codegen-name>/。在這個例子中,我們將 codegen 命名為 “codegen_c”,并將其放在?/src/relay/backend/contrib/codegen_c/?目錄下。你可以隨時查看這個文件,了解完整的實現過程。

具體來說,我們將在這個文件中實現兩個類,兩個類的關系如下:

            subgraph                                subgraph
TVM backend -----------------------------> CSourceCodegen -------------> CodegenC
       ^                                       |    ^                       |
       |                                       |    |                       |
       ----------------------------------------      ------------------------
          generated C source runtime module              generated C code

當 TVM 后端發現 Relay 計算圖中的函數(子圖),用注冊的編譯器標簽(本例中為?ccompiler)進行了注釋時,TVM 后端就會調用?CSourceCodegen?并傳遞子圖。?CSourceCodegen?的成員函數?CreateCSourceModule?將:

1)為子圖生成 C 代碼;

2)將生成的 C 代碼包裝到 C source runtime 模塊中,以便 TVM 后端進行編譯和部署。

特別是,C codegen 對?CodegenC?類是透明的,因為它提供了許多有用的實用程序來簡化 codegen 實現。下面的章節將自下而上實現這兩個類。

實現 CodegenC

在?src/relay/backend/contrib/codegen_c/codegen.cc?中,首先在?tvm.relay.contrib?的命名空間下創建一個 codegen 類骨架:

#include <tvm/relay/expr_functor.h>
#include <tvm/relay/transform.h>
#include <tvm/relay/type.h>
#include <tvm/runtime/module.h>
#include <tvm/runtime/object.h>

#include <fstream>
#include <sstream>

#include "codegen_c.h"

namespace tvm {
namespace relay {
namespace contrib {

class CodegenC : public ExprVisitor, public CodegenCBase {
  public:
    explicit CodegenC(const std::string& id) { this->ext_func_id_ = id; }

    void VisitExpr_(const VarNode* node) { ; }
    void VisitExpr_(const CallNode* call) final { ; }
    std::string JIT() { ; }

  private:
    /*! \brief The function id that represents a C source function. */
    std::string ext_func_id_ = "";
    /*! \brief The index of a wrapped C function. */
    int func_idx = 0;
    /*! \brief The index of allocated buffers. */
    int buf_idx_ = 0;
    /*! \brief The arguments of a C compiler compatible function. */
    std::vector<std::string> ext_func_args_;
    /*! \brief The statements of a C compiler compatible function. */
    std::vector<std::string> ext_func_body;
    /*! \brief The declaration statements of a C compiler compatible function. */
    std::vector<std::string> func_decl_;
    /*! \brief The declaration statements of buffers. */
    std::vector<std::string> buf_decl_;
    /*! \brief The name and index pairs for output. */
    std::vector<std::pair<std::string, int>> out_;
}

CodegenC?類繼承了兩個類:?ExprVisitor?提供遍歷子圖的能力,然后收集所需的信息并生成子圖函數,例如?gcc_0_

CodegenCBase?提供了生成包裝函數的能力和實用程序,例如上例中的?gcc_0。可以看出,我們只需要在這個 codegen 類中實現三個函數就可以了。

算子的代碼生成

首先實現?VisitExpr_(const CallNode* call)。該函數在遍歷子圖時會訪問所有調用節點。每個調用節點都包含一個我們想要卸載(offload)到硬件中的算子。因此,我們需要按照拓撲順序生成具有正確算子的相應 C 代碼。完整實現過程如下:

1. 生成函數聲明

示例結果:GCC_BINARY_OP_2D(gcc_0_0, *, 10, 10);

要生成函數聲明,如上所示,我們需要:

1)函數名(例如?gcc_0_0

2)算子的類型(例如?*?)

3)輸入張量 shape(例如?(10, 10)?)

這些信息可以從?CallNode?輕松獲取:

std::ostringstream macro_stream;
std::ostringstream decl_stream;
std::ostringstream buf_stream;

// Generate a unique function name you like.
std::string func_name = ext_func_id_ + "_" + std::to_string(func_idx++);

// Make function declaration string.
macro_stream << "CSOURCE_BINARY_OP_" << call->args.size() << "D(" << func_name << ", ";

// Check the operator type.
if (IsOp(call, "add")) {
  macro_stream << "+";
} else if (IsOp(call, "subtract")) {
  macro_stream << "-";
} else if (IsOp(call, "multiply")) {
  macro_stream << "*";
} else {
  LOG(FATAL) << "Unrecognized op";
}

// Extract the input tensor shape.
auto in_shape = GetShape(call->args[0]->checked_type());
for (size_t i = 0; i < in_shape.size(); ++i) {
  macro_stream << ", " << in_shape[i];
}
macro_stream << ");";
func_decl_.push_back(macro_stream.str());

可以看出,我們將生成的代碼推送到類成員變量?func_decl_?中。這意味著在我們完成遍歷整個子圖之后,我們已經收集了所有必需的函數聲明,我們唯一需要做的就是用 GCC 編譯它們。?VisitExpr_(const CallNode* call)?的其余實現也遵循這個概念。

2. 生成函數調用

示例結果:gcc_0_0(buf_1, gcc_input3, out);

生成函數聲明后,我們需要生成一個具有正確輸入和輸出的函數調用。要想知道調用這個函數時應該放置哪些輸入或數組,必須訪問它的參數:

bool first = true;
decl_stream << func_name << "(";
for (size_t i = 0; i < call->args.size(); ++i) {
  VisitExpr(call->args[i]); // 注 1
  for (auto out : out_) {
    if (!first) {
      decl_stream << ", ";
    }
    first = false;
    decl_stream << out.first;
  }
}
// 注 2

同樣,重點介紹一下上述代碼中的注釋:

注1VisitExpr(call->args[i])?是訪問當前函數參數的遞歸調用。參數可以是另一個節點的輸出或輸入張量。在該示例中,需要確保每個節點在離開訪問器之前,都更新一個類變量?out_。圖解如下:

 arg_node                 arg_node <- Visit arg (Note 1)       arg_node
     |                        |                                    |
 curr_node <- Process      curr_node                            curr_node <- Put "buf_0" as an input buffer

(a) out_ = {}            (b) out_ = {}                   (c) out_ = {("buf_0", 20)}

從上圖中可以看出,類變量?out_?在訪問參數節點前是空的,它被填充了?arg_node?輸出數組的名稱和大小。因此在完成對參數節點的訪問時,可以通過查看?out_?得知應該放置的正確輸入數組。本節末尾以及下一節中,我們將介紹如何更新?out_

注2:你可能注意到,我們在這一步沒有關閉函數調用字符串。當前函數調用字符串看起來像:gcc_0_0(buf_1, gcc_input3。這是因為我們沒有將最后一個參數(如 output)放入此調用中。函數調用的輸出可以是分配的臨時數組或子圖輸出張量。簡單起見,在本例中我們為每個調用節點都分配老一個輸出數組(下一步),并將最后一個數組中的結果復制到了輸出張量。

3. 生成輸出數組(output buffer)

示例結果:float buf_0 = (float)malloc(4 * 100);

如上一步所述,除了子圖輸入和輸出張量外,還需要數組來保存中間結果。為了生成數組,我們提取 shape 信息,以確定數組的類型和大小:

// 這個例子僅支持單個輸出。
auto type_node = call->checked_type().as<TensorTypeNode>();
ICHECK(type_node != nullptr && runtime::TypeMatch(type_node->dtype, kDLFloat, 32))
      << "Only support single output tensor with float type";

// 生成一個唯一的數組名字。
std::string out = "buf_" + std::to_string(buf_idx_++);

// 提取 shape 作為數組大小。
auto out_shape = GetShape(call->checked_type());
int out_size = 1;
for (size_t i = 0; i < out_shape.size(); ++i) {
  out_size *= out_shape[i];
}

// 分配數組并推送至數組聲明
buf_stream << "float* " << out << " = (float*)std::malloc(4 * " << out_size << ");";
buf_decl_.push_back(buf_stream.str());

分配了輸出數組之后,現在可以關閉函數調用字符串,并將生成的函數調用推送到類變量?ext_func_body

decl_stream << ", " << out << ");";
ext_func_body.push_back(decl_stream.str());

4. 更新輸出數組

為了使得下一個節點(接受當前調用節點的輸出,作為其輸入)知道它應該使用哪個數組,我們需要在離開這個訪問函數之前更新類變量?out_

out_.clear();
out_.push_back({out, out_size});

恭喜!到這一步我們已經完成了這個類中最困難的函數。接下來的兩節中,我們將進一步完善這個函數的功能。

輸入變量的代碼生成

回想一下,我們通過訪問調用節點的參數(上一節中的第 2 步)收集了輸入數組信息,并處理了參數是另一個調用節點的情況(第 4 步)。本節我們將以?VarNode?為例,演示如何處理其他節點。

VarNode?表示模型中的輸入張量。它非常重要的一點就是名稱提示(例如,dataweight?等)。訪問?VarNode?時,只需更新類變量?out_?傳遞名稱提示,后代(descendant)調用節點就可以生成正確的函數調用。

void VisitExpr_(const VarNode* node) {
  ext_func_args_.push_back(node->name_hint());
  out_.clear();
  out_.push_back({node->name_hint(), 0});
}

注意:在這個例子中,我們假設要卸載的子圖只有調用節點和變量節點。如果子圖包含其他類型的節點,如?TupleNode,那么你也需要訪問它們并繞過輸出數組信息。

Code Emitting

Codegen Class 的最后一部分是?JIT?函數,它為子圖 emit 一個 C 函數,并將剛生成的 C 代碼作為函數體。注意,除了在前幾節中生成的子圖函數外,還需要一個具有統一參數的 wrapper 函數,供 TVM runtime 調用和傳遞數據。幸運的是,我們繼承的基類已經提供了一個實現,即?JitImpl,來生成該函數。調用?JitImpl的方式如下:

JitImpl("gcc_0" /* Subgraph symbol (ID) */,
        {"gcc_input0", "gcc_input1", "gcc_input2", "gcc_input3"} /* Input arguments */,
        {"float *buf_0 = (float*)malloc(4 * 20)", ...} /* Buffer allocations */,
        {"gcc_0_2(gcc_input0, gcc_input1, buf_0);"} /* Function body */,
        {"out"} /* Output */);

上述調用將生成三個函數(一個來自 TVM wrapper 宏):

  1. 子圖函數?gcc_0_(函數名末尾多了一個下劃線)以及為執行子圖而生成的所有 C 代碼;
  2. 帶有?DLTensor?參數列表的 wrapper 函數?gcc_0__wrapper_?,將數據轉換為正確的類型并調用?gcc_0_
  3. TVM runtime 兼容函數?gcc_0?具有 TVM 統一函數參數,可解包 TVM 打包張量并調用?gcc_0__wrapper_

因此,在?JIT?實現中唯一要做的,就是將生成的所有子圖函數代碼傳遞給?JitImpl

std::string JIT() {
  // Write function macros
  for (auto decl : func_decl_) {
    code_stream_ << decl << "\n";
  }
  return JitImpl(ext_func_id_, ext_func_args_, buf_decl_, ext_func_body, out_);
}

傳遞的所有變量(ext_func_id?等)都是類變量,并在遍歷子圖時被填充。

實現 CSourceCodegen

創建一個類并實現所需功能,注意:需要繼承自?CSourceModuleCodegenBase

class CSourceCodegen : public CSourceModuleCodegenBase {
 public:
  // 傳遞一個子圖函數, 并生成 C 代碼。
  void GenCFunc(const Function& func) { ; }

  // 使用 GenCFunc 來生成 C 代碼并將它包裝成一個 C 源模塊。
  runtime::Module CreateCSourceModule(const NodeRef& ref) override { ; }

 private:
  std::ostringstream code_stream_;
};

實現 GenCFunc

GenCFunc?只是簡單地使用我們剛剛實現的?CodegenC?來遍歷一個 Relay 函數(子圖),得到生成的 C 代碼。內置函數?GetExtSymbol?在 Relay 函數中檢索唯一的符號名稱(例如?gcc_0),注意:必須將其用作 C 函數名稱,因為該符號將用于 DSO 運行查找。

void GenCFunc(const Function& func) {
  ICHECK(func.defined()) << "Input error: expect a Relay function.";

  // 記錄運行查找的外部符號。
  auto sid = GetExtSymbol(func);

  CodeGenC builder(sid);
  builder.VisitExpr(func->body);
  code_stream_ << builder.JIT();
}

實現 CreateCSourceModule

此函數為外部庫創建了一個 runtime 模塊。本事例中,我們創建了一個可以直接被編譯并與 TVM 生成的 DSOModule 鏈接在一起的 CSourceModule。CodegenC?實現之后,再實現這個功能就比較簡單了:

runtime::Module CreateCSourceModule(const NodeRef& ref) override {
  // 創建頭文件
  code_stream_ << "#include <cstdint>\n";
  code_stream_ << "#include <iostream>\n";
  code_stream_ << "#include <cstdlib>\n";
  code_stream_ << "#include <stdio.h>\n";
  code_stream_ << "#include <cstring>\n";
  code_stream_ << "#include <tvm/runtime/c_runtime_api.h>\n";
  code_stream_ << "#include <dlpack/dlpack.h>\n";

  // 為算子定義添加一些公共宏。
  const char* operator_macro = R"op_macro(
  #define CSOURCE_BINARY_OP_1D(p_ID_, p_OP_, p_DIM1_)       \
    extern "C" void p_ID_(float* a, float* b, float* out) { \
      for (int64_t i = 0; i < p_DIM1_; ++i) {               \
        out[i] = a[i] p_OP_ b[i];                           \
      }                                                     \
    }

  #define CSOURCE_BINARY_OP_2D(p_ID_, p_OP_, p_DIM1_, p_DIM2_)  \
    extern "C" void p_ID_(float* a, float* b, float* out) {     \
      for (int64_t i = 0; i < p_DIM1_; ++i) {                   \
        for (int64_t j = 0; j < p_DIM2_; ++j) {                 \
          int64_t k = i * p_DIM2_ + j;                          \
          out[k] = a[k] p_OP_ b[k];                             \
        }                                                       \
      }                                                         \
    }
  )op_macro";

  code_stream_ << operator_macro << "\n\n";

  // 為子圖生成 C 代碼。
  if (ref->IsInstance<FunctionNode>()) {
    GenCFunc(Downcast<Function>(ref));
  } else if (ref->IsInstance<relay::ModuleNode>()) {
    relay::Module mod = Downcast<relay::Module>(ref);
    for (const auto& it : mod->functions) {
      GenCFunc(Downcast<Function>(it.second));
    }
  } else {
    LOG(FATAL) << "The input ref is expected to be a Relay function or module"
               << "\n";
  }

  // 創建一個 CSourceModule
  const auto* pf = runtime::Registry::Get("module.csource_module_create");
  ICHECK(pf != nullptr) << "Cannot find csource module to create the external runtime module";
  return (*pf)(code_stream_.str(), "cc");
}

注冊 CodegenC

最后一步是將 codegen 注冊到 TVM 后端。首先實現一個簡單的函數,調用 codegen 并生成一個 runtime 模塊:

runtime::Module CCompiler(const NodeRef& ref) {
  CSourceCodegen csource;
  return csource.CreateCSourceModule(ref);
}

接下來將此函數注冊到 TVM 后端:

TVM_REGISTER_GLOBAL("relay.ext.ccompiler").set_body_typed(CCompiler);

其中?ccompiler?是一個自定義標簽,它告知 TVM 這是用?ccompiler?注釋子圖時,應該用來生成和卸載子圖的 codegen。

最后,設置一個 CMake 配置標志,只包含客戶的編譯器。首先創建一個 cmake 文件:cmake/modules/contrib/CODEGENC.cmake

if(USE_CODEGENC)
  file(GLOB CSOURCE_RELAY_CONTRIB_SRC src/relay/backend/contrib/codegen_c/codegen.cc)
  list(APPEND COMPILER_SRCS ${CSOURCE_RELAY_CONTRIB_SRC})
endif(USE_CODEGENC)

用戶在使用?config.cmake?配置 TVM 時,可以自行決定是否配置編譯器:

set(USE_CODEGENC ON)

為表征(Representation)實現 Codegen

盡管我們已經演示了如何實現 C codegen,但用戶硬件可能還需要其他形式的計算圖表征(Graph Representation),如 JSON。在這種情況下,用戶可以通過修改?CodegenC?類,生成自己的計算圖表征,并實現一個自定義 runtime 模塊,告訴 TVM runtime 如何執行這個計算圖表征。

簡單起見,本指南中定義了一個名為 “ExampleJSON” 的計算圖表征。 ExampleJSON 并不是 JSON,而是沒有控制流的計算圖的簡單表示。例如,假設有以下名為?subgraph_0?的子圖:

input0
   |
  add <-- input1
   |
subtract <-- input2
   |
multiply <-- input3
   |
  out

那么這個子圖的 ExampleJON 看起來類似:

subgraph_0
  input 0 10 10
  input 1 10 10
  input 2 10 10
  input 3 10 10
  add 4 inputs: 0 1 shape: 10 10
  sub 5 inputs: 4 2 shape: 10 10
  mul 6 inputs: 5 3 shape: 10 10

input?關鍵字聲明一個輸入張量及其 ID 和 shape;其他語句用?<op> <output ID> inputs: [input ID] shape: [shape]?語法描述了其計算過程。

在本節中,我們試圖實現以下自定義 TVM runtime 模塊,來執行 ExampleJSON 計算圖。

runtime::Module ExampleJsonCompiler(const NodeRef& ref) {
    ExampleJsonCodeGen codegen(ref);
    std::string code = codegen.gen(); // 注 1
    const auto* pf = runtime::Registry::Get("module.examplejson_module_create"); // 注 2
    ICHECK(pf != nullptr) << "Cannot find ExampleJson module to create the external runtime module";
    return (*pf)(code);
}
TVM_REGISTER_GLOBAL("relay.ext.examplejsoncompiler").set_body_typed(ExampleJsonCompiler);

注1:稍后我們將實現一個自定義 codegen,通過取一個子圖來生成一個 ExampleJSON 代碼字符串。

注2:此行獲取了一個用于創建自定義 runtime 模塊的函數的指針。可以看到它采用剛剛生成的 ExampleJSON 格式的子圖代碼,并對一個 runtime 模塊進行了初始化。

后續章節中,我們將介紹 1)如何實現?ExampleJsonCodeGen?和 2)如何實現和注冊?examplejson_module_create

實現 ExampleJsonCodeGen

與 C codegen 類似,從?ExprVisitor?派生?ExampleJsonCodeGen?以訪問器模式進行子圖遍歷。另一方面,因為不會用到 TVM C++ wrapper,所以不必繼承?CodegenCBase。 codegen 類實現如下:

#include <tvm/relay/expr_functor.h>
#include <tvm/relay/transform.h>
#include <tvm/relay/type.h>
#include <tvm/runtime/module.h>
#include <tvm/runtime/object.h>

#include <fstream>
#include <sstream>

namespace tvm {
namespace relay {
namespace contrib {

class ExampleJsonCodeGen : public ExprVisitor {
  public:
    explicit ExampleJsonCodeGen();

    // 注 1
    void VisitExpr_(const VarNode* node) { /* Skip in this example. */ }
    void VisitExpr_(const CallNode* call) final { /* Skip in this example. */ }

    // 注 2
    std::string gen(NodeRef& ref) {
        this->code = "";
        if (ref->IsInstance<FunctionNode>()) {
            this->visit(Downcast<Function>(ref));
        } else if (ref->IsInstance<relay::ModuleNode>()) {
            relay::Module mod = Downcast<relay::Module>(ref);
            for (const auto& it : mod->functions) {
                this->visit(Downcast<Function>(it.second));
            }
        } else {
            LOG(FATAL) << "The input ref is expected to be a Relay function or module";
        }
        return this->code;
    }

  private:
      /*! \brief The function id that represents a C source function. */
     std::string code;
}

注1:再次實現相應的 visitor 函數,以生成 ExampleJSON 代碼,并將其存儲到類變量?code?中(由于與 C codegen 基本一致,這里跳過了 visitor 函數的實現)。完成計算圖訪問后,在?code?中會生成一個 ExampleJSON 計算圖。

注2:定義內部 API?gen?來獲取子圖,并生成 ExampleJSON 代碼。用戶可以依據個人喜好,為這個 API 命名。

接下來,實現一個自定義 runtime,來利用?ExampleJsonCodeGen?的輸出。

實現自定義 runtime

本節將逐步演示如何自定義 TVM runtime,并將其注冊到 TVM runtime 模塊。自定義 runtime 應位于?src/runtime/contrib/<your-runtime-name>/。本示例中,我們將 runtime 命名為 “example_ext_runtime”。

首先,如下所示定義一個自定義 runtime 類。注意:這個類必須由 TVM?ModuleNode?派生,以保證與其他 TVM runtime 模塊兼容。

#include <dmlc/logging.h>
#include <tvm/runtime/c_runtime_api.h>
#include <tvm/runtime/memory.h>
#include <tvm/runtime/module.h>
#include <tvm/runtime/ndarray.h>
#include <tvm/runtime/object.h>
#include <tvm/runtime/packed_func.h>
#include <tvm/runtime/registry.h>

#include <fstream>
#include <cmath>
#include <map>
#include <sstream>
#include <string>
#include <vector>

namespace tvm {
namespace runtime {
class ExampleJsonModule : public ModuleNode {
 public:
  explicit ExampleJsonModule(std::string graph_json);

  PackedFunc GetFunction(const std::string& name,
                         const ObjectPtr<Object>& sptr_to_self) final;

  const char* type_key() const { return "examplejson"; }

  void SaveToBinary(dmlc::Stream* stream) final;

  static Module LoadFromBinary(void* strm);

  static Module Create(const std::string& path);

  std::string GetSource(const std::string& format = "");

  void Run(int id, const std::vector<int>& inputs, int output);

  void ParseJson(const std::string& json);

 private:
  /* \brief 代表計算圖的 json 字符串。 */
  std::string graph_json_;
  /* \brief 正在被處理的子圖。 */
  std::string curr_subgraph_;
  /*! \brief 由子圖 id 到節點條目的簡單圖。 */
  std::map<std::string, std::vector<NodeEntry> > graph_;
  /* \brief 包含圖中每一個節點的張量的簡單池。 */
  std::vector<NDArray> data_entry_;
  /* \brief 從節點 id 到算子名字的映射。 */
  std::vector<std::string> op_id_;
};

以下這些從?ModuleNode?派生的函數,必須在?ExampleJsonModule?中實現:

  • 構造函數:這個類的構造函數,應該接收一個表征中的子圖,用戶可以自行決定處理和存儲的格式。保存的子圖可以被以下兩個函數使用。
  • GetFunction:這是這個類中最重要的函數。當 TVM runtime 要使用編譯器標簽(compiler tag)執行子圖時,它會從自定義 runtime 模塊中調用此函數。它提供函數名及 runtime 參數,GetFunction?會返回一個打包的函數實現,以供 TVM runtime 執行。
  • SaveToBinary?和?LoadFromBinarySaveToBinary?將 runtime 模塊序列化為二進制格式以供后續部署。用戶使用?export_library?API 時,TVM 會調用這個函數。另一方面,由于用戶這時使用的是自己的計算圖表征,因此必須確保?LoadFromBinary?能夠采用SaveToBinary?生成的序列化二進制文件,來構造相同的 runtime 模塊。
  • GetSource(可選):如果想查看生成的 ExampleJSON 代碼,可以實現這個函數來轉存;否則則可以跳過實現。

實現構造函數

explicit ExampleJsonModule(std::string graph_json) {
  this->graph_json_ = graph_json;
  ParseJson(this->graph_json_);
}

接下來,實現?ParseJson?來解析 ExampleJSON 格式的子圖,并在內存中構造一個計算圖供后續使用。由于本示例不支持帶有分支的子圖,因此只需用一個數組,按順序存儲子圖中的每個節點。

void ParseJson(const std::string& json) {
  std::string line;
  std::string curr_subgraph;
  std::stringstream ss(json);

  while (std::getline(ss, line, '\n')) {
    std::stringstream ss2(line);
    std::string token;
    int id = 0;

    ss2 >> token;
    if (token.find("subgraph_") != std::string::npos) {
      curr_subgraph = token;
      continue;
    }

    ss2 >> id;
    if (op_id_.size() <= static_cast<size_t>(id)) {
      op_id_.resize(id + 1);
      data_entry_.resize(id + 1);
    }

    int64_t total_elements = 1;
    std::vector<int64_t> shape;
    if (token == "input") {
      int64_t size = 0;
      while (ss2 >> size) {
        total_elements *= size;
        shape.push_back(size);
      }
    } else {
      op_id_[id] = token; // 注 1
      bool shape_data = false;
      NodeEntry entry;
      while (ss2 >> token) {
        if (token == "shape:") {
          shape_data = true;
        } else if (shape_data) {
          total_elements *= std::stoll(token);
          shape.push_back(std::stoll(token));
        } else if (token != "inputs:") {
          entry.inputs.push_back(std::stoi(token));
        }
      }
      entry.id = id;
      entry.output = id;
      graph_[curr_subgraph].push_back(entry); // 注 2
    }
    DLDevice dev;
    dev.device_type = static_cast<DLDeviceType>(1);
    dev.device_id = 0;
    data_entry_[id] = NDArray::Empty(shape, DLDataType{kDLFloat, 32, 1}, dev); // 注 3
  }
}

注1:使用類變量?op_id_?將子圖節點 ID 映射到算子名稱(例如?add),以便在 runtime 中調用相應的算子函數。

注2:使用類變量?graph_?從子圖名稱映射到節點數組。GetFunction?將在 runtime 通過子圖 ID 查詢計算圖節點。

注3:使用類變量?data_entry_ ?將子圖節點 ID 映射到張量數據占位符。將輸入和輸出放入 runtime 中對應的數據條目中。

實現 GetFunction

構造函數實現后,以上類變量準備就緒。接下來實現?GetFunction?為 TVM runtime 提供可執行的子圖函數:

PackedFunc GetFunction(const std::string& name,
                       const ObjectPtr<Object>& sptr_to_self) final {
  if (this->graph_.find(name) != this->graph_.end()) {
    this->curr_subgraph_ = name;
    return PackedFunc([sptr_to_self, this](TVMArgs args, TVMRetValue* rv) {

      // Copy input tensors to corresponding data entries.
      for (auto i = 0; i < args.size(); ++i) {
        ICHECK(args[i].type_code() == kNDArrayContainer || args[i].type_code() == kArrayHandle)
            << "Expect NDArray or DLTensor as inputs\n";
        if (args[i].type_code() == kArrayHandle) {
          DLTensor* arg = args[i];
          this->data_entry_[i].CopyFrom(arg);
        } else {
          NDArray arg = args[i];
          this->data_entry_[i].CopyFrom(arg);
        }
      }

      // Execute the subgraph.
      for (const auto& it : this->graph_[this->curr_subgraph_]) {
        this->Run(it.id, it.inputs, it.output);
      }
      ICHECK_GT(graph_.count(this->curr_subgraph_), 0U);

      // Copy the output from a data entry back to TVM runtime argument.
      auto out_idx = graph_[this->curr_subgraph_].back().output;
      if (args[args.size() - 1].type_code() == kArrayHandle) {
        DLTensor* arg = args[args.size() - 1];
        this->data_entry_[out_idx].CopyTo(arg);
      } else {
        NDArray arg = args[args.size() - 1];
        this->data_entry_[out_idx].CopyTo(arg);
      }
      *rv = data_entry_.back();
    });
  } else {
    LOG(FATAL) << "Unknown subgraph: " << name << "\n";
    return PackedFunc();
  }
}

可以看出,GetFunction?由三個主要部分組成。第一部分將數據從 TVM runtime 參數,復制到構造函數中指定的對應數據條目。第二部分使用?Run?函數執行子圖(并稍后實現),并將結果保存到另一個數據條目。第三部分將輸出數據條目中的結果,復制回對應的 TVM runtime 參數進行輸出。

實現 Run

Run?函數接收 1)子圖 ID,2)輸入數據條目索引列表和 3)輸出數據條目索引。

void Run(int id, const std::vector<int>& inputs, int output) {
  // Make a list data entry indexs.
  std::vector<int> args(inputs.begin(), inputs.end());
  args.push_back(output);

  // Initialize data holders.
  std::vector<TVMValue> values(args.size());
  std::vector<int> type_codes(args.size());

  // Initialize a TVM arg setter with TVMValue and its type code.
  TVMArgsSetter setter(values.data(), type_codes.data());

  // Set each argument to its corresponding data entry.
  if (op_id_[id] == "add" || op_id_[id] == "sub" || op_id_[id] == "mul") {
    for (size_t i = 0; i < args.size(); i++) {
      setter(i, data_entry_[args[i]]);
    }
  }

  // Invoke the corresponding operator function.
  if (op_id_[id] == "add") {
    Add(values.data(), type_codes.data(), args.size());
  } else if (op_id_[id] == "sub") {
    Sub(values.data(), type_codes.data(), args.size());
  } else if (op_id_[id] == "mul") {
    Mul(values.data(), type_codes.data(), args.size());
  } else {
    LOG(FATAL) << "Unknown op: " << op_id_[id] << "\n";
  }
}

Run?函數主要包括兩部分。第一部分負責分配?TVMValue?列表,并映射相應的數據輸入塊。這也會成為算子函數的參數。第二部分調用算子函數。盡管使用的 C 函數與上一個示例相同,但用戶可以將?AddSub?和?Mul?替換為自己的引擎。注意,這里需要確保引擎將結果存儲到最后一個參數,從而使得它們可以傳輸回 TVM runtime。

實現上述功能后,用戶自定義的 codegen 和 runtime 就可以執行子圖了。最后一步是注冊一個 API(examplejson_module_create)來創建這個模塊:

TVM_REGISTER_GLOBAL("module.examplejson_module_create")
.set_body_typed([](std::string code){
    auto n = make_object<ExampleJsonModule>(code);
    return runtime::Module(n);
});

實現 SaveToBinary 和 LoadFromBinary

到目前為止,我們已經實現了與其他 TVM runtime 用法一致的自定義 runtime 的主要功能。但是,當用戶想要將構建的 runtime 保存到磁盤以進行部署時,TVM 不知道如何保存。這就是實現?SaveToBinary?和?LoadFromBinary?的原因,它們會告訴 TVM 這個自定義 runtime 如何持久化和復原。

首先實現?SaveToBinary?函數,允許用戶將此模塊保存在磁盤中。

void SaveToBinary(dmlc::Stream* stream) final {
    stream->Write(this->graph_json_);
}

這個函數非常簡單。在構造函數中,我們采取的唯一參數是一個子圖表征(subgraph representation)。也就是說只需一個子圖表征來構造/恢復這個自定義 runtime 模塊。SaveToBinary?只是將子圖寫到一個輸出的 DMLC 流中,當用戶使用?export_library?API 輸出模塊時,自定義模塊將是一個子圖的 ExampleJSON 流。

LoadFromBinary?讀取子圖流并重新構建自定義 runtime 模塊的流程與此類似:

static Module LoadFromBinary(void* strm) {
  dmlc::Stream* stream = static_cast<dmlc::Stream*>(strm);
  std::string graph_json;
  stream->Read(&graph_json);
  auto n = tvm::runtime::make_object<ExampleJsonModule>(graph_json);
  return Module(n);
}

此外,還需要注冊以下函數,啟用相應的 Python API:

TVM_REGISTER_GLOBAL("module.loadbinary_examplejson")
.set_body_typed(ExampleJsonModule::LoadFromBinary);

上述注冊意味著當用戶調用?tvm.runtime.load_module(lib_path)?API,并且導出庫有一個 ExampleJSON 流時,LoadFromBinary?將被調用以創建相同的自定義 runtime 模塊。

另外,如果想支持直接從 ExampleJSON 文件創建模塊,還可以實現一個非常簡單的函數,并注冊一個 Python API,如下所示:

static Module Create(const std::string& path) {
    std::ifstream filep;
    filep.open(path, std::ios::in);
    std::string graph_json;
    std::string line;
    while (std::getline(filep, line)) {
        graph_json += line;
        graph_json += "\n";
    }
    filep.close();
    auto n = tvm::runtime::make_object<ExampleJsonModule>(graph_json);
    return Module(n);
}

TVM_REGISTER_GLOBAL("module.loadfile_examplejson")
.set_body([](TVMArgs args, TVMRetValue* rv) {
    *rv = ExampleJsonModule::Create(args[0]);
});

這意味著用戶可以手動編寫/修改 ExampleJSON 文件,并使用 Python API?tvm.runtime.load_module("mysubgraph.examplejson", "examplejson")?構建自定義模塊。

總結

匯總前文重點:

  • 從?ExprVisitor?和?CodegenCBase(僅適用于 C codegen)派生的 codegen 類,具有以下功能:

    • VisitExpr_(const CallNode* call)?收集調用節點信息。
    • 收集子圖信息所需的其他 visitor 函數。
    • JIT?生成子圖代碼。
    • 注冊 codegen。
  • 創建?CSourceModule?的函數(用于 C codegen)。

  • 從?ModuleNode?派生的 runtime 模塊類,具有以下功能(用于計算圖表征)。

    • 構造函數。
    • GetFunction?生成與 TVM runtime 兼容的?PackedFunc
    • Run?執行子圖。
    • 注冊 runtime creation API。
    • SaveToBinary?和?LoadFromBinary?序列化/反序列化自定義 runtime 模塊。
    • 注冊?LoadFromBinary?API 為tvm.runtime.load_module(your_module_lib_path)提供支持。
    • (可選)Create?支持從表征的子圖文件,構建自定義 runtime 模塊。
  • 一個注釋器,用于注釋用戶 Relay 程序,利用編譯器和 runtime(待定)。

?著作權歸作者所有,如需轉載,請注明出處,否則將追究法律責任
收藏
回復
舉報
回復
相關推薦
午夜亚洲福利老司机| 视频一区视频二区国产精品| 日韩理论片在线观看| 69精品国产久热在线观看| 精品久久久久久| 成人免费毛片网| 久久久久国产精品午夜一区| 国产精品99久久久久久白浆小说| 国模冰冰炮一区二区| 精品久久久香蕉免费精品视频| 缅甸午夜性猛交xxxx| 一级片在线视频| 国产女同互慰高潮91漫画| 亚洲精品日韩成人| 日韩在线二区| 欧美亚洲视频在线看网址| 福利一区和二区| 精品国产麻豆免费人成网站| 黑料吃瓜在线观看| 久久综合久久综合亚洲| 性做爰过程免费播放| 亚洲欧美卡通另类91av| av一区二区久久| 中文字幕欧美三区| 国产精品㊣新片速递bt| 日本韩国在线不卡| 一区视频在线播放| 亚洲一区 二区| 无人在线观看的免费高清视频| 欧美一区二区三区播放老司机| 国产精品日本| caopo在线| 久久久综合亚洲91久久98| 一区二区免费视频| 日韩精品久久久久久久电影99爱| 免费白浆视频| 日韩暖暖在线视频| 日韩欧美黄色动漫| 岛国av在线一区| 国产成人久久| 理论片播放午夜国外| 欧美日韩日本视频| 玖玖玖视频精品| 欧美一区二区三区四区在线| 久久99精品一区二区三区三区| 日本理论片午伦夜理片在线观看| 成年人精品视频| 粉嫩一区二区三区在线看| 在线观看一区| 久久bbxx| 国产精品九九九| 久久久三级国产网站| 涩涩涩久久久成人精品| 国精产品一区一区三区视频| 日韩一区二区免费在线观看| 色综合咪咪久久网| 特黄特色欧美大片| 黄网站在线播放| 欧美日韩亚洲综合在线| 粉嫩绯色av一区二区在线观看| 西野翔中文久久精品字幕| 玖玖玖精品中文字幕| 日本老师69xxx| 精品日韩99亚洲| 国产真实精品久久二三区| 日韩亚洲精品在线观看| 三级久久三级久久| 一区二区三区免费播放| 亚洲黄色中文字幕| 黄色91在线观看| 亚洲男人av| 亚洲综合中文字幕68页| 国产一区二区三区精品欧美日韩一区二区三区 | 国产精品免费网站在线观看| 第九色区av在线| 久久精品久久久久电影| 黄色成人精品网站| 羞羞在线视频| 国产亚洲美女久久| 亚洲国产精品第一区二区三区| 青青草精品视频在线观看| 精品免费视频.| 91精品啪在线观看国产81旧版| 国产aaa一级片| 精品处破学生在线二十三| 国产精品精品| 999精品视频在线| 国产视频亚洲视频| 亚洲成人中文| 在线黄色.com| 欧美激情在线观看视频| 国产在线看一区| 欧美69xxxx| 91九色视频在线| 亚洲私人黄色宅男| 曰本一区二区| 日韩视频在线观看视频| 日韩欧美有码在线| 国产伦一区二区三区| 成人免费视频久久| 色七七影院综合| 国产一区二区0| 福利小视频在线| 久久亚洲国产精品日日av夜夜| 黑人巨大精品欧美一区二区一视频| 欧美精品三级在线| www.国产在线视频| 亚洲欧美制服中文字幕| 日韩经典一区二区| 国产激情在线观看| 丁香五月网久久综合| 高跟丝袜一区二区三区| 国产精品欧美在线观看| 成色在线视频| 欧美一级大胆视频| 亚洲欧洲av色图| 极品一区美女高清| 波多结衣在线观看| 国产+人+亚洲| 国产精品婷婷午夜在线观看| 9999精品| 国模吧无码一区二区三区| 日韩一区二区三区在线播放| www.欧美色图| 欧美高清hd| 制服影音先锋| 国产精品你懂得| 精品久久久久久亚洲国产300| 日韩大片在线| 国产在线一二三区| 久久偷看各类wc女厕嘘嘘偷窃 | 成人6969www免费视频| 好男人社区在线视频| 久久全国免费视频| 亚洲精选在线视频| 欧美丰满日韩| 日韩免费啪啪| 午夜精品一区二区在线观看的| 精品精品国产高清一毛片一天堂| 久久综合综合久久综合| 亚洲性受xxx喷奶水| 丝袜老师办公室里做好紧好爽| 欧美肥婆姓交大片| 亚洲欧美日韩国产综合| 成人在线免费小视频| seseavlu视频在线| 亚洲无玛一区| 久久久99久久精品女同性| 中文字幕中文乱码欧美一区二区| 欧美一级精品片在线看| 欧美另类极品| 日本在线视频www色| 久久国产视频网站| 亚洲午夜影视影院在线观看| 影音先锋一区| 成人影院入口| 一本色道久久亚洲综合精品蜜桃 | 在线观看福利电影| 国产精品动漫网站| 国产美女被下药99| 国产一区二区在线免费播放| 欧美在线观看网站| 欧美日韩国产综合一区二区| 久久精品99国产精品| 欧洲一区在线| 三级做a全过程在线观看| 蜜桃麻豆www久久国产精品| 亚洲视屏在线播放| 中文字幕一区av| 亚洲视频成人| 成人影院网站ww555久久精品| 中文字幕电影在线观看| 新呦u视频一区二区| 久久久久久综合网天天| 91国内精品野花午夜精品| 国产成人免费网站| 91精品1区| 国产成人免费9x9x人网站视频| 国产福利图片| 亚洲精品电影在线一区| 性欧美激情精品| 日韩三级免费观看| **欧美大码日韩| 久久国产三级精品| 精品国产网站| 丝袜美腿诱惑一区二区三区| 裸体免费网站| 亚洲免费视频播放| 成人精品视频在线| 日韩视频免费中文字幕| 精品视频在线免费观看| 久久综合久久久久88| 激情久久婷婷| 电影一区二区在线观看| 污污的网站在线看| h网站在线播放| 国产在线青青草| 日韩精品久久久| 国产日产欧美a一级在线| www.欧美精品|