ARTICLE DETAIL

资讯详情

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

算力过剩但推理慢?大模型硬件调度才是关键

算力过剩但推理慢?大模型硬件调度才是关键 我经常在群里看到这种场面有人晒出 8 卡 A100 的监控截图显存占用不到一半算力利用率只有百分之十几然后配文“跑个 7B 模型推理速度还是慢得离谱”。下面一群人讨论换卡、加节点、上更好的推理框架但很少有人问一句算力明明过剩了为什么推理还是跑不快这个问题我琢磨了很久也和不少做部署的朋友聊过。今天想把自己的思考整理一下重点聊聊大模型推理场景里“硬件调度”这件事。先说明我不是学术派讲的都是自己实际部署和调优过程中踩过的坑、验证过的方法。如果你也在做大模型部署、推理服务优化或者正准备买卡搭集群这篇文章应该能帮你少走一些弯路。我要表达的核心观点很简单在大模型推理场景下算力从来不是唯一的稀缺资源甚至很多时候根本不是最稀缺的资源。真正决定服务快慢的往往是调度策略——怎么把显存、带宽、算力、请求队列这些资源匹配好才是关键所在。1. 先拆问题算力“过剩”的到底是什么1.1 账面算力不等于有效算力要聊清楚这个问题先得理解“算力过剩”在说什么。以 A100 为例FP16 算力高达 312 TFLOPS一张卡的理论算力非常恐怖。但实际跑 Llama 7B 的推理服务时GPU 利用率能到 30% 就算不错了绝大多数时间都在 5%~20% 之间徘徊。为什么因为大模型推理不是一个“算得越猛就越快”的任务。它既吃算力也吃显存带宽还吃显存容量。模型权重、KV cache、中间激活、CUDA context 这些都要占显存而每次生成一个 token 都要把整个模型的权重从显存里读一遍。我做一个粗略的估算FP16 格式的 7B 模型权重大约是 14GB。即使按 2TB/s 的显存带宽算A100 的水平完整读一遍权重就需要大约 7 毫秒。这还只是读一次权重的时间实际 decode 阶段还需要读写 KV cache还有各种算子开销。换句话说即使算力再高显存带宽已经锁死了单个请求的生成速度上限。所以这里说的“算力过剩”其实是账面上的 FLOPs 远大于实际能消费掉的 FLOPs。推理服务跑不快不是不缺算力而是算力之外的约束条件先把路堵死了。1.2 推理慢到底慢在哪个环节很多人在排查推理性能问题时习惯性地先看 GPU 利用率。如果利用率不高就觉得是“代码写得不行”或者“框架不够好”。但实际上推理链路里有四个环节都可能成为瓶颈单纯看利用率很难定位。第一个环节是请求排队。服务端收到的请求多了如果调度器来不及分配资源请求就只能在队列里等着。我见过不少部署GPU 明明很闲但服务端因为配置了过大的 max_num_seqs请求全部堆在进程里排队整体响应时间惨不忍睹。第二个环节是模型计算过程。prefill 阶段处理输入 prompt是计算密集型的需要做大量的矩阵乘法这个阶段确实吃算力但 decode 阶段逐个生成 token是访存密集型的绝大多数时间都花在从显存读权重和 KV cache 上。第三个环节是设备间通信。如果模型切分到了多张卡张量并行会引入通信开销。卡与卡之间走 NVLink 还好一旦跨节点走 RDMA 网络通信耗时可能比计算耗时还高。第四个环节是显存不足导致的换入换出。显存不够时框架可能会把 KV cache 或者在 CPU 内存和显存之间搬数据这个开销极高一旦触发性能立刻断崖式下跌。平时大家笼统说的“推理卡”其实是这四个环节叠加的结果。其中任何一个环节处理不好算力再高也白搭。1.3 一个容易被忽视的“二八现象”还有一个更隐蔽的问题就是请求本身的“长短不均”。在真实业务里用户输入和输出的长度差异极大。有的请求 prompt 很短、回答也很短有的请求则要处理几万字的长文档。这个过程里会出现一个典型的“二八现象”少部分长请求会消耗掉绝大部分显存和算力而短请求虽然数量多占用的资源比例却很小。如果调度器没有区分长短请求让短请求和长请求混在一个 batch 里那么一次迭代的耗时就被最长的那个请求拖住了所有人都得等它算完。这种场景下算力不是不够而是被极端请求“绑架”了。这也是为什么现在调度器要专门做动态批处理、prefill/decode 分离之类的优化目的就是把长短请求分开放别让它们互相拖累。2. 搞懂推理特征才能理解调度为什么难2.1 prefill 和 decode同一模型两个世界要理解大模型硬件调度必须先把推理过程拆开看。大模型生成回答的过程不是一口气算完的它分两个交替的阶段。prefill 阶段是“你的请求”到达服务端之后先把整个 prompt 拿去并行计算一次性算出每个位置上的 KV cache 和第一个输出 token。这个阶段计算量很大因为要同时处理几千个 token 的注意力计算但又因为可以充分并行所以算力利用率可以做得很高。decode 阶段则完全不同。模型一次只生成一个 token然后把这个 token 追加到序列后面再计算下一个 token。这个过程没法像 prefill 那样大规模并行因为下一个 token 依赖前一个 token 的结果。更要命的是每生成一个 token 都要把模型权重全部读一遍所以 decode 的速度基本由显存带宽决定。我做一个对比prefill 阶段的算力利用率可以轻松跑到 70% 以上而 decode 阶段往往连 20% 都不到。如果用生活中的例子来类比prefill 就像是批发搬运一次能扛很多箱货decode 则像单件配送跑得再快也得一家一家送。很多人在调优时没有区分这两个阶段给出的配置往往顾此失彼。比如拼命加大 batch size 想提高吞吐结果 response 的首字延迟飙升又比如为了压低延迟把 batch 设得很小结果 prefill 的计算资源又被浪费了。调度在这方面做得好坏直接影响服务的整体表现。2.2 显存不只是“放模型”这么简单接着说说显存。很多人都知道模型权重会占显存但推理场景下KV cache 的占用往往才是最大的变量。KV cache 是 Transformer 在生成过程中缓存的历史 attention 信息用来避免每次生成时重算前面的内容。它的大小和模型结构、batch size、序列长度直接相关。我以 7B 模型为例给个估算公式KV cache 字节数 2key 和 value × 层数 × 头的数量 × 每个头的维度 × 序列长度 × batch size × 每个元素的字节数。7B 模型一般是 32 层、32 个 head、每头维度 128。如果序列长度是 2048、batch size 是 8那么 KV cache 的大小就是 2 × 32 × 32 × 128 × 2048 × 8 × 2FP16算下来是 4GB。如果序列长度涨到 8192batch 也涨到 16KV cache 就变成了 32GB直接把显存吃光都有可能。所以调度器的核心任务之一就是管理好这块动态变化的内存池。谁分配多少显存给权重多少给 KV cachebatch 最大能容纳多少请求都需要动态调节。传统那种“跑模型前先把显存一次性占满”的思路在大模型推理场景根本行不通因为 KV cache 的大小是随着请求实时变化的。2.3 请求到达是随机的静态策略必然吃亏本地部署一两个模型可能体会不到调度的压力。但线上推理服务面对的是随机到达的请求流这个“随机性”才是硬件调度最难的地方。想象一下某个瞬间服务端同时收到 20 个请求其中有两个是超长文档剩下的是普通聊天。如果没有精细的调度这批请求就会被一股脑塞进 batch。但它们的长度差异太大导致整个 batch 的计算时间被最长的请求拉得很长。长请求还没算完新的请求又到了系统窗口期大量空转算力被白白浪费。这也是为什么早期静态批处理static batching在大模型推理里那么被动。静态批处理要求同一批请求同时开始、同时结束任何一个请求拖后腿整批都得等着。而现在的动态批处理、连续批处理continuous batching在实现上已经完全改变了思路不把一个 batch 当作整体而是逐个 token 粒度去调度。当 batch 里某个请求已经生成完一个 token新到的请求可以立刻插进来和大家一起继续下一步计算。这种机制的本质是在多个时间片上做“拼车”谁准备好了就上车谁到站了就下车。它靠的是实时调度而不是预先规划。这也是为什么我坚持认为在大模型推理语境下硬件调度的核心矛盾不是“算力不够”而是“资源匹配不上”。3. 硬件调度的演进从“人肉分配”到“实时编排”3.1 早期阶段单卡部署与人工切分早几年做 AI 服务GPU 调度基本是靠“人肉”。想上一个模型先看单卡能不能装下。能装下就塞一张卡装不下就想办法砍模型、砍 batch或者手动把不同模型拆到不同卡上。这种方式的缺陷非常明显。一切都是静态的模型和卡的绑定关系写死之后负载不均衡几乎无法避免。有的卡跑得满负载有的卡闲置谁也没有办法实时调配。而且人肉切分无法应对模型规模的增长。当 Llama 这种百亿参数模型出来后单卡放不下靠人工已经搞不定了。当时行业里也出现过一些“土办法”比如自己写脚本监控显存、动态地往显存有富余的卡上派任务。但这类方案只适合实验环境一旦请求量上来脚本本身就成了新的瓶颈。3.2 并行策略从“搬过来”到“拆开算”到了大模型训练和推理都要用多卡的阶段并行计算策略就成了核心话题。三个最基础的并行维度是张量并行、流水线并行、数据并行。张量并行把模型权重按层内切分到多张卡上比如把一整层 attention 的矩阵运算拆成多份每张卡算一部分。它的好处是能把超大规模模型放到多张卡上但坏处是每算一步都要做卡间通信。在同一节点内走 NVLink 问题不大跨节点就非常吃网络带宽。我的经验是能不放跨节点就别放通信开销往往是隐性杀手。流水线并行则是按层切分把模型的不同层放到不同卡上数据像流水线一样逐层推进。它的好处是通信压力比张量并行小但缺点是流水线有“气泡”——上下游速度不匹配时部分卡会空转。数据并行则是最简单的一种每个卡上放完整模型把不同请求分给不同卡各自推理。它适合低延迟优先的场景但显存利用率低模型一旦超过单卡容量就没法用了。早期做推理的人直接把训练阶段的并行策略照搬过来但效果并不好。原因是推理请求的到达是动态的静态切分根本没有办法应对突发流量。合理做法是把并行策略和动态调度结合起来比如多个副本之间做负载均衡单副本内部再叠加张量并行或流水线并行。3.3 调度器接管连续批处理和显存页式管理真正让推理性能上了一个台阶的是调度器从“配角”变成了“主角”。vLLM 提出的 PagedAttention 和 Continuous Batching在我看来是这个领域最有代表性的思路转变。PagedAttention 解决的是显存碎片问题。它模仿操作系统虚拟内存的机制把 KV cache 切成固定大小的块按需分配而不是预先给每个请求分配一块连续的大内存。这个设计让显存利用率提高了不少尤其在长序列、高并发的场景下效果很明显。Continuous Batching 解决的则是请求调度问题。它不再等一个 batch 整体跑完再调度下一批而是在单个 token 粒度上做调度。当一个请求在 decode 阶段生成完当前 token调度器就会决定下一步是把新请求加入当前批处理还是继续让旧请求生成下一个 token。这么一来整套系统的吞吐量提升非常显著。我印象很深的是自己第一次把推理框架从静态批处理切到支持连续批处理的方案时同样一台 A10吞吐量翻了接近三倍首字延迟还明显下降。当时我就意识到硬件本身没变只是调度策略变了效果就完全不同。这个阶段还有另外几个关键优化方向。比如 inflight batchingTensorRT-LLM 里的叫法作用类似 Continuous Batching不过对做动态批处理和抢占调度的支持更精细。还有 prefix caching缓存相同 prompt 前缀的 KV cache遇到相似请求可以跳过 prefill 阶段直接开始 decode这个在 Agent 场景里非常实用。3.4 异构混布与弹性调度从“一人一卡”到“资源池”再往后发展调度不再局限在单机内的几张卡上而是走向了“资源池化”。异构混布就是把不同类型的 GPUA 系列、V 系列、消费级卡放到同一个资源池里调度器按任务对显存、带宽、算力的需求自动分配合适的卡。比如为了省成本可以把轻量级模型的推理请求放到消费级卡上把重型模型的请求放到 A100 上。弹性调度解决的是“流量来去不定”的问题。上线一个模型后不再固定占用多少张卡而是按需扩容缩容。流量高峰时拉起更多推理副本低谷时缩小副本规模。这种方案对平台型团队特别友好但对基础设施的要求也高要求推理服务和调度平台之间做很深的集成。我自己的感受是这种演进路径背后是思维方式的转变——从“每个人都守着自己的一亩三分地”变成“算力是一池子水按需取用”。GPU 不再属于某个模型或某条业务线而是交给调度器统一编排。这也是当下 Serverless 推理、模型编排平台都在走的方向。4. 实操中真正值得复用的调度调优思路4.1 别拍脑袋先看监控数据网上有太多“三个月精通大模型推理”的帖子上来就给出各种参数配置。但我想说的是任何脱离了监控数据的调参都是自欺欺人。我每次接手一个推理服务第一件事就是把下面几项监控盯起来GPU 利用率看的是算力占用情况显存占用重点看是模型权重大还是 KV cache 大内存带宽利用率如果显存带宽被打满说明 decode 阶段已经到头了平均首 token 时延和生成速度这是用户能感知到的核心指标队列长度和排队等待时间如果队列一直在涨调度配置可能需要调大并发。举一个我自己遇到的例子。之前调一个 7B 模型的部署首 token 时延高得离谱但 GPU 利用率只有 8%。一开始我以为是模型加载或者框架问题折腾了半天最后看监控才发现请求到达速率很高但服务端在等待 prefill 阶段释放显存大量请求在队列里排队。问题根源是 KV cache 预留空间太小调度器不敢同时处理多个长请求。把 gpu_memory_utilization 和 max_num_seqs 按实际请求长度调大后首 token 时延立刻降了下来。所以我的建议是调优之前先花一天时间把监控做全。没有监控数据后面所有操作都是在赌。4.2 一次完整的显存估算与 batch 配置过程这里用一个实际案例带大家完整走一遍显存估算和 batch 配置的思路。假设我要在一张 24GB 显存的 RTX 3090 上部署一个 7B 模型目标是尽可能提高并发吞吐。第一步是确定模型权重占用。如果直接加载 FP16 权重大约需要 14GB。这就意味着留给推理中间状态的空间只剩 10GB。通常 CUDA context 和激活内存会占 1~2GB那么 KV cache 最多只剩下 8GB 左右。第二步是估算单请求的 KV cache 占用。假设模型是 32 层、32 个 head、每 head 维度 128序列长度平均 1024单请求的 KV cache 大约是 2 × 32 × 32 × 128 × 1024 × 2 字节 512MB。也就是说在 8GB 的 KV cache 空间里理论上最多容纳 16 个并发请求。但如果模型支持长上下文用户经常用到 4096 序列单请求就变成 2GB并发能力立刻降到 4。这时候就面临取舍如果业务场景长请求占比高就需要把 batch 和 KV cache 上限调低避免显存溢出如果短请求为主则可以适当拉高并发。实际配置里我会重点关注 max_num_seqs 和 max_num_batched_tokens 这两个参数。前者控制最多同时处理的请求数后者控制单次迭代最多处理的 token 总数。我的经验是把 max_num_batched_tokens 设为并发请求数和平均序列长度的乘积再留一些余量这样不容易出现显存峰值超限的情况。4.3 prefill 和 decode 分开调度如果说前面是基础配置那么 prefill 和 decode 的分离优化就是进阶玩法。为什么要把这两个阶段分开因为它们对资源的需求完全不同。prefill 是计算密集型的给它更多算力就能更快处理完decode 是访存密集型的并行加更多的请求反而能摊薄访存成本。如果混在一起跑调度器很难做到两种需求的兼顾。现在一些框架已经开始支持 prefill 和 decode 的独立调度比如把 prefill 任务安排到专门的 GPU 上decode 任务则在另外的 GPU 上执行中间通过 KV cache 传递状态。这种设计的价值在于可以根据两个阶段不同的负载特征分别做扩容。prefill 压力大就加 prefill 节点的卡decode 压力大就加 decode 节点的卡互不干扰。这种拆分的代价是增加系统复杂度需要管理两个阶段的 KV cache 传输和节点间的状态协调。对于单机部署也可以退而求其次在调度器上做优先级控制——比如当 prefill 任务堆积时让给计算密集的请求先跑当 decode 请求多时则尽量合并它们做连续批处理。4.4 别忘了网络和 CPU 这些“边角料”还有一个常被忽略的点硬件调度不只在 GPU 之间进行CPU、内存、网络同样参与调度。我见过一个项目GPU 配置很好但 CPU 核数太少。结果做 tokenization、预处理和 sampling 的 CPU 进程成了瓶颈GPU 经常空转等着 CPU 喂数据。大模型推理不仅仅是 GPU 在干活分词、采样、请求解析、结果返回这些操作都依赖 CPU。如果在跑多路推理服务建议把 CPU 核数、内存大小和 GPU 卡数一起做预算。多机部署时网络更是硬约束。张量并行需要频繁通信如果跨节点用普通万兆网而不是 RDMA 网络通信延迟会直接把推理速度拖垮。我的建议是能够单机放下的模型尽量别做跨节点张量并行实在要跨节点优先考虑流水线并行而不是张量并行因为它的通信频率要低很多。5. 常见问题与排查技巧实录5.1 一张实用的问题速查表平时答疑时我发现很多推理性能问题都能归到几个固定类型。下面这张表是我根据自己的排查经验整理的不一定覆盖所有场景但可以作为第一轮排查的参考。现象最常见原因排查方向GPU 利用率很高但每秒生成 token 数很低decode 阶段访存瓶颈看显存带宽占用确认是否已经打到上限首 token 时延很长后续生成正常prefill 排队或 prefill 节点过载检查请求队列长度和 prefill 阶段耗时显存占用看起来不高却报 OOM峰值显存超限长序列或大 batch 导致监控 KV cache 的峰值变化降低并发上限并发一高就超时服务端队列配置过小或批次过大调整 max_num_seqs 和 max_num_batched_tokens重启时模型加载非常慢模型权重在 CPU 与 GPU 之间反复搬运检查加载逻辑是否用了 mmap 或 safetensors 分片加载多机推理比单机还慢跨节点通信开销过大检查网络配置考虑改用流水线并行或减少跨节点张量并行这个表的价值不在于“答案”而在于帮你快速锁定方向。很多问题一旦方向对了解决就只是时间问题。5.2 一个典型 caseGPU 利用率高但吞吐上不去我挑一个印象最深的 case 详细说说。有次帮朋友调优一个在线推理服务现象是 GPU 利用率长期跑在 90% 以上但服务吞吐却只有预期的一半。第一反应是“GPU 在满负荷工作那应该是算力不够吧”。但仔细看了一下 nvidia-smi 和 profiling 工具发现显存带宽的利用率已经接近 100%而 GPU 的 SM流式多处理器利用率其实只有 30% 左右。这说明什么说明 GPU 大量时间都在等待显存数据返回也就是访存受限而不是真正的计算受限。利用率高是因为显存带宽占满了SM 算力却很空闲。问题定位到这儿就清楚了decode 阶段单个 batch 里跑太多请求KV cache 读写的访存压力太大。每个请求都要读自己的 KV cachebatch 越大访存压力越高但计算量的增长并没有跟上。后来把 max_num_seqs 从 64 降到 32同时开启 prefill/decode 分离策略后单 token 生成速度明显提升整体吞吐反而上去了。这个 case 让我得到一个很重要的认知GPU 利用率高不代表系统健康先分清“算力利用率”和“访存利用率”这两件事再决定是加卡还是调配置。5.3 避免踩坑几条实操避坑建议最后分享几条比较“私房”的经验算是给大家的避坑指南。第一不要盲目抄别人的配置。同一个模型A100 和 4090 的最优 batch 配置差别很大同一个硬件长文本业务和短对话业务的最优参数也完全不同。任何人的经验都只能做参考必须在自己的场景里压测验证。第二压测时别只用“平均延迟”做指标。平均值会掩盖大量问题。我看服务健康度时更关注 P99 延迟和长尾请求的比例。在线推理场景P99 抖动往往是用户体验崩坏的第一步。第三升级框架前先做回归测试。有一年我试着把框架从 A 版本升到 B 版本结果模型的输出质量和生成速度都对但在高并发下出现了随机性的卡死最后排查发现是某个调度参数的默认值变了。框架升级不是换了就完事必须拿线上流量回放做验证。第四显存估算永远要留 20% 的余量。别把显存算得刚刚好因为真实业务中除了模型和 KV cache还有采样、beam search、各种临时 buffer 都要占空间。我见过太多线上环境因为显存峰值超限直接 OOM 的。最后说几句掏心窝的话刚才讲的这些实际上都指向一个共同的底层问题你需要的不是更多算力而是让已有硬件更“服帖”地服务于推理任务。我在实际配置和调优过程中最大的体会是大模型推理的优化本质上是一个“匹配”的过程——让模型的访存特征、计算特征和请求特征匹配上 GPU 的算力、带宽和显存容量。这需要的是对细节的耐心而不是简单地堆卡。如果你正在为推理性能发愁我建议你先别急着升级硬件。花一周时间把监控做全把请求特征摸清楚把调度参数一项一项验证大概率能在现有设备上得到不小的提升。最后再分享一个小技巧每次调整完调度配置只改一个变量然后记录前后的对比数据。这样几轮下来你会对线上服务的行为模式有远比“感觉流畅多了”要准确得多的理解。
返回列表