gputaskman 深度:KV 估算 504 MB 与实测 83 MB 差在哪

微软 GitHub 副总裁 Scott Hanselman 在 11 月 6 日公开了他新开源的工具 gputaskman,它把显卡占用从整机 15 GB 已用这种粗颗粒读数,拆到了哪个进程占了多少、里面装的是哪个模型、权重和 KV cache 各占多少这一层。对本地跑大模型的人来说,这件事的价值比它听上去更大,因为显存不够时,真正缺的信息从来不是总量,而是结构。

一句话结论:gputaskman 是一套用 Python 写的终端界面工具,用 Windows WDDM 计数器加 NVML、AMD ADL/SMI 来做进程级 GPU 归因,还能识别 Ollama、llama.cpp、LM Studio 里的具体模型并给出权重与 KV cache 拆分,Windows、Linux、WSL 和 Apple Silicon Mac 四端都能跑,MIT 协议。代价是它仍是 v0.3.0 的实验性软件,很多读数是推断而非实测。

它到底比现有工具多看到了哪一层

Windows 任务管理器的详细信息页已经能按进程列显存,性能页也能看到 GPU 利用率、温度、功耗。英伟达官方的 GPU 活动监视器则列出正在占用显卡的程序。问题在于,这两条路都停在哪个程序占的,不回答这个程序在干什么。

gputaskman 想补的就是这最后一层。它在进程列表之外多做了三件事。

第一,把系统内存里显卡可访问的部分单独拆成一列。Windows 的显存管理里,显卡放不下的数据会挪到系统内存,这部分在任务管理器里混在别的数字里,gputaskman 把它叫 Spill,用排序键直接排在最前面。

第二,给每个进程做模型鉴定。它能读出进程加载了哪个 GGUF 文件、量化到什么位、堆了多少层,并按 GGUF 头部元数据判断模型类型:带 decision.type 键的是决策模型,带 pooling_type 的是嵌入模型或重排模型,带 stt.* 的是语音模型,其余带层数和已训练上下文的才按因果语言模型算。这套判定会顺带标出哪些是决策模型,例如 Laya 和 OpenJev 这类只做决策不分配 KV cache 的模型。

第三,给出证据等级。每个模型行都会标明它是怎么被认出来的:api 表示运行时本地接口报告模型已加载,mapped 表示操作系统显示该文件此刻正被映射进进程,cmdline 表示文件是启动参数带进来的,installed 只是安装目录里有这个文件、并不能证明加载。两个来源里只要有一个是 api 或 mapped,才算确认。仓库里特意写明,加载了某个 DLL 不构成这个库正在跑 GPU 运算的证据,映射了某个模型文件也不构成这个模型此刻在显卡上。

和任务管理器、nvidia-smi 的能力对照

把这几套工具放在一张表里对照,差别会清楚很多。

问题 Windows 任务管理器 nvidia-smi gputaskman
整机显存占用 能 能 能
进程级显存占用 能,但含共享资源重复计数 WDDM 下常报 N/A,WSL 下完全不列进程 能,优先用 Local Usage 与 Non Local Usage
进程级 GPU 占用率 不提供 能 能,分 SM、编码器、解码器三路
显卡可访问的系统内存 混在进程内存里 不提供 单独成列,Spill 与 Paged 分开
识别加载的模型 不能 不能 能,给出模型名、量化、层数与证据等级
权重 / KV cache 拆分 不能 不能 能给,KV 为估算值
快照对比 不能 不能 有 diff 子命令

其中最值得单独说的是 Windows 的显存计数重复问题。同一块共享的图形表面,可能同时记在它的应用、csrss.exe 和 dwm.exe 三行上,直接把三行加起来就重复算了。gputaskman 优先采用 Local Usage 和 Non Local Usage 这组计数器,只有拿不到时才退回 Dedicated Usage 与 Shared Usage。仓库里记了一次实测对照:同一次采样下,桌面窗口管理器的旧计数器读出约 15.3 GiB,而 Local Usage 只有约 1.4 GiB,差了十倍量级。适配器总量仍然是最终参照,当各进程之和超过适配器总量时,界面会做归一化并明确标注,而不是让数字看起来自洽。

