CUDA VMM多GPU显存管理:跨设备协同的设计哲学

发布时间:2026/9/19 9:17:47
CUDA VMM多GPU显存管理:跨设备协同的设计哲学 1. 为什么说 VMM 是“虚拟内存革命”而不是又一个显存分配接口如果你在多卡节点上做大模型训练大概率经历过这种尴尬卡 0 的显存已经红到发紫卡 1 却还空着 20GB你想把其中一块中间结果挪过去却发现所有指针都要重建所有访问路径都要改写。CUDA 的 VMM API 就是冲着这个痛点来的它把 GPU 显存重新拆成“虚拟地址空间 物理内存 访问权限”三个维度让多 GPU 下的跨设备协同第一次有了可编程、细粒度的控制手段。很多同学看到“虚拟内存”四个字第一反应是 Linux 的 swap 分区、Windows 的虚拟内存设置或者“为什么我的 CUDA 安装总提示 gzip: stdin: invalid compressed data”。这些并不是一回事。操作系统的虚拟内存解决的是进程地址空间和物理 RAM 之间的映射而 CUDA VMM API 解决的是 GPU 显存里的地址空间和物理显存页之间的映射。虽然原理相似但 VMM 的设计目标更像一个“显存上的 mmap 体系”而不是“显存版 swap”。1.1 cudaMalloc 时代被隐藏的三个真相cudaMalloc用起来太舒服了以致于很多同学根本意识不到它把显存管理的复杂度藏在了哪里。我梳理了一下至少有三个真相是被它隐藏掉的。第一分配即绑定。cudaMalloc返回一个指针时虚拟地址和物理资源已经被绑成一个整体。你拿到的地址背后是哪一段物理映射、粒度如何、是否适合跨设备共享上层一概不知。一旦你想做更精细的操作比如只映射某一块显存给另一张卡cudaMalloc帮不上忙。第二地址空间默认为设备私有。在传统模型里device 0 的显存地址只在 device 0 的上下文里有效。想从 device 0 给 device 1 分享数据几乎只有cudaMemcpyPeer一条路而且拷贝是“复制”而非法“共享”。你要么接受复制开销要么去写绕开驱动约束的取巧代码。第三物理页提交过于粗放。分配器为了性能往往倾向于一次性提交足够多的物理页。如果一个应用只需要稀疏访问一块很大的显存区域传统 API 很难把“虚拟地址范围”和“物理页实际占用”分开表达。VMM API 把这三层全部解耦了。你可以先预留地址再创建物理内存然后决定什么时刻映射、允许哪张卡访问、什么时候解映射并释放物理内存。这种解耦在单卡上看起来只是“更麻烦”但在多 GPU 场景里恰恰是协同设计的基础。1.2 VMM API 在 CUDA 生态里的定位从 CUDA 10.2 开始driver API 提供了一组cuMem*接口也就是俗称的 CUDA VMM API。它并不是用来完全替代cudaMalloc的高级分配器而是更底层的内存管理原语。就像malloc之上可以有各种内存池cudaMalloc之上也可以继续封装但 VMM 允许你直接控制分配过程中的每一个环节。这套接口的平台支持属性可以通过下列 device attribute 查询CU_DEVICE_ATTRIBUTE_VIRTUAL_ADDRESS_MANAGEMENT_SUPPORTED、CU_DEVICE_ATTRIBUTE_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR_SUPPORTED、CU_DEVICE_ATTRIBUTE_HANDLE_TYPE_WIN32_HANDLE_SUPPORTED等。如果你的环境不支持调用相关函数大概率会直接返回CUDA_ERROR_NOT_SUPPORTED。这里还要区分 VMM 与受管内存。cudaMallocManaged/cuMemAllocManaged提供的是统一虚拟地址空间让你异步迁移数据VMM API 并没有自动迁移能力它把物理位置、映射、访问权限全部交给你。严格来说VMM 更像绘图的画布而受管内存更像一个会自动调整布局的智能相册。1.3 多卡场景真正稀缺的资源很多优化文章说“多卡时代显存总容量很富余”这句话对了一半。真正稀缺的不是单卡显存总量而是“可被多张卡高效访问的数据布局”。VMM 把这个问题拆成了两个清晰的问题为什么样的物理内存可以被哪张卡访问以及什么时候把虚拟地址更新到新的物理内存。举个例子。Transformer 推理里KV Cache 的大小随序列长度动态变化。传统做法是给每块卡预留一个最大长度对应的显存结果序列一短大片显存被浪费。VMM 可以先把虚拟地址段保留出来再按需提交物理页当序列长度增长时只补映射即可。这种思路一旦放到多卡环境里就自然延伸成“一张卡显存不够可以借旁边卡的空闲显存做二级缓存”。这才是标题里“跨设备协同设计哲学”的真实含义。2. 四个原子操作把显存拆成可跨设备传递的对象VMM API 和传统分配接口最大的区别在于它把一次分配拆成了四个可以独立操作的阶段cuMemAddressReserve、cuMemCreate、cuMemMap、cuMemSetAccess。我习惯把这四个操作称为“显存里的原子操作”因为它们分别对应虚拟地址预留、物理内存创建、映射建立、访问权限授予。这四个阶段不是简单的流程而是四种不同的语义资产。下面逐个讲透。2.1 cuMemAddressReserve先占地址后装物理页cuMemAddressReserve负责在进程的地址空间里预留一段虚拟地址不绑定任何物理显存。它和cudaMalloc的最大区别在于预留地址几乎不消耗显存只是占用了地址空间。这对多卡协同非常重要因为你可以提前规划好一套稳定的指针后续物理内存怎么换指针都不变。这个接口有两个关键参数需要关注granularity和addr。granularity必须从cuMemGetAllocationGranularity查询不能自己拍脑袋写 4096。常见的 granularity 有 64KB、2MB 等具体取决于平台和分配类型。addr用于“地址固定”如果你想在某个指定地址上分配可以传入候选地址并配合CU_MEM_ADDRESS_RESERVE_GRANULARITY等 flag 使用。我曾经在项目里跳过 granularity 查询直接按 4KB 对齐预留地址结果cuMemMap阶段反复报错。排查半天才发现VMM 要求映射地址和大小都要按 allocation granularity 对齐而不是普通 host 页的 4KB 对齐。这是非常典型的 VMM 入门坑。2.2 cuMemCreate物理内存从分配到对象cuMemCreate负责创建真正的物理内存返回值不是指针而是一个CUmemGenericAllocationHandle句柄。这一步已经把物理内存从“地址”中抽离出来变成了一个可以传递、导入导出、重新映射的资源对象。创建时需要指定CUmemAllocationProp其中最重要的是 location。如果你想在 device 0 上创建物理内存就要把 location 的类型设为CU_MEM_LOCATION_TYPE_DEVICEid设为 0。如果是 host pinned memory则用CU_MEM_LOCATION_TYPE_HOST。CUmemAllocationProp里还有一个requestedHandleTypes字段用于声明这个句柄未来是否能被导出成文件描述符或 Windows handle。跨设备、跨进程共享时这个字段必须提前设好。有一个容易被忽略的点VMM 创建的物理内存可能是惰性提交的。也就是说cuMemCreate返回句柄时驱动不一定立刻把物理页全部钉住而是在实际访问或映射时才逐步提交。这既是优势也是风险优势是稀疏场景可以节省显存风险是显存占用看起来比cudaMalloc更加“飘忽”你用cudaMemGetInfo观察到的可用显存可能比预想的多也可能在某次访问瞬间掉下来。2.3 cuMemMap / cuMemUnmap映射才是真正“用起来”cuMemMap把一个CUmemGenericAllocationHandle映射到一段虚拟地址区间。至此物理内存和虚拟地址才发生绑定。cuMemUnmap负责解除绑定但解绑后物理内存并不一定被释放还要通过cuMemRelease释放句柄。映射阶段真正体现了“跨设备”的可能性。你可以在 device 0 上创建物理内存却把它映射到 device 1 需要的地址区间也可以在后续某次重新映射时把新的物理内存挂到同一段虚拟地址上。也就是说指针本身不是“属于某块卡”的它只是一个稳定门牌号而门牌号后面的房子可以换。习惯上cuMemMap之后必须配合cuMemSetAccess否则即便映射成功当前设备也不一定有权访问。这个细节我见过很多人踩坑。// 查询推荐粒度 size_t granularity 0; cuMemGetAllocationGranularity(granularity, prop, CU_MEM_ALLOC_GRANULARITY_RECOMMENDED); // 在 device 0 上创建物理内存句柄 CUmemGenericAllocationHandle handle; CUmemAllocationProp prop {}; prop.type CU_MEM_ALLOCATION_TYPE_PINNED; prop.location.type CU_MEM_LOCATION_TYPE_DEVICE; prop.location.id 0; cuMemCreate(handle, size, prop, 0); // 预留一段虚拟地址 CUdeviceptr va; cuMemAddressReserve(va, size, granularity, 0, 0); // 建立映射 cuMemMap(va, size, 0, handle, 0); // 授权给 device 1 访问 CUmemAccessDesc accessDesc {}; accessDesc.location.type CU_MEM_LOCATION_TYPE_DEVICE; accessDesc.location.id 1; accessDesc.flags CU_MEM_ACCESS_FLAGS_PROT_READWRITE; cuMemSetAccess(va, size, accessDesc, 1);代码逻辑并不复杂但每一步的错误码都必须认真检查。VMM API 不像cudaMalloc那样失败后会给你一个“NULL 指针”而是会给出一堆你去查表才知道意思的 CUDA 错误码。2.4 cuMemSetAccess跨设备访问的权限开关cuMemSetAccess是把一段已映射地址的访问权限授予某个或某几个设备。它接受一个CUmemAccessDesc数组每个元素描述一个目标 location 和访问 flag。CU_MEM_ACCESS_FLAGS_PROT_READWRITE是最常用的权限。这一步是跨设备协同里最容易被忽视的。很多同学以为只要用cuMemMap把显存映射到地址空间其他卡就能自动访问了。实际上VMM 的默认行为并不保证跨设备可见。如果 device 1 要访问 device 0 的物理内存你必须在cuMemSetAccess中显式授予 device 1 访问权限。如果两张卡之间没有 P2P 能力驱动也会在这里拒绝执行。我在调一台旧 PCIe 显卡时遇到过CUDA_ERROR_NO_PEER_ACCESS。一开始以为是驱动问题后来检查发现两张卡确实不支持 peer-to-peerVMM 并不会替你兜底。因此做跨设备授权前先查cudaDeviceCanAccessPeer或用两卡通信相关的样本验证底层能力比直接写业务代码要靠谱得多。3. 跨设备协同的三种玩法迁移、映射和缓冲池理解了四个原子操作下面聊实际使用形态。在我接触过的工程里跨设备协同基本可以归纳成三种模式分别对应不同需求迁移、映射、缓冲池。这三种模式没有优劣之分只是适用的性能模型不同。下面展开说。3.1 跨设备迁移地址不变物理资源换位置第一种玩法是“让数据从卡 A 搬到卡 B但上层看到的指针不变”。这非常适合多卡训练中的显存均衡卡 A 显存紧张卡 B 空闲把一部分权重或中间结果挪到卡 B同时保持原指针仍然可用。具体流程大致是先把旧的 physical handle 从虚拟地址段上cuMemUnmap然后通过cuMemCreate在 device B 上创建新的物理内存再将同一段 VA 映射到新 handle最后用cudaMemcpyAsync把数据拷过去。由于虚拟地址始终没变上层所有 kernel 里的 pointer 都不需要改动唯一变化的只是“这个地址后面到底指向哪块物理显存”。这个模式的实际收益要看你能否忍受一次 PCIe 或 NVLink 的拷贝开销。如果数据本身就是低频访问的 embedding 表迁移成本可以被后续的显存释放收益轻松覆盖如果每个 step 都要来回迁移那还是要谨慎评估。3.2 跨设备映射让远端显存像本地内存一样访问第二种玩法是“数据不用搬直接把远端显存映射成本地可访问”。这依赖底层 P2P 能力但不是传统cudaMemcpyPeer那种“显式拷贝”而是通过cuMemSetAccess把物理内存的访问权直接授予另一张卡。举例来说你在 device 0 上创建了一块显存映射到 VA 后通过cuMemSetAccess把这块 VA 对应的访问权授予 device 1。只要两张卡之间支持 P2Pdevice 1 的 kernel 就能像访问本地显存一样访问这块远端显存。这种模式对多卡推理很有用。例如大模型推理时某些层只存在于卡 A卡 B 需要访问它算 attention 时不用先跑到卡 A 拷一份再回本地算。访问远端显存的带宽和延迟虽然不如本地但节省了一次完整拷贝。对于 KV Cache 这类“读得多、写得多但不需要永久驻留两份”的数据跨设备映射的价值非常直观。3.3 基于 VMM 构建跨 GPU 缓冲池第三种玩法是把 VMM 当作资源池的底座。你可以在一开始用cuMemCreate预先创建一批大块 physical handle分别挂在不同的 device 上然后维护一个空闲句柄列表。上层需要显存时只需要 reserve VA map set access而不是走一次完整的cudaMalloc/cudaFree。这样做最大的好处是分配开销稳定。cudaMalloc本身有全局分配器的加锁和状态管理频繁调用的延迟在一些框架里甚至能成为瓶颈。VMM 让你把“创建物理内存”和“建立虚拟地址映射”拆成两件可以预执行的事于是快速路径上只剩cuMemMap和cuMemSetAccess。从资源池角度设计时通常会引入一个通用MemoryBuffer对象内部记录设备 id、physical handle、虚拟地址、映射大小、是否已授予某卡访问权限。上层框架只需要持有这个对象而不需要关心底层是卡上的 local memory 还是 peer memory。跨设备协同在设计层面就完成了。4. 只有实操才体会到的细节稀疏分配、粒度分配和生命周期管理如果只看官方文档VMM 看起来就是四个函数的组合难在读懂每个参数。但真正用起来你会发现文档不会告诉你工程里的几个关键细节。4.1 预留大段虚拟地址比预分配显存便宜得多VMM 能把“虚拟地址”和“物理显存”分开这意味着你可以预留一个很大的 VA 段来容纳可能的最大内存需求然后按需创建物理内存。在稀疏数据场景下这个特性可以显著减少显存占用的虚高。我做过一个 CUDA 样本级验证预留 1TB 的虚拟地址段但只往里面映射几十 MB 的物理显存。只要不访问未映射区域整个过程完全合法。这和操作系统里mmap之后不实际写入内存是一样的道理。对多卡推理服务来说这给了上层框架一种优雅的“显存配额”实现方式先告诉系统“我可能需要这么多地址空间”真正占用的物理显存按实际访问逐步提交。当然cuMemAddressReserve预留过大的地址空间也有代价。最直接的是cudaMemGetInfo无法反映真实占用量做容量规划时会比较困惑。建议在工程里额外维护一个“已创建物理句柄总大小”的统计值。4.2 粒度别猜去查VMM 对地址和映射的粒度要求非常严格。不同驱动、不同 GPU、不同分配类型下granularity 都可能不一样。我强烈建议每次初始化时都调用cuMemGetAllocationGranularity去查一次 recommended granularity而不是把 2MB 写死在代码里。因为映射粒度直接影响地址对齐和大小对齐。如果大小不是粒度的整数倍cuMemMap可能返回CUDA_ERROR_INVALID_VALUE。很多人在 4096 对齐上栽跟头是因为默认用 host page 对齐思维去理解 GPU 显存管理。另外如果上层需要频繁分配小于粒度的小对象直接走 VMM 会浪费显存。实践上我一般先向 VMM 申请一个 2MB 或更大级别的 chunk然后在上层用 buddy allocator 或 slab allocator 做细粒度切分。这样既享受 VMM 的跨设备特性又不至于因为粒度太粗浪费显存。4.3 句柄、映射与释放次序VMM 的生命周期顺序是一个容易埋雷的点。cuMemAddressReserve得到的地址段cuMemCreate得到的 handlecuMemMap建立的映射三者各有独立的生命周期而且相互依赖。正确释放顺序一般是先cuMemUnmap解除映射再cuMemRelease释放 handle最后cuMemAddressFree释放地址段。如果不按顺序来比如先释放地址段再 unmap轻则报 INVALID_VALUE重则导致句柄泄漏。另外需要注意一个 handle 可能被多个地址段映射只有所有映射都解除后才能安全释放物理内存对象。我在一次跨进程共享显存的代码里因为提前释放了 handle导致另一个进程访问时直接触发CUDA_ERROR_INVALID_RESOURCE_HANDLE。排查时看了半天地址映射代码最后才发现是对端进程尚未完成 detach本进程就把底层 handle 扔掉。VMM 的句柄机制在这里很像 shared_ptr你需要明确“谁持有句柄、谁负责释放”。5. 排错记录VMM 项目最常见的几个“为什么”说几个我在实际项目里踩过的坑。这些坑不是因为 VMM 文档少而是因为错误信息太容易让人误判。5.1 CUDA_ERROR_INVALID_RESOURCE_HANDLE句柄生命周期没理清这个错误最常见的场景一个 handle 被映射到 VA 后你在某个分支里提前cuMemRelease了然后另一段代码还在尝试访问。本质上VMM 中的句柄是所有者VA 只是映射方。物理内存必须至少被一个映射或一个句柄持有。排查思路很简单全局搜索cuMemRelease的调用点检查它是否在所有cuMemUnmap之后才执行。如果涉及跨进程导入还要检查cuMemImportFromShareableHandle后是否复制过句柄引用。5.2 CUDA_ERROR_INVALID_VALUE地址和大小没对齐这个错误在 VMM 里非常高频。多数情况下是因为忘记按cuMemGetAllocationGranularity返回的 granularity 对齐地址或大小。少数情况是在cuMemMap时把 offset 写错。排查时我习惯打印三个值VA 起始地址、大小、granularity。然后看地址和大小是否都是 granularity 的整数倍。Python 里写个va % granularity很低成本但能解决大量玄学问题。CUDA 官方常用的computeSanity工具在采购时很有用但真到自己代码里还是要把它封装成断言带进项目。5.3 CUDA_ERROR_NO_PEER_ACCESSP2P 能力不支持或权限未设置这个错误出现得也很频繁。一种是两张卡之间确实不支持 P2P另一种是支持 P2P但你忘了在cuMemSetAccess数组里加上目标设备。第二种情况最常见特别是目标设备不止一张时。排查思路分两步第一步用cudaDeviceCanAccessPeer确认硬件能力。第二步确认CUmemAccessDesc数组包含了所有需要访问这段 VA 的设备而不仅仅是创建物理内存的设备。有人会问“为什么要这么显式”这就是 VMM 的授权模型所有访问都必须显式声明默认没有权限。5.4 环境检查版本、属性和样本代码还有一个容易忽略的点如果你的 CUDA 版本低于 10.2或者驱动太老VMM API 可能并不存在。即便如此编译时也不一定报错因为头文件里的函数声明可能被宏包住了。此时调用会返回CUDA_ERROR_NOT_FOUND或无法解析符号。遇到这种情况我一般会跑一下官方 CUDA sample 里的simpleVMM或simpleIPC。如果 sample 能跑通说明环境没问题如果 sample 都报错问题就在驱动、CUDA 版本或虚拟化环境。很多云主机和高性能计算节点会把 P2P 能力屏蔽导致跨设备 VMM 限制表现得很诡异但这个限制并不是 VMM 本身的错误而是底层硬件拓扑不允许。6. 这个 API 背后的设计哲学以及它教给上层框架的事技术细节看完了最后聊聊 VMM 的设计哲学。这才是它在多 GPU 时代真正值得关注的地方。6.1 显式是跨设备协同的前提VMM API 和传统cudaMalloc最大的哲学差异是“显式性”。传统分配器把你当用户VMM 把你当内存管理器。你在 VMM 里的每一次操作都必须明确回答物理内存在哪、虚拟地址在哪、映射到哪、谁可以访问。这种显式化看似增加负担却是多设备协同得以成立的前提。当你只有一块卡时隐式行为是方便当你有多块卡、多个进程、多种访问路径时隐式行为就成了不确定性来源。VMM 把每个决策都暴露出来反而让上层框架可以在这个清晰的模型上做可靠调度。6.2 所有权从设备手里转移到用户代码手里传统观点里显存地址天然属于某块卡VMM 把这个观点改成了“物理内存是一个资源对象而设备只是它当前的 location”。所有权被显式地交到用户代码手里因此你既可以把 handle 从一个进程导出给另一个进程也可以把 handle 挂到另一张卡的地址空间。这个转变的意义在于上层框架不再需要为每张卡单独设计一套内存管理逻辑。你可以用统一的对象模型去描述“显存资源”把设备差异延迟到物理创建和访问授权阶段。对于多 GPU 训练、推理调度、显存池化这类问题这种抽象是刚需。6.3 它启发的不止是内存池还有调度VMM 让我重新理解了资源调度。以前做显存池本质上是“一块显存用完还给池子”VMM 之后可以做到“一段地址空间不变但物理内存动态切换”。这不只是性能优化更是设计思路的升级。比如端侧推理引擎里KV Cache 的容量随并发请求动态变化。用 VMM 预留一个很大的 VA 缓存区再按需映射物理显存当多卡部署时甚至可以把一部分 cache 放到旁边空闲卡的显存里。这样的系统设计能从“硬性预分配”走向“软性、按需、跨设备弹性分配”。就我个人的实践体会来说真正学会 VMM 的标志不是会用cuMemMap而是设计内存生命周期时习惯性先问三个问题物理资源归谁虚拟地址是否稳定跨设备访问谁有权这三个问题想清楚多 GPU 内存协同的大多数难题都能提前化解。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询