很多人把线上LLM服务的延迟问题归结于“模型还不够大”,但我在一线跑推理服务这几年,最大的体会恰恰相反:把每一个请求都交给同一个大模型走完整的自回归解码,才是架构上最奢侈也最不划算的决定。P99延迟不断被长尾请求拉高,GPU显存翻几番都填不满KV Cache的胃口,而真实流量里却有大量请求根本用不到模型那么深的推理能力。
“System One Models”这个说法,最近在几个技术社区里反复出现。它借用了卡尼曼在《思考,快与慢》里提出的双系统理论——系统1负责快速、直觉式的反应,系统2负责谨慎、深度的思考。放到LLM Serving的语境下,意思就是:不要再让所有流量都无差别地涌进那个“深度思考型”大模型,而是按请求复杂度拆出快慢两条路径,用轻量模型和结构化解码承接大部分低难度请求,让大模型专心处理真正需要长链推理的部分。
这篇文章我想把这个重构思路完整讲透,包括为什么现在必须重新做架构分层、System One Models有哪些具体技术形态、搭建低延迟推理链路时最容易翻车的工程细节,以及我实践下来的一些调优数据和踩坑经验。适合正在做推理服务架构、或者被GPU成本和延迟SLO反复折磨的团队参考。
1. 为什么需要重新思考LLM Serving
1.1 生成式推理的延迟结构:串行与排队的双重诅咒
先看一个基本事实:大模型的生成推理是自回归的,也就是第N个token必须等前N-1个token全部生成完才能开始计算。这个串行结构决定了端到端延迟的公式:
端到端延迟 ≈ 排队等待 + TTFT(首Token延迟)+ TPOT(每Token生成时间)×(输出长度 - 1)
这意味着哪怕模型单token生成速度已经压到20ms,只要用户的提示词输出2000个token,纯解码时间就要40秒。更麻烦的是,排队时间在某些时段会突然暴涨,把P99直接拉垮。
我见过太多这样的场景:一个共享推理集群里,某个业务方突然提交了一批长文档阅读理解任务,每条输入上下文都超过20k tokens。这批任务涌进来之后,整个batch的KV Cache占用量急剧膨胀,GPU算力被长序列的注意力计算吃满,普通聊天请求的TPOT从25ms被拖到150ms以上,用户体感就是从“丝滑输出”变成“一个字一个字往外蹦”。
这里面有个很反直觉的现象:计算量增加了,但吞吐反而下降,因为长序列请求在连续批处理(continuous batching)里会压制同batch内短请求的调度。短请求本来能快速结束,却要陪着长请求跑完整个decoding step。这也是为什么一刀切的全服务大模型架构,在混合负载下几乎是必然会出现长尾延迟问题的。
1.2 系统1与系统2的映射:多数请求根本不需要“深思熟虑”
卡尼曼的双系统理论给了我一个很重要的视角:人类做大部分决策时根本不会调用深度推理,而是靠直觉和经验直接给出答案。只有遇到真正复杂的问题时,系统2才会接管,启动慢速、耗费大量认知资源的深度思考。
把这个映射到LLM服务流量上,你会发现生产环境里的请求分布惊人地符合这个规律。我在多个团队里看过真实流量日志,大量请求属于以下类型:
| 请求类型 | 特征 | 需要的推理深度 |
|---|---|---|
| 信息抽取 | 从文本中提取实体、时间、金额 | 低 |
| 格式转换 | JSON转表格、文本改写 | 低 |
| 简单问答 | 事实查询、知识问答 | 中低 |
| 代码补全 | 单行函数补全、模板生成 | 中 |
| 逻辑推理 | 多步数学题、代码调试 | 高 |
| 长文档分析 | 合同比对、论文理解 | 高 |
粗略统计下来,超过60%的请求属于“系统1级任务”——它们不需要模型进行多步推理,只需要从已有的语料或模式中快速映射出结果。这些请求硬走一个70B大模型的全量解码,本质上就是在用大炮打蚊子。
1.3 快慢分层:重新定义LLM Serving的架构范式
想通了这一点之后,架构重构的方向就很清晰了:把服务拆成快路径(System One)和慢路径(System Two)两层,中间用一个路由层做分流。
快路径的核心是“最小延迟完成大多数请求”,通常由三类技术组成:轻量小模型、提前退出(Early Exit)机制、结构化解码约束。慢路径则保留大模型的完整推理能力,处理真正复杂的请求,或者负责对快路径的结果做校验兜底。
这个思路我在实际落地中最大的体会是:分层本身不复杂,复杂的是“信任边界”——快路径给出的结果质量是否可靠?什么时候应该降级到慢路径?这个问题回答不清楚,分层方案就只是空中楼阁。所以整个架构里最关键的设计不是模型本身,而是一个可靠的置信度评估与降级机制,这个后面我会详细展开。
2. System One Models的技术形态与适用边界
2.1 快路径第一招:轻量模型配结构化解码
快路径最朴素的形态就是部署一个小模型,让它直接输出最终回答,不走大模型。但选型上有很多细节门道,我踩过几个坑之后总结了三个关键点。
第一,模型规模不是越小越好。在这个场景里,一般推荐7B~14B这个级别。模型太小(比如1B级别以下),基础能力明显断层,英文可以但中文表达很容易出现语义漂移;模型太大(比如32B以上),量化部署成本、显存占用和推理延迟的优势就不明显了。目前市面上很多7B~8B模型都采用了GQA(Grouped Query Attention)架构,KV Cache占用大幅降低,这是快路径选型的核心竞争力之一。
第二,量化要慎重选精度。快路径要吃延迟红利,INT4量化几乎是必选项。但要注意:INT4对某些数学计算和指令遵循任务的精度损伤比较明显,如果快路径要承接代码补全这类任务,我建议至少用INT8或者混合精度(关键层保留FP16)。如果你的任务有严格的正确性要求,干脆做两套量化版本,按任务类型分发。
第三,结构化解码是快路径和慢路径拉开差距的关键。所谓结构化解码,就是用JSON Schema、正则表达式或者文法约束来限制模型的输出格式。比如信息抽取任务,要求模型输出一个固定字段的JSON对象,完全可以用约束解码(constrained decoding)把输出空间限制在合法格式内。
我自己的经验是:纯自由文本生成的快路径模型,和加了结构化约束的同一模型,在业务对接成本上天差地别。后者可以让下游直接解析字段,不需要再做一层文本清洗和正则匹配。而且结构化约束还能天然避免模型输出“抱歉,我无法回答”这类占位废话——这在生产环境中的意义比很多人想象得大。
2.2 快路径第二招:提前退出机制
提前退出(Early Exit)是快路径的另一条技术路线,思路是在模型中间层插入分类头,当某层输出的置信度足够高时,直接在该层输出token,不再继续往深层计算。
这招本质上是在“模型深度”和“推理质量”之间做了一条可变的曲线。浅层输出通常更偏“语言流畅但语义平庸”,所以Early Exit适合的是那些不需要复杂语义推理的生成任务,比如机械性的文本改写、模板化回复。
Early Exit最大的坑在于阈值设置。阈值设得过高(比如0.99),模型几乎永远走不到提前退出,收益完全消失;阈值设得过低(比如0.8),生成质量会出现断崖式下跌,而且这种质量问题是静默的——用户只会觉得“回答变笨了”,但不会报错,排查起来极其被动。
我的建议是:不要拍脑袋设阈值,而是拿一批典型快速任务样本做阈值扫描。把0.85到0.98之间的区间按0.01步长各跑一遍,记录每一档的退出率、质量分数和P99延迟,然后画一条帕累托曲线,在可接受质量分水的最高点选阈值。这个流程听起来繁琐,但比上线之后被质量事故教训一次划算得多。
另外要提醒的是,Early Exit不是所有模型都支持,需要训练阶段就在中间层加入分类头。如果你用的是现成的开源模型,可以直接看有没有对应release;如果是自研模型,在预训练阶段就要把这个结构设计进去,事后补装的成本很高。
2.3 路由分流与降级机制:分层架构的信任中枢
路由层的设计决定了整个分层方案的成败。我见过不少团队把路由做成一个简单的“长度判断”——输入超过多少字就走大模型。这条规则对某些场景有效,但局限性非常大,因为请求复杂度从来不只是输入长度决定的。
这里总结我认为最实用的三种路由实现方式,从简单到复杂排列:
首先是规则分流,基于关键词、意图词表、请求来源和API路径直接打标。比如翻译接口、抽取接口、格式化接口可以直接打上快路径标签。这类规则成本最低、最稳定,但覆盖不了真正的长尾请求,属于兜底策略。
其次是Embedding分类器。把query用一个小embedding模型编码,用一个轻量分类器判断该请求适合走快路径还是慢路径,分类器可以按任务类别训练,甚至能区分出类比难度。这个方案的效果明显优于规则,我实测在快慢路径分流准确率上能做到85%以上。
最后是LLM打分器。直接用一个小模型对请求做复杂度评估,输出一个0-1之间的复杂度分数。这是三个方案里最准的,但成本也最高——毕竟它本身也是一个推理调用。我的建议是:不要对这个打分器本身要求太高,它的延迟预算一般在几十ms以内,一旦超过这个预算,路由本身就成了瓶颈。
降级机制是路由之外的第二道保险。快路径返回的结果,要有一个置信度评估模块来判断可信度。如果置信度低于预设阈值,就把请求降级到慢路径重新处理。我在实际项目中用的策略包括:模型output logits的熵值、两次不同采样的一致性比对(输出完全一致说明结果稳定)、字段完整性校验(结构化任务中必填字段是否齐全)。这三类信号组合起来,能有效识别出绝大多数的低质量输出。
3. 核心实现:构建低延迟推理链路的五个关键细节
3.1 连续批处理的调度参数权衡
连续批处理(Continuous Batching)是现代推理引擎的基石技术。它的核心逻辑是不再等一个batch全部生成完再接收新请求,而是只要某个序列生成结束,立刻把空出来的位置让给新请求。这个机制对吞吐和延迟的均衡起到了决定性作用,但参数调起来非常微妙。
最关键的两个参数是最大batch size和等待时间窗口。max_batch_size设大了,GPU利用率看起来很高,但同一batch内的长短请求互相牵制,长尾延迟会显著恶化;设小了,吞吐上不去,GPU空转率提高,成本难看。我在实操里的做法是:先压测出“吞吐-延迟”曲线,找到那个曲线上开始出现“延迟陡增”的拐点,把batch size设在这个拐点以下20%左右的位置,为流量突刺留出缓冲。
等待时间窗口(max_waiting_ms)则是控制“凑batch的耐心”。设得太大会增加空等时间,抬高TTFT;设得太小则batch总是装不满,吞吐过低。我的经验值是10~30ms之间,具体取决于你的单token生成时间。如果TPOT是25ms,那20ms等待窗口基本可以保证一次调度就能吃到新请求。
调度算法的选择也会影响延迟。FCFS(先来先服务)实现最简,但几乎必然会导致长短请求互相踩踏。我推荐按请求的剩余时间评估做SLO感知调度——预估每个序列还要多少token才能结束,优先调度那些“快完成”的请求出batch,让它们尽快释放资源。这个策略对P99延迟的改善非常直观。
3.2 KV Cache的显存博弈:分页、复用与量化
KV Cache是推理服务显存成本的最大头,没有之一。
先给个直观的计算公式:
KV Cache大小 = 层数 × KV头数 × 头维度 × 2(K和V) × 序列长度 × 并发数 × 每元素字节数
拿一个70B级模型举例:80层、8个KV头、每个头128维、FP16存储,序列长度4096、并发16,算下来是80 × 8 × 128 × 2 × 4096 × 16 × 2(字节)≈ 4.3GB。注意这只是“一个服务副本”的KV Cache占用。如果模型本身还要FP16跑权重,配上140GB的权重显存,整机的KV Cache预算非常紧张。
现在的推理引擎普遍用PagedAttention这类分页管理方案来解决碎片化问题。page size默认是16个token,这个值我建议不要乱动。page调大(比如32)可以降低管理开销,但浪费的padding空间也随之增加;调小则前缀复用命中率下降。16是多数框架做了大量基准测试后的折中点。
KV Cache量化是更激进的一招。INT8量化KV Cache在不牺牲太多质量的情况下,能砍掉一半显存。我实测下来对P99延迟的影响在3%~5%,但显存压力的改善是实打实的。不过要注意:量化KV Cache对高熵场景(比如代码生成,注意力分布更散)的误差会更明显,建议在高精度场景保留FP16,其他批量场景走INT8。
前缀缓存(prefix caching)是另一个常被忽略的大杀器。如果流量中存在大量共享的系统提示词、固定工具说明,把这段KV Cache缓存起来,能直接跳过它们的预填充计算。我在一个知识库问答场景中,前缀缓存把TTFT降低了整整60%,因为知识库指令动辄几千token,每条请求都要重新计算一遍注意力,浪费非常严重。
3.3 投机解码的工程落地
投机解码(Speculative Decoding)是延迟优化的经典技术,原理是让一个小草稿模型先快速生成一串候选token,再去大模型做并行验证。验证通过则加速,不通过则丢弃重来。这个技术对System One架构尤其友好,因为快路径本身就可能有一个小模型在工作,把它的输出拿去大模型验证几乎不增加额外成本。
但投机解码的收益非常依赖两个参数:draft模型选择和draft长度。draft模型的能力必须和主模型的能力“同频”——草稿模型太弱,命中率低到20%以下,验证大部分失败,反而因为要清零重算而拖慢速度;草稿模型太强,那你自己直接用它不就行了,又失去了验证的意义。
draft length(草稿长度)我推荐从4起步,逐步调到8做对比。draft长度增加,理论上加速比更大,但计算失败后回滚的代价也成倍上升。我在实践中发现,4~5步的draft长度是大多数场景的甜点区,超过8步之后加速收益基本饱和。
更进阶的玩法是自适应draft length:连续几轮命中率高,就动态延长draft length;连续失败就缩短。这个机制能让投机解码在简单请求和复杂请求之间自动调整策略,和System One的分层思路是同构的——用最少的验证成本处理最简单的token序列。
3.4 端到端超时预算分配
延迟优化做到一定阶段,瓶颈往往不再是GPU,而是整个请求链路的超时设计。网关、路由、快路径、慢路径、后处理,任何一个环节超时都会把P99拉爆。
我的做法是从SLO倒推每个环节的预算。假设端到端P99目标是800ms,按下面的比例分配:
| 链路环节 | 预算比例 | 典型耗时 |
|---|---|---|
| 网关接入与鉴权 | 5% | 40ms |
| 路由与分流判定 | 5% | 40ms |
| 快路径推理生成 | 70% | 560ms |
| 置信度校验与降级判断 | 10% | 80ms |
| 后处理与响应序列化 | 10% | 80ms |
这里有个关键认知:快路径的推理时长预算,应该基于“输出长度上限”来设计,而不是基于平均长度。很多慢请求会在流式生成后期被掐断——网关侧需要设置单请求的最大生成时长,超过预算直接返回截断结果,同时标记该请求为降级候选,转异步慢路径补全。
重试策略和熔断机制同样重要。慢路径本身耗时越长,就越容易触发超时,下游依赖偶发抖动时,如果客户端盲目重试,会在短时间内涌入大量重复请求,把整个服务打穿。我的经验是:重试只允许一次,且限定在网关层,业务侧一律不做自动重试;后端连续返回5xx达到阈值时,直接在网关层进行熔断,优先保证快路径稳定。
3.5 流式协议与首Token加速
用户感知的延迟不只是“完整返回时间”,更是首Token时间。很多场景下用户看到第一个字符开始输出,就会觉得服务“响应了”。所以流式协议(SSE)不是一个可选项,而是低延迟体验的基础设施。
SSE的实现已经把序列化传输的开销摊薄了,但我在实践里发现还有一个容易被忽略的点:不要等模型解码完一个完整的word才推送,而是按token边界甚至更细粒度推流。否则在中文场景下,模型一次输出几个汉字,但你要凑齐一个完整英文单词才推,TTFT直接多了好几倍体感。
另外,流式响应下的取消机制要设计好。用户在界面上点击“停止生成”之后,服务端应该立刻停止解码并释放该序列占用的KV Cache资源。我在监控中看到过不少事故,用户早就停掉了请求,但服务端还在傻乎乎解码几千个token,白烧算力。
4. 性能评估与调优实录
4.1 指标体系:别只盯着吞吐量
LLM服务评估有个经典误区:只关注吞吐量(tokens/s)而忽略延迟分布。吞吐量高喊“GPU利用率拉满”,但P99延迟可能早就丑得没法看。正确做法是把延迟和吞吐放在同一张图上评估。
我日常监控的核心指标有这么几组:
- TTFT P95/P99:首Token延迟,反映排队和预填充效率
- TPOT P95/P99:单Token生成时间,反映解码和批处理效率
- 快路径命中率:成功在快路径完成且未降级的请求占比
- 降级率:快路径触发降级到慢路径的占比
- 每千token成本:单位输出token的综合机器成本
命中率和降级率这两个指标,比单纯看吞吐更能反映System One架构的健康度。命中率过高(比如超过90%)可能意味着路由层过于保守,有些其实该走慢路径的复杂请求被硬塞给了快路径小模型,输出质量正在悄悄劣化;降级率持续偏高则说明快路径能力与流量分布不匹配,需要调整阈值或模型规模。
4.2 压测方法与容量规划
做容量规划时,我最不推荐的是用单条请求反复打点测延迟,然后乘一个并发数来估算容量。真实流量的长尾特征和请求分布根本没法用这种方法复现。
正确的做法是准备一个流量回放工具集,把线上真实的请求日志脱敏后做成压测数据集。压测过程分三步走:
第一,跑基准负载,建立“延迟-并发”基线曲线。固定请求分布,从低并发开始,逐步加压,记录每一档并发下的P95/P99延迟。第二,找膝盖点。如果P99延迟开始脱离线性区间出现陡增,就说明集群的调度能力已经进入过载区,这个点通常就是当前负载模型下的容量上限。第三,按峰值流量乘以冗余系数(我一般取1.5倍冗余)来规划副本数。
压测过程中有个细节要提醒:不要把延迟数据只看平均数。我见过太多团队汇报“平均延迟50ms”,但P99已经500ms,完全不是一个量级。LLM服务的负载特征决定了它的延迟分布一定是重尾的,P50和P99之间往往差一个数量级。
4.3 生产环境的长尾问题排查实录
长尾延迟是分布式系统里最磨人的问题,LLM服务尤甚。因为每一层都可能成为延迟异常点。我在多个集群里实际排查过的长尾来源包括:
显存碎片化。运行一段时间后KV Cache的分页出现大量碎片,新请求无法在连续显存块中分配缓存,触发额外的复制和压缩操作,一次压缩往往几十毫秒起步。这个问题的特征是延迟曲线出现周期性尖刺,而且和请求到达率没有明显相关性。排查工具可以用NVIDIA的DCGM监控显存利用率,长期运行后显存利用率居高不下且无法回落,基本就是碎片化问题。
GPU占用率掉底但延迟升高。这种诡异现象通常是CPU侧瓶颈——tokenizer计算、采样逻辑(超过词表大小的softmax)、batch组装都在CPU上,CPU忙不过来GPU就只能空转。用perf或者火焰图抓一次,很快就能定位到是采样算子还是序列组装在耗CPU。
冷启动与缓存失效。新扩容的副本需要加载模型权重、预热KV Cache,这个过程中延迟会比稳态高数倍,如果流量调度没有做“预热后再放量”的治理,冷节点会极大地拉高P99。重启一次70B模型的加载可能需要几十秒,期间打进来的请求全部超时。解决方案是在K8s就绪探针里加入模型预热检查,确认推理服务真正可用后再接收流量。
5. 常见问题速查与经验总结
我在多个项目里沉淀了一份问题速查表,几乎可以覆盖System One架构落地初期的90%问题场景:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 快路径P99延迟不降反升 | batch_size过大,长短请求互相拖累 | 调小max_batch_size,启用SLO感知调度 |
| 快路径命中率很低 | 路由规则过于保守 | 补充Embedding分类器,放宽阈值 |
| 降级率突增 | 快路径模型能力与流量结构不匹配 | 升级快路径模型规模,或调整Early Exit阈值 |
| TTFT持续偏高 | 前缀缓存未生效,预填充计算量过大 | 检查prefix caching开关,缓存系统提示词 |
| TPOT周期性尖刺 | KV Cache显存碎片化 | 定期清理缓存,使用分页管理并调整page大小策略 |
| 流式输出卡顿 | SSE推送粒度太粗,或解码线程阻塞 | 细化推送粒度,用异步推送管道 |
| 快路径输出质量差但置信度高 | 置信度评估过于依赖单一信号 | 组合熵值、一致性校验、字段完整性三重判断 |
最后分享一个我个人的经验,关于这套架构的生命周期管理。快路径的模型必须持续迭代,不能上线后一劳永逸。网络上的新表达、新词汇、新的热门任务格式,都会让快路径模型的性能在几个月内逐渐跟不上。我每个版本迭代会做一次回归对比,用固定的benchmark集跑快慢两条路径,把质量差距控制在可接受范围内。如果差距持续拉大,就是时候换模型或者调整分流策略了。
还有一点想说的是:System One架构的精髓不是“永远求快”,而是“该快则快,该稳则稳”。快路径能给你带来延迟和成本上的巨大收益,但前提是慢路径的兜底始终可靠。我见过一些团队把快路径比例调到90%以上,结果一个质量事故就赔进去一周的口碑。把降级机制做好,把置信度评估做扎实,比多压榨一个百分点的延迟重要得多。