至于 nvidia-smi 为什么在 Windows 上常常给不出进程列表,原因在 WDDM:Windows 自己管理显存,NVML 的进程级查询经常返回 N/A。设备级读数它依然准,但在 WSL 里它连一个进程都列不出来,因为显卡归 Windows 侧所有,Windows 把所有 Linux 进程的显存都记在 vmwp.exe 这个虚拟机工作进程头上。要在 WSL 里看到逐进程数据,就得走 gputaskman 的主机适配器路径。

一组实测数字:估算 504 MB,实际只涨了 83 MB

这是这个工具最有说服力的一个例子。开发者在一台配 RTX 4080 SUPER 的机器上,用 Docker Model Runner 里的 llama-server(走 Vulkan)加载 gemma-3-270m,先把上下文长度从 4096 改到 32768,工具给出的 KV cache 估算值是增加约 504 MB,而服务器实测的专用显存只增加了约 83 MB。

差距为什么这么大,有两层原因。第一层是估算口径本身偏保守:Gemma 3 用的是滑动窗口注意力,注意力实际能触及的上下文远小于标称值,按满窗口算出来的 KV 需求自然是个上限。第二层是分配器的行为:CUDA 和 Vulkan 都是惰性分配,释放的显存还会被缓存起来不立刻归还,所以变小了这件事本身也可能在快照里滞后出现。

工具的做法不是把这个矛盾藏掉,而是把它显式摆出来。运行时自己上报的缓冲区大小和基于 GGUF 元数据的估算值会被单独列在每个进程下面,永远不会加进实测总数里;一个模型文件出现在某个进程里,也只作为这个进程打开或提到过它的证据,不计入显存增长。这套把测得值和推断值分开放的原则贯穿了整个设计。

另一个数值来自进程匹配。在同一台机器上,2 GB 和 5 GB 两次 PyTorch 分配被匹配到正确的 PID,显示为 2.119 GiB 和 4.914 GiB,差额约 0.26 GiB 来自 CUDA 上下文本身。作者在文档里明确标注了这是一台机器一个驱动的结果,不构成保证:早于首次使用 GPU 就启动的进程会破坏按启动顺序的猜测,同一刷新周期内启动的多个进程也只能靠尺寸是否相同来区分。

四个平台的差异比想象中大

README 里有一张矩阵,把 Linux 原生、WSL 互通开启、WSL 互通关闭三种情况下的可测量项列得很清楚。

项目 Linux 原生 WSL(互通开启) WSL(互通关闭 / –no-host)
设备总量、占用率、温度、功耗、频率 NVML NVML NVML
逐进程显存 NVML 实测,容器内带 * 或 ? 标记 从 Windows 主机匹配,带 * 或 ? 标记 显示 n/a
逐进程 GPU 占用率 NVML 分 SM、编码、解码 Linux 进程为 n/a n/a
Spill / Paged 不可用(留空而非显示 0) 仅 Windows 行 不可用
WSL 虚拟机总量、Windows 应用 无 读取 Windows 主机计数器 无
运行时、模型文件、LLM 拆分 读 /proc/<pid>/maps 与命令行 同左 同左

WSL 那一列背后是一整套推断机制。Linux 侧无法知道自己用掉了多少显存,NVML 采样回来的进程 ID 又是 Windows 的 PID,Windows 把所有 Linux GPU 显存都记在 vmwp.exe 名下。gputaskman 的做法是启动一个常驻的 powershell.exe,通过同一套 PDH 数组 API 读 Windows 侧计数器,每秒回传一次,再把 vmwp.exe 的 GPU 上下文和 Linux 进程做配对。首次主机采样约需 2.5 秒,界面上先显示 starting。

配对结果用三种标记区分可信度:* 表示已匹配,是进程与上下文一一对应得出的确切数字;? 表示猜测,针对的是监控启动前就已在跑的进程,靠新进程对应新上下文、老的归最老这类启发式推的;n/a 表示找不到可分配上下文。文档里还有一条原则:猜测每次刷新都会重算,绝不把自己当成自己的证据;已经退出的进程其上下文会被单独扣留,不让给新来的进程,防止串号。

容器场景是另一个坑。NVML 报的是宿主机 PID,在容器的 /proc 里根本没有意义。gputaskman 会把 NVML 未解析的 PID 与容器内映射了 libcuda.so 的进程配对,因为纯 NVML 工具比如 nvidia-smi 只会打开同样的设备文件而从不加载 libcuda,两者的进程集合因此可以区分开。两个 PID 命名空间都按创建顺序编号,配对只能按顺序进行,所以顺序对上了标 *,数量对不上标 ?,剩下的显示成 H:<pid> 表示在本容器之外。文档里记了一次在 Vast.ai 的 Ubuntu 22.04 容器、RTX 3060 Ti、驱动 580.82.09 上的实测:两个启动前已在跑的 PyTorch 任务、一个监控期间新启动的任务和一个被杀掉的任务,都配到了正确的容器 PID,显存分别为 1.12、2.52 和 1.59 GB。

macOS 侧的路径完全不同。Apple GPU 与 CPU 共享一块内存池,没有独立显存可分,工具改用 IOAccelerator 的 PerformanceStatistics 取设备级分配量,每个 Metal 客户端通过 AGXDeviceUserClient 报出创建者 PID 和累计 GPU 时间,两次采样之间的增量就是该进程的占用。进程显存则取 footprint,也就是 macOS 记在图形名目下的 Metal 缓冲与纹理。这项测量每个进程约 70 毫秒,因此放在后台线程里跑:繁忙进程和新进程每轮约 2 秒测一次,空闲进程每 30 秒测一次。实测环境是一台 M4 Mac mini(macOS 26.3.1),一个带 2 GiB 缓冲的 PyTorch MPS 任务被显示为 3.06 GB、99% 占用,设备利用率同为 99%。

怎么装怎么跑

Windows 用户有两条路。 Releases 页面提供免安装的便携包,Intel 与 AMD 平台的 x64 机器取 gputaskman-VERSION-windows-x64.zip,原生 Windows ARM64 取 gputaskman-VERSION-windows-arm64.zip,两者是各自独立的原生构建,不是通用可执行文件。解压时要把整个 ZIP 里的 _internal 目录和 gputaskman.exe 放在一起,然后打开 Windows Terminal 或 PowerShell 直接运行。

命令 作用
gputaskman 进入实时终端界面
gputaskman –once 输出一份文本快照后退出
gputaskman –json 输出一份 JSON 快照,自动隐含 –once
gputaskman diff before.json after.json 对比两份 JSON 快照,看哪块内存真的涨了
gputaskman -i 2 改为每 2 秒采样一次
gputaskman –list-gpus 列出可选适配器及其厂商与 LUID
gputaskman –gpu N 指定适配器,越界会报错而非静默改选
gputaskman –no-host WSL 下不通过互通读取 Windows 主机计数器

不想装 Python 环境的话 Releases 页直接下包就行。需要源码运行的,仓库推荐用 uv,uv run --locked gputaskman 一条命令会自己拿到锁定的 Python 版本、创建虚拟环境并装好锁定依赖,第一次运行需要联网。已经有 Python 3.11 以上版本的用户用 pip 也可以:py -m venv .venv 然后 .\.venv\Scripts\python.exe -m pip install .。Linux 与 WSL 的便携包要求 glibc 2.35 以上,也就是 Ubuntu 22.04、Debian 12 或更新的系统。

要验证 KV cache 涨了多少,标准流程是存两份快照再对比。先 --json > before.json,加载模型或改完上下文参数、让它处理完一个请求之后再 --json > after.json,然后跑 diff。对比时只比较实测计数器,运行时上报的段和估算值会分开列示。进程的配对顺序是先按 PID,再按名称和监听端口,最后才按唯一名称,因为模型重载经常换一个 PID 重启服务器。两个快照间变化小于 16 MB 的进程在文本输出里会被隐藏,但在 JSON 里仍然保留。

读数时最容易踩的三个坑

第一个坑是 Spill 的含义。它表示显卡可访问的系统内存,不等于发生了溢出。里面可能包含有意分配的、固定(pinned)的宿主缓冲。数值很大且显卡接近满载,通常暗示推理已经回落到系统内存、速度会变慢,但单看这个计数器推不出原因。

第二个坑是 Paged 是估算值,计算式是 max(总提交量 - Local Usage - Non Local Usage, 0)。它不是淘汰轨迹,也不是页面文件测量,更不能证明 Windows 刚把多少字节挪出了显存。而且在使用回退计数器时,这个值本身就更不可靠。

