本文档介绍了 Constantine's Threadpool 的设计,它受到了 Weave 和 nim-taskpools 的启发,旨在提供高性能、低开销、节能的多线程运行时环境,并着重考虑了高可靠性、可审计性和可维护性。设计关键包括分布式任务队列、减少内存分配、自适应工作窃取、数据并行中的惰性二分分割以及在等待 future 时的回退机制。
线程池的设计很大程度上受到了 Weave,大量的准备 研究 和简化的 Weave,nim-taskpools 的启发。
目标是产生一个极高性能、低开销、节能的多线程运行时。 然而,作为加密库的后端,它需要是高保证的,特别是可审计和可维护的。
不幸的是,基于工作请求的 Weave 设计,相比于工作窃取,需要更多的机制,这意味着更多的状态。此外,它还包括一个自定义的内存池。
另一方面,nim-taskpools 不支持数据并行(并行 for 循环)。
此外,当它们想要完成的 future 没有完成并且没有剩余工作时,两者都不支持将等待线程置于休眠状态。
特性 | OpenMP | Weave | nim-taskpools | Constantine 的线程池 |
---|---|---|---|---|
任务并行(带有 spawn/sync 的 Futures) | 否 | 是 | 是 | 是 |
数据并行(并行 for 循环) | 是 | 是 | 否 | 是 |
嵌套的 parallel-for 区域支持 | 否(导致过度订阅) | 是 | N/A | 是 |
数据流并行(由事件触发的任务 / 精确的任务依赖性) | 是 | 是 | 否 | 是 |
通信机制 | 共享内存 | 消息传递 / 通道 | 共享内存 | 共享内存 |
负载均衡策略 | 静态(GCC),工作窃取(Intel/LLVM) | 工作共享 / 工作请求 | 工作窃取 | 工作窃取 |
被阻塞的任务不会阻塞运行时 | N/A | 否 | 是 | 是 |
任务并行的负载均衡策略(对于细粒度并行非常重要) | 全局队列(GCC),窃取一个(Intel/LLVM) | 自适应的窃取一个/窃取一半 | 窃取一个 | 窃取一个 |
数据并行的负载均衡策略 | 取决于迭代计数和 CPU 计数的热切拆分 | 取决于空闲 CPU 和工作负载的惰性拆分 | N/A | 取决于空闲 CPU 和工作负载的惰性拆分 |
空闲时回退 worker | 是(?) | 是 | 是 | 是 |
等待任务但没有工作时回退 worker | N/A | 否 | 否 | 是 |
调度器开销/争用(在 Fibonacci 40 上测量),对于细粒度并行非常重要 | 极端情况:运行时冻结(GCC),高(Intel/LLVM) | 低到非常低 | 中等 | 低 |
为了启用细粒度并行,即并行化微秒范围内的任务,减少争用至关重要。 全局任务队列将被 N 个线程频繁访问,导致每个线程都频繁刷新彼此的缓存。 相比之下,具有随机受害者选择的分布式任务队列可以显著减少争用。
另一个开销来源是分配器,分配器最糟糕的情况是在一个线程中分配,在另一个线程中释放,特别是当 分配线程始终相同时。不幸的是,这在生产者-消费者工作负载中很常见。 此外,多线程分配/释放将触发代价高昂的原子交换,并可能导致碎片。 最大限度地减少分配将显著有助于细粒度任务。
当一个 worker 耗尽任务时,它会从其他 worker 的任务队列中窃取。 它们可以窃取一个或多个任务。 在严重负载不平衡的情况下,窃取一半的策略可以快速地将 worker 队列重新平衡到全局平均水平。 这也有助于通过减少对数级的窃取尝试来减少调度器开销。 但是,如果 worker 生成的任务很少,则可能会导致更多的重新平衡。
Weave 实现了自适应工作窃取,并在运行时选择窃取一个/窃取一半
如果以下任务队列可以以低开销实现,Constantine 的线程池可能会采用相同的策略
HPC 中使用的大多数(全部?)运行时(尤其是 OpenMP 和 Intel TBB)中的一个关键问题是,它们提前拆分其并行 for 循环。 他们不知道有多少空闲线程,或者将要运行的工作负载的代价有多大。这导致了显著的低效率和性能不可移植性。 例如,这个 repo https://github.com/zy97140/omp-benchmark-for-pytorch 给出了元素数量的阈值,低于该阈值并行化对于常见的浮点运算来说是不划算的,甚至会损害性能:
CPU 型号 插槽 每个插槽的内核数 频率 Intel(R) Xeon(R) CPU E5-2699 v4 2 22 2.20GHz Intel(R) Xeon(R) Platinum 8180 CPU 2 28 2.50GHz Intel(R) Core(TM) i7-5960X CPU 1 8 3.00GHz
Xeon(R) Platinum 8180 CPU Xeon(R) CPU E5-2699 v4 i7-5960X CPU copy 80k 20k 8k add 80k 20k 8k div 50k 10k 2k exp 1k 1k 1k sin 1k 1k 1k sum 1k 1k 1k prod 1k 1k 1k 关于 Xeon Platinum 的详细信息
张量大小 串行 并行 加速 1k 1.04 5.15 0.20X 2k 1.23 5.47 0.22X 3k 1.33 5.34 0.24X 4k 1.47 5.41 0.27X 5k 1.48 5.40 0.27X 8k 1.81 5.55 0.32X 10k 1.98 5.66 0.35X 20k 2.74 6.74 0.40X 50k 5.12 6.59 0.77X 80k 14.79 6.59 2.24X 100k 21.97 6.70 3.27X
相反,我们可以让每个线程开始工作,并使用背压来惰性地评估何时拆分是有利可图的:
这个问题非常棘手:
除了设计问题外,还存在工程问题,因为我们无法在公共 futex 或条件变量上唤醒特定线程。
我们的解决方案是将回退结构嵌入到任务中,并添加一个额外的标志来通知何时可以安全地释放任务。
- 原文链接: github.com/mratsim/const...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!