你可以在Apple Silicon Mac上通过PCI穿透进行CUDA推理
摘要
本文探讨了通过Thunderbolt在Apple Silicon Mac上使用外部NVIDIA RTX 5090 GPU进行CUDA推理和游戏的可行性,涵盖了tinygrad eGPU驱动以及向Linux虚拟机进行PCI穿透等方法。
我一直在从事一个项目,旨在适配在macOS上运行的QEMU,使其支持将GPU穿透到Linux虚拟机。本文介绍了一些有趣的挑战以及基准测试。文章主要关注游戏,但也涉及了AI基准测试。
查看缓存全文
缓存时间: 2026/05/08 17:38
# RTX 5090 + M4 MacBook Air:能玩游戏吗?
Source: https://scottjg.com/posts/2026-05-05-egpu-mac-gaming/
如果你能把一块完整桌面级 GPU 绑到你的 MacBook Air 上会怎样?事实证明,可以的。
FTC 要求说明:通过我的链接购买,我可能会获得佣金。
## 别管概率多大
虽然很不情愿承认,但现在我大多数项目的第一步都是去问 AI。说不定它能告诉我一些我不知道的事。
ChatGPT 说不
幸运的是,搞点近乎不切实际的事正是我的风格。
## 什么是 Thunderbolt 外接显卡?
好的,计划是把一块大型 PC 游戏 GPU——NVIDIA RTX 5090——插到我的 M4 MacBook Air 上。具体做法是:把它接到一个 Thunderbolt 扩展坞,该扩展坞将 PCIe 转换为 Thunderbolt,然后再接到 USB-C 端口。
Thunderbolt 通过 USB-C 线缆隧道传输 PCIe,因此从计算机的角度看,Thunderbolt 设备实际上就是 PCIe 设备,而非 USB 设备。Thunderbolt 4 提供 4 条 PCIe 通道,最高 40Gbps,但隧道传输会带来少量性能损失。USB4 也将相同的 PCIe 隧道作为可选功能包含在内,因此某些非 Thunderbolt 的 USB4 端口也能实现此功能。你可以利用这一点,将 GPU 插入带有兼容端口的笔记本电脑。
笔记本电脑的 Thunderbolt 接入 [GPU 扩展坞](https://amzn.to/3R2JWRe)。GPU 通过 DisplayPort 连接到显示器。拍完这张照片后不久,我就弄坏了这个扩展坞。
从计算机的角度看,该设备大致就像一个稍慢的 PCIe 设备,因此通常可以使用平常用于这些设备的相同驱动程序。外接显卡在 Linux 和 Windows 上基本开箱即用。甚至可以在树莓派上使用(尽管是用 Oculink 连接,而非 Thunderbolt)。
第一个障碍是,macOS 在 Apple Silicon 上并未提供 NVIDIA 或 AMD GPU 的驱动程序。
## tinygrad 怎么样?
tinygrad 近期发布了他们自己的 [macOS 外接显卡驱动程序](https://x.com/__tinygrad__/status/2039213719155310736)。这是一个全新的 AI 栈,带有针对 NVIDIA 和 AMD 硬件的开源驱动管线。
可惜的是,如果你的主要目标是运行 AI 推理或玩游戏,tinygrad 很可能不是你要找的解决方案。[YouTuber Alex Ziskind 的这段视频](https://www.youtube.com/watch?v=C4KWsmezXm4)显示,通过 tinygrad 使用外接显卡进行推理,比直接在 M4 Pro 上运行原生 Metal 推理(不带外接显卡)慢大约 **10 倍**。tinygrad 的外接显卡驱动程序只能与 tinygrad 栈一起使用,不能用于其他用途,而且对不同 AI 模型的支持也非常有限。

让 NVIDIA PTX 代码在 GPU 上运行是一回事。编写一个能与任意软件配合的通用显示驱动程序则是一个难得多的问题。所以目前,用外接显卡和 Mac 到底能做什么?
## 现有的 Linux 驱动程序
Linux 现在已经可以在 [Apple Silicon Mac](https://asahilinux.org/) 上运行了。遗憾的是,目前 Linux 内核不支持 Apple Silicon 上的 Thunderbolt(仅支持内部设备和 USB3)。但……
你可以在 macOS 宿主机上运行 64 位 ARM 虚拟机。macOS 支持 Thunderbolt 设备。Linux 支持 NVIDIA GPU。让我们把这些拼图组合起来,将 GPU 直通到 Linux 虚拟机中。

从高层次来看,我们只需将 GPU 放入 Linux 虚拟机。虚拟机与 Mac 宿主机架构相同(arm64),因此性能应该相当。当然,细节才是关键。
> ARM64 Windows 上没有 NVIDIA 显卡的驱动程序。这就是我们使用 Linux 的原因。
快速视频演示结果如下:
<iframe width="560" height="315" src="https://www.youtube.com/embed/0EgK0V0G0s4" frameborder="0" allowfullscreen></iframe>
在本文剩余部分,我将详细讲述如何历经曲折让这一切真正工作。如果你只想看截图和基准测试,可以直接跳到 [基准测试部分](https://scottjg.com/posts/2026-05-05-egpu-mac-gaming/#benchmarks)。
## 在 macOS 上实现 PCI 直通
## PCI 设备基础
我们来看看让虚拟机与 PCI 设备通信所需要的两样东西:
1. **PCI BAR(基地址寄存器)** - 每个 PCI 设备通过计算机可读写的内存块进行通信。计算机上每个设备基本都有一个保留的内存区域。这些内存区域必须镜像到虚拟机中,PCI 直通才能工作。
2. **DMA(直接内存访问)** - 设备通过这种方式直接在计算机内存中读取和写入信息。无需 CPU 消耗周期从设备复制数据,设备可以自动复制内存。对于 GPU,它可以用来将纹理直接从计算机内存复制到自己的显存中。
## 映射 PCI BAR
当 QEMU 启动虚拟机时,它会设置客户机的内存布局。对于普通 RAM,这归结为调用 QEMU 中的 `hvf_set_phys_mem()`,该函数使用 `Hypervisor.framework` 方法:
```
hv_vm_map(mem, guest_physical_address, size, HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC);
```
接下来,我们连接到宿主机 PCIDriverKit 驱动程序,并请求将 PCI 设备的内存映射到我们的进程中。(暂时省略驱动程序端的代码,但它们是类似的样板代码。)
```
// map BAR0 into the current process and set `addr` to the location
// where it was mapped
mach_vm_address_t addr = 0;
mach_vm_size_t size = 0;
IOConnectMapMemory64(driverConnection, 0, mach_task_self(), &addr, &size, kIOMapAnywhere);
```
好的,现在我们有了 `addr`,它指向 BAR0 内存,我们可以直接在我们的进程中访问。此时,你可以像对待任何其他内存一样对其进行读写。
```
volatile uint32_t *bar0 = (volatile uint32_t *)addr;
printf("BAR0[0] = %x\n", bar0[0]);
// this would output: BAR0[0] = 0x1b2000a1
// which is a device-specific constant that describes my RTX 5090
//
// BAR0[0] is the BOOT_0 register. The fields break down as:
// arch = 0x1b → GB200 GPU family
// impl = 0x2 → GB202 die (RTX 5090)
// major_rev = 0xa → stepping A
// minor_rev = 0x1 → revision 1 (together: stepping A1)
```
现在我们只需确保 QEMU 为我们的设备内存调用 `hvf_set_phys_mem()`,我们就可以将其映射到客户机中。当客户机代码触及该映射时,它将直接与 GPU 通信,宿主机开销最小。这是性能最佳的情况。至少理论上是这样。
实际上,一旦虚拟机触及 PCI BAR 内存,宿主机内核就会崩溃。

如果你以前从未经历过这种情况,会觉得很不可思议。整个电脑会卡死,而且由于触控板反馈由软件控制,触控板突然无法点击了。邻居家的猫狗开始嚎叫。墙上的画掉了下来。最终电脑会重启,然后你会看到这个对话框。
好吧,我们不能直接映射设备内存,但我们还有其他办法。我们可以捕获对内存的每一次访问,退出客户机回到 QEMU,让 QEMU 将每次读写转发到设备。这样能保持行为正确,但速度极慢。在许多工作负载中,瓶颈在其他地方。大部分对性能敏感的工作是 DMA,但某些路径仍然关心你能多快地将命令推送通过 BAR。
我开始为 Apple 准备错误报告,并编写了一个小型复现示例(嗯,AI 辅助的)来演示问题:
用大约 100 行 C 代码,你就可以启动一个虚拟机,将设备 BAR 映射到客户机,并运行触及它的代码。我仍然不确定这更令人沮丧还是鼓舞,但那个版本运行没有崩溃,而 QEMU 仍然让宿主机内核崩溃。我困惑了一段时间。是客户机页表的问题吗?还是 BAR 以某种微妙的方式与客户机 RAM 冲突?为什么猫狗还在嚎叫?
最后,在绝望中,我询问了一个 AI 编程助手来比较我的示例和 QEMU。它立刻指出我的映射使用了 `HV_MEMORY_READ | HV_MEMORY_WRITE`,而 QEMU 使用了 `HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC`。唉,又被 AI 打败了。就连愚蠢的博客项目也不安全了(主要是开玩笑)。
QEMU 中的解决方法是做一个小的改动:
它能工作,但并不完美。ARM 有几种设备内存类型(Device-nGnRnE/nGnRE/nGRE/GRE 系列),对于写入是否可以合并、重排序或提前确认,有不同的规则。在最宽松的一端,它大致类似于 x86 的写合并。
在真实硬件上,我的 GPU 上的可预取 BAR 应该允许合并,这使得批量写入速度比 BAR0 快几倍。但是 `hv_vm_map()` 没有配置此功能的标志,因此每个设备映射最终都变成了最严格的 nGnRnE。我们对此无能为力,这仍然比捕获每次访问快约 30 倍,但使得 BAR 写入速度比正常情况下慢约 10 倍。
## DMA
这是项目中最棘手的一部分。首先,让我们回顾一下在运行 Linux 的 PC 上使用虚拟机 PCI 直通时是如何工作的,然后将其与我们 macOS 上的挑战进行比较。
当只有计算机与设备通信(没有虚拟机)时,它们可以直接对话。PC 会告诉设备“嘿,我在这个内存地址准备好了 DMA 缓冲区”,设备可以直接访问该内存(即 DMA)。很简单。
当涉及虚拟机时,情况就更复杂了。客户机物理地址与宿主机物理地址不一一对应。虚拟机的 RAM 只是宿主机内存中某块可用的分配空间。因此,如果客户机告诉设备“DMA 到 0x00000000”,设备会愉快地覆盖宿主机上实际存在的任何内容。最简单的解决方法是两件事:
1. 锁定所有客户机内存,以便设备在可能访问它时不会被换出。
2. 在设备和宿主机内存之间放置一个称为 IOMMU 的硬件单元。虚拟机管理程序用客户机到宿主机转换对其进行编程,设备发出的每个 DMA 请求都会实时重新映射。
DMA 请求:读/写 0x00000000
IOMMU
转换表
0x00000000 - 0x80000000
0x20000000 - 0xA0000000
宿主机物理内存
0x20000000 - 0xA0000000
这是一个粗放的解决方案。客户机无需做任何特殊操作,但宿主机必须保持所有客户机 RAM 锁定。还有更高级的方法(比如虚拟 IOMMU),但这超出了本文范围。
### Apple Silicon 上的 DMA
在 Apple Silicon 上,有一个称为 DART 的硬件单元,它大致相当于 IOMMU。它并非虚拟机专用;它还充当安全边界,防止设备访问任意宿主机内存。理想情况下,我们只需像 Linux 在上述简单情况中使用 IOMMU 那样使用 DART。
不幸的是,DART(至少通过 PCIDriverKit 用于 Thunderbolt 设备)有一些硬限制:
- **约 1.5GB 映射限制。** 具有 1.5GB RAM 的虚拟机理论上可以启动,但 CUDA 会耗尽内存,任何现代游戏都需要 8–16GB。
- **约 64k 映射上限。** 如果有许多小 DMA 缓冲区,映射表会填满。
- **无法控制地址或对齐。** PCIDriverKit 会为你分配映射地址。你无法选择它们,也无法指定对齐约束。这排除了虚拟 IOMMU 的可能性,因为虚拟 IOMMU 要求客户机自行选择 DMA 地址。
1.5GB 的上限是最大的初始障碍。我尝试了一些变通方法:预映射我猜测 DMA 可能落入的范围(显然不行),以及使用 `restricted-dma-pool` 设备树属性来强制所有 DMA 通过预分配区域。限制池方法实际上适用于较简单的设备,但 GPU 驱动程序太奇怪,无法适应这种模型。(如果你对细节感兴趣,有一个 [qemu-devel 讨论帖](https://lore.kernel.org/qemu-devel/[email protected]/),我在那里讨论过。)
### apple-dma-pci
我最终在 QEMU 中设计了一个名为 `apple-dma-pci` 的新虚拟 PCI 设备。它与直通的 GPU 一起插入虚拟机,客户机中的配套内核驱动程序会拦截 NVIDIA 驱动程序的 DMA 映射调用。坦率地说,这个解决方案是一个非常令人不安的黑客手段,但它确实有效。
由于映射是按每个 DMA 请求按需创建,并在缓冲区释放时拆除,我们减少了在任何给定时间所需映射内存的数量。在任何时刻,只有实时 DMA 缓冲区的工作集需要适应我们的 1.5GB 限制,而不是整个客户机内存。
客户机驱动程序会提前加载(通过 `/etc/modules-load.d/` 配置),这样它可以在探测时找到 GPU,并在 NVIDIA 驱动程序触及之前换入自定义 DMA 操作:
```
static struct dma_map_ops apple_dma_ops = {
.map_page = apple_dma_map_page,
.unmap_page = apple_dma_unmap_page,
.map_sg = apple_dma_map_sg,
.unmap_sg = apple_dma_unmap_sg,
.alloc = apple_dma_alloc,
.free = apple_dma_free,
};
static int apple_dma_pci_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
struct pci_dev *gpu = pci_get_device(PCI_VENDOR_NVIDIA,
PCI_ANY_ID, NULL);
if (!gpu)
return -ENODEV;
set_dma_ops(&gpu->dev, &apple_dma_ops);
pci_dev_put(gpu);
return 0;
}
```
每个自定义操作都是一个轻量封装器。它将参数编排成一个小的请求,写入 `apple-dma-pci` 虚拟 BAR 的内存,触发门铃寄存器,然后等待回复。在宿主机端,QEMU 接收请求,将其交给 PCIDriverKit 驱动程序,后者执行实际的 DART 映射,生成的 DMA 地址被写回客户机内存。NVIDIA 驱动程序应该不会察觉不同。
Linux 虚拟机(客户机)
apple-dma-pci 虚拟设备
VM 退出
映射地址返回栈上层
### NVIDIA 对齐怪癖
不过,它并非一开始就工作良好。虽然驱动程序最初加载并初始化了显卡,但当我尝试运行 CUDA 工作负载时,立即看到了这个有趣的日志消息:
```
[ 456.194883] NVRM: nvAssertOkFailedNoLog: Assertion failed: The offset passed is not valid [NV_ERR_INVALID_OFFSET] (0x00000037) returned from pRmApi->Alloc(pRmApi, device->session->handle, isSystemMemory ? device->handle : device->subhandle, &physHandle, isSystemMemory ? NV01_MEMORY_SYSTEM : NV01_MEMORY_LOCAL_USER, &memAllocParams, sizeof(memAllocParams)) @ nv_gpu_ops.c:4972
[ 456.371282] NVRM: GPU0 nvAssertFailedNoLog: Assertion failed: 0 == (physAddr & (RM_PAGE_SIZE_HUGE - 1)) @ mem_mgr_gm107.c:1312
[ 456.372020] NVRM: nvAssertOkFailedNoLog: Assertion failed: The offset passed is not valid [NV_ERR_INVALID_OFFSET] (0x00000037) returned from pRmApi->Alloc(pRmApi, device->session->handle, isSystemMemory ? device->handle : device->subhandle, &physHandle, isSystemMemory ? NV01_MEMORY_SYSTEM : NV01_MEMORY_LOCAL_USER, &memAllocParams, sizeof(memAllocParams)) @ nv_gpu_ops.c:4972
```
如果你还记得之前的 DMA 部分,我们提到无法控制 DMA 映射缓冲区的对齐。真糟糕。此时,我深入研究了驱动程序,看看是否有简单的东西可以修补。
以下是
相似文章
NVIDIA机密计算助力苹果扩展私有云计算
NVIDIA的机密计算技术采用Blackwell GPU,已被苹果用于将其私有云计算扩展到Google Cloud,从而在保持强大隐私保护的同时,为Apple Intelligence功能提供安全的服务器端推理。
MAX models 现在可以在 Apple silicon GPU 上运行
MAX models 已更新,可在 Apple silicon GPU 上运行,从而在 Mac 上实现更快的推理。
我应该买哪台电脑:Mac还是自组5090?[D]
用户寻求建议,询问是购买Mac(M5)还是自组的RTX 5090用于机器学习项目,涉及微调、自定义管道以及图像/视频密集型工作流,同时对苹果的MLX框架作为NVIDIA CUDA的替代方案感到好奇。
TRELLIS.2 图像转3D 现已可在Mac(Apple Silicon)上运行 - 无需NVIDIA GPU
一位开发者将微软的TRELLIS.2图像转3D模型移植到Apple Silicon Mac上运行,通过用PyTorch MPS等价物替换仅CUDA依赖项,实现了无需NVIDIA GPU的离线3D网格生成。
BaseRT:通过原生Metal在Apple Silicon上实现最佳LLM推理
BaseRT是一个专为Apple Silicon上的LLM构建的原生Metal推理运行时,在测试模型上,其解码吞吐量比llama.cpp高出1.56倍,比MLX高出1.35倍。