与xnn/qnn卷积的异同点
tengine中实现了NHWC的winograd,目前nhwc的卷积主要以xnn/qnn框架为典型,所以这里拿tengine中的U*V和xnn/qnn卷积做个比较,简单总结一下,因为笔者觉得有相似的地方,举一反三嘛~
区别的地方:
1、分块大小不同,tengine是4x12的分块,xnn/qnn一般是4x4或4x8 (浮点引擎)
2、winograd毕竟在kernel维度不需要累加,只需在channel维度累加即可,因此实现的时候遍历输入通道求和即可
3、xnn/qnn计算的时候是一个输入broad到多个kernel的output维度相乘,然后遍历kernel_w*kernel_h*input_c,输入可以同时做4个,这样可以得到4x8或者4x4的output分块输出结果;tengine是将kernel broad到输入块维度相乘
4、输入的pack方式不同,虽然xnn/qnn是采用indirect buffer的形式,但是从内存来看也指pack的wh维度上的值,即kenelsize*block*input_c,而tengine的输入pack是kernelsize*input*block,这样的pack方式取决于卷积和点乘的实现方式不同
实质上是一样的
相似的地方:
1、kernel的pack都是按照nchw的维度,即输出通道,然后是输入通道,最后是kernelsize维度
汇编代码解析
关于预加载指令讲解可以参考博客:
https://jzwdsb.github.io/2018/07/neon_frequently_use/
https://www.jianshu.com/p/5f75fa02c5d0
//函数声明:
extern void wino_sgemm_4x12_A17(float* output, float* input, float* kernel, long cin);
.section .text, "ax"
//.section指示把代码划分成若干个段(Section),程序被操作系统加载执行时,每个段被加载到不同的地址,操作系统对不同的页面设置不同的读、写、执行权限
//.text段保存代码,是只读和可执行的,后面那些指令都属于.text段
//"ax"表示该节区可分配并且可执行, ax是 allocation execute的缩写
.align 5
//2^5 4字节对齐
.type wino_sgemm_4x12_A17 STT_FUNC
.global wino_sgemm_4x12_A17
//wino_sgemm_4x12_A17 是一个符号(Symbol),符号在汇编程序中代表一个地址,可以用在指令中,
汇编程序经过汇编器的处理之后,所有的符号都被替换成它所代表的地址值。在C语言中我们通过变量名访问一
个变量,其实就是读写某个地址的内存单元,我们通过函数名调用一个函数,其实就是跳转到该函数第一条指令
所在的地址,所以变量名和函数名都是符号,本质上是代表内存地址的。
//.global指示告诉汇编器,wino_sgemm_4x12_A17这个符号要被链接器用到,所以要在目标文件的符号
表中标记它是一个全局符号。_start就像C程序的main函数一样特殊,是整个程序的入口,链接器在链接时会查
找目标文件中的wino_sgemm_4x12_A17符号代表的地址,把它设置为整个程序的入口地址,所以每个汇编程序
都要提供一个wino_sgemm_4x12_A17符号并且用.global声明。如果一个符号没有用.global声明,就表示这
个符号不会被链接器用到
.hidden wino_sgemm_4x12_A17
#void wino_sgemm_4x12_A17(float* output,
# float* input,
# float* kernel,
# long cin)
wino_sgemm_4x12_A17:
.fpu neon
pld [r1,#0x80] #预加载128Byte的input数据
#一次计算4x4个输入,即4个kernelsize维度x4个输入通道
push {r4, lr} #入栈保存r4和链接寄存器r14=lr
vpush {d8-d15} #入栈保存d8-d15向量寄存器
#q4-q15 12 x 128bit(12x4个float) 并置0
vmov.i64 q4, #0x0
vmov.i64 q5, #0x0
vmov.i64 q6, #0x0
vmov.i64 q7, #0x0
vmov.i64 q8, #0x0
vmov.i64 q9, #0x0
vmov.i64 q10, #0x0
vmov.i64 q11, #0x0
vmov.i64 q12, #0x0
vmov.i64 q13, #0x0
vmov.i64 q14, #0x0
vmov.i64 q15, #0x0
cmp r3, #0x4 #r3-4 cpsr相关状态置位
blt loop4_end #if r3<4 then 跳转
lsr r4, r3, #0x2 #r4=r3/4
loop4:
#一次计算4个inputchannel维度,跟cacheline吻合,每次在计算之前提前加载input和kernel的数据
#并在每次大循环前预加载pld 输入数据
vldm r1!,{d0-d1} // i[3-0][0]
vldm r2,{d2-d7} // k[11-0][0]
subs r4, r4, #1
vmla.f32 q4, q0, d2[0]
vmla.f32 q5, q0, d2[1]
vmla.f32 q6, q0, d3[0]
vmla.f32 q7, q0, d3[1]
vmla.f32 q8, q0, d4[0]
vmla.f32 q9, q0, d4[1]
vldr d2,[r2,#0x30]
vldr d3,[r2,#0x38]
vmla.f32 q10,q0, d5[0]
vmla.f32 q11,q0, d5[1]
vmla.f32 q12,q0, d6[0]
vmla.f32 q13,q0, d6[1]
vmla.f32 q14,q0, d7[0]
vmla.f32 q15,q0, d7[1]
vldm r1!,{d0-d1} // i[3-0][0]
vldr d4,[r2,#0x40]
vldr d5,[r2,#0x48]
vmla.f32 q4, q0, d2[0]
vmla.f32 q5, q0, d2[1]
vmla.f32 q6, q0, d3[0]
vmla.f32 q7, q0, d3[1]
vldr d6,[r2,#0x50]
vldr d7,[r2,#0x58]
vmla.f32 q8, q0, d4[0]
vmla.f32 q9, q0, d4[1]
vmla.f32 q10,q0, d5[0]
vmla.f32 q11,q0, d5[1]
vldr d2,[r2,#0x60]
vldr d3,[r2,#0x68]
vmla.f32 q12,q0, d6[0]
vmla.f32 q13,q0, d6[1]
vmla.f32 q14,q0, d7[0]
vmla.f32 q15,q0, d7[1]
vldm r1!,{d0-d1} // i[3-0][0]
vldr d4,[r2,#0x70]
vldr d5,[r2,#0x78]
vmla.f32 q4, q0, d2[0]
vmla.f32 q5, q0, d2[1]
vmla.f32 q6, q0, d3[0]
vmla.f32 q7, q0, d3[1]
vldr d6,[r2,#0x80]
vldr d7,[r2,#0x88]
vmla.f32 q8, q0, d4[0]
vmla.f32 q9, q0, d4[1]
vmla.f32 q10,q0, d5[0]
vmla.f32 q11,q0, d5[1]
vldr d2,[r2,#0x90]
vldr d3,[r2,#0x98]
vmla.f32 q12,q0, d6[0]
vmla.f32 q13,q0, d6[1]
vmla.f32 q14,q0, d7[0]
vmla.f32 q15,q0, d7[1]
vldm r1!,{d0-d1} // i[3-0][0]
vldr d4,[r2,#0xa0]
vldr d5,[r2,#0xa8]
vmla.f32 q4, q0, d2[0]
vmla.f32 q5, q0, d2[1]
pld [r1,#0x140] //提前prefetch输入数据,为下一循环准备数据
vmla.f32 q6, q0, d3[0]
vmla.f32 q7, q0, d3[1]
vldr d6,[r2,#0xb0]
vldr d7,[r2,#0xb8]
vmla.f32 q8, q0, d4[0]
vmla.f32 q9, q0, d4[1]
pld [r2,#0x380] //提前prefetch权重数据,为下一循环准备数据
vmla.f32 q10,q0, d5[0]
pld [r2,#0x3c0]
vmla.f32 q11,q0, d5[1]
pld [r2,#0x400]
vmla.f32 q12,q0, d6[0]
add r2, r2, #0xc0
vmla.f32 q13,q0, d6[1]
vmla.f32 q14,q0, d7[0]
vmla.f32 q15,q0, d7[1]
bne loop4
loop4_end:
ands r3, r3, #0x3
beq save_result
loop1:
vldm r1!,{d0-d1} // i[3-0][0]
vldm r2!,{d2-d7} // k[11-0][0]
vmla.f32 q4, q0, d2[0]
vmla.f32 q5, q0, d2[1]
vmla.f32 q6, q0, d3[0]
vmla.f32 q7, q0, d3[1]
vmla.f32 q8, q0, d4[0]
vmla.f32 q9, q0, d4[1]
vmla.f32 q10,q0, d5[0]
vmla.f32 q11,q0, d5[1]
vmla.f32 q12,q0, d6[0]
vmla.f32 q13,q0, d6[1]
vmla.f32 q14,q0, d7[0]
vmla.f32 q15,q0, d7[1]
subs r3, r3, #0x1
bne loop1
save_result:、
#最后保存4x12个output值
vstm r0!, {d8-d23}
vstm r0, {d24-d31}
end:
vpop {d8-d15}
pop {r4,pc}
.end