TinyML-TVM是如何驯服Tiny的(上)
TinyML-TVM是如何馴服Tiny的(上)
低成本、人工智能驅動的消費類設備的激增,導致了ML研究人員和從業者對“裸智能”(低功耗,通常沒有操作系統)設備的廣泛興趣。雖然專家已經可以在一些裸機設備上運行某些模型,但是為不同設備集優化模型是一個挑戰,通常需要手動優化特定于設備的庫。對于那些沒有Linux支持的平臺,沒有可伸縮的模型部署解決方案。正因為如此,為了瞄準新設備,開發人員必須實現一次性定制軟件堆棧,以管理系統資源和調度模型執行。
機器學習軟件的手動優化并不是裸機領域所獨有的。事實上,這一直是與其他硬件后端(例如,gpu和fpga)一起工作的開發人員的共同主題。TVM已經被證明能夠抵御新硬件目標的沖擊,但直到現在,它還無法與微控制器的獨特配置相抗衡。為了解決這個領域的問題,擴展了TVM,使其具有一個微控制器后端,稱為μTVM(腳注:發音為“MicroTVM”)。μTVM有助于在裸機設備上執行tensor程序,并通過TVM的內置tensor程序優化器AutoTVM自動優化這些程序。下圖顯示了μTVM+AutoTVM基礎設施的鳥瞰圖:
讓看看它的行動
在討論什么是TVM/MicroTVM或它是如何工作之前,先看一個它在實際中的快速示例。
A standard μTVM setup, where the host communicates with the device via JTAG.
上面,有一個STM32F746ZG板,里面有一個ARM Cortex-M7處理器,考慮到它在低功耗封裝中的強大性能,這是邊緣人工智能的理想部件。使用它的USB-JTAG端口將其連接到臺式機。在桌面上,運行OpenOCD來打開與設備的JTAG連接;反過來,OpenOCD允許μTVM使用與設備無關的TCP套接字控制M7處理器。有了這個設置,可以使用TVM代碼運行CIFAR-10分類器,如下所示(此處為完整腳本):
OPENOCD_SERVER_ADDR = ‘127.0.0.1’
OPENOCD_SERVER_PORT = 6666
TARGET = tvm.target.create(‘c -device=micro_dev’)
DEV_CONFIG = stm32f746xx.default_config(OPENOCD_SERVER_ADDR, OPENOCD_SERVER_PORT)
module, params = get_cifar10_cnn()
with micro.Session(device_config) as sess:
graph, c_module, params = relay.build(module[‘main’], target=TARGET, params=params)
micro_mod = micro.create_micro_mod(c_module, DEV_CONFIG)
graph_mod = graph_runtime.create(graph, micro_mod, ctx=tvm.micro_dev(0))
graph_mod.run(data=data_np)
prediction = CIFAR10_CLASSES[np.argmax(graph_mod.get_output(0).asnumpy())]
print(f’prediction was {prediction}’)
下面是MicroTVM的性能結果,與CMSIS-NN版本5.7.0(commit a65b7c9a)相比,后者是一個手工優化的ML內核庫。
開箱即用的性能不是很好,但這正是AutoTVM的救命稻草。可以為設備編寫一個調度模板,進行一輪自動調整,然后獲得顯著更好的結果。要插入自動調諧結果,只需要替換這一行:
graph, c_module, params = relay.build(module[‘main’], target=TARGET, params=params)
with these lines:
with TARGET, autotvm.apply_history_best(TUNING_RESULTS_FILE):
graph, c_module, params = relay.build(module[‘main’], target=TARGET, params=params)
And our results now look like this:
性能提高了約2倍,現在離CMSIS-NN更近了。盡管MicroTVM CIFAR10的實現與類似的TFLite/CMSIS-NN模型相比具有競爭力,但這項工作剛剛開始利用TVM的優化特性。通過加速其他運營商(如密集/全連接dense/fully-connected)和利用TVM的模型特定量化和運算符融合功能,還有進一步優化的空間。帶有μTVM的TVM能夠發揮最佳性能。如何工作的呢?幕后是怎么回事?現在就開始吧。
Design
The μTVM Device Memory Layout in RAM
μTVM旨在通過最小化必須滿足的一組要求來支持設備的最低公分母。特別是,用戶只需提供:
- 設備的C交叉編譯器工具鏈
- 一種讀/寫設備存儲器并在設備上執行代碼的方法
- 包含設備內存布局和一般體系結構特征的規范
- 為設備準備函數執行的代碼段
大多數裸機設備都支持C和JTAG(調試協議),所以(1)和(2)通常是免費的!此外,(3)和(4)通常是非常小的要求。以下是STM32F746系列板的(3)和(4)示例。
device_config = {
‘device_id’: ‘arm.stm32f746xx’, # unique identifier for the device
‘toolchain_prefix’: ‘arm-none-eabi-’, # prefix of each binary in the cross-compilation toolchain (e.g., arm-none-eabi-gcc)
‘base_addr’: 0x20000000, # first address of RAM
‘section_sizes’: { # dictionary of desired section sizes in bytes
‘text’: 18000,
‘rodata’: 100,
‘data’: 100,
…
},
‘word_size’: 4, # device word size
‘thumb_mode’: True, # whether to use ARM’s thumb ISA
‘comms_method’: ‘openocd’, # method of communication with the device
‘server_addr’: ‘127.0.0.1’, # OpenOCD server address (if ‘comms_method’ is ‘openocd’)
‘server_port’: 6666, # OpenOCD server port (if ‘comms_method’ is ‘openocd’)
}
.syntax unified
.cpu cortex-m7
.fpu softvfp
.thumb
.section .text.UTVMInit
.type UTVMInit, %function
UTVMInit:
/* enable fpu /
ldr r0, =0xE000ED88
ldr r1, [r0]
ldr r2, =0xF00000
orr r1, r2
str r1, [r0]
dsb
isb
/ set stack pointer /
ldr sp, =_utvm_stack_pointer_init
bl UTVMMain
.size UTVMInit, .-UTVMInit
μTVM基礎架構和設備runtime的構建僅僅是為了利用這些需求,正在努力通過支持常見的開源runtime平臺(如mBED OS)來處理編譯和鏈接過程來減少這些需求。
設備會話
考慮到微控制器交互的網絡特性,引入微會話的概念,稍微偏離了標準的TVM代碼。
μTVM中的每一項功能都依賴于與目標設備的開放會話。如果熟悉TVM,可能已經注意到在第一個代碼片段中有一行代碼偏離了規范,即這一行:
…
with micro.Session(device_config) as sess:
…
此with塊內的每一行都可以調用μTVM中的函數,上下文是device_config指定的設備。這條線在hood下面做了很多事情,讓把它拆開。
首先,它使用指定的任何通信方法(通常是OpenOCD)初始化與設備的連接。然后使用指定的交叉編譯器交叉編譯μTVM設備 runtime。最后,主機為編譯后的二進制文件分配空間,并使用打開的連接將二進制文件加載到設備上。
由于 runtime現在位于設備上,自然需要一些函數來運行它。
Module Loading
TVM的核心抽象之一是模塊。模塊為特定的設備/ runtime目標存儲一組相關函數。考慮到微控制器通常沒有操作系統,μTVM需要做大量額外的工作來維護這種高級抽象。為了了解發生了什么,將跟蹤創建和加載μTVM兼容模塊的過程。
假設有一個微型會議打開設備和實現二維卷積的TVM調度。如果想把它加載到微控制器上,需要它發出C代碼。要做到這一點,只需要設定目標tvm.build or relay.build. Example:
graph, c_module, params = relay.build(module[‘main’], target=‘c -device=micro_dev’, params=params)
通過這樣設置目標,構建過程將通過C代碼生成后端運行。但是,生成的C模塊仍然駐留在主機上。為了將其加載到設備上,通過μTVM基礎設施中的一個核心功能來運行它:create_micro_mod。
例子:
micro_mod = micro.create_micro_mod(c_module, DEV_CONFIG)
上面的行交叉編譯模塊中的C源代碼,為生成的二進制文件分配空間(這樣它就可以與設備內存中的 runtime共存),然后將二進制文件的每個部分發送到設備上分配的插槽中。一旦模塊二進制文件在設備內存中處于合適的位置,二進制文件中的函數指針將被修補,使模塊能夠在設備 runtime訪問help函數(例如,分配草稿行)。
現在,在設備上加載內核后,可以獲取卷積函數的遠程句柄,如下所示:
micro_func = micro_mod[‘conv2d’]
Tensor Loading
If we want to call an operator, we first need some tensors as arguments:
data_np, kernel_np = get_conv_inputs()
ctx = tvm.micro_dev(0)
data = tvm.nd.array(data_np, ctx=ctx)
kernel = tvm.nd.array(kernel_np, ctx=ctx)
根據其數據類型(例如int8、float32等)和形狀,計算每個張量的字節大小,主機在設備堆上分配內存區域。然后將張量的數據加載到分配的區域中。
函數調用
算子執行可能是這個系統中最棘手的部分。為了簡化它的表示,將首先討論嚴格執行(算子一被調用就立即執行),然后是延遲執行(只有在需要算子的結果時才執行算子)——后者是系統的實際工作方式。
嚴格執行
調用函數時,輸入和輸出張量都作為參數傳遞,這就是所謂的目標傳遞樣式:
conv2D(data, kernel, output)
考慮到這些張量已經在設備上分配,只需要向設備發送元數據(device address, shape, and data type)(設備地址、形狀和數據類型),這樣設備就知道要使用哪個駐留張量。下面顯示的是一個名為“runtime”的函數的調用。在構造這個表示之前,需要將元數據序列化到專門為此目的而存在的設備上的arguments部分中。
/
- task struct for uTVM
/
typedef struct {
/ pointer to function to call for this task /
int32_t (func)(void, void, int32_t);
/* array of argument tensors /
TVMValue arg_values;
/* array of datatype codes for each argument /
int arg_type_codes;
/* number of arguments */
int32_t num_args;
} UTVMTask;
在嚴格的設置中,有一個全局UTVMTask實例,從主機端寫入該實例。一旦寫入任務,runtime就擁有了執行函數所需的一切,可以在runtime的入口點開始執行。runtime將執行一些輕量級初始化,運行算子,然后將控制權返回給主機。
總結
以上是生活随笔為你收集整理的TinyML-TVM是如何驯服Tiny的(上)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何在TVM上集成Codegen(下)
- 下一篇: TinyML-TVM是如何驯服Tiny的