TinyML-TVM是如何馴服Tiny的(上)

wujianming_110117發表於2020-12-16

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旨在通過最小化必須滿足的一組要求來支援裝置的最低公分母。特別是,使用者只需提供:

  1. 裝置的C交叉編譯器工具鏈
  2. 一種讀/寫裝置儲存器並在裝置上執行程式碼的方法
  3. 包含裝置記憶體佈局和一般體系結構特徵的規範
  4. 為裝置準備函式執行的程式碼段
    大多數裸機裝置都支援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將執行一些輕量級初始化,執行運算元,然後將控制權返回給主機。

相關文章