ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CUDA VMM API:多GPU显存管理的虚拟内存革命

CUDA VMM API:多GPU显存管理的虚拟内存革命 最近几年做多GPU大模型训练的朋友应该都体会过显存管理的折磨一张卡装不下模型参数拆到多张卡上又发现激活值、梯度、KV Cache到处都缺显存。CUDA VMM API这套虚拟内存管理接口就是为这个困局准备的底层武器它的全称是Virtual Memory Management从CUDA 10.2开始进入Driver API。核心思想一句话就能说清把“虚拟地址”和“物理显存”彻底解耦你先在地址空间里圈地再分配物理显存然后按需建立映射、授予访问权限。借助统一虚拟地址空间甚至能把多张GPU的物理显存映射到同一段虚拟地址里让不同设备上的kernel像访问本地内存一样访问同一份数据。这篇文章我会先讲清楚VMM API解决什么问题、设计哲学是什么再给出一个能跑的多GPU示例代码最后把我实际踩过的坑和排查思路整理成速查表。适合正在搞多卡训练、推理服务或者单纯想深入理解CUDA底层内存机制的同学。不需要你有多深的CUDA基础但建议先写过简单的kernel知道cudaMalloc返回的指针是怎么回事。1. 多GPU时代的内存困局从cudaMalloc到VMM的必然演进1.1 cudaMalloc在显存管理上的三个硬伤cudaMalloc确实好用一行代码就能在GPU上要一块连续的显存对新手极其友好。但这个“好用”在多卡大模型场景下很快就变成“不够用”。我总结下来有三个硬伤做训练的朋友应该都有共鸣。第一个硬伤是碎片化。训练过程中要频繁分配、释放各种形状的中间张量显存被切得七零八落而cudaMalloc只会去找“足够大的连续段”找不到就返回OOM。经常出现的场景是nvidia-smi显示还剩12GB程序里申请一个8GB张量却失败了。PyTorch的caching allocator就是为了对抗这个问题才搞出来的但它也只能缓解无法根治因为底层接口就没给细粒度控制的能力。第二个硬伤是跨设备协同太粗糙。多卡场景下device 0想读device 1上的数据传统做法要么是cudaMemcpyPeer整块拷贝要么是cudaDeviceEnablePeerAccess开启整卡级别的P2P。前者浪费带宽和时间后者权限粒度太粗——一旦开启两张卡之间几乎所有可见内存都能互通想精细控制“哪些段可以互访、哪些不行”基本做不到。流水线并行里每轮都要搬运中间激活传输开销非常可观。第三个硬伤是虚拟地址和物理分配绑定死了。cudaMalloc返回一个指针背后就是一块确定的物理显存。你想把它挪到另一张卡上想临时改变它的归属想在同一段地址上切换底层物理存储都做不到。在大模型训练中“运行到一半调整显存布局”是很常见的需求传统接口给不了这种弹性。这三个硬伤的根源是同一个GPU显存管理缺少一个“虚拟内存层”。CPU世界在几十年前就用MMU解决了碎片化、共享、按需加载的问题GPU的显存管理却长期停留在“每次分配都必须对应一块物理连续内存”的原始阶段。CUDA VMM API的出现就是把CPU虚拟内存那套设计思想搬到了GPU显存上。1.2 VMM API是什么Driver API里那组被低估的接口CUDA有两套API一套是Runtime API也就是cudaMalloc、cudaMemcpy这种以cuda开头的接口另一套是Driver APIcuMemAllocate、cuLaunchKernel这种以cu开头的接口。Runtime API是Driver API的上层封装用起来省心但很多底层能力也被藏起来了。VMM API就是Driver API里专门做虚拟内存管理的一组接口核心只有六个函数。cuMemAddressReserve / cuMemAddressFree在统一虚拟地址空间中预留或释放一段虚拟地址这一步不碰任何物理显存cuMemCreate / cuMemRelease在指定GPU上创建或释放一块物理显存返回一个不透明句柄cuMemMap / cuMemUnmap把物理显存映射到预留的虚拟地址上或者解除映射cuMemSetAccess设置某段虚拟地址映射允许哪些设备访问读还是读写注意这些函数全部是Driver API。写代码时要用cuInit初始化编译时链接的是libcudaWindows下对应cuda.lib而不是libcudart。如果你一直用Runtime API写代码第一次切换到这套接口时很可能在初始化环节就卡住因为习惯的cudaSetDevice在这里不起作用。1.3 先厘清概念GPU虚拟内存和系统虚拟内存不是一回事很多人一看到“虚拟内存”四个字第一反应是Windows那个“虚拟内存设置”也就是页面文件。这两者完全不是一回事。系统虚拟内存是把内存数据换到磁盘上解决物理内存不够用的问题CUDA VMM API里说的虚拟内存指的是GPU地址空间里的虚拟地址映射机制它管理的是显存不是磁盘。如果你玩过Linux的mmap就会发现CUDA VMM的整个思路跟mmap如出一辙先留地址、再开物理页、然后映射、最后设置权限。只是GPU世界把这套流程搬到了显存上并且加入了“设备”这个维度——同一段虚拟地址可以让多张GPU同时可见这就是多GPU协同的起点。2. 核心设计哲学地址空间与物理显存解耦2.1 四步模型Reserve、Create、Map、SetAccessVMM API把显存的“生命周期”拆成了四个独立阶段这是它和cudaMalloc最本质的区别。cudaMalloc把“分配”当成一个原子操作地址、物理内存、归属设备在一次调用里全部定死VMM API则把每个决策拆开让你可以单独控制。第一步是Reserve在虚拟地址空间里圈出一段连续地址。最关键的是这一步“不花钱”——它不消耗任何显存只是在你进程的地址空间里划了一块地。你可以一次性圈出64GB哪怕物理显存总共只有48GB也完全合法。第二步是Create在某张GPU上创建一块物理显存得到一个句柄CUmemGenericAllocationHandle。这个句柄代表“位于某某设备上、大小为某字节的一块物理显存”和任何虚拟地址还没有关系。同一个句柄可以被映射到多个虚拟地址位置也可以被多个不同的映射共享。第三步是Map把物理显存挂到虚拟地址上。你可以把从不同GPU上创建的物理显存挂到同一段虚拟地址的不同区间这就是跨设备协同的核心操作。第四步是SetAccess告诉驱动“这段虚拟地址允许哪些设备访问”权限分为读和读写。这一步把“物理在哪”和“谁能看”彻底分开——显存在GPU0上但只要权限允许GPU1的kernel也能直接读写。这四个阶段彼此独立意味着可以随时修改任何一环训练跑了一半把某张卡的显存换掉映射位置参数只读给其他卡只读权限显存不够把不常用的映射解掉腾出空间。这种灵活度cudaMalloc给不了。2.2 统一虚拟地址空间跨设备协同的地基前文说的“把多张卡的显存映射到同一段虚拟地址”听起来很美但为什么能做到答案是统一虚拟地址空间Unified Virtual AddressUVA。从CUDA 4.0开始UVA让同一进程里的所有GPU分配共享一个48位的虚拟地址空间。也就是说不管指针来自哪张卡都在同一张“大地图”上地址空间天然不会互相冲突。这跟CPU多进程的内存映射不同——每个进程有自己的地址空间而GPU这边同一进程内所有设备共享一份。UVA带来的直接好处是指针本身不用记录“我是哪张卡的”任何拿到这个指针的kernel都可以用它只要底层映射和访问权限允许。VMM API的Reserve、Map、SetAccess全都建立在这个共享地址空间之上没有UVA跨设备映射就无从谈起。这里有个实际建议多卡工程里尽量每张卡都保留primary context也就是用cuDevicePrimaryCtxRetain获取主上下文而不是用cuCtxCreate创建一堆独立context。primary context是Runtime API和Driver API共享的混用两种API时不容易出乱子独立context多了以后地址空间和资源的归属会变得很难排查。2.3 粒度与对齐最容易踩的隐形陷阱用VMM API有一个概念如果不提前搞懂代码大概率跑不通那就是“粒度”granularity。GPU的映射不是按字节来的而是按块来的。你需要调用cuMemGetAllocationGranularity查询两个数值最小粒度和推荐粒度。最小粒度决定了Reserve时的对齐要求和Create时物理分配的最小单位一般不小于1MB我见过的卡大多数是2MB推荐粒度则是驱动建议大块分配时使用的对齐单位在A100、H100这类数据中心卡上常见的是64MB。为什么要在乎推荐粒度因为GPU的TLB和大页机制更喜欢大块对齐的映射。你用最小粒度去分配1GB显存能跑但可能性能打折用推荐粒度去对齐底层映射更规整访问延迟和带宽都更稳。所以规范做法是先用推荐粒度把分配大小向上取整再去做Reserve、Create、Map。另一个常见误解是把“字节数”直接传给cuMemMap的size参数如果这个size不是粒度的整数倍驱动直接返回CUDA_ERROR_INVALID_VALUE。看起来是莫名其妙报错实际原因只是没有对齐。2.4 这套设计解决的关键矛盾回头看一下VMM API的整个设计哲学可以浓缩成一句话把“地址、容量、位置、权限”四个维度彻底解耦让开发者像操作系统一样管理显存。这样做最直接的收益是碎片化问题大大缓解。你可以先圈一大块虚拟地址然后按需把物理显存“填”进去物理显存不必连续虚拟地址却是连续的这跟CPU端“不连续物理页映射成连续虚拟地址”的思路完全一致。另一个收益是显存池化句柄可以被缓存、复用、重新映射分配和释放不再是“一次性买卖”。还有一个容易被忽略的价值是权限控制的粒度。传统P2P是设备级的一开全开VMM可以精细到映射块给GPU0读写权限给GPU1只读权限这在多租户、多服务共享GPU的部署场景里很有想象力。3. 实操用VMM API搭建多GPU跨设备显存池3.1 环境准备与版本要求动手之前先确认环境。VMM API从CUDA 10.2开始才有建议直接用CUDA 11.x或12.x接口更稳定相关bug修得也更多。驱动版本建议在450.80.02以上太老的驱动部分接口行为不正常。Linux上编译时链接的是libcuda命令大致这样nvcc -o vmm_demo vmm_demo.cu -lcuda如果是纯C文件用gcc也可以头文件包含cuda.h链接同样的库。Windows上对应链接cuda.lib驱动模式建议切到TCC由nvidia-smi -g 0 -dm 1设置WDDM模式下多卡P2P映射限制很多实测容易碰壁。这里插一句安装相关的坑。如果CUDA的.run安装包解压时报“gzip: stdin: invalid compressed data -- format violated”不要怀疑人生基本就是下载文件损坏了。常见原因是下载工具断点续传出问题或者磁盘空间不足。解决办法是到官网校验md5或者换一个网络环境重新下载或者直接用apt/pacman这类包管理器安装。多版本CUDA共存的话/usr/local下各版本独立目录用update-alternatives切换nvcc环境变量里把PATH和LD_LIBRARY_PATH指好就行。3.2 最小可运行示例两张GPU映射到同一段虚拟地址下面这个例子做了一件事在GPU0和GPU1上各分配512MiB物理显存然后映射到同一段1GiB虚拟地址上前半段是GPU0的后半段是GPU1的并且让两张卡都拥有整段地址的读写权限。#include cuda.h #include cstdio #include cstdlib #define CHECK_CUDA(call) \ do { \ CUresult err (call); \ if (err ! CUDA_SUCCESS) { \ const char* msg nullptr; \ cuGetErrorString(err, msg); \ fprintf(stderr, CUDA error %s at %s:%d\n, msg, __FILE__, __LINE__); \ exit(EXIT_FAILURE); \ } \ } while (0) int main() { CHECK_CUDA(cuInit(0)); CUdevice devA 0, devB 1; CUcontext ctxA nullptr, ctxB nullptr; CHECK_CUDA(cuDeviceGet(devA, 0)); CHECK_CUDA(cuDeviceGet(devB, 1)); CHECK_CUDA(cuDevicePrimaryCtxRetain(ctxA, devA)); CHECK_CUDA(cuDevicePrimaryCtxRetain(ctxB, devB)); const size_t halfBytes (1ULL 30) / 2; // 每张卡 512 MiB // 1. 查询粒度 size_t minGran 0, recGran 0; CHECK_CUDA(cuMemGetAllocationGranularity(minGran, nullptr, CU_MEM_ALLOC_GRANULARITY_MINIMUM)); CHECK_CUDA(cuMemGetAllocationGranularity(recGran, nullptr, CU_MEM_ALLOC_GRANULARITY_RECOMMENDED)); size_t allocSize (halfBytes recGran - 1) / recGran * recGran; // 2. 在 ctxA 中预留整段虚拟地址1 GiB CHECK_CUDA(cuCtxSetCurrent(ctxA)); CUdeviceptr dptr 0; CHECK_CUDA(cuMemAddressReserve(dptr, allocSize * 2, recGran, 0, 0)); // 3. 分别在 GPU0、GPU1 上创建物理显存 CUmemAllocationProp prop {}; prop.type CU_MEM_ALLOCATION_TYPE_PINNED; prop.location.type CU_MEM_LOCATION_TYPE_DEVICE; CUmemGenericAllocationHandle handleA 0, handleB 0; prop.location.id 0; CHECK_CUDA(cuCtxSetCurrent(ctxA)); CHECK_CUDA(cuMemCreate(handleA, allocSize, prop, 0)); prop.location.id 1; CHECK_CUDA(cuCtxSetCurrent(ctxB)); CHECK_CUDA(cuMemCreate(handleB, allocSize, prop, 0)); // 4. 回到 ctxA把两份物理显存映射进同一段虚拟地址 CHECK_CUDA(cuCtxSetCurrent(ctxA)); CHECK_CUDA(cuMemMap(dptr, allocSize, 0, handleA, 0)); CHECK_CUDA(cuMemMap(dptr allocSize, allocSize, 0, handleB, 0)); // 5. 授予 GPU0 和 GPU1 对整段地址的读写权限 CUmemAccessDesc desc[2] {}; desc[0].location.type CU_MEM_LOCATION_TYPE_DEVICE; desc[0].location.id 0; desc[0].flags CU_MEM_ACCESS_FLAGS_PROT_READWRITE; desc[1].location.type CU_MEM_LOCATION_TYPE_DEVICE; desc[1].location.id 1; desc[1].flags CU_MEM_ACCESS_FLAGS_PROT_READWRITE; CHECK_CUDA(cuMemSetAccess(dptr, allocSize * 2, desc, 2)); printf(VMM OK: dptr%p, minGran%zu, recGran%zu\n, (void*)dptr, minGran, recGran); // 6. 清理先 Unmap、再 Release 句柄、最后 AddressFree CHECK_CUDA(cuMemUnmap(dptr, allocSize * 2)); CHECK_CUDA(cuMemRelease(handleA)); CHECK_CUDA(cuMemRelease(handleB)); CHECK_CUDA(cuMemAddressFree(dptr, allocSize * 2)); return 0; }编译运行后如果输出“VMM OK”说明映射已经建立。注意第5步能成功的前提是GPU0和GPU1之间支持P2P互访如果平台不支持cuMemSetAccess这一步就会报错或者只能授予本卡访问权限。到这里你可以把这个dptr打包成库对外暴露一个看似普通的device指针上层根本不知道它背后是两张卡。3.3 关键参数解读prop、粒度、句柄与清理顺序这个例子虽然短但每个结构体字段都有讲究。CUmemAllocationProp里type字段目前实测只能填CU_MEM_ALLOCATION_TYPE_PINNED这是驱动支持的物理分配类型location.type填CU_MEM_LOCATION_TYPE_DEVICElocation.id填设备序号决定物理显存落在哪张卡上。注意location.id决定的是“物理位置”和当前context是哪个设备没有必然关系但为了兼容旧驱动我习惯在Create之前把context切到目标设备。粒度相关的那几行是很多人的第一个坑。如果把allocSize直接写成512MiB再传给cuMemCreate在推荐粒度是64MB的卡上512MiB刚好是整数倍所以能过但如果你分配一个非整数倍的大小比如400MiB就等着收CUDA_ERROR_INVALID_VALUE吧。分配前先做向上取整对齐这是铁律。句柄的生命周期也要拎清楚。cuMemCreate返回的handle代表物理显存的所有权在cuMemRelease之前这块显存一直存在哪怕Unmap了也一样。所以典型的清理顺序是先cuMemUnmap解除映射再cuMemRelease释放句柄最后cuMemAddressFree释放虚拟地址。顺序反了轻则报错重则把整个context搞脏后面再分配处处报错。另外cuMemMap和cuMemCreate都有offset参数。利用offset你可以创建一块大物理显存然后只映射其中的一部分或者把不同物理分配映射进同一段虚拟地址的不同位置这为显存池的实现提供了很大的弹性。3.4 性能验证与三方案对比映射建立之后怎么验证它真的能跨设备访问最简单的方法是接一段Runtime API代码cudaSetDevice(1)之后起一个kernel去读写dptr指向的整段数据。因为是primary contextRuntime API和Driver API能互通。需要提醒的是GPU1访问位于GPU0上的上半段物理显存时走的是P2P路径所以务必确认两张GPU之间支持P2P。用nvidia-smi topo -m可以看到拓扑用cudaDeviceCanAccessPeer可以编程判断。带宽方面实测下来NVLink连接的卡对P2P带宽能到数百GB/s级别只有PCIe连接的话受限于PCIe传输能力通常只有32GB/s左右对应PCIe 4.0 x16单向理论值。如果你发现跨设备访问特别慢先别怀疑VMM API大概率是拓扑本身就不支持高速P2P驱动走了host中转的慢速路径。把三种方案摆在一起对比会很清楚场景cudaMalloc cudaMemcpyPeercudaMalloc EnablePeerAccessCUDA VMM API地址与物理是否解耦否否是多卡映射到同一虚拟地址否有限是权限控制粒度无设备级映射块级支持显存池化与复用弱弱强调用开销低低中等一次性配置成本适合场景小模型、低频拷贝常规多卡通信大模型、显存池、精细管理我的结论是如果只是偶尔在卡之间拷贝数据别用VMMcudaMemcpyPeer就够了如果要做显存池、做超卖、做动态重映射VMM才是正解。4. 进阶玩法动态重映射、显存超卖与多卡大模型实践4.1 动态重映射与显存超卖把多卡当一张卡用掌握了基础四步之后VMM最好玩的地方就来了你可以随时改变虚拟地址和物理显存之间的映射关系。先说说动态重映射。流水线并行里上一层的输出是下一层的输入传统做法是在卡之间搬运数据。用VMM你可以让这些中间缓冲区的虚拟地址固定不变训练中按需把不同GPU的物理显存映射到这个固定地址上。数据不用搬换的只是一张“映射表”。当然换映射之前要保证没有kernel还在用旧映射这里要用事件或者流同步做好屏障否则会出现访问到一半映射被换掉的诡异问题。再说显存超卖。因为Reserve虚拟地址空间不消耗物理显存你可以先圈一块比物理显存大得多的虚拟地址段然后按需创建物理显存并映射这跟CPU的按需分页思路几乎一样。典型的做法是维护一个“虚拟地址到物理句柄”的映射表kernel要访问某个区域时如果还没映射就现场Create加Map用完了或者长期不用就Unmap并Release腾出显存给其他区域。这样多张卡的显存可以统一调度GPU0满了就把新分配放到GPU1上同一个虚拟地址照样能访问。不过要说清楚VMM的“超卖”不是自动的不像Unified Memory也就是UVM缺页时驱动会自动搬数据。你必须自己写管理逻辑自己决定什么时候映射、什么时候卸载。它给你的不是省心是控制力。4.2 真正的多卡协同张量并行、流水线与KV Cache落到真实场景VMM在多卡大模型里的价值主要体现在三个地方。第一是张量并行。模型参数按列切分到多张卡后每个设备只持有分片但做all-gather时需要把全量参数聚合起来。传统做法是每张卡分配一块完整缓冲区再用集合通信库把分片拷过去。用VMM你可以预留一段和全量参数等大的虚拟地址然后把各卡的分片物理显存依次映射到这个地址段的不同偏移上。这样每张卡只要知道自己的rank就能直接算偏移去访问全量参数省掉一次聚合拷贝。当然实际工程还要考虑通信、同步以及P2P是否真的比拷贝快但这套思路确实能搭出更优雅的框架。第二是流水线并行。前文提到的中间激活动态重映射是核心用法。我在一个8卡流水线并行项目里把跨stage的激活缓冲区固定映射到每个stage本地的物理显存上stage切换时只改映射不再整块搬数据。实测下来通信耗时降了一个量级。第三是KV Cache。LLM推理时KV Cache是动态增长的分配策略直接影响吞吐。用VMM按块映射KV Cache可以实现类似“显存页表”的效果一次性Reserve足够大的虚拟地址需要扩容时再Create加Map新的物理块空闲时Unmap回收。这比每次扩容都重新分配一整块要灵活得多也天然支持多条推理请求共享同一段地址空间。4.3 与cudaMallocAsync、CUDA Graphs的配合很多同学会问有了VMM API是不是以后都用它不一定。NVIDIA在这套底层之上还封装了流序内存分配器也就是cudaMallocAsync和cuMemAllocAsync。它内部就是基于VMM和内存池实现的但暴露出来的是更友好的接口还能在stream里异步分配避免分配操作阻塞kernel执行。如果你的目标只是让分配更快一点或者自动池化直接用cudaMallocAsync更划算VMM API适合需要自己掌控映射关系和权限的场景。和CUDA Graphs配合时有一个大坑Graph捕获期间不要做Unmap或Remap操作。Graph捕获的是kernel和拷贝操作捕获过程中改变地址映射会导致执行期行为不可预知轻则graph实例化失败重则跑出错误结果。我的建议是在Graph捕获之前把映射全部准备好Graph内部只访问已经映射稳定的指针需要改映射时重新实例化Graph或者用Graph Update只更新参数。5. 常见问题与排查技巧实录5.1 问题速查表这一节我把实际开发中遇到最多的问题整理成一张表方便你出了错能快速对号入座。现象常见原因解决办法cuInit返回CUDA_ERROR_INVALID_DEVICE驱动没装好或容器内没挂载GPU先跑nvidia-smi确认设备可见容器加--gpus allcuMemCreate返回CUDA_ERROR_INVALID_VALUEsize没有按最小粒度对齐prop.type或location字段填错打印粒度对size做向上取整检查prop字段cuMemMap返回CUDA_ERROR_INVALID_VALUEoffset或size未对齐handle已经被Release检查offset和size是否粒度整数倍确认句柄还活着cuMemSetAccess返回CUDA_ERROR_INVALID_DEVICE目标设备不支持对源设备显存做P2P访问用cudaDeviceCanAccessPeer先测确认NVLink或PCIe拓扑kernel访问跨设备地址报illegal memory access忘记SetAccess或accessDesc里漏了当前设备确认整段地址都设置过权限用compute-sanitizer定位跨设备访问带宽明显偏低没有原生P2P驱动走了host中转nvidia-smi topo -m看拓扑换NVLink或减少跨设备访问程序退出显存没释放没有按序Unmap、Release、AddressFree检查清理顺序句柄释放后不能再Map5.2 几个独家避坑细节先说context问题。把Runtime API和Driver API混用时最容易出现“驱动说设备不对”的怪问题。我踩过最狠的一次是cuCtxCreate创建了独立context然后又用cudaSetDevice切设备结果Runtime API的操作全跑到默认primary context上去了两边各管各的指针互相不认识。从那以后多卡项目一律用cuDevicePrimaryCtxRetain保留primary contextRuntime和Driver操作都在primary context上进行再也没出过这种乱子。再说多线程。VMM API操作的是整个context的地址空间不是线程安全的。如果开了多个线程同时Reserve或Map需要自己加锁。我见过有人图方便在prefetch线程里直接调cuMemMap结果主线程的kernel突然开始报invalid address查了半天是映射被并发改掉了。还有Windows和Linux的差异。Linux上只要驱动正常VMM基本开箱即用。Windows上如果GPU处于WDDM模式很多P2P和映射特性会被限制表现就是同样的代码在Linux上跑得好好的Windows上各种报错。解决办法是把GPU切到TCC模式管理员权限执行nvidia-smi -g 0 -dm 1然后重启生效。如果你的卡不支持TCC那多卡VMM在Windows上基本很难施展。5.3 调试工具与定位思路排查VMM问题时我常用的工具链是这么一套。先看内存访问是否合法用compute-sanitizer跑一遍。这个工具对非法地址访问、越界、未初始化都能精确定位尤其是跨设备指针访问比肉眼debug快太多。命令是compute-sanitizer --tool memcheck ./your_app。再看P2P拓扑用nvidia-smi topo -m。输出里NV#、SYS这些标志能告诉你卡之间是NVLink直连还是走PCIe再绕CPU。如果跨设备性能不达标这一步基本就能定位原因。查指针归属用cuPointerGetAttribute。传一个设备指针进去能查到这个地址对应哪个设备、属于哪个分配、分配大小是多少。调试“这个指针到底是不是我映射的那段”非常有用。最后是nsys profile。如果性能有问题用nsys抓一次kernel时间线和内存操作能看到是不是有隐性的host同步或者Memcpy DtoH、HtoD在偷偷搬数据。我遇到过“以为全程P2P结果驱动偷偷走host中转”的情况就是靠nsys的Memcpy标记抓出来的。最后分享一点实际体会。第一次把VMM API跑通看到两张GPU的显存挂在同一个指针底下时确实有种打开了新世界的感觉。但用久了你会发现它更像一把手术刀而不是万能钥匙给你极致控制力代价是你要亲手管理所有边界粒度对齐、句柄生命周期、访问权限、同步屏障、线程安全每一个都是坑。我的建议是先从cudaMallocAsync这类高层接口用起等确实遇到了高层接口解决不了的痛点比如动态重映射、精细权限、显存超卖再下沉到VMM API。到那时候你会有更深的底气也更理解这套设计为什么配得上“多GPU时代的虚拟内存革命”这个说法。
返回列表