4.3 线程与 SessionOptions


4.3 线程与 SessionOptions

本节摘要:intra_op_num_threads 控制单算子内并行(CPU EP/OpenMP);inter_op_num_threads 控制算子间并行。对 CUDA EP 几乎无意义,却常误把 CPU 线程拉满导致 P99 抖动。

线程配置如何毁掉 P99

复制网上「intra=16, inter=16」到 Jetson——CPU 与 GPU 抢资源,延迟波动 **300%** 级(工业现场常见量级)。线程配置必须 **按 EP 与硬件核数** 单独标定:同一组参数在 8 核 x86 上合理,在 4 核 ARM 边缘盒子上就是灾难。线程是资源维度的调优,脱离硬件谈参数等于在随机数表里找最优解。

两个线程数的分工要钉死:intra_op_num_threads 控制单个算子内部的并行度(一个 MatMul 被切几块),inter_op_num_threads 控制算子之间的并行度(几个无依赖节点同时跑)。对 GPU EP,两者几乎都不起作用——算子内并行由 CUDA 内核负责,算子间并行被单 stream 天然串行——但线程池本身仍会创建,配置过高就让 CPU 在空转中争抢资源。

线程参数速查

参数 影响对象 GPU EP
intra_op_num_threads 单 MatMul/Conv 分块 基本忽略
inter_op_num_threads 多节点并行调度 基本忽略
graph_optimization_level Session 创建 通用
enable_profiling 生成 profile 文件 通用
log_severity_level 日志量 调试时降低性能

配置代码

import onnxruntime as ort so = ort.SessionOptions() so.intra_op_num_threads = 8 # CPU EP:物理核数 so.inter_op_num_threads = 1 # 默认起步,Profile 后再加 sess = ort.InferenceSession("model.onnx", sess_options=so, providers=["CPUExecutionProvider"])

边缘低延迟策略示例(SOURCE 7.2 思路):

policy = { "execution_mode": "SEQUENTIAL", "graph_optimization_level": "ORT_ENABLE_EXTENDED", "intra_op_num_threads": 2, "inter_op_num_threads": 1, "enable_mem_pattern": True, }

可写入模型 metadata_props 或由 Sidecar 注入,实现 调优即策略——策略随模型走,换环境不换代码,参数不再散落在部署脚本里。

按部署形态的推荐配置

部署 intra inter mode
云 GPU 1 1 SEQUENTIAL
8 核 CPU 服务器 8 2 PARALLEL 试
树莓派 2 1 SEQUENTIAL
4 核边缘盒子 3 1 SEQUENTIAL

原则:GPU 场景 intra/inter 保持 1 或默认,让 GPU 成为唯一算力来源;CPU 场景 intra 设为物理核数(不是逻辑核数,超线程共享执行单元),inter 从 1 起步,Profile 确认有可并行分支再加。

判断直觉与常见误区

⚠️ 生产开启 verbose 日志 + profiling 忘关——性能掉 10%+ 且磁盘爆。调优结束后务必关闭,把开关交给策略文件而不是常驻代码。

💡 GPU 推理 intra/inter 保持 1 或默认;CPU 推理 intra≈物理核,inter=1 起步。这条直觉覆盖 80% 场景,剩下 20% 用 Profile 修正。

与 GPU 的协作

GPU 场景线程配置的意义不在"加速计算",而在别拖后腿:CPU 线程池占满时,host 端的数据搬运、kernel launch 排队、CUDA 同步都会变慢。正确做法是给 CPU 预留核心:比如 16 核机器跑 GPU 推理服务,ORT 线程留 2~4,其余交给预处理与整体服务进程,避免超线程争用导致的 P99 长尾。

要点串联

  • intra/inter 主要作用于 CPU EP
  • SessionOptions 在创建 Session 时固定
  • GPU 场景勿盲目拉高 CPU 线程
  • profiling 仅短期开启,完事即关
  • 策略可版本化进 CI,随模型走
  • 线程数绑定物理核与部署形态,不跨场景照搬

线程调参的测量方法

线程数不能拍脑袋,测量流程是:固定输入与 warmup,对候选配置各跑 100 次,记录 P50/P99/吞吐,做一张对照表再选。线程参数是离散的,完全值得穷举小范围:

import onnxruntime as ort import numpy as np import time def bench(intra, inter, runs=100): so = ort.SessionOptions() so.intra_op_num_threads = intra so.inter_op_num_threads = inter sess = ort.InferenceSession("model.onnx", sess_options=so, providers=["CPUExecutionProvider"]) feed = {"input": np.random.randn(1, 3, 224, 224).astype(np.float32)} for _ in range(10): # warmup sess.run(None, feed) times = [] for _ in range(runs): t0 = time.perf_counter() sess.run(None, feed) times.append((time.perf_counter() - t0) * 1000) times.sort() return times[runs // 2], times[int(runs * 0.99)] # P50, P99 for intra in (1, 2, 4, 8): for inter in (1, 2): p50, p99 = bench(intra, inter) print(f"intra={intra} inter={inter} P50={p50:.2f}ms P99={p99:.2f}ms")

选择标准:P99 优先于 P50。吞吐与 P99 冲突时,看业务是延迟敏感还是吞吐敏感——这决定你在表中选哪一列。

常见问题速查

症状 根因 处置
GPU 场景 CPU 满载 intra/inter 抄了 CPU 配置 归 1 或默认
CPU 推理核没用满 intra < 物理核 intra 绑核数
P99 长尾 超线程争用 预留核心给其他服务
多模型服务卡顿 各 Session 线程叠加 全局线程预算
profiling 后性能掉 忘了关 配置化开关

容器与虚拟机里的线程预算

容器化部署时,nproc 看到的 CPU 数可能与宿主机不同,ORT 若按"检测到的核数"自动设线程,会超占容器配额。显式指定线程数不仅是调优,更是资源配额治理

import os, onnxruntime as ort # 从配额获取线程预算,而不是靠 ort 自动检测 cpu_limit = os.environ.get("CPU_LIMIT", "2") # k8s 注入 so = ort.SessionOptions() so.intra_op_num_threads = int(cpu_limit) so.inter_op_num_threads = 1 sess = ort.InferenceSession("model.onnx", sess_options=so, providers=["CPUExecutionProvider"])

k8s/容器场景要特别注意两点:一是线程数受 CPU 配额约束,设大了线程池仍会创建(空转与上下文切换);二是多个 replica 在同一节点时,各实例的线程预算要能加总 ≤ 节点核数,否则节点超卖、P99 集体恶化。线程调优的边界从"单进程"扩展到了"整个调度单元"。

线程与中断/亲和性

Linux 上 ORT 线程池与 NUMA 亲和性相互作用:跨 NUMA 的内存访问会让延迟翻倍。生产环境可用 taskset 或 cgroup 把进程钉在某个 NUMA 节点,配合 OMP_NUM_THREADS 让 OpenMP 不跨节点调度。Windows 上则是处理器组(processor group)的边界问题。亲和性调优属于"最后 5%"的优化,前提是前 95% 的 EP 与内存策略已就绪。

第5章:量化与 INT8/FP16 压缩。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U