第三个坑是内存单位的标注。界面用的 GB 和 MB 实际是二进制单位,也就是 GiB 和 MiB。另外 Mem activity 那一栏读的是 NVML 的显存活动百分比,不是实测的带宽 GB/s。还有一处需要区分:显卡上 CUDA 的活动有时会挂在 3D 引擎那一列上,这不代表它在跑图形渲染。

温度、功耗、频率、PCIe 读数在拿不到时显示 N/A 或 JSON 里的 null,这是驱动没暴露该读数,不能直接当成硬件故障。反过来也一样,进程内存或引擎采样缺失也是显示 N/A,而不是显示 0。文档里有一句提醒很实在:进程的命令行、监听端口这类探测,单次成本在 1 毫秒以内,所以模型与运行时的发现做了节流,新加载的模型可能要等大约 15 秒才出现。

它做不到的事

这个工具的价值有边界,作者本人写得很清楚。它不扫描显存内容,不枚举游戏贴图,不读取别的进程内存里的模型名称,也不做 3D、计算、拷贝、视频这几类引擎的拆分。想看纹理、张量或 kernel 级别的占用,还是得用 PIX、Nsight 或运行时自己的插桩。CPU 推理、NPU 推理和云端负载都在范围之外,Windows 的 Studio Effects 里如果实际是 NPU 执行,不能把 GPU 上的相机或渲染活动当成模型运行的证据。

平台覆盖上也有明确的缺口。仓库里的 issue 显示,原生 Linux 目前只发现 NVIDIA 的 GPU,AMD 在 Linux 侧的支持是一个待办;多 GPU 机器也有用户报告说程序只在第一张卡上工作。这两条都还在 open 状态。PyPI 发布的计划同样只是 issue 里的提问,还没有落地。另外 Windows 上还没有代码签名,也没有安装程序,便携包是唯一分发形式,不过每个 ZIP 都附带许可证和独立的 sha256 校验文件。

常见问题

gputaskman 和 nvidia-smi 该用哪个

看平台和目的。设备级的显存、占用率、功耗、频率,nvidia-smi 依然准,而且轻量得多。要追哪个进程吃掉显存、这个进程跑的是哪个模型、权重和 KV cache 各占多少,Windows 上就得靠 gputaskman,因为 WDDM 下 nvidia-smi 的进程级查询经常给不出答案,WSL 下它更是完全列不出进程。两者可以并行用:nvidia-smi 看卡,gputaskman 看进程。

在 WSL2 里跑 Ollama,为什么看到的显存数字对不上

这是路径本身的问题,不是 bug。WSL 下显卡归 Windows 所有,Windows 把所有 Linux 进程的显存都记在 vmwp.exe 头上,Linux 侧看不到自己的占用。gputaskman 会读 Windows 主机计数器再回传并做配对,标 * 的是确切匹配,标 ? 的是启发式猜测。如果不想要这层推断,可以加 --no-host,代价是逐进程显存直接显示 n/a。想彻底避开这套复杂度,用原生 Linux 或直接跑 Windows 版会干净很多。

KV cache 估算值和实测值差这么多,该信哪个

信实测。估算值来自 GGUF 元数据和上下文长度参数,滑动窗口注意力这类机制会让它明显偏高,它的作用是给出一个上限参考。实测的专用显存增长才是显卡真正被占掉的量。要判断换上下文长度到底加了多少显存,用 --json 存前后两份快照再 diff,这比看单次读数可靠得多。

小结

gputaskman 目前的形态更像一个把显存黑箱拆成可核对清单的工具,而不是一个替代任务管理器的监控面板。它的核心主张是:显卡占用的数字必须能追到具体进程和具体模型头上,并且明确区分哪些是测得的、哪些是推断出来的。做本地大模型推理的人对这件事的痛感往往比做训练的人更深,因为训练时显存不够会直接报错,本地推理时显存不足的表现是悄悄变慢,而原因往往藏在系统内存那一栏里。

要留意的是成熟度。v0.3.0、MIT 协议、149 星、9 个 issue,Windows 便携包只有几百次下载。仓库自己把软件定义为早期实验性质,多 GPU、原生 Linux 上的 AMD、PyPI 分发都还悬着。想尝鲜的话,先在一台有明确用途的机器上跑 --once 和一次快照 diff,比一上来就盯实时界面更容易拿到有用的结论。

相关阅读