Skip to content

Repository files navigation

sm-efficiency

nvidia-smi 报告 94% 时,硬件计数器可能只有 5%。

基于 Nsight Systems GPU Metrics,在 RTX 5090 / GB20x 上旁路观察整卡的 SM 吞吐、Tensor 管道、显存带宽与占用率

CI GitHub Code License GitHub last commit GitHub pull request Platform GPU

中文 | 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 推理中)

dashboard

同一 30 秒窗口:nvidia-smi vs 硬件计数器(下图由数据集原始 CSV 生成)

chart

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% 了没什么可压榨"。

📌 为什么消费级 Blackwell 卡 profiling 难

想在 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

第 0 步:环境

需要: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 vs SM Throughput(同一 30s 窗口)

指标 数值
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 证明)。

采样开销验证(A/B,各 5 次,中位数)

条件 吞吐
基线(无采样) 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 -q SIGPIPE 竞态导致的引擎误判(消费级 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

📌 指标对照(gb20x-top,nsys 2026.1.3 实测)

概念 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 数"当百分比读——本仓库解析器已处理。

❓ FAQ

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 开源。

About

消费卡真实 SM 吞吐监测:nvidia-smi 报 94% 时,硬件计数器可能只有 5% | Real SM utilization for NVIDIA consumer GPUs — when nvidia-smi says 94%, hardware counters may say 5%

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages