当 nvidia-smi 报告 94% 时,硬件计数器可能只有 5%。
基于 Nsight Systems GPU Metrics,在 RTX 5090 / GB20x 上旁路观察整卡的 SM 吞吐、Tensor 管道、显存带宽与占用率
中文 | English
- 在 RTX 5090 上跑 LLM 推理时,
nvidia-smi显示 GPU-Util 94%,而硬件计数器显示 SM 吞吐仅约持续峰值的 5% —— kernel 几乎全程驻留(时间占比高),但 SM 的计算管道大部分时间在等显存喂数据。GPU-Util 是"有 kernel 在跑"的时间占比,不是硬件吞吐利用率。 - 本项目用 Nsight Systems 设备级(device-level)硬件计数器采样绕过消费卡 profiling 限制,在推理服务运行期间读取整卡的 SM Throughput、显存带宽、Tensor 管道活跃率与占用率(不区分进程,建议目标服务独占 GPU)。
- 纯 Bash + Python 标准库实现,零第三方依赖;守护模式只读硬件计数器(不注入进程),A/B 实测未观察到吞吐下降(样本有限,是"低开销"证据而非"零开销"证明)。
- 全部结论附带可公开复核的原始数据(50Hz 计数器 CSV + nvidia-smi 对照采样),一条命令可在你自己的卡上复现。
- 实测环境:RTX 5090(GB20x)/ nsys 2026.1.3 / vLLM。GB20x-first:其他架构 GPU 需通过环境变量更换指标集(无自动发现,见 FAQ)。
注:文中所有数字来自本仓库 benchmarks/results/ 的真实采集(RTX 5090 32GB,Qwen3.6-27B-NVFP4 on vLLM,MTP 投机解码,纯解码负载),原始 CSV 一并开源。
指标语义提前说明:
SM Throughput [Throughput %]的 NVIDIA 定义是 SM 各组成计数器中最高者距其 peak sustained rate 的百分比,不等于实际 FLOP/s 占理论峰值的比例。本项目沿用习惯叫法"SM Efficiency",但语义以本段为准。
如果这个工具帮到了你,欢迎点一个 Star ⭐,这是对开源作者最大的鼓励!
gpu_watch.sh 实时面板(真实终端抓取,RTX 5090 + vLLM 推理中)
同一 30 秒窗口:nvidia-smi vs 硬件计数器(下图由数据集原始 CSV 生成)
GPU-Util 稳在 94%,SM 吞吐只有 5%,显存带宽 67% —— 符合 memory-bound 解码特征(启发式判断)
在消费级 GPU 上自托管 LLM(llama.cpp / vLLM / ollama)时,几乎所有人判断"GPU 是否吃满"的方式都是盯 nvidia-smi 的 GPU-Util。但这个指标的 NVML 语义是:过去采样周期内,至少一个 kernel 驻留在 SM 上的时间占比。它不关心每个 kernel 把 SM 的计算单元填得多满。
LLM 解码阶段(逐 token 生成)是 memory-bound:kernel 一个接一次连续不断(时间占比 → 94%+),但每个 kernel 的计算量极小,主要时间在等权重从显存搬运到 SM。于是出现"GPU-Util 94% 但 SM 吞吐只约持续峰值 5%"的巨大反差 —— 在 GPU-Util 上做调优,等于在看不见瓶颈的数字上做优化。
真实案例(本仓库数据集,同一 30 秒窗口):
| 指标 | 数值 | 含义 |
|---|---|---|
| GPU-Util(nvidia-smi) | 94% | kernel 驻留时间占比 —— 时间维度,易误读 |
| SM Throughput(硬件计数器) | 5.0% | SM 顶层吞吐距持续峰值的百分比 —— 硬件吞吐维度 |
| 显存带宽 | 67.6% | 真正接近饱和的资源 |
| Tensor 管道活跃率 | 5.0% | 矩阵管道同样闲置 |
| Occupancy | 11.1% | 每 SM 驻留 warp 比例 |
正确的调优方向因此完全不同:显存带宽已用到 68% → 加大 batch 摊薄权重搬运(带宽换吞吐);而不是"GPU 已经 94% 了没什么可压榨"。
想在 GeForce 上拿到硬件吞吐视角的指标,常规路径全部走不通:
| 常规手段 | 问题 |
|---|---|
| DCGM profiling 指标(数据中心标配) | profiling 字段组仅支持数据中心 GPU(Tesla/Hopper/…),GeForce 上被禁用 |
| Nsight Compute (ncu) | 必须由 ncu 启动目标进程,无法 attach 已运行的推理服务;且 kernel 重放/序列化会破坏真实服务行为 |
| nvidia-smi / NVML | 只有时间占比型 GPU-Util,无 SM 吞吐 / 带宽 / 占用率 |
| Nsight Systems (nsys) | 唯一可行:设备级硬件计数器采样(--gpu-metrics,Blackwell 消费卡走 gb20x-* 指标集)。但它是批处理工具:产出二进制 .nsys-rep、metricId 是跨版本会变的裸整数、nsys start/stop 会话模式不支持 --gpu-metrics、每次采集有秒级启停开销 |
信息就在硬件计数器里,但没有现成工具能对着运行中的服务把它持续读出来。本仓库补的就是这最后一公里。
┌────────────────────────────┐
nsys (sudo) │ gb20x-top 指标集 @50Hz │ 设备级硬件计数器采样,
--trace=none ──► │ SM吞吐 / 显存带宽 / │ 不 attach、不注入
(daemon 模式) │ Tensor管道 / 占用率 │ 推理进程
└─────────────┬──────────────┘
│ 导出 sqlite
▼
sm_parse.py: 从 TARGET_INFO_GPU_METRICS
按名解析指标; 名称表存在时失配指标一律
N/A, 绝不静默回退旧 ID; 仅名称表整体
缺失时才用已实测的回退 ID 并显式警告
│
▼
sm_status.json ──► gpu_watch.sh 实时面板
三个关键设计:
- 引擎无关:测量层完全不碰推理引擎,可选负载生成器只是向任意 OpenAI 兼容端点发请求(llama.cpp / vLLM / ollama…)。
- 构造上低开销:守护模式只采样硬件计数器(
--trace=none),不做 CUDA API trace、不注入进程 —— NVIDIA 官方对该模式的表述是 minimal overhead;本仓库另有 A/B 实测背书(未观察到吞吐下降,样本有限)。 - 宁缺毋错的指标解析:metricId 优先按名解析(nsys 导出自带名称表);名称表存在而指标失配时直接 N/A,只有名称表整体缺失才回退到实测过的 gb20x-top ID 并打印警告 —— 绝不把别的指标静默当成 SM Throughput 展示。
- 三个开箱即用的工具:一次性精确测量、常驻低开销守护、实时终端面板
- 面板自动识别 llama.cpp / vLLM,实时吞吐、TPOT、并发、排队、KV cache 水位一屏可见
- 一键最小示例:自动选择负载路径(推理服务 / matmul 负载 / 裸测),任何环境都能跑通
- 自动化基准脚手架:吞吐 A/B(开销验证)+ GPU-Util vs SM 吞吐成对采集 + CSV 导出 + 汇总
- 解析器自带瓶颈提示(疑似 memory-bound / 占用率不足 / Tensor 主导,均为启发式),零依赖、单文件
- GPU 序号与指标集可用环境变量替换(
GPU_DEVICE/METRICS_SET),适配非 GB20x 架构
本项目的实测软硬件(供参考)
- GPU: NVIDIA GeForce RTX 5090 (32GB, GB20x)
- 驱动: 610.57.04
- Nsight Systems: 2026.1.3(CLI 即可,无需 UI)
- 推理: vLLM (Qwen3.6-27B-NVFP4, MTP 投机解码, fp8 KV cache),Docker 部署,独占 GPU 0
- OS: Debian 13, 内核 6.12
需要:Linux + NVIDIA GPU(实测 RTX 5090/GB20x)、Nsight Systems CLI 2024.x+、python3(标准库即可)、curl,以及当前用户免密 sudo(系统级计数器采样需要 root,与 ncu 同理):
git clone --depth 1 https://github.com/luckyops/sm-efficiency
cd sm-efficiency
# 免密 sudo 自检: sudo -n true && echo OK
# 非 GB20x 架构: nsys profile --gpu-metrics-set=help 查可用指标集,
# 运行时用 METRICS_SET=<name> 覆盖 (多卡再加 GPU_DEVICE=<n>)./examples/quickstart.sh # 采集 15s,自动找负载自动三选一,任何环境都能跑通:
| 路径 | 条件 | 负载 | 预期结果 |
|---|---|---|---|
| A | 本机有 OpenAI 兼容推理服务(8000/8080/5000/11434) | 真实 LLM 解码 | GPU-Util 高、SM 吞吐低、显存带宽高 → 疑似 memory-bound(启发式) |
| B | 无服务,但 pip install torch 可用 |
matmul 循环 | SM 吞吐高 → compute-bound,与 A 形成对照 |
| C | 都没有 | GPU 上已有负载 | 无 kernel 时 SM 吞吐=0% 是正确值 |
路径 A + B 一起跑就是最好的自证实验:同一张卡,nvidia-smi 两种负载都是 "90%+",硬件计数器却能区分"计算吞吐接近持续峰值"与"显存带宽受限"。 两条路径都在整卡层面采样 —— 请确保被测 GPU 上没有其他 CUDA 进程混跑。
./sm_efficiency.sh 30 # 一次性精确测量(30s,产出 .nsys-rep + sqlite + 解析报告)
./sm_daemon.sh start # 常驻低开销采样(双 worker 接力, 约每 ~4.5s 更新)
./gpu_watch.sh # 实时终端面板(支持 llama.cpp / vLLM 指标)
./sm_daemon.sh stop # 停止守护sm_efficiency.sh |
sm_daemon.sh |
gpu_watch.sh |
|
|---|---|---|---|
| 用途 | 一次性精确测量 | 常驻后台采样 | 实时观察面板 |
| 开销 | 一次性(含 CUDA trace,不宜常驻) | 低(纯计数器,不注入进程;A/B 未观察到吞吐影响) | 可忽略(仅查询 nvidia-smi / 读守护状态) |
| 产出 | .nsys-rep + sqlite + 报告 | sm_status.json(双 worker 接力 ~4.5s 一轮) | 终端面板 |
| 引擎耦合 | 无(可选向任意 OpenAI 端点发负载) | 无 | 读 llamacpp: / vllm: Prometheus 指标 |
cd benchmarks && ./run_benchmark.sh # 约 3 分钟,输出 results/<日期>_<GPU>/自动完成:基线吞吐 → 采样中吞吐(开销验证)→ GPU-Util 与 SM 吞吐同窗口成对采集 → 导出原始 CSV → 汇总打印。
完整数据集:benchmarks/results/20260818_5090_qwen36nvfp4/(原始 50Hz 计数器 CSV 7470 行 + nvidia-smi 采样 + 环境快照,可直接复核)
环境:RTX 5090 32GB · 驱动 610.57.04 · nsys 2026.1.3 · Qwen3.6-27B-NVFP4 on vLLM(MTP 投机解码 ×2,fp8 KV,独占 GPU 0)· 纯解码流量
| 指标 | 数值 |
|---|---|
| GPU-Util(nvidia-smi,0.5s 采样) | 中位数 94%,均值 88.3% |
| SM Throughput(nsys,50Hz,距持续峰值) | 5.0%(最大 6.0%) |
| Tensor 管道活跃率 | 5.0% |
| 显存带宽 | 67.6% |
| Occupancy | 11.1% |
| 功耗 | 478W / 575W TDP |
GPU-Util / SM-Throughput ≈ 19×:kernel 几乎全程驻留,计算管道基本空闲,显存带宽才是接近饱和的资源 —— 符合 memory-bound 解码特征(带宽% 与 SM% 的比值判断,启发式而非 roofline 证明)。
| 条件 | 吞吐 |
|---|---|
| 基线(无采样) | 105.6 tok/s |
| 50Hz 计数器采样中 | 124.6 tok/s |
未观察到吞吐下降(采样中反而略高,在 MTP 接受率波动带内;两组分布均横跨 102–131 tok/s)。注意:样本仅 5+5 次且方差大,这只能说明"未检出开销",不是零开销的证明;一次性模式(sm_efficiency.sh)含 CUDA trace,开销更高,不适合常驻。守护模式适合常驻运行。
2026-08-18 · v0.1.2 守护采样提速(数据点 8s → ~4.5s)
- 发现设备级计数器会话由驱动互斥:并发 nsys 客户端中后来者立即失败(此前被误判为"并发可行")
- 双 worker 交错接力设计:会话被占视为预期竞争,静默退避 0.5s 重试,接管对方启停间隙
- 采样窗口 5s→3s、去掉无谓的 1s 启动延迟、中间文件落 tmpfs(/dev/shm)
- 状态文件原子写(多 worker 无半截读);stop 改 setsid 精确组杀并清理 tmpfs 残留
- gpu_watch 数据过期阈值 300s→30s
- 修复 stop 中全局 pkill 的自匹配/跨实例误杀问题
2026-08-18 · v0.1.1 严谨性修正(响应外部工程审查)
- metric ID 回退安全化:名称表存在而指标失配时一律 N/A,绝不静默使用旧 ID(旧 ID 在其他 nsys 版本/指标集下可能指向别的指标);仅名称表整体缺失才回退并显式警告
- 指标语义精确化:
SM Throughput %= SM 各组成计数器最高者距 peak sustained rate 的百分比,不是 FLOP/s 占理论峰值比例 - memory-bound 判定改为"疑似(likely,启发式)",明确非 roofline 证明
- 全文"零开销"改为"低开销 / 未观察到吞吐下降",并区分守护模式(纯计数器)与一次性模式(含 CUDA trace)
- 明确 device-level 采样边界:不区分进程,建议目标服务独占 GPU
GPU_DEVICE/METRICS_SET环境变量化,非 GB20x 架构可换指标集(无自动发现)- gpu_watch 显存拆解改为"非权重(估算)"命名,注明含 CUDA 预留/workspace 等无法区分
2026-08-18 · v0.1.0 首个开源版本
- 核心三件套:
sm_efficiency.sh(一次性测量)/sm_daemon.sh(常驻低开销采样)/gpu_watch.sh(实时面板) - 面板支持 llama.cpp 与 vLLM 双引擎指标(vLLM:实时 tok/s 差分、并发/排队、KV cache 水位)
- metricId 按名动态解析(TARGET_INFO_GPU_METRICS)
- 一键示例
examples/quickstart.sh(自动三路径)+ 独立 matmul 负载 - 自动化基准
benchmarks/run_benchmark.sh+ 首批真实数据集(RTX 5090 / vLLM) - 修复
pipefail + grep -qSIGPIPE 竞态导致的引擎误判(消费级 shell 脚本的经典坑)
sm-efficiency/
├── sm_efficiency.sh # 【测量】一次性精确测量(nsys 报告 + 解析 + 瓶颈提示)
├── sm_daemon.sh # 【守护】常驻低开销采样(start/stop/status/once)
├── gpu_watch.sh # 【观察】实时终端面板(llama.cpp / vLLM 自动识别)
├── sm_parse.py # 【解析】GPU_METRICS 解析器(按名解析, 失配即 N/A, 启发式解读)
├── sm_loadgen.sh # 【负载】OpenAI 兼容请求生成器
├── examples/
│ ├── quickstart.sh # 一键最小示例(自动选择负载路径 A/B/C)
│ ├── matmul_load.py # 独立计算型 GPU 负载(torch,可选)
│ └── README.md
├── benchmarks/
│ ├── bench_decode.sh # 端到端解码吞吐测量
│ ├── run_benchmark.sh # 完整基准(A/B 开销验证 + 成对采集 + 汇总)
│ └── results/ # 真实数据集(CSV / 工具输出 / 环境快照)
├── images/ # README 图片资源(真实数据生成)
└── .github/workflows/ # CI(bash/python 语法检查)
| 主题 | 说明 |
|---|---|
| Throughput % 的含义 | NVIDIA 定义:某单元各组成计数器中最高者距其 peak sustained rate 的百分比。SM Throughput 5% 读作"SM 子单元中最忙的一个也只到持续峰值的 5%",不等于 实际 FLOP/s ÷ 理论峰值 FLOP/s |
| memory-bound 判定 | 带宽% 与 SM% 的比值启发式,输出为"疑似(likely)";严格判定需要 roofline(achieved FLOP/s + 访存量 + 两个峰值) |
| 进程归因 | nsys GPU Metrics 是 device-level(NVIDIA 文档明确不区分 process/context)。GPU 上多进程混跑时读数是混合值 —— 请让目标服务独占 GPU |
| 架构适用范围 | 实测 RTX 5090 / gb20x-top / nsys 2026.1.3。其他架构用 METRICS_SET= 换指标集(解析器按名自适应 ID),没有 GPU→指标集自动发现 |
| 更新频率的边界 | 设备级计数器会话由驱动互斥、nsys 每次启停有 ~2.2s 固有开销、会话模式数据仅在 stop 时落地 → 无法真流式。守护用双 worker 接力把数据点周期从 8s 压到 ~4.5s(窗口 3s),这是当前 nsys CLI 的实际下界附近 |
| 回退 ID 的边界 | 仅当导出完全缺少名称表时使用(并在输出/json 中警告);名称表存在而失配 → N/A |
| 概念 | nsys 指标名 | 回退 ID |
|---|---|---|
| SM Throughput(习惯称 SM Efficiency) | SM Throughput [Throughput %] |
12 |
| Tensor 管道活跃 | SM Tensor Pipe Active [Throughput %] |
51 |
| 显存带宽 | VRAM Total Bandwidth [Throughput %] |
7 |
| Occupancy | Sync CS SM Warps [Occupancy %] |
166 |
| GPU-Util 等价物 | GR Engine Active [Throughput %] |
0 |
同名指标有多个单位变体(
[Total]/[Bytes]/[Avg Warps]…),解析必须匹配完整名称,否则会把"平均 warp 数"当百分比读——本仓库解析器已处理。
Q1: GPU-Util 到底是什么? NVML 语义:过去采样周期内"至少一个 kernel 驻留在至少一个 SM 上"的时间占比。它是时间维度,不是硬件吞吐维度;两个都显示 94% 的负载,一个可能在真满载、一个在等显存。
Q2: 为什么无负载时 SM 吞吐显示 0%?是 bug 吗? 不是。解码 kernel 只有微秒级,若采样窗口落在请求间隙,SM 确实完全空闲,0% 就是硬件计数器的真实读数。
Q3: 为什么必须 sudo? 系统级 CUPTI 硬件计数器采样需要 root 权限(与 ncu / nsys 常规用法一致)。所有脚本都会预检免密 sudo 并快速失败,不会中途卡住。
Q4: 支持哪些 GPU?
实测 RTX 5090(gb20x-top)。其他 Blackwell 消费卡(RTX 50xx)通常同集;更早架构需 nsys profile --gpu-metrics-set=help 查可用集并用 METRICS_SET=<name>(多卡加 GPU_DEVICE=<n>)覆盖默认值。解析器按名匹配指标,ID 变化可自适应;但没有自动的 GPU→指标集发现。
Q5: 采样开销到底多低?
机制:守护模式 --trace=none 只读硬件性能计数器,不注入 CUPTI、不做 API interposition —— NVIDIA 官方表述为 minimal overhead。实测:A/B 各 5 次,吞吐中位数 124.6 vs 105.6 tok/s,未观察到下降;但样本少、方差大(MTP 接受率波动),只能说明"未检出开销",不能证明开销为零。可用 run_benchmark.sh 在自己的环境复验。注意一次性模式(sm_efficiency.sh)含 CUDA trace,开销更高,不建议常驻。数据点更新频率见「指标语义与边界」的更新频率条目。
Q6: 和 DCGM / ncu 的本质区别? DCGM 读不到(消费卡禁用);ncu 进不去(无法 attach 运行中进程,且重放破坏真实行为)。本工具用 nsys 设备级采样绕开两者限制,专门面向"正在服务的消费卡"。
Q7: 能测出"vLLM 这个进程"用了多少 SM 吗? 不能。nsys GPU Metrics 是 device-level 的(NVIDIA 文档明确),不提供 per-process 归因。GPU 上只有目标服务时,整卡读数即服务读数;多进程混跑时是混合值。这也是工具提示"建议独占"的原因。
Q8: 面板里"非权重(估)"是什么? 用 (总显存占用 − 磁盘模型文件大小) 估算,包含 KV cache、激活、CUDA allocator 预留、CUDA graph/workspace 等,无法进一步区分 —— 是估算不是测量,故不称"KV/激活"。
如果 sm-efficiency 对你的工作有帮助,欢迎引用:
@misc{sm-efficiency,
title = {sm-efficiency: Device-level SM Throughput Monitoring for NVIDIA Consumer GPUs},
author = {luckybear},
year = {2026},
url = {https://github.com/luckyops/sm-efficiency},
note = {GitHub repository}
}本项目基于 MIT License 开源。