news 2026/9/10 6:22:13

AsyncRT CPUDevice 运行时剖析:以库式设计为核心的线程池与内存分配抽象(Mojo/MAX 底层基石)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AsyncRT CPUDevice 运行时剖析:以库式设计为核心的线程池与内存分配抽象(Mojo/MAX 底层基石)

AsyncRT CPUDevice 运行时剖析:以库式设计为核心的线程池与内存分配抽象(Mojo/MAX 底层基石)

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

M::AsyncRT::CPUDevice是 Modular 平台(MAX 与 Mojo)中面向现代多核 CPU 的底层并发运行时抽象,它把"线程池如何实现"与"堆内存如何分配"这两类策略性问题从业务算法中彻底剥离出来,让上层代码(如 MLIR 编译器、张量计算)可以在完全不了解操作系统线程细节的前提下表达并行计算。本文以 AsyncRT/docs/AsyncRTRuntime.md 为主线,结合仓库中的头文件、实现与 Bazel 配置,讲解其设计哲学、WorkQueueAllocator两套抽象接口的完整契约,以及MODULAR_ALLOC_LOGGING分配追踪的启用与输出格式,帮助你理解这套可嵌入、可组合、可替换策略的运行时内核。

一、设计哲学:这是一套"库",而不是"虚拟机"

CPUDevice定位为面向现代 CPU 系统的低层并发库(low-level concurrency library),负责管理系统资源。同类竞品包括 Intel TBB(Thread Building Blocks)、Apple GCD(Grand Central Dispatch)等,但 AsyncRT 有三点核心差异(AsyncRT/docs/AsyncRTRuntime.md):

  1. 严格的库式设计:不存在"针对某个隐式全局线程池的全局 parallel-for-each"这类操作。任何并行操作都显式绑定到某个CPUDevice实例之上。
  2. 与应用内其他线程使用者协作:包括多个CPUDevice实例共存于同一进程,也不假设自己独占整台机器。
  3. 关键策略完全抽象:线程池怎么实现、内存怎么分配,都与使用它的业务代码解耦。

这一设计的目标场景非常明确:AsyncRT 技术要能嵌入既有应用。文档中举了一个形象例子——高性能主机游戏本质上是一个"自洽的操作系统",既要与加速器通信,又有大量其他并发活动在同时进行,AsyncRT 技术必须能在这种环境下工作(AsyncRT/docs/AsyncRTRuntime.md)。

同时,大量数值算法、编译器算法和并发负载与底层执行模型无关。设计者希望这些算法可以表达为"不过度暴露 OS 细节"的形式——事实上其中许多算法完全可以在裸机(bare-metal)系统上运行。

但请注意反方向的边界:AsyncRT 不是虚拟机。如果CPUDevice的使用者想要以系统特定的方式编写代码,完整的机器能力是开放的,客户不会被阻止做必要时的"花哨与奇特"操作(AsyncRT/docs/AsyncRTRuntime.md)。换句话说:默认给你干净的抽象,需要时也绝不拦着你直接触碰底层。

CPUDevice 在代码库中的组织方式

从源码结构看,AsyncRT 分为SupportRuntime两大部分(AsyncRT/docs/README.md):

  • AsyncRT/Support:无依赖的底层并发容器、引用计数(RCRef<>ReferenceCounted<>)、原子操作与算法辅助,仅依赖 C++ 标准库。
  • AsyncRT/Runtime:更"有主见"的低层并发库,包含线程池与内存分配分区机制,即本篇文章的主体。

CPUDevice头文件自述为"组织 AsyncRT 线程池、内存分配器等的顶层 god object",且刻意保持低依赖,便于其他组件在其上组合(AsyncRT/include/AsyncRT/Runtime/CPUDevice.h)。

二、WorkQueue:线程池的抽象接口

