大规模模型服务核心设计:调度、显存与性能优化实战
这几天一直在消化 SOSP 2026 的论文Session 1A 这场的主题是 Model Serving at Scale也就是大规模模型服务。说句实话这个方向在系统领域不算新但最近两年因为大模型把推理成本推到了几乎每个团队都头疼的程度一下子变成了顶会里最拥挤的赛道之一。这一场的几篇论文集中讨论了一个共同问题当模型服务的请求规模、模型规模、GPU 规模和集群规模同时涨上去之后传统 Serving 系统的设计逻辑还撑不撑得住撑不住的地方又该怎么改。你如果只是做应用层开发可能对这些论文没什么感觉但如果你是负责部署推理服务、调优 GPU 利用率、或者正在从单卡部署往多机集群迁移的工程师这一场的内容基本就是为你准备的。文章里很多设计并不是停留在纸面上的 idea而是直接可以影响你选型、配参数、设计服务架构的决策。这篇文章我就按自己的理解把这一场里最有价值的几个系统设计思路拆开讲顺便把我自己实操中踩过的坑和验证过的做法穿插进去希望能给你一些可直接参考的东西。围绕 Model Serving 的讨论其实有一条很清晰的暗线单卡能跑通模型只是第一步真正难的是让几十张甚至几百张 GPU 在持续变化的请求流下保持高吞吐、低延迟和低成本。这背后牵扯到调度策略、显存管理、通信设计、甚至 kernel 层面的优化不是单纯把模型塞进显存就完事的。1. 这届 Session 1A 到底在讨论什么Model Serving 的核心矛盾1.1 从单机部署到集群化推理变化发生在哪前几年我们部署推理服务思路其实很简单模型加载进 GPU起一个 HTTP Server请求来了就前向推理一次返回结果。那时候模型不大并发不高Serving 系统要解决的只是“别让 GPU 闲着”而已。但到了大模型时代情况完全变了。单是这个“别让 GPU 闲着”就已经很难做到因为请求之间不是独立的。每个请求都有自己的 prompt、自己的上下文长度、自己已经生成的历史 token服务端要为每个请求动态维护一份 KV Cache。这意味着 Serving 不能像传统 Web 服务那样无状态地横向扩展每个请求的内存占用是动态变化的GPU 显存很容易在几分钟内被打满。这一场论文里反复出现的一个词是“协调”。单卡推理的利用率再高放到集群层面也可能因为排队不均衡、数据倾斜、上下游带宽不匹配而整体吞吐很差。协调的核心对象有三个算力、显存、时间。算力决定你单位时间能跑多少个 token显存决定你能同时放多少个请求时间则决定了请求从进入到返回的整个延迟。三个因素互相耦合操作任何一个都会影响另外两个这就是规模化的核心矛盾。1.2 论文给出的三个关键约束我梳理了一下这一场的论文基本默认了三个物理约束所有设计都围绕它们展开GPU 显存的增长速度跟不上模型参数的增长但更跟不上并发请求对 KV Cache 的需求。于是论文普遍把 KV Cache 当作一种可以池化、调度、换入换出的资源而不是绑定在某张卡上的固定内存块。单卡计算峰值很高但内存带宽远低于计算速度。也就是说decode 阶段每生成一个 token 都要把整个模型参数学一遍此时算力不是瓶颈显存带宽才是。这个约束直接决定了很多论文为什么在 decode 阶段采用 batch 合并和特殊 kernel。请求的到达模式高度不均匀而且长请求和短请求混在一起。简单 FCFS 会导致长尾延迟爆炸纯优先级调度又会饿死大批量短任务必须在“公平”和“效率”之间做文章。这三个约束单独任何一个都容易理解难的是同时优化。Session 1A 里好几篇论文的设计差异恰恰体现在约束的取舍顺序上有的优先保证延时有的优先保证吞吐有的优先做成本归一化但骨架都是围绕算力、显存、时间三者建模再决定调度和分配策略。这套取舍逻辑我认为是最值得学的比具体的代码实现更有普适性。1.3 评述视角这套方案解决了什么没解决什么如果只看论文标题Model Serving at Scale 听起来是一个通用问题但细读会发现作者们基本把目光锁定在“单集群内、多租户、长文本交互”这个典型场景。优点是问题定义非常清楚评测指标统一关注 TTFT首 token 延迟、ITLtoken 间延迟、吞吐和 GPU 利用率结果可复现。缺点是这套设计对“跨数据中心”“超大模型分布式训练叠加推理”“边缘设备服务”这些更极端的场景没有太多讨论。也就是说这一场的成果对绝大多数做线上推理服务的团队都有参考价值但如果你是想找一套能直接解决手机端或者多地域容灾的 Serving 方案可能还要结合其他方向的工作。不过这不影响我们从中提取可迁移的设计原则。我需要特别说明一下下面几节我不会逐篇复述论文而是把这一场里反复出现的系统设计思路提炼出来结合我自己上线推理服务时用过的方案和踩过的坑给你一套能带走的东西。2. 核心设计思路拆解以 request 为中心的调度2.1 Continuous batching 为什么成了默认答案很多没深入做过推理系统的人可能不理解为什么两年前大家都在吹动态批处理continuous batching现在它反而成了 Serving 引擎的默认配置似乎没什么好讲的。但恰恰因为它太重要了这一场论文里几乎所有实验的 baseline 都建立在它的基础上所以我先把它的原理说透。传统的 static batching 做法是收集一批请求等数量满了或者超时了一起做前向推理。问题在于这批请求里如果有几个生成了 20 个 token 就结束了另外几个还在生成 200 个 token那么 GPU 算力的利用率就会随着请求不断结束而下降。因为 batch 里空出来的位置没法再填新请求必须等整批结束才能开启下一批。Continuous batching 的做法是每个序列在 GPU 上的执行不再是“整批绑定”而是以 iteration一次前向为粒度动态调度。某个序列结束了下一轮迭代立刻插入新请求某个序列的 KV Cache 不够了系统会在迭代边界给别的序列腾位置。这样一来GPU 上同时运转的序列数始终接近显存容量允许的上限吞吐自然就上来了。但代价也藏在里面。动态调度意味着每一个 iteration 都要重新决定 GPU kernel 要处理哪些序列调度开销会随序列数上升。这一场论文里凡是自称高性能的 Serving 方案基本都在这个“动态调度”层做了优化要么把调度器放到独立线程要么用流水线把调度的计算和 GPU 执行重叠。我自己的经验是当单机并发请求数超过 200 且序列长度不均匀时没有优化过的动态调度器反而会成为瓶颈CPU 占用冲到 100%GPU 利用率却只有 60%。所以 “continuous batching”不等于免费午餐它需要配合高效的调度实现。2.2 Prefill/Decode 分离带来的调度革命这一场讨论热度最高的话题应该是 prefill 阶段和 decode 阶段的分离调度。很多同学看到这个名词以为只是把两个阶段放到不同 GPU 上执行其实背后的动机非常鲜明。Prefill 阶段处理的是一个长 prompt计算量大但是并行度高因为可以一次性对所有输入 token 做矩阵乘法。Decode 阶段则是一个 token 一个 token 地生成每一步都依赖上一步的输出串行特性很强。把这两个阶段混在同一批请求里会导致系统很难同时优化二者prefill 想要大 batch、长序列、高计算强度decode 想要的却是小 batch、高带宽、低延迟。两者对资源的需求几乎是反的。所以现在很多 Serving 架构会设置两类实例一类专门做 prefill把请求的 KV Cache 计算好另一类专门做 decode只负责后续 token 的生成。请求在 prefill 实例上完成后把 KV Cache 传给 decode 实例继续生成。论文里对这种“分离式架构”的改进主要集中在这几点Prefill 和 decode 实例之间的比例不是固定的而是根据流量动态调整避免某个阶段排队过长。KV Cache 的传输不需要通过 CPU 中转而是通过 GPU 间的 NVLink 或者 RDMA 直接交换减少拷贝延迟。当 decode 实例空闲时它也可以反过来接受一些轻量 prefill 任务避免算力浪费。这里我要多说一句PD 分离不是银弹。如果你的线上流量是短文本、短回答比例偏向轻量请求那么 prefill 和 decode 混跑其实并不会差很多强行拆开反而增加数据传输开销。所以论文里给出的在 8 卡到 32 卡场景下收益比较明显小集群收益有限。这个结论跟我在自己环境里的测试是一致的。2.3 KV Cache 资源池化显存不再是单卡约束传统推理部署里KV Cache 是跟着请求走的。请求在哪个 GPU 上KV Cache 就在哪个 GPU 上分配。但请求长度变化太随机了可能某张卡上的请求全是长文本把显存吃满另一张卡却全是短请求留下大片空洞。Session 1A 多篇论文的解决思路是把 KV Cache 做成集群级资源池由中心调度器统一管理。每个请求在运行前先“预留”一块虚拟显存配额实际物理分配则按需从池子中领取页面。当某张 GPU 显存不够了可以把部分请求的旧 KV Cache 页面替换或换出再重新调度到有空间的 GPU 上继续生成。这个思路听着很像操作系统里的虚拟内存但实现起来难点在粒度。如果按 token 一个个管理缓存页调度开销会大到你没法接受如果按整个请求管理又没法处理变长序列带来的碎片。我比较欣赏的一种做法是分层管理请求级调度器负责把请求分配到 GPU 节点节点级运行时负责把 KV Cache 按固定大小的块比如 64 token 一组管理同一请求的多个块可以分散在不同 GPU 上但尽量保证当前正在生成的 token 所在的块在本地避免频繁跨卡读取。用生活类比就是不要让每个旅客请求必须住一整间房整块显存而是把房间拆成可组合的床位旅客可以灵活入住多名室友同时保证每个旅客常用的随身物品最近的 KV就在手边。这套机制让显存利用率从 60% 提升到 85% 以上直接带来的效果就是同样的 GPU 数量能承载更多的并发和更长的上下文。2.4 多级队列与优先级策略不要让一个长任务毁了所有短任务最后一个在论文里反复出现、但容易被忽视的设计是调度队列。很多人以为调度就是把请求按到达顺序排好按先来后到执行。但在多租户场景下这种策略非常容易出事故。设想一下一个请求的 prompt 是 8000 token回复也要 8000 token另一个请求只有 30 token 的输入输出。如果它们混在一个队列里长请求会占住 GPU 资源和 KV Cache短请求被迫等待最终短请求的 TTFT 会从几十毫秒涨到几秒前端直接超时。论文里给出的多级队列方案大体是这样的队列级别服务对象策略典型场景L1 快速通道短请求、首 token 敏感请求抢占式可中断长任务的空闲 slot在线对话、搜索摘要L2 普通通道中长请求、批处理任务保证最小吞吐内容生成、批量翻译L3 后台通道离线任务、预填充只在 GPU 空闲运行日志摘要、数据增强实际落地时调度器会在迭代边界做“抢占”只要 L1 队列有请求就优先把它插进下一轮 iteration哪怕这意味着要暂时打断 L2 队列里正在进行的某条序列。这种设计需要代价被打断的序列会多等一两个迭代但在短请求 SLO 和长请求吞吐之间取得了更好的平衡。我在部署时也遇到过类似问题。一开始我们用简单的 FCFS 队列结果线上经常出现一个用户输入超长文档、其他用户全部超时的投诉。后来借鉴了这种多级队列的思路把在线推理请求和离线批处理请求分离效果立竿见影p99 延迟直接降了一个数量级。多级队列的配置参数说穿了就是你在延迟、吞吐、成本之间的偏好没有标准答案但方向是对的。3. 实操落地从论文到生产的三个可迁移动作3.1 第一步测量自己的请求分布论文里的调度策略之所以有效很大程度上因为作者能精确测量请求长度、到达速率、token 分布等指标。但很多生产团队的 Serving 配置都是拍脑袋定的这是我见到的最大的问题。你要做的第一件事不是调参而是把线上请求日志捞出来统计下面几个指标请求的 prompt 长度分布尤其关注 p50、p95、p99。生成的回复 token 数分布这里通常跟模型类型强相关对话类模型和生成类模型的差异非常大。每秒到达的请求数RPS的峰值和低谷最好能画出时间曲线。同一时刻并发请求的 token 总量这个决定了 KV Cache 的峰值需求量。拿到这些数据后你再回看论文里的设计会发现很多选择都非常合理。比如你的流量如果是“少量长文档 大量短对话”那 prefill/decode 分离几乎是必须的如果是“绝大多数请求都很短”那么单卡混跑加 continuous batching 就够了没必要上集群级调度。我自己刚接手推理服务的时候第一件事就是写了一个简单的统计脚本把线上 7 天的请求日志做了分布统计结果发现接近一半的请求 prompt 长度不足 200 token但同时有 5% 的请求 prompt 超过 4000 token。不看这个数据我可能会花大量精力去优化长文本的 KV Cache 池化但实际上短请求的调度优化才是核心收益点。3.2 第二步合理切分 prefill/decode 实例如果你确定了要走 PD 分离这条路那就要考虑实例比例问题。比较朴素的做法是直接按流量占比来分配 GPU假设 30% 的时间在跑 prefill70% 的时间在跑 decode那就按 3:7 的比例分卡。但这忽略了 token 吞吐的差异prefill 阶段虽然耗时短但计算量大decode 阶段耗时虽长但每个 token 的计算强度低所以不能只看时间占比。更合理的估算方式是按 token 吞吐量来算假设你的 GPU 做 prefill 时能达到每秒处理 X 万 token 的输入速度。decode 时能达到每秒生成 Y 千 token 的输出速度。根据线上统计的输入输出比例算出单卡处理单位请求的时间占比。再根据 RPS 和并发量反推需要多少张 prefill 卡、多少张 decode 卡。这个算法写起来很简单但真正常用的方案是让 prefill 和 decode 实例的比例不是固定的而是支持动态迁移。比如把一部分 GPU 标记为“弹性角色”流量高峰时可以自动从 decode 模式切换到 prefill 模式。论文里对这种弹性角色切换的设计做了很多优化核心点在于切换时 KV Cache 和模型权重如何快速重新布局避免伤害在线请求。我自己在生产环境里最开始就是静态比例后来发现每天下午的流量形态跟上午差别很大最终改成白天 prefill:decode 2:8晚上 4:6靠定时任务切换虽然没有论文里的动态联动那么优雅但已经能省下不少成本。如果你条件允许可以尝试让调度器根据排队长度自动调整角色这是投入产出比很高的一件事。3.3 第三步用缓存、量化、投机解码降本Session 1A 里不少篇幅在讨论如何用更少 GPU 扛住同样流量这也符合“at Scale”的现实诉求。除了调度之外有三个手段几乎每篇论文都会作为模块提到Prefix Cache把公共前缀的 KV Cache 缓存起来后续请求只要命中同一个系统提示词或者文档前缀就能跳过 prefill 直接进入 decode。实测在 RAG 场景下命中率能到 40% 以上TTFT 下降非常明显。量化感知 Serving模型权重从 FP16 降到 INT8 或 INT4能省一半显存代价是精度略微下降。Serving 系统要做的是根据请求类型动态切换量化版本比如对内部测试流量用低精度对核心用户保留高精度。投机解码用小模型先生成若干个候选 token再用大模型一次验证。如果候选被接受那么一次前向可以产出多个 tokendecode 吞吐直接翻倍甚至更高。但它对生成的接受率敏感不是所有场景都有效。我特别想强调 Prefix Cache 的重要性。很多人以为 KV Cache 是跟随请求动态生成的没有太多复用价值但实际上在一个固定场景里用户输入往往带有一段很长的公共指令前缀。启用 Prefix Cache 后新请求只需要计算差异部分的 KV大量复用已有结果。我这里给一个简单的配置思路# 框架无关的伪配置示意 serving: cache: enabled: true max_prefix_tokens: 4096 min_match_length: 64 quantization: default: int8 fallback: fp16 speculative_decoding: draft_model: small_v2 verify_interval: 4这段配置的核心意义不是参数本身而是你要在 Serving 层把“缓存”“量化”“投机解码”都当成一等公民来设计而不是事后再补。等流量上涨再想加这些功能改动成本要高得多。4. 常见问题与排查技巧实录我从 Session 1A 延伸到实战场4.1 显存碎片化导致推理引擎 OOM我做过很多次性能优化最崩溃的一类问题是明明 GPU 显存还有几个 GB但启动新请求时直接报 Out of Memory。原因就是 KV Cache 的分配和释放不连续显存被切割成很多小块哪一块都不够塞进一个长序列的 KV。Session 1A 里推荐的思路是引入“Cache Blk”抽象把所有显存预先切成固定大小的 block请求需要的 KV 按 block 分配而不是按“整段连续内存”分配。这样即使显存碎片很多只要总 free block 数够请求就能成功分配。这个方法我实际操作之后基本上杜绝了“显存剩余但 OOM”的怪现象。如果你想排查自己的服务是否存在碎片化最直接的方式是观察显存分配曲线如果分配总量看起来有空闲但并发请求的规模上不去那大概率是碎片问题。可以考虑在代码层引入类似 PyTorch caching allocator 的机制或者直接改用论文里推荐的 block-based KV Cache 实现。4.2 长尾延迟p99 总是不达标这一场多次讲到延迟的分布问题。稍微上一点规模后你会发现平均延迟看起来还行但 p99 或者 p999 总是异常高。原因往往不在模型本身而是在调度环节要么是某个长请求占住 GPU 时间太久要么是某个 decode 实例负载过高导致所有排到它上面的请求都变慢。排查时我习惯分三层去看看队列排队时间如果一个请求从进入到执行的时间占了总延迟的 40% 以上说明调度策略有问题。看单 iteration 耗时如果某个阶段执行时间突然从 30ms 涨到 100ms可能是 key-value cache 换页或者跨卡访问。看 GPU 带宽如果 decode 阶段大量跨卡取 KV带宽会成为隐形瓶颈。对症下药的方式可能是调整 prefill/decode 比例也可能是给短请求提升优先级。论文里给出的动态队列方案就是解决这个问题的但你需要先定位问题在哪个环节才能真正用对方向。我在实际调优时发现很多团队的 p99 问题其实出在“跨卡 KV Cache 访问”上改掉之后效果立竿见影。4.3 多模型共存时的资源干扰Session 1A 有一个容易被忽略的应用场景是多模型同时服务比如一个模型中英文翻译一个模型做摘要还有一个模型做向量化。很多人一开始把它们部署在同一台机器上以为共享 GPU 能省钱结果发现两个模型互相干扰各自的吞吐都下降了。这是因为不同模型的 kernel 执行时间不同对显存带宽、SM 占用、缓存行利用都存在竞争。论文里提到的一个方案是“资源池隔离 时间片轮转”把 GPU 的 SM 划分为不同 partition让不同模型占用独立 partition同时用时间片避免长任务独占。但这类方案对 kernel 的配合度要求高如果模型来自不同框架可能没法精细隔离。从实操角度我的建议是在模型之间用“小型独立实例 统一入口路由”来代替硬隔离。每个模型一组 GPU共享一个请求入口入口根据模型类型路由。虽然牺牲了一些共享收益但稳定性和可维护性高很多。论文里的精细隔离方案更需要底层硬件的支持适合大厂或者专用集群小团队落地成本偏高。5. 写在最后从论文评审到工程实现我更关注什么Session 1A 的论文读下来我最深刻的体会是Model Serving 的工程难度已经不再局限于模型本身了。过去我们常讨论模型结构、训练方法但线上系统设计的好坏直接决定了用户等多久才能看到第一个 token、你一个月要烧多少 GPU 费用、以及流量十倍增长时服务会不会直接崩溃。这些论文虽然来自学术界但提出的很多模块化设计已经值得生产环境借鉴。不用贪多求全地一次性把所有论文里的机制都搬进自己的系统。我的建议是从一个痛点开始如果你的问题是显存利用率低就先去实现 block-based KV Cache如果问题是长请求导致短请求超时就先去做多级队列。系统设计的提升从来不是推倒重来而是把瓶颈点逐个击破。最后还有一个小技巧想分享无论你采用的是哪个开源推理引擎都记得在压测时加上 token 级的性能指标采集而不是只看请求成功率。TTFT、ITL、生成吞吐这三个指标分开追踪你才能知道瓶颈是在 prefill 还是 decode是在网络还是显存。我在排查性能问题时这个习惯帮我省了大量时间。