ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ONNX Runtime 线程模型解析:ThreadPool 抽象、OpenMP 取舍与算子级并行开发指南

ONNX Runtime 线程模型解析:ThreadPool 抽象、OpenMP 取舍与算子级并行开发指南 ONNX Runtime 线程模型解析ThreadPool 抽象、OpenMP 取舍与算子级并行开发指南【免费下载链接】onnxruntimeONNX Runtime: cross-platform, high performance ML inferencing and training accelerator项目地址: https://gitcode.com/GitHub_Trending/on/onnxruntime本文面向 ONNX RuntimeORT的算子开发者与性能调优工程师系统讲解 ORT 内部的线程管理与并行执行机制。文章以 docs/NotesOnThreading.md 为核心骨架结合 include/onnxruntime/core/platform/threadpool.h 与 onnxruntime/core/util/thread_utils.h 的源码实现帮助读者掌握TryParallelFor等线程池抽象的正确用法、ParallelSection的适用场景以及 intra-op算子内与 inter-op算子间并行在实现方式上的根本差异。一、ORT 的两种并行维度OpenMP 与 ORT 线程池ONNX Runtime 在执行推理时存在两个层次的并行算子内并行intra-op parallelism单个算子如 MatMul、Conv、Softmax内部的循环并行化。算子间并行inter-op parallelism计算图中多个相互独立的节点并行执行。根据 docs/NotesOnThreading.md 的说明ORT 允许在执行时使用两种线程实现之一OpenMP或非 OpenMP即 ORT 自有的线程池。两者的分工如下并行维度线程实现说明intra-op 并行OpenMP或ORT 线程池是否启用 OpenMP 由构建时的--use_openmp开关决定inter-op 并行始终使用 ORT 线程池不存在 OpenMP 选项这意味着即便一个 ORT 二进制是使用 OpenMP 构建的跨算子的并行调度也依然走 ORT 自己的线程池两条路径互不干扰。二、线程管理抽象层两大入口文件无论采用哪种底层实现算子开发者都不应该直接接触 OpenMP 或操作系统线程 API而是通过以下两个抽象层ThreadPool类定义于 include/onnxruntime/core/platform/threadpool.h提供算子代码中直接调用的并行循环等用户态 API。thread_utils.h中的工具函数定义于 onnxruntime/core/util/thread_utils.h负责线程池的创建、参数配置与生命周期管理如OrtThreadPoolParams、OrtThreadingOptions、CreateThreadPool。从源码结构看该抽象层集中管理两件事当 OpenMP 启用时所有并行调用最终回落fall back到 OpenMP 实现当 OpenMP 禁用时若线程池指针ThreadPool*为NULL则按顺序sequential执行否则将任务调度到线程池上执行。threadpool.h头文件开头的注释对此给出了更精确的三层映射静态方法会把操作映射到三种底层实现之一——#1未配置线程池时直接执行#2使用经过修改的 Eigen 线程池执行并行场景下的首选方案#3使用 OpenMP 执行。此外头文件通过 PIMPL 手法前向声明Eigen::ThreadPoolInterface避免在头文件中引入 Eigen 头文件从而降低编译依赖。三、算子并行开发的核心 APIThreadPool类对外暴露一组静态方法它们屏蔽了ORT 线程池 / OpenMP / 顺序执行三种实现差异是算子代码并行化的标准入口。下面逐一结合源码说明。3.1 TryParallelFor按成本估算的区间式并行static void TryParallelFor(ThreadPool* tp, std::ptrdiff_t total, double cost_per_unit, const std::functionvoid(std::ptrdiff_t first, std::ptrdiff_t last) fn);total总工作量迭代次数。cost_per_unit单个工作单元的大致开销单位为 CPU 周期非 CPU 密集时也可视为纳秒。这是分片sharding的关键输入。回调签名接收[first, last)的迭代区间避免为每次迭代创建std::function调用也避免依赖内联来消除这些调用开销。threadpool.h中同时提供了接受TensorOpCost的重载版本允许分别给出bytes_loaded、bytes_stored与compute_cycles三种成本分量供底层做更精细的调度决策struct TensorOpCost { double bytes_loaded; double bytes_stored; double compute_cycles; };关于cost_per_unit的估算源码给出了明确警告高估会生成过多分片使 CPU 时间被每分片的上下文创建开销所主导低估则无法充分利用并行度并可能引发负载不均衡与掉队线程straggler问题。3.2 TrySimpleParallelFor单迭代式并行static void TrySimpleParallelFor(ThreadPool* tp, std::ptrdiff_t total, const std::functionvoid(std::ptrdiff_t) fn);该接口假定调用方已经将工作划分到了合适的粒度每个回调只处理一个迭代索引。从 include/onnxruntime/core/platform/threadpool.h 的实现可以看到当tp nullptr时它退化为普通的串行for循环由于fn往往可被内联串行路径的开销极小。3.3 TryBatchParallelFor按批次的并行template typename F static void TryBatchParallelFor(ThreadPool* tp, std::ptrdiff_t total, F fn, std::ptrdiff_t num_batches);num_batches为 0 时会被替换为DegreeOfParallelism()的返回值源码明确提示调用方应根据fn的成本与total的规模合理设限num_batches。例如当fn只是简单累加且total为 100 时num_batches取 1 即可批次切分由静态工具函数PartitionWork(batch_idx, num_batches, total_work)完成其算法基于 MLAS 的MlasPartitionWork保证各批次负载大致均等。3.4 ShouldParallelize是否值得并行static bool ShouldParallelize(const ThreadPool* tp);它向调用方提供是否应该并行化的提示让算子可以在并行版本与串行版本算法之间切换。底层由私有的ShouldParallelizeLoop(num_iterations, block_size)支撑——当迭代数或分块大小不足以摊薄并行开销时返回false避免小循环也开线程的典型性能陷阱。3.5 DegreeOfParallelism并行度查询static int DegreeOfParallelism(const ThreadPool* tp);返回代码在使用线程池时应当假设的并行度。该值与线程池中实际创建的线程数是解耦的源码注释明确指出并行度为 N 的循环由 N-1 个线程配合发起循环的调用线程共同完成。这一语义对 MLAS 等库的自适应并行策略至关重要。四、ParallelSection多循环的并行分区ThreadPool::ParallelSection允许将一系列循环组合进同一个并行区内执行。它的价值在于当算子无法把代码重构为单个大循环时可以通过它摊薄多次循环进入/退出的开销并提升缓存局部性。threadpool.h中的典型用法示例如下{ onnxruntime::concurrency::ThreadPool::ParallelSection ps(tp); for (int x 0; x seq_len; x) { TrySimpleParallelFor(tp, 16, []() { ... }); } }需要特别注意源码中标注的使用约束并行区通过构造/析构进入与退出目前依赖线程局部状态thread-local state来跟踪当前线程是否处于并行区中仅在 Eigen 线程池实现下生效对 OpenMP 无效不允许嵌套也不允许在并行循环内部使用。从实现层面看ParallelSection内部持有ThreadPoolParallelSection* ps_与ThreadPool* tp_并通过显式删除器custom deleter规避对 Eigen 头文件的依赖。另外LoopCounter::ClaimIterations的动态分配机制还能让循环 X 的迭代 i 倾向于运行在之前执行过对应迭代的同一线程上这一亲和性affinity特性对 GRU 这类短循环序列算子的 per-core 缓存命中率有明显帮助。五、算子开发硬性规范不要直接写 OpenMP 指令原文档给出了一条针对算子代码的硬性规范Please do not write#ifdef pragma ompin operator code.即算子实现中严禁出现#ifdef pragma omp这类直接操控 OpenMP 指令的代码。理由非常直接一旦算子代码绕过ThreadPool抽象层直接依赖 OpenMP该算子就失去了OpenMP 与 ORT 线程池二选一的可移植性在非 OpenMP 构建下要么无法编译、要么只能串行破坏了整个运行时统一调度线程资源的设计。所有并行化诉求都应通过上文所述的五个静态 API 与ParallelSection表达。六、线程池的配置参数OrtThreadPoolParams 详解线程池的创建参数集中在 onnxruntime/core/util/thread_utils.h 的OrtThreadPoolParams结构体中它是理解 ORT 线程行为的关键字段默认值含义thread_pool_size00 表示使用默认设置读ORT_INTRA_OP_NUM_THREADS/ORT_INTER_OP_NUM_THREADS环境变量未设置时取全部物理核或一半逻辑核1 表示不创建线程池n 表示创建 n 个线程的线程池auto_set_affinityfalse为 true 且thread_pool_size 0时在ThreadOptions中填充线程亲和性信息allow_spinningtrue客户端包构建为false队列变空后线程是否自旋等待。面向客户端/端侧设备的 ORT 构建默认关闭自旋以降低 CPU 占用、提升能效spin_duration_uskSpinDurationDefault(-1)线程阻塞前自旋的微秒数-1 为默认基于迭代次数的自旋0 表示完全禁用自旋0 表示校准后的定长自旋尽力而为实际时长可能随 CPU 频率变化。该值受allow_spinning约束spin_backoff_max1自旋窗口内的指数退避上限1 保持传统行为每迭代一次SpinPause()2 时按 1、2、4… 递增降低 CPU/功耗密度dynamic_block_base_0非负时任务将按剩余迭代数 / (线程数 * dynamic_block_base_)的递减块大小切分stack_size0线程栈大小affinity_str空UTF-8 亲和性字符串格式如1,2,3绑定逻辑处理器 1、2、3或1-8绑定前 8 个逻辑处理器以;分隔各线程配置set_denormal_as_zerofalse是否将 denormal 置零custom_create_thread_fn/custom_join_thread_fnnullptr自定义线程创建/回收回调用于接管线程生命周期OrtThreadingOptions则聚合了两组参数intra_op_thread_pool_params与inter_op_thread_pool_params分别对应 onnxruntime/core/util/thread_utils.h 中ThreadPoolType::INTRA_OP与ThreadPoolType::INTER_OP两类池。线程池的构造入口ThreadPool(Env*, const ThreadOptions, name, degree_of_parallelism, spin_duration_us, force_hybrid, spin_backoff_max)要求degree_of_parallelism 0。七、线程池在运行时中的落地从框架代码看inter-op 线程池在会话执行阶段被显式传递与持有。例如 onnxruntime/core/framework/session_state.cc 中SessionState的构造函数接收concurrency::ThreadPool* inter_op_thread_pool参数并在 onnxruntime/core/framework/session_state.h 中通过GetInterOpThreadPool()对外暴露。这印证了原文档inter-op 并行始终使用 ORT 线程池的结论跨节点调度依赖的是 ORT 自有池的显式对象引用而不是 OpenMP 的全局并行区。底层实现位于 onnxruntime/core/common/threadpool.cc当线程池以degree_of_parallelism ! 1创建时会实例化一个修改过的 Eigen 线程池extended_eigen_threadpool_来创建 OS 线程并处理工作分发当并行度为 1 时underlying_threadpool_保持为nullptr并行工作直接在调用线程上执行。所有并行循环最终映射到ParallelForFixedBlockSizeScheduling结合成本启发式得出的并行度与分块大小经由LoopCounter实现动态的迭代认领从而对迭代执行时间的不均匀性以及多个循环并发竞争线程池的情形保持稳健。八、开发者行动清单并行化算子循环时优先使用TryParallelFor工作已预切分到合适粒度时用TrySimpleParallelFor需要按批次控制并行时用TryBatchParallelFor。多个短循环连续执行且无法合并为单个循环时用ParallelSection包裹并遵守不嵌套、不在并行循环内使用的约束。用ShouldParallelize与DegreeOfParallelism编写自适应算法避免对小规模工作强行并行。切勿在算子代码中书写#ifdef pragma omp始终经由 include/onnxruntime/core/platform/threadpool.h 提供的抽象。理解构建与运行时的分工--use_openmp只影响 intra-op 并行路径inter-op 并行始终由 ORT 线程池承担线程数量、亲和性、自旋等行为则通过OrtThreadPoolParams或ORT_INTRA_OP_NUM_THREADS/ORT_INTER_OP_NUM_THREADS环境变量控制。综上ORT 的线程模型本质上是统一抽象 多后端适配开发者只面对一套稳定的静态 API运行时根据构建开关OpenMP 与否、线程池指针是否为 NULL以及并行度设置在 OpenMP、Eigen 线程池与顺序执行三种模式之间自动切换。掌握这套抽象是写出高性能、可移植 ORT 算子的前提。【免费下载链接】onnxruntimeONNX Runtime: cross-platform, high performance ML inferencing and training accelerator项目地址: https://gitcode.com/GitHub_Trending/on/onnxruntime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表