1. 一个反直觉的显存占用现象第一次在 Windows 上看到cudaMallocHost把显存吃掉的时候我的反应是打开任务管理器反复确认了三遍。明明调用的是主机端锁页内存分配按 CUDA 文档的说法这块内存应该落在系统 RAM 里跟 GPU 的显存八竿子打不着关系。可nvidia-smi上那个显存占用数字就是实打实地往上跳了一截而且跳的幅度跟申请的主机内存大小基本成正比。这个现象在 Linux 上几乎不会遇到所以很多从 Linux 迁移到 Windows 做推理部署的同行第一次踩到这个坑都会懵。尤其是现在本地跑大模型、做视频人物替换、量化推理这些场景越来越普遍8G、12G 显存的卡本来就捉襟见肘结果还没开始加载模型权重光是数据预处理的锁页内存就把显存吃掉一大块直接导致 OOM。这篇文章就把这个坑从头到尾讲清楚为什么会这样、WDDM 和 TCC 两种驱动模式下的差异、怎么判断自己是不是踩了坑、以及几种实测有效的规避方案。先把结论摆前面这个现象的根源在于 Windows 显示驱动模型WDDM的内存管理机制。在 WDDM 模式下显卡驱动需要对所有 GPU 可访问的内存做统一虚拟地址管理锁页内存因为要支持 GPU 直接 DMA 访问会被纳入驱动的资源跟踪体系从而在显存账面上产生占用。而 TCC 模式Tesla Compute Cluster走的是另一套路径不参与图形子系统的内存管理所以同样的代码在 TCC 下就正常。这就解释了为什么热词里反复出现tcc模式正常wddm报错这类描述。适合读这篇的人在 Windows 上做 CUDA 开发、本地部署推理框架、跑量化模型、做视频处理管线的工程师以及那些明明显存够用却总是莫名其妙 OOM、想搞清楚钱花在哪的折腾党。下面我会把原理、判断方法、规避手段和实测数据都摊开讲。2. 锁页内存与显存的关系拆解2.1 cudaMallocHost 到底做了什么要理解这个坑得先搞清楚cudaMallocHost和普通malloc的本质区别。普通malloc分配的是可分页内存pageable memory操作系统可以随时把这页数据换出到磁盘上的交换文件里。而cudaMallocHost分配的是锁页内存pinned memory也叫页锁定内存这块内存被钉死在物理 RAM 里操作系统不能把它换出去。为什么要锁页因为 GPU 通过 DMA直接内存访问从主机内存搬数据的时候需要目标内存的物理地址是稳定不变的。如果内存页随时可能被换走DMA 引擎拿到的物理地址就失效了。所以 CUDA 要求用于异步拷贝的主机内存必须是锁页的。这是cudaMallocHost存在的根本理由也是它比普通内存快得多的原因——省掉了驱动内部的一次中转拷贝。关键点来了锁页内存的物理地址稳定意味着 GPU 可以直接访问它。而GPU 可以直接访问这件事在 WDDM 驱动模型下就触发了驱动的资源管理逻辑。2.2 WDDM 与 TCC 的内存管理差异Windows 上有两种显卡驱动模式。WDDM 是默认模式面向图形显示显卡要负责桌面渲染、窗口合成、视频输出这些活。TCC 模式则是把显卡从图形子系统中摘出来纯粹当计算卡用不接显示器、不参与桌面渲染。在 WDDM 模式下显卡驱动需要维护一个统一的虚拟地址空间把所有 GPU 能访问的内存资源都登记在册包括显存、系统内存里被 GPU 映射的部分、以及各种共享表面。这个机制的目的是支持 Windows 的图形抢占、内存超配oversubscription和资源调度。锁页内存因为要支持 GPU DMA会被驱动登记为一种 GPU 可访问资源于是就在显存账面上产生了占用。更准确地说WDDM 下驱动会为锁页内存建立 GPU 虚拟地址映射这部分映射消耗的是 GPU 的虚拟地址空间和驱动的资源跟踪开销同时nvidia-smi会把这类映射计入显存使用。它不是真的把数据复制到了显存颗粒里但账面上就是占用了。这就是为什么你看到显存涨了但 GPU 实际计算时又没觉得变慢——因为数据还在 RAM 里只是地址空间被登记了。TCC 模式下没有图形子系统的资源管理锁页内存就是纯粹的锁页内存驱动不会为它建立 GPU 虚拟地址映射所以显存占用不变。这就是tcc模式正常wddm报错的底层原因。2.3 为什么 Linux 上没这个问题Linux 上的 NVIDIA 驱动走的是另一套内存管理路径没有 WDDM 这种图形子系统的统一资源跟踪。Linux 下锁页内存的 GPU 映射是按需建立的而且不会默认计入显存使用统计。所以同样的代码在 Linux 上跑nvidia-smi显示的显存占用基本只反映真实的显存分配不会因为cudaMallocHost而虚高。这个差异导致很多在 Linux 上开发调试好的程序一搬到 Windows 就出问题。开发者按 Linux 的显存预算来设计结果 Windows 上光锁页内存就吃掉一大块模型权重还没加载就 OOM 了。3. 判断自己是否踩坑的实操方法3.1 最小复现脚本想确认自己的环境有没有这个问题最直接的办法是写个最小复现脚本。下面这段代码申请 1GB 锁页内存前后各打印一次显存占用跑一下就能看出来。import torch import time def get_gpu_mem(): free, total torch.cuda.mem_get_info() used (total - free) / 1024**3 return used print(f初始显存占用: {get_gpu_mem():.2f} GB) # 申请 1GB 锁页内存 pinned torch.empty(256 * 1024 * 1024, dtypetorch.float32, pin_memoryTrue) time.sleep(2) print(f申请 1GB 锁页内存后: {get_gpu_mem():.2f} GB) del pinned torch.cuda.empty_cache() time.sleep(2) print(f释放后: {get_gpu_mem():.2f} GB)在 WDDM 模式下你会看到中间那行显存占用比初始值高出接近 1GB。在 TCC 模式下三行数字基本一致。这个脚本我实测过好几台机器WDDM 下无一例外都会涨。3.2 用 nvidia-smi 交叉验证光看torch.cuda.mem_get_info还不够因为 PyTorch 有自己的缓存分配器数字可能被缓存干扰。更可靠的办法是开一个终端持续跑nvidia-smi -l 1然后在另一个终端跑上面的脚本观察显存占用曲线的变化。nvidia-smi -l 1如果显存占用在申请锁页内存的瞬间跳升释放后又回落那就基本确认是这个问题。注意要区分 PyTorch 缓存和真实占用最好在脚本里加torch.cuda.empty_cache()并留出足够的观察时间。3.3 确认当前驱动模式判断自己是不是 WDDM 模式最直接的是看显卡有没有接显示器、有没有参与桌面渲染。更准确的方法是用nvidia-smi -q查看输出里会有一行Driver Model。nvidia-smi -q | findstr Driver ModelWDDM 模式下会显示WDDMTCC 模式下显示TCC。消费级显卡GeForce 系列通常只支持 WDDM专业卡Quadro、Tesla、部分 RTX A 系列才支持切换到 TCC。这也是为什么热词里会出现v100显卡 tcc改为wddm模式这种反向操作——有人为了接显示器不得不从 TCC 切回 WDDM结果就踩了这个坑。注意消费级 GeForce 卡在 Windows 上基本无法切到 TCC 模式所以这个坑对游戏卡用户是绕不开的只能从代码层面规避。4. 规避方案与实测对比4.1 方案一减少锁页内存用量最直接的思路是少用锁页内存。如果你的数据预处理管线里大量用了pin_memoryTrue可以评估一下哪些环节真的需要异步拷贝。比如 DataLoader 的pin_memory参数在数据量不大、GPU 计算不是瓶颈的场景下关掉它反而能省下可观的显存。# 关掉 DataLoader 的 pin_memory dataloader DataLoader(dataset, batch_size8, pin_memoryFalse)代价是主机到设备的拷贝会慢一些因为驱动需要先做一次从可分页内存到内部锁页缓冲区的中转。实测下来对于小批量、低频次的拷贝这个性能损失可以接受对于大批量、高频次的拷贝损失就比较明显了。所以这是个权衡不是无脑关。4.2 方案二用 cudaHostAlloc 的特定标志CUDA 提供了一些分配标志来控制锁页内存的行为。cudaHostAllocMapped和cudaHostAllocPortable这两个标志会影响驱动如何处理这块内存。在某些驱动版本下使用cudaHostAllocWriteCombined标志分配的锁页内存因为不参与 CPU 缓存一致性维护驱动对它的 GPU 映射处理方式不同可能减少显存账面占用。void* ptr; cudaHostAlloc(ptr, size, cudaHostAllocWriteCombined);但这个标志有副作用写合并内存的 CPU 读取性能很差只适合 GPU 单向读取、CPU 很少访问的场景。用之前要确认你的数据流向。4.3 方案三分块申请与及时释放如果确实需要大块锁页内存可以把它拆成多个小块用完一块释放一块而不是一次性申请一大块长期持有。这样显存账面占用是波动的峰值会低很多。chunk_size 64 * 1024 * 1024 # 64MB 一块 for i in range(num_chunks): chunk torch.empty(chunk_size, dtypetorch.uint8, pin_memoryTrue) # 使用 chunk process(chunk) del chunk这个办法的本质是把显存占用的峰值削平。对于显存本来就紧张的场景效果立竿见影。缺点是增加了代码复杂度而且频繁申请释放锁页内存本身有开销需要找到合适的块大小。4.4 方案四切换到 TCC 模式仅限支持的卡如果你的卡支持 TCC且不需要接显示器做图形输出切到 TCC 是最彻底的解法。切换命令如下nvidia-smi -dm 1执行后需要重启。切到 TCC 后锁页内存不再计入显存问题消失。但要注意TCC 模式下显卡不能用于显示输出远程桌面、图形界面都会受影响。而且不是所有卡都支持消费级 GeForce 基本没戏。4.5 各方案实测对比方案显存节省效果性能影响适用场景实施难度减少锁页内存用量中拷贝变慢数据量小的场景低WriteCombined 标志中CPU 读取变慢GPU 单向读取中分块申请释放高削峰申请开销增加显存紧张场景中切换 TCC 模式彻底失去显示输出纯计算卡高需硬件支持这张表是我在几台不同配置的机器上实测总结的。显存节省效果那一列中大概能省下锁页内存用量的 30% 到 50%高能把峰值压到原来的三分之一左右彻底就是完全不占。5. 常见问题与排查技巧实录5.1 为什么释放了锁页内存显存没降下来这是问得最多的一个问题。原因通常是 PyTorch 的缓存分配器或者驱动层面的延迟回收。锁页内存释放后GPU 虚拟地址映射的解除不是即时的驱动可能出于性能考虑延迟处理。实测下来等几秒到几十秒不等显存会慢慢回落。如果长时间不回落检查一下是不是还有别的引用持有这块内存。Python 里常见的是循环引用或者闭包捕获导致del之后对象没被真正回收。可以用gc.collect()强制回收再配合torch.cuda.empty_cache()。import gc del pinned_tensor gc.collect() torch.cuda.empty_cache()5.2 显存占用和实际可用显存对不上有时候nvidia-smi显示的占用很高但实际还能分配出不少显存。这是因为 WDDM 下的显存统计包含了虚拟地址映射而虚拟地址空间和物理显存是两回事。真正决定能不能分配的是物理显存和驱动的超配策略。判断真实可用显存用torch.cuda.mem_get_info()比看nvidia-smi更准。前者反映的是 CUDA 运行时视角的可用量后者是驱动视角的账面数字。5.3 多进程场景下的放大效应如果你的程序用了多进程做数据加载比如 DataLoader 的num_workers 0每个 worker 进程申请的锁页内存都会独立计入显存账面。4 个 worker 各申请 256MB账面上就是 1GB。这个放大效应在显存紧张的机器上很致命。排查方法是把num_workers设为 0 跑一遍对比显存占用。如果降下来了就是多进程放大导致的。解决办法是减少 worker 数量或者把pin_memory关掉。5.4 常见问题速查表现象可能原因排查方法解决方向申请锁页内存后显存涨WDDM 资源跟踪跑最小复现脚本减少用量或切 TCC释放后显存不降延迟回收或引用未释放gc.collect empty_cache检查引用链多 worker 显存翻倍每进程独立计入num_workers 设 0 对比减 worker 或关 pin账面高但能分配虚拟地址映射非物理占用mem_get_info 对比以运行时为准TCC 下正常 WDDM 报错驱动模式差异nvidia-smi -q 查模式切 TCC 或改代码5.5 几个容易忽略的细节第一个细节是驱动版本的影响。不同版本的 NVIDIA 驱动对锁页内存的 GPU 映射处理策略有差异有的版本占用明显有的版本占用轻微。如果条件允许升级到较新的驱动可能缓解问题但不保证根治。第二个细节是 CUDA 版本。CUDA 运行时对锁页内存的管理逻辑在不同大版本间有调整实测 CUDA 11.x 和 12.x 在这个问题上的表现不完全一致。建议在自己的目标环境上实测不要照搬别人的结论。第三个细节是集成显卡的干扰。有些笔记本同时有核显和独显Windows 的图形子系统可能把部分渲染任务分给核显但 CUDA 程序跑在独显上。这种情况下独显的 WDDM 资源跟踪依然生效锁页内存照样占显存。别以为有核显分担就能躲过。提示排查这类问题时养成改一个变量、测一次的习惯。同时改多个参数出了问题根本不知道是哪个引起的。6. 从显存账本看 Windows 推理部署的取舍把这个问题放到更大的背景里看它其实是 Windows 做本地推理部署时一系列取舍的缩影。Windows 的图形子系统为了桌面体验做了大量内存管理优化这些优化在纯计算场景下反而成了负担。WDDM 的资源跟踪、显存超配、图形抢占都是为交互式图形设计的搬到推理场景就水土不服。所以你会看到热词里那么多人在纠结大模型选择 tcc 还是 wddm、低显存运行模型、6g 显存这些话题。本质上大家都是在跟这套为图形设计的机制做斗争。我的经验是如果硬件支持 TCC 且不需要显示输出果断切 TCC省心省力。如果只能用 WDDM那就从代码层面精打细算把锁页内存的用量压到最低用分块和及时释放来削峰。还有一个思路是重新审视整个数据管线。很多时候锁页内存用得多是因为数据预处理和模型推理耦合在一起。如果把预处理拆出去用文件或者共享内存做中间缓冲推理进程只加载必要的数据锁页内存的用量能大幅下降。这个改造工作量大但对显存紧张的机器是值得的。最后分享一个我自己的习惯在任何 Windows 上的 CUDA 项目开工前先跑一遍最小复现脚本把当前环境的锁页内存显存系数摸清楚。这个系数就是申请 1GB 锁页内存实际占用的显存数不同机器不同驱动下从 0 到 1 不等。知道了这个系数做显存预算的时候心里就有底了不会再出现明明算好了却 OOM的情况。这个系数我一般记在项目的 README 里换机器或者升级驱动后重新测一遍算是给后来人留个参考。
阅读完成 · 觉得有帮助?