1. 后端视角下的 GPU为什么这块卡值得你搞明白1.1 一切从一次“翻车”开始先讲一个我自己的例子。去年年底我们团队接到一个把图像分类服务从 CPU 迁移到 GPU 的任务。当时我作为纯后端工程师已经写了七八年 Java 和 Go对进程、线程、内存模型这些东西自认为很熟。迁移方案也很“标准”把模型加载好用框架提供的接口做推理再走一遍现有的 RPC 链路。结果上线之后服务器倒是没崩但延迟比 CPU 版本还差。监控面板上 GPU 利用率只有 3%可一个推理请求要跑 120 毫秒。查了半天最后发现根源特别低级我按照后端调用数据库的习惯每条请求单独发起一次模型推理batch size 永远是 1。这样 GPU 大部分时间都在“空转”就等一个样本的数据从内存搬到显存、再从显存搬回内存。真正执行计算的周期可能只有几毫秒剩下的全部时间都耗在了搬运和等待上。这个经历让我意识到一个很扎心的事实后端工程师那套“多线程 缓存 连接池”的心智模型放在 GPU 上不仅不好用还会误导人。GPU 的思维方式完全不同它不是“更多核的 CPU”而是另一类计算设备。今天这篇系列第 2 篇就想把这块硬件——更准确地说把一块 GPU 内部到底有什么、数据怎么流动、延迟去哪了——讲给同样从后端转过来的朋友听。1.2 GPU 到底是怎么“并行”的理解 GPU从一句话开始CPU 是“一个人干很多不同的活”GPU 是“一堆人同时干同一件简单的活”。这句话听着简单但展开说很有分量。CPU 的物理结构里一个核有一整套复杂的调度和分支预测硬件单个核处理逻辑上互相依赖的任务速度非常快。比如一个 HTTP 请求进来要解析协议头、查路由、调服务、拼接响应这些步骤前后有依赖关系CPU 的思路是“我把每一步都跑得飞快用高主频和流水线把你的等待时间压下去”。GPU 不一样。GPU 最有价值的工作是“对大量数据执行同一种规则”比如图像里几百万个像素点做同样的滤镜神经网络里几千个向量做同样的矩阵乘法。这种工作不需要复杂的判断和跳转只需要把计算单元铺得足够多。所以 GPU 上所谓的“核”和 CPU 里的核完全是两个概念GPU 的单个计算单元极其精简没有复杂的分支预测、没有乱序执行它的强项是数量。你可以把硬件整体看成一座“工厂车间”CPU 是几个全能的老师傅什么活都能接干得快GPU 是上百个只会一招的实习生单独看笨手笨脚但一旦任务能拆成几百份同样的小活效率立刻反超。你还需要知道一个词SIMT单指令多线程。翻译成人话就是“同一个指令很多线程一起执行”。显卡内部会把很多个线程打包成一组组内所有线程必须跟着同一个指令走。只要其中一个线程因为数据不对、条件不满足而走岔路整组人就得等它。这个特性直接决定了代码里一旦出现大量 if/else 的分叉逻辑GPU 的优势就会快速缩水。后端工程师习惯写的复杂业务逻辑在 GPU 上简直就是灾难适合 GPU 的永远是结构整齐、数据密集、规则单一的计算。2. GPU 硬件心智模型从芯片到系统2.1 一张显卡拆开看算力块、显存块、传输通道想建立硬件心智模型别急着扯什么深度学习框架、CUDA 编程先把一张显卡拆成三个大块第一块是计算单元。GPU 整颗芯片由许多称为 SM流多处理器的计算块组成每块里又有几十上百个更小的处理器常被称为“CUDA 核心”。厂商宣传的“几千个核心”说的就是这些小家伙。它们擅长执行最简单的整数和浮点运算最大的特点是“多”。我在文章里管这层叫“算力块”。第二块是显存也就是显卡上自带的存储空间。注意显存和要讲的内存是两回事。显存靠近计算单元专供显卡读写带宽极高单位是 GB/s 甚至超过 TB/s 这个量级。大模型训练、大数据吞吐拼的就是显存大小和带宽。显存不够模型装不下什么优化都是空中楼阁。第三块是芯片之间的传输通道典型的是 PCIe 总线。CPU 和 GPU 之间的大块数据交换都得走这条通道它负责把硬盘里的图片、数据库里的内容搬到显存或者把 GPU 算完的结果搬回内存。PCIe 的带宽比显存速度低一两个数量级就好比工厂车间跟外部仓库之间是一条窄路进出货物特别慢。这三块东西在产品设计中互相博弈算力越大发热越高电路越复杂显存越宽成本越高传输越宽位宽和时钟越难平衡。你看到的每一款显卡本质上都是在这三块之间做取舍的结果。2.2 计算单元进一步看标量核与张量核后端工程师对“CPU 核心数”非常敏感看到 GPU 几千个核心时容易陷入一个误区以为这 3000 个核心和那 2000 个核心之间只是“量的差距”。实际上计算单元里还分专精方向其中最关键的两个是标量核和张量核。标量核处理单个数的运算。所谓“标量”就是一次只处理一个数比如一个浮点数。这类核和 CPU 的 ALU 有点像只是更简单、更密集。适合处理图像像素点、物理仿真这类每个数据点都要执行同样轻量计算的任务。张量核是专门为深度学习设计的“矩阵加速”单元。神经网络计算的核心本质是矩阵乘法比如 Y W·X其中 W 是权重X 是输入。矩阵乘法看起来只是大量的乘加操作但如果能让多个数一次性乘完再加效率就会大幅提升。张量核干的就是这件事——它能在一条指令里完成对一批小矩阵的乘加运算速度比标量核跑同样代码快一个数量级以上。这个设计类似生活里的“打包”单核处理就像一件件扛行李箱上楼张量核则是先把一层楼的箱子装进电梯一次到位。一个关键点张量核不是所有 GPU 都有也不是所有运算都适合它。你说我做个 Redis 客户端缓存用 GPU 显然毫无意义但如果你面对的是推荐系统里的向量召回、大模型推理、图像检索那就要认真考虑算力块里到底有没有张量核。2.3 显存层级决定数据访问速度的隐形骨架很多后端工程师对内存分配的态度很随意因为 CPU 缓存和多级存储早就替我们把大部分优化做了。但 GPU 的存储层级需要你亲自关心否则写出来的推理服务性能会差得离谱。从最快到最慢排一下寄存器、共享内存、L1/L2 缓存、显存、内存。寄存器紧贴计算单元速度极快但容量极小每个线程私有一份共享内存是同一个计算块内线程共享的快速存储相当于一个团队内部的小黑板大家可以在上面交换信息L1/L2 缓存层次和 CPU 差不多靠时间局部性和空间局部性做加速往下走就是显存容量大但访问速度开始明显下降最慢的是内存和磁盘那已经是另一台机器和另一个世界。这里有一个后端工程师特别容易忽略的细节GPU 计算快不代表你来回搬运数据也快。很多人在第一次写 GPU 推理时习惯把模型参数放过一次然后把每个请求的输入数据也搬到显存里算完再搬回来。他们用 GPU 执行计算只花了 2 毫秒但搬数据花 20 毫秒——这还不如在 CPU 上老老实实跑。最理想的状态是数据能留显存就留显存把搬运次数压到最低。这就是为什么推荐系统通常会把整个召回索引直接加载进显存而不是每次查询都从内存往显存拷。数据局部性这条原则在 GPU 环境里被放大了十倍。CPU 上你也许可以容忍偶尔一次缓存未命中但在 GPU 上一次全局内存访问没踩中缓存等待时间就能让整组线程卡住这时候只有让旁边的线程继续做其他计算才能遮掩延迟。这也是编写 GPU 代码时一个非常核心的优化思想用并行度掩盖延迟。3. 从后端到 GPU 的执行模型延迟 vs 吞吐量3.1 一次推理调用的完整时间线我们试着画一次 GPU 推理请求的时间线。假设客户端发来一张图片后端服务收到之后调用一个推理框架的接口最终拿到分类结果。第一步数据序列化。图片要么是 base64 字符串要么是二进制字节数组后端先要把它解析成一个可运算的数组。第二步内存拷贝。把解析后的数组从 CPU 内存拷贝到显存。这需要调用一个类似 device copy 的接口数据穿过 PCIe 总线送到显卡。这一步看起来只是“赋值”实际代价很高因为每次拷贝都有固定启动开销数据量越大耗时长。第三步内核启动。后端程序告诉 GPU“喂有一批线程去执行这个算子。”GPU 收到指令后开始分配线程块、加载权重、读取输入数据。这里有一个隐藏成本内核启动不是瞬间完成的它本身也需要几十微秒甚至上百微秒。第四步实际计算。各种标量核、张量核真正开始在显存里干活。通常这一步时间很短尤其是单条样本的推理可能就几千微秒。第五步结果回传。计算完了把显存里的结果拷回内存再序列化成响应返回。整条链路里后端工程师最容易高估第四步低估第二步和第三步。你说“GPU 算一个分类只要 1 毫秒”但整个 API 延迟可能是 40 毫秒那 39 毫秒去哪儿了全在路上。我把这一步起个名字叫“传输边界开销”。排查 GPU 服务性能问题时第一件事永远是问时间到底花在搬运上还是计算上千万别只看计算那一截。3.2 为什么批处理是后端工具的“咒语”我前边说翻车案例里 batch size 1当时觉得是参数没调好。后来深入理解 GPU 执行模型才明白那不是参数问题是思维模式错误。从 CPU 的视角一条请求过来能立刻开一条线程去处理快刀斩乱麻延迟最优。但 GPU 不一样——它的设计目标不是“让某一条请求尽快算完”而是“让整块芯片在单位时间内算最多的数据”。请记住这个区别CPU 追求低延迟GPU 追求高吞吐。GPU 的每个 SM 都是为同时处理非常多线程设计的。如果一批数据只有一个样本芯片上的大部分计算单元就闲着。即便你把计算单元都唤醒了单个样本的串行依赖也会让张量核很难完全吃饱。而如果你把 32 个、64 个请求攒在一起统一成为一个大的张量操作整个矩阵乘法的形状变大了张量核才能满负荷运转。你可以把批处理想象成拼车一辆核载 64 人的大巴一趟拉完比你开 64 辆私家车来回进退要划算得多。但这个拼车优势有边界。批大小并不是越大约好一方面显存是有限的一批数据装不下另一方面单个请求在批里的完成时间会受最慢的一条拖累延迟会往上走。实际项目中我们一般先做压测画出一条“批大小 vs 吞吐量”和“批大小 vs p95 延迟”的曲线取交叉区域附近的值。常用做法是优先关注 p95 而不是平均延迟因为尾部请求往往决定了用户体验。批处理是后端做高吞吐系统时本来就会用的招比如 Kafka 消费可以攒一批再处理数据库批量插入也快得多。GPU 把这一原则推向极致没有批GPU 几乎等于白买有了批你甚至可以用中低端显卡干出高端卡的效果。3.3 被忽略的卡间传输代价聊完批处理还得聊一个非常现实的问题数据在 CPU 和 GPU 之间走的通道就那么宽你就算在代码层面把批处理做到完美硬件层面一次搬运的延迟还是躲不开。举个例子假设 PCIe 通道的可用带宽是 24 GB/s实际受代际、通道数、拓扑结构影响很大不同设备差别很多此处只是量级参考你要把一个大小 1 GB 的参数矩阵从内存搬到显存理想情况需要大约 40 毫秒。如果你选择模型加载时搬一次后面所有请求都用这个参数矩阵那这个成本平摊下来可以接受但如果你每条请求都更新参数等于每秒搬几十次光搬运就占满整个通道。多显卡场景里还有一个更隐蔽的坑卡与卡之间通信。有些方案是多卡并行每张卡持有模型的一部分中间结果需要频繁交换。卡间互连如果走 PCIe延迟和带宽会比卡内显存差得多如果换用专门的卡间互连技术价格立刻上一个台阶。后端工程师搭分布式系统时对跨机房网络延迟有直觉——大约几十毫秒但 GPU 卡间通信的延迟是微秒级甚至更低只是你依然得把它当成“网络问题”来设计否则多卡性能会直线下降。所以我在团队里立了一条规矩凡是涉及 GPU 的改动先看数据流向和搬运次数再看计算量。计算优化做得好不如搬运次数少这就是硬件心智模型的核心之一。4. 实操选型与资源估算给后端团队的量级感4.1 显存需求估算公式后端工程师自己大概率不会写模型但你要负责搭建服务、购买算力、评估成本。这就要会估算“一个模型需要多少显存”不然别人说需要 4 张卡你就买 4 张预算超了你都不知道钱去哪了。推理和训练要分开算。推理时显存主要装模型参数、KV 缓存如果是大模型和少量临时张量。模型参数占用可以粗略这样算参数数量 × 每个参数占用的字节数。比如一个 7B 参数的模型用 16 位浮点存储每个参数 2 字节那么光模型权重就要大概 14 GB。加上推理过程中的激活值、中间张量以及运行时的固有余量实际你至少会看到 16 GB 到 20 GB 的显存占用。训练时则要再加。一是训练要保存梯度权重反传时要计算梯度这部分显存和权重一样大二是如果用 Adam 这类优化器还需要保存两倍于权重大小的动量变量。所以同样一个 7B 模型初始阶段训练可能要吃下 4 到 6 倍于权重显存。这里我给一个粗略公式大家有个量级概念就好训练显存占用 ≈ 参数数量 × 字节数 × 系数常见在 4 到 6 之间。这个系数会随框架、优化器、混合精度策略变化不精确但足够给你一个心理锚点。量化技术会显著降低这些数字。把模型从 16 位浮点压到 8 位整数权重占用的显存直接减半。代价是精度可能会受影响需要根据业务对精度的容忍度来权衡。后端同学不需要自己动手做量化但看懂这些数字对应的成本跟供应商谈算力采购时心里能有底。4.2 不同工作负载的选型参考后端团队常用的 GPU 负载大致分三类我给他们做了个对照表。这个表是量级和倾向性参考绝对数值会随硬件迭代变化重点看思路。负载类型核心诉求最关心的硬件参数典型策略在线推理低延迟、稳定吞吐显存带宽、PCIe 链路、定长批处理能力偏中小规格卡多张并行分摊不太需要卡间高速互连离线训练高吞吐、模型迭代快总显存、算力核心、多卡通信效率偏大规格卡通常配套高速卡间互连方案因为梯度交换频繁向量检索/特征计算高并发、结果召回显存容量、带宽、批量计算幂先考虑数据能否全进显存再考虑算力很多后端团队刚接触 GPU 时总想说“买最好的那张卡”。但从成本角度看在线推理买一张超大规格的卡往往是浪费。推理模型通常已经训练完成参数固定推理请求是许多条独立的小计算天然适合把小请求攒成批。如果你把模型拆到多张小卡上再配合良好的批处理既能降低单卡故障影响也能让预算花得更划算。反过来离线训练场景里多个训练任务会在不同阶段同时开始、结束卡与卡之间经常要同步梯度。这时候显卡之间的通信带宽太重要了选型时要把“卡间互连”当成第一优先级别只看单张卡算力。4.3 从容器到集群GPU 调度和运维的新姿势后端工程师的日常是和容器、编排系统打交道。GPU 引入之后运维层面也会多出不少新约束。首先是驱动和运行库。GPU 的使用依赖底层驱动以及一套运行库容器镜像里也要对应安装。不同版本之间一旦不匹配最常见的报错就是“找不到设备”或“初始化失败”。建议团队尽早把包含驱动的镜像固化而不是每次部署现场装。其次是调度。Kubernetes 这类编排系统默认情况下不认识 GPU需要你配置扩展资源。调度器需要知道某个 Pod 要多少张显卡按资源名去匹配节点。由于 GPU 设备在物理上不能像 CPU 那样随意超卖节点机选型时要留足余量不然一个负载高的 Pod 会把整个节点的 GPU 都占满其他 Pod 全部排队。再次是 GPU 共享。很多场景下一个推理函数只用了一张显卡很少一部分算力。你可以在节点层面做显存隔离和时间片共享让多个任务共同使用一张卡。但共享模式容易引入波动不适合对延迟敏感的业务。我的经验是先把一批任务分成“延迟敏感”和“吞吐优先”两类前者独占资源后者再考虑共享。5. 常见误解与排查经验5.1 把 GPU 当 CPU 调优后端工程师最容易犯的错是拿 CPU 的监控思路去理解 GPU。我们看 CPU 利用率看到 90% 会觉得系统很忙降到 10% 会觉得资源没用满。但在 GPU 的世界里“利用率”这个指标极容易骗人。我见过一个团队把 GPU 利用率监控得漂漂亮亮曲线拉满到 99%结果服务延迟照样高挑不出毛病。后来细看才发现他们的算子里大量使用了一个高开销的函数这个函数让 GPU 的核心几乎一直在“等待内存数据”计算单元看似没闲着但实际上没有产出有效结果。GPU 利用率高只能说明计算单元没有彻底睡觉绝不代表它在做有用功。你得把“计算时间”和“资源利用率”分开看利用率高可能是真忙也可能是在空转等数据。排查时不要只盯单卡利用率还要关注内存带宽利用率、队列长度、内核启动频率。我会先开监控面板观察显存使用量和卡上活动线程数的变化如果显存带宽接近瓶颈而利用率不是 100%基本可以判断是数据搬运和访存模式的问题而不是算力不够。5.2 只盯着算力忽略显存带宽另一个常见误解是“这张卡算力强那它跑什么都快”。实际上很多业务负载根本不是算力瓶颈而是显存带宽瓶颈。大模型推理就是典型例子。现在流行的自回归生成每一步只需要生成一个 token计算量听起来不大但每一步都要读取完整的模型权重不断从显存里捞一遍参数。如果你的模型权重是 14 GB每一步生成都至少要把这 14 GB 数据从显存取出来跟当前状态算一遍显存带宽就成了绝对瓶颈。这种情况下算力再高的显卡也救不了真正有效的是换一张显存带宽更高的卡或者把权重压缩后加载。一个更形象的类比算力好比一辆跑车发动机的功率显存带宽是油箱到发动机的油管粗细。功率再大油管子细油门踩到底也跑不出速度。后端工程师在评估模型服务性能时一定要分清这个瓶颈是在“缸里”还是“管道上”。5.3 序列化调用背后的隐藏开销后端服务通常是按请求串行处理的一个请求进来处理完再出去。到了 GPU 环境如果还用这种模式性能会非常难看。我遇到过一个向量检索服务逻辑上看已经很优化了模型加载到显存查询向量也拷贝过去GPU 一次算一批结果。可上线后发现吞吐量远低于预期。排查时发现问题出在请求处理流程里有一连串小的 GPU 推理调用每个都单独同步等待结果。每次调用都有内核启动和传输开销哪怕每次只算一点几十次串行下来延迟就甩出去了。解决思路不是“把每次调用调快”而是想办法把多个调用合并成一次批量调用。可以引入一个轻量级的队列请求先进入一个缓冲攒够一定条数或者每隔几毫秒触发一次一次性把整批结果算完返回。这和数据库的 bulk insert 其实是同一个道理。后端工程师要用“攒一批跑一批”的思路替代“来一条跑一条”的思路GPU 才能真正吃透数据。6. 一点个人经验先建立量级感再谈调优如果非要把这篇内容浓缩成一句话我会对团队说你不需要先学会写 GPU 代码但你先要有 GPU 量级感。什么是量级感就是知道一次内存拷贝、一次内核启动大概多少微秒知道模型权重占多少显存知道同一个模型在不同规格的卡上跑显存带宽差异可能比算力差异更影响结果。有了量级感之后你会主动回避那些“把几十毫秒的搬运重复成百上千次”的设计也会在别人告诉你“显存不够加钱买大卡”的时候先算清楚到底是因为模型装不下还是因为代码反复搬数据。我自己就是在踩过几次坑之后才养成一个习惯任何 GPU 相关项目第一周不急着写代码先画一幅“数据流图”把哪些数据在 CPU 上处理、哪些数据搬去 GPU、搬运多久一次、中间有没有等待全标清楚。这幅图画完性能和成本基本已经能估算个七七八八。最后分享一个实用小技巧压测 GPU 推理服务时别只看平均延迟和平均值多注意 p95 和 p99同时记录“每次请求在 GPU 卡上待了多久”。如果 p99 明显高于 p95大概率是某个请求拖累了整批处理这时候就得调整批处理策略或限流阈值了。硬件心智模型说白了就是三句话数据搬运少一点批处理大一点瓶颈分清楚在通道还是计算。这三句话想明白了你再去看各类 GPU 架构和优化方案都是顺理成章的事。
阅读完成 · 觉得有帮助?