探索 TVM 进行量化方法
探索 TVM 進行量化方法
Relay框架
如上圖所示,有兩種不同的并行工作正在進行中
? 自動整數量化 - 采用 FP32 框架圖,在 Relay 中自動轉換為 Int8。
? 接受預量化整數模型 - 這種方法接受預量化模型,引入稱為 QNN 的Relay方言,生成 Int8 Relay圖。
關于 Relay Automatic FP16 Downcasting 的討論很少。還沒有任何 RFC。 正在對此進行探索/原型設計,計劃提出 RFC。
Relay優化
? 與目標無關的Relay pass- TVM 社區不斷添加這些 pass。例子是fuse常量,公共子表達式消除等。
? 依賴于目標的Relay pass- 這些 pass轉換Relay圖,針對目標對進行優化。一個例子是 Legalize,或 AlterOpLayout 變換,改變卷積/密集層的布局。TVM 社區正在努力改進基礎架構,實現此類轉換,添加特定于目標的布局轉換。一些基礎架構工作,良好整體設計的先決條件。
Relay到硬件
有了優化的Relay圖,就需要編寫優化的調度。像 FP32 一樣,必須只專注于昂貴的算子,如 conv2d、dense 等。有分散的努力,一些在不同后端工作的開發人員(不一定是 Int8),TVM 社區正在努力統一。
? Intel x86 - 近期 Int8 探索僅限于 Skylake 和 Cascade Lake
? ARM - 目前正在為 FP32 進行一些 NHWC 工作。計劃是在 FP32 工作完成后,將這項工作擴展到 Int8。
? 英偉達——
基于搜索的自動量化
背景
在 tvm 中實現了一個量化工作流程,從一些現有的量化框架中,選擇采用注釋-校準-實現3階段設計:
? Annotation:annotation pass根據每個算子的rewrite函數,重寫graph,插入模擬的量化算子。模擬量化算子模擬,從浮點數到整數量化的舍入誤差和飽和誤差,
? 校準:校準通道將調整模擬量化算子的閾值,減少精度下降。
? 實現:實現過程將實際使用float32計算的模擬圖,轉換為真正的低精度整數圖。
在開發過程中,存在一些缺點:
? 在注釋中,每個注釋作為張INPUT/ WEIGHT/ACTIVATION種不同的量化戰略,這是種特殊算子的,需要不同的組合發生在不同的模式。這個 make annotation 有很多手動規則,變得很難維護。
? 模擬圖沒有總比例和數據類型信息。將尺度推理和數據類型選擇,推遲到實現,這使得這部分的邏輯很難理解。此外,缺乏這些信息,無法在模擬過程中,捕獲溢出錯誤。
? 嘗試將量化模型,部署到不同硬件時,面臨硬件差異。有兩種解決方案: 1.注解時檢查目標,邏輯比較復雜;2.添加一個新的partition pass,首先決定量化拓撲,每個硬件都需要實現一個定制的partition pass。
提出了一個新的量化框架,在循環中,引入了硬件和學習方法。已經進行了多項改進以,解決之前的問題:
? 在每條邊上插入 SimQ (simulated_quantize) 算子,不是通過手動注釋規則。讓學習算法在每條邊上,發現最佳量化策略,不是通過標記。
? 將in_scale, in_dtype,添加out_dtype到 SimQ 的定義中。在模擬期間,執行比例推理和數據類型分配。在 SimQ 中,模擬溢出錯誤。
? 提出Hardware抽象,描述硬件屬性和算子約束。通過這種聲明方式,用戶只需Hardware,為不同的硬件定義不同的對象,無需了解量化邏輯。
工作流程概述
工作流程
給定目標硬件的模型和描述,系統將生成一組,位的選擇空間和Topology量化的空間。這里的Topology意思,考慮到硬件和算子約束,哪些節點/邊將被量化,這將在后面討論。
然后搜索循環開始:學習算法將從選擇空間中,選擇一組參數——每條邊上的位數。閾值可以通過從小型校準數據集,收集的統計數據估計。結合拓撲、位和閾值,可以生成模擬圖,在校準數據集(大約 128 個樣本)上,對其進行評估。輸出/精度作為反饋,學習算法可以選擇下一組位設置。
最后,通過搜索找到的最佳策略,將模擬模型實現真實的低精度整數模型。
規格:位、閾值、標度
將介紹幾種重要性符號:位、閾值、比例。
一般而言,量化的目標,將浮點數(實數值)運行的圖,轉換為整數(定量值)運行的圖,不會犧牲太多精度。給定一個具有真實值的張量,轉換后的 quant 值的關系是什么?這是將在當前實現中遵循的規范:
硬件說明
硬件描述,試圖為在量化過程中,需要考慮的硬件屬性,提供一個中心抽象。通過聲明這些屬性,可以避免在隨后的量化步驟中,處理硬件特定條件。
目前可以指定每個算子的,輸入數據類型和輸出數據類型。
desc = Hardware()
desc[‘add’].append(OpDesc(in_dtypes=[‘int32’, ‘int32’], out_dtypes=[‘int32’]))
desc[‘add’].append(OpDesc(in_dtypes=[‘float32’, ‘float32’], out_dtypes=[‘float32’]))
desc[‘nn.conv2d’].append(OpDesc(in_dtypes=[‘int16’, ‘int16’], out_dtypes=[‘int32’]))
desc[‘nn.conv2d’].append(OpDesc(in_dtypes=[‘int8’, ‘int8’], out_dtypes=[‘int32’]))
desc[‘nn.global_avg_pool2d’].append(OpDesc(in_dtypes=[‘float32’, ‘float32’], out_dtypes=[‘float32’]))
硬件信息在整個過程中已經使用了多次:
? 通過指定算子只支持浮點計算,系統將實現一個結束,需要放在算子之前。可以解決 VTA 流水線的一些問題,指定一些算子,在 VTA 核心上,使用整數指令運行,一些算子,在普通 cpu 上,使用浮點指令。
? 位選擇空間由此產生。對于每條邊,可以推理出使用的最大位,取決于數據類型約束。
? 在決定了每條邊使用的位數后,根據硬件信息,選擇合適的數據類型。
模擬
閾值估計
為了估計閾值,在校準數據集上運行模型,收集需要的統計信息。目前將保存中間算子的所有輸出。為了從收集的輸出中確定閾值,有幾種策略:
? max_range:使用輸出的最大值作為對應節點的閾值。
? power2_range:將最大值四舍五入到最接近的兩個值的冪,作為閾值。
? kl_estimate:選擇一個閾值,使實際輸出和量化輸出之間,KL 距離足夠小。
目前,選擇了這種power2_range方法,可以使用移位來代替乘法,在最終的量化模型中,提供更好的性能。雖然kl_estimate帶來更好的準確度,但相當耗時,目前在搜索中使用不可行。
一個棘手的問題是,對于像加法這樣的算子,只能在其算子的標度為 eqaul 時執行。首先統一其算子的規模。為了實現這一點,估計閾值將在模擬之前進行調整。threshold_rectify引入了一個命名轉換和一個特定于算子的屬性:
@register_fthreshold_rectify(‘add’)
def threshold_rectify_for_add(in_bits, out_bits, in_tholds, out_tholds):
choose scale of the one with maximum threshold
idx = np.argmax(in_tholds)
unified_scale = in_tholds[idx] / (2**(in_bits[idx] - sign_bit))
adjust thresholds according to the unified scale
…
模擬量化
給定比特和閾值,可以嘗試生成一個模型,模擬量化帶來的誤差。經過分析,可以發現誤差來自幾個方面: 1.舍入誤差;2.飽和誤差;3.溢出錯誤。
將simulated_quantize在每條邊上,插入一個算子,試圖模擬這些錯誤。定義如下:
def simulated_quantize(data, in_scale, out_scale, clip_min, clip_max, in_dtype, out_dtype):
if in_dtype == ‘float32’ and out_dtype == ‘float32’:
# no need to quantize
return data
# simulated overflow error
data = data / in_scale
data = topi.cast(data, in_dtype)
data = data * in_scalescaled_data = data / out_scale
# simulate saturated error
clipped_data = topi.clip(scaled_data, clip_min, clip_max)
# simulate round error
rounded_data = topi.cast(topi.round(scaled_data), out_dtype)
out = rounded_data * out_scalereturn out
如何通過位和閾值,計算這些參數呢?out_scale、clip_min、clip_max 是非常嚴格的:
integer_range = 2**(bit - sign_bit)
out_scale = threshold / integer_range
clip_min = - (integer_range - 1)
clip_max = integer_range - 1
對于in_scale、in_dtype、out_dtype,需要做額外推理。
尺度推理
可以在上面的模型中,發現in_scale,SimQ 的實際上,前一個算子輸出的尺度,可以根據算子定義計算。為這樣的屬性,提供了一個注冊函數:
@register_finfer_scale(‘nn.conv2d’):
def infer_scale_for_conv2d(in_scales):
return in_scales[0] * in_scales[1]
數據類型分配
對于數據類型,將遍歷算子,從硬件描述中,選擇滿足輸入位和輸出位要求的算子規范。
學習
有了上面描述的所有準備工作,量化問題轉換為學習問題:希望從選擇空間中,找到最佳設置,以實現模擬模型的最佳精度(或其它目標,如性能),可以使用每輪的輸出(準確度)作為反饋。
對于這個學習問題,實現了random_search, simulated_anealing, 也是一個貪心算法。目前實驗表明貪婪搜索是最可行的。
日志格式
搜索空間很大,搜索過程可能很長,最好有一個正式的日志格式,記錄實驗細節,實現可重復性和可交換性。選擇json格式,詳細信息如下:
? version : 日志格式版本。
? 策略:量化策略。
o model_hash:模型的哈希值,可用于驗證模型是否匹配策略。
o 拓撲:量化模型的拓撲
? node_conds : 哪些節點將被量化
? edge_conds : 哪些邊將被量化
o bits : 每條邊上的位數。
o 閾值:每個節點輸出的閾值。
? 結果:實驗結果
o sim_acc : 模擬模型的精度
搜索速度
實現
在得到最佳量化策略后:拓撲、比特、閾值實現模擬圖,到低精度量化圖,相當直截了當的。只需要用低精度整數運算,替換每條邊上的 SimQ 運算。
調試
調試量化模型哪里出了問題,因為通常只知道最終的準確性很差。實現了inspect_graph_statistic逐層量化前后統計差異的功能,可以快速定位到哪里出錯了。開發過程中,證明非常有幫助。
接口演示
from tvm import hago
ideally we will have predefined description for x86, arm, gpu and vta
hardware = hago.create_sample_hardware()
strategy, sim_acc = hago.search_quantize_strategy(graph, hardware, dataset)
quantizer = hago.create_quantizer(graph, hardware, strategy)
simulated_graph = quantizer.simulate()
quantized_graph = quantizer.quantize()
當前狀態
在 resnet18_v1 上獲得了 68.7% 的初步結果,沒有跳過第一個卷積層,只使用 2 的冪范圍,不是 kl 距離,應該還有更多的改進空間。
參考鏈接:
https://discuss.tvm.apache.org/t/rfc-search-based-automated-quantization/5483
https://discuss.tvm.apache.org/t/quantization-story/3920
總結
以上是生活随笔為你收集整理的探索 TVM 进行量化方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CPU0 处理器的架构及应用
- 下一篇: 一些量化(quantization)技巧