M::AsyncRT::WorkQueue是工作队列的抽象接口,通常由一个线程池实现,负责并行执行提交的工作。该接口刻意保持极简,但承载了几个重要的设计点(AsyncRT/docs/AsyncRTRuntime.md):

  1. 它是抽象接口,可以为线程抽象层级不同、约束不同的系统提供多种实现。
  2. 接口最小化:你只能"添加工作",以及"阻塞调用线程直到某些AsyncValue就绪或全部工作完成"。
  3. 设计假设工作项永不阻塞(如不得在队列内等待 I/O)。

关于心智模型,文档给出了一条核心经验法则(AsyncRT/docs/AsyncRTRuntime.md):在现代多核系统上高效利用机器的正确方式是每个 CPU 执行上下文(核、超线程)对应一个 OS 线程,持续不中断地干活。你不应该拉起数千个内核线程并在它们之间频繁上下文切换——这对缓存效率有害,且带来其他开销。WorkQueue的接口本身与这一模型解耦,但设计上正是为了在不同场景下高效实现它。

2.1 抽象接口的价值:执行方式的"正确"答案只有客户知道

如果栈底不是恰当的库式设计,那么建立在它之上的任何东西都不可能是。因此WorkQueue抽象对 AsyncRT"作为各种库式设计的根基技术"这一目标至关重要。

实现线程的方式有太多选择(AsyncRT/docs/AsyncRTRuntime.md):pthreads、Windows 线程、纤程(fibers)、无同步的单线程上下文、多核裸机嵌入式系统、Linux 内核内、只钉在移动设备"小核"上、钉在多插槽 NUMA 服务器的某个插槽上、随机器负载动态变化核数的服务器进程……这里没有"唯一正确答案"——只有客户自己知道执行工作的正确方式

仓库中的实际实现印证了这一点。AsyncRT/lib/Runtime/SingleThreadWorkQueue.cpp 实现的SingleThreadWorkQueue完全不派生额外线程,只用调用者线程执行工作,同时队列本身是线程安全的,addTaskawait可从任意线程调用。此外还有ThreadPoolWorkQueue(基于std::thread与信号量)和用于 NUMA 分区场景的DelegateThreadPoolWorkQueue+ 分区队列(见 AsyncRT/include/AsyncRT/Runtime/WorkQueue.h)。

2.2 最小接口:addTask / addLocalTask / await / shutdown

WorkQueue的接口极简,只有少量纯虚方法(AsyncRT/include/AsyncRT/Runtime/WorkQueue.h):

