news 2026/4/25 7:10:42

ONNX模型多线程推理并解决线程踩踏与显存溢出问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ONNX模型多线程推理并解决线程踩踏与显存溢出问题

模型推理慢问题

在进行模型推理时,摄像头以30帧/秒的速度持续采集图像,而模型推理速度相对较慢,难以实时处理每一帧图像,从而导致丢帧现象。这种情况下,部分已完成的动作可能因对应帧被丢弃而未被识别。

多线程推理

为解决该问题,我们设计了图像存储队列,设计如下:
frame_queue(图像待处理队列),result_queue(图像处理结果队列),record_queue(记录队列)

每个队列的最大存储数量为10

同时要开启多个线程去进行模型推理,这里max_worker设置为4

#定义并开启推理线程self.process_thread=threading.Thread(target=self._process_loop,daemon=True)self.process_thread.start()#定义并开启记录线程self.record_thread=threading.Thread(target=self._record_loop,daemon=True)self.record_thread.start()
def_process_loop(self):"""主循环"""# 初始化线程池withThreadPoolExecutor(max_workers=self.max_workers)asself.executor:frame_id=0whileself.running:try:# --- 步骤 1: 从摄像头/输入源获取帧 ---ifnotself.frame_queue.empty():frame=self.frame_queue.get(timeout=0.1)frame_id+=1# --- 步骤 2: 将处理任务交给线程池 ---try:self.executor.submit(self._worker_task,frame,frame_id)exceptExceptionase:print(f"提交任务失败:{e}")# --- 步骤 3: 检查并获取处理结果 (非阻塞) ---try:whilenotself.result_queue.empty():res_id,annotated_frame=self.result_queue.get_nowait()# 更新显示帧withself.lock:self.current_annotated_frame=annotated_frame# 录像ifself.is_recordingandframeisnotNone:try:self.record_queue.put_nowait(frame)exceptqueue.Full:passexceptqueue.Empty:passtime.sleep(0.001)# 稍微让出CPUexceptExceptionase:print(f"主循环错误:{e}")continue

推理线程可同步执行

def_worker_task(self,frame,frame_id):"""线程池中执行的具体任务"""try:# 1. 执行算法处理annotated_frame=self._process_frame(frame)# 2. 将结果放入回传队列try:self.result_queue.put_nowait((frame_id,annotated_frame))exceptqueue.Full:pass# 如果结果队列满了,丢弃旧结果exceptExceptionase:print(f"处理线程出错:{e}")

记录线程实现如下,将记录功能单开一个线程,可以有效避免写入延迟

def_record_loop(self):"""录像线程:独立运行,只负责写入"""whileself.running:try:frame=self.record_queue.get(timeout=0.1)ifself.is_recordingandself.video_writerisnotNone:self.video_writer.write(frame)exceptqueue.Empty:time.sleep(0.01)exceptException:continue

当开启多线程推理后,显存占用与利用率对比如下:


通过任务管理器我们可以看到,CPU依旧坚挺

线程踩踏问题

通过这个方法,可以有效的解决推理延迟导致的丢帧问题,然而,正当我满心欢喜时,发现我保存的图像出现了乱码现象,如下图所示:

线程踩踏问题解决方法

这是由于我开启了多线程,每个线程中的模型均检测到了对应动作,此时同时进行了图像写入操作导致的,这是多线程环境下的经典“踩踏”问题:多个线程同时检测通过,然后一起挤向cv2.imwrite,结果就是文件被覆盖、路径冲突,甚至程序崩溃。

要解决这个问题,核心思路就一个:让“写文件”这件事变成“单行道”。可以用 queue 把保存图片的任务从推理线程里剥离出来,交给一个专门的“后台搬运工”去处理。这样推理线程只管算,算完把图片扔进篮子就走,完全不会堵车。

这是一种典型生产者-消费者模式
推理线程(生产者):只负责计算,算出结果后,把 (图片数据, 文件名) 扔进一个队列,然后立刻返回继续干活。
保存线程(消费者):专门有一个后台线程盯着这个队列,谁把图片扔进来,它就按顺序一张张保存。
设计如下:

