ARTICLE DETAIL

资讯详情

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

System One范式:把LLM服务从慢等待改造成流式快响应

System One范式:把LLM服务从慢等待改造成流式快响应 做LLM服务的同学应该都有过这种经历请求发出去前端转圈圈好几秒之后一大段文字突然一次性涌出来。多数时候我们默认这是正常的毕竟大模型“想”得慢。但你有没有想过这种“等全量回复”的交互方式本身才是LLM服务里最反直觉的瓶颈。标题里的System One指的并不是某个具体模型的名字而是一类以流式交互、快速首token为设计目标的serving范式——对应认知科学里那种不假思索的“快系统”。真正把LLM服务从“慢系统”改成“快系统”需要把推理栈里的调度、缓存、显存预算、超时策略全部重新审视一遍。这篇文章就是我实际改造过程中积累的经验总结。先说清楚这篇文章不是理论科普也不是对某个框架的软文而是一个做LLM应用和推理服务的人在真实项目里的踩坑记录。内容覆盖System One范式的设计思路、TTFT和ITL这些核心指标的拆解、从普通API改造成流式接口的实操过程以及上线后最容易遇到的几个工程问题。无论你是刚接触LLM serving的新手还是已经在负责推理服务的老手下文中的大部分场景你应该都会遇到。1. 先从“快思考”和“慢思考”说起System One到底在讲什么1.1 双系统认知与LLM交互范式的对应关系卡尼曼在《思考快与慢》里把人类的认知模式分成两个系统System One负责快速、直觉、自动化的反应比如你看到“22”立刻说出4System Two负责缓慢、理性、需要消耗注意力的推导比如你算一道复杂的微积分题。这个概念用到LLM服务上是一个非常贴切的类比。传统的大模型API调用是典型的System Two你提交一段prompt服务端把整段回复全部计算完再把一个完整文本作为响应返回给你。整个过程用户看不到任何中间状态只能干等。System One模型指的就是反过来。它把解码过程当作一种流式输出模型每预测出一个token服务端立刻把它推送给前端用户看到的是一个一个字蹦出来的过程。这种方式并没有加快模型的总推理时间但它从根本上改变了“等待”的分布。用户感知到的延迟不再等于完整的端到端生成时间而是被压缩成了“首token出现的时间”。只要把首token延迟控制在几百毫秒以内用户就会觉得系统响应很快后续的等待反倒成了一种可感知的生成过程。我在实际项目里见过太多“重后端、轻交互”的架构。工程师们花大量精力去压token生成速度却忽视了接口层面还停留在“等全量”的老路上。一个30B模型如果直接以System Two方式部署用户发一句话可能等十几秒才看到结果这中间产生的焦虑和超时重试往往比推理本身的延迟更伤害业务。而同样的模型只需改成流式输出用户可能在两秒内就看到第一个字蹦出来体验差异极其明显。1.2 传统批处理模式对用户体验的伤害传统request/response模式的问题还不只是“用户等着难受”那么简单。让我用一个具体的例子说明。假设你的LLM服务需要生成800字的内容模型平均每秒输出30个token也就是差不多每秒20到25个汉字那么生成完整个回答大约需要25到30秒。在这段时间里用户的浏览器一直处于等待响应的状态任何代理服务器、网关和定时器都在默默倒计时。结果就是三类非常典型的事故第一客户端超时。很多HTTP客户端默认超时是30秒长篇生成的场景下请求会直接报timeout用户压根看不到已经生成出来的部分。第二网关缓冲。如果前方还有一层Nginx或者API网关默认配置通常会缓冲整个响应体流式数据会被憋在缓冲区里直到完全结束才一次性释放用户感受到的还是“一口气吐完”的老体验。第三错误重试。客户端发现长时间没响应后会自动重试同一个请求结果模型重复生成资源被浪费数据库和应用逻辑也可能出现重复写入。这三类事故我都真实踩过。后来把服务改成System One风格的流式输出前端通过EventSource或者ReadableStream一点点接收文本“长时间无响应”这个感知被彻底消除了超时重试的问题也基本消失。这里有个很关键的认知System One不是把生成速度变快了它只是把“不可见的等待”变成了“可见的生成”但这一点微小的变化对用户体验和心理预期的影响是巨大的。2. 流式服务背后真正吃性能的是这3个环节2.1 TTFT、ITL、TBT延迟指标要拆开看很多团队评估LLM服务性能时只看一个数字端到端生成耗时。这个指标在System One范式下几乎没有意义因为用户根本不关心整个回答什么时候结束他们只关心第一个字什么时候出来以及后续的生成节奏是否稳定。做serving优化必须把延迟拆成三段来看。第一段是TTFTTime to First Token即从请求到达服务端到返回第一个token的时间。它包含了网络传输、排队、prompt预填充prefill三部分开销。TTFT是用户感知延迟的核心来源也是最值得优化的指标。第二段是ITLInter-Token Latency即两个相邻token返回之间的时间间隔。ITL决定了一个字蹦出来的节奏是否稳定如果间隔忽长忽短前端就会出现明显的卡顿感。第三段是TBTTime Between Tokens它通常指端到端视角下客户端实际收到的token间隔和ITL的区别在于它还包含了网络传输和前端渲染的延迟。我举个实测数据帮大家建立直觉。我自己在一个7B模型、单张消费级显卡、并发数为8的场景下压测TTFT大概在300到800毫秒波动ITL稳定在40到60毫秒左右。如果把prompt加长到1000个tokenTTFT会明显恶化到1秒以上但ITL几乎不变。这个现象说明prefill和decode是两个完全不同的瓶颈阶段优化手段也完全不同。后面我会拆开讲这两者的差异因为它们决定了你怎么配置GPU资源、怎么选择量化方案、怎么设置batch大小。2.2 prefill和decode一个吃算力一个吃带宽LLM的一次完整推理分成两个阶段。第一个阶段叫prefill也叫做预填充这时模型拿到你的整个prompt一次性并行计算出第一个输出token对应的隐状态。这个阶段是典型的计算密集型负载它的耗时和你的prompt长度强相关也和GPU的算力强相关。第二个阶段叫decode也叫解码阶段模型逐token生成输出每一步都要把之前生成的token作为输入读取全部模型权重算一次前向传播。这个阶段是典型的内存带宽密集型负载。为什么decode吃带宽而不是算力这里可以做一道简单的估算题。假设一个13B模型用FP16存储模型权重大约26GB。每生成一个token需要把这26GB的权重全部读一遍。一张A100的HBM带宽大约是2TB/s理想情况下每token的理论耗时就是26GB除以2TB/s约13毫秒。但在真实环境里还有KV Cache读写、注意力计算、采样开销实际ITL一般在30到60毫秒之间。如果你换到带宽只有1TB/s左右的消费级显卡上同样一个模型ITL会接近70到100毫秒用户能明显感受到AI“反应变慢”。理解prefill和decode的差异对资源规划很重要。prefill慢会影响TTFTdecode慢会影响ITL两者混在一起跑时一个长prompt的长请求可能霸占GPU导致其他请求的TTFT全部飙升。所以现在主流serving框架普遍采用连续批处理continuous batching和chunked prefill技术把长prompt切成小块插到生成间隙里避免让一个用户的长输入拖垮所有人的首字延迟。2.3 Continuous Batching与位置编码的隐性约束传统批处理这批人换一个类比静态批处理就像一辆大巴凑满一车人才发车某个乘客晚到了就得等下一班。连续批处理则更像地铁——随时有人进站随时有人下车列车循环运行哪个序列生成了结束符或者达到了最大长度它占用的显存槽位立刻让给新请求。连续批处理是System One服务能够同时维持“多用户流式输出”和“高GPU利用率”的关键。但连续批处理并不是银弹它有一个隐性约束KV Cache的显存占用是动态且巨大的。KV Cache是Transformer在推理时保存的中间状态序列越长、batch越大KV Cache占用越夸张。尤其当用户请求都是长上下文时系统最突出的风险不是算力不足而是显存被KV Cache吃光引发OOM或者被迫频繁驱逐已有请求。这也是PagedAttention这类思路的价值所在它像操作系统的虚拟内存一样把KV Cache切成固定大小的块按需分配极大减少了显存碎片和浪费。位置编码的约束往往被新手忽略。使用传统绝对位置编码的模型如果训练时只见过4096长度的序列部署时直接把上下文窗口拉到8192效果会迅速劣化。而RoPE旋转位置编码虽然自带一定外推能力但过长位置仍会引发困惑度暴涨和生成质量下降。所以做长上下文服务时除了优化KV Cache还要考虑位置编码方案。更多人采用的方案是在训练时把窗口拉长或者引入YaRN、ALiBi这类设计。这决定了你能支持的上下文长度上限也直接影响显存预算。3. 从架构到代码System One推理服务的落地过程3.1 流式协议选型SSE的简单与WebSocket的重把LLM服务改造成System One风格第一个技术选型就是用什么协议做流式传输。市面上主流选项有两个SSEServer-Sent Events和WebSocket。我的建议是除非你需要双向实时交互否则默认选SSE它足够简单且坑少。SSE的本质是HTTP长连接服务端通过Content-Type: text/event-stream持续向同一个响应连接写入事件。它的实现成本极低前端直接用EventSource就能订阅后端FastAPI写一个async generator就能逐段yield数据网关和负载均衡器对它也有很好的兼容性。WebSocket则能实现双向通信适合聊天场景里需要前端随时发指令打断生成这种需求但它要维护独立的连接生命周期、心跳、重连和消息帧协议工程复杂度高一个量级。从LLM服务场景看绝大多数的需求只是“服务端往客户端单向推送token”这个语义用SSE天然匹配。你唯一需要额外处理的是给前端的EventSource加一个自定义请求头用来传递会话ID或者鉴权信息——原生EventSource不支持自定义header可以用fetch加ReadableStream去读响应体把SSE解析逻辑放到前端。这样既能保留SSE的简化又绕开了浏览器对EventSource请求头的限制。3.2 把“等全量”改成“边算边发”的第一步改造代码层面的改造核心就是让推理服务不再等完整输出而是逐token产出。很多成熟框架已经做了这一步vLLM、TensorRT-LLM、SGLang都有原生的流式接口返回一个迭代器你只需要把它接上HTTP流式响应。但如果你的服务是自己基于HuggingFace Transformers封装的你大概率还在用generate()一把梭。这里有两条路可以走。第一条路升级到支持流式的新框架。例如用vLLM替换Transformers的在线serving它的采样器原生支持streamTrue你迭代它返回的output即可拿到每个token。还有SGLang对RadixAttention的支持在前缀缓存上做得很好重复的系统提示词和大段公共上下文可以明显省掉prefill时间。第二条路自己实现一个最简生成循环手动调用model的forward做一次KV Cache的复用逻辑通过iterator逐token返回。对于验证原型的场景这个方案也能跑但并发上去以后性能和显存管理都会吃力建议只在模型研究或debug时用。我个人建议做生产项目时直接用成熟serving层不要重复造轮子。你真正要花精力的是推理服务和你自己的业务服务之间的对接上游业务服务拿到token流后可能要做敏感词过滤、格式化、去重、统计增量计费这些逻辑都必须设计成“逐段处理”而不是“等全量”。我给你一个比较稳妥的模式业务服务从推理层拉取token迭代器对每个token做清理加工再通过SSE写入响应流同时用一个聚合buffer在后台累积完整结果用于后续落库和审计两路并行但互不阻塞。3.3 典型的连续批处理参数和显存预算怎么配如果你用vLLM或者类似框架做serving层几个核心参数直接影响System One体验。max_num_seqs是单次batch的最大序列数它决定并发上限和显存分配策略一般按并发用户数除以一个保守系数来设max_num_batched_tokens限制单批总共能处理的token数太大容易让单个长请求独占整批计算太小会频繁触发调度导致GPU利用率下降gpu_memory_utilization则决定预留多少显存给KV Cache这个值最建议根据模型权重和显存总量推算。举个例子直观说一下显存预算怎么算。假设一张80GB的A100部署一个13B模型FP16权重约占26GB。activations和运行时开销通常再留几GB剩下的50GB左右可以分配给KV Cache。如果模型的layer数和head数已知可以估算每token所需的KV Cache大小比如大约几百KB到1MB那么50GB大概能支撑数万个token的KV Cache总量。把这些平均到代表性并发数上就能估算出每个请求可以支持多长的上下文。这个预算算完之后还要清楚有一个纪律把max_model_len设成一个服务能稳定承载的上限而不是模型训练时的上限。很多团队为了炫参数把上下文窗口拉满结果一旦并发升高KV Cache溢出服务端频繁驱逐序列流式输出被随机中断体验反而比短窗口还差。我自己的习惯是先按75%的显存利用率配置然后通过压测逐步往上顶找到吞吐和稳定性的平衡点而不是一上来就拉满。3.4 压测时怎么埋点记录TTFT和ITL改完流式服务后很多人直接用ab或者curl测“总耗时”得到的结果完全没有区分度。System One场景下压测脚本必须自己管好时间戳直接把每个token的到达时间记录下来。这种观测方式比任何吞吐数字都更能反映用户体验。我一般用Python写一个简短的压测客户端记录请求发出时间t0开始读取响应流后记录第一个token到达时间t1TTFT就是t1 - t0之后每读到一个新的token记录一次当前时间相邻token到达时间的差值就是ITL序列。跑完一轮后统计TTFT的p50和p95ITL的p50和p95以及有没有超过500毫秒的“卡顿token间隔”。我实测中发现一个健康的System One服务p95 TTFT应该控制在2秒以内ITL的p95不应该超过ITL均值的3倍。如果ITL出现明显抖动优先怀疑调度时间片和KV Cache驱逐。压测时还要注意请求内容要覆盖几种典型负载短prompt短输出、长prompt短输出、短prompt长输出、多用户并发。只测单一种类很容易漏掉问题。我自己就遇到过一种典型情况短prompt并发测试一切正常但一旦prompt超过2000个tokenTTFT直接从500毫秒劣化到4秒。原因就是prefill阶段没有做chunked切分长输入独占GPU计算好几秒把其他并发请求全部卡在排队里。这个坑放到后面细说。4. 上线之后最容易踩的4个坑4.1 首字延迟反而变高的“调度失控”问题理论上说System One应该让用户更快看到首个token但在并发场景下有不少团队发现改完之后TTFT反而恶化了。原因多半出在调度策略上用户请求进入后没有及时抢占GPU执行而是被塞进队列等待。尤其当连续批处理被配置成“凑够整批才调度”时后到的请求会等前面的序列走得差不多首字延迟自然飙升。解决方案是给调度器配置更激进的抢占策略。在vLLM这类框架里可以参考每收到新请求就触发一次调度检查只要GPU有空闲槽位就立刻插入执行而不是攒批。还有一个非常实用的技巧是把请求分成不同优先级队列那些只有几百token的轻量请求可以让它插队优先执行长上下文请求则放慢一点。优先级调度的收益在混合负载下非常明显我自己用这个调整把p95 TTFT从2.8秒降到了1.2秒。4.2 流式响应被网关或代理“憋住”的坑前面提到过网关缓冲问题但它在流式场景里还是会反复出现。很多企业服务的前面必然要挂一层网关做鉴权、限流和负载均衡。Nginx默认的proxy_buffering是开启的这意味着它会缓存上游数据直到连接关闭才把完整内容返回给客户端——你的SSE流式改造在网关这一层就被悄悄打回去了。排查思路很简单直接用curl请求你的流式服务观察是不是每个数据块都能及时到达如果本地直连没问题但走网关就变成一次性到达十有八九是缓冲配置的问题。解决方法是把Nginx的proxy_buffering设为off同时确保代理层和业务层都关闭TCP层面的Nagle算法开启TCP_NODELAY。另外网关超时时间也要单独调大因为流式请求的HTTP连接会持续几十秒甚至几分钟默认的60秒代理超时会在生成到一半时强行断开连接。4.3 长上下文把KV Cache吃光引发连锁故障这是流式服务上线后最隐蔽的坑刚开始一切正常跑了一段时间后服务突然频繁报显存不足还伴随随机请求被中断。我在排障时看到监控曲线KV Cache用量在长会话堆积后逐渐攀升直到触顶。之后调度器为了腾出空间开始驱逐某些序列的缓存被驱逐的序列被迫重新做prefillTTFT飙升甚至直接异常退出。这个问题的根源是每个用户的上下文长度都在随对话轮次增长而你对“单用户占用多少KV Cache”完全没有上限控制。治本的办法是给每条会话设置明确的上下文长度上限超限就触发摘要或者裁剪策略千万不要让一个空闲的聊天会话把宝贵的KV Cache占住不放。治标的办法是把KV Cache设置为自动释放空闲序列的缓存比如连续30秒没有新的token输入就把它的缓存标记为可驱逐并在有需要时优先回收。结合前缀缓存技术用户重新发起请求时公共前缀部分可以复用缓存不需要完整重新prefill实际回退成本低很多。4.4 日志洪水把观测系统冲垮流式服务还有一个许多人低估的“副作用”日志量爆炸。同样的QPS下System Two模式每个请求就产生一两行访问日志System One模式每个token都可能触发日志、回调或者指标埋点。我见过一个项目上线流式后日志系统一周内磁盘告警原因就是业务团队在生成循环的每次迭代里都打了一行info日志。我的建议是分级处理token级别的日志走采样或者聚合比如每100个token记一条统计日志只有异常事件超时、中断、OOM、驱逐才记录完整链路信息用户会话级日志保持在请求粒度把token数量、首字延迟、平均间隔这些汇总指标写进去。观测这块做扎实了排障效率会高很多否则你的监控系统会比推理服务先崩溃。5. 给“要不要全面转向流式”的一个判断标准不是所有LLM场景都适合System One。判断标准其实很简单用户是否在等待过程中与生成结果发生交互如果答案是肯定的比如ChatBot、写作助手、代码生成、翻译工具那么流式输出几乎是必须的。如果答案是否定的比如离线批量分析、后台摘要生成、异步处理流水线那么保持System Two反而更简单合理省去了流式带来的连接管理和日志开销。另一个判断维度是你的业务是否依赖“完整结果才可信”。有些场景的中间结果是不完整的Markdown、未闭合的代码块、截断的JSON前端处理起来比较麻烦。这种情况可以先流式输出文本占位最终用完整结果做一次客户端校验替换。但不要因为这个就否定流式让用户先看到内容永远比让用户面对空白等待要好。我在实际项目中还有一个体会System One的关键不是某个高深的技术而是一种全局视角。你需要同时处理推理引擎的调度、网络层的缓冲、前端的流解析、日志的回归只要其中一个环节还在等待全量整个链路就是慢的。这个认知听起来简单但真正影响的是你在每个环节的判断和取舍。如果你正在做LLM应用开发我建议下一次需求评审时多问一句用户在这个等待过程里能看到什么如果答案是“除了加载动画什么都没有”那你就该考虑换一种服务架构了。
返回列表