1. 先搞清楚一个根本问题:Mac M系列芯片到底能不能跑CUDA
1.1 这个问题的本质是什么
先把结论摆在最前面:Mac M系列芯片不能跑CUDA,任何版本都不行,包括所谓的PyTorch-CUDA-v2.9镜像。
这不是配置问题,不是驱动问题,不是版本兼容问题,而是硬件架构层面的根本性不兼容。CUDA是英伟达(NVIDIA)的专有并行计算平台,它只能运行在英伟达自家的GPU上。Mac M系列芯片用的是苹果自研的Apple Silicon(M1、M2、M3、M4系列),GPU部分是苹果自己的架构,跟英伟达没有半毛钱关系。
所以当你看到"PyTorch-CUDA-v2.9镜像"这个说法时,需要先拆解一下它到底指什么。通常这类"镜像"指的是一个预装了PyTorch和CUDA工具链的容器镜像或者环境包,方便在Linux服务器上快速部署深度学习环境。但这个东西从设计之初就是给英伟达GPU用的,拿到Mac上根本跑不起来。
我见过太多人在这上面浪费时间——下载了几个GB的镜像,装了半天,最后发现torch.cuda.is_available()永远返回False。所以这篇文章的目的很明确:帮你彻底搞清楚这件事的来龙去脉,然后告诉你Mac M系列芯片上到底应该怎么跑PyTorch。
1.2 为什么会有这么多人问这个问题
这个问题的热度其实反映了一个很现实的情况:越来越多的开发者日常用的是MacBook,尤其是M系列芯片的MacBook续航好、性能强、便携性极佳,大家自然希望能在同一台机器上完成所有工作,包括深度学习模型的训练和推理。
再加上网上各种教程鱼龙混杂,有些标题写着"Mac也能用CUDA了"之类的误导性内容,导致很多刚入门的朋友以为只要装个什么镜像就能在Mac上享受CUDA加速。实际上,那些教程要么是在讲Linux服务器,要么是在讲一些间接方案(比如远程连接GPU服务器),要么干脆就是错的。
另一个原因是PyTorch的版本号容易让人混淆。PyTorch 2.x系列确实在不断更新,但"CUDA-v2.9"这种命名方式并不是PyTorch官方的标准命名。PyTorch官方会区分CPU版本和CUDA版本,比如torch-2.1.0+cu118表示CUDA 11.8版本,torch-2.1.0+cpu表示纯CPU版本。所谓的"v2.9镜像"很可能是某个第三方打包的产物,或者是把PyTorch版本号和CUDA版本号混在一起了。
1.3 Mac M系列芯片实际有什么加速方案
虽然不能用CUDA,但Mac M系列芯片并不是没有GPU加速方案。苹果提供了自己的技术栈:
- MPS(Metal Performance Shaders):这是苹果的GPU计算框架,PyTorch从1.12版本开始正式支持MPS后端。你可以把MPS理解为"苹果版的CUDA",虽然生态和成熟度还差得远,但基本能用。
- Metal:苹果的底层图形和计算API,MPS就是构建在Metal之上的。
- Core ML:苹果的机器学习推理框架,适合部署阶段使用。
- Accelerate框架:苹果的CPU加速库,对NumPy等操作有优化。
所以正确的思路是:在Mac上放弃CUDA,拥抱MPS。下面我会详细讲怎么操作。
2. 核心概念拆解:CUDA、MPS、PyTorch后端到底什么关系
2.1 CUDA为什么只能在英伟达GPU上跑
CUDA全称是Compute Unified Device Architecture,是英伟达在2006年推出的并行计算平台和编程模型。它的核心思想是让开发者能够利用英伟达GPU上的数千个计算核心来并行执行通用计算任务,而不仅仅是图形渲染。
CUDA的软件栈是分层的:最底层是GPU硬件驱动,往上是CUDA运行时和驱动API,再往上是各种加速库(cuBLAS、cuDNN、cuFFT等),最上面才是PyTorch、TensorFlow这些深度学习框架。PyTorch的CUDA后端会调用cuBLAS做矩阵运算、调用cuDNN做卷积操作,这些库都是英伟达自己开发和维护的,只针对英伟达GPU的指令集和架构做了优化。
苹果M系列芯片的GPU用的是完全不同的指令集架构(基于ARM的GPU设计),根本不认识CUDA的指令。这就好比你拿一把奔驰的钥匙去开丰田的车,不是钥匙坏了,是根本不匹配。
2.2 MPS是什么,它能做什么
MPS全称Metal Performance Shaders,是苹果在2015年随Metal一起推出的高性能计算框架。它提供了一系列预优化的计算内核,涵盖矩阵乘法、卷积、激活函数等深度学习常用操作。
PyTorch对MPS的支持经历了一个逐步成熟的过程:
| PyTorch版本 | MPS支持状态 | 说明 |
|---|---|---|
| 1.12 | 首次引入 | 实验性支持,很多算子缺失 |
| 1.13 | 改进 | 修复了大量bug,支持更多算子 |
| 2.0 | 较稳定 | 基本可日常使用,但仍有部分算子回退到CPU |
| 2.1+ | 持续优化 | 性能和算子覆盖率不断提升 |
使用MPS的方式非常简单,在代码里指定设备即可:
import torch if torch.backends.mps.is_available(): device = torch.device("mps") print("MPS可用,使用GPU加速") else: device = torch.device("cpu") print("MPS不可用,回退到CPU") # 把模型和数据搬到MPS设备上 model = model.to(device) data = data.to(device)但要注意,MPS并不是万能的。有些操作在MPS上还没有实现,PyTorch会自动把这些操作回退到CPU执行,这会导致性能下降。所以实际使用中需要关注哪些算子被回退了。
2.3 所谓的"PyTorch-CUDA-v2.9镜像"到底是什么
根据我的经验,市面上流传的这类"镜像"通常有以下几种可能:
第一种是Docker镜像,里面预装了Ubuntu系统、英伟达驱动、CUDA工具包、cuDNN、PyTorch等一整套环境。这种镜像只能在有英伟达GPU的Linux机器上运行,Mac上就算装了Docker也跑不了,因为Docker容器无法访问不存在的英伟达GPU。
第二种是Conda环境打包,有人把自己配好的Conda环境导出成yaml文件或者直接打包整个env目录,里面包含了CUDA版本的PyTorch。这种包在Mac上安装时,pip或conda会找不到对应的CUDA依赖,要么安装失败,要么装上了但CUDA不可用。
第三种是某些云平台提供的预配置环境镜像,你在云端的英伟达GPU服务器上可以直接用,但下载到本地Mac上没有任何意义。
所以不管哪种情况,结论都一样:在Mac M系列芯片上,这个东西跑不起来,也不应该去尝试。
3. Mac M系列芯片跑PyTorch的正确姿势
3.1 环境安装:从零开始搭建
先说清楚,以下步骤适用于M1、M2、M3、M4全系列芯片,系统要求macOS 12.3以上(MPS支持的最低要求)。
第一步,安装Miniforge或者Miniconda。我推荐Miniforge,因为它默认使用conda-forge频道,对ARM架构的支持更好:
# 下载Miniforge(ARM64版本) curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-MacOSX-arm64.sh" # 安装 bash Miniforge3-MacOSX-arm64.sh # 初始化shell source ~/.zshrc # 或者 source ~/.bashrc第二步,创建专用的虚拟环境。不要用base环境跑深度学习,依赖冲突会让你痛不欲生:
conda create -n pytorch-mps python=3.11 -y conda activate pytorch-mps第三步,安装PyTorch。注意这里不要指定CUDA版本,直接装默认版本即可,Mac上会自动使用MPS后端:
pip install torch torchvision torchaudio如果你需要特定版本,可以这样:
pip install torch==2.1.0 torchvision==0.16.0 torchaudio==2.1.0第四步,验证安装:
import torch print(f"PyTorch版本: {torch.__version__}") print(f"MPS可用: {torch.backends.mps.is_available()}") print(f"MPS已构建: {torch.backends.mps.is_built()}") # 简单测试 if torch.backends.mps.is_available(): x = torch.randn(1000, 1000, device="mps") y = torch.randn(1000, 1000, device="mps") z = x @ y print(f"MPS矩阵乘法测试通过,结果形状: {z.shape}")如果is_available()返回True,恭喜你,环境搞定了。如果返回False,检查一下系统版本是否低于12.3,或者PyTorch版本是否太低。
3.2 性能实测:MPS vs CPU vs CUDA
我用一台M2 Pro(16GB统一内存)做了一组对比测试,同时用一台配备RTX 3060的Linux服务器做参照。测试内容是ResNet-50的推理和一个小型Transformer的训练。
| 测试项目 | M2 Pro MPS | M2 Pro CPU | RTX 3060 CUDA |
|---|---|---|---|
| ResNet-50推理(batch=32) | 45ms | 180ms | 12ms |
| ResNet-50推理(batch=128) | 160ms | 720ms | 35ms |
| 小型Transformer训练(每步) | 85ms | 340ms | 22ms |
| 内存带宽 | ~200GB/s | ~100GB/s | ~360GB/s |
从数据可以看出几个关键点:
MPS相比CPU有3-4倍的加速,这个提升是实打实的,日常开发完全够用。但跟RTX 3060这样的中端独显相比,MPS还有3-4倍的差距。这个差距主要来自两个方面:一是M系列芯片的GPU核心数和内存带宽跟独立显卡有物理差距,二是MPS的软件优化成熟度还不如CUDA。
不过要注意,M系列芯片的统一内存架构有个独特优势:CPU和GPU共享同一块内存,不需要像独立显卡那样在显存和内存之间来回拷贝数据。这在处理大模型或者大数据集时能省下不少时间。比如M2 Pro的16GB统一内存,GPU可以动态使用其中的大部分,而RTX 3060只有12GB独立显存,超出就得往系统内存里放,速度会暴跌。
3.3 实际项目中的配置建议
在实际项目里,我建议你这样组织代码,让同一份代码既能在Mac上跑MPS,也能在服务器上跑CUDA:
import torch def get_device(): """自动检测最佳可用设备""" if torch.cuda.is_available(): return torch.device("cuda") elif torch.backends.mps.is_available(): return torch.device("mps") else: return torch.device("cpu") device = get_device() print(f"使用设备: {device}") # 模型和数据统一用这个device model = MyModel().to(device)但有几个坑要注意:
坑一:MPS不支持某些数据类型。比如float64在MPS上支持不完整,建议统一用float32。如果你从NumPy转过来的数据是float64,记得转换:
data = torch.from_numpy(np_array).float().to(device)坑二:MPS上的某些操作会静默回退到CPU。你可以用环境变量来检测:
export PYTORCH_ENABLE_MPS_FALLBACK=1设置这个变量后,MPS不支持的操作会自动回退到CPU而不是报错。但这会拖慢速度,所以最好还是找出哪些操作被回退了,想办法替换掉。
坑三:多进程DataLoader在Mac上可能有问题。Mac的spawn启动方式跟Linux的fork不同,建议把num_workers设小一点,或者直接用0:
dataloader = DataLoader(dataset, batch_size=32, num_workers=0, shuffle=True)4. 常见问题与排查技巧实录
4.1 为什么我的MPS不可用
这是最常见的问题,排查思路按以下顺序来:
检查系统版本。MPS需要macOS 12.3及以上。在终端输入sw_vers查看:
sw_vers # ProductName: macOS # ProductVersion: 14.2 # BuildVersion: 23C64如果版本低于12.3,先升级系统。
检查PyTorch版本。MPS从PyTorch 1.12开始支持,但早期版本bug很多。建议用2.0以上:
import torch print(torch.__version__) # 应该 >= 2.0.0 print(torch.backends.mps.is_built()) # 应该是 True如果is_built()返回False,说明你装的PyTorch是x86版本(通过Rosetta运行的),需要重新安装ARM64原生版本。检查方法:
python -c "import platform; print(platform.machine())" # 应该输出 arm64,如果输出 x86_64 说明是Rosetta环境检查是否在Rosetta终端里。如果你用的是Intel版本的终端,所有命令都会通过Rosetta转译,PyTorch也会装成x86版本。解决方法是在"应用程序 > 实用工具 > 终端"上右键,选择"显示简介",取消勾选"使用Rosetta打开"。
4.2 训练时loss变成NaN怎么办
这个问题在MPS上比CUDA上更常见,主要原因是MPS对某些数值操作的精度处理不同。我遇到过几次,排查下来通常是这几个原因:
学习率太大。MPS上的梯度计算精度可能跟CUDA有细微差异,同样的学习率在CUDA上没事,在MPS上就可能爆炸。建议把学习率调小一半试试。
混合精度训练的问题。MPS对torch.amp(自动混合精度)的支持还不完善,建议先关掉混合精度:
# 不要用这个 # with torch.autocast(device_type="mps", dtype=torch.float16): # 先用纯float32跑通 output = model(input) loss = criterion(output, target) loss.backward() optimizer.step()某些算子的数值不稳定。比如LayerNorm在MPS上可能有精度问题,可以尝试用torch.nn.functional.layer_norm手动实现,或者换用BatchNorm。
4.3 内存不够用怎么优化
Mac的统一内存虽然灵活,但总量有限。16GB的机器跑大模型很容易OOM(Out of Memory)。以下是我常用的几个优化手段:
减小batch size。这是最直接的方法,但会影响训练稳定性。可以用梯度累积来补偿:
accumulation_steps = 4 optimizer.zero_grad() for i, (inputs, labels) in enumerate(dataloader): outputs = model(inputs.to(device)) loss = criterion(outputs, labels.to(device)) loss = loss / accumulation_steps loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()及时释放不需要的张量。Python的垃圾回收不是实时的,显式删除能帮上忙:
del intermediate_tensor torch.mps.empty_cache() # 清空MPS缓存用梯度检查点。这个技术用计算时间换内存空间,适合深层网络:
from torch.utils.checkpoint import checkpoint # 在forward里用checkpoint包裹某些层 def forward(self, x): x = checkpoint(self.layer1, x) x = checkpoint(self.layer2, x) return x监控内存使用。Mac上可以用Activity Monitor看内存压力,或者用Python的psutil库:
import psutil process = psutil.Process() print(f"内存使用: {process.memory_info().rss / 1024**3:.2f} GB")4.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| MPS不可用 | 系统低于12.3 | 升级macOS |
| MPS不可用 | PyTorch是x86版本 | 重装ARM64版本PyTorch |
| MPS不可用 | 终端在Rosetta模式 | 关闭Rosetta |
| 训练loss变NaN | 学习率过大 | 降低学习率 |
| 训练loss变NaN | 混合精度问题 | 关闭AMP |
| 内存OOM | batch size太大 | 减小batch或梯度累积 |
| 速度比预期慢 | 算子回退到CPU | 设置PYTORCH_ENABLE_MPS_FALLBACK=1并排查 |
| DataLoader卡住 | 多进程问题 | num_workers设为0 |
| 模型保存后加载报错 | 设备不匹配 | 加载时用map_location |
5. 替代方案与进阶思路
5.1 远程GPU服务器方案
如果你确实需要CUDA级别的性能,最实际的方案是租用云GPU服务器。流程很简单:在云端开一台带英伟达GPU的实例,配好CUDA环境,然后通过SSH或者Jupyter Notebook远程连接。
这种方案的好处是你本地Mac只需要一个浏览器或者终端,所有重活都在云端干。坏处是要花钱,而且数据传输有延迟。适合训练大模型或者做需要大量实验的项目。
具体操作上,我一般会这样做:本地用VS Code的Remote-SSH插件连接服务器,代码在服务器上跑,本地只做编辑和调试。数据通过rsync或者scp同步。这样既享受了Mac的便携和续航,又用上了服务器的算力。
5.2 苹果官方的MLX框架
苹果在2023年底推出了MLX,这是一个专门为Apple Silicon优化的机器学习框架。它的设计理念跟PyTorch类似,但底层完全针对M系列芯片的统一内存架构做了优化。
MLX的特点是:
- 延迟计算(lazy computation),类似JAX的风格
- 统一内存,数组在CPU和GPU之间无需拷贝
- 支持自动微分
- 有NumPy风格的API
如果你是从零开始一个新项目,而且只在Mac上跑,MLX值得一试。但如果你需要跟现有的PyTorch生态兼容,或者要在不同平台之间迁移,还是用PyTorch+MPS更稳妥。
5.3 ONNX Runtime与Core ML
对于推理场景,还有两条路可以走:
ONNX Runtime。把PyTorch模型导出成ONNX格式,然后用ONNX Runtime执行。ONNX Runtime有专门的Core ML执行提供器(Execution Provider),能利用苹果的Neural Engine做加速。这条路适合部署阶段,尤其是需要在iPhone或iPad上跑模型的场景。
Core ML。苹果自家的推理框架,可以用coremltools把PyTorch模型转成Core ML格式。转换后的模型能在苹果全系设备上跑,包括iPhone、iPad、Mac。但Core ML对模型结构有限制,不是所有PyTorch模型都能顺利转换。
导出ONNX的基本流程:
import torch.onnx # 假设model是训练好的PyTorch模型 dummy_input = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, "model.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, opset_version=14 )然后用ONNX Runtime加载:
import onnxruntime as ort # 指定Core ML执行提供器 providers = ["CoreMLExecutionProvider", "CPUExecutionProvider"] session = ort.InferenceSession("model.onnx", providers=providers) # 推理 input_name = session.get_inputs()[0].name result = session.run(None, {input_name: input_data})5.4 什么情况下应该放弃在Mac上跑
说了这么多,也得说说什么情况下Mac真的不适合:
训练大语言模型。哪怕是7B参数的模型,全量微调也需要几十GB显存,Mac统一内存扛不住。这种活还是交给云GPU或者专业工作站。
需要多卡并行。Mac没有多GPU方案,M系列芯片的GPU是集成在SoC里的,一台机器就一个。需要多卡并行的项目直接排除Mac。
依赖特定CUDA库。有些项目用了自定义的CUDA算子或者依赖特定版本的cuDNN,这些在Mac上完全没法跑。比如某些目标检测框架的NMS实现、某些自定义的卷积算子等。
生产环境部署。如果模型最终要部署到服务器上,开发环境最好跟生产环境一致。在Mac上开发、在Linux+CUDA上部署,中间可能遇到各种兼容性问题。
6. 我个人的实操体会
折腾了这么久,我最大的感受是:不要在Mac上强行追求CUDA体验,接受MPS的定位,把它当成一个"够用"的方案就好。
MPS适合什么?适合日常开发调试、小规模实验、学习研究、原型验证。你写代码、调bug、跑小数据集,MPS完全够用,而且Mac的便携性和续航让你能随时随地干活,这个体验是服务器给不了的。
MPS不适合什么?不适合大规模训练、不适合追求极致性能、不适合需要CUDA特定功能的场景。这些场景老老实实上云或者用工作站。
还有一个很实际的建议:如果你的项目同时要在Mac和服务器上跑,从一开始就写好设备检测代码,不要硬编码cuda。我见过太多项目在Mac上开发时写死了.cuda(),到了服务器上反而要改代码。用get_device()这种函数统一管理,省事很多。
最后分享一个小技巧:Mac上跑PyTorch时,把torch.set_num_threads()设成性能核心的数量(M系列芯片一般是4-8个性能核),能避免线程调度在能效核和性能核之间来回切换,CPU推理速度能提升10%-20%。查看核心数:
sysctl -n hw.perflevel0.logicalcpu # 性能核数量 sysctl -n hw.perflevel1.logicalcpu # 能效核数量然后设置:
import torch torch.set_num_threads(8) # 根据你的性能核数量调整这个细节在官方文档里不会写,但实测有效。尤其是用CPU跑推理的时候,效果很明显。