|
|
本帖最后由 yhl 于 2025-7-4 14:54 编辑
问题背景
近日在3559AV100上部署单通道超分模型,由于NNIE框架限制,有DepthToSpace、Clip、Mul(constant)算子无法直接部署,如下图所示,对于项目需求:保证单路视频帧率在30;所以可以得出算法的处理目标时间应该在30ms以内,对于超分模型,尤其是不支持算法的数据处理部分来说还是比较困难的。且在实际优化过程中,遇到了C语言编程以及caffe网络的优化问题值得思考,编写项目文档以防止其他人员踩坑。
部署部分 裁剪算子后,如下图所示: 首先通过onnxruntime进行推理验证,查看DepthToSpace(通过deepseek,如下所示)、Clip、Mul(constant)算子原理,并编写python推理代码: 经过验证后可以得到网络输入,将网络输出保存在本地,通过7YUV软件显示(如下图所示),达到功能预期,明白大致运行规则;
- def depth_to_space_crd(input_arr, block_size):
- if input_arr.ndim != 4:
- raise ValueError("输入必须是4D数组(N,C,H,W)")
-
- N, C, H, W = input_arr.shape
- print(N, C, H, W)
- if C % (block_size ** 2) != 0:
- raise ValueError(f"通道数{C}必须能被block_size平方{block_size**2}整除")
-
- # 计算新维度
- new_C = C // (block_size ** 2)
- new_H = H * block_size
- new_W = W * block_size
-
- # 重塑为[N, new_C, block_size, block_size, H, W]
- reshaped = input_arr.reshape(N, new_C, block_size, block_size, H, W)
-
- # 维度置换为[N, new_C, H, block_size, W, block_size]
- transposed = reshaped.transpose(0, 1, 4, 2, 5, 3) # 1 * 1 * 2 * 2 * 512 * 640 -> 1 * 1 * 512 * 2 * 640 * 2
-
- # 合并空间维度[N, new_C, H*block_size, W*block_size]
- output = transposed.reshape(N, new_C, new_H, new_W)
-
- return output
- ans = depth_to_space_crd(ops[23], 2) * 255
- ans = np.clip(ans, 0, 255)
复制代码
接下来通过脚本进行onnx到caffe的转换,并经过ruyistudio转换为wk文件,板端推理解码(C/C++),发现推理耗时在300ms以上,与目标的30ms差距十倍以上,需要进一步的优化。
优化策略
笔者提供整体优化思路包括两个部分:对裁剪掉的算子在caffe框架中拆解计算步骤并等效替换(仁者见仁智者见智),效果也是比较理想;cpu部分进行多线程并行计算,填充输出超分图像,对于测试用例来说,笔者采用双核双线程进行优化;
在cpu推理方面,遇到了相当大的坑,将其看做是一个数组检索计算的过程,经过NNIE优化后由一开始串行的40ms,修改为双线程计算突增为150ms,经过绑核调高优先级后没有改善,部分代码和时间如下所示:
- static td_void *dts_crd_batch_2(td_void *args)
- {
- cpu_set_t cpuset;
- CPU_ZERO(&cpuset);
- CPU_SET(0, &cpuset);
- pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
- struct sched_param param;
- param.sched_priority = 99;
- pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m);
- svp_npu_thread_args *thread_args = (svp_npu_thread_args *)args;
-
- td_float *src_data = thread_args->acl_info.data;
- td_u8 *dst_data = thread_args->sr_info.data;
- td_u32 offset = thread_args->sr_info.ori_width * 2, height = thread_args->sr_info.ori_height,
- width = thread_args->sr_info.ori_width;
- td_u32 dts_group_nums = thread_args->sr_info.ori_width * thread_args->sr_info.ori_height *
- thread_args->sr_info.blk_size;
- for (td_s32 h = 0; h < height; h++) {
- for (td_s32 w = 0; w < width; w++){
- dst_data[offset++] = (td_u8)clip(src_data[dts_group_nums + h*width + w]);
- dst_data[offset++] = (td_u8)clip(src_data[dts_group_nums + h*width + w + height*width]);
- }
- offset += width * 2;
- }
- return NULL;
- }
复制代码
对其产生的问题感到很困扰,经过仔细排查和对照发现是:1、无符号类型数据在数组计算中更为耗时(td_u32),虽然知道几种数据一定是非负数,但是计算时也应采用td_s32也就是有符号类型;2、在线程计算中频繁访问结构体数据的访问开销太大;一些需要注意的点如下:
·在循环计数、数组索引等场景使用int而非unsigned,避免隐式转换开销;混合有符号与无符号运算时,C/C++标准要求将操作数统一为无符号类型,导致有符号 数被隐式转换。
·避免混合运算:统一表达式中的符号类型,减少编译器插入转换指令。
·缓存对齐:对高频修改的无符号数组进行缓存行对齐(alignas(64)),减轻伪共享。
·循环变量的优化差异:使用有符号整数作为循环计数器时,编译器可更安全地应用向量化优化(如SSE指令),因有符号溢出是未定义行为(UB),允许假设无溢出; 而无符号溢出是定义行为,限制了优化空间。
修改后,推理时间减少到25ms,符合预期。
未完待续...
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|