self.save_queue=queue.Queue(maxsize=20)# --- 新增:启动后台保存线程 ---self.save_thread=threading.Thread(target=self._save_worker,daemon=True)self.save_thread.start()def_save_worker(self):"""后台线程:专门负责从队列取图片并保存到硬盘"""whileTrue:try:# 从队列获取任务 (阻塞式,没任务时会等待)img_data,img_path=self.save_queue.get()ifimg_dataisNone:break# 收到 None 信号表示退出# 确保目录存在os.makedirs(os.path.dirname(img_path),exist_ok=True)# 执行保存cv2.imwrite(img_path,img_data)print(f"[SaveWorker] 保存成功:{img_path}")# 告诉队列任务完成self.save_queue.task_done()exceptExceptionase:print(f"[SaveWorker] 保存出错:{e}")

效果如下,尽管还是会多次写入,但此时图像每次只保存一张,因此便避免了图像重复写入问题。

显存溢出问题

然而,解决了线程踩踏问题后,又产生了一个新的问题,我的模型在运行时显存不会释放,这就导致发生了显存溢出问题,报错如下:

2026-04-24 15:53:24.2588959 [E:onnxruntime:,sequential_executor.cc:494 onnxruntime::ExecuteKernel] Non-zero status code returned while running Conv node. Name:‘/model.2/cv2/conv/Conv’ Status Message: D:\a_work\1\s\onnxruntime\core\framework\bfc_arena.cc:368 onnxruntime::BFCArena::AllocateRawInternal Available memory of 45793024 is smaller than requested bytes of 134217728

这是模型在执行 /model.2/cv2/conv/Conv时崩溃了。原因是底层的显存分配器(BFCArena)发现,当前显卡上剩余可用显存只有约 43MB(45793024 字节),但当前操作需要申请 128MB(134217728 字节)的连续空间,空间不足导致申请失败。

为什么会这样?
GPU上做卷积运算时,ONNX Runtime会调用NVIDIAcuDNN库。为了追求极致的速度,cuDNN默认会开启一个叫EXHAUSTIVE(穷举)的搜索模式。
它会疯狂尝试几十种不同的卷积算法,试图找出当前显卡上最快的那一个。
为了进行这场“算法大比武”,它需要申请一块巨大的临时工作空间。报错里需要的128MB,正是cuDNN用来做算法测试的临时场地!
在多线程共享显存池的环境下,显存被反复申请、释放,产生了大量的“显存碎片”。虽然总空闲显存可能有几百兆,但没有一块是连续的128MB,导致cuDNN无法搭建测试场地,直接报错崩溃。

显存溢出解决方法

针对这个问题,博主采用了一个比较简单粗暴的方式,限制每个模型分配的显存数量,并且使用使用默认的、最稳妥的卷积算法。
设置如下:

cuda_options={'device_id':0,'gpu_mem_limit':int(1*1024*1024*1024),# 2GB 上限'arena_extend_strategy':'kSameAsRequested',# 严格按需分配'cudnn_conv_algo_search':'DEFAULT'# 【关键新增】禁用穷举搜索,使用默认算法}providers=[('CUDAExecutionProvider',cuda_options),'CPUExecutionProvider']
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 7:09:19

【Qt】常用控件(十四)QListWidget的属性和使用

小编个人主页详情<—请点击 小编个人gitee代码仓库<—请点击 Qt系列专栏<—请点击 倘若命中无此运&#xff0c;孤身亦可登昆仑&#xff0c;送给屏幕面前的读者朋友们和小编自己! 目录 前言一、QListWidgetQListWidget的属性在QListWidget中新增或者删除元素 总结 前言…

作者头像 李华
网站建设 2026/4/25 7:01:21

简单了解Python中的几种函数

几个特殊的函数&#xff08;待补充&#xff09;python是支持多种范型的语言&#xff0c;可以进行所谓函数式编程&#xff0c;其突出体现在有这么几个函数&#xff1a; filter、map、reduce、lambda、yieldlambda12345>>> g lambda x,y:xy #xy,并返回结果>>>…

作者头像 李华
网站建设 2026/4/25 7:00:29

现代Python编程的四个关键点你知道几个

1、Python 中的类型提示Python 最近引入的类型提示语法允许 linter 和第三方代码质量工具在运行前分析你的代码&#xff0c;并在检测出可能发生的错误。你创建的 Python 代码与他人共享的次数越多&#xff0c;大家就越有可能从使用类型提示中受益。Python 的每个后续版本都推出…

作者头像 李华