Python多线程运行ONNX模型并行推理比串行慢的原因与解决方法
线程并行比串行更慢是完全正常的现象
这是Python生态下运行ONNX模型的典型场景,并非代码逻辑错误,核心原因有三点:
- Python全局解释器锁(GIL)的限制:纯Python实现的逻辑无法跨线程实现真并行,同一时刻仅一个线程能持有GIL执行Python字节码,锁的来回切换本身会带来额外开销。
- ONNX Runtime的默认配置和并行预期不匹配:默认初始化的每个
InferenceSession会自动占用所有可用CPU核心,通过内部多线程加速单模型推理。两个模型同时触发推理时,内部的工作线程会争抢CPU核心、内存带宽,操作系统的线程调度成本会盖过并行的收益,最终导致总耗时比串行更长。 - 原有并行逻辑的设计问题:把纯Python实现的tokenizer预处理放到子线程里并行,这部分逻辑必须抢GIL才能运行,根本没法真正并行,反而加剧了锁竞争的成本。
适配你需求的改造方案
你需要保留“类初始化时加载两个模型、输入触发后并行推理”的逻辑,可按以下步骤调整:
1. 调整ONNX Runtime会话配置,消除资源争抢
初始化推理会话时,通过SessionOptions给每个模型分配固定的、不重叠的CPU核心数,避免两个会话无限制抢占所有核心:
import onnxruntime as ort # 自定义会话运行配置 sess_options = ort.SessionOptions() # 根据机器CPU总核心数分配算力,例如4核CPU就给每个模型分配2核 sess_options.intra_op_num_threads = 2 sess_options.inter_op_num_threads = 1 sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL # 初始化两个模型会话时传入自定义配置 self.hate_sess = ort.InferenceSession( os.path.join(curr_dir, "model", "hatev5.onnx"), sess_options=sess_options ) self.corona_sess = ort.InferenceSession( os.path.join(curr_dir, "model", "corona.onnx"), sess_options=sess_options )
2. 优化并行逻辑顺序,最大化真并行收益
纯Python实现的tokenizer、序列填充这类预处理逻辑运行速度极快,完全没有必要拆分到子线程并行。正确的并行逻辑顺序是:
- 先在主线程串行完成两个模型的全部输入预处理(这部分通常耗时不到1ms,对总延迟的影响可以忽略)
- 再并行触发两个模型的推理:ONNX Runtime的C++推理后端在执行计算时会主动释放GIL,此时两个推理任务可以真正利用独立的CPU核心并行计算,不会产生锁竞争的额外开销。
参考改造后的推理逻辑:
import concurrent.futures # 单独抽离两个模型的推理流程 def _run_hate_inference(self, hate_input): hate_pred = self.hate_sess.run(None, {self.hate_input_name: hate_input}) return hate_pred[0][0][1] def _run_corona_inference(self, corona_input): corona_pred = self.corona_sess.run(None, {self.corona_input_name: corona_input}) return corona_pred[0][0][1] def Eval(self, data): # 保留原有参数校验、query提取的全部逻辑 # 第一步:串行完成两个模型的全流程预处理 hate_line = ' '.join(trim(self.wordpiece_tokenizer.tokenize(query.strip()), self.hate_seq_len)) hate_seq = self.hate_tokenizer.texts_to_sequences([hate_line]) hate_input = sequence.pad_sequences(hate_seq, padding='post', maxlen=self.hate_seq_len) corona_line = ' '.join(trim(self.wordpiece_tokenizer.tokenize(query.strip()), self.corona_seq_len)) corona_seq = self.corona_tokenizer.texts_to_sequences([corona_line]) corona_input = sequence.pad_sequences(corona_seq, padding='post', maxlen=self.corona_seq_len) # 第二步:并行触发两个模型的推理计算 with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: hate_task = executor.submit(self._run_hate_inference, hate_input) corona_task = executor.submit(self._run_corona_inference, corona_input) hate_prob = hate_task.result() corona_prob = corona_task.result() # 保留原有结果计算、JSON序列化的全部逻辑
3. GPU场景的特殊校验
如果你的ONNX模型是运行在GPU上的,开启并行前需要先确认两项配置:
- 默认配置下两个会话共用同一个CUDA流,推理会串行执行在GPU上,此时并行操作只会带来额外的调度开销,完全没有收益
- 需要给每个会话配置独立的CUDA执行选项,同时确认GPU的剩余算力足够同时承载两个模型的推理任务,再开启并行逻辑。
按照上述方案改造后,通常两个模型的并行推理耗时可以接近单个模型的推理耗时,相比串行运行可减少接近50%的端到端延迟。
内容的提问来源于stack exchange,提问作者Abhishek Gangwar
相关产品推荐
相关产品推荐

