ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DSH页面GPU仪表盘:快速判断模型在GPU还是CPU运行

DSH页面GPU仪表盘:快速判断模型在GPU还是CPU运行 1. 为什么要在 DSH 页面上折腾一个“仪表盘”做模型推理和训练的人大概都经历过这种场景任务提交上去日志刷得飞快loss 在跳但心里始终悬着一个问题——这模型到底是在 GPU 上跑还是偷偷退回 CPU 了尤其是笔记本双显卡环境一块 Intel 核显加一块独立显卡驱动、运行时、框架版本稍微对不上PyTorch 就会安安静静地回落到 CPU你看着进度条在动实际上算力利用率低得可怜。我在 DSH 页面上装这个“仪表盘”起因就是被这件事坑过好几次。DSH 本身是一个带插件体系的 Web 工作台页面可以挂载各种面板我想要的很简单在页面上直接看到当前设备、显存占用、GPU 利用率、以及模型实际落在哪个设备上不用切终端、不用反复敲命令。这个仪表盘适合所有在 DSH 上跑模型的人不管你是刚装完 PyTorch 的新手还是已经在调 kernel 算子的老手只要你有“我到底用没用上 GPU”这个疑问它就值得装。核心关键词就三个DSH、GPU、CPU。仪表盘要回答的问题也只有一个——算力到底花在哪了。下面我把整套思路、插件加载机制、设备探测逻辑、踩过的坑完整拆一遍。2. 整体设计思路与方案选型2.1 为什么不做成独立脚本而是塞进 DSH 页面最省事的做法当然是写个nvidia-smi的循环脚本或者用watch盯着。但这种方式有几个硬伤一是要来回切窗口二是没法跟 DSH 的任务状态联动三是团队协作时别人看不到你的终端。DSH 的插件体系允许把面板挂到 Web 页面上数据源和展示层分离谁打开页面谁就能看到当前设备状态这才是“仪表盘”该有的样子。从架构上讲我把它拆成三层采集层负责读取设备和运行时信息聚合层负责把原始数据整理成统一结构渲染层负责在 DSH 页面上画出来。三层之间用轻量的消息传递采集层可以定时轮询也可以事件驱动。这样设计的好处是将来想加显存历史曲线、想加多卡对比只需要动聚合层和渲染层采集逻辑不用重写。2.2 设备探测怎么判断模型落在 GPU 还是 CPU这是整个仪表盘的核心。判断逻辑不能只看“有没有 GPU”而要看“当前这个模型/张量在哪个设备上”。我用的判断链路是这样的第一层查运行时可见设备。PyTorch 里torch.cuda.is_available()返回 True 只代表驱动和运行时没问题不代表模型在 GPU 上。第二层查模型参数所在设备。遍历model.parameters()看p.device是cuda:0还是cpu这是最直接的证据。第三层查输入张量设备。有时候模型在 GPU 上但输入数据还在 CPU前向传播时会报设备不匹配或者框架自动搬运这一层能提前暴露问题。第四层查实际算力占用。用nvidia-smi或 NVML 读取 GPU 利用率和显存占用如果模型声称在 GPU 上但利用率长期为 0那大概率是假象。这四层组合起来才能给出一个可信的结论。只靠任何单一指标都会误判我后面会讲具体踩过的坑。2.3 插件加载机制DSH 插件树是怎么工作的DSH 的插件体系是树状结构页面启动时会去加载插件清单。热词里出现的plugin tree failed to load、plugin(s) failed to load这类报错本质上是插件依赖没满足或者入口文件路径不对。我的仪表盘插件遵循最小依赖原则只依赖 DSH 提供的基础面板 API 和一个设备查询模块不引入重型前端框架避免因为依赖冲突导致整棵树加载失败。插件注册时用dsh plugin --profile web add这类命令挂载到 web profile 下加载顺序上仪表盘插件放在基础面板之后、业务插件之前保证它拿到的设备上下文是最新的。这里有个细节如果插件在初始化阶段就去读 GPU 信息而此时驱动还没完全就绪会拿到空值。我的做法是延迟到第一次渲染时再采集并且加一个重试机制。3. 核心细节解析与实操要点3.1 设备信息采集的关键字段仪表盘上我最终展示的字段不多但每一个都有明确用途字段来源用途注意事项设备类型torch device判断 GPU/CPU注意cuda:0和cuda的区别设备名称驱动查询区分核显/独显双显卡环境必须区分显存占用NVML判断是否真的在用单位统一为 MBGPU 利用率NVML判断算力是否被调用采样间隔不宜过短模型设备参数遍历最终结论大模型遍历有开销输入设备张量检查提前发现不匹配动态输入需每次检查这张表是我反复调整后定下来的。早期我加了温度、功耗、频率后来发现对“模型在哪跑”这个问题帮助不大反而让面板变卡就砍掉了。仪表盘的原则是只展示能支撑决策的信息。3.2 双显卡环境的识别难点笔记本上 Intel 核显加独立显卡的组合非常常见热词里也提到了intel uhd graphics和nvidia rtx 4060 laptop gpu并存的情况。这种环境下nvidia-smi只能看到独立显卡核显要走另一套接口。如果仪表盘只查 NVML就会漏掉核显信息用户可能误以为系统只有一块 GPU。我的处理方式是主设备查询走框架运行时辅助信息走系统接口。框架运行时比如 PyTorch 的torch.cuda会告诉你当前进程能用哪些设备这是最贴近模型实际运行环境的。系统接口只用来补充名称和显存这类展示信息。两者不一致时以框架运行时为准因为模型是跑在框架里的不是跑在系统接口里的。3.3 采样频率与性能开销的平衡仪表盘要定时刷新但刷新太频繁会拖慢主任务。我实测下来1 到 2 秒一次是比较舒服的区间。低于 500 毫秒NVML 查询本身会占用可观 CPU高于 5 秒用户会觉得数据“卡住了”失去实时感。另外模型参数遍历这个操作在小模型上无所谓但在几十亿参数的大模型上每次刷新都遍历一遍model.parameters()会有明显开销。我的优化是只在设备状态可能变化时做全量遍历比如任务启动、模型加载完成、手动点击刷新。平时刷新只读缓存的设备结论配合 GPU 利用率做交叉验证。提示如果你的模型参数特别多遍历时可以用next(model.parameters()).device只取第一个参数绝大多数情况下第一个参数的设备就代表了整个模型的设备。只有做混合精度或模型并行时才需要全量遍历。3.4 插件入口与加载顺序的坑DSH 插件树加载失败十有八九是入口文件的问题。我遇到过两种情况一是入口文件里import了一个不存在的模块整个插件树直接挂掉二是插件注册的时机太早DSH 的基础 API 还没初始化完。解决办法很直接入口文件只做注册不做重逻辑。所有设备查询、数据聚合的逻辑放到插件激活后的回调里。入口文件里用 try-catch 包住所有可能失败的 import失败时降级为一个空面板而不是让整棵树崩掉。这个习惯救了我好几次尤其是在 DSH 版本升级、API 变动的时候。4. 实操过程与核心环节实现4.1 环境准备与依赖确认动手之前先把环境摸清楚。这一步不能省我见过太多人跳过这步后面排查半天发现是驱动没装好。# 确认框架能否看到 GPU python -c import torch; print(torch.cuda.is_available()); print(torch.cuda.device_count()) # 确认系统层面的设备信息 nvidia-smi # 确认 DSH 版本和插件命令可用 dsh --version dsh plugin --help如果torch.cuda.is_available()返回 False先别急着装仪表盘先把驱动和运行时理顺。热词里pytorch安装教程gpu和pytorch安装教程cpu是两个完全不同的路径装错了就是 CPU 版本怎么折腾都用不上 GPU。判断方法很简单torch.version.cuda如果是 None那就是 CPU 版本。4.2 插件目录结构与注册我的插件目录结构是这样的dsh-gpu-dashboard/ ├── manifest.json # 插件元信息 ├── entry.py # 入口只做注册 ├── collector.py # 设备信息采集 ├── aggregator.py # 数据聚合 └── panel/ # 前端面板资源 ├── index.html └── dashboard.jsmanifest.json里声明插件名称、版本、依赖和挂载点。挂载点选在页面主区域这样仪表盘能占一块独立空间不跟其他面板挤在一起。注册命令用dsh plugin --profile web add ./dsh-gpu-dashboard注册完刷新页面如果插件树没报错面板应该能出来。如果报plugin tree failed to load先看 DSH 的日志定位是哪个插件失败再单独排查。4.3 采集层实现读取设备与运行时信息采集层的核心是一个函数返回当前设备状态字典。我把它写成纯函数不依赖全局状态方便测试。import torch def collect_device_info(modelNone, inputsNone): info { cuda_available: torch.cuda.is_available(), device_count: torch.cuda.device_count() if torch.cuda.is_available() else 0, model_device: None, input_device: None, gpu_util: None, mem_used_mb: None, } if model is not None: try: info[model_device] str(next(model.parameters()).device) except StopIteration: info[model_device] no-parameters if inputs is not None: if isinstance(inputs, torch.Tensor): info[input_device] str(inputs.device) elif isinstance(inputs, (list, tuple)) and inputs: first inputs[0] if isinstance(first, torch.Tensor): info[input_device] str(first.device) if info[cuda_available]: try: import pynvml pynvml.nvmlInit() handle pynvml.nvmlDeviceGetHandleByIndex(0) util pynvml.nvmlDeviceGetUtilizationRates(handle) mem pynvml.nvmlDeviceGetMemoryInfo(handle) info[gpu_util] util.gpu info[mem_used_mb] mem.used // (1024 * 1024) except Exception as e: info[nvml_error] str(e) return info这段代码有几个设计点值得说。第一model_device用next(model.parameters())而不是全量遍历前面解释过原因。第二NVML 查询包在 try 里因为不同驱动版本接口可能有差异失败时不影响其他字段。第三返回结构是扁平的字典方便前端直接绑定。4.4 聚合层实现把原始数据变成结论采集层给的是原始数据聚合层要把它变成人能看懂的结论。我的聚合逻辑是这样的def aggregate(info): conclusion {} model_dev info.get(model_device) or input_dev info.get(input_device) or util info.get(gpu_util) if cuda in model_dev: conclusion[status] GPU conclusion[message] 模型运行在 GPU 上 elif model_dev cpu: conclusion[status] CPU conclusion[message] 模型运行在 CPU 上请检查设备配置 else: conclusion[status] UNKNOWN conclusion[message] 无法确定模型设备 if cuda in model_dev and util is not None and util 5: conclusion[warning] 模型声称在 GPU但利用率极低可能未真正调用算力 if model_dev and input_dev and model_dev ! input_dev: conclusion[warning] 模型与输入设备不一致可能触发隐式搬运 return conclusion这个聚合逻辑里交叉验证是关键。单看model_device可能被骗加上利用率就能识破“假 GPU”。单看设备一致性可能漏掉隐式搬运加上输入设备检查就能提前预警。4.5 渲染层实现在 DSH 页面上画出来渲染层我用最朴素的方式一个 HTML 面板加一段轮询脚本。不引入前端框架避免依赖冲突。async function refresh() { const resp await fetch(/api/dsh-gpu-dashboard/status); const data await resp.json(); document.getElementById(status).textContent data.status; document.getElementById(message).textContent data.message; document.getElementById(util).textContent data.gpu_util ?? N/A; document.getElementById(mem).textContent data.mem_used_mb ?? N/A; if (data.warning) { document.getElementById(warning).textContent data.warning; document.getElementById(warning).style.display block; } else { document.getElementById(warning).style.display none; } } setInterval(refresh, 1500); refresh();刷新间隔设成 1500 毫秒跟前面说的采样频率对应。状态用颜色区分GPU 绿色CPU 橙色UNKNOWN 灰色。警告信息单独一行红色显示。这样一眼扫过去就知道有没有问题。4.6 完整验证流程装完之后我按这个流程验证了一遍启动一个明确在 CPU 上的小模型看仪表盘是否显示 CPU。把模型.to(cuda)看是否切换成 GPU。故意把输入留在 CPU看是否出现设备不一致警告。跑一个空转的 GPU 任务看利用率是否接近 0 并触发警告。断开 GPU 占用看显存是否回落。五步走完仪表盘的准确性基本就确认了。任何一步不符合预期就回到对应层去查。5. 常见问题与排查技巧实录5.1 插件树加载失败怎么定位plugin tree failed to load这个报错信息很笼统实际原因可能有很多。我的排查顺序是先看 DSH 日志里具体是哪个插件失败报错堆栈是什么。如果是 import 错误检查入口文件的依赖是否都装了。如果是路径错误检查manifest.json里的入口路径是否跟实际文件一致。如果是版本不兼容检查插件声明的 DSH 版本范围。热词里dsh: plugin(s) failed to load: deep这种带包名的报错说明是某个具体插件的问题直接定位到那个插件单独排查就行。5.2 仪表盘显示 GPU 但实际没跑起来这是最坑的一种情况。模型参数确实在cuda:0上但 GPU 利用率长期为 0。可能的原因数据加载是瓶颈GPU 一直在等数据。这时候要看数据管道的吞吐。模型太小计算量不足以让 GPU 忙起来利用率采样到 0 很正常。有隐式的 CPU 同步操作比如频繁的.item()调用把 GPU 拖住了。多进程环境下仪表盘查的是主进程实际计算在子进程。排查方法先看显存占用如果显存占着但利用率低多半是数据或同步问题如果显存也没占那模型可能根本没加载到 GPU 上。5.3 双显卡环境下设备识别错误前面提过核显和独显并存时系统接口和框架运行时的设备列表可能不一致。我遇到过仪表盘显示设备数量为 2但框架只能用 1 块的情况。原因是系统层面能看到核显但框架的运行时只编译了独显支持。处理原则以框架运行时为准。仪表盘上可以展示系统层面的设备列表作为参考但结论必须基于框架运行时。如果两者不一致在面板上给个提示说明系统检测到 N 块设备框架可用 M 块。5.4 常见问题速查表现象可能原因排查方向插件树加载失败入口 import 错误看日志堆栈检查依赖显示 CPU 但以为在 GPU模型未 to 设备检查 model.to 调用显示 GPU 但利用率 0数据瓶颈或同步查数据管道和 .item 调用设备数量不对核显独显混淆以框架运行时为准面板不刷新轮询被阻塞检查 fetch 和定时器显存数据为空NVML 接口异常检查驱动和 pynvml 版本5.5 几个我踩过的坑第一个坑在插件初始化阶段读 GPU 信息。DSH 启动时驱动可能还没就绪读到的全是空值。改成延迟采集后解决。第二个坑用torch.cuda.is_available()作为唯一判断。这个函数只说明环境可用不说明模型在用。必须结合模型参数设备。第三个坑采样频率设成 200 毫秒。面板是流畅了但主任务性能掉了将近一成。改成 1500 毫秒后恢复正常。第四个坑忘记处理模型没有参数的情况。有些模型是纯函数式的model.parameters()为空next()会抛异常。加了 try 之后解决。第五个坑NVML 句柄没释放。长时间运行后句柄泄漏查询变慢。后来改成每次查询后nvmlShutdown()或者复用一个全局句柄。6. 仪表盘后续可以怎么扩展这个仪表盘目前只解决了“模型在哪跑”这一个问题但它的架构留了扩展空间。我接下来打算加两个东西一是显存历史曲线把采样数据存一个环形缓冲区画成折线图这样能看出显存是稳定占用还是缓慢泄漏二是多卡对比视图如果机器上有多块 GPU并排展示每块的利用率和显存方便做负载均衡。还有一个想法是跟 DSH 的任务状态联动。现在仪表盘是独立刷新的如果能在任务启动、暂停、结束时自动触发一次全量采集数据会更准也能减少不必要的轮询。这个需要 DSH 提供任务事件钩子我还在看它的 API 文档。最后分享一个小技巧如果你只是临时想确认一下模型设备不想装整个仪表盘可以在代码里加一行print(next(model.parameters()).device)配合nvidia-smi一起看也能快速判断。但如果你要长期跑任务、要团队协作、要一眼看出问题那还是把仪表盘装上省下来的排查时间远超装它的成本。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进