上一篇小編和大家分享了在運(yùn)行客戶的一個(gè)模型時(shí)遇到了一個(gè)PRelu算子,在利用TFLm自帶的PRelu參考實(shí)現(xiàn)的代碼,其中PRelu竟然拋出了188ms的天文數(shù)字...因此小編開(kāi)始準(zhǔn)備PRelu算子的優(yōu)化工作。
分析了參考實(shí)現(xiàn)后,發(fā)現(xiàn)了兩個(gè)優(yōu)化方向,其一是PRelu中alpha參數(shù)的特殊性所帶來(lái)的內(nèi)存訪問(wèn)優(yōu)化;以及量化模型所帶來(lái)的反量化問(wèn)題。
本期小編就和大家一起來(lái)看下對(duì)于反量化問(wèn)題的優(yōu)化細(xì)節(jié)。在開(kāi)始前,再來(lái)回顧一下小編所特殊定制的模型:
這是一個(gè)具有5個(gè)節(jié)點(diǎn)的小巧的深度神經(jīng)網(wǎng)絡(luò),輸入時(shí)128*128*3,模型推理時(shí)間(采用Keil IDE,ofast優(yōu)化):
跳過(guò)PRelu算子,模型推理時(shí)間:
這樣我們就可以得出PRelu算子的執(zhí)行時(shí)間為13ms,接下來(lái)就將以此為基礎(chǔ)進(jìn)行算法優(yōu)化,TFLm算法實(shí)現(xiàn):
output_value = MultiplyByQuantizedMultiplier(
input_value, params.output_multiplier_1, params.output_shift_1);
output_value = MultiplyByQuantizedMultiplier(
input_value * alpha_value, params.output_multiplier_2, params.output_shift_2);
上一篇小編給大家解釋了為何需要進(jìn)行反量化操作以及其必要性。所謂反量化操作的本質(zhì),就是要用int8類型的中間結(jié)果來(lái)準(zhǔn)確表達(dá)浮點(diǎn)結(jié)果。那么具體來(lái)說(shuō)需要怎么操作呢?下面就是嚴(yán)謹(jǐn)?shù)耐乒江h(huán)節(jié),請(qǐng)讀友們不要眨眼:
首先是整數(shù)環(huán)節(jié),我們假設(shè)輸入為input, 輸出為output,參數(shù)alpha;其參數(shù)類型均為int8。而想要將其反量化為浮點(diǎn)數(shù),需要為其設(shè)定對(duì)應(yīng)的量化參數(shù),分別為scale以及zero_point。這樣一來(lái),變量的浮點(diǎn)數(shù)表示即為:
v_fp=scale* (v_i8+zero_point)
為了分析簡(jiǎn)單,我們假設(shè)zero_point為0,那么上式可被簡(jiǎn)化為,當(dāng)然實(shí)際計(jì)算式,只需要將輸入值提前加上其zero_point再進(jìn)行操作即可:
v_fp=scale* v_i8
接下來(lái)我們根據(jù)輸入數(shù)據(jù)的符號(hào)進(jìn)行區(qū)分,當(dāng)輸入為正時(shí),其輸出結(jié)果為,
scale_o* output=scale_i* v_i8
output=scale_i / scale_0* v_i8
這樣我們就可以根據(jù)輸入直接獲取int8類型的輸出結(jié)果。
當(dāng)輸入為負(fù)時(shí):
scale_o* output=(scale_a*alpha)*(scale_i* v_i8)
output=((scale_a* scale_i)/scale_0)* 〖alpha*v〗_i8)
這樣也就獲得了相對(duì)應(yīng)的負(fù)數(shù)輸入所對(duì)應(yīng)的輸出結(jié)果。不過(guò),征程還沒(méi)有結(jié)束,TFLm的參考實(shí)現(xiàn)會(huì)將這兩組浮點(diǎn)數(shù)代表的scale參數(shù)轉(zhuǎn)換為指數(shù)形式,并以mul+shift的形式保存為:正數(shù)output_multipiler_1和output_shift_1, 負(fù)數(shù)output_multipiler_2和output_shift_2。
知道了結(jié)果是如何進(jìn)行反量化操作的,回過(guò)頭我們看看TFLm的實(shí)現(xiàn):
inline std::int16_t SaturatingRoundingDoublingHighMul(std::int16_t a,
std::int16_t b) {
bool overflow = a == b && a == std::numeric_limits<std::int16_t>::min();
std::int32_t a_32(a);
std::int32_t b_32(b);
std::int32_t ab_32 = a_32 * b_32;
std::int16_t nudge = ab_32 >= 0 ? (1 << 14) : (1 - (1 << 14));
std::int16_t ab_x2_high16 =
static_cast<std::int16_t>((ab_32 + nudge) / (1 << 15));
return overflow ? std::numeric_limits<std::int16_t>::max() : ab_x2_high16;
}
inline int32_t MultiplyByQuantizedMultiplier(int32_t x,
int32_t quantized_multiplier,
int shift) {
using gemmlowp::RoundingDivideByPOT;
using gemmlowp::SaturatingRoundingDoublingHighMul;
int left_shift = shift > 0 ? shift : 0;
int right_shift = shift > 0 ? 0 : -shift;
return RoundingDivideByPOT(SaturatingRoundingDoublingHighMul(
x * (1 << left_shift), quantized_multiplier),
right_shift);
}
首先arm的cmsis-nn庫(kù)是兼容這種量化方式的,那么他也一定有一個(gè)這樣的實(shí)現(xiàn),功夫不負(fù)有心人,這個(gè)函數(shù)叫做arm_nn_requantize,直接替換MultiplyByQuantizedMultiplier函數(shù)讓我們先看一下速度:
嗯,不錯(cuò),有效果,44ms->42ms,相當(dāng)于PRelu算子執(zhí)行速度從13ms->11ms; 還可以,無(wú)痛漲點(diǎn)。翻看arm_nn_requantize函數(shù),其中也不乏一些手撕浮點(diǎn)數(shù)的神秘操作??紤]到我們的RT1170本身兼?zhèn)湟粋€(gè)FPU單元,為啥不直接用浮點(diǎn)數(shù)計(jì)算呢?這次我們不對(duì)scale參數(shù)進(jìn)行指數(shù)化轉(zhuǎn)換,而是直接將其作為浮點(diǎn)數(shù)參與運(yùn)算,公式就是上面我們推導(dǎo)的:
// init the float mul, shift
float real_multiplier_1 = (input->params.scale) / (output->params.scale);
float real_multiplier_2 = (input->params.scale) * (alpha->params.scale) / (output->params.scale);
計(jì)算方式重新定義為:
output_value = MultiplyByQuantizedMultiplierFP32(
input_value, multiplier_pos);
static inline int32_t MultiplyByQuantizedMultiplierFP32(int32_t x, float mul){
return roundf(x * mul);
是不是看著非常清爽?讓我們看下時(shí)間:
額。。。有點(diǎn)尷尬,竟然沒(méi)有長(zhǎng)點(diǎn),而且和TFLm的原始實(shí)現(xiàn)速度一樣。小編才提到的內(nèi)存優(yōu)化不是還沒(méi)有上?浮點(diǎn)運(yùn)算這邊還有小插曲,讓我們繼續(xù)前行:
首先讓我們先看下浮點(diǎn)操作再如何進(jìn)行優(yōu)化,由于我們的代碼由于采用了Ofast優(yōu)化策略,因此代碼的可閱讀性變得很差。為了進(jìn)行代碼優(yōu)化,小編需要特殊編寫一組浮點(diǎn)運(yùn)算代碼以供優(yōu)化參考,因?yàn)槲覀冏罱K實(shí)現(xiàn)的是一個(gè)int32數(shù)據(jù)與浮點(diǎn)數(shù)相乘:
static inline int32_t MultiplyByQuantizedMultiplierFP32(int32_t x, float mul){
return roundf(x * mul);
}
編寫代碼如下:
int32_t v1 = (float)SysTick->VAL;
float v2 = SysTick->VAL * 0.0001f;
int32_t v3 = (v1 * v2);
PRINTF("%d", v3);
其所生成的匯編代碼為:
int32_t v1 = (float)SysTick->VAL;
800040DC LDR R2, [R0]
800040DE STRD R2, R1, [SP]
800040E2 VLDR D0, [SP]
800040E8 VSUB.F64 D0, D0, D1
800040F0 VCVT.F32.F64 S0, D0
800040F8 VCVT.S32.F32 S0, S0
800040FE VMOV R0, S0
float v2 = SysTick->VAL * 0.0001f;
800040E6 LDR R0, [R0]
800040EC STRD R0, R1, [SP, #16]
800040F4 VLDR D2, [SP, #16]
80004102 VSUB.F64 D0, D2, D1
80004106 VLDR D2, =0x4330000080000000
80004110 VCVT.F32.F64 S0, D0
80004122 VMUL.F32 S0, S0, S4
int32_t v3 = (v1 * v2);
800040FC STR R1, [SP, #12]
8000410A EOR R0, R0, #0x80000000
8000410E STR R0, [SP, #8]
80004116 VLDR D1, [SP, #8]
8000411A VSUB.F64 D1, D1, D2
8000411E VLDR S4, =0x38D1B717
80004126 VCVT.F32.F64 S2, D1
8000412A VMUL.F32 S0, S2, S0
到這里,小伙伴們可能已經(jīng)看到了端倪,小編也特意為大家標(biāo)紅了幾條匯編代碼。那小編就先拋出疑問(wèn):我們明明定義的浮點(diǎn)型, 咋還用上double類型了呢?相同的代碼用GCC編譯會(huì)是什么樣的呢?
int32_t v1 = (float)SysTick->VAL;
300030f2: mov.w r3, #3758153728 ; 0xe000e000
300030f6: vldr s15, [r3, #24]
71 float v2 = SysTick->VAL * 0.0001f;
300030fa: vldr s14, [r3, #24]
300030fe: vcvt.f32.u32 s14, s14
30003102: vldr s13, [pc, #92] ; 0x30003160 +148>
30003106: vmul.f32 s14, s14, s13
72 int32_t v3 = __builtin_roundf(v1 * v2);
3000310a: vcvt.f32.s32 s15, s15
3000310e: vmul.f32 s15, s15, s14
30003112: vrinta.f32 s15, s15
看似正常,沒(méi)有使用double類型寄存器;那問(wèn)題出在哪呢?難道Keil對(duì)于浮點(diǎn)數(shù)的支持不太行?翻閱了一萬(wàn)件資料之后,小編在編譯時(shí)使用一個(gè)叫做-ffp-mode = full的參數(shù),這個(gè)參數(shù)的意思是:
同時(shí)還有兩個(gè)參數(shù),是-fp-mode=fast和-fp-mode=std,簡(jiǎn)單來(lái)講就是full會(huì)保證轉(zhuǎn)換精度,因此會(huì)出現(xiàn)使用double類型的情況。而fast可能會(huì)丟失一點(diǎn)精度,而std介于兩者之間。那么我們定義-fp-mode=std試試?
代碼如下:
int32_t v1 = (float)SysTick->VAL;
800040D4 VLDR S0, [R0]
800040E2 VCVT.F32.U32 S0, S0
float v2 = SysTick->VAL * 0.0001f;
800040D8 VLDR S2, [R0]
800040DC VCVT.F32.U32 S2, S2
800040E6 VMUL.F32 S2, S2, S4
int32_t v3 = (v1 * v2);
800040EA VRINTZ.F32 S0, S0
800040EE VMUL.F32 S0, S2, S0
嗯,優(yōu)雅,就是這么簡(jiǎn)單。指令條數(shù)減少了很多啊,讓我們?cè)賮?lái)看看時(shí)間:
這樣一來(lái)就和arm提供的方式一致了,相比實(shí)現(xiàn)就清爽了很多。
接下來(lái)小編還有一個(gè)殺手锏,內(nèi)存優(yōu)化,不過(guò)此處的內(nèi)存優(yōu)化是有個(gè)前提,我們知道PRelu的alpha參數(shù)是按通道的,這里要做個(gè)特殊的假設(shè),假設(shè)輸入維度為 h w c,而且alpha參數(shù)是按h w共享的,即只有最后一維參數(shù),維度為11 c:
if((alpha_shape.Dims(0) == 1) && (alpha_shape.Dims(1) == 1))
這樣我們就可以按c通道進(jìn)行展開(kāi),并進(jìn)行順序訪問(wèn);
其次,輸入數(shù)據(jù)為int8類型,原始實(shí)現(xiàn)方式中每次只取一個(gè)數(shù)據(jù)進(jìn)行計(jì)算:
const int32_t input_value =
params.input_offset + input_data[input_index];
這樣編譯器會(huì)將起編譯為L(zhǎng)DRB指令,即每次只獲取一個(gè)字節(jié)的數(shù)據(jù)。對(duì)此進(jìn)行優(yōu)化,每次讀取4個(gè)字節(jié)的數(shù)據(jù),這樣可以編譯為L(zhǎng)DR指令,并放置于寄存器中,減少訪存次數(shù):
uint32_t steps = alpha_shape.Dims(2);
uint32_t total_size = input_shape.Dims(0) * input_shape.Dims(1) * input_shape.Dims(2) * input_shape.Dims(3);
for(int value_index=0;value_index T *alpha = (T *)alpha_data;
// each 4, calc the time_tick
uint32_t inner_loop = steps >> 2;
int8_t *input_data_ptr = (int8_t*)input_data + value_index;
int8_t *output_data_ptr = (int8_t*)output_data + value_index;
while(inner_loop --){
int32_t input_data_32 = *((int32_t*)(input_data_ptr));
input_data_ptr += 4;
uint32_t count = 4;
while(count--){
int8_t input_data_8 = input_data_32 & 0xFF;
input_data_32 >>= 8;
。。。。
;value_index+=steps){>
這樣一來(lái),就可以順序取數(shù)據(jù),并且每次讀取4個(gè)字節(jié),看下時(shí)間:
Nice!~
PRelu的時(shí)間變?yōu)?7ms – 31ms = 6ms。經(jīng)過(guò)兩步優(yōu)化,將PRelu的執(zhí)行時(shí)間降低了7ms。用客戶的模型測(cè)試一下,PRelu算子運(yùn)行時(shí)間從之前的188ms降低到了51ms。Perfect!
不過(guò),小編精益求精,還有一些微小的優(yōu)化空間,后續(xù)將會(huì)進(jìn)一步優(yōu)化。
歡迎朋友們持續(xù)關(guān)注~
-
mcu
+關(guān)注
關(guān)注
146文章
17984瀏覽量
367075 -
NXP
+關(guān)注
關(guān)注
61文章
1348瀏覽量
189411 -
恩智浦
+關(guān)注
關(guān)注
14文章
5981瀏覽量
116866 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4381瀏覽量
64897 -
算子
+關(guān)注
關(guān)注
0文章
16瀏覽量
7348
原文標(biāo)題:PRelu算子調(diào)優(yōu)經(jīng)歷-函數(shù)優(yōu)化策略
文章出處:【微信號(hào):NXP_SMART_HARDWARE,微信公眾號(hào):恩智浦MCU加油站】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
Nginx在企業(yè)環(huán)境中的調(diào)優(yōu)策略
手把手教你如何調(diào)優(yōu)Linux網(wǎng)絡(luò)參數(shù)
xgboost超參數(shù)調(diào)優(yōu)技巧 xgboost在圖像分類中的應(yīng)用
華為云 X 實(shí)例 CPU 性能測(cè)試詳解與優(yōu)化策略

MCF8316A調(diào)優(yōu)指南

MCT8316A調(diào)優(yōu)指南

MCT8315A調(diào)優(yōu)指南

MMC DLL調(diào)優(yōu)

TDA3xx ISS調(diào)優(yōu)和調(diào)試基礎(chǔ)設(shè)施

大數(shù)據(jù)從業(yè)者必知必會(huì)的Hive SQL調(diào)優(yōu)技巧
智能調(diào)優(yōu),使步進(jìn)電機(jī)安靜而高效地運(yùn)行

MMC SW調(diào)優(yōu)算法

TAS58xx系列通用調(diào)優(yōu)指南

AM6xA ISP調(diào)優(yōu)指南

OSPI控制器PHY調(diào)優(yōu)算法

評(píng)論