Runtime系统
Runtime系統
TVM 支持多種編程語言用于編譯器堆棧的開發和部署。解釋了 TVM runtime的關鍵元素。
滿足以下需求:
? 部署:從 python/javascript/c++ 語言調用編譯函數。
? 調試:在 python 中定義一個函數并從編譯的函數中調用。
? 鏈接:編寫驅動程序代碼,調用設備特定代碼(CUDA),從編譯的主機函數中調用。
? 原型:從 python 定義一個 IR pass ,并用 C++ 后端調用。
? Expose:用c++開發的編譯器棧到前端(即python)
? 實驗:將編譯后的函數發送到嵌入式設備,直接在那里運行。
用任何語言定義一個函數,用另一種語言調用。runtime核心盡可能少,以便部署到嵌入式設備。
PackedFunc
PackedFunc是一個簡單而優雅的解決方案,可以解決列出的挑戰。單個PackedFunc對象代表一個函數調用,調用和被調用可能使用不同的語言。
以下代碼塊提供了一個 C++ 示例
#include <tvm/runtime/packed_func.h>
void MyAdd(TVMArgs args, TVMRetValue* rv) {
// automatically convert arguments to desired type.
int a = args[0];
int b = args[1];
// automatically assign value return to rv
*rv = a + b;
}
void CallPacked() {
PackedFunc myadd = PackedFunc(MyAdd);
// get back 3
int c = myadd(1, 2);
}
在上面的代碼塊中,定義了一個 PackedFunc MyAdd。需要兩個參數:args代表輸入參數,rv代表返回值。該函數是type-erased,表明函數簽名不限制pass傳入或返回的輸入類型。調用 PackedFunc 時,將輸入參數打包到堆棧上的 TVMArgs,并通過 TVMRetValue 取回結果。
使用 C++ 中的模板技巧,可以像調用普通函數一樣調用 PackedFunc。由于type-erased的性質,可以從像 python動態語言中調用 PackedFunc,而無需為創建的每個新類型函數添加額外的膠水代碼。以下示例在 C++ 中注冊 PackedFunc 并從 python 調用。
// register a global packed function in c++
TVM_REGISTER_GLOBAL(“myadd”)
.set_body(MyAdd);
import tvm
myadd = tvm.get_global_func(“myadd”)
prints 3
print(myadd(1, 2))
大多數PackedFunc的奇妙之處在于TVMArgs和TVMRetValue結構。限制了可以傳遞的可能類型列表。以下是常見的:
? 整數、浮點數和字符串
? PackedFunc 本身
? 編譯模塊
? 用于張量對象交換的 DLtensor*
? TVM表示 任何IR 中的對象
該限制使實現變得簡單,無需序列化。盡管是最小的,但 PackedFunc 足以滿足深度學習部署的用例,因為大多數函數只采用 DLtensor 或數字。
由于一個 PackedFunc 可以將另一個 PackedFunc 作為參數,可以將函數從 python(作為 PackedFunc)傳遞給 C++。
TVM_REGISTER_GLOBAL(“callhello”)
.set_body([](TVMArgs args, TVMRetValue* rv) {
PackedFunc f = args[0];
f(“hello world”);
});
import tvm
def callback(msg):
print(msg)
convert to PackedFunc
f = tvm.convert(callback)
callhello = tvm.get_global_func(“callhello”)
prints hello world
callhello(f)
TVM 提供了一個最小的 C API,將 PackedFunc 嵌入到任何語言中。除了python,目前還支持 java和javascript。這種嵌入式 API 的理念與 Lua 非常相似,只是沒有新語言而是使用 C++。
關于 PackedFunc 的一個有趣事實是,用于編譯器和部署堆棧。
? TVM 的所有編譯器傳遞函數都作為 PackedFunc 暴露給前端
? 編譯后的模塊也將編譯后的函數返回為 PackedFunc
為了保持runtime最少,將 IR 對象支持與部署runtime隔離。結果runtime需要大約 200K - 600K,具體取決于包含多少runtime驅動程序模塊(例如,CUDA)。
與普通函數相比,調用 PackedFunc 的開銷很小,只在堆棧中保存了幾個值。所以只要不包裝小函數就可以了。總之,PackedFunc 是 TVM 中的通用粘合劑,廣泛使用支持編譯器和部署。
模塊
由于 TVM 支持多種類型的設備,需要支持不同類型的驅動程序。必須使用驅動程序 API 來加載內核,以打包格式設置參數并執行內核啟動。需要修改驅動程序 API,以便公開的函數是線程安全的。經常需要在 C++ 中實現這些驅動粘合,暴露給用戶。不能對每種類型的函數都這樣做,所以PackedFunc正確。
TVM 將編譯對象定義為Module。用戶可以從 Module 中獲取編譯后的函數作為 PackedFunc。生成的編譯代碼可以在runtime從 Module 動態獲取函數。緩存函數句柄在第一次調用中,并在后續重用。將設備代碼和從生成的代碼鏈接到任何 PackedFunc(例如,python)回調。
ModuleNode 是一個抽象類,可以由每種類型的設備實現。到目前為止,支持 CUDA、Metal、OpenCL 和加載動態共享庫的模塊。這種抽象使得引入新設備變得容易,不需要為每種類型的設備重新生成主機代碼。
遠程部署
PackedFunc 和 Module 系統,可以輕松地將函數直接傳送到遠程設備。有一個 RPCModule 序列化參數數據移動,并在遠程啟動計算。
RPC 服務器本身是最小的,可以捆綁到runtime中。可以在 iPhone/android/raspberry pi 甚至瀏覽器上啟動一個最小的 TVM RPC 服務器。服務器上的交叉編譯和測試模塊的交付可以在同一個腳本中完成。查看 交叉編譯和 RPC以獲取更多詳細信息。
這種即時反饋帶來了很多好處。例如,為了在 iPhone 上測試生成代碼的正確性,不再需要從頭開始在 swift/objective-c 中編寫測試用例——可以使用 RPC 在 iPhone 上執行,將結果復制回來并在主機上進行驗證通過 numpy。還可以使用相同的腳本分析。
TVM 對象和編譯器堆棧
在 PackedFunc runtime系統構建編譯器堆棧 API。為了研究的需要,面臨著編譯器 API 的不斷變化。每當想測試新的原語時,都需要一個新的語言對象或 IR 節點。但是,不希望每次更改 API。除此之外,還希望
? 能夠序列化任何語言對象和 IR
? 能夠以前端語言探索、打印和操作 IR 對象,進行快速原型設計。
引入了一個名為Object的基類來解決這個問題。編譯器堆棧中的所有語言對象都是Object. 每個對象都包含一個字符串 type_key,唯一標識對象的類型。選擇 string 而不是 int 作為類型鍵,因此,Object可以以分散的方式添加新類,而無需將代碼添加回中央存儲庫。為了加快調度速度,在runtime為每個 type_key 分配一個整數 type_index。
通常Object可以在語言中的多個地方引用,使用 shared_ptr 來跟蹤引用。使用ObjectRefclass 來表示對Object。 可以粗略地將ObjectRef類看作Object容器的shared_ptr 。還可以定義子類ObjectRef來保存Object。每個Object子類都需要定義 VisitAttr 函數。
class AttrVisitor {
public:
virtual void Visit(const char* key, double* value) = 0;
virtual void Visit(const char* key, int64_t* value) = 0;
virtual void Visit(const char* key, uint64_t* value) = 0;
virtual void Visit(const char* key, int* value) = 0;
virtual void Visit(const char* key, bool* value) = 0;
virtual void Visit(const char* key, std::string* value) = 0;
virtual void Visit(const char* key, void** value) = 0;
virtual void Visit(const char* key, Type* value) = 0;
virtual void Visit(const char* key, ObjectRef* value) = 0;
// …
};
class BaseAttrsNode : public Object {
public:
virtual void VisitAttrs(AttrVisitor* v) {}
// …
};
每個Object子類將override 覆蓋,以訪問其成員。這是 TensorNode 的一個示例實現。
class TensorNode : public Object {
public:
/*! \brief The shape of the tensor /
Array shape;
/! \brief data type in the content of the tensor /
Type dtype;
/! \brief the source operation, can be None /
Operation op;
/! \brief the output index from source operation /
int value_index{0};
/! \brief constructor */
TensorNode() {}
void VisitAttrs(AttrVisitor* v) final {
v->Visit(“shape”, &shape);
v->Visit(“dtype”, &dtype);
v->Visit(“op”, &op);
v->Visit(“value_index”, &value_index);
}
};
在上面的例子中,Operation和Array都是 ObjectRef。VisitAttrs 為提供了一個API反射,訪問對象的每個成員。可以使用這個函數來訪問節點并遞歸地序列化任何語言對象。允許在前端語言中輕松獲取對象的成員。例如,在下面的代碼中,訪問了 TensorNode 的 op 字段。
import tvm
from tvm import te
x = te.placeholder((3,4), name=“x”)
access the op field of TensorNode
print(x.op.name)
Object可以在不更改前端runtime的情況下,添加到 C++,從而可以輕松地對編譯器堆棧進行擴展。這不是將成員暴露給前端語言的最快方法,但可能是最簡單的方法之一。使用 Python 進行測試和原型設計,仍然使用 C++ 來完成繁重的工作。
實施細則
PackedFunc 中的每個參數都包含一個union融合值TVMValue 和一個類型代碼。這種設計允許動態類型語言直接轉換為相應的類型,而靜態類型語言在轉換過程中進行runtime類型檢查。
相關文件是
? C++ API 的packed_func.h
? c_runtime_api.cc用于 C API 以及如何提供回調。
為了支持擴展類型,使用了一個注冊系統來注冊類型相關的信息,比如在 C++ 中支持 any,更多細節參見擴展類型。
總結
- 上一篇: TVM/Relay 的 Partitio
- 下一篇: DSP与CEVA芯片