方法语义
addTask(WorkItem &&work, int taskId = kDefaultTaskId)入队一个工作项供稍后执行(可能在其他线程),线程安全;绝不会立即运行,不保证公平性,调用方需用 AsyncValue 等机制防止任务饿死。taskId >= 0表示入队到对应线程的本地环形缓冲,kDefaultTaskId(-1)表示进入所有 worker 共享的全局任务列表。当前实现中只有 Mojo 的async_parallelize会给出非负taskId(AsyncRT/include/AsyncRT/Runtime/WorkQueue.h)
addLocalTask(WorkItem &&work)尽可能在当前线程执行,同样绝不立即运行。适合耗时极短的工作项(上下文切换成本会超过执行本身),例如 AsyncValue 机制用它让等待者及时执行(AsyncRT/include/AsyncRT/Runtime/WorkQueue.h)
await(ArrayRef<AnyAsyncValueRef> values)阻塞直到给定值就绪(置值或出错)。实现可能休眠、可能"捐赠"调用线程去跑工作项,或两者兼有;允许递归调用await,但应尽量避免,优先只用 AsyncValue 同步。注意:await返回不保证所有等待者及其触发的工作都已运行到静默(quiescence),只有shutdown()才能保证在途计算全部完成(AsyncRT/include/AsyncRT/Runtime/WorkQueue.h)
getParallelismLevel()返回线程池规模,内核可用它作为"拆分成多少个工作项"的提示
getCpuIds()/getNumaNode()队列被分区到的 CPU ID / NUMA 节点(未分区时返回空/kAnyNumaNode
shutdown()关闭线程池并静默,销毁前必须调用,且必须在任何任务之外调用;mainWillDonate模式下必须由创建线程调用

2.3 面向非阻塞工作设计:await 的"客户专用"定位

WorkQueue实现可以假设提交的工作项不会阻塞(例如不阻塞在 I/O 上),这大大简化了实现并提高了机器利用效率——相关议题详见 AsyncRT/docs/WorkQueueNonblocking.md。

但顶层客户通常只想提交工作、不想自己拥抱非阻塞风格。因此存在一个顶层await调用,用于等待一组值计算完成。这个例程仅供客户使用,不得被工作队列内的工作项或其他应当非阻塞的东西调用(AsyncRT/docs/AsyncRTRuntime.md)。

2.4 组合在接口之上的并行算法:Algorithms.h

保持接口最小化,是为了让客户算法(例如"并行 for 循环")与WorkQueue的实现保持正交。这些算法单独实现在 AsyncRT/include/AsyncRT/Runtime/Algorithms.h,包含:

  • addTask(CPUDevice&, work):向设备的工作队列添加非阻塞工作;当工作返回非 void 类型时,重载返回AsyncValueRef<R>,例如AsyncValueRef<int> r = addTask(cpuDevice, [a, b] { return a + b; });
  • await/awaitOrError:等待值就绪并传播错误。
  • andThenSync/andThenAsync(含 Copying/Moving 变体):多个值全部就绪后触发完成回调,Sync版本在最后一个值被填充的线程上直接执行回调,Async版本将回调加入工作队列。
  • parallelForEachN系列:将[0, N)范围内的元素并行调度。其中parallelForEachNChain返回一个AsyncValueRef<Chain>,全部元素完成后该 Chain 被置为就绪,可用.andThenSync链式接续;parallelForEachNChainWithTaskIds允许用taskIdFn(elementIdx)把每个元素钉到特定 worker(例如面向 GPU 的 NUMA 本地核亲和);parallelForEachN则在前 N-1 个元素后台执行的同时,由调用线程执行最后一个元素再等待(AsyncRT/include/AsyncRT/Runtime/Algorithms.h)。

三、Allocator:堆内存分配的策略抽象

M::AsyncRT::Allocator提供堆分配的接口,与WorkQueue同理:抽象接口让算法代码与客户特定策略相互独立、协同工作(AsyncRT/docs/AsyncRTRuntime.md)。

核心接口类似malloc/free,但有两处细化(AsyncRT/docs/AsyncRTRuntime.md):

  1. 分配接口带对齐参数virtual void *allocateBytes(size_t size, size_t alignment) = 0;,从而可以组合式地分配需要 16 或 32 字节对齐的 SIMD 向量等类型。
  2. 释放接口也带大小参数virtual void deallocateBytes(void *ptr, size_t size = 0) = 0;,这让某些分配器算法高效得多(例如 tcmalloc 的 size-class 回收)。

接口还提供了类型化的便利方法:allocate<T>(n)/deallocate<T>(ptr, n)/construct<T>(args...)/destroy<T>(ptr)/destroyAndDeallocate<T>(ptr, n),以及 NUMA 放置查询getNumaPlacement()(AsyncRT/include/AsyncRT/Runtime/Allocator.h)。

仓库内置的工厂函数(AsyncRT/include/AsyncRT/Runtime/Allocator.h):

工厂函数行为
createMallocAllocator()直接调用malloc/free
createTCMallocAllocator()/createTCMallocAllocator(int numaPlacement)使用 tcmalloc,可绑定 NUMA 节点
createLeakCheckAllocator(base)包装器:析构时校验所有内存均已释放(泄漏检查)
createProfilingAllocator(base)包装器:析构时打印内存剖析信息,同时做泄漏检查
createUseAfterFreeAllocator()非 Windows 可用;对每个分配的块做读写保护,用于在无 ASAN 时尽早发现 use-after-free(开销大)
profiledMemcpy(dst, src, size)std::memcpy相同但受剖析开关控制

3.1 抽象带来的关键机会:泄漏追踪 / NUMA / 大页

这一抽象带来若干重要机会(AsyncRT/docs/AsyncRTRuntime.md):

  • 泄漏追踪与剖析:AsyncRT 提供泄漏追踪与剖析分配器接口,在把请求转发给另一个分配器的同时记录分配统计。因此单元测试套件默认启用泄漏追踪分配器,对尽早发现 bug 非常有用。调试构建下CPUDeviceOptions的默认allocatorType就是kLeakChecker,发布构建默认kMalloc(AsyncRT/include/AsyncRT/Runtime/CPUDevice.h)。
  • NUMA 亲和:大型服务器系统常有多插槽 NUMA CPU。此时你希望把计算(用WorkQueue)和数据(用Allocator)都钉在同一个插槽,以避免打满 CPU 之间带宽相对较低的互联总线。createTCMallocAllocator(int numaPlacement)正是为此设计;注意 tcmalloc 最多使用两个分区,NUMA 节点按node % 2映射到交替分区,这是仓库已知的 tcmalloc 局限(AsyncRT/lib/Runtime/TCMallocAllocator.cpp)。
  • 大页(huge page)集成:OS 的大页特性"需要繁琐的逻辑才能用好",但在某些场景下可通过减少 TLB 缺失大幅改善延迟。Allocator抽象为集成此类特性留出了空间。

文档给出的实操建议(AsyncRT/docs/AsyncRTRuntime.md):在构建数据密集型应用时(例如在机器学习应用中分配张量数据),最好用你正在其中执行的 Runtime 的分配器来分配那些数据

3.2 不要用 Allocator 做微小分配

Allocator接口虽然对大规模分配很重要,但它带有少量开销(一次 vtable 间接调用),不适合融入细粒度 C++ 分配器(AsyncRT/docs/AsyncRTRuntime.md)。这意味着:

  • 不要试图把所有std::stringstd::vector的分配都灌进它;
  • 不要为每个微小的链表节点走它。

请聚焦于能影响工作负载内存带宽的大规模分配。头文件同样明确注释:本接口"供大对象分配(如张量数据)使用,而不是程序执行中每次小分配都走它(不要把std::string路由进来!)"(AsyncRT/include/AsyncRT/Runtime/Allocator.h)。

四、CPUDeviceOptions:把策略集中在一个可配置结构里

CPUDevice构造所需的全部策略选项集中在CPUDeviceOptions结构(AsyncRT/include/AsyncRT/Runtime/CPUDevice.h):

字段默认值说明
allocatorTypeDebug 构建kLeakChecker,否则kMallockMalloc/kTCMalloc/kLeakChecker/kProfiler/kUseAfterFree
workQueueTypekThreadPoolkSingleThread/kThreadPool
numThreads/maxThreads0 / 00 表示按物理系统自动感知;maxThreads用于在自动感知时封顶
mainWillDonatetrue为 true 时只创建numThreads-1个 worker,假定调用线程会调用await并"捐赠"自己参与干活(适合 REPL/执行工具这类单一主线程驱动场景);为 false 时创建numThreads个 worker,任意线程可addTask/await但不参与干活(适合多请求线程共享同一队列的多线程服务器)
withAffinity默认关闭受环境变量MODULAR_ENABLE_AFFINITY控制,可被--cpu-affinityCLI 参数覆盖;默认关闭是因为多进程场景下有性能问题(AsyncRT/include/AsyncRT/Runtime/CPUDevice.h)
threadBusyWaitTime200(微秒)受环境变量MODULAR_THREAD_BUSY_WAIT_US覆盖
numaPartitionedfalse为 true 且workQueueType == kThreadPool时,为每个 NUMA 节点创建一个分区队列并用DelegateThreadPoolWorkQueue包装成统一队列
poolName"🔥 Thread"线程池名称
profileFilename非空则启用时间剖析,输出 JSON 与文本剖析文件(需启用剖析的构建,见MODULAR_ASYNCRT_MAX_PROFILING_LEVEL
runtimeProfilingTypeMaskTrace::kFullyEnabled剖析类型过滤器
profilerDebuginfokNoProfilerkPerfProfiler(Linux perf)/kSOProfiler(以共享库方式加载内核,兼容所有剖析器)

CPUDeviceOptions还提供了流畅的链式构建辅助方法(withSingleThreadedwithNumThreadswithLeakCheckedAllocatorwithMainWillNotDonatewithCPUAffinity等),以及forDebug()——一键切换为单线程工作队列 + 泄漏检查分配器的调试配置(AsyncRT/include/AsyncRT/Runtime/CPUDevice.h)。CPUDevice::printRuntimeConfig()会向标准输出打印当前使用的分配器、工作队列类型与线程数,方便核对配置。

另外,CPUDevice是引用计数的(继承M::ReferenceCounted,必须通过dropRef()销毁),CPUDeviceRef = RCRef<CPUDevice>。它有三种拓扑类型(AsyncRT/include/AsyncRT/Runtime/CPUDevice.h):kGlobal(无 NUMA 分区的顶层设备)、kGlobalPartitioned(按 NUMA 节点分区的顶层设备)、kNUMAPartition(被kGlobalPartitioned拥有的每个 NUMA 节点分区)。

五、实操:用 MODULAR_ALLOC_LOGGING 追踪每一次分配与释放

TCMallocAllocator与 Mojo 堆分配器(KGEN_CompilerRT_AlignedAlloc/Free)支持可选的DEBUG级逐分配/释放日志。由于这些函数位于热路径上,日志默认在编译期被剔除,必须在构建时显式启用(AsyncRT/docs/AsyncRTRuntime.md)。

5.1 编译期开启

在 Bazel 构建中通过//AsyncRT:alloc_logging标志开启:

./bazelw run //your:target --//AsyncRT:alloc_logging=true

该标志在 AsyncRT/BUILD.bazel 中定义为bool_flag,通过config_setting映射为编译宏MODULAR_ALLOC_LOGGING=1注入Runtime库的local_defines(AsyncRT/BUILD.bazel)。源码中则以#if MODULAR_ALLOC_LOGGING包裹MLOG_DEBUG调用(AsyncRT/lib/Runtime/TCMallocAllocator.cpp)。

5.2 运行时打开 DEBUG 日志级别

编译后再在运行时把日志级别调到DEBUG

MODULAR_LOG_LEVEL=DEBUG ./your_program

5.3 输出示例与格式解读

文档给出的典型输出(AsyncRT/docs/AsyncRTRuntime.md):

[13:41:13] [ DBG] tcmalloc alloc: ptr=0x7f1234560000 size=128 alignment=8 [13:41:13] [ DBG] tcmalloc free: ptr=0x7f1234560000 size=128 [13:41:13] [ DBG] mojo alloc: ptr=0x112f3fc00000 size=12582912 alignment=1 [13:41:13] [ DBG] mojo free: ptr=0x112f3fc00000

可以看到:tcmalloc行同时打印指针、大小与对齐;NUMA 分区模式下日志还会带上分区号,形如tcmalloc alloc (numa partition {n}): ptr=... size=... alignment=...(AsyncRT/lib/Runtime/TCMallocAllocator.cpp);mojo行则是 Mojo 堆分配器(KGEN_CompilerRT_AlignedAlloc/Free)的记录,典型如 12 MB(12582912 字节)的张量缓冲。

性能保证:编译期开关确保常规构建零开销——没有级别检查、没有分支(AsyncRT/docs/AsyncRTRuntime.md)。日志调用在#if MODULAR_ALLOC_LOGGING之外完全不存在于二进制中。

六、相关文档与代码地图

若要继续深入,以下是仓库中与该主题直接相关的资源:

  • AsyncRT/docs/AsyncValue.md:AsyncValue类型文档——理解await等待的值对象。
  • AsyncRT/docs/WorkQueueNonblocking.md:非阻塞工作队列的详细讨论——理解"工作项永不阻塞"假设的成因与代价。
  • AsyncRT/docs/README.md:AsyncRT 总览(Support / Runtime 分工)。
  • 接口定义:AsyncRT/include/AsyncRT/Runtime/WorkQueue.h、AsyncRT/include/AsyncRT/Runtime/Allocator.h、AsyncRT/include/AsyncRT/Runtime/CPUDevice.h、AsyncRT/include/AsyncRT/Runtime/Algorithms.h。
  • 实现与测试:AsyncRT/lib/Runtime/SingleThreadWorkQueue.cpp、AsyncRT/lib/Runtime/TCMallocAllocator.cpp、AsyncRT/lib/Runtime/ThreadPoolWorkQueue.cpp、AsyncRT/lib/Runtime/DebuggingAllocators.cpp,以及单元测试 AsyncRT/unittests/WorkQueueTest.cpp、AsyncRT/unittests/AllocatorTest.cpp、AsyncRT/unittests/CPUDeviceTest.cpp、AsyncRT/unittests/DebuggingAllocatorsTest.cpp。
  • 构建配置:AsyncRT/BUILD.bazel(alloc_loggingworker_stats两个构建标志及consumers包组)。

结语

M::AsyncRT::CPUDevice的价值不在于"又一个线程池",而在于它把执行策略WorkQueue)与数据放置策略Allocator)做成可替换的抽象契约,并用CPUDeviceOptions把策略集中到一处、用CPUDevice作为组合二者的引用计数容器。无论是调试阶段的泄漏检查分配器、生产环境的 tcmalloc + NUMA 绑定、面向 REPL 的"主线程捐赠"线程池,还是多请求线程共享的服务端队列,都可以在不改变业务算法代码的前提下完成切换。理解这套抽象,也就理解了 Mojo/MAX 运行时如何在"库式设计"与"全机器开放"之间取得平衡。

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 6:21:40

低功耗开发实战:从安卓wakelock到MCU寄存器级优化

1. 这不是“省电小技巧”&#xff0c;而是设备工程师的生存基本功低功耗开发&#xff0c;四个字听着像手机调个深色模式、关个后台APP那么简单。但如果你真这么想&#xff0c;去投安卓或嵌入式岗位时&#xff0c;简历可能连HR那关都过不了——因为招聘JD里写的“具备低功耗优化…

作者头像 李华
网站建设 2026/9/10 6:21:02

hermes-agent:智能体消息路由与自动化任务调度实战解析

你们有没有过这种经历——消息提醒从早响到晚&#xff0c;钉钉群、邮件、GitHub通知、监控告警轮番轰炸&#xff0c;真正要处理的任务却一个都没推进。我去年在这种状态下熬了大半年&#xff0c;最后决定不再忍了&#xff0c;动手写了一个叫hermes-agent的项目。名字取自希腊神…

作者头像 李华
网站建设 2026/9/10 6:20:28

Chrome侧边栏AI扩展:sidePanel+iframe实现多模型并排对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:18:01

CANN/ge GE Python API - GeApi接口文档

GeApi 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端的友…

作者头像 李华
网站建设 2026/9/10 6:17:43

AI代码审查落地C/C++:22万行代码全量扫描实战与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:16:21

ponytail:一种可穿戴的状态切换操作系统

1. 项目概述&#xff1a;从“ponytail”这个词开始&#xff0c;我们到底在聊什么&#xff1f; 最近刷短视频或看时尚博主动态时&#xff0c;你可能已经连续三次看到评论区有人打“ponytail”——不是拼写错误&#xff0c;也不是英文课复习&#xff0c;而是一种正在快速沉淀为视…

作者头像 李华