找回密码
 立即注册
搜索
热搜: 星闪 最新 活动
查看: 350|回复: 0

NNIE 红外超分模型部署优化

[复制链接]

9

主题

0

回帖

73

积分

注册会员

积分
73
发表于 2025-7-3 16:28:34 | 显示全部楼层 |阅读模式
本帖最后由 yhl 于 2025-7-4 14:54 编辑

问题背景
    近日在3559AV100上部署单通道超分模型,由于NNIE框架限制,有DepthToSpace、Clip、Mul(constant)算子无法直接部署,如下图所示,对于项目需求:保证单路视频帧率在30;所以可以得出算法的处理目标时间应该在30ms以内,对于超分模型,尤其是不支持算法的数据处理部分来说还是比较困难的。且在实际优化过程中,遇到了C语言编程以及caffe网络的优化问题值得思考,编写项目文档以防止其他人员踩坑。

部署部分
    裁剪算子后,如下图所示:
    首先通过onnxruntime进行推理验证,查看DepthToSpace(通过deepseek,如下所示)、Clip、Mul(constant)算子原理,并编写python推理代码:

    经过验证后可以得到网络输入,将网络输出保存在本地,通过7YUV软件显示(如下图所示),达到功能预期,明白大致运行规则;


  1. def depth_to_space_crd(input_arr, block_size):

  2.     if input_arr.ndim != 4:
  3.         raise ValueError("输入必须是4D数组(N,C,H,W)")
  4.    
  5.     N, C, H, W = input_arr.shape
  6.     print(N, C, H, W)
  7.     if C % (block_size ** 2) != 0:
  8.         raise ValueError(f"通道数{C}必须能被block_size平方{block_size**2}整除")
  9.    
  10.     # 计算新维度
  11.     new_C = C // (block_size ** 2)
  12.     new_H = H * block_size
  13.     new_W = W * block_size
  14.    
  15.     # 重塑为[N, new_C, block_size, block_size, H, W]
  16.     reshaped = input_arr.reshape(N, new_C, block_size, block_size, H, W)
  17.    
  18.     # 维度置换为[N, new_C, H, block_size, W, block_size]
  19.     transposed = reshaped.transpose(0, 1, 4, 2, 5, 3) # 1 * 1 * 2 * 2 * 512 * 640 -> 1 * 1 * 512 * 2 * 640 * 2
  20.    
  21.     # 合并空间维度[N, new_C, H*block_size, W*block_size]
  22.     output = transposed.reshape(N, new_C, new_H, new_W)
  23.    
  24.     return output

  25. ans = depth_to_space_crd(ops[23], 2) * 255
  26. ans = np.clip(ans, 0, 255)
复制代码

    接下来通过脚本进行onnx到caffe的转换,并经过ruyistudio转换为wk文件,板端推理解码(C/C++),发现推理耗时在300ms以上,与目标的30ms差距十倍以上,需要进一步的优化。

优化策略
    笔者提供整体优化思路包括两个部分:对裁剪掉的算子在caffe框架中拆解计算步骤并等效替换(仁者见仁智者见智),效果也是比较理想;cpu部分进行多线程并行计算,填充输出超分图像,对于测试用例来说,笔者采用双核双线程进行优化;
    在cpu推理方面,遇到了相当大的坑,将其看做是一个数组检索计算的过程,经过NNIE优化后由一开始串行的40ms,修改为双线程计算突增为150ms,经过绑核调高优先级后没有改善,部分代码和时间如下所示:
  1. static td_void *dts_crd_batch_2(td_void *args)
  2. {
  3.     cpu_set_t cpuset;
  4.     CPU_ZERO(&cpuset);
  5.     CPU_SET(0, &cpuset);
  6.     pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);

  7.     struct sched_param param;
  8.     param.sched_priority = 99;
  9.     pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

  10.     svp_npu_thread_args *thread_args = (svp_npu_thread_args *)args;
  11.    
  12.     td_float *src_data = thread_args->acl_info.data;
  13.     td_u8 *dst_data = thread_args->sr_info.data;
  14.     td_u32 offset = thread_args->sr_info.ori_width * 2, height = thread_args->sr_info.ori_height,
  15.         width = thread_args->sr_info.ori_width;
  16.     td_u32 dts_group_nums = thread_args->sr_info.ori_width * thread_args->sr_info.ori_height *
  17.         thread_args->sr_info.blk_size;

  18.     for (td_s32 h = 0; h < height; h++) {
  19.         for (td_s32 w = 0; w < width; w++){
  20.             dst_data[offset++] = (td_u8)clip(src_data[dts_group_nums + h*width + w]);
  21.             dst_data[offset++] = (td_u8)clip(src_data[dts_group_nums + h*width + w + height*width]);
  22.         }
  23.         offset += width * 2;
  24.     }

  25.     return NULL;
  26. }
复制代码

    对其产生的问题感到很困扰,经过仔细排查和对照发现是:1、无符号类型数据在数组计算中更为耗时(td_u32),虽然知道几种数据一定是非负数,但是计算时也应采用td_s32也就是有符号类型;2、在线程计算中频繁访问结构体数据的访问开销太大;一些需要注意的点如下:

·在循环计数、数组索引等场景使用int而非unsigned,避免隐式转换开销;混合有符号与无符号运算时,C/C++标准要求将操作数统一为无符号类型,导致有符号 数被隐式转换。   
·避免混合运算‌:统一表达式中的符号类型,减少编译器插入转换指令。
·缓存对齐‌:对高频修改的无符号数组进行缓存行对齐(alignas(64)),减轻伪共享。
·循环变量的优化差异‌:使用有符号整数作为循环计数器时,编译器可更安全地应用向量化优化(如SSE指令),因有符号溢出是未定义行为(UB),允许假设无溢出;  而无符号溢出是定义行为,限制了优化空间。

修改后,推理时间减少到25ms,符合预期。

未完待续...








本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|【淇诺科技】技术论坛 ( 粤ICP备14010465号-1|粤ICP备14010465号-1 )

GMT+8, 2026-4-2 17:42 , Processed in 0.133118 second(s), 20 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表