ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

后端工程师的GPU硬件地图:从nvidia-smi到计算强度

后端工程师的GPU硬件地图:从nvidia-smi到计算强度 第一篇分享我留了一个钩子说后端工程师学 GPU第一步不是去背 CUDA 编程手册而是先在脑子里装一张和 CPU 完全不同的硬件地图。这篇就干这件事。我不打算讲太深的微架构细节目标是让你读完以后再看到nvidia-smi的一堆输出、再听到显存带宽SM 占用率CUDA 上下文这些词不会再一头雾水。你会发现GPU 这套东西骨子里还是一套计算机系统只是它为了一个极端目标把另一堆东西全部牺牲掉了。而我们要做的就是把 CPU 时代那套心智模型拆掉三分之一再重新拼出另一套。1. 从 nvidia-smi 的第一行说起GPU 不是 CPU 的加强版而是另一套计算哲学先讲个我见过无数次的场景一个纯后端工程师拿到一台带 GPU 的服务器第一个动作通常就是敲nvidia-smi然后盯着输出看半天。能看到温度、风扇转速、显存占用唯独看不到他最熟悉的CPU 使用率和平均负载。紧接着脑子里冒出一堆问题为什么这块卡有 80G 存储空间利用率 30% 算高算低这个功耗正常吗这些问题我能理解因为在 CPU 的世界里一个进程占用多少 CPU、多少内存基本能对应到业务负载。但在 GPU 的世界里观察指标不同底层逻辑完全不同。如果还用 CPU 的直觉去理解 GPU第一步就会走偏。1.1 GPU 用控制逻辑换来了计算密度要建立心智模型先看一个最关键的事实一个现代 CPU 芯片的大部分面积都给了缓存、乱序执行、分支预测器和各种控制逻辑。真正干算术运算的 ALU 只占一小块。为什么因为 CPU 要面对的是什么任务都可能有的场景有 if/else、有随机内存访问、有依赖关系复杂的运算它必须用大量硬件去猜测下一步会发生什么猜错了还能回滚重来最大程度保证每一条指令都在流水线里高效运行。GPU 干了件非常极端的事把 CPU 引以为傲的控制逻辑、大缓存、分支预测全部砍到最低然后把省下来的面积全部堆成简单的小计算单元。NVIDIA 把这些小单元组织成一个个 Streaming MultiprocessorSM每个 SM 里面有大量小核心。这些小核心没有复杂的乱序执行没有大容量私有缓存它们靠的就是数量碾压和一个指令喂给一堆数据同时算。我用到的一个类比对后端工程师特别有用CPU 像一个米其林餐厅的顶级大厨能力全面什么菜都能做但一次只能做几道GPU 像一条自动化快餐流水线动作单一但同样时间里能出几百份薯条。你让流水线去做米其林主厨的工作它做不了你让主厨去炸薯条他一个人也扛不住连锁店的订单量。GPU 之所以在深度学习和科学计算里这么好用是因为这些场景的工作负载恰好是大量简单重复的算术运算正对流水线胃口。理解了这一点你再回看nvidia-smi里那行 GPU 利用率就不会拿它和 CPU 使用率等同了。它反映的是 SM也就是那些流水线在一段时间内有多少比例在干活。但注意SM 在干活不代表它在高效地算它可能是在排队等显存数据——这个误区后面单独讲。1.2 nvidia-smi 输出里的每一列到底在告诉你什么我第一次带后端同事看nvidia-smi的时候用了整整一下午把那几列数据一个个掰开揉碎讲清楚。这里我简单写一下最有用的几列指标后端直觉理解GPU 层面的真正含义GPU-Util类似 CPU 使用率SM 有 warp 在执行的时间占比不代表计算单元在做有效运算Memory-Usage类似内存占用Device Memory显存已分配量包括上下文、模型、中间张量Power类似 PSU 功率当前功耗能间接反映负载状态但受时钟频率和冷却策略影响Temperature类似机房温度核心温度超过阈值会降频导致性能断崖Volatile GPU-Util类似瞬时负载一段时间窗口内的 SM 占用统计短任务会让它反复跳动我刚接触 GPU 服务器时犯过一个错看到批处理任务里 GPU 利用率只有 50%就以为任务写得低效。后来用 profiler 一看其实瓶颈在显存读写SM 闲着等数据利用率自然上不去。这个现象对后端工程师特别有迷惑性因为你把 CPU 上的经验搬过来——CPU 利用率 50% 通常意味着还有一半算力可以压榨——但在 GPU 上利用率低可能是数据喂得太慢你再怎么优化计算逻辑也没用。1.3 异构计算你的程序同时在两个机器上跑CPU 和 GPU 协同工作才是常态。这也是异构计算这个词的来源同一份程序里一部分逻辑跑在 CPUHost上一部分跑在 GPUDevice上。CPU 负责控制流、系统调用、网络 I/O、任务调度这些复杂逻辑GPU 负责计算密集的张量运算、矩阵乘法、大规模并行数据处理。这个模型对后端工程师来说并不难理解它很像一个 Node.js 服务把 CPU 密集任务丢给一个独立的 Worker 集群。主机端发指令、搬运数据设备端做重活干完再传回来。但代价就是数据搬运本身要花钱时间而且这条通道比你想的窄得多。这是下一章的核心话题。2. 内存模型与服务端缓存思维显存和 CPU 内存不是一回事后端工程师对内存模型的理解往往是内存就是一大块数组进程虚拟地址空间一套物理内存一套通过页表映射。这套模型放在 CPU 上基本成立但只要一涉及 GPU你会发现显存虽然听着像显卡上的内存它的物理特性和逻辑层级完全不是同一个用法。不搞清楚这层差异后面看性能分析报告的时候会非常痛苦。2.1 显存的带宽有多夸张一条极宽的公路先说最重要也最容易被忽略的指标——显存带宽。拿 DDR5 内存举例主流服务器单条带宽大致几十 GB/s 这个量级而一块 H100 的 HBM3 显存带宽可以到 3TB/s 以上A100 也有 2TB/s 左右。差了将近两个数量级。为什么需要这么夸张的带宽因为 GPU 有上万个计算单元同时在跑每一个单元都需要源源不断地喂数据。如果显存速度跟不上所有计算单元都只能空转等待。你可以把 GPU 核心想象成一家店里的几百个收银员他们处理一笔交易很快但要是货架到收银台的传送带一次只能送一件商品那收银员再快也白搭。带宽就是那条传送带的宽度。反过来高带宽不等于低延迟。显存的延迟其实比 CPU 访问主存还要高只是 GPU 通过超大规模的并行掩盖了延迟——当一个线程组在等数据时调度器立刻切到另一组线程去算。这个策略对你而言可能有点别扭因为你习惯的优化思路是减少等待、预言缓存GPU 的做法更接近让等待的人足够多多到浪费等待时间也无所谓。2.2 GPU 的三级内存结构从寄存器到共享内存再到全局显存和 CPU 有 L1/L2/L3 缓存一样GPU 的内存也有明确的层级但更关键的是共享内存Shared Memory这一层它和 CPU 的缓存有本质区别寄存器Register每个线程私有访问速度最快容量极小。共享内存Shared Memory一个 Block 内的线程可以共同访问的手动管理缓存延迟远低于全局显存但数据需要显式加载和显式回收。全局显存Global Memory就是nvidia-smi里看到的那个容量也是所有复制进来的数据、模型参数、中间张量存放的地方。访问它要通过 L2 缓存延迟最高。如果你把这套结构映射到后端系统里大概可以这样对应寄存器 ≈ 栈上的局部变量共享内存 ≈ 你可以手工控制的 Redis 缓存但要自己管理过期时间全局显存 ≈ 分布式数据库存储。区别在于CPU 的缓存对程序员是透明的你写普通代码根本不用管 L1/L2但 GPU 的共享内存是显式管理的想让程序跑得快程序员得自己决定哪些数据放在共享内存里复用哪些数据直接从显存读。这也是为什么很多写惯了 Java/Go 的后端工程师初学 CUDA 都会有点不适你在 CPU 上很少需要手动优化缓存命中但在 GPU 上这是家常便饭。哪怕是调用 PyTorch显存带宽和访存模式也会直接影响训练和推理的速度只不过框架帮你把底层优化屏蔽掉了一部分。2.3 PCIe 和 NVLinkGPU 之间以及 GPU 与 CPU 之间的命脉绝大多数后端工程师第一次搞 GPU 集群时都会忽略一个事实——GPU 和 GPU 之间、GPU 和 CPU 之间的通信链路往往是整个系统真正的瓶颈。数据不会凭空出现在显存里。你要把训练数据或者推理输入从内存拷贝到显存这个过程要走 PCIe 总线如果模型太大需要多卡并行卡与卡之间的梯度同步要走 NVLink 或 InfiniBand。典型的 PCIe 5.0 x16 的实际带宽大约在 50~60 GB/s 这个范围看起来不小但和 GPU 内部 2TB/s 的显存带宽一比就是一条乡间小路和高速公路的区别。于是你会看到一个反直觉的现象计算极快的 Kernel可能被一次小小的内存拷贝毁掉。尤其是推理场景里如果每次请求都要把数据从 CPU 内存拷贝到显存再触发计算哪怕 Kernel 只要 0.5ms一次 H2DHost to Device拷贝加 D2H 拷贝可能就要 0.2ms。占比高得惊人。我在给一个推荐系统服务做 GPU 加速时就遇到过这个情况用 GPU 的确把矩阵运算缩短了 80%但因为输入向量小、请求频繁数据拷贝开销几乎抵消了所有收益。最后方案是改用批量请求、常驻显存的池化方式才把端到端延迟真正降下来。所以后面无论你怎么深入学习脑子里永远要留一根弦算得快不算赢数据搬得少才算赢。3. 计算强度才是 GPU 性能的分水岭为什么大模型训练和推理要的不一样一个后端工程师第一次接触 GPU 选型时最常见的操作是拿一张 TFLOPS 指标表哪块卡的算力高就觉得哪块好。这个思路错得不算离谱但粗了。真实项目里GPU 能不能跑满不是一个算力指标能决定的还得看你的计算模式是计算密集还是访存密集——用行话说是 Compute-Bound 还是 Memory-Bound。3.1 一次矩阵乘法就能看明白的算术强度矩阵乘法可能是 GPU 上最常见的计算任务了。一个简单的例子两个 N×N 的矩阵相乘计算量大约是 2N³ 次浮点运算。如果 N 4096那差不多是 1370 亿次浮点运算。要完成这次乘法需要把两个矩阵读进来也就是 2×4096² 3300 万个浮点数的数据量约 134MB按 FP32 算。用这 134MB 访存换取 1370 亿次的计算算术强度大约在 100 以上。含义是每读一个字节可以做 100 次浮点运算。算术强度高的任务是 GPU 最喜欢的因为它可以让计算单元满负荷运行不必频繁等数据。但如果你的业务是讲一句话逐个 token 生成答案情况就完全不同了自回归式的生成任务每一步只计算一个 token 的推理结果矩阵的规模通常很小算术强度急剧下降大量时间花在把模型参数从显存搬到计算单元上。于是你看到的就是 GPU 利用率 30%、显存带宽拉满但算力没吃满的假低效状态。3.2 训练吃算力推理吃带宽基于上面的算术强度概念你可以得出一个后端工程师特别宝贵的判断思路场景主要瓶颈硬件偏好大模型训练大 batch、长序列计算吞吐量Compute-Bound高 TFLOPS、多卡并行、NVLink 高速互联在线推理单请求、自回归显存带宽和请求串行延迟高显存带宽、低延迟、合适的 batch 合并推荐系统/特征工程数据搬运和频繁小矩阵运算带宽与算力均衡注意 PCIe 传输科学计算/分子模拟依赖具体算法常为混合型双精度性能、显存容量、带宽这个表格不会告诉你哪块卡最好但能告诉你什么场景不该只看算力。我自己做 LLM 在线推理服务的时候就曾经困惑为什么一张 4090 在跑一个小模型时生成速度反而不如 A100——后来理解了因为个人级显卡算力虽强但显存带宽和互联能力被定位策略限制喂不饱自回归生成的频繁参数读取。很多后端同事在 GPU 选型上栽跟头都是因为把训练基准直接套到了推理场景上。3.3 一张看懂 GPU 选型参数的对照表为了方便你做最初的判断我整理了一份常见 GPU 的粗对比参数都是公开资料里常见的大致数值具体以官方为准GPU 型号FP16 算力Tensor Core显存带宽显存容量典型定位A100 80G约 312 TFLOPS约 2TB/s80GB HBM2e训练/多卡集群均衡H100约 990 TFLOPS约 3.35TB/s80GB HBM3大模型训练算力极高L40S约 362 TFLOPS约 864GB/s48GB GDDR6推理/图形/中等训练RTX 4090约 330 TFLOPS非官方锁定驱动下参考约 1TB/s24GB GDDR6X个人开发/小模型推理看这张表时我希望你重点观察两列算力和带宽。你会看到 H100 算力是 A100 的三倍多带宽只提升了不到 70%。如果做大规模训练算力提升能直接转成更短的训练时间但做在线推理H100 相对 A100 的收益可能远没有纸面那么夸张因为推理瓶颈在带宽和延迟。这类结论如果你不建立计算强度的心智模型是很难自己想明白的。4. 像排查 CPU 服务一样排查 GPU 问题后端工程师的四个高频事故现场后端工程师最擅长的能力之一就是排障。服务 CPU 飙高、内存泄漏、连接池占满这些套路你都熟。但 GPU 服务的排障思路有它自己的特点很多故障现象长得和 CPU 场景很像实际根因却完全不相干。我把这几年带后端团队踩过的四个典型事故写下来每条都对应一个真实场景能帮你少走不少弯路。4.1 显存泄漏进程死了卡还占着如果你把 GPU 服务当成普通后端服务来发版大概率会遇到一个诡异场景服务进程已经 kill 掉了nvidia-smi里却还显示原进程占用了几个 GB 显存。更进一步如果同一块卡上跑着多个进程某个进程退出后显存不释放新任务就报 CUDA Out Of Memory但你就是查不到是谁占的。这个问题的本质和普通内存泄漏有点像但多了个运行时上下文的概念。每个进程在第一次调用 CUDA API 时CUDA Runtime 会为它创建一个 ContextContext 里包含显存分配信息、模块加载信息等。正常退出时 Context 会销毁显存释放。但如果进程被kill -9、或者某个线程被异常终止而 CUDA 没有机会做清理上下文就会残留。排查手段也不复杂nvidia-smi --query-compute-appspid,used_memory,process_name --formatcsv能看到当前哪些进程占用了显存。如果看到 PID 已经不存在但显存还挂着那就nvidia-smi --id卡号 --gpu-reset注意先把卡上其他稳定任务停了这招会让卡上所有 CUDA 进程失效。另外在代码层面养成用 PyTorch 的torch.cuda.empty_cache()释放显存缓存配合pynvml在服务内周期性监控显存增长可以尽早发现泄漏。我在实际项目中见过最典型的一次一个日志打点线程每处理一条日志就创建一个 CUDA 流从不释放跑了一个月后显存占满了服务 OOM。这个错误放到 CPU 场景里就是线程池只创建不回收所以排查思路完全可以复用只是监控工具从top变成了nvidia-smi和pynvml。4.2 GPU 利用率 100% 但吞吐就是上不去SM 在忙不等于数据在流动这是继显存泄漏之后第二个高频误区。现象非常明确nvidia-smi显示 GPU 利用率接近 100%按理说算力应该已经吃满了但服务的 QPS 和延迟数据远低于预期。很多后端同事会怀疑业务侧逻辑太慢查来查去最后发现 GPU 其实在无效忙碌。前面提过GPU 利用率统计的是 SM 上有没有 warp 在执行指令。如果程序正在反复从全局显存读取数据每条指令需要等待数千个时钟周期的访存返回SM 看起来依然有 warp 占据但实际上计算单元大部分时间都在原地等数据。这种场景下nvidia-smi的利用率数字会很高但性能极其拉胯。要想定位单靠nvidia-smi不够必须上性能分析工具。NVIDIA 的 Nsight Compute命令行是ncu可以提供准确的 SM 忙碌占比、Memory Throughput、以及各种 stall 原因。我排查过一次自研推理引擎的问题用ncu一看发现Memory Throughput超过 90%明显访存受限。这时候我压根不需要去逐行优化 Kernel而是先去修改数据布局、减少中间张量拷贝、加大 batch 批量复用模型参数三个改动下来吞吐翻了一倍多。后端排障里常用的是 CPU 瓶颈还是 IO 瓶颈的思路在 GPU 上完全适用只是把 CPU 换成 SM把 IO 换成显存带宽。4.3 MIG 与多租户切分GPU 也有虚拟机的概念后端工程师对隔离都很敏感。服务之间不能互相抢占内存和 CPU 配额要有上限。但在 GPU 上通常一张卡会被多个任务共享如果纯靠时间片调度A 任务的峰值负载就可能拉垮 B 任务的延迟。这时候 NVIDIA 的 MIGMulti-Instance GPU技术就派上用场了。MIG 可以把一块物理 GPU 切分成多个逻辑独立的 GPU 实例每个实例拥有自己专属的显存切片、SM 子集、内存带宽配额互不干扰。它的隔离性比软件层的时间片调度强得多非常像 CPU 场景里的虚拟机 vs 多进程——MIG 就是 GPU 时代的 KVM时间片调度就像单纯跑一堆容器但不加 cgroup 限制隔离性差。用nvidia-smi配置 MIG 其实不复杂但要先通过nvidia-smi -mig-mode 1开启 MIG 模式然后用nvidia-smi mig -cgi创建实例再把实例分配给容器或进程。我第一次给团队搭 MIG 环境时踩过一个坑MIG 创建后容器里必须配置对应的NVIDIA_VISIBLE_DEVICES环境变量否则容器里看不到 MIG 实例GPU 资源进不来。这点和给容器透传显卡、需要在docker run里加--gpus是同一个套路但 MIG 的粒度要更细一层。多租户场景里我建议给每个服务实例固定 MIG 实例而不是靠默认调度器随机分卡。否则服务间会出现一损俱损的连锁问题一个任务在跑训练把显存带宽吃满旁边推理服务的 token 生成速度瞬间掉一半。用 MIG 做资源隔离后高峰期延时的稳定性提升了一个量级运维那边的告警也清净多了。4.4 CUDA 版本地狱比 JDK8 和 JDK17 的冲突更隐蔽后端工程师对 JDK 版本、Go 版本、Node 版本的兼容性问题应该都深有体会。CUDA 生态的版本地狱有过之而无不及因为它有三层概念很多人一开始只看到两个界面nvcc --version和nvidia-smi里的 CUDA Version这两者经常不一致导致不少人误判环境。简单拆解一下CUDA Driver驱动是驻留在操作系统内核态的底层组件它决定你的 GPU 最高能支持到哪个 CUDA 运行时版本CUDA Toolkit 是用户态的开发库和编译器nvcc是它的编译器前端CUDA Runtimecudart则是程序运行时链接的库文件。nvidia-smi里显示的 CUDA Version 通常是 Driver 支持的最高版本而nvcc --version显示的是你当前安装的 Toolkit 版本。只要 Toolkit 版本 Driver 支持的版本通常就能正常工作但如果驱动太老而 Toolkit 太新程序一跑就报CUDA driver version is insufficient。这种问题在 Docker 容器里尤其常见宿主机驱动是新的但容器镜像里带的是旧 CUDA Toolkit或者反过来宿主机驱动太老、容器里新框架需要高版本 CUDA。排查思路和排查依赖冲突一样先确定宿主机驱动版本和支持的上限再确认容器内python -c import torch; print(torch.version.cuda)对应的 CUDA Runtime 版本最后确认路径上有没有多个 CUDA 版本互相干扰。用环境变量CUDA_HOME、LD_LIBRARY_PATH锁定版本跟你在 Java 里管理 JAVA_HOME 其实是同一个方法论。5. 不写一行 CUDA也能让 GPU 跑得更好后端工程师的三条实用路径听到这里你可能会觉得理解是理解了但总不会让我去写 CUDA Kernel 吧当然不需要。后端工程师在 GPU 生态里最大价值不是写出最快的 Kernel而是把 GPU 当作一个分布式计算资源管好、用好、优化好。我用过且验证有效的路径有这么几条按上手成本从低到高排。5.1 路径一站在推理框架的肩膀上用 TensorRT 和 OnnxRuntime大多数 GPU 加速场景你并不需要直接操作 GPU。现成的推理框架已经把底层 Kernel 优化得很好了。比如 NVIDIA 的 TensorRT可以对 ONNX 模型做层融合、精度校准、内存复用在很多场景下能比原始的 PyTorch 推理快几倍。OnnxRuntime 的 CUDA Execution Provider 也是类似思路部署起来更简单适合快速验证。我印象最深的一次给一个自然语言处理服务加速原始模型在 PyTorch 上用 GPU 推理P99 延迟 40ms。导出 ONNX 再走 OnnxRuntime没做任何算子级优化延迟直接降到 18ms。后来又改用 TensorRT进一步优化 batch 和显存复用压到了 10ms 以下。很多后端同事会觉得框架调优是算法工程师的事但实际经验告诉我作为一个后端工程师你只要能熟练完成 PyTorch 导出 ONNX → 用 TensorRT 重新部署 → 对比性能 这条链路就已经能解决大量与 GPU 相关的性能问题。这条路径的重点不在 CUDA 编程而在工程化你要理解模型转换带来的算子变化、动态 shape 的处理、显存碎片的控制。这些东西本质上都是你熟悉的部署和性能测试工作只不过对象从服务代码变成了模型。5.2 路径二用 PyTorch 层面优化而不是直接操作 CUDA如果你想更进一步也没必要马上跳到 CUDA。PyTorch 提供了大量高层 API能让你在 Python 环境里就实现可观的 GPU 性能优化把多个小张量操作合并成批次操作减少 Kernel 启动次数。Kernel 启动是有开销的后端工程师可以把它理解为一次 RPC 调用开销调用多了性能自然下降。设置torch.backends.cudnn.benchmark True让 cuDNN 自动挑选最优卷积/矩阵算法这对固定 shape 的模型非常有效。用torch.compile或torch.cuda.graphs捕获 CUDA Graph把一连串 Kernel 启动变成一次图执行大幅降低 CPU 侧调度开销。这套思路最吸引人的地方是它不需要你懂 GPU 微架构只需要你有减少开销的后端工程直觉然后找到对应的 API。我之前在优化一个多路召回模型时只是把原先逐条处理用户的循环改成了用 padding 打包成 batchGPU 利用率直接从 35% 到了 70% 多。这个优化放在你的后端服务里等价于把 N1 次查询合并成一次批量查询——你早就会了只是不知道 GPU 上也要这么做。5.3 给 GPU 服务做容量规划一套后端工程师的实用方法论最后说说 GPU 资源容量规划。这块后端工程师特别容易忽略因为习惯了 CPU 服务可以靠水平扩容堆机器但 GPU 服务器成本高而且单卡资源有限规划不好会浪费大量预算。我的建议是算清楚三笔账。第一笔是模型的显存底价一个 7B 参数模型FP16 权重大约需要 14GB 显存再加上 KV Cache、激活值和 CUDA Context实际上你需要按 20-24GB 去预留。第二笔是并发和吞吐推理服务的 token 生成吞吐往往受显存带宽限制单卡能同时服务的并发数有限该并发量和模型大小成反比。第三笔是扩展策略是小 Batch 低延迟还是大 Batch 高吞吐方向不同对卡的选择完全不同。你不需要一开始就做得很精细但至少要有容量规划这个概念。我见过一个团队为了图省事把几十个小模型全塞在同一张卡上结果互相争抢显存带宽整体吞吐比分开部署还低。后来按模型调用频率和显存占用重新分了组同样的卡数P99 延迟降了 40%。这在 CPU 后端里就是服务混部问题你完全可以用已有的架构思维来解决 GPU 资源调度。我个人的看法是后端工程师学 GPU不用从 CUDA 汇编入门更不需要背死板的理论。真正有价值的是先搞清楚三个问题——GPU 硬件把资源花在了哪里、计算强度和访存强度哪个才是你业务的瓶颈、以及 GPU 服务应该怎么观察和隔离。把这套硬件心智模型装进脑子里后面不管你是用 PyTorch 调模型、还是用 TensorRT 加速推理方向都不会跑偏。如果你正带着团队做 GPU 相关项目我建议你干的第一件事不是囤卡而是打开你的 Nsight 或至少认真读一次nvidia-smi的关键指标把 SM 占用率、显存带宽、PCIe 拷贝这三组数字解读明白。能做到这一步你就已经比大多数从零开始的入门者领先了。
返回列表