MLX 统一内存(Unified Memory)深度解析:在 Apple silicon 上实现零拷贝的 CPU/GPU 协作编程

发布时间:2026/9/10 11:43:18
MLX 统一内存(Unified Memory)深度解析:在 Apple silicon 上实现零拷贝的 CPU/GPU 协作编程 MLX 统一内存Unified Memory深度解析在 Apple silicon 上实现零拷贝的 CPU/GPU 协作编程【免费下载链接】mlxMLX: An array framework for Apple silicon项目地址: https://gitcode.com/GitHub_Trending/ml/mlxMLX 是专为 Apple silicon 设计的机器学习数组框架其核心设计之一是统一内存模型CPU 与 GPU 共享同一物理内存池数组天然存在于共享内存中无需在设备间搬运数据。本文将围绕官方文档 unified_memory.rst 展开结合仓库源码深入讲解统一内存的工作原理、基于stream的设备调度机制、依赖自动管理并给出混合 CPU/GPU 计算的实战示例与内存监控 API帮助你写出更高效的 MLX 程序。什么是统一内存架构Apple siliconM 系列芯片采用统一内存Unified Memory架构CPU 和 GPU 直接访问同一个物理内存池而不是像传统独立显卡那样各自持有显存、通过 PCIe 总线拷贝数据。这意味着数据只存在一份CPU 与 GPU 都能以指针方式直接访问省去了 host-to-device / device-to-host 的数据搬运开销内存容量对 CPU 和 GPU 是共享的例如 MacBook 的 16GB/32GB/64GB 统一内存。MLX 从设计之初就针对这一特性做了优化。在项目根目录 README.md 中明确写道Unified memory: A notable difference from MLX and other frameworks is theunified memory model. Arrays in MLX live in shared memory. Operations on MLX arrays can be performed on any of the supported device types without transferring data.同一份 README.md 还列出了Multi-device特性所有操作可以在任意受支持设备目前为 CPU 和 GPU上运行。这两条特性正是统一内存模型的直接体现——数组共享一份内存设备只是执行者而非数据的家。创建数组无需指定位置得益于统一内存在 MLX 中创建数组时完全不需要指定它存放在哪个设备上。官方文档给出了最简单的例子import mlx.core as mx a mx.random.normal((100,)) b mx.random.normal((100,))a和b都直接存在于统一内存中。它们既不是CPU 张量也不是GPU 张量就是一块普通的内存缓冲区任何设备都可以随时读写。从源码看Device类型本身也非常简洁——它只有cpu和gpu两种设备类型加上一个索引字段见 mlx/device.henum class DeviceType { cpu, gpu, }; Device(DeviceType type, int index 0) : type(type), index(index) {}没有任何显存指针设备端句柄之类的概念。数组的内存分配与设备解耦这正是统一内存模型在框架层最直接的体现。用 stream 参数指定执行设备传统框架的惯用模式是把张量搬到某个设备上再计算如.to(cuda)而 MLX 的模式是数组不动指定在哪里执行这个操作。官方文档的示例mx.add(a, b, streammx.cpu) mx.add(a, b, streammx.gpu)上面两行代码CPU 和 GPU 会各自执行一次add运算但操作的是同一对数组a、b无需任何数据迁移。关于stream参数的语义官方文档 using_streams.rst 给出了准确定义所有操作包括随机数生成都接受可选关键字参数streamstream可以是Stream对象也可以是Device例如streammx.gpu传入Device时等价于在该设备的默认流上执行mx.default_stream(device)如果不指定操作运行在默认设备的默认流上即mx.default_stream(mx.default_device())。Python 绑定层也完整暴露了这些 API见 python/src/stream.cppAPI说明mx.default_stream(device)获取某设备的默认流mx.set_default_stream(stream)将某流设为该设备的默认流不改变默认设备mx.new_stream(device)在设备上创建新流只能用于创建它的线程mx.new_thread_unsafe_stream(device)创建可在任意线程使用的流mx.new_thread_local_stream(device)创建每线程唯一的流mx.stream(...)上下文管理器临时设置默认设备与流mx.synchronize(...)与指定流或默认流同步从 C 侧看Stream就是(index, device)的二元组见 mlx/stream.h设备侧则有default_device、set_default_device、device_count、device_info等接口见 mlx/device.h。无依赖操作自动并行官方文档指出上面两个add操作之间没有数据依赖因此它们可以而且很可能并行运行。这个行为背后的机制在 mlx/scheduler.cpp 中清晰可见调度器为每个流维护一个独立的执行线程StreamThread每个线程有自己的任务队列std::queuestd::functionvoid()和条件变量。被派发到不同流上的任务会进入不同线程的队列由各自的线程并发执行——这正是 CPU 流和 GPU 流能同时干活的原因。有依赖操作调度器自动插入依赖如果操作之间存在依赖就涉及跨流同步问题。官方文档的示例c mx.add(a, b, streammx.cpu) # c 在 CPU 上计算 d mx.add(a, c, streammx.gpu) # d 在 GPU 上计算但依赖 c第二个add在 GPU 上执行却依赖第一个add在 CPU 上执行的输出c。MLX 会自动在两条流之间插入依赖关系保证第二个add只在第一个add完成、c可用之后才开始执行。这一机制同样有源码佐证在 mlx/scheduler.cpp 中调度器通过wait_event/signal_event在流之间传递Event对象来实现跨流依赖与错误传播mlx/stream.h 则提供了synchronize系列接口用于显式同步。也就是说跨设备的数据一致性由框架自动保证你不需要手写任何同步原语也不会有数据竞争。实战示例混合 CPU/GPU 流水线提速官方文档给出了一个略显刻意但非常能说明问题的例子假设我们要执行如下计算def fun(a, b, d1, d2): x mx.matmul(a, b, streamd1) for _ in range(500): b mx.exp(b, streamd2) return x, b并传入如下参数a mx.random.uniform(shape(4096, 512)) b mx.random.uniform(shape(512, 4))为什么会这样分工这里体现了基于统一内存做异构调度的核心直觉第一个matmul是4096×512 乘 512×4的矩阵乘计算密度高是 GPU 的强项后面 500 次exp作用在非常小的b512×4上属于又小又碎的操作在 GPU 上大概率受启动开销overhead支配反而更适合 CPU。于是让d1mx.gpu、d2mx.cpu两个设备在同一份数据上各干各的活x, b fun(a, b, d1mx.gpu, d2mx.cpu)官方文档给出的实测数据在 M1 Max 上测得执行方式耗时全部在 GPU 上执行约 2.8 毫秒d1mx.gpud2mx.cpu混合执行约 1.4 毫秒也就是说仅仅把计算密集的 matmul 留在 GPU、把 500 次小规模exp挪到 CPU整体耗时就能减半。这背后有两层收益零拷贝数据从始至终只有一份CPU 和 GPU 都在操作同一块内存没有搬运成本真并行matmul 在 GPU 线程上跑的同时CPU 线程可以同步处理那 500 次exp两个设备的计算资源都被用上了。需要说明的是这组数字来自官方文档在 M1 Max 上的实测实际加速比会因芯片型号、数组形状、核数分配而不同但按操作特性选择合适的设备这一思路是普适的。配套能力统一内存下的监控与限制既然是共享同一块内存那么显存上限这类概念就不存在了取而代之的是对整个统一内存池的管理。MLX 在 docs/src/python/memory_management.rst 中提供了一组内存管理 APIPython 绑定实现在 python/src/memory.cppAPI作用mx.get_active_memory()当前实际使用内存字节不含缓存缓冲区mx.get_peak_memory()程序启动或上次reset_peak_memory以来的内存峰值mx.reset_peak_memory()将峰值计数器清零mx.get_cache_memory()当前缓存中的内存大小已释放但未归还系统的部分mx.set_memory_limit(limit)设置图求值期间的总体内存上限超出且无更多 RAM含 swap可用时会抛异常Metal 可用时默认约为设备建议工作集大小的 1.5 倍mx.set_cache_limit(limit)设置空闲缓存上限设为0可禁用缓存默认等于内存上限mx.set_wired_limit(limit)设置常驻wired内存上限默认0仅 macOS 15.0 有效且必须严格小于总内存mx.clear_cache()清空内存缓存之后get_cache_memory()应返回0其中set_wired_limit还涉及系统侧配置。文档指出设置超过系统 wired 上限的值会报错可通过如下命令提高系统上限单位 MBsudo sysctl iogpu.wired_limit_mbsize_in_megabytes并用mx.device_info()查询系统 wired 上限max_recommended_working_set_size与总内存大小memory_size见 python/src/memory.cpp。由于 CPU/GPU 共享内存这些限制实际上管理的是整台机器的统一内存这对大模型加载、推理部署场景尤其重要。理解 MLX 统一内存模型的关键要点综合官方文档与仓库源码可以提炼出理解 MLX 统一内存模型的四个要点数组无家数组不归属于任何设备只是统一内存里的一份数据见 unified_memory.rst设备是执行者通过stream参数Stream或Device指定操作在哪执行见 using_streams.rst调度器管同步无依赖操作自动并行有依赖操作自动插入跨流依赖见 scheduler.cpp配合惰性求值更佳MLX 默认惰性求值mx.eval才真正触发计算。将设备分配与求值时机结合起来可以更精细地编排 CPU/GPU 流水线相关讨论见 lazy_evaluation.rst。适用前提与限制需要强调两点边界统一内存是 Apple silicon 的硬件特性上述零拷贝、任意设备直访的行为主要适用于 macOS / Metal 环境。仓库中也存在面向 Linux 的后端CUDA / CPU-only例如 mlx/backend/no_gpu/linux_memory.h 通过sysinfo读取系统 RAM 大小这类环境并不具备 Apple silicon 的统一内存特性stream调度语义也会有所不同性能收益取决于工作负载混合 CPU/GPU 调度并非总能带来加速对于本身计算密集、可在 GPU 上高效完成的任务强行拆分到 CPU 反而可能引入调度开销。合理的做法是像官方示例那样识别计算密集型与开销敏感型操作再决定各自落在哪个设备上。小结统一内存是 MLX 区别于其他框架的标志性设计它让数组共享、设备自由成为可能配合基于stream的设备指定和调度器的自动依赖管理开发者可以用极少的代码实现 CPU/GPU 混合流水线获得接近两倍的端到端提速以官方文档在 M1 Max 上的实测为例。理解这一模型是写出高性能 MLX 程序、并在 Apple silicon 上充分发挥硬件潜力的第一步。【免费下载链接】mlxMLX: An array framework for Apple silicon项目地址: https://gitcode.com/GitHub_Trending/ml/mlx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考